Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

setup announcement actor, add endpoint for actor #42

Merged
merged 52 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
5320e9f
wip: setup announcement actor, add endpoint for actor
catdevnull Feb 13, 2024
5f04cc2
WIP: announcements post script
catdevnull Feb 13, 2024
3d25d1a
WIP
catdevnull Feb 22, 2024
58db44c
chore(vscode): always use ts-standard
catdevnull Feb 22, 2024
5687b42
announcements: add endpoints for outbox
catdevnull Feb 22, 2024
0051dea
chore: add-post-to-announcements
catdevnull Feb 22, 2024
1deddd8
chore: clean up
catdevnull Feb 22, 2024
7ed3030
feat: announce on creation
catdevnull Feb 22, 2024
58600ad
fix: correct http-signed-fetch types and related changes
catdevnull Feb 23, 2024
165557d
fix(client): remove trailing slash to setInfo endpoint
catdevnull Feb 23, 2024
f9406ed
fix: only announce if doesn't exist already
catdevnull Feb 23, 2024
3e41d81
chore: add basic client CLI
catdevnull Feb 23, 2024
1abce31
fix: try to get announcements inbox working
catdevnull Feb 23, 2024
261d120
fix: missing publicKeyId
catdevnull Feb 23, 2024
18db375
Update src/server/api/announcements.ts
catdevnull Feb 29, 2024
e8eb6c7
chore: remove TODO comment
catdevnull Feb 29, 2024
c646585
fix: sort announcements outbox by latest first
catdevnull Feb 29, 2024
fbb6006
fix: move announcements stuff to APSystem subclass, fix stuff
catdevnull Feb 29, 2024
246a1b5
chore: fix linting errors
catdevnull Feb 29, 2024
31e9b2d
fix: remove trailing slash everywhere
catdevnull Mar 4, 2024
1997e48
feat: add webfinger route
catdevnull Mar 4, 2024
59ad921
perf: check if actor already exists using `.get`
catdevnull Mar 6, 2024
a6b1fa7
chore: move outbox getter to Announcements class
catdevnull Mar 6, 2024
7a334a9
delete cli
catdevnull Mar 7, 2024
83c7488
delete client API CLI
catdevnull Mar 7, 2024
7aa12c5
fix: setup announcements actor as a normal "mention" actor
catdevnull Mar 7, 2024
551f2f5
fix: make getActor sync
catdevnull Mar 8, 2024
1f0a840
fix: generate correct mention for announcements actor
catdevnull Mar 8, 2024
f801a7a
fix: add missing fields to announcements actor activitypub
catdevnull Mar 8, 2024
0823e0f
fix: do not error when trying to announce non-existant actor
catdevnull Mar 11, 2024
b0d1dc9
chore: add test for announcements
catdevnull Mar 11, 2024
32bff3a
fix: correctly check actor presence
catdevnull Mar 11, 2024
0676758
fix: try announcing new actor before creating it
catdevnull Mar 13, 2024
a82ff74
fix: use actual DB to check if actor exists already
catdevnull Mar 13, 2024
c5c2516
fix: use announcements actor id for activity
catdevnull Mar 13, 2024
b4184a3
fix: make tests pass
catdevnull Mar 13, 2024
613d0d1
fix: prevent race condition and correctly attribute announcement acti…
catdevnull Mar 13, 2024
fa1cd98
fix: wrap announcement Note in a Create
catdevnull Mar 13, 2024
5b4e28e
fix: unused
catdevnull Mar 13, 2024
fdf3706
Fix ts error in announcement test
RangerMauve Mar 13, 2024
debdd54
Implement ActorStore.delete()
RangerMauve Mar 13, 2024
056279e
Store note separate, use mention syntax to tag new account
RangerMauve Mar 13, 2024
87de7aa
Fix linting, move actor definition into announcements
RangerMauve Mar 13, 2024
e0cfec2
Fix linting, move actor definition into announcements
RangerMauve Mar 13, 2024
1e22903
progress on fixing announcement tests
RangerMauve Mar 13, 2024
9816374
chore: move APActorNonStandard into schemas.ts
catdevnull Mar 15, 2024
600f074
fix: do not include Notes in outbox
catdevnull Mar 16, 2024
fb1fb77
fix announcements tests
RangerMauve Mar 20, 2024
13a632b
Formalize auto-accepting follow requests
RangerMauve Mar 20, 2024
2f97d57
Better ts lint friendly auto approve check
RangerMauve Mar 20, 2024
a61a483
Better ts lint friendly announce check
RangerMauve Mar 20, 2024
8a5af82
Better ts lint friendly announce check
RangerMauve Mar 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"standard.enable": true,
"standard.autoFixOnSave": true,
"standard.engine": "ts-standard",
"[typescript]": {
"editor.defaultFormatter": ""
}
Expand Down
1 change: 0 additions & 1 deletion @types/http-signed-fetch.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,5 @@ declare module 'http-signed-fetch' {
export function generateKeypair (): {
publicKeyPem: string
privateKeyPem: string
publicKeyId: string
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"generate-identity": "ts-node-esm src/scripts/generate-identity.ts",
"import-blocklist": "ts-node-esm src/scripts/import-blocklist.ts",
"import-admins": "ts-node-esm src/scripts/import-admins.ts",
"client-cli": "ts-node-esm src/client/cli.ts",
"lint": "ts-standard --fix && tsc --noEmit",
"dev": "ts-node-esm src/bin.ts run | pino-pretty -c -t",
"start": "node dist/bin.js run | pino-pretty -c -t",
Expand Down
2 changes: 1 addition & 1 deletion src/client/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ test('Local Server Communication', async t => {
const client = new SocialInboxClient({
instance: `http://localhost:${port}`,
account: 'testAccount',
keypair: generateKeypair(),
keypair: { ...generateKeypair(), publicKeyId: 'testAccount#main-key' },
fetch: globalThis.fetch
})

Expand Down
10 changes: 7 additions & 3 deletions src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ export type SignedFetchLike = (
init?: RequestInit & { publicKeyId: string, keypair: KeyPair }
) => Promise<Response>

type Keypair = ReturnType<typeof generateKeypair> & {
publicKeyId: string
}

export interface SocialInboxOptions {
instance: string
account: string
keypair: ReturnType<typeof generateKeypair>
keypair: Keypair
fetch?: SignedFetchLike
}

Expand Down Expand Up @@ -40,7 +44,7 @@ type VALID_TYPES = typeof TYPE_TEXT | typeof TYPE_JSON | typeof TYPE_LDJSON | un
export class SocialInboxClient {
instance: string
account: string
keypair: ReturnType<typeof generateKeypair>
keypair: Keypair
fetch: SignedFetchLike

constructor (options: SocialInboxOptions) {
Expand Down Expand Up @@ -83,7 +87,7 @@ export class SocialInboxClient {

// Actorinfo
async setActorInfo (info: ActorInfo, actor: string = this.account): Promise<void> {
await this.sendRequest(POST, `/${actor}/`, TYPE_JSON, info)
await this.sendRequest(POST, `/${actor}`, TYPE_JSON, info)
}

async getActorInfo (actor: string = this.account): Promise<ActorInfo> {
Expand Down
3 changes: 2 additions & 1 deletion src/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ export const ActorInfoSchema = Type.Object({
// The actor for the domain inbox
actorUrl: Type.String(),
publicKeyId: Type.String(),
keypair: KeyPairSchema
keypair: KeyPairSchema,
announce: Type.Boolean({ default: false })
})

export type ActorInfo = Static<typeof ActorInfoSchema>
46 changes: 46 additions & 0 deletions src/server/announcements.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import test from 'ava'
import sinon from 'sinon'
import ActivityPubSystem, { FetchLike } from './apsystem'
import Store from './store/index.js'
import { ModerationChecker } from './moderation.js'
import HookSystem from './hooksystem'
import { MemoryLevel } from 'memory-level'

// Create some mock dependencies
const mockStore = new Store(new MemoryLevel())

const mockModCheck = new ModerationChecker(mockStore)
const mockFetch: FetchLike = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
return new Response(JSON.stringify({}), { status: 200 })
}
const mockHooks = new HookSystem(mockStore, mockFetch)

const aps = new ActivityPubSystem('http://localhost', mockStore, mockModCheck, mockHooks)

test.beforeEach(async () => {
// Restore stubs before setting them up again
sinon.restore()

await aps.announcements.init()
})

const keypair = {
publicKeyPem: 'mockPublicKey',
privateKeyPem: 'mockPrivateKey',
publicKeyId: 'mockPublicKeyId'
}

test('actor gets announced on .announce', async t => {
const fakeActor = '@test@url'
const actorInfo = {
actorUrl: 'https://url/@test',
announce: true,
keypair,
publicKeyId: keypair.publicKeyId
}
await aps.announcements.announce(fakeActor, actorInfo)
await aps.store.forActor(fakeActor).setInfo(actorInfo)

const outbox = await aps.store.forActor('@announcements@localhost').outbox.list()
t.is(outbox.length, 1, 'something is in outbox')
})
107 changes: 107 additions & 0 deletions src/server/announcements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { nanoid } from 'nanoid'
import { ActorInfo } from '../schemas'
import ActivityPubSystem, { DEFAULT_PUBLIC_KEY_FIELD } from './apsystem'
import { generateKeypair } from 'http-signed-fetch'
import { APOrderedCollection } from 'activitypub-types'
import { ActorStore } from './store/ActorStore'

export class Announcements {
apsystem: ActivityPubSystem
publicURL: string

constructor (apsystem: ActivityPubSystem, publicURL: string) {
this.apsystem = apsystem
this.publicURL = publicURL
}

get actorUrl (): string {
return `${this.publicURL}/v1/${this.mention}/`
}

get outboxUrl (): string {
return `${this.actorUrl}outbox`
}

get mention (): string {
const url = new URL(this.publicURL)
return `@announcements@${url.hostname}`
}

getActor (): ActorStore {
return this.apsystem.store.forActor(this.mention)
}

async init (): Promise<void> {
const actorUrl = this.actorUrl
const actor = this.getActor()

try {
const prev = await actor.getInfo()
if (prev.actorUrl !== actorUrl) {
await actor.setInfo({
...prev,
actorUrl
})
}
} catch {
const { privateKeyPem, publicKeyPem } = generateKeypair()
await actor.setInfo({
actorUrl,
publicKeyId: `${actorUrl}#${DEFAULT_PUBLIC_KEY_FIELD}`,
keypair: {
privateKeyPem,
publicKeyPem
},
announce: false
})
}
}

async announce (actor: string, info: ActorInfo): Promise<void> {
let existedAlready = false
try {
const existingActor = await this.apsystem.store.forActor(actor).getInfo()
if (existingActor !== undefined) existedAlready = true
} catch (err) {
if (!(err as { notFound: boolean }).notFound) {
throw err
}
}

if (!existedAlready && info.announce) {
const activity = {
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Note',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ActivityStreams hold activities, not Notes. This should be wrapped in a Create activity with the Note being the object.

e.g. here's how we link to notes in the staticpub example: https://github.com/RangerMauve/staticpub.mauve.moe/blob/default/outbox.jsonld#L13

We may need to store the notes in a separate store and add a new /:actor/note/:id URL to resolve them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a requirement in the spec that states that OrderedCollections must hold Activitys. Actually, I was mostly copying off of Sutty's Jekyll implementation, which has plain Updates inside the outbox:

Am I getting something wrong?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, notice how the update you linked wraps the note in an Update activity. We should do the same here, but with a Create.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OH lol i thought Update was similar to a Note. But the ID there is just a link to the post, so do we need an endpoint for the individual Note?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might, lets see if just wrapping is enough for now though.

id: `${this.outboxUrl}/${nanoid()}`,
actor: this.actorUrl,
attributedTo: info.actorUrl,
catdevnull marked this conversation as resolved.
Show resolved Hide resolved
published: new Date().toUTCString(),
to: ['https://www.w3.org/ns/activitystreams#Public'],
cc: [`${this.actorUrl}followers`],
// TODO: add a template in config
content: `a wild site appears! ${actor}`
}
await this.getActor().outbox.add(activity)
await this.apsystem.notifyFollowers(this.mention, activity)
}
}

async getOutbox (): Promise<APOrderedCollection> {
const actor = this.getActor()
const activities = await actor.outbox.list()
const orderedItems = activities
// XXX: maybe `new Date()` doesn't correctly parse possible dates?
.map(a => ({ ...a, published: typeof a.published === 'string' ? new Date(a.published) : a.published }))
.sort((a, b) => +(b.published ?? 0) - +(a.published ?? 0))
.map(a => a.id)
.filter((id): id is string => id !== undefined)

return {
'@context': 'https://www.w3.org/ns/activitystreams',
id: `${this.actorUrl}outbox`,
type: 'OrderedCollection',
totalItems: orderedItems.length,
orderedItems
}
}
}
102 changes: 102 additions & 0 deletions src/server/api/announcements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { APActor } from 'activitypub-types'

import type { APIConfig, FastifyTypebox } from '.'
import Store from '../store'
import type ActivityPubSystem from '../apsystem'
import { Type } from '@sinclair/typebox'

type APActorNonStandard = APActor & {
publicKey: {
id: string
owner: string
publicKeyPem: string
}
}

export const announcementsRoutes = (cfg: APIConfig, store: Store, apsystem: ActivityPubSystem) => async (server: FastifyTypebox): Promise<void> => {
server.get<{
Reply: APActorNonStandard
}>(`/${apsystem.announcements.mention}/`, {
schema: {
params: {},
// XXX: even with Type.Any(), the endpoint returns `{}` :/
// response: {
// // TODO: typebox APActor
// 200: Type.Any()
// },
description: 'Announcements ActivityPub actor',
tags: ['ActivityPub']
}
}, async (request, reply) => {
const actor = apsystem.announcements.getActor()
const actorInfo = await actor.getInfo()

return await reply.send({
'@context': [
// TODO: I copied this from Mastodon, is this correct?
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1'
],
// https://www.w3.org/TR/activitystreams-vocabulary/#actor-types
id: apsystem.announcements.actorUrl,
type: 'Person',
name: 'Announcements',
summary: `Announcements for ${new URL(cfg.publicURL).hostname}`,
preferredUsername: 'Announcements',
following: `${actorInfo.actorUrl}following`,
followers: `${actorInfo.actorUrl}followers`,
inbox: `${actorInfo.actorUrl}inbox`,
outbox: `${actorInfo.actorUrl}outbox`,
publicKey: {
id: `${actorInfo.actorUrl}#main-key`,

owner: actorInfo.actorUrl,
publicKeyPem: actorInfo.keypair.publicKeyPem
}
})
})

server.get<{
// TODO: typebox APOrderedCollection
Reply: any
}>(`/${apsystem.announcements.mention}/outbox`, {
schema: {
params: {},
// XXX: even with Type.Any(), the endpoint returns `{}` :/
// response: {
// // TODO: typebox APOrderedCollection
// 200: Type.Any()
// },
description: 'Announcements ActivityPub outbox',
tags: ['ActivityPub']
}
}, async (request, reply) => {
return await reply.send(await apsystem.announcements.getOutbox())
})

server.get<{
Params: {
id: string
}
// TODO: typebox APOrderedCollection
Reply: any
}>(`/${apsystem.announcements.mention}/outbox/:id`, {
schema: {
params: Type.Object({
id: Type.String()
}),
// XXX: even with Type.Any(), the endpoint returns `{}` :/
// response: {
// // TODO: typebox APOrderedCollection
// 200: Type.Any()
// },
description: 'Announcements ActivityPub get activity',
tags: ['ActivityPub']
}
}, async (request, reply) => {
const actor = apsystem.announcements.getActor()
const actorInfo = await actor.getInfo()
const activity = await actor.outbox.get(`${actorInfo.actorUrl}outbox/${request.params.id}`)
return await reply.send(activity)
})
}
2 changes: 2 additions & 0 deletions src/server/api/creation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ export const creationRoutes = (cfg: APIConfig, store: Store, apsystem: ActivityP
}

const info = request.body
await apsystem.announcements.announce(actor, info)
catdevnull marked this conversation as resolved.
Show resolved Hide resolved
await store.forActor(actor).setInfo(info)
catdevnull marked this conversation as resolved.
Show resolved Hide resolved

return await reply.send(info)
})

Expand Down
6 changes: 6 additions & 0 deletions src/server/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import { blockAllowListRoutes } from './blockallowlist.js'
import { adminRoutes } from './admins.js'
import { followerRoutes } from './followers.js'
import { hookRoutes } from './hooks.js'
import { announcementsRoutes } from './announcements.js'
import { wellKnownRoutes } from './wellKnown.js'

export const paths = envPaths('distributed-press')

Expand Down Expand Up @@ -94,7 +96,10 @@ async function apiBuilder (cfg: APIConfig): Promise<FastifyTypebox> {
return 'ok\n'
})

await apsystem.announcements.init()

await server.register(v1Routes(cfg, store, apsystem, hookSystem), { prefix: '/v1' })
await server.register(wellKnownRoutes(cfg, store, apsystem))

await server.ready()

Expand Down Expand Up @@ -123,6 +128,7 @@ const v1Routes = (cfg: APIConfig, store: Store, apsystem: ActivityPubSystem, hoo
})
}

await server.register(announcementsRoutes(cfg, store, apsystem))
await server.register(creationRoutes(cfg, store, apsystem))
await server.register(inboxRoutes(cfg, store, apsystem))
await server.register(outboxRoutes(cfg, store, apsystem))
Expand Down
Loading
Loading