From 2ce4cd3bc4fc54dd4e33e4c4f6a978cb8e07db1b Mon Sep 17 00:00:00 2001 From: Michael Barrett Date: Fri, 30 Aug 2024 13:29:43 +0100 Subject: [PATCH] wip --- src/_/Entity.ts | 3 ++ src/_/README.md | 60 +++++++++++++++++++++++ src/_/Repository.ts | 8 ++++ src/_/db.png | Bin 0 -> 52113 bytes src/_/entity/ActivityEntity.ts | 32 +++++++++++++ src/_/entity/ActorEntity.ts | 20 ++++++++ src/_/entity/ObjectEntity.ts | 20 ++++++++ src/_/entity/SiteEntity.ts | 20 ++++++++ src/_/repository/ActivityRepository.ts | 40 ++++++++++++++++ src/_/repository/ActorRepository.ts | 34 +++++++++++++ src/_/repository/InboxRepository.ts | 34 +++++++++++++ src/_/repository/ObjectRepository.ts | 34 +++++++++++++ src/_/repository/SiteRepository.ts | 18 +++++++ src/_/service/ActivityService.ts | 59 +++++++++++++++++++++++ src/_/service/ActorService.ts | 27 +++++++++++ src/_/service/InboxService.ts | 26 ++++++++++ src/_/service/ObjectService.ts | 28 +++++++++++ src/_/service/SiteService.ts | 18 +++++++ src/app.ts | 48 +++++++++++++++++-- src/db.ts | 48 +++++++++++++++++++ src/dispatchers.ts | 57 ++++++++++++++++------ src/handlers.ts | 63 ++++++++++++++----------- 22 files changed, 650 insertions(+), 47 deletions(-) create mode 100644 src/_/Entity.ts create mode 100644 src/_/README.md create mode 100644 src/_/Repository.ts create mode 100644 src/_/db.png create mode 100644 src/_/entity/ActivityEntity.ts create mode 100644 src/_/entity/ActorEntity.ts create mode 100644 src/_/entity/ObjectEntity.ts create mode 100644 src/_/entity/SiteEntity.ts create mode 100644 src/_/repository/ActivityRepository.ts create mode 100644 src/_/repository/ActorRepository.ts create mode 100644 src/_/repository/InboxRepository.ts create mode 100644 src/_/repository/ObjectRepository.ts create mode 100644 src/_/repository/SiteRepository.ts create mode 100644 src/_/service/ActivityService.ts create mode 100644 src/_/service/ActorService.ts create mode 100644 src/_/service/InboxService.ts create mode 100644 src/_/service/ObjectService.ts create mode 100644 src/_/service/SiteService.ts diff --git a/src/_/Entity.ts b/src/_/Entity.ts new file mode 100644 index 000000000..484008027 --- /dev/null +++ b/src/_/Entity.ts @@ -0,0 +1,3 @@ +export interface Entity { + serialize(): DTO +} diff --git a/src/_/README.md b/src/_/README.md new file mode 100644 index 000000000..ec64fb623 --- /dev/null +++ b/src/_/README.md @@ -0,0 +1,60 @@ +# ActivityPub Domain + +## Architecture + +### Service + +A service provides functionality for the application layer. Common operations include: + - Retrieving data from the repository layer and constructing entities + +### Entity + +An entity is a representation of a real-world object or concept. The service layer is responsible for +the creation of entities. Entities should define how they are serialized to a DTO. + +### Repository + +A repository provides functionality for interacting with a database. It should not be used directly by the application layer. Instead, it should be used by the service layer. DTOs are used to pass data between the repository and the service layer. + +### DTO + +A DTO is a data transfer object. It is used to pass data between the repository and the service layer. + +## Database + +DB Schema + +- `sites` - Information about each site that utilises the service +- `actors` - ActivityPub actors associated with objects and activities +- `objects` - ActivityPub objects associated with activities +- `activities` - ActivityPub activities + - Pivot table that references `actors` and `objects` +- `inbox` - Received activities for an actor + - Pivot table that references `actors` and `activities` +- ... + +## FAQs + +### Why is actvities a pivot table? + +ActivityPub activities largely follow the same structure: + +```json +{ + "id": "...", + "type": "...", + "actor": {}, + "object": {}, + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/data-integrity/v1" + ] +} +``` + +Rather than storing repated actor and object data, we can store a single actor and +object entity and reference them in the activity. This reduces the amount of data +that needs to be stored and allows for easier querying and indexing. This also makes +it easier to keeper actor data up-to-date, as we only need to update the data in one +place as well as allowing multiple activities to reference the same actor or object +without having to duplicate the data. diff --git a/src/_/Repository.ts b/src/_/Repository.ts new file mode 100644 index 000000000..1d65776cc --- /dev/null +++ b/src/_/Repository.ts @@ -0,0 +1,8 @@ +import { Knex } from 'knex' + +export abstract class Repository { + constructor( + protected readonly db: Knex, + protected readonly tableName: string, + ) {} +} diff --git a/src/_/db.png b/src/_/db.png new file mode 100644 index 0000000000000000000000000000000000000000..f93386a9d264a3708e748b9f51787b1c0633d1dc GIT binary patch literal 52113 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 Ky>&Uxi}nvOK^9Q} literal 0 HcmV?d00001 diff --git a/src/_/entity/ActivityEntity.ts b/src/_/entity/ActivityEntity.ts new file mode 100644 index 000000000..3d236ad19 --- /dev/null +++ b/src/_/entity/ActivityEntity.ts @@ -0,0 +1,32 @@ +import { type Entity } from '../Entity' +import { ActorEntity } from './ActorEntity' +import { ObjectEntity } from './ObjectEntity' + +export enum ActivityType { + LIKE = 'Like' +} + +export type ActivityDTO = { + id: string + type: ActivityType + actor_id: string + object_id: string +} + +export class ActivityEntity implements Entity { + constructor( + readonly id: string, + readonly type: ActivityType, + readonly actor: ActorEntity, + readonly object: ObjectEntity, + ) {} + + serialize() { + return { + id: this.id, + type: this.type, + actor_id: this.actor.id, + object_id: this.object.id, + } + } +} diff --git a/src/_/entity/ActorEntity.ts b/src/_/entity/ActorEntity.ts new file mode 100644 index 000000000..e0fa7bdef --- /dev/null +++ b/src/_/entity/ActorEntity.ts @@ -0,0 +1,20 @@ +import { type Entity } from '../Entity' + +export type ActorDTO = { + id: string + data: JSON +} + +export class ActorEntity implements Entity { + constructor( + readonly id: string, + readonly data: JSON, + ) {} + + serialize() { + return { + id: this.id, + data: this.data, + } + } +} diff --git a/src/_/entity/ObjectEntity.ts b/src/_/entity/ObjectEntity.ts new file mode 100644 index 000000000..68d2e4bf2 --- /dev/null +++ b/src/_/entity/ObjectEntity.ts @@ -0,0 +1,20 @@ +import { type Entity } from '../Entity' + +export type ObjectDTO = { + id: string + data: JSON +}; + +export class ObjectEntity implements Entity { + constructor( + readonly id: string, + readonly data: JSON, + ) {} + + serialize() { + return { + id: this.id, + data: this.data, + } + } +} diff --git a/src/_/entity/SiteEntity.ts b/src/_/entity/SiteEntity.ts new file mode 100644 index 000000000..397656085 --- /dev/null +++ b/src/_/entity/SiteEntity.ts @@ -0,0 +1,20 @@ +import { type Entity } from '../Entity' + +export type SiteDTO = { + id: number + hostname: string +} + +export class SiteEntity implements Entity { + constructor( + readonly id: number, + readonly hostname: string, + ) {} + + serialize() { + return { + id: this.id, + hostname: this.hostname, + } + } +} diff --git a/src/_/repository/ActivityRepository.ts b/src/_/repository/ActivityRepository.ts new file mode 100644 index 000000000..5a648edc3 --- /dev/null +++ b/src/_/repository/ActivityRepository.ts @@ -0,0 +1,40 @@ +import { Knex } from 'knex' + +import { Repository } from '../Repository' +import { type ActivityDTO } from '../entity/ActivityEntity' + +export class ActivityRepository extends Repository { + constructor( + db: Knex, + ) { + super(db, 'activities') + } + + async findById(id: string): Promise { + const result = await this.db(this.tableName).where('id', id).first() + + return result ?? null + } + + async findByIds(ids: string[]): Promise { + const results = await this.db(this.tableName).whereIn('id', ids) + + return results + } + + async create(data: ActivityDTO): Promise { + const result = await this.db(this.tableName).insert(data) + + if (result.length !== 1) { + throw new Error('Failed to create activity') + } + + const activity = await this.findById(data.id); + + if (!activity) { + throw new Error('Failed to create activity') + } + + return activity + } +} diff --git a/src/_/repository/ActorRepository.ts b/src/_/repository/ActorRepository.ts new file mode 100644 index 000000000..838fa3327 --- /dev/null +++ b/src/_/repository/ActorRepository.ts @@ -0,0 +1,34 @@ +import { Knex } from 'knex' + +import { Repository } from '../Repository' +import { type ActorDTO } from '../entity/ActorEntity' + +export class ActorRepository extends Repository { + constructor( + db: Knex, + ) { + super(db, 'actors') + } + + async findById(id: string): Promise { + const result = await this.db(this.tableName).where('id', id).first() + + return result ?? null + } + + async create(data: ActorDTO): Promise { + const result = await this.db(this.tableName).insert(data) + + if (result.length !== 1) { + throw new Error('Failed to create actor') + } + + const object = await this.findById(data.id); + + if (!object) { + throw new Error('Failed to create actor') + } + + return object + } +} diff --git a/src/_/repository/InboxRepository.ts b/src/_/repository/InboxRepository.ts new file mode 100644 index 000000000..4845edd1f --- /dev/null +++ b/src/_/repository/InboxRepository.ts @@ -0,0 +1,34 @@ +import { Knex } from 'knex' + +import { Repository } from '../Repository' + +type InboxItemDTO = { + site_id: number + actor_id: string + activity_id: string +} + +export class InboxRepository extends Repository { + constructor( + db: Knex, + ) { + super(db, 'inbox') + } + + async findByActorId(actorId: string, siteId: number): Promise { + const results = await this.db(this.tableName) + .where('actor_id', actorId) + .where('site_id', siteId) + .select('*') + + return results + } + + async create(data: InboxItemDTO): Promise { + const result = await this.db(this.tableName).insert(data) + + if (result.length !== 1) { + throw new Error('Failed to create inbox item') + } + } +} diff --git a/src/_/repository/ObjectRepository.ts b/src/_/repository/ObjectRepository.ts new file mode 100644 index 000000000..01b0c82a4 --- /dev/null +++ b/src/_/repository/ObjectRepository.ts @@ -0,0 +1,34 @@ +import { Knex } from 'knex' + +import { Repository } from '../Repository' +import { type ObjectDTO } from '../entity/ObjectEntity' + +export class ObjectRepository extends Repository { + constructor( + db: Knex, + ) { + super(db, 'objects') + } + + async findById(id: string): Promise { + const result = await this.db(this.tableName).where('id', id).first() + + return result ?? null + } + + async create(data: ObjectDTO): Promise { + const result = await this.db(this.tableName).insert(data) + + if (result.length !== 1) { + throw new Error('Failed to create object') + } + + const object = await this.findById(data.id); + + if (!object) { + throw new Error('Failed to create object') + } + + return object + } +} diff --git a/src/_/repository/SiteRepository.ts b/src/_/repository/SiteRepository.ts new file mode 100644 index 000000000..9884e8838 --- /dev/null +++ b/src/_/repository/SiteRepository.ts @@ -0,0 +1,18 @@ +import { Knex } from 'knex' + +import { Repository } from '../Repository' +import { type SiteDTO } from '../entity/SiteEntity' + +export class SiteRepository extends Repository { + constructor( + db: Knex, + ) { + super(db, 'sites') + } + + async findById(id: number): Promise { + const result = await this.db(this.tableName).where('id', id).first() + + return result ?? null + } +} diff --git a/src/_/service/ActivityService.ts b/src/_/service/ActivityService.ts new file mode 100644 index 000000000..00eb9cdc6 --- /dev/null +++ b/src/_/service/ActivityService.ts @@ -0,0 +1,59 @@ +import { ActivityEntity, type ActivityDTO } from '../entity/ActivityEntity' +import { ActivityRepository } from '../repository/ActivityRepository' +import { ActorService } from '../service/ActorService' +import { ObjectService } from '../service/ObjectService' + +export class ActivityService { + constructor( + private readonly activityRepository: ActivityRepository, + private readonly actorService: ActorService, + private readonly objectService: ObjectService, + ) {} + + async findById(id: string): Promise { + const activity = await this.activityRepository.findById(id) + + if (activity) { + return await this.#buildActivity(activity) + } + + return null + } + + async findByIds(ids: string[]): Promise { + const serializedActivities = await this.activityRepository.findByIds(ids) + const activities: ActivityEntity[] = [] + + for (const activity of serializedActivities) { + const builtActivity = await this.#buildActivity(activity) + + if (builtActivity) { + activities.push(builtActivity) + } + } + + return activities + } + + async create(data: ActivityDTO): Promise { + const serializedActivity = await this.activityRepository.create(data) + const activity = await this.#buildActivity(serializedActivity); + + if (!activity) { + throw new Error('Failed to create activity') + } + + return activity; + } + + async #buildActivity(activity: ActivityDTO) { + const object = await this.objectService.findById(activity.object_id) + const actor = await this.actorService.findById(activity.actor_id) + + if (object && actor) { + return new ActivityEntity(activity.id, activity.type, actor, object) + } + + return null + } +} diff --git a/src/_/service/ActorService.ts b/src/_/service/ActorService.ts new file mode 100644 index 000000000..45e277f81 --- /dev/null +++ b/src/_/service/ActorService.ts @@ -0,0 +1,27 @@ +import { ActorEntity, type ActorDTO } from '../entity/ActorEntity' +import { ActorRepository } from '../repository/ActorRepository' +export class ActorService { + constructor( + private readonly actorRepository: ActorRepository, + ) {} + + async findById(id: string): Promise { + const object = await this.actorRepository.findById(id) + + if (!object) { + return null + } + + return new ActorEntity(object.id, object.data) + } + + async create(data: ActorDTO): Promise { + const object = await this.actorRepository.create(data) + + if (!object) { + throw new Error('Failed to create object') + } + + return new ActorEntity(object.id, object.data) + } +} diff --git a/src/_/service/InboxService.ts b/src/_/service/InboxService.ts new file mode 100644 index 000000000..2bda78519 --- /dev/null +++ b/src/_/service/InboxService.ts @@ -0,0 +1,26 @@ +import { ActivityEntity } from '../entity/ActivityEntity' +import { ActorEntity } from '../entity/ActorEntity' +import { ActivityService } from './ActivityService' +import { InboxRepository } from '../repository/InboxRepository' +import { SiteEntity } from '../entity/SiteEntity' +export class InboxService { + constructor( + private readonly activityService: ActivityService, + private readonly inboxRepository: InboxRepository, + ) {} + + async getInboxForActor(actor: ActorEntity, site: SiteEntity): Promise { + const actorActivities = await this.inboxRepository.findByActorId(actor.id, site.id); + const activities = await this.activityService.findByIds(actorActivities.map(activity => activity.activity_id)); + + return activities; + } + + async addActivityForActor(site: SiteEntity, actor: ActorEntity, activity: ActivityEntity) { + await this.inboxRepository.create({ + site_id: site.id, + actor_id: actor.id, + activity_id: activity.id, + }); + } +} diff --git a/src/_/service/ObjectService.ts b/src/_/service/ObjectService.ts new file mode 100644 index 000000000..0308a5988 --- /dev/null +++ b/src/_/service/ObjectService.ts @@ -0,0 +1,28 @@ +import { ObjectEntity, type ObjectDTO } from '../entity/ObjectEntity' +import { ObjectRepository } from '../repository/ObjectRepository' + +export class ObjectService { + constructor( + private readonly objectRepository: ObjectRepository, + ) {} + + async findById(id: string): Promise { + const object = await this.objectRepository.findById(id) + + if (!object) { + return null + } + + return new ObjectEntity(object.id, object.data) + } + + async create(data: ObjectDTO): Promise { + const object = await this.objectRepository.create(data) + + if (!object) { + throw new Error('Failed to create object') + } + + return new ObjectEntity(object.id, object.data) + } +} diff --git a/src/_/service/SiteService.ts b/src/_/service/SiteService.ts new file mode 100644 index 000000000..ba11cda31 --- /dev/null +++ b/src/_/service/SiteService.ts @@ -0,0 +1,18 @@ +import { SiteEntity } from '../entity/SiteEntity' +import { SiteRepository } from '../repository/SiteRepository' + +export class SiteService { + constructor( + private readonly siteRepository: SiteRepository, + ) {} + + async findById(id: number): Promise { + const site = await this.siteRepository.findById(id) + + if (!site) { + return null + } + + return new SiteEntity(site.id, site.hostname) + } +} diff --git a/src/app.ts b/src/app.ts index dbb99018c..8b3f9def7 100644 --- a/src/app.ts +++ b/src/app.ts @@ -63,9 +63,37 @@ await configure({ loggers: [{ category: 'fedify', sinks: ['console'], level: 'debug' }], }); +/** Services */ + +import { ActivityRepository } from './_/repository/ActivityRepository'; +import { ActivityService } from './_/service/ActivityService'; +import { ActorRepository } from './_/repository/ActorRepository'; +import { ActorService } from './_/service/ActorService'; +import { InboxRepository } from './_/repository/InboxRepository'; +import { InboxService } from './_/service/InboxService'; +import { ObjectRepository } from './_/repository/ObjectRepository'; +import { ObjectService } from './_/service/ObjectService'; +import { SiteRepository } from './_/repository/SiteRepository'; +import { SiteService } from './_/service/SiteService'; + +const actorService = new ActorService(new ActorRepository(client)); +const objectService = new ObjectService(new ObjectRepository(client)); +const siteService = new SiteService(new SiteRepository(client)); +const activityService = new ActivityService(new ActivityRepository(client), actorService, objectService); +const inboxService = new InboxService(activityService, new InboxRepository(client)); + +export const db = await KnexKvStore.create(client, 'key_value'); + +/** Fedify */ + export type ContextData = { db: KvStore; globaldb: KvStore; + activityService: ActivityService; + actorService: ActorService; + inboxService: InboxService; + objectService: ObjectService; + siteService: SiteService; }; const fedifyKv = await KnexKvStore.create(client, 'key_value'); @@ -75,10 +103,6 @@ export const fedify = createFederation({ skipSignatureVerification: process.env.SKIP_SIGNATURE_VERIFICATION === 'true' && process.env.NODE_ENV === 'testing', }); -export const db = await KnexKvStore.create(client, 'key_value'); - -/** Fedify */ - /** * Fedify does not pass the correct context object when running outside of the request context * for example in the context of the Inbox Queue - so we need to wrap handlers with this. @@ -178,6 +202,11 @@ fedify.setObjectDispatcher( export type HonoContextVariables = { db: KvStore; globaldb: KvStore; + activityService: ActivityService; + actorService: ActorService; + inboxService: InboxService; + objectService: ObjectService; + siteService: SiteService; }; const app = new Hono<{ Variables: HonoContextVariables }>(); @@ -241,7 +270,11 @@ app.use(async (ctx, next) => { ctx.set('db', scopedDb); ctx.set('globaldb', db); - + ctx.set('activityService', activityService); + ctx.set('actorService', actorService); + ctx.set('inboxService', inboxService); + ctx.set('objectService', objectService); + ctx.set('siteService', siteService); await next(); }); @@ -267,6 +300,11 @@ app.use( return { db: ctx.get('db'), globaldb: ctx.get('globaldb'), + activityService: ctx.get('activityService'), + actorService: ctx.get('actorService'), + inboxService: ctx.get('inboxService'), + objectService: ctx.get('objectService'), + siteService: ctx.get('siteService'), }; }, ), diff --git a/src/db.ts b/src/db.ts index 471d58fe1..7de7ced97 100644 --- a/src/db.ts +++ b/src/db.ts @@ -17,3 +17,51 @@ await client.schema.createTableIfNotExists('key_value', function (table) { table.json('value').notNullable(); table.datetime('expires').nullable(); }); + +const TABLE_SITES = 'sites'; +await client.schema.createTableIfNotExists(TABLE_SITES, function (table) { + table.increments('id').primary(); + table.string('hostname', 2048); +}); +await client.table(TABLE_SITES).truncate(); +await client.table(TABLE_SITES).insert({ + id: 1, + hostname: 'localhost', +}); + +const TABLE_ACTORS = 'actors'; +await client.schema.createTableIfNotExists(TABLE_ACTORS, function (table) { + table.string('id').primary(); + table.json('data').notNullable(); +}); +await client.table(TABLE_ACTORS).truncate(); +await client.table(TABLE_ACTORS).insert({ + id: 'https://localhost/users/1', + data: { + id: 'https://localhost/users/1' + }, +}); + +const TABLE_OBJECTS = 'objects'; +await client.schema.createTableIfNotExists(TABLE_OBJECTS, function (table) { + table.string('id').primary(); + table.json('data').notNullable(); +}); +await client.table(TABLE_OBJECTS).truncate(); + +const TABLE_ACTIVITIES = 'activities'; +await client.schema.createTableIfNotExists(TABLE_ACTIVITIES, function (table) { + table.string('id').primary(); + table.enum('type', ['Like']); + table.string('actor_id'); + table.string('object_id'); +}); +await client.table(TABLE_ACTIVITIES).truncate(); + +const TABLE_INBOX = 'inbox'; +await client.schema.createTableIfNotExists(TABLE_INBOX, function (table) { + table.integer('site_id'); + table.string('actor_id'); + table.string('activity_id'); +}); +await client.table(TABLE_INBOX).truncate(); diff --git a/src/dispatchers.ts b/src/dispatchers.ts index 926690113..5a1553051 100644 --- a/src/dispatchers.ts +++ b/src/dispatchers.ts @@ -22,6 +22,7 @@ import { addToList } from './kv-helpers'; import { ContextData } from './app'; import { ACTOR_DEFAULT_HANDLE } from './constants'; import { getUserData, getUserKeypair } from './user'; +import { ActivityType } from '_/entity/ActivityEntity'; export async function actorDispatcher( ctx: RequestContext, @@ -236,7 +237,7 @@ export async function handleLike( ) { console.log('Handling Like'); - // Validate like + // Validate activity if (!like.id) { console.log('Invalid Like - no id'); return; @@ -255,18 +256,29 @@ export async function handleLike( return; } - // Lookup liked object - If not found in globalDb, perform network lookup + // Persist sender details + let storedSender = await ctx.data.actorService.findById(sender.id.href); + if (!storedSender) { + await ctx.data.actorService.create({ + id: sender.id.href, + data: await sender.toJsonLd() as JSON + }); + } else { + // Update sender? + } + + // Lookup associated object - If not found locally, perform network lookup let object = null; - let existing = await ctx.data.globaldb.get([like.objectId.href]) ?? null; + let storedObject = await ctx.data.objectService.findById(like.objectId.href); - if (!existing) { - console.log('Object not found in globalDb, performing network lookup'); + if (!storedObject) { + console.log('Object not found locally, performing network lookup'); object = await like.getObject(); } // Validate object - if (!existing && !object) { + if (!storedObject && !object) { console.log('Invalid Like - could not find object'); return; } @@ -276,21 +288,36 @@ export async function handleLike( return; } - // Persist like - const likeJson = await like.toJsonLd(); - ctx.data.globaldb.set([like.id.href], likeJson); - // Persist object if not already persisted - if (!existing && object && object.id) { - console.log('Storing object in globalDb'); + if (!storedObject && object && object.id) { + console.log('Storing object in db'); - const objectJson = await object.toJsonLd(); + const objectJSON = await object.toJsonLd() as JSON; - ctx.data.globaldb.set([object.id.href], objectJson); + storedObject = await ctx.data.objectService.create({ + id: object.id.href, + data: objectJSON + }); } + // Persist activity + const actor = await ctx.data.actorService.findById('https://localhost/users/1'); + if (!actor) { + throw new Error('actor not found'); + } + const site = await ctx.data.siteService.findById(1); + if (!site) { + throw new Error('site not found'); + } + const activity = await ctx.data.activityService.create({ + id: like.id.href, + type: ActivityType.LIKE, + actor_id: actor.id, + object_id: storedObject.id, + }); + // Add to inbox - await addToList(ctx.data.db, ['inbox'], like.id.href); + await ctx.data.inboxService.addActivityForActor(site, actor, activity); } export async function inboxErrorHandler( diff --git a/src/handlers.ts b/src/handlers.ts index 6e0ab92c5..01dfbdc51 100644 --- a/src/handlers.ts +++ b/src/handlers.ts @@ -79,6 +79,11 @@ export async function followAction( const apCtx = fedify.createContext(ctx.req.raw as Request, { db: ctx.get('db'), globaldb: ctx.get('globaldb'), + activityService: ctx.get('activityService'), + actorService: ctx.get('actorService'), + inboxService: ctx.get('inboxService'), + objectService: ctx.get('objectService'), + siteService: ctx.get('siteService'), }); const actor = await apCtx.getActor(ACTOR_DEFAULT_HANDLE); // TODO This should be the actor making the request const followId = apCtx.getObjectUri(Follow, { @@ -118,6 +123,11 @@ export async function postPublishedWebhook( const apCtx = fedify.createContext(ctx.req.raw as Request, { db: ctx.get('db'), globaldb: ctx.get('globaldb'), + activityService: ctx.get('activityService'), + actorService: ctx.get('actorService'), + inboxService: ctx.get('inboxService'), + objectService: ctx.get('objectService'), + siteService: ctx.get('siteService'), }); const { article, preview } = await postToArticle( apCtx, @@ -207,6 +217,11 @@ export async function siteChangedWebhook( const apCtx = fedify.createContext(ctx.req.raw as Request, { db, globaldb: ctx.get('globaldb'), + activityService: ctx.get('activityService'), + actorService: ctx.get('actorService'), + inboxService: ctx.get('inboxService'), + objectService: ctx.get('objectService'), + siteService: ctx.get('siteService'), }); const actor = await apCtx.getActor(handle); @@ -241,35 +256,29 @@ export async function inboxHandler( ctx: Context<{ Variables: HonoContextVariables }>, next: Next, ) { - const results = (await ctx.get('db').get(['inbox'])) || []; - let items: unknown[] = []; - for (const result of results) { - try { - const db = ctx.get('globaldb'); - const thing = await db.get([result]); - - // If the object is a string, it's probably a URI, so we should - // look it up the db. If it's not in the db, we should just leave - // it as is - if (thing && typeof thing.object === 'string') { - thing.object = await db.get([thing.object]) ?? thing.object; - } + const actor = await ctx.get('actorService').findById('https://localhost/users/1') + if (!actor) { + throw new Error('actor not found'); + } + const site = await ctx.get('siteService').findById(1); + if (!site) { + throw new Error('site not found'); + } - // Sanitize HTML content - if (thing?.object && typeof thing.object !== 'string') { - thing.object.content = sanitizeHtml(thing.object.content, { - allowedTags: ['a', 'p', 'img', 'br', 'strong', 'em', 'span'], - allowedAttributes: { - a: ['href'], - img: ['src'], - } - }); - } + const results = await ctx.get('inboxService').getInboxForActor(actor, site); + const items = []; - items.push(thing); - } catch (err) { - console.log(err); - } + for (const result of results) { + items.push({ + id: result.id, + type: result.type, + actor: result.actor.data, + object: result.object.data, + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/data-integrity/v1" + ] + }); } return new Response( JSON.stringify({