import { readPage, writePage } from "@silverbulletmd/plugos-silverbullet-syscall/space"; import { invokeFunction } from "@silverbulletmd/plugos-silverbullet-syscall/system"; import { getCurrentPage, getText } from "@silverbulletmd/plugos-silverbullet-syscall/editor"; import { cleanMarkdown } from "../markdown/util"; import { parseMarkdown } from "@silverbulletmd/plugos-silverbullet-syscall/markdown"; import { extractMeta } from "../query/data"; type GhostConfig = { url: string; adminKey: string; postPrefix: string; pagePrefix: string; }; type Post = { id: string; uuid: string; title: string; slug: string; mobiledoc: string; status: "draft" | "published"; visibility: string; created_at: string; upblished_at: string; updated_at: string; tags: Tag[]; primary_tag: Tag; url: string; excerpt: string; }; type Tag = { id: string; name: string; slug: string; description: string | null; }; type MobileDoc = { version: string; atoms: any[]; cards: Card[]; }; type Card = any[]; function mobileDocToMarkdown(doc: string): string | null { let mobileDoc = JSON.parse(doc) as MobileDoc; if (mobileDoc.cards.length > 0 && mobileDoc.cards[0][0] === "markdown") { return mobileDoc.cards[0][1].markdown; } return null; } function markdownToMobileDoc(text: string): string { return JSON.stringify({ version: "0.3.1", atoms: [], cards: [["markdown", { markdown: text }]], markups: [], sections: [ [10, 0], [1, "p", []], ], }); } class GhostAdmin { private token?: string; constructor(private url: string, private key: string) {} async init() { const [id, secret] = this.key.split(":"); this.token = await self.syscall( "jwt.jwt", secret, id, "HS256", "5m", "/v3/admin/" ); } async listPosts(): Promise { let result = await fetch( `${this.url}/ghost/api/v3/admin/posts?order=published_at+DESC`, { headers: { Authorization: `Ghost ${this.token}`, }, } ); return (await result.json()).posts; } async listMarkdownPosts(): Promise { let markdownPosts: Post[] = []; for (let post of await this.listPosts()) { let mobileDoc = JSON.parse(post.mobiledoc) as MobileDoc; if (mobileDoc.cards.length > 0 && mobileDoc.cards[0][0] === "markdown") { markdownPosts.push(post); } } return markdownPosts; } publishPost(post: Partial): Promise { return this.publish("posts", post); } publishPage(post: Partial): Promise { return this.publish("pages", post); } async publish(what: "pages" | "posts", post: Partial): Promise { let oldPostQueryR = await fetch( `${this.url}/ghost/api/v3/admin/${what}/slug/${post.slug}`, { headers: { Authorization: `Ghost ${this.token}`, "Content-Type": "application/json", }, } ); let oldPostQuery = await oldPostQueryR.json(); if (!oldPostQuery[what]) { // New! if (!post.status) { post.status = "draft"; } let result = await fetch(`${this.url}/ghost/api/v3/admin/${what}`, { method: "POST", headers: { Authorization: `Ghost ${this.token}`, "Content-Type": "application/json", }, body: JSON.stringify({ [what]: [post], }), }); return (await result.json())[what][0]; } else { let oldPost: Post = oldPostQuery[what][0]; post.updated_at = oldPost.updated_at; let result = await fetch( `${this.url}/ghost/api/v3/admin/${what}/${oldPost.id}`, { method: "PUT", headers: { Authorization: `Ghost ${this.token}`, "Content-Type": "application/json", }, body: JSON.stringify({ [what]: [post], }), } ); return (await result.json())[what][0]; } } } function postToMarkdown(post: Post): string { let text = mobileDocToMarkdown(post.mobiledoc); return `# ${post.title}\n${text}`; } const postRegex = /#\s*([^\n]+)\n([^$]+)$/; async function markdownToPost(text: string): Promise> { let match = postRegex.exec(text); if (match) { let [, title, content] = match; return { title, mobiledoc: markdownToMobileDoc(await cleanMarkdown(content)), }; } throw Error("Post should stat with a # header"); } async function getConfig(): Promise { let { text } = await readPage("ghost-config"); let parsedContent = await parseMarkdown(text); let pageMeta = await extractMeta(parsedContent); return pageMeta as GhostConfig; } export async function downloadAllPostsCommand() { await invokeFunction("server", "downloadAllPosts"); } export async function downloadAllPosts() { let config = await getConfig(); let admin = new GhostAdmin(config.url, config.adminKey); await admin.init(); let allPosts = await admin.listMarkdownPosts(); for (let post of allPosts) { let text = mobileDocToMarkdown(post.mobiledoc); text = `# ${post.title}\n${text}`; await writePage(`${config.postPrefix}/${post.slug}`, text); } } export async function publishCommand() { await invokeFunction( "server", "publish", await getCurrentPage(), await getText() ); } export async function publish(name: string, text: string) { let config = await getConfig(); let admin = new GhostAdmin(config.url, config.adminKey); await admin.init(); let post = await markdownToPost(text); if (name.startsWith(config.postPrefix)) { post.slug = name.substring(config.postPrefix.length + 1); await admin.publishPost(post); console.log("Done!"); } else if (name.startsWith(config.pagePrefix)) { post.slug = name.substring(config.pagePrefix.length + 1); await admin.publishPage(post); console.log("Done!"); } else { console.error("Not in either the post or page prefix"); } }