236 lines
5.9 KiB
TypeScript
236 lines
5.9 KiB
TypeScript
import { readPage, writePage } from "@plugos/plugos-silverbullet-syscall/space";
|
|
import { invokeFunction } from "@plugos/plugos-silverbullet-syscall/system";
|
|
import {
|
|
getCurrentPage,
|
|
getText,
|
|
} from "@plugos/plugos-silverbullet-syscall/editor";
|
|
import { cleanMarkdown } from "../markdown/util";
|
|
import { parseMarkdown } from "@plugos/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<Post[]> {
|
|
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<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 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<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 { 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");
|
|
}
|
|
}
|