Skip to content

Commit

Permalink
Added handling for Group announced Create activities
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mike182uk committed Nov 26, 2024
1 parent f068f0d commit 42079f2
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 7 deletions.
13 changes: 13 additions & 0 deletions features/handle-group-announce.feature
Original file line number Diff line number Diff line change
@@ -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
81 changes: 75 additions & 6 deletions features/step_definitions/stepdefs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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': [
Expand All @@ -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',
Expand Down Expand Up @@ -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}`,
Expand Down Expand Up @@ -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 = {}) {
Expand Down Expand Up @@ -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),
},
);
});
34 changes: 33 additions & 1 deletion src/dispatchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
type Context,
Create,
Follow,
Group,
Like,
Note,
Person,
Expand Down Expand Up @@ -136,14 +137,33 @@ export async function handleAccept(ctx: Context<ContextData>, accept: Accept) {

export async function handleCreate(ctx: Context<ContextData>, 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;
Expand Down Expand Up @@ -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');
Expand Down

0 comments on commit 42079f2

Please sign in to comment.