diff --git a/fluxer_api/pkgs/cassandra/src/CassandraTypes.ts b/fluxer_api/pkgs/cassandra/src/CassandraTypes.ts deleted file mode 100644 index 92d808340..000000000 --- a/fluxer_api/pkgs/cassandra/src/CassandraTypes.ts +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {types} from 'cassandra-driver'; - -type CassandraValue = - | string - | number - | bigint - | boolean - | Buffer - | Date - | types.LocalDate - | Set - | Map - | Array - | Record - | null; -export type CassandraParams = Record; - -export interface PreparedQuery

{ - cql: string; - params: P; -} diff --git a/fluxer_api/pkgs/cassandra/src/Client.ts b/fluxer_api/pkgs/cassandra/src/Client.ts index 891c91981..9a5ecd95d 100644 --- a/fluxer_api/pkgs/cassandra/src/Client.ts +++ b/fluxer_api/pkgs/cassandra/src/Client.ts @@ -1,7 +1,5 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import type {CassandraParams, PreparedQuery} from '@pkgs/cassandra/src/CassandraTypes'; -import {type Logger, NoopLogger} from '@pkgs/cassandra/src/Logger'; import cassandra from 'cassandra-driver'; const distance = cassandra.types.distance; @@ -22,45 +20,19 @@ interface CassandraConfig { readTimeoutMs?: number | undefined; } -interface CassandraClientOptions { - logger?: Logger | undefined; -} - -interface CassandraExecuteOptions { - prepare?: boolean | undefined; -} - -interface CassandraBatchOptions { - prepare?: boolean | undefined; -} - -interface ICassandraClient { - connect(): Promise; - shutdown(): Promise; - execute

( - query: PreparedQuery

, - options?: CassandraExecuteOptions, - ): Promise; - batch(queries: Array, options?: CassandraBatchOptions): Promise; - getNativeClient(): cassandra.Client; -} - interface DefaultClientState { client: CassandraClient | null; - logger: Logger; } const defaultClientState: DefaultClientState = { client: null, - logger: NoopLogger, }; -class CassandraClient implements ICassandraClient { +class CassandraClient { private readonly config: CassandraConfig; - private logger: Logger; private client: cassandra.Client | null; - public constructor(config: CassandraConfig, options: CassandraClientOptions = {}) { + public constructor(config: CassandraConfig) { this.config = { hosts: [...config.hosts], port: config.port, @@ -70,7 +42,6 @@ class CassandraClient implements ICassandraClient { password: config.password, readTimeoutMs: config.readTimeoutMs, }; - this.logger = options.logger ?? NoopLogger; this.client = null; } @@ -109,14 +80,6 @@ class CassandraClient implements ICassandraClient { }); await client.connect(); this.client = client; - this.logger.info( - { - hosts: this.config.hosts, - keyspace: this.config.keyspace, - local_dc: this.config.localDc, - }, - 'Connected to Cassandra', - ); } public async shutdown(): Promise { @@ -126,26 +89,6 @@ class CassandraClient implements ICassandraClient { const activeClient = this.client; this.client = null; await activeClient.shutdown(); - this.logger.info({}, 'Cassandra connection closed'); - } - - public async execute

( - query: PreparedQuery

, - options: CassandraExecuteOptions = {}, - ): Promise { - return this.getNativeClient().execute(query.cql, query.params, { - prepare: options.prepare ?? true, - }); - } - - public async batch(queries: Array, options: CassandraBatchOptions = {}): Promise { - if (queries.length === 0) { - return; - } - const batch = queries.map((query) => ({query: query.cql, params: query.params})); - await this.getNativeClient().batch(batch, { - prepare: options.prepare ?? true, - }); } public getNativeClient(): cassandra.Client { @@ -160,7 +103,7 @@ export async function initCassandra(config: CassandraConfig): Promise { if (defaultClientState.client !== null) { await defaultClientState.client.shutdown(); } - const client = new CassandraClient(config, {logger: defaultClientState.logger}); + const client = new CassandraClient(config); await client.connect(); defaultClientState.client = client; } @@ -173,7 +116,7 @@ export async function shutdownCassandra(): Promise { defaultClientState.client = null; } -function getDefaultCassandraClient(): ICassandraClient { +function getDefaultCassandraClient(): CassandraClient { if (defaultClientState.client === null) { throw new Error('Cassandra client is not initialized. Call initCassandra() first.'); } diff --git a/fluxer_api/pkgs/cassandra/src/Logger.ts b/fluxer_api/pkgs/cassandra/src/Logger.ts deleted file mode 100644 index ceb0fb93d..000000000 --- a/fluxer_api/pkgs/cassandra/src/Logger.ts +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -export interface Logger { - info(context: Record, message: string): void; - debug(context: Record, message: string): void; - error(context: Record, message: string): void; -} - -export const NoopLogger: Logger = { - info(): void {}, - debug(): void {}, - error(): void {}, -}; diff --git a/fluxer_api/pkgs/email/src/EmailI18nService.ts b/fluxer_api/pkgs/email/src/EmailI18nService.ts deleted file mode 100644 index 8c7666f47..000000000 --- a/fluxer_api/pkgs/email/src/EmailI18nService.ts +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {I18nResult} from '@fluxer/i18n/src/runtime/I18nTypes'; -import {getEmailTemplate} from '@pkgs/email/src/email_i18n/EmailI18n'; -import type {EmailTemplateVariables} from '@pkgs/email/src/email_i18n/EmailI18nTypes'; -import type {EmailTemplate, EmailTemplateKey} from '@pkgs/email/src/email_i18n/EmailI18nTypes.generated'; - -export interface IEmailI18nService { - getTemplate( - templateKey: T, - locale: string | null, - variables: EmailTemplateVariables[T], - productName: string, - ): I18nResult; -} - -export class EmailI18nService implements IEmailI18nService { - getTemplate( - templateKey: T, - locale: string | null, - variables: EmailTemplateVariables[T], - productName: string, - ): I18nResult { - return getEmailTemplate(templateKey, locale, variables, productName); - } -} diff --git a/fluxer_api/pkgs/email/src/EmailService.ts b/fluxer_api/pkgs/email/src/EmailService.ts index 18e202b4e..116de4b85 100644 --- a/fluxer_api/pkgs/email/src/EmailService.ts +++ b/fluxer_api/pkgs/email/src/EmailService.ts @@ -1,8 +1,8 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {createLogger} from '@fluxer/logger/src/Logger'; -import type {IEmailI18nService} from '@pkgs/email/src/EmailI18nService'; import type {EmailConfig, IEmailProvider, UserBouncedEmailChecker} from '@pkgs/email/src/EmailProviderTypes'; +import {getEmailTemplate} from '@pkgs/email/src/email_i18n/EmailI18n'; import type { EmailLegalLinks, EmailTemplateVariables, @@ -37,18 +37,15 @@ function legalLinkVariables(config: EmailConfig): { export class EmailService implements IEmailService { private readonly config: EmailConfig; - private readonly emailI18n: IEmailI18nService; private readonly provider: IEmailProvider | null; private readonly bouncedEmailChecker: UserBouncedEmailChecker | null; constructor( config: EmailConfig, - emailI18n: IEmailI18nService, provider: IEmailProvider | null = null, bouncedEmailChecker: UserBouncedEmailChecker | null = null, ) { this.config = config; - this.emailI18n = emailI18n; this.provider = provider; this.bouncedEmailChecker = bouncedEmailChecker; } @@ -394,7 +391,7 @@ export class EmailService implements IEmailService { locale: string | null, variables: EmailTemplateVariables[T], ): Promise { - const result = this.emailI18n.getTemplate(templateKey, locale, variables, this.config.productName); + const result = getEmailTemplate(templateKey, locale, variables, this.config.productName); if (!result.ok) { logger.error({key: templateKey, locale: result.locale, error: result.error}, 'Failed to resolve email template'); return false; diff --git a/fluxer_api/pkgs/email/src/ITestEmailService.ts b/fluxer_api/pkgs/email/src/ITestEmailService.ts deleted file mode 100644 index 5856574f4..000000000 --- a/fluxer_api/pkgs/email/src/ITestEmailService.ts +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {IEmailService} from '@pkgs/email/src/IEmailService'; - -export interface SentEmailRecord { - to: string; - type: string; - timestamp: Date; - metadata: Record; -} - -export interface ITestEmailService extends IEmailService { - listSentEmails(): Array; - clearSentEmails(): void; -} diff --git a/fluxer_api/pkgs/email/src/TestEmailService.ts b/fluxer_api/pkgs/email/src/TestEmailService.ts index a38b0c8d3..8f8bf38d0 100644 --- a/fluxer_api/pkgs/email/src/TestEmailService.ts +++ b/fluxer_api/pkgs/email/src/TestEmailService.ts @@ -1,7 +1,14 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {ReportReceivedTargetKind} from '@pkgs/email/src/email_i18n/EmailI18nTypes'; -import type {ITestEmailService, SentEmailRecord} from '@pkgs/email/src/ITestEmailService'; +import type {IEmailService} from '@pkgs/email/src/IEmailService'; + +export interface SentEmailRecord { + to: string; + type: string; + timestamp: Date; + metadata: Record; +} function maskToken(token: string): string { if (token.length <= 8) { @@ -12,7 +19,7 @@ function maskToken(token: string): string { const noopLogger = {info: (_message: string) => {}}; -export class TestEmailService implements ITestEmailService { +export class TestEmailService implements IEmailService { private readonly sentEmails: Array = []; private readonly logger: { info: (message: string) => void; diff --git a/fluxer_api/pkgs/kv_client/src/__tests__/KVClientClusterSlots.test.ts b/fluxer_api/pkgs/kv_client/src/__tests__/KVClientClusterSlots.test.ts index a79601bea..c1b42a3fb 100644 --- a/fluxer_api/pkgs/kv_client/src/__tests__/KVClientClusterSlots.test.ts +++ b/fluxer_api/pkgs/kv_client/src/__tests__/KVClientClusterSlots.test.ts @@ -63,26 +63,6 @@ describe('KVClient cluster hash slots', () => { store.clear(); }); - it('reads several keys without a command spanning hash slots', async () => { - expect(computeHashSlot('slot:alpha')).not.toBe(computeHashSlot('slot:beta')); - const client = createClusteredClient(); - await client.set('slot:alpha', 'one'); - - await expect(client.mget('slot:alpha', 'slot:beta')).resolves.toEqual(['one', null]); - expect(crossSlotCommands()).toEqual([]); - }); - - it('writes several keys without a command spanning hash slots', async () => { - expect(computeHashSlot('slot:alpha')).not.toBe(computeHashSlot('slot:beta')); - const client = createClusteredClient(); - - await client.mset('slot:alpha', 'one', 'slot:beta', 'two'); - - expect(store.get('slot:alpha')).toBe('one'); - expect(store.get('slot:beta')).toBe('two'); - expect(crossSlotCommands()).toEqual([]); - }); - it('deletes several keys without a command spanning hash slots', async () => { expect(computeHashSlot('slot:alpha')).not.toBe(computeHashSlot('slot:beta')); const client = createClusteredClient(); diff --git a/fluxer_api/pkgs/locale/src/LocaleService.ts b/fluxer_api/pkgs/locale/src/LocaleService.ts deleted file mode 100644 index 8b1d79f7f..000000000 --- a/fluxer_api/pkgs/locale/src/LocaleService.ts +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {LocaleCode} from '@fluxer/constants/src/Locales'; -import {resolveLocaleFromAcceptLanguageHeader} from '@pkgs/locale/src/resolution/AcceptLanguageNegotiation'; - -export function parseAcceptLanguage(acceptLanguageHeader: string | null | undefined): LocaleCode { - return resolveLocaleFromAcceptLanguageHeader(acceptLanguageHeader); -} diff --git a/fluxer_api/pkgs/locale/src/resolution/AcceptLanguageNegotiation.ts b/fluxer_api/pkgs/locale/src/resolution/AcceptLanguageNegotiation.ts index 143df977b..7cfea443a 100644 --- a/fluxer_api/pkgs/locale/src/resolution/AcceptLanguageNegotiation.ts +++ b/fluxer_api/pkgs/locale/src/resolution/AcceptLanguageNegotiation.ts @@ -17,7 +17,7 @@ interface AcceptLanguagePreference { const TRADITIONAL_CHINESE_SUBTAGS: ReadonlySet = new Set(['hant', 'tw', 'hk', 'mo']); const SIMPLIFIED_CHINESE_SUBTAGS: ReadonlySet = new Set(['hans', 'cn', 'sg']); -export function resolveLocaleFromAcceptLanguageHeader(acceptLanguageHeader: string | null | undefined): LocaleCode { +export function parseAcceptLanguage(acceptLanguageHeader: string | null | undefined): LocaleCode { if (!acceptLanguageHeader) { return DEFAULT_LOCALE; } diff --git a/fluxer_api/pkgs/locale/src/tests/LocaleService.test.ts b/fluxer_api/pkgs/locale/src/tests/LocaleService.test.ts index 423e66596..6954bed5f 100644 --- a/fluxer_api/pkgs/locale/src/tests/LocaleService.test.ts +++ b/fluxer_api/pkgs/locale/src/tests/LocaleService.test.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {type LocaleCode, Locales} from '@fluxer/constants/src/Locales'; -import {parseAcceptLanguage} from '@pkgs/locale/src/LocaleService'; +import {parseAcceptLanguage} from '@pkgs/locale/src/resolution/AcceptLanguageNegotiation'; import {describe, expect, it} from 'vitest'; interface AcceptLanguageCase { diff --git a/fluxer_api/pkgs/media_proxy_utils/package.json b/fluxer_api/pkgs/media_proxy_utils/package.json index 022329509..62e1caffb 100644 --- a/fluxer_api/pkgs/media_proxy_utils/package.json +++ b/fluxer_api/pkgs/media_proxy_utils/package.json @@ -12,18 +12,10 @@ "import": "./src/MediaProxyUtils.ts", "types": "./src/MediaProxyUtils.ts" }, - "./src/ExternalMediaProxyUrlBuilder": { - "import": "./src/ExternalMediaProxyUrlBuilder.ts", - "types": "./src/ExternalMediaProxyUrlBuilder.ts" - }, "./src/ExternalMediaProxyPathCodec": { "import": "./src/ExternalMediaProxyPathCodec.ts", "types": "./src/ExternalMediaProxyPathCodec.ts" }, - "./src/MediaProxySigner": { - "import": "./src/MediaProxySigner.ts", - "types": "./src/MediaProxySigner.ts" - }, "./src/AttachmentUrlSignature": { "import": "./src/AttachmentUrlSignature.ts", "types": "./src/AttachmentUrlSignature.ts" diff --git a/fluxer_api/pkgs/media_proxy_utils/src/ExternalMediaProxyUrlBuilder.ts b/fluxer_api/pkgs/media_proxy_utils/src/ExternalMediaProxyUrlBuilder.ts deleted file mode 100644 index 8617fcfab..000000000 --- a/fluxer_api/pkgs/media_proxy_utils/src/ExternalMediaProxyUrlBuilder.ts +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {buildExternalMediaProxyPath} from '@pkgs/media_proxy_utils/src/ExternalMediaProxyPathCodec'; -import {createMediaProxySigner, type IMediaProxySigner} from '@pkgs/media_proxy_utils/src/MediaProxySigner'; - -export interface ExternalMediaProxyUrlBuilderOptions { - mediaProxyEndpoint: string; - mediaProxySecretKey: string; -} - -export interface IExternalMediaProxyUrlBuilder { - buildExternalMediaProxyUrl(inputUrl: string): string; -} - -interface InternalExternalMediaProxyUrlBuilderOptions { - mediaProxyEndpoint: string; - mediaProxySigner: IMediaProxySigner; -} - -function normalizeMediaProxyEndpoint(mediaProxyEndpoint: string): string { - return mediaProxyEndpoint.replace(/\/+$/u, ''); -} - -class ExternalMediaProxyUrlBuilder implements IExternalMediaProxyUrlBuilder { - private readonly mediaProxyEndpoint: string; - private readonly mediaProxySigner: IMediaProxySigner; - - constructor(options: InternalExternalMediaProxyUrlBuilderOptions) { - this.mediaProxyEndpoint = options.mediaProxyEndpoint; - this.mediaProxySigner = options.mediaProxySigner; - } - - buildExternalMediaProxyUrl(inputUrl: string): string { - const proxyUrlPath = buildExternalMediaProxyPath(inputUrl); - const signature = this.mediaProxySigner.createSignature(proxyUrlPath); - return `${this.mediaProxyEndpoint}/external/${signature}/${proxyUrlPath}`; - } -} - -export function createExternalMediaProxyUrlBuilder( - options: ExternalMediaProxyUrlBuilderOptions, -): IExternalMediaProxyUrlBuilder { - const mediaProxySigner = createMediaProxySigner({ - mediaProxySecretKey: options.mediaProxySecretKey, - }); - return new ExternalMediaProxyUrlBuilder({ - mediaProxyEndpoint: normalizeMediaProxyEndpoint(options.mediaProxyEndpoint), - mediaProxySigner, - }); -} diff --git a/fluxer_api/pkgs/media_proxy_utils/src/MediaProxySigner.ts b/fluxer_api/pkgs/media_proxy_utils/src/MediaProxySigner.ts deleted file mode 100644 index f893363df..000000000 --- a/fluxer_api/pkgs/media_proxy_utils/src/MediaProxySigner.ts +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import crypto from 'node:crypto'; - -const BASE64_URL_PADDING_REGEX = /=*$/; - -export interface MediaProxySignerOptions { - mediaProxySecretKey: string; -} - -export interface IMediaProxySigner { - createSignature(proxyUrlPath: string): string; - verifySignature(proxyUrlPath: string, providedSignature: string): boolean; -} - -class MediaProxySigner implements IMediaProxySigner { - private readonly mediaProxySecretKey: string; - - constructor(options: MediaProxySignerOptions) { - this.mediaProxySecretKey = options.mediaProxySecretKey; - } - - createSignature(proxyUrlPath: string): string { - const hmac = crypto.createHmac('sha256', this.mediaProxySecretKey); - hmac.update(proxyUrlPath); - return hmac.digest('base64url').replace(BASE64_URL_PADDING_REGEX, ''); - } - - verifySignature(proxyUrlPath: string, providedSignature: string): boolean { - const expectedSignature = this.createSignature(proxyUrlPath); - const expectedBuffer = Buffer.from(expectedSignature, 'utf8'); - const providedBuffer = Buffer.from(providedSignature, 'utf8'); - if (expectedBuffer.length !== providedBuffer.length) { - return false; - } - return crypto.timingSafeEqual(expectedBuffer, providedBuffer); - } -} - -export function createMediaProxySigner(options: MediaProxySignerOptions): IMediaProxySigner { - return new MediaProxySigner(options); -} diff --git a/fluxer_api/pkgs/media_proxy_utils/src/MediaProxyUtils.ts b/fluxer_api/pkgs/media_proxy_utils/src/MediaProxyUtils.ts index fe678c980..1059397c3 100644 --- a/fluxer_api/pkgs/media_proxy_utils/src/MediaProxyUtils.ts +++ b/fluxer_api/pkgs/media_proxy_utils/src/MediaProxyUtils.ts @@ -1,25 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import crypto from 'node:crypto'; -import {createExternalMediaProxyUrlBuilder} from '@pkgs/media_proxy_utils/src/ExternalMediaProxyUrlBuilder'; - -const BASE64_URL_REGEX = /=*$/; - -export function createSignature(inputString: string, mediaProxySecretKey: string): string { - const hmac = crypto.createHmac('sha256', mediaProxySecretKey); - hmac.update(inputString); - return hmac.digest('base64url').replace(BASE64_URL_REGEX, ''); -} - -export function verifySignature(proxyUrlPath: string, providedSignature: string, mediaProxySecretKey: string): boolean { - const expectedSignature = createSignature(proxyUrlPath, mediaProxySecretKey); - const expectedBuffer = Buffer.from(expectedSignature); - const providedBuffer = Buffer.from(providedSignature); - if (expectedBuffer.length !== providedBuffer.length) { - return false; - } - return crypto.timingSafeEqual(expectedBuffer, providedBuffer); -} +import {buildExternalMediaProxyPath} from '@pkgs/media_proxy_utils/src/ExternalMediaProxyPathCodec'; export interface ExternalMediaProxyURLOptions { inputURL: string; @@ -28,9 +10,12 @@ export interface ExternalMediaProxyURLOptions { } export function getExternalMediaProxyURL(options: ExternalMediaProxyURLOptions): string { - const builder = createExternalMediaProxyUrlBuilder({ - mediaProxyEndpoint: options.mediaProxyEndpoint, - mediaProxySecretKey: options.mediaProxySecretKey, - }); - return builder.buildExternalMediaProxyUrl(options.inputURL); + const endpoint = options.mediaProxyEndpoint.replace(/\/+$/u, ''); + const proxyUrlPath = buildExternalMediaProxyPath(options.inputURL); + const signature = crypto + .createHmac('sha256', options.mediaProxySecretKey) + .update(proxyUrlPath) + .digest('base64url') + .replace(/=*$/, ''); + return `${endpoint}/external/${signature}/${proxyUrlPath}`; } diff --git a/fluxer_api/pkgs/virus_scan/package.json b/fluxer_api/pkgs/virus_scan/package.json index c28af7071..5ea6956a6 100644 --- a/fluxer_api/pkgs/virus_scan/package.json +++ b/fluxer_api/pkgs/virus_scan/package.json @@ -9,10 +9,6 @@ "scripts": { "typecheck": "tsc --noEmit" }, - "dependencies": { - "@fluxer/logger": "workspace:*", - "itty-time": "catalog:" - }, "devDependencies": { "@types/node": "catalog:", "typescript": "catalog:ts7" diff --git a/fluxer_api/pkgs/virus_scan/src/IVirusScanProvider.ts b/fluxer_api/pkgs/virus_scan/src/IVirusScanProvider.ts deleted file mode 100644 index 86f1c6648..000000000 --- a/fluxer_api/pkgs/virus_scan/src/IVirusScanProvider.ts +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {VirusScanProviderResult} from '@pkgs/virus_scan/src/VirusScanProviderResult'; - -export interface IVirusScanProvider { - scanFile(filePath: string): Promise; - scanBuffer(buffer: Buffer): Promise; -} diff --git a/fluxer_api/pkgs/virus_scan/src/VirusScanService.ts b/fluxer_api/pkgs/virus_scan/src/VirusScanService.ts deleted file mode 100644 index 075370382..000000000 --- a/fluxer_api/pkgs/virus_scan/src/VirusScanService.ts +++ /dev/null @@ -1,183 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import crypto from 'node:crypto'; -import {createReadStream} from 'node:fs'; -import path from 'node:path'; -import type {LoggerInterface} from '@fluxer/logger/src/LoggerInterface'; -import type {IVirusHashCache} from '@pkgs/virus_scan/src/cache/IVirusHashCache'; -import type {IVirusScanFailureReporter} from '@pkgs/virus_scan/src/failures/IVirusScanFailureReporter'; -import {NoopVirusScanFailureReporter} from '@pkgs/virus_scan/src/failures/NoopVirusScanFailureReporter'; -import type {IVirusScanProvider} from '@pkgs/virus_scan/src/IVirusScanProvider'; -import type {IVirusScanService} from '@pkgs/virus_scan/src/IVirusScanService'; -import type {VirusScanResult} from '@pkgs/virus_scan/src/VirusScanResult'; - -interface VirusScanConfig { - failOpen: boolean; - cachedThreatLabel?: string; -} - -interface VirusScanServiceDependencies { - provider: IVirusScanProvider; - virusHashCache: IVirusHashCache; - logger: LoggerInterface; - config: VirusScanConfig; - failureReporter?: IVirusScanFailureReporter; -} - -export class VirusScanService implements IVirusScanService { - readonly enabled = true; - private readonly cachedThreatLabel: string; - private readonly failureReporter: IVirusScanFailureReporter; - - constructor(private dependencies: VirusScanServiceDependencies) { - this.cachedThreatLabel = dependencies.config.cachedThreatLabel ?? 'Cached virus signature'; - this.failureReporter = dependencies.failureReporter ?? new NoopVirusScanFailureReporter(); - } - - async initialize(): Promise { - await this.failureReporter.initialize(); - } - - async scanFile(filePath: string): Promise { - const filename = path.basename(filePath); - const fileHash = await this.hashFile(filePath); - const isCachedVirus = await this.dependencies.virusHashCache.isKnownVirusHash(fileHash); - if (isCachedVirus) { - return { - isClean: false, - threat: this.cachedThreatLabel, - fileHash, - }; - } - try { - const scanResult = await this.dependencies.provider.scanFile(filePath); - if (scanResult.isClean) { - return { - isClean: true, - fileHash, - }; - } - if (!scanResult.threat) { - throw new Error('Virus scan provider returned infected status without threat name'); - } - await this.dependencies.virusHashCache.cacheVirusHash(fileHash); - return { - isClean: false, - threat: scanResult.threat, - fileHash, - }; - } catch (error) { - this.dependencies.logger.error( - { - error: this.describeError(error), - filename, - fileHash, - }, - 'Virus scan failed', - ); - await this.reportScanFailure(error, filename, fileHash); - if (this.dependencies.config.failOpen) { - return { - isClean: true, - fileHash, - }; - } - throw new Error(`Virus scan failed: ${this.describeError(error)}`); - } - } - - async scanBuffer(buffer: Buffer, filename: string): Promise { - const fileHash = this.createFileHashFromBuffer(buffer); - const isCachedVirus = await this.dependencies.virusHashCache.isKnownVirusHash(fileHash); - if (isCachedVirus) { - return { - isClean: false, - threat: this.cachedThreatLabel, - fileHash, - }; - } - try { - const scanResult = await this.dependencies.provider.scanBuffer(buffer); - if (scanResult.isClean) { - return { - isClean: true, - fileHash, - }; - } - if (!scanResult.threat) { - throw new Error('Virus scan provider returned infected status without threat name'); - } - await this.dependencies.virusHashCache.cacheVirusHash(fileHash); - return { - isClean: false, - threat: scanResult.threat, - fileHash, - }; - } catch (error) { - this.dependencies.logger.error( - { - error: this.describeError(error), - filename, - fileHash, - }, - 'Virus scan failed', - ); - await this.reportScanFailure(error, filename, fileHash); - if (this.dependencies.config.failOpen) { - return { - isClean: true, - fileHash, - }; - } - throw new Error(`Virus scan failed: ${this.describeError(error)}`); - } - } - - private createFileHashFromBuffer(buffer: Buffer): string { - return crypto.createHash('sha256').update(buffer).digest('hex'); - } - - private async hashFile(filePath: string): Promise { - const hash = crypto.createHash('sha256'); - const stream = createReadStream(filePath, {highWaterMark: 1024 * 1024}); - try { - for await (const chunk of stream) { - hash.update(chunk as Buffer); - } - } catch (error) { - stream.destroy(); - throw error; - } - return hash.digest('hex'); - } - - private describeError(error: unknown): string { - if (typeof error === 'string') { - return error; - } - if (error instanceof Error) { - return error.message; - } - return 'Unknown error'; - } - - private async reportScanFailure(error: unknown, filename: string, fileHash: string): Promise { - try { - await this.failureReporter.reportFailure({ - error, - filename, - fileHash, - failOpen: this.dependencies.config.failOpen, - }); - } catch (reportError) { - this.dependencies.logger.warn( - { - error: this.describeError(reportError), - filename, - fileHash, - }, - 'Failed to report virus scan failure', - ); - } - } -} diff --git a/fluxer_api/pkgs/virus_scan/src/cache/IVirusHashCache.ts b/fluxer_api/pkgs/virus_scan/src/cache/IVirusHashCache.ts deleted file mode 100644 index da26664bf..000000000 --- a/fluxer_api/pkgs/virus_scan/src/cache/IVirusHashCache.ts +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -export interface IVirusHashCache { - isKnownVirusHash(fileHash: string): Promise; - cacheVirusHash(fileHash: string): Promise; -} diff --git a/fluxer_api/pkgs/virus_scan/src/cache/IVirusScanCacheStore.ts b/fluxer_api/pkgs/virus_scan/src/cache/IVirusScanCacheStore.ts deleted file mode 100644 index 1422a2e14..000000000 --- a/fluxer_api/pkgs/virus_scan/src/cache/IVirusScanCacheStore.ts +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -export interface IVirusScanCacheStore { - get(key: string): Promise; - set(key: string, value: T, ttlSeconds?: number): Promise; -} diff --git a/fluxer_api/pkgs/virus_scan/src/cache/VirusHashCache.ts b/fluxer_api/pkgs/virus_scan/src/cache/VirusHashCache.ts deleted file mode 100644 index f78f70b69..000000000 --- a/fluxer_api/pkgs/virus_scan/src/cache/VirusHashCache.ts +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {IVirusHashCache} from '@pkgs/virus_scan/src/cache/IVirusHashCache'; -import type {IVirusScanCacheStore} from '@pkgs/virus_scan/src/cache/IVirusScanCacheStore'; -import {seconds} from 'itty-time'; - -interface VirusHashCacheConfig { - keyPrefix?: string; - ttlSeconds?: number; -} - -export class VirusHashCache implements IVirusHashCache { - private readonly keyPrefix: string; - private readonly ttlSeconds: number; - - constructor( - private cacheStore: IVirusScanCacheStore, - config: VirusHashCacheConfig = {}, - ) { - this.keyPrefix = config.keyPrefix ?? 'virus'; - this.ttlSeconds = config.ttlSeconds ?? seconds('7 days'); - } - - async isKnownVirusHash(fileHash: string): Promise { - const cachedValue = await this.cacheStore.get(this.buildCacheKey(fileHash)); - return cachedValue != null; - } - - async cacheVirusHash(fileHash: string): Promise { - await this.cacheStore.set(this.buildCacheKey(fileHash), 'true', this.ttlSeconds); - } - - private buildCacheKey(fileHash: string): string { - return `${this.keyPrefix}:${fileHash}`; - } -} diff --git a/fluxer_api/pkgs/virus_scan/src/failures/IVirusScanFailureReporter.ts b/fluxer_api/pkgs/virus_scan/src/failures/IVirusScanFailureReporter.ts deleted file mode 100644 index e5329b875..000000000 --- a/fluxer_api/pkgs/virus_scan/src/failures/IVirusScanFailureReporter.ts +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {VirusScanFailureContext} from '@pkgs/virus_scan/src/failures/VirusScanFailureContext'; - -export interface IVirusScanFailureReporter { - initialize(): Promise; - reportFailure(context: VirusScanFailureContext): Promise; -} diff --git a/fluxer_api/pkgs/virus_scan/src/failures/NoopVirusScanFailureReporter.ts b/fluxer_api/pkgs/virus_scan/src/failures/NoopVirusScanFailureReporter.ts deleted file mode 100644 index 040a567b1..000000000 --- a/fluxer_api/pkgs/virus_scan/src/failures/NoopVirusScanFailureReporter.ts +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {IVirusScanFailureReporter} from '@pkgs/virus_scan/src/failures/IVirusScanFailureReporter'; -import type {VirusScanFailureContext} from '@pkgs/virus_scan/src/failures/VirusScanFailureContext'; - -export class NoopVirusScanFailureReporter implements IVirusScanFailureReporter { - async initialize(): Promise {} - - async reportFailure(_context: VirusScanFailureContext): Promise {} -} diff --git a/fluxer_api/pkgs/virus_scan/src/failures/VirusScanFailureContext.ts b/fluxer_api/pkgs/virus_scan/src/failures/VirusScanFailureContext.ts deleted file mode 100644 index 06457d713..000000000 --- a/fluxer_api/pkgs/virus_scan/src/failures/VirusScanFailureContext.ts +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -export interface VirusScanFailureContext { - error: unknown; - filename: string; - fileHash: string; - failOpen: boolean; -} diff --git a/fluxer_api/pkgs/virus_scan/src/providers/ClamAVProvider.ts b/fluxer_api/pkgs/virus_scan/src/providers/ClamAVProvider.ts index 0cc005e85..46304f377 100644 --- a/fluxer_api/pkgs/virus_scan/src/providers/ClamAVProvider.ts +++ b/fluxer_api/pkgs/virus_scan/src/providers/ClamAVProvider.ts @@ -3,7 +3,6 @@ import {createReadStream} from 'node:fs'; import {createConnection} from 'node:net'; import {Readable} from 'node:stream'; -import type {IVirusScanProvider} from '@pkgs/virus_scan/src/IVirusScanProvider'; import type {VirusScanProviderResult} from '@pkgs/virus_scan/src/VirusScanProviderResult'; interface ClamAVConfig { @@ -12,7 +11,7 @@ interface ClamAVConfig { streamChunkBytes?: number; } -export class ClamAVProvider implements IVirusScanProvider { +export class ClamAVProvider { private readonly streamChunkBytes: number; constructor(private config: ClamAVConfig) { diff --git a/fluxer_api/pkgs/worker/package.json b/fluxer_api/pkgs/worker/package.json index 312bcf8aa..0e4840bc6 100644 --- a/fluxer_api/pkgs/worker/package.json +++ b/fluxer_api/pkgs/worker/package.json @@ -4,10 +4,6 @@ "private": true, "type": "module", "exports": { - "./WorkerContext": { - "import": "./src/context/WorkerContext.ts", - "types": "./src/context/WorkerContext.ts" - }, "./WorkerTask": { "import": "./src/contracts/WorkerTask.ts", "types": "./src/contracts/WorkerTask.ts" diff --git a/fluxer_api/pkgs/worker/src/context/WorkerContext.ts b/fluxer_api/pkgs/worker/src/context/WorkerContext.ts deleted file mode 100644 index 7f735f2b1..000000000 --- a/fluxer_api/pkgs/worker/src/context/WorkerContext.ts +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -let workerDependencies: unknown | null = null; - -export function setWorkerDependencies(dependencies: T): void { - workerDependencies = dependencies; -} - -export function getWorkerDependencies(): T { - if (!workerDependencies) { - throw new Error('Worker dependencies have not been initialized. Call setWorkerDependencies() first.'); - } - return workerDependencies as T; -} - -export function clearWorkerDependencies(): void { - workerDependencies = null; -} diff --git a/fluxer_api/src/api/ApiContext.ts b/fluxer_api/src/api/ApiContext.ts index e27488b24..45d1075e9 100644 --- a/fluxer_api/src/api/ApiContext.ts +++ b/fluxer_api/src/api/ApiContext.ts @@ -6,7 +6,7 @@ import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {IMediaService} from '@app/api/infrastructure/IMediaService'; import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService'; import type {BotMfaMirrorService} from '@app/api/oauth/BotMfaMirrorService'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import type {UserActivityBuffer} from '@app/api/user/services/UserActivityBuffer'; import type {UserContactChangeLogService} from '@app/api/user/services/UserContactChangeLogService'; import type {WorkerTaskName} from '@app/api/worker/WorkerLaneConfig'; @@ -17,7 +17,7 @@ import type {IRateLimitService} from '@pkgs/rate_limit/src/IRateLimitService'; import type {IWorkerService} from '@pkgs/worker/src/contracts/IWorkerService'; export interface ApiServices { - users: IUserRepository; + users: UserRepository; cache: ICacheService; gateway: IGatewayService; kv: IKVProvider; diff --git a/fluxer_api/src/api/admin/AdminRepository.ts b/fluxer_api/src/api/admin/AdminRepository.ts index bedfc9dc7..7e88bb831 100644 --- a/fluxer_api/src/api/admin/AdminRepository.ts +++ b/fluxer_api/src/api/admin/AdminRepository.ts @@ -1,7 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import type {AdminAuditLog, BannedIpEntry, BannedIpKind, IAdminRepository} from '@app/api/admin/IAdminRepository'; -import {createUserID} from '@app/api/BrandedTypes'; +import {createUserID, type UserID} from '@app/api/BrandedTypes'; import {isIpBanExempt} from '@app/api/ban/IpBanExemptions'; import {ContentBlocklistCategory} from '@app/api/constants/ContentModeration'; import { @@ -35,6 +34,27 @@ import {parseIpBanEntry, tryParseSingleIp} from '@app/api/utils/IpRangeUtils'; import {canonicalizeStoredPhrase} from '@app/api/utils/PhraseBlocklistNormalization'; import {getSameIpDecisionKey} from '@fluxer/ip_utils/src/IpAddress'; +export interface AdminAuditLog { + logId: bigint; + adminUserId: UserID; + targetType: string; + targetId: bigint; + action: string; + auditLogReason: string | null; + metadata: Map; + createdAt: Date; +} + +export type BannedIpKind = 'permanent' | 'temporary_24h'; + +export interface BannedIpEntry { + ip: string; + kind: BannedIpKind; + reason: string | null; + expiresAt: Date | null; + createdAt: Date | null; +} + const FETCH_AUDIT_LOG_BY_ID_QUERY = AdminAuditLogs.select({ where: AdminAuditLogs.where.eq('log_id'), }); @@ -84,7 +104,7 @@ function canonicalizeBannedIpEntry(value: string): string { return parseIpBanEntry(value)?.canonical ?? value; } -export class AdminRepository implements IAdminRepository { +export class AdminRepository { async createAuditLog(log: AdminAuditLogRow): Promise { await upsertOne(AdminAuditLogs.insert(log)); return this.mapRowToAuditLog(log); diff --git a/fluxer_api/src/api/admin/AdminService.ts b/fluxer_api/src/api/admin/AdminService.ts index 194da8cec..43bd00cf1 100644 --- a/fluxer_api/src/api/admin/AdminService.ts +++ b/fluxer_api/src/api/admin/AdminService.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {ApiContext} from '@app/api/ApiContext'; -import type {IAdminRepository} from '@app/api/admin/IAdminRepository'; +import type {AdminRepository} from '@app/api/admin/AdminRepository'; import {AdminApplicationService} from '@app/api/admin/services/AdminApplicationService'; import {AdminAssetPurgeService} from '@app/api/admin/services/AdminAssetPurgeService'; import {AdminAuditService} from '@app/api/admin/services/AdminAuditService'; @@ -18,9 +18,9 @@ import {AdminUserService} from '@app/api/admin/services/AdminUserService'; import {AdminVoiceService} from '@app/api/admin/services/AdminVoiceService'; import {ReporterResolutionNotifier} from '@app/api/admin/services/ReporterResolutionNotifier'; import type {UserID} from '@app/api/BrandedTypes'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {ChannelService} from '@app/api/channel/services/ChannelService'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {GuildService} from '@app/api/guild/services/GuildService'; import type {IDiscriminatorService} from '@app/api/infrastructure/DiscriminatorService'; import type {EntityAssetService} from '@app/api/infrastructure/EntityAssetService'; @@ -29,10 +29,10 @@ import type {IStorageService} from '@app/api/infrastructure/IStorageService'; import type {KVBulkMessageDeletionQueueService} from '@app/api/infrastructure/KVBulkMessageDeletionQueueService'; import type {UserCacheService} from '@app/api/infrastructure/UserCacheService'; import type {InviteRepository} from '@app/api/invite/InviteRepository'; -import type {IJobLedgerRepository} from '@app/api/jobs/IJobLedgerRepository'; import {JobAdminService} from '@app/api/jobs/JobAdminService'; +import type {JobLedgerRepository} from '@app/api/jobs/JobLedgerRepository'; import {getGuildDiscoveryRepository, getKVAccountDeletionQueue} from '@app/api/middleware/ServiceSingletons'; -import type {IApplicationRepository} from '@app/api/oauth/repositories/IApplicationRepository'; +import type {ApplicationRepository} from '@app/api/oauth/repositories/ApplicationRepository'; import type {ReportService} from '@app/api/report/ReportService'; import type {StoreEntitlementService} from '@app/api/store_billing/StoreEntitlementService'; import type {UserService} from '@app/api/user/services/UserService'; @@ -59,9 +59,9 @@ export class AdminService { constructor( private readonly apiContext: ApiContext, - private readonly guildRepository: IGuildRepositoryAggregate, - private readonly channelRepository: IChannelRepository, - private readonly adminRepository: IAdminRepository, + private readonly guildRepository: GuildRepository, + private readonly channelRepository: ChannelRepository, + private readonly adminRepository: AdminRepository, private readonly inviteRepository: InviteRepository, private readonly discriminatorService: IDiscriminatorService, private readonly guildService: GuildService, @@ -74,16 +74,16 @@ export class AdminService { private readonly reportService: ReportService, private readonly voiceRepository: VoiceRepository, private readonly bulkMessageDeletionQueue: KVBulkMessageDeletionQueueService, - private readonly applicationRepository: IApplicationRepository, + private readonly applicationRepository: ApplicationRepository, private readonly stripe: Stripe | null = null, - private readonly jobLedger: IJobLedgerRepository, + private readonly jobLedger: JobLedgerRepository, private readonly storeEntitlementService: StoreEntitlementService, ) { const {users, gateway, worker, snowflake} = this.apiContext.services; this.auditService = new AdminAuditService(this.adminRepository, snowflake, { userRepository: users, guildRepository: this.guildRepository, - channelRepository: this.channelRepository, + channelRepository: this.channelRepository.channelData, }); this.banManagementService = new AdminBanManagementService({ apiContext: this.apiContext, diff --git a/fluxer_api/src/api/admin/IAdminRepository.ts b/fluxer_api/src/api/admin/IAdminRepository.ts deleted file mode 100644 index 79d43e7d0..000000000 --- a/fluxer_api/src/api/admin/IAdminRepository.ts +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {UserID} from '@app/api/BrandedTypes'; -import type { - AdminAuditLogRow, - BannedAvatarHashRow, - BannedFileShaRow, - BannedProfileSubstringRow, - BannedProfileSubstringScope, - BannedUrlDomainRow, - BannedUrlRow, -} from '@app/api/database/types/AdminArchiveTypes'; - -export interface AdminAuditLog { - logId: bigint; - adminUserId: UserID; - targetType: string; - targetId: bigint; - action: string; - auditLogReason: string | null; - metadata: Map; - createdAt: Date; -} - -export type BannedIpKind = 'permanent' | 'temporary_24h'; - -export interface BannedIpEntry { - ip: string; - kind: BannedIpKind; - reason: string | null; - expiresAt: Date | null; - createdAt: Date | null; -} - -export abstract class IAdminRepository { - abstract createAuditLog(log: AdminAuditLogRow): Promise; - - abstract getAuditLog(logId: bigint): Promise; - - abstract listAuditLogsByIds(logIds: Array): Promise>; - - abstract listAllAuditLogsPaginated(limit: number, lastLogId?: bigint): Promise>; - - abstract isIpBanned(ip: string): Promise; - - abstract banIp(ip: string, ttlSeconds?: number | null): Promise; - - abstract banIpTemp(ip: string, ttlSeconds: number): Promise; - - abstract unbanIp(ip: string): Promise; - - abstract loadAllBannedIpEntries(): Promise>; - - abstract isEmailBanned(email: string): Promise; - - abstract banEmail(email: string): Promise; - - abstract unbanEmail(email: string): Promise; - - abstract loadAllBannedEmails(): Promise>; - - abstract banPhrase(phrase: string): Promise; - - abstract unbanPhrase(phrase: string): Promise; - - abstract loadAllBannedPhrases(): Promise>; - - abstract loadAllBannedIps(): Promise>; - - abstract banUrl(row: BannedUrlRow): Promise; - - abstract unbanUrl(url: string): Promise; - - abstract loadAllBannedUrls(): Promise>; - - abstract banUrlDomain(row: BannedUrlDomainRow): Promise; - - abstract unbanUrlDomain(domain: string): Promise; - - abstract loadAllBannedUrlDomains(): Promise>; - - abstract banFileSha(row: BannedFileShaRow): Promise; - - abstract unbanFileSha(sha256Hex: string): Promise; - - abstract unbanFeedFileSha(sha256Hex: string): Promise; - - abstract loadAllBannedFileShas(): Promise>; - - abstract banAvatarHash(row: BannedAvatarHashRow): Promise; - - abstract unbanAvatarHash(hashShort: string): Promise; - - abstract loadAllBannedAvatarHashes(): Promise>; - - abstract banProfileSubstring(row: BannedProfileSubstringRow): Promise; - - abstract unbanProfileSubstring(scope: BannedProfileSubstringScope, substring: string): Promise; - - abstract loadAllBannedProfileSubstrings(): Promise>; -} diff --git a/fluxer_api/src/api/admin/controllers/DiscoveryAdminController.ts b/fluxer_api/src/api/admin/controllers/DiscoveryAdminController.ts index beb1347f3..658c86f51 100644 --- a/fluxer_api/src/api/admin/controllers/DiscoveryAdminController.ts +++ b/fluxer_api/src/api/admin/controllers/DiscoveryAdminController.ts @@ -13,7 +13,7 @@ import {OpenAPI} from '@app/api/middleware/ResponseTypeMiddleware'; import type {User} from '@app/api/models/User'; import {RateLimitConfigs} from '@app/api/RateLimitConfig'; import type {HonoApp} from '@app/api/types/HonoEnv'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {Validator} from '@app/api/Validator'; import {AdminACLs} from '@fluxer/constants/src/AdminACLs'; import {DiscoveryApplicationStatus, DiscoveryCategoryLabels} from '@fluxer/constants/src/DiscoveryConstants'; @@ -65,13 +65,13 @@ interface GuildEnrichment { async function enrichGuilds( rows: ReadonlyArray, guildService: GuildService, - userRepository: IUserRepository, + userRepository: UserRepository, ): Promise> { const map = new Map(); const guilds = await Promise.all( rows.map(async (row) => { try { - return await guildService.data.getGuildSystem(row.guild_id); + return await guildService.data.operations.getGuildSystem(row.guild_id); } catch { return null; } diff --git a/fluxer_api/src/api/admin/controllers/ThreadAdminController.ts b/fluxer_api/src/api/admin/controllers/ThreadAdminController.ts index 5e872afa8..58b34d292 100644 --- a/fluxer_api/src/api/admin/controllers/ThreadAdminController.ts +++ b/fluxer_api/src/api/admin/controllers/ThreadAdminController.ts @@ -62,7 +62,7 @@ export function ThreadAdminController(app: HonoApp) { }), async (ctx) => { const channelId = createChannelID(ctx.req.valid('param').channel_id); - const thread = await ctx.get('channelRepository').findUnique(channelId); + const thread = await ctx.get('channelRepository').channelData.findUnique(channelId); if (!thread) throw new UnknownChannelError(); if (!thread.isThread()) throw new InvalidChannelTypeError(); const adminUserId = ctx.get('adminUserId'); diff --git a/fluxer_api/src/api/admin/repositories/AdminApiKeyRepository.ts b/fluxer_api/src/api/admin/repositories/AdminApiKeyRepository.ts index 582191e25..8342fc9c9 100644 --- a/fluxer_api/src/api/admin/repositories/AdminApiKeyRepository.ts +++ b/fluxer_api/src/api/admin/repositories/AdminApiKeyRepository.ts @@ -1,10 +1,5 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import type { - CreateAdminApiKeyData, - IAdminApiKeyRepository, - UpdateAdminApiKeyData, -} from '@app/api/admin/repositories/IAdminApiKeyRepository'; import type {UserID} from '@app/api/BrandedTypes'; import { BatchBuilder, @@ -19,12 +14,23 @@ import {AdminApiKey} from '@app/api/models/AdminApiKey'; import {AdminApiKeys, AdminApiKeysByCreator} from '@app/api/Tables'; import {hashPassword} from '@app/api/utils/PasswordUtils'; +export interface CreateAdminApiKeyData { + name: string; + expiresAt: Date | null; + acls: Set; +} + +export interface UpdateAdminApiKeyData { + name?: string; + acls?: Set; +} + function computeTtlSeconds(expiresAt: Date): number { const diffSeconds = Math.floor((expiresAt.getTime() - Date.now()) / 1000); return Math.max(diffSeconds, 1); } -export class AdminApiKeyRepository implements IAdminApiKeyRepository { +export class AdminApiKeyRepository { async create(data: CreateAdminApiKeyData, createdBy: UserID, keyId: bigint, rawKey: string): Promise { const keyHash = await hashPassword(rawKey); const createdAt = new Date(); diff --git a/fluxer_api/src/api/admin/repositories/IAdminApiKeyRepository.ts b/fluxer_api/src/api/admin/repositories/IAdminApiKeyRepository.ts deleted file mode 100644 index 9322ccb0d..000000000 --- a/fluxer_api/src/api/admin/repositories/IAdminApiKeyRepository.ts +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {UserID} from '@app/api/BrandedTypes'; -import type {AdminApiKey} from '@app/api/models/AdminApiKey'; - -export interface CreateAdminApiKeyData { - name: string; - expiresAt: Date | null; - acls: Set; -} - -export interface UpdateAdminApiKeyData { - name?: string; - acls?: Set; -} - -export interface IAdminApiKeyRepository { - create(data: CreateAdminApiKeyData, createdBy: UserID, keyId: bigint, rawKey: string): Promise; - findById(keyId: bigint): Promise; - update(apiKey: AdminApiKey, data: UpdateAdminApiKeyData): Promise; - listByCreator(createdBy: UserID): Promise>; - updateLastUsed(keyId: bigint, expiresAt: Date | null): Promise; - revoke(keyId: bigint, createdBy: UserID): Promise; -} diff --git a/fluxer_api/src/api/admin/services/AdminApiKeyService.ts b/fluxer_api/src/api/admin/services/AdminApiKeyService.ts index 83314cdea..8eb1befed 100644 --- a/fluxer_api/src/api/admin/services/AdminApiKeyService.ts +++ b/fluxer_api/src/api/admin/services/AdminApiKeyService.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {randomInt} from 'node:crypto'; -import type {IAdminApiKeyRepository} from '@app/api/admin/repositories/IAdminApiKeyRepository'; +import type {AdminApiKeyRepository} from '@app/api/admin/repositories/AdminApiKeyRepository'; import type {UserID} from '@app/api/BrandedTypes'; import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService'; import type {AdminApiKey} from '@app/api/models/AdminApiKey'; @@ -39,7 +39,7 @@ export interface AdminApiKeyView { export class AdminApiKeyService { constructor( - private readonly adminApiKeyRepository: IAdminApiKeyRepository, + private readonly adminApiKeyRepository: AdminApiKeyRepository, private readonly snowflakeService: ISnowflakeService, ) {} diff --git a/fluxer_api/src/api/admin/services/AdminApplicationService.ts b/fluxer_api/src/api/admin/services/AdminApplicationService.ts index 12af16a2f..61e6800d4 100644 --- a/fluxer_api/src/api/admin/services/AdminApplicationService.ts +++ b/fluxer_api/src/api/admin/services/AdminApplicationService.ts @@ -3,9 +3,9 @@ import type {ApiContext} from '@app/api/ApiContext'; import type {AdminAuditService} from '@app/api/admin/services/AdminAuditService'; import {type ApplicationID, createApplicationID, createUserID, type GuildID, type UserID} from '@app/api/BrandedTypes'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {Application} from '@app/api/models/Application'; -import type {IApplicationRepository} from '@app/api/oauth/repositories/IApplicationRepository'; +import type {ApplicationRepository} from '@app/api/oauth/repositories/ApplicationRepository'; import {UnknownGuildError} from '@fluxer/errors/src/domains/guild/UnknownGuildError'; import {UnknownApplicationError} from '@fluxer/errors/src/domains/oauth/UnknownApplicationError'; import {UnknownUserError} from '@fluxer/errors/src/domains/user/UnknownUserError'; @@ -18,9 +18,9 @@ import type { interface AdminApplicationServiceDeps { apiContext: ApiContext; - applicationRepository: IApplicationRepository; + applicationRepository: ApplicationRepository; auditService: AdminAuditService; - guildRepository: IGuildRepositoryAggregate; + guildRepository: GuildRepository; } interface UserDisplay { diff --git a/fluxer_api/src/api/admin/services/AdminArchiveService.ts b/fluxer_api/src/api/admin/services/AdminArchiveService.ts index c313c612b..2c610e779 100644 --- a/fluxer_api/src/api/admin/services/AdminArchiveService.ts +++ b/fluxer_api/src/api/admin/services/AdminArchiveService.ts @@ -4,10 +4,10 @@ import {AdminArchive} from '@app/api/admin/models/AdminArchiveModel'; import type {AdminArchiveRepository} from '@app/api/admin/repositories/AdminArchiveRepository'; import type {GuildID, UserID} from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService'; import type {IStorageService} from '@app/api/infrastructure/IStorageService'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import type {WorkerTaskName} from '@app/api/worker/WorkerLaneConfig'; import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError'; import {UnknownGuildError} from '@fluxer/errors/src/domains/guild/UnknownGuildError'; @@ -35,8 +35,8 @@ interface ListArchivesParams { export class AdminArchiveService { constructor( private readonly adminArchiveRepository: AdminArchiveRepository, - private readonly userRepository: IUserRepository, - private readonly guildRepository: IGuildRepositoryAggregate, + private readonly userRepository: UserRepository, + private readonly guildRepository: GuildRepository, private readonly storageService: IStorageService, private readonly snowflakeService: ISnowflakeService, private readonly workerService: IWorkerService, diff --git a/fluxer_api/src/api/admin/services/AdminAssetPurgeService.ts b/fluxer_api/src/api/admin/services/AdminAssetPurgeService.ts index 5cd14d75b..6a2b766d6 100644 --- a/fluxer_api/src/api/admin/services/AdminAssetPurgeService.ts +++ b/fluxer_api/src/api/admin/services/AdminAssetPurgeService.ts @@ -3,7 +3,7 @@ import type {AdminAuditService} from '@app/api/admin/services/AdminAuditService'; import {createEmojiID, createStickerID, type GuildID, type UserID} from '@app/api/BrandedTypes'; import {mapGuildEmojiToResponse, mapGuildStickerToResponse} from '@app/api/guild/GuildModel'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import {ExpressionAssetPurger} from '@app/api/guild/services/content/ExpressionAssetPurger'; import type {IAssetDeletionQueue} from '@app/api/infrastructure/IAssetDeletionQueue'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; @@ -15,7 +15,7 @@ import type { } from '@fluxer/schema/src/domains/admin/AdminSchemas'; interface AdminAssetPurgeServiceDeps { - guildRepository: IGuildRepositoryAggregate; + guildRepository: GuildRepository; gatewayService: IGatewayService; assetDeletionQueue: IAssetDeletionQueue; auditService: AdminAuditService; diff --git a/fluxer_api/src/api/admin/services/AdminAuditService.ts b/fluxer_api/src/api/admin/services/AdminAuditService.ts index 21fb2e8c9..66a031a10 100644 --- a/fluxer_api/src/api/admin/services/AdminAuditService.ts +++ b/fluxer_api/src/api/admin/services/AdminAuditService.ts @@ -1,18 +1,18 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {ADMIN_AUDIT_READ_ACTIONS, getAdminAuditAccess} from '@app/api/admin/AdminAuditActions'; -import type {AdminAuditLog, IAdminRepository} from '@app/api/admin/IAdminRepository'; +import type {AdminAuditLog, AdminRepository} from '@app/api/admin/AdminRepository'; import type {UserID} from '@app/api/BrandedTypes'; import {createChannelID, createGuildID, createUserID} from '@app/api/BrandedTypes'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {ChannelDataRepository} from '@app/api/channel/repositories/ChannelDataRepository'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService'; import {Logger} from '@app/api/Logger'; import type {Channel} from '@app/api/models/Channel'; import type {Guild} from '@app/api/models/Guild'; import type {User} from '@app/api/models/User'; import {getAuditLogSearchService} from '@app/api/SearchFactory'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import type {AuditLogSearchFilters} from '@fluxer/schema/src/contracts/search/SearchDocumentTypes'; import type { AdminAuditAccess, @@ -34,7 +34,7 @@ interface CreateAdminAuditLogParams { export class AdminAuditService { constructor( - private readonly adminRepository: IAdminRepository, + private readonly adminRepository: AdminRepository, private readonly snowflakeService: ISnowflakeService, private readonly enrichmentDeps: AuditLogEnrichmentDeps = {}, ) {} @@ -341,9 +341,9 @@ function accessFilters(access: AdminAuditAccess | undefined): AuditLogSearchFilt } interface AuditLogEnrichmentDeps { - userRepository?: Pick; - guildRepository?: Pick; - channelRepository?: Pick; + userRepository?: Pick; + guildRepository?: Pick; + channelRepository?: Pick; } interface AuditLogEnrichment { diff --git a/fluxer_api/src/api/admin/services/AdminBanManagementService.ts b/fluxer_api/src/api/admin/services/AdminBanManagementService.ts index ee47c9f95..3f8cb25ab 100644 --- a/fluxer_api/src/api/admin/services/AdminBanManagementService.ts +++ b/fluxer_api/src/api/admin/services/AdminBanManagementService.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {ApiContext} from '@app/api/ApiContext'; -import type {IAdminRepository} from '@app/api/admin/IAdminRepository'; +import type {AdminRepository} from '@app/api/admin/AdminRepository'; import type {AdminAuditService} from '@app/api/admin/services/AdminAuditService'; import {createUserID, type UserID} from '@app/api/BrandedTypes'; import {isIpBanExempt} from '@app/api/ban/IpBanExemptions'; @@ -36,7 +36,7 @@ import type {AdminBlocklistListType} from '@fluxer/schema/src/domains/admin/Admi interface AdminBanManagementServiceDeps { apiContext: ApiContext; - adminRepository: IAdminRepository; + adminRepository: AdminRepository; auditService: AdminAuditService; } diff --git a/fluxer_api/src/api/admin/services/AdminCodeGenerationService.ts b/fluxer_api/src/api/admin/services/AdminCodeGenerationService.ts index f0d3715c8..3a0dc1009 100644 --- a/fluxer_api/src/api/admin/services/AdminCodeGenerationService.ts +++ b/fluxer_api/src/api/admin/services/AdminCodeGenerationService.ts @@ -3,7 +3,7 @@ import type {UserID} from '@app/api/BrandedTypes'; import type {GiftCodeDurationType, GiftCodeRow} from '@app/api/database/types/PaymentTypes'; import {mapGiftCodeDurationToMonths} from '@app/api/models/GiftCode'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import * as RandomUtils from '@app/api/utils/RandomUtils'; const CODE_LENGTH = 32; @@ -19,7 +19,7 @@ interface GenerateGiftCodesOptions { } export class AdminCodeGenerationService { - constructor(private readonly userRepository: IUserRepository) {} + constructor(private readonly userRepository: UserRepository) {} async generateGiftCodes(options: GenerateGiftCodesOptions): Promise> { const {count, durationType, durationQuantity, createdByUserId} = this.validateOptions(options); diff --git a/fluxer_api/src/api/admin/services/AdminGuildService.ts b/fluxer_api/src/api/admin/services/AdminGuildService.ts index 20593216a..e58e463bd 100644 --- a/fluxer_api/src/api/admin/services/AdminGuildService.ts +++ b/fluxer_api/src/api/admin/services/AdminGuildService.ts @@ -8,27 +8,27 @@ import {AdminGuildUpdatePropagator} from '@app/api/admin/services/guild/AdminGui import {AdminGuildUpdateService} from '@app/api/admin/services/guild/AdminGuildUpdateService'; import {AdminGuildVanityService} from '@app/api/admin/services/guild/AdminGuildVanityService'; import {createGuildID, createUserID} from '@app/api/BrandedTypes'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; -import type {IGuildDiscoveryRepository} from '@app/api/guild/repositories/GuildDiscoveryRepository'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; +import type {GuildDiscoveryRepository} from '@app/api/guild/repositories/GuildDiscoveryRepository'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {GuildService} from '@app/api/guild/services/GuildService'; import type {EntityAssetService} from '@app/api/infrastructure/EntityAssetService'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {InviteRepository} from '@app/api/invite/InviteRepository'; import {createRequestCache} from '@app/api/middleware/RequestCacheMiddleware'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import type {ListGuildAuditLogsRequest} from '@fluxer/schema/src/domains/admin/AdminGuildSchemas'; interface AdminGuildServiceDeps { - guildRepository: IGuildRepositoryAggregate; - userRepository: IUserRepository; - channelRepository: IChannelRepository; + guildRepository: GuildRepository; + userRepository: UserRepository; + channelRepository: ChannelRepository; inviteRepository: InviteRepository; guildService: GuildService; gatewayService: IGatewayService; entityAssetService: EntityAssetService; auditService: AdminAuditService; - discoveryRepository: IGuildDiscoveryRepository; + discoveryRepository: GuildDiscoveryRepository; } export class AdminGuildService { diff --git a/fluxer_api/src/api/admin/services/AdminMessageDeletionService.ts b/fluxer_api/src/api/admin/services/AdminMessageDeletionService.ts index 959b4d3d0..99f21a838 100644 --- a/fluxer_api/src/api/admin/services/AdminMessageDeletionService.ts +++ b/fluxer_api/src/api/admin/services/AdminMessageDeletionService.ts @@ -4,7 +4,7 @@ import type {AdminAuditService} from '@app/api/admin/services/AdminAuditService' import type {AdminMessageShredService} from '@app/api/admin/services/AdminMessageShredService'; import type {ChannelID, MessageID, UserID} from '@app/api/BrandedTypes'; import {createUserID} from '@app/api/BrandedTypes'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import {Logger} from '@app/api/Logger'; import type { DeleteAllUserMessagesRequest, @@ -12,7 +12,7 @@ import type { } from '@fluxer/schema/src/domains/admin/AdminMessageSchemas'; interface AdminMessageDeletionServiceDeps { - channelRepository: IChannelRepository; + channelRepository: ChannelRepository; messageShredService: AdminMessageShredService; auditService: AdminAuditService; } @@ -78,7 +78,7 @@ export class AdminMessageDeletionService { let messageCount = 0; let channelCount = 0; while (true) { - const messageRefs = await this.deps.channelRepository.listMessagesByAuthor( + const messageRefs = await this.deps.channelRepository.messages.listMessagesByAuthor( authorId, FETCH_CHUNK_SIZE, lastMessageId, diff --git a/fluxer_api/src/api/admin/services/AdminMessageService.ts b/fluxer_api/src/api/admin/services/AdminMessageService.ts index fb30e9f53..bf4674e93 100644 --- a/fluxer_api/src/api/admin/services/AdminMessageService.ts +++ b/fluxer_api/src/api/admin/services/AdminMessageService.ts @@ -11,7 +11,7 @@ import { type MessageID, type UserID, } from '@app/api/BrandedTypes'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import {withThreadContext} from '@app/api/channel/services/ChannelGatewayDispatch'; import { enqueueCrosspostFamilyPurgeFromCopies, @@ -25,7 +25,7 @@ import { messageResponseAccessForGuild, } from '@app/api/channel/services/message/MessageResponseDataService'; import {resolveNsfwScopeChannel} from '@app/api/channel/utils/ThreadNsfwScope'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import {getPurgeQueue, getStorageService} from '@app/api/middleware/ServiceSingletons'; import {getMessageSearchService} from '@app/api/SearchFactory'; import {deleteMessageSearchDocuments} from '@app/api/search/MessageSearchIndexCleanup'; @@ -44,8 +44,8 @@ import type {MessageResponse} from '@fluxer/schema/src/domains/message/MessageRe interface AdminMessageServiceDeps { apiContext: ApiContext; - channelRepository: IChannelRepository; - guildRepository: IGuildRepositoryAggregate; + channelRepository: ChannelRepository; + guildRepository: GuildRepository; auditService: AdminAuditService; } @@ -84,7 +84,7 @@ export class AdminMessageService { async lookupMessageByAttachment(data: LookupMessageByAttachmentRequest) { const channelId = createChannelID(data.channel_id); const attachmentId = createAttachmentID(data.attachment_id); - const messageId = await this.deps.channelRepository.lookupAttachmentByChannelAndFilename( + const messageId = await this.deps.channelRepository.messages.lookupAttachmentByChannelAndFilename( channelId, attachmentId, data.filename, @@ -113,11 +113,11 @@ export class AdminMessageService { const {gateway: gatewayService, worker: workerService} = this.deps.apiContext.services; const channelId = createChannelID(data.channel_id); const messageId = createMessageID(data.message_id); - const channel = await channelRepository.findUnique(channelId); - const message = await channelRepository.getMessage(channelId, messageId); + const channel = await channelRepository.channelData.findUnique(channelId); + const message = await channelRepository.messages.getMessage(channelId, messageId); if (message) { await purgeMessageAttachments(message, getStorageService(), getPurgeQueue()); - await channelRepository.deleteMessage( + await channelRepository.messages.deleteMessage( channelId, messageId, message.authorId || createUserID(0n), @@ -227,7 +227,7 @@ export class AdminMessageService { } private async getMessageResponseAccessForAdmin(channelId: ChannelID): Promise { - const channel = await this.deps.channelRepository.findUnique(channelId); + const channel = await this.deps.channelRepository.channelData.findUnique(channelId); const access = channel ? messageResponseAccessForChannel(channel) : messageResponseAccessForGuild(null); return {...access, includeHidden: true}; } @@ -283,7 +283,7 @@ export class AdminMessageService { private async getChannelNsfwContext(channelId: ChannelID): Promise { const {channelRepository, guildRepository} = this.deps; - const channel = await channelRepository.findUnique(channelId); + const channel = await channelRepository.channelData.findUnique(channelId); if (!channel) { return { channelNsfw: null, @@ -304,7 +304,7 @@ export class AdminMessageService { } const [guild, scope] = await Promise.all([ guildRepository.findUnique(channel.guildId), - resolveNsfwScopeChannel(channel, (id) => channelRepository.findUnique(id)), + resolveNsfwScopeChannel(channel, (id) => channelRepository.channelData.findUnique(id)), ]); return { channelNsfw: scope.isNsfw, diff --git a/fluxer_api/src/api/admin/services/AdminReportService.ts b/fluxer_api/src/api/admin/services/AdminReportService.ts index 5c1b50b00..f81ff0148 100644 --- a/fluxer_api/src/api/admin/services/AdminReportService.ts +++ b/fluxer_api/src/api/admin/services/AdminReportService.ts @@ -15,7 +15,7 @@ import { type UserID, } from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import {makeAttachmentCdnKey} from '@app/api/channel/services/message/MessageHelpers'; import { createMessageResponseDataService, @@ -25,7 +25,7 @@ import { } from '@app/api/channel/services/message/MessageResponseDataService'; import {resolveNsfwScopeChannel} from '@app/api/channel/utils/ThreadNsfwScope'; import type {MessageAttachment} from '@app/api/database/types/MessageTypes'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {IStorageService} from '@app/api/infrastructure/IStorageService'; import type {UserCacheService} from '@app/api/infrastructure/UserCacheService'; import {Logger} from '@app/api/Logger'; @@ -33,7 +33,7 @@ import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware'; import {createRequestCache} from '@app/api/middleware/RequestCacheMiddleware'; import {describeReportFlowAnswers} from '@app/api/report/flows/ReportFlowRegistry'; import {findReportReason, listReportReasons} from '@app/api/report/flows/ReportReasonCatalog'; -import type {IARMessageContext, IARSubmission} from '@app/api/report/IReportRepository'; +import type {IARMessageContext, IARSubmission} from '@app/api/report/ReportModels'; import type {ReportService} from '@app/api/report/ReportService'; import {getReportSearchService} from '@app/api/SearchFactory'; import {isHiddenPartial} from '@app/api/user/ProfileVisibility'; @@ -57,8 +57,8 @@ import {seconds} from 'itty-time'; interface AdminReportServiceDeps { apiContext: ApiContext; reportService: ReportService; - guildRepository: IGuildRepositoryAggregate; - channelRepository: IChannelRepository; + guildRepository: GuildRepository; + channelRepository: ChannelRepository; storageService: IStorageService; auditService: AdminAuditService; userCacheService: UserCacheService; @@ -541,7 +541,7 @@ export class AdminReportService { } private async getMessageResponseAccessForAdmin(channelId: ChannelID): Promise { - const channel = await this.deps.channelRepository.findUnique(channelId); + const channel = await this.deps.channelRepository.channelData.findUnique(channelId); const access = channel ? messageResponseAccessForChannel(channel) : messageResponseAccessForGuild(null); return {...access, includeHidden: true}; } @@ -628,9 +628,9 @@ export class AdminReportService { if (reportLookupCache.channelNsfwByChannelId.has(channelIdString)) { return reportLookupCache.channelNsfwByChannelId.get(channelIdString) ?? null; } - const channel = await this.deps.channelRepository.findUnique(channelId); + const channel = await this.deps.channelRepository.channelData.findUnique(channelId); const scope = channel - ? await resolveNsfwScopeChannel(channel, (id) => this.deps.channelRepository.findUnique(id)) + ? await resolveNsfwScopeChannel(channel, (id) => this.deps.channelRepository.channelData.findUnique(id)) : null; const channelNsfw = scope?.isNsfw ?? null; reportLookupCache.channelNsfwByChannelId.set(channelIdString, channelNsfw); @@ -665,7 +665,7 @@ export class AdminReportService { if (!channelId) { return null; } - const channel = await this.deps.channelRepository.findUnique(channelId); + const channel = await this.deps.channelRepository.channelData.findUnique(channelId); if (!channel?.guildId) { return null; } diff --git a/fluxer_api/src/api/admin/services/AdminSearchService.ts b/fluxer_api/src/api/admin/services/AdminSearchService.ts index 195c6b60c..9b9c6d47d 100644 --- a/fluxer_api/src/api/admin/services/AdminSearchService.ts +++ b/fluxer_api/src/api/admin/services/AdminSearchService.ts @@ -7,7 +7,7 @@ import type {AdminAuditService} from '@app/api/admin/services/AdminAuditService' import {createGuildID, createUserID, type UserID} from '@app/api/BrandedTypes'; import {isSyntheticUserId} from '@app/api/constants/Core'; import {channelThreadsEnabled} from '@app/api/experiment/ChannelThreadsGate'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import {Logger} from '@app/api/Logger'; import {getGuildSearchService, getUserSearchService} from '@app/api/SearchFactory'; import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes'; @@ -36,7 +36,7 @@ interface RefreshSearchIndexJobPayload extends WorkerJobPayload { interface AdminSearchServiceDeps { apiContext: ApiContext; - guildRepository: IGuildRepositoryAggregate; + guildRepository: GuildRepository; auditService: AdminAuditService; } diff --git a/fluxer_api/src/api/admin/services/AdminUserDeletionService.ts b/fluxer_api/src/api/admin/services/AdminUserDeletionService.ts index e9c515525..e7f36faa2 100644 --- a/fluxer_api/src/api/admin/services/AdminUserDeletionService.ts +++ b/fluxer_api/src/api/admin/services/AdminUserDeletionService.ts @@ -15,7 +15,7 @@ import type {KVAccountDeletionQueueService} from '@app/api/infrastructure/KVAcco import {Logger} from '@app/api/Logger'; import type {User} from '@app/api/models/User'; import type {OAuth2TokenRepository} from '@app/api/oauth/repositories/OAuth2TokenRepository'; -import {type IARSubmission, ReportStatus} from '@app/api/report/IReportRepository'; +import {type IARSubmission, ReportStatus} from '@app/api/report/ReportModels'; import type {ReportService} from '@app/api/report/ReportService'; import {getReportSearchService} from '@app/api/SearchFactory'; import type {StoreEntitlementService} from '@app/api/store_billing/StoreEntitlementService'; diff --git a/fluxer_api/src/api/admin/services/AdminUserProfileService.ts b/fluxer_api/src/api/admin/services/AdminUserProfileService.ts index e32a6b333..b6a64970f 100644 --- a/fluxer_api/src/api/admin/services/AdminUserProfileService.ts +++ b/fluxer_api/src/api/admin/services/AdminUserProfileService.ts @@ -5,7 +5,7 @@ import {mapUserToAdminResponse} from '@app/api/admin/models/UserTypes'; import type {AdminAuditService} from '@app/api/admin/services/AdminAuditService'; import type {AdminUserUpdatePropagator} from '@app/api/admin/services/AdminUserUpdatePropagator'; import {createUserID, type UserID} from '@app/api/BrandedTypes'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import {GuildMemberSearchIndexService} from '@app/api/guild/services/member/GuildMemberSearchIndexService'; import type {IDiscriminatorService} from '@app/api/infrastructure/DiscriminatorService'; import type {EntityAssetService, PreparedAssetUpload} from '@app/api/infrastructure/EntityAssetService'; @@ -37,7 +37,7 @@ interface AdminUserProfileServiceDeps { entityAssetService: EntityAssetService; auditService: AdminAuditService; updatePropagator: AdminUserUpdatePropagator; - guildRepository: IGuildRepositoryAggregate; + guildRepository: GuildRepository; } export class AdminUserProfileService { diff --git a/fluxer_api/src/api/admin/services/AdminUserService.ts b/fluxer_api/src/api/admin/services/AdminUserService.ts index 280136a91..3e9453959 100644 --- a/fluxer_api/src/api/admin/services/AdminUserService.ts +++ b/fluxer_api/src/api/admin/services/AdminUserService.ts @@ -12,8 +12,8 @@ import {AdminUserSecurityService} from '@app/api/admin/services/AdminUserSecurit import {AdminUserUpdatePropagator} from '@app/api/admin/services/AdminUserUpdatePropagator'; import {ReporterResolutionNotifier} from '@app/api/admin/services/ReporterResolutionNotifier'; import {createChannelID, createUserID, type UserID} from '@app/api/BrandedTypes'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {IDiscriminatorService} from '@app/api/infrastructure/DiscriminatorService'; import type {EntityAssetService} from '@app/api/infrastructure/EntityAssetService'; import type {KVAccountDeletionQueueService} from '@app/api/infrastructure/KVAccountDeletionQueueService'; @@ -23,7 +23,7 @@ import {getBillingRepository} from '@app/api/middleware/ServiceRegistry'; import {OAuth2TokenRepository} from '@app/api/oauth/repositories/OAuth2TokenRepository'; import type {ReportService} from '@app/api/report/ReportService'; import type {StoreEntitlementService} from '@app/api/store_billing/StoreEntitlementService'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import type {UserContactChangeLogService} from '@app/api/user/services/UserContactChangeLogService'; import {AdminACLs} from '@fluxer/constants/src/AdminACLs'; import {UnknownUserError} from '@fluxer/errors/src/domains/user/UnknownUserError'; @@ -38,8 +38,8 @@ import type Stripe from 'stripe'; interface AdminUserServiceDeps { apiContext: ApiContext; - guildRepository: IGuildRepositoryAggregate; - channelRepository: IChannelRepository; + guildRepository: GuildRepository; + channelRepository: ChannelRepository; discriminatorService: IDiscriminatorService; entityAssetService: EntityAssetService; auditService: AdminAuditService; @@ -62,9 +62,9 @@ export class AdminUserService { private readonly updatePropagator: AdminUserUpdatePropagator; private readonly contactChangeLogService: UserContactChangeLogService; private readonly auditService: AdminAuditService; - private readonly userRepository: IUserRepository; - private readonly guildRepository: IGuildRepositoryAggregate; - private readonly channelRepository: IChannelRepository; + private readonly userRepository: UserRepository; + private readonly guildRepository: GuildRepository; + private readonly channelRepository: ChannelRepository; private readonly bulkMessageDeletionQueue: KVBulkMessageDeletionQueueService; private readonly cacheService: ICacheService; @@ -132,7 +132,9 @@ export class AdminUserService { beforeChannelId: data.before ? createChannelID(data.before) : undefined, afterChannelId: data.after ? createChannelID(data.after) : undefined, }); - const channelModels = await this.channelRepository.listChannels(channels.map((channel) => channel.channelId)); + const channelModels = await this.channelRepository.channelData.listChannels( + channels.map((channel) => channel.channelId), + ); const channelModelById = new Map( channelModels.filter((c): c is NonNullable => c != null).map((c) => [c.id, c]), ); @@ -234,7 +236,9 @@ export class AdminUserService { if (groupDmSummaries.length === 0) { return {channels: []}; } - const channelModels = await this.channelRepository.listChannels(groupDmSummaries.map((s) => s.channelId)); + const channelModels = await this.channelRepository.channelData.listChannels( + groupDmSummaries.map((s) => s.channelId), + ); const allUserIds = new Set(); for (const model of channelModels) { if (model) { diff --git a/fluxer_api/src/api/admin/services/AdminUserUpdatePropagator.ts b/fluxer_api/src/api/admin/services/AdminUserUpdatePropagator.ts index aad830101..2e03bbedd 100644 --- a/fluxer_api/src/api/admin/services/AdminUserUpdatePropagator.ts +++ b/fluxer_api/src/api/admin/services/AdminUserUpdatePropagator.ts @@ -1,19 +1,19 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {UserID} from '@app/api/BrandedTypes'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {UserCacheService} from '@app/api/infrastructure/UserCacheService'; import type {User} from '@app/api/models/User'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {BaseUserUpdatePropagator} from '@app/api/user/services/BaseUserUpdatePropagator'; import {propagatePartialUserChange} from '@app/api/user/services/PartialUserChangePropagation'; import {hasPartialUserFieldsChanged} from '@app/api/user/UserMappers'; interface AdminUserUpdatePropagatorDeps { userCacheService: UserCacheService; - userRepository: IUserRepository; - guildRepository: IGuildRepositoryAggregate; + userRepository: UserRepository; + guildRepository: GuildRepository; gatewayService: IGatewayService; } diff --git a/fluxer_api/src/api/admin/services/ReporterResolutionNotifier.ts b/fluxer_api/src/api/admin/services/ReporterResolutionNotifier.ts index c40962345..f43af212f 100644 --- a/fluxer_api/src/api/admin/services/ReporterResolutionNotifier.ts +++ b/fluxer_api/src/api/admin/services/ReporterResolutionNotifier.ts @@ -12,7 +12,7 @@ import {getInstanceProductName} from '@app/api/instance/ProductName'; import {Logger} from '@app/api/Logger'; import {createRequestCache} from '@app/api/middleware/RequestCacheMiddleware'; import type {User} from '@app/api/models/User'; -import type {IARSubmission} from '@app/api/report/IReportRepository'; +import type {IARSubmission} from '@app/api/report/ReportModels'; import type {UserChannelService} from '@app/api/user/services/UserChannelService'; import {UserFlags} from '@fluxer/constants/src/UserConstants'; import {getEmailTemplate} from '@pkgs/email/src/email_i18n/EmailI18n'; diff --git a/fluxer_api/src/api/admin/services/guild/AdminGuildLookupService.ts b/fluxer_api/src/api/admin/services/guild/AdminGuildLookupService.ts index 67ffd116a..422c28ef7 100644 --- a/fluxer_api/src/api/admin/services/guild/AdminGuildLookupService.ts +++ b/fluxer_api/src/api/admin/services/guild/AdminGuildLookupService.ts @@ -4,11 +4,11 @@ import {mapGuildsToAdminResponse} from '@app/api/admin/models/GuildTypes'; import type {GuildID} from '@app/api/BrandedTypes'; import {createGuildID, createUserID} from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import {mapGuildFeatures} from '@app/api/guild/GuildFeatureUtils'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {MEDIA_PROXY_ICON_SIZE_DEFAULT} from '@fluxer/constants/src/MediaProxyAssetSizes'; import type {MediaProxyImageSize} from '@fluxer/constants/src/MediaProxyImageSizes'; import {UnknownUserError} from '@fluxer/errors/src/domains/user/UnknownUserError'; @@ -22,9 +22,9 @@ import type {ListGuildEmojisResponse, ListGuildStickersResponse} from '@fluxer/s const ADMIN_STICKER_MEDIA_RUNG: MediaProxyImageSize = 320; interface AdminGuildLookupServiceDeps { - guildRepository: IGuildRepositoryAggregate; - userRepository: IUserRepository; - channelRepository: IChannelRepository; + guildRepository: GuildRepository; + userRepository: UserRepository; + channelRepository: ChannelRepository; gatewayService: IGatewayService; } @@ -39,7 +39,7 @@ export class AdminGuildLookupService { return {guild: null}; } const [channels, roles, ownerUser] = await Promise.all([ - channelRepository.listGuildChannels(guildId, 'maintenance'), + channelRepository.channelData.listGuildChannels(guildId, 'maintenance'), guildRepository.listRoles(guildId), userRepository.findUnique(guild.ownerId), ]); diff --git a/fluxer_api/src/api/admin/services/guild/AdminGuildManagementService.ts b/fluxer_api/src/api/admin/services/guild/AdminGuildManagementService.ts index fae09b10d..256559678 100644 --- a/fluxer_api/src/api/admin/services/guild/AdminGuildManagementService.ts +++ b/fluxer_api/src/api/admin/services/guild/AdminGuildManagementService.ts @@ -2,14 +2,14 @@ import type {AdminAuditService} from '@app/api/admin/services/AdminAuditService'; import {createGuildID, type GuildID, type UserID} from '@app/api/BrandedTypes'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {GuildService} from '@app/api/guild/services/GuildService'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import {UnknownGuildError} from '@fluxer/errors/src/domains/guild/UnknownGuildError'; import type {SuccessResponse} from '@fluxer/schema/src/domains/common/CommonParamSchemas'; interface AdminGuildManagementServiceDeps { - guildRepository: IGuildRepositoryAggregate; + guildRepository: GuildRepository; gatewayService: IGatewayService; guildService: GuildService; auditService: AdminAuditService; @@ -63,7 +63,7 @@ export class AdminGuildManagementService { async deleteGuild(guildIdRaw: bigint, adminUserId: UserID, auditLogReason: string | null): Promise { const {guildService, auditService} = this.deps; const guildId = createGuildID(guildIdRaw); - await guildService.data.deleteGuildForAdmin(guildId, auditLogReason); + await guildService.data.operations.deleteGuildById(guildId); await auditService.createAuditLog({ adminUserId, targetType: 'guild', diff --git a/fluxer_api/src/api/admin/services/guild/AdminGuildMembershipService.ts b/fluxer_api/src/api/admin/services/guild/AdminGuildMembershipService.ts index 7e0d1f4af..37c798514 100644 --- a/fluxer_api/src/api/admin/services/guild/AdminGuildMembershipService.ts +++ b/fluxer_api/src/api/admin/services/guild/AdminGuildMembershipService.ts @@ -4,7 +4,7 @@ import type {AdminAuditService} from '@app/api/admin/services/AdminAuditService' import {createGuildID, createUserID, type UserID} from '@app/api/BrandedTypes'; import type {GuildService} from '@app/api/guild/services/GuildService'; import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {JoinSourceTypes} from '@fluxer/constants/src/GuildConstants'; import {UnknownUserError} from '@fluxer/errors/src/domains/user/UnknownUserError'; import type { @@ -15,7 +15,7 @@ import type { import type {SuccessResponse} from '@fluxer/schema/src/domains/common/CommonParamSchemas'; interface AdminGuildMembershipServiceDeps { - userRepository: IUserRepository; + userRepository: UserRepository; guildService: GuildService; auditService: AdminAuditService; } diff --git a/fluxer_api/src/api/admin/services/guild/AdminGuildUpdatePropagator.ts b/fluxer_api/src/api/admin/services/guild/AdminGuildUpdatePropagator.ts index ab927a5ba..45b59b7ae 100644 --- a/fluxer_api/src/api/admin/services/guild/AdminGuildUpdatePropagator.ts +++ b/fluxer_api/src/api/admin/services/guild/AdminGuildUpdatePropagator.ts @@ -2,7 +2,7 @@ import type {GuildID, UserID} from '@app/api/BrandedTypes'; import {mapGuildToGuildResponse} from '@app/api/guild/GuildModel'; -import type {IGuildDiscoveryRepository} from '@app/api/guild/repositories/GuildDiscoveryRepository'; +import type {GuildDiscoveryRepository} from '@app/api/guild/repositories/GuildDiscoveryRepository'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import {Logger} from '@app/api/Logger'; import type {Guild} from '@app/api/models/Guild'; @@ -13,7 +13,7 @@ import {GuildFeatures} from '@fluxer/constants/src/GuildConstants'; interface AdminGuildUpdatePropagatorDeps { gatewayService: IGatewayService; - discoveryRepository: IGuildDiscoveryRepository; + discoveryRepository: GuildDiscoveryRepository; } interface AdminGuildUpdateDispatchOptions { diff --git a/fluxer_api/src/api/admin/services/guild/AdminGuildUpdateService.ts b/fluxer_api/src/api/admin/services/guild/AdminGuildUpdateService.ts index 6eed49710..17f55ab1c 100644 --- a/fluxer_api/src/api/admin/services/guild/AdminGuildUpdateService.ts +++ b/fluxer_api/src/api/admin/services/guild/AdminGuildUpdateService.ts @@ -5,7 +5,7 @@ import type {AdminAuditService} from '@app/api/admin/services/AdminAuditService' import type {AdminGuildUpdatePropagator} from '@app/api/admin/services/guild/AdminGuildUpdatePropagator'; import {createGuildID, createUserID, type GuildID, type UserID} from '@app/api/BrandedTypes'; import type {GuildRow} from '@app/api/database/types/GuildTypes'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {EntityAssetService, PreparedAssetUpload} from '@app/api/infrastructure/EntityAssetService'; import {Logger} from '@app/api/Logger'; import {UnknownGuildError} from '@fluxer/errors/src/domains/guild/UnknownGuildError'; @@ -17,7 +17,7 @@ import type { } from '@fluxer/schema/src/domains/admin/AdminGuildSchemas'; interface AdminGuildUpdateServiceDeps { - guildRepository: IGuildRepositoryAggregate; + guildRepository: GuildRepository; entityAssetService: EntityAssetService; auditService: AdminAuditService; updatePropagator: AdminGuildUpdatePropagator; diff --git a/fluxer_api/src/api/admin/services/guild/AdminGuildVanityService.ts b/fluxer_api/src/api/admin/services/guild/AdminGuildVanityService.ts index d188de904..2e672b415 100644 --- a/fluxer_api/src/api/admin/services/guild/AdminGuildVanityService.ts +++ b/fluxer_api/src/api/admin/services/guild/AdminGuildVanityService.ts @@ -10,7 +10,7 @@ import { type UserID, vanityCodeToInviteCode, } from '@app/api/BrandedTypes'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {InviteRepository} from '@app/api/invite/InviteRepository'; import {InviteTypes} from '@fluxer/constants/src/ChannelConstants'; import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes'; @@ -19,7 +19,7 @@ import {UnknownGuildError} from '@fluxer/errors/src/domains/guild/UnknownGuildEr import type {UpdateGuildVanityRequest} from '@fluxer/schema/src/domains/admin/AdminGuildSchemas'; interface AdminGuildVanityServiceDeps { - guildRepository: IGuildRepositoryAggregate; + guildRepository: GuildRepository; inviteRepository: InviteRepository; auditService: AdminAuditService; updatePropagator: AdminGuildUpdatePropagator; diff --git a/fluxer_api/src/api/admin/tests/AdminBanNotes.test.ts b/fluxer_api/src/api/admin/tests/AdminBanNotes.test.ts index cf6ef9b00..fda5a80d9 100644 --- a/fluxer_api/src/api/admin/tests/AdminBanNotes.test.ts +++ b/fluxer_api/src/api/admin/tests/AdminBanNotes.test.ts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import type {AdminAuditLog} from '@app/api/admin/IAdminRepository'; +import type {AdminAuditLog} from '@app/api/admin/AdminRepository'; import {createTestAccount, setUserACLs, type TestAccount} from '@app/api/auth/tests/AuthTestUtils'; import {decodeHeaderUtf8} from '@app/api/middleware/AuditLogMiddleware'; import {getAdminRepository} from '@app/api/middleware/ServiceSingletons'; diff --git a/fluxer_api/src/api/admin/tests/AdminIpBanExpiry.test.ts b/fluxer_api/src/api/admin/tests/AdminIpBanExpiry.test.ts index 46e0b1624..2408fe33c 100644 --- a/fluxer_api/src/api/admin/tests/AdminIpBanExpiry.test.ts +++ b/fluxer_api/src/api/admin/tests/AdminIpBanExpiry.test.ts @@ -1,7 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import {AdminRepository} from '@app/api/admin/AdminRepository'; -import type {AdminAuditLog} from '@app/api/admin/IAdminRepository'; +import {type AdminAuditLog, AdminRepository} from '@app/api/admin/AdminRepository'; import {createTestAccount, setUserACLs, type TestAccount} from '@app/api/auth/tests/AuthTestUtils'; import {ipBanCache} from '@app/api/middleware/IpBanMiddleware'; import {getAdminRepository} from '@app/api/middleware/ServiceSingletons'; diff --git a/fluxer_api/src/api/admin/tests/AdminReportDelete.test.ts b/fluxer_api/src/api/admin/tests/AdminReportDelete.test.ts index 240148297..80c7cd18f 100644 --- a/fluxer_api/src/api/admin/tests/AdminReportDelete.test.ts +++ b/fluxer_api/src/api/admin/tests/AdminReportDelete.test.ts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import type {AdminAuditLog} from '@app/api/admin/IAdminRepository'; +import type {AdminAuditLog} from '@app/api/admin/AdminRepository'; import {createTestAccount, setUserACLs, type TestAccount} from '@app/api/auth/tests/AuthTestUtils'; import { createAttachmentID, diff --git a/fluxer_api/src/api/admin/tests/AdminSyntheticUserServiceGuards.test.ts b/fluxer_api/src/api/admin/tests/AdminSyntheticUserServiceGuards.test.ts index 7a7a13c5a..f09af3607 100644 --- a/fluxer_api/src/api/admin/tests/AdminSyntheticUserServiceGuards.test.ts +++ b/fluxer_api/src/api/admin/tests/AdminSyntheticUserServiceGuards.test.ts @@ -1,10 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {createGuildID, createUserID, type UserID} from '@app/api/BrandedTypes'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {CrosspostWorkerService} from '@app/api/channel/services/message/CrosspostPropagation'; import {UserMessageDeletionService} from '@app/api/channel/services/message/UserMessageDeletionService'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import {GuildMemberOperationsService} from '@app/api/guild/services/member/GuildMemberOperationsService'; import type {IPurgeQueue} from '@app/api/infrastructure/CachePurgeQueue'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; @@ -34,7 +34,7 @@ function unusableDependency(): object { function createMessageDeletionService(): UserMessageDeletionService { const unusable = unusableDependency(); return new UserMessageDeletionService({ - channelRepository: unusable as IChannelRepository, + channelRepository: unusable as ChannelRepository, gatewayService: unusable as IGatewayService, storageService: unusable as IStorageService, purgeQueue: unusable as IPurgeQueue, @@ -50,7 +50,7 @@ function createGuildMemberOperationsService(): GuildMemberOperationsService { async getMember() { return null; }, - } as unknown as IGuildRepositoryAggregate; + } as unknown as GuildRepository; const unusable = unusableDependency(); return new GuildMemberOperationsService( guildRepository, diff --git a/fluxer_api/src/api/admin/tests/InstanceConfigAdminController.test.ts b/fluxer_api/src/api/admin/tests/InstanceConfigAdminController.test.ts index 76b94410d..a4211eb3b 100644 --- a/fluxer_api/src/api/admin/tests/InstanceConfigAdminController.test.ts +++ b/fluxer_api/src/api/admin/tests/InstanceConfigAdminController.test.ts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import type {AdminAuditLog} from '@app/api/admin/IAdminRepository'; +import type {AdminAuditLog} from '@app/api/admin/AdminRepository'; import type {TestAccount} from '@app/api/auth/tests/AuthTestUtils'; import {createTestAccount, setUserACLs} from '@app/api/auth/tests/AuthTestUtils'; import {setCassandraQueryExecutorForTesting} from '@app/api/database/CassandraQueryExecution'; diff --git a/fluxer_api/src/api/admin/tests/audit_coverage/AdminAuditCoverage.ts b/fluxer_api/src/api/admin/tests/audit_coverage/AdminAuditCoverage.ts index 75f24f0b3..057079eef 100644 --- a/fluxer_api/src/api/admin/tests/audit_coverage/AdminAuditCoverage.ts +++ b/fluxer_api/src/api/admin/tests/audit_coverage/AdminAuditCoverage.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {getAdminAuditAccess} from '@app/api/admin/AdminAuditActions'; -import type {AdminAuditLog} from '@app/api/admin/IAdminRepository'; +import type {AdminAuditLog} from '@app/api/admin/AdminRepository'; import {createTestAccount, setUserACLs, type TestAccount} from '@app/api/auth/tests/AuthTestUtils'; import {getAdminRepository} from '@app/api/middleware/ServiceSingletons'; import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness'; diff --git a/fluxer_api/src/api/auth/AuthController.ts b/fluxer_api/src/api/auth/AuthController.ts index 53285989c..9fa2ab645 100644 --- a/fluxer_api/src/api/auth/AuthController.ts +++ b/fluxer_api/src/api/auth/AuthController.ts @@ -1,7 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {requireSudoMode} from '@app/api/auth/services/SudoVerificationService'; -import {Config} from '@app/api/Config'; import { onUsernameInstance, RequireEmailAccountIdentity, @@ -16,8 +15,8 @@ import {OpenAPI} from '@app/api/middleware/ResponseTypeMiddleware'; import {SudoModeMiddleware} from '@app/api/middleware/SudoModeMiddleware'; import {RateLimitConfigs} from '@app/api/RateLimitConfig'; import type {HonoApp} from '@app/api/types/HonoEnv'; +import {requireRequestClientIp} from '@app/api/utils/RequestClientIp'; import {Validator} from '@app/api/Validator'; -import {requireClientIp} from '@fluxer/ip_utils/src/ClientIp'; import { AuthLoginResponse, AuthorizeIpRequest, @@ -646,10 +645,7 @@ export function AuthController(app: HonoApp) { 'Retrieve device and location information about a pending handoff request. Non-destructive – the code remains valid after this call.', }), async (ctx) => { - const clientIp = requireClientIp(ctx.req.raw, { - trustClientIpHeader: Config.proxy.trust_client_ip_header, - clientIpHeaderName: Config.proxy.client_ip_header, - }); + const clientIp = requireRequestClientIp(ctx); const response = await ctx.get('authRequestService').getHandoffInfo({ code: ctx.req.valid('param').code, clientIp, @@ -672,10 +668,7 @@ export function AuthController(app: HonoApp) { 'Complete the handoff process and authenticate on the target device using the handoff code. With the deep_link return method, responds with the deep link that hands the one-time grant back to the initiating app. Otherwise responds with no content.', }), async (ctx) => { - const clientIp = requireClientIp(ctx.req.raw, { - trustClientIpHeader: Config.proxy.trust_client_ip_header, - clientIpHeaderName: Config.proxy.client_ip_header, - }); + const clientIp = requireRequestClientIp(ctx); const response = await ctx.get('authRequestService').completeHandoff({ data: ctx.req.valid('json'), clientIp, @@ -700,10 +693,7 @@ export function AuthController(app: HonoApp) { 'Decline a handoff request after looking it up. The initiating device sees the denied status and the code can no longer be approved.', }), async (ctx) => { - const clientIp = requireClientIp(ctx.req.raw, { - trustClientIpHeader: Config.proxy.trust_client_ip_header, - clientIpHeaderName: Config.proxy.client_ip_header, - }); + const clientIp = requireRequestClientIp(ctx); await ctx.get('authRequestService').denyHandoff({code: ctx.req.valid('param').code, clientIp}); return ctx.body(null, 204); }, @@ -723,10 +713,7 @@ export function AuthController(app: HonoApp) { 'Check the status of a handoff session. Returns whether the handoff has been completed or is still pending.', }), async (ctx) => { - const clientIp = requireClientIp(ctx.req.raw, { - trustClientIpHeader: Config.proxy.trust_client_ip_header, - clientIpHeaderName: Config.proxy.client_ip_header, - }); + const clientIp = requireRequestClientIp(ctx); const response = await ctx.get('authRequestService').getHandoffStatus({ code: ctx.req.valid('param').code, clientIp, @@ -750,10 +737,7 @@ export function AuthController(app: HonoApp) { 'Check the status of a handoff session using the poll secret from initiation. Returns the authentication token once the handoff is complete and the presented secret matches.', }), async (ctx) => { - const clientIp = requireClientIp(ctx.req.raw, { - trustClientIpHeader: Config.proxy.trust_client_ip_header, - clientIpHeaderName: Config.proxy.client_ip_header, - }); + const clientIp = requireRequestClientIp(ctx); const response = await ctx.get('authRequestService').getHandoffStatus({ code: ctx.req.valid('param').code, clientIp, diff --git a/fluxer_api/src/api/auth/AuthEmailRevert.ts b/fluxer_api/src/api/auth/AuthEmailRevert.ts index b0409e330..f7b2f7612 100644 --- a/fluxer_api/src/api/auth/AuthEmailRevert.ts +++ b/fluxer_api/src/api/auth/AuthEmailRevert.ts @@ -8,9 +8,9 @@ import {createEmailRevertToken} from '@app/api/BrandedTypes'; import type {User} from '@app/api/models/User'; import {enqueueStripeCustomerEmailSync} from '@app/api/stripe/StripeCustomer'; import {mapUserToPrivateResponse} from '@app/api/user/UserMappers'; +import {requireConfiguredClientIp} from '@app/api/utils/RequestClientIp'; import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes'; import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError'; -import {requireClientIp} from '@fluxer/ip_utils/src/ClientIp'; interface IssueEmailRevertTokenParams { user: User; @@ -80,13 +80,7 @@ export async function revertEmailChange( await users.deleteAllWebAuthnCredentials(user.id); await users.deleteAllAuthorizedIps(user.id); await AuthSession.terminateAllUserSessions(ctx, user.id); - await users.createAuthorizedIp( - user.id, - requireClientIp(request, { - trustClientIpHeader: config.proxy.trust_client_ip_header, - clientIpHeaderName: config.proxy.client_ip_header, - }), - ); + await users.createAuthorizedIp(user.id, requireConfiguredClientIp(request, config.proxy)); await gateway.dispatchPresence({ userId: updatedUser.id, event: 'USER_UPDATE', diff --git a/fluxer_api/src/api/auth/AuthLogin.ts b/fluxer_api/src/api/auth/AuthLogin.ts index edc39d51b..e826566ee 100644 --- a/fluxer_api/src/api/auth/AuthLogin.ts +++ b/fluxer_api/src/api/auth/AuthLogin.ts @@ -31,6 +31,7 @@ import type {User} from '@app/api/models/User'; import {findPersonByLoginHandle, type ParsedLoginHandle, parseLoginHandle} from '@app/api/user/UniqueUsernames'; import {lookupGeoip} from '@app/api/utils/IpUtils'; import {createRateLimitError} from '@app/api/utils/RateLimitUtils'; +import {requireConfiguredClientIp} from '@app/api/utils/RequestClientIp'; import {AccountIdentityModes} from '@fluxer/constants/src/AccountIdentityConstants'; import {UserAuthenticatorTypes, UserFlags} from '@fluxer/constants/src/UserConstants'; import {type ValidationErrorCode, ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes'; @@ -42,7 +43,6 @@ import {RegistrationPendingApprovalError} from '@fluxer/errors/src/domains/auth/ import {RegistrationRejectedError} from '@fluxer/errors/src/domains/auth/RegistrationRejectedError'; import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError'; import {UnknownUserError} from '@fluxer/errors/src/domains/user/UnknownUserError'; -import {requireClientIp} from '@fluxer/ip_utils/src/ClientIp'; import {getSameIpDecisionKey} from '@fluxer/ip_utils/src/IpAddress'; import type {LoginRequest} from '@fluxer/schema/src/domains/auth/AuthSchemas'; import {EmailType} from '@fluxer/schema/src/primitives/UserValidators'; @@ -240,10 +240,7 @@ export async function login( } }; await enforceRateLimits(identifier.rateLimits); - const clientIp = requireClientIp(request, { - trustClientIpHeader: config.proxy.trust_client_ip_header, - clientIpHeaderName: config.proxy.client_ip_header, - }); + const clientIp = requireConfiguredClientIp(request, config.proxy); const sourceKey = getSameIpDecisionKey(clientIp) ?? clientIp; await enforceRateLimits([ ...identifier.sourceRateLimits(sourceKey), diff --git a/fluxer_api/src/api/auth/AuthPassword.ts b/fluxer_api/src/api/auth/AuthPassword.ts index cda0ad59a..d4b2ebeb2 100644 --- a/fluxer_api/src/api/auth/AuthPassword.ts +++ b/fluxer_api/src/api/auth/AuthPassword.ts @@ -17,12 +17,12 @@ import {EXTERNAL_RESPONSE_LIMITS} from '@app/api/utils/ExternalResponseLimits'; import * as FetchUtils from '@app/api/utils/FetchUtils'; import {hashPassword as hashPasswordUtil, verifyPassword as verifyPasswordUtil} from '@app/api/utils/PasswordUtils'; import {createRateLimitError} from '@app/api/utils/RateLimitUtils'; +import {requireConfiguredClientIp} from '@app/api/utils/RequestClientIp'; import {AccountIdentityModes} from '@fluxer/constants/src/AccountIdentityConstants'; import {FLUXER_USER_AGENT} from '@fluxer/constants/src/Core'; import {UserAuthenticatorTypes, UserFlags} from '@fluxer/constants/src/UserConstants'; import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes'; import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError'; -import {requireClientIp} from '@fluxer/ip_utils/src/ClientIp'; import {getSameIpDecisionKey} from '@fluxer/ip_utils/src/IpAddress'; import type {ForgotPasswordRequest, ResetPasswordRequest} from '@fluxer/schema/src/domains/auth/AuthSchemas'; import {ms} from 'itty-time'; @@ -197,10 +197,7 @@ export async function isPasswordPwned(_ctx: ApiContext, password: string): Promi export async function forgotPassword(ctx: ApiContext, {data, request}: ForgotPasswordParams): Promise { const {users, email, rateLimit, emailDnsValidation, config} = ctx.services; - const clientIp = requireClientIp(request, { - trustClientIpHeader: config.proxy.trust_client_ip_header, - clientIpHeaderName: config.proxy.client_ip_header, - }); + const clientIp = requireConfiguredClientIp(request, config.proxy); const ipRateLimit = await rateLimit.checkLimit({ identifier: `password_reset:ip:${getSameIpDecisionKey(clientIp) ?? clientIp}`, maxAttempts: 20, diff --git a/fluxer_api/src/api/auth/AuthRecoveryKit.ts b/fluxer_api/src/api/auth/AuthRecoveryKit.ts index 208e69e5e..c1c24662c 100644 --- a/fluxer_api/src/api/auth/AuthRecoveryKit.ts +++ b/fluxer_api/src/api/auth/AuthRecoveryKit.ts @@ -19,6 +19,7 @@ import {getInstanceConfigRepository} from '@app/api/middleware/ServiceSingletons import type {User} from '@app/api/models/User'; import {findPersonByLoginHandle, parseLoginHandle} from '@app/api/user/UniqueUsernames'; import {createRateLimitError} from '@app/api/utils/RateLimitUtils'; +import {requireConfiguredClientIp} from '@app/api/utils/RequestClientIp'; import {AccountIdentityModes} from '@fluxer/constants/src/AccountIdentityConstants'; import { generateRecoveryKey, @@ -30,7 +31,6 @@ import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes'; import {RegistrationPendingApprovalError} from '@fluxer/errors/src/domains/auth/RegistrationPendingApprovalError'; import {RegistrationRejectedError} from '@fluxer/errors/src/domains/auth/RegistrationRejectedError'; import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError'; -import {requireClientIp} from '@fluxer/ip_utils/src/ClientIp'; import {getSameIpDecisionKey} from '@fluxer/ip_utils/src/IpAddress'; import type {RecoverAccountRequest} from '@fluxer/schema/src/domains/auth/AuthSchemas'; import type { @@ -101,10 +101,7 @@ function invalidRecoveryKeyError(): InputValidationError { async function checkRecoverRateLimits(ctx: ApiContext, username: string | null, request: Request): Promise { const {rateLimit, config} = ctx.services; - const clientIp = requireClientIp(request, { - trustClientIpHeader: config.proxy.trust_client_ip_header, - clientIpHeaderName: config.proxy.client_ip_header, - }); + const clientIp = requireConfiguredClientIp(request, config.proxy); const sourceKey = getSameIpDecisionKey(clientIp) ?? clientIp; const ipRateLimit = await rateLimit.checkLimit({ identifier: `recover:ip:${sourceKey}`, diff --git a/fluxer_api/src/api/auth/AuthRegistration.ts b/fluxer_api/src/api/auth/AuthRegistration.ts index 58e968ba1..ca25ec972 100644 --- a/fluxer_api/src/api/auth/AuthRegistration.ts +++ b/fluxer_api/src/api/auth/AuthRegistration.ts @@ -42,6 +42,7 @@ import * as AgeUtils from '@app/api/utils/AgeUtils'; import {extractEmailDomain} from '@app/api/utils/EmailDomainUtils'; import {lookupGeoip} from '@app/api/utils/IpUtils'; import {createRateLimitError} from '@app/api/utils/RateLimitUtils'; +import {requireConfiguredClientIp} from '@app/api/utils/RequestClientIp'; import {generateRandomUsername} from '@app/api/utils/UsernameGenerator'; import {deriveUsernameFromDisplayName} from '@app/api/utils/UsernameSuggestionUtils'; import {inputValidationErrorFromZodIssues} from '@app/api/Validator'; @@ -54,11 +55,10 @@ import {RegistrationClosedError} from '@fluxer/errors/src/domains/auth/Registrat import {RegistrationUrlInvalidError} from '@fluxer/errors/src/domains/auth/RegistrationUrlInvalidError'; import {ContentBlockedError} from '@fluxer/errors/src/domains/content/ContentBlockedError'; import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError'; -import {requireClientIp} from '@fluxer/ip_utils/src/ClientIp'; import {getSameIpDecisionKey, getSubnet} from '@fluxer/ip_utils/src/IpAddress'; import type {RegisterRequest} from '@fluxer/schema/src/domains/auth/AuthSchemas'; import {UsernameType} from '@fluxer/schema/src/primitives/UserValidators'; -import {parseAcceptLanguage} from '@pkgs/locale/src/LocaleService'; +import {parseAcceptLanguage} from '@pkgs/locale/src/resolution/AcceptLanguageNegotiation'; import {types} from 'cassandra-driver'; import {ms} from 'itty-time'; @@ -143,10 +143,7 @@ async function registerAccount( } const now = new Date(); const registrationAccess = await resolveRegistrationAccess(instanceConfigRepository, data.registration_url_code); - const clientIp = requireClientIp(request, { - trustClientIpHeader: config.proxy.trust_client_ip_header, - clientIpHeaderName: config.proxy.client_ip_header, - }); + const clientIp = requireConfiguredClientIp(request, config.proxy); const geoipResult = await lookupGeoip(clientIp); const countryCode = geoipResult.countryCode; const collectDateOfBirth = appPublicConfig.registration.collect_date_of_birth; diff --git a/fluxer_api/src/api/auth/AuthSession.ts b/fluxer_api/src/api/auth/AuthSession.ts index 8946aa11e..1843f4675 100644 --- a/fluxer_api/src/api/auth/AuthSession.ts +++ b/fluxer_api/src/api/auth/AuthSession.ts @@ -14,6 +14,7 @@ import {getKVAccountDeletionQueue} from '@app/api/middleware/ServiceSingletons'; import type {AuthSession} from '@app/api/models/AuthSession'; import type {User} from '@app/api/models/User'; import {lookupGeoip} from '@app/api/utils/IpUtils'; +import {requireConfiguredClientIp} from '@app/api/utils/RequestClientIp'; import {isFluxerNativeUserAgent, parseReportedClientOs} from '@app/api/utils/SessionClientIdentity'; import {BotUserAuthSessionCreationDeniedError} from '@fluxer/errors/src/domains/auth/BotUserAuthSessionCreationDeniedError'; import {RegistrationPendingApprovalError} from '@fluxer/errors/src/domains/auth/RegistrationPendingApprovalError'; @@ -21,7 +22,6 @@ import {RegistrationRejectedError} from '@fluxer/errors/src/domains/auth/Registr import {SessionTokenMismatchError} from '@fluxer/errors/src/domains/auth/SessionTokenMismatchError'; import {InvalidTokenError} from '@fluxer/errors/src/domains/core/InvalidTokenError'; import {UnknownUserError} from '@fluxer/errors/src/domains/user/UnknownUserError'; -import {requireClientIp} from '@fluxer/ip_utils/src/ClientIp'; import type {AuthSessionResponse} from '@fluxer/schema/src/domains/auth/AuthSchemas'; export interface SessionOrigin { @@ -67,10 +67,7 @@ interface CreateAdditionalAuthSessionFromTokenParams { export function resolveSessionOrigin(ctx: ApiContext, request: Request): SessionOrigin { const {config} = ctx.services; - const ip = requireClientIp(request, { - trustClientIpHeader: config.proxy.trust_client_ip_header, - clientIpHeaderName: config.proxy.client_ip_header, - }); + const ip = requireConfiguredClientIp(request, config.proxy); const userAgent = request.headers.get('user-agent')?.trim() || null; const clientOs = isFluxerNativeUserAgent(userAgent) ? parseReportedClientOs(request.headers.get('x-fluxer-client-properties')) diff --git a/fluxer_api/src/api/auth/AuthSessionRevocation.ts b/fluxer_api/src/api/auth/AuthSessionRevocation.ts index 006a6fc1b..5fd34def6 100644 --- a/fluxer_api/src/api/auth/AuthSessionRevocation.ts +++ b/fluxer_api/src/api/auth/AuthSessionRevocation.ts @@ -1,10 +1,10 @@ import type {UserID} from '@app/api/BrandedTypes'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; -import type {IUserRepositoryAggregate} from '@app/api/user/repositories/IUserRepositoryAggregate'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {runAllInOrder} from '@app/api/utils/ConcurrencyUtils'; interface SessionRevocationDependencies { - users: IUserRepositoryAggregate; + users: UserRepository; gateway: IGatewayService; } diff --git a/fluxer_api/src/api/auth/tests/AuthSudoRequiredOperations.test.ts b/fluxer_api/src/api/auth/tests/AuthSudoRequiredOperations.test.ts index c13308861..0786c9ec7 100644 --- a/fluxer_api/src/api/auth/tests/AuthSudoRequiredOperations.test.ts +++ b/fluxer_api/src/api/auth/tests/AuthSudoRequiredOperations.test.ts @@ -1,10 +1,16 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import {randomBytes} from 'node:crypto'; -import {createAuthHarness, createTestAccount, loginAccount, type TestAccount} from '@app/api/auth/tests/AuthTestUtils'; +import { + createAuthHarness, + createTestAccount, + createTotpSecret, + loginAccount, + loginWithTotp, + type TestAccount, + totpCodeNow, +} from '@app/api/auth/tests/AuthTestUtils'; import type {ApiTestHarness} from '@app/api/test/ApiTestHarness'; -import {createBuilder, createBuilderWithoutAuth} from '@app/api/test/TestRequestBuilder'; -import {TotpGenerator} from '@app/api/utils/TotpGenerator'; +import {createBuilder} from '@app/api/test/TestRequestBuilder'; import type {AuthSessionResponse} from '@fluxer/schema/src/domains/auth/AuthSchemas'; import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest'; @@ -25,65 +31,6 @@ interface OAuth2ApplicationResponse { }; } -function generateTotpSecret(): string { - const buffer = randomBytes(20); - const base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; - let result = ''; - for (let i = 0; i < buffer.length; i += 5) { - const bytes = [buffer[i]!, buffer[i + 1]!, buffer[i + 2]!, buffer[i + 3]!, buffer[i + 4]!]; - const n = (bytes[0]! << 24) | (bytes[1]! << 16) | (bytes[2]! << 8) | bytes[3]!; - const indices = [ - (n >> 3) & 0x1f, - ((n >> 11) | ((bytes[4]! << 4) & 0xf)) & 0x1f, - ((n >> 19) | ((bytes[4]! << 2) & 0x3c)) & 0x1f, - (bytes[4]! >> 1) & 0x1f, - ]; - result += base32Chars[indices[0]!]; - result += base32Chars[indices[1]!]; - result += base32Chars[indices[2]!]; - result += base32Chars[indices[3]!]; - } - return result; -} - -async function generateTotpCode(secret: string): Promise { - const totp = new TotpGenerator(secret); - const codes = await totp.generateTotp(); - return codes[0]!; -} - -async function loginWithTotp(harness: ApiTestHarness, account: TestAccount, secret: string): Promise { - const loginResp = await createBuilderWithoutAuth< - | { - mfa: true; - ticket: string; - } - | { - mfa: false; - token: string; - } - >(harness) - .post('/auth/login') - .body({ - email: account.email, - password: account.password, - }) - .execute(); - if (!loginResp.mfa) { - throw new Error('Expected MFA login'); - } - const mfaLoginResp = await createBuilderWithoutAuth<{ - token: string; - }>(harness) - .post('/auth/login/mfa/totp') - .body({ - ticket: loginResp.ticket, - code: await generateTotpCode(secret), - }) - .execute(); - return {...account, token: mfaLoginResp.token}; -} - async function createOAuth2BotApplication( harness: ApiTestHarness, account: TestAccount, @@ -177,8 +124,8 @@ describe('Auth sudo required operations', () => { }); it('requires sudo for disable TOTP MFA', async () => { let account = await createTestAccount(harness); - const secret = generateTotpSecret(); - const code = await generateTotpCode(secret); + const secret = createTotpSecret(); + const code = totpCodeNow(secret); const enableResp = await createBuilder(harness, account.token) .post('/users/@me/mfa/totp/enable') .body({ @@ -202,15 +149,15 @@ describe('Auth sudo required operations', () => { .body({ code: backupCode, mfa_method: 'totp', - mfa_code: await generateTotpCode(secret), + mfa_code: totpCodeNow(secret), }) .expect(204) .execute(); }); it('requires sudo for enable TOTP MFA', async () => { const account = await createTestAccount(harness); - const secret = generateTotpSecret(); - const code = await generateTotpCode(secret); + const secret = createTotpSecret(); + const code = totpCodeNow(secret); await createBuilder(harness, account.token) .post('/users/@me/mfa/totp/enable') .body({ diff --git a/fluxer_api/src/api/auth/tests/AuthSudoTOTPVerification.test.ts b/fluxer_api/src/api/auth/tests/AuthSudoTOTPVerification.test.ts index 28c9f8f0e..838142809 100644 --- a/fluxer_api/src/api/auth/tests/AuthSudoTOTPVerification.test.ts +++ b/fluxer_api/src/api/auth/tests/AuthSudoTOTPVerification.test.ts @@ -4,11 +4,11 @@ import { createAuthHarness, createTestAccount, createTotpSecret, - type TestAccount, + loginWithTotp, totpCodeNow, } from '@app/api/auth/tests/AuthTestUtils'; import type {ApiTestHarness} from '@app/api/test/ApiTestHarness'; -import {createBuilder, createBuilderWithoutAuth} from '@app/api/test/TestRequestBuilder'; +import {createBuilder} from '@app/api/test/TestRequestBuilder'; import type {AuthSessionResponse} from '@fluxer/schema/src/domains/auth/AuthSchemas'; import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest'; @@ -21,38 +21,6 @@ interface BackupCodesResponse { const SUDO_MODE_HEADER = 'X-Fluxer-Sudo-Mode-JWT'; -async function loginWithTotp(harness: ApiTestHarness, account: TestAccount, secret: string): Promise { - const loginResp = await createBuilderWithoutAuth< - | { - mfa: true; - ticket: string; - } - | { - mfa: false; - token: string; - } - >(harness) - .post('/auth/login') - .body({ - email: account.email, - password: account.password, - }) - .execute(); - if (!loginResp.mfa) { - throw new Error('Expected MFA login'); - } - const mfaLoginResp = await createBuilderWithoutAuth<{ - token: string; - }>(harness) - .post('/auth/login/mfa/totp') - .body({ - ticket: loginResp.ticket, - code: totpCodeNow(secret), - }) - .execute(); - return {...account, token: mfaLoginResp.token}; -} - describe('Auth sudo TOTP verification', () => { let harness: ApiTestHarness; beforeAll(async () => { diff --git a/fluxer_api/src/api/auth/tests/AuthTestUtils.ts b/fluxer_api/src/api/auth/tests/AuthTestUtils.ts index 48fad628d..0ecdfe1bf 100644 --- a/fluxer_api/src/api/auth/tests/AuthTestUtils.ts +++ b/fluxer_api/src/api/auth/tests/AuthTestUtils.ts @@ -260,6 +260,22 @@ export function totpCodeNow(secret: string): string { return generateTotpCode(secret, Date.now()); } +export async function loginWithTotp( + harness: ApiTestHarness, + account: TestAccount, + secret: string, +): Promise { + const login = await loginUser(harness, {email: account.email, password: account.password}); + if (!('mfa' in login)) { + throw new Error('Expected MFA login'); + } + const mfaLogin = await createBuilder<{token: string}>(harness, '') + .post('/auth/login/mfa/totp') + .body({ticket: login.ticket, code: totpCodeNow(secret)}) + .execute(); + return {...account, token: mfaLogin.token}; +} + export async function seedMfaTicket( harness: ApiTestHarness, ticket: string, diff --git a/fluxer_api/src/api/auth/tests/MfaConsistency.test.ts b/fluxer_api/src/api/auth/tests/MfaConsistency.test.ts index a05acb572..06f4e3bdf 100644 --- a/fluxer_api/src/api/auth/tests/MfaConsistency.test.ts +++ b/fluxer_api/src/api/auth/tests/MfaConsistency.test.ts @@ -4,6 +4,7 @@ import { createTestAccount, createTotpSecret, generateTotpCode, + loginWithTotp, type TestAccount, } from '@app/api/auth/tests/AuthTestUtils'; import { @@ -51,27 +52,6 @@ interface SudoModeRequiredResponse { }; } -async function loginWithTotp(harness: ApiTestHarness, account: TestAccount, secret: string): Promise { - const login = await createBuilderWithoutAuth(harness) - .post('/auth/login') - .body({ - email: account.email, - password: account.password, - }) - .execute(); - expect(login.mfa).toBe(true); - const mfaLogin = await createBuilderWithoutAuth<{ - token: string; - }>(harness) - .post('/auth/login/mfa/totp') - .body({ - code: generateTotpCode(secret), - ticket: login.ticket, - }) - .execute(); - return {...account, token: mfaLogin.token}; -} - async function setupWebAuthnOnlyUser( harness: ApiTestHarness, account: TestAccount, diff --git a/fluxer_api/src/api/auth/tests/SudoModeNegativeCases.test.ts b/fluxer_api/src/api/auth/tests/SudoModeNegativeCases.test.ts index c8b2188c1..f6c06d0d9 100644 --- a/fluxer_api/src/api/auth/tests/SudoModeNegativeCases.test.ts +++ b/fluxer_api/src/api/auth/tests/SudoModeNegativeCases.test.ts @@ -1,11 +1,17 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import {randomBytes} from 'node:crypto'; -import {createAuthHarness, createTestAccount, loginUser, type TestAccount} from '@app/api/auth/tests/AuthTestUtils'; +import { + createAuthHarness, + createTestAccount, + createTotpSecret, + loginUser, + loginWithTotp, + type TestAccount, + totpCodeNow, +} from '@app/api/auth/tests/AuthTestUtils'; import type {ApiTestHarness} from '@app/api/test/ApiTestHarness'; import {HTTP_STATUS} from '@app/api/test/TestConstants'; -import {createBuilder, createBuilderWithoutAuth} from '@app/api/test/TestRequestBuilder'; -import {TotpGenerator} from '@app/api/utils/TotpGenerator'; +import {createBuilder} from '@app/api/test/TestRequestBuilder'; import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest'; const SUDO_MODE_HEADER = 'X-Fluxer-Sudo-Mode-JWT'; @@ -22,35 +28,8 @@ interface BackupCodesResponse { }>; } -function generateTotpSecret(): string { - const buffer = randomBytes(20); - const base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; - let result = ''; - for (let i = 0; i < buffer.length; i += 5) { - const bytes = [buffer[i]!, buffer[i + 1]!, buffer[i + 2]!, buffer[i + 3]!, buffer[i + 4]!]; - const n = (bytes[0]! << 24) | (bytes[1]! << 16) | (bytes[2]! << 8) | bytes[3]!; - const indices = [ - (n >> 3) & 0x1f, - ((n >> 11) | ((bytes[4]! << 4) & 0xf)) & 0x1f, - ((n >> 19) | ((bytes[4]! << 2) & 0x3c)) & 0x1f, - (bytes[4]! >> 1) & 0x1f, - ]; - result += base32Chars[indices[0]!]; - result += base32Chars[indices[1]!]; - result += base32Chars[indices[2]!]; - result += base32Chars[indices[3]!]; - } - return result; -} - -async function generateTotpCode(secret: string): Promise { - const totp = new TotpGenerator(secret); - const codes = await totp.generateTotp(); - return codes[0]!; -} - async function enableTotpForAccount(harness: ApiTestHarness, account: TestAccount, secret: string): Promise { - const code = await generateTotpCode(secret); + const code = totpCodeNow(secret); await createBuilder(harness, account.token) .post('/users/@me/mfa/totp/enable') .body({ @@ -61,29 +40,12 @@ async function enableTotpForAccount(harness: ApiTestHarness, account: TestAccoun .execute(); } -async function loginWithTotp(harness: ApiTestHarness, account: TestAccount, secret: string): Promise { - const login = await loginUser(harness, {email: account.email, password: account.password}); - if (!('mfa' in login)) { - throw new Error('Expected MFA login'); - } - const mfaLoginResp = await createBuilderWithoutAuth<{ - token: string; - }>(harness) - .post('/auth/login/mfa/totp') - .body({ - ticket: login.ticket, - code: await generateTotpCode(secret), - }) - .execute(); - return {...account, token: mfaLoginResp.token}; -} - async function getSudoTokenViaMfa(harness: ApiTestHarness, token: string, secret: string): Promise { const {response} = await createBuilder(harness, token) .post('/users/@me/mfa/backup-codes') .body({ mfa_method: 'totp', - mfa_code: await generateTotpCode(secret), + mfa_code: totpCodeNow(secret), regenerate: false, }) .executeWithResponse(); @@ -108,7 +70,7 @@ describe('Sudo mode negative cases', () => { describe('invalid sudo token rejected', () => { it('rejects malformed sudo token', async () => { const account = await createTestAccount(harness); - const secret = generateTotpSecret(); + const secret = createTotpSecret(); await enableTotpForAccount(harness, account, secret); const loggedIn = await loginWithTotp(harness, account, secret); const invalidTokens = [ @@ -128,7 +90,7 @@ describe('Sudo mode negative cases', () => { }); it('rejects empty sudo token header', async () => { const account = await createTestAccount(harness); - const secret = generateTotpSecret(); + const secret = createTotpSecret(); await enableTotpForAccount(harness, account, secret); const loggedIn = await loginWithTotp(harness, account, secret); const {json: errResp} = await createBuilder(harness, loggedIn.token) @@ -166,7 +128,7 @@ describe('Sudo mode negative cases', () => { describe('wrong MFA code rejected', () => { it('rejects incorrect TOTP codes', async () => { const account = await createTestAccount(harness); - const secret = generateTotpSecret(); + const secret = createTotpSecret(); await enableTotpForAccount(harness, account, secret); const loggedIn = await loginWithTotp(harness, account, secret); const wrongCodes = ['000000', '123456', '999999', '12345', '1234567', 'abcdef']; @@ -185,12 +147,12 @@ describe('Sudo mode negative cases', () => { describe('sudo token for wrong user rejected', () => { it('rejects sudo token from different user', async () => { const account1 = await createTestAccount(harness); - const secret1 = generateTotpSecret(); + const secret1 = createTotpSecret(); await enableTotpForAccount(harness, account1, secret1); const loggedIn1 = await loginWithTotp(harness, account1, secret1); const user1SudoToken = await getSudoTokenViaMfa(harness, loggedIn1.token, secret1); const account2 = await createTestAccount(harness); - const secret2 = generateTotpSecret(); + const secret2 = createTotpSecret(); await enableTotpForAccount(harness, account2, secret2); const loggedIn2 = await loginWithTotp(harness, account2, secret2); const {json: errResp} = await createBuilder(harness, loggedIn2.token) @@ -243,7 +205,7 @@ describe('Sudo mode negative cases', () => { describe('MFA registration without token fails', () => { it('WebAuthn registration options do not issue sudo token', async () => { const account = await createTestAccount(harness); - const secret = generateTotpSecret(); + const secret = createTotpSecret(); await enableTotpForAccount(harness, account, secret); const loggedIn = await loginWithTotp(harness, account, secret); const sudoToken = await getSudoTokenViaMfa(harness, loggedIn.token, secret); @@ -259,7 +221,7 @@ describe('Sudo mode negative cases', () => { describe('existing MFA token allows skipping MFA', () => { it('sudo token from MFA allows skipping MFA verification on sensitive endpoint', async () => { const account = await createTestAccount(harness); - const secret = generateTotpSecret(); + const secret = createTotpSecret(); await enableTotpForAccount(harness, account, secret); const loggedIn = await loginWithTotp(harness, account, secret); const sudoToken = await getSudoTokenViaMfa(harness, loggedIn.token, secret); diff --git a/fluxer_api/src/api/auth/tests/WebAuthnTestUtils.ts b/fluxer_api/src/api/auth/tests/WebAuthnTestUtils.ts index b4d1c8977..ac09a831b 100644 --- a/fluxer_api/src/api/auth/tests/WebAuthnTestUtils.ts +++ b/fluxer_api/src/api/auth/tests/WebAuthnTestUtils.ts @@ -1,17 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import { - createHash, - createHmac, - createPrivateKey, - createPublicKey, - createSign, - generateKeyPairSync, - randomBytes, -} from 'node:crypto'; +import {createHash, createPrivateKey, createPublicKey, createSign, generateKeyPairSync, randomBytes} from 'node:crypto'; import type {ApiTestHarness} from '@app/api/test/ApiTestHarness'; import {createBuilder, createBuilderWithoutAuth} from '@app/api/test/TestRequestBuilder'; -import {decode as base32Decode, encode as base32Encode} from 'hi-base32'; + +export {createTotpSecret, generateTotpCode} from '@app/api/auth/tests/AuthTestUtils'; export interface WebAuthnDevice { privateKey: string; @@ -103,30 +96,6 @@ interface WebAuthnAuthenticationResponse { response: AuthenticatorAssertionResponse; } -export function createTotpSecret(): string { - const buf = randomBytes(20); - return base32Encode(buf).replace(/=/g, ''); -} - -export function generateTotpCode(secret: string, time = Date.now()): string { - const key = Buffer.from(base32Decode.asBytes(secret.toUpperCase())); - const epoch = Math.floor(time / 1000); - const counter = Math.floor(epoch / 30); - const counterBuf = Buffer.alloc(8); - counterBuf.writeBigUInt64BE(BigInt(counter)); - const hmac = createHmac('sha1', key); - hmac.update(counterBuf); - const hash = hmac.digest(); - const offset = hash[hash.length - 1] & 0x0f; - const binary = - ((hash[offset] & 0x7f) << 24) | - ((hash[offset + 1] & 0xff) << 16) | - ((hash[offset + 2] & 0xff) << 8) | - (hash[offset + 3] & 0xff); - const otp = binary % 1000000; - return otp.toString().padStart(6, '0'); -} - function resolveWebAuthnOrigin(): { rpId: string; origin: string; diff --git a/fluxer_api/src/api/channel/ChannelRepository.ts b/fluxer_api/src/api/channel/ChannelRepository.ts index 75d01e158..387dad79f 100644 --- a/fluxer_api/src/api/channel/ChannelRepository.ts +++ b/fluxer_api/src/api/channel/ChannelRepository.ts @@ -1,124 +1,24 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import type {AttachmentID, ChannelID, GuildID, MessageID, UserID} from '@app/api/BrandedTypes'; -import {IChannelRepository} from '@app/api/channel/IChannelRepository'; -import {ChannelRepository as NewChannelRepository} from '@app/api/channel/repositories/ChannelRepository'; -import type {GuildChannelListMode} from '@app/api/channel/repositories/IChannelDataRepository'; -import type {UpsertMessageOptions} from '@app/api/channel/repositories/IMessageRepository'; -import type {ChannelRow} from '@app/api/database/types/ChannelTypes'; -import type {MessageRow} from '@app/api/database/types/MessageTypes'; +import {ChannelDataRepository} from '@app/api/channel/repositories/ChannelDataRepository'; +import {CrosspostedMessageRepository} from '@app/api/channel/repositories/CrosspostedMessageRepository'; +import {MessageInteractionRepository} from '@app/api/channel/repositories/MessageInteractionRepository'; +import {MessageRepository} from '@app/api/channel/repositories/MessageRepository'; +import {ThreadRepository} from '@app/api/channel/repositories/ThreadRepository'; +import {enqueueRepairThreadIndexes} from '@app/api/channel/threads/ThreadJobs'; import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware'; -import type {Channel} from '@app/api/models/Channel'; -import type {Message} from '@app/api/models/Message'; -export class ChannelRepository extends IChannelRepository { - private repository: NewChannelRepository; +export class ChannelRepository { + readonly channelData: ChannelDataRepository; + readonly messages: MessageRepository; + readonly messageInteractions: MessageInteractionRepository; + readonly threads: ThreadRepository; + readonly crossposts = new CrosspostedMessageRepository(); constructor(requestCache?: RequestCache) { - super(); - this.repository = new NewChannelRepository(requestCache); - } - - get channelData() { - return this.repository.channelData; - } - - get messages() { - return this.repository.messages; - } - - get messageInteractions() { - return this.repository.messageInteractions; - } - - get threads() { - return this.repository.threads; - } - - get crossposts() { - return this.repository.crossposts; - } - - async findUnique(channelId: ChannelID): Promise { - return this.repository.channelData.findUnique(channelId); - } - - async upsert(data: ChannelRow): Promise { - return this.repository.channelData.upsert(data); - } - - async delete(channelId: ChannelID, guildId?: GuildID, type?: number): Promise { - return this.repository.channelData.delete(channelId, guildId, type); - } - - async listMessages( - channelId: ChannelID, - beforeMessageId?: MessageID, - limit?: number, - afterMessageId?: MessageID, - ): Promise> { - return this.repository.messages.listMessages(channelId, beforeMessageId, limit, afterMessageId); - } - - async getMessage(channelId: ChannelID, messageId: MessageID): Promise { - return this.repository.messages.getMessage(channelId, messageId); - } - - async upsertMessage(data: MessageRow, oldData?: MessageRow | null, opts?: UpsertMessageOptions): Promise { - return this.repository.messages.upsertMessage(data, oldData, opts); - } - - async deleteMessage( - channelId: ChannelID, - messageId: MessageID, - authorId: UserID, - pinnedTimestamp?: Date, - ): Promise { - return this.repository.messages.deleteMessage(channelId, messageId, authorId, pinnedTimestamp); - } - - async bulkDeleteMessages(channelId: ChannelID, messageIds: Array): Promise { - return this.repository.messages.bulkDeleteMessages(channelId, messageIds); - } - - async listGuildChannels(guildId: GuildID, mode: GuildChannelListMode): Promise> { - return this.repository.channelData.listGuildChannels(guildId, mode); - } - - async listChannels(channelIds: Array): Promise> { - return this.repository.channelData.listChannels(channelIds); - } - - async lookupAttachmentByChannelAndFilename( - channelId: ChannelID, - attachmentId: AttachmentID, - filename: string, - ): Promise { - return this.repository.messages.lookupAttachmentByChannelAndFilename(channelId, attachmentId, filename); - } - - async listMessagesByAuthor( - authorId: UserID, - limit?: number, - lastMessageId?: MessageID, - ): Promise< - Array<{ - channelId: ChannelID; - messageId: MessageID; - }> - > { - return this.repository.messages.listMessagesByAuthor(authorId, limit, lastMessageId); - } - - async anonymizeMessage(channelId: ChannelID, messageId: MessageID, newAuthorId: UserID): Promise { - return this.repository.messages.anonymizeMessage(channelId, messageId, newAuthorId); - } - - async deleteAllChannelMessages(channelId: ChannelID): Promise { - return this.repository.messages.deleteAllChannelMessages(channelId); - } - - async updateEmbeds(message: Message): Promise { - return this.repository.messages.updateEmbeds(message); + this.channelData = new ChannelDataRepository(requestCache); + this.messages = new MessageRepository(this.channelData); + this.messageInteractions = new MessageInteractionRepository(this.messages); + this.threads = new ThreadRepository(this.channelData, this.messages, enqueueRepairThreadIndexes); } } diff --git a/fluxer_api/src/api/channel/IChannelRepository.ts b/fluxer_api/src/api/channel/IChannelRepository.ts deleted file mode 100644 index 27019a93d..000000000 --- a/fluxer_api/src/api/channel/IChannelRepository.ts +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {AttachmentID, ChannelID, GuildID, MessageID, UserID} from '@app/api/BrandedTypes'; -import type {GuildChannelListMode} from '@app/api/channel/repositories/IChannelDataRepository'; -import {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; -import type {UpsertMessageOptions} from '@app/api/channel/repositories/IMessageRepository'; -import type {ChannelRow} from '@app/api/database/types/ChannelTypes'; -import type {MessageRow} from '@app/api/database/types/MessageTypes'; -import type {Channel} from '@app/api/models/Channel'; -import type {Message} from '@app/api/models/Message'; - -export abstract class IChannelRepository extends IChannelRepositoryAggregate { - abstract findUnique(channelId: ChannelID): Promise; - - abstract upsert(data: ChannelRow): Promise; - - abstract delete(channelId: ChannelID, guildId?: GuildID, type?: number): Promise; - - abstract listGuildChannels(guildId: GuildID, mode: GuildChannelListMode): Promise>; - - abstract listChannels(channelIds: Array): Promise>; - - abstract listMessages( - channelId: ChannelID, - beforeMessageId?: MessageID, - limit?: number, - afterMessageId?: MessageID, - ): Promise>; - - abstract getMessage(channelId: ChannelID, messageId: MessageID): Promise; - - abstract upsertMessage(data: MessageRow, oldData?: MessageRow | null, opts?: UpsertMessageOptions): Promise; - - abstract deleteMessage( - channelId: ChannelID, - messageId: MessageID, - authorId: UserID, - pinnedTimestamp?: Date, - ): Promise; - - abstract bulkDeleteMessages(channelId: ChannelID, messageIds: Array): Promise; - - abstract lookupAttachmentByChannelAndFilename( - channelId: ChannelID, - attachmentId: AttachmentID, - filename: string, - ): Promise; - - abstract listMessagesByAuthor( - authorId: UserID, - limit?: number, - lastMessageId?: MessageID, - ): Promise< - Array<{ - channelId: ChannelID; - messageId: MessageID; - }> - >; - - abstract anonymizeMessage(channelId: ChannelID, messageId: MessageID, newAuthorId: UserID): Promise; - - abstract deleteAllChannelMessages(channelId: ChannelID): Promise; - - abstract updateEmbeds(message: Message): Promise; -} diff --git a/fluxer_api/src/api/channel/controllers/MessageController.ts b/fluxer_api/src/api/channel/controllers/MessageController.ts index 111c38914..f1fcfb843 100644 --- a/fluxer_api/src/api/channel/controllers/MessageController.ts +++ b/fluxer_api/src/api/channel/controllers/MessageController.ts @@ -2,7 +2,6 @@ import {requireSudoMode} from '@app/api/auth/services/SudoVerificationService'; import {createAttachmentID, createChannelID, createMessageID} from '@app/api/BrandedTypes'; -import {Config} from '@app/api/Config'; import type {MessageRequest, MessageUpdateRequest} from '@app/api/channel/MessageTypes'; import {normalizeMessageRequestPayload} from '@app/api/channel/services/message/MessageRequestCompatibility'; import {parseMultipartMessageData} from '@app/api/channel/services/message/MessageRequestParser'; @@ -16,10 +15,10 @@ import {readStateCapable} from '@app/api/read_state/ReadStateChannelMeta'; import type {HonoApp} from '@app/api/types/HonoEnv'; import {assertAccountNotLimited} from '@app/api/user/AccountLimit'; import {parseJsonPreservingLargeIntegers} from '@app/api/utils/LosslessJsonParser'; +import {requireRequestClientIp} from '@app/api/utils/RequestClientIp'; import {Validator} from '@app/api/Validator'; import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes'; import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError'; -import {requireClientIp} from '@fluxer/ip_utils/src/ClientIp'; import {SudoVerificationSchema} from '@fluxer/schema/src/domains/auth/AuthSchemas'; import { ChannelIdMessageIdAttachmentIdParam, @@ -227,10 +226,7 @@ export function MessageController(app: HonoApp) { 'Returns presigned upload URLs for message attachments in the target channel. Small files (<=10MB) return a singlepart PUT URL; larger files return a multipart plan (upload_id + per-part URLs) that the client should complete via the matching /attachments/complete endpoint. Requires message send and attachment permissions in guild channels.', }), async (ctx) => { - const clientIp = requireClientIp(ctx.req.raw, { - trustClientIpHeader: Config.proxy.trust_client_ip_header, - clientIpHeaderName: Config.proxy.client_ip_header, - }); + const clientIp = requireRequestClientIp(ctx); const channelId = createChannelID(ctx.req.valid('param').channel_id); const {attachments} = ctx.req.valid('json'); return ctx.json({ @@ -261,10 +257,7 @@ export function MessageController(app: HonoApp) { 'Finalizes one or more multipart attachment uploads. Called after all chunks have been PUT to their presigned URLs. The server lists the uploaded parts and issues S3 CompleteMultipartUpload. Returns the finalized upload keys, which can be referenced in a subsequent message create request.', }), async (ctx) => { - const clientIp = requireClientIp(ctx.req.raw, { - trustClientIpHeader: Config.proxy.trust_client_ip_header, - clientIpHeaderName: Config.proxy.client_ip_header, - }); + const clientIp = requireRequestClientIp(ctx); const channelId = createChannelID(ctx.req.valid('param').channel_id); const {uploads} = ctx.req.valid('json'); return ctx.json({ diff --git a/fluxer_api/src/api/channel/controllers/StreamController.ts b/fluxer_api/src/api/channel/controllers/StreamController.ts index 2642e6e00..7c9b40c12 100644 --- a/fluxer_api/src/api/channel/controllers/StreamController.ts +++ b/fluxer_api/src/api/channel/controllers/StreamController.ts @@ -1,15 +1,14 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {createChannelID} from '@app/api/BrandedTypes'; -import {Config} from '@app/api/Config'; import {viewerFromCtx} from '@app/api/experiment/ChannelThreadsGate'; import {DefaultUserOnly, LoginRequired} from '@app/api/middleware/AuthMiddleware'; import {RateLimitMiddleware} from '@app/api/middleware/RateLimitMiddleware'; import {OpenAPI} from '@app/api/middleware/ResponseTypeMiddleware'; import {RateLimitConfigs} from '@app/api/RateLimitConfig'; import type {HonoApp} from '@app/api/types/HonoEnv'; +import {requireRequestClientIp} from '@app/api/utils/RequestClientIp'; import {Validator} from '@app/api/Validator'; -import {requireClientIp} from '@fluxer/ip_utils/src/ClientIp'; import { StreamPreviewResponse, StreamPreviewUploadBodySchema, @@ -102,10 +101,7 @@ export function StreamController(app: HonoApp) { const user = ctx.get('user'); const {channel_id, content_type} = ctx.req.valid('json'); const streamKey = ctx.req.valid('param').stream_key; - const clientIp = requireClientIp(ctx.req.raw, { - trustClientIpHeader: Config.proxy.trust_client_ip_header, - clientIpHeaderName: Config.proxy.client_ip_header, - }); + const clientIp = requireRequestClientIp(ctx); const response = await ctx.get('streamService').createPreviewUploadUrl({ viewer: viewerFromCtx(ctx), streamKey, diff --git a/fluxer_api/src/api/channel/repositories/ChannelDataRepository.ts b/fluxer_api/src/api/channel/repositories/ChannelDataRepository.ts index c262a6c0e..732daf640 100644 --- a/fluxer_api/src/api/channel/repositories/ChannelDataRepository.ts +++ b/fluxer_api/src/api/channel/repositories/ChannelDataRepository.ts @@ -6,7 +6,6 @@ import { privateChannelLastMessageIdPatch, privateChannelMetadataPatch, } from '@app/api/channel/PrivateChannelSnapshot'; -import {type GuildChannelListMode, IChannelDataRepository} from '@app/api/channel/repositories/IChannelDataRepository'; import { BatchBuilder, executeConditional, @@ -27,6 +26,8 @@ import {Channel} from '@app/api/models/Channel'; import {Channels, ChannelsByGuild, PrivateChannels, ThreadOnlyChannelsByGuild, ThreadStats} from '@app/api/Tables'; import {THREAD_CHANNEL_TYPES, THREAD_ONLY_CHANNEL_TYPES} from '@fluxer/constants/src/ThreadConstants'; +export type GuildChannelListMode = 'enrolled' | 'maintenance' | 'complete'; + const FETCH_CHANNEL_BY_ID = Channels.select({ where: [Channels.where.eq('channel_id'), Channels.where.eq('soft_deleted')], limit: 1, @@ -48,10 +49,8 @@ const FETCH_OPEN_PRIVATE_CHANNEL_TARGET = PrivateChannels.selectCql({ limit: 1, }); -export class ChannelDataRepository extends IChannelDataRepository { - constructor(private readonly requestCache?: RequestCache) { - super(); - } +export class ChannelDataRepository { + constructor(private readonly requestCache?: RequestCache) {} async findUnique(channelId: ChannelID): Promise { const prefetched = this.requestCache?.takeChannel(channelId); diff --git a/fluxer_api/src/api/channel/repositories/ChannelRepository.ts b/fluxer_api/src/api/channel/repositories/ChannelRepository.ts deleted file mode 100644 index 17601eda5..000000000 --- a/fluxer_api/src/api/channel/repositories/ChannelRepository.ts +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {ChannelDataRepository} from '@app/api/channel/repositories/ChannelDataRepository'; -import {CrosspostedMessageRepository} from '@app/api/channel/repositories/CrosspostedMessageRepository'; -import {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; -import {MessageInteractionRepository} from '@app/api/channel/repositories/MessageInteractionRepository'; -import {MessageRepository} from '@app/api/channel/repositories/MessageRepository'; -import {ThreadRepository} from '@app/api/channel/repositories/ThreadRepository'; -import {enqueueRepairThreadIndexes} from '@app/api/channel/threads/ThreadJobs'; -import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware'; - -export class ChannelRepository extends IChannelRepositoryAggregate { - readonly channelData: ChannelDataRepository; - readonly messages: MessageRepository; - readonly messageInteractions: MessageInteractionRepository; - readonly threads: ThreadRepository; - readonly crossposts: CrosspostedMessageRepository; - - constructor(requestCache?: RequestCache) { - super(); - this.channelData = new ChannelDataRepository(requestCache); - this.messages = new MessageRepository(this.channelData); - this.messageInteractions = new MessageInteractionRepository(this.messages); - this.threads = new ThreadRepository(this.channelData, this.messages, enqueueRepairThreadIndexes); - this.crossposts = new CrosspostedMessageRepository(); - } -} diff --git a/fluxer_api/src/api/channel/repositories/CrosspostedMessageRepository.ts b/fluxer_api/src/api/channel/repositories/CrosspostedMessageRepository.ts index 79560fd12..e453bfe2e 100644 --- a/fluxer_api/src/api/channel/repositories/CrosspostedMessageRepository.ts +++ b/fluxer_api/src/api/channel/repositories/CrosspostedMessageRepository.ts @@ -1,12 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {ChannelID, MessageID, WebhookID} from '@app/api/BrandedTypes'; -import { - type CrosspostedMessageKey, - type CrosspostSource, - type CrosspostSyncState, - ICrosspostedMessageRepository, -} from '@app/api/channel/repositories/ICrosspostedMessageRepository'; import { deleteOneOrMany, executeConditional, @@ -18,6 +12,21 @@ import {Db} from '@app/api/database/CassandraTypes'; import type {CrosspostedMessageRow, CrosspostSourceByChannelRow} from '@app/api/database/types/ChannelTypes'; import {CrosspostedMessages, CrosspostSourcesByChannel} from '@app/api/Tables'; +export interface CrosspostedMessageKey { + sourceMessageId: MessageID; + webhookId: WebhookID; +} + +export interface CrosspostSyncState { + targetMessageId: MessageID; + sourceFingerprint: string | null; +} + +export interface CrosspostSource { + sourceChannelId: ChannelID; + sourceMessageId: MessageID; +} + const FETCH_CROSSPOSTED_MESSAGE_CQL = CrosspostedMessages.selectCql({ where: [CrosspostedMessages.where.eq('source_message_id'), CrosspostedMessages.where.eq('webhook_id')], limit: 1, @@ -64,7 +73,7 @@ function toPk(key: CrosspostedMessageKey): Pick { return fetchOne(FETCH_CROSSPOSTED_MESSAGE_CQL, { source_message_id: sourceMessageId, diff --git a/fluxer_api/src/api/channel/repositories/IChannelDataRepository.ts b/fluxer_api/src/api/channel/repositories/IChannelDataRepository.ts deleted file mode 100644 index 4086cb618..000000000 --- a/fluxer_api/src/api/channel/repositories/IChannelDataRepository.ts +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {ChannelID, GuildID, MessageID} from '@app/api/BrandedTypes'; -import type {ChannelRow} from '@app/api/database/types/ChannelTypes'; -import type {Channel} from '@app/api/models/Channel'; - -export type GuildChannelListMode = 'enrolled' | 'maintenance' | 'complete'; - -export abstract class IChannelDataRepository { - abstract findUnique(channelId: ChannelID): Promise; - - abstract upsert(data: ChannelRow, oldData?: ChannelRow | null): Promise; - - abstract updateLastMessageId(channelId: ChannelID, messageId: MessageID, opts?: {isInsert?: boolean}): Promise; - - abstract delete(channelId: ChannelID, guildId?: GuildID, type?: number): Promise; - - abstract listGuildChannels(guildId: GuildID, mode: GuildChannelListMode): Promise>; - - abstract listChannels(channelIds: Array): Promise>; - - abstract countGuildChannels(guildId: GuildID): Promise; - - abstract patchIndexedAt(channelId: ChannelID, indexedAt: Date): Promise; -} diff --git a/fluxer_api/src/api/channel/repositories/IChannelRepositoryAggregate.ts b/fluxer_api/src/api/channel/repositories/IChannelRepositoryAggregate.ts deleted file mode 100644 index 2bba1900e..000000000 --- a/fluxer_api/src/api/channel/repositories/IChannelRepositoryAggregate.ts +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {IChannelDataRepository} from '@app/api/channel/repositories/IChannelDataRepository'; -import type {ICrosspostedMessageRepository} from '@app/api/channel/repositories/ICrosspostedMessageRepository'; -import type {IMessageInteractionRepository} from '@app/api/channel/repositories/IMessageInteractionRepository'; -import type {IMessageRepository} from '@app/api/channel/repositories/IMessageRepository'; -import type {IThreadRepository} from '@app/api/channel/repositories/IThreadRepository'; - -export abstract class IChannelRepositoryAggregate { - abstract readonly channelData: IChannelDataRepository; - abstract readonly messages: IMessageRepository; - abstract readonly messageInteractions: IMessageInteractionRepository; - abstract readonly threads: IThreadRepository; - abstract readonly crossposts: ICrosspostedMessageRepository; -} diff --git a/fluxer_api/src/api/channel/repositories/ICrosspostedMessageRepository.ts b/fluxer_api/src/api/channel/repositories/ICrosspostedMessageRepository.ts deleted file mode 100644 index c8a968e53..000000000 --- a/fluxer_api/src/api/channel/repositories/ICrosspostedMessageRepository.ts +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {ChannelID, MessageID, WebhookID} from '@app/api/BrandedTypes'; -import type {CrosspostedMessageRow} from '@app/api/database/types/ChannelTypes'; - -export interface CrosspostedMessageKey { - sourceMessageId: MessageID; - webhookId: WebhookID; -} - -export interface CrosspostSyncState { - targetMessageId: MessageID; - sourceFingerprint: string | null; -} - -export interface CrosspostSource { - sourceChannelId: ChannelID; - sourceMessageId: MessageID; -} - -export abstract class ICrosspostedMessageRepository { - abstract get(sourceMessageId: MessageID, webhookId: WebhookID): Promise; - - abstract insertPending(row: CrosspostedMessageRow): Promise; - - abstract reclaimPending( - key: CrosspostedMessageKey, - data: { - fromTargetMessageId: MessageID; - toTargetMessageId: MessageID; - reservedAt: Date; - }, - ): Promise; - - abstract markDelivered(key: CrosspostedMessageKey, data: CrosspostSyncState): Promise; - - abstract updateSynced(key: CrosspostedMessageKey, data: CrosspostSyncState): Promise; - - abstract listBySourceMessage( - sourceMessageId: MessageID, - options: {afterWebhookId?: WebhookID; limit: number}, - ): Promise>; - - abstract delete( - key: CrosspostedMessageKey, - expected?: Partial>, - ): Promise; - - abstract addSource(source: CrosspostSource): Promise; - - abstract listSourcesByChannel( - sourceChannelId: ChannelID, - options: {afterMessageId?: MessageID; limit: number}, - ): Promise>; - - abstract deleteSource(source: CrosspostSource): Promise; -} diff --git a/fluxer_api/src/api/channel/repositories/IMessageInteractionRepository.ts b/fluxer_api/src/api/channel/repositories/IMessageInteractionRepository.ts deleted file mode 100644 index 20b7ce93b..000000000 --- a/fluxer_api/src/api/channel/repositories/IMessageInteractionRepository.ts +++ /dev/null @@ -1,69 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {ChannelID, EmojiID, MessageID, UserID} from '@app/api/BrandedTypes'; -import type {Message} from '@app/api/models/Message'; -import type {MessageReaction} from '@app/api/models/MessageReaction'; - -export abstract class IMessageInteractionRepository { - abstract listChannelPins(channelId: ChannelID, beforePinnedTimestamp: Date, limit?: number): Promise>; - - abstract addChannelPin(channelId: ChannelID, messageId: MessageID, pinnedTimestamp: Date): Promise; - - abstract removeChannelPin(channelId: ChannelID, messageId: MessageID): Promise; - - abstract listMessageReactions(channelId: ChannelID, messageId: MessageID): Promise>; - - abstract listReactionUsers( - channelId: ChannelID, - messageId: MessageID, - emojiName: string, - limit?: number, - after?: UserID, - emojiId?: EmojiID, - ): Promise>; - - abstract addReaction( - channelId: ChannelID, - messageId: MessageID, - userId: UserID, - emojiName: string, - emojiId?: EmojiID, - emojiAnimated?: boolean, - ): Promise; - - abstract removeReaction( - channelId: ChannelID, - messageId: MessageID, - userId: UserID, - emojiName: string, - emojiId?: EmojiID, - ): Promise; - - abstract removeAllReactions(channelId: ChannelID, messageId: MessageID): Promise; - - abstract removeAllReactionsForEmoji( - channelId: ChannelID, - messageId: MessageID, - emojiName: string, - emojiId?: EmojiID, - ): Promise; - - abstract countReactionUsers( - channelId: ChannelID, - messageId: MessageID, - emojiName: string, - emojiId?: EmojiID, - ): Promise; - - abstract countUniqueReactions(channelId: ChannelID, messageId: MessageID): Promise; - - abstract checkUserReactionExists( - channelId: ChannelID, - messageId: MessageID, - userId: UserID, - emojiName: string, - emojiId?: EmojiID, - ): Promise; - - abstract setHasReaction(channelId: ChannelID, messageId: MessageID, hasReaction: boolean): Promise; -} diff --git a/fluxer_api/src/api/channel/repositories/IMessageRepository.ts b/fluxer_api/src/api/channel/repositories/IMessageRepository.ts deleted file mode 100644 index 0fbfbf7e8..000000000 --- a/fluxer_api/src/api/channel/repositories/IMessageRepository.ts +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {AttachmentID, ChannelID, MessageID, UserID} from '@app/api/BrandedTypes'; -import type {MessageRow} from '@app/api/database/types/MessageTypes'; -import type {Message} from '@app/api/models/Message'; - -export interface ListMessagesOptions { - restrictToBeforeBucket?: boolean; - immediateAfter?: boolean; -} - -export interface UpsertMessageOptions { - isInsert?: boolean; - skipParentLastMessageId?: boolean; -} - -export abstract class IMessageRepository { - abstract listMessages( - channelId: ChannelID, - beforeMessageId?: MessageID, - limit?: number, - afterMessageId?: MessageID, - options?: ListMessagesOptions, - ): Promise>; - - abstract getMessage(channelId: ChannelID, messageId: MessageID): Promise; - - abstract upsertMessage(data: MessageRow, oldData?: MessageRow | null, opts?: UpsertMessageOptions): Promise; - - abstract updateEmbeds(message: Message): Promise; - - abstract deleteMessage( - channelId: ChannelID, - messageId: MessageID, - authorId: UserID, - pinnedTimestamp?: Date, - ): Promise; - - abstract bulkDeleteMessages(channelId: ChannelID, messageIds: Array): Promise; - - abstract deleteAllChannelMessages(channelId: ChannelID): Promise; - - abstract listMessagesByAuthor( - authorId: UserID, - limit?: number, - lastMessageId?: MessageID, - ): Promise< - Array<{ - channelId: ChannelID; - messageId: MessageID; - }> - >; - - abstract anonymizeMessage(channelId: ChannelID, messageId: MessageID, newAuthorId: UserID): Promise; - - abstract lookupAttachmentByChannelAndFilename( - channelId: ChannelID, - attachmentId: AttachmentID, - filename: string, - ): Promise; -} diff --git a/fluxer_api/src/api/channel/repositories/IThreadRepository.ts b/fluxer_api/src/api/channel/repositories/IThreadRepository.ts deleted file mode 100644 index d373073ff..000000000 --- a/fluxer_api/src/api/channel/repositories/IThreadRepository.ts +++ /dev/null @@ -1,172 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {ChannelID, GuildID, MessageID, UserID} from '@app/api/BrandedTypes'; -import type {ChannelRow} from '@app/api/database/types/ChannelTypes'; -import type {GuildThreadStateRow, ThreadParentConfigRow, ThreadStateRow} from '@app/api/database/types/ThreadTypes'; -import type {MuteConfig} from '@app/api/database/types/UserTypes'; -import type {ThreadMember} from '@app/api/models/ThreadMember'; -import type {ThreadParentConfig} from '@app/api/models/ThreadParentConfig'; -import type {ThreadState} from '@app/api/models/ThreadState'; -import type {ThreadStats} from '@app/api/models/ThreadStats'; - -export interface CreateThreadMember { - userId: UserID; - flags: number; -} - -export interface CreateThreadParams { - channel: ChannelRow; - parentType: number; - autoArchiveDuration: number; - invitable: boolean | null; - flags: number; - appliedTags: Array; - hasStarter: boolean; - createdAt: Date; - members: Array; -} - -export type ThreadStatePatch = Partial< - Pick< - ThreadStateRow, - 'archived' | 'locked' | 'invitable' | 'auto_archive_duration' | 'archive_timestamp' | 'flags' | 'applied_tags' - > ->; - -export interface ThreadStateTransition { - previous: ThreadState; - state: ThreadState; -} - -export interface ArchivedThreadPage { - threads: Array; - hasMore: boolean; -} - -export interface ThreadMemberSettingsPatch { - flags?: number; - muted?: boolean; - muteConfig?: MuteConfig | null; -} - -export interface ThreadMemberAddResult { - added: Array; - state: ThreadState; -} - -export interface ThreadMemberRemoveResult { - removed: Array; - state: ThreadState | null; -} - -export type ThreadParentConfigPatch = Partial>; - -export abstract class IThreadRepository { - abstract getState(threadId: ChannelID): Promise; - - abstract getStates(threadIds: Array): Promise>; - - abstract getStats(threadId: ChannelID): Promise; - - abstract getStatsMany(threadIds: Array): Promise>; - - abstract adjustMessageCount(threadId: ChannelID, delta: number): Promise; - - abstract create(params: CreateThreadParams): Promise; - - abstract updateState( - threadId: ChannelID, - mutate: (current: ThreadState) => ThreadStatePatch | null, - ): Promise; - - abstract claimForumPin(parentId: ChannelID, threadId: ChannelID): Promise; - - abstract releaseForumPin(parentId: ChannelID, threadId: ChannelID): Promise; - - abstract getForumPin(parentId: ChannelID): Promise; - - abstract countActiveThreads(guildId: GuildID): Promise; - - abstract listActiveThreads(guildId: GuildID): Promise>; - - abstract listArchivedThreads( - parentId: ChannelID, - isPrivate: boolean, - opts: {before?: Date; limit: number}, - ): Promise; - - abstract listJoinedPrivateArchivedThreads( - userId: UserID, - guildId: GuildID, - parentId: ChannelID, - opts: {before?: ChannelID; limit: number}, - ): Promise; - - abstract listJoinedThreadIds(userId: UserID, guildId: GuildID): Promise>; - - abstract listJoinedPrivateThreadIds( - userId: UserID, - guildId: GuildID, - parentId: ChannelID, - limit: number, - ): Promise>; - - abstract repairThreadIndexes(threadIds: Array): Promise; - - abstract getMember(threadId: ChannelID, userId: UserID): Promise; - - abstract getMembers(threadId: ChannelID, userIds: Array): Promise>; - - abstract listMembers(threadId: ChannelID, opts: {after?: UserID; limit: number}): Promise>; - - abstract addMembers( - threadId: ChannelID, - members: Array, - opts?: {joinTimestamp?: Date}, - ): Promise; - - abstract removeMembers(threadId: ChannelID, userIds: Array): Promise; - - abstract updateMemberSettings(expected: ThreadMember, patch: ThreadMemberSettingsPatch): Promise; - - abstract listThreadIdsByParent( - parentId: ChannelID, - opts: {after?: ChannelID; limit: number}, - ): Promise>; - - abstract listParentThreads(parentId: ChannelID): Promise>; - - abstract setThreadType(threadId: ChannelID, type: number): Promise; - - abstract listGuildThreadIds( - guildId: GuildID, - opts?: {activeSince?: Date; parents?: ReadonlyArray<{id: ChannelID; type: number}>}, - ): Promise>; - - abstract purgeThread(threadId: ChannelID): Promise; - - abstract revertParentLastMessageId( - parentId: ChannelID, - threadId: ChannelID, - previous: MessageID | null, - ): Promise; - - abstract purgeGuild(guildId: GuildID): Promise; - - abstract getGuildMarker(guildId: GuildID): Promise; - - abstract ensureGuildMarker(guildId: GuildID, opts?: {permsSeededAt?: Date}): Promise; - - abstract markGuildPermsSeeded(guildId: GuildID, at: Date): Promise; - - abstract markGuildSearchBackfilled(guildId: GuildID, at: Date): Promise; - abstract clearGuildSearchBackfilled(guildId: GuildID): Promise; - - abstract getParentConfig(guildId: GuildID, channelId: ChannelID): Promise; - - abstract listParentConfigs(guildId: GuildID): Promise>; - - abstract patchParentConfig(guildId: GuildID, channelId: ChannelID, patch: ThreadParentConfigPatch): Promise; - - abstract deleteParentConfig(guildId: GuildID, channelId: ChannelID): Promise; -} diff --git a/fluxer_api/src/api/channel/repositories/MessageInteractionRepository.ts b/fluxer_api/src/api/channel/repositories/MessageInteractionRepository.ts index 7eeedefef..27473becd 100644 --- a/fluxer_api/src/api/channel/repositories/MessageInteractionRepository.ts +++ b/fluxer_api/src/api/channel/repositories/MessageInteractionRepository.ts @@ -2,7 +2,6 @@ import type {ChannelID, EmojiID, MessageID, UserID} from '@app/api/BrandedTypes'; import {createEmojiID} from '@app/api/BrandedTypes'; -import {IMessageInteractionRepository} from '@app/api/channel/repositories/IMessageInteractionRepository'; import type {MessageRepository} from '@app/api/channel/repositories/MessageRepository'; import {deleteOneOrMany, fetchMany, fetchOne, upsertOne} from '@app/api/database/CassandraQueryExecution'; import {Db} from '@app/api/database/CassandraTypes'; @@ -66,11 +65,10 @@ const CHECK_USER_REACTION_EXISTS_QUERY = MessageReactions.selectCql({ limit: 1, }); -export class MessageInteractionRepository extends IMessageInteractionRepository { +export class MessageInteractionRepository { private messageRepository: MessageRepository; constructor(messageRepository: MessageRepository) { - super(); this.messageRepository = messageRepository; } diff --git a/fluxer_api/src/api/channel/repositories/MessageRepository.ts b/fluxer_api/src/api/channel/repositories/MessageRepository.ts index 82731dc08..044f07b5c 100644 --- a/fluxer_api/src/api/channel/repositories/MessageRepository.ts +++ b/fluxer_api/src/api/channel/repositories/MessageRepository.ts @@ -2,11 +2,6 @@ import type {AttachmentID, ChannelID, MessageID, UserID} from '@app/api/BrandedTypes'; import type {ChannelDataRepository} from '@app/api/channel/repositories/ChannelDataRepository'; -import { - IMessageRepository, - type ListMessagesOptions, - type UpsertMessageOptions, -} from '@app/api/channel/repositories/IMessageRepository'; import {MessageAttachmentRepository} from '@app/api/channel/repositories/message/MessageAttachmentRepository'; import {MessageAuthorRepository} from '@app/api/channel/repositories/message/MessageAuthorRepository'; import {MessageDataRepository} from '@app/api/channel/repositories/message/MessageDataRepository'; @@ -14,7 +9,17 @@ import {MessageDeletionRepository} from '@app/api/channel/repositories/message/M import type {MessageRow} from '@app/api/database/types/MessageTypes'; import type {Message} from '@app/api/models/Message'; -export class MessageRepository extends IMessageRepository { +export interface ListMessagesOptions { + restrictToBeforeBucket?: boolean; + immediateAfter?: boolean; +} + +export interface UpsertMessageOptions { + isInsert?: boolean; + skipParentLastMessageId?: boolean; +} + +export class MessageRepository { private dataRepo: MessageDataRepository; private deletionRepo: MessageDeletionRepository; private attachmentRepo: MessageAttachmentRepository; @@ -22,7 +27,6 @@ export class MessageRepository extends IMessageRepository { private channelDataRepo: ChannelDataRepository; constructor(channelDataRepo: ChannelDataRepository) { - super(); this.dataRepo = new MessageDataRepository(); this.deletionRepo = new MessageDeletionRepository(this.dataRepo); this.attachmentRepo = new MessageAttachmentRepository(); diff --git a/fluxer_api/src/api/channel/repositories/ThreadRepository.test.ts b/fluxer_api/src/api/channel/repositories/ThreadRepository.test.ts index 8ad754c67..6bce7d016 100644 --- a/fluxer_api/src/api/channel/repositories/ThreadRepository.test.ts +++ b/fluxer_api/src/api/channel/repositories/ThreadRepository.test.ts @@ -13,9 +13,8 @@ import { createUserID, type GuildID, } from '@app/api/BrandedTypes'; -import {ChannelRepository} from '@app/api/channel/repositories/ChannelRepository'; -import type {CreateThreadParams} from '@app/api/channel/repositories/IThreadRepository'; -import {ThreadRepository} from '@app/api/channel/repositories/ThreadRepository'; +import {ChannelRepository} from '@app/api/channel/ChannelRepository'; +import {type CreateThreadParams, ThreadRepository} from '@app/api/channel/repositories/ThreadRepository'; import { type CassandraQueryExecutorForTesting, deleteOneOrMany, diff --git a/fluxer_api/src/api/channel/repositories/ThreadRepository.ts b/fluxer_api/src/api/channel/repositories/ThreadRepository.ts index b0815d2b8..9f97f26a4 100644 --- a/fluxer_api/src/api/channel/repositories/ThreadRepository.ts +++ b/fluxer_api/src/api/channel/repositories/ThreadRepository.ts @@ -2,19 +2,7 @@ import {type ChannelID, channelIdToMessageId, type GuildID, type MessageID, type UserID} from '@app/api/BrandedTypes'; import type {ChannelDataRepository} from '@app/api/channel/repositories/ChannelDataRepository'; -import type {IMessageRepository} from '@app/api/channel/repositories/IMessageRepository'; -import { - type ArchivedThreadPage, - type CreateThreadMember, - type CreateThreadParams, - IThreadRepository, - type ThreadMemberAddResult, - type ThreadMemberRemoveResult, - type ThreadMemberSettingsPatch, - type ThreadParentConfigPatch, - type ThreadStatePatch, - type ThreadStateTransition, -} from '@app/api/channel/repositories/IThreadRepository'; +import type {MessageRepository} from '@app/api/channel/repositories/MessageRepository'; import { BatchBuilder, deleteOneOrMany, @@ -37,6 +25,7 @@ import type { ThreadStatsRow, ThreadsByParentRow, } from '@app/api/database/types/ThreadTypes'; +import type {MuteConfig} from '@app/api/database/types/UserTypes'; import {insertGuildThreadMarker, isTainted} from '@app/api/experiment/ChannelThreadsGate'; import {Logger} from '@app/api/Logger'; import {ThreadMember} from '@app/api/models/ThreadMember'; @@ -70,6 +59,58 @@ import {ThreadAlreadyCreatedForMessageError} from '@fluxer/errors/src/domains/ch import {ServiceUnavailableError} from '@fluxer/errors/src/domains/core/ServiceUnavailableError'; import {snowflakeToDate} from '@fluxer/snowflake/src/Snowflake'; +export interface CreateThreadMember { + userId: UserID; + flags: number; +} + +export interface CreateThreadParams { + channel: ChannelRow; + parentType: number; + autoArchiveDuration: number; + invitable: boolean | null; + flags: number; + appliedTags: Array; + hasStarter: boolean; + createdAt: Date; + members: Array; +} + +export type ThreadStatePatch = Partial< + Pick< + ThreadStateRow, + 'archived' | 'locked' | 'invitable' | 'auto_archive_duration' | 'archive_timestamp' | 'flags' | 'applied_tags' + > +>; + +export interface ThreadStateTransition { + previous: ThreadState; + state: ThreadState; +} + +export interface ArchivedThreadPage { + threads: Array; + hasMore: boolean; +} + +export interface ThreadMemberSettingsPatch { + flags?: number; + muted?: boolean; + muteConfig?: MuteConfig | null; +} + +export interface ThreadMemberAddResult { + added: Array; + state: ThreadState; +} + +export interface ThreadMemberRemoveResult { + removed: Array; + state: ThreadState | null; +} + +export type ThreadParentConfigPatch = Partial>; + const STATE_CAS_ATTEMPTS = 3; const DANGLING_CREATE_REPAIR_MS = 30_000; const ENUMERATION_PAGE_SIZE = 1000; @@ -197,14 +238,12 @@ function toStateOps(patch: ThreadStatePatch): StatePatchOps { return ops; } -export class ThreadRepository extends IThreadRepository { +export class ThreadRepository { constructor( private readonly channelData: ChannelDataRepository, - private readonly messages: IMessageRepository, + private readonly messages: MessageRepository, private readonly onIndexDrift?: (threadIds: Array) => void, - ) { - super(); - } + ) {} async getState(threadId: ChannelID): Promise { const row = await fetchOne(FETCH_STATE.bind({thread_id: threadId})); diff --git a/fluxer_api/src/api/channel/repositories/message/MessageDataRepository.ts b/fluxer_api/src/api/channel/repositories/message/MessageDataRepository.ts index 3979b4f93..43ec62fec 100644 --- a/fluxer_api/src/api/channel/repositories/message/MessageDataRepository.ts +++ b/fluxer_api/src/api/channel/repositories/message/MessageDataRepository.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {ChannelID, MessageID} from '@app/api/BrandedTypes'; -import type {ListMessagesOptions} from '@app/api/channel/repositories/IMessageRepository'; +import type {ListMessagesOptions} from '@app/api/channel/repositories/MessageRepository'; import {BucketScanDirection, scanBucketsWithIndex} from '@app/api/channel/repositories/message/BucketScanEngine'; import {BatchBuilder, deleteOneOrMany, fetchMany, fetchOne, upsertOne} from '@app/api/database/CassandraQueryExecution'; import {Db, type QueryTemplate} from '@app/api/database/CassandraTypes'; diff --git a/fluxer_api/src/api/channel/services/AttachmentUploadService.ts b/fluxer_api/src/api/channel/services/AttachmentUploadService.ts index 239b42904..8662f5348 100644 --- a/fluxer_api/src/api/channel/services/AttachmentUploadService.ts +++ b/fluxer_api/src/api/channel/services/AttachmentUploadService.ts @@ -3,7 +3,7 @@ import type {AttachmentID, ChannelID, MessageID, UserID} from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; import type {UploadedAttachment} from '@app/api/channel/AttachmentDTOs'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type { AttachmentUploadMode, AttachmentUploadTraceRepository, @@ -33,7 +33,7 @@ import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware'; import type {Attachment} from '@app/api/models/Attachment'; import type {Channel} from '@app/api/models/Channel'; import type {Message} from '@app/api/models/Message'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {mapWithConcurrency} from '@app/api/utils/ConcurrencyUtils'; import {assertGuildMemberCanCommunicate} from '@app/api/utils/GuildCommunicationUtils'; import {Permissions, TEXT_BASED_CHANNEL_TYPES} from '@fluxer/constants/src/ChannelConstants'; @@ -111,8 +111,8 @@ const FORM_DATA_UPLOAD_CONCURRENCY = 2; export class AttachmentUploadService { constructor( - private channelRepository: IChannelRepositoryAggregate, - private userRepository: IUserRepository, + private channelRepository: ChannelRepository, + private userRepository: UserRepository, private storageService: IStorageService, private attachmentUploadTraceRepository: AttachmentUploadTraceRepository, private purgeQueue: IPurgeQueue, diff --git a/fluxer_api/src/api/channel/services/BaseChannelAuthService.ts b/fluxer_api/src/api/channel/services/BaseChannelAuthService.ts index 0c77a8f7c..f22a63f21 100644 --- a/fluxer_api/src/api/channel/services/BaseChannelAuthService.ts +++ b/fluxer_api/src/api/channel/services/BaseChannelAuthService.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {ChannelID, GuildID, UserID} from '@app/api/BrandedTypes'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {AuthenticatedChannel, AuthenticatedThread} from '@app/api/channel/services/AuthenticatedChannel'; import {DMPermissionValidator} from '@app/api/channel/services/DMPermissionValidator'; import { @@ -26,13 +26,13 @@ import { viewerActive, } from '@app/api/experiment/ChannelThreadsGate'; import {isGuildMemberTimedOut} from '@app/api/guild/GuildModel'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import {createGuildMfaEnforcer} from '@app/api/guild/services/GuildMfaEnforcement'; import type {GuildChannelAuthContext, IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {Channel} from '@app/api/models/Channel'; import type {GuildMember} from '@app/api/models/GuildMember'; import type {User} from '@app/api/models/User'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {canUserAccessNsfwContent} from '@app/api/utils/AgeUtils'; import {ChannelTypes, Permissions} from '@fluxer/constants/src/ChannelConstants'; import { @@ -80,9 +80,9 @@ export abstract class BaseChannelAuthService { protected dmPermissionValidator: DMPermissionValidator; constructor( - protected channelRepository: IChannelRepositoryAggregate, - protected userRepository: IUserRepository, - protected guildRepository: IGuildRepositoryAggregate, + protected channelRepository: ChannelRepository, + protected userRepository: UserRepository, + protected guildRepository: GuildRepository, protected gatewayService: IGatewayService, ) { this.dmPermissionValidator = new DMPermissionValidator({ diff --git a/fluxer_api/src/api/channel/services/CallService.ts b/fluxer_api/src/api/channel/services/CallService.ts index b6df5fd8a..fc77669c3 100644 --- a/fluxer_api/src/api/channel/services/CallService.ts +++ b/fluxer_api/src/api/channel/services/CallService.ts @@ -2,11 +2,11 @@ import {type ChannelID, createMessageID, type UserID} from '@app/api/BrandedTypes'; import {mapChannelToResponse} from '@app/api/channel/ChannelMappers'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import {DMPermissionValidator} from '@app/api/channel/services/DMPermissionValidator'; import {buildBroadcastMessageData} from '@app/api/channel/services/message/MessageGatewayDispatch'; import {incrementDmMentionCounts} from '@app/api/channel/services/message/ReadStateHelpers'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {CallData, IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService'; import type {IVoiceRoomStore} from '@app/api/infrastructure/IVoiceRoomStore'; @@ -14,8 +14,8 @@ import type {UserCacheService} from '@app/api/infrastructure/UserCacheService'; import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware'; import type {Channel} from '@app/api/models/Channel'; import type {ReadStateService} from '@app/api/read_state/ReadStateService'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; import {assertMayStartConversation} from '@app/api/user/NewConversationLimit'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import type {VoiceAccessContext, VoiceAvailabilityService} from '@app/api/voice/VoiceAvailabilityService'; import {AUTOMATIC_VOICE_REGION_ID, ChannelTypes, MessageTypes} from '@fluxer/constants/src/ChannelConstants'; import {IncomingCallFlags, RelationshipTypes} from '@fluxer/constants/src/UserConstants'; @@ -31,9 +31,9 @@ export class CallService { private dmPermissionValidator: DMPermissionValidator; constructor( - private channelRepository: IChannelRepository, - private userRepository: IUserRepository, - private guildRepository: IGuildRepositoryAggregate, + private channelRepository: ChannelRepository, + private userRepository: UserRepository, + private guildRepository: GuildRepository, private gatewayService: IGatewayService, private userCacheService: UserCacheService, private snowflakeService: ISnowflakeService, @@ -51,7 +51,7 @@ export class CallService { ringable: boolean; silent?: boolean; }> { - const channel = await this.channelRepository.findUnique(channelId); + const channel = await this.channelRepository.channelData.findUnique(channelId); if (!channel) throw new UnknownChannelError(); if (channel.type !== ChannelTypes.DM && channel.type !== ChannelTypes.GROUP_DM) { throw new InvalidChannelTypeForCallError(); @@ -148,7 +148,7 @@ export class CallService { latitude?: string; longitude?: string; }): Promise { - const channel = await this.channelRepository.findUnique(channelId); + const channel = await this.channelRepository.channelData.findUnique(channelId); if (!channel) throw new UnknownChannelError(); if (channel.type !== ChannelTypes.DM && channel.type !== ChannelTypes.GROUP_DM) { throw new InvalidChannelTypeForCallError(); @@ -175,7 +175,7 @@ export class CallService { user: caller, targetId: dmRecipientIds[0]!, users: this.userRepository, - messages: this.channelRepository, + messages: this.channelRepository.messages, channel, }); } @@ -191,7 +191,7 @@ export class CallService { const selectedRegion = region && region !== AUTOMATIC_VOICE_REGION_ID ? region : AUTOMATIC_VOICE_REGION_ID; const allRecipients = Array.from(new Set([userId, ...Array.from(channel.recipientIds)])); const messageId = createMessageID(await this.snowflakeService.generateForChannel(channelId)); - await this.channelRepository.upsertMessage({ + await this.channelRepository.messages.upsertMessage({ channel_id: channelId, bucket: BucketUtils.makeBucket(messageId), message_id: messageId, @@ -258,7 +258,7 @@ export class CallService { emitGateway: false, }); } - const message = await this.channelRepository.getMessage(channelId, messageId); + const message = await this.channelRepository.messages.getMessage(channelId, messageId); if (message) { const messageResponse = await buildBroadcastMessageData({ channel, @@ -286,7 +286,7 @@ export class CallService { latitude?: string; longitude?: string; }): Promise { - const channel = await this.channelRepository.findUnique(channelId); + const channel = await this.channelRepository.channelData.findUnique(channelId); if (!channel) throw new UnknownChannelError(); if (channel.type !== ChannelTypes.DM && channel.type !== ChannelTypes.GROUP_DM) { throw new InvalidChannelTypeForCallError(); @@ -326,7 +326,7 @@ export class CallService { latitude?: string; longitude?: string; }): Promise { - const channel = await this.channelRepository.findUnique(channelId); + const channel = await this.channelRepository.channelData.findUnique(channelId); if (!channel) throw new UnknownChannelError(); if (channel.type !== ChannelTypes.DM && channel.type !== ChannelTypes.GROUP_DM) { throw new InvalidChannelTypeForCallError(); @@ -353,7 +353,7 @@ export class CallService { user: caller, targetId: dmRecipientIds[0]!, users: this.userRepository, - messages: this.channelRepository, + messages: this.channelRepository.messages, channel, }); } @@ -472,7 +472,7 @@ export class CallService { channelId: ChannelID; recipients?: Array; }): Promise { - const channel = await this.channelRepository.findUnique(channelId); + const channel = await this.channelRepository.channelData.findUnique(channelId); if (!channel) throw new UnknownChannelError(); if (channel.type !== ChannelTypes.DM && channel.type !== ChannelTypes.GROUP_DM) { throw new InvalidChannelTypeForCallError(); diff --git a/fluxer_api/src/api/channel/services/ChannelDataService.ts b/fluxer_api/src/api/channel/services/ChannelDataService.ts index 55cf99495..d532af5bf 100644 --- a/fluxer_api/src/api/channel/services/ChannelDataService.ts +++ b/fluxer_api/src/api/channel/services/ChannelDataService.ts @@ -2,7 +2,7 @@ import type {ChannelID, UserID} from '@app/api/BrandedTypes'; import {createUserID} from '@app/api/BrandedTypes'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import {ChannelAuthService} from '@app/api/channel/services/channel_data/ChannelAuthService'; import type { ChannelTypeConversion, @@ -16,7 +16,7 @@ import {ThreadModifyService} from '@app/api/channel/services/thread/ThreadModify import {pickThreadParentSettings} from '@app/api/channel/services/thread/ThreadParentSettings'; import type {ThreadViewer} from '@app/api/experiment/ChannelThreadsGate'; import type {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {AvatarService} from '@app/api/infrastructure/AvatarService'; import type {IPurgeQueue} from '@app/api/infrastructure/CachePurgeQueue'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; @@ -25,13 +25,13 @@ import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService' import type {IStorageService} from '@app/api/infrastructure/IStorageService'; import type {IVoiceRoomStore} from '@app/api/infrastructure/IVoiceRoomStore'; import type {UserCacheService} from '@app/api/infrastructure/UserCacheService'; -import type {IInviteRepository} from '@app/api/invite/IInviteRepository'; +import type {InviteRepository} from '@app/api/invite/InviteRepository'; import type {LimitConfigService} from '@app/api/limits/LimitConfigService'; import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware'; import type {Channel} from '@app/api/models/Channel'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import type {VoiceAvailabilityService} from '@app/api/voice/VoiceAvailabilityService'; -import type {IWebhookRepository} from '@app/api/webhook/IWebhookRepository'; +import type {WebhookRepository} from '@app/api/webhook/WebhookRepository'; import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants'; import type { ChannelUpdateGatedRequest, @@ -57,9 +57,9 @@ export class ChannelDataService { public readonly threadModify: ThreadModifyService; constructor( - channelRepository: IChannelRepositoryAggregate, - userRepository: IUserRepository, - guildRepository: IGuildRepositoryAggregate, + channelRepository: ChannelRepository, + userRepository: UserRepository, + guildRepository: GuildRepository, userCacheService: UserCacheService, storageService: IStorageService, gatewayService: IGatewayService, @@ -71,8 +71,8 @@ export class ChannelDataService { voiceAvailabilityService: VoiceAvailabilityService | null, messagePersistenceService: MessagePersistenceService, guildAuditLogService: GuildAuditLogService, - inviteRepository: IInviteRepository, - webhookRepository: IWebhookRepository, + inviteRepository: InviteRepository, + webhookRepository: WebhookRepository, limitConfigService: LimitConfigService, rateLimitService: IRateLimitService, cacheService: ICacheService, diff --git a/fluxer_api/src/api/channel/services/ChannelFollowers.ts b/fluxer_api/src/api/channel/services/ChannelFollowers.ts index 740c2cf94..a29c05930 100644 --- a/fluxer_api/src/api/channel/services/ChannelFollowers.ts +++ b/fluxer_api/src/api/channel/services/ChannelFollowers.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {ChannelID} from '@app/api/BrandedTypes'; -import type {ICrosspostedMessageRepository} from '@app/api/channel/repositories/ICrosspostedMessageRepository'; +import type {CrosspostedMessageRepository} from '@app/api/channel/repositories/CrosspostedMessageRepository'; import {Logger} from '@app/api/Logger'; import {getSnowflakeService, getWorkerService} from '@app/api/middleware/ServiceRegistry'; import type {Channel} from '@app/api/models/Channel'; @@ -89,7 +89,7 @@ export async function enqueueChannelFollowerRemoval(params: ChannelFollowerRemov async function channelMayHaveFollowerCopies( channel: Pick, - crossposts: Pick, + crossposts: Pick, ): Promise { if (channel.type === ChannelTypes.GUILD_ANNOUNCEMENT) return true; const sources = await crossposts.listSourcesByChannel(channel.id, {limit: 1}); @@ -98,7 +98,7 @@ async function channelMayHaveFollowerCopies( export async function scheduleDeletedChannelFollowerRemoval(params: { channel: Pick; - crossposts: Pick; + crossposts: Pick; copyMode: ChannelFollowerRemovalCopyMode; }): Promise { if (!(await channelMayHaveFollowerCopies(params.channel, params.crossposts))) return; diff --git a/fluxer_api/src/api/channel/services/ChannelService.ts b/fluxer_api/src/api/channel/services/ChannelService.ts index 315bd7f18..d50b0dfa7 100644 --- a/fluxer_api/src/api/channel/services/ChannelService.ts +++ b/fluxer_api/src/api/channel/services/ChannelService.ts @@ -2,7 +2,7 @@ import type {ApiContext} from '@app/api/ApiContext'; import type {ChannelID} from '@app/api/BrandedTypes'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {AttachmentUploadTraceRepository} from '@app/api/channel/repositories/message/AttachmentUploadTraceRepository'; import {AttachmentUploadService} from '@app/api/channel/services/AttachmentUploadService'; import type {AuthenticatedChannel} from '@app/api/channel/services/AuthenticatedChannel'; @@ -14,9 +14,9 @@ import {MessageService} from '@app/api/channel/services/MessageService'; import {MessagePersistenceService} from '@app/api/channel/services/message/MessagePersistenceService'; import {UserMessageDeletionService} from '@app/api/channel/services/message/UserMessageDeletionService'; import {type ThreadViewer, viewerActive} from '@app/api/experiment/ChannelThreadsGate'; -import type {IFavoriteMemeRepository} from '@app/api/favorite_meme/IFavoriteMemeRepository'; +import type {FavoriteMemeRepository} from '@app/api/favorite_meme/FavoriteMemeRepository'; import type {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {AvatarService} from '@app/api/infrastructure/AvatarService'; import type {IPurgeQueue} from '@app/api/infrastructure/CachePurgeQueue'; import type {EmbedService} from '@app/api/infrastructure/EmbedService'; @@ -24,13 +24,13 @@ import type {ILiveKitService} from '@app/api/infrastructure/ILiveKitService'; import type {IStorageService} from '@app/api/infrastructure/IStorageService'; import type {IVoiceRoomStore} from '@app/api/infrastructure/IVoiceRoomStore'; import type {UserCacheService} from '@app/api/infrastructure/UserCacheService'; -import type {IInviteRepository} from '@app/api/invite/IInviteRepository'; +import type {InviteRepository} from '@app/api/invite/InviteRepository'; import type {LimitConfigService} from '@app/api/limits/LimitConfigService'; import type {User} from '@app/api/models/User'; import type {ReadStateService} from '@app/api/read_state/ReadStateService'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import type {VoiceAvailabilityService} from '@app/api/voice/VoiceAvailabilityService'; -import type {IWebhookRepository} from '@app/api/webhook/IWebhookRepository'; +import type {WebhookRepository} from '@app/api/webhook/WebhookRepository'; import {Permissions} from '@fluxer/constants/src/ChannelConstants'; import {TEXT_THREAD_PARENT_CHANNEL_TYPES} from '@fluxer/constants/src/ThreadConstants'; import type {IRateLimitService} from '@pkgs/rate_limit/src/IRateLimitService'; @@ -60,9 +60,9 @@ export class ChannelService { constructor( apiContext: ApiContext, - channelRepository: IChannelRepository, - userRepository: IUserRepository, - guildRepository: IGuildRepositoryAggregate, + channelRepository: ChannelRepository, + userRepository: UserRepository, + guildRepository: GuildRepository, userCacheService: UserCacheService, embedService: EmbedService, readStateService: ReadStateService, @@ -71,12 +71,12 @@ export class ChannelService { avatarService: AvatarService, virusScanService: IVirusScanService, purgeQueue: IPurgeQueue, - favoriteMemeRepository: IFavoriteMemeRepository, + favoriteMemeRepository: FavoriteMemeRepository, guildAuditLogService: GuildAuditLogService, voiceRoomStore: IVoiceRoomStore, liveKitService: ILiveKitService, - inviteRepository: IInviteRepository, - webhookRepository: IWebhookRepository, + inviteRepository: InviteRepository, + webhookRepository: WebhookRepository, limitConfigService: LimitConfigService, voiceAvailabilityService: VoiceAvailabilityService | null, ) { diff --git a/fluxer_api/src/api/channel/services/DMPermissionValidator.ts b/fluxer_api/src/api/channel/services/DMPermissionValidator.ts index f125da40a..29cd1deaf 100644 --- a/fluxer_api/src/api/channel/services/DMPermissionValidator.ts +++ b/fluxer_api/src/api/channel/services/DMPermissionValidator.ts @@ -2,11 +2,11 @@ import type {UserID} from '@app/api/BrandedTypes'; import {SYSTEM_USER_ID} from '@app/api/constants/Core'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {Guild} from '@app/api/models/Guild'; import type {User} from '@app/api/models/User'; import type {UserSettings} from '@app/api/models/UserSettings'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {isBugHunterBotUser} from '@app/api/user/UserHelpers'; import {checkGuildVerificationWithGuildModel} from '@app/api/utils/GuildVerificationUtils'; import {getMutualGuildsForDmAccess} from '@app/api/utils/MutualGuildDmAccess'; @@ -15,8 +15,8 @@ import {CannotSendMessagesToUserError} from '@fluxer/errors/src/domains/channel/ import {UnclaimedAccountCannotSendDirectMessagesError} from '@fluxer/errors/src/domains/channel/UnclaimedAccountCannotSendDirectMessagesError'; interface DMPermissionValidatorDeps { - userRepository: IUserRepository; - guildRepository: IGuildRepositoryAggregate; + userRepository: UserRepository; + guildRepository: GuildRepository; } export class DMPermissionValidator { diff --git a/fluxer_api/src/api/channel/services/MessageInteractionService.ts b/fluxer_api/src/api/channel/services/MessageInteractionService.ts index 4791f9a1c..8defdc4ff 100644 --- a/fluxer_api/src/api/channel/services/MessageInteractionService.ts +++ b/fluxer_api/src/api/channel/services/MessageInteractionService.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {ChannelID, MessageID, UserID} from '@app/api/BrandedTypes'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {AuthenticatedChannel} from '@app/api/channel/services/AuthenticatedChannel'; import {MessageInteractionAuthService} from '@app/api/channel/services/interaction/MessageInteractionAuthService'; import {MessagePinAuthService} from '@app/api/channel/services/interaction/MessagePinAuthService'; @@ -14,19 +14,19 @@ import {maskThreadArtifactsFor} from '@app/api/channel/services/message/ThreadMe import {assertThreadAllowed} from '@app/api/channel/services/thread/ThreadDenials'; import type {ThreadViewer} from '@app/api/experiment/ChannelThreadsGate'; import type {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService'; import type {LimitConfigService} from '@app/api/limits/LimitConfigService'; import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware'; import type {Channel} from '@app/api/models/Channel'; import type {Message} from '@app/api/models/Message'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; import { assertMayStartConversation, getNewConversationLimit, oneToOneDmRecipient, } from '@app/api/user/NewConversationLimit'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {assertGuildMemberCanCommunicate} from '@app/api/utils/GuildCommunicationUtils'; import {ChannelTypes, Permissions} from '@fluxer/constants/src/ChannelConstants'; import {threadWriteBlock} from '@fluxer/constants/src/ThreadPermissionUtils'; @@ -43,9 +43,9 @@ export class MessageInteractionService { private reactionService: MessageReactionService; constructor( - private channelRepository: IChannelRepository, - private userRepository: IUserRepository, - guildRepository: IGuildRepositoryAggregate, + private channelRepository: ChannelRepository, + private userRepository: UserRepository, + guildRepository: GuildRepository, private gatewayService: IGatewayService, snowflakeService: ISnowflakeService, messagePersistenceService: MessagePersistenceService, diff --git a/fluxer_api/src/api/channel/services/MessageService.ts b/fluxer_api/src/api/channel/services/MessageService.ts index acd684b3d..386571e1b 100644 --- a/fluxer_api/src/api/channel/services/MessageService.ts +++ b/fluxer_api/src/api/channel/services/MessageService.ts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {AttachmentUploadTraceRepository} from '@app/api/channel/repositories/message/AttachmentUploadTraceRepository'; import {CrosspostPropagation} from '@app/api/channel/services/message/CrosspostPropagation'; import {MessageAnonymizationService} from '@app/api/channel/services/message/MessageAnonymizationService'; @@ -21,9 +21,9 @@ import {MessageSystemService} from '@app/api/channel/services/message/MessageSys import {MessageValidationService} from '@app/api/channel/services/message/MessageValidationService'; import {MessageWriteLock} from '@app/api/channel/services/message/MessageWriteLock'; import {ThreadMessageActivity} from '@app/api/channel/services/message/ThreadMessageActivity'; -import type {IFavoriteMemeRepository} from '@app/api/favorite_meme/IFavoriteMemeRepository'; +import type {FavoriteMemeRepository} from '@app/api/favorite_meme/FavoriteMemeRepository'; import type {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {IPurgeQueue} from '@app/api/infrastructure/CachePurgeQueue'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {IMediaService} from '@app/api/infrastructure/IMediaService'; @@ -32,7 +32,7 @@ import type {IStorageService} from '@app/api/infrastructure/IStorageService'; import type {UserCacheService} from '@app/api/infrastructure/UserCacheService'; import type {LimitConfigService} from '@app/api/limits/LimitConfigService'; import type {ReadStateService} from '@app/api/read_state/ReadStateService'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import type {WorkerTaskName} from '@app/api/worker/WorkerLaneConfig'; import type {ICacheService} from '@pkgs/cache/src/ICacheService'; import type {IRateLimitService} from '@pkgs/rate_limit/src/IRateLimitService'; @@ -58,9 +58,9 @@ export class MessageService { public readonly threadActivity: ThreadMessageActivity; constructor( - channelRepository: IChannelRepositoryAggregate, - userRepository: IUserRepository, - guildRepository: IGuildRepositoryAggregate, + channelRepository: ChannelRepository, + userRepository: UserRepository, + guildRepository: GuildRepository, userCacheService: UserCacheService, readStateService: ReadStateService, cacheService: ICacheService, @@ -71,7 +71,7 @@ export class MessageService { snowflakeService: ISnowflakeService, rateLimitService: IRateLimitService, purgeQueue: IPurgeQueue, - favoriteMemeRepository: IFavoriteMemeRepository, + favoriteMemeRepository: FavoriteMemeRepository, guildAuditLogService: GuildAuditLogService, persistenceService: MessagePersistenceService, attachmentUploadTraceRepository: AttachmentUploadTraceRepository, diff --git a/fluxer_api/src/api/channel/services/channel_data/ChannelOperationsService.ts b/fluxer_api/src/api/channel/services/channel_data/ChannelOperationsService.ts index 90f8130d6..d23eae5b1 100644 --- a/fluxer_api/src/api/channel/services/channel_data/ChannelOperationsService.ts +++ b/fluxer_api/src/api/channel/services/channel_data/ChannelOperationsService.ts @@ -2,7 +2,7 @@ import type {ChannelID, GuildID, RoleID, UserID} from '@app/api/BrandedTypes'; import {createChannelID, createGuildID, createRoleID, createUserID} from '@app/api/BrandedTypes'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import { enqueueChannelFollowerRemoval, scheduleDeletedChannelFollowerRemoval, @@ -35,7 +35,7 @@ import { import {resolveExperimentTargeting} from '@app/api/experiment/ExperimentTargeting'; import type {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService'; import {mapGuildToGuildResponse} from '@app/api/guild/GuildModel'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import {ChannelHelpers} from '@app/api/guild/services/channel/ChannelHelpers'; import {createGuildMfaEnforcer} from '@app/api/guild/services/GuildMfaEnforcement'; import {hasThreadPermissionBits, resolveProtectedBitActor} from '@app/api/guild/services/ThreadPermissionBits'; @@ -44,7 +44,7 @@ import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {ILiveKitService} from '@app/api/infrastructure/ILiveKitService'; import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService'; import type {IVoiceRoomStore} from '@app/api/infrastructure/IVoiceRoomStore'; -import type {IInviteRepository} from '@app/api/invite/IInviteRepository'; +import type {InviteRepository} from '@app/api/invite/InviteRepository'; import {Logger} from '@app/api/Logger'; import type {LimitConfigService} from '@app/api/limits/LimitConfigService'; import {createLimitMatchContext} from '@app/api/limits/LimitMatchContextBuilder'; @@ -54,13 +54,13 @@ import type {Channel} from '@app/api/models/Channel'; import {ChannelPermissionOverwrite} from '@app/api/models/ChannelPermissionOverwrite'; import type {ThreadState} from '@app/api/models/ThreadState'; import {deleteChannelMessageSearchDocuments} from '@app/api/search/MessageSearchIndexCleanup'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {serializeChannelForAudit} from '@app/api/utils/AuditSerializationUtils'; import {applyProtectedOverwriteBits, permissionWriteMask, protectedThreadBits} from '@app/api/utils/featureUtils'; import {overwriteGrantedBits} from '@app/api/utils/PermissionUtils'; import type {VoiceAvailabilityService} from '@app/api/voice/VoiceAvailabilityService'; import type {VoiceRegionAvailability} from '@app/api/voice/VoiceModel'; -import type {IWebhookRepository} from '@app/api/webhook/IWebhookRepository'; +import type {WebhookRepository} from '@app/api/webhook/WebhookRepository'; import {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType'; import { ANNOUNCEMENT_CONVERTIBLE_CHANNEL_TYPES, @@ -123,8 +123,8 @@ function assertOverwriteTarget(channel: Channel, viewer: ThreadViewer | undefine export class ChannelOperationsService { constructor( - private channelRepository: IChannelRepositoryAggregate, - private userRepository: IUserRepository, + private channelRepository: ChannelRepository, + private userRepository: UserRepository, private gatewayService: IGatewayService, private channelAuthService: ChannelAuthService, private channelUtilsService: ChannelUtilsService, @@ -132,9 +132,9 @@ export class ChannelOperationsService { private liveKitService: ILiveKitService, private voiceAvailabilityService: VoiceAvailabilityService | null, private readonly guildAuditLogService: GuildAuditLogService, - private inviteRepository: IInviteRepository, - private webhookRepository: IWebhookRepository, - private guildRepository: IGuildRepositoryAggregate, + private inviteRepository: InviteRepository, + private webhookRepository: WebhookRepository, + private guildRepository: GuildRepository, private limitConfigService: LimitConfigService, private rateLimitService: IRateLimitService, private cacheService: ICacheService, diff --git a/fluxer_api/src/api/channel/services/channel_data/ChannelUtilsService.ts b/fluxer_api/src/api/channel/services/channel_data/ChannelUtilsService.ts index 1640c1322..d60f93997 100644 --- a/fluxer_api/src/api/channel/services/channel_data/ChannelUtilsService.ts +++ b/fluxer_api/src/api/channel/services/channel_data/ChannelUtilsService.ts @@ -2,7 +2,7 @@ import type {MessageID, UserID} from '@app/api/BrandedTypes'; import {mapChannelToResponse} from '@app/api/channel/ChannelMappers'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import {dispatchChannelEvent} from '@app/api/channel/services/ChannelGatewayDispatch'; import {dispatchMessageCreateBroadcast} from '@app/api/channel/services/message/MessageGatewayDispatch'; import {purgeMessageAttachments} from '@app/api/channel/services/message/MessageHelpers'; @@ -17,7 +17,7 @@ import type {Message} from '@app/api/models/Message'; export class ChannelUtilsService { constructor( - private channelRepository: IChannelRepositoryAggregate, + private channelRepository: ChannelRepository, private userCacheService: UserCacheService, private storageService: IStorageService, private gatewayService: IGatewayService, diff --git a/fluxer_api/src/api/channel/services/channel_data/GroupDmUpdateService.ts b/fluxer_api/src/api/channel/services/channel_data/GroupDmUpdateService.ts index 54e0f5956..cd8dd6975 100644 --- a/fluxer_api/src/api/channel/services/channel_data/GroupDmUpdateService.ts +++ b/fluxer_api/src/api/channel/services/channel_data/GroupDmUpdateService.ts @@ -2,7 +2,7 @@ import type {ChannelID, UserID} from '@app/api/BrandedTypes'; import {createMessageID} from '@app/api/BrandedTypes'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {ChannelUtilsService} from '@app/api/channel/services/channel_data/ChannelUtilsService'; import type {MessagePersistenceService} from '@app/api/channel/services/message/MessagePersistenceService'; import type {ChannelRow} from '@app/api/database/types/ChannelTypes'; @@ -11,7 +11,7 @@ import {contentModerationService} from '@app/api/infrastructure/ContentModeratio import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService'; import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware'; import type {Channel} from '@app/api/models/Channel'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {canUserAccessNsfwContent} from '@app/api/utils/AgeUtils'; import {ChannelTypes, MessageTypes} from '@fluxer/constants/src/ChannelConstants'; import {GroupDmMatureContentIneligibleError} from '@fluxer/errors/src/domains/channel/GroupDmMatureContentIneligibleError'; @@ -24,8 +24,8 @@ import {UnknownUserError} from '@fluxer/errors/src/domains/user/UnknownUserError export class GroupDmUpdateService { constructor( - private channelRepository: IChannelRepositoryAggregate, - private userRepository: IUserRepository, + private channelRepository: ChannelRepository, + private userRepository: UserRepository, private avatarService: AvatarService, private snowflakeService: ISnowflakeService, private channelUtilsService: ChannelUtilsService, diff --git a/fluxer_api/src/api/channel/services/group_dm/GroupDmOperationsService.ts b/fluxer_api/src/api/channel/services/group_dm/GroupDmOperationsService.ts index 127a98612..44b705470 100644 --- a/fluxer_api/src/api/channel/services/group_dm/GroupDmOperationsService.ts +++ b/fluxer_api/src/api/channel/services/group_dm/GroupDmOperationsService.ts @@ -4,11 +4,11 @@ import {randomInt} from 'node:crypto'; import type {ChannelID, UserID} from '@app/api/BrandedTypes'; import {createMessageID} from '@app/api/BrandedTypes'; import {mapChannelToResponse} from '@app/api/channel/ChannelMappers'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import {dispatchChannelDelete} from '@app/api/channel/services/group_dm/GroupDmHelpers'; import {dispatchMessageCreateBroadcast} from '@app/api/channel/services/message/MessageGatewayDispatch'; import type {MessagePersistenceService} from '@app/api/channel/services/message/MessagePersistenceService'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {ILiveKitService} from '@app/api/infrastructure/ILiveKitService'; import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService'; @@ -23,7 +23,7 @@ import type {Channel} from '@app/api/models/Channel'; import type {User} from '@app/api/models/User'; import {deleteChannelMessageSearchDocuments} from '@app/api/search/MessageSearchIndexCleanup'; import {assertAccountNotLimited} from '@app/api/user/AccountLimit'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {canUserAccessNsfwContent} from '@app/api/utils/AgeUtils'; import {UserPermissionUtils} from '@app/api/utils/UserPermissionUtils'; import {ChannelTypes, MessageTypes} from '@fluxer/constants/src/ChannelConstants'; @@ -44,9 +44,9 @@ export class GroupDmOperationsService { private readonly userPermissionUtils: UserPermissionUtils; constructor( - private channelRepository: IChannelRepositoryAggregate, - private userRepository: IUserRepository, - guildRepository: IGuildRepositoryAggregate, + private channelRepository: ChannelRepository, + private userRepository: UserRepository, + guildRepository: GuildRepository, private userCacheService: UserCacheService, private gatewayService: IGatewayService, private snowflakeService: ISnowflakeService, diff --git a/fluxer_api/src/api/channel/services/interaction/MessagePinService.test.ts b/fluxer_api/src/api/channel/services/interaction/MessagePinService.test.ts index 64f8cf2a9..055d69eb5 100644 --- a/fluxer_api/src/api/channel/services/interaction/MessagePinService.test.ts +++ b/fluxer_api/src/api/channel/services/interaction/MessagePinService.test.ts @@ -2,7 +2,7 @@ import type {ChannelID, MessageID} from '@app/api/BrandedTypes'; import {createChannelID, createGuildID, createMessageID, createUserID} from '@app/api/BrandedTypes'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {AuthenticatedChannel} from '@app/api/channel/services/AuthenticatedChannel'; import {MessagePinService} from '@app/api/channel/services/interaction/MessagePinService'; import type {MessagePersistenceService} from '@app/api/channel/services/message/MessagePersistenceService'; @@ -69,7 +69,7 @@ function createPinService({canReadHistory, pins}: {canReadHistory: boolean; pins .slice(0, limit); }, }, - } as unknown as IChannelRepositoryAggregate; + } as unknown as ChannelRepository; const channel = {id: CHANNEL_ID, type: ChannelTypes.GUILD_TEXT, guildId: GUILD_ID}; const authChannel: AuthenticatedChannel = { channel: channel as AuthenticatedChannel['channel'], diff --git a/fluxer_api/src/api/channel/services/interaction/MessagePinService.ts b/fluxer_api/src/api/channel/services/interaction/MessagePinService.ts index 296a30940..bf5a50e5d 100644 --- a/fluxer_api/src/api/channel/services/interaction/MessagePinService.ts +++ b/fluxer_api/src/api/channel/services/interaction/MessagePinService.ts @@ -2,7 +2,7 @@ import type {MessageID, UserID} from '@app/api/BrandedTypes'; import {createMessageID} from '@app/api/BrandedTypes'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {AuthenticatedChannel} from '@app/api/channel/services/AuthenticatedChannel'; import {dispatchChannelEvent} from '@app/api/channel/services/ChannelGatewayDispatch'; import {MessageInteractionBase} from '@app/api/channel/services/interaction/MessageInteractionBase'; @@ -35,7 +35,7 @@ const PIN_LIST_UNBOUNDED_TIMESTAMP = new Date('9999-12-31T23:59:59.999Z'); export class MessagePinService extends MessageInteractionBase { constructor( gatewayService: IGatewayService, - private channelRepository: IChannelRepositoryAggregate, + private channelRepository: ChannelRepository, private snowflakeService: ISnowflakeService, private messagePersistenceService: MessagePersistenceService, private readonly guildAuditLogService: GuildAuditLogService, diff --git a/fluxer_api/src/api/channel/services/interaction/MessageReactionService.ts b/fluxer_api/src/api/channel/services/interaction/MessageReactionService.ts index ff6c9b677..bfbbab273 100644 --- a/fluxer_api/src/api/channel/services/interaction/MessageReactionService.ts +++ b/fluxer_api/src/api/channel/services/interaction/MessageReactionService.ts @@ -2,12 +2,12 @@ import {requireEmailVerified} from '@app/api/auth/EmailVerificationUtils'; import {createEmojiID, type MessageID, type UserID} from '@app/api/BrandedTypes'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {AuthenticatedChannel} from '@app/api/channel/services/AuthenticatedChannel'; import {dispatchChannelEvent} from '@app/api/channel/services/ChannelGatewayDispatch'; import {MessageInteractionBase, type ParsedEmoji} from '@app/api/channel/services/interaction/MessageInteractionBase'; import {assertThreadInteractionAllowed} from '@app/api/channel/services/thread/ThreadInteractionGuards'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {LimitConfigService} from '@app/api/limits/LimitConfigService'; import {resolveLimitSafe} from '@app/api/limits/LimitConfigUtils'; @@ -16,7 +16,7 @@ import type {Channel} from '@app/api/models/Channel'; import type {MessageReaction} from '@app/api/models/MessageReaction'; import type {User} from '@app/api/models/User'; import {assertAccountNotLimited} from '@app/api/user/AccountLimit'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {mapUserToPartialResponse} from '@app/api/user/UserMappers'; import {assertGuildMemberCanCommunicate} from '@app/api/utils/GuildCommunicationUtils'; import {Permissions} from '@fluxer/constants/src/ChannelConstants'; @@ -40,9 +40,9 @@ const REACTION_CUSTOM_EMOJI_REGEX = /^(.+):(\d+)$/; export class MessageReactionService extends MessageInteractionBase { constructor( gatewayService: IGatewayService, - private channelRepository: IChannelRepositoryAggregate, - private userRepository: IUserRepository, - private guildRepository: IGuildRepositoryAggregate, + private channelRepository: ChannelRepository, + private userRepository: UserRepository, + private guildRepository: GuildRepository, private limitConfigService: LimitConfigService, ) { super(gatewayService); diff --git a/fluxer_api/src/api/channel/services/message/CrosspostDeliveryService.ts b/fluxer_api/src/api/channel/services/message/CrosspostDeliveryService.ts index df940fb85..942b32da5 100644 --- a/fluxer_api/src/api/channel/services/message/CrosspostDeliveryService.ts +++ b/fluxer_api/src/api/channel/services/message/CrosspostDeliveryService.ts @@ -4,7 +4,7 @@ import {createHash} from 'node:crypto'; import type {ChannelID, GuildID, MessageID, UserID, WebhookID} from '@app/api/BrandedTypes'; import {createMessageID, createUserID} from '@app/api/BrandedTypes'; import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; -import type {CrosspostedMessageKey} from '@app/api/channel/repositories/ICrosspostedMessageRepository'; +import type {CrosspostedMessageKey} from '@app/api/channel/repositories/CrosspostedMessageRepository'; import {dispatchChannelEvent} from '@app/api/channel/services/ChannelGatewayDispatch'; import { collectEmbedContentHashes, @@ -36,7 +36,7 @@ import type { MessageEmbedChild, MessageStickerItem, } from '@app/api/database/types/MessageTypes'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {AvatarService} from '@app/api/infrastructure/AvatarService'; import type {IPurgeQueue} from '@app/api/infrastructure/CachePurgeQueue'; import {contentModerationService, type ModerationContext} from '@app/api/infrastructure/ContentModerationService'; @@ -49,8 +49,8 @@ import type {Channel} from '@app/api/models/Channel'; import type {Message} from '@app/api/models/Message'; import type {Webhook} from '@app/api/models/Webhook'; import {deleteMessageSearchDocuments} from '@app/api/search/MessageSearchIndexCleanup'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; -import type {IWebhookRepository} from '@app/api/webhook/IWebhookRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; +import type {WebhookRepository} from '@app/api/webhook/WebhookRepository'; import { CROSSPOST_PENDING_RECLAIM_AFTER_MS, CROSSPOST_SOURCE_DELETED_CONTENT, @@ -109,9 +109,9 @@ type CrosspostCopyPayloadResult = {kind: 'blocked'} | {kind: 'ready'; payload: C export interface CrosspostDeliveryDeps { channelRepository: ChannelRepository; - webhookRepository: IWebhookRepository; - userRepository: IUserRepository; - guildRepository: IGuildRepositoryAggregate; + webhookRepository: WebhookRepository; + userRepository: UserRepository; + guildRepository: GuildRepository; gatewayService: IGatewayService; storageService: IStorageService; avatarService: AvatarService; @@ -222,7 +222,7 @@ export class CrosspostDeliveryService { async loadSource(channelId: ChannelID, messageId: MessageID): Promise { const message = await this.deps.channelRepository.messages.getMessage(channelId, messageId); if (!message) return null; - const channel = await this.deps.channelRepository.findUnique(channelId); + const channel = await this.deps.channelRepository.channelData.findUnique(channelId); if (!channel?.guildId) return null; const guild = await this.loadGuild(channel.guildId); if (!guild) return null; @@ -316,7 +316,7 @@ export class CrosspostDeliveryService { const copyChannel = existing.target_channel_id === target.channel.id ? target.channel - : await this.deps.channelRepository.findUnique(existing.target_channel_id); + : await this.deps.channelRepository.channelData.findUnique(existing.target_channel_id); if (copyChannel) { await this.announceCopy(copyChannel, copy); } @@ -370,7 +370,7 @@ export class CrosspostDeliveryService { sourceFingerprint: context.fingerprint, }); if (!marked) { - await this.deps.channelRepository.deleteMessage(target.channel.id, messageId, createUserID(0n)); + await this.deps.channelRepository.messages.deleteMessage(target.channel.id, messageId, createUserID(0n)); return 'skipped'; } await this.announceCopy(target.channel, copy); @@ -417,7 +417,7 @@ export class CrosspostDeliveryService { }); return message; } catch (error) { - await this.deps.channelRepository.deleteMessage(target.channel.id, messageId, createUserID(0n)); + await this.deps.channelRepository.messages.deleteMessage(target.channel.id, messageId, createUserID(0n)); await this.deps.channelRepository.crossposts.delete( {sourceMessageId: context.message.id, webhookId: webhook.id}, {state: 'pending', target_message_id: messageId}, @@ -604,7 +604,7 @@ export class CrosspostDeliveryService { }, ); if (!updated) return; - const channel = await this.deps.channelRepository.findUnique(row.target_channel_id); + const channel = await this.deps.channelRepository.channelData.findUnique(row.target_channel_id); if (!channel) return; await dispatchMessageUpdateBroadcast({gatewayService: this.deps.gatewayService, channel, message: updated}); if (channel.indexedAt != null) { @@ -663,7 +663,7 @@ export class CrosspostDeliveryService { return; } if (outcome.kind === 'marked') { - const channel = await this.deps.channelRepository.findUnique(row.target_channel_id); + const channel = await this.deps.channelRepository.channelData.findUnique(row.target_channel_id); if (channel) { await dispatchMessageUpdateBroadcast({ gatewayService: this.deps.gatewayService, @@ -686,7 +686,7 @@ export class CrosspostDeliveryService { row.target_message_id, async (fresh) => { if (!fresh) return null; - await this.deps.channelRepository.deleteMessage( + await this.deps.channelRepository.messages.deleteMessage( fresh.channelId, fresh.id, fresh.authorId ?? createUserID(0n), @@ -701,7 +701,7 @@ export class CrosspostDeliveryService { } return; } - const channel = await this.deps.channelRepository.findUnique(row.target_channel_id); + const channel = await this.deps.channelRepository.channelData.findUnique(row.target_channel_id); if (channel) { await dispatchChannelEvent({ gatewayService: this.deps.gatewayService, @@ -717,7 +717,7 @@ export class CrosspostDeliveryService { async deleteSourceMessage(channelId: ChannelID, messageId: MessageID): Promise { const removed = await this.writeLock.withFreshMessage(channelId, messageId, async (fresh) => { if (!fresh) return null; - await this.deps.channelRepository.deleteMessage( + await this.deps.channelRepository.messages.deleteMessage( channelId, messageId, fresh.authorId ?? createUserID(0n), @@ -727,7 +727,7 @@ export class CrosspostDeliveryService { }); if (!removed) return; await purgeMessageAttachments(removed, this.deps.storageService, this.deps.purgeQueue); - const channel = await this.deps.channelRepository.findUnique(channelId); + const channel = await this.deps.channelRepository.channelData.findUnique(channelId); if (channel) { await dispatchChannelEvent({ gatewayService: this.deps.gatewayService, @@ -877,7 +877,7 @@ export class CrosspostDeliveryService { } private async loadSyncTarget(channelId: ChannelID): Promise { - const channel = await this.deps.channelRepository.findUnique(channelId); + const channel = await this.deps.channelRepository.channelData.findUnique(channelId); if (!channel?.guildId) return null; const guild = await this.loadGuild(channel.guildId); if (!guild) return null; @@ -886,7 +886,7 @@ export class CrosspostDeliveryService { private async loadParent(channel: Channel): Promise { if (!channel.parentId || channel.type === ChannelTypes.GUILD_CATEGORY) return null; - return this.deps.channelRepository.findUnique(channel.parentId); + return this.deps.channelRepository.channelData.findUnique(channel.parentId); } private async loadGuild(guildId: GuildID): Promise { diff --git a/fluxer_api/src/api/channel/services/message/CrosspostSourceService.ts b/fluxer_api/src/api/channel/services/message/CrosspostSourceService.ts index 740282070..0c8bb8957 100644 --- a/fluxer_api/src/api/channel/services/message/CrosspostSourceService.ts +++ b/fluxer_api/src/api/channel/services/message/CrosspostSourceService.ts @@ -3,8 +3,8 @@ import {createGuildID, type GuildID} from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; import {mapGuildToPartialResponse} from '@app/api/guild/GuildModel'; -import type {IGuildDiscoveryRepository} from '@app/api/guild/repositories/GuildDiscoveryRepository'; -import type {IGuildDataRepository} from '@app/api/guild/repositories/IGuildDataRepository'; +import type {GuildDiscoveryRepository} from '@app/api/guild/repositories/GuildDiscoveryRepository'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import {Logger} from '@app/api/Logger'; import {MessageFlags, MessageTypes} from '@fluxer/constants/src/ChannelConstants'; @@ -45,8 +45,8 @@ function getCrosspostSourceGuildId(message: MessageResponse): GuildID | null { export class CrosspostSourceService { constructor( - private readonly guildRepository: IGuildDataRepository, - private readonly discoveryRepository: IGuildDiscoveryRepository, + private readonly guildRepository: GuildRepository, + private readonly discoveryRepository: GuildDiscoveryRepository, private readonly gatewayService: IGatewayService, private readonly cacheService: ICacheService, ) {} diff --git a/fluxer_api/src/api/channel/services/message/DmScopeUtils.ts b/fluxer_api/src/api/channel/services/message/DmScopeUtils.ts index 132414891..bae60de86 100644 --- a/fluxer_api/src/api/channel/services/message/DmScopeUtils.ts +++ b/fluxer_api/src/api/channel/services/message/DmScopeUtils.ts @@ -2,7 +2,7 @@ import type {UserID} from '@app/api/BrandedTypes'; import type {Channel} from '@app/api/models/Channel'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants'; export type DmSearchScope = 'all_dms' | 'open_dms'; @@ -10,7 +10,7 @@ export type DmSearchScope = 'all_dms' | 'open_dms'; interface DmScopeOptions { scope: DmSearchScope; userId: UserID; - userRepository: IUserRepository; + userRepository: UserRepository; includeChannel?: Channel | null; } diff --git a/fluxer_api/src/api/channel/services/message/MessageActivity.ts b/fluxer_api/src/api/channel/services/message/MessageActivity.ts index 37fb759ff..37b0d7760 100644 --- a/fluxer_api/src/api/channel/services/message/MessageActivity.ts +++ b/fluxer_api/src/api/channel/services/message/MessageActivity.ts @@ -8,7 +8,7 @@ import {getGuildRepository, getInviteRepository} from '@app/api/middleware/Servi import type {Channel} from '@app/api/models/Channel'; import type {Message} from '@app/api/models/Message'; import type {User} from '@app/api/models/User'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {findInvites} from '@app/api/utils/InviteUtils'; import {extractLinkHosts} from '@app/api/utils/UrlNormalizer'; import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants'; @@ -67,7 +67,7 @@ export interface MessageCreatedActivity { dmRecipientId: UserID | null; channelHadMessages: boolean; delivered: boolean; - userRepository: Pick; + userRepository: Pick; } function attachmentMeta(message: Message): Array { diff --git a/fluxer_api/src/api/channel/services/message/MessageAnonymizationService.ts b/fluxer_api/src/api/channel/services/message/MessageAnonymizationService.ts index 9b037249b..e428b9213 100644 --- a/fluxer_api/src/api/channel/services/message/MessageAnonymizationService.ts +++ b/fluxer_api/src/api/channel/services/message/MessageAnonymizationService.ts @@ -1,11 +1,11 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {MessageID, UserID} from '@app/api/BrandedTypes'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import {Logger} from '@app/api/Logger'; export class MessageAnonymizationService { - constructor(private channelRepository: IChannelRepositoryAggregate) {} + constructor(private channelRepository: ChannelRepository) {} async anonymizeMessagesByAuthor(originalAuthorId: UserID, newAuthorId: UserID): Promise { const CHUNK_SIZE = 100; diff --git a/fluxer_api/src/api/channel/services/message/MessageContentService.ts b/fluxer_api/src/api/channel/services/message/MessageContentService.ts index e867ed835..13d469c25 100644 --- a/fluxer_api/src/api/channel/services/message/MessageContentService.ts +++ b/fluxer_api/src/api/channel/services/message/MessageContentService.ts @@ -1,10 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {GuildID, UserID, WebhookID} from '@app/api/BrandedTypes'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {LimitConfigService} from '@app/api/limits/LimitConfigService'; import type {Channel} from '@app/api/models/Channel'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import * as EmojiUtils from '@app/api/utils/EmojiUtils'; import {ChannelTypes, GUILD_TEXT_BASED_CHANNEL_TYPES} from '@fluxer/constants/src/ChannelConstants'; import {GuildExplicitContentFilterTypes, GuildFeatures, GuildNSFWLevel} from '@fluxer/constants/src/GuildConstants'; @@ -20,8 +20,8 @@ export interface DmNsfwContext { export class MessageContentService { constructor( - private userRepository: IUserRepository, - private guildRepository: IGuildRepositoryAggregate, + private userRepository: UserRepository, + private guildRepository: GuildRepository, private limitConfigService: LimitConfigService, ) {} diff --git a/fluxer_api/src/api/channel/services/message/MessageCrosspostService.ts b/fluxer_api/src/api/channel/services/message/MessageCrosspostService.ts index 5f46037c0..2f1d691b7 100644 --- a/fluxer_api/src/api/channel/services/message/MessageCrosspostService.ts +++ b/fluxer_api/src/api/channel/services/message/MessageCrosspostService.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {ChannelID, MessageID, UserID} from '@app/api/BrandedTypes'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {AuthenticatedChannel} from '@app/api/channel/services/AuthenticatedChannel'; import {collectEmbedContentHashes} from '@app/api/channel/services/message/CrosspostEmbedObjects'; import { @@ -36,7 +36,7 @@ import type {GuildResponse} from '@fluxer/schema/src/domains/guild/GuildResponse import type {IRateLimitService} from '@pkgs/rate_limit/src/IRateLimitService'; interface MessageCrosspostServiceDeps { - channelRepository: IChannelRepositoryAggregate; + channelRepository: ChannelRepository; channelAuthService: MessageChannelAuthService; dispatchService: MessageDispatchService; rateLimitService: IRateLimitService; diff --git a/fluxer_api/src/api/channel/services/message/MessageDeleteService.ts b/fluxer_api/src/api/channel/services/message/MessageDeleteService.ts index 72ca1c55c..d0bb53ef3 100644 --- a/fluxer_api/src/api/channel/services/message/MessageDeleteService.ts +++ b/fluxer_api/src/api/channel/services/message/MessageDeleteService.ts @@ -2,7 +2,7 @@ import type {ChannelID, GuildID, MessageID, UserID} from '@app/api/BrandedTypes'; import {createMessageID, createUserID} from '@app/api/BrandedTypes'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {CrosspostPropagation} from '@app/api/channel/services/message/CrosspostPropagation'; import type {MessageChannelAuthService} from '@app/api/channel/services/message/MessageChannelAuthService'; import type {MessageDispatchService} from '@app/api/channel/services/message/MessageDispatchService'; @@ -37,7 +37,7 @@ import {createSnowflakeFromTimestamp} from '@fluxer/snowflake/src/Snowflake'; import {ms} from 'itty-time'; interface MessageDeleteServiceDeps { - channelRepository: IChannelRepositoryAggregate; + channelRepository: ChannelRepository; storageService: IStorageService; purgeQueue: IPurgeQueue; validationService: MessageValidationService; diff --git a/fluxer_api/src/api/channel/services/message/MessageEditService.ts b/fluxer_api/src/api/channel/services/message/MessageEditService.ts index 8721b83e5..ae672368f 100644 --- a/fluxer_api/src/api/channel/services/message/MessageEditService.ts +++ b/fluxer_api/src/api/channel/services/message/MessageEditService.ts @@ -1,8 +1,8 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {type ChannelID, createGuildID, createUserID, type MessageID, type UserID} from '@app/api/BrandedTypes'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {MessageUpdateRequest} from '@app/api/channel/MessageTypes'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; import type {AuthenticatedChannel} from '@app/api/channel/services/AuthenticatedChannel'; import type {CrosspostPropagation} from '@app/api/channel/services/message/CrosspostPropagation'; import {emitMessageUpdated} from '@app/api/channel/services/message/MessageActivity'; @@ -22,8 +22,8 @@ import {Logger} from '@app/api/Logger'; import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware'; import type {Message} from '@app/api/models/Message'; import {assertAccountNotLimited} from '@app/api/user/AccountLimit'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; import {assertMayStartConversation, oneToOneDmRecipient} from '@app/api/user/NewConversationLimit'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {isDirectDeliverySuppressed} from '@app/api/user/UserHelpers'; import {assertGuildMemberCanCommunicate} from '@app/api/utils/GuildCommunicationUtils'; import {Permissions} from '@fluxer/constants/src/ChannelConstants'; @@ -40,8 +40,8 @@ interface EditMessageResult { } interface MessageEditServiceDeps { - channelRepository: IChannelRepositoryAggregate; - userRepository: IUserRepository; + channelRepository: ChannelRepository; + userRepository: UserRepository; validationService: MessageValidationService; persistenceService: MessagePersistenceService; channelAuthService: MessageChannelAuthService; diff --git a/fluxer_api/src/api/channel/services/message/MessageHelpers.ts b/fluxer_api/src/api/channel/services/message/MessageHelpers.ts index 02e685f6f..3ed416ea3 100644 --- a/fluxer_api/src/api/channel/services/message/MessageHelpers.ts +++ b/fluxer_api/src/api/channel/services/message/MessageHelpers.ts @@ -3,7 +3,7 @@ import type {AttachmentID, ChannelID, MessageID, UserID} from '@app/api/BrandedTypes'; import {createAttachmentID, userIdToChannelId} from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import {forEachEmbedMedia} from '@app/api/channel/services/message/CrosspostEmbedObjects'; import type { MessageSnapshot as CassandraMessageSnapshot, @@ -479,7 +479,7 @@ export function countedThreadMessages(channel: Channel, messageIds: Array, ): Promise { diff --git a/fluxer_api/src/api/channel/services/message/MessageMentionService.ts b/fluxer_api/src/api/channel/services/message/MessageMentionService.ts index b0232fbcd..e3f8e53c8 100644 --- a/fluxer_api/src/api/channel/services/message/MessageMentionService.ts +++ b/fluxer_api/src/api/channel/services/message/MessageMentionService.ts @@ -11,12 +11,12 @@ import { } from '@app/api/BrandedTypes'; import {isOperationDisabled, isPersonalNotesChannel} from '@app/api/channel/services/message/MessageHelpers'; import type {MessageResponseDataService} from '@app/api/channel/services/message/MessageResponseDataService'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {GatewayChannelMention, IGatewayService} from '@app/api/infrastructure/IGatewayService'; import {Logger} from '@app/api/Logger'; import type {Channel} from '@app/api/models/Channel'; import type {Message} from '@app/api/models/Message'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import type {WorkerTaskName} from '@app/api/worker/WorkerLaneConfig'; import {WorkerQueueOverflowError} from '@app/api/worker/WorkerQueueOverflowError'; import {ChannelTypes, MessageTypes, SENDABLE_MESSAGE_FLAGS} from '@fluxer/constants/src/ChannelConstants'; @@ -55,8 +55,8 @@ interface MentionableUser { export class MessageMentionService { constructor( - private userRepository: IUserRepository, - private guildRepository: IGuildRepositoryAggregate, + private userRepository: UserRepository, + private guildRepository: GuildRepository, private gatewayService: IGatewayService, private workerService: IWorkerService, private responseDataService: MessageResponseDataService, diff --git a/fluxer_api/src/api/channel/services/message/MessageOperationsHelpers.ts b/fluxer_api/src/api/channel/services/message/MessageOperationsHelpers.ts index c40bc97c2..a07cae13f 100644 --- a/fluxer_api/src/api/channel/services/message/MessageOperationsHelpers.ts +++ b/fluxer_api/src/api/channel/services/message/MessageOperationsHelpers.ts @@ -3,10 +3,10 @@ import type {ChannelID, UserID} from '@app/api/BrandedTypes'; import {createAttachmentID, createChannelID, createMemeID, createMessageID} from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import {makeAttachmentCdnKey} from '@app/api/channel/services/message/MessageHelpers'; import type {MessageAttachment} from '@app/api/database/types/MessageTypes'; -import type {IFavoriteMemeRepository} from '@app/api/favorite_meme/IFavoriteMemeRepository'; +import type {FavoriteMemeRepository} from '@app/api/favorite_meme/FavoriteMemeRepository'; import type {IMediaService} from '@app/api/infrastructure/IMediaService'; import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService'; import type {IStorageService} from '@app/api/infrastructure/IStorageService'; @@ -22,11 +22,11 @@ import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidat import type {ICacheService} from '@pkgs/cache/src/ICacheService'; interface MessageOperationsHelpersDeps { - channelRepository: IChannelRepositoryAggregate; + channelRepository: ChannelRepository; cacheService: ICacheService; storageService: IStorageService; snowflakeService: ISnowflakeService; - favoriteMemeRepository: IFavoriteMemeRepository; + favoriteMemeRepository: FavoriteMemeRepository; mediaService: IMediaService; } diff --git a/fluxer_api/src/api/channel/services/message/MessagePersistenceService.ts b/fluxer_api/src/api/channel/services/message/MessagePersistenceService.ts index 79f2f4bb3..2c494218a 100644 --- a/fluxer_api/src/api/channel/services/message/MessagePersistenceService.ts +++ b/fluxer_api/src/api/channel/services/message/MessagePersistenceService.ts @@ -6,8 +6,8 @@ import type {ChannelID, GuildID, MessageID, RoleID, StickerID, UserID, WebhookID import {createAttachmentID, createGuildID} from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; import type {AttachmentToProcess} from '@app/api/channel/AttachmentDTOs'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {MessageUpdateRequest} from '@app/api/channel/MessageTypes'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; import type {AttachmentUploadTraceRepository} from '@app/api/channel/repositories/message/AttachmentUploadTraceRepository'; import {AttachmentProcessingService} from '@app/api/channel/services/message/AttachmentProcessingService'; import {type DmNsfwContext, MessageContentService} from '@app/api/channel/services/message/MessageContentService'; @@ -27,7 +27,7 @@ import type { MessageReference, MessageStickerItem, } from '@app/api/database/types/MessageTypes'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {EmbedService} from '@app/api/infrastructure/EmbedService'; import type {IMediaService, MediaProxyNsfwMode} from '@app/api/infrastructure/IMediaService'; import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService'; @@ -40,7 +40,7 @@ import type {Message} from '@app/api/models/Message'; import type {MessageSnapshot} from '@app/api/models/MessageSnapshot'; import type {User} from '@app/api/models/User'; import type {ReadStateService} from '@app/api/read_state/ReadStateService'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {hasVisibleContent} from '@app/api/utils/StringUtils'; import {MessageFlags, Permissions, SENDABLE_MESSAGE_FLAGS} from '@fluxer/constants/src/ChannelConstants'; import {ATTACHMENT_MAX_SIZE_NON_PREMIUM} from '@fluxer/constants/src/LimitConstants'; @@ -129,9 +129,9 @@ export class MessagePersistenceService { private readonly attachmentDecayService: AttachmentDecayService; constructor( - private channelRepository: IChannelRepositoryAggregate, - private userRepository: IUserRepository, - guildRepository: IGuildRepositoryAggregate, + private channelRepository: ChannelRepository, + private userRepository: UserRepository, + guildRepository: GuildRepository, private embedService: EmbedService, private readonly storageService: IStorageService, attachmentUploadTraceRepository: AttachmentUploadTraceRepository, diff --git a/fluxer_api/src/api/channel/services/message/MessageProcessingService.test.ts b/fluxer_api/src/api/channel/services/message/MessageProcessingService.test.ts index 88c3892d6..d69272d54 100644 --- a/fluxer_api/src/api/channel/services/message/MessageProcessingService.test.ts +++ b/fluxer_api/src/api/channel/services/message/MessageProcessingService.test.ts @@ -14,7 +14,7 @@ import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {UserCacheService} from '@app/api/infrastructure/UserCacheService'; import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware'; import {Channel} from '@app/api/models/Channel'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants'; import {describe, expect, it} from 'vitest'; @@ -62,7 +62,7 @@ function buildService(): {service: MessageProcessingService; opened: Array { opened.push(channel); }, - } as unknown as IUserRepository; + } as unknown as UserRepository; const userCacheService = { getUserPartialResponses: async (userIds: Array) => new Map(userIds.map((userId) => [userId, {id: userId.toString()}])), diff --git a/fluxer_api/src/api/channel/services/message/MessageProcessingService.ts b/fluxer_api/src/api/channel/services/message/MessageProcessingService.ts index cb55e4310..169894973 100644 --- a/fluxer_api/src/api/channel/services/message/MessageProcessingService.ts +++ b/fluxer_api/src/api/channel/services/message/MessageProcessingService.ts @@ -2,8 +2,8 @@ import {type ChannelID, createChannelID, createGuildID, type MessageID, type UserID} from '@app/api/BrandedTypes'; import {mapChannelToResponse} from '@app/api/channel/ChannelMappers'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {MessageRequest, MessageUpdateRequest} from '@app/api/channel/MessageTypes'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; import type {MessageDispatchService} from '@app/api/channel/services/message/MessageDispatchService'; import {isPersonalNotesChannel} from '@app/api/channel/services/message/MessageHelpers'; import type {MessageMentionService} from '@app/api/channel/services/message/MessageMentionService'; @@ -17,7 +17,7 @@ import {Channel} from '@app/api/models/Channel'; import type {Message} from '@app/api/models/Message'; import type {User} from '@app/api/models/User'; import type {ReadStateService} from '@app/api/read_state/ReadStateService'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {ChannelTypes, MessageFlags} from '@fluxer/constants/src/ChannelConstants'; import {CannotEditOtherUserMessageError} from '@fluxer/errors/src/domains/channel/CannotEditOtherUserMessageError'; import type {GuildResponse} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas'; @@ -42,8 +42,8 @@ function channelWithLastMessageId(channel: Channel, messageId: MessageID): Chann export class MessageProcessingService { constructor( - private channelRepository: IChannelRepositoryAggregate, - private userRepository: IUserRepository, + private channelRepository: ChannelRepository, + private userRepository: UserRepository, private userCacheService: UserCacheService, private gatewayService: IGatewayService, private readStateService: ReadStateService, diff --git a/fluxer_api/src/api/channel/services/message/MessageRequestParser.ts b/fluxer_api/src/api/channel/services/message/MessageRequestParser.ts index ba7322993..83a945d21 100644 --- a/fluxer_api/src/api/channel/services/message/MessageRequestParser.ts +++ b/fluxer_api/src/api/channel/services/message/MessageRequestParser.ts @@ -1,13 +1,12 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {ChannelID} from '@app/api/BrandedTypes'; -import {Config} from '@app/api/Config'; import { type AttachmentRequestData, mergeUploadWithClientData, type UploadedAttachment, } from '@app/api/channel/AttachmentDTOs'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {MessageRequest, MessageUpdateRequest} from '@app/api/channel/MessageTypes'; import {normalizeMessageRequestPayload} from '@app/api/channel/services/message/MessageRequestCompatibility'; import {SYSTEM_THREAD_VIEWER, viewerFromCtx} from '@app/api/experiment/ChannelThreadsGate'; @@ -18,11 +17,11 @@ import {createLimitMatchContext} from '@app/api/limits/LimitMatchContextBuilder' import type {User} from '@app/api/models/User'; import type {HonoEnv} from '@app/api/types/HonoEnv'; import {parseJsonPreservingLargeIntegers} from '@app/api/utils/LosslessJsonParser'; +import {requireRequestClientIp} from '@app/api/utils/RequestClientIp'; import {inputValidationErrorFromZodIssues} from '@app/api/Validator'; import {MAX_ATTACHMENTS_PER_MESSAGE} from '@fluxer/constants/src/LimitConstants'; import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes'; import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError'; -import {requireClientIp} from '@fluxer/ip_utils/src/ClientIp'; import type { ClientAttachmentReferenceRequest, ClientAttachmentRequest, @@ -145,10 +144,7 @@ export async function parseMultipartMessageData( existingAttachments.push(metadata); } if (filesWithIndices.length > 0) { - const clientIp = requireClientIp(ctx.req.raw, { - trustClientIpHeader: Config.proxy.trust_client_ip_header, - clientIpHeaderName: Config.proxy.client_ip_header, - }); + const clientIp = requireRequestClientIp(ctx); const inlineNewAttachments = filesWithIndices.map(({file, index}) => buildInlineAttachmentMetadata({ file, @@ -331,13 +327,13 @@ async function resolveMessageAttachmentLimit(ctx: Context, user: User, return MAX_ATTACHMENTS_PER_MESSAGE; } let guildFeatures: Iterable | null = null; - const channelRepository = ctx.get('channelRepository') as IChannelRepository | undefined; + const channelRepository = ctx.get('channelRepository') as ChannelRepository | undefined; const guildService = ctx.get('guildService') as GuildService | undefined; if (channelRepository) { try { - const channel = await channelRepository.findUnique(channelId); + const channel = await channelRepository.channelData.findUnique(channelId); if (channel?.guildId && guildService) { - const guild = await guildService.data.getGuildSystem(channel.guildId); + const guild = await guildService.data.operations.getGuildSystem(channel.guildId); guildFeatures = guild.features; } } catch { diff --git a/fluxer_api/src/api/channel/services/message/MessageRetrievalService.test.ts b/fluxer_api/src/api/channel/services/message/MessageRetrievalService.test.ts index ac834c608..a1324e5d0 100644 --- a/fluxer_api/src/api/channel/services/message/MessageRetrievalService.test.ts +++ b/fluxer_api/src/api/channel/services/message/MessageRetrievalService.test.ts @@ -3,7 +3,7 @@ import type {AttachmentDecayService} from '@app/api/attachment/AttachmentDecayService'; import type {ChannelID, MessageID} from '@app/api/BrandedTypes'; import {createChannelID, createMessageID, createUserID} from '@app/api/BrandedTypes'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {AuthenticatedChannel} from '@app/api/channel/services/AuthenticatedChannel'; import type {MessageChannelAuthService} from '@app/api/channel/services/message/MessageChannelAuthService'; import type {MessageProcessingService} from '@app/api/channel/services/message/MessageProcessingService'; @@ -12,7 +12,7 @@ import type {MessageSearchService} from '@app/api/channel/services/message/Messa import {SYSTEM_THREAD_VIEWER} from '@app/api/experiment/ChannelThreadsGate'; import type {UserCacheService} from '@app/api/infrastructure/UserCacheService'; import {Message} from '@app/api/models/Message'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {MessageTypes} from '@fluxer/constants/src/ChannelConstants'; import {createSnowflakeFromTimestamp} from '@fluxer/snowflake/src/Snowflake'; import {describe, expect, it} from 'vitest'; @@ -83,7 +83,7 @@ function createRetrievalService({ messages: { getMessage: async (_channelId: ChannelID, messageId: MessageID) => storedById.get(messageId.toString()) ?? null, }, - } as unknown as IChannelRepositoryAggregate; + } as unknown as ChannelRepository; const channelAuthService = { getChannelAuthenticated: async ({channelId}: {channelId: ChannelID}) => { authenticationCalls.push(channelId.toString()); @@ -102,7 +102,7 @@ function createRetrievalService({ channelAuthService, processingService, {} as unknown as MessageSearchService, - {} as unknown as IUserRepository, + {} as unknown as UserRepository, attachmentDecayService, ); return {service, permissionChecks, authenticationCalls}; diff --git a/fluxer_api/src/api/channel/services/message/MessageRetrievalService.ts b/fluxer_api/src/api/channel/services/message/MessageRetrievalService.ts index 78ffc1848..6d099dfc8 100644 --- a/fluxer_api/src/api/channel/services/message/MessageRetrievalService.ts +++ b/fluxer_api/src/api/channel/services/message/MessageRetrievalService.ts @@ -3,7 +3,7 @@ import {AttachmentDecayService} from '@app/api/attachment/AttachmentDecayService'; import type {AttachmentID, ChannelID, MessageID, UserID} from '@app/api/BrandedTypes'; import {mapChannelToResponse} from '@app/api/channel/ChannelMappers'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {AuthenticatedChannel} from '@app/api/channel/services/AuthenticatedChannel'; import {getDmChannelIdsForScope} from '@app/api/channel/services/message/DmScopeUtils'; import type {MessageChannelAuthService} from '@app/api/channel/services/message/MessageChannelAuthService'; @@ -29,7 +29,7 @@ import {getMessageSearchService} from '@app/api/SearchFactory'; import {buildMessageSearchFilters} from '@app/api/search/BuildMessageSearchFilters'; import {channelNeedsReindexing} from '@app/api/search/ChannelIndexingUtils'; import {searchExistingMessages} from '@app/api/search/MessageSearchResultReconciler'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {ChannelTypes, Permissions} from '@fluxer/constants/src/ChannelConstants'; import {UnknownMessageError} from '@fluxer/errors/src/domains/channel/UnknownMessageError'; import {FeatureTemporarilyDisabledError} from '@fluxer/errors/src/domains/core/FeatureTemporarilyDisabledError'; @@ -39,12 +39,12 @@ import {snowflakeToDate} from '@fluxer/snowflake/src/Snowflake'; export class MessageRetrievalService { constructor( - private channelRepository: IChannelRepositoryAggregate, + private channelRepository: ChannelRepository, private userCacheService: UserCacheService, private channelAuthService: MessageChannelAuthService, private processingService: MessageProcessingService, private searchService: MessageSearchService, - private userRepository: IUserRepository, + private userRepository: UserRepository, private attachmentDecayService: AttachmentDecayService = new AttachmentDecayService(), ) {} diff --git a/fluxer_api/src/api/channel/services/message/MessageSearchService.ts b/fluxer_api/src/api/channel/services/message/MessageSearchService.ts index dc8de634c..339edd68a 100644 --- a/fluxer_api/src/api/channel/services/message/MessageSearchService.ts +++ b/fluxer_api/src/api/channel/services/message/MessageSearchService.ts @@ -6,7 +6,7 @@ import type {Message} from '@app/api/models/Message'; import {getMessageSearchService} from '@app/api/SearchFactory'; import type {IMessageSearchService} from '@app/api/search/IMessageSearchService'; import {deleteMessageSearchDocuments} from '@app/api/search/MessageSearchIndexCleanup'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import type {WorkerTaskName} from '@app/api/worker/WorkerLaneConfig'; import {MessageTypes} from '@fluxer/constants/src/ChannelConstants'; import type {IWorkerService} from '@pkgs/worker/src/contracts/IWorkerService'; @@ -31,7 +31,7 @@ export function isMessageSearchIndexable(message: Message): boolean { export class MessageSearchService { constructor( - private userRepository: IUserRepository, + private userRepository: UserRepository, private workerService: IWorkerService, ) {} diff --git a/fluxer_api/src/api/channel/services/message/MessageSendService.ts b/fluxer_api/src/api/channel/services/message/MessageSendService.ts index 7f7336c8e..9d847d70a 100644 --- a/fluxer_api/src/api/channel/services/message/MessageSendService.ts +++ b/fluxer_api/src/api/channel/services/message/MessageSendService.ts @@ -12,8 +12,8 @@ import { } from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; import type {AttachmentRequestData, AttachmentToProcess} from '@app/api/channel/AttachmentDTOs'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {MessageRequest, MessageUpdateRequest} from '@app/api/channel/MessageTypes'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; import type {AttachmentUploadTraceRepository} from '@app/api/channel/repositories/message/AttachmentUploadTraceRepository'; import type {AuthenticatedChannel} from '@app/api/channel/services/AuthenticatedChannel'; import type {CrosspostPropagation} from '@app/api/channel/services/message/CrosspostPropagation'; @@ -41,7 +41,7 @@ import {enqueueThreadSearchSync} from '@app/api/channel/threads/ThreadJobs'; import {SYSTEM_USER_ID} from '@app/api/constants/Core'; import type {MessageAttachment, MessageReference} from '@app/api/database/types/MessageTypes'; import type {ThreadViewer} from '@app/api/experiment/ChannelThreadsGate'; -import type {IFavoriteMemeRepository} from '@app/api/favorite_meme/IFavoriteMemeRepository'; +import type {FavoriteMemeRepository} from '@app/api/favorite_meme/FavoriteMemeRepository'; import type {GatewayChannelMention, IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService'; import type {IStorageService} from '@app/api/infrastructure/IStorageService'; @@ -54,8 +54,8 @@ import type {MessageSnapshot} from '@app/api/models/MessageSnapshot'; import type {User} from '@app/api/models/User'; import type {Webhook} from '@app/api/models/Webhook'; import {assertAccountNotLimited} from '@app/api/user/AccountLimit'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; import {assertMayStartConversation} from '@app/api/user/NewConversationLimit'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {isContentHidden, isDirectDeliverySuppressed} from '@app/api/user/UserHelpers'; import {assertGuildMemberCanCommunicate} from '@app/api/utils/GuildCommunicationUtils'; import { @@ -83,13 +83,13 @@ import type {IRateLimitService} from '@pkgs/rate_limit/src/IRateLimitService'; interface MessageSendServiceDeps { threadActivity: ThreadMessageActivity; - channelRepository: IChannelRepositoryAggregate; - userRepository: IUserRepository; + channelRepository: ChannelRepository; + userRepository: UserRepository; storageService: IStorageService; gatewayService: IGatewayService; snowflakeService: ISnowflakeService; rateLimitService: IRateLimitService; - favoriteMemeRepository: IFavoriteMemeRepository; + favoriteMemeRepository: FavoriteMemeRepository; validationService: MessageValidationService; mentionService: MessageMentionService; searchService: MessageSearchService; diff --git a/fluxer_api/src/api/channel/services/message/MessageStickerService.ts b/fluxer_api/src/api/channel/services/message/MessageStickerService.ts index 5194652fe..4d729e3ca 100644 --- a/fluxer_api/src/api/channel/services/message/MessageStickerService.ts +++ b/fluxer_api/src/api/channel/services/message/MessageStickerService.ts @@ -2,11 +2,11 @@ import type {GuildID, StickerID, UserID} from '@app/api/BrandedTypes'; import type {MessageStickerItem} from '@app/api/database/types/MessageTypes'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {LimitConfigService} from '@app/api/limits/LimitConfigService'; import {resolveLimitSafe} from '@app/api/limits/LimitConfigUtils'; import {createLimitMatchContext} from '@app/api/limits/LimitMatchContextBuilder'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {Permissions} from '@fluxer/constants/src/ChannelConstants'; import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes'; import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError'; @@ -14,8 +14,8 @@ import {MissingPermissionsError} from '@fluxer/errors/src/domains/core/MissingPe export class MessageStickerService { constructor( - private userRepository: IUserRepository, - private guildRepository: IGuildRepositoryAggregate, + private userRepository: UserRepository, + private guildRepository: GuildRepository, private readonly limitConfigService: LimitConfigService, ) {} diff --git a/fluxer_api/src/api/channel/services/message/MessageSystemService.ts b/fluxer_api/src/api/channel/services/message/MessageSystemService.ts index adce38800..34ac9fb9b 100644 --- a/fluxer_api/src/api/channel/services/message/MessageSystemService.ts +++ b/fluxer_api/src/api/channel/services/message/MessageSystemService.ts @@ -2,18 +2,18 @@ import type {GuildID, UserID} from '@app/api/BrandedTypes'; import {createMessageID} from '@app/api/BrandedTypes'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {MessageDispatchService} from '@app/api/channel/services/message/MessageDispatchService'; import type {MessagePersistenceService} from '@app/api/channel/services/message/MessagePersistenceService'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService'; import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware'; import {MessageTypes} from '@fluxer/constants/src/ChannelConstants'; export class MessageSystemService { constructor( - private channelRepository: IChannelRepositoryAggregate, - private guildRepository: IGuildRepositoryAggregate, + private channelRepository: ChannelRepository, + private guildRepository: GuildRepository, private snowflakeService: ISnowflakeService, private persistenceService: MessagePersistenceService, private dispatchService: MessageDispatchService, diff --git a/fluxer_api/src/api/channel/services/message/ReadStateHelpers.ts b/fluxer_api/src/api/channel/services/message/ReadStateHelpers.ts index 1878b53d1..f98505699 100644 --- a/fluxer_api/src/api/channel/services/message/ReadStateHelpers.ts +++ b/fluxer_api/src/api/channel/services/message/ReadStateHelpers.ts @@ -3,12 +3,12 @@ import type {ChannelID, MessageID} from '@app/api/BrandedTypes'; import type {User} from '@app/api/models/User'; import type {ReadStateService} from '@app/api/read_state/ReadStateService'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {RelationshipTypes} from '@fluxer/constants/src/UserConstants'; interface IncrementDmMentionCountsParams { readStateService: ReadStateService; - userRepository: Pick; + userRepository: Pick; user: User | null; recipients: Array; channelId: ChannelID; diff --git a/fluxer_api/src/api/channel/services/message/ThreadMessageActivity.ts b/fluxer_api/src/api/channel/services/message/ThreadMessageActivity.ts index 7d6dc3b08..84bfc5934 100644 --- a/fluxer_api/src/api/channel/services/message/ThreadMessageActivity.ts +++ b/fluxer_api/src/api/channel/services/message/ThreadMessageActivity.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {UserID} from '@app/api/BrandedTypes'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import { dispatchThreadEvents, type ThreadDispatchEvent, @@ -17,7 +17,7 @@ import {getKVThreadAutoArchiveQueue} from '@app/api/middleware/ServiceSingletons import type {Channel} from '@app/api/models/Channel'; import type {ThreadMember} from '@app/api/models/ThreadMember'; import type {ThreadState} from '@app/api/models/ThreadState'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {addThreadMembersWithinCap, threadRecipients} from '@app/api/worker/tasks/ThreadMentionScope'; import {Permissions} from '@fluxer/constants/src/ChannelConstants'; import { @@ -34,9 +34,9 @@ const INTERACTED_CAS_ATTEMPTS = 3; export class ThreadMessageActivity { constructor( - private readonly channelRepository: IChannelRepositoryAggregate, + private readonly channelRepository: ChannelRepository, private readonly gatewayService: IGatewayService, - private readonly userRepository: IUserRepository, + private readonly userRepository: UserRepository, ) {} async assertCanUnarchive(state: ThreadState): Promise { diff --git a/fluxer_api/src/api/channel/services/message/ThreadMessageResponses.ts b/fluxer_api/src/api/channel/services/message/ThreadMessageResponses.ts index 0d2e5ec3e..afebd7656 100644 --- a/fluxer_api/src/api/channel/services/message/ThreadMessageResponses.ts +++ b/fluxer_api/src/api/channel/services/message/ThreadMessageResponses.ts @@ -2,7 +2,7 @@ import type {ChannelID, GuildID, MessageID, UserID} from '@app/api/BrandedTypes'; import {channelIdToMessageId, createChannelID, createMessageID} from '@app/api/BrandedTypes'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {AuthenticatedChannel} from '@app/api/channel/services/AuthenticatedChannel'; import { type MessageResponseDataService, @@ -104,7 +104,7 @@ export function maskThreadArtifactsByChannel( export class ThreadMessageResponses { constructor( - private readonly channelRepository: IChannelRepositoryAggregate, + private readonly channelRepository: ChannelRepository, private readonly responseDataService: MessageResponseDataService, private readonly userCacheService: UserCacheService, ) {} diff --git a/fluxer_api/src/api/channel/services/message/UserMessageDeletionService.ts b/fluxer_api/src/api/channel/services/message/UserMessageDeletionService.ts index 0b9fda8df..28c0ec107 100644 --- a/fluxer_api/src/api/channel/services/message/UserMessageDeletionService.ts +++ b/fluxer_api/src/api/channel/services/message/UserMessageDeletionService.ts @@ -2,7 +2,7 @@ import type {ChannelID, GuildID, MessageID, UserID} from '@app/api/BrandedTypes'; import {createChannelID} from '@app/api/BrandedTypes'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import { type CrosspostWorkerService, enqueueCrosspostSourceRemoval, @@ -26,7 +26,7 @@ import {ChannelEventDispatcher} from '@app/api/worker/services/ChannelEventDispa import {snowflakeToDate} from '@fluxer/snowflake/src/Snowflake'; interface UserMessageDeletionServiceDeps { - channelRepository: IChannelRepository; + channelRepository: ChannelRepository; gatewayService: IGatewayService; storageService: IStorageService; purgeQueue: IPurgeQueue; @@ -125,7 +125,7 @@ export class UserMessageDeletionService { const channelEligibility = new Map(); let lastMessageId: MessageID | undefined; while (true) { - const messageRefs = await this.deps.channelRepository.listMessagesByAuthor( + const messageRefs = await this.deps.channelRepository.messages.listMessagesByAuthor( userId, this.FETCH_BATCH_SIZE, lastMessageId, @@ -141,14 +141,14 @@ export class UserMessageDeletionService { const channelIdStr = channelId.toString(); let eligible = channelEligibility.get(channelIdStr); if (eligible === undefined) { - const channel = await this.deps.channelRepository.findUnique(channelId); + const channel = await this.deps.channelRepository.channelData.findUnique(channelId); eligible = channel ? isChannelEligible(channel, userId, filter, context) : false; channelEligibility.set(channelIdStr, eligible); } if (!eligible) { continue; } - const message = await this.deps.channelRepository.getMessage(channelId, messageId); + const message = await this.deps.channelRepository.messages.getMessage(channelId, messageId); if (message && message.authorId === userId) { if (!messagesByChannel.has(channelIdStr)) { messagesByChannel.set(channelIdStr, []); @@ -191,7 +191,7 @@ export class UserMessageDeletionService { const messagesByChannel = new Map>(); let lastMessageId: MessageID | undefined; while (true) { - const messageRefs = await this.deps.channelRepository.listMessagesByAuthor( + const messageRefs = await this.deps.channelRepository.messages.listMessagesByAuthor( userId, this.FETCH_BATCH_SIZE, lastMessageId, @@ -207,7 +207,7 @@ export class UserMessageDeletionService { if (messageTimestamp > beforeTimestamp) { continue; } - const message = await this.deps.channelRepository.getMessage(channelId, messageId); + const message = await this.deps.channelRepository.messages.getMessage(channelId, messageId); if (message && message.authorId === userId) { const channelIdStr = channelId.toString(); if (!messagesByChannel.has(channelIdStr)) { @@ -226,7 +226,7 @@ export class UserMessageDeletionService { return 0; } const channelId = createChannelID(BigInt(channelIdStr)); - const channel = await this.deps.channelRepository.findUnique(channelId); + const channel = await this.deps.channelRepository.channelData.findUnique(channelId); if (!channel) { Logger.debug({channelId: channelIdStr}, 'Channel not found, skipping messages'); return 0; @@ -241,7 +241,7 @@ export class UserMessageDeletionService { purgeMessageAttachments(message, this.deps.storageService, this.deps.purgeQueue), ), ); - await this.deps.channelRepository.bulkDeleteMessages(channelId, messageIds); + await this.deps.channelRepository.messages.bulkDeleteMessages(channelId, messageIds); await decrementThreadMessageCount(this.deps.channelRepository, channel, messageIds); await this.eventDispatcher.dispatchBulkDelete(channel, messageIds); await enqueueCrosspostSourceRemoval(this.deps.workerService, { diff --git a/fluxer_api/src/api/channel/services/tests/ChannelAuthThreadGate.test.ts b/fluxer_api/src/api/channel/services/tests/ChannelAuthThreadGate.test.ts index b1d8c4fda..93bd0f58b 100644 --- a/fluxer_api/src/api/channel/services/tests/ChannelAuthThreadGate.test.ts +++ b/fluxer_api/src/api/channel/services/tests/ChannelAuthThreadGate.test.ts @@ -1,17 +1,17 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {createChannelID, createGuildID, createUserID} from '@app/api/BrandedTypes'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import {ChannelAuthService} from '@app/api/channel/services/channel_data/ChannelAuthService'; import { SYSTEM_THREAD_VIEWER, syncChannelThreadsConfig, type ThreadViewer, } from '@app/api/experiment/ChannelThreadsGate'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {Channel} from '@app/api/models/Channel'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants'; import {UnknownChannelError} from '@fluxer/errors/src/domains/channel/UnknownChannelError'; import {ChannelThreadsConfigSchema} from '@fluxer/schema/src/domains/admin/ChannelThreadsSchemas'; @@ -27,7 +27,7 @@ function createService(type: number): ChannelAuthService { const channelRepository = { channelData: {findUnique: async () => channel}, threads: {getState: async () => null, getMember: async () => null}, - } as unknown as IChannelRepositoryAggregate; + } as unknown as ChannelRepository; const gatewayService = { getGuildAuthContext: async () => { throw REACHED_GATEWAY; @@ -36,12 +36,7 @@ function createService(type: number): ChannelAuthService { throw REACHED_GATEWAY; }, } as unknown as IGatewayService; - return new ChannelAuthService( - channelRepository, - {} as IUserRepository, - {} as IGuildRepositoryAggregate, - gatewayService, - ); + return new ChannelAuthService(channelRepository, {} as UserRepository, {} as GuildRepository, gatewayService); } function enroll(): void { diff --git a/fluxer_api/src/api/channel/services/thread/ThreadDeletionService.ts b/fluxer_api/src/api/channel/services/thread/ThreadDeletionService.ts index ace9e233a..5a96958a0 100644 --- a/fluxer_api/src/api/channel/services/thread/ThreadDeletionService.ts +++ b/fluxer_api/src/api/channel/services/thread/ThreadDeletionService.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {createMessageID, type UserID} from '@app/api/BrandedTypes'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import {buildBroadcastMessageData} from '@app/api/channel/services/message/MessageGatewayDispatch'; import {MessageWriteLock} from '@app/api/channel/services/message/MessageWriteLock'; import { @@ -26,7 +26,7 @@ import {InvalidChannelTypeError} from '@fluxer/errors/src/domains/channel/Invali import type {ICacheService} from '@pkgs/cache/src/ICacheService'; export async function clearThreadSourceFlag( - channelRepository: IChannelRepositoryAggregate, + channelRepository: ChannelRepository, cacheService: ICacheService, state: ThreadState, parent?: Channel | null, diff --git a/fluxer_api/src/api/channel/services/thread/ThreadModifyService.ts b/fluxer_api/src/api/channel/services/thread/ThreadModifyService.ts index d62465c5a..4674137f1 100644 --- a/fluxer_api/src/api/channel/services/thread/ThreadModifyService.ts +++ b/fluxer_api/src/api/channel/services/thread/ThreadModifyService.ts @@ -1,8 +1,8 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {createMessageID, type UserID} from '@app/api/BrandedTypes'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; -import type {ThreadStatePatch} from '@app/api/channel/repositories/IThreadRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; +import type {ThreadStatePatch} from '@app/api/channel/repositories/ThreadRepository'; import type {AuthenticatedChannel, AuthenticatedThread} from '@app/api/channel/services/AuthenticatedChannel'; import type {ChannelUtilsService} from '@app/api/channel/services/channel_data/ChannelUtilsService'; import type {MessagePersistenceService} from '@app/api/channel/services/message/MessagePersistenceService'; @@ -55,7 +55,7 @@ import type {ICacheService} from '@pkgs/cache/src/ICacheService'; import type {IRateLimitService} from '@pkgs/rate_limit/src/IRateLimitService'; interface ThreadModifyServiceDeps { - channelRepository: IChannelRepositoryAggregate; + channelRepository: ChannelRepository; gatewayService: IGatewayService; guildAuditLogService: GuildAuditLogService; rateLimitService: IRateLimitService; @@ -92,7 +92,7 @@ function requiresModerator(patch: ThreadPatch, thread: AuthenticatedThread): boo export class ThreadModifyService { constructor(private readonly deps: ThreadModifyServiceDeps) {} - get repository(): IChannelRepositoryAggregate { + get repository(): ChannelRepository { return this.deps.channelRepository; } diff --git a/fluxer_api/src/api/channel/services/thread/ThreadParentConversion.ts b/fluxer_api/src/api/channel/services/thread/ThreadParentConversion.ts index 4a2562b58..650953829 100644 --- a/fluxer_api/src/api/channel/services/thread/ThreadParentConversion.ts +++ b/fluxer_api/src/api/channel/services/thread/ThreadParentConversion.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {ChannelID, GuildID} from '@app/api/BrandedTypes'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import {type ThreadDispatchEvent, threadUpdateEvent} from '@app/api/channel/services/thread/ThreadDispatch'; import {loadThreadViews} from '@app/api/channel/services/thread/ThreadViews'; import {everEnabled, isTainted} from '@app/api/experiment/ChannelThreadsGate'; @@ -19,7 +19,7 @@ export const PARENT_CONVERSION_LOCK_TTL_SECONDS = 30; export type ParentThreads = Array<{threadId: ChannelID; type: number}>; export async function loadConvertibleParentThreads( - channelRepository: IChannelRepositoryAggregate, + channelRepository: ChannelRepository, guildId: GuildID, parent: Channel, nextType: number, @@ -36,7 +36,7 @@ export async function loadConvertibleParentThreads( } export async function retypeParentThreads( - channelRepository: IChannelRepositoryAggregate, + channelRepository: ChannelRepository, threads: ParentThreads, parentType: number, writeParent: () => Promise, @@ -63,7 +63,7 @@ export async function retypeParentThreads( } export async function retypedThreadEvents( - channelRepository: IChannelRepositoryAggregate, + channelRepository: ChannelRepository, parent: Channel, active: Array, ): Promise> { diff --git a/fluxer_api/src/api/channel/services/thread/ThreadParentSettings.ts b/fluxer_api/src/api/channel/services/thread/ThreadParentSettings.ts index f924f0b1f..b47675c31 100644 --- a/fluxer_api/src/api/channel/services/thread/ThreadParentSettings.ts +++ b/fluxer_api/src/api/channel/services/thread/ThreadParentSettings.ts @@ -1,10 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {createEmojiID, type GuildID} from '@app/api/BrandedTypes'; -import type {IThreadRepository, ThreadParentConfigPatch} from '@app/api/channel/repositories/IThreadRepository'; +import type {ThreadParentConfigPatch, ThreadRepository} from '@app/api/channel/repositories/ThreadRepository'; import type {ForumTagUdt} from '@app/api/database/types/ThreadTypes'; import {guildActive, type ThreadViewer, viewerActive} from '@app/api/experiment/ChannelThreadsGate'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {Channel} from '@app/api/models/Channel'; import type {ThreadParentConfig} from '@app/api/models/ThreadParentConfig'; import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants'; @@ -88,7 +88,7 @@ function assertSingleEmoji( } async function assertGuildEmoji( - guildRepository: IGuildRepositoryAggregate, + guildRepository: GuildRepository, guildId: GuildID, emojiId: bigint | null | undefined, ): Promise { @@ -101,7 +101,7 @@ async function buildTags(params: { input: Array; current: ReadonlyArray; guildId: GuildID; - guildRepository: IGuildRepositoryAggregate; + guildRepository: GuildRepository; generateId: () => Promise; }): Promise> { if (params.input.length > MAX_FORUM_TAGS_PER_CHANNEL) throw new MaxForumTagsError(MAX_FORUM_TAGS_PER_CHANNEL); @@ -135,7 +135,7 @@ export async function buildThreadParentPatch(params: { guildId: GuildID; input: ThreadParentSettingsInput; current: ThreadParentConfig | null; - guildRepository: IGuildRepositoryAggregate; + guildRepository: GuildRepository; generateId: () => Promise; }): Promise { const {input, current} = params; @@ -235,7 +235,7 @@ function isThreadParentInGuild(channel: Channel): channel is Channel & {guildId: } export async function loadThreadParentConfig( - threads: IThreadRepository, + threads: ThreadRepository, channel: Channel, ): Promise { if (!isThreadParentInGuild(channel) || !guildActive(channel.guildId)) return null; @@ -243,7 +243,7 @@ export async function loadThreadParentConfig( } export async function withThreadParentFields( - threads: IThreadRepository, + threads: ThreadRepository, channel: Channel, response: ChannelResponse, viewer?: ThreadViewer, @@ -255,7 +255,7 @@ export async function withThreadParentFields( } export async function withThreadParentFieldsMany( - threads: IThreadRepository, + threads: ThreadRepository, guildId: GuildID, channels: ReadonlyArray, responses: Array, diff --git a/fluxer_api/src/api/channel/services/thread/ThreadRpcData.ts b/fluxer_api/src/api/channel/services/thread/ThreadRpcData.ts index 3a184fe4e..f5b4a6e82 100644 --- a/fluxer_api/src/api/channel/services/thread/ThreadRpcData.ts +++ b/fluxer_api/src/api/channel/services/thread/ThreadRpcData.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {ChannelID, GuildID, MessageID, UserID} from '@app/api/BrandedTypes'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import {mapThreadMemberToRpcResponse, mapThreadToRpcResponse} from '@app/api/channel/services/thread/ThreadMappers'; import {mapThreadParentFields} from '@app/api/channel/services/thread/ThreadParentSettings'; import {loadThreadViews} from '@app/api/channel/services/thread/ThreadViews'; @@ -49,10 +49,7 @@ function mapParentSettingsToRpc( return {channel_id: parent.id.toString(), ...mapThreadParentFields(parent.type, config)}; } -async function listAllMembers( - repository: IChannelRepositoryAggregate, - threadId: ChannelID, -): Promise> { +async function listAllMembers(repository: ChannelRepository, threadId: ChannelID): Promise> { const members: Array = []; let after: UserID | undefined; while (members.length < MAX_THREAD_MEMBERS * 2) { @@ -65,7 +62,7 @@ async function listAllMembers( } export async function loadThreadCollectionRpcData( - repository: IChannelRepositoryAggregate, + repository: ChannelRepository, guildId: GuildID, parents: ReadonlyArray, {members: includeMembers}: {members: boolean}, @@ -92,7 +89,7 @@ export async function loadThreadCollectionRpcData( } export async function loadActiveThreadMemberships( - repository: IChannelRepositoryAggregate, + repository: ChannelRepository, guildId: GuildID, userId: UserID, ): Promise> { @@ -105,7 +102,7 @@ export async function loadActiveThreadMemberships( } export async function listThreadMembersPage( - repository: IChannelRepositoryAggregate, + repository: ChannelRepository, params: {guildId: GuildID; threadIds: Array; limit: number; after?: UserID}, ): Promise<{ members: Array; @@ -149,7 +146,7 @@ export interface ForumUnreadRpcEntry { } export async function loadForumUnreads( - repository: IChannelRepositoryAggregate, + repository: ChannelRepository, params: { guildId: GuildID; channelId: ChannelID; diff --git a/fluxer_api/src/api/channel/services/thread/ThreadServiceContext.ts b/fluxer_api/src/api/channel/services/thread/ThreadServiceContext.ts index 20afca8ce..83bac3653 100644 --- a/fluxer_api/src/api/channel/services/thread/ThreadServiceContext.ts +++ b/fluxer_api/src/api/channel/services/thread/ThreadServiceContext.ts @@ -1,8 +1,8 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {ChannelID, GuildID, UserID} from '@app/api/BrandedTypes'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {MessageRequest} from '@app/api/channel/MessageTypes'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; import type {AuthenticatedChannel} from '@app/api/channel/services/AuthenticatedChannel'; import type {BaseChannelAuthService} from '@app/api/channel/services/BaseChannelAuthService'; import type {MessagePersistenceService} from '@app/api/channel/services/message/MessagePersistenceService'; @@ -11,7 +11,7 @@ import type {ThreadViewer} from '@app/api/experiment/ChannelThreadsGate'; import type {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService'; import type {GuildAuditLogChange} from '@app/api/guild/GuildAuditLogTypes'; import {isGuildMemberTimedOut} from '@app/api/guild/GuildModel'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService'; import type {KVThreadAutoArchiveQueueService} from '@app/api/infrastructure/KVThreadAutoArchiveQueueService'; @@ -21,7 +21,7 @@ import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware'; import type {Channel} from '@app/api/models/Channel'; import type {User} from '@app/api/models/User'; import type {ReadStateService} from '@app/api/read_state/ReadStateService'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import type {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType'; import type {ThreadActor} from '@fluxer/constants/src/ThreadPermissionUtils'; import type {MessageResponse} from '@fluxer/schema/src/domains/message/MessageResponseSchemas'; @@ -29,9 +29,9 @@ import type {ICacheService} from '@pkgs/cache/src/ICacheService'; import type {IRateLimitService} from '@pkgs/rate_limit/src/IRateLimitService'; export interface ThreadServiceDeps { - channelRepository: IChannelRepositoryAggregate; - guildRepository: IGuildRepositoryAggregate; - userRepository: IUserRepository; + channelRepository: ChannelRepository; + guildRepository: GuildRepository; + userRepository: UserRepository; channelAuth: BaseChannelAuthService; gatewayService: IGatewayService; snowflakeService: ISnowflakeService; @@ -58,9 +58,9 @@ export interface ForumStarterParams { } export class ThreadServiceContext { - readonly channelRepository: IChannelRepositoryAggregate; - readonly guildRepository: IGuildRepositoryAggregate; - readonly userRepository: IUserRepository; + readonly channelRepository: ChannelRepository; + readonly guildRepository: GuildRepository; + readonly userRepository: UserRepository; readonly channelAuth: BaseChannelAuthService; readonly gatewayService: IGatewayService; readonly snowflakeService: ISnowflakeService; diff --git a/fluxer_api/src/api/channel/services/thread/ThreadViews.ts b/fluxer_api/src/api/channel/services/thread/ThreadViews.ts index 595e61453..c82166b16 100644 --- a/fluxer_api/src/api/channel/services/thread/ThreadViews.ts +++ b/fluxer_api/src/api/channel/services/thread/ThreadViews.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {ChannelID, GuildID} from '@app/api/BrandedTypes'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {ThreadView} from '@app/api/channel/services/thread/ThreadMappers'; import type {Channel} from '@app/api/models/Channel'; import {ThreadState} from '@app/api/models/ThreadState'; @@ -9,7 +9,7 @@ import {ThreadStats} from '@app/api/models/ThreadStats'; import {THREAD_ONLY_CHANNEL_TYPES} from '@fluxer/constants/src/ThreadConstants'; async function liveTagIds( - repository: IChannelRepositoryAggregate, + repository: ChannelRepository, guildId: GuildID, parentId: ChannelID, ): Promise> { @@ -23,7 +23,7 @@ function withLiveTags(state: ThreadState, tagIds: ReadonlySet | undefine } export async function loadThreadViews( - repository: IChannelRepositoryAggregate, + repository: ChannelRepository, states: Array, knownParents: ReadonlyArray = [], ): Promise> { @@ -70,7 +70,7 @@ export async function loadThreadViews( } export async function loadThreadView( - repository: IChannelRepositoryAggregate, + repository: ChannelRepository, channel: Channel, state: ThreadState, parent: Channel, diff --git a/fluxer_api/src/api/channel/tests/AnnouncementThreadRaces.test.ts b/fluxer_api/src/api/channel/tests/AnnouncementThreadRaces.test.ts index 9abdef5b6..25eb9b403 100644 --- a/fluxer_api/src/api/channel/tests/AnnouncementThreadRaces.test.ts +++ b/fluxer_api/src/api/channel/tests/AnnouncementThreadRaces.test.ts @@ -111,7 +111,7 @@ describe('announcement thread races', () => { }); expect(await repository.threads.setThreadType(threadId, ChannelTypes.ANNOUNCEMENT_THREAD)).toBeNull(); vi.restoreAllMocks(); - expect(await repository.findUnique(threadId)).toBeNull(); + expect(await repository.channelData.findUnique(threadId)).toBeNull(); expect(await repository.threads.getState(threadId)).toBeNull(); expect(await repository.threads.listParentThreads(createChannelID(BigInt(s.textId)))).toEqual([]); expect(await repository.threads.countActiveThreads(createGuildID(BigInt(s.guildId)))).toBe(0); @@ -136,7 +136,7 @@ describe('announcement thread races', () => { .expect(200) .execute(); vi.restoreAllMocks(); - const stored = await repository.findUnique(threadId); + const stored = await repository.channelData.findUnique(threadId); expect(stored?.name).toBe('renamed'); expect(stored?.type).toBe(ChannelTypes.ANNOUNCEMENT_THREAD); expect((await repository.threads.getState(threadId))?.type).toBe(ChannelTypes.ANNOUNCEMENT_THREAD); @@ -161,7 +161,7 @@ describe('announcement thread races', () => { }); await threadsRequest(harness, s.owner.token).put(`/channels/${thread.id}/pins/${message.id}`).expect(204).execute(); vi.restoreAllMocks(); - const stored = await repository.findUnique(threadId); + const stored = await repository.channelData.findUnique(threadId); expect(stored?.lastPinTimestamp).not.toBeNull(); expect(stored?.type).toBe(ChannelTypes.ANNOUNCEMENT_THREAD); expect((await repository.threads.getState(threadId))?.type).toBe(ChannelTypes.ANNOUNCEMENT_THREAD); @@ -184,7 +184,7 @@ describe('announcement thread races', () => { .expect(204) .execute(); vi.restoreAllMocks(); - const parent = await repository.findUnique(createChannelID(BigInt(s.textId))); + const parent = await repository.channelData.findUnique(createChannelID(BigInt(s.textId))); expect(parent?.type).toBe(ChannelTypes.GUILD_ANNOUNCEMENT); expect(parent?.permissionOverwrites?.size ?? 0).toBeGreaterThan(0); expect((await repository.threads.getState(createChannelID(BigInt(thread.id))))?.type).toBe( @@ -215,8 +215,10 @@ describe('announcement thread races', () => { .expect(500) .execute(); vi.restoreAllMocks(); - expect((await repository.findUnique(createChannelID(BigInt(s.textId))))?.type).toBe(ChannelTypes.GUILD_TEXT); - expect((await repository.findUnique(threadId))?.type).toBe(ChannelTypes.PUBLIC_THREAD); + expect((await repository.channelData.findUnique(createChannelID(BigInt(s.textId))))?.type).toBe( + ChannelTypes.GUILD_TEXT, + ); + expect((await repository.channelData.findUnique(threadId))?.type).toBe(ChannelTypes.PUBLIC_THREAD); expect((await repository.threads.getState(threadId))?.type).toBe(ChannelTypes.PUBLIC_THREAD); }); diff --git a/fluxer_api/src/api/channel/tests/AnnouncementThreads.test.ts b/fluxer_api/src/api/channel/tests/AnnouncementThreads.test.ts index 286b6ef56..85f9e9b59 100644 --- a/fluxer_api/src/api/channel/tests/AnnouncementThreads.test.ts +++ b/fluxer_api/src/api/channel/tests/AnnouncementThreads.test.ts @@ -97,7 +97,10 @@ describe('announcement threads', () => { async function storedType(threadId: string): Promise<{state?: number; channel?: number}> { const id = createChannelID(BigInt(threadId)); - const [state, channel] = await Promise.all([repository.threads.getState(id), repository.findUnique(id)]); + const [state, channel] = await Promise.all([ + repository.threads.getState(id), + repository.channelData.findUnique(id), + ]); return {state: state?.type, channel: channel?.type}; } @@ -306,7 +309,9 @@ describe('announcement threads', () => { state: ChannelTypes.PRIVATE_THREAD, channel: ChannelTypes.PRIVATE_THREAD, }); - expect((await repository.findUnique(createChannelID(BigInt(s.textId))))?.type).toBe(ChannelTypes.GUILD_TEXT); + expect((await repository.channelData.findUnique(createChannelID(BigInt(s.textId))))?.type).toBe( + ChannelTypes.GUILD_TEXT, + ); }); it('rolls back a failed retype and leaves the parent unconverted so a retry converges', async () => { @@ -323,7 +328,9 @@ describe('announcement threads', () => { .body({type: ChannelTypes.GUILD_ANNOUNCEMENT}) .expect(503) .execute(); - expect((await repository.findUnique(createChannelID(BigInt(s.textId))))?.type).toBe(ChannelTypes.GUILD_TEXT); + expect((await repository.channelData.findUnique(createChannelID(BigInt(s.textId))))?.type).toBe( + ChannelTypes.GUILD_TEXT, + ); expect(update.mock.calls.filter(([params]) => params.event === 'CHANNEL_UPDATE')).toEqual([]); vi.restoreAllMocks(); for (const thread of threads) { @@ -360,7 +367,9 @@ describe('announcement threads', () => { .body({type: ChannelTypes.GUILD_ANNOUNCEMENT}) .expect(400, APIErrorCodes.CHANNEL_HAS_THREADS) .execute(); - expect((await repository.findUnique(createChannelID(BigInt(s.textId))))?.type).toBe(ChannelTypes.GUILD_TEXT); + expect((await repository.channelData.findUnique(createChannelID(BigInt(s.textId))))?.type).toBe( + ChannelTypes.GUILD_TEXT, + ); expect(await storedType(secret.id)).toEqual({ state: ChannelTypes.PRIVATE_THREAD, channel: ChannelTypes.PRIVATE_THREAD, diff --git a/fluxer_api/src/api/channel/tests/CrosspostModeration.test.ts b/fluxer_api/src/api/channel/tests/CrosspostModeration.test.ts index 460ce5a9e..9e43c14e5 100644 --- a/fluxer_api/src/api/channel/tests/CrosspostModeration.test.ts +++ b/fluxer_api/src/api/channel/tests/CrosspostModeration.test.ts @@ -209,7 +209,11 @@ function helpers(): WorkerTaskHelpers { } async function listCopies(channelId: string, sourceId: string): Promise> { - const messages = await new ChannelRepository().listMessages(createChannelID(BigInt(channelId)), undefined, 100); + const messages = await new ChannelRepository().messages.listMessages( + createChannelID(BigInt(channelId)), + undefined, + 100, + ); return messages .filter( (message) => diff --git a/fluxer_api/src/api/channel/tests/CrosspostWorkerTestUtils.ts b/fluxer_api/src/api/channel/tests/CrosspostWorkerTestUtils.ts index 7d580613b..351ffd14b 100644 --- a/fluxer_api/src/api/channel/tests/CrosspostWorkerTestUtils.ts +++ b/fluxer_api/src/api/channel/tests/CrosspostWorkerTestUtils.ts @@ -190,11 +190,11 @@ export function workerHelpers(worker: SyncTaskWorkerService): WorkerTaskHelpers export async function patchChannelRow(channelId: string, patch: Partial): Promise { const repository = new ChannelRepository(); - const channel = await repository.findUnique(createChannelID(BigInt(channelId))); + const channel = await repository.channelData.findUnique(createChannelID(BigInt(channelId))); if (!channel) { throw new Error(`channel ${channelId} not found`); } - await repository.upsert({...channel.toRow(), ...patch}); + await repository.channelData.upsert({...channel.toRow(), ...patch}); } export async function writeRow(message: Message, patch: Partial): Promise { diff --git a/fluxer_api/src/api/channel/tests/EmbedAttachmentUrlResolution.test.ts b/fluxer_api/src/api/channel/tests/EmbedAttachmentUrlResolution.test.ts index 62c9e5418..e0311ca91 100644 --- a/fluxer_api/src/api/channel/tests/EmbedAttachmentUrlResolution.test.ts +++ b/fluxer_api/src/api/channel/tests/EmbedAttachmentUrlResolution.test.ts @@ -9,7 +9,6 @@ import { } from '@app/api/channel/tests/AttachmentTestUtils'; import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness'; import {createBuilder} from '@app/api/test/TestRequestBuilder'; -import {MessageAttachmentFlags} from '@fluxer/constants/src/ChannelConstants'; import {beforeAll, beforeEach, describe, expect, it} from 'vitest'; describe('Embed Attachment URL Resolution', () => { @@ -50,71 +49,8 @@ describe('Embed Attachment URL Resolution', () => { expect(embed.image?.url).toBeTruthy(); expect(embed.image?.url).not.toContain('attachment://'); }); - it('should resolve attachment:// URLs in embed thumbnail field', async () => { - const account = await createTestAccount(harness); - const guild = await createGuild(harness, account.token, 'Thumbnail Test Guild'); - const channel = await createChannel(harness, account.token, guild.id, 'test-channel'); - const channelId = guild.system_channel_id ?? channel.id; - const fileData = loadFixture('yeah.png'); - const payload = { - content: 'Test with thumbnail', - attachments: [{id: 0, filename: 'yeah.png'}], - embeds: [ - { - title: 'Thumbnail Test', - description: 'This embed uses a thumbnail', - thumbnail: {url: 'attachment://yeah.png'}, - }, - ], - }; - const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [ - {index: 0, filename: 'yeah.png', data: fileData}, - ]); - expect(response.status).toBe(200); - expect(json.embeds).toBeDefined(); - expect(json.embeds).toHaveLength(1); - const embed = json.embeds![0]; - expect(embed.title).toBe('Thumbnail Test'); - expect(embed.thumbnail?.url).toBeTruthy(); - expect(embed.thumbnail?.url).not.toContain('attachment://'); - }); }); describe('Image and Thumbnail Fields', () => { - it('should handle single embed using attachment:// for both image and thumbnail', async () => { - const account = await createTestAccount(harness); - const guild = await createGuild(harness, account.token, 'Both Fields Test Guild'); - const channel = await createChannel(harness, account.token, guild.id, 'test-channel'); - const channelId = guild.system_channel_id ?? channel.id; - const file1Data = loadFixture('yeah.png'); - const file2Data = loadFixture('animated.gif'); - const payload = { - content: 'Both image and thumbnail', - attachments: [ - {id: 0, filename: 'yeah.png'}, - {id: 1, filename: 'animated.gif'}, - ], - embeds: [ - { - title: 'Complete Embed', - description: 'This embed uses both image and thumbnail', - image: {url: 'attachment://animated.gif'}, - thumbnail: {url: 'attachment://yeah.png'}, - }, - ], - }; - const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [ - {index: 0, filename: 'yeah.png', data: file1Data}, - {index: 1, filename: 'animated.gif', data: file2Data}, - ]); - expect(response.status).toBe(200); - expect(json.embeds).toBeDefined(); - expect(json.embeds).toHaveLength(1); - const embed = json.embeds![0]; - expect(embed.image?.url).toBeTruthy(); - expect(embed.thumbnail?.url).toBeTruthy(); - expect(embed.image?.url).not.toContain('attachment://'); - expect(embed.thumbnail?.url).not.toContain('attachment://'); - }); it('should handle image and thumbnail in same embed from different files', async () => { const account = await createTestAccount(harness); const guild = await createGuild(harness, account.token, 'Image And Thumbnail Guild'); @@ -147,35 +83,6 @@ describe('Embed Attachment URL Resolution', () => { expect(embed.thumbnail?.url).not.toContain('attachment://'); expect(embed.image?.url).not.toBe(embed.thumbnail?.url); }); - it('should handle mixed attachment:// and https:// URLs in embeds', async () => { - const account = await createTestAccount(harness); - const guild = await createGuild(harness, account.token, 'Mixed URLs Test Guild'); - const channel = await createChannel(harness, account.token, guild.id, 'test-channel'); - const channelId = guild.system_channel_id ?? channel.id; - const fileData = loadFixture('yeah.png'); - const payload = { - content: 'Test message with mixed embed URLs', - attachments: [{id: 0, filename: 'local-image.png'}], - embeds: [ - { - title: 'Mixed URL Embed', - description: 'This embed uses both attachment and external URLs', - image: {url: 'attachment://local-image.png'}, - thumbnail: {url: 'https://example.com/external-image.png'}, - }, - ], - }; - const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [ - {index: 0, filename: 'local-image.png', data: fileData}, - ]); - expect(response.status).toBe(200); - expect(json.embeds).toBeDefined(); - expect(json.embeds).toHaveLength(1); - const embed = json.embeds![0]; - expect(embed.image?.url).toBeTruthy(); - expect(embed.image?.url).not.toContain('attachment://'); - expect(embed.thumbnail?.url).toBe('https://example.com/external-image.png'); - }); it('should resolve attachment:// URL while preserving external thumbnail URL', async () => { const account = await createTestAccount(harness); const guild = await createGuild(harness, account.token, 'Preserve External URL Guild'); @@ -239,124 +146,6 @@ describe('Embed Attachment URL Resolution', () => { ]); expect(response2.status).toBe(400); }); - it('should match correct file by filename', async () => { - const account = await createTestAccount(harness); - const guild = await createGuild(harness, account.token, 'Filename Match Test Guild'); - const channel = await createChannel(harness, account.token, guild.id, 'test-channel'); - const channelId = guild.system_channel_id ?? channel.id; - const fileData = loadFixture('yeah.png'); - const payload = { - content: 'Testing filename matching', - attachments: [ - {id: 0, filename: 'alpha.png'}, - {id: 1, filename: 'beta.png'}, - ], - embeds: [ - { - title: 'Beta Image', - image: {url: 'attachment://beta.png'}, - }, - { - title: 'Alpha Image', - image: {url: 'attachment://alpha.png'}, - }, - ], - }; - const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [ - {index: 0, filename: 'alpha.png', data: fileData}, - {index: 1, filename: 'beta.png', data: fileData}, - ]); - expect(response.status).toBe(200); - expect(json.attachments ?? []).toHaveLength(0); - expect(json.embeds).toHaveLength(2); - expect(json.embeds![0].title).toBe('Beta Image'); - expect(json.embeds![0].image?.url).not.toContain('attachment://'); - expect(json.embeds![1].title).toBe('Alpha Image'); - expect(json.embeds![1].image?.url).not.toContain('attachment://'); - }); - it('should resolve spoiler attachment in embed', async () => { - const account = await createTestAccount(harness); - const guild = await createGuild(harness, account.token, 'Spoiler Test Guild'); - const channel = await createChannel(harness, account.token, guild.id, 'test-channel'); - const channelId = guild.system_channel_id ?? channel.id; - const fileData = loadFixture('yeah.png'); - const payload = { - content: 'Test message with spoiler attachment in embed', - attachments: [{id: 0, filename: 'SPOILER_secret.png', flags: MessageAttachmentFlags.IS_SPOILER}], - embeds: [ - { - title: 'Spoiler Embed', - description: 'This embed uses a spoiler attachment', - image: {url: 'attachment://SPOILER_secret.png'}, - }, - ], - }; - const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [ - {index: 0, filename: 'SPOILER_secret.png', data: fileData}, - ]); - expect(response.status).toBe(200); - expect(json.attachments ?? []).toHaveLength(0); - expect(json.embeds).toBeDefined(); - expect(json.embeds).toHaveLength(1); - const embed = json.embeds![0]; - expect(embed.image?.url).toBeTruthy(); - expect(embed.image?.url).not.toContain('attachment://'); - }); - it('should preserve spoiler flag on attachment when referenced by embed', async () => { - const account = await createTestAccount(harness); - const guild = await createGuild(harness, account.token, 'Spoiler Flag Test Guild'); - const channel = await createChannel(harness, account.token, guild.id, 'test-channel'); - const channelId = guild.system_channel_id ?? channel.id; - const fileData = loadFixture('yeah.png'); - const payload = { - content: 'Spoiler flag preservation test', - attachments: [{id: 0, filename: 'SPOILER_hidden.png', flags: MessageAttachmentFlags.IS_SPOILER}], - embeds: [ - { - title: 'Hidden Content', - image: {url: 'attachment://SPOILER_hidden.png'}, - }, - ], - }; - const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [ - {index: 0, filename: 'SPOILER_hidden.png', data: fileData}, - ]); - expect(response.status).toBe(200); - expect(json.attachments ?? []).toHaveLength(0); - }); - it('should handle spoiler attachment alongside non-spoiler in embed', async () => { - const account = await createTestAccount(harness); - const guild = await createGuild(harness, account.token, 'Mixed Spoiler Guild'); - const channel = await createChannel(harness, account.token, guild.id, 'test-channel'); - const channelId = guild.system_channel_id ?? channel.id; - const fileData = loadFixture('yeah.png'); - const payload = { - content: 'Mixed spoiler and non-spoiler attachments', - attachments: [ - {id: 0, filename: 'SPOILER_hidden.png', flags: MessageAttachmentFlags.IS_SPOILER}, - {id: 1, filename: 'visible.png'}, - ], - embeds: [ - { - title: 'Spoiler Image', - image: {url: 'attachment://SPOILER_hidden.png'}, - }, - { - title: 'Visible Image', - thumbnail: {url: 'attachment://visible.png'}, - }, - ], - }; - const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [ - {index: 0, filename: 'SPOILER_hidden.png', data: fileData}, - {index: 1, filename: 'visible.png', data: fileData}, - ]); - expect(response.status).toBe(200); - expect(json.attachments ?? []).toHaveLength(0); - expect(json.embeds).toHaveLength(2); - expect(json.embeds![0].image?.url).not.toContain('attachment://'); - expect(json.embeds![1].thumbnail?.url).not.toContain('attachment://'); - }); }); describe('Error Handling', () => { it('should reject embed with attachment:// URL when no files are uploaded', async () => { @@ -425,28 +214,6 @@ describe('Embed Attachment URL Resolution', () => { ]); expect(response.status).toBe(400); }); - it('should reject PDF attachment in embed image field', async () => { - const account = await createTestAccount(harness); - const guild = await createGuild(harness, account.token, 'PDF Rejection Test Guild'); - const channel = await createChannel(harness, account.token, guild.id, 'test-channel'); - const channelId = guild.system_channel_id ?? channel.id; - const pdfHeader = Buffer.from('%PDF-1.4\n'); - const pdfData = Buffer.concat([pdfHeader, Buffer.alloc(100)]); - const payload = { - content: 'Test message with PDF in embed', - attachments: [{id: 0, filename: 'document.pdf'}], - embeds: [ - { - title: 'PDF Embed Attempt', - image: {url: 'attachment://document.pdf'}, - }, - ], - }; - const {response} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [ - {index: 0, filename: 'document.pdf', data: pdfData}, - ]); - expect(response.status).toBe(400); - }); it('should reject JSON attachment in embed thumbnail field', async () => { const account = await createTestAccount(harness); const guild = await createGuild(harness, account.token, 'JSON Rejection Test Guild'); @@ -468,27 +235,6 @@ describe('Embed Attachment URL Resolution', () => { ]); expect(response.status).toBe(400); }); - it('should reject executable attachment in embed image field', async () => { - const account = await createTestAccount(harness); - const guild = await createGuild(harness, account.token, 'Executable Rejection Guild'); - const channel = await createChannel(harness, account.token, guild.id, 'test-channel'); - const channelId = guild.system_channel_id ?? channel.id; - const exeData = Buffer.from('MZ'); - const payload = { - content: 'Test message with executable in embed', - attachments: [{id: 0, filename: 'program.exe'}], - embeds: [ - { - title: 'Executable Embed Attempt', - image: {url: 'attachment://program.exe'}, - }, - ], - }; - const {response} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [ - {index: 0, filename: 'program.exe', data: exeData}, - ]); - expect(response.status).toBe(400); - }); it('should accept file with image extension but corrupted content for embed reference', async () => { const account = await createTestAccount(harness); const guild = await createGuild(harness, account.token, 'Content Type Mismatch Guild'); @@ -606,51 +352,6 @@ describe('Embed Attachment URL Resolution', () => { expect(json.embeds![0].image?.url).toContain('yeah.png'); expect(json.embeds![1].image?.url).toContain('animated.gif'); }); - it('should resolve multiple files referenced by embeds', async () => { - const account = await createTestAccount(harness); - const guild = await createGuild(harness, account.token, 'Multiple Files Test Guild'); - const channel = await createChannel(harness, account.token, guild.id, 'test-channel'); - const channelId = guild.system_channel_id ?? channel.id; - const fileData = loadFixture('yeah.png'); - const payload = { - content: 'Test message with multiple attachments in embeds', - attachments: [ - {id: 0, filename: 'image1.png'}, - {id: 1, filename: 'image2.png'}, - {id: 2, filename: 'image3.png'}, - ], - embeds: [ - { - title: 'First Image', - image: {url: 'attachment://image1.png'}, - }, - { - title: 'Second Image', - image: {url: 'attachment://image2.png'}, - }, - { - title: 'Third Image', - thumbnail: {url: 'attachment://image3.png'}, - }, - ], - }; - const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [ - {index: 0, filename: 'image1.png', data: fileData}, - {index: 1, filename: 'image2.png', data: fileData}, - {index: 2, filename: 'image3.png', data: fileData}, - ]); - expect(response.status).toBe(200); - expect(json.attachments ?? []).toHaveLength(0); - expect(json.embeds).toHaveLength(3); - for (const embed of json.embeds!) { - if (embed.image) { - expect(embed.image.url).not.toContain('attachment://'); - } - if (embed.thumbnail) { - expect(embed.thumbnail.url).not.toContain('attachment://'); - } - } - }); it('should allow same attachment to be used in multiple embeds', async () => { const account = await createTestAccount(harness); const guild = await createGuild(harness, account.token, 'Reused Attachment Guild'); diff --git a/fluxer_api/src/api/channel/tests/ThreadCrossCuttingHelpers.test.ts b/fluxer_api/src/api/channel/tests/ThreadCrossCuttingHelpers.test.ts index e79f4b89a..0c877120a 100644 --- a/fluxer_api/src/api/channel/tests/ThreadCrossCuttingHelpers.test.ts +++ b/fluxer_api/src/api/channel/tests/ThreadCrossCuttingHelpers.test.ts @@ -5,10 +5,10 @@ import {MessageContentService} from '@app/api/channel/services/message/MessageCo import {maskThreadArtifacts} from '@app/api/channel/services/message/ThreadMessageResponses'; import {resolveNsfwScopeChannel} from '@app/api/channel/utils/ThreadNsfwScope'; import type {ChannelRow} from '@app/api/database/types/ChannelTypes'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {LimitConfigService} from '@app/api/limits/LimitConfigService'; import {Channel} from '@app/api/models/Channel'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {ChannelTypes, MessageTypes} from '@fluxer/constants/src/ChannelConstants'; import {GuildExplicitContentFilterTypes} from '@fluxer/constants/src/GuildConstants'; import {ServerMessageFlags} from '@fluxer/constants/src/ThreadConstants'; @@ -77,8 +77,8 @@ function message(overrides: Partial): MessageResponse { describe('thread NSFW scope', () => { const contentService = new MessageContentService( - {} as IUserRepository, - {} as IGuildRepositoryAggregate, + {} as UserRepository, + {} as GuildRepository, {} as LimitConfigService, ); const guild = { diff --git a/fluxer_api/src/api/channel/tests/ThreadLimitsAndTimeouts.test.ts b/fluxer_api/src/api/channel/tests/ThreadLimitsAndTimeouts.test.ts index 5f57498f6..7cf8f1bdd 100644 --- a/fluxer_api/src/api/channel/tests/ThreadLimitsAndTimeouts.test.ts +++ b/fluxer_api/src/api/channel/tests/ThreadLimitsAndTimeouts.test.ts @@ -286,7 +286,7 @@ describe('thread limits and timeouts', () => { .body({name: 'renamed', rate_limit_per_user: 30}) .expect(404, APIErrorCodes.UNKNOWN_CHANNEL) .execute(); - const stored = await repository.findUnique(threadId(thread.id)); + const stored = await repository.channelData.findUnique(threadId(thread.id)); expect(stored?.name).toBe('topic'); expect(stored?.rateLimitPerUser ?? 0).toBe(thread.rate_limit_per_user ?? 0); }); diff --git a/fluxer_api/src/api/channel/tests/ThreadMessagePath.test.ts b/fluxer_api/src/api/channel/tests/ThreadMessagePath.test.ts index df52049ee..a2bafe946 100644 --- a/fluxer_api/src/api/channel/tests/ThreadMessagePath.test.ts +++ b/fluxer_api/src/api/channel/tests/ThreadMessagePath.test.ts @@ -222,7 +222,7 @@ describe('thread message path', () => { it('indexes thread messages for search while the parent is still unindexed', async () => { const s = await setup(); const thread = await startThread(s.owner.token, s.channelId); - expect((await repository.findUnique(createChannelID(BigInt(s.channelId))))?.indexedAt).toBeNull(); + expect((await repository.channelData.findUnique(createChannelID(BigInt(s.channelId))))?.indexedAt).toBeNull(); const indexMessage = vi.spyOn(MessageSearchService.prototype, 'indexMessage'); const message = await send(s.member.token, thread.id, 'searchable'); expect(indexMessage).toHaveBeenCalledWith( diff --git a/fluxer_api/src/api/channel/tests/ThreadRoutes.test.ts b/fluxer_api/src/api/channel/tests/ThreadRoutes.test.ts index a2802c8f0..988ec464e 100644 --- a/fluxer_api/src/api/channel/tests/ThreadRoutes.test.ts +++ b/fluxer_api/src/api/channel/tests/ThreadRoutes.test.ts @@ -288,7 +288,7 @@ describe('thread routes', () => { expect(announced).toHaveLength(1); expect(announced[0]).toMatchObject({channel_id: s.channelId, content: 'old news'}); expect(announced[0]!.message_reference?.channel_id).toBe(pushedUp.id); - const parent = await repository.findUnique(createChannelID(BigInt(s.channelId))); + const parent = await repository.channelData.findUnique(createChannelID(BigInt(s.channelId))); expect(parent?.lastMessageId?.toString()).toBe(latest.id); }); @@ -985,7 +985,7 @@ describe('thread routes', () => { await createBuilder(harness, admin.token).delete(`/admin/channels/${s.channelId}`).expect(400).execute(); await createBuilder(harness, admin.token).delete(`/admin/channels/${open.id}`).expect(204).execute(); expect(await repository.threads.getState(createChannelID(BigInt(open.id)))).toBeNull(); - expect(await repository.findUnique(createChannelID(BigInt(open.id)))).toBeNull(); + expect(await repository.channelData.findUnique(createChannelID(BigInt(open.id)))).toBeNull(); const after = await createBuilder<{threads: Array}>(harness, admin.token) .get(`/admin/guilds/${s.guildId}/threads`) .execute(); diff --git a/fluxer_api/src/api/connection/ConnectionRepository.ts b/fluxer_api/src/api/connection/ConnectionRepository.ts index e88c2dd9d..9217af808 100644 --- a/fluxer_api/src/api/connection/ConnectionRepository.ts +++ b/fluxer_api/src/api/connection/ConnectionRepository.ts @@ -11,13 +11,6 @@ import { loadConnectionMembership, sealConnectionMembership, } from '@app/api/connection/ConnectionMembership'; -import { - type ConnectionCreationResult, - type ConnectionSortOrderUpdate, - type CreateConnectionParams, - IConnectionRepository, - type UpdateConnectionParams, -} from '@app/api/connection/IConnectionRepository'; import { executeConditional, executeGroupedBatches, @@ -38,6 +31,40 @@ import { MAX_CONNECTIONS_PER_USER, } from '@fluxer/constants/src/ConnectionConstants'; +export interface CreateConnectionParams { + user_id: UserID; + connection_id: string; + connection_type: ConnectionType; + identifier: string; + name: string; + visibility_flags: number; + sort_order?: number; + verification_token: string; + oauth_grant_id?: string | null; + verified?: boolean; + verified_at?: Date | null; + last_verified_at?: Date | null; +} + +export interface UpdateConnectionParams { + name?: string; + visibility_flags?: number; + sort_order?: number; + oauth_grant_id?: string | null; + verified?: boolean; + verified_at?: Date | null; + last_verified_at?: Date | null; +} + +export interface ConnectionSortOrderUpdate { + snapshot: RevisionedUserConnectionRow; + sortOrder: number; +} + +export type ConnectionCreationResult = + | {status: 'created'; connection: RevisionedUserConnectionRow} + | {status: 'duplicate' | 'limit_reached' | 'conflict'}; + const FETCH_CONNECTIONS_BY_USER_CQL = UserConnections.selectCql({ columns: USER_CONNECTION_COLUMNS, where: [UserConnections.where.eq('user_id'), UserConnections.where.in('connection_type', 'connection_types')], @@ -64,7 +91,7 @@ function hasConnectionRevision(connection: UserConnectionRow): connection is Rev return typeof connection.revision === 'string'; } -export class ConnectionRepository extends IConnectionRepository { +export class ConnectionRepository { async findByUserId(userId: UserID): Promise> { return fetchMany(FETCH_CONNECTIONS_BY_USER_CQL, { user_id: userId, diff --git a/fluxer_api/src/api/connection/ConnectionRequestService.ts b/fluxer_api/src/api/connection/ConnectionRequestService.ts index 62ff6fb51..cf057d886 100644 --- a/fluxer_api/src/api/connection/ConnectionRequestService.ts +++ b/fluxer_api/src/api/connection/ConnectionRequestService.ts @@ -4,8 +4,8 @@ import {createHmac} from 'node:crypto'; import type {UserID} from '@app/api/BrandedTypes'; import {signInitiationToken, verifyInitiationToken} from '@app/api/connection/ConnectionInitiationToken'; import {mapConnectionToResponse} from '@app/api/connection/ConnectionMappers'; +import type {ConnectionService} from '@app/api/connection/ConnectionService'; import {ConnectionInitiationTokenInvalidError} from '@app/api/connection/errors/ConnectionInitiationTokenInvalidError'; -import type {IConnectionService} from '@app/api/connection/IConnectionService'; import { CONNECTION_INITIATION_TOKEN_EXPIRY_MS, CONNECTION_VERIFICATION_TOKEN_LENGTH, @@ -22,7 +22,7 @@ import type { export class ConnectionRequestService { constructor( - private readonly connectionService: IConnectionService, + private readonly connectionService: ConnectionService, private readonly connectionInitiationSecret: string, ) {} diff --git a/fluxer_api/src/api/connection/ConnectionService.ts b/fluxer_api/src/api/connection/ConnectionService.ts index e7d2a7208..44fbb2a04 100644 --- a/fluxer_api/src/api/connection/ConnectionService.ts +++ b/fluxer_api/src/api/connection/ConnectionService.ts @@ -4,6 +4,12 @@ import {randomUUID} from 'node:crypto'; import type {UserID} from '@app/api/BrandedTypes'; import type {BlueskyCallbackResult} from '@app/api/bluesky/IBlueskyOAuthService'; import {mapConnectionToResponse} from '@app/api/connection/ConnectionMappers'; +import type { + ConnectionRepository, + ConnectionSortOrderUpdate, + CreateConnectionParams, + UpdateConnectionParams, +} from '@app/api/connection/ConnectionRepository'; import {createDomainConnectionId} from '@app/api/connection/DomainConnectionId'; import {BlueskyOAuthNotEnabledError} from '@app/api/connection/errors/BlueskyOAuthNotEnabledError'; import {ConnectionAlreadyExistsError} from '@app/api/connection/errors/ConnectionAlreadyExistsError'; @@ -11,13 +17,6 @@ import {ConnectionInvalidTypeError} from '@app/api/connection/errors/ConnectionI import {ConnectionLimitReachedError} from '@app/api/connection/errors/ConnectionLimitReachedError'; import {ConnectionNotFoundError} from '@app/api/connection/errors/ConnectionNotFoundError'; import {ConnectionVerificationFailedError} from '@app/api/connection/errors/ConnectionVerificationFailedError'; -import type { - ConnectionSortOrderUpdate, - CreateConnectionParams, - IConnectionRepository, - UpdateConnectionParams, -} from '@app/api/connection/IConnectionRepository'; -import {IConnectionService, type InitiateConnectionResult} from '@app/api/connection/IConnectionService'; import {DomainConnectionVerifier} from '@app/api/connection/verification/DomainConnectionVerifier'; import type {RevisionedUserConnectionRow, UserConnectionRow} from '@app/api/database/types/ConnectionTypes'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; @@ -30,13 +29,13 @@ import { } from '@fluxer/constants/src/ConnectionConstants'; import {ConflictError} from '@fluxer/errors/src/domains/core/ConflictError'; -export class ConnectionService extends IConnectionService { +export type InitiateConnectionResult = Record; + +export class ConnectionService { constructor( - private readonly repository: IConnectionRepository, + private readonly repository: ConnectionRepository, private readonly gateway: IGatewayService, - ) { - super(); - } + ) {} async getConnectionsForUser(userId: UserID): Promise> { return this.repository.findByUserId(userId); diff --git a/fluxer_api/src/api/connection/IConnectionRepository.ts b/fluxer_api/src/api/connection/IConnectionRepository.ts deleted file mode 100644 index 1198f2be9..000000000 --- a/fluxer_api/src/api/connection/IConnectionRepository.ts +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {UserID} from '@app/api/BrandedTypes'; -import type {RevisionedUserConnectionRow, UserConnectionRow} from '@app/api/database/types/ConnectionTypes'; -import type {ConnectionType} from '@fluxer/constants/src/ConnectionConstants'; - -export interface CreateConnectionParams { - user_id: UserID; - connection_id: string; - connection_type: ConnectionType; - identifier: string; - name: string; - visibility_flags: number; - sort_order?: number; - verification_token: string; - oauth_grant_id?: string | null; - verified?: boolean; - verified_at?: Date | null; - last_verified_at?: Date | null; -} - -export interface UpdateConnectionParams { - name?: string; - visibility_flags?: number; - sort_order?: number; - oauth_grant_id?: string | null; - verified?: boolean; - verified_at?: Date | null; - last_verified_at?: Date | null; -} - -export interface ConnectionSortOrderUpdate { - snapshot: RevisionedUserConnectionRow; - sortOrder: number; -} - -export type ConnectionCreationResult = - | {status: 'created'; connection: RevisionedUserConnectionRow} - | {status: 'duplicate' | 'limit_reached' | 'conflict'}; - -export abstract class IConnectionRepository { - abstract findByUserId(userId: UserID): Promise>; - - abstract findById( - userId: UserID, - connectionType: ConnectionType, - connectionId: string, - ): Promise; - - abstract findByTypeAndIdentifier( - userId: UserID, - connectionType: ConnectionType, - identifier: string, - ): Promise; - - abstract create(params: CreateConnectionParams): Promise; - - abstract ensureRevision(snapshot: UserConnectionRow): Promise; - - abstract update( - snapshot: RevisionedUserConnectionRow, - params: UpdateConnectionParams, - ): Promise; - - abstract updateSortOrders(entries: ReadonlyArray): Promise; - - abstract delete(snapshot: RevisionedUserConnectionRow): Promise; - - abstract sealAndDeleteForUser(userId: UserID): Promise; - - abstract count(userId: UserID): Promise; -} diff --git a/fluxer_api/src/api/connection/IConnectionService.ts b/fluxer_api/src/api/connection/IConnectionService.ts deleted file mode 100644 index a05ab4053..000000000 --- a/fluxer_api/src/api/connection/IConnectionService.ts +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {UserID} from '@app/api/BrandedTypes'; -import type {BlueskyCallbackResult} from '@app/api/bluesky/IBlueskyOAuthService'; -import type {UpdateConnectionParams} from '@app/api/connection/IConnectionRepository'; -import type {UserConnectionRow} from '@app/api/database/types/ConnectionTypes'; -import type {ConnectionType} from '@fluxer/constants/src/ConnectionConstants'; - -export type InitiateConnectionResult = Record; - -export abstract class IConnectionService { - abstract getConnectionsForUser(userId: UserID): Promise>; - - abstract initiateConnection( - userId: UserID, - type: ConnectionType, - identifier: string, - ): Promise; - - abstract verifyAndCreateConnection( - userId: UserID, - type: ConnectionType, - identifier: string, - verificationCode: string, - visibilityFlags: number, - ): Promise; - - abstract updateConnection( - userId: UserID, - connectionType: ConnectionType, - connectionId: string, - patch: UpdateConnectionParams, - ): Promise; - - abstract deleteConnection(userId: UserID, connectionType: ConnectionType, connectionId: string): Promise; - - abstract reorderConnections(userId: UserID, connectionIds: Array): Promise; - - abstract createOrUpdateBlueskyConnection(result: BlueskyCallbackResult): Promise; -} diff --git a/fluxer_api/src/api/database/PostgresKvQueryExecutor.ts b/fluxer_api/src/api/database/PostgresKvQueryExecutor.ts index ae2c1765f..822ffed1f 100644 --- a/fluxer_api/src/api/database/PostgresKvQueryExecutor.ts +++ b/fluxer_api/src/api/database/PostgresKvQueryExecutor.ts @@ -129,7 +129,7 @@ function planStatementName(prefix: string, plan: CandidatePlan): string | undefi } } -function normalizeCql(cql: string): string { +export function normalizeCql(cql: string): string { return cql.replace(/\s+/g, ' ').trim(); } @@ -283,7 +283,7 @@ function compareValues(left: unknown, right: unknown): number { return (l as number | string) < (r as number | string) ? -1 : 1; } -function valuesEqual(left: unknown, right: unknown): boolean { +export function valuesEqual(left: unknown, right: unknown): boolean { if (left == null && right == null) return true; if (left instanceof Date && right instanceof Date) return left.getTime() === right.getTime(); if (Buffer.isBuffer(left) && Buffer.isBuffer(right)) return left.equals(right); @@ -363,7 +363,7 @@ function compareColumns(columns: ReadonlyArray, left: Row, right: Row): return 0; } -function rowComparator(meta: KvQueryMeta): (left: Row, right: Row) => number { +export function rowComparator(meta: KvQueryMeta): (left: Row, right: Row) => number { const primaryKey = meta.table.primaryKey as ReadonlyArray; if (!meta.orderBy) return (left, right) => compareColumns(primaryKey, left, right); const column = meta.orderBy.col as string; diff --git a/fluxer_api/src/api/donation/DonationRepository.ts b/fluxer_api/src/api/donation/DonationRepository.ts index 366dc3d1c..8f75a2d4a 100644 --- a/fluxer_api/src/api/donation/DonationRepository.ts +++ b/fluxer_api/src/api/donation/DonationRepository.ts @@ -17,7 +17,6 @@ import { DonorsByStripeCustomerId, DonorsByStripeSubscriptionId, } from '@app/api/donation/DonationTables'; -import {IDonationRepository} from '@app/api/donation/IDonationRepository'; import {Donor} from '@app/api/donation/models/Donor'; import {DonorMagicLinkToken} from '@app/api/donation/models/DonorMagicLinkToken'; @@ -44,7 +43,7 @@ const FETCH_MAGIC_LINK_TOKENS_BY_EMAIL_QUERY = DonorMagicLinkTokensByEmail.selec where: DonorMagicLinkTokensByEmail.where.eq('donor_email'), }); -export class DonationRepository extends IDonationRepository { +export class DonationRepository { async findDonorByEmail(email: string): Promise { const row = await fetchOne(FETCH_DONOR_BY_EMAIL_QUERY, {email}); return row ? new Donor(row) : null; diff --git a/fluxer_api/src/api/donation/DonationService.ts b/fluxer_api/src/api/donation/DonationService.ts index 7c72cedbf..22c413fa9 100644 --- a/fluxer_api/src/api/donation/DonationService.ts +++ b/fluxer_api/src/api/donation/DonationService.ts @@ -1,11 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import type {IDonationService} from '@app/api/donation/IDonationService'; import type {DonationCheckoutService} from '@app/api/donation/services/DonationCheckoutService'; import type {DonationMagicLinkService} from '@app/api/donation/services/DonationMagicLinkService'; import type {DonationCurrency} from '@fluxer/schema/src/domains/donation/DonationSchemas'; -export class DonationService implements IDonationService { +export class DonationService { constructor( private magicLinkService: DonationMagicLinkService, private checkoutService: DonationCheckoutService, diff --git a/fluxer_api/src/api/donation/IDonationRepository.ts b/fluxer_api/src/api/donation/IDonationRepository.ts deleted file mode 100644 index 5526dd7db..000000000 --- a/fluxer_api/src/api/donation/IDonationRepository.ts +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {Donor} from '@app/api/donation/models/Donor'; -import type {DonorMagicLinkToken} from '@app/api/donation/models/DonorMagicLinkToken'; - -export abstract class IDonationRepository { - abstract findDonorByEmail(email: string): Promise; - - abstract findDonorByStripeCustomerId(customerId: string): Promise; - - abstract findDonorByStripeSubscriptionId(subscriptionId: string): Promise; - - abstract createDonor(data: { - email: string; - stripeCustomerId: string | null; - businessName?: string | null; - taxId?: string | null; - taxIdType?: string | null; - stripeSubscriptionId: string | null; - subscriptionAmountCents: number | null; - subscriptionCurrency: string | null; - subscriptionInterval: string | null; - subscriptionCurrentPeriodEnd: Date | null; - subscriptionCancelAt?: Date | null; - subscriptionStatus?: string | null; - }): Promise; - - abstract updateDonorSubscription( - email: string, - data: { - stripeCustomerId: string | null; - businessName?: string | null; - taxId?: string | null; - taxIdType?: string | null; - stripeSubscriptionId: string | null; - subscriptionAmountCents: number | null; - subscriptionCurrency: string | null; - subscriptionInterval: string | null; - subscriptionCurrentPeriodEnd: Date | null; - subscriptionCancelAt?: Date | null; - subscriptionStatus?: string | null; - }, - ): Promise; - - abstract updateDonorCustomerDetails( - email: string, - data: { - stripeCustomerId: string | null; - businessName?: string | null; - taxId?: string | null; - taxIdType?: string | null; - }, - ): Promise; - - abstract linkDonorStripeCustomer(email: string, stripeCustomerId: string): Promise; - - abstract clearDonorStripeCustomer(stripeCustomerId: string): Promise; - - abstract cancelDonorSubscription(email: string): Promise; - - abstract deleteDonorSubscriptionMapping(stripeSubscriptionId: string, email: string): Promise; - - abstract createMagicLinkToken(token: DonorMagicLinkToken): Promise; - - abstract findMagicLinkToken(token: string): Promise; - - abstract markMagicLinkTokenUsed(token: string, usedAt: Date): Promise; - - abstract invalidateTokensForEmail(email: string): Promise; -} diff --git a/fluxer_api/src/api/donation/IDonationService.ts b/fluxer_api/src/api/donation/IDonationService.ts deleted file mode 100644 index 9c1ee3e0e..000000000 --- a/fluxer_api/src/api/donation/IDonationService.ts +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {DonationCurrency} from '@fluxer/schema/src/domains/donation/DonationSchemas'; - -export interface IDonationService { - requestMagicLink(email: string, locale?: string | null): Promise; - validateMagicLinkToken(token: string): Promise<{ - email: string; - stripeCustomerId: string | null; - }>; - redeemMagicLinkToken(token: string): Promise; - createDonationCheckout(params: { - email: string; - amountCents: number; - currency: DonationCurrency; - interval: 'month' | 'year' | null; - isBusiness?: boolean; - locale?: string | null; - }): Promise; -} diff --git a/fluxer_api/src/api/donation/services/DonationCheckoutService.ts b/fluxer_api/src/api/donation/services/DonationCheckoutService.ts index 0658c774b..db1b3adb0 100644 --- a/fluxer_api/src/api/donation/services/DonationCheckoutService.ts +++ b/fluxer_api/src/api/donation/services/DonationCheckoutService.ts @@ -2,7 +2,7 @@ import {Config} from '@app/api/Config'; import {getContentMessage} from '@app/api/content_i18n/ContentI18n'; -import type {IDonationRepository} from '@app/api/donation/IDonationRepository'; +import type {DonationRepository} from '@app/api/donation/DonationRepository'; import type {IEmailDnsValidationService} from '@app/api/infrastructure/IEmailDnsValidationService'; import {getInstanceProductName} from '@app/api/instance/ProductName'; import {Logger} from '@app/api/Logger'; @@ -97,7 +97,7 @@ function getDonationProductData(interval: 'month' | 'year' | null, locale: strin export class DonationCheckoutService { constructor( private stripe: Stripe | null, - private donationRepository: IDonationRepository, + private donationRepository: DonationRepository, private emailDnsValidationService: IEmailDnsValidationService, ) {} diff --git a/fluxer_api/src/api/donation/services/DonationMagicLinkService.ts b/fluxer_api/src/api/donation/services/DonationMagicLinkService.ts index 560e61ce3..5769aafbb 100644 --- a/fluxer_api/src/api/donation/services/DonationMagicLinkService.ts +++ b/fluxer_api/src/api/donation/services/DonationMagicLinkService.ts @@ -2,7 +2,7 @@ import {randomBytes} from 'node:crypto'; import {Config} from '@app/api/Config'; -import type {IDonationRepository} from '@app/api/donation/IDonationRepository'; +import type {DonationRepository} from '@app/api/donation/DonationRepository'; import {DonorMagicLinkToken} from '@app/api/donation/models/DonorMagicLinkToken'; import type {IEmailDnsValidationService} from '@app/api/infrastructure/IEmailDnsValidationService'; import {Logger} from '@app/api/Logger'; @@ -16,7 +16,7 @@ import type {IEmailService} from '@pkgs/email/src/IEmailService'; export class DonationMagicLinkService { constructor( - private donationRepository: IDonationRepository, + private donationRepository: DonationRepository, private emailService: IEmailService, private emailDnsValidationService: IEmailDnsValidationService, ) {} diff --git a/fluxer_api/src/api/donation/tests/DonationRequestLink.test.ts b/fluxer_api/src/api/donation/tests/DonationRequestLink.test.ts index 044cd802d..b30feba96 100644 --- a/fluxer_api/src/api/donation/tests/DonationRequestLink.test.ts +++ b/fluxer_api/src/api/donation/tests/DonationRequestLink.test.ts @@ -1,11 +1,6 @@ +import {clearTestEmails, createUniqueEmail, listTestEmails} from '@app/api/auth/tests/AuthTestUtils'; import {DonationRepository} from '@app/api/donation/DonationRepository'; -import { - clearDonationTestEmails, - createDonationRequestLinkBuilder, - createUniqueEmail, - listDonationTestEmails, - TEST_DONOR_EMAIL, -} from '@app/api/donation/tests/DonationTestUtils'; +import {createDonationRequestLinkBuilder, TEST_DONOR_EMAIL} from '@app/api/donation/tests/DonationTestUtils'; import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness'; import {afterAll, beforeAll, beforeEach, describe, expect, test} from 'vitest'; @@ -21,7 +16,7 @@ describe('POST /donations/request-link', () => { }); beforeEach(async () => { await harness.reset(); - await clearDonationTestEmails(harness); + await clearTestEmails(harness); }); async function createDonor(email: string): Promise { await donationRepository.createDonor({ @@ -41,20 +36,20 @@ describe('POST /donations/request-link', () => { test('sends magic link email when donor exists', async () => { await createDonor(TEST_DONOR_EMAIL); await createDonationRequestLinkBuilder(harness).body({email: TEST_DONOR_EMAIL}).expect(204).execute(); - const emails = await listDonationTestEmails(harness, {recipient: TEST_DONOR_EMAIL}); + const emails = await listTestEmails(harness, {recipient: TEST_DONOR_EMAIL}); expect(emails).toHaveLength(1); expect(emails[0]?.type).toBe('donation_magic_link'); }); test('does not send email when donor does not exist', async () => { const unknownEmail = createUniqueEmail('unknown'); await createDonationRequestLinkBuilder(harness).body({email: unknownEmail}).expect(204).execute(); - const emails = await listDonationTestEmails(harness, {recipient: unknownEmail}); + const emails = await listTestEmails(harness, {recipient: unknownEmail}); expect(emails).toHaveLength(0); }); test('magic link token is 64-character hex string', async () => { await createDonor(TEST_DONOR_EMAIL); await createDonationRequestLinkBuilder(harness).body({email: TEST_DONOR_EMAIL}).expect(204).execute(); - const emails = await listDonationTestEmails(harness, {recipient: TEST_DONOR_EMAIL}); + const emails = await listTestEmails(harness, {recipient: TEST_DONOR_EMAIL}); const token = emails[0]?.metadata.token; expect(token).toBeDefined(); expect(token).toHaveLength(64); @@ -66,7 +61,7 @@ describe('POST /donations/request-link', () => { await createDonor(TEST_DONOR_EMAIL); await createDonationRequestLinkBuilder(harness).body({email: TEST_DONOR_EMAIL}).expect(204).execute(); await createDonationRequestLinkBuilder(harness).body({email: TEST_DONOR_EMAIL}).expect(204).execute(); - const emails = await listDonationTestEmails(harness, {recipient: TEST_DONOR_EMAIL}); + const emails = await listTestEmails(harness, {recipient: TEST_DONOR_EMAIL}); expect(emails).toHaveLength(2); const token1 = emails[0]?.metadata.token; const token2 = emails[1]?.metadata.token; diff --git a/fluxer_api/src/api/donation/tests/DonationTestUtils.ts b/fluxer_api/src/api/donation/tests/DonationTestUtils.ts index 1e7308c57..0fcb3572a 100644 --- a/fluxer_api/src/api/donation/tests/DonationTestUtils.ts +++ b/fluxer_api/src/api/donation/tests/DonationTestUtils.ts @@ -1,36 +1,9 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import {randomUUID} from 'node:crypto'; import type {ApiTestHarness} from '@app/api/test/ApiTestHarness'; import {createBuilderWithoutAuth, type TestRequestBuilder} from '@app/api/test/TestRequestBuilder'; import {DONATION_CURRENCIES, type DonationCurrency} from '@fluxer/schema/src/domains/donation/DonationAmountUtils'; -interface DonationTestEmailRecord { - to: string; - type: string; - timestamp: string; - metadata: Record; -} - -export async function listDonationTestEmails( - harness: ApiTestHarness, - params?: { - recipient?: string; - }, -): Promise> { - const query = params?.recipient ? `?recipient=${encodeURIComponent(params.recipient)}` : ''; - const response = await createBuilderWithoutAuth<{ - emails: Array; - }>(harness) - .get(`/test/emails${query}`) - .execute(); - return response.emails; -} - -export async function clearDonationTestEmails(harness: ApiTestHarness): Promise { - await createBuilderWithoutAuth(harness).delete('/test/emails').expect(204).execute(); -} - export const TEST_DONOR_EMAIL = 'donor@test.com'; export const TEST_MAGIC_LINK_TOKEN = 'a'.repeat(64); export const DONATION_AMOUNTS = { @@ -84,7 +57,3 @@ export function createValidCheckoutBody(overrides?: Partial> = { 'admin/services/AdminAuditService.ts': [1, 'admin_or_system'], 'admin/services/AdminMessageService.ts': [4, 'admin_or_system'], 'admin/services/AdminReportService.ts': [4, 'admin_or_system'], - 'channel/ChannelRepository.ts': [1, 'admin_or_system'], 'channel/services/AttachmentUploadService.ts': [1, 'gated'], 'channel/services/BaseChannelAuthService.ts': [4, 'gated'], 'channel/services/CallService.ts': [5, 'dm_or_voice_only'], diff --git a/fluxer_api/src/api/favorite_meme/FavoriteMemeRepository.ts b/fluxer_api/src/api/favorite_meme/FavoriteMemeRepository.ts index 9665fd067..f357c6e0c 100644 --- a/fluxer_api/src/api/favorite_meme/FavoriteMemeRepository.ts +++ b/fluxer_api/src/api/favorite_meme/FavoriteMemeRepository.ts @@ -1,12 +1,33 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import type {MemeID, UserID} from '@app/api/BrandedTypes'; +import type {AttachmentID, MemeID, UserID} from '@app/api/BrandedTypes'; import {BatchBuilder, fetchMany, fetchOne, upsertOne} from '@app/api/database/CassandraQueryExecution'; import {Db} from '@app/api/database/CassandraTypes'; import type {FavoriteMemeRow} from '@app/api/database/types/UserTypes'; -import {type CreateFavoriteMemeParams, IFavoriteMemeRepository} from '@app/api/favorite_meme/IFavoriteMemeRepository'; import {FavoriteMeme} from '@app/api/models/FavoriteMeme'; import {FavoriteMemes, FavoriteMemesByMemeId} from '@app/api/Tables'; +import type {GifMediaFormat} from '@fluxer/schema/src/domains/gif/GifSchemas'; + +export interface CreateFavoriteMemeParams { + user_id: UserID; + meme_id: MemeID; + name: string; + alt_text?: string | null; + tags?: Array; + attachment_id: AttachmentID; + filename: string; + content_type: string; + content_hash?: string | null; + size: bigint; + width?: number | null; + height?: number | null; + duration?: number | null; + is_gifv?: boolean; + gif_slug?: string | null; + gif_provider?: string | null; + media_formats?: Record | null; + placeholder?: string | null; +} const FETCH_FAVORITE_MEME_CQL = FavoriteMemes.selectCql({ where: [FavoriteMemes.where.eq('user_id'), FavoriteMemes.where.eq('meme_id')], @@ -19,7 +40,7 @@ const COUNT_FAVORITE_MEMES_CQL = FavoriteMemes.selectCountCql({ where: FavoriteMemes.where.eq('user_id'), }); -export class FavoriteMemeRepository extends IFavoriteMemeRepository { +export class FavoriteMemeRepository { async findById(userId: UserID, memeId: MemeID): Promise { const meme = await fetchOne(FETCH_FAVORITE_MEME_CQL, { user_id: userId, diff --git a/fluxer_api/src/api/favorite_meme/FavoriteMemeService.ts b/fluxer_api/src/api/favorite_meme/FavoriteMemeService.ts index 2c3a433c5..672354907 100644 --- a/fluxer_api/src/api/favorite_meme/FavoriteMemeService.ts +++ b/fluxer_api/src/api/favorite_meme/FavoriteMemeService.ts @@ -9,7 +9,7 @@ import type {ChannelService} from '@app/api/channel/services/ChannelService'; import {attachmentStorageChannelId, makeAttachmentCdnKey} from '@app/api/channel/services/message/MessageHelpers'; import type {ThreadViewer} from '@app/api/experiment/ChannelThreadsGate'; import {mapFavoriteMemeToResponse} from '@app/api/favorite_meme/FavoriteMemeModel'; -import type {IFavoriteMemeRepository} from '@app/api/favorite_meme/IFavoriteMemeRepository'; +import type {FavoriteMemeRepository} from '@app/api/favorite_meme/FavoriteMemeRepository'; import { isOptionalGifProviderError, type ResolvedGifProviderSlug, @@ -96,7 +96,7 @@ function resolveFavoriteMemeAnimationFlag( export class FavoriteMemeService { constructor( private readonly apiContext: ApiContext, - private readonly favoriteMemeRepository: IFavoriteMemeRepository, + private readonly favoriteMemeRepository: FavoriteMemeRepository, private readonly channelService: ChannelService, private readonly storageService: IStorageService, private readonly unfurlerService: IUnfurlerService, diff --git a/fluxer_api/src/api/favorite_meme/IFavoriteMemeRepository.ts b/fluxer_api/src/api/favorite_meme/IFavoriteMemeRepository.ts deleted file mode 100644 index 404203c2f..000000000 --- a/fluxer_api/src/api/favorite_meme/IFavoriteMemeRepository.ts +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {AttachmentID, MemeID, UserID} from '@app/api/BrandedTypes'; -import type {FavoriteMeme} from '@app/api/models/FavoriteMeme'; -import type {GifMediaFormat} from '@fluxer/schema/src/domains/gif/GifSchemas'; - -export interface CreateFavoriteMemeParams { - user_id: UserID; - meme_id: MemeID; - name: string; - alt_text?: string | null; - tags?: Array; - attachment_id: AttachmentID; - filename: string; - content_type: string; - content_hash?: string | null; - size: bigint; - width?: number | null; - height?: number | null; - duration?: number | null; - is_gifv?: boolean; - gif_slug?: string | null; - gif_provider?: string | null; - media_formats?: Record | null; - placeholder?: string | null; -} - -export abstract class IFavoriteMemeRepository { - abstract create(data: CreateFavoriteMemeParams): Promise; - - abstract findById(userId: UserID, memeId: MemeID): Promise; - - abstract findByUserId(userId: UserID): Promise>; - - abstract update(userId: UserID, memeId: MemeID, data: CreateFavoriteMemeParams): Promise; - - abstract updatePlaceholder(userId: UserID, memeId: MemeID, placeholder: string): Promise; - - abstract delete(userId: UserID, memeId: MemeID): Promise; - - abstract deleteAllByUserId(userId: UserID): Promise; - - abstract count(userId: UserID): Promise; -} diff --git a/fluxer_api/src/api/guild/GuildAuditLogService.ts b/fluxer_api/src/api/guild/GuildAuditLogService.ts index 1082747c0..d7b159d51 100644 --- a/fluxer_api/src/api/guild/GuildAuditLogService.ts +++ b/fluxer_api/src/api/guild/GuildAuditLogService.ts @@ -9,7 +9,7 @@ import { THREAD_SCOPED_AUDIT_OPTION, } from '@app/api/guild/GuildAuditLogEntryMapper'; import type {AuditLogChange, GuildAuditLogChange} from '@app/api/guild/GuildAuditLogTypes'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService'; import {Logger} from '@app/api/Logger'; @@ -61,7 +61,7 @@ function normalizeAuditLogMetadata(metadata?: CreateGuildAuditLogParams['metadat export class GuildAuditLogService { constructor( - private readonly guildRepository: IGuildRepositoryAggregate, + private readonly guildRepository: GuildRepository, private readonly snowflakeService: ISnowflakeService, private readonly workerService: IWorkerService, private readonly gatewayService: IGatewayService, diff --git a/fluxer_api/src/api/guild/controllers/GuildBaseController.ts b/fluxer_api/src/api/guild/controllers/GuildBaseController.ts index e01c54149..bae66cd46 100644 --- a/fluxer_api/src/api/guild/controllers/GuildBaseController.ts +++ b/fluxer_api/src/api/guild/controllers/GuildBaseController.ts @@ -85,7 +85,7 @@ export function GuildBaseController(app: HonoApp) { } const auditLogReason = ctx.get('auditLogReason') ?? null; const locale = ctx.get('requestLocale') ?? null; - return ctx.json(await ctx.get('guildService').data.createGuild({user, data, locale}, auditLogReason)); + return ctx.json(await ctx.get('guildService').data.operations.createGuild({user, data, locale}, auditLogReason)); }, ); app.get( @@ -106,7 +106,7 @@ export function GuildBaseController(app: HonoApp) { async (ctx) => { const userId = ctx.get('user').id; const {before, after, limit, with_counts} = ctx.req.valid('query'); - const guilds = await ctx.get('guildService').data.getUserGuilds(userId, { + const guilds = await ctx.get('guildService').data.operations.getUserGuilds(userId, { before: before != null ? createGuildID(before) : undefined, after: after != null ? createGuildID(after) : undefined, limit, @@ -148,7 +148,7 @@ export function GuildBaseController(app: HonoApp) { const {delete_messages} = ctx.req.valid('query'); const body = ctx.req.valid('json'); if (delete_messages) { - await ctx.get('guildService').data.getGuild({userId, guildId}); + await ctx.get('guildService').data.operations.getGuild({userId, guildId}); await requireSudoMode(ctx, user, body); await ctx.get('channelService').userMessageDeletion.deleteUserMessagesInScope(userId, {guildId}); } @@ -178,7 +178,7 @@ export function GuildBaseController(app: HonoApp) { const userId = user.id; const guildId = createGuildID(ctx.req.valid('param').guild_id); const body = ctx.req.valid('json'); - await ctx.get('guildService').data.getGuild({userId, guildId}); + await ctx.get('guildService').data.operations.getGuild({userId, guildId}); await requireSudoMode(ctx, user, body); await ctx.get('channelService').userMessageDeletion.deleteUserMessagesInScope(userId, {guildId}); return ctx.body(null, 202); @@ -203,7 +203,7 @@ export function GuildBaseController(app: HonoApp) { async (ctx) => { const userId = ctx.get('user').id; const guildId = createGuildID(ctx.req.valid('param').guild_id); - const guild = await ctx.get('guildService').data.getGuild({userId, guildId}); + const guild = await ctx.get('guildService').data.operations.getGuild({userId, guildId}); return ctx.json(await maskGuildResponseThreadBits(guildId, viewerFromCtx(ctx), guild)); }, ); @@ -231,7 +231,7 @@ export function GuildBaseController(app: HonoApp) { const data = ctx.req.valid('json'); let shouldRequireSudoMode = false; if (data.mfa_level !== undefined) { - const currentGuild = await ctx.get('guildService').data.getGuild({userId, guildId}); + const currentGuild = await ctx.get('guildService').data.operations.getGuild({userId, guildId}); shouldRequireSudoMode = currentGuild.mfa_level !== data.mfa_level; } if (shouldRequireSudoMode) { @@ -270,7 +270,7 @@ export function GuildBaseController(app: HonoApp) { const body = ctx.req.valid('json'); await requireSudoMode(ctx, user, body); const auditLogReason = ctx.get('auditLogReason') ?? null; - await ctx.get('guildService').data.deleteGuild({user, guildId}, auditLogReason); + await ctx.get('guildService').data.operations.deleteGuild({user, guildId}, auditLogReason); return ctx.body(null, 204); }, ); @@ -291,7 +291,7 @@ export function GuildBaseController(app: HonoApp) { async (ctx) => { const userId = ctx.get('user').id; const guildId = createGuildID(ctx.req.valid('param').guild_id); - return ctx.json(await ctx.get('guildService').data.getVanityURL({userId, guildId})); + return ctx.json(await ctx.get('guildService').data.vanity.getVanityURL({userId, guildId})); }, ); app.patch( @@ -317,7 +317,7 @@ export function GuildBaseController(app: HonoApp) { const auditLogReason = ctx.get('auditLogReason') ?? null; const {code: newCode} = await ctx .get('guildService') - .data.updateVanityURL({userId, guildId, code: code ?? null, requestCache}, auditLogReason); + .data.vanity.updateVanityURL({userId, guildId, code: code ?? null, requestCache}, auditLogReason); return ctx.json({code: newCode}); }, ); diff --git a/fluxer_api/src/api/guild/controllers/GuildDiscoveryController.ts b/fluxer_api/src/api/guild/controllers/GuildDiscoveryController.ts index f550756bd..b73f1a1a3 100644 --- a/fluxer_api/src/api/guild/controllers/GuildDiscoveryController.ts +++ b/fluxer_api/src/api/guild/controllers/GuildDiscoveryController.ts @@ -151,7 +151,7 @@ export function GuildDiscoveryController(app: HonoApp) { if (!status || status.status !== DiscoveryApplicationStatus.APPROVED) { throw new DiscoveryNotDiscoverableError(); } - const guild = await ctx.get('guildService').data.getGuildSystem(guildId); + const guild = await ctx.get('guildService').data.operations.getGuildSystem(guildId); if (guild.features.has(GuildFeatures.INVITES_DISABLED)) { throw new InvitesDisabledError(); } diff --git a/fluxer_api/src/api/guild/controllers/GuildEmojiController.ts b/fluxer_api/src/api/guild/controllers/GuildEmojiController.ts index 2aa3a38eb..cd3432db7 100644 --- a/fluxer_api/src/api/guild/controllers/GuildEmojiController.ts +++ b/fluxer_api/src/api/guild/controllers/GuildEmojiController.ts @@ -45,7 +45,9 @@ export function GuildEmojiController(app: HonoApp) { const guildId = createGuildID(ctx.req.valid('param').guild_id); const {name, image} = ctx.req.valid('json'); const auditLogReason = ctx.get('auditLogReason') ?? null; - const emoji = await ctx.get('guildService').content.createEmoji({user, guildId, name, image}, auditLogReason); + const emoji = await ctx + .get('guildService') + .content.emojis.createEmoji({user, guildId, name, image}, auditLogReason); return ctx.json(emoji); }, ); @@ -70,7 +72,9 @@ export function GuildEmojiController(app: HonoApp) { const guildId = createGuildID(ctx.req.valid('param').guild_id); const {emojis} = ctx.req.valid('json'); const auditLogReason = ctx.get('auditLogReason') ?? null; - const result = await ctx.get('guildService').content.bulkCreateEmojis({user, guildId, emojis}, auditLogReason); + const result = await ctx + .get('guildService') + .content.emojis.bulkCreateEmojis({user, guildId, emojis}, auditLogReason); return ctx.json(result); }, ); @@ -96,7 +100,9 @@ export function GuildEmojiController(app: HonoApp) { const {source_emoji_id} = ctx.req.valid('json'); const sourceEmojiId = createEmojiID(source_emoji_id); const auditLogReason = ctx.get('auditLogReason') ?? null; - const emoji = await ctx.get('guildService').content.cloneEmoji({user, guildId, sourceEmojiId}, auditLogReason); + const emoji = await ctx + .get('guildService') + .content.emojis.cloneEmoji({user, guildId, sourceEmojiId}, auditLogReason); return ctx.json(emoji); }, ); @@ -120,7 +126,7 @@ export function GuildEmojiController(app: HonoApp) { const userId = ctx.get('user').id; const guildId = createGuildID(guild_id); const requestCache = ctx.get('requestCache'); - return ctx.json(await ctx.get('guildService').content.getEmojis({userId, guildId, requestCache})); + return ctx.json(await ctx.get('guildService').content.emojis.getEmojis({userId, guildId, requestCache})); }, ); app.patch( @@ -146,7 +152,9 @@ export function GuildEmojiController(app: HonoApp) { const emojiId = createEmojiID(emoji_id); const {name} = ctx.req.valid('json'); const auditLogReason = ctx.get('auditLogReason') ?? null; - const emoji = await ctx.get('guildService').content.updateEmoji({userId, guildId, emojiId, name}, auditLogReason); + const emoji = await ctx + .get('guildService') + .content.emojis.updateEmoji({userId, guildId, emojiId, name}, auditLogReason); return ctx.json(emoji); }, ); @@ -174,7 +182,7 @@ export function GuildEmojiController(app: HonoApp) { const emojiId = createEmojiID(emoji_id); const auditLogReason = ctx.get('auditLogReason') ?? null; const {purge = false} = ctx.req.valid('query'); - await ctx.get('guildService').content.deleteEmoji({userId, guildId, emojiId, purge}, auditLogReason); + await ctx.get('guildService').content.emojis.deleteEmoji({userId, guildId, emojiId, purge}, auditLogReason); return ctx.body(null, 204); }, ); diff --git a/fluxer_api/src/api/guild/controllers/GuildMemberController.ts b/fluxer_api/src/api/guild/controllers/GuildMemberController.ts index 4c1d26d94..b45eb03a0 100644 --- a/fluxer_api/src/api/guild/controllers/GuildMemberController.ts +++ b/fluxer_api/src/api/guild/controllers/GuildMemberController.ts @@ -217,7 +217,7 @@ export function GuildMemberController(app: HonoApp) { const newOwnerId = createUserID(new_owner_id); const auditLogReason = ctx.get('auditLogReason') ?? null; return ctx.json( - await ctx.get('guildService').data.transferOwnership({userId, guildId, newOwnerId}, auditLogReason), + await ctx.get('guildService').data.ownership.transferOwnership({userId, guildId, newOwnerId}, auditLogReason), ); }, ); diff --git a/fluxer_api/src/api/guild/controllers/GuildStickerController.ts b/fluxer_api/src/api/guild/controllers/GuildStickerController.ts index 01ade3464..0cbd78cad 100644 --- a/fluxer_api/src/api/guild/controllers/GuildStickerController.ts +++ b/fluxer_api/src/api/guild/controllers/GuildStickerController.ts @@ -51,7 +51,7 @@ export function GuildStickerController(app: HonoApp) { const auditLogReason = ctx.get('auditLogReason') ?? null; const sticker = await ctx .get('guildService') - .content.createSticker({user, guildId, name, description, tags, image}, auditLogReason); + .content.stickers.createSticker({user, guildId, name, description, tags, image}, auditLogReason); return ctx.json(sticker); }, ); @@ -78,7 +78,7 @@ export function GuildStickerController(app: HonoApp) { const auditLogReason = ctx.get('auditLogReason') ?? null; const result = await ctx .get('guildService') - .content.bulkCreateStickers({user, guildId, stickers}, auditLogReason); + .content.stickers.bulkCreateStickers({user, guildId, stickers}, auditLogReason); return ctx.json(result); }, ); @@ -106,7 +106,7 @@ export function GuildStickerController(app: HonoApp) { const auditLogReason = ctx.get('auditLogReason') ?? null; const sticker = await ctx .get('guildService') - .content.cloneSticker({user, guildId, sourceStickerId}, auditLogReason); + .content.stickers.cloneSticker({user, guildId, sourceStickerId}, auditLogReason); return ctx.json(sticker); }, ); @@ -130,7 +130,7 @@ export function GuildStickerController(app: HonoApp) { const userId = ctx.get('user').id; const guildId = createGuildID(guild_id); const requestCache = ctx.get('requestCache'); - return ctx.json(await ctx.get('guildService').content.getStickers({userId, guildId, requestCache})); + return ctx.json(await ctx.get('guildService').content.stickers.getStickers({userId, guildId, requestCache})); }, ); app.patch( @@ -159,7 +159,7 @@ export function GuildStickerController(app: HonoApp) { return ctx.json( await ctx .get('guildService') - .content.updateSticker({userId, guildId, stickerId, name, description, tags}, auditLogReason), + .content.stickers.updateSticker({userId, guildId, stickerId, name, description, tags}, auditLogReason), ); }, ); @@ -187,7 +187,7 @@ export function GuildStickerController(app: HonoApp) { const stickerId = createStickerID(sticker_id); const auditLogReason = ctx.get('auditLogReason') ?? null; const {purge = false} = ctx.req.valid('query'); - await ctx.get('guildService').content.deleteSticker({userId, guildId, stickerId, purge}, auditLogReason); + await ctx.get('guildService').content.stickers.deleteSticker({userId, guildId, stickerId, purge}, auditLogReason); return ctx.body(null, 204); }, ); diff --git a/fluxer_api/src/api/guild/repositories/GuildContentRepository.ts b/fluxer_api/src/api/guild/repositories/GuildContentRepository.ts index eeb561106..e90b452ef 100644 --- a/fluxer_api/src/api/guild/repositories/GuildContentRepository.ts +++ b/fluxer_api/src/api/guild/repositories/GuildContentRepository.ts @@ -9,7 +9,6 @@ import { type GuildEmojiRow, type GuildStickerRow, } from '@app/api/database/types/GuildTypes'; -import {IGuildContentRepository} from '@app/api/guild/repositories/IGuildContentRepository'; import {GuildEmoji} from '@app/api/models/GuildEmoji'; import {GuildSticker} from '@app/api/models/GuildSticker'; import {GuildEmojis, GuildEmojisByEmojiId, GuildStickers, GuildStickersByStickerId} from '@app/api/Tables'; @@ -37,7 +36,7 @@ const FETCH_GUILD_STICKER_BY_STICKER_ID_ONLY_QUERY = GuildStickersByStickerId.se limit: 1, }); -export class GuildContentRepository extends IGuildContentRepository { +export class GuildContentRepository { async getEmoji(emojiId: EmojiID, guildId: GuildID): Promise { const emoji = await fetchOne(FETCH_GUILD_EMOJI_BY_ID_QUERY, { guild_id: guildId, diff --git a/fluxer_api/src/api/guild/repositories/GuildDataRepository.ts b/fluxer_api/src/api/guild/repositories/GuildDataRepository.ts index 8951d80ef..4564cc280 100644 --- a/fluxer_api/src/api/guild/repositories/GuildDataRepository.ts +++ b/fluxer_api/src/api/guild/repositories/GuildDataRepository.ts @@ -4,7 +4,6 @@ import type {GuildID, UserID} from '@app/api/BrandedTypes'; import {BatchBuilder, fetchMany, fetchOne} from '@app/api/database/CassandraQueryExecution'; import {buildPatchFromData, executeVersionedUpdate} from '@app/api/database/CassandraVersionedUpdate'; import {GUILD_COLUMNS, type GuildMemberByUserIdRow, type GuildRow} from '@app/api/database/types/GuildTypes'; -import {IGuildDataRepository} from '@app/api/guild/repositories/IGuildDataRepository'; import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware'; import {Guild} from '@app/api/models/Guild'; import {GuildMembersByUserId, Guilds} from '@app/api/Tables'; @@ -26,10 +25,8 @@ function createFetchAllGuildsFirstPageQuery(limit: number) { return Guilds.select({limit}); } -export class GuildDataRepository extends IGuildDataRepository { - constructor(private readonly requestCache?: RequestCache) { - super(); - } +export class GuildDataRepository { + constructor(private readonly requestCache?: RequestCache) {} async findUnique(guildId: GuildID): Promise { const prefetched = this.requestCache?.takeGuild(guildId); diff --git a/fluxer_api/src/api/guild/repositories/GuildDiscoveryRepository.ts b/fluxer_api/src/api/guild/repositories/GuildDiscoveryRepository.ts index 102ddfaf0..427d44822 100644 --- a/fluxer_api/src/api/guild/repositories/GuildDiscoveryRepository.ts +++ b/fluxer_api/src/api/guild/repositories/GuildDiscoveryRepository.ts @@ -14,26 +14,7 @@ const FETCH_DISCOVERY_BY_STATUS = GuildDiscoveryByStatus.selectCql({ where: GuildDiscoveryByStatus.where.eq('status'), }); -export abstract class IGuildDiscoveryRepository { - abstract findByGuildId(guildId: GuildID): Promise; - - abstract listByStatus(status: string): Promise>; - - abstract listFullByStatus(status: string): Promise>; - - abstract upsert(row: GuildDiscoveryRow): Promise; - - abstract deleteByGuildId(guildId: GuildID, status: string, appliedAt: Date): Promise; - - abstract updateStatus( - guildId: GuildID, - oldStatus: string, - oldAppliedAt: Date, - updatedRow: GuildDiscoveryRow, - ): Promise; -} - -export class GuildDiscoveryRepository extends IGuildDiscoveryRepository { +export class GuildDiscoveryRepository { async findByGuildId(guildId: GuildID): Promise { const row = await fetchOne(FETCH_DISCOVERY_BY_GUILD_ID, { guild_id: guildId, diff --git a/fluxer_api/src/api/guild/repositories/GuildMemberRepository.ts b/fluxer_api/src/api/guild/repositories/GuildMemberRepository.ts index 8d73613b1..c8f76aaaf 100644 --- a/fluxer_api/src/api/guild/repositories/GuildMemberRepository.ts +++ b/fluxer_api/src/api/guild/repositories/GuildMemberRepository.ts @@ -5,7 +5,6 @@ import {BatchBuilder, fetchMany, fetchOne, upsertOne} from '@app/api/database/Ca import {buildPatchFromData, executeVersionedUpdate} from '@app/api/database/CassandraVersionedUpdate'; import type {GuildMemberRow, GuildMembershipMetadataRow} from '@app/api/database/types/GuildTypes'; import {GUILD_MEMBER_COLUMNS} from '@app/api/database/types/GuildTypes'; -import {IGuildMemberRepository} from '@app/api/guild/repositories/IGuildMemberRepository'; import {GuildMember} from '@app/api/models/GuildMember'; import {GuildMembers, GuildMembersByUserId, GuildMembershipMetadata} from '@app/api/Tables'; @@ -38,7 +37,7 @@ function createPaginatedQuery(limit: number) { }); } -export class GuildMemberRepository extends IGuildMemberRepository { +export class GuildMemberRepository { async getMember(guildId: GuildID, userId: UserID): Promise { const member = await fetchOne(FETCH_GUILD_MEMBER_BY_GUILD_AND_USER_ID_QUERY, { guild_id: guildId, diff --git a/fluxer_api/src/api/guild/repositories/GuildModerationRepository.ts b/fluxer_api/src/api/guild/repositories/GuildModerationRepository.ts index 5e663fab3..d2cee4a01 100644 --- a/fluxer_api/src/api/guild/repositories/GuildModerationRepository.ts +++ b/fluxer_api/src/api/guild/repositories/GuildModerationRepository.ts @@ -9,7 +9,6 @@ import type { GuildBanByUserIdRow, GuildBanRow, } from '@app/api/database/types/GuildTypes'; -import {IGuildModerationRepository} from '@app/api/guild/repositories/IGuildModerationRepository'; import {GuildAuditLog} from '@app/api/models/GuildAuditLog'; import {GuildBan} from '@app/api/models/GuildBan'; import { @@ -47,7 +46,7 @@ const FETCH_GUILD_AUDIT_LOGS_BY_IDS_QUERY = GuildAuditLogs.selectCql({ where: [GuildAuditLogs.where.eq('guild_id'), GuildAuditLogs.where.in('log_id', 'log_ids')], }); -export class GuildModerationRepository extends IGuildModerationRepository { +export class GuildModerationRepository { async getBan(guildId: GuildID, userId: UserID): Promise { const ban = await fetchOne(FETCH_GUILD_BAN_BY_GUILD_AND_USER_ID_QUERY, { guild_id: guildId, diff --git a/fluxer_api/src/api/guild/repositories/GuildRepository.ts b/fluxer_api/src/api/guild/repositories/GuildRepository.ts index 983054d7b..4133fbebe 100644 --- a/fluxer_api/src/api/guild/repositories/GuildRepository.ts +++ b/fluxer_api/src/api/guild/repositories/GuildRepository.ts @@ -18,7 +18,6 @@ import {GuildDataRepository} from '@app/api/guild/repositories/GuildDataReposito import {GuildMemberRepository} from '@app/api/guild/repositories/GuildMemberRepository'; import {GuildModerationRepository} from '@app/api/guild/repositories/GuildModerationRepository'; import {GuildRoleRepository} from '@app/api/guild/repositories/GuildRoleRepository'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware'; import type {Guild} from '@app/api/models/Guild'; import type {GuildAuditLog} from '@app/api/models/GuildAuditLog'; @@ -29,7 +28,7 @@ import type {GuildRole} from '@app/api/models/GuildRole'; import type {GuildSticker} from '@app/api/models/GuildSticker'; import type {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType'; -export class GuildRepository implements IGuildRepositoryAggregate { +export class GuildRepository { private dataRepo: GuildDataRepository; private memberRepo: GuildMemberRepository; private roleRepo: GuildRoleRepository; diff --git a/fluxer_api/src/api/guild/repositories/GuildRoleRepository.ts b/fluxer_api/src/api/guild/repositories/GuildRoleRepository.ts index 366f3ff25..e1f2041e3 100644 --- a/fluxer_api/src/api/guild/repositories/GuildRoleRepository.ts +++ b/fluxer_api/src/api/guild/repositories/GuildRoleRepository.ts @@ -5,7 +5,6 @@ import {deleteOneOrMany, fetchMany, fetchOne} from '@app/api/database/CassandraQ import {buildPatchFromData, executeVersionedUpdate} from '@app/api/database/CassandraVersionedUpdate'; import type {GuildRoleRow} from '@app/api/database/types/GuildTypes'; import {GUILD_ROLE_COLUMNS} from '@app/api/database/types/GuildTypes'; -import {IGuildRoleRepository} from '@app/api/guild/repositories/IGuildRoleRepository'; import {GuildRole} from '@app/api/models/GuildRole'; import {GuildRoles} from '@app/api/Tables'; @@ -20,7 +19,7 @@ const FETCH_ROLES_BY_IDS_QUERY = GuildRoles.selectCql({ where: [GuildRoles.where.eq('guild_id'), GuildRoles.where.in('role_id', 'role_ids')], }); -export class GuildRoleRepository extends IGuildRoleRepository { +export class GuildRoleRepository { async getRole(roleId: RoleID, guildId: GuildID): Promise { const role = await fetchOne(FETCH_GUILD_ROLE_BY_ID_QUERY, { guild_id: guildId, diff --git a/fluxer_api/src/api/guild/repositories/IGuildContentRepository.ts b/fluxer_api/src/api/guild/repositories/IGuildContentRepository.ts deleted file mode 100644 index 667ffabba..000000000 --- a/fluxer_api/src/api/guild/repositories/IGuildContentRepository.ts +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {EmojiID, GuildID, StickerID} from '@app/api/BrandedTypes'; -import type {GuildEmojiRow, GuildStickerRow} from '@app/api/database/types/GuildTypes'; -import type {GuildEmoji} from '@app/api/models/GuildEmoji'; -import type {GuildSticker} from '@app/api/models/GuildSticker'; - -export abstract class IGuildContentRepository { - abstract getEmoji(emojiId: EmojiID, guildId: GuildID): Promise; - - abstract getEmojiById(emojiId: EmojiID): Promise; - - abstract listEmojis(guildId: GuildID): Promise>; - - abstract upsertEmoji(data: GuildEmojiRow): Promise; - - abstract deleteEmoji(guildId: GuildID, emojiId: EmojiID): Promise; - - abstract getSticker(stickerId: StickerID, guildId: GuildID): Promise; - - abstract getStickerById(stickerId: StickerID): Promise; - - abstract listStickers(guildId: GuildID): Promise>; - - abstract upsertSticker(data: GuildStickerRow): Promise; - - abstract deleteSticker(guildId: GuildID, stickerId: StickerID): Promise; -} diff --git a/fluxer_api/src/api/guild/repositories/IGuildDataRepository.ts b/fluxer_api/src/api/guild/repositories/IGuildDataRepository.ts deleted file mode 100644 index 9ea2d3dec..000000000 --- a/fluxer_api/src/api/guild/repositories/IGuildDataRepository.ts +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {GuildID, UserID} from '@app/api/BrandedTypes'; -import type {GuildRow} from '@app/api/database/types/GuildTypes'; -import type {Guild} from '@app/api/models/Guild'; - -export abstract class IGuildDataRepository { - abstract findUnique(guildId: GuildID): Promise; - - abstract listGuilds(guildIds: Array): Promise>; - - abstract listAllGuildsPaginated(limit: number, lastGuildId?: GuildID): Promise>; - - abstract listUserGuilds(userId: UserID): Promise>; - - abstract countUserGuilds(userId: UserID): Promise; - - abstract listOwnedGuildIds(userId: UserID): Promise>; - - abstract upsert(data: GuildRow, oldData?: GuildRow | null, previousOwnerId?: UserID): Promise; - - abstract upsertPartial( - guildId: GuildID, - patch: Partial, - oldData?: GuildRow | null, - previousOwnerId?: UserID, - ): Promise; - - abstract delete(guildId: GuildID, ownerId?: UserID): Promise; -} diff --git a/fluxer_api/src/api/guild/repositories/IGuildMemberRepository.ts b/fluxer_api/src/api/guild/repositories/IGuildMemberRepository.ts deleted file mode 100644 index 1995bd4b9..000000000 --- a/fluxer_api/src/api/guild/repositories/IGuildMemberRepository.ts +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {GuildID, UserID} from '@app/api/BrandedTypes'; -import type {GuildMemberRow, GuildMembershipMetadataRow} from '@app/api/database/types/GuildTypes'; -import type {GuildMember} from '@app/api/models/GuildMember'; - -export abstract class IGuildMemberRepository { - abstract getMember(guildId: GuildID, userId: UserID): Promise; - - abstract listMembers(guildId: GuildID): Promise>; - - abstract countMembers(guildId: GuildID): Promise; - - abstract upsertMember(data: GuildMemberRow): Promise; - - abstract deleteMember(guildId: GuildID, userId: UserID): Promise; - - abstract listMembersPaginated(guildId: GuildID, limit: number, afterUserId?: UserID): Promise>; - - abstract getMembershipMetadata(guildId: GuildID, userId: UserID): Promise; - - abstract upsertMembershipMetadata(data: GuildMembershipMetadataRow, ttlSeconds: number): Promise; -} diff --git a/fluxer_api/src/api/guild/repositories/IGuildModerationRepository.ts b/fluxer_api/src/api/guild/repositories/IGuildModerationRepository.ts deleted file mode 100644 index 45203ff98..000000000 --- a/fluxer_api/src/api/guild/repositories/IGuildModerationRepository.ts +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {GuildID, UserID} from '@app/api/BrandedTypes'; -import type {GuildAuditLogRow, GuildBanRow} from '@app/api/database/types/GuildTypes'; -import type {GuildAuditLog} from '@app/api/models/GuildAuditLog'; -import type {GuildBan} from '@app/api/models/GuildBan'; -import type {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType'; - -export abstract class IGuildModerationRepository { - abstract getBan(guildId: GuildID, userId: UserID): Promise; - - abstract listBans(guildId: GuildID): Promise>; - - abstract upsertBan(data: GuildBanRow): Promise; - - abstract deleteBan(guildId: GuildID, userId: UserID): Promise; - - abstract deleteAllBansForUser(userId: UserID): Promise; - - abstract getBanByEmail(guildId: GuildID, email: string): Promise; - - abstract createAuditLog(data: GuildAuditLogRow): Promise; - - abstract batchDeleteAndCreateAuditLogs( - guildId: GuildID, - logsToDelete: Array, - logToCreate: GuildAuditLogRow, - ): Promise; - - abstract getAuditLog(guildId: GuildID, logId: bigint): Promise; - - abstract listAuditLogs(params: { - guildId: GuildID; - limit: number; - afterLogId?: bigint; - beforeLogId?: bigint; - userId?: UserID; - actionType?: AuditLogActionType; - }): Promise>; - - abstract listAuditLogsByIds(guildId: GuildID, logIds: Array): Promise>; - - abstract deleteAuditLogs(guildId: GuildID, logs: Array): Promise; -} diff --git a/fluxer_api/src/api/guild/repositories/IGuildRepositoryAggregate.ts b/fluxer_api/src/api/guild/repositories/IGuildRepositoryAggregate.ts deleted file mode 100644 index 08e868b23..000000000 --- a/fluxer_api/src/api/guild/repositories/IGuildRepositoryAggregate.ts +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {IGuildContentRepository} from '@app/api/guild/repositories/IGuildContentRepository'; -import type {IGuildDataRepository} from '@app/api/guild/repositories/IGuildDataRepository'; -import type {IGuildMemberRepository} from '@app/api/guild/repositories/IGuildMemberRepository'; -import type {IGuildModerationRepository} from '@app/api/guild/repositories/IGuildModerationRepository'; -import type {IGuildRoleRepository} from '@app/api/guild/repositories/IGuildRoleRepository'; - -export interface IGuildRepositoryAggregate - extends IGuildDataRepository, - IGuildMemberRepository, - IGuildRoleRepository, - IGuildModerationRepository, - IGuildContentRepository {} diff --git a/fluxer_api/src/api/guild/repositories/IGuildRoleRepository.ts b/fluxer_api/src/api/guild/repositories/IGuildRoleRepository.ts deleted file mode 100644 index 15f6f315e..000000000 --- a/fluxer_api/src/api/guild/repositories/IGuildRoleRepository.ts +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {GuildID, RoleID} from '@app/api/BrandedTypes'; -import type {GuildRoleRow} from '@app/api/database/types/GuildTypes'; -import type {GuildRole} from '@app/api/models/GuildRole'; - -export abstract class IGuildRoleRepository { - abstract getRole(roleId: RoleID, guildId: GuildID): Promise; - - abstract listRoles(guildId: GuildID): Promise>; - - abstract listRolesByIds(roleIds: Array, guildId: GuildID): Promise>; - - abstract countRoles(guildId: GuildID): Promise; - - abstract upsertRole(data: GuildRoleRow, oldData?: GuildRoleRow | null): Promise; - - abstract deleteRole(guildId: GuildID, roleId: RoleID): Promise; -} diff --git a/fluxer_api/src/api/guild/services/GuildChannelService.ts b/fluxer_api/src/api/guild/services/GuildChannelService.ts index 9eb324223..a63524922 100644 --- a/fluxer_api/src/api/guild/services/GuildChannelService.ts +++ b/fluxer_api/src/api/guild/services/GuildChannelService.ts @@ -2,11 +2,11 @@ import type {ChannelID, GuildID, UserID} from '@app/api/BrandedTypes'; import {mapChannelToResponse} from '@app/api/channel/ChannelMappers'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import {withThreadParentFieldsMany} from '@app/api/channel/services/thread/ThreadParentSettings'; import {SYSTEM_THREAD_VIEWER, type ThreadViewer, viewerActive} from '@app/api/experiment/ChannelThreadsGate'; import type {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import {ChannelOperationsService} from '@app/api/guild/services/channel/ChannelOperationsService'; import {createGuildMfaEnforcer} from '@app/api/guild/services/GuildMfaEnforcement'; import {maskChannelResponseThreadBits} from '@app/api/guild/services/ThreadPermissionBits'; @@ -15,7 +15,7 @@ import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService' import type {UserCacheService} from '@app/api/infrastructure/UserCacheService'; import type {LimitConfigService} from '@app/api/limits/LimitConfigService'; import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {Permissions} from '@fluxer/constants/src/ChannelConstants'; import {MissingPermissionsError} from '@fluxer/errors/src/domains/core/MissingPermissionsError'; import {UnknownGuildError} from '@fluxer/errors/src/domains/guild/UnknownGuildError'; @@ -27,15 +27,15 @@ export class GuildChannelService { private readonly channelOps: ChannelOperationsService; constructor( - private readonly channelRepository: IChannelRepository, - guildRepository: IGuildRepositoryAggregate, + private readonly channelRepository: ChannelRepository, + guildRepository: GuildRepository, private readonly userCacheService: UserCacheService, private readonly gatewayService: IGatewayService, cacheService: ICacheService, snowflakeService: ISnowflakeService, guildAuditLogService: GuildAuditLogService, limitConfigService: LimitConfigService, - private readonly userRepository: IUserRepository, + private readonly userRepository: UserRepository, ) { this.channelOps = new ChannelOperationsService( channelRepository, @@ -67,7 +67,7 @@ export class GuildChannelService { guildId: params.guildId, userId: params.userId, }); - const channels = await this.channelRepository.listGuildChannels(params.guildId, 'enrolled'); + const channels = await this.channelRepository.channelData.listGuildChannels(params.guildId, 'enrolled'); const viewer = params.viewer; const viewableChannels = channels.filter( (channel) => diff --git a/fluxer_api/src/api/guild/services/GuildContentService.ts b/fluxer_api/src/api/guild/services/GuildContentService.ts index d57f30a25..c53f63d00 100644 --- a/fluxer_api/src/api/guild/services/GuildContentService.ts +++ b/fluxer_api/src/api/guild/services/GuildContentService.ts @@ -1,8 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import type {EmojiID, GuildID, StickerID, UserID} from '@app/api/BrandedTypes'; import type {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import {ContentHelpers} from '@app/api/guild/services/content/ContentHelpers'; import {EmojiService} from '@app/api/guild/services/content/EmojiService'; import {ExpressionAssetPurger} from '@app/api/guild/services/content/ExpressionAssetPurger'; @@ -13,22 +12,14 @@ import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService'; import type {UserCacheService} from '@app/api/infrastructure/UserCacheService'; import type {LimitConfigService} from '@app/api/limits/LimitConfigService'; -import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware'; -import type {User} from '@app/api/models/User'; -import type { - GuildEmojiResponse, - GuildEmojiWithUserResponse, - GuildStickerResponse, - GuildStickerWithUserResponse, -} from '@fluxer/schema/src/domains/guild/GuildEmojiSchemas'; export class GuildContentService { private readonly contentHelpers: ContentHelpers; - private readonly emojiService: EmojiService; - private readonly stickerService: StickerService; + readonly emojis: EmojiService; + readonly stickers: StickerService; constructor( - guildRepository: IGuildRepositoryAggregate, + guildRepository: GuildRepository, userCacheService: UserCacheService, gatewayService: IGatewayService, avatarService: AvatarService, @@ -39,7 +30,7 @@ export class GuildContentService { ) { this.contentHelpers = new ContentHelpers(gatewayService, guildAuditLogService); const expressionAssetPurger = new ExpressionAssetPurger(assetDeletionQueue); - this.emojiService = new EmojiService( + this.emojis = new EmojiService( guildRepository, userCacheService, gatewayService, @@ -49,7 +40,7 @@ export class GuildContentService { expressionAssetPurger, limitConfigService, ); - this.stickerService = new StickerService( + this.stickers = new StickerService( guildRepository, userCacheService, gatewayService, @@ -60,160 +51,4 @@ export class GuildContentService { limitConfigService, ); } - - async getEmojis(params: { - userId: UserID; - guildId: GuildID; - requestCache: RequestCache; - }): Promise> { - return this.emojiService.getEmojis(params); - } - - async createEmoji( - params: { - user: User; - guildId: GuildID; - name: string; - image: string; - }, - auditLogReason?: string | null, - ): Promise { - return this.emojiService.createEmoji(params, auditLogReason); - } - - async cloneEmoji( - params: { - user: User; - guildId: GuildID; - sourceEmojiId: EmojiID; - }, - auditLogReason?: string | null, - ): Promise { - return this.emojiService.cloneEmoji(params, auditLogReason); - } - - async bulkCreateEmojis( - params: { - user: User; - guildId: GuildID; - emojis: Array<{ - name: string; - image: string; - }>; - }, - auditLogReason?: string | null, - ): Promise<{ - success: Array; - failed: Array<{ - name: string; - error: string; - }>; - }> { - return this.emojiService.bulkCreateEmojis(params, auditLogReason); - } - - async updateEmoji( - params: { - userId: UserID; - guildId: GuildID; - emojiId: EmojiID; - name: string; - }, - auditLogReason?: string | null, - ): Promise { - return this.emojiService.updateEmoji(params, auditLogReason); - } - - async deleteEmoji( - params: { - userId: UserID; - guildId: GuildID; - emojiId: EmojiID; - purge?: boolean; - }, - auditLogReason?: string | null, - ): Promise { - return this.emojiService.deleteEmoji(params, auditLogReason); - } - - async getStickers(params: { - userId: UserID; - guildId: GuildID; - requestCache: RequestCache; - }): Promise> { - return this.stickerService.getStickers(params); - } - - async createSticker( - params: { - user: User; - guildId: GuildID; - name: string; - description?: string | null; - tags: Array; - image: string; - }, - auditLogReason?: string | null, - ): Promise { - return this.stickerService.createSticker(params, auditLogReason); - } - - async cloneSticker( - params: { - user: User; - guildId: GuildID; - sourceStickerId: StickerID; - }, - auditLogReason?: string | null, - ): Promise { - return this.stickerService.cloneSticker(params, auditLogReason); - } - - async bulkCreateStickers( - params: { - user: User; - guildId: GuildID; - stickers: Array<{ - name: string; - description?: string | null; - tags: Array; - image: string; - }>; - }, - auditLogReason?: string | null, - ): Promise<{ - success: Array; - failed: Array<{ - name: string; - error: string; - }>; - }> { - return this.stickerService.bulkCreateStickers(params, auditLogReason); - } - - async updateSticker( - params: { - userId: UserID; - guildId: GuildID; - stickerId: StickerID; - name: string; - description?: string | null; - tags: Array; - }, - auditLogReason?: string | null, - ): Promise { - return this.stickerService.updateSticker(params, auditLogReason); - } - - async deleteSticker( - params: { - userId: UserID; - guildId: GuildID; - stickerId: StickerID; - purge?: boolean; - }, - auditLogReason?: string | null, - ): Promise { - return this.stickerService.deleteSticker(params, auditLogReason); - } } diff --git a/fluxer_api/src/api/guild/services/GuildDataService.ts b/fluxer_api/src/api/guild/services/GuildDataService.ts index 60405df82..12ac839f5 100644 --- a/fluxer_api/src/api/guild/services/GuildDataService.ts +++ b/fluxer_api/src/api/guild/services/GuildDataService.ts @@ -1,11 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import type {GuildID, UserID} from '@app/api/BrandedTypes'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {ChannelService} from '@app/api/channel/services/ChannelService'; import type {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService'; import {GuildDiscoveryRepository} from '@app/api/guild/repositories/GuildDiscoveryRepository'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import {GuildDataHelpers} from '@app/api/guild/services/data/GuildDataHelpers'; import {GuildOperationsService} from '@app/api/guild/services/data/GuildOperationsService'; import {GuildOwnershipService} from '@app/api/guild/services/data/GuildOwnershipService'; @@ -15,43 +14,30 @@ import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService'; import type {InviteRepository} from '@app/api/invite/InviteRepository'; import type {LimitConfigService} from '@app/api/limits/LimitConfigService'; -import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware'; -import type {Guild} from '@app/api/models/Guild'; -import type {User} from '@app/api/models/User'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; -import type {IWebhookRepository} from '@app/api/webhook/IWebhookRepository'; -import type { - GuildCreateRequest, - GuildUpdateRequest, - GuildVanityURLUpdateResponse, -} from '@fluxer/schema/src/domains/guild/GuildRequestSchemas'; -import type { - GuildPartialResponse, - GuildResponse, - GuildVanityURLResponse, -} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; +import type {WebhookRepository} from '@app/api/webhook/WebhookRepository'; export class GuildDataService { private readonly helpers: GuildDataHelpers; - private readonly operationsService: GuildOperationsService; - private readonly vanityService: GuildVanityService; - private readonly ownershipService: GuildOwnershipService; + readonly operations: GuildOperationsService; + readonly vanity: GuildVanityService; + readonly ownership: GuildOwnershipService; constructor( - private readonly guildRepository: IGuildRepositoryAggregate, - private readonly channelRepository: IChannelRepository, + private readonly guildRepository: GuildRepository, + private readonly channelRepository: ChannelRepository, private readonly inviteRepository: InviteRepository, private readonly channelService: ChannelService, private readonly gatewayService: IGatewayService, private readonly entityAssetService: EntityAssetService, - private readonly userRepository: IUserRepository, + private readonly userRepository: UserRepository, private readonly snowflakeService: ISnowflakeService, - private readonly webhookRepository: IWebhookRepository, + private readonly webhookRepository: WebhookRepository, private readonly guildAuditLogService: GuildAuditLogService, private readonly limitConfigService: LimitConfigService, ) { this.helpers = new GuildDataHelpers(this.gatewayService, this.guildAuditLogService, this.userRepository); - this.operationsService = new GuildOperationsService( + this.operations = new GuildOperationsService( this.guildRepository, this.channelRepository, this.inviteRepository, @@ -65,99 +51,7 @@ export class GuildDataService { this.limitConfigService, new GuildDiscoveryRepository(), ); - this.vanityService = new GuildVanityService(this.guildRepository, this.inviteRepository, this.helpers); - this.ownershipService = new GuildOwnershipService(this.guildRepository, this.userRepository, this.helpers); - } - - async getGuild({userId, guildId}: {userId: UserID; guildId: GuildID}): Promise { - return this.operationsService.getGuild({userId, guildId}); - } - - async getUserGuilds( - userId: UserID, - options?: { - before?: GuildID; - after?: GuildID; - limit?: number; - withCounts?: boolean; - }, - ): Promise> { - return this.operationsService.getUserGuilds(userId, options); - } - - async getPublicGuildData(guildId: GuildID): Promise { - return this.operationsService.getPublicGuildData(guildId); - } - - async getGuildSystem(guildId: GuildID): Promise { - return this.operationsService.getGuildSystem(guildId); - } - - async createGuild( - params: { - user: User; - data: GuildCreateRequest; - locale?: string | null; - }, - auditLogReason?: string | null, - ): Promise { - return this.operationsService.createGuild(params, auditLogReason); - } - - async updateGuild( - params: { - userId: UserID; - guildId: GuildID; - data: GuildUpdateRequest; - requestCache: RequestCache; - }, - auditLogReason?: string | null, - ): Promise<{ - guild: GuildResponse; - previousFeatures: Set; - updatedFeatures: Set; - }> { - return this.operationsService.updateGuild(params, auditLogReason); - } - - async getVanityURL(params: {userId: UserID; guildId: GuildID}): Promise { - return this.vanityService.getVanityURL(params); - } - - async updateVanityURL( - params: { - userId: UserID; - guildId: GuildID; - code: string | null; - requestCache: RequestCache; - }, - auditLogReason?: string | null, - ): Promise { - return this.vanityService.updateVanityURL(params, auditLogReason); - } - - async deleteGuild( - params: { - user: User; - guildId: GuildID; - }, - auditLogReason?: string | null, - ): Promise { - return this.operationsService.deleteGuild(params, auditLogReason); - } - - async deleteGuildForAdmin(guildId: GuildID, _auditLogReason?: string | null): Promise { - return this.operationsService.deleteGuildById(guildId); - } - - async transferOwnership( - params: { - userId: UserID; - guildId: GuildID; - newOwnerId: UserID; - }, - auditLogReason?: string | null, - ): Promise { - return this.ownershipService.transferOwnership(params, auditLogReason); + this.vanity = new GuildVanityService(this.guildRepository, this.inviteRepository, this.helpers); + this.ownership = new GuildOwnershipService(this.guildRepository, this.userRepository, this.helpers); } } diff --git a/fluxer_api/src/api/guild/services/GuildDiscoveryService.ts b/fluxer_api/src/api/guild/services/GuildDiscoveryService.ts index b900e3a26..11e4d2aa1 100644 --- a/fluxer_api/src/api/guild/services/GuildDiscoveryService.ts +++ b/fluxer_api/src/api/guild/services/GuildDiscoveryService.ts @@ -2,12 +2,12 @@ import {type ChannelID, type GuildID, guildIdToRoleId, type UserID} from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; -import type {IChannelDataRepository} from '@app/api/channel/repositories/IChannelDataRepository'; +import type {ChannelDataRepository} from '@app/api/channel/repositories/ChannelDataRepository'; import type {GuildDiscoveryRow} from '@app/api/database/types/GuildDiscoveryTypes'; import {type ThreadViewer, viewerActive} from '@app/api/experiment/ChannelThreadsGate'; import {mapGuildToGuildResponse} from '@app/api/guild/GuildModel'; -import type {IGuildDiscoveryRepository} from '@app/api/guild/repositories/GuildDiscoveryRepository'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildDiscoveryRepository} from '@app/api/guild/repositories/GuildDiscoveryRepository'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import {contentModerationService} from '@app/api/infrastructure/ContentModerationService'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import {Logger} from '@app/api/Logger'; @@ -54,60 +54,6 @@ function resolveLanguage(language: string | null | undefined, fallback: string | return DISCOVERY_DEFAULT_LANGUAGE; } -export abstract class IGuildDiscoveryService { - abstract apply(params: { - guildId: GuildID; - userId: UserID; - description: string; - categoryId: number; - primaryLanguage?: string; - customTags?: ReadonlyArray; - }): Promise; - - abstract editApplication(params: { - guildId: GuildID; - userId: UserID; - data: DiscoveryApplicationPatchRequest; - }): Promise; - - abstract withdraw(params: {guildId: GuildID; userId: UserID}): Promise; - - abstract getStatus(guildId: GuildID): Promise; - - abstract approve(params: {guildId: GuildID; adminUserId: UserID; reason?: string}): Promise; - - abstract reject(params: {guildId: GuildID; adminUserId: UserID; reason: string}): Promise; - - abstract remove(params: {guildId: GuildID; adminUserId: UserID; reason: string}): Promise; - - abstract getEligibility(guildId: GuildID): Promise<{ - eligible: boolean; - min_member_count: number; - }>; - - abstract listByStatus(params: {status: string}): Promise>; - - abstract getChannelPreview( - guildId: GuildID, - channelId: ChannelID, - viewer: ThreadViewer, - ): Promise; - - abstract searchDiscoverable(params: { - query?: string; - categoryId?: number; - primaryLanguage?: string; - tag?: string; - sortBy?: string; - limit: number; - offset: number; - }): Promise<{ - guilds: Array; - total: number; - category_counts: Array; - }>; -} - interface DiscoveryCategoryCount { category_type: number; count: number; @@ -153,16 +99,14 @@ function toDiscoveryCategoryCounts( return entries.sort((left, right) => left.category_type - right.category_type); } -export class GuildDiscoveryService extends IGuildDiscoveryService { +export class GuildDiscoveryService { constructor( - private readonly discoveryRepository: IGuildDiscoveryRepository, - private readonly guildRepository: IGuildRepositoryAggregate, + private readonly discoveryRepository: GuildDiscoveryRepository, + private readonly guildRepository: GuildRepository, private readonly gatewayService: IGatewayService, private readonly guildSearchService: IGuildSearchService | null, - private readonly channelDataRepository: IChannelDataRepository, - ) { - super(); - } + private readonly channelDataRepository: ChannelDataRepository, + ) {} async apply(params: { guildId: GuildID; diff --git a/fluxer_api/src/api/guild/services/GuildMemberService.ts b/fluxer_api/src/api/guild/services/GuildMemberService.ts index 27897a888..aa1c53998 100644 --- a/fluxer_api/src/api/guild/services/GuildMemberService.ts +++ b/fluxer_api/src/api/guild/services/GuildMemberService.ts @@ -3,7 +3,7 @@ import type {GuildID, InviteCode, RoleID, UserID} from '@app/api/BrandedTypes'; import type {ChannelService} from '@app/api/channel/services/ChannelService'; import type {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import {GuildMemberAuditService} from '@app/api/guild/services/member/GuildMemberAuditService'; import {GuildMemberAuthService} from '@app/api/guild/services/member/GuildMemberAuthService'; import {GuildMemberEventService} from '@app/api/guild/services/member/GuildMemberEventService'; @@ -18,7 +18,7 @@ import type {LimitConfigService} from '@app/api/limits/LimitConfigService'; import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware'; import type {Guild} from '@app/api/models/Guild'; import type {GuildMember} from '@app/api/models/GuildMember'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType'; import type {JoinSourceType} from '@fluxer/constants/src/GuildConstants'; import {UnknownGuildMemberError} from '@fluxer/errors/src/domains/guild/UnknownGuildMemberError'; @@ -34,15 +34,15 @@ export class GuildMemberService { private readonly operationsService: GuildMemberOperationsService; private readonly roleService: GuildMemberRoleService; private readonly searchIndexService: GuildMemberSearchIndexService; - private readonly userRepository: IUserRepository; + private readonly userRepository: UserRepository; constructor( - private readonly guildRepository: IGuildRepositoryAggregate, + private readonly guildRepository: GuildRepository, channelService: ChannelService, userCacheService: UserCacheService, gatewayService: IGatewayService, entityAssetService: EntityAssetService, - userRepository: IUserRepository, + userRepository: UserRepository, rateLimitService: IRateLimitService, private readonly guildAuditLogService: GuildAuditLogService, limitConfigService: LimitConfigService, diff --git a/fluxer_api/src/api/guild/services/GuildMfaEnforcement.ts b/fluxer_api/src/api/guild/services/GuildMfaEnforcement.ts index caeb3d307..7a136fa3e 100644 --- a/fluxer_api/src/api/guild/services/GuildMfaEnforcement.ts +++ b/fluxer_api/src/api/guild/services/GuildMfaEnforcement.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {UserID} from '@app/api/BrandedTypes'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {Permissions} from '@fluxer/constants/src/ChannelConstants'; import {GuildMFALevel} from '@fluxer/constants/src/GuildConstants'; import {ThreadPermissionFlags} from '@fluxer/constants/src/ThreadPermissionUtils'; @@ -21,7 +21,7 @@ const ELEVATED_MFA_PERMISSIONS = ThreadPermissionFlags.MANAGE_THREADS; export async function createGuildMfaEnforcer(params: { - userRepository: IUserRepository; + userRepository: UserRepository; guildData: Pick; userId: UserID; }): Promise<(permission: bigint) => void> { diff --git a/fluxer_api/src/api/guild/services/GuildModerationService.ts b/fluxer_api/src/api/guild/services/GuildModerationService.ts index be7aafd88..258388a1b 100644 --- a/fluxer_api/src/api/guild/services/GuildModerationService.ts +++ b/fluxer_api/src/api/guild/services/GuildModerationService.ts @@ -5,7 +5,7 @@ import {isIpBanExempt} from '@app/api/ban/IpBanExemptions'; import type {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService'; import type {GuildAuditLogChange} from '@app/api/guild/GuildAuditLogTypes'; import {mapGuildBansToResponse} from '@app/api/guild/GuildModel'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import {createGuildMfaEnforcer} from '@app/api/guild/services/GuildMfaEnforcement'; import {GuildMemberSearchIndexService} from '@app/api/guild/services/member/GuildMemberSearchIndexService'; import type {BanBy} from '@app/api/infrastructure/activity/Contract.generated'; @@ -16,7 +16,7 @@ import {Logger} from '@app/api/Logger'; import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware'; import type {Guild} from '@app/api/models/Guild'; import type {GuildBan} from '@app/api/models/GuildBan'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import type {WorkerTaskName} from '@app/api/worker/WorkerLaneConfig'; import {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType'; import {Permissions} from '@fluxer/constants/src/ChannelConstants'; @@ -39,8 +39,8 @@ export class GuildModerationService { private readonly searchIndexService: GuildMemberSearchIndexService; constructor( - private readonly guildRepository: IGuildRepositoryAggregate, - private readonly userRepository: IUserRepository, + private readonly guildRepository: GuildRepository, + private readonly userRepository: UserRepository, private readonly gatewayService: IGatewayService, private readonly userCacheService: UserCacheService, private readonly workerService: IWorkerService, diff --git a/fluxer_api/src/api/guild/services/GuildRoleService.ts b/fluxer_api/src/api/guild/services/GuildRoleService.ts index f573451aa..c63e78621 100644 --- a/fluxer_api/src/api/guild/services/GuildRoleService.ts +++ b/fluxer_api/src/api/guild/services/GuildRoleService.ts @@ -6,8 +6,7 @@ import type {ThreadViewer} from '@app/api/experiment/ChannelThreadsGate'; import type {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService'; import type {GuildAuditLogChange} from '@app/api/guild/GuildAuditLogTypes'; import {mapGuildRoleToResponse} from '@app/api/guild/GuildModel'; -import type {IGuildMemberRepository} from '@app/api/guild/repositories/IGuildMemberRepository'; -import type {IGuildRoleRepository} from '@app/api/guild/repositories/IGuildRoleRepository'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import {createGuildMfaEnforcer} from '@app/api/guild/services/GuildMfaEnforcement'; import {computeMovedIds, getMemberListRoleOrderIds} from '@app/api/guild/services/role/RoleOrderAuditUtils'; import { @@ -23,7 +22,7 @@ import type {LimitConfigService} from '@app/api/limits/LimitConfigService'; import {resolveLimitSafe} from '@app/api/limits/LimitConfigUtils'; import {createLimitMatchContext} from '@app/api/limits/LimitMatchContextBuilder'; import {GuildRole} from '@app/api/models/GuildRole'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {applyProtectedRolePermissions, permissionWriteMask, protectedThreadBits} from '@app/api/utils/featureUtils'; import {computePermissionsDiff} from '@app/api/utils/PermissionUtils'; import {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType'; @@ -45,8 +44,6 @@ import type {GuildResponse} from '@fluxer/schema/src/domains/guild/GuildResponse import type {GuildRoleResponse} from '@fluxer/schema/src/domains/guild/GuildRoleSchemas'; import type {ICacheService} from '@pkgs/cache/src/ICacheService'; -interface GuildRoleRepository extends IGuildRoleRepository, IGuildMemberRepository {} - interface GuildAuth { guildData: GuildResponse; checkPermission: (permission: bigint) => Promise; @@ -67,13 +64,13 @@ type RoleUpdateData = Partial<{ export class GuildRoleService { constructor( - private readonly guildRepository: GuildRoleRepository, + private readonly guildRepository: GuildRepository, private readonly snowflakeService: ISnowflakeService, private readonly cacheService: ICacheService, private readonly gatewayService: IGatewayService, private readonly guildAuditLogService: GuildAuditLogService, private readonly limitConfigService: LimitConfigService, - private readonly userRepository: IUserRepository, + private readonly userRepository: UserRepository, ) {} async systemCreateRole(params: { @@ -270,7 +267,7 @@ export class GuildRoleService { hoist: updateData.hoist ?? role.isHoisted, mentionable: updateData.mentionable ?? role.isMentionable, }; - const updatedRole = await this.guildRepository.upsertRole(updatedRoleData, role.toRow()); + const updatedRole = await this.guildRepository.upsertRole(updatedRoleData); await this.dispatchGuildRoleUpdate({guildId, role: updatedRole}); const changes = this.guildAuditLogService.computeChanges(previousSnapshot, this.serializeRoleForAudit(updatedRole)); if (role.permissions !== updatedRole.permissions) { @@ -449,14 +446,10 @@ export class GuildRoleService { for (const update of updates) { const role = roleMap.get(update.roleId)!; if (role.hoistPosition === update.hoistPosition) continue; - const roleRow = role.toRow(); - const updatedRole = await this.guildRepository.upsertRole( - { - ...roleRow, - hoist_position: update.hoistPosition, - }, - roleRow, - ); + const updatedRole = await this.guildRepository.upsertRole({ + ...role.toRow(), + hoist_position: update.hoistPosition, + }); changedRoles.push(updatedRole); } if (changedRoles.length > 0) { @@ -497,14 +490,7 @@ export class GuildRoleService { const changedRoles: Array = []; for (const role of allRoles) { if (role.hoistPosition === null) continue; - const roleRow = role.toRow(); - const updatedRole = await this.guildRepository.upsertRole( - { - ...roleRow, - hoist_position: null, - }, - roleRow, - ); + const updatedRole = await this.guildRepository.upsertRole({...role.toRow(), hoist_position: null}); changedRoles.push(updatedRole); } if (changedRoles.length > 0) { @@ -697,11 +683,7 @@ export class GuildRoleService { }); const reorderedIds = targetOrder.map((r) => r.id); const reorderedRoles = this.reorderRolePositions({allRoles, reorderedIds, guildId}); - const updatePromises = reorderedRoles.map((role) => { - const roleRow = role.toRow(); - const oldRole = roleMap.get(role.id); - return this.guildRepository.upsertRole(roleRow, oldRole ? oldRole.toRow() : undefined); - }); + const updatePromises = reorderedRoles.map((role) => this.guildRepository.upsertRole(role.toRow())); await Promise.all(updatePromises); const updatedRoles = await this.guildRepository.listRoles(guildId); const changedRoles = updatedRoles.filter((role) => { diff --git a/fluxer_api/src/api/guild/services/GuildSearchService.ts b/fluxer_api/src/api/guild/services/GuildSearchService.ts index 9b037e740..25408daa1 100644 --- a/fluxer_api/src/api/guild/services/GuildSearchService.ts +++ b/fluxer_api/src/api/guild/services/GuildSearchService.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {ChannelID, GuildID, UserID} from '@app/api/BrandedTypes'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import {threadSearchIndexEnqueueCappedTotal} from '@app/api/channel/threads/ThreadMetrics'; import { guildActive, @@ -21,7 +21,7 @@ import {MessageSearchResponseMapper} from '@app/api/search/MessageSearchResponse import {searchExistingMessages} from '@app/api/search/MessageSearchResultReconciler'; import {channelRequiresAgeVerification} from '@app/api/search/SearchNsfwUtils'; import {accessibleRequestedThreadIds, accessibleThreadIds} from '@app/api/search/ThreadSearchScope'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {canUserAccessNsfwContent} from '@app/api/utils/AgeUtils'; import {mapWithConcurrency} from '@app/api/utils/ConcurrencyUtils'; import type {WorkerTaskName} from '@app/api/worker/WorkerLaneConfig'; @@ -46,10 +46,10 @@ export class GuildSearchService { private readonly responseMapper: MessageSearchResponseMapper; constructor( - private readonly channelRepository: IChannelRepository, + private readonly channelRepository: ChannelRepository, private readonly userCacheService: UserCacheService, private readonly gatewayService: IGatewayService, - private readonly userRepository: IUserRepository, + private readonly userRepository: UserRepository, private readonly workerService: IWorkerService, ) { this.responseMapper = new MessageSearchResponseMapper(this.channelRepository, this.userCacheService); @@ -74,7 +74,7 @@ export class GuildSearchService { const threadsVisible = viewerActive(viewer, guildId); const explicitChannelIds = channelIds.length > 0; if (!explicitChannelIds) { - const channels = await this.channelRepository.listGuildChannels(guildId, 'enrolled'); + const channels = await this.channelRepository.channelData.listGuildChannels(guildId, 'enrolled'); channelIds = channels.filter((c) => threadsVisible || !c.isThreadOnly()).map((c) => c.id); } const includeNsfwRequested = searchParams.include_nsfw ?? false; @@ -85,7 +85,7 @@ export class GuildSearchService { } const canIncludeNsfw = canUserAccessNsfw && (includeNsfwRequested || guildIsAgeRestricted); const guildNsfw = guildData?.nsfw ?? false; - const channels = await this.channelRepository.listChannels(channelIds); + const channels = await this.channelRepository.channelData.listChannels(channelIds); const channelMap = new Map(); for (const channel of channels) { if (channel.guildId === guildId && (threadsVisible || !THREAD_FEATURE_CHANNEL_TYPES.has(channel.type))) { @@ -103,7 +103,7 @@ export class GuildSearchService { if (requestedThreads.length > 0) { const threadIds = new Set(requestedThreads.map((thread) => thread.id.toString())); const parentIds = requestedThreads.flatMap((thread) => (thread.parentId ? [thread.parentId] : [])); - for (const parent of await this.channelRepository.listChannels( + for (const parent of await this.channelRepository.channelData.listChannels( parentIds.filter((id) => !channelMap.has(id.toString())), )) { if (parent.guildId === guildId) channelMap.set(parent.id.toString(), parent); @@ -357,7 +357,7 @@ export class GuildSearchService { } } if (missingParentIds.length > 0) { - const parents = await this.channelRepository.listChannels(missingParentIds); + const parents = await this.channelRepository.channelData.listChannels(missingParentIds); for (const parent of parents) { lookup.set(parent.id.toString(), parent); } @@ -393,7 +393,7 @@ export class GuildSearchService { threadParents: Map, ): Promise { const threadsByGuild = new Map>(); - for (const thread of await this.channelRepository.listChannels([...threadIds])) { + for (const thread of await this.channelRepository.channelData.listChannels([...threadIds])) { const parent = thread.parentId === null ? undefined : parentById.get(thread.parentId.toString()); if (!thread.isThread() || !parent || thread.guildId === null || parent.guildId !== thread.guildId) continue; const threads = threadsByGuild.get(thread.guildId.toString()); @@ -438,7 +438,7 @@ export class GuildSearchService { await mapWithConcurrency(guildIds, GUILD_FANOUT_CONCURRENCY, async (guildId) => { const [guildData, guildChannels, viewableChannels] = await Promise.all([ this.gatewayService.getGuildData({guildId, userId}), - this.channelRepository + this.channelRepository.channelData .listGuildChannels(guildId, 'enrolled') .then((channels) => channels.filter( diff --git a/fluxer_api/src/api/guild/services/GuildService.ts b/fluxer_api/src/api/guild/services/GuildService.ts index 6ba672442..268bb6f1b 100644 --- a/fluxer_api/src/api/guild/services/GuildService.ts +++ b/fluxer_api/src/api/guild/services/GuildService.ts @@ -3,7 +3,7 @@ import type {ApiContext} from '@app/api/ApiContext'; import type {EmojiID, GuildID, RoleID, StickerID, UserID} from '@app/api/BrandedTypes'; import {createChannelID, createWebhookID} from '@app/api/BrandedTypes'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {ChannelService} from '@app/api/channel/services/ChannelService'; import {mapThreadToResponse} from '@app/api/channel/services/thread/ThreadMappers'; import {everEnabled, isTainted, type ThreadViewer, viewerActive} from '@app/api/experiment/ChannelThreadsGate'; @@ -19,7 +19,7 @@ import { THREAD_AUDIT_LOG_ACTION_TYPES, } from '@app/api/guild/GuildAuditLogEntryMapper'; import type {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import {GuildChannelService} from '@app/api/guild/services/GuildChannelService'; import {GuildContentService} from '@app/api/guild/services/GuildContentService'; import {GuildDataService} from '@app/api/guild/services/GuildDataService'; @@ -38,9 +38,9 @@ import type {LimitConfigService} from '@app/api/limits/LimitConfigService'; import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware'; import type {GuildAuditLog} from '@app/api/models/GuildAuditLog'; import type {Webhook} from '@app/api/models/Webhook'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {getCachedUserPartialResponses} from '@app/api/user/UserCacheHelpers'; -import type {IWebhookRepository} from '@app/api/webhook/IWebhookRepository'; +import type {WebhookRepository} from '@app/api/webhook/WebhookRepository'; import {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType'; import {Permissions} from '@fluxer/constants/src/ChannelConstants'; import {GuildFeatures} from '@fluxer/constants/src/GuildConstants'; @@ -99,25 +99,25 @@ export class GuildService { public readonly content: GuildContentService; public readonly channels: GuildChannelService; public readonly search: GuildSearchService; - private readonly guildRepository: IGuildRepositoryAggregate; + private readonly guildRepository: GuildRepository; private readonly cacheService: ICacheService; private readonly userCacheService: UserCacheService; - private readonly webhookRepository: IWebhookRepository; + private readonly webhookRepository: WebhookRepository; private readonly guildAuditLogService: GuildAuditLogService; private readonly gatewayService: IGatewayService; - private readonly userRepository: IUserRepository; + private readonly userRepository: UserRepository; constructor( apiContext: ApiContext, - guildRepository: IGuildRepositoryAggregate, - private readonly channelRepository: IChannelRepository, + guildRepository: GuildRepository, + private readonly channelRepository: ChannelRepository, inviteRepository: InviteRepository, channelService: ChannelService, userCacheService: UserCacheService, entityAssetService: EntityAssetService, avatarService: AvatarService, assetDeletionQueue: IAssetDeletionQueue, - webhookRepository: IWebhookRepository, + webhookRepository: WebhookRepository, guildAuditLogService: GuildAuditLogService, limitConfigService: LimitConfigService, ) { @@ -218,7 +218,7 @@ export class GuildService { ): Promise { const {guildId, requestCache} = params; const {guild, previousFeatures, updatedFeatures} = await this.withGuildUpdateLock(guildId, requestCache, () => - this.data.updateGuild(params, auditLogReason), + this.data.operations.updateGuild(params, auditLogReason), ); if ( previousFeatures.has(GuildFeatures.TEXT_CHANNEL_FLEXIBLE_NAMES) && @@ -258,7 +258,7 @@ export class GuildService { async getEmojiMetadata(emojiId: EmojiID): Promise { const emoji = await this.guildRepository.getEmojiById(emojiId); if (!emoji) throw new UnknownGuildEmojiError(); - const guild = await this.data.getGuildSystem(emoji.guildId); + const guild = await this.data.operations.getGuildSystem(emoji.guildId); return { id: emoji.id.toString(), guild_id: guild.id.toString(), @@ -271,7 +271,7 @@ export class GuildService { async getStickerMetadata(stickerId: StickerID): Promise { const sticker = await this.guildRepository.getStickerById(stickerId); if (!sticker) throw new UnknownGuildStickerError(); - const guild = await this.data.getGuildSystem(sticker.guildId); + const guild = await this.data.operations.getGuildSystem(sticker.guildId); return { id: sticker.id.toString(), guild_id: guild.id.toString(), @@ -421,7 +421,7 @@ export class GuildService { ); const channels = candidateIds.length > 0 - ? await this.channelRepository.listChannels(candidateIds.map((id) => createChannelID(BigInt(id)))) + ? await this.channelRepository.channelData.listChannels(candidateIds.map((id) => createChannelID(BigInt(id)))) : []; const gatedIds = new Set( channels @@ -435,7 +435,7 @@ export class GuildService { const channelIds = [...new Set(webhooks.flatMap((webhook) => (webhook.channelId ? [webhook.channelId] : [])))]; if (channelIds.length === 0) return webhooks; const forumIds = new Set( - (await this.channelRepository.listChannels(channelIds)) + (await this.channelRepository.channelData.listChannels(channelIds)) .filter((channel) => channel.isThreadOnly()) .map((channel) => channel.id), ); @@ -454,12 +454,14 @@ export class GuildService { .map((id) => createChannelID(BigInt(id))); if (threadIds.length === 0) return []; const [channels, states, stats] = await Promise.all([ - this.channelRepository.listChannels(threadIds), + this.channelRepository.channelData.listChannels(threadIds), this.channelRepository.threads.getStates(threadIds), this.channelRepository.threads.getStatsMany(threadIds), ]); const stateById = new Map(states.map((state) => [state.threadId.toString(), state])); - const parents = await this.channelRepository.listChannels([...new Set(states.map((state) => state.parentId))]); + const parents = await this.channelRepository.channelData.listChannels([ + ...new Set(states.map((state) => state.parentId)), + ]); const parentTypeById = new Map(parents.map((parent) => [parent.id.toString(), parent.type])); return channels.flatMap((channel) => { const state = stateById.get(channel.id.toString()); diff --git a/fluxer_api/src/api/guild/services/ThreadPermissionBits.test.ts b/fluxer_api/src/api/guild/services/ThreadPermissionBits.test.ts index 01f35b7d2..9b0fd05b3 100644 --- a/fluxer_api/src/api/guild/services/ThreadPermissionBits.test.ts +++ b/fluxer_api/src/api/guild/services/ThreadPermissionBits.test.ts @@ -14,7 +14,7 @@ import { resolveThreadPermissionMode, shouldMaskThreadPermissionBits, } from '@app/api/guild/services/ThreadPermissionBits'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import { applyProtectedOverwriteBits, applyProtectedRolePermissions, @@ -212,7 +212,7 @@ describe('guild MFA enforcement', () => { it('treats MANAGE_THREADS as elevated', async () => { const userRepository = { findUnique: async () => ({authenticatorTypes: new Set()}), - } as unknown as IUserRepository; + } as unknown as UserRepository; const enforce = await createGuildMfaEnforcer({ userRepository, guildData: {mfa_level: GuildMFALevel.ELEVATED, owner_id: '1'}, diff --git a/fluxer_api/src/api/guild/services/channel/ChannelOperationsService.ts b/fluxer_api/src/api/guild/services/channel/ChannelOperationsService.ts index bad35e8a8..1a5b9f528 100644 --- a/fluxer_api/src/api/guild/services/channel/ChannelOperationsService.ts +++ b/fluxer_api/src/api/guild/services/channel/ChannelOperationsService.ts @@ -3,7 +3,7 @@ import type {ChannelID, EmojiID, GuildID, RoleID, StickerID, UserID} from '@app/api/BrandedTypes'; import {createChannelID, createRoleID, createUserID} from '@app/api/BrandedTypes'; import {mapChannelToResponse} from '@app/api/channel/ChannelMappers'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import { buildThreadParentPatch, loadThreadParentConfig, @@ -21,7 +21,7 @@ import { } from '@app/api/experiment/ChannelThreadsGate'; import type {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService'; import type {GuildAuditLogChange} from '@app/api/guild/GuildAuditLogTypes'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import {ChannelHelpers, type ChannelReorderOperation} from '@app/api/guild/services/channel/ChannelHelpers'; import {resolveProtectedBitActor} from '@app/api/guild/services/ThreadPermissionBits'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; @@ -64,8 +64,8 @@ import type {ICacheService} from '@pkgs/cache/src/ICacheService'; export class ChannelOperationsService { constructor( - private readonly channelRepository: IChannelRepository, - private readonly guildRepository: IGuildRepositoryAggregate, + private readonly channelRepository: ChannelRepository, + private readonly guildRepository: GuildRepository, private readonly userCacheService: UserCacheService, private readonly gatewayService: IGatewayService, private readonly cacheService: ICacheService, @@ -86,7 +86,7 @@ export class ChannelOperationsService { auditLogReason?: string | null, ): Promise { await this.ensureGuildHasCapacity(params.guildId); - const channels = await this.channelRepository.listGuildChannels(params.guildId, 'enrolled'); + const channels = await this.channelRepository.channelData.listGuildChannels(params.guildId, 'enrolled'); const parentId = params.data.parent_id ? createChannelID(params.data.parent_id) : null; const parentChannel = this.validateParentCategory({ parentId, @@ -212,7 +212,7 @@ export class ChannelOperationsService { const requestedContentWarningText = trimmedContentWarningText && trimmedContentWarningText.length > 0 ? trimmedContentWarningText : null; const channelId = createChannelID(await this.snowflakeService.generate()); - const channel = await this.channelRepository.upsert({ + const channel = await this.channelRepository.channelData.upsert({ channel_id: channelId, guild_id: params.guildId, type: params.data.type, @@ -289,7 +289,7 @@ export class ChannelOperationsService { async sanitizeTextChannelNames(params: {guildId: GuildID; requestCache: RequestCache}): Promise { const {guildId, requestCache} = params; - const channels = await this.channelRepository.listGuildChannels(guildId, 'enrolled'); + const channels = await this.channelRepository.channelData.listGuildChannels(guildId, 'enrolled'); let hasChanges = false; const updatedChannels: Array = []; for (const channel of channels) { @@ -305,7 +305,7 @@ export class ChannelOperationsService { updatedChannels.push(channel); continue; } - const updated = await this.channelRepository.upsert({ + const updated = await this.channelRepository.channelData.upsert({ ...channel.toRow(), name: normalized, }); @@ -372,7 +372,7 @@ export class ChannelOperationsService { } private async listPositionableChannels(guildId: GuildID, hideThreadOnly: boolean): Promise> { - const channels = await this.channelRepository.listGuildChannels(guildId, 'enrolled'); + const channels = await this.channelRepository.channelData.listGuildChannels(guildId, 'enrolled'); return hideThreadOnly ? channels.filter((ch) => !ch.isThreadOnly()) : channels; } @@ -461,9 +461,9 @@ export class ChannelOperationsService { clientFeatures?: ReadonlySet; viewer?: ThreadViewer; }): Promise { - const parent = await this.channelRepository.findUnique(params.parentId); + const parent = await this.channelRepository.channelData.findUnique(params.parentId); if (!parent || parent.guildId !== params.guildId || parent.type !== ChannelTypes.GUILD_CATEGORY) return; - const child = await this.channelRepository.findUnique(params.channelId); + const child = await this.channelRepository.channelData.findUnique(params.channelId); if (!child || child.guildId !== params.guildId) return; const userPermissions = await this.gatewayService.getUserPermissions({ guildId: params.guildId, @@ -508,7 +508,7 @@ export class ChannelOperationsService { const existingAllow = child.permissionOverwrites.get(targetId)?.allow ?? 0n; if ((incoming.allow & ~existingAllow & ~userPermissions) !== 0n) throw new MissingPermissionsError(); } - await this.channelRepository.upsert({ + await this.channelRepository.channelData.upsert({ ...child.toRow(), permission_overwrites: new Map( Array.from(incomingOverwrites.entries()).map(([targetId, overwrite]) => [ @@ -525,7 +525,7 @@ export class ChannelOperationsService { requestCache: RequestCache; }): Promise { const {guildId, operation, requestCache} = params; - const allChannels = await this.channelRepository.listGuildChannels(guildId, 'enrolled'); + const allChannels = await this.channelRepository.channelData.listGuildChannels(guildId, 'enrolled'); const planResult = computeGuildChannelReorderPlan({channels: allChannels, operation}); if (!planResult.ok) { this.throwReorderPlanError(planResult.code, operation); @@ -561,14 +561,14 @@ export class ChannelOperationsService { const currentParent = channel.parentId ?? null; if (channel.position !== desiredPosition || currentParent !== desiredParent) { updatePromises.push( - this.channelRepository + this.channelRepository.channelData .upsert({...channel.toRow(), position: desiredPosition, parent_id: desiredParent}) .then(() => {}), ); } } await Promise.all(updatePromises); - const updatedChannels = await this.channelRepository.listGuildChannels(guildId, 'enrolled'); + const updatedChannels = await this.channelRepository.channelData.listGuildChannels(guildId, 'enrolled'); await this.dispatchChannelUpdateBulk({guildId, channels: updatedChannels, requestCache}); } diff --git a/fluxer_api/src/api/guild/services/content/EmojiService.ts b/fluxer_api/src/api/guild/services/content/EmojiService.ts index 3722c9c8b..f224857e5 100644 --- a/fluxer_api/src/api/guild/services/content/EmojiService.ts +++ b/fluxer_api/src/api/guild/services/content/EmojiService.ts @@ -3,7 +3,7 @@ import {createEmojiID, type EmojiID, type GuildID, type UserID} from '@app/api/BrandedTypes'; import {getContentMessage} from '@app/api/content_i18n/ContentI18n'; import {mapGuildEmojisWithUsersToResponse, mapGuildEmojiToResponse} from '@app/api/guild/GuildModel'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {ContentHelpers} from '@app/api/guild/services/content/ContentHelpers'; import type {ExpressionAssetPurger} from '@app/api/guild/services/content/ExpressionAssetPurger'; import type {AvatarService} from '@app/api/infrastructure/AvatarService'; @@ -29,7 +29,7 @@ import type {GuildEmojiResponse, GuildEmojiWithUserResponse} from '@fluxer/schem export class EmojiService { constructor( - private readonly guildRepository: IGuildRepositoryAggregate, + private readonly guildRepository: GuildRepository, private readonly userCacheService: UserCacheService, private readonly gatewayService: IGatewayService, private readonly avatarService: AvatarService, diff --git a/fluxer_api/src/api/guild/services/content/StickerService.ts b/fluxer_api/src/api/guild/services/content/StickerService.ts index bec1cab23..e5a78e00f 100644 --- a/fluxer_api/src/api/guild/services/content/StickerService.ts +++ b/fluxer_api/src/api/guild/services/content/StickerService.ts @@ -3,7 +3,7 @@ import {createStickerID, type GuildID, type StickerID, type UserID} from '@app/api/BrandedTypes'; import {getContentMessage} from '@app/api/content_i18n/ContentI18n'; import {mapGuildStickersWithUsersToResponse, mapGuildStickerToResponse} from '@app/api/guild/GuildModel'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {ContentHelpers} from '@app/api/guild/services/content/ContentHelpers'; import type {ExpressionAssetPurger} from '@app/api/guild/services/content/ExpressionAssetPurger'; import type {AvatarService} from '@app/api/infrastructure/AvatarService'; @@ -33,7 +33,7 @@ import type { export class StickerService { constructor( - private readonly guildRepository: IGuildRepositoryAggregate, + private readonly guildRepository: GuildRepository, private readonly userCacheService: UserCacheService, private readonly gatewayService: IGatewayService, private readonly avatarService: AvatarService, diff --git a/fluxer_api/src/api/guild/services/data/GuildDataHelpers.ts b/fluxer_api/src/api/guild/services/data/GuildDataHelpers.ts index f30ed0d5e..bbf68208b 100644 --- a/fluxer_api/src/api/guild/services/data/GuildDataHelpers.ts +++ b/fluxer_api/src/api/guild/services/data/GuildDataHelpers.ts @@ -9,7 +9,7 @@ import {createGuildMfaEnforcer} from '@app/api/guild/services/GuildMfaEnforcemen import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import {Logger} from '@app/api/Logger'; import type {Guild} from '@app/api/models/Guild'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {serializeGuildForAudit as serializeGuildForAuditUtil} from '@app/api/utils/AuditSerializationUtils'; import {requirePermission} from '@app/api/utils/PermissionUtils'; import type {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType'; @@ -26,7 +26,7 @@ export class GuildDataHelpers { constructor( private readonly gatewayService: IGatewayService, private readonly guildAuditLogService: GuildAuditLogService, - private readonly userRepository: IUserRepository, + private readonly userRepository: UserRepository, ) {} private readonly guildRepository = new GuildRepository(); diff --git a/fluxer_api/src/api/guild/services/data/GuildOperationsService.ts b/fluxer_api/src/api/guild/services/data/GuildOperationsService.ts index 5b4a51fd7..74b631fe6 100644 --- a/fluxer_api/src/api/guild/services/data/GuildOperationsService.ts +++ b/fluxer_api/src/api/guild/services/data/GuildOperationsService.ts @@ -2,7 +2,7 @@ import type {ChannelID, GuildID, RoleID, UserID} from '@app/api/BrandedTypes'; import {createChannelID, createGuildID, createRoleID, guildIdToRoleId} from '@app/api/BrandedTypes'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import { type ChannelFollowerRemovalCopyMode, scheduleDeletedChannelFollowerRemoval, @@ -14,8 +14,8 @@ import type {PermissionOverwrite} from '@app/api/database/types/ChannelTypes'; import type {GuildRow} from '@app/api/database/types/GuildTypes'; import {guildActive, isTainted} from '@app/api/experiment/ChannelThreadsGate'; import {mapGuildToGuildResponse, mapGuildToPartialResponse} from '@app/api/guild/GuildModel'; -import type {IGuildDiscoveryRepository} from '@app/api/guild/repositories/GuildDiscoveryRepository'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildDiscoveryRepository} from '@app/api/guild/repositories/GuildDiscoveryRepository'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {GuildDataHelpers} from '@app/api/guild/services/data/GuildDataHelpers'; import {contentModerationService} from '@app/api/infrastructure/ContentModerationService'; import type {EntityAssetService, PreparedAssetUpload} from '@app/api/infrastructure/EntityAssetService'; @@ -35,11 +35,11 @@ import type {GuildDiscoveryContext} from '@app/api/search/guild/GuildSearchSeria import {deleteChannelMessageSearchDocuments} from '@app/api/search/MessageSearchIndexCleanup'; import {deleteThreadSearchDocuments} from '@app/api/search/thread/ThreadSearchService'; import {Channels, ChannelsByGuild, GuildMembers, GuildMembersByUserId, GuildRoles, Guilds} from '@app/api/Tables'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {mapUserSettingsToResponse} from '@app/api/user/UserMappers'; import {addGuildToUncategorizedFolder, removeGuildFromUserFolders} from '@app/api/user/utils/GuildFolderUtils'; import {mapWithConcurrency} from '@app/api/utils/ConcurrencyUtils'; -import type {IWebhookRepository} from '@app/api/webhook/IWebhookRepository'; +import type {WebhookRepository} from '@app/api/webhook/WebhookRepository'; import {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType'; import {ALL_PERMISSIONS, ChannelTypes, DEFAULT_PERMISSIONS, Permissions} from '@fluxer/constants/src/ChannelConstants'; import {DiscoveryApplicationStatus} from '@fluxer/constants/src/DiscoveryConstants'; @@ -185,18 +185,18 @@ function isSystemChannelType(type: number): boolean { export class GuildOperationsService { constructor( - private readonly guildRepository: IGuildRepositoryAggregate, - private readonly channelRepository: IChannelRepository, + private readonly guildRepository: GuildRepository, + private readonly channelRepository: ChannelRepository, private readonly inviteRepository: InviteRepository, private readonly channelService: ChannelService, private readonly gatewayService: IGatewayService, private readonly entityAssetService: EntityAssetService, - private readonly userRepository: IUserRepository, + private readonly userRepository: UserRepository, private readonly snowflakeService: ISnowflakeService, - private readonly webhookRepository: IWebhookRepository, + private readonly webhookRepository: WebhookRepository, private readonly helpers: GuildDataHelpers, private readonly limitConfigService: LimitConfigService, - private readonly discoveryRepository: IGuildDiscoveryRepository, + private readonly discoveryRepository: GuildDiscoveryRepository, ) {} async getGuild({userId, guildId}: {userId: UserID; guildId: GuildID}): Promise { @@ -521,7 +521,7 @@ export class GuildOperationsService { if (data.afk_channel_id !== undefined) { if (data.afk_channel_id) { const afkChannelId = createChannelID(data.afk_channel_id); - const afkChannel = await this.channelRepository.findUnique(afkChannelId); + const afkChannel = await this.channelRepository.channelData.findUnique(afkChannelId); if (!afkChannel || afkChannel.guildId !== guildId) { throw InputValidationError.fromCode('afk_channel_id', ValidationErrorCodes.AFK_CHANNEL_MUST_BE_IN_GUILD); } @@ -539,7 +539,7 @@ export class GuildOperationsService { if (data.system_channel_id !== undefined) { if (data.system_channel_id) { const systemChannelId = createChannelID(data.system_channel_id); - const systemChannel = await this.channelRepository.findUnique(systemChannelId); + const systemChannel = await this.channelRepository.channelData.findUnique(systemChannelId); if (!systemChannel || systemChannel.guildId !== guildId) { throw InputValidationError.fromCode( 'system_channel_id', @@ -768,7 +768,7 @@ export class GuildOperationsService { if (!guild) { throw new UnknownGuildError(); } - const channels = await this.channelRepository.listGuildChannels(guildId, 'complete'); + const channels = await this.channelRepository.channelData.listGuildChannels(guildId, 'complete'); for (const channel of channels) { await scheduleDeletedChannelFollowerRemoval({channel, crossposts: this.channelRepository.crossposts, copyMode}); } @@ -805,7 +805,7 @@ export class GuildOperationsService { for (const channel of channels) { await this.channelService.attachments.purgeChannelAttachments(channel); } - await Promise.all(channels.map((channel) => this.channelRepository.deleteAllChannelMessages(channel.id))); + await Promise.all(channels.map((channel) => this.channelRepository.messages.deleteAllChannelMessages(channel.id))); await Promise.all( channels.map((channel) => deleteChannelMessageSearchDocuments(channel.id, {context: {source: 'guild_delete'}})), ); @@ -815,7 +815,7 @@ export class GuildOperationsService { } if (threadIds.length > 0) { const threadChannels = new Map( - (await this.channelRepository.listChannels(threadIds)).map((channel) => [channel.id, channel]), + (await this.channelRepository.channelData.listChannels(threadIds)).map((channel) => [channel.id, channel]), ); await mapWithConcurrency(threadIds, THREAD_PURGE_CONCURRENCY, async (threadId) => { const thread = threadChannels.get(threadId); diff --git a/fluxer_api/src/api/guild/services/data/GuildOwnershipService.ts b/fluxer_api/src/api/guild/services/data/GuildOwnershipService.ts index ffdf5fdaf..7002e389c 100644 --- a/fluxer_api/src/api/guild/services/data/GuildOwnershipService.ts +++ b/fluxer_api/src/api/guild/services/data/GuildOwnershipService.ts @@ -2,9 +2,9 @@ import type {GuildID, UserID} from '@app/api/BrandedTypes'; import {mapGuildToGuildResponse} from '@app/api/guild/GuildModel'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {GuildDataHelpers} from '@app/api/guild/services/data/GuildDataHelpers'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType'; import {MissingAccessError} from '@fluxer/errors/src/domains/core/MissingAccessError'; import {MissingPermissionsError} from '@fluxer/errors/src/domains/core/MissingPermissionsError'; @@ -17,8 +17,8 @@ const OWNERSHIP_AUDIT_KEYS: ReadonlySet = new Set(['owner_id']); export class GuildOwnershipService { constructor( - private readonly guildRepository: IGuildRepositoryAggregate, - private readonly userRepository: IUserRepository, + private readonly guildRepository: GuildRepository, + private readonly userRepository: UserRepository, private readonly helpers: GuildDataHelpers, ) {} diff --git a/fluxer_api/src/api/guild/services/data/GuildVanityService.ts b/fluxer_api/src/api/guild/services/data/GuildVanityService.ts index 84c79fd59..1ee18e9d5 100644 --- a/fluxer_api/src/api/guild/services/data/GuildVanityService.ts +++ b/fluxer_api/src/api/guild/services/data/GuildVanityService.ts @@ -7,7 +7,7 @@ import { type UserID, vanityCodeToInviteCode, } from '@app/api/BrandedTypes'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {GuildDataHelpers} from '@app/api/guild/services/data/GuildDataHelpers'; import type {InviteRepository} from '@app/api/invite/InviteRepository'; import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware'; @@ -23,7 +23,7 @@ const VANITY_AUDIT_KEYS: ReadonlySet = new Set(['vanity_url_code']); export class GuildVanityService { constructor( - private readonly guildRepository: IGuildRepositoryAggregate, + private readonly guildRepository: GuildRepository, private readonly inviteRepository: InviteRepository, private readonly helpers: GuildDataHelpers, ) {} diff --git a/fluxer_api/src/api/guild/services/member/GuildMemberAuthService.ts b/fluxer_api/src/api/guild/services/member/GuildMemberAuthService.ts index bf13275c2..861380cc7 100644 --- a/fluxer_api/src/api/guild/services/member/GuildMemberAuthService.ts +++ b/fluxer_api/src/api/guild/services/member/GuildMemberAuthService.ts @@ -3,7 +3,7 @@ import type {GuildID, RoleID, UserID} from '@app/api/BrandedTypes'; import {createGuildMfaEnforcer} from '@app/api/guild/services/GuildMfaEnforcement'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {MissingAccessError} from '@fluxer/errors/src/domains/core/MissingAccessError'; import {MissingPermissionsError} from '@fluxer/errors/src/domains/core/MissingPermissionsError'; import type {GuildResponse} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas'; @@ -20,7 +20,7 @@ interface GuildAuth { export class GuildMemberAuthService { constructor( private readonly gatewayService: IGatewayService, - private readonly userRepository: IUserRepository, + private readonly userRepository: UserRepository, ) {} async getGuildAuthenticated({userId, guildId}: {userId: UserID; guildId: GuildID}): Promise { diff --git a/fluxer_api/src/api/guild/services/member/GuildMemberOperationsService.ts b/fluxer_api/src/api/guild/services/member/GuildMemberOperationsService.ts index fcd0d3636..31b7c35c4 100644 --- a/fluxer_api/src/api/guild/services/member/GuildMemberOperationsService.ts +++ b/fluxer_api/src/api/guild/services/member/GuildMemberOperationsService.ts @@ -10,7 +10,7 @@ import type {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService'; import type {GuildAuditLogChange} from '@app/api/guild/GuildAuditLogTypes'; import {resolveMaxGuildMembersLimit} from '@app/api/guild/GuildMemberLimitUtils'; import {mapGuildMemberToResponse} from '@app/api/guild/GuildModel'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {GuildMemberAuthService} from '@app/api/guild/services/member/GuildMemberAuthService'; import type {GuildMemberEventService} from '@app/api/guild/services/member/GuildMemberEventService'; import type {GuildMemberSearchIndexService} from '@app/api/guild/services/member/GuildMemberSearchIndexService'; @@ -32,7 +32,7 @@ import type {User} from '@app/api/models/User'; import type {UserGuildSettings} from '@app/api/models/UserGuildSettings'; import type {UserSettings} from '@app/api/models/UserSettings'; import {assertAccountNotLimited} from '@app/api/user/AccountLimit'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {dispatchUserGuildSettingsUpdate} from '@app/api/user/UserGuildSettingsThreadView'; import {isProfileSubstringExempt} from '@app/api/user/UserHelpers'; import {mapUserSettingsToResponse} from '@app/api/user/UserMappers'; @@ -145,12 +145,12 @@ function joinSourceName(type: JoinSourceType): JoinSource { export class GuildMemberOperationsService { constructor( - private readonly guildRepository: IGuildRepositoryAggregate, + private readonly guildRepository: GuildRepository, private readonly channelService: ChannelService, private readonly userCacheService: UserCacheService, private readonly gatewayService: IGatewayService, private readonly entityAssetService: EntityAssetService, - private readonly userRepository: IUserRepository, + private readonly userRepository: UserRepository, private readonly rateLimitService: IRateLimitService, private readonly authService: GuildMemberAuthService, private readonly validationService: GuildMemberValidationService, diff --git a/fluxer_api/src/api/guild/services/member/GuildMemberRoleService.ts b/fluxer_api/src/api/guild/services/member/GuildMemberRoleService.ts index 0705d1fb9..701b05750 100644 --- a/fluxer_api/src/api/guild/services/member/GuildMemberRoleService.ts +++ b/fluxer_api/src/api/guild/services/member/GuildMemberRoleService.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {GuildID, RoleID, UserID} from '@app/api/BrandedTypes'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {GuildMemberAuthService} from '@app/api/guild/services/member/GuildMemberAuthService'; import type {GuildMemberValidationService} from '@app/api/guild/services/member/GuildMemberValidationService'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; @@ -10,7 +10,7 @@ import {UnknownGuildMemberError} from '@fluxer/errors/src/domains/guild/UnknownG export class GuildMemberRoleService { constructor( - private readonly guildRepository: IGuildRepositoryAggregate, + private readonly guildRepository: GuildRepository, private readonly gatewayService: IGatewayService, private readonly authService: GuildMemberAuthService, private readonly validationService: GuildMemberValidationService, diff --git a/fluxer_api/src/api/guild/services/member/GuildMemberSearchIndexService.ts b/fluxer_api/src/api/guild/services/member/GuildMemberSearchIndexService.ts index 390131d95..0e2d69683 100644 --- a/fluxer_api/src/api/guild/services/member/GuildMemberSearchIndexService.ts +++ b/fluxer_api/src/api/guild/services/member/GuildMemberSearchIndexService.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {GuildID, UserID} from '@app/api/BrandedTypes'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import {Logger} from '@app/api/Logger'; import type {GuildMember} from '@app/api/models/GuildMember'; import type {User} from '@app/api/models/User'; @@ -19,11 +19,7 @@ function getGuildMemberIndexService(options: GuildMemberSearchIndexOptions = {}) } export class GuildMemberSearchIndexService { - async updateUserMembers( - user: User, - guildIds: Array, - guildRepository: IGuildRepositoryAggregate, - ): Promise { + async updateUserMembers(user: User, guildIds: Array, guildRepository: GuildRepository): Promise { if (guildIds.length === 0) return; const guilds = await guildRepository.listGuilds(guildIds); const indexedGuilds = guilds.filter((guild) => guild.membersIndexedAt != null); diff --git a/fluxer_api/src/api/guild/services/member/GuildMemberValidationService.ts b/fluxer_api/src/api/guild/services/member/GuildMemberValidationService.ts index 011d81bbc..46d2bb0a9 100644 --- a/fluxer_api/src/api/guild/services/member/GuildMemberValidationService.ts +++ b/fluxer_api/src/api/guild/services/member/GuildMemberValidationService.ts @@ -3,9 +3,9 @@ import type {GuildID, RoleID, UserID} from '@app/api/BrandedTypes'; import {guildIdToRoleId} from '@app/api/BrandedTypes'; import {isIpBanExempt} from '@app/api/ban/IpBanExemptions'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {GuildMember} from '@app/api/models/GuildMember'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {Permissions} from '@fluxer/constants/src/ChannelConstants'; import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes'; import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError'; @@ -24,8 +24,8 @@ function ensureNotEveryoneRole(roleId: RoleID, guildId: GuildID, path: string): export class GuildMemberValidationService { constructor( - private readonly guildRepository: IGuildRepositoryAggregate, - private readonly userRepository: IUserRepository, + private readonly guildRepository: GuildRepository, + private readonly userRepository: UserRepository, ) {} async validateAndGetRoleIds(params: { diff --git a/fluxer_api/src/api/guild/tests/GuildAuditLogService.test.ts b/fluxer_api/src/api/guild/tests/GuildAuditLogService.test.ts index 2fdb097e9..df397cd4f 100644 --- a/fluxer_api/src/api/guild/tests/GuildAuditLogService.test.ts +++ b/fluxer_api/src/api/guild/tests/GuildAuditLogService.test.ts @@ -11,7 +11,7 @@ import { import type {GuildAuditLogRow} from '@app/api/database/types/GuildTypes'; import {mapGuildAuditLogEntry} from '@app/api/guild/GuildAuditLogEntryMapper'; import {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService'; import {ChannelPermissionOverwrite} from '@app/api/models/ChannelPermissionOverwrite'; @@ -29,19 +29,6 @@ const MEMBER_ID = createUserID(1420000000000000002n); const ROLE_ID = createRoleID(1420000000000000003n); const CHANNEL_ID = createChannelID(1420000000000000004n); -const SKIPPABLE_ACTIONS = [ - AuditLogActionType.GUILD_UPDATE, - AuditLogActionType.CHANNEL_UPDATE, - AuditLogActionType.CHANNEL_OVERWRITE_UPDATE, - AuditLogActionType.MEMBER_UPDATE, - AuditLogActionType.MEMBER_ROLE_UPDATE, - AuditLogActionType.MEMBER_MOVE, - AuditLogActionType.ROLE_UPDATE, - AuditLogActionType.WEBHOOK_UPDATE, - AuditLogActionType.EMOJI_UPDATE, - AuditLogActionType.STICKER_UPDATE, -]; - function createService(roleNames: Map = new Map()) { let nextLogId = 1420000000000001000n; const createAuditLog = vi.fn(async (row: GuildAuditLogRow) => new GuildAuditLog(row)); @@ -55,7 +42,7 @@ function createService(roleNames: Map = new Map()) { const dispatchGuild = vi.fn().mockResolvedValue(undefined); const addJob = vi.fn().mockResolvedValue(undefined); const service = new GuildAuditLogService( - {createAuditLog, batchDeleteAndCreateAuditLogs, getRole} as unknown as IGuildRepositoryAggregate, + {createAuditLog, batchDeleteAndCreateAuditLogs, getRole} as unknown as GuildRepository, {generate: vi.fn(async () => nextLogId++)} as unknown as ISnowflakeService, {addJob} as unknown as IWorkerService, {dispatchGuild} as unknown as IGatewayService, @@ -175,11 +162,11 @@ describe('GuildAuditLogService dispatch', () => { }); describe('GuildAuditLogBuilder.commit', () => { - it.each(SKIPPABLE_ACTIONS)('skips action %i with empty changes', async (actionType) => { + it('skips an update with empty changes', async () => { const {service, createAuditLog, dispatchGuild} = createService(); const result = await service .createBuilder(GUILD_ID, ACTOR_ID) - .withAction(actionType, MEMBER_ID.toString()) + .withAction(AuditLogActionType.MEMBER_UPDATE, MEMBER_ID.toString()) .withReason('ignored reason') .withMetadata({role_name: 'Moderators'}) .withChanges([]) @@ -189,31 +176,6 @@ describe('GuildAuditLogBuilder.commit', () => { expect(dispatchGuild).not.toHaveBeenCalled(); }); - it.each(SKIPPABLE_ACTIONS)('skips action %i without changes', async (actionType) => { - const {service, createAuditLog, dispatchGuild} = createService(); - const result = await service - .createBuilder(GUILD_ID, ACTOR_ID) - .withAction(actionType, MEMBER_ID.toString()) - .commit(); - expect(result).toBeNull(); - expect(createAuditLog).not.toHaveBeenCalled(); - expect(dispatchGuild).not.toHaveBeenCalled(); - }); - - it('skips a guild update that only changes internal keys', async () => { - const {service, createAuditLog} = createService(); - const result = await service - .createBuilder(GUILD_ID, ACTOR_ID) - .withAction(AuditLogActionType.GUILD_UPDATE, GUILD_ID.toString()) - .withChanges([ - {key: 'member_count', old_value: 1, new_value: 2}, - {key: 'splash_width', old_value: null, new_value: 1920}, - ]) - .commit(); - expect(result).toBeNull(); - expect(createAuditLog).not.toHaveBeenCalled(); - }); - it('records a kick without changes', async () => { const {service, createAuditLog, dispatchGuild} = createService(); const result = await service @@ -227,18 +189,6 @@ describe('GuildAuditLogBuilder.commit', () => { expect(writtenRow(createAuditLog).changes).toBeNull(); expect(dispatchGuild).toHaveBeenCalledTimes(1); }); - - it('records a webhook create with empty changes', async () => { - const {service, createAuditLog, dispatchGuild} = createService(); - const result = await service - .createBuilder(GUILD_ID, ACTOR_ID) - .withAction(AuditLogActionType.WEBHOOK_CREATE, '1420000000000000005') - .withChanges([]) - .commit(); - expect(result).not.toBeNull(); - expect(createAuditLog).toHaveBeenCalledTimes(1); - expect(dispatchGuild).toHaveBeenCalledTimes(1); - }); }); describe('GuildAuditLogService.recordPermissionOverwriteDiff', () => { diff --git a/fluxer_api/src/api/guild/tests/GuildChannelManagement.test.ts b/fluxer_api/src/api/guild/tests/GuildChannelManagement.test.ts index 2c5c4ef8a..dd929eed3 100644 --- a/fluxer_api/src/api/guild/tests/GuildChannelManagement.test.ts +++ b/fluxer_api/src/api/guild/tests/GuildChannelManagement.test.ts @@ -202,7 +202,7 @@ describe('Guild Channel Management', () => { expect.objectContaining({path: 'rtc_p2p', code: ValidationErrorCodes.GUILD_FEATURE_NOT_TOGGLEABLE}), ); } - const stored = await getChannelRepository().findUnique(createChannelID(BigInt(voiceChannel.id))); + const stored = await getChannelRepository().channelData.findUnique(createChannelID(BigInt(voiceChannel.id))); expect(stored?.rtcP2p).toBe(false); await setVoiceP2pConfig({enabled: true, included_user_ids: [account.userId]}); await createBuilder(harness, account.token) @@ -225,7 +225,7 @@ describe('Guild Channel Management', () => { .body({type: ChannelTypes.GUILD_TEXT, rtc_p2p: true}) .execute(); expect(data.rtc_p2p).toBeUndefined(); - const stored = await getChannelRepository().findUnique(createChannelID(BigInt(textChannel.id))); + const stored = await getChannelRepository().channelData.findUnique(createChannelID(BigInt(textChannel.id))); expect(stored?.rtcP2p).toBe(false); }); test('should require MANAGE_CHANNELS to update rtc_p2p', async () => { @@ -242,7 +242,7 @@ describe('Guild Channel Management', () => { .body({type: ChannelTypes.GUILD_VOICE, rtc_p2p: true}) .expect(HTTP_STATUS.FORBIDDEN) .execute(); - const stored = await getChannelRepository().findUnique(createChannelID(BigInt(voiceChannel.id))); + const stored = await getChannelRepository().channelData.findUnique(createChannelID(BigInt(voiceChannel.id))); expect(stored?.rtcP2p).toBe(false); }); test('should clamp bitrate to 96000 without an audio bitrate feature', async () => { diff --git a/fluxer_api/src/api/guild/tests/GuildChannelPositions.test.ts b/fluxer_api/src/api/guild/tests/GuildChannelPositions.test.ts index 46cad30ee..dae2ccda9 100644 --- a/fluxer_api/src/api/guild/tests/GuildChannelPositions.test.ts +++ b/fluxer_api/src/api/guild/tests/GuildChannelPositions.test.ts @@ -227,7 +227,10 @@ describe('Guild Channel Positions', () => { {id: coopVoice.id, parent_id: coopGames.id}, ]); const channelRepository = new ChannelRepository(); - const channelsBeforeMove = await channelRepository.listGuildChannels(createGuildID(BigInt(guild.id)), 'enrolled'); + const channelsBeforeMove = await channelRepository.channelData.listGuildChannels( + createGuildID(BigInt(guild.id)), + 'enrolled', + ); const storedCoopText = channelsBeforeMove.find((channel) => channel.id.toString() === coopText.id); const storedCoopVoice = channelsBeforeMove.find((channel) => channel.id.toString() === coopVoice.id); expect(storedCoopText).toBeDefined(); @@ -235,8 +238,8 @@ describe('Guild Channel Positions', () => { if (!storedCoopText || !storedCoopVoice) { return; } - await channelRepository.upsert({...storedCoopVoice.toRow(), position: 1}); - await channelRepository.upsert({...storedCoopText.toRow(), position: 2}); + await channelRepository.channelData.upsert({...storedCoopVoice.toRow(), position: 1}); + await channelRepository.channelData.upsert({...storedCoopText.toRow(), position: 2}); await updateChannelPositions(harness, account.token, guild.id, [ { id: frontDoor.id, diff --git a/fluxer_api/src/api/guild/tests/ThreadPermissionBitsGate.test.ts b/fluxer_api/src/api/guild/tests/ThreadPermissionBitsGate.test.ts index 924143d6b..b59dbf68b 100644 --- a/fluxer_api/src/api/guild/tests/ThreadPermissionBitsGate.test.ts +++ b/fluxer_api/src/api/guild/tests/ThreadPermissionBitsGate.test.ts @@ -318,7 +318,7 @@ describe('thread permission bits across guild modes', () => { .execute(); const channelId = createChannelID(BigInt(channel.id)); const stored = async () => { - const overwrites = (await new ChannelRepository().findUnique(channelId))!.permissionOverwrites; + const overwrites = (await new ChannelRepository().channelData.findUnique(channelId))!.permissionOverwrites; return { everyone: overwrites.get(createRoleID(BigInt(guild.id))), target: overwrites.get(createRoleID(BigInt(target.id))), @@ -358,9 +358,14 @@ describe('thread permission bits across guild modes', () => { .execute(); const repository = new ChannelRepository(); const before = await repository.channelData.countGuildChannels(guildId); - const source = (await repository.findUnique(createChannelID(BigInt(text.id))))!; + const source = (await repository.channelData.findUnique(createChannelID(BigInt(text.id))))!; const forumId = createChannelID(BigInt(text.id) + 1n); - await repository.upsert({...source.toRow(), channel_id: forumId, type: ChannelTypes.GUILD_FORUM, name: 'forum'}); + await repository.channelData.upsert({ + ...source.toRow(), + channel_id: forumId, + type: ChannelTypes.GUILD_FORUM, + name: 'forum', + }); expect(await repository.channelData.countGuildChannels(guildId)).toBe(before + 1); await createBuilder(harness, owner.token) .delete(`/channels/${forumId}`) @@ -383,9 +388,14 @@ describe('thread permission bits across guild modes', () => { .body({name: 'text', type: ChannelTypes.GUILD_TEXT, parent_id: category.id}) .execute(); const repository = new ChannelRepository(); - const source = (await repository.findUnique(createChannelID(BigInt(text.id))))!; + const source = (await repository.channelData.findUnique(createChannelID(BigInt(text.id))))!; const forumId = createChannelID(BigInt(text.id) + 1n); - await repository.upsert({...source.toRow(), channel_id: forumId, type: ChannelTypes.GUILD_FORUM, name: 'forum'}); + await repository.channelData.upsert({ + ...source.toRow(), + channel_id: forumId, + type: ChannelTypes.GUILD_FORUM, + name: 'forum', + }); expect(await fetchOne(FETCH_MARKER, {guild_id: guildId})).not.toBeNull(); await setChannelThreadsConfig({enabled: false}); const dispatchSpy = vi.spyOn(NoopGatewayService.prototype, 'dispatchGuild'); @@ -398,12 +408,12 @@ describe('thread permission bits across guild modes', () => { .patch(`/channels/${category.id}`) .body({permission_overwrites: [{id: guild.id, type: 0, allow: '0', deny: Permissions.VIEW_CHANNEL.toString()}]}) .execute(); - expect((await repository.findUnique(forumId))?.permissionOverwrites.size).toBe(1); + expect((await repository.channelData.findUnique(forumId))?.permissionOverwrites.size).toBe(1); expect(updatedIds()).toContain(text.id); expect(updatedIds()).not.toContain(forumId.toString()); dispatchSpy.mockClear(); await createBuilder(harness, owner.token).delete(`/channels/${category.id}`).expect(204).execute(); - expect((await repository.findUnique(forumId))?.parentId).toBeNull(); + expect((await repository.channelData.findUnique(forumId))?.parentId).toBeNull(); expect(updatedIds()).toContain(text.id); expect(updatedIds()).not.toContain(forumId.toString()); } finally { @@ -429,7 +439,7 @@ describe('thread permission bits across guild modes', () => { const channelId = createChannelID(BigInt(channel.id)); const everyoneId = createRoleID(BigInt(guild.id)); const storedAllow = async () => - (await new ChannelRepository().findUnique(channelId))?.permissionOverwrites.get(everyoneId)?.allow; + (await new ChannelRepository().channelData.findUnique(channelId))?.permissionOverwrites.get(everyoneId)?.allow; await createBuilder(harness, owner.token) .patch(`/channels/${channel.id}`) .body({permission_overwrites: [{id: guild.id, type: 0, allow: Permissions.VIEW_CHANNEL.toString(), deny: '0'}]}) @@ -480,7 +490,7 @@ describe('thread permission bits across guild modes', () => { .body([{id: child.id, parent_id: category.id, lock_permissions: true}]) .expect(204); await (capable ? builder.header(FEATURES, CAPABLE) : builder).execute(); - const stored = await new ChannelRepository().findUnique(createChannelID(BigInt(child.id))); + const stored = await new ChannelRepository().channelData.findUnique(createChannelID(BigInt(child.id))); return stored?.permissionOverwrites.get(everyoneId)?.allow; }; expect(await moveAndRead(true)).toBe(Permissions.SEND_MESSAGES | inThreads); diff --git a/fluxer_api/src/api/infrastructure/DiscriminatorService.ts b/fluxer_api/src/api/infrastructure/DiscriminatorService.ts index e4b15f4fa..9d47f0337 100644 --- a/fluxer_api/src/api/infrastructure/DiscriminatorService.ts +++ b/fluxer_api/src/api/infrastructure/DiscriminatorService.ts @@ -8,7 +8,7 @@ import {resolveLimitSafe} from '@app/api/limits/LimitConfigUtils'; import {createLimitMatchContext} from '@app/api/limits/LimitMatchContextBuilder'; import type {User} from '@app/api/models/User'; import {isPremiumTieringActive} from '@app/api/stripe/BillingConfigCache'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes'; import {NON_SELF_HOSTED_RESERVED_DISCRIMINATORS} from '@fluxer/constants/src/DiscriminatorConstants'; import {BadRequestError} from '@fluxer/errors/src/domains/core/BadRequestError'; @@ -59,7 +59,7 @@ export class DiscriminatorService implements IDiscriminatorService { private static readonly DISCRIM_CACHE_TTL_S = seconds('30 seconds'); constructor( - private userRepository: IUserRepository, + private userRepository: UserRepository, private cacheService: ICacheService, private limitConfigService: LimitConfigService, ) {} diff --git a/fluxer_api/src/api/infrastructure/KVThreadAutoArchiveQueueService.test.ts b/fluxer_api/src/api/infrastructure/KVThreadAutoArchiveQueueService.test.ts index 41f7beb4f..837445a57 100644 --- a/fluxer_api/src/api/infrastructure/KVThreadAutoArchiveQueueService.test.ts +++ b/fluxer_api/src/api/infrastructure/KVThreadAutoArchiveQueueService.test.ts @@ -7,7 +7,7 @@ import { createGuildID, createUserID, } from '@app/api/BrandedTypes'; -import {ChannelRepository} from '@app/api/channel/repositories/ChannelRepository'; +import {ChannelRepository} from '@app/api/channel/ChannelRepository'; import {setCassandraQueryExecutorForTesting, upsertOne} from '@app/api/database/CassandraQueryExecution'; import type {ChannelRow} from '@app/api/database/types/ChannelTypes'; import { diff --git a/fluxer_api/src/api/infrastructure/VirusScanService.ts b/fluxer_api/src/api/infrastructure/VirusScanService.ts index 99e82d158..5dadbd736 100644 --- a/fluxer_api/src/api/infrastructure/VirusScanService.ts +++ b/fluxer_api/src/api/infrastructure/VirusScanService.ts @@ -1,43 +1,80 @@ // SPDX-License-Identifier: AGPL-3.0-or-later +import crypto from 'node:crypto'; +import {createReadStream} from 'node:fs'; +import path from 'node:path'; import {Config} from '@app/api/Config'; import {Logger} from '@app/api/Logger'; import type {ICacheService} from '@pkgs/cache/src/ICacheService'; -import {VirusHashCache} from '@pkgs/virus_scan/src/cache/VirusHashCache'; import type {IVirusScanService} from '@pkgs/virus_scan/src/IVirusScanService'; import {ClamAVProvider} from '@pkgs/virus_scan/src/providers/ClamAVProvider'; +import type {VirusScanProviderResult} from '@pkgs/virus_scan/src/VirusScanProviderResult'; import type {VirusScanResult} from '@pkgs/virus_scan/src/VirusScanResult'; -import {VirusScanService as SharedVirusScanService} from '@pkgs/virus_scan/src/VirusScanService'; +import {seconds} from 'itty-time'; + +function describeError(error: unknown): string { + if (typeof error === 'string') { + return error; + } + if (error instanceof Error) { + return error.message; + } + return 'Unknown error'; +} export class VirusScanService implements IVirusScanService { readonly enabled = true; - private readonly service: SharedVirusScanService; + private readonly provider = new ClamAVProvider({host: Config.clamav.host, port: Config.clamav.port}); + private readonly failOpen = Config.clamav.failOpen; - constructor(cacheService: ICacheService) { - const provider = new ClamAVProvider({ - host: Config.clamav.host, - port: Config.clamav.port, - }); - const virusHashCache = new VirusHashCache(cacheService); - this.service = new SharedVirusScanService({ - provider, - virusHashCache, - logger: Logger, - config: { - failOpen: Config.clamav.failOpen, - }, - }); - } + constructor(private cacheService: ICacheService) {} - async initialize(): Promise { - await this.service.initialize(); - } + async initialize(): Promise {} async scanFile(filePath: string): Promise { - return this.service.scanFile(filePath); + const hash = crypto.createHash('sha256'); + const stream = createReadStream(filePath, {highWaterMark: 1024 * 1024}); + try { + for await (const chunk of stream) { + hash.update(chunk as Buffer); + } + } catch (error) { + stream.destroy(); + throw error; + } + return this.scan(hash.digest('hex'), path.basename(filePath), () => this.provider.scanFile(filePath)); } async scanBuffer(buffer: Buffer, filename: string): Promise { - return this.service.scanBuffer(buffer, filename); + const fileHash = crypto.createHash('sha256').update(buffer).digest('hex'); + return this.scan(fileHash, filename, () => this.provider.scanBuffer(buffer)); + } + + private async scan( + fileHash: string, + filename: string, + run: () => Promise, + ): Promise { + const cacheKey = `virus:${fileHash}`; + if ((await this.cacheService.get(cacheKey)) != null) { + return {isClean: false, threat: 'Cached virus signature', fileHash}; + } + try { + const scanResult = await run(); + if (scanResult.isClean) { + return {isClean: true, fileHash}; + } + if (!scanResult.threat) { + throw new Error('Virus scan provider returned infected status without threat name'); + } + await this.cacheService.set(cacheKey, 'true', seconds('7 days')); + return {isClean: false, threat: scanResult.threat, fileHash}; + } catch (error) { + Logger.error({error: describeError(error), filename, fileHash}, 'Virus scan failed'); + if (this.failOpen) { + return {isClean: true, fileHash}; + } + throw new Error(`Virus scan failed: ${describeError(error)}`); + } } } diff --git a/fluxer_api/src/api/infrastructure/activity/ActivityMeta.ts b/fluxer_api/src/api/infrastructure/activity/ActivityMeta.ts index 3fdbf6df2..db434d815 100644 --- a/fluxer_api/src/api/infrastructure/activity/ActivityMeta.ts +++ b/fluxer_api/src/api/infrastructure/activity/ActivityMeta.ts @@ -7,7 +7,7 @@ import type {HonoEnv} from '@app/api/types/HonoEnv'; import {lookupGeoip} from '@app/api/utils/IpUtils'; import {getRequestClientIp} from '@app/api/utils/RequestClientIp'; import {stripApiPrefix} from '@app/api/utils/RequestPathUtils'; -import {parseAcceptLanguage} from '@pkgs/locale/src/LocaleService'; +import {parseAcceptLanguage} from '@pkgs/locale/src/resolution/AcceptLanguageNegotiation'; import type {Context} from 'hono'; import {createMiddleware} from 'hono/factory'; diff --git a/fluxer_api/src/api/infrastructure/activity/ModerationEvents.ts b/fluxer_api/src/api/infrastructure/activity/ModerationEvents.ts index e8d0babb3..9b41815a8 100644 --- a/fluxer_api/src/api/infrastructure/activity/ModerationEvents.ts +++ b/fluxer_api/src/api/infrastructure/activity/ModerationEvents.ts @@ -4,8 +4,7 @@ import type {GuildID, UserID} from '@app/api/BrandedTypes'; import {emitActivity} from '@app/api/infrastructure/activity/ActivityEvents'; import {anonymousActivityMeta} from '@app/api/infrastructure/activity/ActivityMeta'; import type {BanBy, ReportOutcome, ReportTarget, ResolvedBy} from '@app/api/infrastructure/activity/Contract.generated'; -import type {IARSubmission} from '@app/api/report/IReportRepository'; -import {ReportType} from '@app/api/report/IReportRepository'; +import {type IARSubmission, ReportType} from '@app/api/report/ReportModels'; interface GuildBanFacts { guildId: GuildID; diff --git a/fluxer_api/src/api/infrastructure/tests/LiveKitWebhookService.test.ts b/fluxer_api/src/api/infrastructure/tests/LiveKitWebhookService.test.ts index db7c344ce..1eb376455 100644 --- a/fluxer_api/src/api/infrastructure/tests/LiveKitWebhookService.test.ts +++ b/fluxer_api/src/api/infrastructure/tests/LiveKitWebhookService.test.ts @@ -6,7 +6,7 @@ import type {ILiveKitService} from '@app/api/infrastructure/ILiveKitService'; import type {IVoiceRoomStore} from '@app/api/infrastructure/IVoiceRoomStore'; import {SERVER_MUTE_ATTRIBUTE} from '@app/api/infrastructure/LiveKitService'; import {LiveKitWebhookService} from '@app/api/infrastructure/LiveKitWebhookService'; -import type {IVoiceRepository} from '@app/api/voice/IVoiceRepository'; +import type {VoiceRepository} from '@app/api/voice/VoiceRepository'; import {VoiceTopology} from '@app/api/voice/VoiceTopology'; import {TrackSource, type WebhookEvent} from 'livekit-server-sdk'; import {describe, expect, it, vi} from 'vitest'; @@ -33,7 +33,7 @@ function harness(pinnedServerId: string | null) { voiceRoomStore, gatewayService, liveKitService, - new VoiceTopology({} as unknown as IVoiceRepository, null), + new VoiceTopology({} as unknown as VoiceRepository, null), ); return {service, deleteRoomServer}; } @@ -92,7 +92,7 @@ function trackHarness() { {} as unknown as IVoiceRoomStore, {} as unknown as IGatewayService, {muteMicrophoneTrack} as unknown as ILiveKitService, - new VoiceTopology({} as unknown as IVoiceRepository, null), + new VoiceTopology({} as unknown as VoiceRepository, null), ); return {service, muteMicrophoneTrack}; } diff --git a/fluxer_api/src/api/instance/SingleCommunityService.ts b/fluxer_api/src/api/instance/SingleCommunityService.ts index 5cb73ae38..40810b654 100644 --- a/fluxer_api/src/api/instance/SingleCommunityService.ts +++ b/fluxer_api/src/api/instance/SingleCommunityService.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {createGuildID, type GuildID, type UserID} from '@app/api/BrandedTypes'; -import type {GuildDataService} from '@app/api/guild/services/GuildDataService'; +import type {GuildOperationsService} from '@app/api/guild/services/data/GuildOperationsService'; import type {GuildMemberService} from '@app/api/guild/services/GuildMemberService'; import type {InstanceConfigRepository} from '@app/api/instance/InstanceConfigRepository'; import {Logger} from '@app/api/Logger'; @@ -13,7 +13,7 @@ import {UnknownGuildError} from '@fluxer/errors/src/domains/guild/UnknownGuildEr export class SingleCommunityService { constructor( private readonly instanceConfigRepository: InstanceConfigRepository, - private readonly guildDataService: GuildDataService, + private readonly guildOperations: GuildOperationsService, private readonly guildMemberService: GuildMemberService, ) {} @@ -66,7 +66,7 @@ export class SingleCommunityService { } const guildId = createGuildID(BigInt(rawGuildId)); try { - await this.guildDataService.getGuildSystem(guildId); + await this.guildOperations.getGuildSystem(guildId); return guildId; } catch (error) { if (error instanceof UnknownGuildError) return null; @@ -75,7 +75,7 @@ export class SingleCommunityService { } async createStockCommunity(params: {owner: User; name: string}): Promise { - const guild = await this.guildDataService.createGuild({user: params.owner, data: {name: params.name}}); + const guild = await this.guildOperations.createGuild({user: params.owner, data: {name: params.name}}); const guildId = createGuildID(BigInt(guild.id)); await this.instanceConfigRepository.setInstancePolicyConfig({ single_community_enabled: true, diff --git a/fluxer_api/src/api/invite/IInviteRepository.ts b/fluxer_api/src/api/invite/IInviteRepository.ts deleted file mode 100644 index a29a5bc6b..000000000 --- a/fluxer_api/src/api/invite/IInviteRepository.ts +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {ChannelID, GuildID, InviteCode, UserID} from '@app/api/BrandedTypes'; -import type {Invite} from '@app/api/models/Invite'; - -export abstract class IInviteRepository { - abstract findUnique(code: InviteCode): Promise; - - abstract listChannelInvites(channelId: ChannelID): Promise>; - - abstract listGuildInvites(guildId: GuildID): Promise>; - - abstract create(data: { - code: InviteCode; - type: number; - guild_id: GuildID | null; - channel_id?: ChannelID | null; - inviter_id?: UserID | null; - uses: number; - max_uses: number; - max_age: number; - temporary?: boolean; - }): Promise; - - abstract updateInviteUses(code: InviteCode, uses: number, invite: Invite): Promise; - - abstract compareAndSetInviteUses(invite: Invite, uses: number): Promise; - - abstract delete(code: InviteCode): Promise; -} diff --git a/fluxer_api/src/api/invite/InviteRepository.ts b/fluxer_api/src/api/invite/InviteRepository.ts index 9ac8cf2fd..366d4fb76 100644 --- a/fluxer_api/src/api/invite/InviteRepository.ts +++ b/fluxer_api/src/api/invite/InviteRepository.ts @@ -11,7 +11,6 @@ import { } from '@app/api/database/CassandraQueryExecution'; import {Db} from '@app/api/database/CassandraTypes'; import type {InviteRow} from '@app/api/database/types/ChannelTypes'; -import {IInviteRepository} from '@app/api/invite/IInviteRepository'; import {Invite} from '@app/api/models/Invite'; import {Invites, InvitesByChannel, InvitesByGuild} from '@app/api/Tables'; @@ -31,7 +30,7 @@ const FETCH_INVITES_BY_GUILD_CQL = InvitesByGuild.selectCql({ interface CreateInviteParams { code: InviteCode; type: number; - guild_id: GuildID; + guild_id: GuildID | null; channel_id?: ChannelID | null; inviter_id?: UserID | null; uses: number; @@ -40,7 +39,7 @@ interface CreateInviteParams { temporary?: boolean; } -export class InviteRepository extends IInviteRepository { +export class InviteRepository { async findUnique(code: InviteCode): Promise { const invite = await fetchOne(FETCH_INVITE_BY_CODE_CQL, {code}); return invite ? new Invite(invite) : null; diff --git a/fluxer_api/src/api/invite/InviteRequestService.ts b/fluxer_api/src/api/invite/InviteRequestService.ts index aac044188..4be4f4993 100644 --- a/fluxer_api/src/api/invite/InviteRequestService.ts +++ b/fluxer_api/src/api/invite/InviteRequestService.ts @@ -140,7 +140,7 @@ export class InviteRequestService { await this.channelService.channelData.operations.getChannelSystem(channelId), getChannelMemberCount: async (channelId: ChannelID) => await this.channelService.channelData.operations.getChannelMemberCount(channelId), - getGuildResponse: async (guildId: GuildID) => await this.guildService.data.getPublicGuildData(guildId), + getGuildResponse: async (guildId: GuildID) => await this.guildService.data.operations.getPublicGuildData(guildId), getGuildCounts: async (guildId: GuildID) => await this.gatewayService.getGuildCounts(guildId), gatewayService: this.gatewayService, }; diff --git a/fluxer_api/src/api/invite/InviteService.ts b/fluxer_api/src/api/invite/InviteService.ts index 805b1f51e..2c4f7da98 100644 --- a/fluxer_api/src/api/invite/InviteService.ts +++ b/fluxer_api/src/api/invite/InviteService.ts @@ -7,7 +7,7 @@ import type {ChannelService} from '@app/api/channel/services/ChannelService'; import {THREAD_FEATURE_CHANNEL_TYPES, type ThreadViewer} from '@app/api/experiment/ChannelThreadsGate'; import type {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService'; import type {GuildService} from '@app/api/guild/services/GuildService'; -import type {IInviteRepository} from '@app/api/invite/IInviteRepository'; +import type {InviteRepository} from '@app/api/invite/InviteRepository'; import {Logger} from '@app/api/Logger'; import type {LimitConfigService} from '@app/api/limits/LimitConfigService'; import {resolveLimitSafe} from '@app/api/limits/LimitConfigUtils'; @@ -105,7 +105,7 @@ interface ReusableInviteCriteria { export class InviteService { constructor( private readonly apiContext: ApiContext, - private inviteRepository: IInviteRepository, + private inviteRepository: InviteRepository, private guildService: GuildService, private channelService: ChannelService, private readonly guildAuditLogService: GuildAuditLogService, @@ -251,7 +251,7 @@ export class InviteService { if (!invite) throw new UnknownInviteError(); if (invite.maxUses > 0 && invite.uses >= invite.maxUses) { if (invite.type === InviteTypes.GUILD && invite.guildId) { - const guild = await this.guildService.data.getGuildSystem(invite.guildId); + const guild = await this.guildService.data.operations.getGuildSystem(invite.guildId); const vanityCode = guild.vanityUrlCode ? vanityCodeToInviteCode(guild.vanityUrlCode) : null; if (invite.code !== vanityCode) { await this.inviteRepository.delete(invite.code); @@ -286,7 +286,7 @@ export class InviteService { return this.completeInviteUse(reservedInvite, {deleteWhenExhausted: true}); } if (!invite.guildId) throw new UnknownInviteError(); - const guild = await this.guildService.data.getGuildSystem(invite.guildId); + const guild = await this.guildService.data.operations.getGuildSystem(invite.guildId); if ((guild.disabledOperations & GuildOperations.INSTANT_INVITES) !== 0) { throw new FeatureTemporarilyDisabledError(); } diff --git a/fluxer_api/src/api/invite/tests/InvitePermissions.test.ts b/fluxer_api/src/api/invite/tests/InvitePermissions.test.ts index a989d9b22..b097a0ac1 100644 --- a/fluxer_api/src/api/invite/tests/InvitePermissions.test.ts +++ b/fluxer_api/src/api/invite/tests/InvitePermissions.test.ts @@ -105,38 +105,4 @@ describe('Invite Permissions', () => { await deleteInvite(harness, owner.token, invite.code); await createBuilder(harness, owner.token).get(`/invites/${invite.code}`).expect(HTTP_STATUS.NOT_FOUND).execute(); }); - test('full permission flow: stripped permissions block all invite operations for member', async () => { - const owner = await createTestAccount(harness); - const member = await createTestAccount(harness); - const guild = await createGuild(harness, owner.token, 'Full Permission Test Guild'); - const systemChannel = await getChannel(harness, owner.token, guild.system_channel_id!); - const ownerInvite = await createChannelInvite(harness, owner.token, systemChannel.id); - await updateRole(harness, owner.token, guild.id, guild.id, { - permissions: BASIC_PERMISSIONS.toString(), - }); - const joinInvite = await createChannelInvite(harness, owner.token, systemChannel.id); - await acceptInvite(harness, member.token, joinInvite.code); - await createBuilder(harness, member.token) - .post(`/channels/${systemChannel.id}/invites`) - .body({}) - .expect(HTTP_STATUS.FORBIDDEN) - .execute(); - await createBuilder(harness, member.token) - .delete(`/invites/${ownerInvite.code}`) - .expect(HTTP_STATUS.FORBIDDEN) - .execute(); - await createBuilder(harness, member.token) - .get(`/channels/${systemChannel.id}/invites`) - .expect(HTTP_STATUS.FORBIDDEN) - .execute(); - await createBuilder(harness, member.token) - .get(`/guilds/${guild.id}/invites`) - .expect(HTTP_STATUS.FORBIDDEN) - .execute(); - await deleteInvite(harness, owner.token, ownerInvite.code); - await createBuilder(harness, owner.token) - .get(`/invites/${ownerInvite.code}`) - .expect(HTTP_STATUS.NOT_FOUND) - .execute(); - }); }); diff --git a/fluxer_api/src/api/invite/tests/InviteSecurityChecks.test.ts b/fluxer_api/src/api/invite/tests/InviteSecurityChecks.test.ts index dc144ab7f..52fb2dcca 100644 --- a/fluxer_api/src/api/invite/tests/InviteSecurityChecks.test.ts +++ b/fluxer_api/src/api/invite/tests/InviteSecurityChecks.test.ts @@ -57,23 +57,6 @@ describe('Invite Security Checks', () => { .execute(); await deleteInvite(harness, owner.token, invite.code); }); - test('owner can delete invite', async () => { - const owner = await createTestAccount(harness); - const guild = await createGuild(harness, owner.token, 'Invite Security Guild'); - const systemChannel = await getChannel(harness, owner.token, guild.system_channel_id!); - const invite = await createChannelInvite(harness, owner.token, systemChannel.id); - await deleteInvite(harness, owner.token, invite.code); - await createBuilder(harness, owner.token).get(`/invites/${invite.code}`).expect(HTTP_STATUS.NOT_FOUND).execute(); - }); - test('deleted invite cannot be retrieved', async () => { - const owner = await createTestAccount(harness); - const guild = await createGuild(harness, owner.token, 'Invite Security Guild'); - const systemChannel = await getChannel(harness, owner.token, guild.system_channel_id!); - const invite = await createChannelInvite(harness, owner.token, systemChannel.id); - await createBuilder(harness, owner.token).get(`/invites/${invite.code}`).expect(HTTP_STATUS.OK).execute(); - await deleteInvite(harness, owner.token, invite.code); - await createBuilder(harness, owner.token).get(`/invites/${invite.code}`).expect(HTTP_STATUS.NOT_FOUND).execute(); - }); test('deleted invite cannot be accepted', async () => { const owner = await createTestAccount(harness); const joiner = await createTestAccount(harness); @@ -119,24 +102,6 @@ describe('Invite Security Checks', () => { expect(updatedInvite!.uses).toBe(1); await deleteInvite(harness, owner.token, inviteCode); }); - test('invite with max_uses limit becomes invalid after exhaustion', async () => { - const owner = await createTestAccount(harness); - const joiner1 = await createTestAccount(harness); - const joiner2 = await createTestAccount(harness); - const guild = await createGuild(harness, owner.token, 'Max Uses Guild'); - const systemChannel = await getChannel(harness, owner.token, guild.system_channel_id!); - const invite = await createBuilder(harness, owner.token) - .post(`/channels/${systemChannel.id}/invites`) - .body({max_uses: 1}) - .execute(); - expect(invite.max_uses).toBe(1); - await acceptInvite(harness, joiner1.token, invite.code); - await createBuilder(harness, joiner2.token) - .post(`/invites/${invite.code}`) - .body(null) - .expect(HTTP_STATUS.NOT_FOUND) - .execute(); - }); test('banned user cannot use invite to rejoin guild', async () => { const owner = await createTestAccount(harness); const bannedUser = await createTestAccount(harness); @@ -198,21 +163,6 @@ describe('Invite Security Checks', () => { .expect(HTTP_STATUS.OK) .execute(); }); - test('non-member can view public invite', async () => { - const owner = await createTestAccount(harness); - const nonMember = await createTestAccount(harness); - const guild = await createGuild(harness, owner.token, 'Public Invite Guild'); - const systemChannel = await getChannel(harness, owner.token, guild.system_channel_id!); - const invite = await createChannelInvite(harness, owner.token, systemChannel.id); - const inviteData = await createBuilder<{ - code: string; - }>(harness, nonMember.token) - .get(`/invites/${invite.code}`) - .expect(HTTP_STATUS.OK) - .execute(); - expect(inviteData.code).toBe(invite.code); - await deleteInvite(harness, owner.token, invite.code); - }); test('unauthenticated request can view public invite', async () => { const owner = await createTestAccount(harness); const guild = await createGuild(harness, owner.token, 'Public Invite Guild'); diff --git a/fluxer_api/src/api/jobs/IJobLedgerRepository.ts b/fluxer_api/src/api/jobs/IJobLedgerRepository.ts deleted file mode 100644 index cfce3fc7d..000000000 --- a/fluxer_api/src/api/jobs/IJobLedgerRepository.ts +++ /dev/null @@ -1,69 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {JobByIdRow, JobStatus} from '@app/api/database/types/JobLedgerTypes'; - -export interface CreateJobInput { - jobId: bigint; - taskType: string; - payload: Record; - requestedByUserId: bigint | null; - auditLogReason: string | null; - maxAttempts: number; - runAt: Date | null; - jetStreamLane: string | null; - jetStreamSeq: string | null; -} - -export interface ListJobsCursor { - bucketDay: string; - createdAt: Date; - jobId: bigint; -} - -export interface ListJobsFilters { - status?: JobStatus | null; - taskType?: string | null; - requestedByUserId?: bigint | null; -} - -export interface ListJobsResult { - jobs: Array; - nextCursor: ListJobsCursor | null; -} - -export abstract class IJobLedgerRepository { - abstract createJob(input: CreateJobInput): Promise; - - abstract getJob(jobId: bigint): Promise; - - abstract discardJob(jobId: bigint, createdAt: Date): Promise; - - abstract markRunning(jobId: bigint, lane: string): Promise; - - abstract markSucceeded(jobId: bigint, result: Record | null): Promise; - - abstract markCancelled(jobId: bigint): Promise; - - abstract markDeadletter(jobId: bigint, errorMessage: string): Promise; - - abstract reportProgress(jobId: bigint, current: number, total: number | null, message: string | null): Promise; - - abstract setContextLink(jobId: bigint, link: string): Promise; - - abstract setJetStreamSeq(jobId: bigint, seq: string): Promise; - - abstract requestCancel(jobId: bigint): Promise; - - abstract isCancelRequested(jobId: bigint): Promise; - - abstract incrementAttempts(jobId: bigint): Promise; - - abstract listJobs(opts: { - limit: number; - cursor: ListJobsCursor | null; - filters: ListJobsFilters; - maxLookbackDays: number; - }): Promise; - - abstract listActiveJobs(): Promise>; -} diff --git a/fluxer_api/src/api/jobs/JobAdminService.ts b/fluxer_api/src/api/jobs/JobAdminService.ts index c357308dd..edfba9d09 100644 --- a/fluxer_api/src/api/jobs/JobAdminService.ts +++ b/fluxer_api/src/api/jobs/JobAdminService.ts @@ -1,13 +1,13 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {JobByIdRow} from '@app/api/database/types/JobLedgerTypes'; -import type {IJobLedgerRepository} from '@app/api/jobs/IJobLedgerRepository'; +import type {JobLedgerRepository} from '@app/api/jobs/JobLedgerRepository'; import type {JobLedgerEntry, ListJobsRequest} from '@fluxer/schema/src/domains/admin/JobsSchemas'; import type {IWorkerService} from '@pkgs/worker/src/contracts/IWorkerService'; export class JobAdminService { constructor( - private readonly ledger: IJobLedgerRepository, + private readonly ledger: JobLedgerRepository, private readonly workerService: IWorkerService, ) {} diff --git a/fluxer_api/src/api/jobs/JobLedgerRepository.ts b/fluxer_api/src/api/jobs/JobLedgerRepository.ts index f05c0ce2a..47577ee1d 100644 --- a/fluxer_api/src/api/jobs/JobLedgerRepository.ts +++ b/fluxer_api/src/api/jobs/JobLedgerRepository.ts @@ -11,19 +11,41 @@ import { } from '@app/api/database/CassandraQueryExecution'; import {Db} from '@app/api/database/CassandraTypes'; import type {JobActiveRow, JobByDayBucketRow, JobByIdRow, JobStatus} from '@app/api/database/types/JobLedgerTypes'; -import { - type CreateJobInput, - IJobLedgerRepository, - type ListJobsCursor, - type ListJobsFilters, - type ListJobsResult, -} from '@app/api/jobs/IJobLedgerRepository'; import {JobsActive, JobsByDayBucket, JobsById} from '@app/api/Tables'; import {awaitAll} from '@app/api/utils/ConcurrencyUtils'; import {JOBS_STREAM_MAX_AGE_MS} from '@app/api/worker/JetStreamWorkerQueue'; import {snowflakeToDate} from '@fluxer/snowflake/src/Snowflake'; import {ms, seconds} from 'itty-time'; +export interface CreateJobInput { + jobId: bigint; + taskType: string; + payload: Record; + requestedByUserId: bigint | null; + auditLogReason: string | null; + maxAttempts: number; + runAt: Date | null; + jetStreamLane: string | null; + jetStreamSeq: string | null; +} + +export interface ListJobsCursor { + bucketDay: string; + createdAt: Date; + jobId: bigint; +} + +export interface ListJobsFilters { + status?: JobStatus | null; + taskType?: string | null; + requestedByUserId?: bigint | null; +} + +export interface ListJobsResult { + jobs: Array; + nextCursor: ListJobsCursor | null; +} + export const JOB_LEDGER_TTL_SECONDS = seconds('90 days'); export const JOB_STALE_AFTER_MS = JOBS_STREAM_MAX_AGE_MS + ms('1 day'); export const EXPIRED_JOB_ERROR = 'Expired from the job queue'; @@ -88,7 +110,7 @@ async function fetchDayAfter( return {rows: [...ties, ...older], exhausted: limit === null || older.length < limit}; } -export class JobLedgerRepository extends IJobLedgerRepository { +export class JobLedgerRepository { async createJob(input: CreateJobInput): Promise { const now = new Date(); const status: JobStatus = 'queued'; diff --git a/fluxer_api/src/api/jobs/ListJobsPagingSuite.ts b/fluxer_api/src/api/jobs/ListJobsPagingSuite.ts index f793a5462..34f11484d 100644 --- a/fluxer_api/src/api/jobs/ListJobsPagingSuite.ts +++ b/fluxer_api/src/api/jobs/ListJobsPagingSuite.ts @@ -2,8 +2,7 @@ import {upsertOne} from '@app/api/database/CassandraQueryExecution'; import type {JobByIdRow, JobStatus} from '@app/api/database/types/JobLedgerTypes'; -import type {ListJobsCursor, ListJobsFilters} from '@app/api/jobs/IJobLedgerRepository'; -import {JobLedgerRepository} from '@app/api/jobs/JobLedgerRepository'; +import {JobLedgerRepository, type ListJobsCursor, type ListJobsFilters} from '@app/api/jobs/JobLedgerRepository'; import {JobsByDayBucket, JobsById} from '@app/api/Tables'; import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; diff --git a/fluxer_api/src/api/limits/tests/LimitWireFormat.test.ts b/fluxer_api/src/api/limits/tests/LimitWireFormat.test.ts index cf5077972..40490159a 100644 --- a/fluxer_api/src/api/limits/tests/LimitWireFormat.test.ts +++ b/fluxer_api/src/api/limits/tests/LimitWireFormat.test.ts @@ -54,26 +54,12 @@ describe('Limit Wire Format', () => { .execute()) as WellKnownResponse; expect(response.limits.rules.length).toBeGreaterThan(0); for (const rule of response.limits.rules) { - expect(rule.id).toBeDefined(); + expect(typeof rule.id).toBe('string'); expect(rule.overrides).toBeDefined(); expect(typeof rule.overrides).toBe('object'); expect(Reflect.get(rule, 'limits')).toBeUndefined(); } }); - test('wire format already has overrides field', async () => { - const response = (await createBuilderWithoutAuth(harness) - .get('/.well-known/fluxer') - .expect(HTTP_STATUS.OK) - .execute()) as WellKnownResponse; - expect(response.limits.version).toBe(2); - expect(response.limits.defaultsHash).toBeDefined(); - expect(typeof response.limits.defaultsHash).toBe('string'); - expect(response.limits.defaultsHash.length).toBeGreaterThan(0); - for (const rule of response.limits.rules) { - expect(typeof rule.overrides).toBe('object'); - expect(Reflect.get(rule, 'limits')).toBeUndefined(); - } - }); test('defaultsHash matches computed hash', async () => { const response = (await createBuilderWithoutAuth(harness) .get('/.well-known/fluxer') @@ -82,26 +68,6 @@ describe('Limit Wire Format', () => { const expectedHash = computeDefaultsHash(); expect(response.limits.defaultsHash).toBe(expectedHash); }); - test('wire format can be expanded back to full format', async () => { - const response = (await createBuilderWithoutAuth(harness) - .get('/.well-known/fluxer') - .expect(HTTP_STATUS.OK) - .execute()) as WellKnownResponse; - const expanded = expandWireFormat(response.limits); - expect(expanded.traitDefinitions).toEqual(response.limits.traitDefinitions); - expect(expanded.rules.length).toBe(response.limits.rules.length); - for (let i = 0; i < expanded.rules.length; i++) { - const expandedRule = expanded.rules[i]; - const wireRule = response.limits.rules[i]; - expect(expandedRule.id).toBe(wireRule.id); - expect(expandedRule.filters).toEqual(wireRule.filters); - expect(expandedRule.limits).toBeDefined(); - expect(typeof expandedRule.limits).toBe('object'); - for (const key of Object.keys(DEFAULT_FREE_LIMITS) as Array) { - expect(expandedRule.limits[key]).toBeDefined(); - } - } - }); test('premium rules have correct overrides compared to free defaults', async () => { const response = (await createBuilderWithoutAuth(harness) .get('/.well-known/fluxer') @@ -179,48 +145,4 @@ describe('Limit Wire Format', () => { expect(premiumRule.overrides[key]).toBe(1); } }); - test('wire format preserves trait definitions', async () => { - const response = (await createBuilderWithoutAuth(harness) - .get('/.well-known/fluxer') - .expect(HTTP_STATUS.OK) - .execute()) as WellKnownResponse; - expect(response.limits.traitDefinitions).toBeDefined(); - expect(Array.isArray(response.limits.traitDefinitions)).toBe(true); - }); - test('wire format preserves rule filters', async () => { - const response = (await createBuilderWithoutAuth(harness) - .get('/.well-known/fluxer') - .expect(HTTP_STATUS.OK) - .execute()) as WellKnownResponse; - for (const rule of response.limits.rules) { - if (rule.filters) { - expect(typeof rule.filters).toBe('object'); - } - } - }); - test('defaultsHash is stable across conversions', async () => { - const hash1 = computeDefaultsHash(); - const hash2 = computeDefaultsHash(); - expect(hash1).toBe(hash2); - expect(typeof hash1).toBe('string'); - expect(hash1.length).toBeGreaterThan(0); - }); - test('wire format type definition matches runtime structure', async () => { - const response = (await createBuilderWithoutAuth(harness) - .get('/.well-known/fluxer') - .expect(HTTP_STATUS.OK) - .execute()) as WellKnownResponse; - const wireFormat: LimitConfigWireFormat = response.limits; - expect(wireFormat.version).toBe(2); - expect(typeof wireFormat.defaultsHash).toBe('string'); - expect(Array.isArray(wireFormat.traitDefinitions)).toBe(true); - expect(Array.isArray(wireFormat.rules)).toBe(true); - for (const rule of wireFormat.rules) { - expect(typeof rule.id).toBe('string'); - expect(typeof rule.overrides).toBe('object'); - if (rule.filters) { - expect(typeof rule.filters).toBe('object'); - } - } - }); }); diff --git a/fluxer_api/src/api/middleware/GuildAvailabilityMiddleware.ts b/fluxer_api/src/api/middleware/GuildAvailabilityMiddleware.ts index cd99ef122..252aa62cb 100644 --- a/fluxer_api/src/api/middleware/GuildAvailabilityMiddleware.ts +++ b/fluxer_api/src/api/middleware/GuildAvailabilityMiddleware.ts @@ -63,7 +63,7 @@ async function resolveGuildIdForRequest(ctx: Context, path: string): Pr if (channelId === null) { return null; } - const channel = await ctx.get('channelRepository').findUnique(channelId); + const channel = await ctx.get('channelRepository').channelData.findUnique(channelId); ctx.get('requestCache')?.channels.set(channelId, channel); return channel?.guildId ?? null; } @@ -81,7 +81,7 @@ export const GuildAvailabilityMiddleware = createMiddleware(async (ctx, return; } try { - const guild = await ctx.get('guildService').data.getGuildSystem(guildId); + const guild = await ctx.get('guildService').data.operations.getGuildSystem(guildId); ctx.get('requestCache')?.guilds.set(guildId, guild); if (isGuildUnavailableForUser(guild, user)) { throw new MissingAccessError(); diff --git a/fluxer_api/src/api/middleware/GuildStackServiceFactory.ts b/fluxer_api/src/api/middleware/GuildStackServiceFactory.ts index cce4aac4b..d86162679 100644 --- a/fluxer_api/src/api/middleware/GuildStackServiceFactory.ts +++ b/fluxer_api/src/api/middleware/GuildStackServiceFactory.ts @@ -1,12 +1,12 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {ApiContext} from '@app/api/ApiContext'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {AttachmentUploadTraceRepository} from '@app/api/channel/repositories/message/AttachmentUploadTraceRepository'; import {ChannelService} from '@app/api/channel/services/ChannelService'; -import type {IFavoriteMemeRepository} from '@app/api/favorite_meme/IFavoriteMemeRepository'; +import type {FavoriteMemeRepository} from '@app/api/favorite_meme/FavoriteMemeRepository'; import type {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import {GuildService} from '@app/api/guild/services/GuildService'; import type {AvatarService} from '@app/api/infrastructure/AvatarService'; import type {IPurgeQueue} from '@app/api/infrastructure/CachePurgeQueue'; @@ -21,19 +21,19 @@ import type {InviteRepository} from '@app/api/invite/InviteRepository'; import {InviteService} from '@app/api/invite/InviteService'; import type {LimitConfigService} from '@app/api/limits/LimitConfigService'; import type {ReadStateService} from '@app/api/read_state/ReadStateService'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import type {VoiceAvailabilityService} from '@app/api/voice/VoiceAvailabilityService'; -import type {IWebhookRepository} from '@app/api/webhook/IWebhookRepository'; +import type {WebhookRepository} from '@app/api/webhook/WebhookRepository'; import type {IVirusScanService} from '@pkgs/virus_scan/src/IVirusScanService'; interface GuildStackServiceFactoryDependencies { apiContext: ApiContext; - channelRepository: IChannelRepository; - userRepository: IUserRepository; - guildRepository: IGuildRepositoryAggregate; + channelRepository: ChannelRepository; + userRepository: UserRepository; + guildRepository: GuildRepository; inviteRepository: InviteRepository; - webhookRepository: IWebhookRepository; - favoriteMemeRepository: IFavoriteMemeRepository; + webhookRepository: WebhookRepository; + favoriteMemeRepository: FavoriteMemeRepository; avatarService: AvatarService; entityAssetService: EntityAssetService; assetDeletionQueue: IAssetDeletionQueue; diff --git a/fluxer_api/src/api/middleware/IpBanMiddleware.ts b/fluxer_api/src/api/middleware/IpBanMiddleware.ts index 804ae79ff..13264aac8 100644 --- a/fluxer_api/src/api/middleware/IpBanMiddleware.ts +++ b/fluxer_api/src/api/middleware/IpBanMiddleware.ts @@ -1,7 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import {AdminRepository} from '@app/api/admin/AdminRepository'; -import type {BannedIpEntry, BannedIpKind} from '@app/api/admin/IAdminRepository'; +import {AdminRepository, type BannedIpEntry, type BannedIpKind} from '@app/api/admin/AdminRepository'; import {isIpBanExempt} from '@app/api/ban/IpBanExemptions'; import {IP_BAN_REFRESH_CHANNEL} from '@app/api/constants/IpBan'; import {sharedListHas} from '@app/api/infrastructure/activity/SharedLists'; diff --git a/fluxer_api/src/api/middleware/LocaleMiddleware.ts b/fluxer_api/src/api/middleware/LocaleMiddleware.ts index d48baaf8c..3a6f9086f 100644 --- a/fluxer_api/src/api/middleware/LocaleMiddleware.ts +++ b/fluxer_api/src/api/middleware/LocaleMiddleware.ts @@ -2,7 +2,7 @@ import type {HonoEnv} from '@app/api/types/HonoEnv'; import {Locales} from '@fluxer/constants/src/Locales'; -import {parseAcceptLanguage} from '@pkgs/locale/src/LocaleService'; +import {parseAcceptLanguage} from '@pkgs/locale/src/resolution/AcceptLanguageNegotiation'; import {createMiddleware} from 'hono/factory'; export const LocaleMiddleware = createMiddleware(async (ctx, next) => { diff --git a/fluxer_api/src/api/middleware/RequestErrorTelemetry.ts b/fluxer_api/src/api/middleware/RequestErrorTelemetry.ts index 502ff18a6..d1c68fc57 100644 --- a/fluxer_api/src/api/middleware/RequestErrorTelemetry.ts +++ b/fluxer_api/src/api/middleware/RequestErrorTelemetry.ts @@ -2,11 +2,10 @@ import {createHash} from 'node:crypto'; import {isIpBanExempt} from '@app/api/ban/IpBanExemptions'; -import {Config} from '@app/api/Config'; import {emitActivity} from '@app/api/infrastructure/activity/ActivityEvents'; import {workerMeta} from '@app/api/infrastructure/activity/ActivityMeta'; import type {HonoEnv} from '@app/api/types/HonoEnv'; -import {extractClientIp} from '@fluxer/ip_utils/src/ClientIp'; +import {extractConfiguredClientIp} from '@app/api/utils/RequestClientIp'; import {getSameIpDecisionKey, isPublicIpAddress, parseIpAddress} from '@fluxer/ip_utils/src/IpAddress'; import {createMiddleware} from 'hono/factory'; @@ -60,12 +59,7 @@ function countsFor(ip: string | null): IpErrorCounts | null { export function recordRequestStatus(request: Request, status: number): void { if (status < 400 || status >= 500 || recordedRequests.has(request)) return; recordedRequests.add(request); - const counts = countsFor( - extractClientIp(request, { - trustClientIpHeader: Config.proxy.trust_client_ip_header, - clientIpHeaderName: Config.proxy.client_ip_header, - }), - ); + const counts = countsFor(extractConfiguredClientIp(request)); if (!counts) return; if (status === 401) counts.s401++; else if (status === 403) counts.s403++; diff --git a/fluxer_api/src/api/middleware/ServiceMiddleware.ts b/fluxer_api/src/api/middleware/ServiceMiddleware.ts index ad6a5b628..643fcf8cf 100644 --- a/fluxer_api/src/api/middleware/ServiceMiddleware.ts +++ b/fluxer_api/src/api/middleware/ServiceMiddleware.ts @@ -577,7 +577,7 @@ class RequestServices implements RequestScopedServices { get singleCommunityService(): SingleCommunityService { this.cachedSingleCommunityService ??= new SingleCommunityService( getInstanceConfigRepository(), - this.guildService.data, + this.guildService.data.operations, this.guildService.members, ); return this.cachedSingleCommunityService; diff --git a/fluxer_api/src/api/middleware/ServiceSingletons.ts b/fluxer_api/src/api/middleware/ServiceSingletons.ts index d8eec3525..19a80629a 100644 --- a/fluxer_api/src/api/middleware/ServiceSingletons.ts +++ b/fluxer_api/src/api/middleware/ServiceSingletons.ts @@ -96,7 +96,6 @@ import {SweegoWebhookService} from '@app/api/webhook/SweegoWebhookService'; import {WebhookRepository} from '@app/api/webhook/WebhookRepository'; 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'; @@ -186,7 +185,6 @@ function createEmailServiceForConfig( contactEmails: ContactEmails, productName: string, bouncedEmailChecker: UserBouncedEmailChecker, - emailI18n: EmailI18nService, ): IEmailService { const emailConfig: EmailConfig = { enabled: emailConfigSource.enabled, @@ -201,11 +199,10 @@ function createEmailServiceForConfig( supportEmail: contactEmails.supportEmail, productName, }; - return new EmailService(emailConfig, emailI18n, createEmailProvider(emailConfigSource), bouncedEmailChecker); + return new EmailService(emailConfig, createEmailProvider(emailConfigSource), bouncedEmailChecker); } export function createRuntimeEmailService(bouncedEmailChecker: UserBouncedEmailChecker): IEmailService { - const emailI18n = new EmailI18nService(); return new Proxy({} as IEmailService, { get(_target, property) { return async (...args: Array): Promise => { @@ -220,7 +217,6 @@ export function createRuntimeEmailService(bouncedEmailChecker: UserBouncedEmailC resolveContactEmails(), getInstanceProductName(), bouncedEmailChecker, - emailI18n, ); const method = delegate[property as keyof IEmailService]; if (typeof method !== 'function') { diff --git a/fluxer_api/src/api/oauth/ApplicationMessageAuthorAnonymization.ts b/fluxer_api/src/api/oauth/ApplicationMessageAuthorAnonymization.ts index 0128e812b..3266a7d84 100644 --- a/fluxer_api/src/api/oauth/ApplicationMessageAuthorAnonymization.ts +++ b/fluxer_api/src/api/oauth/ApplicationMessageAuthorAnonymization.ts @@ -1,12 +1,12 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {createUserID, type UserID} from '@app/api/BrandedTypes'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import {MessageAnonymizationService} from '@app/api/channel/services/message/MessageAnonymizationService'; import {EMPTY_USER_ROW} from '@app/api/database/types/UserTypes'; import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService'; import {Logger} from '@app/api/Logger'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import { DELETED_USER_DISCRIMINATOR, DELETED_USER_GLOBAL_NAME, @@ -16,13 +16,13 @@ import { interface RemapAuthorMessagesToDeletedUserParams { originalAuthorId: UserID; - channelRepository: IChannelRepository; - userRepository: IUserRepository; + channelRepository: ChannelRepository; + userRepository: UserRepository; snowflakeService: ISnowflakeService; } async function createDeletedMessageAuthorUser(params: { - userRepository: IUserRepository; + userRepository: UserRepository; snowflakeService: ISnowflakeService; }): Promise { const deletedUserId = createUserID(await params.snowflakeService.generate()); @@ -44,7 +44,7 @@ export async function remapAuthorMessagesToDeletedUser( params: RemapAuthorMessagesToDeletedUserParams, ): Promise { const {originalAuthorId, channelRepository, userRepository, snowflakeService} = params; - const hasMessages = await channelRepository.listMessagesByAuthor(originalAuthorId, 1); + const hasMessages = await channelRepository.messages.listMessagesByAuthor(originalAuthorId, 1); if (hasMessages.length === 0) { return null; } diff --git a/fluxer_api/src/api/oauth/ApplicationService.ts b/fluxer_api/src/api/oauth/ApplicationService.ts index 97afb262a..5f64e9ec5 100644 --- a/fluxer_api/src/api/oauth/ApplicationService.ts +++ b/fluxer_api/src/api/oauth/ApplicationService.ts @@ -3,7 +3,7 @@ import type {ApiContext} from '@app/api/ApiContext'; import type {ApplicationID, UserID} from '@app/api/BrandedTypes'; import {applicationIdToUserId} from '@app/api/BrandedTypes'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {ApplicationRow} from '@app/api/database/types/OAuth2Types'; import type {UserRow} from '@app/api/database/types/UserTypes'; import {contentModerationService} from '@app/api/infrastructure/ContentModerationService'; @@ -17,7 +17,7 @@ import type {User} from '@app/api/models/User'; import {remapAuthorMessagesToDeletedUser} from '@app/api/oauth/ApplicationMessageAuthorAnonymization'; import type {BotAuthService} from '@app/api/oauth/BotAuthService'; import {generateOAuthTokenSecret} from '@app/api/oauth/OAuthTokenSecret'; -import type {IApplicationRepository} from '@app/api/oauth/repositories/IApplicationRepository'; +import type {ApplicationRepository} from '@app/api/oauth/repositories/ApplicationRepository'; import {enforceFluxerTagChangeRateLimit} from '@app/api/user/FluxerTagChangeRateLimit'; import {hasPartialUserFieldsChanged, mapUserToPrivateResponse} from '@app/api/user/UserMappers'; import {runAllInOrder} from '@app/api/utils/ConcurrencyUtils'; @@ -43,8 +43,8 @@ import type {BotProfileUpdateRequest} from '@fluxer/schema/src/domains/oauth/OAu interface ApplicationServiceDeps { discriminatorService: DiscriminatorService; - channelRepository: IChannelRepository; - applicationRepository: IApplicationRepository; + channelRepository: ChannelRepository; + applicationRepository: ApplicationRepository; botAuthService: BotAuthService; entityAssetService: EntityAssetService; userCacheService: UserCacheService; diff --git a/fluxer_api/src/api/oauth/BotAuthService.ts b/fluxer_api/src/api/oauth/BotAuthService.ts index 631c40f7f..866a9a345 100644 --- a/fluxer_api/src/api/oauth/BotAuthService.ts +++ b/fluxer_api/src/api/oauth/BotAuthService.ts @@ -2,15 +2,15 @@ import type {ApplicationID, UserID} from '@app/api/BrandedTypes'; import {generateOAuthTokenSecret} from '@app/api/oauth/OAuthTokenSecret'; -import type {IApplicationRepository} from '@app/api/oauth/repositories/IApplicationRepository'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {ApplicationRepository} from '@app/api/oauth/repositories/ApplicationRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {canOwnerRunBots} from '@app/api/user/UserHelpers'; import {hashPassword, verifyPassword} from '@app/api/utils/PasswordUtils'; export class BotAuthService { constructor( - private readonly applicationRepository: IApplicationRepository, - private readonly users: Pick, + private readonly applicationRepository: ApplicationRepository, + private readonly users: Pick, ) {} private parseBotToken(token: string): { diff --git a/fluxer_api/src/api/oauth/BotMfaMirrorService.ts b/fluxer_api/src/api/oauth/BotMfaMirrorService.ts index 72958d665..88c1bd001 100644 --- a/fluxer_api/src/api/oauth/BotMfaMirrorService.ts +++ b/fluxer_api/src/api/oauth/BotMfaMirrorService.ts @@ -4,14 +4,14 @@ import type {UserID} from '@app/api/BrandedTypes'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {Application} from '@app/api/models/Application'; import type {User} from '@app/api/models/User'; -import type {IApplicationRepository} from '@app/api/oauth/repositories/IApplicationRepository'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {ApplicationRepository} from '@app/api/oauth/repositories/ApplicationRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {mapUserToPrivateResponse} from '@app/api/user/UserMappers'; export class BotMfaMirrorService { constructor( - private readonly applicationRepository: IApplicationRepository, - private readonly userRepository: IUserRepository, + private readonly applicationRepository: ApplicationRepository, + private readonly userRepository: UserRepository, private readonly gatewayService: IGatewayService, ) {} diff --git a/fluxer_api/src/api/oauth/OAuth2ApplicationsRequestService.ts b/fluxer_api/src/api/oauth/OAuth2ApplicationsRequestService.ts index a1fbf0bf8..07a7710fb 100644 --- a/fluxer_api/src/api/oauth/OAuth2ApplicationsRequestService.ts +++ b/fluxer_api/src/api/oauth/OAuth2ApplicationsRequestService.ts @@ -10,7 +10,7 @@ import type {User} from '@app/api/models/User'; import type {ApplicationService} from '@app/api/oauth/ApplicationService'; import {ApplicationNotOwnedError} from '@app/api/oauth/ApplicationService'; import {mapApplicationToResponse, mapBotProfileToResponse} from '@app/api/oauth/OAuth2Mappers'; -import type {IApplicationRepository} from '@app/api/oauth/repositories/IApplicationRepository'; +import type {ApplicationRepository} from '@app/api/oauth/repositories/ApplicationRepository'; import {MAX_APPLICATIONS_PER_USER} from '@fluxer/constants/src/LimitConstants'; import {AccessDeniedError} from '@fluxer/errors/src/domains/core/AccessDeniedError'; import {BotUserNotFoundError} from '@fluxer/errors/src/domains/oauth/BotUserNotFoundError'; @@ -29,7 +29,7 @@ export class OAuth2ApplicationsRequestService { constructor( private readonly apiContext: ApiContext, private readonly applicationService: ApplicationService, - private readonly applicationRepository: IApplicationRepository, + private readonly applicationRepository: ApplicationRepository, ) {} async listApplications(userId: UserID) { diff --git a/fluxer_api/src/api/oauth/OAuth2RequestService.ts b/fluxer_api/src/api/oauth/OAuth2RequestService.ts index 4e057ef50..a49034323 100644 --- a/fluxer_api/src/api/oauth/OAuth2RequestService.ts +++ b/fluxer_api/src/api/oauth/OAuth2RequestService.ts @@ -21,8 +21,8 @@ import { } from '@app/api/oauth/OAuth2Mappers'; import {filterOAuth2Scopes} from '@app/api/oauth/OAuth2ScopeUtils'; import {ACCESS_TOKEN_TTL_SECONDS, type OAuth2Service} from '@app/api/oauth/OAuth2Service'; -import type {IApplicationRepository} from '@app/api/oauth/repositories/IApplicationRepository'; -import type {IOAuth2TokenRepository} from '@app/api/oauth/repositories/IOAuth2TokenRepository'; +import type {ApplicationRepository} from '@app/api/oauth/repositories/ApplicationRepository'; +import type {OAuth2TokenRepository} from '@app/api/oauth/repositories/OAuth2TokenRepository'; import {parseClientCredentials} from '@app/api/oauth/utils/ParseClientCredentials'; import {isSignInRefused} from '@app/api/user/UserHelpers'; import {mapUserToOAuthResponse, mapUserToPartialResponse} from '@app/api/user/UserMappers'; @@ -69,8 +69,8 @@ export class OAuth2RequestService { constructor( private readonly apiContext: ApiContext, private readonly oauth2Service: OAuth2Service, - private readonly applicationRepository: IApplicationRepository, - private readonly oauth2TokenRepository: IOAuth2TokenRepository, + private readonly applicationRepository: ApplicationRepository, + private readonly oauth2TokenRepository: OAuth2TokenRepository, private readonly botAuthService: BotAuthService, private readonly applicationService: ApplicationService, private readonly guildService: GuildService, diff --git a/fluxer_api/src/api/oauth/OAuth2Service.ts b/fluxer_api/src/api/oauth/OAuth2Service.ts index 98a7a40a4..e178ecf1b 100644 --- a/fluxer_api/src/api/oauth/OAuth2Service.ts +++ b/fluxer_api/src/api/oauth/OAuth2Service.ts @@ -15,8 +15,6 @@ import {filterOAuth2ScopeSet, isOAuth2Scope, sortOAuth2Scopes} from '@app/api/oa import {ACCESS_TOKEN_TTL_SECONDS} from '@app/api/oauth/OAuth2TokenConstants'; import {generateOAuthTokenSecret} from '@app/api/oauth/OAuthTokenSecret'; import {ApplicationRepository} from '@app/api/oauth/repositories/ApplicationRepository'; -import type {IApplicationRepository} from '@app/api/oauth/repositories/IApplicationRepository'; -import type {IOAuth2TokenRepository} from '@app/api/oauth/repositories/IOAuth2TokenRepository'; import {OAuth2TokenRepository} from '@app/api/oauth/repositories/OAuth2TokenRepository'; import {isSignInRefused} from '@app/api/user/UserHelpers'; import {mapUserToOAuthResponse} from '@app/api/user/UserMappers'; @@ -33,15 +31,15 @@ import {MissingClientSecretError} from '@fluxer/errors/src/domains/oauth/Missing import {MissingRedirectUriError} from '@fluxer/errors/src/domains/oauth/MissingRedirectUriError'; interface OAuth2ServiceDeps { - applicationRepository?: IApplicationRepository; - oauth2TokenRepository?: IOAuth2TokenRepository; + applicationRepository?: ApplicationRepository; + oauth2TokenRepository?: OAuth2TokenRepository; } export {ACCESS_TOKEN_TTL_SECONDS}; export class OAuth2Service { - private applications: IApplicationRepository; - private tokens: IOAuth2TokenRepository; + private applications: ApplicationRepository; + private tokens: OAuth2TokenRepository; constructor( private readonly apiContext: ApiContext, diff --git a/fluxer_api/src/api/oauth/repositories/ApplicationRepository.ts b/fluxer_api/src/api/oauth/repositories/ApplicationRepository.ts index 0399e6d08..1efc5d2a4 100644 --- a/fluxer_api/src/api/oauth/repositories/ApplicationRepository.ts +++ b/fluxer_api/src/api/oauth/repositories/ApplicationRepository.ts @@ -9,7 +9,6 @@ import type {ApplicationByOwnerRow, ApplicationRow} from '@app/api/database/type import {APPLICATION_COLUMNS} from '@app/api/database/types/OAuth2Types'; import {getInstanceProductName} from '@app/api/instance/ProductName'; import {Application} from '@app/api/models/Application'; -import type {IApplicationRepository} from '@app/api/oauth/repositories/IApplicationRepository'; import {Applications, ApplicationsByOwner} from '@app/api/Tables'; import {hashPassword} from '@app/api/utils/PasswordUtils'; import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes'; @@ -67,7 +66,7 @@ function buildAdminApplication(secretHash: string | null): Application { return new Application(row); } -export class ApplicationRepository implements IApplicationRepository { +export class ApplicationRepository { async getApplication(applicationId: ApplicationID): Promise { if (applicationId === createApplicationID(ADMIN_OAUTH2_APPLICATION_ID)) { const secretHash = await getAdminSecretHash(); diff --git a/fluxer_api/src/api/oauth/repositories/IApplicationRepository.ts b/fluxer_api/src/api/oauth/repositories/IApplicationRepository.ts deleted file mode 100644 index 4bb1b675d..000000000 --- a/fluxer_api/src/api/oauth/repositories/IApplicationRepository.ts +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {ApplicationID, UserID} from '@app/api/BrandedTypes'; -import type {ApplicationRow} from '@app/api/database/types/OAuth2Types'; -import type {Application} from '@app/api/models/Application'; - -export interface IApplicationRepository { - getApplication(applicationId: ApplicationID): Promise; - listApplicationsByOwner(ownerUserId: UserID): Promise>; - upsertApplication(data: ApplicationRow, oldData?: ApplicationRow | null): Promise; - deleteApplication(applicationId: ApplicationID): Promise; -} diff --git a/fluxer_api/src/api/oauth/repositories/IOAuth2TokenRepository.ts b/fluxer_api/src/api/oauth/repositories/IOAuth2TokenRepository.ts deleted file mode 100644 index c64d6808b..000000000 --- a/fluxer_api/src/api/oauth/repositories/IOAuth2TokenRepository.ts +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {ApplicationID, UserID} from '@app/api/BrandedTypes'; -import type { - OAuth2AccessTokenRow, - OAuth2AuthorizationCodeRow, - OAuth2RefreshTokenRow, -} from '@app/api/database/types/OAuth2Types'; -import type {OAuth2AccessToken} from '@app/api/models/OAuth2AccessToken'; -import type {OAuth2AuthorizationCode} from '@app/api/models/OAuth2AuthorizationCode'; -import type {OAuth2RefreshToken} from '@app/api/models/OAuth2RefreshToken'; - -export interface IOAuth2TokenRepository { - createAuthorizationCode(data: OAuth2AuthorizationCodeRow): Promise; - getAuthorizationCode(code: string): Promise; - deleteAuthorizationCode(code: string): Promise; - consumeAuthorizationCode(code: string, applicationId: ApplicationID): Promise; - createAccessToken(data: OAuth2AccessTokenRow): Promise; - getAccessToken(token: string): Promise; - deleteAccessToken(token: string, applicationId: ApplicationID, userId: UserID | null): Promise; - deleteAllAccessTokensForUser(userId: UserID): Promise; - createRefreshToken(data: OAuth2RefreshTokenRow): Promise; - getRefreshToken(token: string): Promise; - consumeRefreshToken(token: string, applicationId: ApplicationID, userId: UserID): Promise; - deleteAllRefreshTokensForUser(userId: UserID): Promise; - listRefreshTokensForUser(userId: UserID): Promise>; - deleteAllTokensForUserAndApplication(userId: UserID, applicationId: ApplicationID): Promise; -} diff --git a/fluxer_api/src/api/oauth/repositories/OAuth2TokenRepository.ts b/fluxer_api/src/api/oauth/repositories/OAuth2TokenRepository.ts index 5cab8a476..20475cd84 100644 --- a/fluxer_api/src/api/oauth/repositories/OAuth2TokenRepository.ts +++ b/fluxer_api/src/api/oauth/repositories/OAuth2TokenRepository.ts @@ -20,7 +20,6 @@ import {OAuth2AccessToken} from '@app/api/models/OAuth2AccessToken'; import {OAuth2AuthorizationCode} from '@app/api/models/OAuth2AuthorizationCode'; import {OAuth2RefreshToken} from '@app/api/models/OAuth2RefreshToken'; import {ACCESS_TOKEN_TTL_SECONDS, AUTHORIZATION_CODE_TTL_SECONDS} from '@app/api/oauth/OAuth2TokenConstants'; -import type {IOAuth2TokenRepository} from '@app/api/oauth/repositories/IOAuth2TokenRepository'; import { OAuth2AccessTokens, OAuth2AccessTokensByUser, @@ -57,7 +56,7 @@ const SELECT_REFRESH_TOKENS_BY_USER = OAuth2RefreshTokensByUser.selectCql({ where: OAuth2RefreshTokensByUser.where.eq('user_id'), }); -export class OAuth2TokenRepository implements IOAuth2TokenRepository { +export class OAuth2TokenRepository { async createAuthorizationCode(data: OAuth2AuthorizationCodeRow): Promise { await upsertOne(OAuth2AuthorizationCodes.insertWithTtl(data, AUTHORIZATION_CODE_TTL_SECONDS)); return new OAuth2AuthorizationCode(data); diff --git a/fluxer_api/src/api/premium/PremiumController.ts b/fluxer_api/src/api/premium/PremiumController.ts index 962b07e90..38c014d08 100644 --- a/fluxer_api/src/api/premium/PremiumController.ts +++ b/fluxer_api/src/api/premium/PremiumController.ts @@ -34,7 +34,7 @@ export function PremiumController(app: HonoApp) { const userId = ctx.get('user').id; const {country_code} = ctx.req.valid('query'); const geoip = await lookupGeoip(ctx.req.raw); - const state = await ctx.get('stripeService').getPremiumState(userId, geoip.countryCode ?? country_code); + const state = await ctx.get('stripeService').premiumState.getState(userId, geoip.countryCode ?? country_code); return ctx.json(state); }, ); @@ -57,7 +57,7 @@ export function PremiumController(app: HonoApp) { async (ctx) => { const userId = ctx.get('user').id; const {disabled} = ctx.req.valid('json'); - const state = await ctx.get('stripeService').setPremiumPerksDisabled(userId, disabled); + const state = await ctx.get('stripeService').premiumState.setPerksDisabled(userId, disabled); return ctx.json(state); }, ); diff --git a/fluxer_api/src/api/read_state/IReadStateRepository.ts b/fluxer_api/src/api/read_state/IReadStateRepository.ts deleted file mode 100644 index ca76471db..000000000 --- a/fluxer_api/src/api/read_state/IReadStateRepository.ts +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {ChannelID, GuildID, MessageID, UserID} from '@app/api/BrandedTypes'; -import type {ReadState} from '@app/api/models/ReadState'; - -export interface ReadStateUpsert { - readState: ReadState; - previous: ReadState | null; -} - -export interface ReadStateMarker { - flags: number; - guildId: GuildID; -} - -export interface ReadStateMentionUpdate { - userId: UserID; - channelId: ChannelID; - messageId: MessageID; - marker?: ReadStateMarker | null; -} - -export abstract class IReadStateRepository { - abstract listReadStates(userId: UserID): Promise>; - - abstract getReadState(userId: UserID, channelId: ChannelID): Promise; - - abstract upsertReadState( - userId: UserID, - channelId: ChannelID, - messageId: MessageID, - mentionCount?: number, - lastPinTimestamp?: Date, - manual?: boolean, - marker?: ReadStateMarker | null, - ): Promise; - - abstract incrementReadStateMentions( - userId: UserID, - channelId: ChannelID, - messageId: MessageID, - incrementBy?: number, - marker?: ReadStateMarker | null, - ): Promise; - - abstract bulkIncrementMentionCounts(updates: Array): Promise< - Array<{ - userId: UserID; - channelId: ChannelID; - }> - >; - - abstract bulkAckMessages( - userId: UserID, - readStates: Array<{ - channelId: ChannelID; - messageId: MessageID; - marker?: ReadStateMarker | null; - }>, - ): Promise>; - - abstract upsertPinAck( - userId: UserID, - channelId: ChannelID, - lastPinTimestamp: Date, - marker?: ReadStateMarker | null, - ): Promise; -} diff --git a/fluxer_api/src/api/read_state/ReadStateChannelMeta.ts b/fluxer_api/src/api/read_state/ReadStateChannelMeta.ts index db658e80b..2d71a3a25 100644 --- a/fluxer_api/src/api/read_state/ReadStateChannelMeta.ts +++ b/fluxer_api/src/api/read_state/ReadStateChannelMeta.ts @@ -10,7 +10,7 @@ import { viewerActive, viewerFromCtx, } from '@app/api/experiment/ChannelThreadsGate'; -import type {ReadStateMarker} from '@app/api/read_state/IReadStateRepository'; +import type {ReadStateMarker} from '@app/api/read_state/ReadStateRepository'; import {Channels} from '@app/api/Tables'; import type {HonoEnv} from '@app/api/types/HonoEnv'; import {ReadStateFlags} from '@fluxer/constants/src/ThreadConstants'; diff --git a/fluxer_api/src/api/read_state/ReadStateRepository.ts b/fluxer_api/src/api/read_state/ReadStateRepository.ts index 908f88497..d3e2e74d8 100644 --- a/fluxer_api/src/api/read_state/ReadStateRepository.ts +++ b/fluxer_api/src/api/read_state/ReadStateRepository.ts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import type {ChannelID, MessageID, UserID} from '@app/api/BrandedTypes'; +import type {ChannelID, GuildID, MessageID, UserID} from '@app/api/BrandedTypes'; import {channelIdToMessageId, createMessageID} from '@app/api/BrandedTypes'; import { BatchBuilder, @@ -12,15 +12,26 @@ import { import {Db, type DbOp} from '@app/api/database/CassandraTypes'; import type {ReadStateRow} from '@app/api/database/types/ChannelTypes'; import {ReadState} from '@app/api/models/ReadState'; -import type { - IReadStateRepository, - ReadStateMarker, - ReadStateMentionUpdate, - ReadStateUpsert, -} from '@app/api/read_state/IReadStateRepository'; import {ReadStates} from '@app/api/Tables'; import {ReadStateFlags} from '@fluxer/constants/src/ThreadConstants'; +export interface ReadStateUpsert { + readState: ReadState; + previous: ReadState | null; +} + +export interface ReadStateMarker { + flags: number; + guildId: GuildID; +} + +export interface ReadStateMentionUpdate { + userId: UserID; + channelId: ChannelID; + messageId: MessageID; + marker?: ReadStateMarker | null; +} + const FETCH_READ_STATES_CQL = ReadStates.selectCql({ where: ReadStates.where.eq('user_id'), }); @@ -67,7 +78,7 @@ function mentionBaseline(channelId: ChannelID, marker: ReadStateMarker | null | return baseline; } -export class ReadStateRepository implements IReadStateRepository { +export class ReadStateRepository { async listReadStates(userId: UserID): Promise> { const rows = await fetchMany(FETCH_READ_STATES_CQL, {user_id: userId}); return rows.map((row) => new ReadState(row)); diff --git a/fluxer_api/src/api/read_state/ReadStateService.test.ts b/fluxer_api/src/api/read_state/ReadStateService.test.ts index 38cca4e50..cd4ae4a05 100644 --- a/fluxer_api/src/api/read_state/ReadStateService.test.ts +++ b/fluxer_api/src/api/read_state/ReadStateService.test.ts @@ -10,7 +10,7 @@ import { } from '@app/api/BrandedTypes'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import {ReadState} from '@app/api/models/ReadState'; -import type {IReadStateRepository} from '@app/api/read_state/IReadStateRepository'; +import type {ReadStateRepository} from '@app/api/read_state/ReadStateRepository'; import {ReadStateService} from '@app/api/read_state/ReadStateService'; import {BadGatewayError} from '@fluxer/errors/src/domains/core/BadGatewayError'; import {describe, expect, it, vi} from 'vitest'; @@ -38,7 +38,7 @@ describe('ReadStateService gateway side effects after the write', () => { stored.push({channelId, messageId}); return {readState: makeReadState(channelId, messageId), previous: null}; }), - } as unknown as IReadStateRepository; + } as unknown as ReadStateRepository; const gatewayService = { clearPushChannelNotifications: vi.fn().mockRejectedValue(new BadGatewayError()), dispatchPresence: vi.fn().mockResolvedValue(undefined), @@ -64,7 +64,7 @@ describe('ReadStateService gateway side effects after the write', () => { readState: makeReadState(channelId, messageId), previous: null, })), - } as unknown as IReadStateRepository; + } as unknown as ReadStateRepository; const gatewayService = { clearPushChannelNotifications: vi.fn().mockResolvedValue(undefined), dispatchPresence: vi.fn().mockRejectedValue(new BadGatewayError()), @@ -88,7 +88,7 @@ describe('ReadStateService gateway side effects after the write', () => { stored.push(channelId.toString()); return {readState: makeReadState(channelId, messageId, 1), previous: null}; }), - } as unknown as IReadStateRepository; + } as unknown as ReadStateRepository; const gatewayService = { clearPushChannelNotifications: vi.fn().mockResolvedValue(undefined), dispatchPresence: vi.fn().mockRejectedValue(new BadGatewayError()), @@ -111,7 +111,7 @@ describe('ReadStateService gateway side effects after the write', () => { const updated = [makeReadState(CHANNEL_ID, MESSAGE_ID)]; const repository = { bulkAckMessages: vi.fn().mockResolvedValue(updated), - } as unknown as IReadStateRepository; + } as unknown as ReadStateRepository; const gatewayService = { clearPushChannelNotifications: vi.fn().mockRejectedValue(new BadGatewayError()), dispatchPresence: vi.fn().mockResolvedValue(undefined), @@ -138,7 +138,7 @@ describe('ReadStateService implicit acknowledgements', () => { readState: makeReadState(channelId, messageId), previous: params.previous, })), - } as unknown as IReadStateRepository; + } as unknown as ReadStateRepository; const clearPushChannelNotifications = vi.fn().mockResolvedValue(undefined); const gatewayService = { clearPushChannelNotifications, @@ -211,7 +211,7 @@ describe('ReadStateService implicit acknowledgements', () => { readState: makeReadState(channelId, messageId), previous: makeReadState(channelId, createMessageID(30n), 1), })), - } as unknown as IReadStateRepository; + } as unknown as ReadStateRepository; const clearPushChannelNotifications = vi.fn(() => new Promise(() => {})); const gatewayService = {clearPushChannelNotifications} as unknown as IGatewayService; const readState = await new ReadStateService(repository, gatewayService).ackMessage({ diff --git a/fluxer_api/src/api/read_state/ReadStateService.ts b/fluxer_api/src/api/read_state/ReadStateService.ts index c89697db6..4039e497d 100644 --- a/fluxer_api/src/api/read_state/ReadStateService.ts +++ b/fluxer_api/src/api/read_state/ReadStateService.ts @@ -4,12 +4,12 @@ import type {ChannelID, MessageID, UserID} from '@app/api/BrandedTypes'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import {Logger} from '@app/api/Logger'; import type {ReadState} from '@app/api/models/ReadState'; +import {type ReadStateChannelHint, resolveReadStateMarker} from '@app/api/read_state/ReadStateChannelMeta'; import type { - IReadStateRepository, ReadStateMarker, ReadStateMentionUpdate, -} from '@app/api/read_state/IReadStateRepository'; -import {type ReadStateChannelHint, resolveReadStateMarker} from '@app/api/read_state/ReadStateChannelMeta'; + ReadStateRepository, +} from '@app/api/read_state/ReadStateRepository'; import {visibleReadStates} from '@app/api/read_state/ReadStateVisibility'; function threadScope(readState: ReadState | null | undefined): Record { @@ -30,7 +30,7 @@ function hadUnreadThrough(previous: ReadState | null, messageId: MessageID, unre export class ReadStateService { constructor( - private repository: IReadStateRepository, + private repository: ReadStateRepository, private gatewayService: IGatewayService, ) {} diff --git a/fluxer_api/src/api/read_state/ThreadReadStateMarkers.test.ts b/fluxer_api/src/api/read_state/ThreadReadStateMarkers.test.ts index 1ebb2dcf7..e8695c8ce 100644 --- a/fluxer_api/src/api/read_state/ThreadReadStateMarkers.test.ts +++ b/fluxer_api/src/api/read_state/ThreadReadStateMarkers.test.ts @@ -9,12 +9,11 @@ import { syncChannelThreadsConfig, } from '@app/api/experiment/ChannelThreadsGate'; import {ReadState} from '@app/api/models/ReadState'; -import type {ReadStateMarker} from '@app/api/read_state/IReadStateRepository'; import { clearReadStateChannelMetaCacheForTesting, resolveReadStateMarker, } from '@app/api/read_state/ReadStateChannelMeta'; -import {ReadStateRepository} from '@app/api/read_state/ReadStateRepository'; +import {type ReadStateMarker, ReadStateRepository} from '@app/api/read_state/ReadStateRepository'; import {mapReadStateResponse} from '@app/api/read_state/ReadStateResponseMapper'; import {badgeReadStates, visibleReadStates} from '@app/api/read_state/ReadStateVisibility'; import {ReadStates} from '@app/api/Tables'; diff --git a/fluxer_api/src/api/report/ReportDeletion.ts b/fluxer_api/src/api/report/ReportDeletion.ts index c4b104938..686027062 100644 --- a/fluxer_api/src/api/report/ReportDeletion.ts +++ b/fluxer_api/src/api/report/ReportDeletion.ts @@ -4,7 +4,8 @@ import type {ReportID} from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; import {makeAttachmentCdnKey} from '@app/api/channel/services/message/MessageHelpers'; import type {IStorageService} from '@app/api/infrastructure/IStorageService'; -import type {IARSubmission, IReportRepository} from '@app/api/report/IReportRepository'; +import type {IARSubmission} from '@app/api/report/ReportModels'; +import type {ReportRepository} from '@app/api/report/ReportRepository'; import type {IReportSearchService} from '@app/api/search/IReportSearchService'; import {ReportUnderLegalHoldError} from '@fluxer/errors/src/domains/moderation/ReportUnderLegalHoldError'; import {UnknownReportError} from '@fluxer/errors/src/domains/moderation/UnknownReportError'; @@ -13,7 +14,7 @@ import {listReportProfileSnapshotAssets} from '@fluxer/schema/src/domains/report const SCAN_PAGE_SIZE = 500; export interface ReportDeletionDeps { - reportRepository: IReportRepository; + reportRepository: ReportRepository; storageService: IStorageService; reportSearchService: IReportSearchService | null; } @@ -51,7 +52,7 @@ export function referencedObjectKeys(report: IARSubmission): Array { } export async function forEachStoredReport( - reportRepository: IReportRepository, + reportRepository: ReportRepository, visit: (report: IARSubmission) => void, pageSize = SCAN_PAGE_SIZE, ): Promise { @@ -112,7 +113,7 @@ export async function deleteReportWithEvidence( } async function loadDeletableReport( - reportRepository: IReportRepository, + reportRepository: ReportRepository, reportId: ReportID, now: Date, ): Promise { diff --git a/fluxer_api/src/api/report/IReportRepository.ts b/fluxer_api/src/api/report/ReportModels.ts similarity index 64% rename from fluxer_api/src/api/report/IReportRepository.ts rename to fluxer_api/src/api/report/ReportModels.ts index 2faacdd4e..5a22c7736 100644 --- a/fluxer_api/src/api/report/IReportRepository.ts +++ b/fluxer_api/src/api/report/ReportModels.ts @@ -2,14 +2,6 @@ import type {ApplicationID, ChannelID, GuildID, MessageID, ReportID, UserID, WebhookID} from '@app/api/BrandedTypes'; import type {MessageAttachment, MessageEmbed, MessageStickerItem} from '@app/api/database/types/MessageTypes'; -import type { - DSAReportEmailVerificationRow, - DSAReportTicketRow, - GuildReportSubmissionByReporterRow, - IARSubmissionRow, - MessageReportSubmissionByReporterRow, - UserReportSubmissionByReporterRow, -} from '@app/api/database/types/ReportTypes'; import type {ReportFlowStepRecord} from '@app/api/report/flows/ReportFlowRegistry'; import type {ReportProfileSnapshot} from '@fluxer/schema/src/domains/report/ReportProfileSnapshotSchemas'; @@ -118,52 +110,3 @@ export interface IARSubmission { legalHoldUntil: Date | null; legalHoldReason: string | null; } - -export abstract class IReportRepository { - abstract createReport(data: IARSubmissionRow): Promise; - - abstract reserveMessageReportByReporter(data: MessageReportSubmissionByReporterRow): Promise; - - abstract releaseMessageReportByReporter(data: MessageReportSubmissionByReporterRow): Promise; - - abstract reserveUserReportByReporter(data: UserReportSubmissionByReporterRow): Promise; - - abstract releaseUserReportByReporter(data: UserReportSubmissionByReporterRow): Promise; - - abstract reserveGuildReportByReporter(data: GuildReportSubmissionByReporterRow): Promise; - - abstract releaseGuildReportByReporter(data: GuildReportSubmissionByReporterRow): Promise; - - abstract getReport(reportId: ReportID): Promise; - - abstract resolveReport( - reportId: ReportID, - resolvedByAdminId: UserID, - publicComment: string | null, - auditLogReason: string | null, - ): Promise; - - abstract setReportLegalHold( - reportId: ReportID, - legalHoldUntil: Date | null, - legalHoldReason: string | null, - ): Promise; - - abstract clearReporterEmail(reportId: ReportID, reporterId: UserID): Promise; - - abstract deleteReport(reportId: ReportID): Promise; - - abstract listAllReportsPaginated(limit: number, lastReportId?: ReportID): Promise>; - - abstract upsertDsaEmailVerification(row: DSAReportEmailVerificationRow): Promise; - - abstract consumeDsaEmailVerification(emailLower: string, codeHash: string): Promise; - - abstract getDsaEmailVerification(emailLower: string): Promise; - - abstract createDsaTicket(row: DSAReportTicketRow): Promise; - - abstract getDsaTicket(ticket: string): Promise; - - abstract consumeDsaTicket(ticket: string, emailLower: string): Promise; -} diff --git a/fluxer_api/src/api/report/ReportRepository.ts b/fluxer_api/src/api/report/ReportRepository.ts index 340dee54d..bf422a9c2 100644 --- a/fluxer_api/src/api/report/ReportRepository.ts +++ b/fluxer_api/src/api/report/ReportRepository.ts @@ -25,10 +25,9 @@ import { type IARMessageContextRow, type IARSubmission, type IARSubmissionRow, - type IReportRepository, ReportStatus, ReportType, -} from '@app/api/report/IReportRepository'; +} from '@app/api/report/ReportModels'; import { DSAReportEmailVerifications, DSAReportTickets, @@ -63,7 +62,7 @@ function createFetchAllReportsFirstPageQuery(limit: number) { return IARSubmissions.select({limit}); } -export class ReportRepository implements IReportRepository { +export class ReportRepository { async createReport(data: IARSubmissionRow): Promise { const row: IARSubmissionRow = { ...data, diff --git a/fluxer_api/src/api/report/ReportRequestService.ts b/fluxer_api/src/api/report/ReportRequestService.ts index be88a56ce..ba4b372eb 100644 --- a/fluxer_api/src/api/report/ReportRequestService.ts +++ b/fluxer_api/src/api/report/ReportRequestService.ts @@ -10,7 +10,7 @@ import { resolveReportFlowAnswers, resolveReportFlowLocale, } from '@app/api/report/flows/ReportFlowRegistry'; -import {type ReportStatus, reportStatusToString} from '@app/api/report/IReportRepository'; +import {type ReportStatus, reportStatusToString} from '@app/api/report/ReportModels'; import type {ReportFlowRecord, ReportService} from '@app/api/report/ReportService'; import {UnclaimedAccountCannotSubmitReportsError} from '@fluxer/errors/src/domains/moderation/UnclaimedAccountCannotSubmitReportsError'; import type { diff --git a/fluxer_api/src/api/report/ReportService.ts b/fluxer_api/src/api/report/ReportService.ts index 168c856aa..f09aa66ec 100644 --- a/fluxer_api/src/api/report/ReportService.ts +++ b/fluxer_api/src/api/report/ReportService.ts @@ -13,7 +13,7 @@ import { guildIdToRoleId, } from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {AuthenticatedChannel} from '@app/api/channel/services/AuthenticatedChannel'; import {MessageChannelAuthService} from '@app/api/channel/services/message/MessageChannelAuthService'; import * as MessageHelpers from '@app/api/channel/services/message/MessageHelpers'; @@ -33,7 +33,7 @@ import type { UserReportSubmissionByReporterRow, } from '@app/api/database/types/ReportTypes'; import type {ThreadViewer} from '@app/api/experiment/ChannelThreadsGate'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import {emitActivity} from '@app/api/infrastructure/activity/ActivityEvents'; import type {ReportOutcome, ReportTarget, ResolvedBy} from '@app/api/infrastructure/activity/Contract.generated'; import {emitReportResolved} from '@app/api/infrastructure/activity/ModerationEvents'; @@ -42,7 +42,7 @@ import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService'; import type {IStorageService} from '@app/api/infrastructure/IStorageService'; import {usesUniqueUsernames} from '@app/api/instance/AccountIdentityModeCache'; -import type {IInviteRepository} from '@app/api/invite/IInviteRepository'; +import type {InviteRepository} from '@app/api/invite/InviteRepository'; import {Logger} from '@app/api/Logger'; import type {Attachment} from '@app/api/models/Attachment'; import type {Channel} from '@app/api/models/Channel'; @@ -52,19 +52,20 @@ import type {Message} from '@app/api/models/Message'; import type {User} from '@app/api/models/User'; import type {Webhook} from '@app/api/models/Webhook'; import {resolveReportFlowAnswers, resolveReportFlowLocale} from '@app/api/report/flows/ReportFlowRegistry'; -import type { - IARMessageContextRow, - IARSubmission, - IARSubmissionRow, - IReportRepository, -} from '@app/api/report/IReportRepository'; -import {ReportStatus, ReportType} from '@app/api/report/IReportRepository'; +import { + type IARMessageContextRow, + type IARSubmission, + type IARSubmissionRow, + ReportStatus, + ReportType, +} from '@app/api/report/ReportModels'; +import type {ReportRepository} from '@app/api/report/ReportRepository'; import type {IReportSearchService} from '@app/api/search/IReportSearchService'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; import {isUnderEnforcement} from '@app/api/user/ProfileVisibility'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {findPersonByLoginHandle, type ParsedLoginHandle, parseLoginHandle} from '@app/api/user/UniqueUsernames'; import {isAccountClosed} from '@app/api/user/UserHelpers'; -import type {IWebhookRepository} from '@app/api/webhook/IWebhookRepository'; +import type {WebhookRepository} from '@app/api/webhook/WebhookRepository'; import {buildHashedAssetKey} from '@app/api/worker/utils/AssetArchiveHelpers'; import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes'; import {ME} from '@fluxer/constants/src/AppConstants'; @@ -267,18 +268,18 @@ export class ReportService { private readonly messageChannelAuthService: MessageChannelAuthService; constructor( - private reportRepository: IReportRepository, - private channelRepository: IChannelRepository, - private guildRepository: IGuildRepositoryAggregate, - private userRepository: IUserRepository, - private inviteRepository: IInviteRepository, + private reportRepository: ReportRepository, + private channelRepository: ChannelRepository, + private guildRepository: GuildRepository, + private userRepository: UserRepository, + private inviteRepository: InviteRepository, private emailService: IEmailService, private emailDnsValidationService: IEmailDnsValidationService, private snowflakeService: ISnowflakeService, private storageService: IStorageService, private gatewayService: IGatewayService, private rateLimitService: IRateLimitService, - private webhookRepository: IWebhookRepository, + private webhookRepository: WebhookRepository, private reportSearchService: IReportSearchService | null = null, ) { this.messageChannelAuthService = new MessageChannelAuthService( @@ -817,11 +818,11 @@ export class ReportService { classification: DsaReportClassification, ): Promise { const {guildSegment, channelId, messageId} = this.extractChannelAndMessageFromLink(report.message_link); - const channel = await this.channelRepository.findUnique(channelId); + const channel = await this.channelRepository.channelData.findUnique(channelId); if (!channel || guildSegment !== (channel.guildId?.toString() ?? ME)) { throw new UnknownMessageError(); } - const message = await this.channelRepository.getMessage(channelId, messageId); + const message = await this.channelRepository.messages.getMessage(channelId, messageId); if (!message || message.channelId !== channelId) { throw new UnknownMessageError(); } @@ -1006,7 +1007,7 @@ export class ReportService { if (!(await this.canAccessMessage(authChannel, messageId))) { throw new UnknownMessageError(); } - const message = await this.channelRepository.getMessage(channelId, messageId); + const message = await this.channelRepository.messages.getMessage(channelId, messageId); if (!message || message.channelId !== channelId) { throw new UnknownMessageError(); } @@ -1037,7 +1038,7 @@ export class ReportService { if (!reference?.messageId) { return null; } - const source = await this.channelRepository.getMessage(reference.channelId, reference.messageId); + const source = await this.channelRepository.messages.getMessage(reference.channelId, reference.messageId); if (!source || (source.flags & MessageFlags.IS_CROSSPOST) !== 0) { return null; } @@ -1094,11 +1095,11 @@ export class ReportService { } const guildView = guildToContentWarningView(guild); const scope = channel - ? await resolveNsfwScopeChannel(channel, (channelId) => this.channelRepository.findUnique(channelId)) + ? await resolveNsfwScopeChannel(channel, (channelId) => this.channelRepository.channelData.findUnique(channelId)) : null; let parentCategoryView: ContentWarningChannelLike | null = null; if (scope?.parentId) { - const parent = await this.channelRepository.findUnique(scope.parentId); + const parent = await this.channelRepository.channelData.findUnique(scope.parentId); if (parent) { parentCategoryView = channelToContentWarningView(parent); } @@ -1346,13 +1347,18 @@ export class ReportService { ): Promise> { const messagesBefore = scope === 'window' - ? await this.channelRepository.listMessages(channelId, targetMessageId, MESSAGE_CONTEXT_WINDOW) + ? await this.channelRepository.messages.listMessages(channelId, targetMessageId, MESSAGE_CONTEXT_WINDOW) : []; const messagesAfter = scope === 'window' - ? await this.channelRepository.listMessages(channelId, undefined, MESSAGE_CONTEXT_WINDOW, targetMessageId) + ? await this.channelRepository.messages.listMessages( + channelId, + undefined, + MESSAGE_CONTEXT_WINDOW, + targetMessageId, + ) : []; - const targetMessage = await this.channelRepository.getMessage(channelId, targetMessageId); + const targetMessage = await this.channelRepository.messages.getMessage(channelId, targetMessageId); if (!targetMessage) { return []; } diff --git a/fluxer_api/src/api/report/flows/ReportFlowRegistry.ts b/fluxer_api/src/api/report/flows/ReportFlowRegistry.ts index 9d2d96397..cb9b841b6 100644 --- a/fluxer_api/src/api/report/flows/ReportFlowRegistry.ts +++ b/fluxer_api/src/api/report/flows/ReportFlowRegistry.ts @@ -30,7 +30,7 @@ import type { ReportFlowSurface, ReportFlowTargetType, } from '@fluxer/schema/src/domains/report/ReportFlowSchemas'; -import {parseAcceptLanguage} from '@pkgs/locale/src/LocaleService'; +import {parseAcceptLanguage} from '@pkgs/locale/src/resolution/AcceptLanguageNegotiation'; export interface ReportFlowLibrary { flows: Record; diff --git a/fluxer_api/src/api/rpc/RpcService.ts b/fluxer_api/src/api/rpc/RpcService.ts index d50eb95ac..e06c663ce 100644 --- a/fluxer_api/src/api/rpc/RpcService.ts +++ b/fluxer_api/src/api/rpc/RpcService.ts @@ -15,7 +15,7 @@ import { } from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; import {mapChannelToResponse} from '@app/api/channel/ChannelMappers'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import {buildBroadcastMessageData} from '@app/api/channel/services/message/MessageGatewayDispatch'; import {ensurePersonalNotesChannelExists} from '@app/api/channel/services/PersonalNotesChannelRepair'; import {withThreadParentFieldsMany} from '@app/api/channel/services/thread/ThreadParentSettings'; @@ -33,7 +33,7 @@ import { type ThreadViewer, } from '@app/api/experiment/ChannelThreadsGate'; import {mapFavoriteMemeToResponse} from '@app/api/favorite_meme/FavoriteMemeModel'; -import type {IFavoriteMemeRepository} from '@app/api/favorite_meme/IFavoriteMemeRepository'; +import type {FavoriteMemeRepository} from '@app/api/favorite_meme/FavoriteMemeRepository'; import { mapGuildEmojiToResponse, mapGuildMemberToResponse, @@ -41,7 +41,7 @@ import { mapGuildStickerToResponse, mapGuildToGuildResponse, } from '@app/api/guild/GuildModel'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {AvatarService} from '@app/api/infrastructure/AvatarService'; import type {IDiscriminatorService} from '@app/api/infrastructure/DiscriminatorService'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; @@ -50,7 +50,7 @@ import type {IStorageService} from '@app/api/infrastructure/IStorageService'; import type {PremiumStateReconciliationQueueService} from '@app/api/infrastructure/PremiumStateReconciliationQueueService'; import type {UserCacheService} from '@app/api/infrastructure/UserCacheService'; import type {InstanceConfigRepository} from '@app/api/instance/InstanceConfigRepository'; -import type {IInviteRepository} from '@app/api/invite/IInviteRepository'; +import type {InviteRepository} from '@app/api/invite/InviteRepository'; import {Logger} from '@app/api/Logger'; import type {LimitConfigService} from '@app/api/limits/LimitConfigService'; import {resolveLimitSafe} from '@app/api/limits/LimitConfigUtils'; @@ -81,7 +81,7 @@ import { timeRpcStep, timeRpcStepSync, } from '@app/api/rpc/RpcTimings'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {CustomStatusValidator} from '@app/api/user/services/CustomStatusValidator'; import {getCachedUserPartialResponse} from '@app/api/user/UserCacheHelpers'; import { @@ -102,7 +102,7 @@ import {calculateDistance, parseCoordinate} from '@app/api/utils/GeoUtils'; import {lookupGeoip} from '@app/api/utils/IpUtils'; import type {VoiceAccessContext, VoiceAvailabilityService} from '@app/api/voice/VoiceAvailabilityService'; import type {VoiceService} from '@app/api/voice/VoiceService'; -import type {IWebhookRepository} from '@app/api/webhook/IWebhookRepository'; +import type {WebhookRepository} from '@app/api/webhook/WebhookRepository'; import {ensureGuildThreadPermissionsSeeded} from '@app/api/worker/tasks/SeedThreadPermissions'; import type {WorkerTaskName} from '@app/api/worker/WorkerLaneConfig'; import {AUTOMATIC_VOICE_REGION_ID, ChannelTypes, MessageTypes} from '@fluxer/constants/src/ChannelConstants'; @@ -259,18 +259,18 @@ export class RpcService { private readonly sessionStartService: RpcSessionStartService; constructor( - private userRepository: IUserRepository, - private guildRepository: IGuildRepositoryAggregate, - private channelRepository: IChannelRepository, + private userRepository: UserRepository, + private guildRepository: GuildRepository, + private channelRepository: ChannelRepository, private userCacheService: UserCacheService, private readStateService: ReadStateService, private apiContext: ApiContext, private gatewayService: IGatewayService, private discriminatorService: IDiscriminatorService, - private favoriteMemeRepository: IFavoriteMemeRepository, + private favoriteMemeRepository: FavoriteMemeRepository, private botAuthService: BotAuthService, - private inviteRepository: IInviteRepository, - private webhookRepository: IWebhookRepository, + private inviteRepository: InviteRepository, + private webhookRepository: WebhookRepository, private storageService: IStorageService, private avatarService: AvatarService, private rateLimitService: IRateLimitService, @@ -298,7 +298,7 @@ export class RpcService { private async ensurePersonalNotesChannel(user: User): Promise { const personalNotesChannelId = userIdToChannelId(user.id); - const existingChannel = await this.channelRepository.findUnique(personalNotesChannelId); + const existingChannel = await this.channelRepository.channelData.findUnique(personalNotesChannelId); if (existingChannel) { if (existingChannel.type !== ChannelTypes.DM_PERSONAL_NOTES) { Logger.warn( @@ -308,7 +308,7 @@ export class RpcService { } return; } - await ensurePersonalNotesChannelExists({channelRepository: this.channelRepository, userId: user.id}); + await ensurePersonalNotesChannelExists({channelRepository: this.channelRepository.channelData, userId: user.id}); } private async updateGuildMemberCount(guild: Guild, actualMemberCount: number): Promise { @@ -1399,7 +1399,7 @@ export class RpcService { requestCache: RequestCache; }): Promise { const guild = await this.getGuildOrThrow(guildId); - const channels = await this.channelRepository.listGuildChannels(guildId, 'enrolled'); + const channels = await this.channelRepository.channelData.listGuildChannels(guildId, 'enrolled'); const channelIds = new Set(channels.map((channel) => channel.id)); let maintenanceChannelIds: Promise> | null = null; const resolveMaintenanceChannelIds = () => { @@ -1462,7 +1462,7 @@ export class RpcService { await this.getGuildOrThrow(guildId); const active = guildActive(guildId); const [channels, roles, tainted] = await Promise.all([ - this.channelRepository.listGuildChannels(guildId, 'enrolled'), + this.channelRepository.channelData.listGuildChannels(guildId, 'enrolled'), this.guildRepository.listRoles(guildId), isTainted(guildId), ]); @@ -1900,7 +1900,7 @@ export class RpcService { enrolledChannelIds: ReadonlySet, ): Promise> { if (guildActive(guildId) || !(await isTainted(guildId))) return enrolledChannelIds; - const channels = await this.channelRepository.listGuildChannels(guildId, 'maintenance'); + const channels = await this.channelRepository.channelData.listGuildChannels(guildId, 'maintenance'); return new Set(channels.map((channel) => channel.id)); } @@ -2137,8 +2137,8 @@ export class RpcService { }): Promise { const {channelId, messageId, participants, endedTimestamp} = params; const [message, channel] = await Promise.all([ - this.channelRepository.getMessage(channelId, createMessageID(messageId)), - this.channelRepository.findUnique(channelId), + this.channelRepository.messages.getMessage(channelId, createMessageID(messageId)), + this.channelRepository.channelData.findUnique(channelId), ]); if (!message || !channel) { return; @@ -2147,7 +2147,7 @@ export class RpcService { return; } const messageRow = message.toRow(); - const updatedMessage = await this.channelRepository.upsertMessage({ + const updatedMessage = await this.channelRepository.messages.upsertMessage({ ...messageRow, call: { participant_ids: new Set(participants), @@ -2203,7 +2203,7 @@ export class RpcService { requestCache: RequestCache; }): Promise { const {channelId, userId, requestCache} = params; - const channel = await this.channelRepository.findUnique(channelId); + const channel = await this.channelRepository.channelData.findUnique(channelId); if (!channel) { return null; } diff --git a/fluxer_api/src/api/rpc/RpcSessionStartService.ts b/fluxer_api/src/api/rpc/RpcSessionStartService.ts index 6dc2906d5..b43b07a15 100644 --- a/fluxer_api/src/api/rpc/RpcSessionStartService.ts +++ b/fluxer_api/src/api/rpc/RpcSessionStartService.ts @@ -2,7 +2,7 @@ import type {UserRow} from '@app/api/database/types/UserTypes'; import {mapGuildMemberToResponse} from '@app/api/guild/GuildModel'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import {emitActivity} from '@app/api/infrastructure/activity/ActivityEvents'; import type {IDiscriminatorService} from '@app/api/infrastructure/DiscriminatorService'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; @@ -19,16 +19,16 @@ import { timeRpcStepSync, } from '@app/api/rpc/RpcTimings'; import type {UserData} from '@app/api/rpc/RpcTypes'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {createPremiumClearPatch, shouldStripExpiredPremium} from '@app/api/user/UserHelpers'; import {mapUserToPrivateResponse} from '@app/api/user/UserMappers'; import {TagStyles} from '@fluxer/constants/src/AccountIdentityConstants'; import {PremiumFlags, UserFlags} from '@fluxer/constants/src/UserConstants'; import type {RpcSessionTimings} from '@fluxer/schema/src/domains/rpc/RpcSchemas'; -interface SessionStartUserRepository extends Pick {} +interface SessionStartUserRepository extends Pick {} -interface SessionStartGuildRepository extends Pick {} +interface SessionStartGuildRepository extends Pick {} interface SessionStartUserCacheService extends Pick {} diff --git a/fluxer_api/src/api/rpc/tests/RpcGuildThreadSeedTrigger.test.ts b/fluxer_api/src/api/rpc/tests/RpcGuildThreadSeedTrigger.test.ts index f6b3aa56e..c7ce1bef6 100644 --- a/fluxer_api/src/api/rpc/tests/RpcGuildThreadSeedTrigger.test.ts +++ b/fluxer_api/src/api/rpc/tests/RpcGuildThreadSeedTrigger.test.ts @@ -84,9 +84,14 @@ describe('RpcService guild load thread permission seeding', () => { const guildId = createGuildID(BigInt(guild.id)); await insertGuildThreadMarker(guildId, null); const channelRepository = getChannelRepository(); - const source = (await channelRepository.findUnique(createChannelID(BigInt(guild.system_channel_id!))))!; + const source = (await channelRepository.channelData.findUnique(createChannelID(BigInt(guild.system_channel_id!))))!; const forumId = createChannelID(BigInt(source.id) + 1n); - await channelRepository.upsert({...source.toRow(), channel_id: forumId, type: ChannelTypes.GUILD_FORUM, name: 'f'}); + await channelRepository.channelData.upsert({ + ...source.toRow(), + channel_id: forumId, + type: ChannelTypes.GUILD_FORUM, + name: 'f', + }); const guildRepository = new GuildRepository(); const stored = (await guildRepository.findUnique(guildId))!; const missingId = createChannelID(BigInt(source.id) + 2n); diff --git a/fluxer_api/src/api/search/GlobalSearchService.ts b/fluxer_api/src/api/search/GlobalSearchService.ts index 0e93d3265..b62d959c1 100644 --- a/fluxer_api/src/api/search/GlobalSearchService.ts +++ b/fluxer_api/src/api/search/GlobalSearchService.ts @@ -2,7 +2,7 @@ import type {ChannelID, UserID} from '@app/api/BrandedTypes'; import {createChannelID} from '@app/api/BrandedTypes'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import { type DmSearchScope, getDmChannelIdsForScope, @@ -20,7 +20,7 @@ import type {IMessageSearchService} from '@app/api/search/IMessageSearchService' import {MessageSearchResponseMapper} from '@app/api/search/MessageSearchResponseMapper'; import {searchExistingMessages} from '@app/api/search/MessageSearchResultReconciler'; import {channelRequiresAgeVerification} from '@app/api/search/SearchNsfwUtils'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {canUserAccessNsfwContent} from '@app/api/utils/AgeUtils'; import {mapWithConcurrency} from '@app/api/utils/ConcurrencyUtils'; import type {WorkerTaskName} from '@app/api/worker/WorkerLaneConfig'; @@ -40,9 +40,9 @@ export class GlobalSearchService { private readonly responseMapper: MessageSearchResponseMapper; constructor( - private readonly channelRepository: IChannelRepository, + private readonly channelRepository: ChannelRepository, private readonly guildService: GuildService, - private readonly userRepository: IUserRepository, + private readonly userRepository: UserRepository, private readonly userCacheService: UserCacheService, private readonly workerService: IWorkerService, ) { @@ -180,7 +180,7 @@ export class GlobalSearchService { if (!channelId) { return null; } - const channel = await this.channelRepository.findUnique(channelId); + const channel = await this.channelRepository.channelData.findUnique(channelId); if (!channel || !isDmScopeChannelForUser(channel, userId)) { return null; } @@ -190,7 +190,7 @@ export class GlobalSearchService { private async ensureChannelsIndexed(channelIds: Array): Promise { const channelBrandedIds = channelIds.map((id) => createChannelID(BigInt(id))); const channels = await mapWithConcurrency(channelBrandedIds, CHANNEL_INDEX_CHECK_CONCURRENCY, (id) => - this.channelRepository.findUnique(id), + this.channelRepository.channelData.findUnique(id), ); const personalNotesIds = channelBrandedIds.filter((_id, i) => channels[i]?.type === ChannelTypes.DM_PERSONAL_NOTES); const personalNotesData = diff --git a/fluxer_api/src/api/search/IAuditLogSearchService.ts b/fluxer_api/src/api/search/IAuditLogSearchService.ts index c9f13a437..1fa86770d 100644 --- a/fluxer_api/src/api/search/IAuditLogSearchService.ts +++ b/fluxer_api/src/api/search/IAuditLogSearchService.ts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import type {AdminAuditLog} from '@app/api/admin/IAdminRepository'; +import type {AdminAuditLog} from '@app/api/admin/AdminRepository'; import type { ISearchAdapter as SchemaISearchAdapter, SearchResult as SchemaSearchResult, diff --git a/fluxer_api/src/api/search/IReportSearchService.ts b/fluxer_api/src/api/search/IReportSearchService.ts index 0a0af8d9f..bdf802897 100644 --- a/fluxer_api/src/api/search/IReportSearchService.ts +++ b/fluxer_api/src/api/search/IReportSearchService.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {ReportID} from '@app/api/BrandedTypes'; -import type {IARSubmission} from '@app/api/report/IReportRepository'; +import type {IARSubmission} from '@app/api/report/ReportModels'; import type { ISearchAdapter as SchemaISearchAdapter, SearchResult as SchemaSearchResult, diff --git a/fluxer_api/src/api/search/MessageSearchResponseMapper.ts b/fluxer_api/src/api/search/MessageSearchResponseMapper.ts index ef8dc5533..93fc4e414 100644 --- a/fluxer_api/src/api/search/MessageSearchResponseMapper.ts +++ b/fluxer_api/src/api/search/MessageSearchResponseMapper.ts @@ -3,7 +3,7 @@ import type {GuildID, UserID} from '@app/api/BrandedTypes'; import {createChannelID} from '@app/api/BrandedTypes'; import {mapChannelToResponse} from '@app/api/channel/ChannelMappers'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import {createMessageResponseDataService} from '@app/api/channel/services/message/MessageResponseDataService'; import {maskThreadArtifactsByChannel} from '@app/api/channel/services/message/ThreadMessageResponses'; import {mapThreadMemberToResponse, mapThreadToResponse} from '@app/api/channel/services/thread/ThreadMappers'; @@ -29,7 +29,7 @@ const CHANNEL_LOOKUP_CONCURRENCY = 16; export class MessageSearchResponseMapper { constructor( - private readonly channelRepository: IChannelRepository, + private readonly channelRepository: ChannelRepository, private readonly userCacheService: UserCacheService, ) {} @@ -46,7 +46,7 @@ export class MessageSearchResponseMapper { }> { const orderedChannelIds = Array.from(new Set(messages.map((message) => message.channelId.toString()))); const channels = await mapWithConcurrency(orderedChannelIds, CHANNEL_LOOKUP_CONCURRENCY, (channelId) => - this.channelRepository.findUnique(createChannelID(BigInt(channelId))), + this.channelRepository.channelData.findUnique(createChannelID(BigInt(channelId))), ); const channelById = new Map( channels diff --git a/fluxer_api/src/api/search/MessageSearchResultReconciler.ts b/fluxer_api/src/api/search/MessageSearchResultReconciler.ts index 287ad8adb..16ed157ed 100644 --- a/fluxer_api/src/api/search/MessageSearchResultReconciler.ts +++ b/fluxer_api/src/api/search/MessageSearchResultReconciler.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {type ChannelID, createChannelID, createMessageID, type MessageID} from '@app/api/BrandedTypes'; -import type {IMessageRepository} from '@app/api/channel/repositories/IMessageRepository'; +import type {MessageRepository} from '@app/api/channel/repositories/MessageRepository'; import {Logger} from '@app/api/Logger'; import type {Message} from '@app/api/models/Message'; import type {IMessageSearchService} from '@app/api/search/IMessageSearchService'; @@ -17,7 +17,7 @@ const MAX_STALE_DELETE_RATIO = 0.5; const HIT_LOOKUP_CONCURRENCY = 32; interface MessageLookupRepository { - readonly messages: Pick; + readonly messages: Pick; } interface MessageSearchLookupResult extends SearchResult { diff --git a/fluxer_api/src/api/search/SearchService.ts b/fluxer_api/src/api/search/SearchService.ts index 201f38738..c8026c991 100644 --- a/fluxer_api/src/api/search/SearchService.ts +++ b/fluxer_api/src/api/search/SearchService.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {createChannelID, createGuildID, type UserID} from '@app/api/BrandedTypes'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {ChannelService} from '@app/api/channel/services/ChannelService'; import type {ThreadViewer} from '@app/api/experiment/ChannelThreadsGate'; import type {GuildService} from '@app/api/guild/services/GuildService'; @@ -9,7 +9,7 @@ import type {UserCacheService} from '@app/api/infrastructure/UserCacheService'; import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware'; import {GlobalSearchService} from '@app/api/search/GlobalSearchService'; import {normalizeQuotedPhrases} from '@app/api/search/SearchQuotedPhrases'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import type {WorkerTaskName} from '@app/api/worker/WorkerLaneConfig'; import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes'; import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError'; @@ -36,10 +36,10 @@ export class SearchService { private readonly guildService: GuildService; constructor(params: { - channelRepository: IChannelRepository; + channelRepository: ChannelRepository; channelService: ChannelService; guildService: GuildService; - userRepository: IUserRepository; + userRepository: UserRepository; userCacheService: UserCacheService; workerService: IWorkerService; }) { diff --git a/fluxer_api/src/api/search/SearchWarmup.ts b/fluxer_api/src/api/search/SearchWarmup.ts index 2d646858a..34ab32e63 100644 --- a/fluxer_api/src/api/search/SearchWarmup.ts +++ b/fluxer_api/src/api/search/SearchWarmup.ts @@ -1,9 +1,9 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {GuildID, ReportID} from '@app/api/BrandedTypes'; -import type {IGuildDataRepository} from '@app/api/guild/repositories/IGuildDataRepository'; +import type {GuildDataRepository} from '@app/api/guild/repositories/GuildDataRepository'; import type {ILogger} from '@app/api/ILogger'; -import type {IReportRepository} from '@app/api/report/IReportRepository'; +import type {ReportRepository} from '@app/api/report/ReportRepository'; import { getAuditLogSearchService, getGuildSearchService, @@ -13,14 +13,14 @@ import { import type {IGuildSearchService} from '@app/api/search/IGuildSearchService'; import type {IReportSearchService} from '@app/api/search/IReportSearchService'; import type {IUserSearchService} from '@app/api/search/IUserSearchService'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; const BATCH_SIZE = 100; interface SearchWarmupDeps { - userRepository: IUserRepository; - guildRepository: IGuildDataRepository; - reportRepository: IReportRepository; + userRepository: UserRepository; + guildRepository: GuildDataRepository; + reportRepository: ReportRepository; logger: ILogger; } @@ -66,7 +66,7 @@ export async function warmupAdminSearchIndexes(deps: SearchWarmupDeps): Promise< } async function warmupGuilds( - guildRepository: IGuildDataRepository, + guildRepository: GuildDataRepository, guildSearchService: IGuildSearchService, logger: ILogger, ): Promise { @@ -87,7 +87,7 @@ async function warmupGuilds( } async function warmupUsers( - userRepository: IUserRepository, + userRepository: UserRepository, userSearchService: IUserSearchService, logger: ILogger, ): Promise { @@ -106,7 +106,7 @@ async function warmupUsers( } async function warmupReports( - reportRepository: IReportRepository, + reportRepository: ReportRepository, reportSearchService: IReportSearchService, logger: ILogger, ): Promise { diff --git a/fluxer_api/src/api/search/auditlog/AuditLogSearchSerializer.ts b/fluxer_api/src/api/search/auditlog/AuditLogSearchSerializer.ts index 8c2da9177..436aaa1ae 100644 --- a/fluxer_api/src/api/search/auditlog/AuditLogSearchSerializer.ts +++ b/fluxer_api/src/api/search/auditlog/AuditLogSearchSerializer.ts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import type {AdminAuditLog} from '@app/api/admin/IAdminRepository'; +import type {AdminAuditLog} from '@app/api/admin/AdminRepository'; import type {SearchableAuditLog} from '@fluxer/schema/src/contracts/search/SearchDocumentTypes'; import {snowflakeToDate} from '@fluxer/snowflake/src/Snowflake'; diff --git a/fluxer_api/src/api/search/elasticsearch/ElasticsearchAuditLogSearchService.ts b/fluxer_api/src/api/search/elasticsearch/ElasticsearchAuditLogSearchService.ts index df07fe55c..47731a1af 100644 --- a/fluxer_api/src/api/search/elasticsearch/ElasticsearchAuditLogSearchService.ts +++ b/fluxer_api/src/api/search/elasticsearch/ElasticsearchAuditLogSearchService.ts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import type {AdminAuditLog} from '@app/api/admin/IAdminRepository'; +import type {AdminAuditLog} from '@app/api/admin/AdminRepository'; import {convertToSearchableAuditLog} from '@app/api/search/auditlog/AuditLogSearchSerializer'; import type {IAuditLogSearchService} from '@app/api/search/IAuditLogSearchService'; import {SearchAdapterServiceBase} from '@app/api/search/SearchAdapterServiceBase'; diff --git a/fluxer_api/src/api/search/elasticsearch/ElasticsearchGuildSearchService.ts b/fluxer_api/src/api/search/elasticsearch/ElasticsearchGuildSearchService.ts index 22f05fc40..134b52532 100644 --- a/fluxer_api/src/api/search/elasticsearch/ElasticsearchGuildSearchService.ts +++ b/fluxer_api/src/api/search/elasticsearch/ElasticsearchGuildSearchService.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {GuildID} from '@app/api/BrandedTypes'; -import type {IGuildDiscoveryRepository} from '@app/api/guild/repositories/GuildDiscoveryRepository'; +import type {GuildDiscoveryRepository} from '@app/api/guild/repositories/GuildDiscoveryRepository'; import type {Guild} from '@app/api/models/Guild'; import {convertToSearchableGuild, type GuildDiscoveryContext} from '@app/api/search/guild/GuildSearchSerializer'; import {resolveDiscoveryContextForIndexing} from '@app/api/search/guild/LazyDiscoveryMigration'; @@ -18,14 +18,14 @@ import { } from '@pkgs/elasticsearch_search/src/adapters/ElasticsearchGuildAdapter'; interface ElasticsearchGuildSearchServiceOptions extends ElasticsearchGuildAdapterOptions { - discoveryRepository?: IGuildDiscoveryRepository; + discoveryRepository?: GuildDiscoveryRepository; } export class ElasticsearchGuildSearchService extends SearchAdapterServiceBase implements IGuildSearchService { - private readonly discoveryRepository: IGuildDiscoveryRepository | undefined; + private readonly discoveryRepository: GuildDiscoveryRepository | undefined; constructor(options: ElasticsearchGuildSearchServiceOptions) { super(new ElasticsearchGuildAdapter({client: options.client, lock: options.lock})); diff --git a/fluxer_api/src/api/search/elasticsearch/ElasticsearchReportSearchService.ts b/fluxer_api/src/api/search/elasticsearch/ElasticsearchReportSearchService.ts index 141264ee4..d2c7993be 100644 --- a/fluxer_api/src/api/search/elasticsearch/ElasticsearchReportSearchService.ts +++ b/fluxer_api/src/api/search/elasticsearch/ElasticsearchReportSearchService.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {ReportID} from '@app/api/BrandedTypes'; -import type {IARSubmission} from '@app/api/report/IReportRepository'; +import type {IARSubmission} from '@app/api/report/ReportModels'; import type {IReportSearchService} from '@app/api/search/IReportSearchService'; import {convertToSearchableReport} from '@app/api/search/report/ReportSearchSerializer'; import {SearchAdapterServiceBase} from '@app/api/search/SearchAdapterServiceBase'; diff --git a/fluxer_api/src/api/search/guild/LazyDiscoveryMigration.ts b/fluxer_api/src/api/search/guild/LazyDiscoveryMigration.ts index 6534bf494..068d6c5ac 100644 --- a/fluxer_api/src/api/search/guild/LazyDiscoveryMigration.ts +++ b/fluxer_api/src/api/search/guild/LazyDiscoveryMigration.ts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import type {IGuildDiscoveryRepository} from '@app/api/guild/repositories/GuildDiscoveryRepository'; +import type {GuildDiscoveryRepository} from '@app/api/guild/repositories/GuildDiscoveryRepository'; import type {Guild} from '@app/api/models/Guild'; import type {GuildDiscoveryContext} from '@app/api/search/guild/GuildSearchSerializer'; import {DiscoveryApplicationStatus} from '@fluxer/constants/src/DiscoveryConstants'; @@ -9,7 +9,7 @@ import {GuildFeatures} from '@fluxer/constants/src/GuildConstants'; export async function resolveDiscoveryContextForIndexing( guild: Guild, provided: GuildDiscoveryContext | undefined, - discoveryRepository: IGuildDiscoveryRepository | undefined, + discoveryRepository: GuildDiscoveryRepository | undefined, ): Promise { const isDiscoverable = guild.features.has(GuildFeatures.DISCOVERABLE); if (!isDiscoverable) return provided; diff --git a/fluxer_api/src/api/search/meilisearch/MeilisearchSearchServices.ts b/fluxer_api/src/api/search/meilisearch/MeilisearchSearchServices.ts index 2f3805f70..89b6729b1 100644 --- a/fluxer_api/src/api/search/meilisearch/MeilisearchSearchServices.ts +++ b/fluxer_api/src/api/search/meilisearch/MeilisearchSearchServices.ts @@ -1,13 +1,13 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import type {AdminAuditLog} from '@app/api/admin/IAdminRepository'; +import type {AdminAuditLog} from '@app/api/admin/AdminRepository'; import type {ChannelID, GuildID, MessageID, ReportID, UserID} from '@app/api/BrandedTypes'; -import type {IGuildDiscoveryRepository} from '@app/api/guild/repositories/GuildDiscoveryRepository'; +import type {GuildDiscoveryRepository} from '@app/api/guild/repositories/GuildDiscoveryRepository'; import type {Guild} from '@app/api/models/Guild'; import type {GuildMember} from '@app/api/models/GuildMember'; import type {Message} from '@app/api/models/Message'; import type {User} from '@app/api/models/User'; -import type {IARSubmission} from '@app/api/report/IReportRepository'; +import type {IARSubmission} from '@app/api/report/ReportModels'; import {convertToSearchableAuditLog} from '@app/api/search/auditlog/AuditLogSearchSerializer'; import {convertToSearchableGuild, type GuildDiscoveryContext} from '@app/api/search/guild/GuildSearchSerializer'; import {resolveDiscoveryContextForIndexing} from '@app/api/search/guild/LazyDiscoveryMigration'; @@ -128,9 +128,9 @@ export class MeilisearchGuildSearchService extends SearchAdapterServiceBase implements IGuildSearchService { - private readonly discoveryRepository: IGuildDiscoveryRepository | undefined; + private readonly discoveryRepository: GuildDiscoveryRepository | undefined; - constructor(client: MeilisearchClient, discoveryRepository?: IGuildDiscoveryRepository) { + constructor(client: MeilisearchClient, discoveryRepository?: GuildDiscoveryRepository) { super(new MeilisearchGuildAdapter({client})); this.discoveryRepository = discoveryRepository; } diff --git a/fluxer_api/src/api/search/report/ReportSearchSerializer.ts b/fluxer_api/src/api/search/report/ReportSearchSerializer.ts index 4648419f5..8b8bd12d5 100644 --- a/fluxer_api/src/api/search/report/ReportSearchSerializer.ts +++ b/fluxer_api/src/api/search/report/ReportSearchSerializer.ts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import type {IARSubmission} from '@app/api/report/IReportRepository'; +import type {IARSubmission} from '@app/api/report/ReportModels'; import type {SearchableReport} from '@fluxer/schema/src/contracts/search/SearchDocumentTypes'; import {snowflakeToDate} from '@fluxer/snowflake/src/Snowflake'; diff --git a/fluxer_api/src/api/search/tests/ThreadMessageSearch.test.ts b/fluxer_api/src/api/search/tests/ThreadMessageSearch.test.ts index c19899241..ed9e6735e 100644 --- a/fluxer_api/src/api/search/tests/ThreadMessageSearch.test.ts +++ b/fluxer_api/src/api/search/tests/ThreadMessageSearch.test.ts @@ -180,7 +180,7 @@ describe('thread message search', () => { }); it('never hands an indexed thread created notice to a control search', async () => { const s = await setup(); - const [notice] = (await new ChannelRepository().listMessages(createChannelID(BigInt(s.channelId)))).filter( + const [notice] = (await new ChannelRepository().messages.listMessages(createChannelID(BigInt(s.channelId)))).filter( (message) => message.type === MessageTypes.THREAD_CREATED, ); if (!notice) expect.fail('Expected a thread created notice'); diff --git a/fluxer_api/src/api/search/thread/ThreadSearchService.ts b/fluxer_api/src/api/search/thread/ThreadSearchService.ts index d84d0b145..49400f135 100644 --- a/fluxer_api/src/api/search/thread/ThreadSearchService.ts +++ b/fluxer_api/src/api/search/thread/ThreadSearchService.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {type ChannelID, createChannelID, type GuildID} from '@app/api/BrandedTypes'; -import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {ThreadView} from '@app/api/channel/services/thread/ThreadMappers'; import {loadThreadViews} from '@app/api/channel/services/thread/ThreadViews'; import {guildActive} from '@app/api/experiment/ChannelThreadsGate'; @@ -51,10 +51,7 @@ async function readyService(): Promise { return service; } -export async function syncThreadSearchDocument( - repository: IChannelRepositoryAggregate, - threadId: ChannelID, -): Promise { +export async function syncThreadSearchDocument(repository: ChannelRepository, threadId: ChannelID): Promise { const service = await readyService(); if (!service) return; const state = await repository.threads.getState(threadId); @@ -78,7 +75,7 @@ export async function deleteThreadSearchDocuments(threadIds: ReadonlyArray { +export async function backfillThreadSearch(repository: ChannelRepository, guildId: GuildID): Promise { const service = await readyService(); if (!service || !guildActive(guildId)) return 0; const threadIds = await repository.threads.listGuildThreadIds(guildId); diff --git a/fluxer_api/src/api/store_billing/StoreEntitlementService.ts b/fluxer_api/src/api/store_billing/StoreEntitlementService.ts index aac61fece..e05d824d9 100644 --- a/fluxer_api/src/api/store_billing/StoreEntitlementService.ts +++ b/fluxer_api/src/api/store_billing/StoreEntitlementService.ts @@ -60,7 +60,7 @@ import { scheduleStorePurchaseRefresh, } from '@app/api/store_billing/StorePurchaseRefresh'; import type {StripeGiftReversalHandler} from '@app/api/stripe/services/StripeGiftReversalHandler'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {UserFlags, UserPremiumTypes} from '@fluxer/constants/src/UserConstants'; import {PremiumPurchaseBlockedError} from '@fluxer/errors/src/domains/payment/PremiumPurchaseBlockedError'; import {StoreBillingUnavailableError} from '@fluxer/errors/src/domains/payment/StoreBillingUnavailableError'; @@ -79,7 +79,7 @@ const FAMILY_SHARED_OWNERSHIP = 'FAMILY_SHARED'; interface StoreEntitlementServiceDeps { repository: StoreBillingRepository; - userRepository: IUserRepository; + userRepository: UserRepository; userCacheService: UserCacheService; gatewayService: IGatewayService; kvClient: IKVProvider; diff --git a/fluxer_api/src/api/store_billing/StoreEntitlementServiceFactory.ts b/fluxer_api/src/api/store_billing/StoreEntitlementServiceFactory.ts index 2c1452cc7..e26464819 100644 --- a/fluxer_api/src/api/store_billing/StoreEntitlementServiceFactory.ts +++ b/fluxer_api/src/api/store_billing/StoreEntitlementServiceFactory.ts @@ -11,11 +11,11 @@ import { } from '@app/api/middleware/ServiceSingletons'; import {StoreEntitlementService} from '@app/api/store_billing/StoreEntitlementService'; import {StripeGiftReversalHandler} from '@app/api/stripe/services/StripeGiftReversalHandler'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import type {IKVProvider} from '@pkgs/kv_client/src/IKVProvider'; interface StoreEntitlementServiceFactoryParams { - userRepository: IUserRepository; + userRepository: UserRepository; userCacheService: UserCacheService; gatewayService: IGatewayService; kvClient: IKVProvider; diff --git a/fluxer_api/src/api/store_billing/StoreEntitlementWriter.ts b/fluxer_api/src/api/store_billing/StoreEntitlementWriter.ts index e95202921..428244c98 100644 --- a/fluxer_api/src/api/store_billing/StoreEntitlementWriter.ts +++ b/fluxer_api/src/api/store_billing/StoreEntitlementWriter.ts @@ -11,7 +11,7 @@ import type {User} from '@app/api/models/User'; import {selectActiveStoreSubscription} from '@app/api/store_billing/StoreBillingMappers'; import type {StoreBillingRepository} from '@app/api/store_billing/StoreBillingRepository'; import {shiftGiftExtensionEnd} from '@app/api/user/GiftExtensionShift'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {clearPerksSanitizedFlag} from '@app/api/user/UserHelpers'; import {mapUserToPrivateResponse} from '@app/api/user/UserMappers'; import {UserFlags, UserPremiumTypes} from '@fluxer/constants/src/UserConstants'; @@ -20,7 +20,7 @@ const USER_WRITE_ATTEMPTS = 3; interface StoreEntitlementWriterDeps { repository: StoreBillingRepository; - userRepository: IUserRepository; + userRepository: UserRepository; userCacheService: UserCacheService; gatewayService: IGatewayService; } diff --git a/fluxer_api/src/api/store_billing/StoreGiftFulfilment.ts b/fluxer_api/src/api/store_billing/StoreGiftFulfilment.ts index 5361920f3..a2b49ecf2 100644 --- a/fluxer_api/src/api/store_billing/StoreGiftFulfilment.ts +++ b/fluxer_api/src/api/store_billing/StoreGiftFulfilment.ts @@ -11,7 +11,7 @@ import type {StoreBillingRepository} from '@app/api/store_billing/StoreBillingRe import {redactStoreKey, type StoreSlot, UNSUPPORTED_QUANTITY_REASON} from '@app/api/store_billing/StoreBillingTypes'; import type {StoreEntitlementWriter} from '@app/api/store_billing/StoreEntitlementWriter'; import type {StripeGiftReversalHandler} from '@app/api/stripe/services/StripeGiftReversalHandler'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import * as RandomUtils from '@app/api/utils/RandomUtils'; const PURCHASE_WRITE_ATTEMPTS = 4; @@ -19,7 +19,7 @@ const GIFT_REFUND_REVERSAL_REASON = 'store_refund'; interface StoreGiftFulfilmentDeps { repository: StoreBillingRepository; - userRepository: IUserRepository; + userRepository: UserRepository; giftReversalHandler: StripeGiftReversalHandler; googlePlaySettlement: GooglePlaySettlement; entitlementWriter: StoreEntitlementWriter; diff --git a/fluxer_api/src/api/store_billing/tests/StoreBillingStripeCoexistence.test.ts b/fluxer_api/src/api/store_billing/tests/StoreBillingStripeCoexistence.test.ts index c3efacf51..02a4a5317 100644 --- a/fluxer_api/src/api/store_billing/tests/StoreBillingStripeCoexistence.test.ts +++ b/fluxer_api/src/api/store_billing/tests/StoreBillingStripeCoexistence.test.ts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import crypto, {randomUUID} from 'node:crypto'; +import {randomUUID} from 'node:crypto'; import {createTestAccount, type TestAccount} from '@app/api/auth/tests/AuthTestUtils'; import {createUserID, type UserID} from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; @@ -17,7 +17,7 @@ import { seedStorePurchase, } from '@app/api/store_billing/tests/StoreBillingTestUtils'; import {StripeGiftReversalHandler} from '@app/api/stripe/services/StripeGiftReversalHandler'; -import {setupSyncStripeWebhookWorker} from '@app/api/stripe/tests/StripeWebhookTestUtils'; +import {sendStripeWebhook, setupSyncStripeWebhookWorker} from '@app/api/stripe/tests/StripeWebhookTestUtils'; import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness'; import { buildFakeSubscriptionPurchase, @@ -25,11 +25,9 @@ import { type FakeGooglePlayDeveloperApi, } from '@app/api/test/msw/handlers/GooglePlayDeveloperApiHandlers'; import { - createMockWebhookPayload, createStripeApiHandlers, createSubscriptionDeletedEvent, type StripeApiHandlers, - type StripeWebhookEventData, } from '@app/api/test/msw/handlers/StripeApiHandlers'; import {server} from '@app/api/test/msw/server'; import {HTTP_STATUS} from '@app/api/test/TestConstants'; @@ -96,20 +94,6 @@ describe('Store purchases alongside Stripe billing', () => { server.resetHandlers(); }); - async function sendStripeWebhook(eventData: StripeWebhookEventData): Promise { - const {payload, timestamp} = createMockWebhookPayload(eventData); - const signature = crypto - .createHmac('sha256', Config.stripe.webhookSecret!) - .update(`${timestamp}.${payload}`) - .digest('hex'); - await createBuilder(harness, '') - .post('/stripe/webhook') - .header('stripe-signature', `t=${timestamp},v1=${signature}`) - .header('content-type', 'application/json') - .body(payload) - .execute(); - } - async function createStoreSubscriber(): Promise<{account: TestAccount; userId: UserID; expiresAt: Date}> { const account = await createTestAccount(harness); const userId = createUserID(BigInt(account.userId)); @@ -129,6 +113,7 @@ describe('Store purchases alongside Stripe billing', () => { ); await sendStripeWebhook( + harness, createSubscriptionDeletedEvent({ subscriptionId: 'sub_store_coexist_old', customerId: 'cus_store_coexist', diff --git a/fluxer_api/src/api/stripe/StripeController.ts b/fluxer_api/src/api/stripe/StripeController.ts index 688007980..201de2f5c 100644 --- a/fluxer_api/src/api/stripe/StripeController.ts +++ b/fluxer_api/src/api/stripe/StripeController.ts @@ -187,7 +187,7 @@ export function StripeController(app: HonoApp) { Validator('param', GiftCodeParam), async (ctx) => { const {code} = ctx.req.valid('param'); - const giftCode = await ctx.get('stripeService').getGiftCode(code); + const giftCode = await ctx.get('stripeService').gifts.getGiftCode(code); const response = await mapGiftCodeToResponse({ giftCode, userCacheService: ctx.get('userCacheService'), @@ -217,7 +217,7 @@ export function StripeController(app: HonoApp) { async (ctx) => { const {code} = ctx.req.valid('param'); const userId = ctx.get('user').id; - await ctx.get('stripeService').redeemGiftCode(userId, code); + await ctx.get('stripeService').gifts.redeemGiftCode(userId, code); return ctx.body(null, 204); }, ); @@ -238,7 +238,7 @@ export function StripeController(app: HonoApp) { }), async (ctx) => { const userId = ctx.get('user').id; - const gifts = await ctx.get('stripeService').getUserGifts(userId); + const gifts = await ctx.get('stripeService').gifts.getUserGifts(userId); const responses = await Promise.all( gifts.map((gift) => mapGiftCodeToMetadataResponse({ @@ -290,7 +290,7 @@ export function StripeController(app: HonoApp) { }), async (ctx) => { const userId = ctx.get('user').id; - const price = await ctx.get('stripeService').getCurrentSubscriptionPrice(userId); + const price = await ctx.get('stripeService').subscriptions.getCurrentSubscriptionPrice(userId); return ctx.json(price); }, ); @@ -334,7 +334,7 @@ export function StripeController(app: HonoApp) { }), async (ctx) => { const userId = ctx.get('user').id; - await ctx.get('stripeService').endPremiumGracePeriod(userId); + await ctx.get('stripeService').premium.endGracePeriod(userId); return ctx.json({success: true as const}); }, ); @@ -382,7 +382,7 @@ export function StripeController(app: HonoApp) { }), async (ctx) => { const userId = ctx.get('user').id; - const eligibility = await ctx.get('stripeService').getSelfServeRefundEligibility(userId); + const eligibility = await ctx.get('stripeService').refunds.getEligibility(userId); return ctx.json(eligibility); }, ); @@ -404,7 +404,7 @@ export function StripeController(app: HonoApp) { }), async (ctx) => { const userId = ctx.get('user').id; - const result = await ctx.get('stripeService').refundLatestPurchase(userId); + const result = await ctx.get('stripeService').refunds.refundLatestPurchase(userId); return ctx.json(result); }, ); @@ -425,7 +425,7 @@ export function StripeController(app: HonoApp) { }), async (ctx) => { const userId = ctx.get('user').id; - await ctx.get('stripeService').cancelSubscriptionAtPeriodEnd(userId); + await ctx.get('stripeService').subscriptions.cancelSubscriptionAtPeriodEnd(userId); return ctx.body(null, 204); }, ); @@ -446,7 +446,7 @@ export function StripeController(app: HonoApp) { }), async (ctx) => { const userId = ctx.get('user').id; - await ctx.get('stripeService').reactivateSubscription(userId); + await ctx.get('stripeService').subscriptions.reactivateSubscription(userId); return ctx.body(null, 204); }, ); @@ -470,7 +470,7 @@ export function StripeController(app: HonoApp) { async (ctx) => { const userId = ctx.get('user').id; const {billing_cycle, effective_at} = ctx.req.valid('json'); - await ctx.get('stripeService').changeSubscriptionBillingCycle(userId, billing_cycle, effective_at); + await ctx.get('stripeService').subscriptions.changeBillingCycle(userId, billing_cycle, effective_at); return ctx.body(null, 204); }, ); @@ -492,7 +492,7 @@ export function StripeController(app: HonoApp) { }), async (ctx) => { const userId = ctx.get('user').id; - const result = await ctx.get('stripeService').switchSubscriptionToCurrentListPrice(userId); + const result = await ctx.get('stripeService').subscriptions.switchToCurrentListPrice(userId); return ctx.json(result); }, ); @@ -514,7 +514,7 @@ export function StripeController(app: HonoApp) { }), async (ctx) => { const userId = ctx.get('user').id; - await ctx.get('stripeService').cancelPendingSubscriptionChange(userId); + await ctx.get('stripeService').subscriptions.cancelPendingSubscriptionChange(userId); return ctx.body(null, 204); }, ); @@ -535,7 +535,7 @@ export function StripeController(app: HonoApp) { }), async (ctx) => { const userId = ctx.get('user').id; - await ctx.get('stripeService').rejoinVisionariesGuild(userId); + await ctx.get('stripeService').premium.rejoinVisionariesGuild(userId); return ctx.body(null, 204); }, ); diff --git a/fluxer_api/src/api/stripe/StripeCustomer.ts b/fluxer_api/src/api/stripe/StripeCustomer.ts index c6e5e848e..3286d05d1 100644 --- a/fluxer_api/src/api/stripe/StripeCustomer.ts +++ b/fluxer_api/src/api/stripe/StripeCustomer.ts @@ -3,7 +3,7 @@ import {Logger} from '@app/api/Logger'; import {getBillingRepository} from '@app/api/middleware/ServiceRegistry'; import type {User} from '@app/api/models/User'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import type {WorkerTaskName} from '@app/api/worker/WorkerLaneConfig'; import {StripeError} from '@fluxer/errors/src/domains/payment/StripeError'; import type {ICacheService} from '@pkgs/cache/src/ICacheService'; @@ -16,7 +16,7 @@ const CUSTOMER_LOCK_TTL_SECONDS = seconds('30 seconds'); interface EnsureStripeCustomerParams { stripe: Stripe; user: User; - userRepository: IUserRepository; + userRepository: UserRepository; cacheService: ICacheService; } diff --git a/fluxer_api/src/api/stripe/StripeService.ts b/fluxer_api/src/api/stripe/StripeService.ts index 604cbceb2..ac7446d1e 100644 --- a/fluxer_api/src/api/stripe/StripeService.ts +++ b/fluxer_api/src/api/stripe/StripeService.ts @@ -3,12 +3,9 @@ import type {UserID} from '@app/api/BrandedTypes'; import type {BillingRepository} from '@app/api/billing/repositories/BillingRepository'; import {Config} from '@app/api/Config'; -import type {GiftCodeDurationType} from '@app/api/database/types/PaymentTypes'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {GuildService} from '@app/api/guild/services/GuildService'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; -import type {GiftCode} from '@app/api/models/GiftCode'; -import type {User} from '@app/api/models/User'; import type {StoreBillingRepository} from '@app/api/store_billing/StoreBillingRepository'; import type {StoreEntitlementService} from '@app/api/store_billing/StoreEntitlementService'; import {getProductRegistry, type ProductRegistry} from '@app/api/stripe/ProductRegistry'; @@ -20,16 +17,9 @@ import {StripeGiftService} from '@app/api/stripe/services/StripeGiftService'; import {StripePremiumService} from '@app/api/stripe/services/StripePremiumService'; import {StripeRefundService} from '@app/api/stripe/services/StripeRefundService'; import {StripeSubscriptionService} from '@app/api/stripe/services/StripeSubscriptionService'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import type {Currency} from '@app/api/utils/CurrencyUtils'; import {PremiumPurchaseBlockedError} from '@fluxer/errors/src/domains/payment/PremiumPurchaseBlockedError'; -import type { - CurrentSubscriptionPriceResponse, - PremiumStateResponse, - SelfServeRefundEligibilityResponse, - SelfServeRefundResponse, - SwitchToListPriceResponse, -} from '@fluxer/schema/src/domains/premium/PremiumSchemas'; import type {ICacheService} from '@pkgs/cache/src/ICacheService'; import type Stripe from 'stripe'; @@ -37,16 +27,16 @@ export class StripeService { private stripe: Stripe | null; private productRegistry: ProductRegistry; private checkoutService: StripeCheckoutService; - private subscriptionService: StripeSubscriptionService; - private giftService: StripeGiftService; - private premiumService: StripePremiumService; - private premiumStateService: PremiumStateService; - private refundService: StripeRefundService; + readonly subscriptions: StripeSubscriptionService; + readonly gifts: StripeGiftService; + readonly premium: StripePremiumService; + readonly premiumState: PremiumStateService; + readonly refunds: StripeRefundService; constructor( - private userRepository: IUserRepository, + private userRepository: UserRepository, private gatewayService: IGatewayService, - private guildRepository: IGuildRepositoryAggregate, + private guildRepository: GuildRepository, private guildService: GuildService, private cacheService: ICacheService, private billingRepository: BillingRepository, @@ -55,13 +45,13 @@ export class StripeService { ) { this.productRegistry = getProductRegistry(); this.stripe = getStripeClient(); - this.premiumService = new StripePremiumService( + this.premium = new StripePremiumService( this.userRepository, this.gatewayService, this.guildRepository, this.guildService, ); - this.premiumStateService = new PremiumStateService( + this.premiumState = new PremiumStateService( this.userRepository, this.gatewayService, this.billingRepository, @@ -76,7 +66,7 @@ export class StripeService { this.cacheService, this.storeEntitlementService, ); - this.subscriptionService = new StripeSubscriptionService( + this.subscriptions = new StripeSubscriptionService( this.stripe, this.userRepository, this.productRegistry, @@ -84,17 +74,17 @@ export class StripeService { this.gatewayService, this.storeEntitlementService, ); - this.giftService = new StripeGiftService( + this.gifts = new StripeGiftService( this.stripe, this.userRepository, this.cacheService, this.gatewayService, this.checkoutService, - this.premiumService, - this.subscriptionService, + this.premium, + this.subscriptions, this.storeEntitlementService, ); - this.refundService = new StripeRefundService(this.stripe, this.userRepository, this.subscriptionService); + this.refunds = new StripeRefundService(this.stripe, this.userRepository, this.subscriptions); } getStripe(): Stripe | null { @@ -132,7 +122,7 @@ export class StripeService { if (!user?.premiumBillingCycle || user.premiumBillingCycle === productInfo.billingCycle) { return null; } - await this.subscriptionService.changeBillingCycle(params.userId, productInfo.billingCycle, 'period_end'); + await this.subscriptions.changeBillingCycle(params.userId, productInfo.billingCycle, 'period_end'); return `${Config.endpoints.webApp}/premium-callback?status=success`; } @@ -154,86 +144,4 @@ export class StripeService { }> { return this.checkoutService.getPriceIds(countryCode); } - - async getCurrentSubscriptionPrice(userId: UserID): Promise { - return this.subscriptionService.getCurrentSubscriptionPrice(userId); - } - - async cancelSubscriptionAtPeriodEnd(userId: UserID): Promise { - return this.subscriptionService.cancelSubscriptionAtPeriodEnd(userId); - } - - async cancelSubscriptionImmediately(userId: UserID, reason?: string): Promise { - return this.subscriptionService.cancelSubscriptionImmediately(userId, reason); - } - - async reactivateSubscription(userId: UserID): Promise { - return this.subscriptionService.reactivateSubscription(userId); - } - - async changeSubscriptionBillingCycle( - userId: UserID, - billingCycle: 'monthly' | 'yearly', - effectiveAt: 'now' | 'period_end' = 'now', - ): Promise { - return this.subscriptionService.changeBillingCycle(userId, billingCycle, effectiveAt); - } - - async switchSubscriptionToCurrentListPrice(userId: UserID): Promise { - return this.subscriptionService.switchToCurrentListPrice(userId); - } - - async cancelPendingSubscriptionChange(userId: UserID): Promise { - return this.subscriptionService.cancelPendingSubscriptionChange(userId); - } - - async extendSubscriptionWithGiftTrialDuration( - user: User, - durationType: GiftCodeDurationType, - durationQuantity: number, - idempotencyKey: string, - ): Promise { - return this.subscriptionService.extendSubscriptionWithGiftTrialDuration( - user, - durationType, - durationQuantity, - idempotencyKey, - ); - } - - async getGiftCode(code: string): Promise { - return this.giftService.getGiftCode(code); - } - - async redeemGiftCode(userId: UserID, code: string): Promise { - return this.giftService.redeemGiftCode(userId, code); - } - - async getUserGifts(userId: UserID): Promise> { - return this.giftService.getUserGifts(userId); - } - - async endPremiumGracePeriod(userId: UserID): Promise { - return this.premiumService.endGracePeriod(userId); - } - - async getPremiumState(userId: UserID, countryCode?: string): Promise { - return this.premiumStateService.getState(userId, countryCode); - } - - async setPremiumPerksDisabled(userId: UserID, disabled: boolean): Promise { - return this.premiumStateService.setPerksDisabled(userId, disabled); - } - - async rejoinVisionariesGuild(userId: UserID): Promise { - return this.premiumService.rejoinVisionariesGuild(userId); - } - - async getSelfServeRefundEligibility(userId: UserID): Promise { - return this.refundService.getEligibility(userId); - } - - async refundLatestPurchase(userId: UserID): Promise { - return this.refundService.refundLatestPurchase(userId); - } } diff --git a/fluxer_api/src/api/stripe/services/AgeVerificationService.ts b/fluxer_api/src/api/stripe/services/AgeVerificationService.ts index 081e36bd0..a5cfa9a40 100644 --- a/fluxer_api/src/api/stripe/services/AgeVerificationService.ts +++ b/fluxer_api/src/api/stripe/services/AgeVerificationService.ts @@ -6,7 +6,7 @@ import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import {Logger} from '@app/api/Logger'; import {getBillingRepository} from '@app/api/middleware/ServiceRegistry'; import {ensureStripeCustomer} from '@app/api/stripe/StripeCustomer'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {mapUserToPrivateResponse} from '@app/api/user/UserMappers'; import {UserFlags} from '@fluxer/constants/src/UserConstants'; import {AgeVerificationAlreadyVerifiedError} from '@fluxer/errors/src/domains/payment/AgeVerificationAlreadyVerifiedError'; @@ -19,7 +19,7 @@ import type Stripe from 'stripe'; export class AgeVerificationService { constructor( private stripe: Stripe | null, - private userRepository: IUserRepository, + private userRepository: UserRepository, private gatewayService: IGatewayService, private cacheService: ICacheService, ) {} diff --git a/fluxer_api/src/api/stripe/services/PremiumStateService.ts b/fluxer_api/src/api/stripe/services/PremiumStateService.ts index 31d504db7..23441bbdf 100644 --- a/fluxer_api/src/api/stripe/services/PremiumStateService.ts +++ b/fluxer_api/src/api/stripe/services/PremiumStateService.ts @@ -29,7 +29,7 @@ import { SELF_SERVE_REFUND_COOLDOWN_DAYS, SELF_SERVE_REFUND_WINDOW_DAYS, } from '@app/api/stripe/services/StripeRefundService'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {checkHasActivePaidPremium} from '@app/api/user/UserHelpers'; import {mapUserToPrivateResponse} from '@app/api/user/UserMappers'; import { @@ -235,7 +235,7 @@ export class PremiumStateService { private readonly productRegistry = getProductRegistry(); constructor( - private readonly userRepository: IUserRepository, + private readonly userRepository: UserRepository, private readonly gatewayService: IGatewayService, private readonly billingRepository: BillingRepository, private readonly stripe: Stripe | null = null, diff --git a/fluxer_api/src/api/stripe/services/RefundAllowance.ts b/fluxer_api/src/api/stripe/services/RefundAllowance.ts index 5025a77a3..f0f49fc31 100644 --- a/fluxer_api/src/api/stripe/services/RefundAllowance.ts +++ b/fluxer_api/src/api/stripe/services/RefundAllowance.ts @@ -3,7 +3,7 @@ import type {BillingRefundRow} from '@app/api/database/types/BillingTypes'; import {getBillingRepository} from '@app/api/middleware/ServiceRegistry'; import type {User} from '@app/api/models/User'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; const REFUND_ALLOWANCE_BLOCK_THRESHOLD = 2; @@ -14,7 +14,7 @@ function isCountedAgainstAllowance(refund: BillingRefundRow): boolean { return (refund.metadata?.get('rejection_reason') ?? null) === null; } -async function listCountedRefundIds(user: User, userRepository: IUserRepository): Promise> { +async function listCountedRefundIds(user: User, userRepository: UserRepository): Promise> { const payments = await userRepository.findPaymentsByUserId(user.id); const paymentIntentIds = [ ...new Set(payments.map((payment) => payment.paymentIntentId).filter((id): id is string => id !== null)), @@ -32,7 +32,7 @@ async function listCountedRefundIds(user: User, userRepository: IUserRepository) export async function shouldBlockFurtherPurchases( user: User, - userRepository: IUserRepository, + userRepository: UserRepository, ): Promise<{blocked: boolean; countedRefundIds: Array}> { const countedRefundIds = await listCountedRefundIds(user, userRepository); return {blocked: countedRefundIds.length >= REFUND_ALLOWANCE_BLOCK_THRESHOLD, countedRefundIds}; diff --git a/fluxer_api/src/api/stripe/services/StripeCheckoutService.ts b/fluxer_api/src/api/stripe/services/StripeCheckoutService.ts index 498bb6166..5e22face4 100644 --- a/fluxer_api/src/api/stripe/services/StripeCheckoutService.ts +++ b/fluxer_api/src/api/stripe/services/StripeCheckoutService.ts @@ -25,7 +25,7 @@ import { } from '@app/api/stripe/StripeSubscriptionPeriod'; import {extractId} from '@app/api/stripe/StripeUtils'; import {shiftGiftExtensionPastPremiumUntil} from '@app/api/user/GiftExtensionShift'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import { type Currency, getCurrencyPreferences, @@ -123,7 +123,7 @@ interface EuWithdrawalWaiverContext { export class StripeCheckoutService { constructor( private stripe: Stripe | null, - private userRepository: IUserRepository, + private userRepository: UserRepository, private productRegistry: ProductRegistry, private cacheService: ICacheService, private storeEntitlementService: StoreEntitlementService | null = null, diff --git a/fluxer_api/src/api/stripe/services/StripeCheckoutWebhookHandler.ts b/fluxer_api/src/api/stripe/services/StripeCheckoutWebhookHandler.ts index 20925e113..3719685a0 100644 --- a/fluxer_api/src/api/stripe/services/StripeCheckoutWebhookHandler.ts +++ b/fluxer_api/src/api/stripe/services/StripeCheckoutWebhookHandler.ts @@ -4,7 +4,7 @@ import {createUserID, type UserID} from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; import type {BillingSubscriptionRow} from '@app/api/database/types/BillingTypes'; import type {UserRow} from '@app/api/database/types/UserTypes'; -import type {IDonationRepository} from '@app/api/donation/IDonationRepository'; +import type {DonationRepository} from '@app/api/donation/DonationRepository'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import {Logger} from '@app/api/Logger'; import {getBillingRepository} from '@app/api/middleware/ServiceRegistry'; @@ -22,7 +22,7 @@ import {extractId} from '@app/api/stripe/StripeUtils'; import {EU_WITHDRAWAL_WAIVER_TEXT_VERSION} from '@app/api/stripe/services/StripeCheckoutService'; import type {StripeGiftService} from '@app/api/stripe/services/StripeGiftService'; import type {StripePremiumService} from '@app/api/stripe/services/StripePremiumService'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {mapUserToPrivateResponse} from '@app/api/user/UserMappers'; import {UserPremiumTypes} from '@fluxer/constants/src/UserConstants'; import {StripeError} from '@fluxer/errors/src/domains/payment/StripeError'; @@ -74,14 +74,14 @@ export class StripeCheckoutWebhookHandler { constructor( private stripe: Stripe | null, - private userRepository: IUserRepository, + private userRepository: UserRepository, private emailService: IEmailService, private gatewayService: IGatewayService, private productRegistry: ProductRegistry, private cacheService: ICacheService, private giftService: StripeGiftService, private premiumService: StripePremiumService, - private donationRepository: IDonationRepository, + private donationRepository: DonationRepository, ) {} async handleCheckoutSessionCompleted(session: Stripe.Checkout.Session): Promise { diff --git a/fluxer_api/src/api/stripe/services/StripeDisputeWebhookHandler.ts b/fluxer_api/src/api/stripe/services/StripeDisputeWebhookHandler.ts index 6edb7f76a..44075aae2 100644 --- a/fluxer_api/src/api/stripe/services/StripeDisputeWebhookHandler.ts +++ b/fluxer_api/src/api/stripe/services/StripeDisputeWebhookHandler.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {UserRow} from '@app/api/database/types/UserTypes'; -import type {IDonationRepository} from '@app/api/donation/IDonationRepository'; +import type {DonationRepository} from '@app/api/donation/DonationRepository'; import type {Donor} from '@app/api/donation/models/Donor'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {KVAccountDeletionQueueService} from '@app/api/infrastructure/KVAccountDeletionQueueService'; @@ -14,7 +14,7 @@ import {extractId} from '@app/api/stripe/StripeUtils'; import {shouldBlockFurtherPurchases} from '@app/api/stripe/services/RefundAllowance'; import type {StripeGiftReversalHandler} from '@app/api/stripe/services/StripeGiftReversalHandler'; import type {StripePaymentFraudService} from '@app/api/stripe/services/StripePaymentFraudService'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {clearPendingDeletion} from '@app/api/user/services/PendingDeletionCoordinator'; import {mapUserToPrivateResponse} from '@app/api/user/UserMappers'; import {DeletionReasons} from '@fluxer/constants/src/Core'; @@ -27,11 +27,11 @@ export const REFUND_ALLOWANCE_CLAIM_PREFIX = 'refund-allowance'; export class StripeDisputeWebhookHandler { constructor( - private userRepository: IUserRepository, + private userRepository: UserRepository, private userCacheService: UserCacheService, private emailService: IEmailService, private gatewayService: IGatewayService, - private donationRepository: IDonationRepository, + private donationRepository: DonationRepository, private kvDeletionQueue: KVAccountDeletionQueueService, private giftReversalHandler: StripeGiftReversalHandler, private paymentFraudService: StripePaymentFraudService, diff --git a/fluxer_api/src/api/stripe/services/StripeGiftReversalHandler.ts b/fluxer_api/src/api/stripe/services/StripeGiftReversalHandler.ts index a06586240..0f51f7218 100644 --- a/fluxer_api/src/api/stripe/services/StripeGiftReversalHandler.ts +++ b/fluxer_api/src/api/stripe/services/StripeGiftReversalHandler.ts @@ -15,7 +15,7 @@ import { getGiftTrialPaidUntil, getGiftTrialSeconds, } from '@app/api/stripe/StripeGiftTrialMetadata'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {clearPerksSanitizedFlag} from '@app/api/user/UserHelpers'; import {mapUserToPrivateResponse} from '@app/api/user/UserMappers'; import {UserPremiumTypes} from '@fluxer/constants/src/UserConstants'; @@ -38,7 +38,7 @@ const MIN_REMAINING_TRIAL_MS = 60_000; export class StripeGiftReversalHandler { constructor( - private userRepository: IUserRepository, + private userRepository: UserRepository, private gatewayService: IGatewayService, private premiumStateReconciliationQueueService: PremiumStateReconciliationQueueService, private storeEntitlementService: StoreEntitlementService | null = null, diff --git a/fluxer_api/src/api/stripe/services/StripeGiftService.ts b/fluxer_api/src/api/stripe/services/StripeGiftService.ts index 9daa22a2c..ce250cf6e 100644 --- a/fluxer_api/src/api/stripe/services/StripeGiftService.ts +++ b/fluxer_api/src/api/stripe/services/StripeGiftService.ts @@ -14,7 +14,7 @@ import type {ProductInfo} from '@app/api/stripe/ProductRegistry'; import type {StripeCheckoutService} from '@app/api/stripe/services/StripeCheckoutService'; import type {StripePremiumService} from '@app/api/stripe/services/StripePremiumService'; import type {StripeSubscriptionService} from '@app/api/stripe/services/StripeSubscriptionService'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {mapUserToPrivateResponse} from '@app/api/user/UserMappers'; import * as RandomUtils from '@app/api/utils/RandomUtils'; import {UserPremiumTypes} from '@fluxer/constants/src/UserConstants'; @@ -32,7 +32,7 @@ import type Stripe from 'stripe'; export class StripeGiftService { constructor( private stripe: Stripe | null, - private userRepository: IUserRepository, + private userRepository: UserRepository, private cacheService: ICacheService, private gatewayService: IGatewayService, private checkoutService: StripeCheckoutService, diff --git a/fluxer_api/src/api/stripe/services/StripePaymentFraudService.ts b/fluxer_api/src/api/stripe/services/StripePaymentFraudService.ts index 7546143a2..de56907ed 100644 --- a/fluxer_api/src/api/stripe/services/StripePaymentFraudService.ts +++ b/fluxer_api/src/api/stripe/services/StripePaymentFraudService.ts @@ -4,7 +4,7 @@ import type {AdminAuditService} from '@app/api/admin/services/AdminAuditService' import type {ISessionTerminator} from '@app/api/auth/ISessionTerminator'; import type {UserID} from '@app/api/BrandedTypes'; import {SYSTEM_USER_ID} from '@app/api/constants/Core'; -import type {IDonationRepository} from '@app/api/donation/IDonationRepository'; +import type {DonationRepository} from '@app/api/donation/DonationRepository'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {KVAccountDeletionQueueService} from '@app/api/infrastructure/KVAccountDeletionQueueService'; import type {UserCacheService} from '@app/api/infrastructure/UserCacheService'; @@ -13,7 +13,7 @@ import {getBillingRepository} from '@app/api/middleware/ServiceRegistry'; import type {User} from '@app/api/models/User'; import type {OAuth2TokenRepository} from '@app/api/oauth/repositories/OAuth2TokenRepository'; import {extractId} from '@app/api/stripe/StripeUtils'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {reschedulePendingDeletion} from '@app/api/user/services/PendingDeletionCoordinator'; import {mapUserToPrivateResponse} from '@app/api/user/UserMappers'; import {DeletionReasons} from '@fluxer/constants/src/Core'; @@ -29,12 +29,12 @@ type RadarValueListItemType = RadarValueListCreateParams['item_type']; interface StripePaymentFraudServiceDeps { stripe: Stripe | null; - userRepository: IUserRepository; + userRepository: UserRepository; userCacheService: UserCacheService; sessionTerminator: ISessionTerminator; emailService: IEmailService; gatewayService: IGatewayService; - donationRepository: IDonationRepository; + donationRepository: DonationRepository; cacheService: ICacheService; auditService: AdminAuditService; kvDeletionQueue: KVAccountDeletionQueueService; diff --git a/fluxer_api/src/api/stripe/services/StripePremiumService.ts b/fluxer_api/src/api/stripe/services/StripePremiumService.ts index eec9bc723..5a8dae2c5 100644 --- a/fluxer_api/src/api/stripe/services/StripePremiumService.ts +++ b/fluxer_api/src/api/stripe/services/StripePremiumService.ts @@ -6,7 +6,7 @@ import {Config} from '@app/api/Config'; import {SYSTEM_USER_ID} from '@app/api/constants/Core'; import type {GiftCodeDurationType} from '@app/api/database/types/PaymentTypes'; import type {UserRow} from '@app/api/database/types/UserTypes'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {GuildService} from '@app/api/guild/services/GuildService'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import {Logger} from '@app/api/Logger'; @@ -18,7 +18,7 @@ import { getSubscriptionPremiumPeriodEnd, } from '@app/api/stripe/StripeSubscriptionPeriod'; import {shiftGiftExtensionPastPremiumUntil} from '@app/api/user/GiftExtensionShift'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {clearPerksSanitizedFlag, createPremiumClearPatch, getEffectivePremiumUntil} from '@app/api/user/UserHelpers'; import {mapUserToPrivateResponse} from '@app/api/user/UserMappers'; import {UserPremiumTypes} from '@fluxer/constants/src/UserConstants'; @@ -28,9 +28,9 @@ import type Stripe from 'stripe'; export class StripePremiumService { constructor( - private userRepository: IUserRepository, + private userRepository: UserRepository, private gatewayService: IGatewayService, - private guildRepository: IGuildRepositoryAggregate, + private guildRepository: GuildRepository, private guildService: GuildService, ) {} diff --git a/fluxer_api/src/api/stripe/services/StripeRefundService.ts b/fluxer_api/src/api/stripe/services/StripeRefundService.ts index 2bb39bb90..7a0cf1df7 100644 --- a/fluxer_api/src/api/stripe/services/StripeRefundService.ts +++ b/fluxer_api/src/api/stripe/services/StripeRefundService.ts @@ -10,7 +10,7 @@ import {extractId} from '@app/api/stripe/StripeUtils'; import {shouldBlockFurtherPurchases} from '@app/api/stripe/services/RefundAllowance'; import {REFUND_ALLOWANCE_CLAIM_PREFIX} from '@app/api/stripe/services/StripeDisputeWebhookHandler'; import type {StripeSubscriptionService} from '@app/api/stripe/services/StripeSubscriptionService'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {PremiumFlags} from '@fluxer/constants/src/UserConstants'; import {FeatureNotAvailableSelfHostedError} from '@fluxer/errors/src/domains/core/FeatureNotAvailableSelfHostedError'; import {StripeError} from '@fluxer/errors/src/domains/payment/StripeError'; @@ -91,7 +91,7 @@ function getInvoicePaymentRef(invoice: Stripe.Invoice): { export class StripeRefundService { constructor( private readonly stripe: Stripe | null, - private readonly userRepository: IUserRepository, + private readonly userRepository: UserRepository, private readonly subscriptionService: StripeSubscriptionService, ) {} diff --git a/fluxer_api/src/api/stripe/services/StripeSubscriptionReconciler.ts b/fluxer_api/src/api/stripe/services/StripeSubscriptionReconciler.ts index 80654bc16..6d9124ed2 100644 --- a/fluxer_api/src/api/stripe/services/StripeSubscriptionReconciler.ts +++ b/fluxer_api/src/api/stripe/services/StripeSubscriptionReconciler.ts @@ -7,7 +7,7 @@ import type {User} from '@app/api/models/User'; import type {ProductInfo, ProductRegistry} from '@app/api/stripe/ProductRegistry'; import {getPrimarySubscriptionItem, getSubscriptionStartDate} from '@app/api/stripe/StripeSubscriptionPeriod'; import {extractId} from '@app/api/stripe/StripeUtils'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {UserPremiumTypes} from '@fluxer/constants/src/UserConstants'; import {StripeError} from '@fluxer/errors/src/domains/payment/StripeError'; import type Stripe from 'stripe'; @@ -21,7 +21,7 @@ interface InvoiceRenewalContext { export class StripeSubscriptionReconciler { constructor( private stripe: Stripe | null, - private userRepository: IUserRepository, + private userRepository: UserRepository, private productRegistry: ProductRegistry, ) {} diff --git a/fluxer_api/src/api/stripe/services/StripeSubscriptionService.ts b/fluxer_api/src/api/stripe/services/StripeSubscriptionService.ts index 02983622f..b1ab627ee 100644 --- a/fluxer_api/src/api/stripe/services/StripeSubscriptionService.ts +++ b/fluxer_api/src/api/stripe/services/StripeSubscriptionService.ts @@ -18,7 +18,7 @@ import { getSubscriptionPremiumPeriodEnd, } from '@app/api/stripe/StripeSubscriptionPeriod'; import {extractId} from '@app/api/stripe/StripeUtils'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {mapUserToPrivateResponse} from '@app/api/user/UserMappers'; import type {Currency} from '@app/api/utils/CurrencyUtils'; import {UserPremiumTypes} from '@fluxer/constants/src/UserConstants'; @@ -111,7 +111,7 @@ function mapSchedulePhaseAddInvoiceItems( export class StripeSubscriptionService { constructor( private stripe: Stripe | null, - private userRepository: IUserRepository, + private userRepository: UserRepository, private productRegistry: { getRecurringSubscriptionPriceId: (billingCycle: RecurringBillingCycle, currency: string) => string | null; getProduct: (priceId: string) => ProductInfo | null; diff --git a/fluxer_api/src/api/stripe/services/StripeSubscriptionWebhookHandler.ts b/fluxer_api/src/api/stripe/services/StripeSubscriptionWebhookHandler.ts index 04a54c84f..266aef12c 100644 --- a/fluxer_api/src/api/stripe/services/StripeSubscriptionWebhookHandler.ts +++ b/fluxer_api/src/api/stripe/services/StripeSubscriptionWebhookHandler.ts @@ -3,7 +3,7 @@ import type {UserID} from '@app/api/BrandedTypes'; import type {BillingRepository} from '@app/api/billing/repositories/BillingRepository'; import type {UserRow} from '@app/api/database/types/UserTypes'; -import type {IDonationRepository} from '@app/api/donation/IDonationRepository'; +import type {DonationRepository} from '@app/api/donation/DonationRepository'; import type {Donor} from '@app/api/donation/models/Donor'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {PremiumStateReconciliationQueueService} from '@app/api/infrastructure/PremiumStateReconciliationQueueService'; @@ -31,8 +31,8 @@ import {extractId} from '@app/api/stripe/StripeUtils'; import type {StripePremiumService} from '@app/api/stripe/services/StripePremiumService'; import type {StripeSubscriptionReconciler} from '@app/api/stripe/services/StripeSubscriptionReconciler'; import {shiftGiftExtensionPastPremiumUntil} from '@app/api/user/GiftExtensionShift'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; import {PaymentRepository} from '@app/api/user/repositories/PaymentRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {getPremiumPaymentRecoveryGraceMs} from '@app/api/user/UserHelpers'; import {mapUserToPrivateResponse} from '@app/api/user/UserMappers'; import {UserPremiumTypes} from '@fluxer/constants/src/UserConstants'; @@ -56,10 +56,10 @@ export class StripeSubscriptionWebhookHandler { private readonly paymentRepository = new PaymentRepository(); constructor( - private userRepository: IUserRepository, + private userRepository: UserRepository, private gatewayService: IGatewayService, private premiumService: StripePremiumService, - private donationRepository: IDonationRepository, + private donationRepository: DonationRepository, private premiumStateReconciliationQueueService: PremiumStateReconciliationQueueService, private reconciler: StripeSubscriptionReconciler, private billingRepository: BillingRepository, diff --git a/fluxer_api/src/api/stripe/services/StripeWebhookService.ts b/fluxer_api/src/api/stripe/services/StripeWebhookService.ts index 9fa3bcc7e..463e111e7 100644 --- a/fluxer_api/src/api/stripe/services/StripeWebhookService.ts +++ b/fluxer_api/src/api/stripe/services/StripeWebhookService.ts @@ -4,7 +4,7 @@ import type {AdminRepository} from '@app/api/admin/AdminRepository'; import {AdminAuditService} from '@app/api/admin/services/AdminAuditService'; import type {ISessionTerminator} from '@app/api/auth/ISessionTerminator'; import type {BillingRepository} from '@app/api/billing/repositories/BillingRepository'; -import type {IDonationRepository} from '@app/api/donation/IDonationRepository'; +import type {DonationRepository} from '@app/api/donation/DonationRepository'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService'; import type {KVAccountDeletionQueueService} from '@app/api/infrastructure/KVAccountDeletionQueueService'; @@ -25,7 +25,7 @@ import type {StripePremiumService} from '@app/api/stripe/services/StripePremiumS import type {StripeRefundService} from '@app/api/stripe/services/StripeRefundService'; import {StripeSubscriptionReconciler} from '@app/api/stripe/services/StripeSubscriptionReconciler'; import {StripeSubscriptionWebhookHandler} from '@app/api/stripe/services/StripeSubscriptionWebhookHandler'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {StripeError} from '@fluxer/errors/src/domains/payment/StripeError'; import {StripeWebhookNotAvailableError} from '@fluxer/errors/src/domains/payment/StripeWebhookNotAvailableError'; import {StripeWebhookSignatureInvalidError} from '@fluxer/errors/src/domains/payment/StripeWebhookSignatureInvalidError'; @@ -46,7 +46,7 @@ export class StripeWebhookService { constructor( private stripe: Stripe | null, - userRepository: IUserRepository, + userRepository: UserRepository, userCacheService: UserCacheService, sessionTerminator: ISessionTerminator, emailService: IEmailService, @@ -55,7 +55,7 @@ export class StripeWebhookService { cacheService: ICacheService, giftService: StripeGiftService, premiumService: StripePremiumService, - private donationRepository: IDonationRepository, + private donationRepository: DonationRepository, kvDeletionQueue: KVAccountDeletionQueueService, premiumStateReconciliationQueueService: PremiumStateReconciliationQueueService, private ageVerificationService: AgeVerificationService | null, diff --git a/fluxer_api/src/api/stripe/tests/OperatorBillingCatalog.test.ts b/fluxer_api/src/api/stripe/tests/OperatorBillingCatalog.test.ts index 3709223dc..c7c6d08af 100644 --- a/fluxer_api/src/api/stripe/tests/OperatorBillingCatalog.test.ts +++ b/fluxer_api/src/api/stripe/tests/OperatorBillingCatalog.test.ts @@ -16,7 +16,7 @@ import { } from '@app/api/stripe/BillingConfigCache'; import {getProductRegistry, ProductRegistry, ProductType} from '@app/api/stripe/ProductRegistry'; import {getStripeClient} from '@app/api/stripe/StripeClient'; -import {setupSyncStripeWebhookWorker} from '@app/api/stripe/tests/StripeWebhookTestUtils'; +import {setupSyncStripeWebhookWorker, signStripeWebhook} from '@app/api/stripe/tests/StripeWebhookTestUtils'; import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness'; import {NoopLogger} from '@app/api/test/mocks/NoopLogger'; import { @@ -133,11 +133,6 @@ function sessionField(session: object | undefined, key: string): unknown { return session ? (session as Record)[key] : undefined; } -function signWebhook(payload: string, timestamp: number, secret: string): string { - const signature = crypto.createHmac('sha256', secret).update(`${timestamp}.${payload}`).digest('hex'); - return `t=${timestamp},v1=${signature}`; -} - describe('operator billing catalog', () => { let harness: ApiTestHarness; let stripeHandlers: StripeApiHandlers; @@ -604,7 +599,7 @@ describe('operator billing catalog', () => { type: 'customer.created', data: {object: {id: `cus_operator_${crypto.randomBytes(4).toString('hex')}`}}, }); - return {body: payload, signature: signWebhook(payload, timestamp, secret)}; + return {body: payload, signature: signStripeWebhook(payload, timestamp, secret)}; } test('accepts a queued event signed with the previous secret in the worker after a rotation', async () => { diff --git a/fluxer_api/src/api/stripe/tests/SelfHostedPremiumRoutes.test.ts b/fluxer_api/src/api/stripe/tests/SelfHostedPremiumRoutes.test.ts index 363fcfca6..655c24f3f 100644 --- a/fluxer_api/src/api/stripe/tests/SelfHostedPremiumRoutes.test.ts +++ b/fluxer_api/src/api/stripe/tests/SelfHostedPremiumRoutes.test.ts @@ -6,6 +6,7 @@ import {getConfig} from '@app/api/Config'; import {getCachedInstancePremiumMode, setCachedInstancePremiumMode} from '@app/api/limits/InstancePremiumModeCache'; import {getInstanceConfigRepository, getLimitConfigService} from '@app/api/middleware/ServiceSingletons'; import {getStoredBillingConfig, setStoredBillingConfig} from '@app/api/stripe/BillingConfigCache'; +import {signStripeWebhook} from '@app/api/stripe/tests/StripeWebhookTestUtils'; import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness'; import {createStripeApiHandlers} from '@app/api/test/msw/handlers/StripeApiHandlers'; import {server} from '@app/api/test/msw/server'; @@ -56,12 +57,6 @@ function restoreGlobalState(state: GlobalState): void { setStoredBillingConfig(state.storedBilling); } -function signWebhook(payload: string, secret: string): string { - const timestamp = Math.floor(Date.now() / 1000); - const signature = crypto.createHmac('sha256', secret).update(`${timestamp}.${payload}`).digest('hex'); - return `t=${timestamp},v1=${signature}`; -} - function webhookPayload(): string { return JSON.stringify({ id: `evt_${crypto.randomBytes(8).toString('hex')}`, @@ -120,7 +115,7 @@ async function postSignedWebhook(harness: ApiTestHarness, secret: string): Promi const payload = webhookPayload(); const {response} = await createBuilderWithoutAuth(harness) .post('/stripe/webhook') - .header('stripe-signature', signWebhook(payload, secret)) + .header('stripe-signature', signStripeWebhook(payload, Math.floor(Date.now() / 1000), secret)) .header('content-type', 'application/json') .body(payload) .executeRaw(); @@ -370,14 +365,14 @@ describe('self-hosted premium routes', () => { const payload = webhookPayload(); const accepted = await createBuilderWithoutAuth(harness) .post('/stripe/webhook') - .header('stripe-signature', signWebhook(payload, OPERATOR_WEBHOOK_SECRET)) + .header('stripe-signature', signStripeWebhook(payload, Math.floor(Date.now() / 1000), OPERATOR_WEBHOOK_SECRET)) .header('content-type', 'application/json') .body(payload) .executeRaw(); expect(accepted.response.status).toBe(HTTP_STATUS.OK); const rejected = await createBuilderWithoutAuth(harness) .post('/stripe/webhook') - .header('stripe-signature', signWebhook(payload, 'whsec_test_fluxer')) + .header('stripe-signature', signStripeWebhook(payload, Math.floor(Date.now() / 1000), 'whsec_test_fluxer')) .header('content-type', 'application/json') .body(payload) .executeRaw(); diff --git a/fluxer_api/src/api/stripe/tests/StripeGiftReversalHandler.test.ts b/fluxer_api/src/api/stripe/tests/StripeGiftReversalHandler.test.ts index 4e67ef56b..292f97014 100644 --- a/fluxer_api/src/api/stripe/tests/StripeGiftReversalHandler.test.ts +++ b/fluxer_api/src/api/stripe/tests/StripeGiftReversalHandler.test.ts @@ -8,7 +8,7 @@ import {getPremiumStateReconciliationQueueService, getUserRepository} from '@app import {findUser} from '@app/api/store_billing/tests/StoreBillingTestUtils'; import {StripeGiftReversalHandler} from '@app/api/stripe/services/StripeGiftReversalHandler'; import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {UserPremiumTypes} from '@fluxer/constants/src/UserConstants'; import {ms} from 'itty-time'; import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest'; @@ -28,7 +28,7 @@ describe('StripeGiftReversalHandler', () => { await harness.resetData(); }); - function createHandler(users: IUserRepository = getUserRepository()): StripeGiftReversalHandler { + function createHandler(users: UserRepository = getUserRepository()): StripeGiftReversalHandler { return new StripeGiftReversalHandler(users, getGatewayService(), getPremiumStateReconciliationQueueService()); } diff --git a/fluxer_api/src/api/stripe/tests/StripeGiftSubscriptionShift.test.ts b/fluxer_api/src/api/stripe/tests/StripeGiftSubscriptionShift.test.ts index f1bc6a738..9c59fda58 100644 --- a/fluxer_api/src/api/stripe/tests/StripeGiftSubscriptionShift.test.ts +++ b/fluxer_api/src/api/stripe/tests/StripeGiftSubscriptionShift.test.ts @@ -5,7 +5,7 @@ import {createTestAccount, type TestAccount} from '@app/api/auth/tests/AuthTestU import {createUserID, type UserID} from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; import type {UserRow} from '@app/api/database/types/UserTypes'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {GuildService} from '@app/api/guild/services/GuildService'; import {getCacheService, getPremiumStateReconciliationQueueService} from '@app/api/middleware/ServiceSingletons'; import {findUser} from '@app/api/store_billing/tests/StoreBillingTestUtils'; @@ -78,7 +78,7 @@ describe('gift time around a Stripe subscription', () => { const premiumService = new StripePremiumService( users, new NoopGatewayService(), - {} as IGuildRepositoryAggregate, + {} as GuildRepository, {} as GuildService, ); diff --git a/fluxer_api/src/api/stripe/tests/StripePremiumService.test.ts b/fluxer_api/src/api/stripe/tests/StripePremiumService.test.ts index f5ecd2d98..a18c0a44b 100644 --- a/fluxer_api/src/api/stripe/tests/StripePremiumService.test.ts +++ b/fluxer_api/src/api/stripe/tests/StripePremiumService.test.ts @@ -3,7 +3,7 @@ import {createTestAccount} from '@app/api/auth/tests/AuthTestUtils'; import {createUserID} from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {GuildService} from '@app/api/guild/services/GuildService'; import {createGuild, createRole, getMember} from '@app/api/guild/tests/GuildTestUtils'; import {StripePremiumService} from '@app/api/stripe/services/StripePremiumService'; @@ -79,7 +79,7 @@ describe('StripePremiumService', () => { const premiumService = new StripePremiumService( new UserRepository(), new NoopGatewayService(), - {} as IGuildRepositoryAggregate, + {} as GuildRepository, {} as GuildService, ); await premiumService.setPremiumFromSubscriptionPeriod( diff --git a/fluxer_api/src/api/stripe/tests/StripeRefundService.test.ts b/fluxer_api/src/api/stripe/tests/StripeRefundService.test.ts index f600dc7f4..6d78cb2e5 100644 --- a/fluxer_api/src/api/stripe/tests/StripeRefundService.test.ts +++ b/fluxer_api/src/api/stripe/tests/StripeRefundService.test.ts @@ -1,16 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import crypto from 'node:crypto'; import {createTestAccount, type TestAccount} from '@app/api/auth/tests/AuthTestUtils'; import {createUserID} from '@app/api/BrandedTypes'; -import {Config} from '@app/api/Config'; -import {setupSyncStripeWebhookWorker} from '@app/api/stripe/tests/StripeWebhookTestUtils'; +import {sendStripeWebhook, setupSyncStripeWebhookWorker} from '@app/api/stripe/tests/StripeWebhookTestUtils'; import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness'; -import { - createMockWebhookPayload, - createStripeApiHandlers, - type StripeWebhookEventData, -} from '@app/api/test/msw/handlers/StripeApiHandlers'; +import {createStripeApiHandlers, type StripeWebhookEventData} from '@app/api/test/msw/handlers/StripeApiHandlers'; import {server} from '@app/api/test/msw/server'; import {createBuilder} from '@app/api/test/TestRequestBuilder'; import {UserRepository} from '@app/api/user/repositories/UserRepository'; @@ -168,25 +162,6 @@ describe('StripeRefundService self-serve refund', () => { harness = await createApiTestHarness(); setupSyncStripeWebhookWorker(); }); - function createWebhookSignature(payload: string, timestamp: number, secret: string): string { - const signedPayload = `${timestamp}.${payload}`; - const signature = crypto.createHmac('sha256', secret).update(signedPayload).digest('hex'); - return `t=${timestamp},v1=${signature}`; - } - async function sendWebhook(eventData: StripeWebhookEventData): Promise<{ - received: boolean; - }> { - const {payload, timestamp} = createMockWebhookPayload(eventData); - const signature = createWebhookSignature(payload, timestamp, Config.stripe.webhookSecret!); - return createBuilder<{ - received: boolean; - }>(harness, '') - .post('/stripe/webhook') - .header('stripe-signature', signature) - .header('content-type', 'application/json') - .body(payload) - .execute(); - } beforeEach(async () => { await harness.reset(); }); @@ -428,8 +403,8 @@ describe('StripeRefundService self-serve refund', () => { }, }, }; - await sendWebhook({...chargeEvent, id: 'evt_cross_path_1'}); - await sendWebhook({...chargeEvent, id: 'evt_cross_path_2'}); + await sendStripeWebhook(harness, {...chargeEvent, id: 'evt_cross_path_1'}); + await sendStripeWebhook(harness, {...chargeEvent, id: 'evt_cross_path_2'}); const afterWebhook = await userRepository.findUnique(userId); expect(afterWebhook!.firstRefundAt!.getTime()).toBe(afterSelfServe!.firstRefundAt!.getTime()); expect(afterWebhook!.premiumFlags & PremiumFlags.PURCHASE_DISABLED).toBe(0); @@ -449,7 +424,7 @@ describe('StripeRefundService self-serve refund', () => { const userRepository = new UserRepository(); const afterSelfServe = await userRepository.findUnique(userId); expect(afterSelfServe!.firstRefundAt).not.toBeNull(); - await sendWebhook({ + await sendStripeWebhook(harness, { id: 'evt_double_confirm_1', type: 'refund.updated', data: { @@ -533,7 +508,8 @@ describe('StripeRefundService self-serve refund', () => { stripe_customer_id: MOCK_CUSTOMER_ID, stripe_subscription_id: 'sub_bought_after_the_refund', }); - await sendWebhook( + await sendStripeWebhook( + harness, buildRefundUpdatedEvent({ eventId: 'evt_stale_teardown', refundId: 're_stale_teardown', @@ -558,7 +534,8 @@ describe('StripeRefundService self-serve refund', () => { stripe_customer_id: MOCK_CUSTOMER_ID, stripe_subscription_id: MOCK_SUBSCRIPTION_ID, }); - await sendWebhook( + await sendStripeWebhook( + harness, buildRefundUpdatedEvent({ eventId: 'evt_current_teardown', refundId: 're_current_teardown', diff --git a/fluxer_api/src/api/stripe/tests/StripeWebhookCheckout.test.ts b/fluxer_api/src/api/stripe/tests/StripeWebhookCheckout.test.ts index bc3993ba6..06c920070 100644 --- a/fluxer_api/src/api/stripe/tests/StripeWebhookCheckout.test.ts +++ b/fluxer_api/src/api/stripe/tests/StripeWebhookCheckout.test.ts @@ -1,23 +1,24 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import crypto from 'node:crypto'; -import {createTestAccount} from '@app/api/auth/tests/AuthTestUtils'; +import {clearTestEmails, createTestAccount, listTestEmails} from '@app/api/auth/tests/AuthTestUtils'; import {createUserID} from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; -import {clearDonationTestEmails, listDonationTestEmails} from '@app/api/donation/tests/DonationTestUtils'; import {ProductType} from '@app/api/stripe/ProductRegistry'; -import {setupSyncStripeWebhookWorker} from '@app/api/stripe/tests/StripeWebhookTestUtils'; +import { + sendStripeWebhook, + sendStripeWebhookExpectStripeError, + sendStripeWebhookRaw, + setupSyncStripeWebhookWorker, +} from '@app/api/stripe/tests/StripeWebhookTestUtils'; import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness'; import { createCheckoutCompletedEvent, - createMockWebhookPayload, createStripeApiHandlers, type StripeApiHandlers, type StripeWebhookEventData, } from '@app/api/test/msw/handlers/StripeApiHandlers'; import {server} from '@app/api/test/msw/server'; import {createBuilder} from '@app/api/test/TestRequestBuilder'; -import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes'; import {UserPremiumTypes} from '@fluxer/constants/src/UserConstants'; import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, test} from 'vitest'; @@ -80,50 +81,6 @@ describe('StripeWebhookService - checkout.session.completed', () => { Config.stripe.webhookSecret = originalWebhookSecret; Config.stripe.prices = originalPrices; }); - function createWebhookSignature(payload: string, timestamp: number, secret: string): string { - const signedPayload = `${timestamp}.${payload}`; - const signature = crypto.createHmac('sha256', secret).update(signedPayload).digest('hex'); - return `t=${timestamp},v1=${signature}`; - } - async function sendWebhook(eventData: StripeWebhookEventData): Promise<{ - received: boolean; - }> { - const {payload, timestamp} = createMockWebhookPayload(eventData); - const signature = createWebhookSignature(payload, timestamp, Config.stripe.webhookSecret!); - return createBuilder<{ - received: boolean; - }>(harness, '') - .post('/stripe/webhook') - .header('stripe-signature', signature) - .header('content-type', 'application/json') - .body(payload) - .execute(); - } - async function sendWebhookRaw(eventData: StripeWebhookEventData): Promise<{ - response: Response; - text: string; - json: unknown; - }> { - const {payload, timestamp} = createMockWebhookPayload(eventData); - const signature = createWebhookSignature(payload, timestamp, Config.stripe.webhookSecret!); - return createBuilder(harness, '') - .post('/stripe/webhook') - .header('stripe-signature', signature) - .header('content-type', 'application/json') - .body(payload) - .executeRaw(); - } - async function sendWebhookExpectStripeError(eventData: StripeWebhookEventData): Promise { - const {payload, timestamp} = createMockWebhookPayload(eventData); - const signature = createWebhookSignature(payload, timestamp, Config.stripe.webhookSecret!); - await createBuilder(harness, '') - .post('/stripe/webhook') - .header('stripe-signature', signature) - .header('content-type', 'application/json') - .body(payload) - .expect(400, APIErrorCodes.STRIPE_ERROR) - .execute(); - } describe('premium checkout', () => { test('processes completed premium checkout session successfully', async () => { const account = await createTestAccount(harness); @@ -147,7 +104,7 @@ describe('StripeWebhookService - checkout.session.completed', () => { subscriptionId: 'sub_test_1', metadata: {}, }); - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); const updatedPayment = await userRepository.getPaymentByCheckoutSession(sessionId); expect(updatedPayment?.status).toBe('completed'); @@ -189,7 +146,7 @@ describe('StripeWebhookService - checkout.session.completed', () => { }); eventData.data.object.payment_intent = null; eventData.data.object.payment_method_types = ['pix']; - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); const updatedPayment = await userRepository.getPaymentByCheckoutSession(sessionId); expect(updatedPayment?.status).toBe('completed'); @@ -226,7 +183,7 @@ describe('StripeWebhookService - checkout.session.completed', () => { customerId: 'cus_new_123', metadata: {}, }); - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); const user = await userRepository.findUnique(createUserID(BigInt(account.userId))); expect(user?.stripeCustomerId).toBe('cus_new_123'); @@ -255,7 +212,10 @@ describe('StripeWebhookService - checkout.session.completed', () => { }), id: 'evt_duplicate_checkout_delivery_1', }; - const concurrentResponses = await Promise.all([sendWebhookRaw(eventData), sendWebhookRaw(eventData)]); + const concurrentResponses = await Promise.all([ + sendStripeWebhookRaw(harness, eventData), + sendStripeWebhookRaw(harness, eventData), + ]); const statuses = concurrentResponses.map((response) => response.response.status).sort(); expect(statuses).toEqual([200, 400]); expect( @@ -277,7 +237,7 @@ describe('StripeWebhookService - checkout.session.completed', () => { ...eventData, id: 'evt_duplicate_checkout_delivery_retry_1', }; - const retryResponse = await sendWebhook(retryEventData); + const retryResponse = await sendStripeWebhook(harness, retryEventData); expect(retryResponse.received).toBe(true); const userAfterRetry = await createBuilder<{ premium_type: number; @@ -316,7 +276,7 @@ describe('StripeWebhookService - checkout.session.completed', () => { sessionId, metadata: {}, }); - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); const afterUser = await createBuilder<{ premium_type: number; @@ -332,7 +292,7 @@ describe('StripeWebhookService - checkout.session.completed', () => { sessionId: 'cs_nonexistent_123', metadata: {}, }); - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); }); test('handles external donate checkout sessions without internal payment records', async () => { @@ -360,7 +320,7 @@ describe('StripeWebhookService - checkout.session.completed', () => { }, }, }; - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); }); test('handles gift purchase correctly', async () => { @@ -383,7 +343,7 @@ describe('StripeWebhookService - checkout.session.completed', () => { sessionId, metadata: {}, }); - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); const updatedPayment = await userRepository.getPaymentByCheckoutSession(sessionId); expect(updatedPayment?.status).toBe('completed'); @@ -401,17 +361,17 @@ describe('StripeWebhookService - checkout.session.completed', () => { const eventData = createCheckoutCompletedEvent({ metadata: {is_donation: 'true'}, }); - await sendWebhookExpectStripeError(eventData); + await sendStripeWebhookExpectStripeError(harness, eventData); }); test('records donation with valid email and subscription', async () => { - await clearDonationTestEmails(harness); + await clearTestEmails(harness); const donationEmail = 'donor@example.com'; const eventData = createCheckoutCompletedEvent({ customerId: 'cus_donation_1', subscriptionId: 'sub_donation_1', metadata: {is_donation: 'true', donation_email: donationEmail}, }); - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); const {DonationRepository} = await import('@app/api/donation/DonationRepository'); const donationRepository = new DonationRepository(); @@ -419,7 +379,7 @@ describe('StripeWebhookService - checkout.session.completed', () => { expect(donor).not.toBeNull(); expect(donor?.stripeCustomerId).toBe('cus_donation_1'); expect(donor?.stripeSubscriptionId).toBe('sub_donation_1'); - const emails = await listDonationTestEmails(harness, {recipient: donationEmail}); + const emails = await listTestEmails(harness, {recipient: donationEmail}); const confirmationEmail = emails.find((e) => e.type === 'donation_confirmation'); expect(confirmationEmail).toBeDefined(); expect(confirmationEmail?.to).toBe(donationEmail); diff --git a/fluxer_api/src/api/stripe/tests/StripeWebhookCore.test.ts b/fluxer_api/src/api/stripe/tests/StripeWebhookCore.test.ts index 4237a437b..c75022369 100644 --- a/fluxer_api/src/api/stripe/tests/StripeWebhookCore.test.ts +++ b/fluxer_api/src/api/stripe/tests/StripeWebhookCore.test.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import crypto from 'node:crypto'; import {Config} from '@app/api/Config'; +import {sendStripeWebhookRaw, signStripeWebhook} from '@app/api/stripe/tests/StripeWebhookTestUtils'; import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness'; import {createMockWebhookPayload, type StripeWebhookEventData} from '@app/api/test/msw/handlers/StripeApiHandlers'; import {createBuilder} from '@app/api/test/TestRequestBuilder'; @@ -22,38 +22,13 @@ describe('Stripe Webhook - Core Handling', () => { beforeEach(async () => { await harness.resetData(); }); - function createWebhookSignature(payload: string, timestamp: number, secret: string): string { - const signedPayload = `${timestamp}.${payload}`; - const signature = crypto.createHmac('sha256', secret).update(signedPayload).digest('hex'); - return `t=${timestamp},v1=${signature}`; - } - async function sendWebhook( - eventData: StripeWebhookEventData, - options?: { - signature?: string; - expectStatus?: number; - }, - ): Promise<{ - response: Response; - text: string; - }> { - const {payload, timestamp} = createMockWebhookPayload(eventData); - const signature = options?.signature ?? createWebhookSignature(payload, timestamp, Config.stripe.webhookSecret!); - const result = await createBuilder(harness, '') - .post('/stripe/webhook') - .header('stripe-signature', signature) - .header('content-type', 'application/json') - .body(payload) - .executeRaw(); - return result; - } describe('handleWebhook', () => { test('rejects webhook with invalid signature', async () => { const eventData: StripeWebhookEventData = { type: 'checkout.session.completed', data: {object: {id: 'cs_test_123'}}, }; - const {response} = await sendWebhook(eventData, {signature: 'invalid_signature'}); + const {response} = await sendStripeWebhookRaw(harness, eventData, 'invalid_signature'); expect(response.status).toBe(401); }); test('accepts webhook with valid signature', async () => { @@ -61,7 +36,7 @@ describe('Stripe Webhook - Core Handling', () => { type: 'customer.created', data: {object: {id: 'cus_test_123'}}, }; - const {response} = await sendWebhook(eventData); + const {response} = await sendStripeWebhookRaw(harness, eventData); expect(response.status).toBe(200); }); test('handles unknown webhook event types gracefully', async () => { @@ -69,7 +44,7 @@ describe('Stripe Webhook - Core Handling', () => { type: 'some.unknown.event', data: {object: {id: 'unknown_123'}}, }; - const {response} = await sendWebhook(eventData); + const {response} = await sendStripeWebhookRaw(harness, eventData); expect(response.status).toBe(200); }); test('rejects webhook with tampered payload', async () => { @@ -78,7 +53,7 @@ describe('Stripe Webhook - Core Handling', () => { data: {object: {id: 'cs_test_123'}}, }; const {payload, timestamp} = createMockWebhookPayload(eventData); - const signature = createWebhookSignature(payload, timestamp, Config.stripe.webhookSecret!); + const signature = signStripeWebhook(payload, timestamp, Config.stripe.webhookSecret!); const tamperedPayload = payload.replace('cs_test_123', 'cs_test_tampered'); const result = await createBuilder(harness, '') .post('/stripe/webhook') @@ -95,7 +70,7 @@ describe('Stripe Webhook - Core Handling', () => { }; const {payload} = createMockWebhookPayload(eventData); const oldTimestamp = Math.floor(Date.now() / 1000) - 600; - const signature = createWebhookSignature(payload, oldTimestamp, Config.stripe.webhookSecret!); + const signature = signStripeWebhook(payload, oldTimestamp, Config.stripe.webhookSecret!); const result = await createBuilder(harness, '') .post('/stripe/webhook') .header('stripe-signature', signature) diff --git a/fluxer_api/src/api/stripe/tests/StripeWebhookDispute.test.ts b/fluxer_api/src/api/stripe/tests/StripeWebhookDispute.test.ts index 77d33ac5f..78314dc9d 100644 --- a/fluxer_api/src/api/stripe/tests/StripeWebhookDispute.test.ts +++ b/fluxer_api/src/api/stripe/tests/StripeWebhookDispute.test.ts @@ -1,25 +1,20 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import crypto from 'node:crypto'; import {createTestAccount, findLastTestEmail, listTestEmails} from '@app/api/auth/tests/AuthTestUtils'; import {createUserID} from '@app/api/BrandedTypes'; -import {Config} from '@app/api/Config'; import { mockStripeWebhookSecret, restoreStripeWebhookSecret, + sendStripeWebhook, + sendStripeWebhookExpectStripeError, setupSyncStripeWebhookWorker, } from '@app/api/stripe/tests/StripeWebhookTestUtils'; import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness'; -import { - createMockWebhookPayload, - createStripeApiHandlers, - type StripeWebhookEventData, -} from '@app/api/test/msw/handlers/StripeApiHandlers'; +import {createStripeApiHandlers, type StripeWebhookEventData} from '@app/api/test/msw/handlers/StripeApiHandlers'; import {server} from '@app/api/test/msw/server'; import {createBuilder, createBuilderWithoutAuth} from '@app/api/test/TestRequestBuilder'; import {PaymentRepository} from '@app/api/user/repositories/PaymentRepository'; import {UserRepository} from '@app/api/user/repositories/UserRepository'; -import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes'; import {DeletionReasons} from '@fluxer/constants/src/Core'; import {UserFlags, UserPremiumTypes} from '@fluxer/constants/src/UserConstants'; import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, test} from 'vitest'; @@ -50,36 +45,6 @@ describe('Stripe Webhook Dispute Events', () => { afterEach(() => { server.resetHandlers(); }); - function createWebhookSignature(payload: string, timestamp: number, secret: string): string { - const signedPayload = `${timestamp}.${payload}`; - const signature = crypto.createHmac('sha256', secret).update(signedPayload).digest('hex'); - return `t=${timestamp},v1=${signature}`; - } - async function sendWebhook(eventData: StripeWebhookEventData): Promise<{ - received: boolean; - }> { - const {payload, timestamp} = createMockWebhookPayload(eventData); - const signature = createWebhookSignature(payload, timestamp, Config.stripe.webhookSecret!); - return createBuilder<{ - received: boolean; - }>(harness, '') - .post('/stripe/webhook') - .header('stripe-signature', signature) - .header('content-type', 'application/json') - .body(payload) - .execute(); - } - async function sendWebhookExpectStripeError(eventData: StripeWebhookEventData): Promise { - const {payload, timestamp} = createMockWebhookPayload(eventData); - const signature = createWebhookSignature(payload, timestamp, Config.stripe.webhookSecret!); - await createBuilder(harness, '') - .post('/stripe/webhook') - .header('stripe-signature', signature) - .header('content-type', 'application/json') - .body(payload) - .expect(400, APIErrorCodes.STRIPE_ERROR) - .execute(); - } describe('charge.dispute.created', () => { test('schedules account deletion on chargeback for direct purchase', async () => { const purchaser = await createTestAccount(harness); @@ -108,7 +73,7 @@ describe('Stripe Webhook Dispute Events', () => { }, }, }; - await sendWebhook(eventData); + await sendStripeWebhook(harness, eventData); const updatedUser = await createBuilderWithoutAuth(harness) .get(`/test/users/${purchaser.userId}/data-exists`) .execute(); @@ -157,7 +122,7 @@ describe('Stripe Webhook Dispute Events', () => { payment_intent_id: paymentIntentId, stripe_customer_id: customerId, }); - await sendWebhook({ + await sendStripeWebhook(harness, { type: 'charge.dispute.created', data: { object: { @@ -250,7 +215,7 @@ describe('Stripe Webhook Dispute Events', () => { }, }, }; - await sendWebhook(eventData); + await sendStripeWebhook(harness, eventData); const redeemerAfterDispute = await createBuilder<{ premium_type: number; }>(harness, redeemer.token) @@ -305,7 +270,7 @@ describe('Stripe Webhook Dispute Events', () => { premium_gift_extension_ends_at: new Date(redeemedAt.getTime() + 2 * 7 * 24 * 60 * 60 * 1000).toISOString(), }) .execute(); - await sendWebhook({ + await sendStripeWebhook(harness, { type: 'charge.dispute.created', data: { object: { @@ -376,7 +341,7 @@ describe('Stripe Webhook Dispute Events', () => { .get('/users/@me') .execute(); expect(redeemerBefore.premium_type).toBe(UserPremiumTypes.SUBSCRIPTION); - await sendWebhook({ + await sendStripeWebhook(harness, { type: 'charge.dispute.created', data: { object: { @@ -419,7 +384,7 @@ describe('Stripe Webhook Dispute Events', () => { premium_type: UserPremiumTypes.LIFETIME, }) .execute(); - await sendWebhook({ + await sendStripeWebhook(harness, { type: 'charge.dispute.created', data: { object: { @@ -447,7 +412,7 @@ describe('Stripe Webhook Dispute Events', () => { }, }, }; - await sendWebhookExpectStripeError(eventData); + await sendStripeWebhookExpectStripeError(harness, eventData); }); }); describe('charge.dispute.closed', () => { @@ -491,7 +456,7 @@ describe('Stripe Webhook Dispute Events', () => { }, }, }; - await sendWebhook(eventData); + await sendStripeWebhook(harness, eventData); const userAfterWin = await createBuilderWithoutAuth(harness) .get(`/test/users/${purchaser.userId}/data-exists`) .execute(); @@ -537,7 +502,7 @@ describe('Stripe Webhook Dispute Events', () => { }, }, }; - await sendWebhook(eventData); + await sendStripeWebhook(harness, eventData); const userAfterLoss = await createBuilderWithoutAuth(harness) .get(`/test/users/${purchaser.userId}/data-exists`) .execute(); @@ -556,7 +521,7 @@ describe('Stripe Webhook Dispute Events', () => { }, }, }; - await sendWebhookExpectStripeError(eventData); + await sendStripeWebhookExpectStripeError(harness, eventData); }); }); }); diff --git a/fluxer_api/src/api/stripe/tests/StripeWebhookEarlyFraudWarning.test.ts b/fluxer_api/src/api/stripe/tests/StripeWebhookEarlyFraudWarning.test.ts index 65fd1db89..ec7d19518 100644 --- a/fluxer_api/src/api/stripe/tests/StripeWebhookEarlyFraudWarning.test.ts +++ b/fluxer_api/src/api/stripe/tests/StripeWebhookEarlyFraudWarning.test.ts @@ -1,6 +1,5 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import crypto from 'node:crypto'; import {AdminRepository} from '@app/api/admin/AdminRepository'; import {findLastTestEmail, listTestEmails} from '@app/api/auth/tests/AuthTestUtils'; import {createApplicationID, createUserID} from '@app/api/BrandedTypes'; @@ -9,16 +8,12 @@ import {OAuth2TokenRepository} from '@app/api/oauth/repositories/OAuth2TokenRepo import { createTestPayment, createTestUserWithPremium, + sendStripeWebhook, setupSyncStripeWebhookWorker, } from '@app/api/stripe/tests/StripeWebhookTestUtils'; import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness'; -import { - createMockWebhookPayload, - createStripeApiHandlers, - type StripeWebhookEventData, -} from '@app/api/test/msw/handlers/StripeApiHandlers'; +import {createStripeApiHandlers} from '@app/api/test/msw/handlers/StripeApiHandlers'; import {server} from '@app/api/test/msw/server'; -import {createBuilder} from '@app/api/test/TestRequestBuilder'; import {UserRepository} from '@app/api/user/repositories/UserRepository'; import {DeletionReasons} from '@fluxer/constants/src/Core'; import {UserFlags, UserPremiumTypes} from '@fluxer/constants/src/UserConstants'; @@ -43,25 +38,6 @@ describe('Stripe Webhook Early Fraud Warning', () => { afterEach(() => { server.resetHandlers(); }); - function createWebhookSignature(payload: string, timestamp: number, secret: string): string { - const signedPayload = `${timestamp}.${payload}`; - const signature = crypto.createHmac('sha256', secret).update(signedPayload).digest('hex'); - return `t=${timestamp},v1=${signature}`; - } - async function sendWebhook(eventData: StripeWebhookEventData): Promise<{ - received: boolean; - }> { - const {payload, timestamp} = createMockWebhookPayload(eventData); - const signature = createWebhookSignature(payload, timestamp, Config.stripe.webhookSecret!); - return await createBuilder<{ - received: boolean; - }>(harness, '') - .post('/stripe/webhook') - .header('content-type', 'application/json') - .header('stripe-signature', signature) - .body(payload) - .execute(); - } async function createDirectPurchaseUser({ customerId, paymentIntentId, @@ -126,7 +102,7 @@ describe('Stripe Webhook Early Fraud Warning', () => { scope: new Set(['identify']), created_at: new Date(), }); - await sendWebhook({ + await sendStripeWebhook(harness, { type: 'radar.early_fraud_warning.created', data: { object: { @@ -201,7 +177,7 @@ describe('Stripe Webhook Early Fraud Warning', () => { paymentIntentId, subscriptionId, }); - await sendWebhook({ + await sendStripeWebhook(harness, { type: 'radar.early_fraud_warning.created', data: { object: { @@ -245,7 +221,7 @@ describe('Stripe Webhook Early Fraud Warning', () => { paymentIntentId, subscriptionId, }); - await sendWebhook({ + await sendStripeWebhook(harness, { type: 'radar.early_fraud_warning.created', data: { object: { @@ -264,7 +240,7 @@ describe('Stripe Webhook Early Fraud Warning', () => { const userAfterEarlyWarning = await userRepository.findUnique(createUserID(BigInt(account.userId))); expect(userAfterEarlyWarning?.pendingDeletionAt).not.toBeNull(); const firstPendingDeletionAt = userAfterEarlyWarning!.pendingDeletionAt!.toISOString(); - await sendWebhook({ + await sendStripeWebhook(harness, { type: 'charge.dispute.created', data: { object: { diff --git a/fluxer_api/src/api/stripe/tests/StripeWebhookInvoice.test.ts b/fluxer_api/src/api/stripe/tests/StripeWebhookInvoice.test.ts index cbbb1e2e3..036383b8a 100644 --- a/fluxer_api/src/api/stripe/tests/StripeWebhookInvoice.test.ts +++ b/fluxer_api/src/api/stripe/tests/StripeWebhookInvoice.test.ts @@ -6,7 +6,11 @@ import {createUserID} from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; import {getBillingRepository} from '@app/api/middleware/ServiceRegistry'; import {ProductType} from '@app/api/stripe/ProductRegistry'; -import {setupSyncStripeWebhookWorker} from '@app/api/stripe/tests/StripeWebhookTestUtils'; +import { + sendStripeWebhook, + sendStripeWebhookExpectStripeError, + setupSyncStripeWebhookWorker, +} from '@app/api/stripe/tests/StripeWebhookTestUtils'; import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness'; import { createInvoiceFinalizationFailedEvent, @@ -14,14 +18,12 @@ import { createInvoicePaymentActionRequiredEvent, createInvoicePaymentFailedEvent, createInvoiceUpdatedEvent, - createMockWebhookPayload, createStripeApiHandlers, type StripeApiHandlers, type StripeWebhookEventData, } from '@app/api/test/msw/handlers/StripeApiHandlers'; import {server} from '@app/api/test/msw/server'; import {createBuilder} from '@app/api/test/TestRequestBuilder'; -import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes'; import {UserPremiumTypes} from '@fluxer/constants/src/UserConstants'; import {HttpResponse, http} from 'msw'; import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, test} from 'vitest'; @@ -78,36 +80,6 @@ describe('Stripe Webhook - Invoice Events', () => { afterEach(() => { server.resetHandlers(); }); - function createWebhookSignature(payload: string, timestamp: number, secret: string): string { - const signedPayload = `${timestamp}.${payload}`; - const signature = crypto.createHmac('sha256', secret).update(signedPayload).digest('hex'); - return `t=${timestamp},v1=${signature}`; - } - async function sendWebhook(eventData: StripeWebhookEventData): Promise<{ - received: boolean; - }> { - const {payload, timestamp} = createMockWebhookPayload(eventData); - const signature = createWebhookSignature(payload, timestamp, Config.stripe.webhookSecret!); - return createBuilder<{ - received: boolean; - }>(harness, '') - .post('/stripe/webhook') - .header('stripe-signature', signature) - .header('content-type', 'application/json') - .body(payload) - .execute(); - } - async function sendWebhookExpectStripeError(eventData: StripeWebhookEventData): Promise { - const {payload, timestamp} = createMockWebhookPayload(eventData); - const signature = createWebhookSignature(payload, timestamp, Config.stripe.webhookSecret!); - await createBuilder(harness, '') - .post('/stripe/webhook') - .header('stripe-signature', signature) - .header('content-type', 'application/json') - .body(payload) - .expect(400, APIErrorCodes.STRIPE_ERROR) - .execute(); - } async function createPaymentRecord(params: { userId: string; subscriptionId: string; @@ -213,7 +185,7 @@ describe('Stripe Webhook - Invoice Events', () => { }, }, }; - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); const user = await createBuilder<{ premium_type: number | null; @@ -264,7 +236,7 @@ describe('Stripe Webhook - Invoice Events', () => { }, }, }; - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); const user = await createBuilder<{ premium_since: string | null; @@ -299,7 +271,7 @@ describe('Stripe Webhook - Invoice Events', () => { }, }, }; - const firstResult = await sendWebhook(firstEventData); + const firstResult = await sendStripeWebhook(harness, firstEventData); expect(firstResult.received).toBe(true); const userAfterFirst = await createBuilder<{ premium_type: number | null; @@ -317,7 +289,7 @@ describe('Stripe Webhook - Invoice Events', () => { ...firstEventData, id: 'evt_duplicate_invoice_renewal_1_b', }; - const secondResult = await sendWebhook(secondEventData); + const secondResult = await sendStripeWebhook(harness, secondEventData); expect(secondResult.received).toBe(true); const userAfterSecond = await createBuilder<{ premium_type: number | null; @@ -347,7 +319,7 @@ describe('Stripe Webhook - Invoice Events', () => { }, }, }; - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); const user = await createBuilder<{ premium_type: number; @@ -369,7 +341,7 @@ describe('Stripe Webhook - Invoice Events', () => { }, }, }; - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); }); test('handles missing subscription info gracefully', async () => { @@ -398,7 +370,7 @@ describe('Stripe Webhook - Invoice Events', () => { }, }, }; - await sendWebhookExpectStripeError(eventData); + await sendStripeWebhookExpectStripeError(harness, eventData); }); test('anchors renewal to Stripe period_end regardless of existing premium time', async () => { const account = await createTestAccount(harness); @@ -434,7 +406,7 @@ describe('Stripe Webhook - Invoice Events', () => { }, }, }; - await sendWebhook(eventData); + await sendStripeWebhook(harness, eventData); const userAfter = await createBuilder<{ premium_until: string | null; }>(harness, account.token) @@ -475,7 +447,7 @@ describe('Stripe Webhook - Invoice Events', () => { }, }, }; - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); const userAfter = await createBuilder<{ premium_type: number | null; @@ -518,7 +490,7 @@ describe('Stripe Webhook - Invoice Events', () => { }, }, }; - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); const userAfter = await createBuilder<{ premium_billing_cycle: string | null; @@ -582,7 +554,7 @@ describe('Stripe Webhook - Invoice Events', () => { }, ], }; - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); const me = await createBuilder<{ premium_type: number | null; @@ -616,7 +588,8 @@ describe('Stripe Webhook - Invoice Events', () => { premiumUntil: failedPeriodStart, billingCycle: 'yearly', }); - const result = await sendWebhook( + const result = await sendStripeWebhook( + harness, createRenewalFailureEvent({ invoiceId: `in_failed_yearly_${Date.now()}`, customerId: 'cus_test_failed_yearly', @@ -667,7 +640,7 @@ describe('Stripe Webhook - Invoice Events', () => { }); eventData.id = `evt_failed_repeat_${attempt}_${Date.now()}`; eventData.data.object.attempt_count = attempt; - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); const me = await createBuilder<{ premium_until: string | null; @@ -710,7 +683,7 @@ describe('Stripe Webhook - Invoice Events', () => { amountDue: 500, }); eventData.data.object.billing_reason = 'subscription_update'; - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); const me = await createBuilder<{ premium_until: string | null; @@ -763,7 +736,7 @@ describe('Stripe Webhook - Invoice Events', () => { }, ], }; - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); const me = await createBuilder<{ premium_until: string | null; @@ -816,7 +789,7 @@ describe('Stripe Webhook - Invoice Events', () => { }, ], }; - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); const me = await createBuilder<{ premium_until: string | null; @@ -871,7 +844,7 @@ describe('Stripe Webhook - Invoice Events', () => { }, ], }; - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); const me = await createBuilder<{ premium_until: string | null; @@ -910,7 +883,7 @@ describe('Stripe Webhook - Invoice Events', () => { nextPaymentAttempt: null, }); eventData.data.object.billing_reason = 'subscription_cycle'; - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); const me = await createBuilder<{ premium_until: string | null; @@ -940,7 +913,7 @@ describe('Stripe Webhook - Invoice Events', () => { currency: 'usd', }); eventData.data.object.billing_reason = 'subscription_cycle'; - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); const me = await createBuilder<{ premium_type: number | null; @@ -981,7 +954,7 @@ describe('Stripe Webhook - Invoice Events', () => { }, }).handlers, ); - const result = await sendWebhook({ + const result = await sendStripeWebhook(harness, { type: 'invoice.payment_succeeded', data: { object: { @@ -1040,7 +1013,7 @@ describe('Stripe Webhook - Invoice Events', () => { }, }).handlers, ); - const result = await sendWebhook({ + const result = await sendStripeWebhook(harness, { type: 'invoice.payment_succeeded', data: { object: { @@ -1099,7 +1072,7 @@ describe('Stripe Webhook - Invoice Events', () => { }, }).handlers, ); - await sendWebhookExpectStripeError({ + await sendStripeWebhookExpectStripeError(harness, { type: 'invoice.payment_succeeded', data: { object: { @@ -1140,7 +1113,7 @@ describe('Stripe Webhook - Invoice Events', () => { subscriptionInterval: 'month', subscriptionCurrentPeriodEnd: null, }); - const result = await sendWebhook({ + const result = await sendStripeWebhook(harness, { type: 'invoice.payment_succeeded', data: { object: { @@ -1181,7 +1154,7 @@ describe('Stripe Webhook - Invoice Events', () => { subscriptionInterval: 'month', subscriptionCurrentPeriodEnd: null, }); - const result = await sendWebhook({ + const result = await sendStripeWebhook(harness, { type: 'invoice.payment_succeeded', data: { object: { diff --git a/fluxer_api/src/api/stripe/tests/StripeWebhookRefund.test.ts b/fluxer_api/src/api/stripe/tests/StripeWebhookRefund.test.ts index 19b787740..8fb160451 100644 --- a/fluxer_api/src/api/stripe/tests/StripeWebhookRefund.test.ts +++ b/fluxer_api/src/api/stripe/tests/StripeWebhookRefund.test.ts @@ -1,12 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import crypto from 'node:crypto'; import {createTestAccount} from '@app/api/auth/tests/AuthTestUtils'; import {createUserID} from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; -import {setupSyncStripeWebhookWorker} from '@app/api/stripe/tests/StripeWebhookTestUtils'; +import {sendStripeWebhook, setupSyncStripeWebhookWorker} from '@app/api/stripe/tests/StripeWebhookTestUtils'; import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness'; -import {createMockWebhookPayload, type StripeWebhookEventData} from '@app/api/test/msw/handlers/StripeApiHandlers'; import {server} from '@app/api/test/msw/server'; import {createBuilder} from '@app/api/test/TestRequestBuilder'; import {UserRepository} from '@app/api/user/repositories/UserRepository'; @@ -30,25 +28,6 @@ describe('Stripe Webhook Refund', () => { beforeEach(async () => { await harness.resetData(); }); - function createWebhookSignature(payload: string, timestamp: number, secret: string): string { - const signedPayload = `${timestamp}.${payload}`; - const signature = crypto.createHmac('sha256', secret).update(signedPayload).digest('hex'); - return `t=${timestamp},v1=${signature}`; - } - async function sendWebhook(eventData: StripeWebhookEventData): Promise<{ - received: boolean; - }> { - const {payload, timestamp} = createMockWebhookPayload(eventData); - const signature = createWebhookSignature(payload, timestamp, Config.stripe.webhookSecret!); - return createBuilder<{ - received: boolean; - }>(harness, '') - .post('/stripe/webhook') - .header('stripe-signature', signature) - .header('content-type', 'application/json') - .body(payload) - .execute(); - } function useRefundListHandler(chargeId: string, refunds: Array> = []): void { server.use( http.get( @@ -88,7 +67,7 @@ describe('Stripe Webhook Refund', () => { completed_at: new Date(), }); useRefundListHandler('ch_test_refund_123'); - await sendWebhook({ + await sendStripeWebhook(harness, { type: 'charge.refunded', data: { object: { @@ -154,7 +133,7 @@ describe('Stripe Webhook Refund', () => { metadata: {}, }; const latest = {...earlier, id: 're_test_refund_second_456_2', amount: 1300, created: nowSeconds}; - await sendWebhook({ + await sendStripeWebhook(harness, { type: 'charge.refunded', data: { object: { @@ -196,7 +175,7 @@ describe('Stripe Webhook Refund', () => { created: Math.floor(Date.now() / 1000), metadata: {}, }; - await sendWebhook({ + await sendStripeWebhook(harness, { type: 'charge.refunded', data: { object: { @@ -259,8 +238,8 @@ describe('Stripe Webhook Refund', () => { }, }, }; - await sendWebhook({...chargeEvent, id: 'evt_test_refund_retry_1'}); - await sendWebhook({...chargeEvent, id: 'evt_test_refund_retry_2'}); + await sendStripeWebhook(harness, {...chargeEvent, id: 'evt_test_refund_retry_1'}); + await sendStripeWebhook(harness, {...chargeEvent, id: 'evt_test_refund_retry_2'}); const updatedUser = await userRepository.findUnique(userId); expect(updatedUser!.firstRefundAt).not.toBeNull(); expect(updatedUser!.premiumFlags & PremiumFlags.PURCHASE_DISABLED).toBe(0); @@ -312,17 +291,17 @@ describe('Stripe Webhook Refund', () => { }, }; useRefundListHandler(chargeId); - await sendWebhook({...chargeEvent, id: 'evt_test_refund_late_record_1'}); + await sendStripeWebhook(harness, {...chargeEvent, id: 'evt_test_refund_late_record_1'}); const afterFallback = await userRepository.findUnique(userId); expect(afterFallback!.firstRefundAt).not.toBeNull(); expect(afterFallback!.premiumFlags & PremiumFlags.PURCHASE_DISABLED).toBe(0); - await sendWebhook({ + await sendStripeWebhook(harness, { id: 'evt_test_refund_late_record_2', type: 'refund.created', data: {object: refund}, }); useRefundListHandler(chargeId, [refund]); - await sendWebhook({...chargeEvent, id: 'evt_test_refund_late_record_3'}); + await sendStripeWebhook(harness, {...chargeEvent, id: 'evt_test_refund_late_record_3'}); const afterRecord = await userRepository.findUnique(userId); expect(afterRecord!.firstRefundAt!.getTime()).toBe(afterFallback!.firstRefundAt!.getTime()); expect(afterRecord!.premiumFlags & PremiumFlags.PURCHASE_DISABLED).toBe(0); @@ -364,7 +343,7 @@ describe('Stripe Webhook Refund', () => { metadata: {rejection_reason: 'duplicate_active_subscription'}, }, ]); - await sendWebhook({ + await sendStripeWebhook(harness, { type: 'charge.refunded', data: { object: { @@ -415,7 +394,7 @@ describe('Stripe Webhook Refund', () => { metadata: {rejection_reason: 'localized_card_country_mismatch'}, }, ]); - await sendWebhook({ + await sendStripeWebhook(harness, { type: 'charge.refunded', data: { object: { @@ -495,11 +474,11 @@ describe('Stripe Webhook Refund', () => { }, }, }; - await sendWebhook({...chargeEvent, id: 'evt_test_refund_mixed_1'}); + await sendStripeWebhook(harness, {...chargeEvent, id: 'evt_test_refund_mixed_1'}); const afterFirst = await userRepository.findUnique(userId); expect(afterFirst!.firstRefundAt).not.toBeNull(); expect(afterFirst!.premiumFlags & PremiumFlags.PURCHASE_DISABLED).toBe(0); - await sendWebhook({...chargeEvent, id: 'evt_test_refund_mixed_2'}); + await sendStripeWebhook(harness, {...chargeEvent, id: 'evt_test_refund_mixed_2'}); const afterRetry = await userRepository.findUnique(userId); expect(afterRetry!.firstRefundAt!.getTime()).toBe(afterFirst!.firstRefundAt!.getTime()); expect(afterRetry!.premiumFlags & PremiumFlags.PURCHASE_DISABLED).toBe(0); @@ -564,11 +543,14 @@ describe('Stripe Webhook Refund', () => { amount: 1300, created: nowSeconds, }; - await sendWebhook({...chargeEventWithRefunds([firstRefund]), id: 'evt_test_refund_ladder_1'}); + await sendStripeWebhook(harness, {...chargeEventWithRefunds([firstRefund]), id: 'evt_test_refund_ladder_1'}); const afterFirst = await userRepository.findUnique(userId); expect(afterFirst!.firstRefundAt).not.toBeNull(); expect(afterFirst!.premiumFlags & PremiumFlags.PURCHASE_DISABLED).toBe(0); - await sendWebhook({...chargeEventWithRefunds([firstRefund, secondRefund]), id: 'evt_test_refund_ladder_2'}); + await sendStripeWebhook(harness, { + ...chargeEventWithRefunds([firstRefund, secondRefund]), + id: 'evt_test_refund_ladder_2', + }); const afterSecond = await userRepository.findUnique(userId); expect(afterSecond!.premiumFlags & PremiumFlags.PURCHASE_DISABLED).toBe(PremiumFlags.PURCHASE_DISABLED); }); @@ -584,7 +566,7 @@ describe('Stripe Webhook Refund', () => { (await userRepository.findUnique(userId))!.toRow(), ); useRefundListHandler('ch_test_refund_sub_789'); - await sendWebhook({ + await sendStripeWebhook(harness, { type: 'charge.refunded', data: { object: { @@ -617,7 +599,7 @@ describe('Stripe Webhook Refund', () => { subscriptionCancelAt: null, }); useRefundListHandler('ch_test_refund_donation_101'); - const result = await sendWebhook({ + const result = await sendStripeWebhook(harness, { type: 'charge.refunded', data: { object: { @@ -670,7 +652,7 @@ describe('Stripe Webhook Refund', () => { }) .execute(); useRefundListHandler('ch_test_refund_gift_multi_123'); - await sendWebhook({ + await sendStripeWebhook(harness, { type: 'charge.refunded', data: { object: { @@ -724,7 +706,7 @@ describe('Stripe Webhook Refund', () => { .execute(); expect(redeemerBefore.premium_type).toBe(UserPremiumTypes.SUBSCRIPTION); useRefundListHandler('ch_test_refund_stripe_cus_only'); - await sendWebhook({ + await sendStripeWebhook(harness, { type: 'charge.refunded', data: { object: { @@ -763,7 +745,7 @@ describe('Stripe Webhook Refund', () => { }) .execute(); useRefundListHandler('ch_test_refund_gift_override_123'); - await sendWebhook({ + await sendStripeWebhook(harness, { type: 'charge.refunded', data: { object: { @@ -797,7 +779,7 @@ describe('Stripe Webhook Refund', () => { HttpResponse.json({id: params.id, object: 'subscription', status: 'canceled'}), ), ); - await sendWebhook({ + await sendStripeWebhook(harness, { type: 'refund.updated', data: { object: { @@ -821,7 +803,7 @@ describe('Stripe Webhook Refund', () => { const account = await createTestAccount(harness); const userId = createUserID(BigInt(account.userId)); const userRepository = new UserRepository(); - await sendWebhook({ + await sendStripeWebhook(harness, { type: 'refund.updated', data: { object: { @@ -846,7 +828,7 @@ describe('Stripe Webhook Refund', () => { const account = await createTestAccount(harness); const userId = createUserID(BigInt(account.userId)); const userRepository = new UserRepository(); - await sendWebhook({ + await sendStripeWebhook(harness, { type: 'refund.failed', data: { object: { diff --git a/fluxer_api/src/api/stripe/tests/StripeWebhookSubscription.test.ts b/fluxer_api/src/api/stripe/tests/StripeWebhookSubscription.test.ts index b1832ed22..a7902cb5d 100644 --- a/fluxer_api/src/api/stripe/tests/StripeWebhookSubscription.test.ts +++ b/fluxer_api/src/api/stripe/tests/StripeWebhookSubscription.test.ts @@ -1,26 +1,26 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import crypto from 'node:crypto'; import {createTestAccount} from '@app/api/auth/tests/AuthTestUtils'; import {createUserID} from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; import {ProductType} from '@app/api/stripe/ProductRegistry'; -import {setupSyncStripeWebhookWorker} from '@app/api/stripe/tests/StripeWebhookTestUtils'; +import { + sendStripeWebhook, + sendStripeWebhookExpectStripeError, + setupSyncStripeWebhookWorker, +} from '@app/api/stripe/tests/StripeWebhookTestUtils'; import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness'; import { - createMockWebhookPayload, createStripeApiHandlers, createSubscriptionDeletedEvent, createSubscriptionUpdatedEvent, type StripeApiHandlers, - type StripeWebhookEventData, } from '@app/api/test/msw/handlers/StripeApiHandlers'; import {server} from '@app/api/test/msw/server'; import {createBuilder} from '@app/api/test/TestRequestBuilder'; import {PaymentRepository} from '@app/api/user/repositories/PaymentRepository'; import {UserRepository} from '@app/api/user/repositories/UserRepository'; import {getPremiumPaymentRecoveryGraceMs, PREMIUM_GRACE_PERIOD_MS} from '@app/api/user/UserHelpers'; -import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes'; import {UserPremiumTypes} from '@fluxer/constants/src/UserConstants'; import {HttpResponse, http} from 'msw'; import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, test} from 'vitest'; @@ -63,36 +63,6 @@ describe('Stripe Webhook Subscription Lifecycle', () => { server.resetHandlers(); server.use(...stripeHandlers.handlers); }); - function createWebhookSignature(payload: string, timestamp: number, secret: string): string { - const signedPayload = `${timestamp}.${payload}`; - const signature = crypto.createHmac('sha256', secret).update(signedPayload).digest('hex'); - return `t=${timestamp},v1=${signature}`; - } - async function sendWebhook(eventData: StripeWebhookEventData): Promise<{ - received: boolean; - }> { - const {payload, timestamp} = createMockWebhookPayload(eventData); - const signature = createWebhookSignature(payload, timestamp, Config.stripe.webhookSecret!); - return createBuilder<{ - received: boolean; - }>(harness, '') - .post('/stripe/webhook') - .header('stripe-signature', signature) - .header('content-type', 'application/json') - .body(payload) - .execute(); - } - async function sendWebhookExpectStripeError(eventData: StripeWebhookEventData): Promise { - const {payload, timestamp} = createMockWebhookPayload(eventData); - const signature = createWebhookSignature(payload, timestamp, Config.stripe.webhookSecret!); - await createBuilder(harness, '') - .post('/stripe/webhook') - .header('stripe-signature', signature) - .header('content-type', 'application/json') - .body(payload) - .expect(400, APIErrorCodes.STRIPE_ERROR) - .execute(); - } describe('customer.subscription.updated', () => { test('updates subscription cancellation status when cancel_at_period_end is true', async () => { const account = await createTestAccount(harness); @@ -133,7 +103,7 @@ describe('Stripe Webhook Subscription Lifecycle', () => { eventData.data.object.items = { data: [{current_period_end: cancelAt}], }; - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); const response = await harness.requestJson({ path: '/users/@me', @@ -190,7 +160,7 @@ describe('Stripe Webhook Subscription Lifecycle', () => { eventData.data.object.items = { data: [{current_period_end: currentPeriodEnd}], }; - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); const response = await harness.requestJson({ path: '/users/@me', @@ -255,7 +225,7 @@ describe('Stripe Webhook Subscription Lifecycle', () => { eventData.data.object.items = { data: [{current_period_end: unpaidFuturePeriodEnd}], }; - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); const updatedUser = await userRepository.findUnique(userId); expect(updatedUser?.premiumWillCancel).toBe(true); @@ -315,7 +285,8 @@ describe('Stripe Webhook Subscription Lifecycle', () => { const renewalSeconds = Math.floor(Date.now() / 1000) - 60 * 60; const renewalAt = new Date(renewalSeconds * 1000); const userId = await seedPastDueUser(subscriptionId, renewalAt); - const result = await sendWebhook( + const result = await sendStripeWebhook( + harness, pastDueEvent(subscriptionId, renewalSeconds, renewalSeconds + 30 * 24 * 60 * 60), ); expect(result.received).toBe(true); @@ -331,7 +302,10 @@ describe('Stripe Webhook Subscription Lifecycle', () => { const periodEndSeconds = periodStartSeconds + 30 * 24 * 60 * 60; const paidThrough = new Date(periodEndSeconds * 1000); const userId = await seedPastDueUser(subscriptionId, paidThrough); - const result = await sendWebhook(pastDueEvent(subscriptionId, periodStartSeconds, periodEndSeconds)); + const result = await sendStripeWebhook( + harness, + pastDueEvent(subscriptionId, periodStartSeconds, periodEndSeconds), + ); expect(result.received).toBe(true); const updatedUser = await userRepository.findUnique(userId); expect(updatedUser?.premiumUntil?.getTime()).toBe(paidThrough.getTime()); @@ -406,7 +380,7 @@ describe('Stripe Webhook Subscription Lifecycle', () => { }), ), ); - await sendWebhookExpectStripeError(eventData); + await sendStripeWebhookExpectStripeError(harness, eventData); const response = await harness.requestJson({ path: '/users/@me', method: 'GET', @@ -460,7 +434,7 @@ describe('Stripe Webhook Subscription Lifecycle', () => { eventData.data.object.items = { data: [{current_period_end: currentPeriodEnd}], }; - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); const me = await createBuilder<{ premium_until: string | null; @@ -505,7 +479,7 @@ describe('Stripe Webhook Subscription Lifecycle', () => { expect(beforeUser?.premiumType).toBe(UserPremiumTypes.SUBSCRIPTION); expect(beforeUser?.stripeSubscriptionId).toBe(subscriptionId); const eventData = createSubscriptionDeletedEvent({subscriptionId}); - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); const afterUser = await userRepository.findUnique(userId); expect(afterUser?.premiumType).toBe(UserPremiumTypes.SUBSCRIPTION); @@ -546,7 +520,7 @@ describe('Stripe Webhook Subscription Lifecycle', () => { const beforeUser = await userRepository.findUnique(userId); expect(beforeUser?.premiumType).toBe(UserPremiumTypes.LIFETIME); const eventData = createSubscriptionDeletedEvent({subscriptionId}); - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); const afterUser = await userRepository.findUnique(userId); expect(afterUser?.premiumType).toBe(UserPremiumTypes.LIFETIME); @@ -570,7 +544,7 @@ describe('Stripe Webhook Subscription Lifecycle', () => { ); const endedAt = Math.floor(Date.now() / 1000) - 60; const eventData = createSubscriptionDeletedEvent({subscriptionId, endedAt}); - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); const afterUser = await userRepository.findUnique(userId); expect(afterUser?.premiumUntil?.getTime()).toBe(endedAt * 1000); @@ -597,7 +571,7 @@ describe('Stripe Webhook Subscription Lifecycle', () => { ); const endedAt = Math.floor(Date.now() / 1000); const eventData = createSubscriptionDeletedEvent({subscriptionId, endedAt}); - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); const afterUser = await userRepository.findUnique(userId); expect(afterUser?.premiumUntil?.getTime()).toBe(premiumUntil.getTime()); @@ -648,7 +622,7 @@ describe('Stripe Webhook Subscription Lifecycle', () => { cancellationReason: 'payment_failed', interval: 'month', }); - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); const afterUser = await userRepository.findUnique(userId); expect(afterUser?.premiumType).toBe(UserPremiumTypes.SUBSCRIPTION); @@ -700,7 +674,7 @@ describe('Stripe Webhook Subscription Lifecycle', () => { cancellationReason: 'payment_failed', interval: 'year', }); - const result = await sendWebhook(eventData); + const result = await sendStripeWebhook(harness, eventData); expect(result.received).toBe(true); const afterUser = await userRepository.findUnique(userId); expect(afterUser?.premiumUntil?.getTime()).toBe(lapseStart.getTime()); @@ -732,7 +706,7 @@ describe('Stripe Webhook Subscription Lifecycle', () => { expect(donorBefore).not.toBeNull(); expect(donorBefore?.stripeSubscriptionId).toBe(subscriptionId); const deleteEvent = createSubscriptionDeletedEvent({subscriptionId}); - const deleteResult = await sendWebhook(deleteEvent); + const deleteResult = await sendStripeWebhook(harness, deleteEvent); expect(deleteResult.received).toBe(true); const donorAfter = await donationRepository.findDonorByEmail(donorEmail); expect(donorAfter?.stripeSubscriptionId).toBeNull(); diff --git a/fluxer_api/src/api/stripe/tests/StripeWebhookTestUtils.ts b/fluxer_api/src/api/stripe/tests/StripeWebhookTestUtils.ts index 2e14d2f7f..d595bbc82 100644 --- a/fluxer_api/src/api/stripe/tests/StripeWebhookTestUtils.ts +++ b/fluxer_api/src/api/stripe/tests/StripeWebhookTestUtils.ts @@ -1,5 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later +import crypto from 'node:crypto'; import {createTestAccount, type TestAccount} from '@app/api/auth/tests/AuthTestUtils'; import type {UserID} from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; @@ -22,11 +23,13 @@ import { } from '@app/api/middleware/ServiceSingletons'; import {STRIPE_API_VERSION} from '@app/api/stripe/StripeApiVersion'; import type {ApiTestHarness} from '@app/api/test/ApiTestHarness'; +import {createMockWebhookPayload, type StripeWebhookEventData} from '@app/api/test/msw/handlers/StripeApiHandlers'; import {SyncTaskWorkerService} from '@app/api/test/SyncTaskWorkerService'; import {createBuilder} from '@app/api/test/TestRequestBuilder'; import {PaymentRepository} from '@app/api/user/repositories/PaymentRepository'; import processStripeWebhook from '@app/api/worker/tasks/ProcessStripeWebhook'; import {setWorkerDependenciesForTest} from '@app/api/worker/WorkerContext'; +import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes'; import Stripe from 'stripe'; export async function createTestUserWithPremium( @@ -136,4 +139,38 @@ export function restoreStripeWebhookSecret(): void { ).webhookSecret; } -export {} from '@app/api/test/msw/handlers/StripeApiHandlers'; +export function signStripeWebhook(payload: string, timestamp: number, secret: string): string { + const signature = crypto.createHmac('sha256', secret).update(`${timestamp}.${payload}`).digest('hex'); + return `t=${timestamp},v1=${signature}`; +} + +function buildStripeWebhookRequest(harness: ApiTestHarness, eventData: StripeWebhookEventData, signature?: string) { + const {payload, timestamp} = createMockWebhookPayload(eventData); + return createBuilder<{received: boolean}>(harness, '') + .post('/stripe/webhook') + .header('stripe-signature', signature ?? signStripeWebhook(payload, timestamp, Config.stripe.webhookSecret!)) + .header('content-type', 'application/json') + .body(payload); +} + +export async function sendStripeWebhook( + harness: ApiTestHarness, + eventData: StripeWebhookEventData, +): Promise<{received: boolean}> { + return buildStripeWebhookRequest(harness, eventData).execute(); +} + +export async function sendStripeWebhookRaw( + harness: ApiTestHarness, + eventData: StripeWebhookEventData, + signature?: string, +): Promise<{response: Response; text: string}> { + return buildStripeWebhookRequest(harness, eventData, signature).executeRaw(); +} + +export async function sendStripeWebhookExpectStripeError( + harness: ApiTestHarness, + eventData: StripeWebhookEventData, +): Promise { + await buildStripeWebhookRequest(harness, eventData).expect(400, APIErrorCodes.STRIPE_ERROR).execute(); +} diff --git a/fluxer_api/src/api/test/InMemoryCassandraQueryExecutor.ts b/fluxer_api/src/api/test/InMemoryCassandraQueryExecutor.ts index 5ac68b54a..ca9eaceb5 100644 --- a/fluxer_api/src/api/test/InMemoryCassandraQueryExecutor.ts +++ b/fluxer_api/src/api/test/InMemoryCassandraQueryExecutor.ts @@ -2,42 +2,12 @@ import {getKvMeta} from '@app/api/database/CassandraMetaRegistry'; import type {CassandraQueryExecutorForTesting} from '@app/api/database/CassandraQueryExecution'; -import type { - CassandraParams, - KvQueryCondition, - KvQueryMeta, - PreparedQuery, - WhereExpr, -} from '@app/api/database/CassandraTypes'; +import type {CassandraParams, KvQueryCondition, KvQueryMeta, PreparedQuery} from '@app/api/database/CassandraTypes'; import {isConditionalQuery} from '@app/api/database/CassandraTypes'; +import {matchesWhere, normalizeCql, rowComparator, valuesEqual} from '@app/api/database/PostgresKvQueryExecutor'; type Row = Record; -function normalizeCql(cql: string): string { - return cql.replace(/\s+/g, ' ').trim(); -} - -function compareValues(a: unknown, b: unknown): number { - if (typeof a === 'bigint' || typeof b === 'bigint') { - const av = typeof a === 'bigint' ? a : BigInt(a as number | string); - const bv = typeof b === 'bigint' ? b : BigInt(b as number | string); - if (av === bv) return 0; - return av < bv ? -1 : 1; - } - const av = a instanceof Date ? a.getTime() : typeof a === 'bigint' ? Number(a) : a; - const bv = b instanceof Date ? b.getTime() : typeof b === 'bigint' ? Number(b) : b; - if (Buffer.isBuffer(av) && Buffer.isBuffer(bv)) return Buffer.compare(av, bv); - if (av === bv) return 0; - return (av as number | string) < (bv as number | string) ? -1 : 1; -} - -function valuesEqual(a: unknown, b: unknown): boolean { - if (a == null && b == null) return true; - if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime(); - if (Buffer.isBuffer(a) && Buffer.isBuffer(b)) return a.equals(b); - return a === b; -} - function matchesConditions( row: Row | undefined, conditions: ReadonlyArray> | undefined, @@ -81,72 +51,6 @@ function valueKey(value: unknown): string { return `${typeof value}:${String(value)}`; } -function getParam(params: CassandraParams, param: string): unknown { - return params[param]; -} - -function matchesWhere(row: Row, where: ReadonlyArray> | undefined, params: CassandraParams): boolean { - for (const clause of where ?? []) { - switch (clause.kind) { - case 'eq': - if (!valuesEqual(row[clause.col], getParam(params, clause.param))) return false; - break; - case 'in': { - const values = getParam(params, clause.param) as ReadonlyArray | Set | undefined; - const haystack = values instanceof Set ? [...values] : (values ?? []); - if (!haystack.some((value) => valuesEqual(row[clause.col], value))) return false; - break; - } - case 'lt': - if (compareValues(row[clause.col], getParam(params, clause.param)) >= 0) return false; - break; - case 'lte': - if (compareValues(row[clause.col], getParam(params, clause.param)) > 0) return false; - break; - case 'gt': - case 'tokenGt': - if (compareValues(row[clause.col], getParam(params, clause.param)) <= 0) return false; - break; - case 'gte': - if (compareValues(row[clause.col], getParam(params, clause.param)) < 0) return false; - break; - case 'tupleGt': { - const left = clause.cols.map((column) => row[column]); - const right = clause.params.map((param) => getParam(params, param)); - let greater = false; - for (let i = 0; i < left.length; i += 1) { - const cmp = compareValues(left[i], right[i]); - if (cmp > 0) { - greater = true; - break; - } - if (cmp < 0) break; - } - if (!greater) return false; - break; - } - } - } - return true; -} - -function compareColumns(columns: ReadonlyArray, left: Row, right: Row): number { - for (const column of columns) { - const cmp = compareValues(left[column], right[column]); - if (cmp !== 0) return cmp; - } - return 0; -} - -function rowComparator(meta: KvQueryMeta): (left: Row, right: Row) => number { - const primaryKey = meta.table.primaryKey as ReadonlyArray; - if (!meta.orderBy) return (left, right) => compareColumns(primaryKey, left, right); - const column = meta.orderBy.col as string; - const columns = [column, ...primaryKey.slice(primaryKey.indexOf(column) + 1)]; - const direction = meta.orderBy.direction === 'DESC' ? -1 : 1; - return (left, right) => compareColumns(columns, left, right) * direction; -} - function projectRow(row: Row, columns: ReadonlyArray | undefined): Row { if (!columns) return cloneRow(row); const projected: Row = {}; diff --git a/fluxer_api/src/api/test/TestHarnessController.ts b/fluxer_api/src/api/test/TestHarnessController.ts index fb6e19cb3..de4bbd7c7 100644 --- a/fluxer_api/src/api/test/TestHarnessController.ts +++ b/fluxer_api/src/api/test/TestHarnessController.ts @@ -42,8 +42,8 @@ import {OAuth2TokenRepository} from '@app/api/oauth/repositories/OAuth2TokenRepo import {IpAuthorizationTokens, OAuth2AccessTokensByUser} from '@app/api/Tables'; import type {HonoApp, HonoEnv} from '@app/api/types/HonoEnv'; import {UserSearchRepository} from '@app/api/user/repositories/account/crud/UserSearchRepository'; +import type {UserDeletionScheduleUpdate} from '@app/api/user/repositories/account/UserAccountRepository'; import {AuthSessionRepository} from '@app/api/user/repositories/auth/AuthSessionRepository'; -import type {UserDeletionScheduleUpdate} from '@app/api/user/repositories/IUserAccountRepository'; import {UserChannelRepository} from '@app/api/user/repositories/UserChannelRepository'; import {UserRepository} from '@app/api/user/repositories/UserRepository'; import {processUserDeletion} from '@app/api/user/services/UserDeletionService'; @@ -76,7 +76,7 @@ import {RpcRequest} from '@fluxer/schema/src/domains/rpc/RpcSchemas'; import {createSnowflakeFromTimestamp, snowflakeToDate} from '@fluxer/snowflake/src/Snowflake'; import * as BucketUtils from '@fluxer/snowflake/src/SnowflakeBuckets'; import type {IEmailService} from '@pkgs/email/src/IEmailService'; -import type {ITestEmailService, SentEmailRecord} from '@pkgs/email/src/ITestEmailService'; +import type {SentEmailRecord, TestEmailService} from '@pkgs/email/src/TestEmailService'; import type {Context} from 'hono'; import {seconds} from 'itty-time'; @@ -179,8 +179,8 @@ function ensureHarnessAccess(ctx: Context) { } } -function isTestEmailService(service: IEmailService): service is ITestEmailService { - return typeof (service as ITestEmailService).listSentEmails === 'function'; +function isTestEmailService(service: IEmailService): service is TestEmailService { + return typeof (service as TestEmailService).listSentEmails === 'function'; } const serializeEmails = (emails: Array) => @@ -851,7 +851,12 @@ export function TestHarnessController(app: HonoApp) { throw new UnknownUserError(); } const stripeService = ctx.get('stripeService'); - await stripeService.extendSubscriptionWithGiftTrialDuration(user, durationType, durationQuantity, idempotencyKey); + await stripeService.subscriptions.extendSubscriptionWithGiftTrialDuration( + user, + durationType, + durationQuantity, + idempotencyKey, + ); return ctx.body(null, 204); }); app.post('/test/users/:userId/private-channels', async (ctx) => { @@ -940,7 +945,7 @@ export function TestHarnessController(app: HonoApp) { indexed_at: null, version: 1, }; - await channelRepository.upsert(channelRow); + await channelRepository.channelData.upsert(channelRow); await userRepository.openDmForUser(userId, channelId); dmChannels.push({ channel_id: channelId.toString(), @@ -983,7 +988,7 @@ export function TestHarnessController(app: HonoApp) { indexed_at: null, version: 1, }; - await channelRepository.upsert(channelRow); + await channelRepository.channelData.upsert(channelRow); await userRepository.openDmForUser(userId, channelId); groupDmChannels.push({ channel_id: channelId.toString(), @@ -1160,7 +1165,7 @@ export function TestHarnessController(app: HonoApp) { } const userId = createUserID(BigInt(userIdParam)); const channelRepository = new ChannelRepository(); - const messages = await channelRepository.listMessagesByAuthor(userId); + const messages = await channelRepository.messages.listMessagesByAuthor(userId); return ctx.json({count: messages.length}, 200); }); app.post('/test/worker/process-pending-deletions', async (ctx) => { @@ -1701,7 +1706,7 @@ export function TestHarnessController(app: HonoApp) { const channelId = createChannelID(BigInt(channelIdParam)); const channelRepository = new ChannelRepository(); const userRepository = new UserRepository(); - const channel = await channelRepository.findUnique(channelId); + const channel = await channelRepository.channelData.findUnique(channelId); if (!channel) { return ctx.json({error: 'Channel not found'}, 404); } @@ -1713,7 +1718,7 @@ export function TestHarnessController(app: HonoApp) { let lastMessageId: MessageID | undefined; let hasMore = true; while (hasMore) { - const messages = await channelRepository.listMessages(channelId, lastMessageId, BATCH_SIZE); + const messages = await channelRepository.messages.listMessages(channelId, lastMessageId, BATCH_SIZE); if (messages.length === 0) { hasMore = false; break; @@ -1735,7 +1740,7 @@ export function TestHarnessController(app: HonoApp) { } } } - await channelRepository.upsert({ + await channelRepository.channelData.upsert({ ...channel.toRow(), indexed_at: new Date(), }); diff --git a/fluxer_api/src/api/test/mocks/RepositoryBackedMessageResponseDataService.ts b/fluxer_api/src/api/test/mocks/RepositoryBackedMessageResponseDataService.ts index 4caf0d11d..21042ee87 100644 --- a/fluxer_api/src/api/test/mocks/RepositoryBackedMessageResponseDataService.ts +++ b/fluxer_api/src/api/test/mocks/RepositoryBackedMessageResponseDataService.ts @@ -321,7 +321,9 @@ export class RepositoryBackedMessageResponseDataService extends MessageResponseD } private async resolveMentionChannels(channelIds: Array): Promise> { - const channels = await Promise.all(channelIds.map((channelId) => getChannelRepository().findUnique(channelId))); + const channels = await Promise.all( + channelIds.map((channelId) => getChannelRepository().channelData.findUnique(channelId)), + ); return channels.flatMap((channel) => channel?.name ? [ diff --git a/fluxer_api/src/api/test/search/InMemorySearchProvider.ts b/fluxer_api/src/api/test/search/InMemorySearchProvider.ts index dcd5c3d6c..ae948e08b 100644 --- a/fluxer_api/src/api/test/search/InMemorySearchProvider.ts +++ b/fluxer_api/src/api/test/search/InMemorySearchProvider.ts @@ -1,12 +1,12 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import type {AdminAuditLog} from '@app/api/admin/IAdminRepository'; +import type {AdminAuditLog} from '@app/api/admin/AdminRepository'; import type {ChannelID, GuildID, MessageID, ReportID, UserID} from '@app/api/BrandedTypes'; import type {Guild} from '@app/api/models/Guild'; import type {GuildMember} from '@app/api/models/GuildMember'; import type {Message} from '@app/api/models/Message'; import type {User} from '@app/api/models/User'; -import type {IARSubmission} from '@app/api/report/IReportRepository'; +import type {IARSubmission} from '@app/api/report/ReportModels'; import {convertToSearchableAuditLog} from '@app/api/search/auditlog/AuditLogSearchSerializer'; import type {GuildDiscoveryContext} from '@app/api/search/guild/GuildSearchSerializer'; import {convertToSearchableGuild} from '@app/api/search/guild/GuildSearchSerializer'; diff --git a/fluxer_api/src/api/theme/tests/ThemeCssSizeLimit.test.ts b/fluxer_api/src/api/theme/tests/ThemeCssSizeLimit.test.ts index e4731b4a0..a1afaabb4 100644 --- a/fluxer_api/src/api/theme/tests/ThemeCssSizeLimit.test.ts +++ b/fluxer_api/src/api/theme/tests/ThemeCssSizeLimit.test.ts @@ -55,16 +55,6 @@ describe('Theme CSS size limits', () => { .execute(); expect(theme.id).toBeDefined(); }); - it('rejects CSS that exceeds the real 8MB limit through the full HTTP stack', async () => { - setThemeCssMaxBytesForTesting(undefined); - const user = await createTestAccount(harness); - const oversizedCss = 'a'.repeat(THEME_CSS_MAX_BYTES + 1); - await createBuilder(harness, user.token) - .post('/users/@me/themes') - .body({css: oversizedCss}) - .expect(HTTP_STATUS.BAD_REQUEST, 'FILE_SIZE_TOO_LARGE') - .execute(); - }); it('rejects CSS that exceeds the limit', async () => { const user = await createTestAccount(harness); const oversizedCss = 'a'.repeat(TEST_MAX_CSS_BYTES + 1); @@ -84,16 +74,6 @@ describe('Theme CSS size limits', () => { .execute(); expect(theme.id).toBeDefined(); }); - it('accepts CSS just under the limit', async () => { - const user = await createTestAccount(harness); - const nearMaxCss = 'a'.repeat(TEST_MAX_CSS_BYTES - 1); - const theme = await createBuilder(harness, user.token) - .post('/users/@me/themes') - .body({css: nearMaxCss}) - .expect(HTTP_STATUS.CREATED) - .execute(); - expect(theme.id).toBeDefined(); - }); it('rejects CSS whose byte length exceeds the limit even when the character count does not', async () => { const user = await createTestAccount(harness); const unicodeChar = '\u{1F600}'; diff --git a/fluxer_api/src/api/types/HonoEnv.ts b/fluxer_api/src/api/types/HonoEnv.ts index cb138e570..cbfb7aeb8 100644 --- a/fluxer_api/src/api/types/HonoEnv.ts +++ b/fluxer_api/src/api/types/HonoEnv.ts @@ -9,7 +9,7 @@ import type {DesktopHandoffService} from '@app/api/auth/services/DesktopHandoffS import type {SsoService} from '@app/api/auth/services/SsoService'; import type {ApplicationID, UserID} from '@app/api/BrandedTypes'; import type {IBlueskyOAuthService} from '@app/api/bluesky/IBlueskyOAuthService'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {ChannelRequestService} from '@app/api/channel/services/ChannelRequestService'; import type {ChannelService} from '@app/api/channel/services/ChannelService'; import type {MessageRequestService} from '@app/api/channel/services/message/MessageRequestService'; @@ -23,7 +23,7 @@ import type {FavoriteMemeRequestService} from '@app/api/favorite_meme/FavoriteMe import type {FavoriteMemeService} from '@app/api/favorite_meme/FavoriteMemeService'; import type {GatewayRequestService} from '@app/api/gateway/GatewayRequestService'; import type {GifService} from '@app/api/gif/GifService'; -import type {IGuildDiscoveryService} from '@app/api/guild/services/GuildDiscoveryService'; +import type {GuildDiscoveryService} from '@app/api/guild/services/GuildDiscoveryService'; import type {GuildService} from '@app/api/guild/services/GuildService'; import type {EmbedService} from '@app/api/infrastructure/EmbedService'; import type {EntityAssetService} from '@app/api/infrastructure/EntityAssetService'; @@ -49,8 +49,8 @@ import type {BotAuthService} from '@app/api/oauth/BotAuthService'; import type {OAuth2ApplicationsRequestService} from '@app/api/oauth/OAuth2ApplicationsRequestService'; import type {OAuth2RequestService} from '@app/api/oauth/OAuth2RequestService'; import type {OAuth2Service} from '@app/api/oauth/OAuth2Service'; -import type {IApplicationRepository} from '@app/api/oauth/repositories/IApplicationRepository'; -import type {IOAuth2TokenRepository} from '@app/api/oauth/repositories/IOAuth2TokenRepository'; +import type {ApplicationRepository} from '@app/api/oauth/repositories/ApplicationRepository'; +import type {OAuth2TokenRepository} from '@app/api/oauth/repositories/OAuth2TokenRepository'; import type {ReadStateRequestService} from '@app/api/read_state/ReadStateRequestService'; import type {ReadStateService} from '@app/api/read_state/ReadStateService'; import type {ReportRequestService} from '@app/api/report/ReportRequestService'; @@ -63,7 +63,7 @@ import type {AgeVerificationService} from '@app/api/stripe/services/AgeVerificat import type {ThemeService} from '@app/api/theme/ThemeService'; import type {EntranceSoundPlayService} from '@app/api/user/entrance_sound/EntranceSoundPlayService'; import type {EntranceSoundService} from '@app/api/user/entrance_sound/EntranceSoundService'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import type {EmailChangeService} from '@app/api/user/services/EmailChangeService'; import type {MfaBackupCodesChallengeService} from '@app/api/user/services/MfaBackupCodesChallengeService'; import type {PasswordChangeService} from '@app/api/user/services/PasswordChangeService'; @@ -126,7 +126,7 @@ export interface HonoEnv { channelRequestService: ChannelRequestService; threadService: ThreadService; messageRequestService: MessageRequestService; - channelRepository: IChannelRepository; + channelRepository: ChannelRepository; connectionService: ConnectionService; connectionRequestService: ConnectionRequestService; blueskyOAuthService: IBlueskyOAuthService; @@ -145,7 +145,7 @@ export interface HonoEnv { favoriteMemeRequestService: FavoriteMemeRequestService; gatewayService: IGatewayService; gatewayRequestService: GatewayRequestService; - discoveryService: IGuildDiscoveryService; + discoveryService: GuildDiscoveryService; guildService: GuildService; inviteService: InviteService; inviteRequestService: InviteRequestService; @@ -167,7 +167,7 @@ export interface HonoEnv { gifService: GifService; themeService: ThemeService; userCacheService: UserCacheService; - userRepository: IUserRepository; + userRepository: UserRepository; userService: UserService; userAccountRequestService: UserAccountRequestService; userAuthRequestService: UserAuthRequestService; @@ -186,8 +186,8 @@ export interface HonoEnv { oauth2Service: OAuth2Service; oauth2RequestService: OAuth2RequestService; oauth2ApplicationsRequestService: OAuth2ApplicationsRequestService; - applicationRepository: IApplicationRepository; - oauth2TokenRepository: IOAuth2TokenRepository; + applicationRepository: ApplicationRepository; + oauth2TokenRepository: OAuth2TokenRepository; botAuthService: BotAuthService; sudoModeValid: boolean; captchaVerified?: boolean; diff --git a/fluxer_api/src/api/user/IUserRepository.ts b/fluxer_api/src/api/user/IUserRepository.ts deleted file mode 100644 index bd74efa80..000000000 --- a/fluxer_api/src/api/user/IUserRepository.ts +++ /dev/null @@ -1,5 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {IUserRepositoryAggregate} from '@app/api/user/repositories/IUserRepositoryAggregate'; - -export interface IUserRepository extends IUserRepositoryAggregate {} diff --git a/fluxer_api/src/api/user/NewConversationLimit.ts b/fluxer_api/src/api/user/NewConversationLimit.ts index 649fb0a3e..1606b8245 100644 --- a/fluxer_api/src/api/user/NewConversationLimit.ts +++ b/fluxer_api/src/api/user/NewConversationLimit.ts @@ -1,12 +1,12 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {createMessageID, type UserID} from '@app/api/BrandedTypes'; -import type {IMessageRepository} from '@app/api/channel/repositories/IMessageRepository'; +import type {MessageRepository} from '@app/api/channel/repositories/MessageRepository'; import {getCacheService, getChannelRepository} from '@app/api/middleware/ServiceSingletons'; import type {Channel} from '@app/api/models/Channel'; import type {User} from '@app/api/models/User'; import {isAccountLimitExempt} from '@app/api/user/AccountLimit'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants'; import {RelationshipTypes} from '@fluxer/constants/src/UserConstants'; import {NewConversationsLimitedError} from '@fluxer/errors/src/domains/user/NewConversationsLimitedError'; @@ -80,8 +80,8 @@ export function oneToOneDmRecipient(channel: Pick; -type ConversationMessages = Pick; +type ConversationUsers = Pick; +type ConversationMessages = Pick; export interface NewConversationCheck { user: Pick; diff --git a/fluxer_api/src/api/user/UniqueUsernames.ts b/fluxer_api/src/api/user/UniqueUsernames.ts index 2765dd391..3096a91ac 100644 --- a/fluxer_api/src/api/user/UniqueUsernames.ts +++ b/fluxer_api/src/api/user/UniqueUsernames.ts @@ -3,7 +3,7 @@ import {randomInt} from 'node:crypto'; import type {UserID} from '@app/api/BrandedTypes'; import type {User} from '@app/api/models/User'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {USERNAME_MODE_DISCRIMINATOR} from '@app/api/user/UserTag'; import {DELETED_USER_USERNAME, UserFlags} from '@fluxer/constants/src/UserConstants'; import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes'; @@ -11,7 +11,7 @@ import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidat import type {ICacheService} from '@pkgs/cache/src/ICacheService'; import {ms} from 'itty-time'; -type UsernameLookup = Pick; +type UsernameLookup = Pick; interface UniqueUsernameDeps { users: UsernameLookup; diff --git a/fluxer_api/src/api/user/entrance_sound/EntranceSoundPlayService.ts b/fluxer_api/src/api/user/entrance_sound/EntranceSoundPlayService.ts index 295c7d625..470e6f01c 100644 --- a/fluxer_api/src/api/user/entrance_sound/EntranceSoundPlayService.ts +++ b/fluxer_api/src/api/user/entrance_sound/EntranceSoundPlayService.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {type ChannelID, createUserID, type EntranceSoundID, type UserID} from '@app/api/BrandedTypes'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import {Logger} from '@app/api/Logger'; import type {EntranceSoundService} from '@app/api/user/entrance_sound/EntranceSoundService'; @@ -18,12 +18,12 @@ export class EntranceSoundPlayService { constructor( private readonly entranceSoundService: EntranceSoundService, private readonly gatewayService: IGatewayService, - private readonly channelRepository: IChannelRepository, + private readonly channelRepository: ChannelRepository, ) {} async play(params: PlayEntranceSoundParams): Promise { const {userId, channelId, soundId} = params; - const channel = await this.channelRepository.findUnique(channelId); + const channel = await this.channelRepository.channelData.findUnique(channelId); const guildId = channel?.guildId ?? undefined; const voiceStates = channel ? (await this.gatewayService.getVoiceStatesForChannel({guildId, channelId})).voiceStates diff --git a/fluxer_api/src/api/user/repositories/IUserAccountRepository.ts b/fluxer_api/src/api/user/repositories/IUserAccountRepository.ts deleted file mode 100644 index d0654e611..000000000 --- a/fluxer_api/src/api/user/repositories/IUserAccountRepository.ts +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {GuildID, UserID} from '@app/api/BrandedTypes'; -import type {UserRow} from '@app/api/database/types/UserTypes'; -import type {User} from '@app/api/models/User'; - -export interface UserDeletionScheduleUpdate { - pending_deletion_at: Date | null; - flags?: bigint; - deletion_reason_code?: number | null; - deletion_public_reason?: string | null; - deletion_audit_log_reason?: string | null; - deletion_scheduled_by?: UserID | null; - deletion_scheduled_at?: Date | null; - temp_banned_until?: Date | null; - first_refund_at?: Date | null; -} - -export interface IUserAccountRepository { - create(data: UserRow): Promise; - upsert(data: UserRow, oldData?: UserRow | null): Promise; - patchUpsert(userId: UserID, patchData: Partial, oldData?: UserRow | null): Promise; - compareAndSetFlags(user: User, flags: bigint): Promise; - updateFlags(userId: UserID, mutate: (flags: bigint) => bigint): Promise; - updateDeletionSchedule(user: User, patch: UserDeletionScheduleUpdate): Promise; - startDeletion(userId: UserID, pendingDeletionAt: Date): Promise; - anonymizeForDeletion(user: User, patch: Partial): Promise; - completeDeletion(user: User): Promise; - findUnique(userId: UserID): Promise; - findUniqueAssert(userId: UserID): Promise; - findByUsernameDiscriminator(username: string, discriminator: number): Promise; - findDiscriminatorsByUsername(username: string): Promise>; - findUsersByUsername(username: string): Promise>; - findByEmail(email: string): Promise; - findByStripeSubscriptionId(stripeSubscriptionId: string): Promise; - findByStripeCustomerId(stripeCustomerId: string): Promise; - listUserIdsByLastActiveIp( - lastActiveIp: string, - limit: number, - offset: number, - ): Promise<{ - userIds: Array; - total: number; - }>; - listUsers(userIds: Array): Promise>; - scanAllUsersPage( - limit: number, - pageState?: string | null, - ): Promise<{ - users: Array; - pageState: string | null; - }>; - getUserGuildIds(userId: UserID): Promise>; - addPendingDeletion(userId: UserID, pendingDeletionAt: Date, deletionReasonCode: number): Promise; - removePendingDeletion(userId: UserID, pendingDeletionAt: Date): Promise; - findUsersPendingDeletionByDate(deletionDate: string): Promise< - Array<{ - user_id: bigint; - deletion_reason_code: number; - }> - >; - scheduleDeletion(userId: UserID, pendingDeletionAt: Date, deletionReasonCode: number): Promise; - deleteUserSecondaryIndices(userId: UserID): Promise; - removeFromAllGuilds(userId: UserID): Promise; - updateLastActiveAt(params: {userId: UserID; lastActiveAt: Date; lastActiveIp?: string}): Promise; - updateSubscriptionStatus( - userId: UserID, - updates: { - premiumWillCancel: boolean; - computedPremiumUntil: Date | null; - periodStart: Date | null; - }, - ): Promise<{ - finalVersion: number | null; - }>; - deleteAllPasswordResetTokens(userId: UserID): Promise; -} diff --git a/fluxer_api/src/api/user/repositories/IUserAuthRepository.ts b/fluxer_api/src/api/user/repositories/IUserAuthRepository.ts deleted file mode 100644 index 490faeea3..000000000 --- a/fluxer_api/src/api/user/repositories/IUserAuthRepository.ts +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {UserID} from '@app/api/BrandedTypes'; -import type { - AuthSessionRow, - EmailRevertTokenRow, - EmailVerificationTokenRow, - PasswordResetTokenRow, -} from '@app/api/database/types/AuthTypes'; -import type {AuthSession, AuthSessionTombstone} from '@app/api/models/AuthSession'; -import type {EmailRevertToken} from '@app/api/models/EmailRevertToken'; -import type {EmailVerificationToken} from '@app/api/models/EmailVerificationToken'; -import type {MfaBackupCode} from '@app/api/models/MfaBackupCode'; -import type {PasswordResetToken} from '@app/api/models/PasswordResetToken'; -import type {WebAuthnCredential} from '@app/api/models/WebAuthnCredential'; - -export interface IUserAuthRepository { - listAuthSessions(userId: UserID): Promise>; - listAuthSessionTombstones(userId: UserID): Promise>; - getAuthSessionByToken(sessionIdHash: Buffer): Promise; - createAuthSession(sessionData: AuthSessionRow): Promise; - updateAuthSessionLastUsed(sessionIdHash: Buffer): Promise; - deleteAuthSessions(userId: UserID, sessionIdHashes: Array): Promise; - deleteAllAuthSessions(userId: UserID): Promise; - listMfaBackupCodes(userId: UserID): Promise>; - createMfaBackupCodes(userId: UserID, codes: Array): Promise>; - clearMfaBackupCodes(userId: UserID): Promise; - consumeMfaBackupCode(userId: UserID, code: string): Promise; - deleteAllMfaBackupCodes(userId: UserID): Promise; - getEmailVerificationToken(token: string): Promise; - createEmailVerificationToken(tokenData: EmailVerificationTokenRow): Promise; - deleteEmailVerificationToken(token: string): Promise; - getPasswordResetToken(token: string): Promise; - createPasswordResetToken(tokenData: PasswordResetTokenRow): Promise; - deleteAllPasswordResetTokens(userId: UserID): Promise; - getEmailRevertToken(token: string): Promise; - createEmailRevertToken(tokenData: EmailRevertTokenRow): Promise; - deleteEmailRevertToken(token: string): Promise; - checkIpAuthorized(userId: UserID, ip: string): Promise; - createAuthorizedIp(userId: UserID, ip: string): Promise; - createIpAuthorizationToken(userId: UserID, token: string, email: string): Promise; - authorizeIpByToken(token: string): Promise<{ - userId: UserID; - email: string; - } | null>; - getAuthorizedIps(userId: UserID): Promise< - Array<{ - ip: string; - }> - >; - deleteAllAuthorizedIps(userId: UserID): Promise; - listWebAuthnCredentials(userId: UserID): Promise>; - getWebAuthnCredential(userId: UserID, credentialId: string): Promise; - createWebAuthnCredential( - userId: UserID, - credentialId: string, - publicKey: Buffer, - counter: bigint, - transports: Set | null, - name: string, - rpId: string | null, - ): Promise; - updateWebAuthnCredentialCounter(userId: UserID, credentialId: string, counter: bigint): Promise; - updateWebAuthnCredentialLastUsed(userId: UserID, credentialId: string): Promise; - updateWebAuthnCredentialName(userId: UserID, credentialId: string, name: string): Promise; - setWebAuthnCredentialSupersededBy(userId: UserID, credentialId: string, supersededBy: string): Promise; - deleteWebAuthnCredential(userId: UserID, credentialId: string): Promise; - getUserIdByCredentialId(credentialId: string): Promise; - deleteAllWebAuthnCredentials(userId: UserID): Promise; -} diff --git a/fluxer_api/src/api/user/repositories/IUserChannelRepository.ts b/fluxer_api/src/api/user/repositories/IUserChannelRepository.ts deleted file mode 100644 index d250db69a..000000000 --- a/fluxer_api/src/api/user/repositories/IUserChannelRepository.ts +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {ChannelID, MessageID, UserID} from '@app/api/BrandedTypes'; -import type {Channel} from '@app/api/models/Channel'; - -export interface PrivateChannelSummary { - channelId: ChannelID; - isGroupDm: boolean; - channelType: number | null; - lastMessageId: MessageID | null; - open: boolean; -} - -export interface ListHistoricalDmChannelOptions { - limit: number; - beforeChannelId?: ChannelID; - afterChannelId?: ChannelID; -} - -export interface HistoricalDmChannelSummary { - channelId: ChannelID; - channelType: number | null; - recipientIds: Array; - lastMessageId: MessageID | null; - open: boolean; -} - -export interface IUserChannelRepository { - listPrivateChannels(userId: UserID): Promise>; - deleteAllPrivateChannels(userId: UserID): Promise; - listPrivateChannelSummaries(userId: UserID): Promise>; - listHistoricalDmChannelIds(userId: UserID): Promise>; - listHistoricalDmChannelsPaginated( - userId: UserID, - options: ListHistoricalDmChannelOptions, - ): Promise>; - recordHistoricalDmChannel(userId: UserID, channelId: ChannelID, isGroupDm: boolean): Promise; - findExistingDmState(user1Id: UserID, user2Id: UserID): Promise; - createDmChannelAndState(user1Id: UserID, user2Id: UserID, channelId: ChannelID): Promise; - createLocalOnlyDmChannel(ownerId: UserID, recipientId: UserID, channelId: ChannelID): Promise; - isDmChannelOpen(userId: UserID, channelId: ChannelID): Promise; - openDmForUser(userId: UserID, channelId: ChannelID, isGroupDm?: boolean): Promise; - openPrivateChannelForUser(userId: UserID, channel: Channel): Promise; - closeDmForUser(userId: UserID, channelId: ChannelID): Promise; - getPinnedDms(userId: UserID): Promise>; - getPinnedDmsWithDetails(userId: UserID): Promise< - Array<{ - channel_id: ChannelID; - sort_order: number; - }> - >; - addPinnedDm(userId: UserID, channelId: ChannelID): Promise>; - removePinnedDm(userId: UserID, channelId: ChannelID): Promise>; - deletePinnedDmsByUserId(userId: UserID): Promise; - deleteAllReadStates(userId: UserID): Promise; -} diff --git a/fluxer_api/src/api/user/repositories/IUserContentRepository.ts b/fluxer_api/src/api/user/repositories/IUserContentRepository.ts deleted file mode 100644 index cec532ff0..000000000 --- a/fluxer_api/src/api/user/repositories/IUserContentRepository.ts +++ /dev/null @@ -1,83 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {ChannelID, MessageID, UserID} from '@app/api/BrandedTypes'; -import type {ExactRow} from '@app/api/database/types/DatabaseRowTypes'; -import type {GiftCodeRow, PaymentBySubscriptionRow, PaymentRow} from '@app/api/database/types/PaymentTypes'; -import type {PushSubscriptionRow, RecentMentionRow} from '@app/api/database/types/UserTypes'; -import type {GiftCode} from '@app/api/models/GiftCode'; -import type {Payment} from '@app/api/models/Payment'; -import type {PushSubscription} from '@app/api/models/PushSubscription'; -import type {RecentMention} from '@app/api/models/RecentMention'; -import type {SavedMessage} from '@app/api/models/SavedMessage'; -import type {VisionarySlot} from '@app/api/models/VisionarySlot'; - -export interface IUserContentRepository { - getRecentMention(userId: UserID, messageId: MessageID): Promise; - listRecentMentions( - userId: UserID, - includeEveryone: boolean, - includeRole: boolean, - includeGuilds: boolean, - limit: number, - before?: MessageID, - ): Promise>; - createRecentMentions(mentions: Array>): Promise; - deleteRecentMention(mention: RecentMention): Promise; - deleteRecentMentions(mentions: Array): Promise; - deleteAllRecentMentions(userId: UserID): Promise; - listSavedMessages(userId: UserID, limit?: number, before?: MessageID): Promise>; - countSavedMessages(userId: UserID): Promise; - createSavedMessage(userId: UserID, channelId: ChannelID, messageId: MessageID): Promise; - deleteSavedMessage(userId: UserID, messageId: MessageID): Promise; - deleteAllSavedMessages(userId: UserID): Promise; - createGiftCode(data: ExactRow): Promise; - findGiftCode(code: string): Promise; - findGiftCodeByPaymentIntent(paymentIntentId: string): Promise; - findGiftCodesByCreator(userId: UserID): Promise>; - findGiftCodesByRedeemer(userId: UserID): Promise>; - redeemGiftCode(code: string, userId: UserID): Promise; - unredeemGiftCode(code: string, userId: UserID): Promise; - revokeGiftCode(code: string): Promise; - unrevokeGiftCode(code: string): Promise; - markGiftPremiumReversed(gift: GiftCode, seconds: number): Promise; - clearGiftPremiumReversed(code: string, seconds: number): Promise; - linkGiftCodeToCheckoutSession(code: string, checkoutSessionId: string): Promise; - listPushSubscriptions(userId: UserID): Promise>; - createPushSubscription(data: ExactRow): Promise; - deletePushSubscription(userId: UserID, subscriptionId: string): Promise; - deletePushSubscriptionsForAuthSessions( - userId: UserID, - authSessionIdHashes: Array, - options: {deleteUnboundSubscriptions: boolean}, - ): Promise; - getBulkPushSubscriptions(userIds: Array): Promise>>; - deleteAllPushSubscriptions(userId: UserID): Promise; - createPayment(data: { - checkout_session_id: string; - user_id: UserID; - price_id: string; - product_type: string; - status: string; - is_gift: boolean; - created_at: Date; - purchase_geoip_country_code?: string | null; - purchase_client_country_code?: string | null; - eu_withdrawal_waiver_required?: boolean; - eu_withdrawal_waiver_accepted?: boolean; - eu_withdrawal_waiver_accepted_at?: Date | null; - eu_withdrawal_waiver_text_version?: string | null; - }): Promise; - updatePayment( - data: Partial & { - checkout_session_id: string; - }, - ): Promise; - findPaymentsByUserId(userId: UserID): Promise>; - getPaymentByCheckoutSession(checkoutSessionId: string): Promise; - getPaymentByPaymentIntent(paymentIntentId: string): Promise; - getSubscriptionInfo(subscriptionId: string): Promise; - listVisionarySlots(): Promise>; - expandVisionarySlots(byCount: number): Promise; - reserveVisionarySlot(slotIndex: number, userId: UserID): Promise; - unreserveVisionarySlot(slotIndex: number, userId: UserID): Promise; -} diff --git a/fluxer_api/src/api/user/repositories/IUserRelationshipRepository.ts b/fluxer_api/src/api/user/repositories/IUserRelationshipRepository.ts deleted file mode 100644 index 15c11332e..000000000 --- a/fluxer_api/src/api/user/repositories/IUserRelationshipRepository.ts +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {UserID} from '@app/api/BrandedTypes'; -import type {RelationshipRow} from '@app/api/database/types/UserTypes'; -import type {Relationship} from '@app/api/models/Relationship'; -import type {UserNote} from '@app/api/models/UserNote'; - -export interface IUserRelationshipRepository { - listRelationships(sourceUserId: UserID): Promise>; - listBlockedUserIds(sourceUserId: UserID): Promise>; - listIncomingRequests(userId: UserID): Promise>; - hasReachedRelationshipLimit(sourceUserId: UserID, limit: number): Promise; - getRelationship(sourceUserId: UserID, targetUserId: UserID, type: number): Promise; - upsertRelationship(relationship: RelationshipRow): Promise; - bulkUpdateFriendShareVoiceActivity(sourceUserId: UserID, value: boolean): Promise>; - deleteRelationship(sourceUserId: UserID, targetUserId: UserID, type: number): Promise; - deleteAllRelationships(userId: UserID): Promise; - getUserNote(sourceUserId: UserID, targetUserId: UserID): Promise; - getUserNotes(sourceUserId: UserID): Promise>; - upsertUserNote(sourceUserId: UserID, targetUserId: UserID, note: string): Promise; - clearUserNote(sourceUserId: UserID, targetUserId: UserID): Promise; - deleteAllNotes(userId: UserID): Promise; -} diff --git a/fluxer_api/src/api/user/repositories/IUserRepositoryAggregate.ts b/fluxer_api/src/api/user/repositories/IUserRepositoryAggregate.ts deleted file mode 100644 index 953a66cfc..000000000 --- a/fluxer_api/src/api/user/repositories/IUserRepositoryAggregate.ts +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {IUserAccountRepository} from '@app/api/user/repositories/IUserAccountRepository'; -import type {IUserAuthRepository} from '@app/api/user/repositories/IUserAuthRepository'; -import type {IUserChannelRepository} from '@app/api/user/repositories/IUserChannelRepository'; -import type {IUserContentRepository} from '@app/api/user/repositories/IUserContentRepository'; -import type {IUserRelationshipRepository} from '@app/api/user/repositories/IUserRelationshipRepository'; -import type {IUserSettingsRepository} from '@app/api/user/repositories/IUserSettingsRepository'; - -export interface IUserRepositoryAggregate - extends IUserAccountRepository, - IUserAuthRepository, - IUserSettingsRepository, - IUserRelationshipRepository, - IUserChannelRepository, - IUserContentRepository {} diff --git a/fluxer_api/src/api/user/repositories/IUserSettingsRepository.ts b/fluxer_api/src/api/user/repositories/IUserSettingsRepository.ts deleted file mode 100644 index dfeb84984..000000000 --- a/fluxer_api/src/api/user/repositories/IUserSettingsRepository.ts +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {GuildID, UserID} from '@app/api/BrandedTypes'; -import type {ExactRow} from '@app/api/database/types/DatabaseRowTypes'; -import type {UserGuildSettingsRow, UserSettingsRow} from '@app/api/database/types/UserTypes'; -import type {UserGuildSettings} from '@app/api/models/UserGuildSettings'; -import type {UserSettings} from '@app/api/models/UserSettings'; - -export interface IUserSettingsRepository { - findSettings(userId: UserID): Promise; - upsertSettings(settings: ExactRow): Promise; - deleteUserSettings(userId: UserID): Promise; - findGuildSettings(userId: UserID, guildId: GuildID | null): Promise; - findAllGuildSettings(userId: UserID): Promise>; - upsertGuildSettings(settings: ExactRow): Promise; - deleteGuildSettings(userId: UserID, guildId: GuildID): Promise; - deleteAllUserGuildSettings(userId: UserID): Promise; -} diff --git a/fluxer_api/src/api/user/repositories/UserAccountRepository.ts b/fluxer_api/src/api/user/repositories/UserAccountRepository.ts deleted file mode 100644 index d968a0c1c..000000000 --- a/fluxer_api/src/api/user/repositories/UserAccountRepository.ts +++ /dev/null @@ -1,190 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {GuildID, UserID} from '@app/api/BrandedTypes'; -import type {UserRow} from '@app/api/database/types/UserTypes'; -import type {User} from '@app/api/models/User'; -import {UserEmailOwnershipRepository} from '@app/api/user/repositories/account/crud/UserEmailOwnershipRepository'; -import {UserAccountRepository as UserAccountCrudRepository} from '@app/api/user/repositories/account/UserAccountRepository'; -import {UserDeletionRepository} from '@app/api/user/repositories/account/UserDeletionRepository'; -import {UserGuildRepository} from '@app/api/user/repositories/account/UserGuildRepository'; -import {UserLookupRepository} from '@app/api/user/repositories/account/UserLookupRepository'; -import {TokenRepository} from '@app/api/user/repositories/auth/TokenRepository'; -import type { - IUserAccountRepository, - UserDeletionScheduleUpdate, -} from '@app/api/user/repositories/IUserAccountRepository'; -import type {IKVProvider} from '@pkgs/kv_client/src/IKVProvider'; - -export class UserAccountRepository implements IUserAccountRepository { - private accountRepo: UserAccountCrudRepository; - private lookupRepo: UserLookupRepository; - private deletionRepo: UserDeletionRepository; - private guildRepo: UserGuildRepository; - private tokenRepo: TokenRepository; - - constructor(kv: IKVProvider) { - this.accountRepo = new UserAccountCrudRepository(kv); - this.lookupRepo = new UserLookupRepository( - this.accountRepo.findUnique.bind(this.accountRepo), - new UserEmailOwnershipRepository(this.accountRepo.findUnique.bind(this.accountRepo), kv), - ); - this.deletionRepo = new UserDeletionRepository(); - this.guildRepo = new UserGuildRepository(); - this.tokenRepo = new TokenRepository(); - } - - async create(data: UserRow): Promise { - return this.accountRepo.create(data); - } - - async findUnique(userId: UserID): Promise { - return this.accountRepo.findUnique(userId); - } - - async findUniqueAssert(userId: UserID): Promise { - return this.accountRepo.findUniqueAssert(userId); - } - - async scanAllUsersPage( - limit: number, - pageState?: string | null, - ): Promise<{ - users: Array; - pageState: string | null; - }> { - return this.accountRepo.scanAllUsersPage(limit, pageState); - } - - async listUsers(userIds: Array): Promise> { - return this.accountRepo.listUsers(userIds); - } - - async upsert(data: UserRow, oldData?: UserRow | null): Promise { - return this.accountRepo.upsert(data, oldData); - } - - async patchUpsert(userId: UserID, patchData: Partial, oldData?: UserRow | null): Promise { - return this.accountRepo.patchUpsert(userId, patchData, oldData); - } - - async compareAndSetFlags(user: User, flags: bigint): Promise { - return this.accountRepo.compareAndSetFlags(user, flags); - } - - async updateFlags(userId: UserID, mutate: (flags: bigint) => bigint): Promise { - return this.accountRepo.updateFlags(userId, mutate); - } - - async updateDeletionSchedule(user: User, patch: UserDeletionScheduleUpdate): Promise { - return this.accountRepo.updateDeletionSchedule(user, patch); - } - - async startDeletion(userId: UserID, pendingDeletionAt: Date): Promise { - return this.accountRepo.startDeletion(userId, pendingDeletionAt); - } - - async anonymizeForDeletion(user: User, patch: Partial): Promise { - return this.accountRepo.anonymizeForDeletion(user, patch); - } - - async completeDeletion(user: User): Promise { - return this.accountRepo.completeDeletion(user); - } - - async deleteUserSecondaryIndices(userId: UserID): Promise { - return this.accountRepo.deleteUserSecondaryIndices(userId); - } - - async findByEmail(email: string): Promise { - return this.lookupRepo.findByEmail(email); - } - - async findByStripeCustomerId(stripeCustomerId: string): Promise { - return this.lookupRepo.findByStripeCustomerId(stripeCustomerId); - } - - async findByStripeSubscriptionId(stripeSubscriptionId: string): Promise { - return this.lookupRepo.findByStripeSubscriptionId(stripeSubscriptionId); - } - - async listUserIdsByLastActiveIp( - lastActiveIp: string, - limit: number, - offset: number, - ): Promise<{ - userIds: Array; - total: number; - }> { - return this.lookupRepo.listUserIdsByLastActiveIp(lastActiveIp, limit, offset); - } - - async findByUsernameDiscriminator(username: string, discriminator: number): Promise { - return this.lookupRepo.findByUsernameDiscriminator(username, discriminator); - } - - async findDiscriminatorsByUsername(username: string): Promise> { - return this.lookupRepo.findDiscriminatorsByUsername(username); - } - - async findUsersByUsername(username: string): Promise> { - return this.lookupRepo.findUsersByUsername(username); - } - - async getActivityTracking(userId: UserID): Promise<{ - last_active_at: Date | null; - last_active_ip: string | null; - }> { - const result = await this.accountRepo.getActivityTracking(userId); - return result ?? {last_active_at: null, last_active_ip: null}; - } - - async addPendingDeletion(userId: UserID, pendingDeletionAt: Date, deletionReasonCode: number): Promise { - return this.deletionRepo.addPendingDeletion(userId, pendingDeletionAt, deletionReasonCode); - } - - async findUsersPendingDeletionByDate(deletionDate: string): Promise< - Array<{ - user_id: bigint; - deletion_reason_code: number; - }> - > { - return this.deletionRepo.findUsersPendingDeletionByDate(deletionDate); - } - - async removePendingDeletion(userId: UserID, pendingDeletionAt: Date): Promise { - return this.deletionRepo.removePendingDeletion(userId, pendingDeletionAt); - } - - async scheduleDeletion(userId: UserID, pendingDeletionAt: Date, deletionReasonCode: number): Promise { - return this.deletionRepo.scheduleDeletion(userId, pendingDeletionAt, deletionReasonCode); - } - - async getUserGuildIds(userId: UserID): Promise> { - return this.guildRepo.getUserGuildIds(userId); - } - - async removeFromAllGuilds(userId: UserID): Promise { - return this.guildRepo.removeFromAllGuilds(userId); - } - - async updateLastActiveAt(params: {userId: UserID; lastActiveAt: Date; lastActiveIp?: string}): Promise { - return this.accountRepo.updateLastActiveAt(params); - } - - async updateSubscriptionStatus( - userId: UserID, - updates: { - premiumWillCancel: boolean; - computedPremiumUntil: Date | null; - periodStart: Date | null; - }, - ): Promise<{ - finalVersion: number | null; - }> { - return this.accountRepo.updateSubscriptionStatus(userId, updates); - } - - async deleteAllPasswordResetTokens(userId: UserID): Promise { - return this.tokenRepo.deleteAllPasswordResetTokens(userId); - } -} diff --git a/fluxer_api/src/api/user/repositories/UserAuthRepository.ts b/fluxer_api/src/api/user/repositories/UserAuthRepository.ts deleted file mode 100644 index 0a7fc336d..000000000 --- a/fluxer_api/src/api/user/repositories/UserAuthRepository.ts +++ /dev/null @@ -1,211 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {UserID} from '@app/api/BrandedTypes'; -import type { - AuthSessionRow, - EmailRevertTokenRow, - EmailVerificationTokenRow, - PasswordResetTokenRow, -} from '@app/api/database/types/AuthTypes'; -import type {AuthSession, AuthSessionTombstone} from '@app/api/models/AuthSession'; -import type {EmailRevertToken} from '@app/api/models/EmailRevertToken'; -import type {EmailVerificationToken} from '@app/api/models/EmailVerificationToken'; -import type {MfaBackupCode} from '@app/api/models/MfaBackupCode'; -import type {PasswordResetToken} from '@app/api/models/PasswordResetToken'; -import type {WebAuthnCredential} from '@app/api/models/WebAuthnCredential'; -import {AuthSessionRepository} from '@app/api/user/repositories/auth/AuthSessionRepository'; -import {IpAuthorizationRepository} from '@app/api/user/repositories/auth/IpAuthorizationRepository'; -import {MfaBackupCodeRepository} from '@app/api/user/repositories/auth/MfaBackupCodeRepository'; -import {TokenRepository} from '@app/api/user/repositories/auth/TokenRepository'; -import {WebAuthnRepository} from '@app/api/user/repositories/auth/WebAuthnRepository'; -import type {IUserAccountRepository} from '@app/api/user/repositories/IUserAccountRepository'; -import type {IUserAuthRepository} from '@app/api/user/repositories/IUserAuthRepository'; - -export class UserAuthRepository implements IUserAuthRepository { - private authSessionRepository: AuthSessionRepository; - private mfaBackupCodeRepository: MfaBackupCodeRepository; - private tokenRepository: TokenRepository; - private ipAuthorizationRepository: IpAuthorizationRepository; - private webAuthnRepository: WebAuthnRepository; - - constructor(userAccountRepository: IUserAccountRepository) { - this.authSessionRepository = new AuthSessionRepository(); - this.mfaBackupCodeRepository = new MfaBackupCodeRepository(); - this.tokenRepository = new TokenRepository(); - this.ipAuthorizationRepository = new IpAuthorizationRepository(userAccountRepository); - this.webAuthnRepository = new WebAuthnRepository(); - } - - async listAuthSessions(userId: UserID): Promise> { - return this.authSessionRepository.listAuthSessions(userId); - } - - async listAuthSessionTombstones(userId: UserID): Promise> { - return this.authSessionRepository.listAuthSessionTombstones(userId); - } - - async getAuthSessionByToken(sessionIdHash: Buffer): Promise { - return this.authSessionRepository.getAuthSessionByToken(sessionIdHash); - } - - async createAuthSession(sessionData: AuthSessionRow): Promise { - return this.authSessionRepository.createAuthSession(sessionData); - } - - async updateAuthSessionLastUsed(sessionIdHash: Buffer): Promise { - const session = await this.getAuthSessionByToken(sessionIdHash); - if (!session) return; - await this.authSessionRepository.updateAuthSessionLastUsed(sessionIdHash); - } - - async deleteAuthSessions(userId: UserID, sessionIdHashes: Array): Promise { - return this.authSessionRepository.deleteAuthSessions(userId, sessionIdHashes); - } - - async deleteAllAuthSessions(userId: UserID): Promise { - return this.authSessionRepository.deleteAllAuthSessions(userId); - } - - async listMfaBackupCodes(userId: UserID): Promise> { - return this.mfaBackupCodeRepository.listMfaBackupCodes(userId); - } - - async createMfaBackupCodes(userId: UserID, codes: Array): Promise> { - return this.mfaBackupCodeRepository.createMfaBackupCodes(userId, codes); - } - - async clearMfaBackupCodes(userId: UserID): Promise { - return this.mfaBackupCodeRepository.clearMfaBackupCodes(userId); - } - - async consumeMfaBackupCode(userId: UserID, code: string): Promise { - return this.mfaBackupCodeRepository.consumeMfaBackupCode(userId, code); - } - - async deleteAllMfaBackupCodes(userId: UserID): Promise { - return this.mfaBackupCodeRepository.deleteAllMfaBackupCodes(userId); - } - - async getEmailVerificationToken(token: string): Promise { - return this.tokenRepository.getEmailVerificationToken(token); - } - - async createEmailVerificationToken(tokenData: EmailVerificationTokenRow): Promise { - return this.tokenRepository.createEmailVerificationToken(tokenData); - } - - async deleteEmailVerificationToken(token: string): Promise { - return this.tokenRepository.deleteEmailVerificationToken(token); - } - - async getPasswordResetToken(token: string): Promise { - return this.tokenRepository.getPasswordResetToken(token); - } - - async createPasswordResetToken(tokenData: PasswordResetTokenRow): Promise { - return this.tokenRepository.createPasswordResetToken(tokenData); - } - - async deleteAllPasswordResetTokens(userId: UserID): Promise { - return this.tokenRepository.deleteAllPasswordResetTokens(userId); - } - - async getEmailRevertToken(token: string): Promise { - return this.tokenRepository.getEmailRevertToken(token); - } - - async createEmailRevertToken(tokenData: EmailRevertTokenRow): Promise { - return this.tokenRepository.createEmailRevertToken(tokenData); - } - - async deleteEmailRevertToken(token: string): Promise { - return this.tokenRepository.deleteEmailRevertToken(token); - } - - async checkIpAuthorized(userId: UserID, ip: string): Promise { - return this.ipAuthorizationRepository.checkIpAuthorized(userId, ip); - } - - async createAuthorizedIp(userId: UserID, ip: string): Promise { - return this.ipAuthorizationRepository.createAuthorizedIp(userId, ip); - } - - async createIpAuthorizationToken(userId: UserID, token: string, email: string): Promise { - return this.ipAuthorizationRepository.createIpAuthorizationToken(userId, token, email); - } - - async authorizeIpByToken(token: string): Promise<{ - userId: UserID; - email: string; - } | null> { - return this.ipAuthorizationRepository.authorizeIpByToken(token); - } - - async getAuthorizedIps(userId: UserID): Promise< - Array<{ - ip: string; - }> - > { - return this.ipAuthorizationRepository.getAuthorizedIps(userId); - } - - async deleteAllAuthorizedIps(userId: UserID): Promise { - return this.ipAuthorizationRepository.deleteAllAuthorizedIps(userId); - } - - async listWebAuthnCredentials(userId: UserID): Promise> { - return this.webAuthnRepository.listWebAuthnCredentials(userId); - } - - async getWebAuthnCredential(userId: UserID, credentialId: string): Promise { - return this.webAuthnRepository.getWebAuthnCredential(userId, credentialId); - } - - async createWebAuthnCredential( - userId: UserID, - credentialId: string, - publicKey: Buffer, - counter: bigint, - transports: Set | null, - name: string, - rpId: string | null, - ): Promise { - return this.webAuthnRepository.createWebAuthnCredential( - userId, - credentialId, - publicKey, - counter, - transports, - name, - rpId, - ); - } - - async updateWebAuthnCredentialCounter(userId: UserID, credentialId: string, counter: bigint): Promise { - return this.webAuthnRepository.updateWebAuthnCredentialCounter(userId, credentialId, counter); - } - - async updateWebAuthnCredentialLastUsed(userId: UserID, credentialId: string): Promise { - return this.webAuthnRepository.updateWebAuthnCredentialLastUsed(userId, credentialId); - } - - async updateWebAuthnCredentialName(userId: UserID, credentialId: string, name: string): Promise { - return this.webAuthnRepository.updateWebAuthnCredentialName(userId, credentialId, name); - } - - async setWebAuthnCredentialSupersededBy(userId: UserID, credentialId: string, supersededBy: string): Promise { - return this.webAuthnRepository.setWebAuthnCredentialSupersededBy(userId, credentialId, supersededBy); - } - - async deleteWebAuthnCredential(userId: UserID, credentialId: string): Promise { - return this.webAuthnRepository.deleteWebAuthnCredential(userId, credentialId); - } - - async getUserIdByCredentialId(credentialId: string): Promise { - return this.webAuthnRepository.getUserIdByCredentialId(credentialId); - } - - async deleteAllWebAuthnCredentials(userId: UserID): Promise { - return this.webAuthnRepository.deleteAllWebAuthnCredentials(userId); - } -} diff --git a/fluxer_api/src/api/user/repositories/UserChannelRepository.ts b/fluxer_api/src/api/user/repositories/UserChannelRepository.ts index b2f48c4b3..154d9d3fa 100644 --- a/fluxer_api/src/api/user/repositories/UserChannelRepository.ts +++ b/fluxer_api/src/api/user/repositories/UserChannelRepository.ts @@ -18,14 +18,30 @@ import type {ChannelRow, DmStateRow, PrivateChannelRow} from '@app/api/database/ import {Logger} from '@app/api/Logger'; import {Channel} from '@app/api/models/Channel'; import {Channels, DmStates, PinnedDms, PrivateChannels, ReadStates, UserDmHistory} from '@app/api/Tables'; -import type { - HistoricalDmChannelSummary, - IUserChannelRepository, - ListHistoricalDmChannelOptions, - PrivateChannelSummary, -} from '@app/api/user/repositories/IUserChannelRepository'; import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants'; +export interface PrivateChannelSummary { + channelId: ChannelID; + isGroupDm: boolean; + channelType: number | null; + lastMessageId: MessageID | null; + open: boolean; +} + +export interface ListHistoricalDmChannelOptions { + limit: number; + beforeChannelId?: ChannelID; + afterChannelId?: ChannelID; +} + +export interface HistoricalDmChannelSummary { + channelId: ChannelID; + channelType: number | null; + recipientIds: Array; + lastMessageId: MessageID | null; + open: boolean; +} + interface PinnedDmRow { user_id: UserID; channel_id: ChannelID; @@ -171,7 +187,7 @@ async function fetchPinnedDms(userId: UserID): Promise> { return fetchMany(FETCH_PINNED_DMS_CQL, {user_id: userId}); } -export class UserChannelRepository implements IUserChannelRepository { +export class UserChannelRepository { async addPinnedDm(userId: UserID, channelId: ChannelID): Promise> { const pinnedDms = [...(await fetchPinnedDms(userId))]; const existingDm = pinnedDms.find((dm) => dm.channel_id === channelId); diff --git a/fluxer_api/src/api/user/repositories/UserContentRepository.ts b/fluxer_api/src/api/user/repositories/UserContentRepository.ts deleted file mode 100644 index 8938bf244..000000000 --- a/fluxer_api/src/api/user/repositories/UserContentRepository.ts +++ /dev/null @@ -1,228 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {ChannelID, MessageID, UserID} from '@app/api/BrandedTypes'; -import type {GiftCodeRow, PaymentBySubscriptionRow, PaymentRow} from '@app/api/database/types/PaymentTypes'; -import type {PushSubscriptionRow, RecentMentionRow} from '@app/api/database/types/UserTypes'; -import type {GiftCode} from '@app/api/models/GiftCode'; -import type {Payment} from '@app/api/models/Payment'; -import type {PushSubscription} from '@app/api/models/PushSubscription'; -import type {RecentMention} from '@app/api/models/RecentMention'; -import type {SavedMessage} from '@app/api/models/SavedMessage'; -import type {VisionarySlot} from '@app/api/models/VisionarySlot'; -import {GiftCodeRepository} from '@app/api/user/repositories/GiftCodeRepository'; -import type {IUserContentRepository} from '@app/api/user/repositories/IUserContentRepository'; -import {PaymentRepository} from '@app/api/user/repositories/PaymentRepository'; -import {PushSubscriptionRepository} from '@app/api/user/repositories/PushSubscriptionRepository'; -import {RecentMentionRepository} from '@app/api/user/repositories/RecentMentionRepository'; -import {SavedMessageRepository} from '@app/api/user/repositories/SavedMessageRepository'; -import {VisionarySlotRepository} from '@app/api/user/repositories/VisionarySlotRepository'; - -export class UserContentRepository implements IUserContentRepository { - private giftCodeRepository: GiftCodeRepository; - private paymentRepository: PaymentRepository; - private pushSubscriptionRepository: PushSubscriptionRepository; - private recentMentionRepository: RecentMentionRepository; - private savedMessageRepository: SavedMessageRepository; - private visionarySlotRepository: VisionarySlotRepository; - - constructor() { - this.giftCodeRepository = new GiftCodeRepository(); - this.paymentRepository = new PaymentRepository(); - this.pushSubscriptionRepository = new PushSubscriptionRepository(); - this.recentMentionRepository = new RecentMentionRepository(); - this.savedMessageRepository = new SavedMessageRepository(); - this.visionarySlotRepository = new VisionarySlotRepository(); - } - - async createGiftCode(data: GiftCodeRow): Promise { - return this.giftCodeRepository.createGiftCode(data); - } - - async findGiftCode(code: string): Promise { - return this.giftCodeRepository.findGiftCode(code); - } - - async findGiftCodeByPaymentIntent(paymentIntentId: string): Promise { - return this.giftCodeRepository.findGiftCodeByPaymentIntent(paymentIntentId); - } - - async findGiftCodesByCreator(userId: UserID): Promise> { - return this.giftCodeRepository.findGiftCodesByCreator(userId); - } - - async findGiftCodesByRedeemer(userId: UserID): Promise> { - return this.giftCodeRepository.findGiftCodesByRedeemer(userId); - } - - async redeemGiftCode(code: string, userId: UserID): Promise { - return this.giftCodeRepository.redeemGiftCode(code, userId); - } - - async unredeemGiftCode(code: string, userId: UserID): Promise { - return this.giftCodeRepository.unredeemGiftCode(code, userId); - } - - async revokeGiftCode(code: string): Promise { - return this.giftCodeRepository.revokeGiftCode(code); - } - - async unrevokeGiftCode(code: string): Promise { - return this.giftCodeRepository.unrevokeGiftCode(code); - } - - async markGiftPremiumReversed(gift: GiftCode, seconds: number): Promise { - return this.giftCodeRepository.markGiftPremiumReversed(gift, seconds); - } - - async clearGiftPremiumReversed(code: string, seconds: number): Promise { - return this.giftCodeRepository.clearGiftPremiumReversed(code, seconds); - } - - async linkGiftCodeToCheckoutSession(code: string, checkoutSessionId: string): Promise { - return this.giftCodeRepository.linkGiftCodeToCheckoutSession(code, checkoutSessionId); - } - - async createPayment(data: { - checkout_session_id: string; - user_id: UserID; - price_id: string; - product_type: string; - status: string; - is_gift: boolean; - created_at: Date; - purchase_geoip_country_code?: string | null; - purchase_client_country_code?: string | null; - eu_withdrawal_waiver_required?: boolean; - eu_withdrawal_waiver_accepted?: boolean; - eu_withdrawal_waiver_accepted_at?: Date | null; - eu_withdrawal_waiver_text_version?: string | null; - }): Promise { - return this.paymentRepository.createPayment(data); - } - - async updatePayment( - data: Partial & { - checkout_session_id: string; - }, - ): Promise { - return this.paymentRepository.updatePayment(data); - } - - async getPaymentByCheckoutSession(checkoutSessionId: string): Promise { - return this.paymentRepository.getPaymentByCheckoutSession(checkoutSessionId); - } - - async findPaymentsByUserId(userId: UserID): Promise> { - return this.paymentRepository.findPaymentsByUserId(userId); - } - - async getPaymentByPaymentIntent(paymentIntentId: string): Promise { - return this.paymentRepository.getPaymentByPaymentIntent(paymentIntentId); - } - - async getSubscriptionInfo(subscriptionId: string): Promise { - return this.paymentRepository.getSubscriptionInfo(subscriptionId); - } - - async listPushSubscriptions(userId: UserID): Promise> { - return this.pushSubscriptionRepository.listPushSubscriptions(userId); - } - - async createPushSubscription(data: PushSubscriptionRow): Promise { - return this.pushSubscriptionRepository.createPushSubscription(data); - } - - async deletePushSubscription(userId: UserID, subscriptionId: string): Promise { - return this.pushSubscriptionRepository.deletePushSubscription(userId, subscriptionId); - } - - async deletePushSubscriptionsForAuthSessions( - userId: UserID, - authSessionIdHashes: Array, - options: {deleteUnboundSubscriptions: boolean}, - ): Promise { - return this.pushSubscriptionRepository.deletePushSubscriptionsForAuthSessions(userId, authSessionIdHashes, options); - } - - async getBulkPushSubscriptions(userIds: Array): Promise>> { - return this.pushSubscriptionRepository.getBulkPushSubscriptions(userIds); - } - - async deleteAllPushSubscriptions(userId: UserID): Promise { - return this.pushSubscriptionRepository.deleteAllPushSubscriptions(userId); - } - - async getRecentMention(userId: UserID, messageId: MessageID): Promise { - return this.recentMentionRepository.getRecentMention(userId, messageId); - } - - async listRecentMentions( - userId: UserID, - includeEveryone: boolean = true, - includeRole: boolean = true, - includeGuilds: boolean = true, - limit: number = 25, - before?: MessageID, - ): Promise> { - return this.recentMentionRepository.listRecentMentions( - userId, - includeEveryone, - includeRole, - includeGuilds, - limit, - before, - ); - } - - async createRecentMentions(mentions: Array): Promise { - return this.recentMentionRepository.createRecentMentions(mentions); - } - - async deleteRecentMention(mention: RecentMention): Promise { - return this.recentMentionRepository.deleteRecentMention(mention); - } - - async deleteRecentMentions(mentions: Array): Promise { - return this.recentMentionRepository.deleteRecentMentions(mentions); - } - - async deleteAllRecentMentions(userId: UserID): Promise { - return this.recentMentionRepository.deleteAllRecentMentions(userId); - } - - async listSavedMessages(userId: UserID, limit: number = 25, before?: MessageID): Promise> { - return this.savedMessageRepository.listSavedMessages(userId, limit, before); - } - - async countSavedMessages(userId: UserID): Promise { - return this.savedMessageRepository.countSavedMessages(userId); - } - - async createSavedMessage(userId: UserID, channelId: ChannelID, messageId: MessageID): Promise { - return this.savedMessageRepository.createSavedMessage(userId, channelId, messageId); - } - - async deleteSavedMessage(userId: UserID, messageId: MessageID): Promise { - return this.savedMessageRepository.deleteSavedMessage(userId, messageId); - } - - async deleteAllSavedMessages(userId: UserID): Promise { - return this.savedMessageRepository.deleteAllSavedMessages(userId); - } - - async listVisionarySlots(): Promise> { - return this.visionarySlotRepository.listVisionarySlots(); - } - - async expandVisionarySlots(byCount: number): Promise { - return this.visionarySlotRepository.expandVisionarySlots(byCount); - } - - async reserveVisionarySlot(slotIndex: number, userId: UserID): Promise { - return this.visionarySlotRepository.reserveVisionarySlot(slotIndex, userId); - } - - async unreserveVisionarySlot(slotIndex: number, userId: UserID): Promise { - return this.visionarySlotRepository.unreserveVisionarySlot(slotIndex, userId); - } -} diff --git a/fluxer_api/src/api/user/repositories/UserRelationshipRepository.ts b/fluxer_api/src/api/user/repositories/UserRelationshipRepository.ts index 868cafb36..f259f1efc 100644 --- a/fluxer_api/src/api/user/repositories/UserRelationshipRepository.ts +++ b/fluxer_api/src/api/user/repositories/UserRelationshipRepository.ts @@ -8,7 +8,6 @@ import type {NoteRow, RelationshipRow} from '@app/api/database/types/UserTypes'; import {Relationship} from '@app/api/models/Relationship'; import {UserNote} from '@app/api/models/UserNote'; import {Notes, Relationships, RelationshipsByTarget} from '@app/api/Tables'; -import type {IUserRelationshipRepository} from '@app/api/user/repositories/IUserRelationshipRepository'; import {RelationshipTypes} from '@fluxer/constants/src/UserConstants'; const FETCH_ALL_NOTES_CQL = Notes.selectCql({ @@ -41,7 +40,7 @@ const FETCH_ALL_NOTES_FOR_DELETE_QUERY = Notes.selectCql({ limit: 10000, }); -export class UserRelationshipRepository implements IUserRelationshipRepository { +export class UserRelationshipRepository { async clearUserNote(sourceUserId: UserID, targetUserId: UserID): Promise { await deleteOneOrMany( Notes.deleteByPk({ diff --git a/fluxer_api/src/api/user/repositories/UserRepository.ts b/fluxer_api/src/api/user/repositories/UserRepository.ts index b28fc01f0..01262189b 100644 --- a/fluxer_api/src/api/user/repositories/UserRepository.ts +++ b/fluxer_api/src/api/user/repositories/UserRepository.ts @@ -35,36 +35,60 @@ import type {UserNote} from '@app/api/models/UserNote'; import type {UserSettings} from '@app/api/models/UserSettings'; import type {VisionarySlot} from '@app/api/models/VisionarySlot'; import type {WebAuthnCredential} from '@app/api/models/WebAuthnCredential'; -import type {UserDeletionScheduleUpdate} from '@app/api/user/repositories/IUserAccountRepository'; -import type { - HistoricalDmChannelSummary, - ListHistoricalDmChannelOptions, - PrivateChannelSummary, -} from '@app/api/user/repositories/IUserChannelRepository'; -import type {IUserRepositoryAggregate} from '@app/api/user/repositories/IUserRepositoryAggregate'; -import {UserAccountRepository} from '@app/api/user/repositories/UserAccountRepository'; -import {UserAuthRepository} from '@app/api/user/repositories/UserAuthRepository'; -import {UserChannelRepository} from '@app/api/user/repositories/UserChannelRepository'; -import {UserContentRepository} from '@app/api/user/repositories/UserContentRepository'; +import {UserEmailOwnershipRepository} from '@app/api/user/repositories/account/crud/UserEmailOwnershipRepository'; +import { + UserAccountRepository, + type UserDeletionScheduleUpdate, +} from '@app/api/user/repositories/account/UserAccountRepository'; +import {UserDeletionRepository} from '@app/api/user/repositories/account/UserDeletionRepository'; +import {UserGuildRepository} from '@app/api/user/repositories/account/UserGuildRepository'; +import {UserLookupRepository} from '@app/api/user/repositories/account/UserLookupRepository'; +import {AuthSessionRepository} from '@app/api/user/repositories/auth/AuthSessionRepository'; +import {IpAuthorizationRepository} from '@app/api/user/repositories/auth/IpAuthorizationRepository'; +import {MfaBackupCodeRepository} from '@app/api/user/repositories/auth/MfaBackupCodeRepository'; +import {TokenRepository} from '@app/api/user/repositories/auth/TokenRepository'; +import {WebAuthnRepository} from '@app/api/user/repositories/auth/WebAuthnRepository'; +import {GiftCodeRepository} from '@app/api/user/repositories/GiftCodeRepository'; +import {PaymentRepository} from '@app/api/user/repositories/PaymentRepository'; +import {PushSubscriptionRepository} from '@app/api/user/repositories/PushSubscriptionRepository'; +import {RecentMentionRepository} from '@app/api/user/repositories/RecentMentionRepository'; +import {SavedMessageRepository} from '@app/api/user/repositories/SavedMessageRepository'; +import { + type HistoricalDmChannelSummary, + type ListHistoricalDmChannelOptions, + type PrivateChannelSummary, + UserChannelRepository, +} from '@app/api/user/repositories/UserChannelRepository'; import {UserRelationshipRepository} from '@app/api/user/repositories/UserRelationshipRepository'; import {UserSettingsRepository} from '@app/api/user/repositories/UserSettingsRepository'; +import {VisionarySlotRepository} from '@app/api/user/repositories/VisionarySlotRepository'; import type {IKVProvider} from '@pkgs/kv_client/src/IKVProvider'; -export class UserRepository implements IUserRepositoryAggregate { +export class UserRepository { private accountRepo: UserAccountRepository; - private settingsRepo: UserSettingsRepository; - private authRepo: UserAuthRepository; - private relationshipRepo: UserRelationshipRepository; - private channelRepo: UserChannelRepository; - private contentRepo: UserContentRepository; + private lookupRepo: UserLookupRepository; + private deletionRepo = new UserDeletionRepository(); + private guildRepo = new UserGuildRepository(); + private tokenRepo = new TokenRepository(); + private settingsRepo = new UserSettingsRepository(); + private authSessionRepository = new AuthSessionRepository(); + private mfaBackupCodeRepository = new MfaBackupCodeRepository(); + private ipAuthorizationRepository: IpAuthorizationRepository; + private webAuthnRepository = new WebAuthnRepository(); + private relationshipRepo = new UserRelationshipRepository(); + private channelRepo = new UserChannelRepository(); + private giftCodeRepository = new GiftCodeRepository(); + private paymentRepository = new PaymentRepository(); + private pushSubscriptionRepository = new PushSubscriptionRepository(); + private recentMentionRepository = new RecentMentionRepository(); + private savedMessageRepository = new SavedMessageRepository(); + private visionarySlotRepository = new VisionarySlotRepository(); constructor(kv: IKVProvider = getKVClient()) { this.accountRepo = new UserAccountRepository(kv); - this.settingsRepo = new UserSettingsRepository(); - this.authRepo = new UserAuthRepository(this.accountRepo); - this.relationshipRepo = new UserRelationshipRepository(); - this.channelRepo = new UserChannelRepository(); - this.contentRepo = new UserContentRepository(); + const findUnique = this.accountRepo.findUnique.bind(this.accountRepo); + this.lookupRepo = new UserLookupRepository(findUnique, new UserEmailOwnershipRepository(findUnique, kv)); + this.ipAuthorizationRepository = new IpAuthorizationRepository(this.accountRepo); } async create(data: UserRow): Promise { @@ -112,27 +136,27 @@ export class UserRepository implements IUserRepositoryAggregate { } async findByUsernameDiscriminator(username: string, discriminator: number): Promise { - return this.accountRepo.findByUsernameDiscriminator(username, discriminator); + return this.lookupRepo.findByUsernameDiscriminator(username, discriminator); } async findDiscriminatorsByUsername(username: string): Promise> { - return this.accountRepo.findDiscriminatorsByUsername(username); + return this.lookupRepo.findDiscriminatorsByUsername(username); } async findUsersByUsername(username: string): Promise> { - return this.accountRepo.findUsersByUsername(username); + return this.lookupRepo.findUsersByUsername(username); } async findByEmail(email: string): Promise { - return this.accountRepo.findByEmail(email); + return this.lookupRepo.findByEmail(email); } async findByStripeSubscriptionId(stripeSubscriptionId: string): Promise { - return this.accountRepo.findByStripeSubscriptionId(stripeSubscriptionId); + return this.lookupRepo.findByStripeSubscriptionId(stripeSubscriptionId); } async findByStripeCustomerId(stripeCustomerId: string): Promise { - return this.accountRepo.findByStripeCustomerId(stripeCustomerId); + return this.lookupRepo.findByStripeCustomerId(stripeCustomerId); } async listUserIdsByLastActiveIp( @@ -143,7 +167,7 @@ export class UserRepository implements IUserRepositoryAggregate { userIds: Array; total: number; }> { - return this.accountRepo.listUserIdsByLastActiveIp(lastActiveIp, limit, offset); + return this.lookupRepo.listUserIdsByLastActiveIp(lastActiveIp, limit, offset); } async listUsers(userIds: Array): Promise> { @@ -161,22 +185,23 @@ export class UserRepository implements IUserRepositoryAggregate { } async getUserGuildIds(userId: UserID): Promise> { - return this.accountRepo.getUserGuildIds(userId); + return this.guildRepo.getUserGuildIds(userId); } async getActivityTracking(userId: UserID): Promise<{ last_active_at: Date | null; last_active_ip: string | null; }> { - return this.accountRepo.getActivityTracking(userId); + const result = await this.accountRepo.getActivityTracking(userId); + return result ?? {last_active_at: null, last_active_ip: null}; } async addPendingDeletion(userId: UserID, pendingDeletionAt: Date, deletionReasonCode: number): Promise { - return this.accountRepo.addPendingDeletion(userId, pendingDeletionAt, deletionReasonCode); + return this.deletionRepo.addPendingDeletion(userId, pendingDeletionAt, deletionReasonCode); } async removePendingDeletion(userId: UserID, pendingDeletionAt: Date): Promise { - return this.accountRepo.removePendingDeletion(userId, pendingDeletionAt); + return this.deletionRepo.removePendingDeletion(userId, pendingDeletionAt); } async findUsersPendingDeletionByDate(deletionDate: string): Promise< @@ -185,11 +210,11 @@ export class UserRepository implements IUserRepositoryAggregate { deletion_reason_code: number; }> > { - return this.accountRepo.findUsersPendingDeletionByDate(deletionDate); + return this.deletionRepo.findUsersPendingDeletionByDate(deletionDate); } async scheduleDeletion(userId: UserID, pendingDeletionAt: Date, deletionReasonCode: number): Promise { - return this.accountRepo.scheduleDeletion(userId, pendingDeletionAt, deletionReasonCode); + return this.deletionRepo.scheduleDeletion(userId, pendingDeletionAt, deletionReasonCode); } async deleteUserSecondaryIndices(userId: UserID): Promise { @@ -197,7 +222,7 @@ export class UserRepository implements IUserRepositoryAggregate { } async removeFromAllGuilds(userId: UserID): Promise { - return this.accountRepo.removeFromAllGuilds(userId); + return this.guildRepo.removeFromAllGuilds(userId); } async updateLastActiveAt(params: {userId: UserID; lastActiveAt: Date; lastActiveIp?: string}): Promise { @@ -250,106 +275,108 @@ export class UserRepository implements IUserRepositoryAggregate { } async listAuthSessions(userId: UserID): Promise> { - return this.authRepo.listAuthSessions(userId); + return this.authSessionRepository.listAuthSessions(userId); } async listAuthSessionTombstones(userId: UserID): Promise> { - return this.authRepo.listAuthSessionTombstones(userId); + return this.authSessionRepository.listAuthSessionTombstones(userId); } async getAuthSessionByToken(sessionIdHash: Buffer): Promise { - return this.authRepo.getAuthSessionByToken(sessionIdHash); + return this.authSessionRepository.getAuthSessionByToken(sessionIdHash); } async createAuthSession(sessionData: AuthSessionRow): Promise { - return this.authRepo.createAuthSession(sessionData); + return this.authSessionRepository.createAuthSession(sessionData); } async updateAuthSessionLastUsed(sessionIdHash: Buffer): Promise { - return this.authRepo.updateAuthSessionLastUsed(sessionIdHash); + const session = await this.authSessionRepository.getAuthSessionByToken(sessionIdHash); + if (!session) return; + await this.authSessionRepository.updateAuthSessionLastUsed(sessionIdHash); } async deleteAuthSessions(userId: UserID, sessionIdHashes: Array): Promise { - return this.authRepo.deleteAuthSessions(userId, sessionIdHashes); + return this.authSessionRepository.deleteAuthSessions(userId, sessionIdHashes); } async deleteAllAuthSessions(userId: UserID): Promise { - return this.authRepo.deleteAllAuthSessions(userId); + return this.authSessionRepository.deleteAllAuthSessions(userId); } async listMfaBackupCodes(userId: UserID): Promise> { - return this.authRepo.listMfaBackupCodes(userId); + return this.mfaBackupCodeRepository.listMfaBackupCodes(userId); } async createMfaBackupCodes(userId: UserID, codes: Array): Promise> { - return this.authRepo.createMfaBackupCodes(userId, codes); + return this.mfaBackupCodeRepository.createMfaBackupCodes(userId, codes); } async clearMfaBackupCodes(userId: UserID): Promise { - return this.authRepo.clearMfaBackupCodes(userId); + return this.mfaBackupCodeRepository.clearMfaBackupCodes(userId); } async consumeMfaBackupCode(userId: UserID, code: string): Promise { - return this.authRepo.consumeMfaBackupCode(userId, code); + return this.mfaBackupCodeRepository.consumeMfaBackupCode(userId, code); } async deleteAllMfaBackupCodes(userId: UserID): Promise { - return this.authRepo.deleteAllMfaBackupCodes(userId); + return this.mfaBackupCodeRepository.deleteAllMfaBackupCodes(userId); } async getEmailVerificationToken(token: string): Promise { - return this.authRepo.getEmailVerificationToken(token); + return this.tokenRepo.getEmailVerificationToken(token); } async createEmailVerificationToken(tokenData: EmailVerificationTokenRow): Promise { - return this.authRepo.createEmailVerificationToken(tokenData); + return this.tokenRepo.createEmailVerificationToken(tokenData); } async deleteEmailVerificationToken(token: string): Promise { - return this.authRepo.deleteEmailVerificationToken(token); + return this.tokenRepo.deleteEmailVerificationToken(token); } async getPasswordResetToken(token: string): Promise { - return this.authRepo.getPasswordResetToken(token); + return this.tokenRepo.getPasswordResetToken(token); } async createPasswordResetToken(tokenData: PasswordResetTokenRow): Promise { - return this.authRepo.createPasswordResetToken(tokenData); + return this.tokenRepo.createPasswordResetToken(tokenData); } async deleteAllPasswordResetTokens(userId: UserID): Promise { - return this.authRepo.deleteAllPasswordResetTokens(userId); + return this.tokenRepo.deleteAllPasswordResetTokens(userId); } async getEmailRevertToken(token: string): Promise { - return this.authRepo.getEmailRevertToken(token); + return this.tokenRepo.getEmailRevertToken(token); } async createEmailRevertToken(tokenData: EmailRevertTokenRow): Promise { - return this.authRepo.createEmailRevertToken(tokenData); + return this.tokenRepo.createEmailRevertToken(tokenData); } async deleteEmailRevertToken(token: string): Promise { - return this.authRepo.deleteEmailRevertToken(token); + return this.tokenRepo.deleteEmailRevertToken(token); } async checkIpAuthorized(userId: UserID, ip: string): Promise { - return this.authRepo.checkIpAuthorized(userId, ip); + return this.ipAuthorizationRepository.checkIpAuthorized(userId, ip); } async createAuthorizedIp(userId: UserID, ip: string): Promise { - return this.authRepo.createAuthorizedIp(userId, ip); + return this.ipAuthorizationRepository.createAuthorizedIp(userId, ip); } async createIpAuthorizationToken(userId: UserID, token: string, email: string): Promise { - return this.authRepo.createIpAuthorizationToken(userId, token, email); + return this.ipAuthorizationRepository.createIpAuthorizationToken(userId, token, email); } async authorizeIpByToken(token: string): Promise<{ userId: UserID; email: string; } | null> { - return this.authRepo.authorizeIpByToken(token); + return this.ipAuthorizationRepository.authorizeIpByToken(token); } async getAuthorizedIps(userId: UserID): Promise< @@ -357,19 +384,19 @@ export class UserRepository implements IUserRepositoryAggregate { ip: string; }> > { - return this.authRepo.getAuthorizedIps(userId); + return this.ipAuthorizationRepository.getAuthorizedIps(userId); } async deleteAllAuthorizedIps(userId: UserID): Promise { - return this.authRepo.deleteAllAuthorizedIps(userId); + return this.ipAuthorizationRepository.deleteAllAuthorizedIps(userId); } async listWebAuthnCredentials(userId: UserID): Promise> { - return this.authRepo.listWebAuthnCredentials(userId); + return this.webAuthnRepository.listWebAuthnCredentials(userId); } async getWebAuthnCredential(userId: UserID, credentialId: string): Promise { - return this.authRepo.getWebAuthnCredential(userId, credentialId); + return this.webAuthnRepository.getWebAuthnCredential(userId, credentialId); } async createWebAuthnCredential( @@ -381,35 +408,43 @@ export class UserRepository implements IUserRepositoryAggregate { name: string, rpId: string | null, ): Promise { - return this.authRepo.createWebAuthnCredential(userId, credentialId, publicKey, counter, transports, name, rpId); + return this.webAuthnRepository.createWebAuthnCredential( + userId, + credentialId, + publicKey, + counter, + transports, + name, + rpId, + ); } async updateWebAuthnCredentialCounter(userId: UserID, credentialId: string, counter: bigint): Promise { - return this.authRepo.updateWebAuthnCredentialCounter(userId, credentialId, counter); + return this.webAuthnRepository.updateWebAuthnCredentialCounter(userId, credentialId, counter); } async updateWebAuthnCredentialLastUsed(userId: UserID, credentialId: string): Promise { - return this.authRepo.updateWebAuthnCredentialLastUsed(userId, credentialId); + return this.webAuthnRepository.updateWebAuthnCredentialLastUsed(userId, credentialId); } async updateWebAuthnCredentialName(userId: UserID, credentialId: string, name: string): Promise { - return this.authRepo.updateWebAuthnCredentialName(userId, credentialId, name); + return this.webAuthnRepository.updateWebAuthnCredentialName(userId, credentialId, name); } async setWebAuthnCredentialSupersededBy(userId: UserID, credentialId: string, supersededBy: string): Promise { - return this.authRepo.setWebAuthnCredentialSupersededBy(userId, credentialId, supersededBy); + return this.webAuthnRepository.setWebAuthnCredentialSupersededBy(userId, credentialId, supersededBy); } async deleteWebAuthnCredential(userId: UserID, credentialId: string): Promise { - return this.authRepo.deleteWebAuthnCredential(userId, credentialId); + return this.webAuthnRepository.deleteWebAuthnCredential(userId, credentialId); } async getUserIdByCredentialId(credentialId: string): Promise { - return this.authRepo.getUserIdByCredentialId(credentialId); + return this.webAuthnRepository.getUserIdByCredentialId(credentialId); } async deleteAllWebAuthnCredentials(userId: UserID): Promise { - return this.authRepo.deleteAllWebAuthnCredentials(userId); + return this.webAuthnRepository.deleteAllWebAuthnCredentials(userId); } async listRelationships(sourceUserId: UserID): Promise> { @@ -553,7 +588,7 @@ export class UserRepository implements IUserRepositoryAggregate { } async getRecentMention(userId: UserID, messageId: MessageID): Promise { - return this.contentRepo.getRecentMention(userId, messageId); + return this.recentMentionRepository.getRecentMention(userId, messageId); } async listRecentMentions( @@ -564,103 +599,110 @@ export class UserRepository implements IUserRepositoryAggregate { limit: number, before?: MessageID, ): Promise> { - return this.contentRepo.listRecentMentions(userId, includeEveryone, includeRole, includeGuilds, limit, before); + return this.recentMentionRepository.listRecentMentions( + userId, + includeEveryone, + includeRole, + includeGuilds, + limit, + before, + ); } async createRecentMentions(mentions: Array): Promise { - return this.contentRepo.createRecentMentions(mentions); + return this.recentMentionRepository.createRecentMentions(mentions); } async deleteRecentMention(mention: RecentMention): Promise { - return this.contentRepo.deleteRecentMention(mention); + return this.recentMentionRepository.deleteRecentMention(mention); } async deleteRecentMentions(mentions: Array): Promise { - return this.contentRepo.deleteRecentMentions(mentions); + return this.recentMentionRepository.deleteRecentMentions(mentions); } async deleteAllRecentMentions(userId: UserID): Promise { - return this.contentRepo.deleteAllRecentMentions(userId); + return this.recentMentionRepository.deleteAllRecentMentions(userId); } async listSavedMessages(userId: UserID, limit?: number, before?: MessageID): Promise> { - return this.contentRepo.listSavedMessages(userId, limit, before); + return this.savedMessageRepository.listSavedMessages(userId, limit, before); } async countSavedMessages(userId: UserID): Promise { - return this.contentRepo.countSavedMessages(userId); + return this.savedMessageRepository.countSavedMessages(userId); } async createSavedMessage(userId: UserID, channelId: ChannelID, messageId: MessageID): Promise { - return this.contentRepo.createSavedMessage(userId, channelId, messageId); + return this.savedMessageRepository.createSavedMessage(userId, channelId, messageId); } async deleteSavedMessage(userId: UserID, messageId: MessageID): Promise { - return this.contentRepo.deleteSavedMessage(userId, messageId); + return this.savedMessageRepository.deleteSavedMessage(userId, messageId); } async deleteAllSavedMessages(userId: UserID): Promise { - return this.contentRepo.deleteAllSavedMessages(userId); + return this.savedMessageRepository.deleteAllSavedMessages(userId); } async createGiftCode(data: GiftCodeRow): Promise { - return this.contentRepo.createGiftCode(data); + return this.giftCodeRepository.createGiftCode(data); } async findGiftCode(code: string): Promise { - return this.contentRepo.findGiftCode(code); + return this.giftCodeRepository.findGiftCode(code); } async findGiftCodeByPaymentIntent(paymentIntentId: string): Promise { - return this.contentRepo.findGiftCodeByPaymentIntent(paymentIntentId); + return this.giftCodeRepository.findGiftCodeByPaymentIntent(paymentIntentId); } async findGiftCodesByCreator(userId: UserID): Promise> { - return this.contentRepo.findGiftCodesByCreator(userId); + return this.giftCodeRepository.findGiftCodesByCreator(userId); } async findGiftCodesByRedeemer(userId: UserID): Promise> { - return this.contentRepo.findGiftCodesByRedeemer(userId); + return this.giftCodeRepository.findGiftCodesByRedeemer(userId); } async redeemGiftCode(code: string, userId: UserID): Promise { - return this.contentRepo.redeemGiftCode(code, userId); + return this.giftCodeRepository.redeemGiftCode(code, userId); } async unredeemGiftCode(code: string, userId: UserID): Promise { - return this.contentRepo.unredeemGiftCode(code, userId); + return this.giftCodeRepository.unredeemGiftCode(code, userId); } async revokeGiftCode(code: string): Promise { - return this.contentRepo.revokeGiftCode(code); + return this.giftCodeRepository.revokeGiftCode(code); } async unrevokeGiftCode(code: string): Promise { - return this.contentRepo.unrevokeGiftCode(code); + return this.giftCodeRepository.unrevokeGiftCode(code); } async markGiftPremiumReversed(gift: GiftCode, seconds: number): Promise { - return this.contentRepo.markGiftPremiumReversed(gift, seconds); + return this.giftCodeRepository.markGiftPremiumReversed(gift, seconds); } async clearGiftPremiumReversed(code: string, seconds: number): Promise { - return this.contentRepo.clearGiftPremiumReversed(code, seconds); + return this.giftCodeRepository.clearGiftPremiumReversed(code, seconds); } async linkGiftCodeToCheckoutSession(code: string, checkoutSessionId: string): Promise { - return this.contentRepo.linkGiftCodeToCheckoutSession(code, checkoutSessionId); + return this.giftCodeRepository.linkGiftCodeToCheckoutSession(code, checkoutSessionId); } async listPushSubscriptions(userId: UserID): Promise> { - return this.contentRepo.listPushSubscriptions(userId); + return this.pushSubscriptionRepository.listPushSubscriptions(userId); } async createPushSubscription(data: PushSubscriptionRow): Promise { - return this.contentRepo.createPushSubscription(data); + return this.pushSubscriptionRepository.createPushSubscription(data); } async deletePushSubscription(userId: UserID, subscriptionId: string): Promise { - return this.contentRepo.deletePushSubscription(userId, subscriptionId); + return this.pushSubscriptionRepository.deletePushSubscription(userId, subscriptionId); } async deletePushSubscriptionsForAuthSessions( @@ -668,15 +710,15 @@ export class UserRepository implements IUserRepositoryAggregate { authSessionIdHashes: Array, options: {deleteUnboundSubscriptions: boolean}, ): Promise { - return this.contentRepo.deletePushSubscriptionsForAuthSessions(userId, authSessionIdHashes, options); + return this.pushSubscriptionRepository.deletePushSubscriptionsForAuthSessions(userId, authSessionIdHashes, options); } async getBulkPushSubscriptions(userIds: Array): Promise>> { - return this.contentRepo.getBulkPushSubscriptions(userIds); + return this.pushSubscriptionRepository.getBulkPushSubscriptions(userIds); } async deleteAllPushSubscriptions(userId: UserID): Promise { - return this.contentRepo.deleteAllPushSubscriptions(userId); + return this.pushSubscriptionRepository.deleteAllPushSubscriptions(userId); } async createPayment(data: { @@ -694,7 +736,7 @@ export class UserRepository implements IUserRepositoryAggregate { eu_withdrawal_waiver_accepted_at?: Date | null; eu_withdrawal_waiver_text_version?: string | null; }): Promise { - return this.contentRepo.createPayment(data); + return this.paymentRepository.createPayment(data); } async updatePayment( @@ -702,38 +744,38 @@ export class UserRepository implements IUserRepositoryAggregate { checkout_session_id: string; }, ): Promise { - return this.contentRepo.updatePayment(data); + return this.paymentRepository.updatePayment(data); } async getPaymentByCheckoutSession(checkoutSessionId: string): Promise { - return this.contentRepo.getPaymentByCheckoutSession(checkoutSessionId); + return this.paymentRepository.getPaymentByCheckoutSession(checkoutSessionId); } async findPaymentsByUserId(userId: UserID): Promise> { - return this.contentRepo.findPaymentsByUserId(userId); + return this.paymentRepository.findPaymentsByUserId(userId); } async getPaymentByPaymentIntent(paymentIntentId: string): Promise { - return this.contentRepo.getPaymentByPaymentIntent(paymentIntentId); + return this.paymentRepository.getPaymentByPaymentIntent(paymentIntentId); } async getSubscriptionInfo(subscriptionId: string): Promise { - return this.contentRepo.getSubscriptionInfo(subscriptionId); + return this.paymentRepository.getSubscriptionInfo(subscriptionId); } async listVisionarySlots(): Promise> { - return this.contentRepo.listVisionarySlots(); + return this.visionarySlotRepository.listVisionarySlots(); } async expandVisionarySlots(byCount: number): Promise { - return this.contentRepo.expandVisionarySlots(byCount); + return this.visionarySlotRepository.expandVisionarySlots(byCount); } async reserveVisionarySlot(slotIndex: number, userId: UserID): Promise { - return this.contentRepo.reserveVisionarySlot(slotIndex, userId); + return this.visionarySlotRepository.reserveVisionarySlot(slotIndex, userId); } async unreserveVisionarySlot(slotIndex: number, userId: UserID): Promise { - return this.contentRepo.unreserveVisionarySlot(slotIndex, userId); + return this.visionarySlotRepository.unreserveVisionarySlot(slotIndex, userId); } } diff --git a/fluxer_api/src/api/user/repositories/UserSettingsRepository.ts b/fluxer_api/src/api/user/repositories/UserSettingsRepository.ts index fb8f0f4e6..a781a8a56 100644 --- a/fluxer_api/src/api/user/repositories/UserSettingsRepository.ts +++ b/fluxer_api/src/api/user/repositories/UserSettingsRepository.ts @@ -10,7 +10,6 @@ import {Logger} from '@app/api/Logger'; import {UserGuildSettings} from '@app/api/models/UserGuildSettings'; import {UserSettings} from '@app/api/models/UserSettings'; import {UserGuildSettings as UserGuildSettingsTable, UserSettings as UserSettingsTable} from '@app/api/Tables'; -import type {IUserSettingsRepository} from '@app/api/user/repositories/IUserSettingsRepository'; const FETCH_USER_SETTINGS_CQL = UserSettingsTable.selectCql({ where: UserSettingsTable.where.eq('user_id'), @@ -24,7 +23,7 @@ const FETCH_ALL_USER_GUILD_SETTINGS_CQL = UserGuildSettingsTable.selectCql({ where: UserGuildSettingsTable.where.eq('user_id'), }); -export class UserSettingsRepository implements IUserSettingsRepository { +export class UserSettingsRepository { async deleteAllUserGuildSettings(userId: UserID): Promise { await deleteOneOrMany( UserGuildSettingsTable.deleteCql({ diff --git a/fluxer_api/src/api/user/repositories/account/UserAccountRepository.ts b/fluxer_api/src/api/user/repositories/account/UserAccountRepository.ts index 84f3cd90d..f91a793ab 100644 --- a/fluxer_api/src/api/user/repositories/account/UserAccountRepository.ts +++ b/fluxer_api/src/api/user/repositories/account/UserAccountRepository.ts @@ -16,7 +16,6 @@ import { import {UserIndexRepository} from '@app/api/user/repositories/account/crud/UserIndexRepository'; import {UserSearchRepository} from '@app/api/user/repositories/account/crud/UserSearchRepository'; import {UserLookupRepository} from '@app/api/user/repositories/account/UserLookupRepository'; -import type {UserDeletionScheduleUpdate} from '@app/api/user/repositories/IUserAccountRepository'; import { extractPremiumFlagsFromLegacyUserFlags, LEGACY_DEAD_USER_FLAGS_MASK, @@ -24,6 +23,18 @@ import { } from '@fluxer/constants/src/UserConstants'; import type {IKVProvider} from '@pkgs/kv_client/src/IKVProvider'; +export interface UserDeletionScheduleUpdate { + pending_deletion_at: Date | null; + flags?: bigint; + deletion_reason_code?: number | null; + deletion_public_reason?: string | null; + deletion_audit_log_reason?: string | null; + deletion_scheduled_by?: UserID | null; + deletion_scheduled_at?: Date | null; + temp_banned_until?: Date | null; + first_refund_at?: Date | null; +} + const USER_FLAGS_WRITE_ATTEMPTS = 3; export class UserAccountRepository { diff --git a/fluxer_api/src/api/user/repositories/auth/IpAuthorizationRepository.ts b/fluxer_api/src/api/user/repositories/auth/IpAuthorizationRepository.ts index 0cbcf0c87..8d9ef7b28 100644 --- a/fluxer_api/src/api/user/repositories/auth/IpAuthorizationRepository.ts +++ b/fluxer_api/src/api/user/repositories/auth/IpAuthorizationRepository.ts @@ -8,7 +8,7 @@ import type { IpAuthorizationTokenRow, } from '@app/api/database/types/AuthTypes'; import {AuthorizedIps, AuthorizedIpTrustKeys, IpAuthorizationTokens} from '@app/api/Tables'; -import type {IUserAccountRepository} from '@app/api/user/repositories/IUserAccountRepository'; +import type {UserAccountRepository} from '@app/api/user/repositories/account/UserAccountRepository'; import {UserFlags} from '@fluxer/constants/src/UserConstants'; import {getSameIpDecisionKey, normalizeIpString, parseIpAddress} from '@fluxer/ip_utils/src/IpAddress'; @@ -40,7 +40,7 @@ function getAuthorizedIpTrustKey(ip: string): string | null { } export class IpAuthorizationRepository { - constructor(private userAccountRepository: IUserAccountRepository) {} + constructor(private userAccountRepository: UserAccountRepository) {} async checkIpAuthorized(userId: UserID, ip: string): Promise { const normalizedIp = normalizeAuthorizedIp(ip); diff --git a/fluxer_api/src/api/user/services/AccountStateApplier.ts b/fluxer_api/src/api/user/services/AccountStateApplier.ts index a5e142e2c..9d72461c7 100644 --- a/fluxer_api/src/api/user/services/AccountStateApplier.ts +++ b/fluxer_api/src/api/user/services/AccountStateApplier.ts @@ -5,7 +5,9 @@ import type {AdminRepository} from '@app/api/admin/AdminRepository'; import type {AdminArchiveService} from '@app/api/admin/services/AdminArchiveService'; import {type ChannelID, createUserID, type MessageID, type UserID} from '@app/api/BrandedTypes'; import {isIpBanExempt} from '@app/api/ban/IpBanExemptions'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; +import type {ChannelDataRepository} from '@app/api/channel/repositories/ChannelDataRepository'; +import type {MessageRepository} from '@app/api/channel/repositories/MessageRepository'; import {dispatchChannelEvent} from '@app/api/channel/services/ChannelGatewayDispatch'; import {SYSTEM_USER_ID} from '@app/api/constants/Core'; import {IP_BAN_REFRESH_CHANNEL} from '@app/api/constants/IpBan'; @@ -20,12 +22,12 @@ import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {KVBulkMessageDeletionQueueService} from '@app/api/infrastructure/KVBulkMessageDeletionQueueService'; import type {User} from '@app/api/models/User'; import {isAccountLimitExempt} from '@app/api/user/AccountLimit'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; import { clearNewConversationLimit, isNewConversationLimitExempt, setNewConversationLimit, } from '@app/api/user/NewConversationLimit'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import { type PartialUserChangePropagationDeps, propagatePartialUserChange, @@ -47,14 +49,14 @@ interface AccountUpdateDispatch { } export interface AccountStateDeps { - users: Pick; + users: Pick; dispatch: AccountUpdateDispatch; ipBans: Pick; cache: Pick; archives: Pick; messageDeletionQueue: Pick; messageDeletionDelayMs: number; - authored: Pick; + authored: Pick; now?: () => number; } @@ -69,7 +71,7 @@ type ProfilePropagation = Omit, + channels: Pick, ): AccountUpdateDispatch { return { async userUpdated(user) { @@ -101,15 +103,19 @@ export function accountStateDepsFromContext( ipBans: AccountStateDeps['ipBans'], profile: Omit, messageDeletion: Pick, - channels: Pick, + channels: Pick, ): AccountStateDeps { return { users: ctx.services.users, - dispatch: gatewayDispatch(ctx.services.gateway, {...profile, userRepository: ctx.services.users}, channels), + dispatch: gatewayDispatch( + ctx.services.gateway, + {...profile, userRepository: ctx.services.users}, + channels.channelData, + ), ipBans, cache: ctx.services.cache, ...messageDeletion, - authored: channels, + authored: channels.messages, }; } diff --git a/fluxer_api/src/api/user/services/CustomStatusValidator.ts b/fluxer_api/src/api/user/services/CustomStatusValidator.ts index 2f3e27e54..36000a1f4 100644 --- a/fluxer_api/src/api/user/services/CustomStatusValidator.ts +++ b/fluxer_api/src/api/user/services/CustomStatusValidator.ts @@ -1,13 +1,13 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {createEmojiID, type EmojiID, type UserID} from '@app/api/BrandedTypes'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import {contentModerationService} from '@app/api/infrastructure/ContentModerationService'; import type {LimitConfigService} from '@app/api/limits/LimitConfigService'; import {resolveLimitSafe} from '@app/api/limits/LimitConfigUtils'; import {createLimitMatchContext} from '@app/api/limits/LimitMatchContextBuilder'; import {assertAccountNotLimited} from '@app/api/user/AccountLimit'; -import type {IUserAccountRepository} from '@app/api/user/repositories/IUserAccountRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes'; import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError'; import type {CustomStatusPayload} from '@fluxer/schema/src/domains/user/UserRequestSchemas'; @@ -22,8 +22,8 @@ interface ValidatedCustomStatus { export class CustomStatusValidator { constructor( - private readonly userAccountRepository: IUserAccountRepository, - private readonly guildRepository: IGuildRepositoryAggregate, + private readonly userAccountRepository: UserRepository, + private readonly guildRepository: GuildRepository, private readonly limitConfigService: LimitConfigService, ) {} diff --git a/fluxer_api/src/api/user/services/PartialUserChangePropagation.ts b/fluxer_api/src/api/user/services/PartialUserChangePropagation.ts index 9e2a1ed1d..9d0a666e8 100644 --- a/fluxer_api/src/api/user/services/PartialUserChangePropagation.ts +++ b/fluxer_api/src/api/user/services/PartialUserChangePropagation.ts @@ -2,7 +2,7 @@ import type {GuildID, UserID} from '@app/api/BrandedTypes'; import {mapGuildMemberToResponse} from '@app/api/guild/GuildModel'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {UserCacheService} from '@app/api/infrastructure/UserCacheService'; import {createRequestCache} from '@app/api/middleware/RequestCacheMiddleware'; @@ -17,7 +17,7 @@ export interface PartialUserChangePropagationDeps { userCacheService: UserCacheService; gatewayService: IGatewayService; userRepository: UserGuildIdReader; - guildRepository: Pick; + guildRepository: Pick; } export async function propagatePartialUserChange(deps: PartialUserChangePropagationDeps, user: User): Promise { diff --git a/fluxer_api/src/api/user/services/UserAccountLifecycleService.ts b/fluxer_api/src/api/user/services/UserAccountLifecycleService.ts index 513e388d3..07e5e1e3f 100644 --- a/fluxer_api/src/api/user/services/UserAccountLifecycleService.ts +++ b/fluxer_api/src/api/user/services/UserAccountLifecycleService.ts @@ -4,9 +4,9 @@ import type {ApiContext} from '@app/api/ApiContext'; import * as AuthSession from '@app/api/auth/AuthSession'; import type {UserID} from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {KVAccountDeletionQueueService} from '@app/api/infrastructure/KVAccountDeletionQueueService'; -import type {IUserAccountRepository} from '@app/api/user/repositories/IUserAccountRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {reschedulePendingDeletion} from '@app/api/user/services/PendingDeletionCoordinator'; import type {UserAccountUpdatePropagator} from '@app/api/user/services/UserAccountUpdatePropagator'; import {hasPartialUserFieldsChanged} from '@app/api/user/UserMappers'; @@ -19,8 +19,8 @@ import {ms} from 'itty-time'; interface UserAccountLifecycleServiceDeps { apiContext: ApiContext; - userAccountRepository: IUserAccountRepository; - guildRepository: IGuildRepositoryAggregate; + userAccountRepository: UserRepository; + guildRepository: GuildRepository; emailService: IEmailService; updatePropagator: UserAccountUpdatePropagator; kvDeletionQueue: KVAccountDeletionQueueService; diff --git a/fluxer_api/src/api/user/services/UserAccountLookupService.ts b/fluxer_api/src/api/user/services/UserAccountLookupService.ts index b1c0a2e45..e555d1332 100644 --- a/fluxer_api/src/api/user/services/UserAccountLookupService.ts +++ b/fluxer_api/src/api/user/services/UserAccountLookupService.ts @@ -1,18 +1,15 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {GuildID, UserID} from '@app/api/BrandedTypes'; -import type {IConnectionRepository} from '@app/api/connection/IConnectionRepository'; +import type {ConnectionRepository} from '@app/api/connection/ConnectionRepository'; import type {UserConnectionRow} from '@app/api/database/types/ConnectionTypes'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {GuildService} from '@app/api/guild/services/GuildService'; import type {IDiscriminatorService} from '@app/api/infrastructure/DiscriminatorService'; import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware'; import type {GuildMember} from '@app/api/models/GuildMember'; import type {User} from '@app/api/models/User'; -import type {IUserAccountRepository} from '@app/api/user/repositories/IUserAccountRepository'; -import type {IUserChannelRepository} from '@app/api/user/repositories/IUserChannelRepository'; -import type {IUserRelationshipRepository} from '@app/api/user/repositories/IUserRelationshipRepository'; -import type {IUserSettingsRepository} from '@app/api/user/repositories/IUserSettingsRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {isUsernameTaken} from '@app/api/user/UniqueUsernames'; import {hasFixedDiscriminator} from '@app/api/user/UserTag'; import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants'; @@ -36,14 +33,14 @@ interface ProfileFieldPrivacyContext { } interface UserAccountLookupServiceDeps { - userAccountRepository: IUserAccountRepository; - userChannelRepository: IUserChannelRepository; - userRelationshipRepository: IUserRelationshipRepository; - userSettingsRepository: IUserSettingsRepository; - guildRepository: IGuildRepositoryAggregate; + userAccountRepository: UserRepository; + userChannelRepository: UserRepository; + userRelationshipRepository: UserRepository; + userSettingsRepository: UserRepository; + guildRepository: GuildRepository; guildService: GuildService; discriminatorService: IDiscriminatorService; - connectionRepository: IConnectionRepository; + connectionRepository: ConnectionRepository; } export class UserAccountLookupService { diff --git a/fluxer_api/src/api/user/services/UserAccountNotesService.ts b/fluxer_api/src/api/user/services/UserAccountNotesService.ts index 0ac42792c..b7ef223b4 100644 --- a/fluxer_api/src/api/user/services/UserAccountNotesService.ts +++ b/fluxer_api/src/api/user/services/UserAccountNotesService.ts @@ -1,14 +1,13 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {UserID} from '@app/api/BrandedTypes'; -import type {IUserAccountRepository} from '@app/api/user/repositories/IUserAccountRepository'; -import type {IUserRelationshipRepository} from '@app/api/user/repositories/IUserRelationshipRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import type {UserAccountUpdatePropagator} from '@app/api/user/services/UserAccountUpdatePropagator'; import {UnknownUserError} from '@fluxer/errors/src/domains/user/UnknownUserError'; interface UserAccountNotesServiceDeps { - userAccountRepository: IUserAccountRepository; - userRelationshipRepository: IUserRelationshipRepository; + userAccountRepository: UserRepository; + userRelationshipRepository: UserRepository; updatePropagator: UserAccountUpdatePropagator; } diff --git a/fluxer_api/src/api/user/services/UserAccountProfileService.ts b/fluxer_api/src/api/user/services/UserAccountProfileService.ts index ad008af7b..7fcce06bb 100644 --- a/fluxer_api/src/api/user/services/UserAccountProfileService.ts +++ b/fluxer_api/src/api/user/services/UserAccountProfileService.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {UserRow} from '@app/api/database/types/UserTypes'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import {contentModerationService} from '@app/api/infrastructure/ContentModerationService'; import type {EntityAssetService, PreparedAssetUpload} from '@app/api/infrastructure/EntityAssetService'; import type {LimitConfigService} from '@app/api/limits/LimitConfigService'; @@ -9,7 +9,7 @@ import {resolveLimitSafe} from '@app/api/limits/LimitConfigUtils'; import {createLimitMatchContext} from '@app/api/limits/LimitMatchContextBuilder'; import {profileSubstringBlocklistCache} from '@app/api/middleware/ProfileSubstringBlocklistCache'; import type {User} from '@app/api/models/User'; -import type {IUserAccountRepository} from '@app/api/user/repositories/IUserAccountRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {isProfileSubstringExempt} from '@app/api/user/UserHelpers'; import {deriveDominantAvatarColor} from '@app/api/utils/AvatarColorUtils'; import * as EmojiUtils from '@app/api/utils/EmojiUtils'; @@ -33,8 +33,8 @@ interface ProfileUpdateResult { } interface UserAccountProfileServiceDeps { - userAccountRepository: IUserAccountRepository; - guildRepository: IGuildRepositoryAggregate; + userAccountRepository: UserRepository; + guildRepository: GuildRepository; entityAssetService: EntityAssetService; rateLimitService: IRateLimitService; limitConfigService: LimitConfigService; diff --git a/fluxer_api/src/api/user/services/UserAccountRequestService.ts b/fluxer_api/src/api/user/services/UserAccountRequestService.ts index f8a7d1de3..b9723f132 100644 --- a/fluxer_api/src/api/user/services/UserAccountRequestService.ts +++ b/fluxer_api/src/api/user/services/UserAccountRequestService.ts @@ -15,8 +15,8 @@ import type {AuthSession} from '@app/api/models/AuthSession'; import type {User} from '@app/api/models/User'; import type {HonoEnv} from '@app/api/types/HonoEnv'; import {assertAccountNotLimited} from '@app/api/user/AccountLimit'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; import {isProfileHidden} from '@app/api/user/ProfileVisibility'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import type {EmailChangeService} from '@app/api/user/services/EmailChangeService'; import type {UserAccountService} from '@app/api/user/services/UserAccountService'; import type {UserChannelService} from '@app/api/user/services/UserChannelService'; @@ -99,7 +99,7 @@ export class UserAccountRequestService { private readonly emailChangeService: EmailChangeService, private readonly userAccountService: UserAccountService, private readonly userChannelService: UserChannelService, - private readonly userRepository: IUserRepository, + private readonly userRepository: UserRepository, private readonly userCacheService: UserCacheService, ) {} diff --git a/fluxer_api/src/api/user/services/UserAccountSecurityService.ts b/fluxer_api/src/api/user/services/UserAccountSecurityService.ts index 15db3ff8e..e81fcc40b 100644 --- a/fluxer_api/src/api/user/services/UserAccountSecurityService.ts +++ b/fluxer_api/src/api/user/services/UserAccountSecurityService.ts @@ -18,7 +18,7 @@ import {getInstanceConfigRepository} from '@app/api/middleware/ServiceSingletons import type {AuthSession as AuthSessionModel} from '@app/api/models/AuthSession'; import type {User} from '@app/api/models/User'; import {enforceFluxerTagChangeRateLimit} from '@app/api/user/FluxerTagChangeRateLimit'; -import type {IUserAccountRepository} from '@app/api/user/repositories/IUserAccountRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {assertNoDiscriminatorChange, reserveUsername, type UsernameReservation} from '@app/api/user/UniqueUsernames'; import {isProfileSubstringExempt} from '@app/api/user/UserHelpers'; import {USERNAME_MODE_DISCRIMINATOR} from '@app/api/user/UserTag'; @@ -40,7 +40,7 @@ type UserFieldUpdates = Partial; interface UserAccountSecurityServiceDeps { apiContext: ApiContext; - userAccountRepository: IUserAccountRepository; + userAccountRepository: UserRepository; discriminatorService: IDiscriminatorService; rateLimitService: IRateLimitService; limitConfigService: LimitConfigService; diff --git a/fluxer_api/src/api/user/services/UserAccountService.ts b/fluxer_api/src/api/user/services/UserAccountService.ts index 7b14188dc..6113ea1d3 100644 --- a/fluxer_api/src/api/user/services/UserAccountService.ts +++ b/fluxer_api/src/api/user/services/UserAccountService.ts @@ -2,8 +2,8 @@ import type {ApiContext} from '@app/api/ApiContext'; import type {SudoVerificationResult} from '@app/api/auth/services/SudoVerificationService'; -import type {IConnectionRepository} from '@app/api/connection/IConnectionRepository'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {ConnectionRepository} from '@app/api/connection/ConnectionRepository'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {GuildService} from '@app/api/guild/services/GuildService'; import {GuildMemberSearchIndexService} from '@app/api/guild/services/member/GuildMemberSearchIndexService'; import type {IDiscriminatorService} from '@app/api/infrastructure/DiscriminatorService'; @@ -15,10 +15,7 @@ import type {LimitConfigService} from '@app/api/limits/LimitConfigService'; import type {AuthSession} from '@app/api/models/AuthSession'; import type {User} from '@app/api/models/User'; import {enqueueStripeCustomerEmailSync} from '@app/api/stripe/StripeCustomer'; -import type {IUserAccountRepository} from '@app/api/user/repositories/IUserAccountRepository'; -import type {IUserChannelRepository} from '@app/api/user/repositories/IUserChannelRepository'; -import type {IUserRelationshipRepository} from '@app/api/user/repositories/IUserRelationshipRepository'; -import type {IUserSettingsRepository} from '@app/api/user/repositories/IUserSettingsRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {UserAccountLifecycleService} from '@app/api/user/services/UserAccountLifecycleService'; import {UserAccountLookupService} from '@app/api/user/services/UserAccountLookupService'; import {UserAccountNotesService} from '@app/api/user/services/UserAccountNotesService'; @@ -50,12 +47,6 @@ interface UpdateUserResult { authSessionReplacement: AuthSessionReplacement | null; } -interface UserAccountRepository - extends IUserAccountRepository, - IUserSettingsRepository, - IUserRelationshipRepository, - IUserChannelRepository {} - export class UserAccountService { readonly lookupService: UserAccountLookupService; private readonly profileService: UserAccountProfileService; @@ -64,8 +55,8 @@ export class UserAccountService { readonly notesService: UserAccountNotesService; readonly lifecycleService: UserAccountLifecycleService; readonly updatePropagator: UserAccountUpdatePropagator; - private readonly userAccountRepository: UserAccountRepository; - private readonly guildRepository: IGuildRepositoryAggregate; + private readonly userAccountRepository: UserRepository; + private readonly guildRepository: GuildRepository; private readonly searchIndexService: GuildMemberSearchIndexService; constructor( @@ -73,11 +64,11 @@ export class UserAccountService { userCacheService: UserCacheService, guildService: GuildService, entityAssetService: EntityAssetService, - guildRepository: IGuildRepositoryAggregate, + guildRepository: GuildRepository, discriminatorService: IDiscriminatorService, kvDeletionQueue: KVAccountDeletionQueueService, private readonly contactChangeLogService: UserContactChangeLogService, - connectionRepository: IConnectionRepository, + connectionRepository: ConnectionRepository, readonly limitConfigService: LimitConfigService, ) { const { diff --git a/fluxer_api/src/api/user/services/UserAccountSettingsService.ts b/fluxer_api/src/api/user/services/UserAccountSettingsService.ts index 7172af81f..b16800322 100644 --- a/fluxer_api/src/api/user/services/UserAccountSettingsService.ts +++ b/fluxer_api/src/api/user/services/UserAccountSettingsService.ts @@ -10,7 +10,7 @@ import { } from '@app/api/BrandedTypes'; import type {ChannelOverride, UserGuildSettingsRow} from '@app/api/database/types/UserTypes'; import {type ThreadViewer, viewerActive} from '@app/api/experiment/ChannelThreadsGate'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {UserCacheService} from '@app/api/infrastructure/UserCacheService'; import type {LimitConfigService} from '@app/api/limits/LimitConfigService'; @@ -19,9 +19,7 @@ import type {GuildChannelOverride} from '@app/api/models/GuildChannelOverride'; import type {User} from '@app/api/models/User'; import type {UserGuildSettings} from '@app/api/models/UserGuildSettings'; import type {UserSettings} from '@app/api/models/UserSettings'; -import type {IUserAccountRepository} from '@app/api/user/repositories/IUserAccountRepository'; -import type {IUserRelationshipRepository} from '@app/api/user/repositories/IUserRelationshipRepository'; -import type {IUserSettingsRepository} from '@app/api/user/repositories/IUserSettingsRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {CustomStatusValidator} from '@app/api/user/services/CustomStatusValidator'; import type {UserAccountUpdatePropagator} from '@app/api/user/services/UserAccountUpdatePropagator'; import {getCachedUserPartialResponse} from '@app/api/user/UserCacheHelpers'; @@ -58,13 +56,13 @@ import type { } from '@fluxer/schema/src/domains/user/UserRequestSchemas'; interface UserAccountSettingsServiceDeps { - userAccountRepository: IUserAccountRepository; - userSettingsRepository: IUserSettingsRepository; - userRelationshipRepository: IUserRelationshipRepository; + userAccountRepository: UserRepository; + userSettingsRepository: UserRepository; + userRelationshipRepository: UserRepository; updatePropagator: UserAccountUpdatePropagator; gatewayService: IGatewayService; userCacheService: UserCacheService; - guildRepository: IGuildRepositoryAggregate; + guildRepository: GuildRepository; limitConfigService: LimitConfigService; } diff --git a/fluxer_api/src/api/user/services/UserAccountUpdatePropagator.ts b/fluxer_api/src/api/user/services/UserAccountUpdatePropagator.ts index 5734437d0..3741eb73d 100644 --- a/fluxer_api/src/api/user/services/UserAccountUpdatePropagator.ts +++ b/fluxer_api/src/api/user/services/UserAccountUpdatePropagator.ts @@ -1,14 +1,14 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {UserID} from '@app/api/BrandedTypes'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {IMediaService} from '@app/api/infrastructure/IMediaService'; import type {UserCacheService} from '@app/api/infrastructure/UserCacheService'; import type {User} from '@app/api/models/User'; import type {UserGuildSettings} from '@app/api/models/UserGuildSettings'; import type {UserSettings} from '@app/api/models/UserSettings'; -import type {IUserAccountRepository} from '@app/api/user/repositories/IUserAccountRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {BaseUserUpdatePropagator} from '@app/api/user/services/BaseUserUpdatePropagator'; import {propagatePartialUserChange} from '@app/api/user/services/PartialUserChangePropagation'; import {dispatchUserGuildSettingsUpdate} from '@app/api/user/UserGuildSettingsThreadView'; @@ -18,8 +18,8 @@ interface UserAccountUpdatePropagatorDeps { userCacheService: UserCacheService; gatewayService: IGatewayService; mediaService: IMediaService; - userRepository: IUserAccountRepository; - guildRepository: IGuildRepositoryAggregate; + userRepository: UserRepository; + guildRepository: GuildRepository; } export class UserAccountUpdatePropagator extends BaseUserUpdatePropagator { diff --git a/fluxer_api/src/api/user/services/UserAuthRequestService.ts b/fluxer_api/src/api/user/services/UserAuthRequestService.ts index 98523dd47..557f34f08 100644 --- a/fluxer_api/src/api/user/services/UserAuthRequestService.ts +++ b/fluxer_api/src/api/user/services/UserAuthRequestService.ts @@ -6,7 +6,7 @@ import {requireEmailVerified} from '@app/api/auth/EmailVerificationUtils'; import {visibleWebAuthnCredentials} from '@app/api/auth/services/PasskeyRelyingParty'; import type {SudoVerificationResult} from '@app/api/auth/services/SudoVerificationService'; import type {User} from '@app/api/models/User'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import * as UserAuth from '@app/api/user/services/UserAuth'; import {mapUserToPrivateResponse, mapWebAuthnCredentialToResponse} from '@app/api/user/UserMappers'; import type { @@ -53,7 +53,7 @@ interface UserAuthWebAuthnDeleteRequest { export class UserAuthRequestService { constructor( private apiContext: ApiContext, - private userRepository: IUserRepository, + private userRepository: UserRepository, ) {} async enableTotp({ diff --git a/fluxer_api/src/api/user/services/UserChannelService.ts b/fluxer_api/src/api/user/services/UserChannelService.ts index b03a3ad01..7e9778acf 100644 --- a/fluxer_api/src/api/user/services/UserChannelService.ts +++ b/fluxer_api/src/api/user/services/UserChannelService.ts @@ -5,7 +5,7 @@ import {requireEmailVerified} from '@app/api/auth/EmailVerificationUtils'; import type {ChannelID, UserID} from '@app/api/BrandedTypes'; import {createChannelID, createMessageID, createUserID} from '@app/api/BrandedTypes'; import {mapChannelToResponse} from '@app/api/channel/ChannelMappers'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {ChannelService} from '@app/api/channel/services/ChannelService'; import {dispatchMessageCreateBroadcast} from '@app/api/channel/services/message/MessageGatewayDispatch'; import { @@ -27,9 +27,7 @@ import type {Message} from '@app/api/models/Message'; import type {User} from '@app/api/models/User'; import {assertAccountNotLimited} from '@app/api/user/AccountLimit'; import {assertMayStartConversation} from '@app/api/user/NewConversationLimit'; -import type {IUserAccountRepository} from '@app/api/user/repositories/IUserAccountRepository'; -import type {IUserChannelRepository} from '@app/api/user/repositories/IUserChannelRepository'; -import type {IUserRelationshipRepository} from '@app/api/user/repositories/IUserRelationshipRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {isDirectDeliverySuppressed} from '@app/api/user/UserHelpers'; import type {UserPermissionUtils} from '@app/api/utils/UserPermissionUtils'; import {ChannelTypes, MessageTypes} from '@fluxer/constants/src/ChannelConstants'; @@ -50,12 +48,10 @@ import type {MessageResponse} from '@fluxer/schema/src/domains/message/MessageRe import type {CreatePrivateChannelRequest} from '@fluxer/schema/src/domains/user/UserRequestSchemas'; import * as BucketUtils from '@fluxer/snowflake/src/SnowflakeBuckets'; -interface UserChannelRepository extends IUserAccountRepository, IUserChannelRepository, IUserRelationshipRepository {} - export class UserChannelService { - private readonly userRepository: UserChannelRepository; + private readonly userRepository: UserRepository; private readonly channelService: ChannelService; - private readonly channelRepository: IChannelRepository; + private readonly channelRepository: ChannelRepository; private readonly gatewayService: IGatewayService; private readonly snowflakeService: ISnowflakeService; private readonly userPermissionUtils: UserPermissionUtils; @@ -64,15 +60,15 @@ export class UserChannelService { constructor( apiContext: ApiContext, channelService: ChannelService, - channelRepository: IChannelRepository, + channelRepository: ChannelRepository, userPermissionUtils: UserPermissionUtils, limitConfigService: LimitConfigService, ); constructor( - userRepository: UserChannelRepository, + userRepository: UserRepository, channelService: ChannelService, - channelRepository: IChannelRepository, + channelRepository: ChannelRepository, gatewayService: IGatewayService, snowflakeService: ISnowflakeService, userPermissionUtils: UserPermissionUtils, @@ -84,14 +80,14 @@ export class UserChannelService { | [ apiContext: ApiContext, channelService: ChannelService, - channelRepository: IChannelRepository, + channelRepository: ChannelRepository, userPermissionUtils: UserPermissionUtils, limitConfigService: LimitConfigService, ] | [ - userRepository: UserChannelRepository, + userRepository: UserRepository, channelService: ChannelService, - channelRepository: IChannelRepository, + channelRepository: ChannelRepository, gatewayService: IGatewayService, snowflakeService: ISnowflakeService, userPermissionUtils: UserPermissionUtils, @@ -172,7 +168,7 @@ export class UserChannelService { user: callingUser, targetId: recipientId, users: this.userRepository, - messages: this.channelRepository, + messages: this.channelRepository.messages, }); const suppressed = isDirectDeliverySuppressed(callingUser); const channel = await this.openOneToOneDMChannel({userId, recipientId, suppressed, userCacheService, requestCache}); @@ -531,14 +527,14 @@ export class UserChannelService { indexed_at: null, version: 1, }; - const newChannel = await this.channelRepository.upsert(channelData); + const newChannel = await this.channelRepository.channelData.upsert(channelData); for (const recipientId of allRecipients) { await this.userRepository.openPrivateChannelForUser(recipientId, newChannel); } const systemMessages: Array = []; for (const recipientId of recipientIds) { const messageId = createMessageID(await this.snowflakeService.generateForChannel(channelId)); - const message = await this.channelRepository.upsertMessage({ + const message = await this.channelRepository.messages.upsertMessage({ channel_id: channelId, bucket: BucketUtils.makeBucket(messageId), message_id: messageId, diff --git a/fluxer_api/src/api/user/services/UserContentService.test.ts b/fluxer_api/src/api/user/services/UserContentService.test.ts index cb790ee1f..4163ff074 100644 --- a/fluxer_api/src/api/user/services/UserContentService.test.ts +++ b/fluxer_api/src/api/user/services/UserContentService.test.ts @@ -3,7 +3,7 @@ import type {ApiContext} from '@app/api/ApiContext'; import type {ChannelID, GuildID, MessageID, UserID} from '@app/api/BrandedTypes'; import {createChannelID, createGuildID, createMessageID, createUserID} from '@app/api/BrandedTypes'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {ChannelService} from '@app/api/channel/services/ChannelService'; import {SYSTEM_THREAD_VIEWER, syncChannelThreadsConfig} from '@app/api/experiment/ChannelThreadsGate'; import type {KVBulkMessageDeletionQueueService} from '@app/api/infrastructure/KVBulkMessageDeletionQueueService'; @@ -125,8 +125,10 @@ function createUserContentService({ (stored ?? readable).map((entry) => `${entry.channelId.toString()}:${entry.messageId.toString()}`), ); const channelRepository = { - listChannels: async (channelIds: Array) => - channels.filter((channel) => channelIds.some((channelId) => channelId === channel.id)), + channelData: { + listChannels: async (channelIds: Array) => + channels.filter((channel) => channelIds.some((channelId) => channelId === channel.id)), + }, messages: { getMessage: async (channelId: ChannelID, messageId: MessageID) => storedKeys.has(`${channelId.toString()}:${messageId.toString()}`) ? makeMessage(channelId, messageId) : null, @@ -136,7 +138,7 @@ function createUserContentService({ {services: {users: userRepository, gateway: {}, worker: {}, snowflake: {}}} as unknown as ApiContext, {} as unknown as UserCacheService, channelService as unknown as ChannelService, - channelRepository as unknown as IChannelRepository, + channelRepository as unknown as ChannelRepository, {} as unknown as KVBulkMessageDeletionQueueService, {} as unknown as LimitConfigService, ); @@ -552,7 +554,7 @@ describe('gateway dispatches after the write', () => { } as unknown as ApiContext, {} as unknown as UserCacheService, channelService as unknown as ChannelService, - {} as unknown as IChannelRepository, + {} as unknown as ChannelRepository, {} as unknown as KVBulkMessageDeletionQueueService, {getConfigSnapshot: () => null} as unknown as LimitConfigService, ); @@ -787,7 +789,7 @@ describe('bookmark ceiling', () => { } as unknown as ApiContext, {} as unknown as UserCacheService, channelService as unknown as ChannelService, - {} as unknown as IChannelRepository, + {} as unknown as ChannelRepository, {} as unknown as KVBulkMessageDeletionQueueService, {getConfigSnapshot: () => snapshot} as unknown as LimitConfigService, ); diff --git a/fluxer_api/src/api/user/services/UserContentService.ts b/fluxer_api/src/api/user/services/UserContentService.ts index c7566604f..25d3388e8 100644 --- a/fluxer_api/src/api/user/services/UserContentService.ts +++ b/fluxer_api/src/api/user/services/UserContentService.ts @@ -5,7 +5,7 @@ import type {Readable} from 'node:stream'; import type {ApiContext} from '@app/api/ApiContext'; import {type ChannelID, createChannelID, createUserID, type MessageID, type UserID} from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {ChannelService} from '@app/api/channel/services/ChannelService'; import {createMessageResponseDataService} from '@app/api/channel/services/message/MessageResponseDataService'; import { @@ -33,8 +33,7 @@ import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware'; import type {Channel} from '@app/api/models/Channel'; import type {Message} from '@app/api/models/Message'; import {PushSubscription} from '@app/api/models/PushSubscription'; -import type {IUserAccountRepository} from '@app/api/user/repositories/IUserAccountRepository'; -import type {IUserContentRepository} from '@app/api/user/repositories/IUserContentRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {BaseUserUpdatePropagator} from '@app/api/user/services/BaseUserUpdatePropagator'; import {verifyHarvestDownloadToken} from '@app/api/user/services/HarvestDownloadToken'; import {buildHarvestDownloadUrl} from '@app/api/user/services/HarvestDownloadUrl'; @@ -104,8 +103,6 @@ interface UnregisterMobileDeviceParams { device: UnregisterMobileDeviceRequest; } -interface UserContentRepository extends IUserAccountRepository, IUserContentRepository {} - const WEB_PUSH_PLATFORM = 'web_push' as const; const DEFAULT_MOBILE_APP_ID = 'stable'; const DEFAULT_APNS_PROVIDER_ENVIRONMENT = 'production'; @@ -192,7 +189,7 @@ const isUnreachableEntityError = (error: unknown): boolean => export class UserContentService { private readonly updatePropagator: BaseUserUpdatePropagator; - private readonly userRepository: UserContentRepository; + private readonly userRepository: UserRepository; private readonly gatewayService: IGatewayService; private readonly workerService: IWorkerService; private readonly snowflakeService: ISnowflakeService; @@ -202,7 +199,7 @@ export class UserContentService { apiContext: ApiContext, userCacheService: UserCacheService, private channelService: ChannelService, - private channelRepository: IChannelRepository, + private channelRepository: ChannelRepository, private bulkMessageDeletionQueue: KVBulkMessageDeletionQueueService, private limitConfigService: LimitConfigService, ) { @@ -405,7 +402,7 @@ export class UserContentService { .filter(([, messages]) => messages === null) .map(([channelId]) => createChannelID(BigInt(channelId))); if (unreachable.length === 0) return new Set(); - const channels = await this.channelRepository.listChannels(unreachable); + const channels = await this.channelRepository.channelData.listChannels(unreachable); return new Set( channels .filter( @@ -886,7 +883,7 @@ export class UserContentService { const channels = new Set(); let messageCount = 0; while (true) { - const messageRefs = await this.channelRepository.listMessagesByAuthor(userId, CHUNK_SIZE, lastMessageId); + const messageRefs = await this.channelRepository.messages.listMessagesByAuthor(userId, CHUNK_SIZE, lastMessageId); if (messageRefs.length === 0) { break; } @@ -969,7 +966,7 @@ export class UserContentService { ): Promise<{responses: Array; channelById: Map}> { if (messages.length === 0) return {responses: [], channelById: new Map()}; const channelIds = Array.from(new Set(messages.map((message) => message.channelId.toString()))); - const channels = await this.channelRepository.listChannels( + const channels = await this.channelRepository.channelData.listChannels( channelIds.map((channelId) => createChannelID(BigInt(channelId))), ); const channelById = new Map(channels.map((channel) => [channel.id.toString(), channel] as const)); diff --git a/fluxer_api/src/api/user/services/UserDeletionService.ts b/fluxer_api/src/api/user/services/UserDeletionService.ts index 9d8690df2..3d3b4bc18 100644 --- a/fluxer_api/src/api/user/services/UserDeletionService.ts +++ b/fluxer_api/src/api/user/services/UserDeletionService.ts @@ -8,7 +8,7 @@ import {Config} from '@app/api/Config'; import {mapChannelToResponse} from '@app/api/channel/ChannelMappers'; import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import {UserMessageDeletionService} from '@app/api/channel/services/message/UserMessageDeletionService'; -import type {IConnectionRepository} from '@app/api/connection/IConnectionRepository'; +import type {ConnectionRepository} from '@app/api/connection/ConnectionRepository'; import type {FavoriteMemeRepository} from '@app/api/favorite_meme/FavoriteMemeRepository'; import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import {emitActivity} from '@app/api/infrastructure/activity/ActivityEvents'; @@ -57,7 +57,7 @@ interface UserDeletionDependencies { stripe: Stripe | null; applicationRepository: ApplicationRepository; workerService: IWorkerService; - connectionRepository: IConnectionRepository; + connectionRepository: ConnectionRepository; storeEntitlementService: StoreEntitlementService; } @@ -293,7 +293,7 @@ export async function processUserDeletion( newOwnerId = Array.from(updatedRecipientIds)[0]; } if (updatedRecipientIds.size === 0) { - await channelRepository.delete(channel.id); + await channelRepository.channelData.delete(channel.id); await userRepository.closeDmForUser(userId, channel.id); const channelResponse = await mapChannelToResponse({ channel, @@ -311,7 +311,7 @@ export async function processUserDeletion( } const updatedNicknames = new Map(channel.nicknames); updatedNicknames.delete(userId.toString()); - await channelRepository.upsert({ + await channelRepository.channelData.upsert({ ...channel.toRow(), owner_id: newOwnerId, recipient_ids: updatedRecipientIds, @@ -319,7 +319,7 @@ export async function processUserDeletion( }); await userRepository.closeDmForUser(userId, channel.id); const messageId = createMessageID(await snowflakeService.generateForChannel(channel.id)); - await channelRepository.upsertMessage({ + await channelRepository.messages.upsertMessage({ channel_id: channel.id, bucket: BucketUtils.makeBucket(messageId), message_id: messageId, @@ -395,12 +395,16 @@ export async function processUserDeletion( let lastMessageId: MessageID | undefined; let processedCount = 0; while (true) { - const messagesToAnonymize = await channelRepository.listMessagesByAuthor(userId, CHUNK_SIZE, lastMessageId); + const messagesToAnonymize = await channelRepository.messages.listMessagesByAuthor( + userId, + CHUNK_SIZE, + lastMessageId, + ); if (messagesToAnonymize.length === 0) { break; } for (const {channelId, messageId} of messagesToAnonymize) { - await channelRepository.anonymizeMessage(channelId, messageId, deletedUserId); + await channelRepository.messages.anonymizeMessage(channelId, messageId, deletedUserId); } processedCount += messagesToAnonymize.length; lastMessageId = messagesToAnonymize[messagesToAnonymize.length - 1].messageId; diff --git a/fluxer_api/src/api/user/services/UserRelationshipService.ts b/fluxer_api/src/api/user/services/UserRelationshipService.ts index 564325e03..75f06bd68 100644 --- a/fluxer_api/src/api/user/services/UserRelationshipService.ts +++ b/fluxer_api/src/api/user/services/UserRelationshipService.ts @@ -14,10 +14,7 @@ import {getInstanceConfigRepository} from '@app/api/middleware/ServiceSingletons import type {Relationship} from '@app/api/models/Relationship'; import type {User} from '@app/api/models/User'; import {assertMayStartConversation} from '@app/api/user/NewConversationLimit'; -import type {IUserAccountRepository} from '@app/api/user/repositories/IUserAccountRepository'; -import type {IUserChannelRepository} from '@app/api/user/repositories/IUserChannelRepository'; -import type {IUserRelationshipRepository} from '@app/api/user/repositories/IUserRelationshipRepository'; -import type {IUserSettingsRepository} from '@app/api/user/repositories/IUserSettingsRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {getCachedUserPartialResponse} from '@app/api/user/UserCacheHelpers'; import {isDirectDeliverySuppressed} from '@app/api/user/UserHelpers'; import {mapRelationshipToResponse} from '@app/api/user/UserMappers'; @@ -44,12 +41,6 @@ import type { } from '@fluxer/schema/src/domains/user/UserRequestSchemas'; import {extractTimestamp} from '@fluxer/snowflake/src/SnowflakeUtils'; -interface UserRelationshipRepository - extends IUserAccountRepository, - IUserChannelRepository, - IUserRelationshipRepository, - IUserSettingsRepository {} - function emitUserBlocked(userId: UserID, targetId: UserID): void { void emitActivity('user_blocked', targetId.toString(), { blocker_id: userId.toString(), @@ -66,7 +57,7 @@ function emitFriendRequest(userId: UserID, targetId: UserID, delivered: boolean) } export class UserRelationshipService { - private readonly userRepository: UserRelationshipRepository; + private readonly userRepository: UserRepository; private readonly gatewayService: IGatewayService; constructor( diff --git a/fluxer_api/src/api/user/services/UserService.ts b/fluxer_api/src/api/user/services/UserService.ts index 65d17b1f1..6e32de7f2 100644 --- a/fluxer_api/src/api/user/services/UserService.ts +++ b/fluxer_api/src/api/user/services/UserService.ts @@ -1,10 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {ApiContext} from '@app/api/ApiContext'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {ChannelService} from '@app/api/channel/services/ChannelService'; -import type {IConnectionRepository} from '@app/api/connection/IConnectionRepository'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {ConnectionRepository} from '@app/api/connection/ConnectionRepository'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {GuildService} from '@app/api/guild/services/GuildService'; import type {IDiscriminatorService} from '@app/api/infrastructure/DiscriminatorService'; import type {EntityAssetService} from '@app/api/infrastructure/EntityAssetService'; @@ -29,16 +29,16 @@ export class UserService { apiContext: ApiContext, userCacheService: UserCacheService, channelService: ChannelService, - channelRepository: IChannelRepository, + channelRepository: ChannelRepository, guildService: GuildService, entityAssetService: EntityAssetService, discriminatorService: IDiscriminatorService, - guildRepository: IGuildRepositoryAggregate, + guildRepository: GuildRepository, userPermissionUtils: UserPermissionUtils, kvDeletionQueue: KVAccountDeletionQueueService, bulkMessageDeletionQueue: KVBulkMessageDeletionQueueService, contactChangeLogService: UserContactChangeLogService, - connectionRepository: IConnectionRepository, + connectionRepository: ConnectionRepository, limitConfigService: LimitConfigService, ) { this.accountService = new UserAccountService( diff --git a/fluxer_api/src/api/user/services/__tests__/PartialUserChangePropagation.test.ts b/fluxer_api/src/api/user/services/__tests__/PartialUserChangePropagation.test.ts index aa8d0ab3d..7cedc6c56 100644 --- a/fluxer_api/src/api/user/services/__tests__/PartialUserChangePropagation.test.ts +++ b/fluxer_api/src/api/user/services/__tests__/PartialUserChangePropagation.test.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {createGuildID, createUserID} from '@app/api/BrandedTypes'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {UserCacheService} from '@app/api/infrastructure/UserCacheService'; import type {GuildMember} from '@app/api/models/GuildMember'; @@ -57,7 +57,7 @@ function createDeps(guildIds: Array<(typeof GUILD_IDS)[number]> = GUILD_IDS) { } as unknown as UserCacheService, gatewayService: {dispatchGuild} as unknown as IGatewayService, userRepository: {getUserGuildIds}, - guildRepository: {getMember} as unknown as IGuildRepositoryAggregate, + guildRepository: {getMember} as unknown as GuildRepository, }; return {deps, dispatchGuild, setUserPartialResponseFromUser, getUserGuildIds, getMember}; } diff --git a/fluxer_api/src/api/user/tests/AccountDeleteAutoCancelOnLogin.test.ts b/fluxer_api/src/api/user/tests/AccountDeleteAutoCancelOnLogin.test.ts deleted file mode 100644 index 0930c47b4..000000000 --- a/fluxer_api/src/api/user/tests/AccountDeleteAutoCancelOnLogin.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {createTestAccount, loginAccount} from '@app/api/auth/tests/AuthTestUtils'; -import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness'; -import {deleteAccount, expectDataExists} from '@app/api/user/tests/UserTestUtils'; -import {beforeEach, describe, expect, test} from 'vitest'; - -describe('Account Delete Auto Cancel on Login', () => { - let harness: ApiTestHarness; - beforeEach(async () => { - harness = await createApiTestHarness(); - }); - test('logging in after account deletion cancels the deletion', async () => { - const account = await createTestAccount(harness); - await deleteAccount(harness, account.token, account.password); - const login = await loginAccount(harness, account); - expect(login.token).not.toBe(''); - const data = await expectDataExists(harness, account.userId); - expect(data.hasSelfDeletedFlag).toBe(false); - expect(data.pendingDeletionAt).toBeNull(); - }); -}); diff --git a/fluxer_api/src/api/user/tests/PasswordChangeFlow.test.ts b/fluxer_api/src/api/user/tests/PasswordChangeFlow.test.ts index b4f3488ee..d7508bcfb 100644 --- a/fluxer_api/src/api/user/tests/PasswordChangeFlow.test.ts +++ b/fluxer_api/src/api/user/tests/PasswordChangeFlow.test.ts @@ -104,7 +104,7 @@ describe('PasswordChangeFlow', () => { const emails = await listTestEmails(harness, {recipient: account.email}); const verificationEmail = findLastTestEmail(emails, 'password_change_verification'); expect(verificationEmail).not.toBeNull(); - expect(verificationEmail!.metadata.code).toBeDefined(); + expect(verificationEmail!.metadata.code).toMatch(/^[A-Z0-9]{4}-[A-Z0-9]{4}$/); }); test('requires authentication', async () => { await createBuilderWithoutAuth(harness) @@ -329,28 +329,4 @@ describe('PasswordChangeFlow', () => { .execute(); }); }); - describe('full flow', () => { - test('start, verify, and complete password change end-to-end', async () => { - const account = await createTestAccount(harness); - const startResult = await startPasswordChange(harness, account.token); - expect(startResult.ticket).toBeDefined(); - expect(startResult.code_expires_at).toBeDefined(); - const code = await getVerificationCode(harness, account.email); - expect(code).toMatch(/^[A-Z0-9]{4}-[A-Z0-9]{4}$/); - const verifyResult = await verifyPasswordChangeCode(harness, account.token, startResult.ticket, code); - expect(verifyResult.verification_proof).toBeDefined(); - await completePasswordChange( - harness, - account.token, - startResult.ticket, - verifyResult.verification_proof, - TEST_CREDENTIALS.ALT_PASSWORD_1, - ); - const login = await loginUser(harness, { - email: account.email, - password: TEST_CREDENTIALS.ALT_PASSWORD_1, - }); - expect('token' in login).toBe(true); - }); - }); }); diff --git a/fluxer_api/src/api/user/utils/GuildFolderUtils.ts b/fluxer_api/src/api/user/utils/GuildFolderUtils.ts index bb017c414..fd38fa830 100644 --- a/fluxer_api/src/api/user/utils/GuildFolderUtils.ts +++ b/fluxer_api/src/api/user/utils/GuildFolderUtils.ts @@ -3,7 +3,7 @@ import type {GuildID, UserID} from '@app/api/BrandedTypes'; import type {GuildFolder} from '@app/api/database/types/UserTypes'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {mapUserSettingsToResponse} from '@app/api/user/UserMappers'; import {type GuildFolderIcon, UNCATEGORIZED_FOLDER_ID} from '@fluxer/constants/src/UserConstants'; @@ -87,7 +87,7 @@ function rankOccurrence( export async function removeGuildFromUserFolders(params: { userId: UserID; guildId: GuildID; - userRepository: IUserRepository; + userRepository: UserRepository; gatewayService: IGatewayService; }): Promise { const {userId, guildId, userRepository, gatewayService} = params; diff --git a/fluxer_api/src/api/utils/EmojiUtils.ts b/fluxer_api/src/api/utils/EmojiUtils.ts index 446a817ee..830288dd2 100644 --- a/fluxer_api/src/api/utils/EmojiUtils.ts +++ b/fluxer_api/src/api/utils/EmojiUtils.ts @@ -2,16 +2,16 @@ import type {EmojiID, GuildID, UserID, WebhookID} from '@app/api/BrandedTypes'; import {createEmojiID} from '@app/api/BrandedTypes'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {LimitConfigService} from '@app/api/limits/LimitConfigService'; import {resolveLimitSafe} from '@app/api/limits/LimitConfigUtils'; import {createLimitMatchContext} from '@app/api/limits/LimitMatchContextBuilder'; import type {GuildEmoji} from '@app/api/models/GuildEmoji'; -import type {IUserAccountRepository} from '@app/api/user/repositories/IUserAccountRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {Permissions} from '@fluxer/constants/src/ChannelConstants'; -type EmojiGuildRepository = Pick; -type EmojiUserRepository = Pick; +type EmojiGuildRepository = Pick; +type EmojiUserRepository = Pick; const CUSTOM_EMOJI_MARKDOWN_REGEX = /<(a)?:([^:]+):(\d+)>/g; const CUSTOM_EMOJI_MARKDOWN_REGEX_GLOBAL = new RegExp(CUSTOM_EMOJI_MARKDOWN_REGEX.source, 'g'); diff --git a/fluxer_api/src/api/utils/IpUtils.ts b/fluxer_api/src/api/utils/IpUtils.ts index 43cb39adf..1732b7d82 100644 --- a/fluxer_api/src/api/utils/IpUtils.ts +++ b/fluxer_api/src/api/utils/IpUtils.ts @@ -2,7 +2,7 @@ import dns from 'node:dns'; import {Config} from '@app/api/Config'; -import {extractClientIp} from '@fluxer/ip_utils/src/ClientIp'; +import {extractConfiguredClientIp} from '@app/api/utils/RequestClientIp'; import type {ICacheService} from '@pkgs/cache/src/ICacheService'; import type {GeoipResult} from '@pkgs/geoip/src/GeoipLookup'; import {formatGeoipLocation, lookupGeoipByIp} from '@pkgs/geoip/src/GeoipLookup'; @@ -16,17 +16,10 @@ interface GetIpAddressReverseOptions { cacheTtlSeconds?: number; } -function resolveRequestClientIp(req: Request): string | null { - return extractClientIp(req, { - trustClientIpHeader: Config.proxy.trust_client_ip_header, - clientIpHeaderName: Config.proxy.client_ip_header, - }); -} - export async function lookupGeoip(req: Request): Promise; export async function lookupGeoip(ip: string): Promise; export async function lookupGeoip(input: string | Request): Promise { - const ip = typeof input === 'string' ? input : resolveRequestClientIp(input); + const ip = typeof input === 'string' ? input : extractConfiguredClientIp(input); if (!ip) { return {countryCode: null, normalizedIp: null, city: null, region: null, countryName: null}; } diff --git a/fluxer_api/src/api/utils/RequestClientIp.ts b/fluxer_api/src/api/utils/RequestClientIp.ts index d7e965108..572cbb1d5 100644 --- a/fluxer_api/src/api/utils/RequestClientIp.ts +++ b/fluxer_api/src/api/utils/RequestClientIp.ts @@ -1,8 +1,14 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {Config} from '@app/api/Config'; +import type {APIConfig} from '@app/api/config/APIConfig'; import type {HonoEnv} from '@app/api/types/HonoEnv'; -import {extractClientIp, MissingClientIpError, resolveClientIpHeaderName} from '@fluxer/ip_utils/src/ClientIp'; +import { + extractClientIp, + MissingClientIpError, + requireClientIp, + resolveClientIpHeaderName, +} from '@fluxer/ip_utils/src/ClientIp'; import type {Context} from 'hono'; export interface ClientIpResolution { @@ -32,11 +38,20 @@ export function resolveClientIpWithOptions(ctx: Context, options: Clien return ip; } +function proxyClientIpOptions(proxy: APIConfig['proxy']): ClientIpResolutionOptions { + return {trustClientIpHeader: proxy.trust_client_ip_header, clientIpHeaderName: proxy.client_ip_header}; +} + +export function extractConfiguredClientIp(request: Request): string | null { + return extractClientIp(request, proxyClientIpOptions(Config.proxy)); +} + +export function requireConfiguredClientIp(request: Request, proxy: APIConfig['proxy']): string { + return requireClientIp(request, proxyClientIpOptions(proxy)); +} + export function getRequestClientIp(ctx: Context): string | null { - return resolveClientIpWithOptions(ctx, { - trustClientIpHeader: Config.proxy.trust_client_ip_header, - clientIpHeaderName: Config.proxy.client_ip_header, - }); + return resolveClientIpWithOptions(ctx, proxyClientIpOptions(Config.proxy)); } export function requireRequestClientIp(ctx: Context): string { diff --git a/fluxer_api/src/api/utils/UserPermissionUtils.ts b/fluxer_api/src/api/utils/UserPermissionUtils.ts index 0de6570ed..f6ceffe3b 100644 --- a/fluxer_api/src/api/utils/UserPermissionUtils.ts +++ b/fluxer_api/src/api/utils/UserPermissionUtils.ts @@ -1,16 +1,16 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {GuildID, UserID} from '@app/api/BrandedTypes'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {Guild} from '@app/api/models/Guild'; import type {Relationship} from '@app/api/models/Relationship'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {FriendSourceFlags, GroupDmAddPermissionFlags, RelationshipTypes} from '@fluxer/constants/src/UserConstants'; import {MissingAccessError} from '@fluxer/errors/src/domains/core/MissingAccessError'; import {FriendRequestBlockedError} from '@fluxer/errors/src/domains/user/FriendRequestBlockedError'; -type UserPermissionRepository = Pick; -type GuildPermissionRepository = Pick; +type UserPermissionRepository = Pick; +type GuildPermissionRepository = Pick; export class UserPermissionUtils { constructor( diff --git a/fluxer_api/src/api/voice/IVoiceRepository.ts b/fluxer_api/src/api/voice/IVoiceRepository.ts deleted file mode 100644 index e0512c4d8..000000000 --- a/fluxer_api/src/api/voice/IVoiceRepository.ts +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {VoiceRegionRecord, VoiceRegionWithServers, VoiceServerRecord} from '@app/api/voice/VoiceModel'; - -export interface IVoiceRepository { - listRegions(): Promise>; - listRegionsWithServers(): Promise>; - getRegion(id: string): Promise; - getRegionWithServers(id: string): Promise; - upsertRegion(region: VoiceRegionRecord): Promise; - deleteRegion(regionId: string): Promise; - createRegion(region: Omit): Promise; - listServersForRegion(regionId: string): Promise>; - listServers(regionId: string): Promise>; - getServer(regionId: string, serverId: string): Promise; - createServer(server: Omit): Promise; - upsertServer(server: VoiceServerRecord): Promise; - deleteServer(regionId: string, serverId: string): Promise; -} diff --git a/fluxer_api/src/api/voice/VoiceRepository.ts b/fluxer_api/src/api/voice/VoiceRepository.ts index fca8613fd..7e417a39d 100644 --- a/fluxer_api/src/api/voice/VoiceRepository.ts +++ b/fluxer_api/src/api/voice/VoiceRepository.ts @@ -9,7 +9,6 @@ import { type VoiceRegionRow, type VoiceServerRow, } from '@app/api/database/types/VoiceTypes'; -import type {IVoiceRepository} from '@app/api/voice/IVoiceRepository'; import type {VoiceRegionRecord, VoiceRegionWithServers, VoiceServerRecord} from '@app/api/voice/VoiceModel'; function toIterable(value: unknown): Array { @@ -40,7 +39,7 @@ const GET_SERVER_CQL = VoiceServers.selectCql({ where: [VoiceServers.where.eq('region_id'), VoiceServers.where.eq('server_id')], }); -export class VoiceRepository implements IVoiceRepository { +export class VoiceRepository { async listRegions(): Promise> { const rows = await fetchMany(LIST_REGIONS_CQL, {}); return rows.map((row) => this.mapRegionRow(row)); diff --git a/fluxer_api/src/api/voice/VoiceService.ts b/fluxer_api/src/api/voice/VoiceService.ts index 15f0d2ccf..1e70b8111 100644 --- a/fluxer_api/src/api/voice/VoiceService.ts +++ b/fluxer_api/src/api/voice/VoiceService.ts @@ -2,16 +2,16 @@ import type {ChannelID, GuildID, UserID} from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import {resolveExperimentTargeting} from '@app/api/experiment/ExperimentTargeting'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {ListParticipantsResult} from '@app/api/infrastructure/ILiveKitService'; import type {LiveKitService} from '@app/api/infrastructure/LiveKitService'; import type {PinnedRoomServer, VoiceRoomStore} from '@app/api/infrastructure/VoiceRoomStore'; import type {InstanceConfigRepository} from '@app/api/instance/InstanceConfigRepository'; import {Logger} from '@app/api/Logger'; import type {User} from '@app/api/models/User'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import type {VoiceAccessContext, VoiceAvailabilityService} from '@app/api/voice/VoiceAvailabilityService'; import type {VoiceRegionAvailability, VoiceServerRecord} from '@app/api/voice/VoiceModel'; import { @@ -79,9 +79,9 @@ interface VoicePermissions { export class VoiceService { constructor( private liveKitService: LiveKitService, - private guildRepository: IGuildRepositoryAggregate, - private userRepository: IUserRepository, - private channelRepository: IChannelRepository, + private guildRepository: GuildRepository, + private userRepository: UserRepository, + private channelRepository: ChannelRepository, private voiceRoomStore: VoiceRoomStore, private voiceAvailabilityService: VoiceAvailabilityService, private instanceConfigRepository: InstanceConfigRepository, @@ -97,7 +97,7 @@ export class VoiceService { if (!user) { throw new UnknownUserError(); } - const channel = await this.channelRepository.findUnique(channelId); + const channel = await this.channelRepository.channelData.findUnique(channelId); if (!channel) { throw new UnknownChannelError(); } diff --git a/fluxer_api/src/api/voice/VoiceTopology.ts b/fluxer_api/src/api/voice/VoiceTopology.ts index 532eb10ac..7b3c28aaf 100644 --- a/fluxer_api/src/api/voice/VoiceTopology.ts +++ b/fluxer_api/src/api/voice/VoiceTopology.ts @@ -2,9 +2,9 @@ import {Logger} from '@app/api/Logger'; import {RefreshSubscription, RefreshSubscriptionUnavailableError} from '@app/api/utils/RefreshSubscription'; -import type {IVoiceRepository} from '@app/api/voice/IVoiceRepository'; import {VOICE_CONFIGURATION_CHANNEL} from '@app/api/voice/VoiceConstants'; import type {VoiceRegionMetadata, VoiceRegionRecord, VoiceServerRecord} from '@app/api/voice/VoiceModel'; +import type {VoiceRepository} from '@app/api/voice/VoiceRepository'; import type {IKVProvider} from '@pkgs/kv_client/src/IKVProvider'; type Subscriber = () => void; @@ -26,7 +26,7 @@ export class VoiceTopology { }); constructor( - private voiceRepository: IVoiceRepository, + private voiceRepository: VoiceRepository, private kvClient: IKVProvider | null, ) {} diff --git a/fluxer_api/src/api/voice/tests/VoiceCallEligibility.test.ts b/fluxer_api/src/api/voice/tests/VoiceCallEligibility.test.ts index 0e2cdec89..dcdd96cc4 100644 --- a/fluxer_api/src/api/voice/tests/VoiceCallEligibility.test.ts +++ b/fluxer_api/src/api/voice/tests/VoiceCallEligibility.test.ts @@ -1,20 +1,11 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import {createTestAccount, unclaimAccount} from '@app/api/auth/tests/AuthTestUtils'; -import { - acceptInvite, - createChannelInvite, - createDmChannel, - createFriendship, - createGuild, - getChannel, -} from '@app/api/channel/tests/ChannelTestUtils'; +import {createTestAccount} from '@app/api/auth/tests/AuthTestUtils'; +import {createDmChannel, createFriendship, createGuild, getChannel} from '@app/api/channel/tests/ChannelTestUtils'; import {ensureSessionStarted} from '@app/api/message/tests/MessageTestUtils'; import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness'; import {HTTP_STATUS} from '@app/api/test/TestConstants'; import {createBuilder} from '@app/api/test/TestRequestBuilder'; -import {updateUserSettings} from '@app/api/user/tests/UserTestUtils'; -import {IncomingCallFlags} from '@fluxer/constants/src/UserConstants'; import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest'; describe('Voice Call Eligibility', () => { @@ -28,16 +19,6 @@ describe('Voice Call Eligibility', () => { afterAll(async () => { await harness?.shutdown(); }); - async function setupUsersWithMutualGuild() { - const user1 = await createTestAccount(harness); - const user2 = await createTestAccount(harness); - await ensureSessionStarted(harness, user1.token); - await ensureSessionStarted(harness, user2.token); - const guild = await createGuild(harness, user1.token, 'Mutual Guild'); - const invite = await createChannelInvite(harness, user1.token, guild.system_channel_id!); - await acceptInvite(harness, user2.token, invite.code); - return {user1, user2, guild}; - } describe('DM call eligibility', () => { it('returns ringable true for DM between friends', async () => { const user1 = await createTestAccount(harness); @@ -54,30 +35,6 @@ describe('Voice Call Eligibility', () => { .execute(); expect(callData.ringable).toBe(true); }); - it('returns ringable true for DM with mutual guild membership', async () => { - const {user1, user2} = await setupUsersWithMutualGuild(); - await updateUserSettings(harness, user2.token, { - incoming_call_flags: IncomingCallFlags.GUILD_MEMBERS, - }); - const dmChannel = await createDmChannel(harness, user1.token, user2.userId); - const callData = await createBuilder<{ - ringable: boolean; - }>(harness, user1.token) - .get(`/channels/${dmChannel.id}/call`) - .execute(); - expect(callData.ringable).toBe(true); - }); - it('returns ringable false for unclaimed account trying DM call', async () => { - const {user1, user2} = await setupUsersWithMutualGuild(); - const dmChannel = await createDmChannel(harness, user1.token, user2.userId); - await unclaimAccount(harness, user1.userId); - const callData = await createBuilder<{ - ringable: boolean; - }>(harness, user1.token) - .get(`/channels/${dmChannel.id}/call`) - .execute(); - expect(callData.ringable).toBe(false); - }); }); describe('Channel type validation', () => { it('returns 404 for non-existent channel', async () => { diff --git a/fluxer_api/src/api/webhook/ChannelFollowService.ts b/fluxer_api/src/api/webhook/ChannelFollowService.ts index a2619142b..3153b242b 100644 --- a/fluxer_api/src/api/webhook/ChannelFollowService.ts +++ b/fluxer_api/src/api/webhook/ChannelFollowService.ts @@ -2,7 +2,7 @@ import type {ChannelID, GuildID, UserID, WebhookID} from '@app/api/BrandedTypes'; import {createMessageID, createWebhookID, createWebhookToken} from '@app/api/BrandedTypes'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import {withChannelFollowLock} from '@app/api/channel/services/ChannelFollowers'; import type {ChannelService} from '@app/api/channel/services/ChannelService'; import type {ThreadViewer} from '@app/api/experiment/ChannelThreadsGate'; @@ -15,7 +15,7 @@ import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware'; import type {Channel} from '@app/api/models/Channel'; import type {Webhook} from '@app/api/models/Webhook'; import * as RandomUtils from '@app/api/utils/RandomUtils'; -import type {IWebhookRepository} from '@app/api/webhook/IWebhookRepository'; +import type {WebhookRepository} from '@app/api/webhook/WebhookRepository'; import type {WebhookService} from '@app/api/webhook/WebhookService'; import { CHANNEL_FOLLOWER_STATS_CACHE_SECONDS, @@ -65,9 +65,9 @@ function truncateToLength(value: string, maxLength: number): string { export class ChannelFollowService { constructor( private readonly webhookService: WebhookService, - private readonly webhookRepository: IWebhookRepository, + private readonly webhookRepository: WebhookRepository, private readonly channelService: ChannelService, - private readonly channelRepository: IChannelRepository, + private readonly channelRepository: ChannelRepository, private readonly guildService: GuildService, private readonly avatarService: AvatarService, private readonly cacheService: ICacheService, @@ -112,7 +112,7 @@ export class ChannelFollowService { targetGuild, }); const webhook = await withChannelFollowLock(this.cacheService, targetChannel.id, async () => { - const current = await this.channelRepository.findUnique(targetChannel.id); + const current = await this.channelRepository.channelData.findUnique(targetChannel.id); if (!current?.guildId || !CHANNEL_FOLLOW_TARGET_TYPES.has(current.type)) { throw new InvalidFollowTargetChannelError(); } @@ -150,7 +150,7 @@ export class ChannelFollowService { sourceChannelId: source.id, }); }); - const currentSource = await this.channelRepository.findUnique(source.id); + const currentSource = await this.channelRepository.channelData.findUnique(source.id); if (!currentSource || currentSource.type !== ChannelTypes.GUILD_ANNOUNCEMENT) { await this.webhookRepository.delete(webhook.id); throw new AnnouncementChannelRequiredError(); diff --git a/fluxer_api/src/api/webhook/IWebhookRepository.ts b/fluxer_api/src/api/webhook/IWebhookRepository.ts deleted file mode 100644 index df3ca0a33..000000000 --- a/fluxer_api/src/api/webhook/IWebhookRepository.ts +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {Webhook} from '@app/api/models/Webhook'; - -export abstract class IWebhookRepository { - abstract findUnique(webhookId: bigint): Promise; - - abstract findByToken(webhookId: bigint, token: string): Promise; - - abstract create(data: { - webhookId: bigint; - token: string; - type: number; - guildId: bigint | null; - channelId: bigint | null; - creatorId: bigint | null; - name: string; - avatarHash: string | null; - sourceGuildId?: bigint | null; - sourceChannelId?: bigint | null; - }): Promise; - - abstract update( - webhookId: bigint, - data: Partial<{ - token: string; - type: number; - guildId: bigint | null; - channelId: bigint | null; - creatorId: bigint | null; - name: string; - avatarHash: string | null; - }>, - ): Promise; - - abstract delete(webhookId: bigint): Promise; - - abstract listByGuild(guildId: bigint): Promise>; - - abstract listByChannel(channelId: bigint): Promise>; - - abstract countByGuild(guildId: bigint): Promise; - - abstract countByChannel(channelId: bigint): Promise; - - abstract listIdsBySourceChannel( - sourceChannelId: bigint, - options: {afterWebhookId?: bigint; limit: number}, - ): Promise>; - - abstract countBySourceChannel(sourceChannelId: bigint): Promise<{channelCount: number; guildCount: number}>; -} diff --git a/fluxer_api/src/api/webhook/WebhookModel.ts b/fluxer_api/src/api/webhook/WebhookModel.ts index 82ff42439..b6753f952 100644 --- a/fluxer_api/src/api/webhook/WebhookModel.ts +++ b/fluxer_api/src/api/webhook/WebhookModel.ts @@ -2,7 +2,7 @@ import type {ChannelID, GuildID, UserID} from '@app/api/BrandedTypes'; import {createUserID} from '@app/api/BrandedTypes'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {UserCacheService} from '@app/api/infrastructure/UserCacheService'; import {Logger} from '@app/api/Logger'; @@ -36,7 +36,7 @@ export function createWebhookSourceResolver({ gatewayService, requestCache, }: { - channelRepository: IChannelRepository; + channelRepository: ChannelRepository; gatewayService: IGatewayService; requestCache: RequestCache; }): WebhookSourceResolver { @@ -74,14 +74,14 @@ async function resolveWebhookSource({ sourceChannelId, creatorId, }: { - channelRepository: IChannelRepository; + channelRepository: ChannelRepository; gatewayService: IGatewayService; sourceGuildId: GuildID; sourceChannelId: ChannelID; creatorId: UserID; }): Promise { try { - const channel = await channelRepository.findUnique(sourceChannelId); + const channel = await channelRepository.channelData.findUnique(sourceChannelId); if (!channel || channel.guildId !== sourceGuildId) return null; const canView = await gatewayService.checkPermission({ guildId: sourceGuildId, diff --git a/fluxer_api/src/api/webhook/WebhookRepository.ts b/fluxer_api/src/api/webhook/WebhookRepository.ts index ae5eb80b2..bc4b1f56c 100644 --- a/fluxer_api/src/api/webhook/WebhookRepository.ts +++ b/fluxer_api/src/api/webhook/WebhookRepository.ts @@ -13,7 +13,6 @@ import type {WebhookRow, WebhooksBySourceChannelRow} from '@app/api/database/typ import {WEBHOOK_COLUMNS} from '@app/api/database/types/ChannelTypes'; import {Webhook} from '@app/api/models/Webhook'; import {Webhooks, WebhooksByChannel, WebhooksByGuild, WebhooksBySourceChannel} from '@app/api/Tables'; -import {IWebhookRepository} from '@app/api/webhook/IWebhookRepository'; const FETCH_WEBHOOK_BY_ID_CQL = Webhooks.selectCql({ where: Webhooks.where.eq('webhook_id'), @@ -57,7 +56,7 @@ function createSourceChannelPageQuery(limit: number) { }); } -export class WebhookRepository extends IWebhookRepository { +export class WebhookRepository { async findUnique(webhookId: WebhookID): Promise { const result = await fetchOne(FETCH_WEBHOOK_BY_ID_CQL, {webhook_id: webhookId}); return result ? new Webhook(result) : null; diff --git a/fluxer_api/src/api/webhook/WebhookRequestService.ts b/fluxer_api/src/api/webhook/WebhookRequestService.ts index d5e8e1265..9e0e4b79e 100644 --- a/fluxer_api/src/api/webhook/WebhookRequestService.ts +++ b/fluxer_api/src/api/webhook/WebhookRequestService.ts @@ -3,7 +3,7 @@ import type {ChannelID, GuildID, MessageID, UserID, WebhookID, WebhookToken} from '@app/api/BrandedTypes'; import {createUserID} from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {MessageUpdateRequest} from '@app/api/channel/MessageTypes'; import {createMessageResponseDataService} from '@app/api/channel/services/message/MessageResponseDataService'; import {maskThreadArtifactsFor} from '@app/api/channel/services/message/ThreadMessageResponses'; @@ -180,7 +180,7 @@ interface SweegoWebhookParams { export class WebhookRequestService { constructor( private readonly webhookService: WebhookService, - private readonly channelRepository: IChannelRepository, + private readonly channelRepository: ChannelRepository, private readonly userCacheService: UserCacheService, private readonly liveKitWebhookService: LiveKitWebhookService | null, private readonly sweegoWebhookService: SweegoWebhookService, @@ -410,7 +410,7 @@ export class WebhookRequestService { } private async mapMessageResponse(message: Message, _requestCache: RequestCache): Promise { - const channel = await this.channelRepository.findUnique(message.channelId); + const channel = await this.channelRepository.channelData.findUnique(message.channelId); const response = await createMessageResponseDataService().buildMessage({ userId: createUserID(0n), message, diff --git a/fluxer_api/src/api/webhook/WebhookService.ts b/fluxer_api/src/api/webhook/WebhookService.ts index cd84c669b..f42c4cd50 100644 --- a/fluxer_api/src/api/webhook/WebhookService.ts +++ b/fluxer_api/src/api/webhook/WebhookService.ts @@ -4,7 +4,7 @@ import {stripOwnAttachmentSignature} from '@app/api/attachment/AttachmentUrls'; import type {ChannelID, GuildID, MessageID, UserID, WebhookID, WebhookToken} from '@app/api/BrandedTypes'; import {createChannelID, createGuildID, createUserID, createWebhookID, createWebhookToken} from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {MessageRequest, MessageUpdateRequest} from '@app/api/channel/MessageTypes'; import {withChannelFollowLock} from '@app/api/channel/services/ChannelFollowers'; import type {ChannelService} from '@app/api/channel/services/ChannelService'; @@ -40,9 +40,9 @@ import type {Message} from '@app/api/models/Message'; import type {Webhook} from '@app/api/models/Webhook'; import * as RandomUtils from '@app/api/utils/RandomUtils'; import {inputValidationErrorFromZodIssues} from '@app/api/Validator'; -import type {IWebhookRepository} from '@app/api/webhook/IWebhookRepository'; import {transform as GitHubTransform} from '@app/api/webhook/transformers/GitHubTransformer'; import {instatusDeliveryKey, transformInstatusWebhook} from '@app/api/webhook/transformers/InstatusTransformer'; +import type {WebhookRepository} from '@app/api/webhook/WebhookRepository'; import {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType'; import { CHANNEL_FOLLOW_TARGET_TYPES, @@ -170,10 +170,10 @@ export class WebhookService { } constructor( - private repository: IWebhookRepository, + private repository: WebhookRepository, private guildService: GuildService, private channelService: ChannelService, - private channelRepository: IChannelRepository, + private channelRepository: ChannelRepository, private cacheService: ICacheService, private gatewayService: IGatewayService, private avatarService: AvatarService, @@ -353,7 +353,7 @@ export class WebhookService { const moveChannelId = followerMoveChannelId; const updatedWebhook = moveChannelId ? await withChannelFollowLock(this.cacheService, moveChannelId, async () => { - const current = await this.channelRepository.findUnique(moveChannelId); + const current = await this.channelRepository.channelData.findUnique(moveChannelId); if (!current || !CHANNEL_FOLLOW_TARGET_TYPES.has(current.type)) { throw new InvalidFollowTargetChannelError(); } @@ -557,7 +557,7 @@ export class WebhookService { const webhook = await this.getTokenAuthenticatedWebhook({webhookId, token}); if (!webhook.channelId) throw new UnknownChannelError(); const thread = await this.resolveWebhookThread(webhook, threadId); - const message = await this.channelRepository.getMessage(thread?.id ?? webhook.channelId, messageId); + const message = await this.channelRepository.messages.getMessage(thread?.id ?? webhook.channelId, messageId); if (!message) throw new UnknownMessageError(); if (message.webhookId !== webhook.id) throw new MissingPermissionsError(); return message; @@ -708,7 +708,7 @@ export class WebhookService { private async resolveParentContentView(channel: Channel): Promise { if (!channel.parentId || channel.type === ChannelTypes.GUILD_CATEGORY) return null; - const parent = await this.channelRepository.findUnique(channel.parentId); + const parent = await this.channelRepository.channelData.findUnique(channel.parentId); return parent ? channelToContentWarningView(parent) : null; } @@ -724,7 +724,7 @@ export class WebhookService { userId: UserID; }): Promise { if (!webhook.sourceChannelId || !webhook.sourceGuildId) return; - const sourceChannel = await this.channelRepository.findUnique(webhook.sourceChannelId); + const sourceChannel = await this.channelRepository.channelData.findUnique(webhook.sourceChannelId); if (!sourceChannel) return; const sourceGuild = await this.gatewayService .getGuildData({guildId: webhook.sourceGuildId, userId, skipMembershipCheck: true}) @@ -768,7 +768,7 @@ export class WebhookService { private async isDormantForumWebhook(webhook: Webhook): Promise { if (!everEnabled() || !webhook.guildId || !webhook.channelId || guildActive(webhook.guildId)) return false; if (!(await isTainted(webhook.guildId))) return false; - const channel = await this.channelRepository.findUnique(webhook.channelId); + const channel = await this.channelRepository.channelData.findUnique(webhook.channelId); return channel?.isThreadOnly() ?? false; } @@ -781,7 +781,7 @@ export class WebhookService { const channelIds = [...new Set(webhooks.flatMap((webhook) => (webhook.channelId ? [webhook.channelId] : [])))]; if (channelIds.length === 0) return webhooks; const forumIds = new Set( - (await this.channelRepository.listChannels(channelIds)) + (await this.channelRepository.channelData.listChannels(channelIds)) .filter((channel) => channel.isThreadOnly()) .map((channel) => channel.id), ); @@ -802,7 +802,7 @@ export class WebhookService { throw new UnknownChannelError(); } if (!/^\d{1,20}$/.test(threadId)) throw new UnknownChannelError(); - const thread = await this.channelRepository.findUnique(createChannelID(BigInt(threadId))); + const thread = await this.channelRepository.channelData.findUnique(createChannelID(BigInt(threadId))); if (!thread?.isThread() || thread.parentId !== webhook.channelId || thread.guildId !== webhook.guildId) { throw new UnknownChannelError(); } @@ -816,7 +816,7 @@ export class WebhookService { const webhook = await this.repository.findByToken(webhookId, token); if (!webhook || webhook.type !== WebhookTypes.INCOMING) throw new UnknownWebhookError(); if (!webhook.channelId) throw new UnknownChannelError(); - const channel = await this.channelRepository.findUnique(webhook.channelId); + const channel = await this.channelRepository.channelData.findUnique(webhook.channelId); if (!channel) throw new UnknownChannelError(); if (channel.isThreadOnly() && webhook.guildId && !guildActive(webhook.guildId)) throw new UnknownWebhookError(); this.assertWebhookTargetChannel(channel); @@ -849,7 +849,7 @@ export class WebhookService { : webhook.avatarHash; let channelId = webhook.channelId; if (data.channel_id !== undefined && data.channel_id !== webhook.channelId) { - const channel = await this.channelRepository.findUnique(createChannelID(data.channel_id)); + const channel = await this.channelRepository.channelData.findUnique(createChannelID(data.channel_id)); if (!channel) { throw new UnknownChannelError(); } diff --git a/fluxer_api/src/api/webhook/tests/WebhookForumThreads.test.ts b/fluxer_api/src/api/webhook/tests/WebhookForumThreads.test.ts index 697948cd1..775352418 100644 --- a/fluxer_api/src/api/webhook/tests/WebhookForumThreads.test.ts +++ b/fluxer_api/src/api/webhook/tests/WebhookForumThreads.test.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {createTestAccount, type TestAccount} from '@app/api/auth/tests/AuthTestUtils'; -import {ChannelRepository} from '@app/api/channel/ChannelRepository'; +import {ChannelDataRepository} from '@app/api/channel/repositories/ChannelDataRepository'; import {MessageSendService} from '@app/api/channel/services/message/MessageSendService'; import {createChannel, createGuild} from '@app/api/channel/tests/ChannelTestUtils'; import { @@ -220,12 +220,19 @@ describe('webhooks on forum and media channels', () => { .post(`/channels/${guild.system_channel_id}/webhooks`) .body({name: 'control'}) .execute(); - const findUnique = vi.spyOn(ChannelRepository.prototype, 'findUnique'); - const listChannels = vi.spyOn(ChannelRepository.prototype, 'listChannels'); + const readChannelIds: Array = []; + const readChannel = ChannelDataRepository.prototype.findUnique; + const findUnique = vi.spyOn(ChannelDataRepository.prototype, 'findUnique').mockImplementation(function ( + this: ChannelDataRepository, + channelId, + ) { + if (!new Error().stack?.includes('NoopGatewayService')) readChannelIds.push(channelId.toString()); + return readChannel.call(this, channelId); + }); + const listChannels = vi.spyOn(ChannelDataRepository.prototype, 'listChannels'); await createBuilderWithoutAuth(harness).get(`/webhooks/${hook.id}/${hook.token}`).execute(); await threadsRequest(harness, owner.token).get(`/guilds/${guild.id}/webhooks`).execute(); await threadsRequest(harness, owner.token).get(`/webhooks/${hook.id}`).execute(); - const readChannelIds = findUnique.mock.calls.map(([channelId]) => channelId.toString()); const listCalls = listChannels.mock.calls.length; findUnique.mockRestore(); listChannels.mockRestore(); diff --git a/fluxer_api/src/api/worker/WorkerContext.ts b/fluxer_api/src/api/worker/WorkerContext.ts index a847220d5..96ed8c55c 100644 --- a/fluxer_api/src/api/worker/WorkerContext.ts +++ b/fluxer_api/src/api/worker/WorkerContext.ts @@ -1,24 +1,24 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {WorkerDependencies} from '@app/api/worker/WorkerDependencies'; -import { - clearWorkerDependencies as clearWorkerDependenciesBase, - getWorkerDependencies as getWorkerDependenciesBase, - setWorkerDependencies as setWorkerDependenciesBase, -} from '@pkgs/worker/src/context/WorkerContext'; + +let workerDependencies: WorkerDependencies | null = null; export function setWorkerDependencies(dependencies: WorkerDependencies): void { - setWorkerDependenciesBase(dependencies); + workerDependencies = dependencies; } export function setWorkerDependenciesForTest(dependencies: Partial): void { - setWorkerDependenciesBase(dependencies); + workerDependencies = dependencies as WorkerDependencies; } export function getWorkerDependencies(): WorkerDependencies { - return getWorkerDependenciesBase(); + if (!workerDependencies) { + throw new Error('Worker dependencies have not been initialized. Call setWorkerDependencies() first.'); + } + return workerDependencies; } export function clearWorkerDependencies(): void { - clearWorkerDependenciesBase(); + workerDependencies = null; } diff --git a/fluxer_api/src/api/worker/WorkerDependencies.ts b/fluxer_api/src/api/worker/WorkerDependencies.ts index 8030fa30d..06de4439b 100644 --- a/fluxer_api/src/api/worker/WorkerDependencies.ts +++ b/fluxer_api/src/api/worker/WorkerDependencies.ts @@ -10,7 +10,6 @@ import type {ChannelService} from '@app/api/channel/services/ChannelService'; import type {ConnectionRepository} from '@app/api/connection/ConnectionRepository'; import {ConnectionService} from '@app/api/connection/ConnectionService'; import {DonationRepository} from '@app/api/donation/DonationRepository'; -import type {IDonationRepository} from '@app/api/donation/IDonationRepository'; import type {FavoriteMemeRepository} from '@app/api/favorite_meme/FavoriteMemeRepository'; import type {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService'; import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; @@ -165,7 +164,7 @@ export interface WorkerDependencies { channelService: ChannelService; guildAuditLogService: GuildAuditLogService; contactChangeLogService: UserContactChangeLogService; - donationRepository: IDonationRepository; + donationRepository: DonationRepository; guildService: GuildService; billingRepository: BillingRepository; storeEntitlementService: StoreEntitlementService; diff --git a/fluxer_api/src/api/worker/WorkerRunner.ts b/fluxer_api/src/api/worker/WorkerRunner.ts index 097d9cffe..bb5a170cb 100644 --- a/fluxer_api/src/api/worker/WorkerRunner.ts +++ b/fluxer_api/src/api/worker/WorkerRunner.ts @@ -3,7 +3,7 @@ import {createHash, randomUUID} from 'node:crypto'; import {ArchiveAttemptSupersededError} from '@app/api/archive/ArchiveAttemptSupersededError'; import {ArchiveTaskDeferredError, ArchiveTerminalFailureError, isArchiveTask} from '@app/api/archive/ArchiveTask'; -import type {IJobLedgerRepository} from '@app/api/jobs/IJobLedgerRepository'; +import type {JobLedgerRepository} from '@app/api/jobs/JobLedgerRepository'; import {Logger} from '@app/api/Logger'; import {getWorkerService} from '@app/api/middleware/ServiceRegistry'; import {isJsonRecord, parseJsonRecord} from '@app/api/utils/JsonBoundaryUtils'; @@ -55,7 +55,7 @@ interface WorkerRunnerOptions { queue: WorkerRunnerQueue; consumerName: string; laneName: string; - ledger: IJobLedgerRepository; + ledger: JobLedgerRepository; workerId?: string; concurrency?: number; maxDeliver?: number; @@ -74,7 +74,7 @@ export class WorkerRunner { private readonly maxDeliver: number; private readonly ackWaitMs: number; private readonly workerService: IWorkerService; - private readonly ledger: IJobLedgerRepository; + private readonly ledger: JobLedgerRepository; private readonly heartbeat: WorkerHeartbeat | null; private heartbeatSignal: WorkerHeartbeatSignal | null = null; private heartbeatTimer: NodeJS.Timeout | null = null; diff --git a/fluxer_api/src/api/worker/WorkerService.ts b/fluxer_api/src/api/worker/WorkerService.ts index 022d89b7a..2254f5076 100644 --- a/fluxer_api/src/api/worker/WorkerService.ts +++ b/fluxer_api/src/api/worker/WorkerService.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService'; -import type {IJobLedgerRepository} from '@app/api/jobs/IJobLedgerRepository'; +import type {JobLedgerRepository} from '@app/api/jobs/JobLedgerRepository'; import {Logger} from '@app/api/Logger'; import type {JetStreamWorkerQueue} from '@app/api/worker/JetStreamWorkerQueue'; import {findLaneForTask, type WorkerTaskName} from '@app/api/worker/WorkerLaneConfig'; @@ -12,9 +12,9 @@ import type {WorkerJobOptions, WorkerJobPayload} from '@pkgs/worker/src/contract export class WorkerService implements IWorkerService { private readonly queue: JetStreamWorkerQueue; private readonly snowflake: ISnowflakeService; - private readonly ledger: IJobLedgerRepository; + private readonly ledger: JobLedgerRepository; - constructor(queue: JetStreamWorkerQueue, snowflake: ISnowflakeService, ledger: IJobLedgerRepository) { + constructor(queue: JetStreamWorkerQueue, snowflake: ISnowflakeService, ledger: JobLedgerRepository) { this.queue = queue; this.snowflake = snowflake; this.ledger = ledger; diff --git a/fluxer_api/src/api/worker/tasks/ClearAuthenticatedReporterEmails.ts b/fluxer_api/src/api/worker/tasks/ClearAuthenticatedReporterEmails.ts index 779a32580..6bddcf12c 100644 --- a/fluxer_api/src/api/worker/tasks/ClearAuthenticatedReporterEmails.ts +++ b/fluxer_api/src/api/worker/tasks/ClearAuthenticatedReporterEmails.ts @@ -2,7 +2,7 @@ import type {ReportID} from '@app/api/BrandedTypes'; import {Logger} from '@app/api/Logger'; -import type {IReportRepository} from '@app/api/report/IReportRepository'; +import type {ReportRepository} from '@app/api/report/ReportRepository'; import {getWorkerDependencies} from '@app/api/worker/WorkerContext'; import type {WorkerTaskHandler} from '@pkgs/worker/src/contracts/WorkerTask'; @@ -16,7 +16,7 @@ export interface ClearAuthenticatedReporterEmailsSummary { } export async function clearAuthenticatedReporterEmails( - reportRepository: IReportRepository, + reportRepository: ReportRepository, pageSize = DEFAULT_SCAN_PAGE_SIZE, ): Promise { const summary: ClearAuthenticatedReporterEmailsSummary = {scanned: 0, cleared: 0, skipped: 0, failed: 0}; diff --git a/fluxer_api/src/api/worker/tasks/ExpireReportSnapshots.ts b/fluxer_api/src/api/worker/tasks/ExpireReportSnapshots.ts index 75499f425..60c679dbd 100644 --- a/fluxer_api/src/api/worker/tasks/ExpireReportSnapshots.ts +++ b/fluxer_api/src/api/worker/tasks/ExpireReportSnapshots.ts @@ -3,7 +3,6 @@ import type {ReportID} from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; import {Logger} from '@app/api/Logger'; -import {type IARSubmission, ReportStatus} from '@app/api/report/IReportRepository'; import { deleteReportWithEvidence, forEachStoredReport, @@ -11,6 +10,7 @@ import { type ReportDeletionDeps, referencedObjectKeys, } from '@app/api/report/ReportDeletion'; +import {type IARSubmission, ReportStatus} from '@app/api/report/ReportModels'; import {getReportSearchService} from '@app/api/SearchFactory'; import {getWorkerDependencies} from '@app/api/worker/WorkerContext'; import type {WorkerTaskHandler} from '@pkgs/worker/src/contracts/WorkerTask'; diff --git a/fluxer_api/src/api/worker/tasks/ExtractEmbeds.ts b/fluxer_api/src/api/worker/tasks/ExtractEmbeds.ts index ad40894b7..8bb897009 100644 --- a/fluxer_api/src/api/worker/tasks/ExtractEmbeds.ts +++ b/fluxer_api/src/api/worker/tasks/ExtractEmbeds.ts @@ -203,7 +203,7 @@ async function readMessageForExpectedContent( ): Promise { let lastMessage: Message | null = null; for (let attempt = 0; attempt < MESSAGE_READ_RETRY_ATTEMPTS; attempt++) { - const message = await channelRepository.getMessage(channelId, messageId); + const message = await channelRepository.messages.getMessage(channelId, messageId); if (message && messageMatchesExpectedContent(message, expectedContentHash)) { return message; } @@ -383,7 +383,7 @@ async function updateMessageEmbeds( ...freshMessage.toRow(), embeds: nextEmbeds.length > 0 ? nextEmbeds : null, }); - await channelRepository.updateEmbeds(messageWithEmbeds); + await channelRepository.messages.updateEmbeds(messageWithEmbeds); return messageWithEmbeds; } @@ -438,7 +438,7 @@ const extractEmbeds: WorkerTaskHandler = async (payload, helpers) => { Logger.info({messageId: messageId.toString()}, 'Skipping extractEmbeds: message not found or no content'); return; } - const channel = await channelRepository.findUnique(channelId); + const channel = await channelRepository.channelData.findUnique(channelId); if (!channel) { Logger.info({channelId: channelId.toString()}, 'Skipping extractEmbeds: channel not found'); return; @@ -465,7 +465,7 @@ const extractEmbeds: WorkerTaskHandler = async (payload, helpers) => { const orderedEmbeds = buildOrderedEmbeds(urls, unfurledEmbedsByUrl); let propagated: Message | null = null; const handled = await withMessageWriteLock(cacheService, channelId, messageId, async () => { - const latestExpectedMessage = await channelRepository.getMessage(channelId, messageId); + const latestExpectedMessage = await channelRepository.messages.getMessage(channelId, messageId); if (!latestExpectedMessage) { Logger.debug({messageId: messageId.toString()}, 'Message no longer exists, skipping embed update'); return false; @@ -494,7 +494,7 @@ const extractEmbeds: WorkerTaskHandler = async (payload, helpers) => { }, 'Content moderation blocked embed unfurl, deleting parent message', ); - await channelRepository.deleteMessage( + await channelRepository.messages.deleteMessage( channelId, messageId, latestExpectedMessage.authorId || createUserID(0n), diff --git a/fluxer_api/src/api/worker/tasks/HandleMentionChunk.ts b/fluxer_api/src/api/worker/tasks/HandleMentionChunk.ts index a950c5b76..b0d8cfc68 100644 --- a/fluxer_api/src/api/worker/tasks/HandleMentionChunk.ts +++ b/fluxer_api/src/api/worker/tasks/HandleMentionChunk.ts @@ -10,7 +10,7 @@ import { } from '@app/api/BrandedTypes'; import {guildActive} from '@app/api/experiment/ChannelThreadsGate'; import {Logger} from '@app/api/Logger'; -import type {ReadStateMarker} from '@app/api/read_state/IReadStateRepository'; +import type {ReadStateMarker} from '@app/api/read_state/ReadStateRepository'; import {mapWithConcurrency} from '@app/api/utils/ConcurrencyUtils'; import {getWorkerDependencies} from '@app/api/worker/WorkerContext'; import {ReadStateFlags} from '@fluxer/constants/src/ThreadConstants'; diff --git a/fluxer_api/src/api/worker/tasks/HandleMentions.ts b/fluxer_api/src/api/worker/tasks/HandleMentions.ts index 295eec483..9e2a80912 100644 --- a/fluxer_api/src/api/worker/tasks/HandleMentions.ts +++ b/fluxer_api/src/api/worker/tasks/HandleMentions.ts @@ -233,12 +233,12 @@ const handleMentions: WorkerTaskHandler = async (payload, helpers) => { const messageId = createMessageID(BigInt(validated.messageId)); const payloadGuildId = validated.guildId ? createGuildID(BigInt(validated.guildId)) : null; const mentionHere = validated.mentionHere ?? false; - const message = await channelRepository.getMessage(channelId, messageId); + const message = await channelRepository.messages.getMessage(channelId, messageId); if (!message) { Logger.debug({messageId}, 'handleMentions: Message not found, skipping'); return; } - const channel = await channelRepository.findUnique(channelId); + const channel = await channelRepository.channelData.findUnique(channelId); if (!channel) { Logger.debug({channelId}, 'handleMentions: Channel not found, skipping'); return; diff --git a/fluxer_api/src/api/worker/tasks/HarvestGuildData.ts b/fluxer_api/src/api/worker/tasks/HarvestGuildData.ts index fb4eb22fb..3f69ef6c3 100644 --- a/fluxer_api/src/api/worker/tasks/HarvestGuildData.ts +++ b/fluxer_api/src/api/worker/tasks/HarvestGuildData.ts @@ -235,7 +235,7 @@ const harvestGuildData: ArchiveTaskHandler = async (payload, helpers, attempt) = let beforeMessageId: MessageID | undefined; let channelDownloads: Array = []; while (messages.length < MESSAGE_LIMIT_PER_CHANNEL) { - const batch = await channelRepository.listMessages(channel.id, beforeMessageId, MESSAGE_BATCH_SIZE); + const batch = await channelRepository.messages.listMessages(channel.id, beforeMessageId, MESSAGE_BATCH_SIZE); if (batch.length === 0) break; for (const msg of batch) { if (msg.authorId == null) continue; diff --git a/fluxer_api/src/api/worker/tasks/HarvestUserData.ts b/fluxer_api/src/api/worker/tasks/HarvestUserData.ts index 52da7ab6c..971463323 100644 --- a/fluxer_api/src/api/worker/tasks/HarvestUserData.ts +++ b/fluxer_api/src/api/worker/tasks/HarvestUserData.ts @@ -23,7 +23,7 @@ import { type UserID, } from '@app/api/BrandedTypes'; import {Config} from '@app/api/Config'; -import type {IThreadRepository} from '@app/api/channel/repositories/IThreadRepository'; +import type {ThreadRepository} from '@app/api/channel/repositories/ThreadRepository'; import { isChannelEligible, isTimestampInWindow, @@ -417,7 +417,7 @@ export async function harvestMessages( } export async function collectThreadMemberships( - threads: Pick, + threads: Pick, userId: UserID, guildIds: ReadonlyArray, ): Promise> { @@ -848,13 +848,18 @@ const harvestUserData: ArchiveTaskHandler = async (payload, helpers, attempt) => filterArgs = { filter, context, - findChannel: (channelId) => channelRepository.findUnique(channelId), + findChannel: (channelId) => channelRepository.channelData.findUnique(channelId), }; } Logger.debug({userId, harvestId, elapsed: Date.now() - startTime}, 'Starting user data harvest'); await progressReporter.updateProgress(INITIAL_PROGRESS, 'Harvesting messages'); Logger.debug({elapsed: Date.now() - startTime}, 'Set progress to INITIAL_PROGRESS'); - const {channelMessagesMap, totalMessages} = await harvestMessages(channelRepository, userId, startTime, filterArgs); + const {channelMessagesMap, totalMessages} = await harvestMessages( + channelRepository.messages, + userId, + startTime, + filterArgs, + ); if (totalMessages > 0) { const progress = Math.min(INITIAL_PROGRESS + Math.floor((totalMessages / 10000) * 50), MESSAGES_PROGRESS_MAX); await progressReporter.updateProgress(progress, `Harvested ${totalMessages} messages`); diff --git a/fluxer_api/src/api/worker/tasks/IndexChannelMessages.ts b/fluxer_api/src/api/worker/tasks/IndexChannelMessages.ts index c9c4c36c6..8ca903814 100644 --- a/fluxer_api/src/api/worker/tasks/IndexChannelMessages.ts +++ b/fluxer_api/src/api/worker/tasks/IndexChannelMessages.ts @@ -40,7 +40,7 @@ const indexChannelMessages: WorkerTaskHandler = async (payload) => { let totalIndexed = 0; try { while (true) { - const messages = await channelRepository.listMessages(channelId, cursor, BULK_BATCH_SIZE); + const messages = await channelRepository.messages.listMessages(channelId, cursor, BULK_BATCH_SIZE); if (messages.length === 0) { break; } @@ -81,11 +81,11 @@ const indexChannelMessages: WorkerTaskHandler = async (payload) => { Logger.info({completionKey: validated.completionKey}, 'All channels indexed'); } } - const channel = await channelRepository.findUnique(channelId); + const channel = await channelRepository.channelData.findUnique(channelId); if (channel?.isThread()) { await channelRepository.channelData.patchIndexedAt(channelId, new Date()); } else if (channel) { - await channelRepository.upsert({...channel.toRow(), indexed_at: new Date()}); + await channelRepository.channelData.upsert({...channel.toRow(), indexed_at: new Date()}); } Logger.info({channelId: channelId.toString(), totalIndexed}, 'Bulk channel indexing complete'); } catch (error) { diff --git a/fluxer_api/src/api/worker/tasks/MessageShred.ts b/fluxer_api/src/api/worker/tasks/MessageShred.ts index 150a0ec41..0ba127b3d 100644 --- a/fluxer_api/src/api/worker/tasks/MessageShred.ts +++ b/fluxer_api/src/api/worker/tasks/MessageShred.ts @@ -103,7 +103,7 @@ const messageShredTask: WorkerTaskHandler = async (payload, helpers) => { for (const validationChunk of chunkArray(typedSlice, VALIDATION_CHUNK_SIZE)) { const messageFetches = validationChunk.map( ({channelId, messageId}: {channelId: ChannelID; messageId: MessageID}) => - channelRepository.getMessage(channelId, messageId), + channelRepository.messages.getMessage(channelId, messageId), ); const fetchedMessages = await Promise.all(messageFetches); const deletableChunk: Array<{ @@ -130,7 +130,7 @@ const messageShredTask: WorkerTaskHandler = async (payload, helpers) => { deletionChunk.map( async ({channelId, messageId, message}: {channelId: ChannelID; messageId: MessageID; message: Message}) => { await purgeMessageAttachments(message, storageService, purgeQueue); - return channelRepository.deleteMessage(channelId, messageId, authorId); + return channelRepository.messages.deleteMessage(channelId, messageId, authorId); }, ), ); diff --git a/fluxer_api/src/api/worker/tasks/ProcessExpiredPremiumSweep.ts b/fluxer_api/src/api/worker/tasks/ProcessExpiredPremiumSweep.ts index d909c0f3a..a8bb0bece 100644 --- a/fluxer_api/src/api/worker/tasks/ProcessExpiredPremiumSweep.ts +++ b/fluxer_api/src/api/worker/tasks/ProcessExpiredPremiumSweep.ts @@ -2,7 +2,7 @@ import type {StorePurchaseRow} from '@app/api/database/types/StoreBillingTypes'; import {mapGuildMemberToResponse} from '@app/api/guild/GuildModel'; -import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate'; +import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {PremiumStateReconciliationQueueService} from '@app/api/infrastructure/PremiumStateReconciliationQueueService'; import type {UserCacheService} from '@app/api/infrastructure/UserCacheService'; @@ -18,7 +18,7 @@ import {resolveStoreAccessEnd} from '@app/api/store_billing/StoreBillingMappers' import {isTerminalStorePurchaseState} from '@app/api/store_billing/StoreBillingTypes'; import type {StoreEntitlementService} from '@app/api/store_billing/StoreEntitlementService'; import {isPremiumTieringActive} from '@app/api/stripe/BillingConfigCache'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import {checkIsPremium, createPremiumClearPatch, shouldStripExpiredPremium} from '@app/api/user/UserHelpers'; import {mapUserToPrivateResponse} from '@app/api/user/UserMappers'; import {getWorkerDependencies} from '@app/api/worker/WorkerContext'; @@ -43,8 +43,8 @@ interface SweepResult { } interface SweepDeps { - userRepository: IUserRepository; - guildRepository: IGuildRepositoryAggregate; + userRepository: UserRepository; + guildRepository: GuildRepository; userCacheService: UserCacheService; gatewayService: IGatewayService; premiumStateReconciliationQueueService: PremiumStateReconciliationQueueService; diff --git a/fluxer_api/src/api/worker/tasks/RefreshSearchIndex.ts b/fluxer_api/src/api/worker/tasks/RefreshSearchIndex.ts index 835762197..4a140e195 100644 --- a/fluxer_api/src/api/worker/tasks/RefreshSearchIndex.ts +++ b/fluxer_api/src/api/worker/tasks/RefreshSearchIndex.ts @@ -205,7 +205,7 @@ const refreshChannelMessages: IndexHandler = async (payload, helpers, kvClient, const {channelRepository} = getWorkerDependencies(); const guildId = createGuildID(BigInt(payload.guild_id!)); const searchService = requireSearchService(getMessageSearchService()); - const guildChannels = await channelRepository.listGuildChannels(guildId, 'complete'); + const guildChannels = await channelRepository.channelData.listGuildChannels(guildId, 'complete'); const threadIds = await channelRepository.threads.listGuildThreadIds(guildId, {parents: guildChannels}); const channels = [...guildChannels.map((channel) => ({id: channel.id})), ...threadIds.map((id) => ({id}))]; if (channels.length === 0) { diff --git a/fluxer_api/src/api/worker/tasks/RemoveChannelFollowers.ts b/fluxer_api/src/api/worker/tasks/RemoveChannelFollowers.ts index b9271223d..0327d3920 100644 --- a/fluxer_api/src/api/worker/tasks/RemoveChannelFollowers.ts +++ b/fluxer_api/src/api/worker/tasks/RemoveChannelFollowers.ts @@ -16,7 +16,7 @@ const removeChannelFollowers: WorkerTaskHandler = async (payload, helpers) => { const {channelRepository, webhookRepository, gatewayService} = getWorkerDependencies(); const sourceChannelId = createChannelID(BigInt(validated.sourceChannelId)); if (validated.reason === 'converted') { - const channel = await channelRepository.findUnique(sourceChannelId); + const channel = await channelRepository.channelData.findUnique(sourceChannelId); if (channel?.type === ChannelTypes.GUILD_ANNOUNCEMENT) { helpers.logger.info( {sourceChannelId: validated.sourceChannelId}, diff --git a/fluxer_api/src/api/worker/tasks/SeedThreadPermissions.ts b/fluxer_api/src/api/worker/tasks/SeedThreadPermissions.ts index e539ac0d1..4af3bf119 100644 --- a/fluxer_api/src/api/worker/tasks/SeedThreadPermissions.ts +++ b/fluxer_api/src/api/worker/tasks/SeedThreadPermissions.ts @@ -2,7 +2,7 @@ import {createGuildID, type GuildID, guildIdToRoleId} from '@app/api/BrandedTypes'; import {mapChannelToResponse} from '@app/api/channel/ChannelMappers'; -import type {IThreadRepository} from '@app/api/channel/repositories/IThreadRepository'; +import type {ThreadRepository} from '@app/api/channel/repositories/ThreadRepository'; import {withThreadParentFieldsMany} from '@app/api/channel/services/thread/ThreadParentSettings'; import {getCompiledChannelThreadsConfig, guildActive} from '@app/api/experiment/ChannelThreadsGate'; import {mapGuildRoleToResponse} from '@app/api/guild/GuildModel'; @@ -41,7 +41,7 @@ async function enqueueThreadPermissionSeed(guildId: string): Promise { ); } -export async function ensureGuildThreadPermissionsSeeded(threads: IThreadRepository, guildId: GuildID): Promise { +export async function ensureGuildThreadPermissionsSeeded(threads: ThreadRepository, guildId: GuildID): Promise { const key = guildId.toString(); if (seededGuildIds.has(key)) return; const marker = await threads.getGuildMarker(guildId); @@ -74,7 +74,7 @@ const seedThreadPermissions: WorkerTaskHandler = async (payload) => { const guild = await guildRepository.findUnique(guildId); if (!guild) return; await channelRepository.threads.ensureGuildMarker(guildId); - const channels = await channelRepository.listGuildChannels(guildId, 'complete'); + const channels = await channelRepository.channelData.listGuildChannels(guildId, 'complete'); const updatedChannels: Array = []; for (const channel of channels) { if (channel.permissionOverwrites.size === 0) continue; diff --git a/fluxer_api/src/api/worker/tasks/ThreadMentionScope.ts b/fluxer_api/src/api/worker/tasks/ThreadMentionScope.ts index 57fda313c..5f95320cd 100644 --- a/fluxer_api/src/api/worker/tasks/ThreadMentionScope.ts +++ b/fluxer_api/src/api/worker/tasks/ThreadMentionScope.ts @@ -1,12 +1,12 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {GuildID, MessageID, UserID} from '@app/api/BrandedTypes'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type { CreateThreadMember, - IThreadRepository, ThreadMemberAddResult, -} from '@app/api/channel/repositories/IThreadRepository'; + ThreadRepository, +} from '@app/api/channel/repositories/ThreadRepository'; import {buildBroadcastMessageData} from '@app/api/channel/services/message/MessageGatewayDispatch'; import {dispatchThreadEvents, threadMembersUpdateEvent} from '@app/api/channel/services/thread/ThreadDispatch'; import {loadThreadView} from '@app/api/channel/services/thread/ThreadViews'; @@ -21,7 +21,7 @@ import { import type {GatewayMentionSourceEntry, IGatewayService} from '@app/api/infrastructure/IGatewayService'; import type {Channel} from '@app/api/models/Channel'; import type {ThreadState} from '@app/api/models/ThreadState'; -import type {IUserRepository} from '@app/api/user/IUserRepository'; +import type {UserRepository} from '@app/api/user/repositories/UserRepository'; import { MAX_ROLE_MENTION_THREAD_ADDS, MAX_THREAD_MEMBERS, @@ -30,13 +30,13 @@ import { import {MaxThreadMembersError} from '@fluxer/errors/src/domains/channel/MaxThreadMembersError'; export interface ThreadMentionScopeDeps { - channelRepository: IChannelRepository; + channelRepository: ChannelRepository; gatewayService: IGatewayService; - userRepository: IUserRepository; + userRepository: UserRepository; } export async function threadRecipients( - userRepository: IUserRepository, + userRepository: UserRepository, guildId: GuildID, userIds: Array, ): Promise> { @@ -51,7 +51,7 @@ export async function threadRecipients( } export async function addThreadMembersWithinCap( - threads: IThreadRepository, + threads: ThreadRepository, state: ThreadState, members: Array, ): Promise { diff --git a/fluxer_api/src/api/worker/tasks/utils/MessageDeletion.ts b/fluxer_api/src/api/worker/tasks/utils/MessageDeletion.ts index 6fc7e4c77..14f9bfc79 100644 --- a/fluxer_api/src/api/worker/tasks/utils/MessageDeletion.ts +++ b/fluxer_api/src/api/worker/tasks/utils/MessageDeletion.ts @@ -1,13 +1,13 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {type ChannelID, createChannelID, type MessageID} from '@app/api/BrandedTypes'; -import type {IChannelRepository} from '@app/api/channel/IChannelRepository'; +import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import {withThreadContext} from '@app/api/channel/services/ChannelGatewayDispatch'; import {decrementThreadMessageCount} from '@app/api/channel/services/message/MessageHelpers'; import type {IGatewayService} from '@app/api/infrastructure/IGatewayService'; interface BulkDeleteDispatcherDeps { - channelRepository: IChannelRepository; + channelRepository: ChannelRepository; gatewayService: IGatewayService; batchSize: number; } @@ -30,7 +30,7 @@ export function createBulkDeleteDispatcher({channelRepository, gatewayService, b continue; } const channelId = createChannelID(BigInt(channelIdStr)); - const channel = await channelRepository.findUnique(channelId); + const channel = await channelRepository.channelData.findUnique(channelId); if (channel) { await decrementThreadMessageCount(channelRepository, channel, messageIdsBatch); const payloadIds = messageIdsBatch.map((id) => id.toString()); diff --git a/fluxer_api/src/api/worker/tests/AdminBulkGuildTasks.test.ts b/fluxer_api/src/api/worker/tests/AdminBulkGuildTasks.test.ts index 0423d62f1..9a983fdc4 100644 --- a/fluxer_api/src/api/worker/tests/AdminBulkGuildTasks.test.ts +++ b/fluxer_api/src/api/worker/tests/AdminBulkGuildTasks.test.ts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import type {AdminAuditLog} from '@app/api/admin/IAdminRepository'; +import type {AdminAuditLog} from '@app/api/admin/AdminRepository'; import {createTestAccount, setUserACLs, type TestAccount} from '@app/api/auth/tests/AuthTestUtils'; import {createGuildID, createUserID} from '@app/api/BrandedTypes'; import {createApiContext} from '@app/api/CreateApiContext'; diff --git a/fluxer_api/src/api/worker/tests/BulkBanFileShasAudit.test.ts b/fluxer_api/src/api/worker/tests/BulkBanFileShasAudit.test.ts index 0a169464d..ca5a1c85b 100644 --- a/fluxer_api/src/api/worker/tests/BulkBanFileShasAudit.test.ts +++ b/fluxer_api/src/api/worker/tests/BulkBanFileShasAudit.test.ts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import type {AdminAuditLog} from '@app/api/admin/IAdminRepository'; +import type {AdminAuditLog} from '@app/api/admin/AdminRepository'; import {createTestAccount, setUserACLs} from '@app/api/auth/tests/AuthTestUtils'; import {BANNED_FILE_SHAS_REFRESH_CHANNEL} from '@app/api/constants/ContentModeration'; import {getSnowflakeService, setInjectedWorkerService} from '@app/api/middleware/ServiceRegistry'; diff --git a/fluxer_api/src/api/worker/tests/BulkDeleteMessagesForUsersAudit.test.ts b/fluxer_api/src/api/worker/tests/BulkDeleteMessagesForUsersAudit.test.ts index 666193e97..0c0841d65 100644 --- a/fluxer_api/src/api/worker/tests/BulkDeleteMessagesForUsersAudit.test.ts +++ b/fluxer_api/src/api/worker/tests/BulkDeleteMessagesForUsersAudit.test.ts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import type {AdminAuditLog} from '@app/api/admin/IAdminRepository'; +import type {AdminAuditLog} from '@app/api/admin/AdminRepository'; import {sendChannelMessage, setupTestGuildWithMembers} from '@app/api/channel/tests/ChannelTestUtils'; import {getGatewayService, getSnowflakeService} from '@app/api/middleware/ServiceRegistry'; import { diff --git a/fluxer_api/src/api/worker/tests/BulkScheduleUserDeletion.test.ts b/fluxer_api/src/api/worker/tests/BulkScheduleUserDeletion.test.ts index 338de8349..a618f5c3a 100644 --- a/fluxer_api/src/api/worker/tests/BulkScheduleUserDeletion.test.ts +++ b/fluxer_api/src/api/worker/tests/BulkScheduleUserDeletion.test.ts @@ -1,7 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import {AdminRepository} from '@app/api/admin/AdminRepository'; -import type {AdminAuditLog} from '@app/api/admin/IAdminRepository'; +import {type AdminAuditLog, AdminRepository} from '@app/api/admin/AdminRepository'; import { clearTestEmails, createTestAccount, @@ -29,7 +28,7 @@ import { getUserPermissionUtils, getUserRepository, } from '@app/api/middleware/ServiceSingletons'; -import {ReportStatus} from '@app/api/report/IReportRepository'; +import {ReportStatus} from '@app/api/report/ReportModels'; import {ReportRepository} from '@app/api/report/ReportRepository'; import {getReportSearchService} from '@app/api/SearchFactory'; import {drainSearchTasks} from '@app/api/search/SearchTaskTracker'; diff --git a/fluxer_api/src/api/worker/tests/ExpireReportSnapshots.test.ts b/fluxer_api/src/api/worker/tests/ExpireReportSnapshots.test.ts index dce264875..daa146c74 100644 --- a/fluxer_api/src/api/worker/tests/ExpireReportSnapshots.test.ts +++ b/fluxer_api/src/api/worker/tests/ExpireReportSnapshots.test.ts @@ -9,8 +9,8 @@ import { type IARMessageContextRow, type IARSubmissionRow, } from '@app/api/database/types/ReportTypes'; -import type {IARSubmission} from '@app/api/report/IReportRepository'; import type {ReportDeletionDeps} from '@app/api/report/ReportDeletion'; +import type {IARSubmission} from '@app/api/report/ReportModels'; import {ReportRepository} from '@app/api/report/ReportRepository'; import type {IReportSearchService} from '@app/api/search/IReportSearchService'; import {MockStorageService} from '@app/api/test/mocks/MockStorageService'; diff --git a/fluxer_api/src/api/worker/tests/HandleMentionsThread.test.ts b/fluxer_api/src/api/worker/tests/HandleMentionsThread.test.ts index e822d7bc5..c92c092d1 100644 --- a/fluxer_api/src/api/worker/tests/HandleMentionsThread.test.ts +++ b/fluxer_api/src/api/worker/tests/HandleMentionsThread.test.ts @@ -12,7 +12,7 @@ import type {ChannelRepository} from '@app/api/channel/ChannelRepository'; import {syncChannelThreadsConfig} from '@app/api/experiment/ChannelThreadsGate'; import type {GatewayMentionSourceEntry, IGatewayService} from '@app/api/infrastructure/IGatewayService'; import {ThreadState} from '@app/api/models/ThreadState'; -import type {ReadStateMentionUpdate} from '@app/api/read_state/IReadStateRepository'; +import type {ReadStateMentionUpdate} from '@app/api/read_state/ReadStateRepository'; import type {ReadStateService} from '@app/api/read_state/ReadStateService'; import {NoopLogger} from '@app/api/test/mocks/NoopLogger'; import type {UserRepository} from '@app/api/user/repositories/UserRepository'; @@ -118,8 +118,6 @@ function createHarness(params: { type: params.channelType ?? ChannelTypes.PUBLIC_THREAD, }; const channelRepository = { - getMessage: async () => message, - findUnique: async () => channel, threads: { getState: async () => state, getMembers: async (_threadId: ChannelID, userIds: Array) => @@ -135,11 +133,10 @@ function createHarness(params: { getParentConfig: async () => ({availableTags: (params.forumTags?.live ?? []).map((id) => ({id}))}), }, channelData: { - findUnique: async () => ({ - id: PARENT, - type: parentType, - isThreadOnly: () => parentType === ChannelTypes.GUILD_FORUM, - }), + findUnique: async (channelId: ChannelID) => + channelId === THREAD + ? channel + : {id: PARENT, type: parentType, isThreadOnly: () => parentType === ChannelTypes.GUILD_FORUM}, }, messages: { getMessage: async () => ({...message, toRow: () => ({flags: message.flags})}), diff --git a/fluxer_api/src/api/worker/tests/IndexChannelMessages.test.ts b/fluxer_api/src/api/worker/tests/IndexChannelMessages.test.ts index 4ae757ae5..736e5af65 100644 --- a/fluxer_api/src/api/worker/tests/IndexChannelMessages.test.ts +++ b/fluxer_api/src/api/worker/tests/IndexChannelMessages.test.ts @@ -35,8 +35,10 @@ function createHarness(messages: Array = []): { setWorkerDependenciesForTest({ kvClient, channelRepository: { - listMessages: async (_channelId: unknown, cursor: unknown) => (cursor === undefined ? messages : []), - findUnique: async () => null, + messages: { + listMessages: async (_channelId: unknown, cursor: unknown) => (cursor === undefined ? messages : []), + }, + channelData: {findUnique: async () => null}, } as unknown as ChannelRepository, userRepository: {listUsers: async () => []} as unknown as UserRepository, }); diff --git a/fluxer_api/src/api/worker/tests/SeedThreadPermissions.test.ts b/fluxer_api/src/api/worker/tests/SeedThreadPermissions.test.ts index e978356cb..6b77af905 100644 --- a/fluxer_api/src/api/worker/tests/SeedThreadPermissions.test.ts +++ b/fluxer_api/src/api/worker/tests/SeedThreadPermissions.test.ts @@ -94,8 +94,8 @@ describe('seedThreadPermissions', () => { }) .execute(); const channelRepository = getChannelRepository(); - const stored = await channelRepository.findUnique(createChannelID(BigInt(channel.id))); - await channelRepository.upsert({ + const stored = await channelRepository.channelData.findUnique(createChannelID(BigInt(channel.id))); + await channelRepository.channelData.upsert({ ...stored!.toRow(), permission_overwrites: new Map([ [ @@ -118,9 +118,9 @@ describe('seedThreadPermissions', () => { await seedThreadPermissions({guildId: guild.id}, helpers()); const everyone = await getGuildRepository().getRole(createRoleID(BigInt(guild.id)), guildId); expect(everyone?.permissions).toBe(DEFAULT_PERMISSIONS | DEFAULT_THREAD_PERMISSIONS); - const seeded = (await channelRepository.findUnique(createChannelID(BigInt(channel.id))))!.permissionOverwrites.get( - createRoleID(BigInt(guild.id)), - ); + const seeded = (await channelRepository.channelData.findUnique( + createChannelID(BigInt(channel.id)), + ))!.permissionOverwrites.get(createRoleID(BigInt(guild.id))); expect(seeded?.allow).toBe(0n); expect(seeded?.deny).toBe(Permissions.SEND_MESSAGES | DEFAULT_THREAD_PERMISSIONS); expect(dispatchGuild.mock.calls.map(([params]) => params.event)).toEqual([ @@ -166,7 +166,7 @@ describe('seedThreadPermissions', () => { setConfig({...ALL_THREADS_ACTIVE, ever_enabled: true, enabled_guild_ids: [guild.id]}); await seedThreadPermissions({guildId: guild.id}, helpers()); - const seeded = (await getChannelRepository().findUnique( + const seeded = (await getChannelRepository().channelData.findUnique( createChannelID(BigInt(channel.id)), ))!.permissionOverwrites.get(createRoleID(BigInt(role.id))); expect(seeded?.allow).toBe(Permissions.SEND_MESSAGES | DEFAULT_THREAD_PERMISSIONS); diff --git a/fluxer_api/src/api/worker/tests/SendSystemDmCancel.test.ts b/fluxer_api/src/api/worker/tests/SendSystemDmCancel.test.ts index 3d32dace1..ed1843be1 100644 --- a/fluxer_api/src/api/worker/tests/SendSystemDmCancel.test.ts +++ b/fluxer_api/src/api/worker/tests/SendSystemDmCancel.test.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {ChannelService} from '@app/api/channel/services/ChannelService'; -import type {IJobLedgerRepository} from '@app/api/jobs/IJobLedgerRepository'; +import type {JobLedgerRepository} from '@app/api/jobs/JobLedgerRepository'; import {setInjectedWorkerService} from '@app/api/middleware/ServiceRegistry'; import {NoopWorkerService} from '@app/api/test/NoopWorkerService'; import type {UserRepository} from '@app/api/user/repositories/UserRepository'; @@ -44,7 +44,7 @@ function createLedgerStub(cancelAfter: number) { cancelChecks.count += 1; return cancelChecks.count > cancelAfter; }, - } as unknown as IJobLedgerRepository; + } as unknown as JobLedgerRepository; return {ledger, markCancelled, markSucceeded}; } diff --git a/fluxer_api/src/api/worker/tests/ThreadMaintenanceSweeps.test.ts b/fluxer_api/src/api/worker/tests/ThreadMaintenanceSweeps.test.ts index 25053d89e..18c84c7e1 100644 --- a/fluxer_api/src/api/worker/tests/ThreadMaintenanceSweeps.test.ts +++ b/fluxer_api/src/api/worker/tests/ThreadMaintenanceSweeps.test.ts @@ -180,7 +180,7 @@ describe('thread maintenance sweeps', () => { it('includes thread messages in user and guild harvests', async () => { const s = await setup(); const harvested = await harvestMessages( - getChannelRepository(), + getChannelRepository().messages, createUserID(BigInt(s.member.userId)), Date.now(), null, diff --git a/fluxer_api/src/api/worker/tests/ThreadMaintenanceTasks.test.ts b/fluxer_api/src/api/worker/tests/ThreadMaintenanceTasks.test.ts index f0eaa76f5..e3fa59ef5 100644 --- a/fluxer_api/src/api/worker/tests/ThreadMaintenanceTasks.test.ts +++ b/fluxer_api/src/api/worker/tests/ThreadMaintenanceTasks.test.ts @@ -134,7 +134,7 @@ describe('thread maintenance tasks', () => { const thread = await startThread(s.owner.token, s.channelId); await deleteChannelThreads({guildId: s.guildId, parentId: s.channelId}, helpers()); expect(await state(thread.id)).toBeNull(); - expect(await getChannelRepository().findUnique(createChannelID(BigInt(thread.id)))).toBeNull(); + expect(await getChannelRepository().channelData.findUnique(createChannelID(BigInt(thread.id)))).toBeNull(); }); it('purges thread memberships of a removed guild member and keeps those of a rejoined one', async () => { diff --git a/fluxer_api/src/api/worker/tests/WorkerAckHeartbeat.test.ts b/fluxer_api/src/api/worker/tests/WorkerAckHeartbeat.test.ts index 0d75d70f3..7a38bbdab 100644 --- a/fluxer_api/src/api/worker/tests/WorkerAckHeartbeat.test.ts +++ b/fluxer_api/src/api/worker/tests/WorkerAckHeartbeat.test.ts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import type {IJobLedgerRepository} from '@app/api/jobs/IJobLedgerRepository'; +import type {JobLedgerRepository} from '@app/api/jobs/JobLedgerRepository'; import {setInjectedWorkerService} from '@app/api/middleware/ServiceRegistry'; import {NoopWorkerService} from '@app/api/test/NoopWorkerService'; import {WorkerRunner} from '@app/api/worker/WorkerRunner'; @@ -32,7 +32,7 @@ function createRunner(task: () => Promise): TestWorkerRunner { queue: queueStub, consumerName: 'workers_batch', laneName: 'batch', - ledger: {} as IJobLedgerRepository, + ledger: {} as JobLedgerRepository, concurrency: 12, maxDeliver: LANE_MAX_DELIVER, ackWaitMs: LANE_ACK_WAIT_MS, @@ -149,7 +149,7 @@ describe('Worker ack heartbeat', () => { queue: queueStub, consumerName: 'workers_batch', laneName: 'batch', - ledger: {} as IJobLedgerRepository, + ledger: {} as JobLedgerRepository, concurrency: 12, maxDeliver: LANE_MAX_DELIVER, ackWaitMs: LANE_ACK_WAIT_MS, diff --git a/fluxer_api/src/api/worker/tests/WorkerHeartbeat.test.ts b/fluxer_api/src/api/worker/tests/WorkerHeartbeat.test.ts index 91a33666a..9d0d01687 100644 --- a/fluxer_api/src/api/worker/tests/WorkerHeartbeat.test.ts +++ b/fluxer_api/src/api/worker/tests/WorkerHeartbeat.test.ts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import type {IJobLedgerRepository} from '@app/api/jobs/IJobLedgerRepository'; +import type {JobLedgerRepository} from '@app/api/jobs/JobLedgerRepository'; import {setInjectedWorkerService} from '@app/api/middleware/ServiceRegistry'; import {NoopWorkerService} from '@app/api/test/NoopWorkerService'; import {CronScheduler} from '@app/api/worker/CronScheduler'; @@ -73,7 +73,7 @@ function createRunner(messages: FakeConsumerMessages, heartbeat: WorkerHeartbeat }, consumerName: 'workers_batch', laneName: 'batch', - ledger: {} as IJobLedgerRepository, + ledger: {} as JobLedgerRepository, concurrency: 12, maxDeliver: 25, ackWaitMs: 120000, diff --git a/fluxer_api/src/api/worker/tests/WorkerRetiredTask.test.ts b/fluxer_api/src/api/worker/tests/WorkerRetiredTask.test.ts index 76898abb0..3d4281f59 100644 --- a/fluxer_api/src/api/worker/tests/WorkerRetiredTask.test.ts +++ b/fluxer_api/src/api/worker/tests/WorkerRetiredTask.test.ts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import type {IJobLedgerRepository} from '@app/api/jobs/IJobLedgerRepository'; +import type {JobLedgerRepository} from '@app/api/jobs/JobLedgerRepository'; import {setInjectedWorkerService} from '@app/api/middleware/ServiceRegistry'; import {NoopWorkerService} from '@app/api/test/NoopWorkerService'; import {WorkerRunner} from '@app/api/worker/WorkerRunner'; @@ -36,7 +36,7 @@ function createRunner(): TestWorkerRunner { queue: queueStub, consumerName: 'workers_lifecycle', laneName: 'lifecycle', - ledger: ledgerStub as unknown as IJobLedgerRepository, + ledger: ledgerStub as unknown as JobLedgerRepository, concurrency: 8, maxDeliver: 25, ackWaitMs: 60000, diff --git a/fluxer_api/src/api/worker/tests/WorkerRunnerResubscribe.test.ts b/fluxer_api/src/api/worker/tests/WorkerRunnerResubscribe.test.ts index 031df0601..8acd6dd9c 100644 --- a/fluxer_api/src/api/worker/tests/WorkerRunnerResubscribe.test.ts +++ b/fluxer_api/src/api/worker/tests/WorkerRunnerResubscribe.test.ts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import type {IJobLedgerRepository} from '@app/api/jobs/IJobLedgerRepository'; +import type {JobLedgerRepository} from '@app/api/jobs/JobLedgerRepository'; import {setInjectedWorkerService} from '@app/api/middleware/ServiceRegistry'; import {NoopWorkerService} from '@app/api/test/NoopWorkerService'; import {WorkerRunner} from '@app/api/worker/WorkerRunner'; @@ -93,7 +93,7 @@ function createRunner(streams: Array): {runner: WorkerRunn queue, consumerName: 'workers_batch', laneName: 'batch', - ledger: {} as IJobLedgerRepository, + ledger: {} as JobLedgerRepository, concurrency: 12, maxDeliver: 25, ackWaitMs: 120000, diff --git a/fluxer_api/src/api/worker/tests/WorkerRunnerShutdown.test.ts b/fluxer_api/src/api/worker/tests/WorkerRunnerShutdown.test.ts index 7be261bd7..da3af954d 100644 --- a/fluxer_api/src/api/worker/tests/WorkerRunnerShutdown.test.ts +++ b/fluxer_api/src/api/worker/tests/WorkerRunnerShutdown.test.ts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import type {IJobLedgerRepository} from '@app/api/jobs/IJobLedgerRepository'; +import type {JobLedgerRepository} from '@app/api/jobs/JobLedgerRepository'; import {setInjectedWorkerService} from '@app/api/middleware/ServiceRegistry'; import {NoopWorkerService} from '@app/api/test/NoopWorkerService'; import {WorkerRunner} from '@app/api/worker/WorkerRunner'; @@ -78,7 +78,7 @@ function createRunner(task: () => Promise, messages: FakeConsumerMessages) queue: createQueueStub(messages), consumerName: 'workers_batch', laneName: 'batch', - ledger: {} as IJobLedgerRepository, + ledger: {} as JobLedgerRepository, concurrency: 12, maxDeliver: 25, ackWaitMs: 120000, diff --git a/fluxer_api/src/api/worker/tests/WorkerServiceLedger.test.ts b/fluxer_api/src/api/worker/tests/WorkerServiceLedger.test.ts index 820929526..675eb6ce1 100644 --- a/fluxer_api/src/api/worker/tests/WorkerServiceLedger.test.ts +++ b/fluxer_api/src/api/worker/tests/WorkerServiceLedger.test.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService'; -import type {CreateJobInput, IJobLedgerRepository} from '@app/api/jobs/IJobLedgerRepository'; +import type {CreateJobInput, JobLedgerRepository} from '@app/api/jobs/JobLedgerRepository'; import type {JetStreamWorkerQueue} from '@app/api/worker/JetStreamWorkerQueue'; import {WorkerService} from '@app/api/worker/WorkerService'; import {describe, expect, test} from 'vitest'; @@ -47,7 +47,7 @@ function createHarness(options?: { if (options?.discardError) throw options.discardError; discarded.push({jobId, createdAt}); }, - } as unknown as IJobLedgerRepository; + } as unknown as JobLedgerRepository; const queue = { enqueue: async (taskType: string, payload: Record) => { calls.push('enqueue'); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1756b2d9b..046156029 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1000,13 +1000,6 @@ importers: version: 5.0.1(@opentelemetry/api@1.9.1)(@types/node@26.6.1)(@vitest/coverage-v8@5.0.1)(happy-dom@20.14.5)(msw@2.15.0(@types/node@26.6.1)(typescript@7.0.2))(vite@8.3.0(@types/node@26.6.1)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.1)) fluxer_api/pkgs/virus_scan: - dependencies: - '@fluxer/logger': - specifier: workspace:* - version: link:../../../packages/logger - itty-time: - specifier: 'catalog:' - version: 2.0.2 devDependencies: '@types/node': specifier: 'catalog:'