diff --git a/atproto-notifications/src/components/NotificationSettings.tsx b/atproto-notifications/src/components/NotificationSettings.tsx index 271dd69..aef44eb 100644 --- a/atproto-notifications/src/components/NotificationSettings.tsx +++ b/atproto-notifications/src/components/NotificationSettings.tsx @@ -24,10 +24,4 @@ export function NotificationSettings({ secondary, secondaryFilter }) { if (secondary === 'all') { return

Notifications default: [todo: toggle mute], unknown sources: [toggle mute]

; } - if (secondaryFilter === null) { - return null; - } - return ( -

{secondaryFilter} [todo: toggle mute]

- ); } diff --git a/lexicons/bits.js b/lexicons/bits.js new file mode 100644 index 0000000..3df9ea8 --- /dev/null +++ b/lexicons/bits.js @@ -0,0 +1,11 @@ +import psl from 'psl'; +import defs from './defs.js'; + +export function getBits(source) { + const [nsid, ...rp] = source.split(':'); + const segments = nsid.split('.'); + const group = segments.slice(0, segments.length - 1).join('.') ?? null; + const unreversed = segments.toReversed().join('.'); + const app = psl.parse(unreversed)?.domain ?? null; + return { app, group }; +} diff --git a/lexicons/defs.js b/lexicons/defs.js new file mode 100644 index 0000000..8d48ee6 --- /dev/null +++ b/lexicons/defs.js @@ -0,0 +1,191 @@ +export default { + 'blue.microcosm': { + name: 'microcosm', + clients: [ + { + app_name: 'Spacedust notifications demo', + canonical: true, + main: 'https://notifications.microcosm.blue', + icon: '/icons/microcosm.png', + }, + ], + known_sources: { + 'test.notification:hello': 'Hello spacedust!', + }, + }, + 'app.bsky': { + name: 'Bluesky', + profile: { + display_name: 'app.bsky.actor.profile:displayName', + avatar: 'app.bsky.actor.profile:avatar', + }, + clients: [ + { + app_name: 'Bluesky Social', + canonical: true, + main: 'https://bsky.app', + icon: '/icons/app.bsky.png', + notifications: 'https://bsky.app/notifications', + direct_links: { + 'at_uri:feed.like:subject.uri': 'https://bsky.app/profile/{subject.did}/post/{subject.rkey}', + 'at_uri:feed.post:reply.parent.uri': 'https://bsky.app/profile/{source_record.did}/post/{source_record.rkey}', + 'at_uri:feed.post:reply.root.uri': 'https://bsky.app/profile/{source_record.did}/post/{source_record.rkey}', + 'at_uri:feed.post:embed.record.uri': 'https://bsky.app/profile/{source_record.did}/post/{source_record.rkey}', + 'at_uri:feed.post:embed.record.record.uri': 'https://bsky.app/profile/{source_record.did}/post/{source_record.rkey}', + 'did:graph.follow:subject': 'https://bsky.app/profile/{source_record.did}', + 'did:feed.post:facets[app.bsky.richtext.facet].features[app.bsky.richtext.facet#mention].did': 'https://bsky.app/profile/{source_record.did}/post/{source_record.rkey}', + }, + }, + { + app_name: 'Deer Social', + main: 'https://deer.social', + notifications: 'https://deer.social/notifications', + direct_links: { + 'at_uri:feed.post': 'https://deer.social/profile/{did}/post/{rkey}', + 'did': 'https://deer.social/profile/{did}', + }, + }, + ], + known_sources: { + 'graph.follow:subject': 'Follow', + 'graph.verification:subject': 'Verification', + 'feed.like:subject.uri': 'Like', + 'feed.like:via.uri': 'Repost like', + 'feed.post:reply.parent.uri': 'Reply', + 'feed.post:reply.root.uri': 'Reply in thread', + 'feed.post:embed.record.uri': 'Quote', + 'feed.post:embed.record.record.uri': 'Quote', // with media + 'feed.post:facets[app.bsky.richtext.facet].features[app.bsky.richtext.facet#mention].did': 'Mention', + 'feed.repost:subject.uri': 'Repost', + 'feed.repost:via.uri': 'Repost repost', + }, + torment_sources: { + 'graph.block:subject': null, + 'graph.listitem:subject': null, // we are never ever building listifications + 'graph.listblock:subject': null, // "subscribed to your blocklist?" idk + 'feed.threadgate:hiddenReplies[]': null, + 'feed.postgate:detachedEmbeddingUris[]': null, + }, + }, + 'pub.leaflet': { + name: 'Leaflet', + clients: [ + { + app_name: 'leaflet.pub', + canonical: true, + icon: '/icons/pub.leaflet.jpg', + main: 'https://leaflet.pub/home', + direct_links: { + 'at_uri:graph.subscription:publication': 'https://leaflet.pub/lish/{did}/{rkey}/dashboard', + }, + } + ], + known_sources: { + 'graph.subscription:publication': 'Subscription', + }, + }, + 'sh.tangled': { + name: 'Tangled', + clients: [ + { + app_name: 'Tangled', + canonical: true, + icon: '/icons/sh.tangled.jpg', + main: 'https://tangled.sh', + } + ], + known_sources: { + 'feed.star:subject': 'Star', + 'feed.reaction:subject': 'Reaction', + 'graph.follow:subject': 'Follow', + 'actor.profile:pinnedRepositories[]': 'Pinned repo', + 'repo.issue.comment:issue': 'Issue comment', + 'repo.issue.comment:owner': 'Issue comment', + 'repo.issue.comment:repo': 'Issue comment', + 'repo.pull:targetRepo': 'Pull', + 'repo.pull.comment:owner': 'Pull comment', + 'repo.pull.comment:pull': 'Pull comment', + 'repo.pull.comment:repo': 'Pull comment', + 'knot.member:subject': 'Knot member', + 'spindle.member:subject': 'Spindle member', + }, + }, + 'com.shinolabs': { // TODO: this app isn't exactly tld+1 + name: 'Pinksea', + clients: [ + { + app_name: 'Pinksea', + canonical: true, + icon: '/icons/com.shinolabs.jpg', + main: 'https://pinksea.art', + }, + ], + known_sources: { + 'pinksea.oekaki:inResponseTo.uri': 'Response', + }, + }, + 'place.stream': { + name: 'Streamplace', + clients: [ + { + app_name: 'Streamplace', + canonical: true, + icon: '/icons/place.stream.png', + main: 'https://stream.place', + }, + ], + known_sources: { + 'chat.message:streamer': 'Message', + 'key:signingKey': 'Signing key', + }, + }, + 'so.sprk': { + name: 'Spark', + clients: [ + { + app_name: 'Spark', + canonical: true, + icon: '/icons/so.sprk.png', + main: 'https://spark.so', + }, + ], + known_sources: { + 'feed.like:subject.uri': 'Like', + // it's not actually clear to me if *all* the bsky sources were copied for sprk posts or not + 'feed.post:reply.parent.uri': 'Reply', + 'feed.post:reply.root.uri': 'Reply in thread', + 'feed.post:embed.record.uri': 'Quote', + 'feed.post:embed.record.record.uri': 'Quote', // with media + 'feed.post:facets[app.bsky.richtext.facet].features[app.bsky.richtext.facet#mention].did': 'Mention', + }, + }, + 'events.smokesignal': { + name: 'Smoke Signal', + clients: [ + { + app_name: 'Smoke Signal', + canonical: true, + icon: '/icons/events.smokesignal.png', + main: 'https://smokesignal.events', + }, + ], + known_sources: { + 'calendar.rsvp:subject.uri': 'RSVP', + }, + }, + 'app.popsky': { + name: 'Popsky', + clients: [ + { + app_name: 'Popsky', + canonical: true, + icon: '/icons/app.popsky.png', + main: 'https://popsky.social', + }, + ], + known_sources: { + 'like:subjectUri': 'Like', + 'comment:subjectUri': 'Comment', + }, + }, +}; diff --git a/lexicons/index.js b/lexicons/index.js index 8d48ee6..36b65b6 100644 --- a/lexicons/index.js +++ b/lexicons/index.js @@ -1,191 +1,2 @@ -export default { - 'blue.microcosm': { - name: 'microcosm', - clients: [ - { - app_name: 'Spacedust notifications demo', - canonical: true, - main: 'https://notifications.microcosm.blue', - icon: '/icons/microcosm.png', - }, - ], - known_sources: { - 'test.notification:hello': 'Hello spacedust!', - }, - }, - 'app.bsky': { - name: 'Bluesky', - profile: { - display_name: 'app.bsky.actor.profile:displayName', - avatar: 'app.bsky.actor.profile:avatar', - }, - clients: [ - { - app_name: 'Bluesky Social', - canonical: true, - main: 'https://bsky.app', - icon: '/icons/app.bsky.png', - notifications: 'https://bsky.app/notifications', - direct_links: { - 'at_uri:feed.like:subject.uri': 'https://bsky.app/profile/{subject.did}/post/{subject.rkey}', - 'at_uri:feed.post:reply.parent.uri': 'https://bsky.app/profile/{source_record.did}/post/{source_record.rkey}', - 'at_uri:feed.post:reply.root.uri': 'https://bsky.app/profile/{source_record.did}/post/{source_record.rkey}', - 'at_uri:feed.post:embed.record.uri': 'https://bsky.app/profile/{source_record.did}/post/{source_record.rkey}', - 'at_uri:feed.post:embed.record.record.uri': 'https://bsky.app/profile/{source_record.did}/post/{source_record.rkey}', - 'did:graph.follow:subject': 'https://bsky.app/profile/{source_record.did}', - 'did:feed.post:facets[app.bsky.richtext.facet].features[app.bsky.richtext.facet#mention].did': 'https://bsky.app/profile/{source_record.did}/post/{source_record.rkey}', - }, - }, - { - app_name: 'Deer Social', - main: 'https://deer.social', - notifications: 'https://deer.social/notifications', - direct_links: { - 'at_uri:feed.post': 'https://deer.social/profile/{did}/post/{rkey}', - 'did': 'https://deer.social/profile/{did}', - }, - }, - ], - known_sources: { - 'graph.follow:subject': 'Follow', - 'graph.verification:subject': 'Verification', - 'feed.like:subject.uri': 'Like', - 'feed.like:via.uri': 'Repost like', - 'feed.post:reply.parent.uri': 'Reply', - 'feed.post:reply.root.uri': 'Reply in thread', - 'feed.post:embed.record.uri': 'Quote', - 'feed.post:embed.record.record.uri': 'Quote', // with media - 'feed.post:facets[app.bsky.richtext.facet].features[app.bsky.richtext.facet#mention].did': 'Mention', - 'feed.repost:subject.uri': 'Repost', - 'feed.repost:via.uri': 'Repost repost', - }, - torment_sources: { - 'graph.block:subject': null, - 'graph.listitem:subject': null, // we are never ever building listifications - 'graph.listblock:subject': null, // "subscribed to your blocklist?" idk - 'feed.threadgate:hiddenReplies[]': null, - 'feed.postgate:detachedEmbeddingUris[]': null, - }, - }, - 'pub.leaflet': { - name: 'Leaflet', - clients: [ - { - app_name: 'leaflet.pub', - canonical: true, - icon: '/icons/pub.leaflet.jpg', - main: 'https://leaflet.pub/home', - direct_links: { - 'at_uri:graph.subscription:publication': 'https://leaflet.pub/lish/{did}/{rkey}/dashboard', - }, - } - ], - known_sources: { - 'graph.subscription:publication': 'Subscription', - }, - }, - 'sh.tangled': { - name: 'Tangled', - clients: [ - { - app_name: 'Tangled', - canonical: true, - icon: '/icons/sh.tangled.jpg', - main: 'https://tangled.sh', - } - ], - known_sources: { - 'feed.star:subject': 'Star', - 'feed.reaction:subject': 'Reaction', - 'graph.follow:subject': 'Follow', - 'actor.profile:pinnedRepositories[]': 'Pinned repo', - 'repo.issue.comment:issue': 'Issue comment', - 'repo.issue.comment:owner': 'Issue comment', - 'repo.issue.comment:repo': 'Issue comment', - 'repo.pull:targetRepo': 'Pull', - 'repo.pull.comment:owner': 'Pull comment', - 'repo.pull.comment:pull': 'Pull comment', - 'repo.pull.comment:repo': 'Pull comment', - 'knot.member:subject': 'Knot member', - 'spindle.member:subject': 'Spindle member', - }, - }, - 'com.shinolabs': { // TODO: this app isn't exactly tld+1 - name: 'Pinksea', - clients: [ - { - app_name: 'Pinksea', - canonical: true, - icon: '/icons/com.shinolabs.jpg', - main: 'https://pinksea.art', - }, - ], - known_sources: { - 'pinksea.oekaki:inResponseTo.uri': 'Response', - }, - }, - 'place.stream': { - name: 'Streamplace', - clients: [ - { - app_name: 'Streamplace', - canonical: true, - icon: '/icons/place.stream.png', - main: 'https://stream.place', - }, - ], - known_sources: { - 'chat.message:streamer': 'Message', - 'key:signingKey': 'Signing key', - }, - }, - 'so.sprk': { - name: 'Spark', - clients: [ - { - app_name: 'Spark', - canonical: true, - icon: '/icons/so.sprk.png', - main: 'https://spark.so', - }, - ], - known_sources: { - 'feed.like:subject.uri': 'Like', - // it's not actually clear to me if *all* the bsky sources were copied for sprk posts or not - 'feed.post:reply.parent.uri': 'Reply', - 'feed.post:reply.root.uri': 'Reply in thread', - 'feed.post:embed.record.uri': 'Quote', - 'feed.post:embed.record.record.uri': 'Quote', // with media - 'feed.post:facets[app.bsky.richtext.facet].features[app.bsky.richtext.facet#mention].did': 'Mention', - }, - }, - 'events.smokesignal': { - name: 'Smoke Signal', - clients: [ - { - app_name: 'Smoke Signal', - canonical: true, - icon: '/icons/events.smokesignal.png', - main: 'https://smokesignal.events', - }, - ], - known_sources: { - 'calendar.rsvp:subject.uri': 'RSVP', - }, - }, - 'app.popsky': { - name: 'Popsky', - clients: [ - { - app_name: 'Popsky', - canonical: true, - icon: '/icons/app.popsky.png', - main: 'https://popsky.social', - }, - ], - known_sources: { - 'like:subjectUri': 'Like', - 'comment:subjectUri': 'Comment', - }, - }, -}; +export { getBits } from './bits.js'; +export { default } from './defs.js'; diff --git a/lexicons/package-lock.json b/lexicons/package-lock.json new file mode 100644 index 0000000..4977494 --- /dev/null +++ b/lexicons/package-lock.json @@ -0,0 +1,36 @@ +{ + "name": "lexicons", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "lexicons", + "version": "0.0.1", + "dependencies": { + "psl": "^1.15.0" + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/lexicons/package.json b/lexicons/package.json index f76353e..ab26ee2 100644 --- a/lexicons/package.json +++ b/lexicons/package.json @@ -5,5 +5,8 @@ "main": "index.js", "scripts": {}, "author": "", - "type": "module" + "type": "module", + "dependencies": { + "psl": "^1.15.0" + } } diff --git a/server/notifications.js b/server/notifications.js index 0f450e7..882aba0 100644 --- a/server/notifications.js +++ b/server/notifications.js @@ -1,4 +1,4 @@ -import lexicons from 'lexicons'; +import { default as lexicons, getBits } from 'lexicons'; import psl from 'psl'; import webpush from 'web-push'; import WebSocket from 'ws'; @@ -144,6 +144,24 @@ const handleDust = db => async event => { } } + // like above, this over-assumes that did is the only recipient we could care about for now + const { app, group } = getBits(source); + for (const [selector, selection] of [ + ['source', source], + ['group', group], + ['app', app], + ]) { + const notify = db.getNotificationFilter(did, selector, selection); + if (notify === true) { + console.info(`explicitly allowing notification by filter for ${selector}=${selection}`); + break; + }; + if (notify === false) { + console.warn(`ignoring filtered notification for ${selector}=${selection}`); + return; + } + } + const subs = db.getSubsByDid(did); const payload = JSON.stringify({ subject, source, source_record, timestamp }); try {