mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 04:55:42 +09:00
refactor(api): collapse single-impl interfaces and facades (#3382)
This commit is contained in:
1 parent
e7b13fd19f
commit
c16d414d93
426 files changed
+2085
-6445
No files matched your search
@@ -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<unknown>
|
||||
| Map<unknown, unknown>
|
||||
| Array<unknown>
|
||||
| Record<string, unknown>
|
||||
| null;
|
||||
export type CassandraParams = Record<string, CassandraValue>;
|
||||
|
||||
export interface PreparedQuery<P extends CassandraParams = CassandraParams> {
|
||||
cql: string;
|
||||
params: P;
|
||||
}
|
||||
@@ -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<void>;
|
||||
shutdown(): Promise<void>;
|
||||
execute<P extends CassandraParams>(
|
||||
query: PreparedQuery<P>,
|
||||
options?: CassandraExecuteOptions,
|
||||
): Promise<cassandra.types.ResultSet>;
|
||||
batch(queries: Array<PreparedQuery>, options?: CassandraBatchOptions): Promise<void>;
|
||||
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<void> {
|
||||
@@ -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<P extends CassandraParams>(
|
||||
query: PreparedQuery<P>,
|
||||
options: CassandraExecuteOptions = {},
|
||||
): Promise<cassandra.types.ResultSet> {
|
||||
return this.getNativeClient().execute(query.cql, query.params, {
|
||||
prepare: options.prepare ?? true,
|
||||
});
|
||||
}
|
||||
|
||||
public async batch(queries: Array<PreparedQuery>, options: CassandraBatchOptions = {}): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
defaultClientState.client = null;
|
||||
}
|
||||
|
||||
function getDefaultCassandraClient(): ICassandraClient {
|
||||
function getDefaultCassandraClient(): CassandraClient {
|
||||
if (defaultClientState.client === null) {
|
||||
throw new Error('Cassandra client is not initialized. Call initCassandra() first.');
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
export interface Logger {
|
||||
info(context: Record<string, unknown>, message: string): void;
|
||||
debug(context: Record<string, unknown>, message: string): void;
|
||||
error(context: Record<string, unknown>, message: string): void;
|
||||
}
|
||||
|
||||
export const NoopLogger: Logger = {
|
||||
info(): void {},
|
||||
debug(): void {},
|
||||
error(): void {},
|
||||
};
|
||||
@@ -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<T extends EmailTemplateKey>(
|
||||
templateKey: T,
|
||||
locale: string | null,
|
||||
variables: EmailTemplateVariables[T],
|
||||
productName: string,
|
||||
): I18nResult<EmailTemplateKey, EmailTemplate>;
|
||||
}
|
||||
|
||||
export class EmailI18nService implements IEmailI18nService {
|
||||
getTemplate<T extends EmailTemplateKey>(
|
||||
templateKey: T,
|
||||
locale: string | null,
|
||||
variables: EmailTemplateVariables[T],
|
||||
productName: string,
|
||||
): I18nResult<EmailTemplateKey, EmailTemplate> {
|
||||
return getEmailTemplate(templateKey, locale, variables, productName);
|
||||
}
|
||||
}
|
||||
@@ -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<boolean> {
|
||||
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;
|
||||
|
||||
@@ -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<string, string>;
|
||||
}
|
||||
|
||||
export interface ITestEmailService extends IEmailService {
|
||||
listSentEmails(): Array<SentEmailRecord>;
|
||||
clearSentEmails(): void;
|
||||
}
|
||||
@@ -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<string, string>;
|
||||
}
|
||||
|
||||
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<SentEmailRecord> = [];
|
||||
private readonly logger: {
|
||||
info: (message: string) => void;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -17,7 +17,7 @@ interface AcceptLanguagePreference {
|
||||
const TRADITIONAL_CHINESE_SUBTAGS: ReadonlySet<string> = new Set(['hant', 'tw', 'hk', 'mo']);
|
||||
const SIMPLIFIED_CHINESE_SUBTAGS: ReadonlySet<string> = 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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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}`;
|
||||
}
|
||||
@@ -9,10 +9,6 @@
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluxer/logger": "workspace:*",
|
||||
"itty-time": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"typescript": "catalog:ts7"
|
||||
|
||||
@@ -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<VirusScanProviderResult>;
|
||||
scanBuffer(buffer: Buffer): Promise<VirusScanProviderResult>;
|
||||
}
|
||||
@@ -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<void> {
|
||||
await this.failureReporter.initialize();
|
||||
}
|
||||
|
||||
async scanFile(filePath: string): Promise<VirusScanResult> {
|
||||
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<VirusScanResult> {
|
||||
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<string> {
|
||||
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<void> {
|
||||
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',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
export interface IVirusHashCache {
|
||||
isKnownVirusHash(fileHash: string): Promise<boolean>;
|
||||
cacheVirusHash(fileHash: string): Promise<void>;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
export interface IVirusScanCacheStore {
|
||||
get<T>(key: string): Promise<T | null>;
|
||||
set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;
|
||||
}
|
||||
@@ -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<boolean> {
|
||||
const cachedValue = await this.cacheStore.get(this.buildCacheKey(fileHash));
|
||||
return cachedValue != null;
|
||||
}
|
||||
|
||||
async cacheVirusHash(fileHash: string): Promise<void> {
|
||||
await this.cacheStore.set(this.buildCacheKey(fileHash), 'true', this.ttlSeconds);
|
||||
}
|
||||
|
||||
private buildCacheKey(fileHash: string): string {
|
||||
return `${this.keyPrefix}:${fileHash}`;
|
||||
}
|
||||
}
|
||||
@@ -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<void>;
|
||||
reportFailure(context: VirusScanFailureContext): Promise<void>;
|
||||
}
|
||||
@@ -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<void> {}
|
||||
|
||||
async reportFailure(_context: VirusScanFailureContext): Promise<void> {}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
export interface VirusScanFailureContext {
|
||||
error: unknown;
|
||||
filename: string;
|
||||
fileHash: string;
|
||||
failOpen: boolean;
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
let workerDependencies: unknown | null = null;
|
||||
|
||||
export function setWorkerDependencies<T>(dependencies: T): void {
|
||||
workerDependencies = dependencies;
|
||||
}
|
||||
|
||||
export function getWorkerDependencies<T>(): 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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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<string, string>;
|
||||
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<AdminAuditLog> {
|
||||
await upsertOne(AdminAuditLogs.insert(log));
|
||||
return this.mapRowToAuditLog(log);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, string>;
|
||||
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<AdminAuditLog>;
|
||||
|
||||
abstract getAuditLog(logId: bigint): Promise<AdminAuditLog | null>;
|
||||
|
||||
abstract listAuditLogsByIds(logIds: Array<bigint>): Promise<Array<AdminAuditLog>>;
|
||||
|
||||
abstract listAllAuditLogsPaginated(limit: number, lastLogId?: bigint): Promise<Array<AdminAuditLog>>;
|
||||
|
||||
abstract isIpBanned(ip: string): Promise<boolean>;
|
||||
|
||||
abstract banIp(ip: string, ttlSeconds?: number | null): Promise<void>;
|
||||
|
||||
abstract banIpTemp(ip: string, ttlSeconds: number): Promise<void>;
|
||||
|
||||
abstract unbanIp(ip: string): Promise<void>;
|
||||
|
||||
abstract loadAllBannedIpEntries(): Promise<Array<BannedIpEntry>>;
|
||||
|
||||
abstract isEmailBanned(email: string): Promise<boolean>;
|
||||
|
||||
abstract banEmail(email: string): Promise<void>;
|
||||
|
||||
abstract unbanEmail(email: string): Promise<void>;
|
||||
|
||||
abstract loadAllBannedEmails(): Promise<Array<string>>;
|
||||
|
||||
abstract banPhrase(phrase: string): Promise<void>;
|
||||
|
||||
abstract unbanPhrase(phrase: string): Promise<void>;
|
||||
|
||||
abstract loadAllBannedPhrases(): Promise<Array<string>>;
|
||||
|
||||
abstract loadAllBannedIps(): Promise<Set<string>>;
|
||||
|
||||
abstract banUrl(row: BannedUrlRow): Promise<void>;
|
||||
|
||||
abstract unbanUrl(url: string): Promise<void>;
|
||||
|
||||
abstract loadAllBannedUrls(): Promise<Array<BannedUrlRow>>;
|
||||
|
||||
abstract banUrlDomain(row: BannedUrlDomainRow): Promise<void>;
|
||||
|
||||
abstract unbanUrlDomain(domain: string): Promise<void>;
|
||||
|
||||
abstract loadAllBannedUrlDomains(): Promise<Array<BannedUrlDomainRow>>;
|
||||
|
||||
abstract banFileSha(row: BannedFileShaRow): Promise<void>;
|
||||
|
||||
abstract unbanFileSha(sha256Hex: string): Promise<void>;
|
||||
|
||||
abstract unbanFeedFileSha(sha256Hex: string): Promise<boolean>;
|
||||
|
||||
abstract loadAllBannedFileShas(): Promise<Array<BannedFileShaRow>>;
|
||||
|
||||
abstract banAvatarHash(row: BannedAvatarHashRow): Promise<void>;
|
||||
|
||||
abstract unbanAvatarHash(hashShort: string): Promise<void>;
|
||||
|
||||
abstract loadAllBannedAvatarHashes(): Promise<Array<BannedAvatarHashRow>>;
|
||||
|
||||
abstract banProfileSubstring(row: BannedProfileSubstringRow): Promise<void>;
|
||||
|
||||
abstract unbanProfileSubstring(scope: BannedProfileSubstringScope, substring: string): Promise<void>;
|
||||
|
||||
abstract loadAllBannedProfileSubstrings(): Promise<Array<BannedProfileSubstringRow>>;
|
||||
}
|
||||
@@ -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<GuildDiscoveryRow>,
|
||||
guildService: GuildService,
|
||||
userRepository: IUserRepository,
|
||||
userRepository: UserRepository,
|
||||
): Promise<Map<string, GuildEnrichment>> {
|
||||
const map = new Map<string, GuildEnrichment>();
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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<string>;
|
||||
}
|
||||
|
||||
export interface UpdateAdminApiKeyData {
|
||||
name?: string;
|
||||
acls?: Set<string>;
|
||||
}
|
||||
|
||||
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<AdminApiKey> {
|
||||
const keyHash = await hashPassword(rawKey);
|
||||
const createdAt = new Date();
|
||||
|
||||
@@ -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<string>;
|
||||
}
|
||||
|
||||
export interface UpdateAdminApiKeyData {
|
||||
name?: string;
|
||||
acls?: Set<string>;
|
||||
}
|
||||
|
||||
export interface IAdminApiKeyRepository {
|
||||
create(data: CreateAdminApiKeyData, createdBy: UserID, keyId: bigint, rawKey: string): Promise<AdminApiKey>;
|
||||
findById(keyId: bigint): Promise<AdminApiKey | null>;
|
||||
update(apiKey: AdminApiKey, data: UpdateAdminApiKeyData): Promise<AdminApiKey>;
|
||||
listByCreator(createdBy: UserID): Promise<Array<AdminApiKey>>;
|
||||
updateLastUsed(keyId: bigint, expiresAt: Date | null): Promise<void>;
|
||||
revoke(keyId: bigint, createdBy: UserID): Promise<void>;
|
||||
}
|
||||
@@ -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,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<WorkerTaskName>,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<IUserRepository, 'findUnique'>;
|
||||
guildRepository?: Pick<IGuildRepositoryAggregate, 'findUnique'>;
|
||||
channelRepository?: Pick<IChannelRepository, 'findUnique'>;
|
||||
userRepository?: Pick<UserRepository, 'findUnique'>;
|
||||
guildRepository?: Pick<GuildRepository, 'findUnique'>;
|
||||
channelRepository?: Pick<ChannelDataRepository, 'findUnique'>;
|
||||
}
|
||||
|
||||
interface AuditLogEnrichment {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Array<string>> {
|
||||
const {count, durationType, durationQuantity, createdByUserId} = this.validateOptions(options);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<MessageResponseAccessContext> {
|
||||
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<ChannelNsfwContext> {
|
||||
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,
|
||||
|
||||
@@ -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<MessageResponseAccessContext> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<typeof c> => 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<UserID>();
|
||||
for (const model of channelModels) {
|
||||
if (model) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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),
|
||||
]);
|
||||
|
||||
@@ -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<SuccessResponse> {
|
||||
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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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<void> {
|
||||
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,
|
||||
|
||||
@@ -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<void> {
|
||||
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}`,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string> {
|
||||
const totp = new TotpGenerator(secret);
|
||||
const codes = await totp.generateTotp();
|
||||
return codes[0]!;
|
||||
}
|
||||
|
||||
async function loginWithTotp(harness: ApiTestHarness, account: TestAccount, secret: string): Promise<TestAccount> {
|
||||
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<BackupCodesResponse>(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({
|
||||
|
||||
@@ -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<TestAccount> {
|
||||
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 () => {
|
||||
|
||||
@@ -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<TestAccount> {
|
||||
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,
|
||||
|
||||
@@ -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<TestAccount> {
|
||||
const login = await createBuilderWithoutAuth<LoginMfaResponse>(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,
|
||||
|
||||
@@ -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<string> {
|
||||
const totp = new TotpGenerator(secret);
|
||||
const codes = await totp.generateTotp();
|
||||
return codes[0]!;
|
||||
}
|
||||
|
||||
async function enableTotpForAccount(harness: ApiTestHarness, account: TestAccount, secret: string): Promise<void> {
|
||||
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<TestAccount> {
|
||||
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<string> {
|
||||
const {response} = await createBuilder<BackupCodesResponse>(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<ErrorResponse>(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<ErrorResponse>(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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Channel | null> {
|
||||
return this.repository.channelData.findUnique(channelId);
|
||||
}
|
||||
|
||||
async upsert(data: ChannelRow): Promise<Channel> {
|
||||
return this.repository.channelData.upsert(data);
|
||||
}
|
||||
|
||||
async delete(channelId: ChannelID, guildId?: GuildID, type?: number): Promise<void> {
|
||||
return this.repository.channelData.delete(channelId, guildId, type);
|
||||
}
|
||||
|
||||
async listMessages(
|
||||
channelId: ChannelID,
|
||||
beforeMessageId?: MessageID,
|
||||
limit?: number,
|
||||
afterMessageId?: MessageID,
|
||||
): Promise<Array<Message>> {
|
||||
return this.repository.messages.listMessages(channelId, beforeMessageId, limit, afterMessageId);
|
||||
}
|
||||
|
||||
async getMessage(channelId: ChannelID, messageId: MessageID): Promise<Message | null> {
|
||||
return this.repository.messages.getMessage(channelId, messageId);
|
||||
}
|
||||
|
||||
async upsertMessage(data: MessageRow, oldData?: MessageRow | null, opts?: UpsertMessageOptions): Promise<Message> {
|
||||
return this.repository.messages.upsertMessage(data, oldData, opts);
|
||||
}
|
||||
|
||||
async deleteMessage(
|
||||
channelId: ChannelID,
|
||||
messageId: MessageID,
|
||||
authorId: UserID,
|
||||
pinnedTimestamp?: Date,
|
||||
): Promise<void> {
|
||||
return this.repository.messages.deleteMessage(channelId, messageId, authorId, pinnedTimestamp);
|
||||
}
|
||||
|
||||
async bulkDeleteMessages(channelId: ChannelID, messageIds: Array<MessageID>): Promise<void> {
|
||||
return this.repository.messages.bulkDeleteMessages(channelId, messageIds);
|
||||
}
|
||||
|
||||
async listGuildChannels(guildId: GuildID, mode: GuildChannelListMode): Promise<Array<Channel>> {
|
||||
return this.repository.channelData.listGuildChannels(guildId, mode);
|
||||
}
|
||||
|
||||
async listChannels(channelIds: Array<ChannelID>): Promise<Array<Channel>> {
|
||||
return this.repository.channelData.listChannels(channelIds);
|
||||
}
|
||||
|
||||
async lookupAttachmentByChannelAndFilename(
|
||||
channelId: ChannelID,
|
||||
attachmentId: AttachmentID,
|
||||
filename: string,
|
||||
): Promise<MessageID | null> {
|
||||
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<void> {
|
||||
return this.repository.messages.anonymizeMessage(channelId, messageId, newAuthorId);
|
||||
}
|
||||
|
||||
async deleteAllChannelMessages(channelId: ChannelID): Promise<void> {
|
||||
return this.repository.messages.deleteAllChannelMessages(channelId);
|
||||
}
|
||||
|
||||
async updateEmbeds(message: Message): Promise<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<Channel | null>;
|
||||
|
||||
abstract upsert(data: ChannelRow): Promise<Channel>;
|
||||
|
||||
abstract delete(channelId: ChannelID, guildId?: GuildID, type?: number): Promise<void>;
|
||||
|
||||
abstract listGuildChannels(guildId: GuildID, mode: GuildChannelListMode): Promise<Array<Channel>>;
|
||||
|
||||
abstract listChannels(channelIds: Array<ChannelID>): Promise<Array<Channel>>;
|
||||
|
||||
abstract listMessages(
|
||||
channelId: ChannelID,
|
||||
beforeMessageId?: MessageID,
|
||||
limit?: number,
|
||||
afterMessageId?: MessageID,
|
||||
): Promise<Array<Message>>;
|
||||
|
||||
abstract getMessage(channelId: ChannelID, messageId: MessageID): Promise<Message | null>;
|
||||
|
||||
abstract upsertMessage(data: MessageRow, oldData?: MessageRow | null, opts?: UpsertMessageOptions): Promise<Message>;
|
||||
|
||||
abstract deleteMessage(
|
||||
channelId: ChannelID,
|
||||
messageId: MessageID,
|
||||
authorId: UserID,
|
||||
pinnedTimestamp?: Date,
|
||||
): Promise<void>;
|
||||
|
||||
abstract bulkDeleteMessages(channelId: ChannelID, messageIds: Array<MessageID>): Promise<void>;
|
||||
|
||||
abstract lookupAttachmentByChannelAndFilename(
|
||||
channelId: ChannelID,
|
||||
attachmentId: AttachmentID,
|
||||
filename: string,
|
||||
): Promise<MessageID | null>;
|
||||
|
||||
abstract listMessagesByAuthor(
|
||||
authorId: UserID,
|
||||
limit?: number,
|
||||
lastMessageId?: MessageID,
|
||||
): Promise<
|
||||
Array<{
|
||||
channelId: ChannelID;
|
||||
messageId: MessageID;
|
||||
}>
|
||||
>;
|
||||
|
||||
abstract anonymizeMessage(channelId: ChannelID, messageId: MessageID, newAuthorId: UserID): Promise<void>;
|
||||
|
||||
abstract deleteAllChannelMessages(channelId: ChannelID): Promise<void>;
|
||||
|
||||
abstract updateEmbeds(message: Message): Promise<void>;
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Channel | null> {
|
||||
const prefetched = this.requestCache?.takeChannel(channelId);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<CrosspostedMessageRow, 'source_m
|
||||
return {source_message_id: key.sourceMessageId, webhook_id: key.webhookId};
|
||||
}
|
||||
|
||||
export class CrosspostedMessageRepository extends ICrosspostedMessageRepository {
|
||||
export class CrosspostedMessageRepository {
|
||||
async get(sourceMessageId: MessageID, webhookId: WebhookID): Promise<CrosspostedMessageRow | null> {
|
||||
return fetchOne<CrosspostedMessageRow>(FETCH_CROSSPOSTED_MESSAGE_CQL, {
|
||||
source_message_id: sourceMessageId,
|
||||
|
||||
@@ -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<Channel | null>;
|
||||
|
||||
abstract upsert(data: ChannelRow, oldData?: ChannelRow | null): Promise<Channel>;
|
||||
|
||||
abstract updateLastMessageId(channelId: ChannelID, messageId: MessageID, opts?: {isInsert?: boolean}): Promise<void>;
|
||||
|
||||
abstract delete(channelId: ChannelID, guildId?: GuildID, type?: number): Promise<void>;
|
||||
|
||||
abstract listGuildChannels(guildId: GuildID, mode: GuildChannelListMode): Promise<Array<Channel>>;
|
||||
|
||||
abstract listChannels(channelIds: Array<ChannelID>): Promise<Array<Channel>>;
|
||||
|
||||
abstract countGuildChannels(guildId: GuildID): Promise<number>;
|
||||
|
||||
abstract patchIndexedAt(channelId: ChannelID, indexedAt: Date): Promise<void>;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<CrosspostedMessageRow | null>;
|
||||
|
||||
abstract insertPending(row: CrosspostedMessageRow): Promise<boolean>;
|
||||
|
||||
abstract reclaimPending(
|
||||
key: CrosspostedMessageKey,
|
||||
data: {
|
||||
fromTargetMessageId: MessageID;
|
||||
toTargetMessageId: MessageID;
|
||||
reservedAt: Date;
|
||||
},
|
||||
): Promise<boolean>;
|
||||
|
||||
abstract markDelivered(key: CrosspostedMessageKey, data: CrosspostSyncState): Promise<boolean>;
|
||||
|
||||
abstract updateSynced(key: CrosspostedMessageKey, data: CrosspostSyncState): Promise<boolean>;
|
||||
|
||||
abstract listBySourceMessage(
|
||||
sourceMessageId: MessageID,
|
||||
options: {afterWebhookId?: WebhookID; limit: number},
|
||||
): Promise<Array<CrosspostedMessageRow>>;
|
||||
|
||||
abstract delete(
|
||||
key: CrosspostedMessageKey,
|
||||
expected?: Partial<Pick<CrosspostedMessageRow, 'state' | 'target_message_id'>>,
|
||||
): Promise<boolean>;
|
||||
|
||||
abstract addSource(source: CrosspostSource): Promise<void>;
|
||||
|
||||
abstract listSourcesByChannel(
|
||||
sourceChannelId: ChannelID,
|
||||
options: {afterMessageId?: MessageID; limit: number},
|
||||
): Promise<Array<MessageID>>;
|
||||
|
||||
abstract deleteSource(source: CrosspostSource): Promise<void>;
|
||||
}
|
||||
@@ -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<Array<Message>>;
|
||||
|
||||
abstract addChannelPin(channelId: ChannelID, messageId: MessageID, pinnedTimestamp: Date): Promise<void>;
|
||||
|
||||
abstract removeChannelPin(channelId: ChannelID, messageId: MessageID): Promise<void>;
|
||||
|
||||
abstract listMessageReactions(channelId: ChannelID, messageId: MessageID): Promise<Array<MessageReaction>>;
|
||||
|
||||
abstract listReactionUsers(
|
||||
channelId: ChannelID,
|
||||
messageId: MessageID,
|
||||
emojiName: string,
|
||||
limit?: number,
|
||||
after?: UserID,
|
||||
emojiId?: EmojiID,
|
||||
): Promise<Array<MessageReaction>>;
|
||||
|
||||
abstract addReaction(
|
||||
channelId: ChannelID,
|
||||
messageId: MessageID,
|
||||
userId: UserID,
|
||||
emojiName: string,
|
||||
emojiId?: EmojiID,
|
||||
emojiAnimated?: boolean,
|
||||
): Promise<MessageReaction>;
|
||||
|
||||
abstract removeReaction(
|
||||
channelId: ChannelID,
|
||||
messageId: MessageID,
|
||||
userId: UserID,
|
||||
emojiName: string,
|
||||
emojiId?: EmojiID,
|
||||
): Promise<void>;
|
||||
|
||||
abstract removeAllReactions(channelId: ChannelID, messageId: MessageID): Promise<void>;
|
||||
|
||||
abstract removeAllReactionsForEmoji(
|
||||
channelId: ChannelID,
|
||||
messageId: MessageID,
|
||||
emojiName: string,
|
||||
emojiId?: EmojiID,
|
||||
): Promise<void>;
|
||||
|
||||
abstract countReactionUsers(
|
||||
channelId: ChannelID,
|
||||
messageId: MessageID,
|
||||
emojiName: string,
|
||||
emojiId?: EmojiID,
|
||||
): Promise<number>;
|
||||
|
||||
abstract countUniqueReactions(channelId: ChannelID, messageId: MessageID): Promise<number>;
|
||||
|
||||
abstract checkUserReactionExists(
|
||||
channelId: ChannelID,
|
||||
messageId: MessageID,
|
||||
userId: UserID,
|
||||
emojiName: string,
|
||||
emojiId?: EmojiID,
|
||||
): Promise<boolean>;
|
||||
|
||||
abstract setHasReaction(channelId: ChannelID, messageId: MessageID, hasReaction: boolean): Promise<void>;
|
||||
}
|
||||
@@ -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<Array<Message>>;
|
||||
|
||||
abstract getMessage(channelId: ChannelID, messageId: MessageID): Promise<Message | null>;
|
||||
|
||||
abstract upsertMessage(data: MessageRow, oldData?: MessageRow | null, opts?: UpsertMessageOptions): Promise<Message>;
|
||||
|
||||
abstract updateEmbeds(message: Message): Promise<void>;
|
||||
|
||||
abstract deleteMessage(
|
||||
channelId: ChannelID,
|
||||
messageId: MessageID,
|
||||
authorId: UserID,
|
||||
pinnedTimestamp?: Date,
|
||||
): Promise<void>;
|
||||
|
||||
abstract bulkDeleteMessages(channelId: ChannelID, messageIds: Array<MessageID>): Promise<void>;
|
||||
|
||||
abstract deleteAllChannelMessages(channelId: ChannelID): Promise<void>;
|
||||
|
||||
abstract listMessagesByAuthor(
|
||||
authorId: UserID,
|
||||
limit?: number,
|
||||
lastMessageId?: MessageID,
|
||||
): Promise<
|
||||
Array<{
|
||||
channelId: ChannelID;
|
||||
messageId: MessageID;
|
||||
}>
|
||||
>;
|
||||
|
||||
abstract anonymizeMessage(channelId: ChannelID, messageId: MessageID, newAuthorId: UserID): Promise<void>;
|
||||
|
||||
abstract lookupAttachmentByChannelAndFilename(
|
||||
channelId: ChannelID,
|
||||
attachmentId: AttachmentID,
|
||||
filename: string,
|
||||
): Promise<MessageID | 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<bigint>;
|
||||
hasStarter: boolean;
|
||||
createdAt: Date;
|
||||
members: Array<CreateThreadMember>;
|
||||
}
|
||||
|
||||
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<ThreadState>;
|
||||
hasMore: boolean;
|
||||
}
|
||||
|
||||
export interface ThreadMemberSettingsPatch {
|
||||
flags?: number;
|
||||
muted?: boolean;
|
||||
muteConfig?: MuteConfig | null;
|
||||
}
|
||||
|
||||
export interface ThreadMemberAddResult {
|
||||
added: Array<ThreadMember>;
|
||||
state: ThreadState;
|
||||
}
|
||||
|
||||
export interface ThreadMemberRemoveResult {
|
||||
removed: Array<ThreadMember>;
|
||||
state: ThreadState | null;
|
||||
}
|
||||
|
||||
export type ThreadParentConfigPatch = Partial<Omit<ThreadParentConfigRow, 'guild_id' | 'channel_id'>>;
|
||||
|
||||
export abstract class IThreadRepository {
|
||||
abstract getState(threadId: ChannelID): Promise<ThreadState | null>;
|
||||
|
||||
abstract getStates(threadIds: Array<ChannelID>): Promise<Array<ThreadState>>;
|
||||
|
||||
abstract getStats(threadId: ChannelID): Promise<ThreadStats>;
|
||||
|
||||
abstract getStatsMany(threadIds: Array<ChannelID>): Promise<Map<ChannelID, ThreadStats>>;
|
||||
|
||||
abstract adjustMessageCount(threadId: ChannelID, delta: number): Promise<void>;
|
||||
|
||||
abstract create(params: CreateThreadParams): Promise<ThreadState>;
|
||||
|
||||
abstract updateState(
|
||||
threadId: ChannelID,
|
||||
mutate: (current: ThreadState) => ThreadStatePatch | null,
|
||||
): Promise<ThreadStateTransition | null>;
|
||||
|
||||
abstract claimForumPin(parentId: ChannelID, threadId: ChannelID): Promise<boolean>;
|
||||
|
||||
abstract releaseForumPin(parentId: ChannelID, threadId: ChannelID): Promise<void>;
|
||||
|
||||
abstract getForumPin(parentId: ChannelID): Promise<ChannelID | null>;
|
||||
|
||||
abstract countActiveThreads(guildId: GuildID): Promise<number>;
|
||||
|
||||
abstract listActiveThreads(guildId: GuildID): Promise<Array<ThreadState>>;
|
||||
|
||||
abstract listArchivedThreads(
|
||||
parentId: ChannelID,
|
||||
isPrivate: boolean,
|
||||
opts: {before?: Date; limit: number},
|
||||
): Promise<ArchivedThreadPage>;
|
||||
|
||||
abstract listJoinedPrivateArchivedThreads(
|
||||
userId: UserID,
|
||||
guildId: GuildID,
|
||||
parentId: ChannelID,
|
||||
opts: {before?: ChannelID; limit: number},
|
||||
): Promise<ArchivedThreadPage>;
|
||||
|
||||
abstract listJoinedThreadIds(userId: UserID, guildId: GuildID): Promise<Array<ChannelID>>;
|
||||
|
||||
abstract listJoinedPrivateThreadIds(
|
||||
userId: UserID,
|
||||
guildId: GuildID,
|
||||
parentId: ChannelID,
|
||||
limit: number,
|
||||
): Promise<Array<ChannelID>>;
|
||||
|
||||
abstract repairThreadIndexes(threadIds: Array<ChannelID>): Promise<void>;
|
||||
|
||||
abstract getMember(threadId: ChannelID, userId: UserID): Promise<ThreadMember | null>;
|
||||
|
||||
abstract getMembers(threadId: ChannelID, userIds: Array<UserID>): Promise<Array<ThreadMember>>;
|
||||
|
||||
abstract listMembers(threadId: ChannelID, opts: {after?: UserID; limit: number}): Promise<Array<ThreadMember>>;
|
||||
|
||||
abstract addMembers(
|
||||
threadId: ChannelID,
|
||||
members: Array<CreateThreadMember>,
|
||||
opts?: {joinTimestamp?: Date},
|
||||
): Promise<ThreadMemberAddResult | null>;
|
||||
|
||||
abstract removeMembers(threadId: ChannelID, userIds: Array<UserID>): Promise<ThreadMemberRemoveResult>;
|
||||
|
||||
abstract updateMemberSettings(expected: ThreadMember, patch: ThreadMemberSettingsPatch): Promise<ThreadMember | null>;
|
||||
|
||||
abstract listThreadIdsByParent(
|
||||
parentId: ChannelID,
|
||||
opts: {after?: ChannelID; limit: number},
|
||||
): Promise<Array<ChannelID>>;
|
||||
|
||||
abstract listParentThreads(parentId: ChannelID): Promise<Array<{threadId: ChannelID; type: number}>>;
|
||||
|
||||
abstract setThreadType(threadId: ChannelID, type: number): Promise<ThreadState | null>;
|
||||
|
||||
abstract listGuildThreadIds(
|
||||
guildId: GuildID,
|
||||
opts?: {activeSince?: Date; parents?: ReadonlyArray<{id: ChannelID; type: number}>},
|
||||
): Promise<Array<ChannelID>>;
|
||||
|
||||
abstract purgeThread(threadId: ChannelID): Promise<void>;
|
||||
|
||||
abstract revertParentLastMessageId(
|
||||
parentId: ChannelID,
|
||||
threadId: ChannelID,
|
||||
previous: MessageID | null,
|
||||
): Promise<void>;
|
||||
|
||||
abstract purgeGuild(guildId: GuildID): Promise<void>;
|
||||
|
||||
abstract getGuildMarker(guildId: GuildID): Promise<GuildThreadStateRow | null>;
|
||||
|
||||
abstract ensureGuildMarker(guildId: GuildID, opts?: {permsSeededAt?: Date}): Promise<void>;
|
||||
|
||||
abstract markGuildPermsSeeded(guildId: GuildID, at: Date): Promise<void>;
|
||||
|
||||
abstract markGuildSearchBackfilled(guildId: GuildID, at: Date): Promise<void>;
|
||||
abstract clearGuildSearchBackfilled(guildId: GuildID): Promise<void>;
|
||||
|
||||
abstract getParentConfig(guildId: GuildID, channelId: ChannelID): Promise<ThreadParentConfig | null>;
|
||||
|
||||
abstract listParentConfigs(guildId: GuildID): Promise<Array<ThreadParentConfig>>;
|
||||
|
||||
abstract patchParentConfig(guildId: GuildID, channelId: ChannelID, patch: ThreadParentConfigPatch): Promise<void>;
|
||||
|
||||
abstract deleteParentConfig(guildId: GuildID, channelId: ChannelID): Promise<void>;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<bigint>;
|
||||
hasStarter: boolean;
|
||||
createdAt: Date;
|
||||
members: Array<CreateThreadMember>;
|
||||
}
|
||||
|
||||
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<ThreadState>;
|
||||
hasMore: boolean;
|
||||
}
|
||||
|
||||
export interface ThreadMemberSettingsPatch {
|
||||
flags?: number;
|
||||
muted?: boolean;
|
||||
muteConfig?: MuteConfig | null;
|
||||
}
|
||||
|
||||
export interface ThreadMemberAddResult {
|
||||
added: Array<ThreadMember>;
|
||||
state: ThreadState;
|
||||
}
|
||||
|
||||
export interface ThreadMemberRemoveResult {
|
||||
removed: Array<ThreadMember>;
|
||||
state: ThreadState | null;
|
||||
}
|
||||
|
||||
export type ThreadParentConfigPatch = Partial<Omit<ThreadParentConfigRow, 'guild_id' | 'channel_id'>>;
|
||||
|
||||
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<ChannelID>) => void,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
) {}
|
||||
|
||||
async getState(threadId: ChannelID): Promise<ThreadState | null> {
|
||||
const row = await fetchOne<ThreadStateRow>(FETCH_STATE.bind({thread_id: threadId}));
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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<CallData> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<UserID>;
|
||||
}): Promise<void> {
|
||||
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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
Loaded 100 of 426 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user