Monorepo with yarn workspaces requires yarn 3.2
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
functions:
|
||||
downloadAllPostsCommand:
|
||||
path: "./ghost.ts:downloadAllPostsCommand"
|
||||
command:
|
||||
name: "Ghost: Download Posts"
|
||||
downloadAllPosts:
|
||||
path: "./ghost.ts:downloadAllPosts"
|
||||
env: server
|
||||
publishCommand:
|
||||
path: "./ghost.ts:publishCommand"
|
||||
command:
|
||||
name: "Ghost: Publish"
|
||||
publish:
|
||||
path: "./ghost.ts:publish"
|
||||
env: server
|
||||
@@ -0,0 +1,232 @@
|
||||
import { readPage, writePage } from "@silverbulletmd/plugos-silverbullet-syscall/space";
|
||||
import { json } from "@silverbulletmd/plugos-syscall/fetch";
|
||||
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<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 { 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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user