Skip to content

Commit 4a2f26d

Browse files
committed
✨ Implemented Import/Export API
1 parent 8811dcf commit 4a2f26d

File tree

3 files changed

+150
-0
lines changed

3 files changed

+150
-0
lines changed

deps/scrapbox.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export type {
2+
ErrorLike,
3+
ExportedData,
4+
ImportedData,
5+
NotFoundError,
6+
NotLoggedInError,
7+
NotPrivilegeError,
8+
} from "https://pax.deno.dev/scrapbox-jp/[email protected]";

rest/page-data.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import type {
2+
ErrorLike,
3+
ExportedData,
4+
ImportedData,
5+
NotFoundError,
6+
NotLoggedInError,
7+
NotPrivilegeError,
8+
} from "../deps/scrapbox.ts";
9+
import { cookie, getCSRFToken } from "./utils.ts";
10+
import type { Result } from "./utils.ts";
11+
12+
/** `importPages`の認証情報 */
13+
export interface ImportInit {
14+
/** connect.sid */ sid: string;
15+
/** CSRF token
16+
*
17+
* If it isn't set, automatically get CSRF token from scrapbox.io server.
18+
*/
19+
csrf?: string;
20+
}
21+
/** projectにページをインポートする
22+
*
23+
* @param project - インポート先のprojectの名前
24+
* @param data - インポートするページデータ
25+
*/
26+
export async function importPages(
27+
project: string,
28+
data: ImportedData<boolean>,
29+
{ sid, csrf }: ImportInit,
30+
): Promise<
31+
Result<{ message: string }, ErrorLike>
32+
> {
33+
if (data.pages.length === 0) {
34+
return { ok: true, message: "No pages to import." };
35+
}
36+
37+
const formData = new FormData();
38+
formData.append(
39+
"import-file",
40+
new Blob([JSON.stringify(data)], {
41+
type: "application/octet-stream",
42+
}),
43+
);
44+
formData.append("name", "undefined");
45+
46+
if (!csrf) {
47+
const result = await getCSRFToken(sid);
48+
if (!result.ok) return result;
49+
csrf = result.csrfToken;
50+
}
51+
52+
const res = await fetch(
53+
`https://scrapbox.io/api/page-data/import/${project}.json`,
54+
{
55+
method: "POST",
56+
headers: {
57+
Cookie: cookie(sid),
58+
Accept: "application/json, text/plain, */*",
59+
"X-CSRF-TOKEN": csrf,
60+
},
61+
body: formData,
62+
},
63+
);
64+
65+
if (!res.ok) {
66+
if (res.status === 503) {
67+
const error = new Error();
68+
error.name = "ServerError";
69+
error.message = "503 Service Unavailable";
70+
throw error;
71+
}
72+
const error = (await res.json()) as ErrorLike;
73+
return { ok: false, ...error };
74+
}
75+
const result = (await res.json()) as { message: string };
76+
return { ok: true, ...result };
77+
}
78+
79+
/** `exportPages`の認証情報 */
80+
export interface ExportInit<withMetadata extends true | false> {
81+
/** connect.sid */ sid: string;
82+
/** whether to includes metadata */ metadata: withMetadata;
83+
}
84+
/** projectの全ページをエクスポートする
85+
*
86+
* @param project exportしたいproject
87+
*/
88+
export async function exportPages<withMetadata extends true | false>(
89+
project: string,
90+
{ sid, metadata }: ExportInit<withMetadata>,
91+
): Promise<
92+
Result<
93+
ExportedData<withMetadata>,
94+
NotFoundError | NotPrivilegeError | NotLoggedInError
95+
>
96+
> {
97+
const res = await fetch(
98+
`https://scrapbox.io/api/page-data/export/${project}.json?metadata=${metadata}`,
99+
{
100+
headers: {
101+
Cookie: cookie(sid),
102+
},
103+
},
104+
);
105+
106+
if (!res.ok) {
107+
const error = (await res.json()) as
108+
| NotFoundError
109+
| NotPrivilegeError
110+
| NotLoggedInError;
111+
return { ok: false, ...error };
112+
}
113+
const result = (await res.json()) as ExportedData<withMetadata>;
114+
return { ok: true, ...result };
115+
}

rest/utils.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { ErrorLike } from "../deps/scrapbox.ts";
2+
3+
/** HTTP headerのCookieに入れる文字列を作る
4+
*
5+
* @param sid connect.sidに入っている文字列
6+
*/
7+
export const cookie = (sid: string) => `connect.sid=${sid}`;
8+
9+
/** CSRF tokenを取得する
10+
*
11+
* @param sid - connect.sidに入っている文字列。不正な文字列を入れてもCSRF tokenを取得できるみたい
12+
*/
13+
export async function getCSRFToken(
14+
sid: string,
15+
): Promise<Result<{ csrfToken: string }, ErrorLike>> {
16+
const res = await fetch("https://scrapbox.io/api/users/me", {
17+
headers: { Cookie: cookie(sid) },
18+
});
19+
if (!res.ok) {
20+
const error = (await res.json()) as ErrorLike;
21+
return { ok: false, ...error };
22+
}
23+
const { csrfToken } = (await res.json()) as { csrfToken: string };
24+
return { ok: true, csrfToken };
25+
}
26+
27+
export type Result<T, E> = ({ ok: true } & T) | ({ ok: false } & E);

0 commit comments

Comments
 (0)