kopia lustrzana https://github.com/cloudflare/wildebeest
MOW-101: hide DM from public
rodzic
5ce31c4d75
commit
196d6fa896
|
@ -1,4 +1,5 @@
|
||||||
import * as actors from 'wildebeest/backend/src/activitypub/actors'
|
import * as actors from 'wildebeest/backend/src/activitypub/actors'
|
||||||
|
import { PUBLIC_GROUP } from 'wildebeest/backend/src/activitypub/activities'
|
||||||
import type { JWK } from 'wildebeest/backend/src/webpush/jwk'
|
import type { JWK } from 'wildebeest/backend/src/webpush/jwk'
|
||||||
import { addObjectInOutbox } from 'wildebeest/backend/src/activitypub/actors/outbox'
|
import { addObjectInOutbox } from 'wildebeest/backend/src/activitypub/actors/outbox'
|
||||||
import { actorURL } from 'wildebeest/backend/src/activitypub/actors'
|
import { actorURL } from 'wildebeest/backend/src/activitypub/actors'
|
||||||
|
@ -131,11 +132,17 @@ export async function handle(
|
||||||
// FIXME: download any attachment Objects
|
// FIXME: download any attachment Objects
|
||||||
|
|
||||||
let recipients: Array<string> = []
|
let recipients: Array<string> = []
|
||||||
|
let target = PUBLIC_GROUP
|
||||||
|
|
||||||
if (Array.isArray(activity.to)) {
|
if (Array.isArray(activity.to) && activity.to.length > 0) {
|
||||||
recipients = [...recipients, ...activity.to]
|
recipients = [...recipients, ...activity.to]
|
||||||
|
|
||||||
|
if (activity.to.length !== 1) {
|
||||||
|
console.warn("multiple `Activity.to` isn't supported")
|
||||||
|
}
|
||||||
|
target = activity.to[0]
|
||||||
}
|
}
|
||||||
if (Array.isArray(activity.cc)) {
|
if (Array.isArray(activity.cc) && activity.cc.length > 0) {
|
||||||
recipients = [...recipients, ...activity.cc]
|
recipients = [...recipients, ...activity.cc]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,7 +179,7 @@ export async function handle(
|
||||||
const fromActor = await actors.getAndCache(getActorAsId(), db)
|
const fromActor = await actors.getAndCache(getActorAsId(), db)
|
||||||
// Add the object in the originating actor's outbox, allowing other
|
// Add the object in the originating actor's outbox, allowing other
|
||||||
// actors on this instance to see the note in their timelines.
|
// actors on this instance to see the note in their timelines.
|
||||||
await addObjectInOutbox(db, fromActor, obj, activity.published)
|
await addObjectInOutbox(db, fromActor, obj, activity.published, target)
|
||||||
|
|
||||||
for (let i = 0, len = recipients.length; i < len; i++) {
|
for (let i = 0, len = recipients.length; i < len; i++) {
|
||||||
const handle = parseHandle(extractID(domain, recipients[i]))
|
const handle = parseHandle(extractID(domain, recipients[i]))
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
export type Activity = any
|
export type Activity = any
|
||||||
|
|
||||||
|
export const PUBLIC_GROUP = 'https://www.w3.org/ns/activitystreams#Public'
|
||||||
|
|
||||||
// Generate a unique ID. Note that currently the generated URL aren't routable.
|
// Generate a unique ID. Note that currently the generated URL aren't routable.
|
||||||
export function uri(domain: string): URL {
|
export function uri(domain: string): URL {
|
||||||
const id = crypto.randomUUID()
|
const id = crypto.randomUUID()
|
||||||
|
|
|
@ -2,20 +2,27 @@ import type { Object } from 'wildebeest/backend/src/activitypub/objects'
|
||||||
import type { Activity } from 'wildebeest/backend/src/activitypub/activities'
|
import type { Activity } from 'wildebeest/backend/src/activitypub/activities'
|
||||||
import type { Actor } from 'wildebeest/backend/src/activitypub/actors'
|
import type { Actor } from 'wildebeest/backend/src/activitypub/actors'
|
||||||
import type { OrderedCollection, OrderedCollectionPage } from 'wildebeest/backend/src/activitypub/core'
|
import type { OrderedCollection, OrderedCollectionPage } from 'wildebeest/backend/src/activitypub/core'
|
||||||
|
import { PUBLIC_GROUP } from 'wildebeest/backend/src/activitypub/activities'
|
||||||
|
|
||||||
export async function addObjectInOutbox(db: D1Database, actor: Actor, obj: Object, published_date?: string) {
|
export async function addObjectInOutbox(
|
||||||
|
db: D1Database,
|
||||||
|
actor: Actor,
|
||||||
|
obj: Object,
|
||||||
|
published_date?: string,
|
||||||
|
target: string = PUBLIC_GROUP
|
||||||
|
) {
|
||||||
const id = crypto.randomUUID()
|
const id = crypto.randomUUID()
|
||||||
let out: any = null
|
let out: any = null
|
||||||
|
|
||||||
if (published_date !== undefined) {
|
if (published_date !== undefined) {
|
||||||
out = await db
|
out = await db
|
||||||
.prepare('INSERT INTO outbox_objects(id, actor_id, object_id, published_date) VALUES(?, ?, ?, ?)')
|
.prepare('INSERT INTO outbox_objects(id, actor_id, object_id, published_date, target) VALUES(?, ?, ?, ?, ?)')
|
||||||
.bind(id, actor.id.toString(), obj.id.toString(), published_date)
|
.bind(id, actor.id.toString(), obj.id.toString(), published_date, target)
|
||||||
.run()
|
.run()
|
||||||
} else {
|
} else {
|
||||||
out = await db
|
out = await db
|
||||||
.prepare('INSERT INTO outbox_objects(id, actor_id, object_id) VALUES(?, ?, ?)')
|
.prepare('INSERT INTO outbox_objects(id, actor_id, object_id, target) VALUES(?, ?, ?, ?)')
|
||||||
.bind(id, actor.id.toString(), obj.id.toString())
|
.bind(id, actor.id.toString(), obj.id.toString(), target)
|
||||||
.run()
|
.run()
|
||||||
}
|
}
|
||||||
if (!out.success) {
|
if (!out.success) {
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
import type { Actor } from 'wildebeest/backend/src/activitypub/actors'
|
import type { Actor } from 'wildebeest/backend/src/activitypub/actors'
|
||||||
import type { Document } from 'wildebeest/backend/src/activitypub/objects'
|
import type { Document } from 'wildebeest/backend/src/activitypub/objects'
|
||||||
import { followersURL } from 'wildebeest/backend/src/activitypub/actors'
|
import { followersURL } from 'wildebeest/backend/src/activitypub/actors'
|
||||||
|
import { PUBLIC_GROUP } from 'wildebeest/backend/src/activitypub/activities'
|
||||||
import * as objects from '.'
|
import * as objects from '.'
|
||||||
|
|
||||||
const NOTE = 'Note'
|
const NOTE = 'Note'
|
||||||
export const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public'
|
|
||||||
|
|
||||||
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-note
|
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-note
|
||||||
export interface Note extends objects.Object {
|
export interface Note extends objects.Object {
|
||||||
|
@ -34,7 +34,7 @@ export async function createPublicNote(
|
||||||
const properties = {
|
const properties = {
|
||||||
attributedTo: actorId,
|
attributedTo: actorId,
|
||||||
content,
|
content,
|
||||||
to: [PUBLIC],
|
to: [PUBLIC_GROUP],
|
||||||
cc: [followersURL(actorId)],
|
cc: [followersURL(actorId)],
|
||||||
|
|
||||||
// FIXME: stub values
|
// FIXME: stub values
|
||||||
|
@ -50,3 +50,34 @@ export async function createPublicNote(
|
||||||
|
|
||||||
return (await objects.createObject(domain, db, NOTE, properties, actorId, true)) as Note
|
return (await objects.createObject(domain, db, NOTE, properties, actorId, true)) as Note
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createPrivateNote(
|
||||||
|
domain: string,
|
||||||
|
db: D1Database,
|
||||||
|
content: string,
|
||||||
|
actor: Actor,
|
||||||
|
targetActor: Actor,
|
||||||
|
attachment: Array<Document> = [],
|
||||||
|
extraProperties: any = {}
|
||||||
|
): Promise<Note> {
|
||||||
|
const actorId = new URL(actor.id)
|
||||||
|
|
||||||
|
const properties = {
|
||||||
|
attributedTo: actorId,
|
||||||
|
content,
|
||||||
|
to: [targetActor.id.toString()],
|
||||||
|
cc: [],
|
||||||
|
|
||||||
|
// FIXME: stub values
|
||||||
|
inReplyTo: null,
|
||||||
|
replies: null,
|
||||||
|
sensitive: false,
|
||||||
|
summary: null,
|
||||||
|
tag: [],
|
||||||
|
attachment,
|
||||||
|
|
||||||
|
...extraProperties,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await objects.createObject(domain, db, NOTE, properties, actorId, true)) as Note
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import type { MastodonStatus } from 'wildebeest/backend/src/types/status'
|
||||||
import { getFollowingId } from 'wildebeest/backend/src/mastodon/follow'
|
import { getFollowingId } from 'wildebeest/backend/src/mastodon/follow'
|
||||||
import type { Actor } from 'wildebeest/backend/src/activitypub/actors/'
|
import type { Actor } from 'wildebeest/backend/src/activitypub/actors/'
|
||||||
import { toMastodonStatusFromRow } from './status'
|
import { toMastodonStatusFromRow } from './status'
|
||||||
|
import { PUBLIC_GROUP } from 'wildebeest/backend/src/activitypub/activities'
|
||||||
|
|
||||||
export async function pregenerateTimelines(domain: string, db: D1Database, cache: KVNamespace, actor: Actor) {
|
export async function pregenerateTimelines(domain: string, db: D1Database, cache: KVNamespace, actor: Actor) {
|
||||||
const timeline = await getHomeTimeline(domain, db, actor)
|
const timeline = await getHomeTimeline(domain, db, actor)
|
||||||
|
@ -31,6 +32,7 @@ WHERE
|
||||||
objects.type = 'Note'
|
objects.type = 'Note'
|
||||||
AND outbox_objects.actor_id IN (SELECT value FROM json_each(?))
|
AND outbox_objects.actor_id IN (SELECT value FROM json_each(?))
|
||||||
AND json_extract(objects.properties, '$.inReplyTo') IS NULL
|
AND json_extract(objects.properties, '$.inReplyTo') IS NULL
|
||||||
|
AND outbox_objects.target = '${PUBLIC_GROUP}'
|
||||||
ORDER by outbox_objects.published_date DESC
|
ORDER by outbox_objects.published_date DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
`
|
`
|
||||||
|
@ -97,6 +99,7 @@ INNER JOIN actors ON actors.id=outbox_objects.actor_id
|
||||||
WHERE objects.type='Note'
|
WHERE objects.type='Note'
|
||||||
AND ${localPreferenceQuery(localPreference)}
|
AND ${localPreferenceQuery(localPreference)}
|
||||||
AND json_extract(objects.properties, '$.inReplyTo') IS NULL
|
AND json_extract(objects.properties, '$.inReplyTo') IS NULL
|
||||||
|
AND outbox_objects.target = '${PUBLIC_GROUP}'
|
||||||
ORDER by outbox_objects.published_date DESC
|
ORDER by outbox_objects.published_date DESC
|
||||||
LIMIT ?1 OFFSET ?2
|
LIMIT ?1 OFFSET ?2
|
||||||
`
|
`
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { makeDB, isUrlValid } from './utils'
|
||||||
import { MessageType } from 'wildebeest/backend/src/types/queue'
|
import { MessageType } from 'wildebeest/backend/src/types/queue'
|
||||||
import type { JWK } from 'wildebeest/backend/src/webpush/jwk'
|
import type { JWK } from 'wildebeest/backend/src/webpush/jwk'
|
||||||
import { createPerson } from 'wildebeest/backend/src/activitypub/actors'
|
import { createPerson } from 'wildebeest/backend/src/activitypub/actors'
|
||||||
import { createPublicNote } from 'wildebeest/backend/src/activitypub/objects/note'
|
import { createPublicNote, createPrivateNote } from 'wildebeest/backend/src/activitypub/objects/note'
|
||||||
import { addObjectInOutbox } from 'wildebeest/backend/src/activitypub/actors/outbox'
|
import { addObjectInOutbox } from 'wildebeest/backend/src/activitypub/actors/outbox'
|
||||||
import { strict as assert } from 'node:assert/strict'
|
import { strict as assert } from 'node:assert/strict'
|
||||||
import { cacheObject } from 'wildebeest/backend/src/activitypub/objects/'
|
import { cacheObject } from 'wildebeest/backend/src/activitypub/objects/'
|
||||||
|
@ -74,7 +74,7 @@ describe('ActivityPub', () => {
|
||||||
await sleep(10)
|
await sleep(10)
|
||||||
await addObjectInOutbox(db, actor, await createPublicNote(domain, db, 'my second status', actor))
|
await addObjectInOutbox(db, actor, await createPublicNote(domain, db, 'my second status', actor))
|
||||||
|
|
||||||
const res = await ap_outbox_page.handleRequest(domain, db, 'sven', userKEK)
|
const res = await ap_outbox_page.handleRequest(domain, db, 'sven')
|
||||||
assert.equal(res.status, 200)
|
assert.equal(res.status, 200)
|
||||||
|
|
||||||
const data = await res.json<any>()
|
const data = await res.json<any>()
|
||||||
|
@ -83,6 +83,46 @@ describe('ActivityPub', () => {
|
||||||
assert.equal(data.orderedItems[0].object.content, 'my second status')
|
assert.equal(data.orderedItems[0].object.content, 'my second status')
|
||||||
assert.equal(data.orderedItems[1].object.content, 'my first status')
|
assert.equal(data.orderedItems[1].object.content, 'my first status')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("doesn't show private notes to anyone", 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 createPrivateNote(domain, db, 'DM', actorA, actorB)
|
||||||
|
await addObjectInOutbox(db, actorA, note, undefined, actorB.id.toString())
|
||||||
|
|
||||||
|
{
|
||||||
|
const res = await ap_outbox_page.handleRequest(domain, db, 'a')
|
||||||
|
assert.equal(res.status, 200)
|
||||||
|
|
||||||
|
const data = await res.json<any>()
|
||||||
|
assert.equal(data.orderedItems.length, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const res = await ap_outbox_page.handleRequest(domain, db, 'b')
|
||||||
|
assert.equal(res.status, 200)
|
||||||
|
|
||||||
|
const data = await res.json<any>()
|
||||||
|
assert.equal(data.orderedItems.length, 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should show private notes to target but doesn't yet", async () => {
|
||||||
|
const db = await makeDB()
|
||||||
|
const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
|
||||||
|
const actorB = await createPerson(domain, db, userKEK, 'target@cloudflare.com')
|
||||||
|
|
||||||
|
const note = await createPrivateNote(domain, db, 'DM', actorA, actorB)
|
||||||
|
await addObjectInOutbox(db, actorA, note)
|
||||||
|
|
||||||
|
const res = await ap_outbox_page.handleRequest(domain, db, 'target')
|
||||||
|
assert.equal(res.status, 200)
|
||||||
|
|
||||||
|
const data = await res.json<any>()
|
||||||
|
assert.equal(data.orderedItems.length, 0)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Objects', () => {
|
describe('Objects', () => {
|
||||||
|
|
|
@ -14,136 +14,6 @@ const vapidKeys = {} as JWK
|
||||||
|
|
||||||
describe('ActivityPub', () => {
|
describe('ActivityPub', () => {
|
||||||
describe('handle Activity', () => {
|
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', () => {
|
describe('Announce', () => {
|
||||||
test('records reblog in db', async () => {
|
test('records reblog in db', async () => {
|
||||||
const db = await makeDB()
|
const db = await makeDB()
|
||||||
|
@ -314,6 +184,157 @@ describe('ActivityPub', () => {
|
||||||
message: '`activity.object` must be of type object',
|
message: '`activity.object` must be of type object',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('preserve Note sent with `to`', 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: ['some actor'],
|
||||||
|
cc: [],
|
||||||
|
object: {
|
||||||
|
id: 'https://example.com/note1',
|
||||||
|
type: 'Note',
|
||||||
|
content: 'test note',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await activityHandler.handle(domain, activity, db, userKEK, adminEmail, vapidKeys)
|
||||||
|
|
||||||
|
const row = await db.prepare('SELECT * FROM outbox_objects').first()
|
||||||
|
assert.equal(row.target, 'some actor')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Update', () => {
|
describe('Update', () => {
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { createPerson } from 'wildebeest/backend/src/activitypub/actors'
|
||||||
import { insertLike } from 'wildebeest/backend/src/mastodon/like'
|
import { insertLike } from 'wildebeest/backend/src/mastodon/like'
|
||||||
import { insertReblog } from 'wildebeest/backend/src/mastodon/reblog'
|
import { insertReblog } from 'wildebeest/backend/src/mastodon/reblog'
|
||||||
import { isUrlValid, makeDB, assertJSON, streamToArrayBuffer, makeQueue } from '../utils'
|
import { isUrlValid, makeDB, assertJSON, streamToArrayBuffer, makeQueue } from '../utils'
|
||||||
import * as note from 'wildebeest/backend/src/activitypub/objects/note'
|
import * as activities from 'wildebeest/backend/src/activitypub/activities'
|
||||||
import { addFollowing, acceptFollowing } from 'wildebeest/backend/src/mastodon/follow'
|
import { addFollowing, acceptFollowing } from 'wildebeest/backend/src/mastodon/follow'
|
||||||
import { MessageType } from 'wildebeest/backend/src/types/queue'
|
import { MessageType } from 'wildebeest/backend/src/types/queue'
|
||||||
|
|
||||||
|
@ -220,7 +220,7 @@ describe('Mastodon APIs', () => {
|
||||||
assert.equal(deliveredNote.actor, `https://${domain}/ap/users/sven`)
|
assert.equal(deliveredNote.actor, `https://${domain}/ap/users/sven`)
|
||||||
assert.equal(deliveredNote.object.attributedTo, `https://${domain}/ap/users/sven`)
|
assert.equal(deliveredNote.object.attributedTo, `https://${domain}/ap/users/sven`)
|
||||||
assert.equal(deliveredNote.object.type, 'Note')
|
assert.equal(deliveredNote.object.type, 'Note')
|
||||||
assert(deliveredNote.object.to.includes(note.PUBLIC))
|
assert(deliveredNote.object.to.includes(activities.PUBLIC_GROUP))
|
||||||
assert.equal(deliveredNote.object.cc.length, 1)
|
assert.equal(deliveredNote.object.cc.length, 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { strict as assert } from 'node:assert/strict'
|
||||||
import { insertReply } from 'wildebeest/backend/src/mastodon/reply'
|
import { insertReply } from 'wildebeest/backend/src/mastodon/reply'
|
||||||
import { createImage } from 'wildebeest/backend/src/activitypub/objects/image'
|
import { createImage } from 'wildebeest/backend/src/activitypub/objects/image'
|
||||||
import { addFollowing, acceptFollowing } from 'wildebeest/backend/src/mastodon/follow'
|
import { addFollowing, acceptFollowing } from 'wildebeest/backend/src/mastodon/follow'
|
||||||
import { createPublicNote } from 'wildebeest/backend/src/activitypub/objects/note'
|
import { createPublicNote, createPrivateNote } from 'wildebeest/backend/src/activitypub/objects/note'
|
||||||
import { addObjectInOutbox } from 'wildebeest/backend/src/activitypub/actors/outbox'
|
import { addObjectInOutbox } from 'wildebeest/backend/src/activitypub/actors/outbox'
|
||||||
import { createPerson } from 'wildebeest/backend/src/activitypub/actors'
|
import { createPerson } from 'wildebeest/backend/src/activitypub/actors'
|
||||||
import { makeDB, assertCORS, assertJSON } from '../utils'
|
import { makeDB, assertCORS, assertJSON } from '../utils'
|
||||||
|
@ -53,6 +53,40 @@ describe('Mastodon APIs', () => {
|
||||||
assert.equal(data[1].reblogs_count, 1)
|
assert.equal(data[1].reblogs_count, 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("home doesn't show private Notes from followed actors", async () => {
|
||||||
|
const db = await makeDB()
|
||||||
|
const actor1 = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
|
||||||
|
const actor2 = await createPerson(domain, db, userKEK, 'sven2@cloudflare.com')
|
||||||
|
const actor3 = await createPerson(domain, db, userKEK, 'sven3@cloudflare.com')
|
||||||
|
|
||||||
|
// actor3 follows actor1 and actor2
|
||||||
|
await addFollowing(db, actor3, actor1, 'not needed')
|
||||||
|
await acceptFollowing(db, actor3, actor1)
|
||||||
|
await addFollowing(db, actor3, actor2, 'not needed')
|
||||||
|
await acceptFollowing(db, actor3, actor2)
|
||||||
|
|
||||||
|
// actor2 sends a DM to actor1
|
||||||
|
const note = await createPrivateNote(domain, db, 'DM', actor2, actor1)
|
||||||
|
await addObjectInOutbox(db, actor2, note, undefined, actor1.id.toString())
|
||||||
|
|
||||||
|
// actor3 shouldn't see the private note
|
||||||
|
const data = await timelines.getHomeTimeline(domain, db, actor3)
|
||||||
|
assert.equal(data.length, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("public doesn't show private Notes", async () => {
|
||||||
|
const db = await makeDB()
|
||||||
|
const actor1 = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
|
||||||
|
const actor2 = await createPerson(domain, db, userKEK, 'sven2@cloudflare.com')
|
||||||
|
|
||||||
|
// actor2 sends a DM to actor1
|
||||||
|
const note = await createPrivateNote(domain, db, 'DM', actor2, actor1)
|
||||||
|
await addObjectInOutbox(db, actor2, note, undefined, actor1.id.toString())
|
||||||
|
|
||||||
|
const data = await timelines.getPublicTimeline(domain, db, timelines.LocalPreference.NotSet)
|
||||||
|
assert.equal(data.length, 0)
|
||||||
|
})
|
||||||
|
|
||||||
test('home returns Notes from ourself', async () => {
|
test('home returns Notes from ourself', async () => {
|
||||||
const db = await makeDB()
|
const db = await makeDB()
|
||||||
const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
|
const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
|
||||||
|
|
|
@ -7,10 +7,11 @@ import type { ContextData } from 'wildebeest/backend/src/types/context'
|
||||||
import type { Env } from 'wildebeest/backend/src/types/env'
|
import type { Env } from 'wildebeest/backend/src/types/env'
|
||||||
import type { Note } from 'wildebeest/backend/src/activitypub/objects/note'
|
import type { Note } from 'wildebeest/backend/src/activitypub/objects/note'
|
||||||
import * as activityCreate from 'wildebeest/backend/src/activitypub/activities/create'
|
import * as activityCreate from 'wildebeest/backend/src/activitypub/activities/create'
|
||||||
|
import { PUBLIC_GROUP } from 'wildebeest/backend/src/activitypub/activities'
|
||||||
|
|
||||||
export const onRequest: PagesFunction<Env, any, ContextData> = async ({ request, env, params }) => {
|
export const onRequest: PagesFunction<Env, any, ContextData> = async ({ request, env, params }) => {
|
||||||
const domain = new URL(request.url).hostname
|
const domain = new URL(request.url).hostname
|
||||||
return handleRequest(domain, env.DATABASE, params.id as string, env.userKEK)
|
return handleRequest(domain, env.DATABASE, params.id as string)
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
|
@ -21,8 +22,7 @@ const headers = {
|
||||||
|
|
||||||
const DEFAULT_LIMIT = 20
|
const DEFAULT_LIMIT = 20
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- TODO: use userKEK
|
export async function handleRequest(domain: string, db: D1Database, id: string): Promise<Response> {
|
||||||
export async function handleRequest(domain: string, db: D1Database, id: string, userKEK: string): Promise<Response> {
|
|
||||||
const handle = parseHandle(id)
|
const handle = parseHandle(id)
|
||||||
|
|
||||||
if (handle.domain !== null) {
|
if (handle.domain !== null) {
|
||||||
|
@ -42,9 +42,11 @@ export async function handleRequest(domain: string, db: D1Database, id: string,
|
||||||
SELECT objects.*
|
SELECT objects.*
|
||||||
FROM outbox_objects
|
FROM outbox_objects
|
||||||
INNER JOIN objects ON objects.id = outbox_objects.object_id
|
INNER JOIN objects ON objects.id = outbox_objects.object_id
|
||||||
WHERE outbox_objects.actor_id = ? AND objects.type = 'Note'
|
WHERE outbox_objects.actor_id = ?1
|
||||||
|
AND objects.type = 'Note'
|
||||||
|
AND outbox_objects.target = '${PUBLIC_GROUP}'
|
||||||
ORDER by outbox_objects.cdate DESC
|
ORDER by outbox_objects.cdate DESC
|
||||||
LIMIT ?
|
LIMIT ?2
|
||||||
`
|
`
|
||||||
|
|
||||||
const { success, error, results } = await db.prepare(QUERY).bind(actorId.toString(), DEFAULT_LIMIT).all()
|
const { success, error, results } = await db.prepare(QUERY).bind(actorId.toString(), DEFAULT_LIMIT).all()
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
-- Migration number: 0002 2023-01-16T13:46:54.975Z
|
||||||
|
|
||||||
|
ALTER TABLE outbox_objects
|
||||||
|
ADD target TEXT NOT NULL DEFAULT 'https://www.w3.org/ns/activitystreams#Public';
|
Ładowanie…
Reference in New Issue