A social knowledge tool for researchers built on ATProto
1import { EnvironmentConfigService } from '../../config/EnvironmentConfigService';
2import { jwtConfig, oauthConfig } from '../../config';
3import { JwtTokenService } from '../../../../modules/user/infrastructure/services/JwtTokenService';
4import {
5 AtProtoOAuthProcessor,
6 OAuthClientFactory,
7} from '../../../../modules/user/infrastructure';
8import { UserAuthenticationService } from '../../../../modules/user/infrastructure/services/UserAuthenticationService';
9import { ATProtoAgentService } from '../../../../modules/atproto/infrastructure/services/ATProtoAgentService';
10import { IFramelyMetadataService } from '../../../../modules/cards/infrastructure/IFramelyMetadataService';
11import { BlueskyProfileService } from '../../../../modules/atproto/infrastructure/services/BlueskyProfileService';
12import { ATProtoCollectionPublisher } from '../../../../modules/atproto/infrastructure/publishers/ATProtoCollectionPublisher';
13import { ATProtoCardPublisher } from '../../../../modules/atproto/infrastructure/publishers/ATProtoCardPublisher';
14import { FakeCollectionPublisher } from '../../../../modules/cards/tests/utils/FakeCollectionPublisher';
15import { FakeCardPublisher } from '../../../../modules/cards/tests/utils/FakeCardPublisher';
16import { CardLibraryService } from '../../../../modules/cards/domain/services/CardLibraryService';
17import { CardCollectionService } from '../../../../modules/cards/domain/services/CardCollectionService';
18import { AuthMiddleware } from '../middleware/AuthMiddleware';
19import { Repositories } from './RepositoryFactory';
20import { NodeOAuthClient } from '@atproto/oauth-client-node';
21import { AppPasswordSessionService } from 'src/modules/atproto/infrastructure/services/AppPasswordSessionService';
22import { AtpAppPasswordProcessor } from 'src/modules/atproto/infrastructure/services/AtpAppPasswordProcessor';
23import { ICollectionPublisher } from 'src/modules/cards/application/ports/ICollectionPublisher';
24import { ICardPublisher } from 'src/modules/cards/application/ports/ICardPublisher';
25import { IMetadataService } from 'src/modules/cards/domain/services/IMetadataService';
26import { BullMQEventSubscriber } from '../../events/BullMQEventSubscriber';
27import { BullMQEventPublisher } from '../../events/BullMQEventPublisher';
28import { InMemoryEventPublisher } from '../../events/InMemoryEventPublisher';
29import { InMemoryEventSubscriber } from '../../events/InMemoryEventSubscriber';
30import Redis from 'ioredis';
31// Mock/Fake service imports
32import { FakeJwtTokenService } from '../../../../modules/user/infrastructure/services/FakeJwtTokenService';
33import { FakeAtProtoOAuthProcessor } from '../../../../modules/atproto/infrastructure/services/FakeAtProtoOAuthProcessor';
34import { FakeUserAuthenticationService } from '../../../../modules/user/infrastructure/services/FakeUserAuthenticationService';
35import { FakeAgentService } from '../../../../modules/atproto/infrastructure/services/FakeAgentService';
36import { FakeBlueskyProfileService } from '../../../../modules/atproto/infrastructure/services/FakeBlueskyProfileService';
37import { FakeAppPasswordSessionService } from '../../../../modules/atproto/infrastructure/services/FakeAppPasswordSessionService';
38import { FakeAtpAppPasswordProcessor } from '../../../../modules/atproto/infrastructure/services/FakeAtpAppPasswordProcessor';
39import { ITokenService } from 'src/modules/user/application/services/ITokenService';
40import { IOAuthProcessor } from 'src/modules/user/application/services/IOAuthProcessor';
41import { IAppPasswordProcessor } from 'src/modules/atproto/application/IAppPasswordProcessor';
42import { IUserAuthenticationService } from 'src/modules/user/domain/services/IUserAuthenticationService';
43import { IAgentService } from 'src/modules/atproto/application/IAgentService';
44import { IProfileService } from 'src/modules/cards/domain/services/IProfileService';
45import { IEventPublisher } from '../../../application/events/IEventPublisher';
46import { QueueName } from '../../events/QueueConfig';
47import { RedisFactory } from '../../redis/RedisFactory';
48import { IEventSubscriber } from 'src/shared/application/events/IEventSubscriber';
49import { FeedService } from '../../../../modules/feeds/domain/services/FeedService';
50import { CardCollectionSaga } from '../../../../modules/feeds/application/sagas/CardCollectionSaga';
51import { ATProtoIdentityResolutionService } from '../../../../modules/atproto/infrastructure/services/ATProtoIdentityResolutionService';
52import { IIdentityResolutionService } from '../../../../modules/atproto/domain/services/IIdentityResolutionService';
53
54// Shared services needed by both web app and workers
55export interface SharedServices {
56 tokenService: ITokenService;
57 userAuthService: IUserAuthenticationService;
58 atProtoAgentService: IAgentService;
59 metadataService: IMetadataService;
60 profileService: IProfileService;
61 feedService: FeedService;
62 nodeOauthClient: NodeOAuthClient;
63 identityResolutionService: IIdentityResolutionService;
64 configService: EnvironmentConfigService;
65}
66
67// Web app specific services (includes publishers, auth middleware)
68export interface WebAppServices extends SharedServices {
69 oauthProcessor: IOAuthProcessor;
70 appPasswordProcessor: IAppPasswordProcessor;
71 collectionPublisher: ICollectionPublisher;
72 cardPublisher: ICardPublisher;
73 cardLibraryService: CardLibraryService;
74 cardCollectionService: CardCollectionService;
75 authMiddleware: AuthMiddleware;
76 eventPublisher: IEventPublisher;
77}
78
79// Worker specific services (includes subscribers)
80export interface WorkerServices extends SharedServices {
81 redisConnection: Redis | null;
82 eventPublisher: IEventPublisher;
83 createEventSubscriber: (queueName: QueueName) => IEventSubscriber;
84 cardCollectionSaga: CardCollectionSaga;
85}
86
87// Legacy interface for backward compatibility
88export interface Services extends WebAppServices {}
89
90export class ServiceFactory {
91 static create(
92 configService: EnvironmentConfigService,
93 repositories: Repositories,
94 ): Services {
95 return this.createForWebApp(configService, repositories);
96 }
97
98 static createForWebApp(
99 configService: EnvironmentConfigService,
100 repositories: Repositories,
101 ): WebAppServices {
102 const sharedServices = this.createSharedServices(
103 configService,
104 repositories,
105 );
106
107 const useMockAuth = process.env.USE_MOCK_AUTH === 'true';
108
109 // App Password Session Service
110 const appPasswordSessionService = useMockAuth
111 ? new FakeAppPasswordSessionService()
112 : new AppPasswordSessionService(
113 repositories.appPasswordSessionRepository,
114 );
115
116 // App Password Processor
117 const appPasswordProcessor = useMockAuth
118 ? new FakeAtpAppPasswordProcessor()
119 : new AtpAppPasswordProcessor(appPasswordSessionService);
120
121 // OAuth Processor
122 const oauthProcessor = useMockAuth
123 ? new FakeAtProtoOAuthProcessor(sharedServices.tokenService)
124 : new AtProtoOAuthProcessor(sharedServices.nodeOauthClient);
125
126 const useFakePublishers = process.env.USE_FAKE_PUBLISHERS === 'true';
127 const collections = configService.getAtProtoCollections();
128
129 const collectionPublisher = useFakePublishers
130 ? new FakeCollectionPublisher()
131 : new ATProtoCollectionPublisher(
132 sharedServices.atProtoAgentService,
133 collections.collection,
134 collections.collectionLink,
135 );
136
137 const cardPublisher = useFakePublishers
138 ? new FakeCardPublisher()
139 : new ATProtoCardPublisher(
140 sharedServices.atProtoAgentService,
141 collections.card,
142 );
143
144 const cardCollectionService = new CardCollectionService(
145 repositories.collectionRepository,
146 collectionPublisher,
147 );
148 const cardLibraryService = new CardLibraryService(
149 repositories.cardRepository,
150 cardPublisher,
151 repositories.collectionRepository,
152 cardCollectionService,
153 );
154
155 const authMiddleware = new AuthMiddleware(sharedServices.tokenService);
156
157 const useInMemoryEvents = process.env.USE_IN_MEMORY_EVENTS === 'true';
158
159 let eventPublisher: IEventPublisher;
160 if (useInMemoryEvents) {
161 eventPublisher = new InMemoryEventPublisher();
162 } else {
163 const redisConnection = RedisFactory.createConnection(
164 configService.getWorkersConfig().redisConfig,
165 );
166 eventPublisher = new BullMQEventPublisher(redisConnection);
167 }
168
169 return {
170 ...sharedServices,
171 oauthProcessor,
172 appPasswordProcessor,
173 collectionPublisher,
174 cardPublisher,
175 cardLibraryService,
176 cardCollectionService,
177 authMiddleware,
178 eventPublisher,
179 };
180 }
181
182 static createForWorker(
183 configService: EnvironmentConfigService,
184 repositories: Repositories,
185 ): WorkerServices {
186 const sharedServices = this.createSharedServices(
187 configService,
188 repositories,
189 );
190
191 const useInMemoryEvents = process.env.USE_IN_MEMORY_EVENTS === 'true';
192
193 let eventPublisher: IEventPublisher;
194 let redisConnection: Redis | null = null;
195 let createEventSubscriber: (queueName: QueueName) => IEventSubscriber;
196
197 if (useInMemoryEvents) {
198 eventPublisher = new InMemoryEventPublisher();
199 createEventSubscriber = (queueName: QueueName) => {
200 return new InMemoryEventSubscriber();
201 };
202 } else {
203 // Redis connection is required for BullMQ workers
204 if (!process.env.REDIS_URL) {
205 throw new Error(
206 'REDIS_URL environment variable is required for BullMQ worker services',
207 );
208 }
209
210 const redisConfig = configService.getWorkersConfig().redisConfig;
211 redisConnection = RedisFactory.createConnection(redisConfig);
212 eventPublisher = new BullMQEventPublisher(redisConnection);
213
214 createEventSubscriber = (queueName: QueueName) => {
215 return new BullMQEventSubscriber(redisConnection!, { queueName });
216 };
217 }
218
219 // Create saga for worker
220 const cardCollectionSaga = new CardCollectionSaga(
221 // We'll need to create this use case in the worker context
222 null as any, // Will be set properly in worker
223 );
224
225 return {
226 ...sharedServices,
227 redisConnection: redisConnection,
228 eventPublisher,
229 createEventSubscriber,
230 cardCollectionSaga,
231 };
232 }
233
234 private static createSharedServices(
235 configService: EnvironmentConfigService,
236 repositories: Repositories,
237 ): SharedServices {
238 const useMockAuth = process.env.USE_MOCK_AUTH === 'true';
239
240 const nodeOauthClient = OAuthClientFactory.createClient(
241 repositories.oauthStateStore,
242 repositories.oauthSessionStore,
243 oauthConfig.baseUrl,
244 );
245
246 // Token Service
247 const tokenService = useMockAuth
248 ? new FakeJwtTokenService(repositories.tokenRepository)
249 : new JwtTokenService(
250 repositories.tokenRepository,
251 jwtConfig.jwtSecret,
252 jwtConfig.accessTokenExpiresIn,
253 jwtConfig.refreshTokenExpiresIn,
254 );
255
256 // User Authentication Service
257 const userAuthService = useMockAuth
258 ? new FakeUserAuthenticationService(repositories.userRepository)
259 : new UserAuthenticationService(repositories.userRepository);
260
261 // App Password Session Service (needed for ATProto Agent Service)
262 const appPasswordSessionService = useMockAuth
263 ? new FakeAppPasswordSessionService()
264 : new AppPasswordSessionService(
265 repositories.appPasswordSessionRepository,
266 );
267
268 // ATProto Agent Service
269 const atProtoAgentService = useMockAuth
270 ? new FakeAgentService()
271 : new ATProtoAgentService(nodeOauthClient, appPasswordSessionService);
272
273 const metadataService = new IFramelyMetadataService(
274 configService.getIFramelyApiKey(),
275 );
276
277 // Profile Service
278 const profileService = useMockAuth
279 ? new FakeBlueskyProfileService()
280 : new BlueskyProfileService(atProtoAgentService);
281
282 // Feed Service
283 const feedService = new FeedService(repositories.feedRepository);
284
285 // Identity Resolution Service
286 const identityResolutionService = new ATProtoIdentityResolutionService(
287 atProtoAgentService,
288 );
289
290 return {
291 tokenService,
292 userAuthService,
293 atProtoAgentService,
294 metadataService,
295 profileService,
296 feedService,
297 nodeOauthClient,
298 identityResolutionService,
299 configService,
300 };
301 }
302}