1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
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());
}
}
}
|