-
Notifications
You must be signed in to change notification settings - Fork 3
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
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 5f04cc2
WIP: announcements post script
catdevnull 3d25d1a
WIP
catdevnull 58db44c
chore(vscode): always use ts-standard
catdevnull 5687b42
announcements: add endpoints for outbox
catdevnull 0051dea
chore: add-post-to-announcements
catdevnull 1deddd8
chore: clean up
catdevnull 7ed3030
feat: announce on creation
catdevnull 58600ad
fix: correct http-signed-fetch types and related changes
catdevnull 165557d
fix(client): remove trailing slash to setInfo endpoint
catdevnull f9406ed
fix: only announce if doesn't exist already
catdevnull 3e41d81
chore: add basic client CLI
catdevnull 1abce31
fix: try to get announcements inbox working
catdevnull 261d120
fix: missing publicKeyId
catdevnull 18db375
Update src/server/api/announcements.ts
catdevnull e8eb6c7
chore: remove TODO comment
catdevnull c646585
fix: sort announcements outbox by latest first
catdevnull fbb6006
fix: move announcements stuff to APSystem subclass, fix stuff
catdevnull 246a1b5
chore: fix linting errors
catdevnull 31e9b2d
fix: remove trailing slash everywhere
catdevnull 1997e48
feat: add webfinger route
catdevnull 59ad921
perf: check if actor already exists using `.get`
catdevnull a6b1fa7
chore: move outbox getter to Announcements class
catdevnull 7a334a9
delete cli
catdevnull 83c7488
delete client API CLI
catdevnull 7aa12c5
fix: setup announcements actor as a normal "mention" actor
catdevnull 551f2f5
fix: make getActor sync
catdevnull 1f0a840
fix: generate correct mention for announcements actor
catdevnull f801a7a
fix: add missing fields to announcements actor activitypub
catdevnull 0823e0f
fix: do not error when trying to announce non-existant actor
catdevnull b0d1dc9
chore: add test for announcements
catdevnull 32bff3a
fix: correctly check actor presence
catdevnull 0676758
fix: try announcing new actor before creating it
catdevnull a82ff74
fix: use actual DB to check if actor exists already
catdevnull c5c2516
fix: use announcements actor id for activity
catdevnull b4184a3
fix: make tests pass
catdevnull 613d0d1
fix: prevent race condition and correctly attribute announcement acti…
catdevnull fa1cd98
fix: wrap announcement Note in a Create
catdevnull 5b4e28e
fix: unused
catdevnull fdf3706
Fix ts error in announcement test
RangerMauve debdd54
Implement ActorStore.delete()
RangerMauve 056279e
Store note separate, use mention syntax to tag new account
RangerMauve 87de7aa
Fix linting, move actor definition into announcements
RangerMauve e0cfec2
Fix linting, move actor definition into announcements
RangerMauve 1e22903
progress on fixing announcement tests
RangerMauve 9816374
chore: move APActorNonStandard into schemas.ts
catdevnull 600f074
fix: do not include Notes in outbox
catdevnull fb1fb77
fix announcements tests
RangerMauve 13a632b
Formalize auto-accepting follow requests
RangerMauve 2f97d57
Better ts lint friendly auto approve check
RangerMauve a61a483
Better ts lint friendly announce check
RangerMauve 8a5af82
Better ts lint friendly announce check
RangerMauve File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
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 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.