source: Klonkt/src/services/Scheduler.js@ e951b7c

main
Last change on this file since e951b7c was 5a6a457, checked in by Robin Genis <roboburr@…>, 3 months ago

feat(fediverse): federate on save+scheduler, delivery retry-queue, music as listen-link

#1 Posts now federate to followers not only on create, but also when a draft/

scheduled post becomes published (editor save) and when the Scheduler flips a
scheduled post live — previously those silently didn't reach followers.

#2 Delivery retry-queue (ap_delivery): a failed delivery (down server/timeout) is

queued and retried with backoff (1/5/15/60/180/360 min, 6 tries) by a worker,
instead of fire-and-forget. Signing key re-derived from the slug, never stored.

#3 Music posts: audio shortcodes federate as a '🎵 listen on the site' link to the

post (protected player) instead of the raw mp3 — keeps Klonkt's audio friction
intact (no downloadable file handed to Mastodon).

Co-Authored-By: Claude <noreply@…>

  • Property mode set to 100644
File size: 2.3 KB
RevLine 
[b9dc94c]1/**
[834bcc3]2 * Scheduler — release planning (premium #3).
[b9dc94c]3 *
[834bcc3]4 * Scheduled posts have status 'scheduled' + publish_at (future). A lightweight
5 * timer flips them to 'published' once publish_at is reached. This means public
6 * queries (status='published') need NO changes — a scheduled post simply isn't
7 * 'published' yet and therefore invisible until that moment.
[b9dc94c]8 */
9
10import db from '../config/database.js';
11import HtmlSanitizerService from './HtmlSanitizerService.js';
[5a6a457]12import ActivityPubService from './ActivityPubService.js';
[b9dc94c]13
14export function flipScheduledPosts() {
15 try {
16 const due = db.prepare(`
[5a6a457]17 SELECT p.id, p.site_id, p.slug, p.title, p.content, p.cover_image_url, p.fan_only,
18 p.published_at, p.publish_at, p.created_at, u.username
[b9dc94c]19 FROM posts p JOIN users u ON u.id = p.author_id
[015ce46]20 WHERE p.status = 'scheduled' AND p.publish_at IS NOT NULL AND datetime(p.publish_at) <= datetime('now')
[b9dc94c]21 `).all();
22 if (!due.length) return 0;
23 const upd = db.prepare(
24 "UPDATE posts SET status = 'published', published_at = COALESCE(published_at, publish_at, CURRENT_TIMESTAMP) WHERE id = ?"
25 );
26 const fts = db.prepare('INSERT INTO posts_fts(content, title, author, post_id) VALUES (?, ?, ?, ?)');
[5a6a457]27 const siteStmt = db.prepare('SELECT * FROM sites WHERE id = ?');
[b9dc94c]28 for (const p of due) {
29 upd.run(p.id);
[834bcc3]30 try { fts.run(HtmlSanitizerService.toPlainText(p.content || ''), p.title || '', p.username || '', p.id); } catch { /* FTS failure is non-fatal */ }
[5a6a457]31 // ActivityPub: federate the now-published post to followers.
32 if (!p.fan_only) {
33 try {
34 const site = siteStmt.get(p.site_id);
35 if (site) {
36 ActivityPubService.deliverCreate(site, {
37 id: p.id, slug: p.slug, title: p.title || p.slug,
38 content: p.content, cover_image_url: p.cover_image_url || null,
39 published_at: p.published_at || p.publish_at, created_at: p.created_at,
40 }).catch(() => { /* best-effort */ });
41 }
42 } catch { /* non-fatal */ }
43 }
[b9dc94c]44 }
45 return due.length;
46 } catch { return 0; }
47}
48
49let _timer = null;
50export function startScheduler() {
[834bcc3]51 flipScheduledPosts(); // run immediately on boot
[b9dc94c]52 if (_timer) return;
[834bcc3]53 _timer = setInterval(flipScheduledPosts, 60 * 1000); // every minute
[b9dc94c]54 if (_timer.unref) _timer.unref();
55}
Note: See TracBrowser for help on using the repository browser.