export type MemosMemo = { name: string; content: string; visibility: string; creator?: string; createTime?: string; updateTime?: string; tags?: string[]; attachments?: { type?: string }[]; resources?: { type?: string }[] }; export type MemosIdentity = { name: string; username?: string; nickname?: string; displayName?: string; avatarUrl?: string; avatar?: string }; export type MemosSyncRules = { creator?: string | null; tags?: string[]; from?: string | null; to?: string | null; attachmentMode?: "all" | "images" | "none" }; const base = (url: string) => url.replace(/\/+$/, "") + "/api/v1"; async function request(url: string, token: string, init?: RequestInit) { const res = await fetch(url, { ...init, headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", ...(init?.headers || {}) }, cache: "no-store" }); if (!res.ok) throw new Error(`Memos API ${res.status}: ${await res.text()}`); return res; } export async function createUserWebhook(baseUrl: string, token: string, userName: string, webhook: { url: string; displayName: string; signingSecret: string }) { const user = userName.split("/").at(-1); if (!user) throw new Error("Invalid Memos user"); return (await request(`${base(baseUrl)}/users/${encodeURIComponent(user)}/webhooks`, token, { method: "POST", body: JSON.stringify(webhook) })).json() as Promise<{ name: string }>; } export async function deleteUserWebhook(baseUrl: string, token: string, userName: string, webhookName: string) { const user = userName.split("/").at(-1), webhook = webhookName.split("/").at(-1); if (!user || !webhook) throw new Error("Invalid Memos webhook"); await request(`${base(baseUrl)}/users/${encodeURIComponent(user)}/webhooks/${encodeURIComponent(webhook)}`, token, { method: "DELETE" }); } export async function verifyMemos(baseUrl: string, token: string) { await request(`${base(baseUrl)}/memos?pageSize=1`, token); } export async function getMemosIdentity(baseUrl: string, token: string) { let user: MemosIdentity; try { user = await (await request(`${base(baseUrl)}/auth/me`, token)).json() as MemosIdentity; if ((user as any).user) user = (user as any).user as MemosIdentity; } catch (error) { if (!(error instanceof Error) || !error.message.startsWith("Memos API 404")) throw error; user = await (await request(`${base(baseUrl)}/auth/status`, token, { method: "POST", body: "{}" })).json() as MemosIdentity; } if (!user.name) throw new Error("Memos did not return an account identity"); return user; } export function memoUrl(baseUrl: string, memoName: string) { const id = memoName.split("/").at(-1); if (!id) return null; const path = /^\d+$/.test(id) ? `m/${encodeURIComponent(id)}` : `memos/${encodeURIComponent(id)}`; return `${baseUrl.replace(/\/$/, "")}/${path}`; } export type MemosPage = { memos: MemosMemo[]; nextPageToken: string }; export async function listMemos(baseUrl: string, token: string, rules: MemosSyncRules = {}, page: { pageToken?: string; pageSize?: number } = {}): Promise { let pageToken = page.pageToken || ""; const tags = rules.tags?.filter(Boolean) || []; const mode = rules.attachmentMode || "all"; const quote = (value: string) => JSON.stringify(value); const filters = [rules.creator && `creator == ${quote(rules.creator)}`, `visibility == "PUBLIC"`, ...tags.map((tag) => `tags.exists(t, t == ${quote(tag)})`)].filter(Boolean).join(" && "); let useServerFilter = Boolean(filters); do { const params = new URLSearchParams({ pageSize: String(Math.max(10, Math.min(100, page.pageSize || 100))) }); if (pageToken) params.set("pageToken", pageToken); if (useServerFilter && filters) params.set("filter", filters); let data: any; try { data = await (await request(`${base(baseUrl)}/memos?${params}`, token)).json(); } catch (error) { if (!pageToken && useServerFilter && error instanceof Error && error.message.startsWith("Memos API 400")) { useServerFilter = false; continue; } throw error; } const memos = (data.memos || []).filter((memo: MemosMemo) => { if (memo.visibility !== "PUBLIC") return false; if (rules.creator && memo.creator !== rules.creator) return false; if (tags.length && !tags.every((tag) => memo.tags?.includes(tag))) return false; const created = memo.createTime?.slice(0, 10); if (rules.from && (!created || created < rules.from)) return false; if (rules.to && (!created || created > rules.to)) return false; return true; }).map((memo: MemosMemo) => { if (mode === "all") return memo; const onlyImages = (items: T[] | undefined) => mode === "none" ? [] : (items || []).filter((item) => item.type?.startsWith("image/")); return { ...memo, attachments: onlyImages(memo.attachments), resources: onlyImages(memo.resources) }; }); return { memos, nextPageToken: data.nextPageToken || "" }; } while (true); } export async function createMemo(baseUrl: string, token: string, memo: Pick & { attachments?: unknown[]; resources?: unknown[] }) { return (await request(`${base(baseUrl)}/memos`, token, { method: "POST", body: JSON.stringify({ state: "NORMAL", ...memo }) })).json() as Promise; } export async function createAttachment(baseUrl: string, token: string, attachment: { filename: string; content: string; type: string }) { return (await request(`${base(baseUrl)}/attachments`, token, { method: "POST", body: JSON.stringify(attachment) })).json() as Promise<{ name: string; filename: string; type: string }>; } export async function setMemoAttachments(baseUrl: string, token: string, memoName: string, attachments: unknown[]) { const memoId = memoName.split("/").at(-1); if (!memoId) throw new Error("Invalid Memos memo name"); await request(`${base(baseUrl)}/memos/${encodeURIComponent(memoId)}/attachments`, token, { method: "PATCH", body: JSON.stringify({ name: memoName, attachments }) }); } export async function createResource(baseUrl: string, token: string, resource: { filename: string; content: string; type: string; size: string }) { return (await request(`${base(baseUrl)}/resources`, token, { method: "POST", body: JSON.stringify(resource) })).json() as Promise<{ name: string; filename: string; type: string; size: string }>; } export async function createRemoteFile(baseUrl: string, token: string, file: { filename: string; content: string; type: string; size: string }) { try { return { kind: "attachment" as const, value: await createAttachment(baseUrl, token, file) }; } catch (error) { if (!(error instanceof Error) || !error.message.startsWith("Memos API 404")) throw error; return { kind: "resource" as const, value: await createResource(baseUrl, token, file) }; } }