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 { 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 { 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 { 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()); } } }