import { err, ok, Result } from 'src/shared/core/Result'; import { UseCase } from 'src/shared/core/UseCase'; import { IFollowsRepository } from '../../../domain/repositories/IFollowsRepository'; import { IIdentityResolutionService } from 'src/modules/atproto/domain/services/IIdentityResolutionService'; import { IProfileService } from 'src/modules/cards/domain/services/IProfileService'; import { DIDOrHandle } from 'src/modules/atproto/domain/DIDOrHandle'; import { FollowTargetType } from '../../../domain/value-objects/FollowTargetType'; import { User } from '@semble/types'; import { ProfileEnricher } from 'src/modules/cards/application/services/ProfileEnricher'; export interface GetFollowersQuery { userId: string; // DID or handle callingUserId?: string; page?: number; limit?: number; } export interface GetFollowersResult { users: User[]; pagination: { currentPage: number; totalPages: number; totalCount: number; hasMore: boolean; limit: number; }; } export class ValidationError extends Error { constructor(message: string) { super(message); this.name = 'ValidationError'; } } export class GetFollowersUseCase implements UseCase> { constructor( private followsRepository: IFollowsRepository, private identityResolver: IIdentityResolutionService, private profileService: IProfileService, ) {} async execute(query: GetFollowersQuery): Promise> { // Set defaults const page = query.page || 1; const limit = Math.min(query.limit || 20, 100); // Cap at 100 // Parse and validate user identifier const identifierResult = DIDOrHandle.create(query.userId); if (identifierResult.isErr()) { return err(new ValidationError('Invalid user identifier')); } // Resolve to DID const didResult = await this.identityResolver.resolveToDID( identifierResult.value, ); if (didResult.isErr()) { return err( new ValidationError( `Could not resolve user identifier: ${didResult.error.message}`, ), ); } try { // Get all followers (not paginated at repo level yet) const followersResult = await this.followsRepository.getFollowers( didResult.value.value, FollowTargetType.USER, ); if (followersResult.isErr()) { return err( new Error( `Failed to retrieve followers: ${followersResult.error instanceof Error ? followersResult.error.message : 'Unknown error'}`, ), ); } const allFollows = followersResult.value; const totalCount = allFollows.length; // Apply pagination manually const offset = (page - 1) * limit; const paginatedFollows = allFollows.slice(offset, offset + limit); // Extract unique follower IDs const uniqueFollowerIds = Array.from( new Set(paginatedFollows.map((follow) => follow.followerId.value)), ); // Fetch profiles for all followers using ProfileEnricher const profileEnricher = new ProfileEnricher(this.profileService); const profileMapResult = await profileEnricher.buildProfileMap( uniqueFollowerIds, query.callingUserId, { skipFailures: true, // Skip profiles that fail to resolve mapToUser: true, // Use full User DTO with isFollowing }, ); if (profileMapResult.isErr()) { return err( new Error( `Failed to fetch user profiles: ${profileMapResult.error.message}`, ), ); } const profileMap = profileMapResult.value; // Build users array in the order of follows (reverse chronological) const users: User[] = paginatedFollows .map((follow) => profileMap.get(follow.followerId.value)) .filter((user): user is User => user !== undefined); return ok({ users, pagination: { currentPage: page, totalPages: Math.ceil(totalCount / limit), totalCount, hasMore: page * limit < totalCount, limit, }, }); } catch (error) { return err( new Error( `Failed to retrieve followers: ${error instanceof Error ? error.message : 'Unknown error'}`, ), ); } } }