diff --git a/backend/src/activitypub/activities/handle.ts b/backend/src/activitypub/activities/handle.ts index 8462492..5c2c0f9 100644 --- a/backend/src/activitypub/activities/handle.ts +++ b/backend/src/activitypub/activities/handle.ts @@ -25,6 +25,7 @@ import { createReblog } from 'wildebeest/backend/src/mastodon/reblog' import { insertReply } from 'wildebeest/backend/src/mastodon/reply' import type { Activity } from 'wildebeest/backend/src/activitypub/activities' import { originalActorIdSymbol } from 'wildebeest/backend/src/activitypub/objects' +import { hasReblog } from 'wildebeest/backend/src/mastodon/reblog' function extractID(domain: string, s: string | URL): string { return s.toString().replace(`https://${domain}/ap/users/`, '') @@ -289,6 +290,12 @@ export async function handle( const fromActor = await actors.getAndCache(actorId, db) + if (await hasReblog(db, fromActor, obj)) { + // A reblog already exists. To avoid dulicated reblog we ignore. + console.warn('probably duplicated Announce message') + break + } + // notify the user const targetActor = await actors.getActorById(db, new URL(obj[originalActorIdSymbol])) if (targetActor === null) { diff --git a/backend/src/mastodon/reblog.ts b/backend/src/mastodon/reblog.ts index 5427c7a..2170ad5 100644 --- a/backend/src/mastodon/reblog.ts +++ b/backend/src/mastodon/reblog.ts @@ -39,3 +39,12 @@ export function getReblogs(db: D1Database, obj: APObject): Promise return getResultsField(statement, 'actor_id') } + +export async function hasReblog(db: D1Database, actor: Actor, obj: APObject): Promise { + const query = ` + SELECT count(*) as count FROM actor_reblogs WHERE object_id=?1 AND actor_id=?2 + ` + + const { count } = await db.prepare(query).bind(obj.id.toString(), actor.id.toString()).first<{ count: number }>() + return count > 0 +} diff --git a/backend/test/activitypub/handle.spec.ts b/backend/test/activitypub/handle.spec.ts index 55288a3..c41c9c4 100644 --- a/backend/test/activitypub/handle.spec.ts +++ b/backend/test/activitypub/handle.spec.ts @@ -545,6 +545,53 @@ describe('ActivityPub', () => { assert(outbox_object) assert.equal(outbox_object.actor_id, remoteActorId) }) + + test('duplicated announce', async () => { + const remoteActorId = 'https://example.com/actor' + const objectId = 'https://example.com/some-object' + globalThis.fetch = async (input: RequestInfo) => { + if (input.toString() === remoteActorId) { + return new Response( + JSON.stringify({ + id: remoteActorId, + icon: { url: 'img.com' }, + type: 'Person', + }) + ) + } + + if (input.toString() === objectId) { + return new Response( + JSON.stringify({ + id: objectId, + type: 'Note', + content: 'foo', + }) + ) + } + + throw new Error('unexpected request to ' + input) + } + + const db = await makeDB() + await createPerson(domain, db, userKEK, 'sven@cloudflare.com') + + const activity: any = { + type: 'Announce', + actor: remoteActorId, + to: [], + cc: [], + object: objectId, + } + await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys) + + // Handle the same Activity + await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys) + + // Ensure only one reblog is kept + const { count } = await db.prepare('SELECT count(*) as count FROM outbox_objects').first<{ count: number }>() + assert.equal(count, 1) + }) }) }) })