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 all 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
13 changes: 12 additions & 1 deletion src/schemas.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import { Type, Static } from '@sinclair/typebox'
import { KeyPairSchema } from './keypair.js'
import { APActor } from 'activitypub-types'

export const ActorInfoSchema = Type.Object({
// The actor for the domain inbox
actorUrl: Type.String(),
publicKeyId: Type.String(),
keypair: KeyPairSchema
keypair: KeyPairSchema,
announce: Type.Optional(Type.Boolean({ default: false })),
manuallyApprovesFollowers: Type.Optional(Type.Boolean({ default: false }))
})

export type ActorInfo = Static<typeof ActorInfoSchema>

export type APActorNonStandard = APActor & {
publicKey: {
id: string
owner: string
publicKeyPem: string
}
}
48 changes: 48 additions & 0 deletions src/server/announcements.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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()

sinon.stub(aps, 'mentionToActor').returns(Promise.resolve('http://actor.url'))

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)
await aps.store.forActor(fakeActor).setInfo(actorInfo)

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

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}`
}

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

async getActor (): Promise<APActorNonStandard> {
const actorInfo = await this.store.getInfo()
return {
'@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: this.actorUrl,
type: 'Person',
name: 'Announcements',
summary: `Announcements for ${new URL(this.actorUrl).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
}
}
}

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

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,
manuallyApprovesFollowers: false
})
}
}

async announce (actor: string): Promise<void> {
const actorUrl = await this.apsystem.mentionToActor(actor)
const published = new Date().toUTCString()
const to = ['https://www.w3.org/ns/activitystreams#Public']
const cc = [`${this.actorUrl}followers`, actorUrl]

const mentionText = `<span class="h-card"><a href="${actorUrl}" class="u-url mention">${actor}</a></span>`

const note = {
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Note',
id: `${this.outboxUrl}/${nanoid()}`,
attributedTo: this.actorUrl,
published,
to,
cc,
// TODO: add a template in config
content: `A wild ${mentionText} appears!`,
tag: [{ type: 'Mention', href: actorUrl, name: actor }]
}
const activity = {
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Create',
id: `${this.outboxUrl}/${nanoid()}`,
actor: this.actorUrl,
published,
to,
cc,
object: note
}
await this.store.outbox.add(activity)
await this.store.outbox.add(note)
catdevnull marked this conversation as resolved.
Show resolved Hide resolved
await this.apsystem.notifyFollowers(this.mention, activity)
}

async getOutbox (): Promise<APOrderedCollection> {
const actor = this.store
const activities = await actor.outbox.list()
const orderedItems = activities
.filter(a => a.type !== 'Note')
// 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
}
}
}
69 changes: 69 additions & 0 deletions src/server/api/announcements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { APIConfig, FastifyTypebox } from '.'
import Store from '../store'
import type ActivityPubSystem from '../apsystem'
import type { APActorNonStandard } from '../../schemas'
import { Type } from '@sinclair/typebox'

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 = await apsystem.announcements.getActor()

return await reply.send(actor)
})

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 actorUrl = apsystem.announcements.actorUrl
const activity = await apsystem.announcements.store.outbox.get(`${actorUrl}outbox/${request.params.id}`)
return await reply.send(activity)
})
}
14 changes: 14 additions & 0 deletions src/server/api/creation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,22 @@ export const creationRoutes = (cfg: APIConfig, store: Store, apsystem: ActivityP
return await reply.code(403).send('Not Allowed')
}

let existedAlready = false
try {
const existingActor = await apsystem.store.forActor(actor).getInfo()
if (existingActor !== undefined) existedAlready = true
} catch (err) {
if (!(err as { notFound: boolean }).notFound) {
throw err
}
}

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

const shouldAnnounce = !existedAlready && info.announce === true
if (shouldAnnounce) await apsystem.announcements.announce(actor)

return await reply.send(info)
})

Expand Down
Loading
Loading