diff --git a/backend/src/activitypub/activities/handle.ts b/backend/src/activitypub/activities/handle.ts index b350c21..8516397 100644 --- a/backend/src/activitypub/activities/handle.ts +++ b/backend/src/activitypub/activities/handle.ts @@ -4,6 +4,7 @@ import type { JWK } from 'wildebeest/backend/src/webpush/jwk' import { addObjectInOutbox } from 'wildebeest/backend/src/activitypub/actors/outbox' import { actorURL } from 'wildebeest/backend/src/activitypub/actors' import * as objects from 'wildebeest/backend/src/activitypub/objects' +import type { Actor } from 'wildebeest/backend/src/activitypub/actors' import * as accept from 'wildebeest/backend/src/activitypub/activities/accept' import { addObjectInInbox } from 'wildebeest/backend/src/activitypub/actors/inbox' import { @@ -31,6 +32,54 @@ function extractID(domain: string, s: string | URL): string { return s.toString().replace(`https://${domain}/ap/users/`, '') } +// Find the actor for this recipient +async function findActorFromReceipient(db: D1Database, domain: string, recipient: URL): Promise { + if (recipient.hostname !== domain) { + // Actor isn't in our instance + return null + } + try { + const handle = parseHandle(extractID(domain, recipient)) + + const actor = await actors.getActorById(db, actorURL(domain, handle.localPart)) + if (actor === null) { + console.warn(`local actor ${recipient} not found`) + return null + } + + return actor + } catch (err: any) { + console.warn('failed to parse handle: ' + recipient) + return null + } +} + +async function findActorsFromCollection(db: D1Database, domain: string, collection: URL): Promise> { + if (collection.hostname !== domain) { + console.warn('findActorsFromCollection not implemented yet for collection: ' + collection) + return [] + } + + // Try to resolve the collection assumin it's a followers collection + const query = ` + SELECT actor_id + FROM actor_following + WHERE target_actor_id=(SELECT id FROM actors WHERE json_extract(properties, '$.followers') = ?1) + ` + + const { results, success, error } = await db.prepare(query).bind(collection.toString()).all<{ actor_id: string }>() + console.log({ results }); + if (!success) { + throw new Error('SQL error: ' + error) + } + if (!results || results.length === 0) { + return [] + } + + // TODO: suboptimal implementation, resulting in many D1 queries for a large collection + return Promise.all(results.map(({ actor_id }) => actors.getActorById(db, new URL(actor_id)))) +} + export function makeGetObjectAsId(activity: Activity) { return () => { let url: any = null @@ -188,30 +237,28 @@ export async function handle( await addObjectInOutbox(db, fromActor, obj, activity.published, target) for (let i = 0, len = recipients.length; i < len; i++) { - const url = new URL(recipients[i]) - if (url.hostname !== domain) { - console.warn('recipients is not for this instance') - continue + const recipient = new URL(recipients[i]) + + // Recipient is a local actor + { + const actor = await findActorFromReceipient(db, domain, recipient) + + if (actor !== null) { + // FIXME: check if the actor is in the mentions/tags + const notifId = await createNotification(db, 'mention', actor, fromActor, obj) + await Promise.all([ + await addObjectInInbox(db, actor, obj), + await sendMentionNotification(db, fromActor, actor, notifId, adminEmail, vapidKeys), + ]) + continue + } } - const handle = parseHandle(extractID(domain, recipients[i])) - if (handle.domain !== null && handle.domain !== domain) { - console.warn('activity not for current instance') - continue + // Recipient is a collection + { + const actors = await findActorsFromCollection(db, domain, recipient) + console.log({ actors, recipient }) } - - const person = await actors.getActorById(db, actorURL(domain, handle.localPart)) - if (person === null) { - console.warn(`person ${recipients[i]} not found`) - continue - } - - // FIXME: check if the actor mentions the person - const notifId = await createNotification(db, 'mention', person, fromActor, obj) - await Promise.all([ - await addObjectInInbox(db, person, obj), - await sendMentionNotification(db, fromActor, person, notifId, adminEmail, vapidKeys), - ]) } break diff --git a/backend/test/activitypub/handle.spec.ts b/backend/test/activitypub/handle.spec.ts index 4032f37..c07c97e 100644 --- a/backend/test/activitypub/handle.spec.ts +++ b/backend/test/activitypub/handle.spec.ts @@ -3,11 +3,12 @@ import { createPublicNote } from 'wildebeest/backend/src/activitypub/objects/not import type { JWK } from 'wildebeest/backend/src/webpush/jwk' import { strict as assert } from 'node:assert/strict' import { cacheObject, getObjectById } from 'wildebeest/backend/src/activitypub/objects/' -import { addFollowing } from 'wildebeest/backend/src/mastodon/follow' +import { addFollowing, acceptFollowing } from 'wildebeest/backend/src/mastodon/follow' import * as activityHandler from 'wildebeest/backend/src/activitypub/activities/handle' import { createPerson } from 'wildebeest/backend/src/activitypub/actors' import { ObjectsRow } from 'wildebeest/backend/src/types/objects' import { originalObjectIdSymbol } from 'wildebeest/backend/src/activitypub/objects' +import { getHomeTimeline } from 'wildebeest/backend/src/mastodon/timeline' const adminEmail = 'admin@example.com' const domain = 'cloudflare.com' @@ -393,6 +394,44 @@ describe('ActivityPub', () => { ) assert.equal(name, 'Dr Evil') }) + + test.only('Note sent to follower collection adds to Actor timelines', async () => { + const db = await makeDB() + const remoteActor = "https://example.com/users/remote-actor" + const noteId = 'https://example.com/note/1' + + globalThis.fetch = async (input: RequestInfo) => { + throw new Error('unexpected request to ' + input) + } + + const actor2 = await createPerson(domain, db, userKEK, 'sven2@cloudflare.com') + + // actor2 follows remoteActor + await db + .prepare( + 'INSERT INTO actor_following (id, actor_id, target_actor_id, target_actor_acct) VALUES (?1, ?2, ?3, \'accepted\')' + ) + .bind( + 'id1', + actor2.id.toString(), + remoteActor, + 'not needed', + ) + .run() + + // actor send the notes to its followers + const activity = { + type: 'Create', + actor: remoteActor, + to: ['https://www.w3.org/ns/activitystreams#Public'], + cc: [remoteActor + '/followers'], + object: noteId, + } + await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys) + + const timeline = await getHomeTimeline(domain, db, actor2) + console.log({ timeline }); + }) }) describe('Update', () => {