diff --git a/src/_/Entity.ts b/src/_/Entity.ts new file mode 100644 index 00000000..b54c524f --- /dev/null +++ b/src/_/Entity.ts @@ -0,0 +1,3 @@ +export interface Entity { + serialize(): TDTO +} diff --git a/src/_/README.md b/src/_/README.md new file mode 100644 index 00000000..487cb970 --- /dev/null +++ b/src/_/README.md @@ -0,0 +1,77 @@ +# ActivityPub Domain + +## Architecture + +### Service + +Provides functionality for the application layer. Common operations include: + - Retrieving data from the repository layer and constructing entities + +### Entity + +Represents a real-world object or concept. The service layer is responsible for +the creation of entities. Entities currently define how they are serialized to a +DTO in lieu of a data-mapper layer. + +### Repository + +Provides functionality for interacting with a database. Typically used by the +service layer. DTOs are used to pass data between the repository and the service +layer. + +### DTO + +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` +- ... + +## Conventions + +- If an entity references another entity, when it is created, the referenced entity +should also be created. This reference is normally indicated via a `_id` field +in the entity's DTO. + +## Notes, Thoughts, Questions + +### How does data get scoped? + +When a request is received, the hostname is extracted from the request and used to +determine the site that the request is scoped to. The only data that requires scoping +is actor data within the context of a site (i.e the same actor can be present on +multiple sites). Site specific actor data includes: +- Inbox + +### 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 keep 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 00000000..1d65776c --- /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 00000000..712e3daf Binary files /dev/null and b/src/_/db.png differ diff --git a/src/_/entity/ActivityEntity.ts b/src/_/entity/ActivityEntity.ts new file mode 100644 index 00000000..3d236ad1 --- /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 00000000..e0fa7bde --- /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 00000000..68d2e4bf --- /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 00000000..39765608 --- /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 00000000..e372d220 --- /dev/null +++ b/src/_/repository/ActivityRepository.ts @@ -0,0 +1,38 @@ +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 00000000..4e0cb93c --- /dev/null +++ b/src/_/repository/ActorRepository.ts @@ -0,0 +1,32 @@ +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 actor = await this.findById(data.id); + + if (!actor) { + throw new Error('Failed to create actor') + } + + return actor + } +} diff --git a/src/_/repository/InboxRepository.ts b/src/_/repository/InboxRepository.ts new file mode 100644 index 00000000..156a386a --- /dev/null +++ b/src/_/repository/InboxRepository.ts @@ -0,0 +1,32 @@ +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 00000000..97c58274 --- /dev/null +++ b/src/_/repository/ObjectRepository.ts @@ -0,0 +1,32 @@ +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 00000000..410b08a3 --- /dev/null +++ b/src/_/repository/SiteRepository.ts @@ -0,0 +1,40 @@ +import { Knex } from 'knex' + +import { Repository } from '../Repository' +import { type SiteDTO } from '../entity/SiteEntity' + +type CreateSiteDTO = Omit + +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 + } + + async findByHostname(hostname: string): Promise { + const result = await this.db(this.tableName).where('hostname', hostname).first() + + return result ?? null + } + + async create(data: CreateSiteDTO): Promise { + const result = await this.db(this.tableName).insert(data) + + if (result.length !== 1) { + throw new Error('Failed to create site') + } + + const object = await this.findById(result[0]); + + if (!object) { + throw new Error('Failed to create site') + } + + return object + } +} diff --git a/src/_/service/ActivityService.ts b/src/_/service/ActivityService.ts new file mode 100644 index 00000000..00eb9cdc --- /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 00000000..45e277f8 --- /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 00000000..2bda7851 --- /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 00000000..0308a598 --- /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 00000000..8081ae5b --- /dev/null +++ b/src/_/service/SiteService.ts @@ -0,0 +1,28 @@ +import { SiteEntity } from '../entity/SiteEntity' +import { SiteRepository } from '../repository/SiteRepository' + +export class SiteService { + constructor( + private readonly siteRepository: SiteRepository, + ) {} + + async findByHostname(host: string): Promise { + const site = await this.siteRepository.findByHostname(host) + + if (!site) { + return null + } + + return new SiteEntity(site.id, site.hostname) + } + + async create(hostname: string): Promise { + const site = await this.siteRepository.create({ hostname }) + + if (!site) { + throw new Error('Failed to create site') + } + + return new SiteEntity(site.id, site.hostname) + } +} diff --git a/src/app.ts b/src/app.ts index dbb99018..12d92bb1 100644 --- a/src/app.ts +++ b/src/app.ts @@ -63,9 +63,38 @@ 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 { SiteEntity } from './_/entity/SiteEntity'; +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; + site: SiteEntity; }; const fedifyKv = await KnexKvStore.create(client, 'key_value'); @@ -75,10 +104,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 +203,11 @@ fedify.setObjectDispatcher( export type HonoContextVariables = { db: KvStore; globaldb: KvStore; + activityService: ActivityService; + actorService: ActorService; + inboxService: InboxService; + objectService: ObjectService; + site: SiteEntity; }; const app = new Hono<{ Variables: HonoContextVariables }>(); @@ -242,6 +272,16 @@ app.use(async (ctx, next) => { ctx.set('db', scopedDb); ctx.set('globaldb', db); + let site = await siteService.findByHostname(host); + if (!site) { + site = await siteService.create(host); + } + ctx.set('activityService', activityService); + ctx.set('actorService', actorService); + ctx.set('inboxService', inboxService); + ctx.set('objectService', objectService); + ctx.set('site', site); + await next(); }); @@ -267,6 +307,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'), + site: ctx.get('site'), }; }, ), diff --git a/src/db.ts b/src/db.ts index 471d58fe..9f27deac 100644 --- a/src/db.ts +++ b/src/db.ts @@ -17,3 +17,47 @@ 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(); + +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 92669011..f5b78eda 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,33 @@ 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 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); + const site = await ctx.data.site + await ctx.data.inboxService.addActivityForActor(site, actor, activity); } export async function inboxErrorHandler( diff --git a/src/handlers.ts b/src/handlers.ts index 6e0ab92c..40d4114f 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'), + site: ctx.get('site'), }); 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'), + site: ctx.get('site'), }); 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'), + site: ctx.get('site'), }); const actor = await apCtx.getActor(handle); @@ -241,35 +256,25 @@ 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; - } - - // 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 actor = await ctx.get('actorService').findById('https://localhost/users/1') + if (!actor) { + throw new Error('actor not found'); + } + const site = await ctx.get('site'); + 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({