support direct visiblity and reject others

Refs https://github.com/cloudflare/wildebeest/issues/303
pull/336/head
Sven Sauleau 2023-02-23 14:27:07 +00:00
rodzic a876ef9ad2
commit c09edecc34
5 zmienionych plików z 165 dodań i 12 usunięć

Wyświetl plik

@ -52,12 +52,12 @@ export async function createPublicNote(
return (await objects.createObject(domain, db, NOTE, properties, actorId, true)) as Note
}
export async function createPrivateNote(
export async function createDirectNote(
domain: string,
db: Database,
content: string,
actor: Actor,
targetActor: Actor,
targetActors: Array<Actor>,
attachment: Array<objects.APObject> = [],
extraProperties: any = {}
): Promise<Note> {
@ -66,7 +66,7 @@ export async function createPrivateNote(
const properties = {
attributedTo: actorId,
content,
to: [targetActor.id.toString()],
to: targetActors.map((a) => a.id.toString()),
cc: [],
// FIXME: stub values

Wyświetl plik

@ -3,7 +3,7 @@ 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 { createDirectNote, 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/'
@ -146,7 +146,7 @@ describe('ActivityPub', () => {
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)
const note = await createDirectNote(domain, db, 'DM', actorA, [actorB])
await addObjectInOutbox(db, actorA, note, undefined, actorB.id.toString())
{
@ -171,7 +171,7 @@ describe('ActivityPub', () => {
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)
const note = await createDirectNote(domain, db, 'DM', actorA, [actorB])
await addObjectInOutbox(db, actorA, note)
const res = await ap_outbox_page.handleRequest(domain, db, 'target')

Wyświetl plik

@ -18,6 +18,7 @@ import { MessageType } from 'wildebeest/backend/src/types/queue'
import { MastodonStatus } from 'wildebeest/backend/src/types'
import { mastodonIdSymbol, getObjectByMastodonId } from 'wildebeest/backend/src/activitypub/objects'
import { addObjectInOutbox } from 'wildebeest/backend/src/activitypub/actors/outbox'
import * as timelines from 'wildebeest/backend/src/mastodon/timeline'
const userKEK = 'test_kek4'
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
@ -1036,5 +1037,130 @@ describe('Mastodon APIs', () => {
assert.equal(res.status, 422)
assertJSON(res)
})
test('create status with direct visibility', async () => {
const db = await makeDB()
const queue = makeQueue()
const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const actor1 = await createPerson(domain, db, userKEK, 'actor1@cloudflare.com')
const actor2 = await createPerson(domain, db, userKEK, 'actor2@cloudflare.com')
let deliveredActivity1: any = null
let deliveredActivity2: any = null
globalThis.fetch = async (input: RequestInfo | Request) => {
if (
input.toString() === 'https://cloudflare.com/.well-known/webfinger?resource=acct%3Aactor1%40cloudflare.com'
) {
return new Response(
JSON.stringify({
links: [
{
rel: 'self',
type: 'application/activity+json',
href: actor1.id,
},
],
})
)
}
if (
input.toString() === 'https://cloudflare.com/.well-known/webfinger?resource=acct%3Aactor2%40cloudflare.com'
) {
return new Response(
JSON.stringify({
links: [
{
rel: 'self',
type: 'application/activity+json',
href: actor2.id,
},
],
})
)
}
// @ts-ignore
if (input.url === actor1.inbox.toString()) {
deliveredActivity1 = await (input as Request).json()
return new Response()
}
// @ts-ignore
if (input.url === actor2.inbox.toString()) {
deliveredActivity2 = await (input as Request).json()
return new Response()
}
throw new Error('unexpected request to ' + input)
}
const body = {
status: '@actor1 @actor2 hey',
visibility: 'direct',
}
const req = new Request('https://' + domain, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
})
const res = await statuses.handleRequest(req, db, actor, userKEK, queue, cache)
assert.equal(res.status, 200)
assert(deliveredActivity1)
assert(deliveredActivity2)
delete deliveredActivity1.id
delete deliveredActivity2.id
assert.deepEqual(deliveredActivity1, deliveredActivity2)
assert.equal(deliveredActivity1.to.length, 2)
assert.equal(deliveredActivity1.to[0], actor1.id.toString())
assert.equal(deliveredActivity1.to[1], actor2.id.toString())
assert.equal(deliveredActivity1.cc.length, 0)
// ensure that the private note doesn't show up in public timeline
const timeline = await timelines.getPublicTimeline(domain, db, timelines.LocalPreference.NotSet)
assert.equal(timeline.length, 0)
})
test('create status with unlisted visibility', async () => {
const db = await makeDB()
const queue = makeQueue()
const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const body = {
status: 'something nice',
visibility: 'unlisted',
}
const req = new Request('https://' + domain, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
})
const res = await statuses.handleRequest(req, db, actor, userKEK, queue, cache)
assert.equal(res.status, 422)
assertJSON(res)
})
test('create status with private visibility', async () => {
const db = await makeDB()
const queue = makeQueue()
const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
const body = {
status: 'something nice',
visibility: 'private',
}
const req = new Request('https://' + domain, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
})
const res = await statuses.handleRequest(req, db, actor, userKEK, queue, cache)
assert.equal(res.status, 422)
assertJSON(res)
})
})
})

Wyświetl plik

@ -2,7 +2,7 @@ import { strict as assert } from 'node:assert/strict'
import { createReply } from 'wildebeest/backend/test/shared.utils'
import { createImage } from 'wildebeest/backend/src/activitypub/objects/image'
import { addFollowing, acceptFollowing } from 'wildebeest/backend/src/mastodon/follow'
import { createPublicNote, createPrivateNote } from 'wildebeest/backend/src/activitypub/objects/note'
import { createPublicNote, createDirectNote } from 'wildebeest/backend/src/activitypub/objects/note'
import { addObjectInOutbox } from 'wildebeest/backend/src/activitypub/actors/outbox'
import { createPerson } from 'wildebeest/backend/src/activitypub/actors'
import { makeDB, assertCORS, assertJSON, makeCache } from '../utils'
@ -67,7 +67,7 @@ describe('Mastodon APIs', () => {
await acceptFollowing(db, actor3, actor2)
// actor2 sends a DM to actor1
const note = await createPrivateNote(domain, db, 'DM', actor2, actor1)
const note = await createDirectNote(domain, db, 'DM', actor2, [actor1])
await addObjectInOutbox(db, actor2, note, undefined, actor1.id.toString())
// actor3 shouldn't see the private note
@ -100,7 +100,7 @@ describe('Mastodon APIs', () => {
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)
const note = await createDirectNote(domain, db, 'DM', actor2, [actor1])
await addObjectInOutbox(db, actor2, note, undefined, actor1.id.toString())
const data = await timelines.getPublicTimeline(domain, db, timelines.LocalPreference.NotSet)

Wyświetl plik

@ -8,7 +8,7 @@ import * as timeline from 'wildebeest/backend/src/mastodon/timeline'
import type { Queue, DeliverMessageBody } from 'wildebeest/backend/src/types/queue'
import type { Document } from 'wildebeest/backend/src/activitypub/objects'
import { getObjectByMastodonId } from 'wildebeest/backend/src/activitypub/objects'
import { createStatus, getMentions } from 'wildebeest/backend/src/mastodon/status'
import { getMentions } from 'wildebeest/backend/src/mastodon/status'
import { getHashtags, insertHashtags } from 'wildebeest/backend/src/mastodon/hashtag'
import * as activities from 'wildebeest/backend/src/activitypub/activities/create'
import type { Env } from 'wildebeest/backend/src/types/env'
@ -27,6 +27,8 @@ import * as idempotency from 'wildebeest/backend/src/mastodon/idempotency'
import { newMention } from 'wildebeest/backend/src/activitypub/objects/mention'
import { originalObjectIdSymbol } from 'wildebeest/backend/src/activitypub/objects'
import { type Database, getDatabase } from 'wildebeest/backend/src/database'
import { createPublicNote, createDirectNote } from 'wildebeest/backend/src/activitypub/objects/note'
import { addObjectInOutbox } from 'wildebeest/backend/src/activitypub/actors/outbox'
type StatusCreate = {
status: string
@ -118,7 +120,16 @@ export async function handleRequest(
}
const content = enrichStatus(body.status, mentions)
const note = await createStatus(domain, db, connectedActor, content, mediaAttachments, extraProperties)
let note
if (body.visibility === 'public') {
note = await createPublicNote(domain, db, content, connectedActor, mediaAttachments, extraProperties)
} else if (body.visibility === 'direct') {
note = await createDirectNote(domain, db, content, connectedActor, mentions, mediaAttachments, extraProperties)
} else {
return errors.validationError(`status with visibility: ${body.visibility}`)
}
if (hashtags.length > 0) {
await insertHashtags(db, note, hashtags)
@ -132,11 +143,27 @@ export async function handleRequest(
const activity = activities.create(domain, connectedActor, note)
await deliverFollowers(db, userKEK, connectedActor, activity, queue)
if (body.visibility === 'public') {
await addObjectInOutbox(db, connectedActor, note)
// A public note is sent to the public group URL and cc'ed any mentioned
// actors.
for (let i = 0, len = mentions.length; i < len; i++) {
const targetActor = mentions[i]
note.cc.push(targetActor.id.toString())
}
} else if (body.visibility === 'direct') {
// A direct note is sent to mentioned people only
for (let i = 0, len = mentions.length; i < len; i++) {
const targetActor = mentions[i]
await addObjectInOutbox(db, connectedActor, note, undefined, targetActor.id.toString())
}
}
{
// If the status is mentioning other persons, we need to delivery it to them.
for (let i = 0, len = mentions.length; i < len; i++) {
const targetActor = mentions[i]
note.cc.push(targetActor.id.toString())
const activity = activities.create(domain, connectedActor, note)
const signingKey = await getSigningKey(userKEK, db, connectedActor)
await deliverToActor(signingKey, connectedActor, targetActor, activity, domain)