MOW-75: handle inbox via a Queue

pull/97/head
Sven Sauleau 2023-01-12 13:01:58 +00:00
rodzic 699c2491f3
commit 7513a88c91
12 zmienionych plików z 775 dodań i 681 usunięć

Wyświetl plik

@ -181,6 +181,55 @@ jobs:
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
- name: retrieve Wildebeest cache KV namespace
uses: cloudflare/wrangler-action@2.0.0
with:
command: kv:namespace list | jq -r '.[] | select( .title == "wildebeest-${{ env.OWNER_LOWER }}-cache" ) | .id' | awk '{print "cache_kv="$1}' >> $GITHUB_ENV
apiToken: ${{ secrets.CF_API_TOKEN }}
preCommands: |
echo "*** pre commands ***"
apt-get update && apt-get -y install jq
echo "******"
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
- name: Publish consumer
uses: cloudflare/wrangler-action@2.0.0
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
preCommands: |
echo "*** pre commands ***"
echo -e "[[d1_databases]]\nbinding=\"DATABASE\"\ndatabase_name=\"wildebeest-${{ env.OWNER_LOWER }}\"\ndatabase_id=\"${{ env.d1_id }}\"\n" >> consumer/wrangler.toml
echo -e "[[kv_namespaces]]\n" >> consumer/wrangler.toml
echo -e "binding=\"KV_CACHE\"\n" >> consumer/wrangler.toml
echo -e "id=\"${{ env.cache_kv }}\"\n" >> consumer/wrangler.toml
echo -e "[vars]\n" >> consumer/wrangler.toml
echo -e "DOMAIN=\"${{ vars.CF_DEPLOY_DOMAIN }}\"\n" >> consumer/wrangler.toml
echo -e "ADMIN_EMAIL=\"${{ vars.ADMIN_EMAIL }}\"\n" >> consumer/wrangler.toml
echo "******"
command: publish --config consumer/wrangler.toml
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
- name: add Queue producer to Pages
run: |
curl https://api.cloudflare.com/client/v4/accounts/${{ secrets.CF_ACCOUNT_ID }}/pages/projects/wildebeest-${{ env.OWNER_LOWER }} \
-XPATCH \
-H 'Authorization: Bearer ${{ secrets.CF_API_TOKEN }}' \
-d ' {
"deployment_configs": {
"production": {
"queue_producers": {
"QUEUE": {
"name": "wildebeest"
}
}
}
}
}' > /dev/null
- name: Publish
uses: cloudflare/wrangler-action@2.0.0
with:
@ -191,7 +240,7 @@ jobs:
yarn build
cp -rv ./frontend/dist/* .
# remove folder that aren't needed in Pages before we upload
rm -rf ./tf ./scripts ./.github ./.npm
rm -rf ./tf ./scripts ./.github ./.npm ./consumer
echo "******"
command: pages publish --project-name=wildebeest-${{ env.OWNER_LOWER }} .
env:

Wyświetl plik

@ -2,6 +2,7 @@ export interface Env {
DATABASE: D1Database
KV_CACHE: KVNamespace
userKEK: string
QUEUE: Queue
CF_ACCOUNT_ID: string
CF_API_TOKEN: string

Wyświetl plik

@ -0,0 +1,15 @@
import type { Activity } from 'wildebeest/backend/src/activitypub/activities'
import type { JWK } from 'wildebeest/backend/src/webpush/jwk'
export type MessageType = 'activity'
export type MessageBody = {
type: MessageType
actorId: string
content: Activity
// Send secrets as part of the message because it's too complicated
// to bind them to the consumer worker.
userKEK: string
vapidKeys: JWK
}

Wyświetl plik

@ -1,22 +1,19 @@
import { makeDB, isUrlValid } from './utils'
import type { JWK } from 'wildebeest/backend/src/webpush/jwk'
import { addFollowing } from 'wildebeest/backend/src/mastodon/follow'
import { createPerson } from 'wildebeest/backend/src/activitypub/actors'
import * as activityHandler from 'wildebeest/backend/src/activitypub/activities/handle'
import { 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'
import { cacheObject } from 'wildebeest/backend/src/activitypub/objects/'
import * as ap_users from 'wildebeest/functions/ap/users/[id]'
import * as ap_outbox from 'wildebeest/functions/ap/users/[id]/outbox'
import * as ap_inbox from 'wildebeest/functions/ap/users/[id]/inbox'
import * as ap_outbox_page from 'wildebeest/functions/ap/users/[id]/outbox/page'
const userKEK = 'test_kek5'
const vapidKeys = {} as JWK
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
const vapidKeys = {} as JWK
const domain = 'cloudflare.com'
const adminEmail = 'admin@example.com'
describe('ActivityPub', () => {
test('fetch non-existant user by id', async () => {
@ -52,168 +49,6 @@ describe('ActivityPub', () => {
assert.equal(data.publicKey.publicKeyPem, pubKey)
})
describe('Accept', () => {
beforeEach(() => {
globalThis.fetch = async (input: RequestInfo) => {
throw new Error('unexpected request to ' + input)
}
})
test('Accept follow request stores in db', async () => {
const db = await makeDB()
const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const actor2 = await createPerson(domain, db, userKEK, 'sven2@cloudflare.com')
await addFollowing(db, actor, actor2, 'not needed')
const activity = {
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Accept',
actor: { id: 'https://' + domain + '/ap/users/sven2' },
object: {
type: 'Follow',
actor: actor.id,
object: 'https://' + domain + '/ap/users/sven2',
},
}
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
const row = await db
.prepare(`SELECT target_actor_id, state FROM actor_following WHERE actor_id=?`)
.bind(actor.id.toString())
.first()
assert(row)
assert.equal(row.target_actor_id, 'https://' + domain + '/ap/users/sven2')
assert.equal(row.state, 'accepted')
})
test('Object must be an object', async () => {
const db = await makeDB()
await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const activity = {
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Accept',
actor: 'https://example.com/actor',
object: 'a',
}
await assert.rejects(activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys), {
message: '`activity.object` must be of type object',
})
})
})
describe('Create', () => {
test('Object must be an object', async () => {
const db = await makeDB()
await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const activity = {
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Create',
actor: 'https://example.com/actor',
object: 'a',
}
await assert.rejects(activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys), {
message: '`activity.object` must be of type object',
})
})
})
describe('Update', () => {
test('Object must be an object', async () => {
const db = await makeDB()
const activity = {
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Update',
actor: 'https://example.com/actor',
object: 'a',
}
await assert.rejects(activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys), {
message: '`activity.object` must be of type object',
})
})
test('Object must exist', async () => {
const db = await makeDB()
const activity = {
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Update',
actor: 'https://example.com/actor',
object: {
id: 'https://example.com/note2',
type: 'Note',
content: 'test note',
},
}
await assert.rejects(activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys), {
message: 'object https://example.com/note2 does not exist',
})
})
test('Object must have the same origin', async () => {
const db = await makeDB()
const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const object = {
id: 'https://example.com/note2',
type: 'Note',
content: 'test note',
}
const obj = await cacheObject(domain, db, object, actor.id, new URL(object.id), false)
assert.notEqual(obj, null, 'could not create object')
const activity = {
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Update',
actor: 'https://example.com/actor',
object: object,
}
await assert.rejects(activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys), {
message: 'actorid mismatch when updating object',
})
})
test('Object is updated', async () => {
const db = await makeDB()
const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const object = {
id: 'https://example.com/note2',
type: 'Note',
content: 'test note',
}
const obj = await cacheObject(domain, db, object, actor.id, new URL(object.id), false)
assert.notEqual(obj, null, 'could not create object')
const newObject = {
id: 'https://example.com/note2',
type: 'Note',
content: 'new test note',
}
const activity = {
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Update',
actor: actor.id,
object: newObject,
}
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
const updatedObject = await db.prepare('SELECT * FROM objects WHERE original_object_id=?').bind(object.id).first()
assert(updatedObject)
assert.equal(JSON.parse(updatedObject.properties).content, newObject.content)
})
})
describe('Outbox', () => {
test('return outbox', async () => {
const db = await makeDB()
@ -249,60 +84,6 @@ describe('ActivityPub', () => {
})
})
describe('Announce', () => {
test('Announce objects are stored and added to the remote actors outbox', 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)
const object = await db.prepare('SELECT * FROM objects').bind(remoteActorId).first()
assert(object)
assert.equal(object.type, 'Note')
assert.equal(object.original_actor_id, remoteActorId)
const outbox_object = await db
.prepare('SELECT * FROM outbox_objects WHERE actor_id=?')
.bind(remoteActorId)
.first()
assert(outbox_object)
assert.equal(outbox_object.actor_id, remoteActorId)
})
})
describe('Objects', () => {
test('cacheObject deduplicates object', async () => {
const db = await makeDB()
@ -334,4 +115,42 @@ describe('ActivityPub', () => {
assert.equal(result.count, 1)
})
})
describe('Inbox', () => {
test('send Note to non existant user', async () => {
const db = await makeDB()
const queue = {
async send() {},
}
const activity: any = {}
const res = await ap_inbox.handleRequest(domain, db, 'sven', activity, queue, userKEK, vapidKeys)
assert.equal(res.status, 404)
})
test('send activity sends message in queue', async () => {
const db = await makeDB()
const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
let msg: any = null
const queue = {
async send(v: any) {
msg = v
},
}
const activity: any = {
type: 'some activity',
}
const res = await ap_inbox.handleRequest(domain, db, 'sven', activity, queue, userKEK, vapidKeys)
assert.equal(res.status, 200)
assert(msg)
assert.equal(msg.type, 'activity')
assert.equal(msg.actorId, actor.id.toString())
assert.equal(msg.content.type, 'some activity')
})
})
})

Wyświetl plik

@ -0,0 +1,468 @@
import { makeDB } from '../utils'
import { createPublicNote } from 'wildebeest/backend/src/activitypub/objects/note'
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 * as activityHandler from 'wildebeest/backend/src/activitypub/activities/handle'
import { createPerson } from 'wildebeest/backend/src/activitypub/actors'
const adminEmail = 'admin@example.com'
const domain = 'cloudflare.com'
const userKEK = 'test_kek15'
const vapidKeys = {} as JWK
describe('ActivityPub', () => {
describe('handle Activity', () => {
test('Note to inbox stores in DB', async () => {
const db = await makeDB()
const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const activity: any = {
type: 'Create',
actor: actor.id.toString(),
to: [actor.id.toString()],
cc: [],
object: {
id: 'https://example.com/note1',
type: 'Note',
content: 'test note',
},
}
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
const entry = await db
.prepare('SELECT objects.* FROM inbox_objects INNER JOIN objects ON objects.id=inbox_objects.object_id')
.first()
const properties = JSON.parse(entry.properties)
assert.equal(properties.content, 'test note')
})
test("Note adds in remote actor's outbox", async () => {
const remoteActorId = 'https://example.com/actor'
globalThis.fetch = async (input: RequestInfo) => {
if (input.toString() === remoteActorId) {
return new Response(
JSON.stringify({
id: remoteActorId,
type: 'Person',
})
)
}
throw new Error('unexpected request to ' + input)
}
const db = await makeDB()
await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const activity: any = {
type: 'Create',
actor: remoteActorId,
to: [],
cc: [],
object: {
id: 'https://example.com/note1',
type: 'Note',
content: 'test note',
},
}
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
const entry = await db.prepare('SELECT * FROM outbox_objects WHERE actor_id=?').bind(remoteActorId).first()
assert.equal(entry.actor_id, remoteActorId)
})
test('local actor sends Note with mention create notification', async () => {
const db = await makeDB()
const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com')
const activity: any = {
type: 'Create',
actor: actorB.id.toString(),
to: [actorA.id.toString()],
cc: [],
object: {
id: 'https://example.com/note2',
type: 'Note',
content: 'test note',
},
}
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
const entry = await db.prepare('SELECT * FROM actor_notifications').first()
assert(entry)
assert.equal(entry.type, 'mention')
assert.equal(entry.actor_id.toString(), actorA.id.toString())
assert.equal(entry.from_actor_id.toString(), actorB.id.toString())
})
test('Note records reply', async () => {
const db = await makeDB()
const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
{
const activity: any = {
type: 'Create',
actor: actor.id.toString(),
to: [actor.id.toString()],
object: {
id: 'https://example.com/note1',
type: 'Note',
content: 'post',
},
}
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
}
{
const activity: any = {
type: 'Create',
actor: actor.id.toString(),
to: [actor.id.toString()],
object: {
inReplyTo: 'https://example.com/note1',
id: 'https://example.com/note2',
type: 'Note',
content: 'reply',
},
}
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
}
const entry = await db.prepare('SELECT * FROM actor_replies').first()
assert.equal(entry.actor_id, actor.id.toString().toString())
const obj: any = await getObjectById(db, entry.object_id)
assert(obj)
assert.equal(obj.originalObjectId, 'https://example.com/note2')
const inReplyTo: any = await getObjectById(db, entry.in_reply_to_object_id)
assert(inReplyTo)
assert.equal(inReplyTo.originalObjectId, 'https://example.com/note1')
})
describe('Announce', () => {
test('records reblog in db', async () => {
const db = await makeDB()
const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com')
const note = await createPublicNote(domain, db, 'my first status', actorA)
const activity: any = {
type: 'Announce',
actor: actorB.id,
object: note.id,
}
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
const entry = await db.prepare('SELECT * FROM actor_reblogs').first()
assert.equal(entry.actor_id.toString(), actorB.id.toString())
assert.equal(entry.object_id.toString(), note.id.toString())
})
test('creates notification', async () => {
const db = await makeDB()
const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com')
const note = await createPublicNote(domain, db, 'my first status', actorA)
const activity: any = {
type: 'Announce',
actor: actorB.id,
object: note.id,
}
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
const entry = await db.prepare('SELECT * FROM actor_notifications').first()
assert(entry)
assert.equal(entry.type, 'reblog')
assert.equal(entry.actor_id.toString(), actorA.id.toString())
assert.equal(entry.from_actor_id.toString(), actorB.id.toString())
})
})
describe('Like', () => {
test('records like in db', async () => {
const db = await makeDB()
const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com')
const note = await createPublicNote(domain, db, 'my first status', actorA)
const activity: any = {
type: 'Like',
actor: actorB.id,
object: note.id,
}
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
const entry = await db.prepare('SELECT * FROM actor_favourites').first()
assert.equal(entry.actor_id.toString(), actorB.id.toString())
assert.equal(entry.object_id.toString(), note.id.toString())
})
test('creates notification', async () => {
const db = await makeDB()
const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com')
const note = await createPublicNote(domain, db, 'my first status', actorA)
const activity: any = {
type: 'Like',
actor: actorB.id,
object: note.id,
}
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
const entry = await db.prepare('SELECT * FROM actor_notifications').first()
assert.equal(entry.type, 'favourite')
assert.equal(entry.actor_id.toString(), actorA.id.toString())
assert.equal(entry.from_actor_id.toString(), actorB.id.toString())
})
test('records like in db', async () => {
const db = await makeDB()
const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com')
const note = await createPublicNote(domain, db, 'my first status', actorA)
const activity: any = {
type: 'Like',
actor: actorB.id,
object: note.id,
}
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
const entry = await db.prepare('SELECT * FROM actor_favourites').first()
assert.equal(entry.actor_id.toString(), actorB.id.toString())
assert.equal(entry.object_id.toString(), note.id.toString())
})
})
describe('Accept', () => {
beforeEach(() => {
globalThis.fetch = async (input: RequestInfo) => {
throw new Error('unexpected request to ' + input)
}
})
test('Accept follow request stores in db', async () => {
const db = await makeDB()
const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const actor2 = await createPerson(domain, db, userKEK, 'sven2@cloudflare.com')
await addFollowing(db, actor, actor2, 'not needed')
const activity = {
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Accept',
actor: { id: 'https://' + domain + '/ap/users/sven2' },
object: {
type: 'Follow',
actor: actor.id,
object: 'https://' + domain + '/ap/users/sven2',
},
}
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
const row = await db
.prepare(`SELECT target_actor_id, state FROM actor_following WHERE actor_id=?`)
.bind(actor.id.toString())
.first()
assert(row)
assert.equal(row.target_actor_id, 'https://' + domain + '/ap/users/sven2')
assert.equal(row.state, 'accepted')
})
test('Object must be an object', async () => {
const db = await makeDB()
await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const activity = {
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Accept',
actor: 'https://example.com/actor',
object: 'a',
}
await assert.rejects(activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys), {
message: '`activity.object` must be of type object',
})
})
})
describe('Create', () => {
test('Object must be an object', async () => {
const db = await makeDB()
await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const activity = {
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Create',
actor: 'https://example.com/actor',
object: 'a',
}
await assert.rejects(activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys), {
message: '`activity.object` must be of type object',
})
})
})
describe('Update', () => {
test('Object must be an object', async () => {
const db = await makeDB()
const activity = {
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Update',
actor: 'https://example.com/actor',
object: 'a',
}
await assert.rejects(activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys), {
message: '`activity.object` must be of type object',
})
})
test('Object must exist', async () => {
const db = await makeDB()
const activity = {
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Update',
actor: 'https://example.com/actor',
object: {
id: 'https://example.com/note2',
type: 'Note',
content: 'test note',
},
}
await assert.rejects(activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys), {
message: 'object https://example.com/note2 does not exist',
})
})
test('Object must have the same origin', async () => {
const db = await makeDB()
const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const object = {
id: 'https://example.com/note2',
type: 'Note',
content: 'test note',
}
const obj = await cacheObject(domain, db, object, actor.id, new URL(object.id), false)
assert.notEqual(obj, null, 'could not create object')
const activity = {
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Update',
actor: 'https://example.com/actor',
object: object,
}
await assert.rejects(activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys), {
message: 'actorid mismatch when updating object',
})
})
test('Object is updated', async () => {
const db = await makeDB()
const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const object = {
id: 'https://example.com/note2',
type: 'Note',
content: 'test note',
}
const obj = await cacheObject(domain, db, object, actor.id, new URL(object.id), false)
assert.notEqual(obj, null, 'could not create object')
const newObject = {
id: 'https://example.com/note2',
type: 'Note',
content: 'new test note',
}
const activity = {
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Update',
actor: actor.id,
object: newObject,
}
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
const updatedObject = await db
.prepare('SELECT * FROM objects WHERE original_object_id=?')
.bind(object.id)
.first()
assert(updatedObject)
assert.equal(JSON.parse(updatedObject.properties).content, newObject.content)
})
})
describe('Announce', () => {
test('Announce objects are stored and added to the remote actors outbox', 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)
const object = await db.prepare('SELECT * FROM objects').bind(remoteActorId).first()
assert(object)
assert.equal(object.type, 'Note')
assert.equal(object.original_actor_id, remoteActorId)
const outbox_object = await db
.prepare('SELECT * FROM outbox_objects WHERE actor_id=?')
.bind(remoteActorId)
.first()
assert(outbox_object)
assert.equal(outbox_object.actor_id, remoteActorId)
})
})
})
})

Wyświetl plik

@ -1,426 +0,0 @@
import { makeDB } from '../utils'
import type { JWK } from 'wildebeest/backend/src/webpush/jwk'
import * as objects from 'wildebeest/backend/src/activitypub/objects'
import { createPublicNote } from 'wildebeest/backend/src/activitypub/objects/note'
import * as ap_inbox from 'wildebeest/functions/ap/users/[id]/inbox'
import { createPerson } from 'wildebeest/backend/src/activitypub/actors'
import { strict as assert } from 'node:assert/strict'
const userKEK = 'test_kek9'
const domain = 'cloudflare.com'
const adminEmail = 'admin@example.com'
const vapidKeys = {} as JWK
const kv_cache: any = {
async put() {},
}
const waitUntil = async (p: Promise<void>) => await p
describe('ActivityPub', () => {
test('send Note to non existant user', async () => {
const db = await makeDB()
const activity: any = {}
const res = await ap_inbox.handleRequest(
domain,
db,
kv_cache,
'sven',
activity,
userKEK,
waitUntil,
adminEmail,
vapidKeys
)
assert.equal(res.status, 404)
})
test('send Note to inbox stores in DB', async () => {
const db = await makeDB()
const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const activity: any = {
type: 'Create',
actor: actor.id.toString(),
to: [actor.id.toString()],
cc: [],
object: {
id: 'https://example.com/note1',
type: 'Note',
content: 'test note',
},
}
const res = await ap_inbox.handleRequest(
domain,
db,
kv_cache,
'sven',
activity,
userKEK,
waitUntil,
adminEmail,
vapidKeys
)
assert.equal(res.status, 200)
const entry = await db
.prepare('SELECT objects.* FROM inbox_objects INNER JOIN objects ON objects.id=inbox_objects.object_id')
.first()
const properties = JSON.parse(entry.properties)
assert.equal(properties.content, 'test note')
})
test("send Note adds in remote actor's outbox", async () => {
const remoteActorId = 'https://example.com/actor'
globalThis.fetch = async (input: RequestInfo) => {
if (input.toString() === remoteActorId) {
return new Response(
JSON.stringify({
id: remoteActorId,
type: 'Person',
})
)
}
throw new Error('unexpected request to ' + input)
}
const db = await makeDB()
await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const activity: any = {
type: 'Create',
actor: remoteActorId,
to: [],
cc: [],
object: {
id: 'https://example.com/note1',
type: 'Note',
content: 'test note',
},
}
const res = await ap_inbox.handleRequest(
domain,
db,
kv_cache,
'sven',
activity,
userKEK,
waitUntil,
adminEmail,
vapidKeys
)
assert.equal(res.status, 200)
const entry = await db.prepare('SELECT * FROM outbox_objects WHERE actor_id=?').bind(remoteActorId).first()
assert.equal(entry.actor_id, remoteActorId)
})
test('local actor sends Note with mention create notification', async () => {
const db = await makeDB()
const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com')
const activity: any = {
type: 'Create',
actor: actorB.id.toString(),
to: [actorA.id.toString()],
cc: [],
object: {
id: 'https://example.com/note2',
type: 'Note',
content: 'test note',
},
}
const res = await ap_inbox.handleRequest(
domain,
db,
kv_cache,
'a',
activity,
userKEK,
waitUntil,
adminEmail,
vapidKeys
)
assert.equal(res.status, 200)
const entry = await db.prepare('SELECT * FROM actor_notifications').first()
assert.equal(entry.type, 'mention')
assert.equal(entry.actor_id.toString(), actorA.id.toString())
assert.equal(entry.from_actor_id.toString(), actorB.id.toString())
})
test('remote actor sends Note with mention create notification and download actor', async () => {
const actorB = 'https://remote.com/actorb'
globalThis.fetch = async (input: RequestInfo) => {
if (input.toString() === actorB) {
return new Response(
JSON.stringify({
id: actorB,
type: 'Person',
})
)
}
throw new Error('unexpected request to ' + input)
}
const db = await makeDB()
const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
const activity: any = {
type: 'Create',
actor: actorB,
to: [actorA.id.toString()],
cc: [],
object: {
id: 'https://example.com/note3',
type: 'Note',
content: 'test note',
},
}
const res = await ap_inbox.handleRequest(
domain,
db,
kv_cache,
'a',
activity,
userKEK,
waitUntil,
adminEmail,
vapidKeys
)
assert.equal(res.status, 200)
const entry = await db.prepare('SELECT * FROM actors WHERE id=?').bind(actorB).first()
assert.equal(entry.id, actorB)
})
test('send Note records reply', async () => {
const db = await makeDB()
const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
{
const activity: any = {
type: 'Create',
actor: actor.id.toString(),
to: [actor.id.toString()],
object: {
id: 'https://example.com/note1',
type: 'Note',
content: 'post',
},
}
const res = await ap_inbox.handleRequest(
domain,
db,
kv_cache,
'sven',
activity,
userKEK,
waitUntil,
adminEmail,
vapidKeys
)
assert.equal(res.status, 200)
}
{
const activity: any = {
type: 'Create',
actor: actor.id.toString(),
to: [actor.id.toString()],
object: {
inReplyTo: 'https://example.com/note1',
id: 'https://example.com/note2',
type: 'Note',
content: 'reply',
},
}
const res = await ap_inbox.handleRequest(
domain,
db,
kv_cache,
'sven',
activity,
userKEK,
waitUntil,
adminEmail,
vapidKeys
)
assert.equal(res.status, 200)
}
const entry = await db.prepare('SELECT * FROM actor_replies').first()
assert.equal(entry.actor_id, actor.id.toString().toString())
const obj: any = await objects.getObjectById(db, entry.object_id)
assert(obj)
assert.equal(obj.originalObjectId, 'https://example.com/note2')
const inReplyTo: any = await objects.getObjectById(db, entry.in_reply_to_object_id)
assert(inReplyTo)
assert.equal(inReplyTo.originalObjectId, 'https://example.com/note1')
})
describe('Announce', () => {
test('records reblog in db', async () => {
const db = await makeDB()
const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com')
const note = await createPublicNote(domain, db, 'my first status', actorA)
const activity: any = {
type: 'Announce',
actor: actorB.id,
object: note.id,
}
const res = await ap_inbox.handleRequest(
domain,
db,
kv_cache,
'a',
activity,
userKEK,
waitUntil,
adminEmail,
vapidKeys
)
assert.equal(res.status, 200)
const entry = await db.prepare('SELECT * FROM actor_reblogs').first()
assert.equal(entry.actor_id.toString(), actorB.id.toString())
assert.equal(entry.object_id.toString(), note.id.toString())
})
test('creates notification', async () => {
const db = await makeDB()
const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com')
const note = await createPublicNote(domain, db, 'my first status', actorA)
const activity: any = {
type: 'Announce',
actor: actorB.id,
object: note.id,
}
const res = await ap_inbox.handleRequest(
domain,
db,
kv_cache,
'a',
activity,
userKEK,
waitUntil,
adminEmail,
vapidKeys
)
assert.equal(res.status, 200)
const entry = await db.prepare('SELECT * FROM actor_notifications').first()
assert(entry)
assert.equal(entry.type, 'reblog')
assert.equal(entry.actor_id.toString(), actorA.id.toString())
assert.equal(entry.from_actor_id.toString(), actorB.id.toString())
})
})
describe('Like', () => {
test('records like in db', async () => {
const db = await makeDB()
const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com')
const note = await createPublicNote(domain, db, 'my first status', actorA)
const activity: any = {
type: 'Like',
actor: actorB.id,
object: note.id,
}
const res = await ap_inbox.handleRequest(
domain,
db,
kv_cache,
'a',
activity,
userKEK,
waitUntil,
adminEmail,
vapidKeys
)
assert.equal(res.status, 200)
const entry = await db.prepare('SELECT * FROM actor_favourites').first()
assert.equal(entry.actor_id.toString(), actorB.id.toString())
assert.equal(entry.object_id.toString(), note.id.toString())
})
test('creates notification', async () => {
const db = await makeDB()
const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com')
const note = await createPublicNote(domain, db, 'my first status', actorA)
const activity: any = {
type: 'Like',
actor: actorB.id,
object: note.id,
}
const res = await ap_inbox.handleRequest(
domain,
db,
kv_cache,
'a',
activity,
userKEK,
waitUntil,
adminEmail,
vapidKeys
)
assert.equal(res.status, 200)
const entry = await db.prepare('SELECT * FROM actor_notifications').first()
assert.equal(entry.type, 'favourite')
assert.equal(entry.actor_id.toString(), actorA.id.toString())
assert.equal(entry.from_actor_id.toString(), actorB.id.toString())
})
test('records like in db', async () => {
const db = await makeDB()
const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
const actorB = await createPerson(domain, db, userKEK, 'b@cloudflare.com')
const note = await createPublicNote(domain, db, 'my first status', actorA)
const activity: any = {
type: 'Like',
actor: actorB.id,
object: note.id,
}
const res = await ap_inbox.handleRequest(
domain,
db,
kv_cache,
'a',
activity,
userKEK,
waitUntil,
adminEmail,
vapidKeys
)
assert.equal(res.status, 200)
const entry = await db.prepare('SELECT * FROM actor_favourites').first()
assert.equal(entry.actor_id.toString(), actorB.id.toString())
assert.equal(entry.object_id.toString(), note.id.toString())
})
})
})

Wyświetl plik

@ -0,0 +1,10 @@
{
"name": "consumer",
"version": "0.0.0",
"devDependencies": {
"@cloudflare/workers-types": "^4.20221111.1",
"typescript": "^4.9.4",
"wrangler": "2.7.1"
},
"private": true
}

Wyświetl plik

@ -0,0 +1,58 @@
import type { MessageBody } from 'wildebeest/backend/src/types/queue'
import type { JWK } from 'wildebeest/backend/src/webpush/jwk'
import type { Actor } from 'wildebeest/backend/src/activitypub/actors'
import * as actors from 'wildebeest/backend/src/activitypub/actors'
import * as timeline from 'wildebeest/backend/src/mastodon/timeline'
import * as notification from 'wildebeest/backend/src/mastodon/notification'
import * as activityHandler from 'wildebeest/backend/src/activitypub/activities/handle'
import type { Activity } from 'wildebeest/backend/src/activitypub/activities'
type Env = {
DATABASE: D1Database
DOMAIN: string
ADMIN_EMAIL: string
KV_CACHE: KVNamespace
}
export default {
async queue(batch: MessageBatch<MessageBody>, env: Env, ctx: ExecutionContext) {
for (const message of batch.messages) {
try {
const actor = await actors.getPersonById(env.DATABASE, new URL(message.body.actorId))
if (actor === null) {
console.warn(`actor ${message.body.actorId} is missing`)
return
}
switch (message.body.type) {
case 'activity': {
await handleActivityMessage(env, actor, message.body)
break
}
default:
throw new Error('unsupported message type: ' + message.body.type)
}
} catch (err: any) {
console.error(err.stack)
// TODO: add sentry
}
}
},
}
async function handleActivityMessage(env: Env, actor: Actor, message: MessageBody) {
const domain = env.DOMAIN
const db = env.DATABASE
const adminEmail = env.ADMIN_EMAIL
const cache = env.KV_CACHE
const activity = message.content
await activityHandler.handle(domain, activity, db, message.userKEK, adminEmail, message.vapidKeys)
// Assuming we received new posts or a like, pregenerate the user's timelines
// and notifications.
await Promise.all([
timeline.pregenerateTimelines(domain, db, cache, actor),
notification.pregenerateNotifications(db, cache, actor),
])
}

Wyświetl plik

@ -0,0 +1,108 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Projects */
// "incremental": true, /* Enable incremental compilation */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"lib": [
"es2021"
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
"jsx": "react" /* Specify what JSX code is generated. */,
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
// "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
/* Modules */
"module": "es2022" /* Specify what module code is generated. */,
// "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
"paths": {
"wildebeest/*": ["../*"]
},
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
"types": [
"@cloudflare/workers-types",
"jest"
] /* Specify type package names to be included without being referenced in a source file. */,
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
"resolveJsonModule": true /* Enable importing .json files */,
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
"allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */,
"checkJs": false /* Enable error reporting in type-checked JavaScript files. */,
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
"noEmit": true /* Disable emitting files from a compilation. */,
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
"isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */,
"allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */,
// "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */
"strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
// "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
// "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

Wyświetl plik

@ -0,0 +1,9 @@
name = "wildebeest-consumer"
compatibility_date = "2023-01-12"
main = "./src/index.ts"
[[queues.consumers]]
queue = "wildebeest"
max_batch_size = 10
max_batch_timeout = 30
max_retries = 10

Wyświetl plik

@ -1,18 +1,16 @@
import { parseHandle } from 'wildebeest/backend/src/utils/parse'
import { getVAPIDKeys } from 'wildebeest/backend/src/config'
import type { JWK } from 'wildebeest/backend/src/webpush/jwk'
import * as activityHandler from 'wildebeest/backend/src/activitypub/activities/handle'
import type { Env } from 'wildebeest/backend/src/types/env'
import * as actors from 'wildebeest/backend/src/activitypub/actors'
import type { Activity } from 'wildebeest/backend/src/activitypub/activities'
import { actorURL } from 'wildebeest/backend/src/activitypub/actors'
import type { Env } from 'wildebeest/backend/src/types/env'
import type { MessageBody } from 'wildebeest/backend/src/types/queue'
import type { Activity } from 'wildebeest/backend/src/activitypub/activities'
import { parseRequest } from 'wildebeest/backend/src/utils/httpsigjs/parser'
import { fetchKey, verifySignature } from 'wildebeest/backend/src/utils/httpsigjs/verifier'
import { generateDigestHeader } from 'wildebeest/backend/src/utils/http-signing-cavage'
import * as timeline from 'wildebeest/backend/src/mastodon/timeline'
import * as notification from 'wildebeest/backend/src/mastodon/notification'
import { getVAPIDKeys } from 'wildebeest/backend/src/config'
export const onRequest: PagesFunction<Env, any> = async ({ params, request, env, waitUntil }) => {
export const onRequest: PagesFunction<Env, any> = async ({ params, request, env }) => {
const parsedSignature = parseRequest(request)
const pubKey = await fetchKey(parsedSignature)
const valid = await verifySignature(parsedSignature, pubKey)
@ -31,28 +29,16 @@ export const onRequest: PagesFunction<Env, any> = async ({ params, request, env,
const activity: Activity = JSON.parse(body)
const domain = new URL(request.url).hostname
return handleRequest(
domain,
env.DATABASE,
env.KV_CACHE,
params.id as string,
activity,
env.userKEK,
waitUntil,
env.ADMIN_EMAIL,
getVAPIDKeys(env)
)
return handleRequest(domain, env.DATABASE, params.id as string, activity, env.QUEUE, env.userKEK, getVAPIDKeys(env))
}
export async function handleRequest(
domain: string,
db: D1Database,
cache: KVNamespace,
id: string,
activity: Activity,
queue: Queue<MessageBody>,
userKEK: string,
waitUntil: (p: Promise<any>) => void,
adminEmail: string,
vapidKeys: JWK
): Promise<Response> {
const handle = parseHandle(id)
@ -60,23 +46,20 @@ export async function handleRequest(
if (handle.domain !== null && handle.domain !== domain) {
return new Response('', { status: 403 })
}
const actorId = actorURL(domain, handle.localPart)
const actor = await actors.getPersonById(db, actorId)
if (actor === null) {
return new Response('', { status: 404 })
}
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
// Assuming we received new posts or a like, pregenerate the user's timelines
// and notifications.
waitUntil(
Promise.all([
timeline.pregenerateTimelines(domain, db, cache, actor),
notification.pregenerateNotifications(db, cache, actor),
])
)
await queue.send({
type: 'activity',
actorId: actor.id.toString(),
content: activity,
userKEK,
vapidKeys,
})
return new Response('', { status: 200 })
}

Wyświetl plik

@ -66,7 +66,7 @@ export async function handleRequest(
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'content-type, authorization',
'Access-Control-Allow-Methods': 'POST',
'content-type': 'application/json',
'content-type': 'application/json; charset=utf-8',
}
return new Response(JSON.stringify(res), { headers })
}