Files
fluxer/fluxer_api/src/api/middleware/ServiceSingletons.ts
T

489 lines
23 KiB
TypeScript

// SPDX-License-Identifier: AGPL-3.0-or-later
import {createMockLogger} from '@fluxer/logger/src/mock';
import type {ICacheService} from '@pkgs/cache/src/ICacheService';
import {KVCacheProvider} from '@pkgs/cache/src/providers/KVCacheProvider';
import {EmailI18nService} from '@pkgs/email/src/EmailI18nService';
import type {EmailConfig, UserBouncedEmailChecker} from '@pkgs/email/src/EmailProviderTypes';
import {EmailService} from '@pkgs/email/src/EmailService';
import type {IEmailService} from '@pkgs/email/src/IEmailService';
import {TestEmailService} from '@pkgs/email/src/TestEmailService';
import type {IKVProvider} from '@pkgs/kv_client/src/IKVProvider';
import {NatsConnectionManager} from '@pkgs/nats/src/NatsConnectionManager';
import {RateLimitService} from '@pkgs/rate_limit/src/RateLimitService';
import type {ISmsProvider} from '@pkgs/sms/src/providers/ISmsProvider';
import {createSmsProvider} from '@pkgs/sms/src/providers/SmsProviderFactory';
import {SmsService} from '@pkgs/sms/src/SmsService';
import type {IVirusScanService} from '@pkgs/virus_scan/src/IVirusScanService';
import {AdminRepository} from '../admin/AdminRepository';
import {AdminApiKeyRepository} from '../admin/repositories/AdminApiKeyRepository';
import {AdminArchiveRepository} from '../admin/repositories/AdminArchiveRepository';
import {AdminApiKeyService} from '../admin/services/AdminApiKeyService';
import {AdminArchiveService} from '../admin/services/AdminArchiveService';
import {AdminAuditService} from '../admin/services/AdminAuditService';
import {PhoneAttemptRiskService} from '../auth/services/PhoneAttemptRiskService';
import {PhoneFraudGraphService} from '../auth/services/PhoneFraudGraphService';
import type {UserID} from '../BrandedTypes';
import {Config} from '../Config';
import {ChannelRepository} from '../channel/ChannelRepository';
import {AttachmentUploadTraceRepository} from '../channel/repositories/message/AttachmentUploadTraceRepository';
import {StreamPreviewService} from '../channel/services/StreamPreviewService';
import type {APIConfig} from '../config/APIConfig';
import {ConnectionRepository} from '../connection/ConnectionRepository';
import {createNcmecApiConfig, NcmecReporter} from '../csam/NcmecReporter';
import {NcmecRepository} from '../csam/NcmecRepository';
import {NcmecSubmissionService} from '../csam/NcmecSubmissionService';
import {DonationRepository} from '../donation/DonationRepository';
import {DownloadService} from '../download/DownloadService';
import {createEmailProvider} from '../email/EmailProviderFactory';
import {FavoriteMemeRepository} from '../favorite_meme/FavoriteMemeRepository';
import {GatewayRequestService} from '../gateway/GatewayRequestService';
import {GifService} from '../gif/GifService';
import {createNatsGifProvider} from '../gif/NatsGifProvider';
import {GuildAuditLogService} from '../guild/GuildAuditLogService';
import {GuildDiscoveryRepository} from '../guild/repositories/GuildDiscoveryRepository';
import {GuildRepository} from '../guild/repositories/GuildRepository';
import {GuildDiscoveryService} from '../guild/services/GuildDiscoveryService';
import {AssetDeletionQueue} from '../infrastructure/AssetDeletionQueue';
import {AvatarService} from '../infrastructure/AvatarService';
import {BunnyPurgeQueue, type IPurgeQueue, NoopPurgeQueue} from '../infrastructure/BunnyPurgeQueue';
import {DisabledVirusScanService} from '../infrastructure/DisabledVirusScanService';
import {DiscriminatorService} from '../infrastructure/DiscriminatorService';
import {EmailDnsValidationService} from '../infrastructure/EmailDnsValidationService';
import {EmbedService} from '../infrastructure/EmbedService';
import {EntityAssetService} from '../infrastructure/EntityAssetService';
import {ErrorI18nService} from '../infrastructure/ErrorI18nService';
import type {IAssetDeletionQueue} from '../infrastructure/IAssetDeletionQueue';
import type {IStorageService} from '../infrastructure/IStorageService';
import type {IUnfurlerService} from '../infrastructure/IUnfurlerService';
import {KVAccountDeletionQueueService} from '../infrastructure/KVAccountDeletionQueueService';
import {KVActivityTracker} from '../infrastructure/KVActivityTracker';
import {KVBulkMessageDeletionQueueService} from '../infrastructure/KVBulkMessageDeletionQueueService';
import {NatsUnfurlerService} from '../infrastructure/NatsUnfurlerService';
import {PremiumStateReconciliationQueueService} from '../infrastructure/PremiumStateReconciliationQueueService';
import {createDownloadsStorageService, createStorageService} from '../infrastructure/StorageServiceFactory';
import {UserCacheService} from '../infrastructure/UserCacheService';
import {createUsersServiceClient} from '../infrastructure/UsersServiceClient';
import {VirusScanService} from '../infrastructure/VirusScanService';
import {GatewayRolloutConfigPublisher} from '../instance/GatewayRolloutConfigPublisher';
import {InstanceConfigRepository} from '../instance/InstanceConfigRepository';
import {InviteRepository} from '../invite/InviteRepository';
import {Logger} from '../Logger';
import {LimitConfigService} from '../limits/LimitConfigService';
import {BotAuthService} from '../oauth/BotAuthService';
import {BotMfaMirrorService} from '../oauth/BotMfaMirrorService';
import {ApplicationRepository} from '../oauth/repositories/ApplicationRepository';
import {OAuth2TokenRepository} from '../oauth/repositories/OAuth2TokenRepository';
import {ReadStateRepository} from '../read_state/ReadStateRepository';
import {ReadStateRequestService} from '../read_state/ReadStateRequestService';
import {ReadStateService} from '../read_state/ReadStateService';
import {ReportRepository} from '../report/ReportRepository';
import {getGuildSearchService} from '../SearchFactory';
import {ThemeService} from '../theme/ThemeService';
import {EntranceSoundPlayService} from '../user/entrance_sound/EntranceSoundPlayService';
import {EntranceSoundRepository} from '../user/entrance_sound/EntranceSoundRepository';
import {EntranceSoundService} from '../user/entrance_sound/EntranceSoundService';
import {EmailChangeRepository} from '../user/repositories/auth/EmailChangeRepository';
import {PasswordChangeRepository} from '../user/repositories/auth/PasswordChangeRepository';
import {UserContactChangeLogRepository} from '../user/repositories/UserContactChangeLogRepository';
import {UserRepository} from '../user/repositories/UserRepository';
import {VisionarySlotRepository} from '../user/repositories/VisionarySlotRepository';
import {UserActivityBuffer} from '../user/services/UserActivityBuffer';
import {UserContactChangeLogService} from '../user/services/UserContactChangeLogService';
import {UserPermissionUtils} from '../utils/UserPermissionUtils';
import {VoiceRepository} from '../voice/VoiceRepository';
import {SweegoWebhookService} from '../webhook/SweegoWebhookService';
import {WebhookRepository} from '../webhook/WebhookRepository';
import {
getGatewayService,
getKVClient,
getMediaService,
getSnowflakeService,
getWorkerService,
} from './ServiceRegistry';
import {clearSingletonsForTesting, singleton} from './Singleton';
export const getUserRepository = singleton(() => new UserRepository(getKVClient()));
export const getGuildRepository = singleton(() => new GuildRepository());
export const getChannelRepository = singleton(() => new ChannelRepository());
export const getInviteRepository = singleton(() => new InviteRepository());
export const getWebhookRepository = singleton(() => new WebhookRepository());
export const getReadStateRepository = singleton(() => new ReadStateRepository());
export const getFavoriteMemeRepository = singleton(() => new FavoriteMemeRepository());
export const getConnectionRepository = singleton(() => new ConnectionRepository());
export const getReportRepository = singleton(() => new ReportRepository());
export const getAdminRepository = singleton(() => new AdminRepository());
export const getAdminArchiveRepository = singleton(() => new AdminArchiveRepository());
export const getVoiceRepository = singleton(() => new VoiceRepository());
export const getApplicationRepository = singleton(() => new ApplicationRepository());
export const getOAuth2TokenRepository = singleton(() => new OAuth2TokenRepository());
export const getGuildDiscoveryRepository = singleton(() => new GuildDiscoveryRepository());
export const getEmailChangeRepository = singleton(() => new EmailChangeRepository());
export const getPasswordChangeRepository = singleton(() => new PasswordChangeRepository());
const getUserContactChangeLogRepository = singleton(() => new UserContactChangeLogRepository());
export const getDonationRepository = singleton(() => new DonationRepository());
const getAdminApiKeyRepository = singleton(() => new AdminApiKeyRepository());
export const getInstanceConfigRepository = singleton(
() => new InstanceConfigRepository(getKVClient()),
(repository) => repository.shutdown(),
);
export const getGatewayRolloutConfigPublisher = singleton(
() =>
new GatewayRolloutConfigPublisher(
new NatsConnectionManager({
url: Config.nats.coreUrl,
token: Config.nats.authToken || undefined,
name: 'fluxer-api-gateway-rollout-config',
}),
),
);
export const getVisionarySlotRepository = singleton(() => new VisionarySlotRepository());
export const getCacheService: () => ICacheService = singleton(() => new KVCacheProvider({client: getKVClient()}));
export const getRateLimitService = singleton(() => new RateLimitService(getKVClient()));
export const getPhoneFraudGraphService = singleton(
() => new PhoneFraudGraphService(getKVClient(), getUserRepository()),
);
export const getPhoneAttemptRiskService = singleton(() => {
const service = new PhoneAttemptRiskService(getCacheService(), getKVClient());
const graph = getPhoneFraudGraphService();
service.onHardBlock(({userId, clientIp}) => {
void graph.propagateHardBlock(userId ? (BigInt(userId) as UserID) : null, clientIp);
});
return service;
});
export const getEmailDnsValidationService = singleton(() => new EmailDnsValidationService());
function createEmailServiceForConfig(
emailConfigSource: APIConfig['email'],
bouncedEmailChecker: UserBouncedEmailChecker,
emailI18n: EmailI18nService,
): IEmailService {
const emailConfig: EmailConfig = {
enabled: emailConfigSource.enabled,
fromEmail: emailConfigSource.fromEmail,
fromName: emailConfigSource.fromName,
appBaseUrl: emailConfigSource.appBaseUrl,
marketingBaseUrl: Config.endpoints.marketing,
};
return new EmailService(emailConfig, emailI18n, createEmailProvider(emailConfigSource), bouncedEmailChecker);
}
function createRuntimeEmailService(bouncedEmailChecker: UserBouncedEmailChecker): IEmailService {
const emailI18n = new EmailI18nService();
return new Proxy({} as IEmailService, {
get(_target, property) {
return async (...args: Array<unknown>): Promise<boolean> => {
const emailConfig = await getInstanceConfigRepository().getEffectiveEmailConfig();
const delegate = createEmailServiceForConfig(emailConfig, bouncedEmailChecker, emailI18n);
const method = delegate[property as keyof IEmailService];
if (typeof method !== 'function') {
throw new Error(`Unknown email service method: ${String(property)}`);
}
return (method as (this: IEmailService, ...methodArgs: Array<unknown>) => Promise<boolean>).apply(
delegate,
args,
);
};
},
});
}
let _injectedStorageService: IStorageService | undefined;
export function setInjectedStorageService(service: IStorageService | undefined): void {
_injectedStorageService = service;
}
export const getStorageService: () => IStorageService = (() => {
const fallback = singleton(() => createStorageService());
return () => _injectedStorageService ?? fallback();
})();
const getDownloadsStorageService: () => IStorageService = (() => {
const override = singleton(() => createDownloadsStorageService());
return () => override() ?? getStorageService();
})();
export const getErrorI18nService = singleton(() => new ErrorI18nService());
export const getLimitConfigService = singleton(
() => new LimitConfigService(getInstanceConfigRepository(), getCacheService(), getKVClient()),
(service) => service.shutdown(),
);
export const getPurgeQueue: () => IPurgeQueue = singleton(() =>
Config.bunny.purgeEnabled ? new BunnyPurgeQueue(getKVClient()) : new NoopPurgeQueue(),
);
export const getAssetDeletionQueue: () => IAssetDeletionQueue = singleton(() => new AssetDeletionQueue(getKVClient()));
let bulkMessageDeletionQueueClient: IKVProvider | null = null;
let bulkMessageDeletionQueue: KVBulkMessageDeletionQueueService | null = null;
export function getKVBulkMessageDeletionQueue(): KVBulkMessageDeletionQueueService {
const kvClient = getKVClient();
if (!bulkMessageDeletionQueue || bulkMessageDeletionQueueClient !== kvClient) {
bulkMessageDeletionQueue = new KVBulkMessageDeletionQueueService(kvClient, getUserRepository());
bulkMessageDeletionQueueClient = kvClient;
}
return bulkMessageDeletionQueue;
}
let premiumStateQueueClient: IKVProvider | null = null;
let premiumStateQueue: PremiumStateReconciliationQueueService | null = null;
export function getPremiumStateReconciliationQueueService(): PremiumStateReconciliationQueueService {
const kvClient = getKVClient();
if (!premiumStateQueue || premiumStateQueueClient !== kvClient) {
premiumStateQueue = new PremiumStateReconciliationQueueService(kvClient);
premiumStateQueueClient = kvClient;
}
return premiumStateQueue;
}
let activityTrackerClient: IKVProvider | null = null;
let activityTracker: KVActivityTracker | null = null;
export function getKVActivityTracker(): KVActivityTracker {
const kvClient = getKVClient();
if (!activityTracker || activityTrackerClient !== kvClient) {
activityTracker = new KVActivityTracker(kvClient);
activityTrackerClient = kvClient;
}
return activityTracker;
}
let activityBufferClient: IKVProvider | null = null;
let activityBuffer: UserActivityBuffer | null = null;
export function getUserActivityBuffer(): UserActivityBuffer {
const kvClient = getKVClient();
if (!activityBuffer || activityBufferClient !== kvClient) {
activityBuffer = new UserActivityBuffer(kvClient);
activityBufferClient = kvClient;
}
return activityBuffer;
}
let accountDeletionQueueClient: IKVProvider | null = null;
let accountDeletionQueue: KVAccountDeletionQueueService | null = null;
export function getKVAccountDeletionQueue(): KVAccountDeletionQueueService {
const kvClient = getKVClient();
if (!accountDeletionQueue || accountDeletionQueueClient !== kvClient) {
accountDeletionQueue = new KVAccountDeletionQueueService(kvClient, getUserRepository());
accountDeletionQueueClient = kvClient;
}
return accountDeletionQueue;
}
export const getDownloadService = singleton(() => new DownloadService(getDownloadsStorageService()));
export const getThemeService = singleton(() => new ThemeService(getStorageService()));
const getNcmecReporter = singleton(() => new NcmecReporter({config: createNcmecApiConfig(), fetch}));
const getNcmecRepository = singleton(() => new NcmecRepository());
export const getAttachmentUploadTraceRepository = singleton(() => new AttachmentUploadTraceRepository());
export const getNcmecSubmissionService = singleton(
() =>
new NcmecSubmissionService({
reportRepository: getReportRepository(),
ncmecApi: getNcmecReporter(),
ncmecRepository: getNcmecRepository(),
attachmentUploadTraceRepository: getAttachmentUploadTraceRepository(),
storageService: getStorageService(),
channelRepository: getChannelRepository(),
userRepository: getUserRepository(),
guildRepository: getGuildRepository(),
gatewayService: getGatewayService(),
userCacheService: createUserCacheService(),
adminArchiveService: new AdminArchiveService(
getAdminArchiveRepository(),
getUserRepository(),
getGuildRepository(),
getStorageService(),
getSnowflakeService(),
getWorkerService(),
),
adminAuditService: new AdminAuditService(getAdminRepository(), getSnowflakeService(), {
userRepository: getUserRepository(),
guildRepository: getGuildRepository(),
channelRepository: getChannelRepository(),
}),
purgeQueue: getPurgeQueue(),
workerService: getWorkerService(),
deletionQueue: getKVAccountDeletionQueue(),
}),
);
let _virusScanInitPromise: Promise<void> | null = null;
export const getVirusScanServiceInstance: () => IVirusScanService = singleton(() => {
const VirusScanServiceClass = Config.clamav.enabled ? VirusScanService : DisabledVirusScanService;
const service = new VirusScanServiceClass(getCacheService());
_virusScanInitPromise = service.initialize();
return service;
});
export async function ensureVirusScanInitialized(): Promise<void> {
getVirusScanServiceInstance();
await _virusScanInitPromise;
}
const getSmsProvider: () => ISmsProvider = singleton(() => {
if (Config.dev.testModeEnabled) {
return createSmsProvider({mode: 'test', logger: createMockLogger()});
}
if (Config.sms.enabled && Config.sms.accountSid && Config.sms.authToken && Config.sms.verifyServiceSid) {
return createSmsProvider({
mode: 'twilio',
config: {
accountSid: Config.sms.accountSid,
authToken: Config.sms.authToken,
verifyServiceSid: Config.sms.verifyServiceSid,
},
});
}
return createSmsProvider({mode: 'unavailable'});
});
export const getSmsService = singleton(() => new SmsService(getSmsProvider()));
export const getEmailService: () => IEmailService = singleton(() => {
if (Config.dev.testModeEnabled) return new TestEmailService();
const userRepository = getUserRepository();
const bouncedEmailChecker: UserBouncedEmailChecker = {
isEmailBounced: async (email: string) => {
const user = await userRepository.findByEmail(email);
return user?.emailBounced ?? false;
},
};
return createRuntimeEmailService(bouncedEmailChecker);
});
let _injectedUnfurlerService: IUnfurlerService | undefined;
export function setInjectedUnfurlerService(service: IUnfurlerService | undefined): void {
_injectedUnfurlerService = service;
}
const getDefaultUnfurlerService = singleton(() => {
const instanceConfigRepository = getInstanceConfigRepository();
const manager = new NatsConnectionManager({
url: Config.nats.coreUrl,
token: Config.nats.authToken || undefined,
name: 'fluxer-api-unfurl',
});
void manager.connect().catch((error) => {
Logger.error({error}, '[nats-unfurl] Failed to establish NATS connection');
});
return new NatsUnfurlerService(
manager,
async () => instanceConfigRepository.getEffectiveYoutubeApiKey(),
async () => (await instanceConfigRepository.getEffectiveGifConfig()).klipy_api_key,
);
});
export function getUnfurlerService(): IUnfurlerService {
return _injectedUnfurlerService ?? getDefaultUnfurlerService();
}
export const getEmbedService = singleton(
() => new EmbedService(getChannelRepository(), getUnfurlerService(), getMediaService(), getWorkerService()),
);
export const getReadStateService = singleton(() => new ReadStateService(getReadStateRepository(), getGatewayService()));
export const getDiscriminatorService = singleton(
() => new DiscriminatorService(getUserRepository(), getCacheService(), getLimitConfigService()),
);
export const getBotAuthService = singleton(() => new BotAuthService(getApplicationRepository()));
export const getBotMfaMirrorService = singleton(
() => new BotMfaMirrorService(getApplicationRepository(), getUserRepository(), getGatewayService()),
);
export const getGifService = singleton(() => {
const instanceConfigRepository = getInstanceConfigRepository();
return new GifService(
createNatsGifProvider(async () => (await instanceConfigRepository.getEffectiveGifConfig()).klipy_api_key),
);
});
export const getGuildAuditLogService = singleton(
() => new GuildAuditLogService(getGuildRepository(), getSnowflakeService(), getWorkerService(), getGatewayService()),
);
export const getUserPermissionUtils = singleton(
() => new UserPermissionUtils(getUserRepository(), getGuildRepository()),
);
export const getContactChangeLogService = singleton(
() => new UserContactChangeLogService(getUserContactChangeLogRepository()),
);
export const getSweegoWebhookService = singleton(
() => new SweegoWebhookService(getUserRepository(), getGatewayService()),
);
export const getStreamPreviewService = singleton(
() => new StreamPreviewService(getStorageService(), getCacheService()),
);
export const getAvatarService = singleton(
() => new AvatarService(getStorageService(), getMediaService(), getLimitConfigService()),
);
export const getEntityAssetService = singleton(
() =>
new EntityAssetService(getStorageService(), getMediaService(), getAssetDeletionQueue(), getLimitConfigService()),
);
export const getAdminApiKeyService = singleton(
() => new AdminApiKeyService(getAdminApiKeyRepository(), getSnowflakeService()),
);
export const getAdminArchiveService = singleton(
() =>
new AdminArchiveService(
getAdminArchiveRepository(),
getUserRepository(),
getGuildRepository(),
getStorageService(),
getSnowflakeService(),
getWorkerService(),
),
);
const getEntranceSoundRepository = singleton(() => new EntranceSoundRepository());
export const getEntranceSoundService = singleton(
() => new EntranceSoundService(getEntranceSoundRepository(), getStorageService(), getMediaService()),
);
export const getEntranceSoundPlayService = singleton(
() => new EntranceSoundPlayService(getEntranceSoundService(), getGatewayService(), getChannelRepository()),
);
export const getGatewayRequestService = singleton(() => new GatewayRequestService(getBotAuthService()));
export const getGuildDiscoveryService = singleton(
() =>
new GuildDiscoveryService(
getGuildDiscoveryRepository(),
getGuildRepository(),
getGatewayService(),
getGuildSearchService(),
),
);
export const getReadStateRequestService = singleton(() => new ReadStateRequestService(getReadStateService()));
export const getUserCacheService = singleton(() => createUserCacheService());
export function createUserCacheService(): UserCacheService {
return new UserCacheService(createUsersServiceClient());
}
let serviceSingletonInitializationPromise: Promise<void> | null = null;
export async function initializeServiceSingletons(): Promise<void> {
if (!serviceSingletonInitializationPromise) {
serviceSingletonInitializationPromise = (async () => {
const snowflakeService = getSnowflakeService();
await snowflakeService.initialize();
const limitConfigService = getLimitConfigService();
await limitConfigService.initialize();
limitConfigService.setAsGlobalInstance();
})();
}
await serviceSingletonInitializationPromise;
}
export function resetServiceSingletonsForTesting(): void {
activityTracker?.shutdown();
clearSingletonsForTesting();
_virusScanInitPromise = null;
serviceSingletonInitializationPromise = null;
bulkMessageDeletionQueue = null;
bulkMessageDeletionQueueClient = null;
premiumStateQueue = null;
premiumStateQueueClient = null;
activityTracker = null;
activityTrackerClient = null;
activityBuffer = null;
activityBufferClient = null;
accountDeletionQueue = null;
accountDeletionQueueClient = null;
}