A social knowledge tool for researchers built on ATProto
45
fork

Configure Feed

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

Merge pull request #54 from cosmik-network/feature/handle-and-uri-for-collections-in-feed

Feature/handle and uri for collections in feed

authored by

Pouria Delfan and committed by
GitHub
9a323bac f5194302

+159 -19
+2
src/modules/atproto/domain/services/IIdentityResolutionService.ts
··· 1 1 import { Result } from 'src/shared/core/Result'; 2 2 import { DID } from '../DID'; 3 3 import { DIDOrHandle } from '../DIDOrHandle'; 4 + import { Handle } from '../Handle'; 4 5 5 6 export interface IIdentityResolutionService { 6 7 resolveToDID(identifier: DIDOrHandle): Promise<Result<DID>>; 8 + resolveToHandle(identifier: DIDOrHandle): Promise<Result<Handle>>; 7 9 }
+61
src/modules/atproto/infrastructure/services/ATProtoIdentityResolutionService.ts
··· 2 2 import { IIdentityResolutionService } from '../../domain/services/IIdentityResolutionService'; 3 3 import { DID } from '../../domain/DID'; 4 4 import { DIDOrHandle } from '../../domain/DIDOrHandle'; 5 + import { Handle } from '../../domain/Handle'; 5 6 import { IAgentService } from '../../application/IAgentService'; 6 7 7 8 export class ATProtoIdentityResolutionService ··· 64 65 return err( 65 66 new Error( 66 67 `Error resolving identifier to DID: ${error instanceof Error ? error.message : String(error)}`, 68 + ), 69 + ); 70 + } 71 + } 72 + 73 + async resolveToHandle(identifier: DIDOrHandle): Promise<Result<Handle>> { 74 + try { 75 + // If it's already a handle, return it directly 76 + if (identifier.isHandle) { 77 + const handle = identifier.getHandle(); 78 + if (!handle) { 79 + return err(new Error('Invalid handle in identifier')); 80 + } 81 + return ok(handle); 82 + } 83 + 84 + // If it's a DID, resolve it to a handle 85 + const did = identifier.getDID(); 86 + if (!did) { 87 + return err(new Error('Invalid DID in identifier')); 88 + } 89 + 90 + // Get an unauthenticated agent to resolve the DID 91 + const agentResult = this.agentService.getUnauthenticatedAgent(); 92 + if (agentResult.isErr()) { 93 + return err( 94 + new Error( 95 + `Failed to get agent for DID resolution: ${agentResult.error.message}`, 96 + ), 97 + ); 98 + } 99 + 100 + const agent = agentResult.value; 101 + 102 + // Get the profile to extract the handle 103 + const profileResult = await agent.getProfile({ actor: did.value }); 104 + 105 + if (!profileResult.success) { 106 + return err( 107 + new Error( 108 + `Failed to resolve DID ${did.value}: ${JSON.stringify(profileResult)}`, 109 + ), 110 + ); 111 + } 112 + 113 + // Create and return the Handle 114 + const handleResult = Handle.create(profileResult.data.handle); 115 + if (handleResult.isErr()) { 116 + return err( 117 + new Error( 118 + `Invalid handle returned from DID resolution: ${handleResult.error.message}`, 119 + ), 120 + ); 121 + } 122 + 123 + return ok(handleResult.value); 124 + } catch (error) { 125 + return err( 126 + new Error( 127 + `Error resolving identifier to handle: ${error instanceof Error ? error.message : String(error)}`, 67 128 ), 68 129 ); 69 130 }
+49
src/modules/cards/tests/utils/FakeIdentityResolutionService.ts
··· 2 2 import { IIdentityResolutionService } from 'src/modules/atproto/domain/services/IIdentityResolutionService'; 3 3 import { DID } from 'src/modules/atproto/domain/DID'; 4 4 import { DIDOrHandle } from 'src/modules/atproto/domain/DIDOrHandle'; 5 + import { Handle } from 'src/modules/atproto/domain/Handle'; 5 6 6 7 export class FakeIdentityResolutionService 7 8 implements IIdentityResolutionService 8 9 { 9 10 private handleToDIDMap: Map<string, string> = new Map(); 11 + private didToHandleMap: Map<string, string> = new Map(); 10 12 private shouldFail = false; 11 13 12 14 async resolveToDID(identifier: DIDOrHandle): Promise<Result<DID>> { ··· 54 56 } 55 57 } 56 58 59 + async resolveToHandle(identifier: DIDOrHandle): Promise<Result<Handle>> { 60 + if (this.shouldFail) { 61 + return err(new Error('Identity resolution service failed')); 62 + } 63 + 64 + try { 65 + // If it's already a handle, return it directly 66 + if (identifier.isHandle) { 67 + const handle = identifier.getHandle(); 68 + if (!handle) { 69 + return err(new Error('Invalid handle in identifier')); 70 + } 71 + return ok(handle); 72 + } 73 + 74 + // If it's a DID, resolve it to a handle 75 + const did = identifier.getDID(); 76 + if (!did) { 77 + return err(new Error('Invalid DID in identifier')); 78 + } 79 + 80 + // Check if we have a mapping for this DID 81 + const mappedHandle = this.didToHandleMap.get(did.value); 82 + if (!mappedHandle) { 83 + return err(new Error(`DID not found: ${did.value}`)); 84 + } 85 + 86 + // Create and return the Handle 87 + const handleResult = Handle.create(mappedHandle); 88 + if (handleResult.isErr()) { 89 + return err( 90 + new Error(`Invalid handle in mapping: ${handleResult.error.message}`), 91 + ); 92 + } 93 + 94 + return ok(handleResult.value); 95 + } catch (error) { 96 + return err( 97 + new Error( 98 + `Error resolving identifier to handle: ${error instanceof Error ? error.message : String(error)}`, 99 + ), 100 + ); 101 + } 102 + } 103 + 57 104 // Test helper methods 58 105 addHandleMapping(handle: string, did: string): void { 59 106 this.handleToDIDMap.set(handle, did); 107 + this.didToHandleMap.set(did, handle); 60 108 } 61 109 62 110 setShouldFail(shouldFail: boolean): void { ··· 65 113 66 114 clear(): void { 67 115 this.handleToDIDMap.clear(); 116 + this.didToHandleMap.clear(); 68 117 this.shouldFail = false; 69 118 } 70 119 }
+44 -19
src/modules/feeds/application/useCases/queries/GetGlobalFeedUseCase.ts
··· 11 11 } from '../../../../cards/domain/ICardQueryRepository'; 12 12 import { ICollectionRepository } from 'src/modules/cards/domain/ICollectionRepository'; 13 13 import { CollectionId } from 'src/modules/cards/domain/value-objects/CollectionId'; 14 + import { IIdentityResolutionService } from '../../../../atproto/domain/services/IIdentityResolutionService'; 15 + import { DID } from '../../../../atproto/domain/DID'; 16 + import { DIDOrHandle } from '../../../../atproto/domain/DIDOrHandle'; 14 17 15 18 export interface GetGlobalFeedQuery { 16 19 page?: number; ··· 33 36 collections: { 34 37 id: string; 35 38 name: string; 39 + authorHandle: string; 40 + uri?: string; 36 41 }[]; 37 42 } 38 43 ··· 65 70 private profileService: IProfileService, 66 71 private cardQueryRepository: ICardQueryRepository, 67 72 private collectionRepository: ICollectionRepository, 73 + private identityResolutionService: IIdentityResolutionService, 68 74 ) {} 69 75 70 76 async execute( ··· 174 180 ), 175 181 ]; 176 182 177 - const collectionDataMap = new Map<string, { id: string; name: string }>(); 183 + const collectionDataMap = new Map< 184 + string, 185 + { id: string; name: string; authorHandle: string; uri?: string } 186 + >(); 178 187 // Fetch all collections in parallel using Promise.all 179 188 const collectionResults = await Promise.all( 180 189 collectionIds.map(async (collectionId) => { ··· 186 195 const collectionResult = await this.collectionRepository.findById( 187 196 collectionIdResult.value, 188 197 ); 189 - if (collectionResult.isOk() && collectionResult.value) { 190 - const collection = collectionResult.value; 191 - return { 192 - id: collection.collectionId.getStringValue(), 193 - name: collection.name.toString(), 194 - collectionId, 195 - }; 198 + if (collectionResult.isErr() || !collectionResult.value) { 199 + return null; 200 + } 201 + 202 + const collection = collectionResult.value; 203 + 204 + const didOrHandleResult = DIDOrHandle.create( 205 + collection.authorId.value, 206 + ); 207 + if (didOrHandleResult.isErr()) { 208 + return null; 209 + } 210 + 211 + const resolvedHandleResult = 212 + await this.identityResolutionService.resolveToHandle( 213 + didOrHandleResult.value, 214 + ); 215 + if (resolvedHandleResult.isErr()) { 216 + return null; 196 217 } 197 - return null; 218 + 219 + const handle = resolvedHandleResult.value; 220 + const uri = collection.publishedRecordId?.uri; 221 + 222 + return { 223 + id: collection.collectionId.getStringValue(), 224 + name: collection.name.toString(), 225 + authorHandle: handle.value, 226 + uri, 227 + collectionId, 228 + }; 198 229 }), 199 230 ); 200 231 ··· 203 234 collectionDataMap.set(result.collectionId, { 204 235 id: result.id, 205 236 name: result.name, 237 + authorHandle: result.authorHandle, 238 + uri: result.uri, 206 239 }); 207 240 } 208 241 }); ··· 223 256 224 257 const collections = (activity.metadata.collectionIds || []) 225 258 .map((collectionId) => collectionDataMap.get(collectionId)) 226 - .filter( 227 - (collection): collection is { id: string; name: string } => 228 - !!collection, 229 - ); 259 + .filter((collection) => !!collection); 230 260 231 261 feedItems.push({ 232 262 id: activity.activityId.getStringValue(), 233 - user: { 234 - id: actor.id, 235 - handle: actor.handle, 236 - name: actor.name, 237 - avatarUrl: actor.avatarUrl, 238 - }, 263 + user: actor, 239 264 card: cardData, 240 265 createdAt: activity.createdAt, 241 266 collections,
+1
src/shared/infrastructure/http/factories/UseCaseFactory.ts
··· 177 177 services.profileService, 178 178 repositories.cardQueryRepository, 179 179 repositories.collectionRepository, 180 + services.identityResolutionService, 180 181 ), 181 182 addActivityToFeedUseCase: new AddActivityToFeedUseCase( 182 183 services.feedService,
+2
src/webapp/api-client/types/responses.ts
··· 283 283 collections: { 284 284 id: string; 285 285 name: string; 286 + authorHandle: string; 287 + uri?: string; 286 288 }[]; 287 289 } 288 290