From 5320e9f12e625b9f4f7a33161a0d2f297f2e875e Mon Sep 17 00:00:00 2001 From: Nulo Date: Tue, 13 Feb 2024 15:20:06 -0300 Subject: [PATCH 01/52] wip: setup announcement actor, add endpoint for actor --- src/server/api/announcements.ts | 53 +++++++++++++++++++++++++++++++++ src/server/api/index.ts | 20 +++++++++++++ src/server/store/index.ts | 3 ++ 3 files changed, 76 insertions(+) create mode 100644 src/server/api/announcements.ts diff --git a/src/server/api/announcements.ts b/src/server/api/announcements.ts new file mode 100644 index 0000000..67f7850 --- /dev/null +++ b/src/server/api/announcements.ts @@ -0,0 +1,53 @@ +import { APActor } from 'activitypub-types' + +import type { APIConfig, FastifyTypebox } from '.' +import Store from '../store' +import type ActivityPubSystem from '../apsystem' + +type APActorNonStandard = APActor & { + publicKey: { + id: string + owner: string + publicKeyPem: string + } +} + +export const announcementsRoutes = (cfg: APIConfig, store: Store, apsystem: ActivityPubSystem) => async (server: FastifyTypebox): Promise => { + server.get<{ + Reply: APActorNonStandard + }>('/announcements', { + schema: { + params: {}, + // XXX: even with Type.Any(), the endpoint returns `{}` :/ + // response: { + // // TODO: typebox APActor + // 200: Type.Any() + // }, + description: 'Announcements ActivityPub actor', + tags: ['ActivityPub'] + } + }, async (request, reply) => { + // return await reply.send({ prueba: 'asdfasd' }) + const actor = await store.announcements.getInfo() + + return await reply.send({ + '@context': [ + // TODO: I copied this from Mastodon, is this correct? + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1' + ], + // https://www.w3.org/TR/activitystreams-vocabulary/#actor-types + type: 'Service', + name: 'Announcements', + inbox: `${actor.actorUrl}/inbox`, + outbox: `${actor.actorUrl}/outbox`, + publicKey: { + // TODO: copied from Mastodon + id: `${actor.actorUrl}#main-key`, + + owner: actor.actorUrl, + publicKeyPem: actor.keypair.publicKeyPem + } + }) + }) +} diff --git a/src/server/api/index.ts b/src/server/api/index.ts index c908491..877080a 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -27,6 +27,8 @@ import { blockAllowListRoutes } from './blockallowlist.js' import { adminRoutes } from './admins.js' import { followerRoutes } from './followers.js' import { hookRoutes } from './hooks.js' +import { generateKeypair } from 'http-signed-fetch' +import { announcementsRoutes } from './announcements.js' export const paths = envPaths('distributed-press') @@ -94,6 +96,23 @@ async function apiBuilder (cfg: APIConfig): Promise { return 'ok\n' }) + // Setup announcements actor + let keys + try { + const prev = await store.announcements.getInfo() + keys = { ...prev.keypair, publicKeyId: prev.publicKeyId } + } catch { + keys = generateKeypair() + } + await store.announcements.setInfo({ + actorUrl: `${cfg.publicURL}/v1/announcements`, + publicKeyId: keys.publicKeyId, + keypair: { + privateKeyPem: keys.privateKeyPem, + publicKeyPem: keys.publicKeyPem + } + }) + await server.register(v1Routes(cfg, store, apsystem, hookSystem), { prefix: '/v1' }) await server.ready() @@ -123,6 +142,7 @@ const v1Routes = (cfg: APIConfig, store: Store, apsystem: ActivityPubSystem, hoo }) } + await server.register(announcementsRoutes(cfg, store, apsystem)) await server.register(creationRoutes(cfg, store, apsystem)) await server.register(inboxRoutes(cfg, store, apsystem)) await server.register(outboxRoutes(cfg, store, apsystem)) diff --git a/src/server/store/index.ts b/src/server/store/index.ts index ea1a13b..124ade0 100644 --- a/src/server/store/index.ts +++ b/src/server/store/index.ts @@ -10,6 +10,7 @@ export default class Store { db: AbstractLevel actorCache: Map actorsDb: AbstractLevel + announcements: ActorStore blocklist: AccountListStore allowlist: AccountListStore admins: AccountListStore @@ -19,6 +20,8 @@ export default class Store { this.db = db this.actorCache = new Map() this.actorsDb = this.db.sublevel('actorCache', { valueEncoding: 'json' }) + const announcementsDb = this.db.sublevel('announcements', { valueEncoding: 'json' }) + this.announcements = new ActorStore(announcementsDb) const blocklistDb = this.db.sublevel('blocklist', { valueEncoding: 'json' }) From 5f04cc262c31b915f40d0f2e536fb7dda7b7e023 Mon Sep 17 00:00:00 2001 From: Nulo Date: Tue, 13 Feb 2024 15:46:14 -0300 Subject: [PATCH 02/52] WIP: announcements post script this will probably be removed later --- src/scripts/add-post-to-announcements.ts | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/scripts/add-post-to-announcements.ts diff --git a/src/scripts/add-post-to-announcements.ts b/src/scripts/add-post-to-announcements.ts new file mode 100644 index 0000000..9e4b136 --- /dev/null +++ b/src/scripts/add-post-to-announcements.ts @@ -0,0 +1,30 @@ +import yargs from 'yargs' +import { hideBin } from 'yargs/helpers' +import envPaths from 'env-paths' +import { Level } from 'level' + +import Store from '../server/store/index.js' + +const paths = envPaths('social.distributed.press') + +const argv = yargs(hideBin(process.argv)).positional('content', {}).options({ + storage: { type: 'string' } +}).parseSync() + +const storage = argv.storage ?? paths.data +const content = argv.$0 + +const db = new Level(storage, { valueEncoding: 'json' }) + +const store = new Store(db) + +console.log(`Posting: ${content}`) +await store.announcements.outbox.add({ +id: +}) +await store.admins.add(list) + +const final = await store.admins.list() + +console.log('Final admin list:') +console.log(final) From 3d25d1a1e8781200b3c9b11cbc986cb21362dc9d Mon Sep 17 00:00:00 2001 From: Nulo Date: Thu, 22 Feb 2024 09:40:47 -0300 Subject: [PATCH 03/52] WIP --- src/scripts/add-post-to-announcements.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/scripts/add-post-to-announcements.ts b/src/scripts/add-post-to-announcements.ts index 9e4b136..24a0c11 100644 --- a/src/scripts/add-post-to-announcements.ts +++ b/src/scripts/add-post-to-announcements.ts @@ -4,6 +4,7 @@ import envPaths from 'env-paths' import { Level } from 'level' import Store from '../server/store/index.js' +import { nanoid } from 'nanoid' const paths = envPaths('social.distributed.press') @@ -19,8 +20,20 @@ const db = new Level(storage, { valueEncoding: 'json' }) const store = new Store(db) console.log(`Posting: ${content}`) +const actor=await store.announcements.getInfo() + await store.announcements.outbox.add({ -id: + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Note', + id: `${actor.actorUrl}/outbox/${nanoid()}`, + actor: actor.actorUrl, + attributedTo: actor.actorUrl, + published: new Date().toUTCString(), + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "cc": ["https://social.distributed.press/v1/@sutty@sutty.nl/followers"], + object: { + + } }) await store.admins.add(list) From 58db44ce47c3d3b798c2b9003b93c7fc4e638ff1 Mon Sep 17 00:00:00 2001 From: Nulo Date: Thu, 22 Feb 2024 09:40:59 -0300 Subject: [PATCH 04/52] chore(vscode): always use ts-standard --- .vscode/settings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index 09042f2..fe42d53 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { "standard.enable": true, "standard.autoFixOnSave": true, + "standard.engine": "ts-standard", "[typescript]": { "editor.defaultFormatter": "" } From 5687b4214b8ca66eb974231be9f0b54f67734ebc Mon Sep 17 00:00:00 2001 From: Nulo Date: Thu, 22 Feb 2024 13:17:10 -0300 Subject: [PATCH 05/52] announcements: add endpoints for outbox --- src/server/api/announcements.ts | 53 +++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/server/api/announcements.ts b/src/server/api/announcements.ts index 67f7850..736bcd2 100644 --- a/src/server/api/announcements.ts +++ b/src/server/api/announcements.ts @@ -3,6 +3,7 @@ import { APActor } from 'activitypub-types' import type { APIConfig, FastifyTypebox } from '.' import Store from '../store' import type ActivityPubSystem from '../apsystem' +import { Type } from '@sinclair/typebox' type APActorNonStandard = APActor & { publicKey: { @@ -50,4 +51,56 @@ export const announcementsRoutes = (cfg: APIConfig, store: Store, apsystem: Acti } }) }) + + server.get<{ + // TODO: typebox APOrderedCollection + Reply: any + }>('/announcements/outbox', { + schema: { + params: {}, + // XXX: even with Type.Any(), the endpoint returns `{}` :/ + // response: { + // // TODO: typebox APOrderedCollection + // 200: Type.Any() + // }, + description: 'Announcements ActivityPub outbox', + tags: ['ActivityPub'] + } + }, async (request, reply) => { + const actor = await store.announcements.getInfo() + const activities = await store.announcements.outbox.list() + const orderedItems = activities.map(a => a.id) + return await reply.send({ + '@context': 'https://www.w3.org/ns/activitystreams', + id: `${actor.actorUrl}/outbox`, + type: 'OrderedCollection', + totalItems: orderedItems.length, + orderedItems + }) + }) + + server.get<{ + Params: { + id: string + } + // TODO: typebox APOrderedCollection + Reply: any + }>('/announcements/outbox/:id', { + schema: { + params: Type.Object({ + id: Type.String() + }), + // XXX: even with Type.Any(), the endpoint returns `{}` :/ + // response: { + // // TODO: typebox APOrderedCollection + // 200: Type.Any() + // }, + description: 'Announcements ActivityPub get activity', + tags: ['ActivityPub'] + } + }, async (request, reply) => { + const actor = await store.announcements.getInfo() + const activity = await store.announcements.outbox.get(`${actor.actorUrl}/outbox/${request.params.id}`) + return await reply.send(activity) + }) } From 0051deabb392cb13a369b60dbf73bef3eabb92ef Mon Sep 17 00:00:00 2001 From: Nulo Date: Thu, 22 Feb 2024 13:18:22 -0300 Subject: [PATCH 06/52] chore: add-post-to-announcements --- src/scripts/add-post-to-announcements.ts | 39 +++++++++++------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/src/scripts/add-post-to-announcements.ts b/src/scripts/add-post-to-announcements.ts index 24a0c11..e9f8120 100644 --- a/src/scripts/add-post-to-announcements.ts +++ b/src/scripts/add-post-to-announcements.ts @@ -8,36 +8,31 @@ import { nanoid } from 'nanoid' const paths = envPaths('social.distributed.press') -const argv = yargs(hideBin(process.argv)).positional('content', {}).options({ - storage: { type: 'string' } -}).parseSync() +const argv = yargs(hideBin(process.argv)) + .command('', 'content for the post') + .demandCommand(1) + .options({ + storage: { type: 'string' } + }).parseSync() const storage = argv.storage ?? paths.data -const content = argv.$0 +const content = `${argv._[0]}` const db = new Level(storage, { valueEncoding: 'json' }) const store = new Store(db) console.log(`Posting: ${content}`) -const actor=await store.announcements.getInfo() +const actor = await store.announcements.getInfo() await store.announcements.outbox.add({ - '@context': 'https://www.w3.org/ns/activitystreams', - type: 'Note', - id: `${actor.actorUrl}/outbox/${nanoid()}`, - actor: actor.actorUrl, - attributedTo: actor.actorUrl, - published: new Date().toUTCString(), - "to": ["https://www.w3.org/ns/activitystreams#Public"], - "cc": ["https://social.distributed.press/v1/@sutty@sutty.nl/followers"], - object: { - - } + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Note', + id: `${actor.actorUrl}/outbox/${nanoid()}`, + actor: actor.actorUrl, + attributedTo: actor.actorUrl, + published: new Date().toUTCString(), + to: ['https://www.w3.org/ns/activitystreams#Public'], + cc: ['https://social.distributed.press/v1/announcements/followers'], + content }) -await store.admins.add(list) - -const final = await store.admins.list() - -console.log('Final admin list:') -console.log(final) From 1deddd8750f7c54ff0d3ce99341230f60c46c0e3 Mon Sep 17 00:00:00 2001 From: Nulo Date: Thu, 22 Feb 2024 17:35:58 -0300 Subject: [PATCH 07/52] chore: clean up --- src/server/api/announcements.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/server/api/announcements.ts b/src/server/api/announcements.ts index 736bcd2..47c2cea 100644 --- a/src/server/api/announcements.ts +++ b/src/server/api/announcements.ts @@ -28,7 +28,6 @@ export const announcementsRoutes = (cfg: APIConfig, store: Store, apsystem: Acti tags: ['ActivityPub'] } }, async (request, reply) => { - // return await reply.send({ prueba: 'asdfasd' }) const actor = await store.announcements.getInfo() return await reply.send({ From 7ed30304ac44438ec36cc84d4d1c04ca6b66718e Mon Sep 17 00:00:00 2001 From: Nulo Date: Thu, 22 Feb 2024 17:36:07 -0300 Subject: [PATCH 08/52] feat: announce on creation --- src/schemas.ts | 3 ++- src/server/api/creation.ts | 19 +++++++++++++++++++ src/server/api/index.ts | 3 ++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/schemas.ts b/src/schemas.ts index 58f12fd..0b9193e 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -5,7 +5,8 @@ export const ActorInfoSchema = Type.Object({ // The actor for the domain inbox actorUrl: Type.String(), publicKeyId: Type.String(), - keypair: KeyPairSchema + keypair: KeyPairSchema, + announce: Type.Boolean({ default: false }) }) export type ActorInfo = Static diff --git a/src/server/api/creation.ts b/src/server/api/creation.ts index d216ce1..1c1c8c2 100644 --- a/src/server/api/creation.ts +++ b/src/server/api/creation.ts @@ -3,6 +3,7 @@ import { Type } from '@sinclair/typebox' import type { APIConfig, FastifyTypebox } from './index.js' import Store, { ActorInfo, ActorInfoSchema } from '../store/index.js' import ActivityPubSystem from '../apsystem.js' +import { nanoid } from 'nanoid' export const creationRoutes = (cfg: APIConfig, store: Store, apsystem: ActivityPubSystem) => async (server: FastifyTypebox): Promise => { // Create a new inbox @@ -35,6 +36,24 @@ export const creationRoutes = (cfg: APIConfig, store: Store, apsystem: ActivityP const info = request.body await store.forActor(actor).setInfo(info) + + if (info.announce) { + const activity = { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Note', + id: `${info.actorUrl}/outbox/${nanoid()}`, + actor: info.actorUrl, + attributedTo: info.actorUrl, + published: new Date().toUTCString(), + to: ['https://www.w3.org/ns/activitystreams#Public'], + cc: ['https://social.distributed.press/v1/announcements/followers'], + // TODO: add a template in config + content: `a wild site appears! ${actor}` + } + await store.announcements.outbox.add(activity) + await apsystem.notifyFollowers(actor, activity) + } + return await reply.send(info) }) diff --git a/src/server/api/index.ts b/src/server/api/index.ts index 877080a..e58de11 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -110,7 +110,8 @@ async function apiBuilder (cfg: APIConfig): Promise { keypair: { privateKeyPem: keys.privateKeyPem, publicKeyPem: keys.publicKeyPem - } + }, + announce: false }) await server.register(v1Routes(cfg, store, apsystem, hookSystem), { prefix: '/v1' }) From 58600ad66dd9d592f9fff7600ce0f5fa7274c799 Mon Sep 17 00:00:00 2001 From: Nulo Date: Fri, 23 Feb 2024 18:08:36 -0300 Subject: [PATCH 09/52] fix: correct http-signed-fetch types and related changes --- @types/http-signed-fetch.d.ts | 1 - src/client/index.ts | 8 ++++++-- src/server/api/index.ts | 7 ++++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/@types/http-signed-fetch.d.ts b/@types/http-signed-fetch.d.ts index 17a4c95..37047cd 100644 --- a/@types/http-signed-fetch.d.ts +++ b/@types/http-signed-fetch.d.ts @@ -4,6 +4,5 @@ declare module 'http-signed-fetch' { export function generateKeypair (): { publicKeyPem: string privateKeyPem: string - publicKeyId: string } } diff --git a/src/client/index.ts b/src/client/index.ts index e1e4045..330400c 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -9,10 +9,14 @@ export type SignedFetchLike = ( init?: RequestInit & { publicKeyId: string, keypair: KeyPair } ) => Promise +type Keypair = ReturnType & { + publicKeyId: string +} + export interface SocialInboxOptions { instance: string account: string - keypair: ReturnType + keypair: Keypair fetch?: SignedFetchLike } @@ -40,7 +44,7 @@ type VALID_TYPES = typeof TYPE_TEXT | typeof TYPE_JSON | typeof TYPE_LDJSON | un export class SocialInboxClient { instance: string account: string - keypair: ReturnType + keypair: Keypair fetch: SignedFetchLike constructor (options: SocialInboxOptions) { diff --git a/src/server/api/index.ts b/src/server/api/index.ts index e58de11..fc4970a 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -16,7 +16,7 @@ import { Level } from 'level' import { MemoryLevel } from 'memory-level' import Store from '../store/index.js' -import ActivityPubSystem from '../apsystem.js' +import ActivityPubSystem, { DEFAULT_PUBLIC_KEY_FIELD } from '../apsystem.js' import HookSystem from '../hooksystem.js' import { ModerationChecker } from '../moderation.js' @@ -97,15 +97,16 @@ async function apiBuilder (cfg: APIConfig): Promise { }) // Setup announcements actor + const actorUrl = `${cfg.publicURL}/v1/announcements` let keys try { const prev = await store.announcements.getInfo() keys = { ...prev.keypair, publicKeyId: prev.publicKeyId } } catch { - keys = generateKeypair() + keys = { ...generateKeypair(), publicKeyId: `${actorUrl}#${DEFAULT_PUBLIC_KEY_FIELD}` } } await store.announcements.setInfo({ - actorUrl: `${cfg.publicURL}/v1/announcements`, + actorUrl, publicKeyId: keys.publicKeyId, keypair: { privateKeyPem: keys.privateKeyPem, From 165557d8e3552c9c854133d5df512e6c6c665510 Mon Sep 17 00:00:00 2001 From: Nulo Date: Fri, 23 Feb 2024 18:08:55 -0300 Subject: [PATCH 10/52] fix(client): remove trailing slash to setInfo endpoint --- src/client/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/index.ts b/src/client/index.ts index 330400c..0477b77 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -87,7 +87,7 @@ export class SocialInboxClient { // Actorinfo async setActorInfo (info: ActorInfo, actor: string = this.account): Promise { - await this.sendRequest(POST, `/${actor}/`, TYPE_JSON, info) + await this.sendRequest(POST, `/${actor}`, TYPE_JSON, info) } async getActorInfo (actor: string = this.account): Promise { From f9406ed5351a68928cba2def1230b22efff54a5a Mon Sep 17 00:00:00 2001 From: Nulo Date: Fri, 23 Feb 2024 18:09:24 -0300 Subject: [PATCH 11/52] fix: only announce if doesn't exist already --- src/server/api/creation.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/server/api/creation.ts b/src/server/api/creation.ts index 1c1c8c2..4a495fa 100644 --- a/src/server/api/creation.ts +++ b/src/server/api/creation.ts @@ -29,6 +29,8 @@ export const creationRoutes = (cfg: APIConfig, store: Store, apsystem: ActivityP }, async (request, reply) => { const { actor } = request.params + const existedAlready = (await store.actorsDb.keys().all()).some(k => k === actor) + const allowed = await apsystem.hasPermissionActorRequest(actor, request, false) if (!allowed) { return await reply.code(403).send('Not Allowed') @@ -37,7 +39,7 @@ export const creationRoutes = (cfg: APIConfig, store: Store, apsystem: ActivityP const info = request.body await store.forActor(actor).setInfo(info) - if (info.announce) { + if (!existedAlready && info.announce) { const activity = { '@context': 'https://www.w3.org/ns/activitystreams', type: 'Note', From 3e41d8174f98f55030027f80684090b1f463a4f8 Mon Sep 17 00:00:00 2001 From: Nulo Date: Fri, 23 Feb 2024 18:09:41 -0300 Subject: [PATCH 12/52] chore: add basic client CLI --- package.json | 1 + src/client/cli.ts | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 src/client/cli.ts diff --git a/package.json b/package.json index d56916b..590a6e9 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "generate-identity": "ts-node-esm src/scripts/generate-identity.ts", "import-blocklist": "ts-node-esm src/scripts/import-blocklist.ts", "import-admins": "ts-node-esm src/scripts/import-admins.ts", + "client-cli": "ts-node-esm src/client/cli.ts", "lint": "ts-standard --fix && tsc --noEmit", "dev": "ts-node-esm src/bin.ts run | pino-pretty -c -t", "start": "node dist/bin.js run | pino-pretty -c -t", diff --git a/src/client/cli.ts b/src/client/cli.ts new file mode 100644 index 0000000..22bde20 --- /dev/null +++ b/src/client/cli.ts @@ -0,0 +1,41 @@ +import yargs from 'yargs' +import { hideBin } from 'yargs/helpers' + +import { SocialInboxClient } from './index.js' +import { generateKeypair } from 'http-signed-fetch' +import { DEFAULT_PUBLIC_KEY_FIELD } from '../server/apsystem.js' + +// https://github.com/yargs/yargs/issues/225 +const argv = yargs(hideBin(process.argv)) + .usage('usage: $0 ') + .demandCommand(1) + .options({ + url: { type: 'string', default: 'http://localhost:8080' }, + announce: { type: 'boolean', default: true } + }).parseSync() + +const account = `${argv._[0]}` +const { url, announce } = argv + +// XXX: is this safe to assume? +const [, username, instance] = account.split('@') +if (username === undefined || instance === undefined) { + throw new Error('missing username or instance') +} +const actorUrl = `https://${instance}/@${username}` +const publicKeyId = `${actorUrl}#${DEFAULT_PUBLIC_KEY_FIELD}` + +const keypair = generateKeypair() +const client = new SocialInboxClient({ + instance: url, + keypair: { ...keypair, publicKeyId }, + account +}) + +// ref: https://github.com/RangerMauve/staticpub.mauve.moe/blob/default/create_account.js +await client.setInfo({ + actorUrl, + publicKeyId, + keypair, + announce +}) From 1abce316c9d34a1ccb9c62a6d42e552ef51423c0 Mon Sep 17 00:00:00 2001 From: Nulo Date: Fri, 23 Feb 2024 18:11:40 -0300 Subject: [PATCH 13/52] fix: try to get announcements inbox working because announcements is a separate ActorStore outside the actorCache, we bypass the actorCache when trying to act on the announcements actor. this should make the generic `/:actor/inbox` endpoint work for announcements.. but not tested --- src/server/store/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/server/store/index.ts b/src/server/store/index.ts index 124ade0..7bf7e0e 100644 --- a/src/server/store/index.ts +++ b/src/server/store/index.ts @@ -37,6 +37,10 @@ export default class Store { } forActor (domain: string): ActorStore { + if (domain === 'announcements') { + return this.announcements + } + if (!this.actorCache.has(domain)) { const sub = this.db.sublevel(domain, { valueEncoding: 'json' }) const store = new ActorStore(sub) From 261d120e54f6d82274d7dbec19249bf91a55d8f1 Mon Sep 17 00:00:00 2001 From: Nulo Date: Fri, 23 Feb 2024 18:39:42 -0300 Subject: [PATCH 14/52] fix: missing publicKeyId --- src/client/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/index.test.ts b/src/client/index.test.ts index 41965a0..4bcc496 100644 --- a/src/client/index.test.ts +++ b/src/client/index.test.ts @@ -33,7 +33,7 @@ test('Local Server Communication', async t => { const client = new SocialInboxClient({ instance: `http://localhost:${port}`, account: 'testAccount', - keypair: generateKeypair(), + keypair: { ...generateKeypair(), publicKeyId: 'testAccount#main-key' }, fetch: globalThis.fetch }) From 18db375ef6fa9f2c91df24e4340a2db13e403b59 Mon Sep 17 00:00:00 2001 From: Nulo Date: Thu, 29 Feb 2024 14:54:06 -0300 Subject: [PATCH 15/52] Update src/server/api/announcements.ts Co-authored-by: Mauve Signweaver --- src/server/api/announcements.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/api/announcements.ts b/src/server/api/announcements.ts index 47c2cea..59bafd4 100644 --- a/src/server/api/announcements.ts +++ b/src/server/api/announcements.ts @@ -16,7 +16,7 @@ type APActorNonStandard = APActor & { export const announcementsRoutes = (cfg: APIConfig, store: Store, apsystem: ActivityPubSystem) => async (server: FastifyTypebox): Promise => { server.get<{ Reply: APActorNonStandard - }>('/announcements', { + }>('/announcements/', { schema: { params: {}, // XXX: even with Type.Any(), the endpoint returns `{}` :/ From e8eb6c7d2180f0c67b717e0ff5e650a43eddcdec Mon Sep 17 00:00:00 2001 From: Nulo Date: Thu, 29 Feb 2024 15:00:06 -0300 Subject: [PATCH 16/52] chore: remove TODO comment mauve said it's fine it's from Mastodon --- src/server/api/announcements.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/server/api/announcements.ts b/src/server/api/announcements.ts index 59bafd4..ac76229 100644 --- a/src/server/api/announcements.ts +++ b/src/server/api/announcements.ts @@ -42,7 +42,6 @@ export const announcementsRoutes = (cfg: APIConfig, store: Store, apsystem: Acti inbox: `${actor.actorUrl}/inbox`, outbox: `${actor.actorUrl}/outbox`, publicKey: { - // TODO: copied from Mastodon id: `${actor.actorUrl}#main-key`, owner: actor.actorUrl, From c6465855887e3d07ac81b577531347d4dd695dd4 Mon Sep 17 00:00:00 2001 From: Nulo Date: Thu, 29 Feb 2024 15:06:49 -0300 Subject: [PATCH 17/52] fix: sort announcements outbox by latest first --- src/server/api/announcements.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/server/api/announcements.ts b/src/server/api/announcements.ts index ac76229..6265259 100644 --- a/src/server/api/announcements.ts +++ b/src/server/api/announcements.ts @@ -67,7 +67,11 @@ export const announcementsRoutes = (cfg: APIConfig, store: Store, apsystem: Acti }, async (request, reply) => { const actor = await store.announcements.getInfo() const activities = await store.announcements.outbox.list() - const orderedItems = activities.map(a => a.id) + const orderedItems = activities + // XXX: maybe `new Date()` doesn't correctly parse possible dates? + .map(a => ({ ...a, published: typeof a.published === 'string' ? new Date(a.published) : a.published })) + .sort((a, b) => +(b.published ?? 0) - +(a.published ?? 0)) + .map(a => a.id) return await reply.send({ '@context': 'https://www.w3.org/ns/activitystreams', id: `${actor.actorUrl}/outbox`, From fbb600625cfb715e5efae8f382e578156a964ae0 Mon Sep 17 00:00:00 2001 From: Nulo Date: Thu, 29 Feb 2024 15:21:47 -0300 Subject: [PATCH 18/52] fix: move announcements stuff to APSystem subclass, fix stuff * announce to announcements actor, not to newly created actor * only update required parts of announcements actor info --- src/server/announcements.ts | 60 +++++++++++++++++++++++++++++++++++++ src/server/api/creation.ts | 20 +------------ src/server/api/index.ts | 19 +----------- src/server/apsystem.ts | 3 ++ 4 files changed, 65 insertions(+), 37 deletions(-) create mode 100644 src/server/announcements.ts diff --git a/src/server/announcements.ts b/src/server/announcements.ts new file mode 100644 index 0000000..03c0e7a --- /dev/null +++ b/src/server/announcements.ts @@ -0,0 +1,60 @@ +import { nanoid } from 'nanoid' +import { ActorInfo } from '../schemas' +import ActivityPubSystem, { DEFAULT_PUBLIC_KEY_FIELD } from './apsystem' +import { generateKeypair } from 'http-signed-fetch' + +export class Announcements { + apsystem: ActivityPubSystem + publicURL: string + + constructor (apsystem: ActivityPubSystem, publicURL: string) { + this.apsystem = apsystem + this.publicURL = publicURL + } + + async init (): Promise { + const actorUrl = `${this.publicURL}/v1/announcements` + + try { + const prev = await this.apsystem.store.announcements.getInfo() + if (prev.actorUrl !== actorUrl) { + await this.apsystem.store.announcements.setInfo({ + ...prev, + actorUrl + }) + } + } catch { + const { privateKeyPem, publicKeyPem } = generateKeypair() + await this.apsystem.store.announcements.setInfo({ + actorUrl, + publicKeyId: `${actorUrl}#${DEFAULT_PUBLIC_KEY_FIELD}`, + keypair: { + privateKeyPem, + publicKeyPem + }, + announce: false + }) + } + } + + async announce (actor: string, info: ActorInfo): Promise { + const existedAlready = (await this.apsystem.store.actorsDb.keys().all()).some(k => k === actor) + + if (!existedAlready && info.announce) { + const activity = { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Note', + id: `${info.actorUrl}/outbox/${nanoid()}`, + actor: info.actorUrl, + attributedTo: info.actorUrl, + published: new Date().toUTCString(), + to: ['https://www.w3.org/ns/activitystreams#Public'], + cc: ['https://social.distributed.press/v1/announcements/followers'], + // TODO: add a template in config + content: `a wild site appears! ${actor}` + } + await this.apsystem.store.announcements.outbox.add(activity) + await this.apsystem.notifyFollowers('announcements', activity) + } + } +} diff --git a/src/server/api/creation.ts b/src/server/api/creation.ts index 4a495fa..5925454 100644 --- a/src/server/api/creation.ts +++ b/src/server/api/creation.ts @@ -29,8 +29,6 @@ export const creationRoutes = (cfg: APIConfig, store: Store, apsystem: ActivityP }, async (request, reply) => { const { actor } = request.params - const existedAlready = (await store.actorsDb.keys().all()).some(k => k === actor) - const allowed = await apsystem.hasPermissionActorRequest(actor, request, false) if (!allowed) { return await reply.code(403).send('Not Allowed') @@ -38,23 +36,7 @@ export const creationRoutes = (cfg: APIConfig, store: Store, apsystem: ActivityP const info = request.body await store.forActor(actor).setInfo(info) - - if (!existedAlready && info.announce) { - const activity = { - '@context': 'https://www.w3.org/ns/activitystreams', - type: 'Note', - id: `${info.actorUrl}/outbox/${nanoid()}`, - actor: info.actorUrl, - attributedTo: info.actorUrl, - published: new Date().toUTCString(), - to: ['https://www.w3.org/ns/activitystreams#Public'], - cc: ['https://social.distributed.press/v1/announcements/followers'], - // TODO: add a template in config - content: `a wild site appears! ${actor}` - } - await store.announcements.outbox.add(activity) - await apsystem.notifyFollowers(actor, activity) - } + await apsystem.announcements.announce(actor, info) return await reply.send(info) }) diff --git a/src/server/api/index.ts b/src/server/api/index.ts index fc4970a..68fb2e5 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -96,24 +96,7 @@ async function apiBuilder (cfg: APIConfig): Promise { return 'ok\n' }) - // Setup announcements actor - const actorUrl = `${cfg.publicURL}/v1/announcements` - let keys - try { - const prev = await store.announcements.getInfo() - keys = { ...prev.keypair, publicKeyId: prev.publicKeyId } - } catch { - keys = { ...generateKeypair(), publicKeyId: `${actorUrl}#${DEFAULT_PUBLIC_KEY_FIELD}` } - } - await store.announcements.setInfo({ - actorUrl, - publicKeyId: keys.publicKeyId, - keypair: { - privateKeyPem: keys.privateKeyPem, - publicKeyPem: keys.publicKeyPem - }, - announce: false - }) + await apsystem.announcements.init() await server.register(v1Routes(cfg, store, apsystem, hookSystem), { prefix: '/v1' }) diff --git a/src/server/apsystem.ts b/src/server/apsystem.ts index 51f476d..5eb45e3 100644 --- a/src/server/apsystem.ts +++ b/src/server/apsystem.ts @@ -15,6 +15,7 @@ import { import type Store from './store/index.js' import { makeSigner } from '../keypair.js' +import { Announcements } from './announcements.js' export const DEFAULT_PUBLIC_KEY_FIELD = 'publicKey' @@ -45,6 +46,7 @@ export default class ActivityPubSystem { modCheck: ModerationChecker fetch: FetchLike hookSystem: HookSystem + announcements: Announcements constructor ( publicURL: string, @@ -58,6 +60,7 @@ export default class ActivityPubSystem { this.modCheck = modCheck this.fetch = fetch this.hookSystem = hookSystem + this.announcements = new Announcements(this, publicURL) } makeURL (path: string): string { From 246a1b5b4f924b5ee703ba691e475c0d5b49d131 Mon Sep 17 00:00:00 2001 From: Nulo Date: Thu, 29 Feb 2024 15:23:30 -0300 Subject: [PATCH 19/52] chore: fix linting errors --- src/server/api/creation.ts | 1 - src/server/api/index.ts | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/server/api/creation.ts b/src/server/api/creation.ts index 5925454..0843ec8 100644 --- a/src/server/api/creation.ts +++ b/src/server/api/creation.ts @@ -3,7 +3,6 @@ import { Type } from '@sinclair/typebox' import type { APIConfig, FastifyTypebox } from './index.js' import Store, { ActorInfo, ActorInfoSchema } from '../store/index.js' import ActivityPubSystem from '../apsystem.js' -import { nanoid } from 'nanoid' export const creationRoutes = (cfg: APIConfig, store: Store, apsystem: ActivityPubSystem) => async (server: FastifyTypebox): Promise => { // Create a new inbox diff --git a/src/server/api/index.ts b/src/server/api/index.ts index 68fb2e5..6679603 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -16,7 +16,7 @@ import { Level } from 'level' import { MemoryLevel } from 'memory-level' import Store from '../store/index.js' -import ActivityPubSystem, { DEFAULT_PUBLIC_KEY_FIELD } from '../apsystem.js' +import ActivityPubSystem from '../apsystem.js' import HookSystem from '../hooksystem.js' import { ModerationChecker } from '../moderation.js' @@ -27,7 +27,6 @@ import { blockAllowListRoutes } from './blockallowlist.js' import { adminRoutes } from './admins.js' import { followerRoutes } from './followers.js' import { hookRoutes } from './hooks.js' -import { generateKeypair } from 'http-signed-fetch' import { announcementsRoutes } from './announcements.js' export const paths = envPaths('distributed-press') From 31e9b2dfee3466297454962cc96cec73be57aecd Mon Sep 17 00:00:00 2001 From: Nulo Date: Mon, 4 Mar 2024 12:45:27 -0300 Subject: [PATCH 20/52] fix: remove trailing slash everywhere --- src/scripts/add-post-to-announcements.ts | 2 +- src/server/announcements.ts | 4 ++-- src/server/api/announcements.ts | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/scripts/add-post-to-announcements.ts b/src/scripts/add-post-to-announcements.ts index e9f8120..4d75c23 100644 --- a/src/scripts/add-post-to-announcements.ts +++ b/src/scripts/add-post-to-announcements.ts @@ -28,7 +28,7 @@ const actor = await store.announcements.getInfo() await store.announcements.outbox.add({ '@context': 'https://www.w3.org/ns/activitystreams', type: 'Note', - id: `${actor.actorUrl}/outbox/${nanoid()}`, + id: `${actor.actorUrl}outbox/${nanoid()}`, actor: actor.actorUrl, attributedTo: actor.actorUrl, published: new Date().toUTCString(), diff --git a/src/server/announcements.ts b/src/server/announcements.ts index 03c0e7a..ba5aed7 100644 --- a/src/server/announcements.ts +++ b/src/server/announcements.ts @@ -13,7 +13,7 @@ export class Announcements { } async init (): Promise { - const actorUrl = `${this.publicURL}/v1/announcements` + const actorUrl = `${this.publicURL}/v1/announcements/` try { const prev = await this.apsystem.store.announcements.getInfo() @@ -44,7 +44,7 @@ export class Announcements { const activity = { '@context': 'https://www.w3.org/ns/activitystreams', type: 'Note', - id: `${info.actorUrl}/outbox/${nanoid()}`, + id: `${info.actorUrl}outbox/${nanoid()}`, actor: info.actorUrl, attributedTo: info.actorUrl, published: new Date().toUTCString(), diff --git a/src/server/api/announcements.ts b/src/server/api/announcements.ts index 6265259..5cbcae7 100644 --- a/src/server/api/announcements.ts +++ b/src/server/api/announcements.ts @@ -39,8 +39,8 @@ export const announcementsRoutes = (cfg: APIConfig, store: Store, apsystem: Acti // https://www.w3.org/TR/activitystreams-vocabulary/#actor-types type: 'Service', name: 'Announcements', - inbox: `${actor.actorUrl}/inbox`, - outbox: `${actor.actorUrl}/outbox`, + inbox: `${actor.actorUrl}inbox`, + outbox: `${actor.actorUrl}outbox`, publicKey: { id: `${actor.actorUrl}#main-key`, @@ -74,7 +74,7 @@ export const announcementsRoutes = (cfg: APIConfig, store: Store, apsystem: Acti .map(a => a.id) return await reply.send({ '@context': 'https://www.w3.org/ns/activitystreams', - id: `${actor.actorUrl}/outbox`, + id: `${actor.actorUrl}outbox`, type: 'OrderedCollection', totalItems: orderedItems.length, orderedItems @@ -102,7 +102,7 @@ export const announcementsRoutes = (cfg: APIConfig, store: Store, apsystem: Acti } }, async (request, reply) => { const actor = await store.announcements.getInfo() - const activity = await store.announcements.outbox.get(`${actor.actorUrl}/outbox/${request.params.id}`) + const activity = await store.announcements.outbox.get(`${actor.actorUrl}outbox/${request.params.id}`) return await reply.send(activity) }) } From 1997e48f3b732ff35a182a7adf2b7ee873a35bae Mon Sep 17 00:00:00 2001 From: Nulo Date: Mon, 4 Mar 2024 12:45:47 -0300 Subject: [PATCH 21/52] feat: add webfinger route i made it into a separate routes file because it needed to not have the /v1/ prefix --- src/server/api/index.ts | 2 ++ src/server/api/wellKnown.ts | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 src/server/api/wellKnown.ts diff --git a/src/server/api/index.ts b/src/server/api/index.ts index 6679603..839cba0 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -28,6 +28,7 @@ import { adminRoutes } from './admins.js' import { followerRoutes } from './followers.js' import { hookRoutes } from './hooks.js' import { announcementsRoutes } from './announcements.js' +import { wellKnownRoutes } from './wellKnown.js' export const paths = envPaths('distributed-press') @@ -98,6 +99,7 @@ async function apiBuilder (cfg: APIConfig): Promise { await apsystem.announcements.init() await server.register(v1Routes(cfg, store, apsystem, hookSystem), { prefix: '/v1' }) + await server.register(wellKnownRoutes(cfg, store, apsystem)) await server.ready() diff --git a/src/server/api/wellKnown.ts b/src/server/api/wellKnown.ts new file mode 100644 index 0000000..6709663 --- /dev/null +++ b/src/server/api/wellKnown.ts @@ -0,0 +1,31 @@ +import { APIConfig, FastifyTypebox } from '.' +import ActivityPubSystem from '../apsystem' +import Store from '../store' + +export const wellKnownRoutes = (cfg: APIConfig, store: Store, apsystem: ActivityPubSystem) => async (server: FastifyTypebox): Promise => { + server.get<{ + Reply: any + }>('/.well-known/webfinger', { + schema: { + description: 'ActivityPub WebFinger', + tags: ['ActivityPub'] + } + }, async (request, reply) => { + const actor = await store.announcements.getInfo() + return await reply + .headers({ + 'Content-Type': 'application/jrd+json' + }) + .send({ + subject: `acct:announcements@${cfg.publicURL}`, + aliases: [actor.actorUrl], + links: [ + { + rel: 'self', + type: 'application/activity+json', + href: actor.actorUrl + } + ] + }) + }) +} From 59ad921d8347edd52aa5a81a04079fcbc8450556 Mon Sep 17 00:00:00 2001 From: Nulo Date: Tue, 5 Mar 2024 22:21:59 -0300 Subject: [PATCH 22/52] perf: check if actor already exists using `.get` not tested --- src/server/announcements.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/announcements.ts b/src/server/announcements.ts index ba5aed7..66567b8 100644 --- a/src/server/announcements.ts +++ b/src/server/announcements.ts @@ -38,9 +38,9 @@ export class Announcements { } async announce (actor: string, info: ActorInfo): Promise { - const existedAlready = (await this.apsystem.store.actorsDb.keys().all()).some(k => k === actor) + const existedAlready = await this.apsystem.store.actorsDb.get(actor) - if (!existedAlready && info.announce) { + if (existedAlready === undefined && info.announce) { const activity = { '@context': 'https://www.w3.org/ns/activitystreams', type: 'Note', From a6b1fa79b2d72c800e8f2eef336147124460e6d1 Mon Sep 17 00:00:00 2001 From: Nulo Date: Tue, 5 Mar 2024 22:33:42 -0300 Subject: [PATCH 23/52] chore: move outbox getter to Announcements class --- src/server/announcements.ts | 20 ++++++++++++++++++++ src/server/api/announcements.ts | 15 +-------------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/server/announcements.ts b/src/server/announcements.ts index 66567b8..4b56dca 100644 --- a/src/server/announcements.ts +++ b/src/server/announcements.ts @@ -2,6 +2,7 @@ import { nanoid } from 'nanoid' import { ActorInfo } from '../schemas' import ActivityPubSystem, { DEFAULT_PUBLIC_KEY_FIELD } from './apsystem' import { generateKeypair } from 'http-signed-fetch' +import { APOrderedCollection } from 'activitypub-types' export class Announcements { apsystem: ActivityPubSystem @@ -57,4 +58,23 @@ export class Announcements { await this.apsystem.notifyFollowers('announcements', activity) } } + + async getOutbox (): Promise { + const actor = await this.apsystem.store.announcements.getInfo() + const activities = await this.apsystem.store.announcements.outbox.list() + const orderedItems = activities + // XXX: maybe `new Date()` doesn't correctly parse possible dates? + .map(a => ({ ...a, published: typeof a.published === 'string' ? new Date(a.published) : a.published })) + .sort((a, b) => +(b.published ?? 0) - +(a.published ?? 0)) + .map(a => a.id) + .filter((id): id is string => id !== undefined) + + return { + '@context': 'https://www.w3.org/ns/activitystreams', + id: `${actor.actorUrl}outbox`, + type: 'OrderedCollection', + totalItems: orderedItems.length, + orderedItems + } + } } diff --git a/src/server/api/announcements.ts b/src/server/api/announcements.ts index 5cbcae7..7e7644e 100644 --- a/src/server/api/announcements.ts +++ b/src/server/api/announcements.ts @@ -65,20 +65,7 @@ export const announcementsRoutes = (cfg: APIConfig, store: Store, apsystem: Acti tags: ['ActivityPub'] } }, async (request, reply) => { - const actor = await store.announcements.getInfo() - const activities = await store.announcements.outbox.list() - const orderedItems = activities - // XXX: maybe `new Date()` doesn't correctly parse possible dates? - .map(a => ({ ...a, published: typeof a.published === 'string' ? new Date(a.published) : a.published })) - .sort((a, b) => +(b.published ?? 0) - +(a.published ?? 0)) - .map(a => a.id) - return await reply.send({ - '@context': 'https://www.w3.org/ns/activitystreams', - id: `${actor.actorUrl}outbox`, - type: 'OrderedCollection', - totalItems: orderedItems.length, - orderedItems - }) + return await reply.send(await apsystem.announcements.getOutbox()) }) server.get<{ From 7a334a94f65c55915c281cdd60b808e1a50828f3 Mon Sep 17 00:00:00 2001 From: Nulo Date: Thu, 7 Mar 2024 17:13:05 -0300 Subject: [PATCH 24/52] delete cli it's pretty hacky and not very useful right now, it can be used as a starting point for a proper client API-based CLI in the future --- src/scripts/add-post-to-announcements.ts | 38 ------------------------ 1 file changed, 38 deletions(-) delete mode 100644 src/scripts/add-post-to-announcements.ts diff --git a/src/scripts/add-post-to-announcements.ts b/src/scripts/add-post-to-announcements.ts deleted file mode 100644 index 4d75c23..0000000 --- a/src/scripts/add-post-to-announcements.ts +++ /dev/null @@ -1,38 +0,0 @@ -import yargs from 'yargs' -import { hideBin } from 'yargs/helpers' -import envPaths from 'env-paths' -import { Level } from 'level' - -import Store from '../server/store/index.js' -import { nanoid } from 'nanoid' - -const paths = envPaths('social.distributed.press') - -const argv = yargs(hideBin(process.argv)) - .command('', 'content for the post') - .demandCommand(1) - .options({ - storage: { type: 'string' } - }).parseSync() - -const storage = argv.storage ?? paths.data -const content = `${argv._[0]}` - -const db = new Level(storage, { valueEncoding: 'json' }) - -const store = new Store(db) - -console.log(`Posting: ${content}`) -const actor = await store.announcements.getInfo() - -await store.announcements.outbox.add({ - '@context': 'https://www.w3.org/ns/activitystreams', - type: 'Note', - id: `${actor.actorUrl}outbox/${nanoid()}`, - actor: actor.actorUrl, - attributedTo: actor.actorUrl, - published: new Date().toUTCString(), - to: ['https://www.w3.org/ns/activitystreams#Public'], - cc: ['https://social.distributed.press/v1/announcements/followers'], - content -}) From 83c7488677cc2d2a34fb9862f9d7700303a7fbaa Mon Sep 17 00:00:00 2001 From: Nulo Date: Thu, 7 Mar 2024 17:14:21 -0300 Subject: [PATCH 25/52] delete client API CLI it doesn't have much functionality yet and is only really useful for internal testing for now. --- src/client/cli.ts | 41 ----------------------------------------- 1 file changed, 41 deletions(-) delete mode 100644 src/client/cli.ts diff --git a/src/client/cli.ts b/src/client/cli.ts deleted file mode 100644 index 22bde20..0000000 --- a/src/client/cli.ts +++ /dev/null @@ -1,41 +0,0 @@ -import yargs from 'yargs' -import { hideBin } from 'yargs/helpers' - -import { SocialInboxClient } from './index.js' -import { generateKeypair } from 'http-signed-fetch' -import { DEFAULT_PUBLIC_KEY_FIELD } from '../server/apsystem.js' - -// https://github.com/yargs/yargs/issues/225 -const argv = yargs(hideBin(process.argv)) - .usage('usage: $0 ') - .demandCommand(1) - .options({ - url: { type: 'string', default: 'http://localhost:8080' }, - announce: { type: 'boolean', default: true } - }).parseSync() - -const account = `${argv._[0]}` -const { url, announce } = argv - -// XXX: is this safe to assume? -const [, username, instance] = account.split('@') -if (username === undefined || instance === undefined) { - throw new Error('missing username or instance') -} -const actorUrl = `https://${instance}/@${username}` -const publicKeyId = `${actorUrl}#${DEFAULT_PUBLIC_KEY_FIELD}` - -const keypair = generateKeypair() -const client = new SocialInboxClient({ - instance: url, - keypair: { ...keypair, publicKeyId }, - account -}) - -// ref: https://github.com/RangerMauve/staticpub.mauve.moe/blob/default/create_account.js -await client.setInfo({ - actorUrl, - publicKeyId, - keypair, - announce -}) From 7aa12c5aabfbd63819da6d5c663efd28fbdb7e5d Mon Sep 17 00:00:00 2001 From: Nulo Date: Thu, 7 Mar 2024 17:24:16 -0300 Subject: [PATCH 26/52] fix: setup announcements actor as a normal "mention" actor store the announcements actor as a "normal" actor in actorsDb, using `@announcements@publicHostname`. also clean up code. --- src/server/announcements.ts | 41 ++++++++++++++++++++++++--------- src/server/api/announcements.ts | 24 ++++++++++--------- src/server/api/wellKnown.ts | 8 +++---- src/server/store/index.ts | 7 ------ 4 files changed, 47 insertions(+), 33 deletions(-) diff --git a/src/server/announcements.ts b/src/server/announcements.ts index 4b56dca..95f6f0f 100644 --- a/src/server/announcements.ts +++ b/src/server/announcements.ts @@ -3,6 +3,7 @@ import { ActorInfo } from '../schemas' import ActivityPubSystem, { DEFAULT_PUBLIC_KEY_FIELD } from './apsystem' import { generateKeypair } from 'http-signed-fetch' import { APOrderedCollection } from 'activitypub-types' +import { ActorStore } from './store/ActorStore' export class Announcements { apsystem: ActivityPubSystem @@ -13,20 +14,38 @@ export class Announcements { this.publicURL = publicURL } + get actorUrl (): string { + return `${this.publicURL}/v1/${this.mention}/` + } + + get outboxUrl (): string { + return `${this.actorUrl}outbox` + } + + get mention (): string { + const url = new URL(this.publicURL) + return url.hostname + } + + async getActor (): Promise { + return this.apsystem.store.forActor(this.mention) + } + async init (): Promise { - const actorUrl = `${this.publicURL}/v1/announcements/` + const actorUrl = this.actorUrl + const actor = await this.getActor() try { - const prev = await this.apsystem.store.announcements.getInfo() + const prev = await actor.getInfo() if (prev.actorUrl !== actorUrl) { - await this.apsystem.store.announcements.setInfo({ + await actor.setInfo({ ...prev, actorUrl }) } } catch { const { privateKeyPem, publicKeyPem } = generateKeypair() - await this.apsystem.store.announcements.setInfo({ + await actor.setInfo({ actorUrl, publicKeyId: `${actorUrl}#${DEFAULT_PUBLIC_KEY_FIELD}`, keypair: { @@ -45,23 +64,23 @@ export class Announcements { const activity = { '@context': 'https://www.w3.org/ns/activitystreams', type: 'Note', - id: `${info.actorUrl}outbox/${nanoid()}`, + id: `${this.outboxUrl}/${nanoid()}`, actor: info.actorUrl, attributedTo: info.actorUrl, published: new Date().toUTCString(), to: ['https://www.w3.org/ns/activitystreams#Public'], - cc: ['https://social.distributed.press/v1/announcements/followers'], + cc: [`${this.actorUrl}followers`], // TODO: add a template in config content: `a wild site appears! ${actor}` } - await this.apsystem.store.announcements.outbox.add(activity) - await this.apsystem.notifyFollowers('announcements', activity) + await (await this.getActor()).outbox.add(activity) + await this.apsystem.notifyFollowers(this.mention, activity) } } async getOutbox (): Promise { - const actor = await this.apsystem.store.announcements.getInfo() - const activities = await this.apsystem.store.announcements.outbox.list() + const actor = await this.getActor() + const activities = await actor.outbox.list() const orderedItems = activities // XXX: maybe `new Date()` doesn't correctly parse possible dates? .map(a => ({ ...a, published: typeof a.published === 'string' ? new Date(a.published) : a.published })) @@ -71,7 +90,7 @@ export class Announcements { return { '@context': 'https://www.w3.org/ns/activitystreams', - id: `${actor.actorUrl}outbox`, + id: `${this.actorUrl}outbox`, type: 'OrderedCollection', totalItems: orderedItems.length, orderedItems diff --git a/src/server/api/announcements.ts b/src/server/api/announcements.ts index 7e7644e..63370f9 100644 --- a/src/server/api/announcements.ts +++ b/src/server/api/announcements.ts @@ -16,7 +16,7 @@ type APActorNonStandard = APActor & { export const announcementsRoutes = (cfg: APIConfig, store: Store, apsystem: ActivityPubSystem) => async (server: FastifyTypebox): Promise => { server.get<{ Reply: APActorNonStandard - }>('/announcements/', { + }>(`/${apsystem.announcements.mention}/`, { schema: { params: {}, // XXX: even with Type.Any(), the endpoint returns `{}` :/ @@ -28,7 +28,8 @@ export const announcementsRoutes = (cfg: APIConfig, store: Store, apsystem: Acti tags: ['ActivityPub'] } }, async (request, reply) => { - const actor = await store.announcements.getInfo() + const actor = await apsystem.announcements.getActor() + const actorInfo = await actor.getInfo() return await reply.send({ '@context': [ @@ -39,13 +40,13 @@ export const announcementsRoutes = (cfg: APIConfig, store: Store, apsystem: Acti // https://www.w3.org/TR/activitystreams-vocabulary/#actor-types type: 'Service', name: 'Announcements', - inbox: `${actor.actorUrl}inbox`, - outbox: `${actor.actorUrl}outbox`, + inbox: `${actorInfo.actorUrl}inbox`, + outbox: `${actorInfo.actorUrl}outbox`, publicKey: { - id: `${actor.actorUrl}#main-key`, + id: `${actorInfo.actorUrl}#main-key`, - owner: actor.actorUrl, - publicKeyPem: actor.keypair.publicKeyPem + owner: actorInfo.actorUrl, + publicKeyPem: actorInfo.keypair.publicKeyPem } }) }) @@ -53,7 +54,7 @@ export const announcementsRoutes = (cfg: APIConfig, store: Store, apsystem: Acti server.get<{ // TODO: typebox APOrderedCollection Reply: any - }>('/announcements/outbox', { + }>(`/${apsystem.announcements.mention}/outbox`, { schema: { params: {}, // XXX: even with Type.Any(), the endpoint returns `{}` :/ @@ -74,7 +75,7 @@ export const announcementsRoutes = (cfg: APIConfig, store: Store, apsystem: Acti } // TODO: typebox APOrderedCollection Reply: any - }>('/announcements/outbox/:id', { + }>(`/${apsystem.announcements.mention}/outbox/:id`, { schema: { params: Type.Object({ id: Type.String() @@ -88,8 +89,9 @@ export const announcementsRoutes = (cfg: APIConfig, store: Store, apsystem: Acti tags: ['ActivityPub'] } }, async (request, reply) => { - const actor = await store.announcements.getInfo() - const activity = await store.announcements.outbox.get(`${actor.actorUrl}outbox/${request.params.id}`) + const actor = await apsystem.announcements.getActor() + const actorInfo = await actor.getInfo() + const activity = await actor.outbox.get(`${actorInfo.actorUrl}outbox/${request.params.id}`) return await reply.send(activity) }) } diff --git a/src/server/api/wellKnown.ts b/src/server/api/wellKnown.ts index 6709663..1e39653 100644 --- a/src/server/api/wellKnown.ts +++ b/src/server/api/wellKnown.ts @@ -11,19 +11,19 @@ export const wellKnownRoutes = (cfg: APIConfig, store: Store, apsystem: Activity tags: ['ActivityPub'] } }, async (request, reply) => { - const actor = await store.announcements.getInfo() + // https://docs.joinmastodon.org/spec/webfinger/ return await reply .headers({ 'Content-Type': 'application/jrd+json' }) .send({ - subject: `acct:announcements@${cfg.publicURL}`, - aliases: [actor.actorUrl], + subject: `acct:${apsystem.announcements.mention.slice(1)}`, + aliases: [apsystem.announcements.actorUrl], links: [ { rel: 'self', type: 'application/activity+json', - href: actor.actorUrl + href: apsystem.announcements.actorUrl } ] }) diff --git a/src/server/store/index.ts b/src/server/store/index.ts index 7bf7e0e..ea1a13b 100644 --- a/src/server/store/index.ts +++ b/src/server/store/index.ts @@ -10,7 +10,6 @@ export default class Store { db: AbstractLevel actorCache: Map actorsDb: AbstractLevel - announcements: ActorStore blocklist: AccountListStore allowlist: AccountListStore admins: AccountListStore @@ -20,8 +19,6 @@ export default class Store { this.db = db this.actorCache = new Map() this.actorsDb = this.db.sublevel('actorCache', { valueEncoding: 'json' }) - const announcementsDb = this.db.sublevel('announcements', { valueEncoding: 'json' }) - this.announcements = new ActorStore(announcementsDb) const blocklistDb = this.db.sublevel('blocklist', { valueEncoding: 'json' }) @@ -37,10 +34,6 @@ export default class Store { } forActor (domain: string): ActorStore { - if (domain === 'announcements') { - return this.announcements - } - if (!this.actorCache.has(domain)) { const sub = this.db.sublevel(domain, { valueEncoding: 'json' }) const store = new ActorStore(sub) From 551f2f57e8c3ecb1b2fbd159211d9ed09224deee Mon Sep 17 00:00:00 2001 From: Nulo Date: Fri, 8 Mar 2024 16:42:00 -0300 Subject: [PATCH 27/52] fix: make getActor sync --- src/server/announcements.ts | 8 ++++---- src/server/api/announcements.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/server/announcements.ts b/src/server/announcements.ts index 95f6f0f..b2baa39 100644 --- a/src/server/announcements.ts +++ b/src/server/announcements.ts @@ -27,13 +27,13 @@ export class Announcements { return url.hostname } - async getActor (): Promise { + getActor (): ActorStore { return this.apsystem.store.forActor(this.mention) } async init (): Promise { const actorUrl = this.actorUrl - const actor = await this.getActor() + const actor = this.getActor() try { const prev = await actor.getInfo() @@ -73,13 +73,13 @@ export class Announcements { // TODO: add a template in config content: `a wild site appears! ${actor}` } - await (await this.getActor()).outbox.add(activity) + await this.getActor().outbox.add(activity) await this.apsystem.notifyFollowers(this.mention, activity) } } async getOutbox (): Promise { - const actor = await this.getActor() + const actor = this.getActor() const activities = await actor.outbox.list() const orderedItems = activities // XXX: maybe `new Date()` doesn't correctly parse possible dates? diff --git a/src/server/api/announcements.ts b/src/server/api/announcements.ts index 63370f9..c3f93cc 100644 --- a/src/server/api/announcements.ts +++ b/src/server/api/announcements.ts @@ -28,7 +28,7 @@ export const announcementsRoutes = (cfg: APIConfig, store: Store, apsystem: Acti tags: ['ActivityPub'] } }, async (request, reply) => { - const actor = await apsystem.announcements.getActor() + const actor = apsystem.announcements.getActor() const actorInfo = await actor.getInfo() return await reply.send({ @@ -89,7 +89,7 @@ export const announcementsRoutes = (cfg: APIConfig, store: Store, apsystem: Acti tags: ['ActivityPub'] } }, async (request, reply) => { - const actor = await apsystem.announcements.getActor() + const actor = apsystem.announcements.getActor() const actorInfo = await actor.getInfo() const activity = await actor.outbox.get(`${actorInfo.actorUrl}outbox/${request.params.id}`) return await reply.send(activity) From 1f0a840021fa3e32c0441144e7b06aa5c529086c Mon Sep 17 00:00:00 2001 From: Nulo Date: Fri, 8 Mar 2024 16:58:13 -0300 Subject: [PATCH 28/52] fix: generate correct mention for announcements actor --- src/server/announcements.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/announcements.ts b/src/server/announcements.ts index b2baa39..8366333 100644 --- a/src/server/announcements.ts +++ b/src/server/announcements.ts @@ -24,7 +24,7 @@ export class Announcements { get mention (): string { const url = new URL(this.publicURL) - return url.hostname + return `@announcements@${url.hostname}` } getActor (): ActorStore { From f801a7ab66d3863456f9be255a71aa900386ef2d Mon Sep 17 00:00:00 2001 From: Nulo Date: Fri, 8 Mar 2024 17:08:50 -0300 Subject: [PATCH 29/52] fix: add missing fields to announcements actor activitypub --- src/server/api/announcements.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/server/api/announcements.ts b/src/server/api/announcements.ts index c3f93cc..b794622 100644 --- a/src/server/api/announcements.ts +++ b/src/server/api/announcements.ts @@ -38,8 +38,13 @@ export const announcementsRoutes = (cfg: APIConfig, store: Store, apsystem: Acti 'https://w3id.org/security/v1' ], // https://www.w3.org/TR/activitystreams-vocabulary/#actor-types - type: 'Service', + id: apsystem.announcements.actorUrl, + type: 'Person', name: 'Announcements', + summary: `Announcements for ${new URL(cfg.publicURL).hostname}`, + preferredUsername: 'Announcements', + following: `${actorInfo.actorUrl}following`, + followers: `${actorInfo.actorUrl}followers`, inbox: `${actorInfo.actorUrl}inbox`, outbox: `${actorInfo.actorUrl}outbox`, publicKey: { From 0823e0fac0794e773b9fe1587e91e9928fd04278 Mon Sep 17 00:00:00 2001 From: Nulo Date: Mon, 11 Mar 2024 17:53:49 -0300 Subject: [PATCH 30/52] fix: do not error when trying to announce non-existant actor --- src/server/announcements.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/server/announcements.ts b/src/server/announcements.ts index 8366333..4cc140d 100644 --- a/src/server/announcements.ts +++ b/src/server/announcements.ts @@ -58,9 +58,17 @@ export class Announcements { } async announce (actor: string, info: ActorInfo): Promise { - const existedAlready = await this.apsystem.store.actorsDb.get(actor) + let existedAlready = false + try { + const existingActor = await this.apsystem.store.actorsDb.get(actor) + if (existingActor === undefined) existedAlready = true + } catch (err) { + if (!(err as { notFound: boolean }).notFound) { + throw err + } + } - if (existedAlready === undefined && info.announce) { + if (!existedAlready && info.announce) { const activity = { '@context': 'https://www.w3.org/ns/activitystreams', type: 'Note', From b0d1dc9c29492ba350d4432df74458c4230ef65d Mon Sep 17 00:00:00 2001 From: Nulo Date: Mon, 11 Mar 2024 17:54:26 -0300 Subject: [PATCH 31/52] chore: add test for announcements --- src/server/announcements.test.ts | 46 ++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/server/announcements.test.ts diff --git a/src/server/announcements.test.ts b/src/server/announcements.test.ts new file mode 100644 index 0000000..af13cc7 --- /dev/null +++ b/src/server/announcements.test.ts @@ -0,0 +1,46 @@ +import test from 'ava' +import sinon from 'sinon' +import ActivityPubSystem, { FetchLike } from './apsystem' +import Store from './store/index.js' +import { ModerationChecker } from './moderation.js' +import HookSystem from './hooksystem' +import { MemoryLevel } from 'memory-level' + +// Create some mock dependencies +const mockStore = new Store(new MemoryLevel()) + +const mockModCheck = new ModerationChecker(mockStore) +const mockFetch: FetchLike = async (input: RequestInfo | URL, init?: RequestInit): Promise => { + return new Response(JSON.stringify({}), { status: 200 }) +} +const mockHooks = new HookSystem(mockStore, mockFetch) + +const aps = new ActivityPubSystem('http://localhost', mockStore, mockModCheck, mockHooks) + +test.beforeEach(async () => { + // Restore stubs before setting them up again + sinon.restore() + + await aps.announcements.init() +}) + +const keypair = { + publicKeyPem: 'mockPublicKey', + privateKeyPem: 'mockPrivateKey', + publicKeyId: 'mockPublicKeyId' +} + +test('actor gets announced on .announce', async t => { + const fakeActor = '@test@url' + const actorInfo = { + actorUrl: 'https://url/@test', + announce: true, + keypair, + publicKeyId: keypair.publicKeyId + } + await aps.store.forActor(fakeActor).setInfo(actorInfo) + await aps.announcements.announce(fakeActor, actorInfo) + + const outbox = await aps.store.forActor('@announcements@localhost').outbox.list() + t.is(outbox.length, 1, 'something is in outbox') +}) From 32bff3a56a8918afcfaa58f70f13b79c58251f2e Mon Sep 17 00:00:00 2001 From: Nulo Date: Mon, 11 Mar 2024 19:51:20 -0300 Subject: [PATCH 32/52] fix: correctly check actor presence --- src/server/announcements.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/announcements.ts b/src/server/announcements.ts index 4cc140d..af8e2f2 100644 --- a/src/server/announcements.ts +++ b/src/server/announcements.ts @@ -61,7 +61,7 @@ export class Announcements { let existedAlready = false try { const existingActor = await this.apsystem.store.actorsDb.get(actor) - if (existingActor === undefined) existedAlready = true + if (existingActor !== undefined) existedAlready = true } catch (err) { if (!(err as { notFound: boolean }).notFound) { throw err From 067675837325d66be17190f79c0a7f352caac221 Mon Sep 17 00:00:00 2001 From: Nulo Date: Wed, 13 Mar 2024 15:11:40 -0300 Subject: [PATCH 33/52] fix: try announcing new actor before creating it otherwise .announce will never announce as it will exist beforehand --- src/server/api/creation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/api/creation.ts b/src/server/api/creation.ts index 0843ec8..89d366d 100644 --- a/src/server/api/creation.ts +++ b/src/server/api/creation.ts @@ -34,8 +34,8 @@ export const creationRoutes = (cfg: APIConfig, store: Store, apsystem: ActivityP } const info = request.body - await store.forActor(actor).setInfo(info) await apsystem.announcements.announce(actor, info) + await store.forActor(actor).setInfo(info) return await reply.send(info) }) From a82ff7408aa9da2362001c438cd84dd6fccd3be5 Mon Sep 17 00:00:00 2001 From: Nulo Date: Wed, 13 Mar 2024 15:12:14 -0300 Subject: [PATCH 34/52] fix: use actual DB to check if actor exists already --- src/server/announcements.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/announcements.ts b/src/server/announcements.ts index af8e2f2..0ff2b94 100644 --- a/src/server/announcements.ts +++ b/src/server/announcements.ts @@ -60,7 +60,7 @@ export class Announcements { async announce (actor: string, info: ActorInfo): Promise { let existedAlready = false try { - const existingActor = await this.apsystem.store.actorsDb.get(actor) + const existingActor = await this.apsystem.store.forActor(actor).getInfo() if (existingActor !== undefined) existedAlready = true } catch (err) { if (!(err as { notFound: boolean }).notFound) { From c5c25165a09231f988e0548d5529ee4d5893ca16 Mon Sep 17 00:00:00 2001 From: Nulo Date: Wed, 13 Mar 2024 15:13:15 -0300 Subject: [PATCH 35/52] fix: use announcements actor id for activity --- src/server/announcements.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/announcements.ts b/src/server/announcements.ts index 0ff2b94..fa5eebf 100644 --- a/src/server/announcements.ts +++ b/src/server/announcements.ts @@ -73,7 +73,7 @@ export class Announcements { '@context': 'https://www.w3.org/ns/activitystreams', type: 'Note', id: `${this.outboxUrl}/${nanoid()}`, - actor: info.actorUrl, + actor: this.actorUrl, attributedTo: info.actorUrl, published: new Date().toUTCString(), to: ['https://www.w3.org/ns/activitystreams#Public'], From b4184a361e321188a496f9816a5165680abd5282 Mon Sep 17 00:00:00 2001 From: Nulo Date: Wed, 13 Mar 2024 15:21:55 -0300 Subject: [PATCH 36/52] fix: make tests pass the same bug fixed in 067675837325d66be17190f79c0a7f352caac221 applied to the test :) --- src/server/announcements.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/announcements.test.ts b/src/server/announcements.test.ts index af13cc7..2aaab94 100644 --- a/src/server/announcements.test.ts +++ b/src/server/announcements.test.ts @@ -38,8 +38,8 @@ test('actor gets announced on .announce', async t => { keypair, publicKeyId: keypair.publicKeyId } - await aps.store.forActor(fakeActor).setInfo(actorInfo) await aps.announcements.announce(fakeActor, actorInfo) + await aps.store.forActor(fakeActor).setInfo(actorInfo) const outbox = await aps.store.forActor('@announcements@localhost').outbox.list() t.is(outbox.length, 1, 'something is in outbox') From 613d0d152738a715bc62279e7e0ced93939d7f93 Mon Sep 17 00:00:00 2001 From: Nulo Date: Wed, 13 Mar 2024 17:18:42 -0300 Subject: [PATCH 37/52] fix: prevent race condition and correctly attribute announcement activity --- src/server/announcements.ts | 40 +++++++++++++------------------------ src/server/api/creation.ts | 12 ++++++++++- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/src/server/announcements.ts b/src/server/announcements.ts index fa5eebf..48fd879 100644 --- a/src/server/announcements.ts +++ b/src/server/announcements.ts @@ -57,33 +57,21 @@ export class Announcements { } } - async announce (actor: string, info: ActorInfo): Promise { - let existedAlready = false - try { - const existingActor = await this.apsystem.store.forActor(actor).getInfo() - if (existingActor !== undefined) existedAlready = true - } catch (err) { - if (!(err as { notFound: boolean }).notFound) { - throw err - } - } - - if (!existedAlready && info.announce) { - const activity = { - '@context': 'https://www.w3.org/ns/activitystreams', - type: 'Note', - id: `${this.outboxUrl}/${nanoid()}`, - actor: this.actorUrl, - attributedTo: info.actorUrl, - published: new Date().toUTCString(), - to: ['https://www.w3.org/ns/activitystreams#Public'], - cc: [`${this.actorUrl}followers`], - // TODO: add a template in config - content: `a wild site appears! ${actor}` - } - await this.getActor().outbox.add(activity) - await this.apsystem.notifyFollowers(this.mention, activity) + async announce (actor: string): Promise { + const activity = { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Note', + id: `${this.outboxUrl}/${nanoid()}`, + actor: this.actorUrl, + attributedTo: this.actorUrl, + published: new Date().toUTCString(), + to: ['https://www.w3.org/ns/activitystreams#Public'], + cc: [`${this.actorUrl}followers`], + // TODO: add a template in config + content: `a wild site appears! ${actor}` } + await this.getActor().outbox.add(activity) + await this.apsystem.notifyFollowers(this.mention, activity) } async getOutbox (): Promise { diff --git a/src/server/api/creation.ts b/src/server/api/creation.ts index 89d366d..5d5dde2 100644 --- a/src/server/api/creation.ts +++ b/src/server/api/creation.ts @@ -33,9 +33,19 @@ export const creationRoutes = (cfg: APIConfig, store: Store, apsystem: ActivityP return await reply.code(403).send('Not Allowed') } + let existedAlready = false + try { + const existingActor = await apsystem.store.forActor(actor).getInfo() + if (existingActor !== undefined) existedAlready = true + } catch (err) { + if (!(err as { notFound: boolean }).notFound) { + throw err + } + } + const info = request.body - await apsystem.announcements.announce(actor, info) await store.forActor(actor).setInfo(info) + if (info.announce && !existedAlready) await apsystem.announcements.announce(actor) return await reply.send(info) }) From fa1cd982295d75f2fab865bb546114a274e87aae Mon Sep 17 00:00:00 2001 From: Nulo Date: Wed, 13 Mar 2024 17:21:08 -0300 Subject: [PATCH 38/52] fix: wrap announcement Note in a Create --- src/server/announcements.ts | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/server/announcements.ts b/src/server/announcements.ts index 48fd879..d0c869c 100644 --- a/src/server/announcements.ts +++ b/src/server/announcements.ts @@ -58,17 +58,29 @@ export class Announcements { } async announce (actor: string): Promise { + const published = new Date().toUTCString() + const to = ['https://www.w3.org/ns/activitystreams#Public'] + const cc = [`${this.actorUrl}followers`] const activity = { '@context': 'https://www.w3.org/ns/activitystreams', - type: 'Note', + type: 'Create', id: `${this.outboxUrl}/${nanoid()}`, actor: this.actorUrl, - attributedTo: this.actorUrl, - published: new Date().toUTCString(), - to: ['https://www.w3.org/ns/activitystreams#Public'], - cc: [`${this.actorUrl}followers`], - // TODO: add a template in config - content: `a wild site appears! ${actor}` + published, + to, + cc, + object: { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Note', + id: `${this.outboxUrl}/${nanoid()}/note`, + actor: this.actorUrl, + attributedTo: this.actorUrl, + published, + to, + cc, + // TODO: add a template in config + content: `a wild site appears! ${actor}` + } } await this.getActor().outbox.add(activity) await this.apsystem.notifyFollowers(this.mention, activity) From 5b4e28e0043c7f8d13c20f3f6083d44344f76290 Mon Sep 17 00:00:00 2001 From: Nulo Date: Wed, 13 Mar 2024 17:25:24 -0300 Subject: [PATCH 39/52] fix: unused --- src/server/announcements.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/server/announcements.ts b/src/server/announcements.ts index d0c869c..07461e3 100644 --- a/src/server/announcements.ts +++ b/src/server/announcements.ts @@ -1,5 +1,4 @@ import { nanoid } from 'nanoid' -import { ActorInfo } from '../schemas' import ActivityPubSystem, { DEFAULT_PUBLIC_KEY_FIELD } from './apsystem' import { generateKeypair } from 'http-signed-fetch' import { APOrderedCollection } from 'activitypub-types' From fdf37065265dda4e3136625b6af0945386af522f Mon Sep 17 00:00:00 2001 From: Mauve Signweaver Date: Wed, 13 Mar 2024 16:37:58 -0400 Subject: [PATCH 40/52] Fix ts error in announcement test --- src/server/announcements.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/announcements.test.ts b/src/server/announcements.test.ts index 2aaab94..b14d441 100644 --- a/src/server/announcements.test.ts +++ b/src/server/announcements.test.ts @@ -38,7 +38,7 @@ test('actor gets announced on .announce', async t => { keypair, publicKeyId: keypair.publicKeyId } - await aps.announcements.announce(fakeActor, actorInfo) + await aps.announcements.announce(fakeActor) await aps.store.forActor(fakeActor).setInfo(actorInfo) const outbox = await aps.store.forActor('@announcements@localhost').outbox.list() From debdd543349e195dadd35a2412b61e81262dfb89 Mon Sep 17 00:00:00 2001 From: Mauve Signweaver Date: Wed, 13 Mar 2024 16:56:57 -0400 Subject: [PATCH 41/52] Implement ActorStore.delete() --- src/server/store/ActorStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/store/ActorStore.ts b/src/server/store/ActorStore.ts index 4c6520b..39d2e3b 100644 --- a/src/server/store/ActorStore.ts +++ b/src/server/store/ActorStore.ts @@ -48,6 +48,6 @@ export class ActorStore { } async delete (): Promise { - // TODO: delete all keys within the db + await this.db.clear() } } From 056279eb6dec2111f4df2e731f69b207d3c0d21a Mon Sep 17 00:00:00 2001 From: Mauve Signweaver Date: Wed, 13 Mar 2024 17:27:06 -0400 Subject: [PATCH 42/52] Store note separate, use mention syntax to tag new account --- src/server/announcements.ts | 40 +++++++++++++++++++-------------- src/server/api/announcements.ts | 2 +- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/server/announcements.ts b/src/server/announcements.ts index 07461e3..11871ee 100644 --- a/src/server/announcements.ts +++ b/src/server/announcements.ts @@ -26,13 +26,13 @@ export class Announcements { return `@announcements@${url.hostname}` } - getActor (): ActorStore { + get store (): ActorStore { return this.apsystem.store.forActor(this.mention) } async init (): Promise { const actorUrl = this.actorUrl - const actor = this.getActor() + const actor = this.store try { const prev = await actor.getInfo() @@ -57,9 +57,25 @@ export class Announcements { } async announce (actor: string): Promise { + const actorUrl = await this.apsystem.mentionToActor(actor) const published = new Date().toUTCString() const to = ['https://www.w3.org/ns/activitystreams#Public'] - const cc = [`${this.actorUrl}followers`] + const cc = [`${this.actorUrl}followers`, actorUrl] + + const mentionText = `${actor}` + + const note = { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Note', + id: `${this.outboxUrl}/${nanoid()}`, + attributedTo: this.actorUrl, + published, + to, + cc, + // TODO: add a template in config + content: `A wild ${mentionText} appears!`, + tag: [{ type: 'Mention', href: actorUrl, name: actor }] + } const activity = { '@context': 'https://www.w3.org/ns/activitystreams', type: 'Create', @@ -68,25 +84,15 @@ export class Announcements { published, to, cc, - object: { - '@context': 'https://www.w3.org/ns/activitystreams', - type: 'Note', - id: `${this.outboxUrl}/${nanoid()}/note`, - actor: this.actorUrl, - attributedTo: this.actorUrl, - published, - to, - cc, - // TODO: add a template in config - content: `a wild site appears! ${actor}` - } + object: note } - await this.getActor().outbox.add(activity) + await this.store.outbox.add(activity) + await this.store.outbox.add(note) await this.apsystem.notifyFollowers(this.mention, activity) } async getOutbox (): Promise { - const actor = this.getActor() + const actor = this.store const activities = await actor.outbox.list() const orderedItems = activities // XXX: maybe `new Date()` doesn't correctly parse possible dates? diff --git a/src/server/api/announcements.ts b/src/server/api/announcements.ts index b794622..6c3ab07 100644 --- a/src/server/api/announcements.ts +++ b/src/server/api/announcements.ts @@ -42,7 +42,7 @@ export const announcementsRoutes = (cfg: APIConfig, store: Store, apsystem: Acti type: 'Person', name: 'Announcements', summary: `Announcements for ${new URL(cfg.publicURL).hostname}`, - preferredUsername: 'Announcements', + preferredUsername: 'announcements', following: `${actorInfo.actorUrl}following`, followers: `${actorInfo.actorUrl}followers`, inbox: `${actorInfo.actorUrl}inbox`, From 87de7aaddf20115502948419d9221f1cea35826f Mon Sep 17 00:00:00 2001 From: Mauve Signweaver Date: Wed, 13 Mar 2024 17:38:41 -0400 Subject: [PATCH 43/52] Fix linting, move actor definition into announcements --- src/server/announcements.ts | 37 ++++++++++++++++++++++++++++++++- src/server/api/announcements.ts | 32 ++++------------------------ 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/src/server/announcements.ts b/src/server/announcements.ts index 11871ee..2d146eb 100644 --- a/src/server/announcements.ts +++ b/src/server/announcements.ts @@ -1,9 +1,17 @@ import { nanoid } from 'nanoid' import ActivityPubSystem, { DEFAULT_PUBLIC_KEY_FIELD } from './apsystem' import { generateKeypair } from 'http-signed-fetch' -import { APOrderedCollection } from 'activitypub-types' +import { APOrderedCollection, APActor } from 'activitypub-types' import { ActorStore } from './store/ActorStore' +type APActorNonStandard = APActor & { + publicKey: { + id: string + owner: string + publicKeyPem: string + } +} + export class Announcements { apsystem: ActivityPubSystem publicURL: string @@ -30,6 +38,33 @@ export class Announcements { return this.apsystem.store.forActor(this.mention) } + async getActor (): Promise { + const actorInfo = await this.store.getInfo() + return { + '@context': [ + // TODO: I copied this from Mastodon, is this correct? + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1' + ], + // https://www.w3.org/TR/activitystreams-vocabulary/#actor-types + id: this.actorUrl, + type: 'Person', + name: 'Announcements', + summary: `Announcements for ${new URL(this.actorUrl).hostname}`, + preferredUsername: 'announcements', + following: `${actorInfo.actorUrl}following`, + followers: `${actorInfo.actorUrl}followers`, + inbox: `${actorInfo.actorUrl}inbox`, + outbox: `${actorInfo.actorUrl}outbox`, + publicKey: { + id: `${actorInfo.actorUrl}#main-key`, + + owner: actorInfo.actorUrl, + publicKeyPem: actorInfo.keypair.publicKeyPem + } + } + } + async init (): Promise { const actorUrl = this.actorUrl const actor = this.store diff --git a/src/server/api/announcements.ts b/src/server/api/announcements.ts index 6c3ab07..9b27658 100644 --- a/src/server/api/announcements.ts +++ b/src/server/api/announcements.ts @@ -28,32 +28,9 @@ export const announcementsRoutes = (cfg: APIConfig, store: Store, apsystem: Acti tags: ['ActivityPub'] } }, async (request, reply) => { - const actor = apsystem.announcements.getActor() - const actorInfo = await actor.getInfo() + const actor = await apsystem.announcements.getActor() - return await reply.send({ - '@context': [ - // TODO: I copied this from Mastodon, is this correct? - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1' - ], - // https://www.w3.org/TR/activitystreams-vocabulary/#actor-types - id: apsystem.announcements.actorUrl, - type: 'Person', - name: 'Announcements', - summary: `Announcements for ${new URL(cfg.publicURL).hostname}`, - preferredUsername: 'announcements', - following: `${actorInfo.actorUrl}following`, - followers: `${actorInfo.actorUrl}followers`, - inbox: `${actorInfo.actorUrl}inbox`, - outbox: `${actorInfo.actorUrl}outbox`, - publicKey: { - id: `${actorInfo.actorUrl}#main-key`, - - owner: actorInfo.actorUrl, - publicKeyPem: actorInfo.keypair.publicKeyPem - } - }) + return await reply.send(actor) }) server.get<{ @@ -94,9 +71,8 @@ export const announcementsRoutes = (cfg: APIConfig, store: Store, apsystem: Acti tags: ['ActivityPub'] } }, async (request, reply) => { - const actor = apsystem.announcements.getActor() - const actorInfo = await actor.getInfo() - const activity = await actor.outbox.get(`${actorInfo.actorUrl}outbox/${request.params.id}`) + const actorUrl = apsystem.announcements.actorUrl + const activity = await apsystem.announcements.store.outbox.get(`${actorUrl}outbox/${request.params.id}`) return await reply.send(activity) }) } From e0cfec27f060c4f36997003d45eb5aedadc16553 Mon Sep 17 00:00:00 2001 From: Mauve Signweaver Date: Wed, 13 Mar 2024 17:41:32 -0400 Subject: [PATCH 44/52] Fix linting, move actor definition into announcements --- src/server/announcements.ts | 2 +- src/server/api/announcements.ts | 11 +---------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/server/announcements.ts b/src/server/announcements.ts index 2d146eb..604345c 100644 --- a/src/server/announcements.ts +++ b/src/server/announcements.ts @@ -4,7 +4,7 @@ import { generateKeypair } from 'http-signed-fetch' import { APOrderedCollection, APActor } from 'activitypub-types' import { ActorStore } from './store/ActorStore' -type APActorNonStandard = APActor & { +export type APActorNonStandard = APActor & { publicKey: { id: string owner: string diff --git a/src/server/api/announcements.ts b/src/server/api/announcements.ts index 9b27658..b020ab1 100644 --- a/src/server/api/announcements.ts +++ b/src/server/api/announcements.ts @@ -1,18 +1,9 @@ -import { APActor } from 'activitypub-types' - import type { APIConfig, FastifyTypebox } from '.' import Store from '../store' import type ActivityPubSystem from '../apsystem' +import type { APActorNonStandard } from '../announcements' import { Type } from '@sinclair/typebox' -type APActorNonStandard = APActor & { - publicKey: { - id: string - owner: string - publicKeyPem: string - } -} - export const announcementsRoutes = (cfg: APIConfig, store: Store, apsystem: ActivityPubSystem) => async (server: FastifyTypebox): Promise => { server.get<{ Reply: APActorNonStandard From 1e229035467e940224e624d6445f72ef25d4039d Mon Sep 17 00:00:00 2001 From: Mauve Signweaver Date: Wed, 13 Mar 2024 17:50:13 -0400 Subject: [PATCH 45/52] progress on fixing announcement tests --- src/server/announcements.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/server/announcements.test.ts b/src/server/announcements.test.ts index b14d441..d120ab5 100644 --- a/src/server/announcements.test.ts +++ b/src/server/announcements.test.ts @@ -21,6 +21,8 @@ test.beforeEach(async () => { // Restore stubs before setting them up again sinon.restore() + sinon.stub(aps, 'mentionToActor').returns(Promise.resolve('http://actor.url')) + await aps.announcements.init() }) From 98163749fcedc0792cb3108c2904bcf84bdc1b78 Mon Sep 17 00:00:00 2001 From: Nulo Date: Fri, 15 Mar 2024 10:58:52 -0300 Subject: [PATCH 46/52] chore: move APActorNonStandard into schemas.ts --- src/schemas.ts | 9 +++++++++ src/server/announcements.ts | 11 ++--------- src/server/api/announcements.ts | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/schemas.ts b/src/schemas.ts index 0b9193e..e79d8a8 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -1,5 +1,6 @@ import { Type, Static } from '@sinclair/typebox' import { KeyPairSchema } from './keypair.js' +import { APActor } from 'activitypub-types' export const ActorInfoSchema = Type.Object({ // The actor for the domain inbox @@ -10,3 +11,11 @@ export const ActorInfoSchema = Type.Object({ }) export type ActorInfo = Static + +export type APActorNonStandard = APActor & { + publicKey: { + id: string + owner: string + publicKeyPem: string + } +} diff --git a/src/server/announcements.ts b/src/server/announcements.ts index 604345c..75fe04c 100644 --- a/src/server/announcements.ts +++ b/src/server/announcements.ts @@ -1,16 +1,9 @@ import { nanoid } from 'nanoid' import ActivityPubSystem, { DEFAULT_PUBLIC_KEY_FIELD } from './apsystem' import { generateKeypair } from 'http-signed-fetch' -import { APOrderedCollection, APActor } from 'activitypub-types' +import { APOrderedCollection } from 'activitypub-types' import { ActorStore } from './store/ActorStore' - -export type APActorNonStandard = APActor & { - publicKey: { - id: string - owner: string - publicKeyPem: string - } -} +import { APActorNonStandard } from '../schemas' export class Announcements { apsystem: ActivityPubSystem diff --git a/src/server/api/announcements.ts b/src/server/api/announcements.ts index b020ab1..4391620 100644 --- a/src/server/api/announcements.ts +++ b/src/server/api/announcements.ts @@ -1,7 +1,7 @@ import type { APIConfig, FastifyTypebox } from '.' import Store from '../store' import type ActivityPubSystem from '../apsystem' -import type { APActorNonStandard } from '../announcements' +import type { APActorNonStandard } from '../../schemas' import { Type } from '@sinclair/typebox' export const announcementsRoutes = (cfg: APIConfig, store: Store, apsystem: ActivityPubSystem) => async (server: FastifyTypebox): Promise => { From 600f0742cb118c64b661f187c3dcb95b042925a3 Mon Sep 17 00:00:00 2001 From: Nulo Date: Fri, 15 Mar 2024 21:30:00 -0300 Subject: [PATCH 47/52] fix: do not include Notes in outbox --- src/server/announcements.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/server/announcements.ts b/src/server/announcements.ts index 75fe04c..3430496 100644 --- a/src/server/announcements.ts +++ b/src/server/announcements.ts @@ -126,6 +126,7 @@ export class Announcements { // XXX: maybe `new Date()` doesn't correctly parse possible dates? .map(a => ({ ...a, published: typeof a.published === 'string' ? new Date(a.published) : a.published })) .sort((a, b) => +(b.published ?? 0) - +(a.published ?? 0)) + .filter(a => a.type !== 'Note') .map(a => a.id) .filter((id): id is string => id !== undefined) From fb1fb77b88cf4ddebbb4186965e14f633d6713cd Mon Sep 17 00:00:00 2001 From: Mauve Signweaver Date: Wed, 20 Mar 2024 00:47:37 -0400 Subject: [PATCH 48/52] fix announcements tests --- src/server/announcements.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/announcements.test.ts b/src/server/announcements.test.ts index d120ab5..140885f 100644 --- a/src/server/announcements.test.ts +++ b/src/server/announcements.test.ts @@ -43,6 +43,6 @@ test('actor gets announced on .announce', async t => { await aps.announcements.announce(fakeActor) await aps.store.forActor(fakeActor).setInfo(actorInfo) - const outbox = await aps.store.forActor('@announcements@localhost').outbox.list() - t.is(outbox.length, 1, 'something is in outbox') + const outbox = await aps.announcements.getOutbox() + t.is(outbox.totalItems, 1, 'something is in outbox') }) From 13a632b8edbc829c17f8ae298543db094d1d6687 Mon Sep 17 00:00:00 2001 From: Mauve Signweaver Date: Wed, 20 Mar 2024 01:44:10 -0400 Subject: [PATCH 49/52] Formalize auto-accepting follow requests --- src/schemas.ts | 3 ++- src/server/announcements.ts | 8 ++++---- src/server/apsystem.ts | 6 +++++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/schemas.ts b/src/schemas.ts index e79d8a8..a438efe 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -7,7 +7,8 @@ export const ActorInfoSchema = Type.Object({ actorUrl: Type.String(), publicKeyId: Type.String(), keypair: KeyPairSchema, - announce: Type.Boolean({ default: false }) + announce: Type.Optional(Type.Boolean({ default: false })), + manuallyApprovesFollowers: Type.Optional(Type.Boolean({ default: false })) }) export type ActorInfo = Static diff --git a/src/server/announcements.ts b/src/server/announcements.ts index 3430496..1dd0cdc 100644 --- a/src/server/announcements.ts +++ b/src/server/announcements.ts @@ -51,7 +51,6 @@ export class Announcements { outbox: `${actorInfo.actorUrl}outbox`, publicKey: { id: `${actorInfo.actorUrl}#main-key`, - owner: actorInfo.actorUrl, publicKeyPem: actorInfo.keypair.publicKeyPem } @@ -79,7 +78,8 @@ export class Announcements { privateKeyPem, publicKeyPem }, - announce: false + announce: false, + manuallyApprovesFollowers: false }) } } @@ -123,10 +123,10 @@ export class Announcements { const actor = this.store const activities = await actor.outbox.list() const orderedItems = activities - // XXX: maybe `new Date()` doesn't correctly parse possible dates? + .filter(a => a.type !== 'Note') + // XXX: maybe `new Date()` doesn't correctly parse possible dates? .map(a => ({ ...a, published: typeof a.published === 'string' ? new Date(a.published) : a.published })) .sort((a, b) => +(b.published ?? 0) - +(a.published ?? 0)) - .filter(a => a.type !== 'Note') .map(a => a.id) .filter((id): id is string => id !== undefined) diff --git a/src/server/apsystem.ts b/src/server/apsystem.ts index 5eb45e3..d4f90c5 100644 --- a/src/server/apsystem.ts +++ b/src/server/apsystem.ts @@ -342,9 +342,13 @@ export default class ActivityPubSystem { const actorStore = this.store.forActor(fromActor) + const { manuallyApprovesFollowers } = await actorStore.getInfo() + await actorStore.inbox.add(activity) - if (activityType === 'Undo') { + if (activityType === 'Follow' && (manuallyApprovesFollowers !== true)) { + await this.approveActivity(fromActor, activityId) + } else if (activityType === 'Undo') { await this.performUndo(fromActor, activity) } else if (moderationState === BLOCKED) { // TODO: Notify of blocks? From 2f97d573a170a5e8e40bec786dbafdad1f819a2e Mon Sep 17 00:00:00 2001 From: Mauve Signweaver Date: Wed, 20 Mar 2024 01:50:56 -0400 Subject: [PATCH 50/52] Better ts lint friendly auto approve check --- src/server/apsystem.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/server/apsystem.ts b/src/server/apsystem.ts index d4f90c5..78f8f47 100644 --- a/src/server/apsystem.ts +++ b/src/server/apsystem.ts @@ -344,9 +344,11 @@ export default class ActivityPubSystem { const { manuallyApprovesFollowers } = await actorStore.getInfo() + const autoApproveFollow = manuallyApprovesFollowers !== undefined && manuallyApprovesFollowers + await actorStore.inbox.add(activity) - if (activityType === 'Follow' && (manuallyApprovesFollowers !== true)) { + if (activityType === 'Follow' && autoApproveFollow) { await this.approveActivity(fromActor, activityId) } else if (activityType === 'Undo') { await this.performUndo(fromActor, activity) From a61a483123e433d323b7b08327480a3a34554695 Mon Sep 17 00:00:00 2001 From: Mauve Signweaver Date: Wed, 20 Mar 2024 02:00:51 -0400 Subject: [PATCH 51/52] Better ts lint friendly announce check --- src/server/api/creation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/api/creation.ts b/src/server/api/creation.ts index 5d5dde2..c4d0b2c 100644 --- a/src/server/api/creation.ts +++ b/src/server/api/creation.ts @@ -45,7 +45,7 @@ export const creationRoutes = (cfg: APIConfig, store: Store, apsystem: ActivityP const info = request.body await store.forActor(actor).setInfo(info) - if (info.announce && !existedAlready) await apsystem.announcements.announce(actor) + if (info.announce == true && !existedAlready) await apsystem.announcements.announce(actor) return await reply.send(info) }) From 8a5af82316b9766bee57559294222f860a7006f8 Mon Sep 17 00:00:00 2001 From: Mauve Signweaver Date: Wed, 20 Mar 2024 02:03:36 -0400 Subject: [PATCH 52/52] Better ts lint friendly announce check --- src/server/api/creation.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/server/api/creation.ts b/src/server/api/creation.ts index c4d0b2c..12cedb5 100644 --- a/src/server/api/creation.ts +++ b/src/server/api/creation.ts @@ -45,7 +45,9 @@ export const creationRoutes = (cfg: APIConfig, store: Store, apsystem: ActivityP const info = request.body await store.forActor(actor).setInfo(info) - if (info.announce == true && !existedAlready) await apsystem.announcements.announce(actor) + + const shouldAnnounce = !existedAlready && info.announce === true + if (shouldAnnounce) await apsystem.announcements.announce(actor) return await reply.send(info) })