]> git.r.bdr.sh - rbdr/linkding-linkblog-updater/blob - index.js
Add GPL
[rbdr/linkding-linkblog-updater] / index.js
1 const { exec } = require('child_process');
2 const { resolve, join} = require('path');
3 const { rm, writeFile } = require('fs/promises');
4 const { promisify } = require('util');
5
6 const internals = {
7 exec: promisify(exec),
8
9 apiUrl: process.env.API_URL,
10 apiToken: process.env.API_TOKEN,
11 blogUrl: process.env.BLOG_URL,
12 archiveUrl: process.env.ARCHIVE_URL,
13 blogPublicUrl: process.env.BLOG_PUBLIC_URL,
14 archivePublicUrl: process.env.ARCHIVE_PUBLIC_URL,
15 tootToken: process.env.TOOT_TOKEN,
16 mastodonDomain: process.env.MASTODON_DOMAIN,
17
18 date: (new Date()).toISOString().split('T')[0],
19
20 generateGemtext(title, text) {
21
22 return `# ${title}\n\n${text}`;
23 },
24
25 getText(posts) {
26
27 return posts.map((post) => {
28
29 const title = post.title || post.website_title
30 return `=> ${post.url} ${title}\n${post.notes}`;
31 }).join('\n\n');
32 },
33
34 getTitle(posts) {
35
36 const title = posts[0].title || posts[0].website_title
37
38 if (posts.length === 1) {
39 return `Link: ${title}`;
40 }
41 return `${posts.length} links for ${internals.date}`;
42 },
43
44 slugify(text) {
45
46 return text.toLowerCase().replace(/[^a-z0-9 ]/g, '').replace(/ +/g, '-')
47 },
48
49 async toot(title) {
50
51 const body = new FormData();
52 body.set(
53 'status',
54 `New post: ${title}\n\nAvailable on:\n\n♊️ the gemini archive ${internals.archivePublicUrl}\n\n or, the ephemeral blog 🌐: ${internals.blogPublicUrl}`
55 );
56 return fetch(`https://${internals.mastodonDomain}/api/v1/statuses`, {
57 method: 'POST',
58 headers: {
59 Authorization: `Bearer ${internals.tootToken}`,
60 },
61 body
62 });
63 },
64
65 async getBookmarks() {
66
67 const url = join(internals.apiUrl, 'bookmarks') + '?q=%23linkblog';
68 const response = await fetch(url, {
69 headers: {
70 'Content-Type': 'application/json',
71 Authorization: `Token ${internals.apiToken}`
72 }
73 });
74 const data = await response.json();
75
76 return data.results;
77 },
78
79 async updateBookmark(bookmark) {
80
81 const url = join(internals.apiUrl, 'bookmarks', `${bookmark.id}`)
82 const response = await fetch(url, {
83 method: 'PATCH',
84 body: JSON.stringify(
85 {
86 tag_names: bookmark.tag_names.map((tag) => tag === 'linkblog' ? 'linkblog-posted' : tag)
87 }
88 ),
89 headers: {
90 'Content-Type': 'application/json',
91 Authorization: `Token ${internals.apiToken}`
92 }
93 });
94 }
95 };
96
97
98 async function run() {
99 const bookmarks = await internals.getBookmarks();
100
101 if (bookmarks.length === 0) {
102 console.error('No links to post');
103 return;
104 }
105
106 const title = internals.getTitle(bookmarks);
107 const text = internals.getText(bookmarks);
108 const gemtext = internals.generateGemtext(title, text);
109 const filename = internals.slugify(title);
110
111 const gemfile = resolve(join(__dirname, `${filename}.gmi`));
112 await writeFile(gemfile, gemtext);
113 await internals.exec(`blog add ${gemfile}`);
114 await internals.exec(`blog publish ${internals.blogUrl}`);
115 await internals.exec(`blog publish-archive ${internals.archiveUrl}`);
116 await rm(gemfile);
117
118 for (const bookmark of bookmarks) {
119 await internals.updateBookmark(bookmark);
120 }
121
122 if (internals.tootToken) {
123 await internals.toot(title);
124 }
125 }
126
127 run()
128 .then(() => process.exit(0))
129 .catch((err) => {
130 console.error(err);
131 process.exit(1);
132 })