@bwog/bwog 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +15 -0
- package/COPYING +21 -0
- package/README.md +191 -3
- package/app.js +20 -0
- package/bwog.config.js +5 -0
- package/bwog.service +12 -0
- package/cli.js +23 -0
- package/cloudflare/Database.js +105 -0
- package/cloudflare/worker.js +104 -0
- package/config.js +13 -0
- package/controller.js +58 -0
- package/databases/DurableObjectDatabase.js +30 -0
- package/databases/JsonDatabase.js +38 -0
- package/databases/SqliteDatabase.js +99 -0
- package/generator.js +460 -0
- package/images/logo.png +0 -0
- package/images/model.blend +0 -0
- package/nginx.config +61 -0
- package/package.json +48 -6
- package/routes/auth.js +94 -0
- package/routes/editor.js +34 -0
- package/scripts/compile_templates.js +21 -0
- package/scripts/generate_site.js +58 -0
- package/scripts/json_to_sqlite.js +20 -0
- package/stores/NodeFsStore.js +14 -0
- package/stores/R2Store.js +11 -0
- package/templates.js +244 -0
- package/views/atom_feed.eta +23 -0
- package/views/basic_html.eta +13 -0
- package/views/default.eta +168 -0
- package/views/sitemap.eta +9 -0
- package/wrangler.jsonc +72 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
|
|
3
|
+
class SqliteDatabase {
|
|
4
|
+
constructor(file) {
|
|
5
|
+
this.file = file;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async connect() {
|
|
9
|
+
this.db = new DatabaseSync(this.file);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async makeFromJsonDatabase(JsonDatabase) {
|
|
13
|
+
this.db.exec(`
|
|
14
|
+
CREATE TABLE posts (
|
|
15
|
+
id TEXT PRIMARY KEY,
|
|
16
|
+
author TEXT NOT NULL,
|
|
17
|
+
created_at TEXT NOT NULL,
|
|
18
|
+
content JSONB NOT NULL
|
|
19
|
+
);
|
|
20
|
+
`);
|
|
21
|
+
|
|
22
|
+
const insertPost = this.db.prepare('INSERT INTO posts (id, author, created_at, content) VALUES (?, ?, ?, jsonb(?));');
|
|
23
|
+
|
|
24
|
+
JsonDatabase.getAll().forEach(post => {
|
|
25
|
+
const content = {...post};
|
|
26
|
+
delete content.id;
|
|
27
|
+
delete content.author;
|
|
28
|
+
delete content.createdAt;
|
|
29
|
+
insertPost.run(post.id, post.author, post.createdAt, JSON.stringify(content));
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async getAll() {
|
|
34
|
+
const rawPosts = this.db.prepare(`
|
|
35
|
+
SELECT
|
|
36
|
+
id,
|
|
37
|
+
author,
|
|
38
|
+
created_at,
|
|
39
|
+
json(content) as content
|
|
40
|
+
FROM posts
|
|
41
|
+
ORDER BY created_at DESC;
|
|
42
|
+
`).all();
|
|
43
|
+
|
|
44
|
+
return rawPosts.map(post => ({
|
|
45
|
+
id: post.id,
|
|
46
|
+
author: post.author,
|
|
47
|
+
createdAt: post.created_at,
|
|
48
|
+
...JSON.parse(post.content)
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async getLastTwoMonthsAtLeast20() {
|
|
53
|
+
const rawPosts = this.db.prepare(`
|
|
54
|
+
WITH latest_two_months AS (
|
|
55
|
+
SELECT DISTINCT strftime('%Y-%m', created_at) AS month
|
|
56
|
+
FROM posts
|
|
57
|
+
ORDER BY month DESC
|
|
58
|
+
LIMIT 2
|
|
59
|
+
),
|
|
60
|
+
suitability AS (
|
|
61
|
+
SELECT *,
|
|
62
|
+
CASE
|
|
63
|
+
WHEN created_at IN latest_two_months THEN 1
|
|
64
|
+
ELSE 0
|
|
65
|
+
END AS in_latest_two_months,
|
|
66
|
+
ROW_NUMBER() OVER (ORDER BY created_at DESC) AS recency
|
|
67
|
+
FROM posts
|
|
68
|
+
)
|
|
69
|
+
SELECT
|
|
70
|
+
id,
|
|
71
|
+
author,
|
|
72
|
+
created_at,
|
|
73
|
+
json(content) as content
|
|
74
|
+
FROM suitability
|
|
75
|
+
WHERE in_latest_two_months = 1
|
|
76
|
+
OR recency <= 20
|
|
77
|
+
ORDER BY created_at DESC;
|
|
78
|
+
`).all();
|
|
79
|
+
|
|
80
|
+
return rawPosts.map(post => ({
|
|
81
|
+
id: post.id,
|
|
82
|
+
author: post.author,
|
|
83
|
+
createdAt: post.created_at,
|
|
84
|
+
...JSON.parse(post.content)
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async createPost(post) {
|
|
89
|
+
const insertPost = this.db.prepare('INSERT INTO posts (id, author, created_at, content) VALUES (?, ?, ?, jsonb(?));');
|
|
90
|
+
|
|
91
|
+
const content = {...post};
|
|
92
|
+
delete content.id;
|
|
93
|
+
delete content.author;
|
|
94
|
+
delete content.createdAt;
|
|
95
|
+
insertPost.run(post.id, post.author, post.createdAt, JSON.stringify(content));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export default SqliteDatabase;
|
package/generator.js
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import { Eta } from 'eta';
|
|
2
|
+
import beautify from 'js-beautify';
|
|
3
|
+
import { minify } from 'html-minifier-next';
|
|
4
|
+
import {createTranslator} from 'short-uuid';
|
|
5
|
+
|
|
6
|
+
import templates from './templates.js';
|
|
7
|
+
|
|
8
|
+
class Generator {
|
|
9
|
+
constructor(store, database) {
|
|
10
|
+
this.store = store;
|
|
11
|
+
this.database = database;
|
|
12
|
+
this.eta = new Eta();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async generate(siteUrl, authorName, authorEmail, enableAP, apHandle, apUsername, apPublicKey) {
|
|
16
|
+
const posts = await this.database.getAll();
|
|
17
|
+
const sitemap = [];
|
|
18
|
+
|
|
19
|
+
const postsByMonth = posts.reduce((acc, post) => {
|
|
20
|
+
let currentMonth = null;
|
|
21
|
+
if (acc.length > 0) {
|
|
22
|
+
const lastDate = new Date(acc[acc.length - 1][0].createdAt);
|
|
23
|
+
const currentDate = new Date(post.createdAt);
|
|
24
|
+
if (lastDate.getFullYear() === currentDate.getFullYear() && lastDate.getMonth() === currentDate.getMonth()) {
|
|
25
|
+
currentMonth = acc[acc.length - 1];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (currentMonth === null) {
|
|
30
|
+
currentMonth = [];
|
|
31
|
+
acc.push(currentMonth);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
currentMonth.push(post);
|
|
35
|
+
|
|
36
|
+
return acc;
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
await this.generateIndex(siteUrl, posts, sitemap);
|
|
41
|
+
await this.writeAtom(siteUrl, authorName, authorEmail, posts, 'feed.atom');
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error(error);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let previousMonth = postsByMonth[0][0].createdAt;
|
|
48
|
+
let currentMonth = null;
|
|
49
|
+
let nextMonth = null;
|
|
50
|
+
for (let i = 0; i < postsByMonth.length; ++i) {
|
|
51
|
+
nextMonth = currentMonth;
|
|
52
|
+
currentMonth = previousMonth;
|
|
53
|
+
|
|
54
|
+
if (postsByMonth.length > i + 1) {
|
|
55
|
+
previousMonth = postsByMonth[i + 1][0].createdAt;
|
|
56
|
+
} else {
|
|
57
|
+
previousMonth = null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
await this.writePage(postsByMonth[i],
|
|
62
|
+
this.displayMonthFromIsoTimestamp(currentMonth),
|
|
63
|
+
`${siteUrl}/${this.filenameFromIsoTimestamp(currentMonth)}`,
|
|
64
|
+
this.filenameFromIsoTimestamp(currentMonth),
|
|
65
|
+
previousMonth === null ? null : this.filenameFromIsoTimestamp(previousMonth),
|
|
66
|
+
nextMonth === null ? null : this.filenameFromIsoTimestamp(nextMonth));
|
|
67
|
+
|
|
68
|
+
sitemap.push({
|
|
69
|
+
url: `${siteUrl}/${this.filenameFromIsoTimestamp(currentMonth)}`,
|
|
70
|
+
updatedAt: currentMonth,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (enableAP) {
|
|
74
|
+
await this.writeAPOutboxPage(postsByMonth[i],
|
|
75
|
+
siteUrl,
|
|
76
|
+
this.filenameFromIsoTimestamp(currentMonth),
|
|
77
|
+
nextMonth === null ? null : this.filenameFromIsoTimestamp(nextMonth),
|
|
78
|
+
previousMonth === null ? null : this.filenameFromIsoTimestamp(previousMonth));
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error(error);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await this.writeSitemap(sitemap, 'sitemap.xml');
|
|
87
|
+
|
|
88
|
+
if (enableAP) {
|
|
89
|
+
await this.writeAPOutbox(posts, siteUrl, 'ap/outbox.json');
|
|
90
|
+
await this.writeAPWebfinger(siteUrl, apHandle, 'ap/webfinger.json');
|
|
91
|
+
await this.writeAPActor(siteUrl, apUsername, apPublicKey, authorName, 'ap/actor.json');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async generatePartial(siteUrl, authorName, authorEmail, enableAP, newPost) {
|
|
96
|
+
const posts = await this.database.getLastTwoMonthsAtLeast20();
|
|
97
|
+
const sitemap = [];
|
|
98
|
+
|
|
99
|
+
this.generateIndex(siteUrl, posts, sitemap);
|
|
100
|
+
await this.writeAtom(siteUrl, authorName, authorEmail, posts, 'feed.atom');
|
|
101
|
+
|
|
102
|
+
const newPostDate = new Date(newPost.createdAt);
|
|
103
|
+
const thisMonthPosts = posts.filter(post => {
|
|
104
|
+
const postDate = new Date(post.createdAt);
|
|
105
|
+
|
|
106
|
+
return postDate.getFullYear() === newPostDate.getFullYear()
|
|
107
|
+
&& postDate.getMonth() === newPostDate.getMonth();
|
|
108
|
+
});
|
|
109
|
+
const isNewMonth = thisMonthPosts.length === 1;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
await this.writePage(thisMonthPosts,
|
|
113
|
+
this.displayMonthFromIsoTimestamp(newPost.createdAt),
|
|
114
|
+
`${siteUrl}/${this.filenameFromIsoTimestamp(newPost.createdAt)}`,
|
|
115
|
+
this.filenameFromIsoTimestamp(newPost.createdAt),
|
|
116
|
+
posts.length > thisMonthPosts.length ? this.filenameFromIsoTimestamp(posts[thisMonthPosts.length].createdAt) : null,
|
|
117
|
+
null);
|
|
118
|
+
|
|
119
|
+
if (enableAP) {
|
|
120
|
+
await this.writeAPOutboxPage(thisMonthPosts,
|
|
121
|
+
siteUrl,
|
|
122
|
+
this.filenameFromIsoTimestamp(newPost.createdAt),
|
|
123
|
+
null,
|
|
124
|
+
posts.length > thisMonthPosts.length ? this.filenameFromIsoTimestamp(posts[thisMonthPosts.length].createdAt) : null);
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error(error);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (isNewMonth) {
|
|
131
|
+
if (posts.length > 1) {
|
|
132
|
+
const prevPostDate = new Date(posts[1].createdAt);
|
|
133
|
+
const lastMonthPosts = posts.filter(post => {
|
|
134
|
+
const postDate = new Date(post.createdAt);
|
|
135
|
+
return postDate.getFullYear() === prevPostDate.getFullYear()
|
|
136
|
+
&& postDate.getMonth() === prevPostDate.getMonth()
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
await this.writePage(lastMonthPosts,
|
|
141
|
+
this.displayMonthFromIsoTimestamp(posts[1].createdAt),
|
|
142
|
+
`${siteUrl}/${this.filenameFromIsoTimestamp(posts[1].createdAt)}`,
|
|
143
|
+
this.filenameFromIsoTimestamp(posts[1].createdAt),
|
|
144
|
+
posts.length > lastMonthPosts.length + 1 ? this.filenameFromIsoTimestamp(posts[lastMonthPosts.length + 1].createdAt) : null,
|
|
145
|
+
this.filenameFromIsoTimestamp(newPost.createdAt));
|
|
146
|
+
|
|
147
|
+
if (enableAP) {
|
|
148
|
+
await this.writeAPOutboxPage(lastMonthPosts,
|
|
149
|
+
siteUrl,
|
|
150
|
+
this.filenameFromIsoTimestamp(posts[1].createdAt),
|
|
151
|
+
this.filenameFromIsoTimestamp(newPost.createdAt),
|
|
152
|
+
posts.length > lastMonthPosts.length + 1 ? this.filenameFromIsoTimestamp(posts[lastMonthPosts.length + 1].createdAt) : null);
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error(error);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const months = posts.reduce((acc, post) => {
|
|
160
|
+
if (acc.length === 0) {
|
|
161
|
+
return [post.createdAt];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const postDate = new Date(post.createdAt);
|
|
165
|
+
const lastDate = new Date(acc[acc.length - 1]);
|
|
166
|
+
|
|
167
|
+
if (postDate.getFullYear() === lastDate.getFullYear() && postDate.getMonth() === lastDate.getMonth()) {
|
|
168
|
+
return acc;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return [...acc, post.createdAt];
|
|
172
|
+
}, []);
|
|
173
|
+
|
|
174
|
+
months.forEach(month => sitemap.push({
|
|
175
|
+
url: `${siteUrl}/${this.filenameFromIsoTimestamp(month)}`,
|
|
176
|
+
updatedAt: month,
|
|
177
|
+
}));
|
|
178
|
+
|
|
179
|
+
await this.writeSitemap(sitemap, 'sitemap.xml');
|
|
180
|
+
|
|
181
|
+
if (enableAP) {
|
|
182
|
+
await this.writeAPOutbox(posts, 'ap/outbox.json');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async generateIndex(siteUrl, posts, sitemap) {
|
|
188
|
+
const mainPage = posts.slice(0, 10);
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
await this.writePage(mainPage,
|
|
192
|
+
null,
|
|
193
|
+
`${siteUrl}/`,
|
|
194
|
+
'index',
|
|
195
|
+
this.filenameFromIsoTimestamp(posts[10].createdAt),
|
|
196
|
+
null);
|
|
197
|
+
sitemap.push({
|
|
198
|
+
url: `${siteUrl}/`,
|
|
199
|
+
updatedAt: mainPage[0].createdAt,
|
|
200
|
+
});
|
|
201
|
+
} catch (error) {
|
|
202
|
+
throw(error);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async writeSitemap(sitemap, file) {
|
|
207
|
+
const xml = beautify.html(
|
|
208
|
+
templates.sitemap.call(this.eta, {pages: sitemap}),
|
|
209
|
+
{end_with_newline: true},
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
await this.store.write(file, xml, 'application/xml');
|
|
214
|
+
} catch (err) {
|
|
215
|
+
throw (`Failed to write file: ${file}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async writePage(posts, title, canonical, file, prevPage, nextPage) {
|
|
220
|
+
const html = beautify.html(
|
|
221
|
+
templates.default.call(this.eta, {posts, title, canonical, prevPage, nextPage}, {tab_size: 4, ignore: ['style', 'script']}),
|
|
222
|
+
{
|
|
223
|
+
end_with_newline: true,
|
|
224
|
+
space_after_anon_function: true,
|
|
225
|
+
operator_position: 'after-newline',
|
|
226
|
+
extra_liners: [],
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
await this.store.write(file, html, 'text/html')
|
|
232
|
+
} catch (err) {
|
|
233
|
+
throw (`Failed to write file: ${file}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async writeAtom(siteUrl, authorName, authorEmail, posts, file) {
|
|
238
|
+
const translator = createTranslator();
|
|
239
|
+
|
|
240
|
+
const latestPosts = posts.slice(0, 20);
|
|
241
|
+
|
|
242
|
+
const postsWithDerivedData = await Promise.all(latestPosts.map(async post => ({
|
|
243
|
+
...post,
|
|
244
|
+
title: post.text.replace(/(https:\/\/\S*)/g, '').replaceAll('\n', ' ').replace(/\s\s+/g, ' ').trimEnd() || 'link',
|
|
245
|
+
uuid: translator.toUUID(post.id),
|
|
246
|
+
archivePage: `${siteUrl}/${this.filenameFromIsoTimestamp(post.createdAt)}`,
|
|
247
|
+
html: await minify(templates.basic_html.call(this.eta, {post})),
|
|
248
|
+
})));
|
|
249
|
+
|
|
250
|
+
const xml = beautify.html(
|
|
251
|
+
templates.atom_feed.call(this.eta, {
|
|
252
|
+
siteUrl,
|
|
253
|
+
authorName,
|
|
254
|
+
authorEmail,
|
|
255
|
+
posts: postsWithDerivedData
|
|
256
|
+
}),
|
|
257
|
+
{end_with_newline: true},
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
await this.store.write(file, xml, 'application/atom+xml')
|
|
262
|
+
} catch (err) {
|
|
263
|
+
throw (`Failed to write file: ${file}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async writeAPWebfinger(siteUrl, apHandle, file) {
|
|
268
|
+
const webfinger = {
|
|
269
|
+
"subject": `acct:${apHandle}`,
|
|
270
|
+
"aliases": [
|
|
271
|
+
siteUrl
|
|
272
|
+
],
|
|
273
|
+
"links": [
|
|
274
|
+
{
|
|
275
|
+
"rel": "self",
|
|
276
|
+
"type": "application/activity+json",
|
|
277
|
+
"href": `${siteUrl}/ap/actor`
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
"rel": "http://webfinger.net/rel/profile-page",
|
|
281
|
+
"type": "text/html",
|
|
282
|
+
"href": siteUrl
|
|
283
|
+
}
|
|
284
|
+
]
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const json = beautify.js(
|
|
288
|
+
JSON.stringify(webfinger),
|
|
289
|
+
{end_with_newline: true},
|
|
290
|
+
);
|
|
291
|
+
try {
|
|
292
|
+
await this.store.write(file, json, 'application/jrd+json');
|
|
293
|
+
} catch (err) {
|
|
294
|
+
throw (`Failed to write file: ${file}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async writeAPActor(siteUrl, apUsername, apPublicKey, authorName, file) {
|
|
299
|
+
const actor = {
|
|
300
|
+
"@context": [
|
|
301
|
+
"https://www.w3.org/ns/activitystreams",
|
|
302
|
+
"https://w3id.org/security/v1"
|
|
303
|
+
],
|
|
304
|
+
"id": `${siteUrl}/ap/actor`,
|
|
305
|
+
"type": "Person",
|
|
306
|
+
"preferredUsername": apUsername,
|
|
307
|
+
"name": authorName,
|
|
308
|
+
"inbox": `${siteUrl}/ap/inbox`,
|
|
309
|
+
"outbox": `${siteUrl}/ap/outbox`,
|
|
310
|
+
"icon": {
|
|
311
|
+
"type": "Image",
|
|
312
|
+
"mediaType": "image/png",
|
|
313
|
+
"url": `${siteUrl}/favicon.png`
|
|
314
|
+
},
|
|
315
|
+
"url": siteUrl,
|
|
316
|
+
"publicKey": {
|
|
317
|
+
"id": `${siteUrl}/ap/actor#main-key`,
|
|
318
|
+
"owner": `${siteUrl}/ap/actor`,
|
|
319
|
+
"publicKeyPem": apPublicKey
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const json = beautify.js(
|
|
324
|
+
JSON.stringify(actor),
|
|
325
|
+
{end_with_newline: true},
|
|
326
|
+
);
|
|
327
|
+
try {
|
|
328
|
+
await this.store.write(file, json, 'application/activity+json');
|
|
329
|
+
} catch (err) {
|
|
330
|
+
throw (`Failed to write file: ${file}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async writeAPOutbox(posts, siteUrl, file) {
|
|
335
|
+
const url = `${siteUrl}/ap/outbox`;
|
|
336
|
+
const first = this.filenameFromIsoTimestamp(new Date(posts[0].createdAt));
|
|
337
|
+
const last = this.filenameFromIsoTimestamp(new Date(posts[posts.length - 1].createdAt));
|
|
338
|
+
|
|
339
|
+
const outbox = {
|
|
340
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
341
|
+
"type": "OrderedCollection",
|
|
342
|
+
"totalItems": posts.length,
|
|
343
|
+
"first": `${url}?page=${first}`,
|
|
344
|
+
"last": `${url}?page=${last}`
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const json = beautify.js(
|
|
348
|
+
JSON.stringify(outbox),
|
|
349
|
+
{end_with_newline: true},
|
|
350
|
+
);
|
|
351
|
+
try {
|
|
352
|
+
await this.store.write(file, json, 'application/activity+json');
|
|
353
|
+
} catch (err) {
|
|
354
|
+
throw (`Failed to write file: ${file}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async writeAPOutboxPage(posts, siteUrl, currentMonth, prevPage, nextPage) {
|
|
359
|
+
const page = {
|
|
360
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
361
|
+
"type": "OrderedCollectionPage",
|
|
362
|
+
"id": `${siteUrl}/ap/outbox?page=${currentMonth}`,
|
|
363
|
+
"partOf": `${siteUrl}/ap/outbox`,
|
|
364
|
+
"prev": `${siteUrl}/ap/outbox?page=${prevPage}`,
|
|
365
|
+
"next": `${siteUrl}/ap/outbox?page=${nextPage}`,
|
|
366
|
+
"orderedItems": []
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
if (!prevPage) {
|
|
370
|
+
delete page.prev;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (!nextPage) {
|
|
374
|
+
delete page.next;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
for (const post of posts) {
|
|
378
|
+
const url = `${siteUrl}/ap/notes/${post.id}`;
|
|
379
|
+
|
|
380
|
+
const noteHtml = await minify(templates.basic_html.call(this.eta, {post}))
|
|
381
|
+
|
|
382
|
+
const note = {
|
|
383
|
+
"id": url,
|
|
384
|
+
"type": "Note",
|
|
385
|
+
"published": post.createdAt,
|
|
386
|
+
"url": `${siteUrl}/${currentMonth}`,
|
|
387
|
+
"attributedTo": `${siteUrl}/ap/actor`,
|
|
388
|
+
"to": [
|
|
389
|
+
`${siteUrl}/ap/followers`
|
|
390
|
+
],
|
|
391
|
+
"cc": [
|
|
392
|
+
"https://www.w3.org/ns/activitystreams#Public"
|
|
393
|
+
],
|
|
394
|
+
"sensitive": false,
|
|
395
|
+
"content": noteHtml
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const create = {
|
|
399
|
+
"id": `${url}?activity=true`,
|
|
400
|
+
"type": "Create",
|
|
401
|
+
"actor": `${siteUrl}/ap/actor`,
|
|
402
|
+
"published": post.createdAt,
|
|
403
|
+
"to": [
|
|
404
|
+
`${siteUrl}/ap/followers`
|
|
405
|
+
],
|
|
406
|
+
"cc": [
|
|
407
|
+
"https://www.w3.org/ns/activitystreams#Public"
|
|
408
|
+
],
|
|
409
|
+
"object": note
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
page.orderedItems.push(create);
|
|
413
|
+
|
|
414
|
+
const notePath = `ap/notes/${post.id}.json`;
|
|
415
|
+
const noteJson = beautify.js(
|
|
416
|
+
JSON.stringify(note),
|
|
417
|
+
{end_with_newline: true},
|
|
418
|
+
);
|
|
419
|
+
try {
|
|
420
|
+
await this.store.write(notePath, noteJson, 'application/activity+json');
|
|
421
|
+
} catch (err) {
|
|
422
|
+
throw (`Failed to write file: ${notePath}`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const createPath = `ap/creates/${post.id}.json`;
|
|
426
|
+
const createJson = beautify.js(
|
|
427
|
+
JSON.stringify(create),
|
|
428
|
+
{end_with_newline: true},
|
|
429
|
+
);
|
|
430
|
+
try {
|
|
431
|
+
await this.store.write(createPath, createJson, 'application/activity+json');
|
|
432
|
+
} catch (err) {
|
|
433
|
+
throw (`Failed to write file: ${createPath}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const pagePath = `ap/outbox_pages/${currentMonth}.json`;
|
|
438
|
+
const pageJson = beautify.js(
|
|
439
|
+
JSON.stringify(page),
|
|
440
|
+
{end_with_newline: true},
|
|
441
|
+
);
|
|
442
|
+
try {
|
|
443
|
+
await this.store.write(pagePath, pageJson, 'application/activity+json');
|
|
444
|
+
} catch (err) {
|
|
445
|
+
throw (`Failed to write file: ${pagePath}`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
filenameFromIsoTimestamp(timestamp) {
|
|
450
|
+
const date = new Date(timestamp);
|
|
451
|
+
return `${date.getFullYear()}-${date.getMonth() + 1}`;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
displayMonthFromIsoTimestamp(timestamp) {
|
|
455
|
+
const date = new Date(timestamp);
|
|
456
|
+
return date.toLocaleDateString(undefined, {year: 'numeric', month: 'long'});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export default Generator;
|
package/images/logo.png
ADDED
|
Binary file
|
|
Binary file
|
package/nginx.config
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
map $arg_resource $webfinger_resource {
|
|
2
|
+
"acct:username@example.com" /ap/webfinger.json;
|
|
3
|
+
"acct%3Ausername%40example.com" /ap/webfinger.json;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
map $arg_page $outbox {
|
|
7
|
+
"" /ap/outbox.json;
|
|
8
|
+
"~^(?<i>[0-9]+-[0-9]+)?" /ap/outbox_pages/$i.json;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
map $uri $note_id {
|
|
12
|
+
"~^/ap/notes/(?<id>.+)$" $id;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
map $arg_activity $notes {
|
|
16
|
+
"true" /ap/creates/$note_id.json;
|
|
17
|
+
default /ap/notes/$note_id.json;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
server {
|
|
21
|
+
listen 443 ssl;
|
|
22
|
+
ssl_certificate /etc/ssl/certs/example.com.crt;
|
|
23
|
+
ssl_certificate_key /etc/ssl/private/example.com.key;
|
|
24
|
+
|
|
25
|
+
server_name example.com;
|
|
26
|
+
root /var/www/example;
|
|
27
|
+
index index.html;
|
|
28
|
+
|
|
29
|
+
location / {
|
|
30
|
+
try_files $uri.html $uri $uri/ =404;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
location ~ ^/(post|login|logout|oauth2/redirect/google) {
|
|
34
|
+
proxy_pass http://localhost:3000;
|
|
35
|
+
proxy_http_version 1.1;
|
|
36
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
37
|
+
proxy_set_header Connection 'upgrade';
|
|
38
|
+
proxy_set_header Host $host;
|
|
39
|
+
proxy_cache_bypass $http_upgrade;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
location /.well-known/webfinger {
|
|
43
|
+
default_type "application/jrd+json";
|
|
44
|
+
try_files $webfinger_resource =404;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
location /ap {
|
|
48
|
+
types { } default_type "application/activity+json";
|
|
49
|
+
try_files $uri.json =404;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
location /ap/outbox {
|
|
53
|
+
types { } default_type "application/activity+json";
|
|
54
|
+
try_files $outbox =404;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
location /ap/notes {
|
|
58
|
+
types { } default_type "application/activity+json";
|
|
59
|
+
try_files $notes =404;
|
|
60
|
+
}
|
|
61
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@bwog/bwog",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@bwog/bwog",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "bwog is a toy static site generator for a tumblelog/microblog",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"blog",
|
|
7
|
+
"cms",
|
|
8
|
+
"fediverse",
|
|
9
|
+
"microblog",
|
|
10
|
+
"tumblelog",
|
|
11
|
+
"smallweb",
|
|
12
|
+
"static-site-generator",
|
|
13
|
+
"ssg"
|
|
14
|
+
],
|
|
15
|
+
"author": "Chase Patterson (https://github.com/chapatt)",
|
|
16
|
+
"homepage": "https://github.com/chapatt/bwog",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/chapatt/bwog.git"
|
|
20
|
+
},
|
|
21
|
+
"bugs": "https://github.com/chapatt/bwog/issues",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22.23.5"
|
|
25
|
+
},
|
|
26
|
+
"type": "module",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@hono/oauth-providers": "^0.9.1",
|
|
29
|
+
"@minify-html/wasm": "^0.18.1",
|
|
30
|
+
"commander": "^13.1.0",
|
|
31
|
+
"dotenv": "^18.0.10",
|
|
32
|
+
"eta": "^4.6.0",
|
|
33
|
+
"express": "^5.1.0",
|
|
34
|
+
"express-session": "^1.18.1",
|
|
35
|
+
"hono": "^4.13.18",
|
|
36
|
+
"html-minifier-next": "^9.1.2",
|
|
37
|
+
"js-beautify": "^2.0.3",
|
|
38
|
+
"passport": "^0.7.0",
|
|
39
|
+
"passport-google-oauth20": "^2.0.0",
|
|
40
|
+
"short-uuid": "^6.0.3"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "node scripts/compile_templates.js"
|
|
44
|
+
},
|
|
45
|
+
"bin": {
|
|
46
|
+
"bwog": "./cli.js"
|
|
47
|
+
}
|
|
48
|
+
}
|