1
0
silverbullet/plugs/ghost/ghost.ts
2022-04-09 18:25:42 +02:00

230 lines
5.6 KiB
TypeScript

import { readPage, writePage } from "plugos-silverbullet-syscall/space";
import { json } from "plugos-syscall/fetch";
import YAML from "yaml";
import { invokeFunction } from "plugos-silverbullet-syscall/system";
import { getCurrentPage, getText } from "plugos-silverbullet-syscall/editor";
import { cleanMarkdown } from "../markdown/markdown";
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<Post[]> {
let result = await json(
`${this.url}/ghost/api/v3/admin/posts?order=published_at+DESC`,
{
headers: {
Authorization: `Ghost ${this.token}`,
},
}
);
return result.posts;
}
async listMarkdownPosts(): Promise<Post[]> {
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<Post>): Promise<any> {
return this.publish("posts", post);
}
publishPage(post: Partial<Post>): Promise<any> {
return this.publish("pages", post);
}
async publish(what: "pages" | "posts", post: Partial<Post>): Promise<any> {
let oldPostQuery = await json(
`${this.url}/ghost/api/v3/admin/${what}/slug/${post.slug}`,
{
headers: {
Authorization: `Ghost ${this.token}`,
"Content-Type": "application/json",
},
}
);
if (!oldPostQuery[what]) {
// New!
if (!post.status) {
post.status = "draft";
}
let result = await json(`${this.url}/ghost/api/v3/admin/${what}`, {
method: "POST",
headers: {
Authorization: `Ghost ${this.token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
[what]: [post],
}),
});
return result[what][0];
} else {
let oldPost: Post = oldPostQuery[what][0];
post.updated_at = oldPost.updated_at;
let result = await json(
`${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 result[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<Partial<Post>> {
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<GhostConfig> {
let configPage = await readPage("ghost-config");
return YAML.parse(configPage.text) 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");
}
}