-
Notifications
You must be signed in to change notification settings - Fork 4
/
_syndicate.ts
161 lines (124 loc) · 4.35 KB
/
_syndicate.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
import { idOf, notePermalinkOf } from './src/_includes/permalinks.ts';
import { extract } from 'std/front_matter/any.ts';
import * as Yaml from 'std/yaml/mod.ts';
export interface Todo {
id: string;
/** Description or excerpt from content. */
description?: string;
/** Specially crafted for social media statuses. */
statusBody?: string;
/** Raw Markdown content. */
content: string;
/** Parsed `content`. */
children: string;
meta: Record<string, any>;
sourcePath: string;
}
export const TODO_PATH = './.mastodon-todo.json';
const LOG_FILE = '.mastodon-notes';
export const maybeSaveTodo = async (page: Lume.Page) => {
const done = await Deno.readTextFile(LOG_FILE);
const id = idOf(page.sourcePath);
// Already posted, bail
if (done.includes(id)) {
console.log(`Page already exists: ${id} (${page.sourcePath}). Skipping…`);
return;
}
if (page.data.draft || page.data.skip_mastodon) {
console.log(
`Skipping posting because one of "draft" or "skip_mastodon" are true for note ${page.sourcePath}`,
);
return;
}
const todo: Todo = {
id,
children: page.data.children,
content: page.data.content as string,
meta: page.data.meta,
sourcePath: page.sourcePath,
description: page.data.excerpt || page.data.description,
statusBody: page.data.statusbody,
};
await Deno.writeTextFile(TODO_PATH, JSON.stringify(todo));
console.log(`Wrote ${page.sourcePath} to ${TODO_PATH}`);
};
export const postStatus = async (todo: Todo, accessToken: string, dryRun = false) => {
const API_ROOT = `https://${todo.meta.mastodon.instance}`;
console.log(`> To be posted: ${todo.id} (${todo.sourcePath})`);
const permalink = todo.meta.site + notePermalinkOf(todo.id);
const statusBody = truncateToStatus(todo, permalink);
console.log(`> Posting status (${statusBody.length} chars):`);
console.log('\n' + indent(`${statusBody}`, 3) + '\n');
if (dryRun) {
await persistStatusUrl(todo, 'https://fake-url.com', true);
return;
}
const url = new URL('/api/v1/statuses', API_ROOT);
const form = new FormData();
form.append('status', statusBody);
const res = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Idempotency-Key': crypto.randomUUID(),
},
body: form,
});
interface Status {
id: string;
url: string;
}
interface Error {
error: string;
}
if (!res.ok) {
const err = await res.json() as Error;
throw new Error(`Posting failed with ${res.status}: ${err.error}`);
}
const json = await res.json() as Status;
console.log(`> Status posted to ${json.url}`);
await persistStatusUrl(todo, json.url);
};
export const persistStatusUrl = async (todo: Todo, url: string, dryRun = false) => {
const data = {
fediUrl: url,
};
const filePath = './src' + todo.sourcePath;
const post = await Deno.readTextFile(filePath);
const res = extract(post);
const newData = {
...res.attrs,
...data,
};
const content = `---\n${Yaml.stringify(newData)}---\n${res.body}`;
if (dryRun) {
console.log(`> Save to ${filePath}`);
console.log(content);
} else {
await Deno.writeTextFile(filePath, content);
}
};
const indent = (str: string, n: number) => {
const INDENT = ' '.repeat(n);
return str.replaceAll('\n', `\n${INDENT}`).replace(/^/, INDENT);
};
const truncateToStatus = (note: Todo, permalink: string) => {
const maxLimit = 500;
const footer = `\n\n→ ${permalink}`;
const statusLimit = maxLimit - footer.length;
const truncate = (str: string) => {
if (str.length <= statusLimit) {
return str + footer;
}
return str.slice(0, statusLimit - 1) + '…' + footer;
};
const status = note.statusBody ?? note.description;
// 1. Use explicit status or description field.
if (status) return truncate(status);
const content = note.content;
// 2. Use first paragraph.
const firstMarkdownParagraph = content.split('\n\n').at(0);
if (firstMarkdownParagraph) return truncate(firstMarkdownParagraph);
// 3. Truncate the whole body.
return truncate(content);
};