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..e16773e8 --- /dev/null +++ b/src/_/README.md @@ -0,0 +1,74 @@ +# 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` +- ... + +## 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 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 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({