about summary refs log tree commit diff
path: root/kittybox-rs/companion-lite/src/micropub_api.ts
diff options
context:
space:
mode:
authorVika <vika@fireburn.ru>2022-10-02 11:54:30 +0300
committerVika <vika@fireburn.ru>2022-10-02 11:54:30 +0300
commit568f98589b6c30bb3f807517d28039b12dd54be3 (patch)
tree885f918885b246d3760cf3b1f2a252b88485f358 /kittybox-rs/companion-lite/src/micropub_api.ts
parentb7d4e5c4686bc8aac41d832567190002542a1743 (diff)
downloadkittybox-568f98589b6c30bb3f807517d28039b12dd54be3.tar.zst
companion-lite: rewrite to use IndieAuth
This is a naive implementation that doesn't have some security
checks. It's ok tho, should work fine... can refine it later
Diffstat (limited to 'kittybox-rs/companion-lite/src/micropub_api.ts')
-rw-r--r--kittybox-rs/companion-lite/src/micropub_api.ts125
1 files changed, 96 insertions, 29 deletions
diff --git a/kittybox-rs/companion-lite/src/micropub_api.ts b/kittybox-rs/companion-lite/src/micropub_api.ts
index 9eb65a2..fa1c431 100644
--- a/kittybox-rs/companion-lite/src/micropub_api.ts
+++ b/kittybox-rs/companion-lite/src/micropub_api.ts
@@ -1,6 +1,6 @@
 export interface MicropubChannel {
-  uid: string,
-  name: string
+  readonly uid: string,
+  readonly name: string
 }
 
 export interface MF2 {
@@ -9,50 +9,117 @@ export interface MF2 {
 }
 
 export interface MicropubConfig {
-  channels: MicropubChannel[],
-  "media-endpoint": string
+  readonly channels?: MicropubChannel[],
+  readonly "media-endpoint"?: string
 }
 
-export async function query_channels(endpoint: string, token: string): Promise<MicropubChannel[]> {
-  const response = await fetch(endpoint + "?q=config", {
-    headers: {
-      "Authorization": `Bearer ${token}`
-    }
-  })
+export interface MicropubErrorMessage {
+  readonly error: string,
+  readonly error_description: string | undefined
+}
+
+export class MicropubError extends Error {
+  readonly status: number | null
+  readonly response: MicropubErrorMessage | null
 
-  if (response.ok) {
-    const config = await response.json() as MicropubConfig;
+  constructor(status: number | null, response: MicropubErrorMessage | null, cause: Error | null = null) {
+    // Needs to pass both `message` and `options` to install the "cause" property.
+    if (status == null) {
+      super("Micropub endpoint didn't respond properly", { cause });
+    } else if (response == null) {
+      super(`Micropub endpoint returned HTTP ${status}`, { cause });
+    } else {
+      super(
+        `Micropub endpoint returned ${response.error}: ${response.error_description ?? "(no description was provided)"}`,
+        { cause }
+      )
+    }
 
-    return config["channels"]
-  } else {
-    throw new Error(`Micropub endpoint returned ${response.status}: ${await response.json()}`)
+    this.status = status;
+    this.response = response;
   }
-  
 }
 
-export async function submit(endpoint: string, token: string, mf2: MF2) {
-  try {
-    const response = await fetch(endpoint, {
+export class Micropub {
+  readonly token: string
+  readonly micropub_endpoint: URL
+  private config_response: MicropubConfig | null
+  
+  constructor({ endpoint, token }: { endpoint: URL, token: string }) {
+    this.micropub_endpoint = endpoint;
+    this.token = token;
+    this.config_response = null;
+  }
+
+  async config(): Promise<MicropubConfig> {
+    if (this.config_response != null) {
+      return this.config_response
+    }
+    let url = this.micropub_endpoint;
+    let params = new URLSearchParams();
+    for (const [key, val] of url.searchParams) {
+      params.append(key, val)
+    }
+    params.set("q", "config")
+
+    url.search = "?" + params.toString();
+
+    const response = await fetch(url, {
+      headers: {
+        "Authorization": `Bearer ${this.token}`
+      }
+    });
+    if (response.ok) {
+      const config = await response.json() as MicropubConfig;
+      this.config_response = config
+
+      return config
+    } else {
+      throw new MicropubError(response.status, await response.json() as MicropubErrorMessage);
+    }
+  }
+
+  async submit(mf2: MF2): Promise<URL> {
+    const response = await fetch(this.micropub_endpoint, {
       method: "POST",
       headers: {
-        "Authorization": `Bearer ${token}`,
+        "Authorization": `Bearer ${this.token}`,
         "Content-Type": "application/json"
       },
       body: JSON.stringify(mf2)
     })
 
     if (response.status != 201 && response.status != 202) {
-      let err = await response.json();
-      console.error("Micropub error!", err);
+      let err = await response.json() as MicropubErrorMessage;
 
-      return err;
+      throw new MicropubError(response.status, err)
     } else {
-      return {
-        "location": response.headers.get("Location")
-      }
+      return new URL(response.headers.get("Location") as string)
+    }
+  }
+
+  async upload(file: File): Promise<URL> {
+    const config = await this.config();
+    const media = config["media-endpoint"];
+    if (media == null) {
+      throw new Error("Micropub endpoint doesn't support file uploads")
+    }
+
+    const form = new FormData();
+    form.set("file", file);
+
+    const response = await fetch(media, {
+      method: "POST",
+      headers: {
+        "Authorization": `Bearer ${this.token}`,
+      },
+      body: form
+    })
+
+    if (response.ok) {
+      return new URL(response.headers.get("Location") as string)
+    } else {
+      throw new MicropubError(response.status, await response.json());
     }
-  } catch (e) {
-    console.error("Network error!", e)
-    throw e
   }
 }