Changeset fe97cc3 in Klonkt
- Timestamp:
- 07/01/2026 10:13:40 PM (2 months ago)
- Branches:
- main
- Children:
- 2abd3d2
- Parents:
- 254939c
- Files:
-
- 1 added
- 7 edited
-
CHANGELOG.de.md (modified) (1 diff)
-
CHANGELOG.md (modified) (1 diff)
-
CHANGELOG.nl.md (modified) (1 diff)
-
src/config/database.js (modified) (1 diff)
-
src/services/ActivityPubService.js (modified) (4 diffs)
-
src/services/i18n.js (modified) (3 diffs)
-
src/views/pages/fedi-notifications.ejs (modified) (4 diffs)
-
test/inbound-mentions.test.js (added)
Legend:
- Unmodified
- Added
- Removed
-
CHANGELOG.de.md
r254939c rfe97cc3 7 7 8 8 ### Hinzugefügt 9 - **Eine Erwähnung ist jetzt eine Benachrichtigung.** Wenn dich jemand im Fediverse in einem 10 Beitrag erwähnt — auch in einem, der keine Antwort an dich ist — erscheint das in deinen 11 Fediverse-Benachrichtigungen mit einem Link zum Original. 9 12 - **Cover-Art bei offen geteiltem Audio.** Ein offen im Fediverse geteilter Track trägt jetzt sein 10 13 Cover (oder das Beitragscover) mit, sodass Audio-Player, die Artwork unterstützen, es statt einer -
CHANGELOG.md
r254939c rfe97cc3 7 7 8 8 ### Added 9 - **A mention is now a notification.** When someone on the fediverse mentions you in a post — 10 even one that isn't a reply to you — it shows up in your fediverse notifications with a link 11 to the original. 9 12 - **Cover art on openly shared audio.** A track shared openly on the fediverse now carries its 10 13 cover art (or the post cover), so audio players that support artwork show it instead of a blank tile. -
CHANGELOG.nl.md
r254939c rfe97cc3 7 7 8 8 ### Toegevoegd 9 - **Een vermelding is nu een melding.** Als iemand op de fediverse je noemt in een post — ook 10 eentje die geen reactie op jou is — verschijnt dat in je fediverse-meldingen met een link naar 11 het origineel. 9 12 - **Cover-art op openbaar gedeelde audio.** Een track die je openbaar op de fediverse deelt draagt 10 13 nu z'n cover-art mee (of de post-cover), zodat audiospelers die artwork ondersteunen die tonen in -
src/config/database.js
r254939c rfe97cc3 347 347 ); 348 348 CREATE INDEX IF NOT EXISTS idx_poll_votes_post ON poll_votes(post_id); 349 CREATE TABLE IF NOT EXISTS ap_mentions ( 350 id INTEGER PRIMARY KEY AUTOINCREMENT, 351 slug TEXT NOT NULL, -- our mentioned site/actor 352 object_uri TEXT NOT NULL, -- the remote note that mentions us 353 note_url TEXT, -- its human URL (open/interact) 354 actor_uri TEXT, actor_name TEXT, actor_handle TEXT, actor_icon TEXT, actor_url TEXT, 355 content TEXT, -- sanitized HTML snippet of the mentioning note 356 published TEXT, 357 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 358 UNIQUE(slug, object_uri) 359 ); 360 CREATE INDEX IF NOT EXISTS idx_ap_mentions_slug ON ap_mentions(slug, created_at); 349 361 CREATE TABLE IF NOT EXISTS ap_reports ( 350 362 id INTEGER PRIMARY KEY AUTOINCREMENT, -
src/services/ActivityPubService.js
r254939c rfe97cc3 660 660 // ── HTTP Signatures + delivery ──────────────────────────────────── 661 661 const slugFromActorUrl = (url) => { const m = String(url || '').match(/\/ap\/users\/([^/?#]+)/); return m ? decodeURIComponent(m[1]) : null; }; 662 // Which of OUR sites are named in a note's Mention tags? Only hrefs on our own base count 663 // (an /ap/users/<slug> path on a remote host is someone else's actor), and the slug must be 664 // an existing site. Deduped. 665 export function localMentionSlugs(tags, base) { 666 if (!base) return []; 667 const out = [], seen = new Set(); 668 for (const t of (Array.isArray(tags) ? tags : (tags ? [tags] : []))) { 669 if (!t || t.type !== 'Mention' || typeof t.href !== 'string') continue; 670 if (!t.href.startsWith(base + '/ap/users/')) continue; 671 const slug = slugFromActorUrl(t.href); 672 if (!slug || seen.has(slug)) continue; seen.add(slug); 673 try { if (db.prepare('SELECT 1 FROM sites WHERE slug = ?').get(slug)) out.push(slug); } catch { /* ignore */ } 674 } 675 return out; 676 } 662 677 663 678 // Sign + POST an activity to a remote inbox (draft-cavage HTTP Signatures, RSA-SHA256). … … 1073 1088 } 1074 1089 console.log('[AP] timeline +', actorUri, 'x' + subs.length); 1090 } 1091 } 1092 // Mentioned in a post that is NOT a reply to our content (a reply to us already returned 1093 // above): store a mention notification for each of our actors named in the Mention tags. 1094 // Requires our own base prefix on the tag href — /ap/users/<slug> on a REMOTE host is 1095 // someone else's actor, not ours. 1096 if (actorUri && !isLocalActor && o.id) { 1097 const slugs = localMentionSlugs(o.tag, base); 1098 if (slugs.length) { 1099 const ai = actorInfo(await resolveActor(actorUri), actorUri); 1100 const html = HtmlSanitizerService.sanitize(o.content || ''); 1101 for (const slug of slugs) { 1102 try { 1103 const r = db.prepare('INSERT OR IGNORE INTO ap_mentions (slug, object_uri, note_url, actor_uri, actor_name, actor_handle, actor_icon, actor_url, content, published, created_at) VALUES (?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP)') 1104 .run(slug, o.id, safeUrl(o.url) || null, actorUri, ai.name, ai.handle, ai.icon, ai.url, html, o.published || null); 1105 if (r.changes) console.log('[AP] mention', actorUri, '→', slug); 1106 } catch { /* ignore */ } 1107 } 1075 1108 } 1076 1109 } … … 2161 2194 } 2162 2195 } catch { /* ignore */ } 2196 try { 2197 for (const r of db.prepare('SELECT object_uri, note_url, actor_uri, actor_name, actor_handle, actor_icon, actor_url, content, created_at FROM ap_mentions WHERE slug = ? ORDER BY created_at DESC LIMIT 50').all(slug)) { 2198 out.push({ type: 'mention', name: r.actor_name, handle: r.actor_handle, url: r.actor_url || r.actor_uri, icon: r.actor_icon, content: stripLeadingMentions(r.content), note_url: r.note_url || r.object_uri, created_at: r.created_at }); 2199 } 2200 } catch { /* ignore */ } 2163 2201 out.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); 2164 2202 return out.slice(0, limit || 60); … … 2335 2373 listOutbox, deliverOutboxDelete, deliverOutboxUpdate, 2336 2374 webfingerResolve, followActor, resolveRemoteActor, unfollowActor, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, sendInteraction, voteOnPoll, voteOnRemotePoll, 2337 parseOwnPoll, pollTally, ownPollView, deliverPollUpdate, maybeCrawlThread, sendReport, 2375 parseOwnPoll, pollTally, ownPollView, deliverPollUpdate, maybeCrawlThread, sendReport, localMentionSlugs, 2338 2376 autoBoostCount, boostedCount, markBoosted, unmarkBoosted, markLiked, unmarkLiked, getTimelineReaction, upsertBoostedNote, getCirkelPosts, getCirkelMembers, selfHealTimeline, 2339 2377 getNotifications, listBlocks, isBlockedAny, blockTarget, unblock, -
src/services/i18n.js
r254939c rfe97cc3 28 28 'nav.language': 'Taal', 29 29 'nav.notifications': 'Meldingen', 30 'notif.title': 'Meldingen', 'notif.empty': 'Nog geen meldingen.', 'notif.someone': 'Iemand', 'notif.followed': 'volgt je nu', 'notif.liked': 'likete je post', 'notif.boosted': 'boostte je post', 'notif.replied': 'reageerde op', 'notif.reported': 'rapporteerde je bij hun server', ' blk.title': 'Blokkeren', 'blk.lead': 'Blokkeer een account of een heel domein — hun reacties, likes en posts verdwijnen en nieuwe worden geweigerd.', 'blk.block_btn': 'Blokkeren', 'blk.empty': 'Niks geblokkeerd.', 'blk.unblock': 'Deblokkeren', 'tl.block': 'Blokkeer',30 'notif.title': 'Meldingen', 'notif.empty': 'Nog geen meldingen.', 'notif.someone': 'Iemand', 'notif.followed': 'volgt je nu', 'notif.liked': 'likete je post', 'notif.boosted': 'boostte je post', 'notif.replied': 'reageerde op', 'notif.reported': 'rapporteerde je bij hun server', 'notif.mentioned': 'noemde je in een post', 'blk.title': 'Blokkeren', 'blk.lead': 'Blokkeer een account of een heel domein — hun reacties, likes en posts verdwijnen en nieuwe worden geweigerd.', 'blk.block_btn': 'Blokkeren', 'blk.empty': 'Niks geblokkeerd.', 'blk.unblock': 'Deblokkeren', 'tl.block': 'Blokkeer', 31 31 'notif.reply': '{actor} reageerde op je reactie', 'notif.comment': '{actor} reageerde op je post', 'notif.like': '{actor} vindt je post leuk', 32 32 'switch.agenda': 'Agenda', … … 959 959 'nav.language': 'Language', 960 960 'nav.notifications': 'Notifications', 961 'notif.title': 'Notifications', 'notif.empty': 'No notifications yet.', 'notif.someone': 'Someone', 'notif.followed': 'followed you', 'notif.liked': 'liked your post', 'notif.boosted': 'boosted your post', 'notif.replied': 'replied to', 'notif.reported': 'reported you to their server', ' blk.title': 'Blocking', 'blk.lead': 'Block an account or a whole domain — their replies, likes and posts disappear and new ones are refused.', 'blk.block_btn': 'Block', 'blk.empty': 'Nothing blocked.', 'blk.unblock': 'Unblock', 'tl.block': 'Block',961 'notif.title': 'Notifications', 'notif.empty': 'No notifications yet.', 'notif.someone': 'Someone', 'notif.followed': 'followed you', 'notif.liked': 'liked your post', 'notif.boosted': 'boosted your post', 'notif.replied': 'replied to', 'notif.reported': 'reported you to their server', 'notif.mentioned': 'mentioned you in a post', 'blk.title': 'Blocking', 'blk.lead': 'Block an account or a whole domain — their replies, likes and posts disappear and new ones are refused.', 'blk.block_btn': 'Block', 'blk.empty': 'Nothing blocked.', 'blk.unblock': 'Unblock', 'tl.block': 'Block', 962 962 'notif.reply': '{actor} replied to your comment', 'notif.comment': '{actor} commented on your post', 'notif.like': '{actor} liked your post', 963 963 'switch.agenda': 'Agenda', … … 1881 1881 'nav.language': 'Sprache', 1882 1882 'nav.notifications': 'Benachrichtigungen', 1883 'notif.title': 'Benachrichtigungen', 'notif.empty': 'Noch keine Benachrichtigungen.', 'notif.someone': 'Jemand', 'notif.followed': 'folgt dir jetzt', 'notif.liked': 'gefällt dein Beitrag', 'notif.boosted': 'teilte deinen Beitrag', 'notif.replied': 'antwortete auf', 'notif.reported': 'hat dich bei ihrem Server gemeldet', ' blk.title': 'Blockieren', 'blk.lead': 'Blockiere ein Konto oder eine ganze Domain — ihre Antworten, Likes und Beiträge verschwinden und neue werden abgelehnt.', 'blk.block_btn': 'Blockieren', 'blk.empty': 'Nichts blockiert.', 'blk.unblock': 'Entsperren', 'tl.block': 'Blockieren',1883 'notif.title': 'Benachrichtigungen', 'notif.empty': 'Noch keine Benachrichtigungen.', 'notif.someone': 'Jemand', 'notif.followed': 'folgt dir jetzt', 'notif.liked': 'gefällt dein Beitrag', 'notif.boosted': 'teilte deinen Beitrag', 'notif.replied': 'antwortete auf', 'notif.reported': 'hat dich bei ihrem Server gemeldet', 'notif.mentioned': 'hat dich in einem Beitrag erwähnt', 'blk.title': 'Blockieren', 'blk.lead': 'Blockiere ein Konto oder eine ganze Domain — ihre Antworten, Likes und Beiträge verschwinden und neue werden abgelehnt.', 'blk.block_btn': 'Blockieren', 'blk.empty': 'Nichts blockiert.', 'blk.unblock': 'Entsperren', 'tl.block': 'Blockieren', 1884 1884 'notif.reply': '{actor} hat auf deinen Kommentar geantwortet', 'notif.comment': '{actor} hat deinen Beitrag kommentiert', 'notif.like': '{actor} gefällt dein Beitrag', 1885 1885 'switch.agenda': 'Termine', -
src/views/pages/fedi-notifications.ejs
r254939c rfe97cc3 7 7 <ul class="nt-list"> 8 8 <% items.forEach(function(n){ 9 var _ic = n.type === 'follow' ? 'follow' : (n.type === 'like' ? 'like' : (n.type === 'announce' ? 'boost' : (n.type === 'report' ? 'report' : 'reply')));9 var _ic = n.type === 'follow' ? 'follow' : (n.type === 'like' ? 'like' : (n.type === 'announce' ? 'boost' : (n.type === 'report' ? 'report' : (n.type === 'mention' ? 'mention' : 'reply')))); 10 10 %> 11 11 <li class="nt-item nt-<%= _ic %>"> … … 15 15 <% } else if (_ic === 'follow') { %><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" y1="8" x2="19" y2="14"/><line x1="22" y1="11" x2="16" y2="11"/></svg> 16 16 <% } else if (_ic === 'report') { %><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" y1="22" x2="4" y2="15"/></svg> 17 <% } else if (_ic === 'mention') { %><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-3.92 7.94"/></svg> 17 18 <% } else { %><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg><% } %> 18 19 </span> … … 28 29 <% } else if (n.type === 'announce') { %><%= t('notif.boosted') %> 29 30 <% } else if (n.type === 'report') { %><%= t('notif.reported') %> 31 <% } else if (n.type === 'mention') { %><%= t('notif.mentioned') %> 30 32 <% } else { %><%= t('notif.replied') %><% } %> 31 33 <% if (n.post_slug) { %><a class="nt-post" href="/<%= n.post_slug %>"><%= n.post_title || n.post_slug %></a><% } %> 34 <% if (n.type === 'mention' && n.note_url) { %><a class="nt-post" href="<%= n.note_url %>" target="_blank" rel="nofollow noopener"><%= t('tl.view_original') %></a><% } %> 32 35 </div> 33 36 <% if (n.type === 'report' && n.content) { %><div class="nt-content"><%= n.content %></div> 34 <% } else if ( n.type === 'reply'&& n.content) { %><div class="nt-content"><%- n.content %></div><% } %>37 <% } else if ((n.type === 'reply' || n.type === 'mention') && n.content) { %><div class="nt-content"><%- n.content %></div><% } %> 35 38 </div> 36 39 </li> … … 59 62 .nt-follow .nt-icon, .nt-reply .nt-icon { background: color-mix(in srgb, var(--accent, #888) 16%, transparent); color: var(--accent, #06c); } 60 63 .nt-report .nt-icon { background: color-mix(in srgb, #c0392b 16%, transparent); color: #c0392b; } 64 .nt-mention .nt-icon { background: color-mix(in srgb, #8e6cf0 16%, transparent); color: #8e6cf0; } 61 65 .nt-body { flex: 1; min-width: 0; } 62 66 .nt-line { display: flex; align-items: baseline; gap: .4rem; }
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)