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