about summary refs log tree commit diff
path: root/companion-lite/src/micropub_api.ts
diff options
context:
space:
mode:
Diffstat (limited to 'companion-lite/src/micropub_api.ts')
-rw-r--r--companion-lite/src/micropub_api.ts125
1 files changed, 125 insertions, 0 deletions
diff --git a/companion-lite/src/micropub_api.ts b/companion-lite/src/micropub_api.ts
new file mode 100644
index 0000000..fa1c431
--- /dev/null
+++ b/companion-lite/src/micropub_api.ts
@@ -0,0 +1,125 @@
+export interface MicropubChannel {
+  readonly uid: string,
+  readonly name: string
+}
+
+export interface MF2 {
+  type: string[],
+  properties: { [key:string]: (string | MF2 | {[key:string]: string})[] | undefined }
+}
+
+export interface MicropubConfig {
+  readonly channels?: MicropubChannel[],
+  readonly "media-endpoint"?: string
+}
+
+export interface MicropubErrorMessage {
+  readonly error: string,
+  readonly error_description: string | undefined
+}
+
+export class MicropubError extends Error {
+  readonly status: number | null
+  readonly response: MicropubErrorMessage | null
+
+  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 }
+      )
+    }
+
+    this.status = status;
+    this.response = response;
+  }
+}
+
+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 ${this.token}`,
+        "Content-Type": "application/json"
+      },
+      body: JSON.stringify(mf2)
+    })
+
+    if (response.status != 201 && response.status != 202) {
+      let err = await response.json() as MicropubErrorMessage;
+
+      throw new MicropubError(response.status, err)
+    } else {
+      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());
+    }
+  }
+}