| … | … | @@ -967,6 +967,20 @@ export function thread (subject, replies, profiles = null, files = null, target |
| 967 | 967 | * only feeds you HOLD can appear, so a stranger interacting with you is invisible. The doorbell is |
| 968 | 968 | * the exception and it is in the list. |
| 969 | 969 | */ |
| 970 | // ⛔ AN OUTSIDE ACTOR IS NOT A FEED (hyper-social #82, I1). A row from a bridge's observation feed names an account on | |
| 971 | // another network by its URI. who() would link /u/<that URI> — a favela profile that does not exist. Shown as the | |
| 972 | // account it is (user@host when the URI says so), linking OUT, and marked as coming from the fediverse. Only an | |
| 973 | // http(s) URI ever becomes a link. | |
| 974 | function outsideActor (uri) { | |
| 975 | let url = null | |
| 976 | try { url = new URL(String(uri)) } catch {} | |
| 977 | if (!url || (url.protocol !== 'https:' && url.protocol !== 'http:')) return '<span class="who">' + esc(String(uri || 'someone')) + '</span> <span class="who">(fediverse)</span>' | |
| 978 | const seg = url.pathname.split('/').filter(Boolean) | |
| 979 | const user = seg.length >= 2 && (seg[0] === 'users' || seg[0] === 'u') ? seg[1] : (seg[0] && seg[0].startsWith('@') ? seg[0].slice(1) : null) | |
| 980 | const label = user ? user + '@' + url.host : url.host + url.pathname | |
| 981 | return '<a class="who" href="' + esc(url.href) + '" rel="nofollow noopener" target="_blank">' + esc(label) + '</a> <span class="who">(fediverse)</span>' | |
| 982 | } | |
| 983 | ||
| 970 | 984 | export function notifications (rows, profiles = null) { |
| 971 | 985 | const VERB = { |
| 972 | 986 | reaction: (n) => (n.value === 'like' ? 'liked' : 'reacted ' + esc(n.value || '')) + ' your post', |
| … | … | @@ -983,7 +997,8 @@ export function notifications (rows, profiles = null) { |
| 983 | 997 | ? ' <a href="/t/' + encodeURIComponent(n.target.feed + '/' + n.target.collection + '/' + n.target.name) + '">→</a>' |
| 984 | 998 | : '' |
| 985 | 999 | const snippet = n.body ? '<div class="who bio">' + esc(n.body.replace(/\s+/g, ' ').trim().slice(0, 120)) + '</div>' : '' |
| 986 | return '<li>' + who(n.author, profiles) + ' ' + verb + link + snippet + '</li>' | |
| 1000 | const by = n.source === 'fediverse' ? outsideActor(n.author) : who(n.author, profiles) | |
| 1001 | return '<li>' + by + ' ' + verb + link + snippet + '</li>' | |
| 987 | 1002 | }).join('') + '</ul>' |
| 988 | 1003 | : empty('No notifications.', |
| 989 | 1004 | 'These are derived from feeds you already hold — nothing is sent to you. Somebody whose feed ' + |
| … | … | @@ -1,6 +1,6 @@ |
| 1 | 1 | { |
| 2 | 2 | "name": "favela-surface", |
| 3 | "version": "0.0.65", | |
| 3 | "version": "0.0.66", | |
| 4 | 4 | "description": "The favela renderer \u2014 server-side HTML over the daemon's socket. No storage, no keys, no swarm.", |
| 5 | 5 | "type": "module", |
| 6 | 6 | "main": "index.js", |
| … | … | @@ -887,3 +887,24 @@ test('status shows the identity root next to "linked"', async (t) => { |
| 887 | 887 | const html = status({ me: { feed, linked: true }, profiles: { [feed]: { root } } }) |
| 888 | 888 | t.ok(html.includes(root), 'the root is on the status page') |
| 889 | 889 | }) |
| 890 | ||
| 891 | // ⛔ AN OUTSIDE ACTOR IS NOT A FEED (hyper-social #82, I1 S4). A notification from a bridge's observation feed names | |
| 892 | // an ActivityPub actor URI. Rendering it through who() would link /u/<that URI> — a favela profile that does not | |
| 893 | // exist. It is shown as the account it is, links OUT to it, and says it came from the fediverse. | |
| 894 | test('★ a fediverse notification names the outside account and links out to it', (t) => { | |
| 895 | const html = notifications([ | |
| 896 | { kind: 'reaction', value: 'like', source: 'fediverse', author: 'https://prsm.space/users/alice', at: '2026-10-14T19:21:17Z', target: { feed: 'ab'.repeat(32), collection: 'post', name: 'p1' }, name: 'o1', body: '' }, | |
| 897 | { kind: 'reply', source: 'fediverse', author: 'https://prsm.space/users/alice', at: '2026-10-14T19:21:25Z', target: null, name: 'o2', body: 'nice post!' } | |
| 898 | ]) | |
| 899 | t.ok(/alice@prsm\.space/.test(html), 'shown as user@host') | |
| 900 | t.ok(/href="https:\/\/prsm\.space\/users\/alice"/.test(html), 'linking out to the account') | |
| 901 | t.absent(/\/u\/https/.test(html), 'never as a favela profile path') | |
| 902 | t.ok(/fediverse/.test(html), 'and says where it came from') | |
| 903 | t.ok(/liked your post/.test(html) && /replied to your post/.test(html)) | |
| 904 | t.ok(/nice post!/.test(html), 'with the reply text') | |
| 905 | }) | |
| 906 | ||
| 907 | test('⛔ a fediverse author that is not an http(s) URL is never made a link', (t) => { | |
| 908 | const html = notifications([{ kind: 'follow', source: 'fediverse', author: 'javascript:alert(1)', at: '', target: null, name: 'x', body: '' }]) | |
| 909 | t.absent(/href="javascript/i.test(html)) | |
| 910 | }) |