This repository has no description
4.8 kB
141 lines
1import { Result, ok, err } from '../../../../../shared/core/Result';
2import { BaseUseCase } from '../../../../../shared/core/UseCase';
3import { UseCaseError } from '../../../../../shared/core/UseCaseError';
4import { AppError } from '../../../../../shared/core/AppError';
5import { IEventPublisher } from '../../../../../shared/application/events/IEventPublisher';
6import { IFollowsRepository } from '../../../domain/repositories/IFollowsRepository';
7import { IFollowPublisher } from '../../ports/IFollowPublisher';
8import { DID } from '../../../domain/value-objects/DID';
9import { FollowTargetType } from '../../../domain/value-objects/FollowTargetType';
10import { IProfileService } from '../../../../cards/domain/services/IProfileService';
11
12export interface UnfollowTargetDTO {
13 followerId: string; // DID
14 targetId: string; // DID or Collection UUID
15 targetType: 'USER' | 'COLLECTION';
16 skipUnpublish?: boolean; // For firehose events - skip unpublishing if true
17}
18
19export class ValidationError extends UseCaseError {
20 constructor(message: string) {
21 super(message);
22 }
23}
24
25export class UnfollowTargetUseCase extends BaseUseCase<
26 UnfollowTargetDTO,
27 Result<void, ValidationError | AppError.UnexpectedError>
28> {
29 constructor(
30 private followsRepository: IFollowsRepository,
31 private followPublisher: IFollowPublisher,
32 private profileService: IProfileService,
33 eventPublisher: IEventPublisher,
34 ) {
35 super(eventPublisher);
36 }
37
38 async execute(
39 request: UnfollowTargetDTO,
40 ): Promise<Result<void, ValidationError | AppError.UnexpectedError>> {
41 try {
42 // 1. Validate followerId (create DID value object)
43 const followerDidResult = DID.create(request.followerId);
44 if (followerDidResult.isErr()) {
45 return err(
46 new ValidationError(
47 `Invalid follower ID: ${followerDidResult.error.message}`,
48 ),
49 );
50 }
51 const followerDid = followerDidResult.value;
52
53 // 2. Validate targetType
54 const targetTypeResult = FollowTargetType.create(
55 request.targetType as any,
56 );
57 if (targetTypeResult.isErr()) {
58 return err(
59 new ValidationError(
60 `Invalid target type: ${targetTypeResult.error.message}`,
61 ),
62 );
63 }
64 const targetType = targetTypeResult.value;
65
66 // 3. Find existing follow record
67 const existingFollowResult =
68 await this.followsRepository.findByFollowerAndTarget(
69 request.followerId,
70 request.targetId,
71 targetType,
72 );
73
74 if (existingFollowResult.isErr()) {
75 return err(AppError.UnexpectedError.create(existingFollowResult.error));
76 }
77
78 // 4. If not found, return success (idempotent)
79 if (!existingFollowResult.value) {
80 return ok(undefined);
81 }
82
83 const follow = existingFollowResult.value;
84
85 // 5. Unpublish from AT Protocol (skip for firehose events)
86 if (!request.skipUnpublish && follow.publishedRecordId) {
87 const unpublishResult =
88 await this.followPublisher.unpublishFollow(follow);
89 if (unpublishResult.isErr()) {
90 // Log but don't fail - we still want to delete locally
91 console.error(
92 'Failed to unpublish follow from AT Protocol:',
93 unpublishResult.error,
94 );
95 }
96 }
97
98 // 6. Call markForRemoval() (raises UserUnfollowedTargetEvent)
99 const markResult = follow.markForRemoval();
100 if (markResult.isErr()) {
101 return err(new ValidationError(markResult.error.message));
102 }
103
104 // 7. Delete from repository
105 const deleteResult = await this.followsRepository.delete(
106 request.followerId,
107 request.targetId,
108 targetType,
109 );
110
111 if (deleteResult.isErr()) {
112 return err(AppError.UnexpectedError.create(deleteResult.error));
113 }
114
115 // 8. Publish domain events
116 const publishEventsResult = await this.publishEventsForAggregate(follow);
117 if (publishEventsResult.isErr()) {
118 console.error(
119 'Failed to publish domain events:',
120 publishEventsResult.error,
121 );
122 // Don't fail the operation
123 }
124
125 // 9. Invalidate profile counts cache
126 // Invalidate follower's counts (followingCount decreased)
127 if (this.profileService.invalidateCounts) {
128 await this.profileService.invalidateCounts(request.followerId);
129 }
130 // Invalidate target's counts (followerCount decreased, only for USER targets)
131 if (targetType.value === 'USER' && this.profileService.invalidateCounts) {
132 await this.profileService.invalidateCounts(request.targetId);
133 }
134
135 // 10. Return success
136 return ok(undefined);
137 } catch (error) {
138 return err(AppError.UnexpectedError.create(error));
139 }
140 }
141}