This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

semble / src / modules / atproto / application / useCases / ProcessCardFirehoseEventUseCase.ts
9.7 kB 297 lines
1import { Result, ok, err } from 'src/shared/core/Result'; 2import { UseCase } from 'src/shared/core/UseCase'; 3import { UseCaseError } from 'src/shared/core/UseCaseError'; 4import { AppError } from 'src/shared/core/AppError'; 5import { IAtUriResolutionService } from '../../../cards/domain/services/IAtUriResolutionService'; 6import { PublishedRecordId } from '../../../cards/domain/value-objects/PublishedRecordId'; 7import { ATUri } from '../../domain/ATUri'; 8import { 9 Record as CardRecord, 10 NoteContent, 11 UrlContent, 12} from '../../infrastructure/lexicon/types/network/cosmik/card'; 13import { AddUrlToLibraryUseCase } from '../../../cards/application/useCases/commands/AddUrlToLibraryUseCase'; 14import { 15 UpdateUrlCardAssociationsUseCase, 16 OperationContext, 17} from '../../../cards/application/useCases/commands/UpdateUrlCardAssociationsUseCase'; 18import { RemoveCardFromLibraryUseCase } from '../../../cards/application/useCases/commands/RemoveCardFromLibraryUseCase'; 19 20export interface ProcessCardFirehoseEventDTO { 21 atUri: string; 22 cid: string | null; 23 eventType: 'create' | 'update' | 'delete'; 24 record?: CardRecord; 25} 26 27export class ProcessCardFirehoseEventUseCase 28 implements UseCase<ProcessCardFirehoseEventDTO, Result<void>> 29{ 30 constructor( 31 private atUriResolutionService: IAtUriResolutionService, 32 private addUrlToLibraryUseCase: AddUrlToLibraryUseCase, 33 private updateUrlCardAssociationsUseCase: UpdateUrlCardAssociationsUseCase, 34 private removeCardFromLibraryUseCase: RemoveCardFromLibraryUseCase, 35 ) {} 36 37 async execute(request: ProcessCardFirehoseEventDTO): Promise<Result<void>> { 38 try { 39 console.log( 40 `Processing card firehose event: ${request.atUri} (${request.eventType})`, 41 ); 42 43 switch (request.eventType) { 44 case 'create': 45 return await this.handleCardCreate(request); 46 case 'update': 47 return await this.handleCardUpdate(request); 48 case 'delete': 49 return await this.handleCardDelete(request); 50 } 51 52 return ok(undefined); 53 } catch (error) { 54 return err(AppError.UnexpectedError.create(error)); 55 } 56 } 57 58 private async handleCardCreate( 59 request: ProcessCardFirehoseEventDTO, 60 ): Promise<Result<void>> { 61 if (!request.record || !request.cid) { 62 console.warn('Card create event missing record or cid, skipping'); 63 return ok(undefined); 64 } 65 66 try { 67 // Parse AT URI to extract curator DID 68 const atUriResult = ATUri.create(request.atUri); 69 if (atUriResult.isErr()) { 70 console.warn( 71 `Invalid AT URI format: ${request.atUri} - ${atUriResult.error.message}`, 72 ); 73 return ok(undefined); 74 } 75 const atUri = atUriResult.value; 76 const curatorDid = atUri.did.value; 77 78 const publishedRecordId = PublishedRecordId.create({ 79 uri: request.atUri, 80 cid: request.cid, 81 }); 82 83 if (request.record.type === 'URL') { 84 // Handle URL card creation 85 const urlContent = request.record.content as UrlContent; 86 if (!urlContent.url) { 87 console.warn(`URL card missing URL: ${request.atUri}`); 88 return ok(undefined); 89 } 90 91 const result = await this.addUrlToLibraryUseCase.execute({ 92 url: urlContent.url, 93 curatorId: curatorDid, 94 publishedRecordId: publishedRecordId, 95 }); 96 97 if (result.isErr()) { 98 console.warn(`Failed to add URL to library: ${result.error.message}`); 99 return ok(undefined); 100 } 101 102 console.log( 103 `Successfully created URL card from firehose event: ${result.value.urlCardId}`, 104 ); 105 } else if (request.record.type === 'NOTE') { 106 // Handle note card creation 107 const noteContent = request.record.content as NoteContent; 108 if (!noteContent.text) { 109 console.warn(`Note card missing text: ${request.atUri}`); 110 return ok(undefined); 111 } 112 113 // Get parent card from parentCard reference 114 if (!request.record.parentCard) { 115 console.warn( 116 `Note card missing parent card reference: ${request.atUri}`, 117 ); 118 return ok(undefined); 119 } 120 121 // Resolve parent card ID from AT URI 122 const parentCardId = await this.atUriResolutionService.resolveCardId( 123 request.record.parentCard.uri, 124 ); 125 if (parentCardId.isErr() || !parentCardId.value) { 126 console.warn( 127 `Failed to resolve parent card: ${request.record.parentCard.uri}`, 128 ); 129 return ok(undefined); 130 } 131 132 const result = await this.updateUrlCardAssociationsUseCase.execute({ 133 cardId: parentCardId.value.getStringValue(), 134 curatorId: curatorDid, 135 note: noteContent.text, 136 context: OperationContext.FIREHOSE_EVENT, 137 publishedRecordIds: { 138 noteCard: publishedRecordId, 139 }, 140 }); 141 142 if (result.isErr()) { 143 console.warn(`Failed to create note card: ${result.error.message}`); 144 return ok(undefined); 145 } 146 147 console.log( 148 `Successfully created note card from firehose event: ${result.value.noteCardId}`, 149 ); 150 } 151 152 return ok(undefined); 153 } catch (error) { 154 console.error(`Error processing card create event: ${error}`); 155 return ok(undefined); // Don't fail the firehose processing 156 } 157 } 158 159 private async handleCardUpdate( 160 request: ProcessCardFirehoseEventDTO, 161 ): Promise<Result<void>> { 162 if (!request.record || !request.cid) { 163 console.warn('Card update event missing record or cid, skipping'); 164 return ok(undefined); 165 } 166 167 // Only handle NOTE card updates for now 168 if (request.record.type !== 'NOTE') { 169 console.log(`Ignoring update for card type: ${request.record.type}`); 170 return ok(undefined); 171 } 172 173 try { 174 // Parse AT URI to extract curator DID 175 const atUriResult = ATUri.create(request.atUri); 176 if (atUriResult.isErr()) { 177 console.warn( 178 `Invalid AT URI format: ${request.atUri} - ${atUriResult.error.message}`, 179 ); 180 return ok(undefined); 181 } 182 const curatorDid = atUriResult.value.did.value; 183 184 const noteContent = request.record.content as NoteContent; 185 if (!noteContent.text) { 186 console.warn(`Note card missing text: ${request.atUri}`); 187 return ok(undefined); 188 } 189 190 // Get parent card from parentCard reference 191 if (!request.record.parentCard) { 192 console.warn( 193 `Note card missing parent card reference: ${request.atUri}`, 194 ); 195 return ok(undefined); 196 } 197 198 // Resolve parent card ID from AT URI 199 const parentCardId = await this.atUriResolutionService.resolveCardId( 200 request.record.parentCard.uri, 201 ); 202 if (parentCardId.isErr() || !parentCardId.value) { 203 console.warn( 204 `Failed to resolve parent card: ${request.record.parentCard.uri}`, 205 ); 206 return ok(undefined); 207 } 208 209 const publishedRecordId = PublishedRecordId.create({ 210 uri: request.atUri, 211 cid: request.cid, 212 }); 213 214 const result = await this.updateUrlCardAssociationsUseCase.execute({ 215 cardId: parentCardId.value.getStringValue(), 216 curatorId: curatorDid, 217 note: noteContent.text, 218 context: OperationContext.FIREHOSE_EVENT, 219 publishedRecordIds: { 220 noteCard: publishedRecordId, 221 }, 222 }); 223 224 if (result.isErr()) { 225 console.warn(`Failed to update note card: ${result.error.message}`); 226 return ok(undefined); 227 } 228 229 console.log( 230 `Successfully updated note card from firehose event: ${result.value.noteCardId}`, 231 ); 232 return ok(undefined); 233 } catch (error) { 234 console.error(`Error processing card update event: ${error}`); 235 return ok(undefined); // Don't fail the firehose processing 236 } 237 } 238 239 private async handleCardDelete( 240 request: ProcessCardFirehoseEventDTO, 241 ): Promise<Result<void>> { 242 try { 243 // Parse AT URI to extract curator DID 244 const atUriResult = ATUri.create(request.atUri); 245 if (atUriResult.isErr()) { 246 console.warn( 247 `Invalid AT URI format: ${request.atUri} - ${atUriResult.error.message}`, 248 ); 249 return ok(undefined); 250 } 251 const curatorDid = atUriResult.value.did.value; 252 253 const cardIdResult = await this.atUriResolutionService.resolveCardId( 254 request.atUri, 255 ); 256 if (cardIdResult.isErr()) { 257 console.warn( 258 `Failed to resolve card ID: ${cardIdResult.error.message}`, 259 ); 260 return ok(undefined); 261 } 262 263 if (cardIdResult.value) { 264 console.log( 265 `Card deleted externally: ${request.atUri}, removing from library`, 266 ); 267 268 const publishedRecordId = PublishedRecordId.create({ 269 uri: request.atUri, 270 cid: request.cid || 'deleted', 271 }); 272 273 const result = await this.removeCardFromLibraryUseCase.execute({ 274 cardId: cardIdResult.value.getStringValue(), 275 curatorId: curatorDid, 276 publishedRecordId: publishedRecordId, 277 }); 278 279 if (result.isErr()) { 280 console.warn( 281 `Failed to remove card from library: ${result.error.message}`, 282 ); 283 return ok(undefined); 284 } 285 286 console.log( 287 `Successfully removed card from library: ${result.value.cardId}`, 288 ); 289 } 290 291 return ok(undefined); 292 } catch (error) { 293 console.error(`Error processing card delete event: ${error}`); 294 return ok(undefined); 295 } 296 } 297}