···11-import { pino } from 'pino'
22-import { Database } from '../db/connection.js'
33-import type { NormalizedEvent } from '../types/events.js'
44-import { ensureActor } from '../utils/actor-utils.js'
55-66-const logger = pino({ name: 'actor-handler' })
77-88-/**
99- * This handler is called by all other handlers to ensure that
1010- * any DID referenced in an event has a corresponding actor entry.
1111- *
1212- * @param evt The normalized event to process
1313- * @param db Database connection
1414- */
1515-export async function handleActorReferences(evt: NormalizedEvent, db: Database): Promise<void> {
1616- try {
1717- // Always ensure the author DID has an actor
1818- if (evt.did) {
1919- await ensureActor(evt.did, evt.handle || undefined, db)
2020- }
2121-2222- // Handle subject DIDs for follow, block, like events
2323- if (['follow', 'block', 'like'].includes(evt.event) && evt.record?.subject) {
2424- // Subject is usually a DID in format did:plc:12345
2525- const subjectDid = evt.record.subject as string
2626- if (subjectDid && subjectDid.startsWith('did:')) {
2727- await ensureActor(subjectDid, undefined, db)
2828- }
2929- }
3030-3131- // Handle reply references for posts
3232- if (evt.collection === 'so.sprk.feed.post' && evt.record?.reply) {
3333- const reply = evt.record.reply as { root?: { uri?: string }, parent?: { uri?: string } }
3434-3535- // Extract DIDs from reply URIs (format: at://did:plc:12345/...)
3636- if (reply.root?.uri) {
3737- const rootDid = extractDidFromUri(reply.root.uri)
3838- if (rootDid) {
3939- await ensureActor(rootDid, undefined, db)
4040- }
4141- }
4242-4343- if (reply.parent?.uri) {
4444- const parentDid = extractDidFromUri(reply.parent.uri)
4545- if (parentDid && parentDid !== extractDidFromUri(reply.root?.uri || '')) {
4646- await ensureActor(parentDid, undefined, db)
4747- }
4848- }
4949- }
5050-5151- // Handle repost subjects
5252- if (evt.collection === 'so.sprk.feed.repost' && evt.record?.subject?.uri) {
5353- const subjectUri = evt.record.subject.uri as string
5454- const subjectDid = extractDidFromUri(subjectUri)
5555- if (subjectDid) {
5656- await ensureActor(subjectDid, undefined, db)
5757- }
5858- }
5959- } catch (error) {
6060- logger.error({ error, uri: evt.uri }, 'Error while handling actor references')
6161- }
6262-}
6363-6464-/**
6565- * Extracts a DID from an AT URI (at://did:plc:12345/...)
6666- *
6767- * @param uri The URI to extract the DID from
6868- * @returns The extracted DID or undefined
6969- */
7070-function extractDidFromUri(uri: string): string | undefined {
7171- if (!uri) return undefined
7272-7373- // Match a DID in an AT URI format
7474- const match = uri.match(/at:\/\/(did:[a-zA-Z0-9:]+)\//)
7575- return match ? match[1] : undefined
7676-}
+1-5
services/ingester/src/handlers/index.ts
···1111import { handleMusicEvent } from './music-handler.js'
1212import { handleLookEvent } from './look-handler.js'
1313import { handleGeneratorEvent } from './generator-handler.js'
1414-import { handleActorReferences } from './actor-handler.js'
15141615const logger = pino({ name: 'event-handler' })
17161817export async function handleEvent(evt: NormalizedEvent, db: Database): Promise<void> {
1918 try {
2020- // First, ensure all actor references are handled properly
2121- await handleActorReferences(evt, db)
2222-2323- // Then handle different events based on collection
1919+ // Handle different events based on collection
2420 if (evt.collection === 'so.sprk.feed.like') {
2521 await handleLikeEvent(evt, db)
2622 return
+3-14
services/ingester/src/handlers/profile-handler.ts
···11import { pino } from 'pino'
22import { Database } from '../db/connection.js'
33import type { NormalizedEvent } from '../types/events.js'
44-import { ensureActor, linkProfileToActor } from '../utils/actor-utils.js'
55-import type { ProfileDocument } from '../db/models.js'
6475const logger = pino({ name: 'profile-handler' })
86···3937 }, 'Processing profile event')
40384139 try {
4242- // First, ensure we have an actor for this DID
4343- await ensureActor(evt.did, evt.handle || undefined, db)
4444-4540 const profileData = {
4641 uri: evt.uri,
4742 displayName: record.displayName,
···5853 cid: evt.commit.cid
5954 }
60556161- // Save the profile
6262- const profile = await db.models.Profile.findOneAndUpdate(
5656+ await db.models.Profile.findOneAndUpdate(
6357 { uri: evt.uri },
6458 profileData,
6559 { upsert: true, new: true }
6666- ) as ProfileDocument
6767-6868- if (profile && profile._id) {
6969- // Link the profile to the actor
7070- await linkProfileToActor(evt.did, profile._id.toString(), evt.commit.cid, db)
7171- }
6060+ )
72617362 logger.info(
7463 { uri: evt.uri },
7575- 'Successfully saved profile to database and linked to actor'
6464+ 'Successfully saved profile to database'
7665 )
7766 } catch (error) {
7867 logger.error(
-91
services/ingester/src/utils/actor-utils.ts
···11-import { pino } from 'pino'
22-import { Database } from '../db/connection.js'
33-44-const logger = pino({ name: 'actor-utils' })
55-66-/**
77- * Ensures that an actor exists for the given DID.
88- * If the actor doesn't exist, it creates a new one.
99- *
1010- * @param did The DID to ensure has an actor
1111- * @param handle Optional handle associated with the DID
1212- * @param db Database connection
1313- * @returns The actor document, either existing or newly created
1414- */
1515-export async function ensureActor(
1616- did: string,
1717- handle?: string,
1818- db?: Database
1919-): Promise<any> {
2020- if (!db) {
2121- logger.warn({ did }, 'No database connection provided to ensureActor')
2222- return null
2323- }
2424-2525- try {
2626- // Try to find existing actor
2727- const existingActor = await db.models.Actor.findOne({ did })
2828-2929- if (existingActor) {
3030- // If handle is provided and different from existing, update it
3131- if (handle && existingActor.handle !== handle) {
3232- existingActor.handle = handle
3333- await existingActor.save()
3434- logger.info({ did, handle }, 'Updated actor handle')
3535- }
3636- return existingActor
3737- }
3838-3939- // Create new actor if none exists
4040- const now = new Date()
4141- const uri = `at://${did}/app.bsky.actor.profile`
4242-4343- const newActor = await db.models.Actor.create({
4444- uri,
4545- did,
4646- handle: handle || undefined,
4747- followersCount: 0,
4848- followingCount: 0,
4949- postsCount: 0,
5050- indexedAt: now.toISOString(),
5151- isLabeler: false,
5252- priorityNotifications: false
5353- })
5454-5555- logger.info({ did, handle }, 'Created new actor')
5656- return newActor
5757- } catch (error) {
5858- logger.error({ error, did, handle }, 'Failed to ensure actor exists')
5959- return null
6060- }
6161-}
6262-6363-/**
6464- * Links a profile to an actor
6565- *
6666- * @param did The DID of the actor
6767- * @param profileId The MongoDB ID of the profile
6868- * @param profileCid The CID of the profile
6969- * @param db Database connection
7070- */
7171-export async function linkProfileToActor(
7272- did: string,
7373- profileId: string,
7474- profileCid: string,
7575- db: Database
7676-): Promise<void> {
7777- try {
7878- await db.models.Actor.findOneAndUpdate(
7979- { did },
8080- {
8181- profile: profileId,
8282- profileCid
8383- },
8484- { new: true }
8585- )
8686-8787- logger.info({ did, profileId }, 'Linked profile to actor')
8888- } catch (error) {
8989- logger.error({ error, did, profileId }, 'Failed to link profile to actor')
9090- }
9191-}