about summary refs log blame commit diff
path: root/companion-lite/src/micropub_api.ts
blob: c8632564cfe0b62929478ce39a1b5e85026636c8 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
                                  
                       






                                                                                     
                                        
 






                                                
 










                                                                                                                         
 
                             
   
 


                                                
 


































                                                                                              
                     
                                                




                                                           
                                                              
 
                                                   
            
























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