kopia lustrzana https://github.com/cloudflare/wildebeest
Merge pull request #336 from cloudflare/sven/status-visiblity
support direct visiblity and reject otherspull/338/head
commit
a815ccdf28
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Ładowanie…
Reference in New Issue