From 42079f269245343c086564d16418a0c65e12e9dd Mon Sep 17 00:00:00 2001 From: Michael Barrett Date: Tue, 26 Nov 2024 13:21:46 +0000 Subject: [PATCH] Added handling for `Group` announced `Create` activities refs [AP-608](https://linear.app/ghost/issue/AP-608/ghost-does-not-work-with-lemmy-or-wordpress) Added handling for `Group` announced `Create` activities so that these activities are recorded processed by the service. Activities are usually only announced as part of Group federation See https://codeberg.org/fediverse/fep/src/branch/main/fep/1b12/fep-1b12.md for more details --- features/handle-group-announce.feature | 13 +++++ features/step_definitions/stepdefs.js | 81 ++++++++++++++++++++++++-- src/dispatchers.ts | 34 ++++++++++- 3 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 features/handle-group-announce.feature diff --git a/features/handle-group-announce.feature b/features/handle-group-announce.feature new file mode 100644 index 00000000..b72f7e1d --- /dev/null +++ b/features/handle-group-announce.feature @@ -0,0 +1,13 @@ +Feature: Handling activities announced by a Group + + Scenario: We recieve a Create(Article) activity announced from a Group + Given an Actor "Alice" + And a Group "Wonderland" + And "Alice" is a member of "Wonderland" + And we follow "Wonderland" + And the request is accepted + And a "Accept(Follow(Wonderland))" Activity "Accept" by "Wonderland" + And "Wonderland" sends "Accept" to the Inbox + When a "Create(Article)" Activity "Create" by "Alice" + And "Wonderland" announces "Create" + Then "Create" is in our Inbox diff --git a/features/step_definitions/stepdefs.js b/features/step_definitions/stepdefs.js index 772fd527..1534e7f8 100644 --- a/features/step_definitions/stepdefs.js +++ b/features/step_definitions/stepdefs.js @@ -93,7 +93,13 @@ async function createActivity(activityType, object, actor, remote = true) { } } -async function createActor(name = 'Test', remote = true) { +const ACTOR_TYPE_PERSON = 'Person'; +const ACTOR_TYPE_GROUP = 'Group'; + +async function createActor( + name, + { remote = true, type = ACTOR_TYPE_PERSON } = {}, +) { if (remote === false) { return { '@context': [ @@ -102,12 +108,12 @@ async function createActor(name = 'Test', remote = true) { ], id: 'http://fake-ghost-activitypub/.ghost/activitypub/users/index', url: 'http://fake-ghost-activitypub/.ghost/activitypub/users/index', - type: 'Person', + type, handle: '@index@fake-ghost-activitypub', preferredUsername: 'index', - name: 'Test Actor', + name, summary: 'A test actor for testing', inbox: 'http://fake-ghost-activitypub/.ghost/activitypub/inbox/index', @@ -136,12 +142,12 @@ async function createActor(name = 'Test', remote = true) { ], id: `http://fake-external-activitypub/user/${name}`, url: `http://fake-external-activitypub/user/${name}`, - type: 'Person', + type, handle: `@${name}@fake-external-activitypub`, preferredUsername: name, - name: name, + name, summary: 'A test actor for testing', inbox: `http://fake-external-activitypub/inbox/${name}`, @@ -398,9 +404,12 @@ Before(async function () { } if (!this.actors) { this.actors = { - Us: await createActor('Test', false), + Us: await createActor('Test', { remote: false }), }; } + if (!this.groups) { + this.groups = {}; + } }); async function fetchActivityPub(url, options = {}) { @@ -1129,3 +1138,63 @@ Then('{string} has the content {string}', function (activityName, content) { assert(activity.object.content === content); }); + +Given('a Group {string}', async function (name) { + this.groups[name] = []; + this.actors[name] = await createActor(name, { + type: ACTOR_TYPE_GROUP, + }); +}); + +Given('{string} is a member of {string}', function (actorName, groupName) { + this.groups[groupName].push(actorName); +}); + +When('{string} announces {string}', async function (actorName, activityName) { + const actor = this.actors[actorName]; + const activity = this.activities[activityName]; + + if (actor.type === ACTOR_TYPE_GROUP) { + const group = this.groups[actorName]; + + let isGroupMember = false; + + for (const groupActorName of group) { + const groupActor = this.actors[groupActorName]; + + if (groupActor.id === activity.actor.id) { + isGroupMember = true; + } + } + + if (isGroupMember === false) { + throw new Error( + `Expected activity actor [${activity.actor.name}] to be member of group [${actorName}]`, + ); + } + } + + const annouce = { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/data-integrity/v1', + ], + type: 'Announce', + id: `http://fake-external-activitypub/announce/${uuidv4()}`, + audience: actor.id, + to: 'as:Public', + object: { ...activity, audience: actor.id }, + actor: actor, + }; + + this.response = await fetchActivityPub( + 'http://fake-ghost-activitypub/.ghost/activitypub/inbox/index', + { + method: 'POST', + headers: { + 'Content-Type': 'application/ld+json', + }, + body: JSON.stringify(annouce), + }, + ); +}); diff --git a/src/dispatchers.ts b/src/dispatchers.ts index ac840714..7f48754b 100644 --- a/src/dispatchers.ts +++ b/src/dispatchers.ts @@ -8,6 +8,7 @@ import { type Context, Create, Follow, + Group, Like, Note, Person, @@ -136,14 +137,33 @@ export async function handleAccept(ctx: Context, accept: Accept) { export async function handleCreate(ctx: Context, create: Create) { ctx.data.logger.info('Handling Create'); + const parsed = ctx.parseUri(create.objectId); ctx.data.logger.info('Parsed create object', { parsed }); + if (!create.id) { ctx.data.logger.info('Create missing id - exit'); return; } - const sender = await create.getActor(ctx); + // Determine the sender of the activity - If the activity has an audience + // that is Group, use that as the sender, otherwise fallback to the actor + // See https://codeberg.org/fediverse/fep/src/branch/main/fep/1b12/fep-1b12.md + const audience = await create.getAudience(ctx); + const actor = await create.getActor(ctx); + + let sender: Actor | null = null; + + if (audience instanceof Group) { + ctx.data.logger.info( + 'Create has audience that is Group, using audience as sender', + ); + + sender = audience; + } else if (actor) { + sender = actor; + } + if (sender === null || sender.id === null) { ctx.data.logger.info('Create sender missing, exit early'); return; @@ -178,6 +198,18 @@ export async function handleAnnounce( ) { ctx.data.logger.info('Handling Announce'); + // Check what was announced - If it's an Activity (which can occur if the + // actor annoucing the object is a Group) we need to forward the activity + // to the appropriate listener and exit early + // See https://codeberg.org/fediverse/fep/src/branch/main/fep/1b12/fep-1b12.md + const announced = await announce.getObject(); + + if (announced instanceof Create) { + ctx.data.logger.info('Handling Announce as Create'); + + return handleCreate(ctx, announced); + } + // Validate announce if (!announce.id) { ctx.data.logger.info('Invalid Announce - no id');