const { exec } = require('child_process'); const { resolve, join} = require('path'); const { rm, writeFile } = require('fs/promises'); const { promisify } = require('util'); const internals = { exec: promisify(exec), apiUrl: process.env.API_URL, apiToken: process.env.API_TOKEN, blogUrl: process.env.BLOG_URL, archiveUrl: process.env.ARCHIVE_URL, blogPublicUrl: process.env.BLOG_PUBLIC_URL, archivePublicUrl: process.env.ARCHIVE_PUBLIC_URL, tootToken: process.env.TOOT_TOKEN, mastodonDomain: process.env.MASTODON_DOMAIN, date: (new Date()).toISOString().split('T')[0], generateGemtext(title, text) { return `# ${title}\n\n${text}`; }, getText(posts) { return posts.map((post) => { const title = post.title || post.website_title return `=> ${post.url} ${title}\n${post.notes}`; }).join('\n\n'); }, getTitle(posts) { const title = posts[0].title || posts[0].website_title if (posts.length === 1) { return `Link: ${title}`; } return `${posts.length} links for ${internals.date}`; }, slugify(text) { return text.toLowerCase().replace(/[^a-z0-9 ]/g, '').replace(/ +/g, '-') }, async toot(title) { const body = new FormData(); body.set( 'status', `New post: ${title}\n\nAvailable on:\n\nā™Šļø the gemini archive ${internals.archivePublicUrl}\n\n or, the ephemeral blog 🌐: ${internals.blogPublicUrl}` ); return fetch(`https://${internals.mastodonDomain}/api/v1/statuses`, { method: 'POST', headers: { Authorization: `Bearer ${internals.tootToken}`, }, body }); }, async getBookmarks() { const url = join(internals.apiUrl, 'bookmarks') + '?q=%23linkblog'; const response = await fetch(url, { headers: { 'Content-Type': 'application/json', Authorization: `Token ${internals.apiToken}` } }); const data = await response.json(); return data.results; }, async updateBookmark(bookmark) { const url = join(internals.apiUrl, 'bookmarks', `${bookmark.id}`) const response = await fetch(url, { method: 'PATCH', body: JSON.stringify( { tag_names: bookmark.tag_names.map((tag) => tag === 'linkblog' ? 'linkblog-posted' : tag) } ), headers: { 'Content-Type': 'application/json', Authorization: `Token ${internals.apiToken}` } }); } }; async function run() { const bookmarks = await internals.getBookmarks(); if (bookmarks.length === 0) { console.error('No links to post'); return; } const title = internals.getTitle(bookmarks); const text = internals.getText(bookmarks); const gemtext = internals.generateGemtext(title, text); const filename = internals.slugify(title); const gemfile = resolve(join(__dirname, `${filename}.gmi`)); await writeFile(gemfile, gemtext); await internals.exec(`blog sync-down`); await internals.exec(`blog add ${gemfile}`); await internals.exec(`blog publish ${internals.blogUrl}`); await internals.exec(`blog publish-archive ${internals.archiveUrl}`); await internals.exec(`blog sync-up`); await rm(gemfile); for (const bookmark of bookmarks) { await internals.updateBookmark(bookmark); } if (internals.tootToken) { await internals.toot(title); } } run() .then(() => process.exit(0)) .catch((err) => { console.error(err); process.exit(1); })