Changeset f5c3870 in Klonkt
- Timestamp:
- 06/24/2026 04:08:34 PM (3 months ago)
- Branches:
- main
- Children:
- 19a72df
- Parents:
- d0cab9d
- Location:
- src
- Files:
-
- 1 added
- 8 edited
-
config/database.js (modified) (1 diff)
-
middleware/render.js (modified) (2 diffs)
-
routes/posts.js (modified) (2 diffs)
-
services/ActivityPubService.js (modified) (3 diffs)
-
services/i18n.js (modified) (3 diffs)
-
views/pages/admin.ejs (modified) (1 diff)
-
views/pages/blocks.ejs (added)
-
views/pages/timeline.ejs (modified) (1 diff)
-
views/partials/topnav.ejs (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/config/database.js
rd0cab9d rf5c3870 379 379 ); 380 380 CREATE INDEX IF NOT EXISTS idx_ap_timeline_slug ON ap_timeline(slug, published); 381 CREATE TABLE IF NOT EXISTS ap_blocks ( 382 id INTEGER PRIMARY KEY AUTOINCREMENT, 383 slug TEXT NOT NULL, -- our site that set the block 384 target TEXT NOT NULL, -- actor URI (actor block) or domain (domain block) 385 kind TEXT NOT NULL, -- 'actor' | 'domain' 386 label TEXT, -- display (@handle or domain) 387 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 388 UNIQUE(slug, target) 389 ); 390 CREATE INDEX IF NOT EXISTS idx_ap_blocks_target ON ap_blocks(target); 381 391 `); 382 392 } -
src/middleware/render.js
rd0cab9d rf5c3870 100 100 const _role = _u ? _u.role : null; 101 101 const canSeeBeheer = !!(_u && (_role === 'god' || _role === 'admin' || _role === 'kijker' || userOwnsSite)); 102 // Who may use the fediverse client (timeline/notifications/blocking) β actual 103 // site managers only (these routes are requireSiteManager; viewers are excluded). 104 const canManageFedi = !!(_u && (_role === 'god' || _role === 'admin' || userOwnsSite)); 102 105 103 106 // Interface language: session choice (this session) β logged-in user's own preference … … 118 121 userOwnsSite, 119 122 canSeeBeheer, 123 canManageFedi, 120 124 isViewer: _isViewer, 121 125 canMutate: !_isViewer, -
src/routes/posts.js
rd0cab9d rf5c3870 86 86 'tag', 'type', 'user', 'users', 'artiesten', 'leden', 'favorieten', 'feed.xml', 'atom.xml', 'sitemap.xml', 87 87 'manifest.webmanifest', 'sw.js', 'favicon.ico', 'favicon.svg', 'assets', 88 'authorize_interaction', 'fediverse', 'tijdlijn', 'meldingen', 88 'authorize_interaction', 'fediverse', 'tijdlijn', 'meldingen', 'blokkeren', 89 89 ]); 90 90 … … 613 613 const items = site ? ActivityPubService.getNotifications(site.slug, 80) : []; 614 614 renderPage(req, res, 'pages/fedi-notifications', { pageTitle: 'Meldingen', bodyClass: 'on-special', items }); 615 }); 616 617 // Blocking / defederation (owner-only). 618 router.get('/blokkeren', requireSiteManager, (req, res) => { 619 const site = res.locals.site; 620 const blocks = site ? ActivityPubService.listBlocks(site.slug) : []; 621 renderPage(req, res, 'pages/blocks', { pageTitle: 'Blokkeren', bodyClass: 'on-special', blocks, success: req.query.success || null, error: req.query.error || null }); 622 }); 623 624 router.post('/blokkeren/add', requireSiteManager, async (req, res) => { 625 const site = res.locals.site; 626 let q = 'success=' + encodeURIComponent('Geblokkeerd'); 627 if (site) { 628 try { 629 const r = await ActivityPubService.blockTarget(site, (req.body.target || '').toString()); 630 if (r && r.error) q = 'error=' + encodeURIComponent(r.error === 'not_found' ? 'Account niet gevonden' : 'Voer een @handle of domein in'); 631 else q = 'success=' + encodeURIComponent(((r && r.label) || '') + ' geblokkeerd'); 632 } catch (e) { q = 'error=' + encodeURIComponent('Blokkeren mislukt'); } 633 } 634 const ref = req.get('Referer') || ''; 635 res.redirect((ref.includes('/tijdlijn') ? '/tijdlijn?' : '/blokkeren?') + q); 636 }); 637 638 router.post('/blokkeren/remove', requireSiteManager, (req, res) => { 639 const site = res.locals.site; 640 if (site) { try { ActivityPubService.unblock(site, (req.body.target || '').toString()); } catch (e) { /* ignore */ } } 641 res.redirect('/blokkeren?success=' + encodeURIComponent('Deblokkeerd')); 615 642 }); 616 643 -
src/services/ActivityPubService.js
rd0cab9d rf5c3870 364 364 // forged replies/likes/follows/timeline posts). GET/discovery stays open. 365 365 const claimedActor = typeof act.actor === 'string' ? act.actor : (act.actor && act.actor.id); 366 // Blocked actor/domain β silently drop (202, don't reveal the block). 367 if (claimedActor && isBlockedAny(claimedActor)) { console.log('[AP] inbox dropped (blocked)', claimedActor); return 202; } 366 368 const GATED = ['Create', 'Like', 'Announce', 'Follow', 'Delete', 'Undo', 'Accept', 'Reject']; 367 369 if (GATED.includes(type)) { … … 774 776 } 775 777 778 // ββ Blocking / defederation βββββββββββββββββββββββββββββββββββββββ 779 let _insBl, _delBl, _listBl; 780 function blStmts() { 781 if (!_insBl) { 782 _insBl = db.prepare('INSERT OR IGNORE INTO ap_blocks (slug, target, kind, label, created_at) VALUES (?,?,?,?,CURRENT_TIMESTAMP)'); 783 _delBl = db.prepare('DELETE FROM ap_blocks WHERE slug = ? AND target = ?'); 784 _listBl = db.prepare('SELECT * FROM ap_blocks WHERE slug = ? ORDER BY created_at DESC'); 785 } 786 return { ins: _insBl, del: _delBl, list: _listBl }; 787 } 788 export function listBlocks(slug) { return blStmts().list.all(slug); } 789 790 // True if an actor (or its whole domain) is blocked anywhere on this instance. 791 export function isBlockedAny(actorUri) { 792 if (!actorUri) return false; 793 let domain = ''; try { domain = new URL(actorUri).host; } catch { /* ignore */ } 794 try { return !!db.prepare("SELECT 1 FROM ap_blocks WHERE (kind='actor' AND target=?) OR (kind='domain' AND target=?) LIMIT 1").get(actorUri, domain); } 795 catch { return false; } 796 } 797 798 function purgeBlocked(kind, target) { 799 try { 800 if (kind === 'domain') { 801 const like = `%//${target}/%`; 802 db.prepare('DELETE FROM ap_interactions WHERE actor_uri LIKE ?').run(like); 803 db.prepare('DELETE FROM ap_timeline WHERE author_uri LIKE ?').run(like); 804 db.prepare('DELETE FROM ap_followers WHERE actor_uri LIKE ?').run(like); 805 } else { 806 db.prepare('DELETE FROM ap_interactions WHERE actor_uri = ?').run(target); 807 db.prepare('DELETE FROM ap_timeline WHERE author_uri = ?').run(target); 808 db.prepare('DELETE FROM ap_followers WHERE actor_uri = ?').run(target); 809 } 810 } catch { /* best-effort */ } 811 } 812 813 // Block an actor (@handle or actor URL) or a whole domain; purges their content. 814 export async function blockTarget(site, input) { 815 const raw = String(input || '').trim(); 816 if (!site || !site.slug || !raw) return { error: 'empty' }; 817 let kind, target, label; 818 if (/^https?:\/\//i.test(raw)) { kind = 'actor'; target = raw; label = raw; } 819 else if (raw.includes('@')) { 820 const actorUrl = await webfingerResolve(raw); 821 if (!actorUrl) return { error: 'not_found' }; 822 kind = 'actor'; target = actorUrl; label = raw.startsWith('@') ? raw : ('@' + raw); 823 } else { kind = 'domain'; target = raw.toLowerCase(); label = raw.toLowerCase(); } 824 blStmts().ins.run(site.slug, target, kind, label); 825 purgeBlocked(kind, target); 826 console.log('[AP] block', site.slug, kind, target); 827 return { ok: true, label }; 828 } 829 830 export function unblock(site, target) { blStmts().del.run(site.slug, target); return { ok: true }; } 831 776 832 export default { 777 833 getOrCreateKeys, apWants, sendAP, actorId, noteId, … … 781 837 listOutbox, deliverOutboxDelete, 782 838 webfingerResolve, followActor, unfollowActor, listFollowing, getTimeline, sendInteraction, 783 getNotifications, 839 getNotifications, listBlocks, isBlockedAny, blockTarget, unblock, 784 840 }; -
src/services/i18n.js
rd0cab9d rf5c3870 26 26 'nav.language': 'Taal', 27 27 'nav.notifications': 'Meldingen', 28 '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', 28 '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', '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', 29 29 'notif.reply': '{actor} reageerde op je reactie', 'notif.comment': '{actor} reageerde op je post', 'notif.like': '{actor} vindt je post leuk', 30 30 'switch.agenda': 'Agenda', … … 1029 1029 'nav.language': 'Language', 1030 1030 'nav.notifications': 'Notifications', 1031 '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', 1031 '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', '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', 1032 1032 'notif.reply': '{actor} replied to your comment', 'notif.comment': '{actor} commented on your post', 'notif.like': '{actor} liked your post', 1033 1033 'switch.agenda': 'Agenda', … … 2023 2023 'nav.language': 'Sprache', 2024 2024 'nav.notifications': 'Benachrichtigungen', 2025 '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', 2025 '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', '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', 2026 2026 'notif.reply': '{actor} hat auf deinen Kommentar geantwortet', 'notif.comment': '{actor} hat deinen Beitrag kommentiert', 'notif.like': '{actor} gefΓ€llt dein Beitrag', 2027 2027 'switch.agenda': 'Termine', -
src/views/pages/admin.ejs
rd0cab9d rf5c3870 59 59 <a href="/tijdlijn" class="btn">π <%= t('tl.title') %></a> 60 60 <a href="/meldingen" class="btn">π <%= t('notif.title') %></a> 61 <a href="/blokkeren" class="btn">π« <%= t('blk.title') %></a> 61 62 <a href="/admin/updates" class="btn"><%= t('admin.b_updates') %></a> 62 63 <a href="/admin/handleiding" class="btn"><%= t('admin.b_help') %></a> -
src/views/pages/timeline.ejs
rd0cab9d rf5c3870 49 49 <form method="post" action="/tijdlijn/boost" class="tl-act-form"><input type="hidden" name="note" value="<%= p.id %>"><input type="hidden" name="author" value="<%= p.author_uri %>"><button type="submit" class="tl-act" title="<%= t('fedi.boosts') %>">π</button></form> 50 50 <button type="button" class="tl-act fedi-remote-reply-btn" data-fedi-uri="<%= p.id %>" data-fedi-ph="<%= t('fedi.remote_ph') %>" title="<%= t('fedi.remote_reply') %>">β©</button> 51 <form method="post" action="/blokkeren/add" class="tl-act-form" onsubmit="return confirm('<%= t('tl.block') %>?');"><input type="hidden" name="target" value="<%= p.author_uri %>"><button type="submit" class="tl-act" title="<%= t('tl.block') %>">π«</button></form> 51 52 </div> 52 53 </div> -
src/views/partials/topnav.ejs
rd0cab9d rf5c3870 99 99 <% } %> 100 100 101 <% if (user ) { %>102 <a class="nav-btn nav-notif" href="/ notifications" aria-label="<%= t('nav.notifications') %>" title="<%= t('nav.notifications') %>"103 hx-get="/ notifications?partial=1" hx-target="#pcms-main" hx-swap="innerHTML" hx-push-url="/notifications" hx-indicator="#pcms-loading">101 <% if (user && typeof canManageFedi !== 'undefined' && canManageFedi) { %> 102 <a class="nav-btn nav-notif" href="/meldingen" aria-label="<%= t('notif.title') %>" title="<%= t('notif.title') %>" 103 hx-get="/meldingen?partial=1" hx-target="#pcms-main" hx-swap="innerHTML" hx-push-url="/meldingen" hx-indicator="#pcms-loading"> 104 104 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg> 105 <% if (typeof notifUnread !== 'undefined' && notifUnread > 0) { %><span class="notif-badge"><%= notifUnread > 9 ? '9+' : notifUnread %></span><% } %>106 105 </a> 107 106 <% } %> … … 139 138 <span><%= t('nav.account') %></span> 140 139 </a> 141 <a href="/notifications" role="menuitem" class="udi" 142 hx-get="/notifications?partial=1" hx-target="#pcms-main" hx-swap="innerHTML" 143 hx-push-url="/notifications" hx-indicator="#pcms-loading"> 140 <% if (typeof canManageFedi !== 'undefined' && canManageFedi) { %> 141 <a href="/meldingen" role="menuitem" class="udi" 142 hx-get="/meldingen?partial=1" hx-target="#pcms-main" hx-swap="innerHTML" 143 hx-push-url="/meldingen" hx-indicator="#pcms-loading"> 144 144 <svg class="udi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg> 145 <span><%= t('n av.notifications') %><% if (typeof notifUnread !== 'undefined' && notifUnread > 0) { %> (<%= notifUnread %>)<% }%></span>145 <span><%= t('notif.title') %></span> 146 146 </a> 147 <% } %> 147 148 <a href="/favorieten" role="menuitem" class="udi" 148 149 hx-get="/favorieten?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
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)