diff --git a/backend/src/activitypub/actors/index.ts b/backend/src/activitypub/actors/index.ts index 1043267..5166f09 100644 --- a/backend/src/activitypub/actors/index.ts +++ b/backend/src/activitypub/actors/index.ts @@ -1,6 +1,7 @@ import { defaultImages } from 'wildebeest/config/accounts' import { generateUserKey } from 'wildebeest/backend/src/utils/key-ops' import { type APObject, sanitizeContent, sanitizeName } from '../objects' +import { addPeer } from 'wildebeest/backend/src/activitypub/peers' const PERSON = 'Person' const isTesting = typeof jest !== 'undefined' @@ -112,6 +113,12 @@ export async function getAndCache(url: URL, db: D1Database): Promise { if (!success) { throw new Error('SQL error: ' + error) } + + // Add peer + { + const domain = actor.id.host + await addPeer(db, domain) + } return actor } diff --git a/backend/src/activitypub/objects/index.ts b/backend/src/activitypub/objects/index.ts index 4cf6511..316eff4 100644 --- a/backend/src/activitypub/objects/index.ts +++ b/backend/src/activitypub/objects/index.ts @@ -1,4 +1,5 @@ import type { UUID } from 'wildebeest/backend/src/types' +import { addPeer } from 'wildebeest/backend/src/activitypub/peers' export const originalActorIdSymbol = Symbol() export const originalObjectIdSymbol = Symbol() @@ -118,6 +119,12 @@ export async function cacheObject( ) .first() + // Add peer + { + const domain = originalObjectId.host + await addPeer(db, domain) + } + { const properties = JSON.parse(row.properties) const object = { diff --git a/backend/src/activitypub/peers.ts b/backend/src/activitypub/peers.ts new file mode 100644 index 0000000..44c6433 --- /dev/null +++ b/backend/src/activitypub/peers.ts @@ -0,0 +1,20 @@ +import { getResultsField } from 'wildebeest/backend/src/mastodon/utils' + +export async function getPeers(db: D1Database): Promise> { + const query = `SELECT domain FROM peers ` + const statement = db.prepare(query) + + return getResultsField(statement, 'domain') +} + +export async function addPeer(db: D1Database, domain: string): Promise { + const query = ` + INSERT OR IGNORE INTO peers (domain) + VALUES (?) + ` + + const out = await db.prepare(query).bind(domain).run() + if (!out.success) { + throw new Error('SQL error: ' + out.error) + } +} diff --git a/backend/test/activitypub.spec.ts b/backend/test/activitypub.spec.ts index 28cba26..584a796 100644 --- a/backend/test/activitypub.spec.ts +++ b/backend/test/activitypub.spec.ts @@ -2,6 +2,7 @@ import { makeDB, isUrlValid } from './utils' import { MessageType } from 'wildebeest/backend/src/types/queue' import type { JWK } from 'wildebeest/backend/src/webpush/jwk' import { createPerson } from 'wildebeest/backend/src/activitypub/actors' +import * as actors from 'wildebeest/backend/src/activitypub/actors' import { createPrivateNote, createPublicNote } from 'wildebeest/backend/src/activitypub/objects/note' import { addObjectInOutbox } from 'wildebeest/backend/src/activitypub/actors/outbox' import { strict as assert } from 'node:assert/strict' @@ -128,6 +129,38 @@ describe('ActivityPub', () => { }) }) + describe('Actors', () => { + test('getAndCache adds peer', async () => { + const actorId = new URL('https://example.com/user/foo') + + globalThis.fetch = async (input: RequestInfo) => { + if (input.toString() === actorId.toString()) { + return new Response( + JSON.stringify({ + id: actorId, + type: 'Person', + preferredUsername: 'sven', + name: 'sven ssss', + + icon: { url: 'icon.jpg' }, + image: { url: 'image.jpg' }, + }) + ) + } + + throw new Error(`unexpected request to "${input}"`) + } + + const db = await makeDB() + + await actors.getAndCache(actorId, db) + + const { results } = (await db.prepare('SELECT domain from peers').all()) as any + assert.equal(results.length, 1) + assert.equal(results[0].domain, 'example.com') + }) + }) + describe('Objects', () => { test('cacheObject deduplicates object', async () => { const db = await makeDB() @@ -159,6 +192,19 @@ describe('ActivityPub', () => { assert.equal(result.count, 1) }) + test('cacheObject adds peer', async () => { + const db = await makeDB() + const properties = { type: 'Note', a: 1, b: 2 } + const actor = await createPerson(domain, db, userKEK, 'a@cloudflare.com') + const originalObjectId = new URL('https://example.com/object1') + + await cacheObject(domain, db, properties, actor.id, originalObjectId, false) + + const { results } = (await db.prepare('SELECT domain from peers').all()) as any + assert.equal(results.length, 1) + assert.equal(results[0].domain, 'example.com') + }) + test('serve unknown object', async () => { const db = await makeDB() const res = await ap_objects.handleRequest(domain, db, 'unknown id') diff --git a/backend/test/mastodon/instance.spec.ts b/backend/test/mastodon/instance.spec.ts new file mode 100644 index 0000000..be13158 --- /dev/null +++ b/backend/test/mastodon/instance.spec.ts @@ -0,0 +1,22 @@ +import { addPeer } from 'wildebeest/backend/src/activitypub/peers' +import { strict as assert } from 'node:assert/strict' +import * as peers from 'wildebeest/functions/api/v1/instance/peers' +import { makeDB } from '../utils' + +describe('Mastodon APIs', () => { + describe('instance', () => { + test('returns peers', async () => { + const db = await makeDB() + await addPeer(db, 'a') + await addPeer(db, 'b') + + const res = await peers.handleRequest(db) + assert.equal(res.status, 200) + + const data = await res.json>() + assert.equal(data.length, 2) + assert.equal(data[0], 'a') + assert.equal(data[1], 'b') + }) + }) +}) diff --git a/functions/api/v1/instance/peers.ts b/functions/api/v1/instance/peers.ts new file mode 100644 index 0000000..dde8c11 --- /dev/null +++ b/functions/api/v1/instance/peers.ts @@ -0,0 +1,16 @@ +import { cors } from 'wildebeest/backend/src/utils/cors' +import type { Env } from 'wildebeest/backend/src/types/env' +import { getPeers } from 'wildebeest/backend/src/activitypub/peers' + +export const onRequest: PagesFunction = async ({ env }) => { + return handleRequest(env.DATABASE) +} + +export async function handleRequest(db: D1Database): Promise { + const headers = { + ...cors(), + 'content-type': 'application/json; charset=utf-8', + } + const peers = await getPeers(db) + return new Response(JSON.stringify(peers), { headers }) +} diff --git a/migrations/0003_add_peers.sql b/migrations/0003_add_peers.sql new file mode 100644 index 0000000..61c5298 --- /dev/null +++ b/migrations/0003_add_peers.sql @@ -0,0 +1,5 @@ +-- Migration number: 0003 2023-02-02T15:03:27.478Z + +CREATE TABLE IF NOT EXISTS peers ( + domain TEXT NOT NULL +);