refactor(api): collapse single-impl interfaces and facades (#3382)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-10 23:04:42 +02:00
1 parent e7b13fd19f
commit c16d414d93
426 files changed
+2085 -6445

No files matched your search

@@ -9,7 +9,7 @@ import {
THREAD_SCOPED_AUDIT_OPTION,
} from '@app/api/guild/GuildAuditLogEntryMapper';
import type {AuditLogChange, GuildAuditLogChange} from '@app/api/guild/GuildAuditLogTypes';
import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate';
import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository';
import type {IGatewayService} from '@app/api/infrastructure/IGatewayService';
import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService';
import {Logger} from '@app/api/Logger';
@@ -61,7 +61,7 @@ function normalizeAuditLogMetadata(metadata?: CreateGuildAuditLogParams['metadat
export class GuildAuditLogService {
constructor(
private readonly guildRepository: IGuildRepositoryAggregate,
private readonly guildRepository: GuildRepository,
private readonly snowflakeService: ISnowflakeService,
private readonly workerService: IWorkerService<WorkerTaskName>,
private readonly gatewayService: IGatewayService,
@@ -85,7 +85,7 @@ export function GuildBaseController(app: HonoApp) {
}
const auditLogReason = ctx.get('auditLogReason') ?? null;
const locale = ctx.get('requestLocale') ?? null;
return ctx.json(await ctx.get('guildService').data.createGuild({user, data, locale}, auditLogReason));
return ctx.json(await ctx.get('guildService').data.operations.createGuild({user, data, locale}, auditLogReason));
},
);
app.get(
@@ -106,7 +106,7 @@ export function GuildBaseController(app: HonoApp) {
async (ctx) => {
const userId = ctx.get('user').id;
const {before, after, limit, with_counts} = ctx.req.valid('query');
const guilds = await ctx.get('guildService').data.getUserGuilds(userId, {
const guilds = await ctx.get('guildService').data.operations.getUserGuilds(userId, {
before: before != null ? createGuildID(before) : undefined,
after: after != null ? createGuildID(after) : undefined,
limit,
@@ -148,7 +148,7 @@ export function GuildBaseController(app: HonoApp) {
const {delete_messages} = ctx.req.valid('query');
const body = ctx.req.valid('json');
if (delete_messages) {
await ctx.get('guildService').data.getGuild({userId, guildId});
await ctx.get('guildService').data.operations.getGuild({userId, guildId});
await requireSudoMode(ctx, user, body);
await ctx.get('channelService').userMessageDeletion.deleteUserMessagesInScope(userId, {guildId});
}
@@ -178,7 +178,7 @@ export function GuildBaseController(app: HonoApp) {
const userId = user.id;
const guildId = createGuildID(ctx.req.valid('param').guild_id);
const body = ctx.req.valid('json');
await ctx.get('guildService').data.getGuild({userId, guildId});
await ctx.get('guildService').data.operations.getGuild({userId, guildId});
await requireSudoMode(ctx, user, body);
await ctx.get('channelService').userMessageDeletion.deleteUserMessagesInScope(userId, {guildId});
return ctx.body(null, 202);
@@ -203,7 +203,7 @@ export function GuildBaseController(app: HonoApp) {
async (ctx) => {
const userId = ctx.get('user').id;
const guildId = createGuildID(ctx.req.valid('param').guild_id);
const guild = await ctx.get('guildService').data.getGuild({userId, guildId});
const guild = await ctx.get('guildService').data.operations.getGuild({userId, guildId});
return ctx.json(await maskGuildResponseThreadBits(guildId, viewerFromCtx(ctx), guild));
},
);
@@ -231,7 +231,7 @@ export function GuildBaseController(app: HonoApp) {
const data = ctx.req.valid('json');
let shouldRequireSudoMode = false;
if (data.mfa_level !== undefined) {
const currentGuild = await ctx.get('guildService').data.getGuild({userId, guildId});
const currentGuild = await ctx.get('guildService').data.operations.getGuild({userId, guildId});
shouldRequireSudoMode = currentGuild.mfa_level !== data.mfa_level;
}
if (shouldRequireSudoMode) {
@@ -270,7 +270,7 @@ export function GuildBaseController(app: HonoApp) {
const body = ctx.req.valid('json');
await requireSudoMode(ctx, user, body);
const auditLogReason = ctx.get('auditLogReason') ?? null;
await ctx.get('guildService').data.deleteGuild({user, guildId}, auditLogReason);
await ctx.get('guildService').data.operations.deleteGuild({user, guildId}, auditLogReason);
return ctx.body(null, 204);
},
);
@@ -291,7 +291,7 @@ export function GuildBaseController(app: HonoApp) {
async (ctx) => {
const userId = ctx.get('user').id;
const guildId = createGuildID(ctx.req.valid('param').guild_id);
return ctx.json(await ctx.get('guildService').data.getVanityURL({userId, guildId}));
return ctx.json(await ctx.get('guildService').data.vanity.getVanityURL({userId, guildId}));
},
);
app.patch(
@@ -317,7 +317,7 @@ export function GuildBaseController(app: HonoApp) {
const auditLogReason = ctx.get('auditLogReason') ?? null;
const {code: newCode} = await ctx
.get('guildService')
.data.updateVanityURL({userId, guildId, code: code ?? null, requestCache}, auditLogReason);
.data.vanity.updateVanityURL({userId, guildId, code: code ?? null, requestCache}, auditLogReason);
return ctx.json({code: newCode});
},
);
@@ -151,7 +151,7 @@ export function GuildDiscoveryController(app: HonoApp) {
if (!status || status.status !== DiscoveryApplicationStatus.APPROVED) {
throw new DiscoveryNotDiscoverableError();
}
const guild = await ctx.get('guildService').data.getGuildSystem(guildId);
const guild = await ctx.get('guildService').data.operations.getGuildSystem(guildId);
if (guild.features.has(GuildFeatures.INVITES_DISABLED)) {
throw new InvitesDisabledError();
}
@@ -45,7 +45,9 @@ export function GuildEmojiController(app: HonoApp) {
const guildId = createGuildID(ctx.req.valid('param').guild_id);
const {name, image} = ctx.req.valid('json');
const auditLogReason = ctx.get('auditLogReason') ?? null;
const emoji = await ctx.get('guildService').content.createEmoji({user, guildId, name, image}, auditLogReason);
const emoji = await ctx
.get('guildService')
.content.emojis.createEmoji({user, guildId, name, image}, auditLogReason);
return ctx.json(emoji);
},
);
@@ -70,7 +72,9 @@ export function GuildEmojiController(app: HonoApp) {
const guildId = createGuildID(ctx.req.valid('param').guild_id);
const {emojis} = ctx.req.valid('json');
const auditLogReason = ctx.get('auditLogReason') ?? null;
const result = await ctx.get('guildService').content.bulkCreateEmojis({user, guildId, emojis}, auditLogReason);
const result = await ctx
.get('guildService')
.content.emojis.bulkCreateEmojis({user, guildId, emojis}, auditLogReason);
return ctx.json(result);
},
);
@@ -96,7 +100,9 @@ export function GuildEmojiController(app: HonoApp) {
const {source_emoji_id} = ctx.req.valid('json');
const sourceEmojiId = createEmojiID(source_emoji_id);
const auditLogReason = ctx.get('auditLogReason') ?? null;
const emoji = await ctx.get('guildService').content.cloneEmoji({user, guildId, sourceEmojiId}, auditLogReason);
const emoji = await ctx
.get('guildService')
.content.emojis.cloneEmoji({user, guildId, sourceEmojiId}, auditLogReason);
return ctx.json(emoji);
},
);
@@ -120,7 +126,7 @@ export function GuildEmojiController(app: HonoApp) {
const userId = ctx.get('user').id;
const guildId = createGuildID(guild_id);
const requestCache = ctx.get('requestCache');
return ctx.json(await ctx.get('guildService').content.getEmojis({userId, guildId, requestCache}));
return ctx.json(await ctx.get('guildService').content.emojis.getEmojis({userId, guildId, requestCache}));
},
);
app.patch(
@@ -146,7 +152,9 @@ export function GuildEmojiController(app: HonoApp) {
const emojiId = createEmojiID(emoji_id);
const {name} = ctx.req.valid('json');
const auditLogReason = ctx.get('auditLogReason') ?? null;
const emoji = await ctx.get('guildService').content.updateEmoji({userId, guildId, emojiId, name}, auditLogReason);
const emoji = await ctx
.get('guildService')
.content.emojis.updateEmoji({userId, guildId, emojiId, name}, auditLogReason);
return ctx.json(emoji);
},
);
@@ -174,7 +182,7 @@ export function GuildEmojiController(app: HonoApp) {
const emojiId = createEmojiID(emoji_id);
const auditLogReason = ctx.get('auditLogReason') ?? null;
const {purge = false} = ctx.req.valid('query');
await ctx.get('guildService').content.deleteEmoji({userId, guildId, emojiId, purge}, auditLogReason);
await ctx.get('guildService').content.emojis.deleteEmoji({userId, guildId, emojiId, purge}, auditLogReason);
return ctx.body(null, 204);
},
);
@@ -217,7 +217,7 @@ export function GuildMemberController(app: HonoApp) {
const newOwnerId = createUserID(new_owner_id);
const auditLogReason = ctx.get('auditLogReason') ?? null;
return ctx.json(
await ctx.get('guildService').data.transferOwnership({userId, guildId, newOwnerId}, auditLogReason),
await ctx.get('guildService').data.ownership.transferOwnership({userId, guildId, newOwnerId}, auditLogReason),
);
},
);
@@ -51,7 +51,7 @@ export function GuildStickerController(app: HonoApp) {
const auditLogReason = ctx.get('auditLogReason') ?? null;
const sticker = await ctx
.get('guildService')
.content.createSticker({user, guildId, name, description, tags, image}, auditLogReason);
.content.stickers.createSticker({user, guildId, name, description, tags, image}, auditLogReason);
return ctx.json(sticker);
},
);
@@ -78,7 +78,7 @@ export function GuildStickerController(app: HonoApp) {
const auditLogReason = ctx.get('auditLogReason') ?? null;
const result = await ctx
.get('guildService')
.content.bulkCreateStickers({user, guildId, stickers}, auditLogReason);
.content.stickers.bulkCreateStickers({user, guildId, stickers}, auditLogReason);
return ctx.json(result);
},
);
@@ -106,7 +106,7 @@ export function GuildStickerController(app: HonoApp) {
const auditLogReason = ctx.get('auditLogReason') ?? null;
const sticker = await ctx
.get('guildService')
.content.cloneSticker({user, guildId, sourceStickerId}, auditLogReason);
.content.stickers.cloneSticker({user, guildId, sourceStickerId}, auditLogReason);
return ctx.json(sticker);
},
);
@@ -130,7 +130,7 @@ export function GuildStickerController(app: HonoApp) {
const userId = ctx.get('user').id;
const guildId = createGuildID(guild_id);
const requestCache = ctx.get('requestCache');
return ctx.json(await ctx.get('guildService').content.getStickers({userId, guildId, requestCache}));
return ctx.json(await ctx.get('guildService').content.stickers.getStickers({userId, guildId, requestCache}));
},
);
app.patch(
@@ -159,7 +159,7 @@ export function GuildStickerController(app: HonoApp) {
return ctx.json(
await ctx
.get('guildService')
.content.updateSticker({userId, guildId, stickerId, name, description, tags}, auditLogReason),
.content.stickers.updateSticker({userId, guildId, stickerId, name, description, tags}, auditLogReason),
);
},
);
@@ -187,7 +187,7 @@ export function GuildStickerController(app: HonoApp) {
const stickerId = createStickerID(sticker_id);
const auditLogReason = ctx.get('auditLogReason') ?? null;
const {purge = false} = ctx.req.valid('query');
await ctx.get('guildService').content.deleteSticker({userId, guildId, stickerId, purge}, auditLogReason);
await ctx.get('guildService').content.stickers.deleteSticker({userId, guildId, stickerId, purge}, auditLogReason);
return ctx.body(null, 204);
},
);
@@ -9,7 +9,6 @@ import {
type GuildEmojiRow,
type GuildStickerRow,
} from '@app/api/database/types/GuildTypes';
import {IGuildContentRepository} from '@app/api/guild/repositories/IGuildContentRepository';
import {GuildEmoji} from '@app/api/models/GuildEmoji';
import {GuildSticker} from '@app/api/models/GuildSticker';
import {GuildEmojis, GuildEmojisByEmojiId, GuildStickers, GuildStickersByStickerId} from '@app/api/Tables';
@@ -37,7 +36,7 @@ const FETCH_GUILD_STICKER_BY_STICKER_ID_ONLY_QUERY = GuildStickersByStickerId.se
limit: 1,
});
export class GuildContentRepository extends IGuildContentRepository {
export class GuildContentRepository {
async getEmoji(emojiId: EmojiID, guildId: GuildID): Promise<GuildEmoji | null> {
const emoji = await fetchOne<GuildEmojiRow>(FETCH_GUILD_EMOJI_BY_ID_QUERY, {
guild_id: guildId,
@@ -4,7 +4,6 @@ import type {GuildID, UserID} from '@app/api/BrandedTypes';
import {BatchBuilder, fetchMany, fetchOne} from '@app/api/database/CassandraQueryExecution';
import {buildPatchFromData, executeVersionedUpdate} from '@app/api/database/CassandraVersionedUpdate';
import {GUILD_COLUMNS, type GuildMemberByUserIdRow, type GuildRow} from '@app/api/database/types/GuildTypes';
import {IGuildDataRepository} from '@app/api/guild/repositories/IGuildDataRepository';
import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware';
import {Guild} from '@app/api/models/Guild';
import {GuildMembersByUserId, Guilds} from '@app/api/Tables';
@@ -26,10 +25,8 @@ function createFetchAllGuildsFirstPageQuery(limit: number) {
return Guilds.select({limit});
}
export class GuildDataRepository extends IGuildDataRepository {
constructor(private readonly requestCache?: RequestCache) {
super();
}
export class GuildDataRepository {
constructor(private readonly requestCache?: RequestCache) {}
async findUnique(guildId: GuildID): Promise<Guild | null> {
const prefetched = this.requestCache?.takeGuild(guildId);
@@ -14,26 +14,7 @@ const FETCH_DISCOVERY_BY_STATUS = GuildDiscoveryByStatus.selectCql({
where: GuildDiscoveryByStatus.where.eq('status'),
});
export abstract class IGuildDiscoveryRepository {
abstract findByGuildId(guildId: GuildID): Promise<GuildDiscoveryRow | null>;
abstract listByStatus(status: string): Promise<Array<GuildDiscoveryByStatusRow>>;
abstract listFullByStatus(status: string): Promise<Array<GuildDiscoveryRow>>;
abstract upsert(row: GuildDiscoveryRow): Promise<void>;
abstract deleteByGuildId(guildId: GuildID, status: string, appliedAt: Date): Promise<void>;
abstract updateStatus(
guildId: GuildID,
oldStatus: string,
oldAppliedAt: Date,
updatedRow: GuildDiscoveryRow,
): Promise<void>;
}
export class GuildDiscoveryRepository extends IGuildDiscoveryRepository {
export class GuildDiscoveryRepository {
async findByGuildId(guildId: GuildID): Promise<GuildDiscoveryRow | null> {
const row = await fetchOne<GuildDiscoveryRow>(FETCH_DISCOVERY_BY_GUILD_ID, {
guild_id: guildId,
@@ -5,7 +5,6 @@ import {BatchBuilder, fetchMany, fetchOne, upsertOne} from '@app/api/database/Ca
import {buildPatchFromData, executeVersionedUpdate} from '@app/api/database/CassandraVersionedUpdate';
import type {GuildMemberRow, GuildMembershipMetadataRow} from '@app/api/database/types/GuildTypes';
import {GUILD_MEMBER_COLUMNS} from '@app/api/database/types/GuildTypes';
import {IGuildMemberRepository} from '@app/api/guild/repositories/IGuildMemberRepository';
import {GuildMember} from '@app/api/models/GuildMember';
import {GuildMembers, GuildMembersByUserId, GuildMembershipMetadata} from '@app/api/Tables';
@@ -38,7 +37,7 @@ function createPaginatedQuery(limit: number) {
});
}
export class GuildMemberRepository extends IGuildMemberRepository {
export class GuildMemberRepository {
async getMember(guildId: GuildID, userId: UserID): Promise<GuildMember | null> {
const member = await fetchOne<GuildMemberRow>(FETCH_GUILD_MEMBER_BY_GUILD_AND_USER_ID_QUERY, {
guild_id: guildId,
@@ -9,7 +9,6 @@ import type {
GuildBanByUserIdRow,
GuildBanRow,
} from '@app/api/database/types/GuildTypes';
import {IGuildModerationRepository} from '@app/api/guild/repositories/IGuildModerationRepository';
import {GuildAuditLog} from '@app/api/models/GuildAuditLog';
import {GuildBan} from '@app/api/models/GuildBan';
import {
@@ -47,7 +46,7 @@ const FETCH_GUILD_AUDIT_LOGS_BY_IDS_QUERY = GuildAuditLogs.selectCql({
where: [GuildAuditLogs.where.eq('guild_id'), GuildAuditLogs.where.in('log_id', 'log_ids')],
});
export class GuildModerationRepository extends IGuildModerationRepository {
export class GuildModerationRepository {
async getBan(guildId: GuildID, userId: UserID): Promise<GuildBan | null> {
const ban = await fetchOne<GuildBanRow>(FETCH_GUILD_BAN_BY_GUILD_AND_USER_ID_QUERY, {
guild_id: guildId,
@@ -18,7 +18,6 @@ import {GuildDataRepository} from '@app/api/guild/repositories/GuildDataReposito
import {GuildMemberRepository} from '@app/api/guild/repositories/GuildMemberRepository';
import {GuildModerationRepository} from '@app/api/guild/repositories/GuildModerationRepository';
import {GuildRoleRepository} from '@app/api/guild/repositories/GuildRoleRepository';
import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate';
import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware';
import type {Guild} from '@app/api/models/Guild';
import type {GuildAuditLog} from '@app/api/models/GuildAuditLog';
@@ -29,7 +28,7 @@ import type {GuildRole} from '@app/api/models/GuildRole';
import type {GuildSticker} from '@app/api/models/GuildSticker';
import type {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType';
export class GuildRepository implements IGuildRepositoryAggregate {
export class GuildRepository {
private dataRepo: GuildDataRepository;
private memberRepo: GuildMemberRepository;
private roleRepo: GuildRoleRepository;
@@ -5,7 +5,6 @@ import {deleteOneOrMany, fetchMany, fetchOne} from '@app/api/database/CassandraQ
import {buildPatchFromData, executeVersionedUpdate} from '@app/api/database/CassandraVersionedUpdate';
import type {GuildRoleRow} from '@app/api/database/types/GuildTypes';
import {GUILD_ROLE_COLUMNS} from '@app/api/database/types/GuildTypes';
import {IGuildRoleRepository} from '@app/api/guild/repositories/IGuildRoleRepository';
import {GuildRole} from '@app/api/models/GuildRole';
import {GuildRoles} from '@app/api/Tables';
@@ -20,7 +19,7 @@ const FETCH_ROLES_BY_IDS_QUERY = GuildRoles.selectCql({
where: [GuildRoles.where.eq('guild_id'), GuildRoles.where.in('role_id', 'role_ids')],
});
export class GuildRoleRepository extends IGuildRoleRepository {
export class GuildRoleRepository {
async getRole(roleId: RoleID, guildId: GuildID): Promise<GuildRole | null> {
const role = await fetchOne<GuildRoleRow>(FETCH_GUILD_ROLE_BY_ID_QUERY, {
guild_id: guildId,
@@ -1,28 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {EmojiID, GuildID, StickerID} from '@app/api/BrandedTypes';
import type {GuildEmojiRow, GuildStickerRow} from '@app/api/database/types/GuildTypes';
import type {GuildEmoji} from '@app/api/models/GuildEmoji';
import type {GuildSticker} from '@app/api/models/GuildSticker';
export abstract class IGuildContentRepository {
abstract getEmoji(emojiId: EmojiID, guildId: GuildID): Promise<GuildEmoji | null>;
abstract getEmojiById(emojiId: EmojiID): Promise<GuildEmoji | null>;
abstract listEmojis(guildId: GuildID): Promise<Array<GuildEmoji>>;
abstract upsertEmoji(data: GuildEmojiRow): Promise<GuildEmoji>;
abstract deleteEmoji(guildId: GuildID, emojiId: EmojiID): Promise<void>;
abstract getSticker(stickerId: StickerID, guildId: GuildID): Promise<GuildSticker | null>;
abstract getStickerById(stickerId: StickerID): Promise<GuildSticker | null>;
abstract listStickers(guildId: GuildID): Promise<Array<GuildSticker>>;
abstract upsertSticker(data: GuildStickerRow): Promise<GuildSticker>;
abstract deleteSticker(guildId: GuildID, stickerId: StickerID): Promise<void>;
}
@@ -1,30 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {GuildID, UserID} from '@app/api/BrandedTypes';
import type {GuildRow} from '@app/api/database/types/GuildTypes';
import type {Guild} from '@app/api/models/Guild';
export abstract class IGuildDataRepository {
abstract findUnique(guildId: GuildID): Promise<Guild | null>;
abstract listGuilds(guildIds: Array<GuildID>): Promise<Array<Guild>>;
abstract listAllGuildsPaginated(limit: number, lastGuildId?: GuildID): Promise<Array<Guild>>;
abstract listUserGuilds(userId: UserID): Promise<Array<Guild>>;
abstract countUserGuilds(userId: UserID): Promise<number>;
abstract listOwnedGuildIds(userId: UserID): Promise<Array<GuildID>>;
abstract upsert(data: GuildRow, oldData?: GuildRow | null, previousOwnerId?: UserID): Promise<Guild>;
abstract upsertPartial(
guildId: GuildID,
patch: Partial<GuildRow>,
oldData?: GuildRow | null,
previousOwnerId?: UserID,
): Promise<Guild>;
abstract delete(guildId: GuildID, ownerId?: UserID): Promise<void>;
}
@@ -1,23 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {GuildID, UserID} from '@app/api/BrandedTypes';
import type {GuildMemberRow, GuildMembershipMetadataRow} from '@app/api/database/types/GuildTypes';
import type {GuildMember} from '@app/api/models/GuildMember';
export abstract class IGuildMemberRepository {
abstract getMember(guildId: GuildID, userId: UserID): Promise<GuildMember | null>;
abstract listMembers(guildId: GuildID): Promise<Array<GuildMember>>;
abstract countMembers(guildId: GuildID): Promise<number>;
abstract upsertMember(data: GuildMemberRow): Promise<GuildMember>;
abstract deleteMember(guildId: GuildID, userId: UserID): Promise<void>;
abstract listMembersPaginated(guildId: GuildID, limit: number, afterUserId?: UserID): Promise<Array<GuildMember>>;
abstract getMembershipMetadata(guildId: GuildID, userId: UserID): Promise<GuildMembershipMetadataRow | null>;
abstract upsertMembershipMetadata(data: GuildMembershipMetadataRow, ttlSeconds: number): Promise<void>;
}
@@ -1,44 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {GuildID, UserID} from '@app/api/BrandedTypes';
import type {GuildAuditLogRow, GuildBanRow} from '@app/api/database/types/GuildTypes';
import type {GuildAuditLog} from '@app/api/models/GuildAuditLog';
import type {GuildBan} from '@app/api/models/GuildBan';
import type {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType';
export abstract class IGuildModerationRepository {
abstract getBan(guildId: GuildID, userId: UserID): Promise<GuildBan | null>;
abstract listBans(guildId: GuildID): Promise<Array<GuildBan>>;
abstract upsertBan(data: GuildBanRow): Promise<GuildBan>;
abstract deleteBan(guildId: GuildID, userId: UserID): Promise<void>;
abstract deleteAllBansForUser(userId: UserID): Promise<void>;
abstract getBanByEmail(guildId: GuildID, email: string): Promise<GuildBan | null>;
abstract createAuditLog(data: GuildAuditLogRow): Promise<GuildAuditLog>;
abstract batchDeleteAndCreateAuditLogs(
guildId: GuildID,
logsToDelete: Array<GuildAuditLog>,
logToCreate: GuildAuditLogRow,
): Promise<GuildAuditLog>;
abstract getAuditLog(guildId: GuildID, logId: bigint): Promise<GuildAuditLog | null>;
abstract listAuditLogs(params: {
guildId: GuildID;
limit: number;
afterLogId?: bigint;
beforeLogId?: bigint;
userId?: UserID;
actionType?: AuditLogActionType;
}): Promise<Array<GuildAuditLog>>;
abstract listAuditLogsByIds(guildId: GuildID, logIds: Array<bigint>): Promise<Array<GuildAuditLog>>;
abstract deleteAuditLogs(guildId: GuildID, logs: Array<GuildAuditLog>): Promise<void>;
}
@@ -1,14 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {IGuildContentRepository} from '@app/api/guild/repositories/IGuildContentRepository';
import type {IGuildDataRepository} from '@app/api/guild/repositories/IGuildDataRepository';
import type {IGuildMemberRepository} from '@app/api/guild/repositories/IGuildMemberRepository';
import type {IGuildModerationRepository} from '@app/api/guild/repositories/IGuildModerationRepository';
import type {IGuildRoleRepository} from '@app/api/guild/repositories/IGuildRoleRepository';
export interface IGuildRepositoryAggregate
extends IGuildDataRepository,
IGuildMemberRepository,
IGuildRoleRepository,
IGuildModerationRepository,
IGuildContentRepository {}
@@ -1,19 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {GuildID, RoleID} from '@app/api/BrandedTypes';
import type {GuildRoleRow} from '@app/api/database/types/GuildTypes';
import type {GuildRole} from '@app/api/models/GuildRole';
export abstract class IGuildRoleRepository {
abstract getRole(roleId: RoleID, guildId: GuildID): Promise<GuildRole | null>;
abstract listRoles(guildId: GuildID): Promise<Array<GuildRole>>;
abstract listRolesByIds(roleIds: Array<RoleID>, guildId: GuildID): Promise<Array<GuildRole>>;
abstract countRoles(guildId: GuildID): Promise<number>;
abstract upsertRole(data: GuildRoleRow, oldData?: GuildRoleRow | null): Promise<GuildRole>;
abstract deleteRole(guildId: GuildID, roleId: RoleID): Promise<void>;
}
@@ -2,11 +2,11 @@
import type {ChannelID, GuildID, UserID} from '@app/api/BrandedTypes';
import {mapChannelToResponse} from '@app/api/channel/ChannelMappers';
import type {IChannelRepository} from '@app/api/channel/IChannelRepository';
import type {ChannelRepository} from '@app/api/channel/ChannelRepository';
import {withThreadParentFieldsMany} from '@app/api/channel/services/thread/ThreadParentSettings';
import {SYSTEM_THREAD_VIEWER, type ThreadViewer, viewerActive} from '@app/api/experiment/ChannelThreadsGate';
import type {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService';
import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate';
import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository';
import {ChannelOperationsService} from '@app/api/guild/services/channel/ChannelOperationsService';
import {createGuildMfaEnforcer} from '@app/api/guild/services/GuildMfaEnforcement';
import {maskChannelResponseThreadBits} from '@app/api/guild/services/ThreadPermissionBits';
@@ -15,7 +15,7 @@ import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService'
import type {UserCacheService} from '@app/api/infrastructure/UserCacheService';
import type {LimitConfigService} from '@app/api/limits/LimitConfigService';
import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware';
import type {IUserRepository} from '@app/api/user/IUserRepository';
import type {UserRepository} from '@app/api/user/repositories/UserRepository';
import {Permissions} from '@fluxer/constants/src/ChannelConstants';
import {MissingPermissionsError} from '@fluxer/errors/src/domains/core/MissingPermissionsError';
import {UnknownGuildError} from '@fluxer/errors/src/domains/guild/UnknownGuildError';
@@ -27,15 +27,15 @@ export class GuildChannelService {
private readonly channelOps: ChannelOperationsService;
constructor(
private readonly channelRepository: IChannelRepository,
guildRepository: IGuildRepositoryAggregate,
private readonly channelRepository: ChannelRepository,
guildRepository: GuildRepository,
private readonly userCacheService: UserCacheService,
private readonly gatewayService: IGatewayService,
cacheService: ICacheService,
snowflakeService: ISnowflakeService,
guildAuditLogService: GuildAuditLogService,
limitConfigService: LimitConfigService,
private readonly userRepository: IUserRepository,
private readonly userRepository: UserRepository,
) {
this.channelOps = new ChannelOperationsService(
channelRepository,
@@ -67,7 +67,7 @@ export class GuildChannelService {
guildId: params.guildId,
userId: params.userId,
});
const channels = await this.channelRepository.listGuildChannels(params.guildId, 'enrolled');
const channels = await this.channelRepository.channelData.listGuildChannels(params.guildId, 'enrolled');
const viewer = params.viewer;
const viewableChannels = channels.filter(
(channel) =>
@@ -1,8 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {EmojiID, GuildID, StickerID, UserID} from '@app/api/BrandedTypes';
import type {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService';
import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate';
import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository';
import {ContentHelpers} from '@app/api/guild/services/content/ContentHelpers';
import {EmojiService} from '@app/api/guild/services/content/EmojiService';
import {ExpressionAssetPurger} from '@app/api/guild/services/content/ExpressionAssetPurger';
@@ -13,22 +12,14 @@ import type {IGatewayService} from '@app/api/infrastructure/IGatewayService';
import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService';
import type {UserCacheService} from '@app/api/infrastructure/UserCacheService';
import type {LimitConfigService} from '@app/api/limits/LimitConfigService';
import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware';
import type {User} from '@app/api/models/User';
import type {
GuildEmojiResponse,
GuildEmojiWithUserResponse,
GuildStickerResponse,
GuildStickerWithUserResponse,
} from '@fluxer/schema/src/domains/guild/GuildEmojiSchemas';
export class GuildContentService {
private readonly contentHelpers: ContentHelpers;
private readonly emojiService: EmojiService;
private readonly stickerService: StickerService;
readonly emojis: EmojiService;
readonly stickers: StickerService;
constructor(
guildRepository: IGuildRepositoryAggregate,
guildRepository: GuildRepository,
userCacheService: UserCacheService,
gatewayService: IGatewayService,
avatarService: AvatarService,
@@ -39,7 +30,7 @@ export class GuildContentService {
) {
this.contentHelpers = new ContentHelpers(gatewayService, guildAuditLogService);
const expressionAssetPurger = new ExpressionAssetPurger(assetDeletionQueue);
this.emojiService = new EmojiService(
this.emojis = new EmojiService(
guildRepository,
userCacheService,
gatewayService,
@@ -49,7 +40,7 @@ export class GuildContentService {
expressionAssetPurger,
limitConfigService,
);
this.stickerService = new StickerService(
this.stickers = new StickerService(
guildRepository,
userCacheService,
gatewayService,
@@ -60,160 +51,4 @@ export class GuildContentService {
limitConfigService,
);
}
async getEmojis(params: {
userId: UserID;
guildId: GuildID;
requestCache: RequestCache;
}): Promise<Array<GuildEmojiWithUserResponse>> {
return this.emojiService.getEmojis(params);
}
async createEmoji(
params: {
user: User;
guildId: GuildID;
name: string;
image: string;
},
auditLogReason?: string | null,
): Promise<GuildEmojiResponse> {
return this.emojiService.createEmoji(params, auditLogReason);
}
async cloneEmoji(
params: {
user: User;
guildId: GuildID;
sourceEmojiId: EmojiID;
},
auditLogReason?: string | null,
): Promise<GuildEmojiResponse> {
return this.emojiService.cloneEmoji(params, auditLogReason);
}
async bulkCreateEmojis(
params: {
user: User;
guildId: GuildID;
emojis: Array<{
name: string;
image: string;
}>;
},
auditLogReason?: string | null,
): Promise<{
success: Array<GuildEmojiResponse>;
failed: Array<{
name: string;
error: string;
}>;
}> {
return this.emojiService.bulkCreateEmojis(params, auditLogReason);
}
async updateEmoji(
params: {
userId: UserID;
guildId: GuildID;
emojiId: EmojiID;
name: string;
},
auditLogReason?: string | null,
): Promise<GuildEmojiResponse> {
return this.emojiService.updateEmoji(params, auditLogReason);
}
async deleteEmoji(
params: {
userId: UserID;
guildId: GuildID;
emojiId: EmojiID;
purge?: boolean;
},
auditLogReason?: string | null,
): Promise<void> {
return this.emojiService.deleteEmoji(params, auditLogReason);
}
async getStickers(params: {
userId: UserID;
guildId: GuildID;
requestCache: RequestCache;
}): Promise<Array<GuildStickerWithUserResponse>> {
return this.stickerService.getStickers(params);
}
async createSticker(
params: {
user: User;
guildId: GuildID;
name: string;
description?: string | null;
tags: Array<string>;
image: string;
},
auditLogReason?: string | null,
): Promise<GuildStickerResponse> {
return this.stickerService.createSticker(params, auditLogReason);
}
async cloneSticker(
params: {
user: User;
guildId: GuildID;
sourceStickerId: StickerID;
},
auditLogReason?: string | null,
): Promise<GuildStickerResponse> {
return this.stickerService.cloneSticker(params, auditLogReason);
}
async bulkCreateStickers(
params: {
user: User;
guildId: GuildID;
stickers: Array<{
name: string;
description?: string | null;
tags: Array<string>;
image: string;
}>;
},
auditLogReason?: string | null,
): Promise<{
success: Array<GuildStickerResponse>;
failed: Array<{
name: string;
error: string;
}>;
}> {
return this.stickerService.bulkCreateStickers(params, auditLogReason);
}
async updateSticker(
params: {
userId: UserID;
guildId: GuildID;
stickerId: StickerID;
name: string;
description?: string | null;
tags: Array<string>;
},
auditLogReason?: string | null,
): Promise<GuildStickerResponse> {
return this.stickerService.updateSticker(params, auditLogReason);
}
async deleteSticker(
params: {
userId: UserID;
guildId: GuildID;
stickerId: StickerID;
purge?: boolean;
},
auditLogReason?: string | null,
): Promise<void> {
return this.stickerService.deleteSticker(params, auditLogReason);
}
}
@@ -1,11 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {GuildID, UserID} from '@app/api/BrandedTypes';
import type {IChannelRepository} from '@app/api/channel/IChannelRepository';
import type {ChannelRepository} from '@app/api/channel/ChannelRepository';
import type {ChannelService} from '@app/api/channel/services/ChannelService';
import type {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService';
import {GuildDiscoveryRepository} from '@app/api/guild/repositories/GuildDiscoveryRepository';
import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate';
import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository';
import {GuildDataHelpers} from '@app/api/guild/services/data/GuildDataHelpers';
import {GuildOperationsService} from '@app/api/guild/services/data/GuildOperationsService';
import {GuildOwnershipService} from '@app/api/guild/services/data/GuildOwnershipService';
@@ -15,43 +14,30 @@ import type {IGatewayService} from '@app/api/infrastructure/IGatewayService';
import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService';
import type {InviteRepository} from '@app/api/invite/InviteRepository';
import type {LimitConfigService} from '@app/api/limits/LimitConfigService';
import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware';
import type {Guild} from '@app/api/models/Guild';
import type {User} from '@app/api/models/User';
import type {IUserRepository} from '@app/api/user/IUserRepository';
import type {IWebhookRepository} from '@app/api/webhook/IWebhookRepository';
import type {
GuildCreateRequest,
GuildUpdateRequest,
GuildVanityURLUpdateResponse,
} from '@fluxer/schema/src/domains/guild/GuildRequestSchemas';
import type {
GuildPartialResponse,
GuildResponse,
GuildVanityURLResponse,
} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas';
import type {UserRepository} from '@app/api/user/repositories/UserRepository';
import type {WebhookRepository} from '@app/api/webhook/WebhookRepository';
export class GuildDataService {
private readonly helpers: GuildDataHelpers;
private readonly operationsService: GuildOperationsService;
private readonly vanityService: GuildVanityService;
private readonly ownershipService: GuildOwnershipService;
readonly operations: GuildOperationsService;
readonly vanity: GuildVanityService;
readonly ownership: GuildOwnershipService;
constructor(
private readonly guildRepository: IGuildRepositoryAggregate,
private readonly channelRepository: IChannelRepository,
private readonly guildRepository: GuildRepository,
private readonly channelRepository: ChannelRepository,
private readonly inviteRepository: InviteRepository,
private readonly channelService: ChannelService,
private readonly gatewayService: IGatewayService,
private readonly entityAssetService: EntityAssetService,
private readonly userRepository: IUserRepository,
private readonly userRepository: UserRepository,
private readonly snowflakeService: ISnowflakeService,
private readonly webhookRepository: IWebhookRepository,
private readonly webhookRepository: WebhookRepository,
private readonly guildAuditLogService: GuildAuditLogService,
private readonly limitConfigService: LimitConfigService,
) {
this.helpers = new GuildDataHelpers(this.gatewayService, this.guildAuditLogService, this.userRepository);
this.operationsService = new GuildOperationsService(
this.operations = new GuildOperationsService(
this.guildRepository,
this.channelRepository,
this.inviteRepository,
@@ -65,99 +51,7 @@ export class GuildDataService {
this.limitConfigService,
new GuildDiscoveryRepository(),
);
this.vanityService = new GuildVanityService(this.guildRepository, this.inviteRepository, this.helpers);
this.ownershipService = new GuildOwnershipService(this.guildRepository, this.userRepository, this.helpers);
}
async getGuild({userId, guildId}: {userId: UserID; guildId: GuildID}): Promise<GuildResponse> {
return this.operationsService.getGuild({userId, guildId});
}
async getUserGuilds(
userId: UserID,
options?: {
before?: GuildID;
after?: GuildID;
limit?: number;
withCounts?: boolean;
},
): Promise<Array<GuildResponse>> {
return this.operationsService.getUserGuilds(userId, options);
}
async getPublicGuildData(guildId: GuildID): Promise<GuildPartialResponse> {
return this.operationsService.getPublicGuildData(guildId);
}
async getGuildSystem(guildId: GuildID): Promise<Guild> {
return this.operationsService.getGuildSystem(guildId);
}
async createGuild(
params: {
user: User;
data: GuildCreateRequest;
locale?: string | null;
},
auditLogReason?: string | null,
): Promise<GuildResponse> {
return this.operationsService.createGuild(params, auditLogReason);
}
async updateGuild(
params: {
userId: UserID;
guildId: GuildID;
data: GuildUpdateRequest;
requestCache: RequestCache;
},
auditLogReason?: string | null,
): Promise<{
guild: GuildResponse;
previousFeatures: Set<string>;
updatedFeatures: Set<string>;
}> {
return this.operationsService.updateGuild(params, auditLogReason);
}
async getVanityURL(params: {userId: UserID; guildId: GuildID}): Promise<GuildVanityURLResponse> {
return this.vanityService.getVanityURL(params);
}
async updateVanityURL(
params: {
userId: UserID;
guildId: GuildID;
code: string | null;
requestCache: RequestCache;
},
auditLogReason?: string | null,
): Promise<GuildVanityURLUpdateResponse> {
return this.vanityService.updateVanityURL(params, auditLogReason);
}
async deleteGuild(
params: {
user: User;
guildId: GuildID;
},
auditLogReason?: string | null,
): Promise<void> {
return this.operationsService.deleteGuild(params, auditLogReason);
}
async deleteGuildForAdmin(guildId: GuildID, _auditLogReason?: string | null): Promise<void> {
return this.operationsService.deleteGuildById(guildId);
}
async transferOwnership(
params: {
userId: UserID;
guildId: GuildID;
newOwnerId: UserID;
},
auditLogReason?: string | null,
): Promise<GuildResponse> {
return this.ownershipService.transferOwnership(params, auditLogReason);
this.vanity = new GuildVanityService(this.guildRepository, this.inviteRepository, this.helpers);
this.ownership = new GuildOwnershipService(this.guildRepository, this.userRepository, this.helpers);
}
}
@@ -2,12 +2,12 @@
import {type ChannelID, type GuildID, guildIdToRoleId, type UserID} from '@app/api/BrandedTypes';
import {Config} from '@app/api/Config';
import type {IChannelDataRepository} from '@app/api/channel/repositories/IChannelDataRepository';
import type {ChannelDataRepository} from '@app/api/channel/repositories/ChannelDataRepository';
import type {GuildDiscoveryRow} from '@app/api/database/types/GuildDiscoveryTypes';
import {type ThreadViewer, viewerActive} from '@app/api/experiment/ChannelThreadsGate';
import {mapGuildToGuildResponse} from '@app/api/guild/GuildModel';
import type {IGuildDiscoveryRepository} from '@app/api/guild/repositories/GuildDiscoveryRepository';
import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate';
import type {GuildDiscoveryRepository} from '@app/api/guild/repositories/GuildDiscoveryRepository';
import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository';
import {contentModerationService} from '@app/api/infrastructure/ContentModerationService';
import type {IGatewayService} from '@app/api/infrastructure/IGatewayService';
import {Logger} from '@app/api/Logger';
@@ -54,60 +54,6 @@ function resolveLanguage(language: string | null | undefined, fallback: string |
return DISCOVERY_DEFAULT_LANGUAGE;
}
export abstract class IGuildDiscoveryService {
abstract apply(params: {
guildId: GuildID;
userId: UserID;
description: string;
categoryId: number;
primaryLanguage?: string;
customTags?: ReadonlyArray<string>;
}): Promise<GuildDiscoveryRow>;
abstract editApplication(params: {
guildId: GuildID;
userId: UserID;
data: DiscoveryApplicationPatchRequest;
}): Promise<GuildDiscoveryRow>;
abstract withdraw(params: {guildId: GuildID; userId: UserID}): Promise<void>;
abstract getStatus(guildId: GuildID): Promise<GuildDiscoveryRow | null>;
abstract approve(params: {guildId: GuildID; adminUserId: UserID; reason?: string}): Promise<GuildDiscoveryRow>;
abstract reject(params: {guildId: GuildID; adminUserId: UserID; reason: string}): Promise<GuildDiscoveryRow>;
abstract remove(params: {guildId: GuildID; adminUserId: UserID; reason: string}): Promise<GuildDiscoveryRow>;
abstract getEligibility(guildId: GuildID): Promise<{
eligible: boolean;
min_member_count: number;
}>;
abstract listByStatus(params: {status: string}): Promise<Array<GuildDiscoveryRow>>;
abstract getChannelPreview(
guildId: GuildID,
channelId: ChannelID,
viewer: ThreadViewer,
): Promise<DiscoveryChannelPreview>;
abstract searchDiscoverable(params: {
query?: string;
categoryId?: number;
primaryLanguage?: string;
tag?: string;
sortBy?: string;
limit: number;
offset: number;
}): Promise<{
guilds: Array<DiscoveryGuildResult>;
total: number;
category_counts: Array<DiscoveryCategoryCount>;
}>;
}
interface DiscoveryCategoryCount {
category_type: number;
count: number;
@@ -153,16 +99,14 @@ function toDiscoveryCategoryCounts(
return entries.sort((left, right) => left.category_type - right.category_type);
}
export class GuildDiscoveryService extends IGuildDiscoveryService {
export class GuildDiscoveryService {
constructor(
private readonly discoveryRepository: IGuildDiscoveryRepository,
private readonly guildRepository: IGuildRepositoryAggregate,
private readonly discoveryRepository: GuildDiscoveryRepository,
private readonly guildRepository: GuildRepository,
private readonly gatewayService: IGatewayService,
private readonly guildSearchService: IGuildSearchService | null,
private readonly channelDataRepository: IChannelDataRepository,
) {
super();
}
private readonly channelDataRepository: ChannelDataRepository,
) {}
async apply(params: {
guildId: GuildID;
@@ -3,7 +3,7 @@
import type {GuildID, InviteCode, RoleID, UserID} from '@app/api/BrandedTypes';
import type {ChannelService} from '@app/api/channel/services/ChannelService';
import type {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService';
import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate';
import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository';
import {GuildMemberAuditService} from '@app/api/guild/services/member/GuildMemberAuditService';
import {GuildMemberAuthService} from '@app/api/guild/services/member/GuildMemberAuthService';
import {GuildMemberEventService} from '@app/api/guild/services/member/GuildMemberEventService';
@@ -18,7 +18,7 @@ import type {LimitConfigService} from '@app/api/limits/LimitConfigService';
import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware';
import type {Guild} from '@app/api/models/Guild';
import type {GuildMember} from '@app/api/models/GuildMember';
import type {IUserRepository} from '@app/api/user/IUserRepository';
import type {UserRepository} from '@app/api/user/repositories/UserRepository';
import {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType';
import type {JoinSourceType} from '@fluxer/constants/src/GuildConstants';
import {UnknownGuildMemberError} from '@fluxer/errors/src/domains/guild/UnknownGuildMemberError';
@@ -34,15 +34,15 @@ export class GuildMemberService {
private readonly operationsService: GuildMemberOperationsService;
private readonly roleService: GuildMemberRoleService;
private readonly searchIndexService: GuildMemberSearchIndexService;
private readonly userRepository: IUserRepository;
private readonly userRepository: UserRepository;
constructor(
private readonly guildRepository: IGuildRepositoryAggregate,
private readonly guildRepository: GuildRepository,
channelService: ChannelService,
userCacheService: UserCacheService,
gatewayService: IGatewayService,
entityAssetService: EntityAssetService,
userRepository: IUserRepository,
userRepository: UserRepository,
rateLimitService: IRateLimitService,
private readonly guildAuditLogService: GuildAuditLogService,
limitConfigService: LimitConfigService,
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {UserID} from '@app/api/BrandedTypes';
import type {IUserRepository} from '@app/api/user/IUserRepository';
import type {UserRepository} from '@app/api/user/repositories/UserRepository';
import {Permissions} from '@fluxer/constants/src/ChannelConstants';
import {GuildMFALevel} from '@fluxer/constants/src/GuildConstants';
import {ThreadPermissionFlags} from '@fluxer/constants/src/ThreadPermissionUtils';
@@ -21,7 +21,7 @@ const ELEVATED_MFA_PERMISSIONS =
ThreadPermissionFlags.MANAGE_THREADS;
export async function createGuildMfaEnforcer(params: {
userRepository: IUserRepository;
userRepository: UserRepository;
guildData: Pick<GuildResponse, 'mfa_level' | 'owner_id'>;
userId: UserID;
}): Promise<(permission: bigint) => void> {
@@ -5,7 +5,7 @@ import {isIpBanExempt} from '@app/api/ban/IpBanExemptions';
import type {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService';
import type {GuildAuditLogChange} from '@app/api/guild/GuildAuditLogTypes';
import {mapGuildBansToResponse} from '@app/api/guild/GuildModel';
import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate';
import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository';
import {createGuildMfaEnforcer} from '@app/api/guild/services/GuildMfaEnforcement';
import {GuildMemberSearchIndexService} from '@app/api/guild/services/member/GuildMemberSearchIndexService';
import type {BanBy} from '@app/api/infrastructure/activity/Contract.generated';
@@ -16,7 +16,7 @@ import {Logger} from '@app/api/Logger';
import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware';
import type {Guild} from '@app/api/models/Guild';
import type {GuildBan} from '@app/api/models/GuildBan';
import type {IUserRepository} from '@app/api/user/IUserRepository';
import type {UserRepository} from '@app/api/user/repositories/UserRepository';
import type {WorkerTaskName} from '@app/api/worker/WorkerLaneConfig';
import {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType';
import {Permissions} from '@fluxer/constants/src/ChannelConstants';
@@ -39,8 +39,8 @@ export class GuildModerationService {
private readonly searchIndexService: GuildMemberSearchIndexService;
constructor(
private readonly guildRepository: IGuildRepositoryAggregate,
private readonly userRepository: IUserRepository,
private readonly guildRepository: GuildRepository,
private readonly userRepository: UserRepository,
private readonly gatewayService: IGatewayService,
private readonly userCacheService: UserCacheService,
private readonly workerService: IWorkerService<WorkerTaskName>,
@@ -6,8 +6,7 @@ import type {ThreadViewer} from '@app/api/experiment/ChannelThreadsGate';
import type {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService';
import type {GuildAuditLogChange} from '@app/api/guild/GuildAuditLogTypes';
import {mapGuildRoleToResponse} from '@app/api/guild/GuildModel';
import type {IGuildMemberRepository} from '@app/api/guild/repositories/IGuildMemberRepository';
import type {IGuildRoleRepository} from '@app/api/guild/repositories/IGuildRoleRepository';
import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository';
import {createGuildMfaEnforcer} from '@app/api/guild/services/GuildMfaEnforcement';
import {computeMovedIds, getMemberListRoleOrderIds} from '@app/api/guild/services/role/RoleOrderAuditUtils';
import {
@@ -23,7 +22,7 @@ import type {LimitConfigService} from '@app/api/limits/LimitConfigService';
import {resolveLimitSafe} from '@app/api/limits/LimitConfigUtils';
import {createLimitMatchContext} from '@app/api/limits/LimitMatchContextBuilder';
import {GuildRole} from '@app/api/models/GuildRole';
import type {IUserRepository} from '@app/api/user/IUserRepository';
import type {UserRepository} from '@app/api/user/repositories/UserRepository';
import {applyProtectedRolePermissions, permissionWriteMask, protectedThreadBits} from '@app/api/utils/featureUtils';
import {computePermissionsDiff} from '@app/api/utils/PermissionUtils';
import {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType';
@@ -45,8 +44,6 @@ import type {GuildResponse} from '@fluxer/schema/src/domains/guild/GuildResponse
import type {GuildRoleResponse} from '@fluxer/schema/src/domains/guild/GuildRoleSchemas';
import type {ICacheService} from '@pkgs/cache/src/ICacheService';
interface GuildRoleRepository extends IGuildRoleRepository, IGuildMemberRepository {}
interface GuildAuth {
guildData: GuildResponse;
checkPermission: (permission: bigint) => Promise<void>;
@@ -67,13 +64,13 @@ type RoleUpdateData = Partial<{
export class GuildRoleService {
constructor(
private readonly guildRepository: GuildRoleRepository,
private readonly guildRepository: GuildRepository,
private readonly snowflakeService: ISnowflakeService,
private readonly cacheService: ICacheService,
private readonly gatewayService: IGatewayService,
private readonly guildAuditLogService: GuildAuditLogService,
private readonly limitConfigService: LimitConfigService,
private readonly userRepository: IUserRepository,
private readonly userRepository: UserRepository,
) {}
async systemCreateRole(params: {
@@ -270,7 +267,7 @@ export class GuildRoleService {
hoist: updateData.hoist ?? role.isHoisted,
mentionable: updateData.mentionable ?? role.isMentionable,
};
const updatedRole = await this.guildRepository.upsertRole(updatedRoleData, role.toRow());
const updatedRole = await this.guildRepository.upsertRole(updatedRoleData);
await this.dispatchGuildRoleUpdate({guildId, role: updatedRole});
const changes = this.guildAuditLogService.computeChanges(previousSnapshot, this.serializeRoleForAudit(updatedRole));
if (role.permissions !== updatedRole.permissions) {
@@ -449,14 +446,10 @@ export class GuildRoleService {
for (const update of updates) {
const role = roleMap.get(update.roleId)!;
if (role.hoistPosition === update.hoistPosition) continue;
const roleRow = role.toRow();
const updatedRole = await this.guildRepository.upsertRole(
{
...roleRow,
hoist_position: update.hoistPosition,
},
roleRow,
);
const updatedRole = await this.guildRepository.upsertRole({
...role.toRow(),
hoist_position: update.hoistPosition,
});
changedRoles.push(updatedRole);
}
if (changedRoles.length > 0) {
@@ -497,14 +490,7 @@ export class GuildRoleService {
const changedRoles: Array<GuildRole> = [];
for (const role of allRoles) {
if (role.hoistPosition === null) continue;
const roleRow = role.toRow();
const updatedRole = await this.guildRepository.upsertRole(
{
...roleRow,
hoist_position: null,
},
roleRow,
);
const updatedRole = await this.guildRepository.upsertRole({...role.toRow(), hoist_position: null});
changedRoles.push(updatedRole);
}
if (changedRoles.length > 0) {
@@ -697,11 +683,7 @@ export class GuildRoleService {
});
const reorderedIds = targetOrder.map((r) => r.id);
const reorderedRoles = this.reorderRolePositions({allRoles, reorderedIds, guildId});
const updatePromises = reorderedRoles.map((role) => {
const roleRow = role.toRow();
const oldRole = roleMap.get(role.id);
return this.guildRepository.upsertRole(roleRow, oldRole ? oldRole.toRow() : undefined);
});
const updatePromises = reorderedRoles.map((role) => this.guildRepository.upsertRole(role.toRow()));
await Promise.all(updatePromises);
const updatedRoles = await this.guildRepository.listRoles(guildId);
const changedRoles = updatedRoles.filter((role) => {
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {ChannelID, GuildID, UserID} from '@app/api/BrandedTypes';
import type {IChannelRepository} from '@app/api/channel/IChannelRepository';
import type {ChannelRepository} from '@app/api/channel/ChannelRepository';
import {threadSearchIndexEnqueueCappedTotal} from '@app/api/channel/threads/ThreadMetrics';
import {
guildActive,
@@ -21,7 +21,7 @@ import {MessageSearchResponseMapper} from '@app/api/search/MessageSearchResponse
import {searchExistingMessages} from '@app/api/search/MessageSearchResultReconciler';
import {channelRequiresAgeVerification} from '@app/api/search/SearchNsfwUtils';
import {accessibleRequestedThreadIds, accessibleThreadIds} from '@app/api/search/ThreadSearchScope';
import type {IUserRepository} from '@app/api/user/IUserRepository';
import type {UserRepository} from '@app/api/user/repositories/UserRepository';
import {canUserAccessNsfwContent} from '@app/api/utils/AgeUtils';
import {mapWithConcurrency} from '@app/api/utils/ConcurrencyUtils';
import type {WorkerTaskName} from '@app/api/worker/WorkerLaneConfig';
@@ -46,10 +46,10 @@ export class GuildSearchService {
private readonly responseMapper: MessageSearchResponseMapper;
constructor(
private readonly channelRepository: IChannelRepository,
private readonly channelRepository: ChannelRepository,
private readonly userCacheService: UserCacheService,
private readonly gatewayService: IGatewayService,
private readonly userRepository: IUserRepository,
private readonly userRepository: UserRepository,
private readonly workerService: IWorkerService<WorkerTaskName>,
) {
this.responseMapper = new MessageSearchResponseMapper(this.channelRepository, this.userCacheService);
@@ -74,7 +74,7 @@ export class GuildSearchService {
const threadsVisible = viewerActive(viewer, guildId);
const explicitChannelIds = channelIds.length > 0;
if (!explicitChannelIds) {
const channels = await this.channelRepository.listGuildChannels(guildId, 'enrolled');
const channels = await this.channelRepository.channelData.listGuildChannels(guildId, 'enrolled');
channelIds = channels.filter((c) => threadsVisible || !c.isThreadOnly()).map((c) => c.id);
}
const includeNsfwRequested = searchParams.include_nsfw ?? false;
@@ -85,7 +85,7 @@ export class GuildSearchService {
}
const canIncludeNsfw = canUserAccessNsfw && (includeNsfwRequested || guildIsAgeRestricted);
const guildNsfw = guildData?.nsfw ?? false;
const channels = await this.channelRepository.listChannels(channelIds);
const channels = await this.channelRepository.channelData.listChannels(channelIds);
const channelMap = new Map<string, Channel>();
for (const channel of channels) {
if (channel.guildId === guildId && (threadsVisible || !THREAD_FEATURE_CHANNEL_TYPES.has(channel.type))) {
@@ -103,7 +103,7 @@ export class GuildSearchService {
if (requestedThreads.length > 0) {
const threadIds = new Set(requestedThreads.map((thread) => thread.id.toString()));
const parentIds = requestedThreads.flatMap((thread) => (thread.parentId ? [thread.parentId] : []));
for (const parent of await this.channelRepository.listChannels(
for (const parent of await this.channelRepository.channelData.listChannels(
parentIds.filter((id) => !channelMap.has(id.toString())),
)) {
if (parent.guildId === guildId) channelMap.set(parent.id.toString(), parent);
@@ -357,7 +357,7 @@ export class GuildSearchService {
}
}
if (missingParentIds.length > 0) {
const parents = await this.channelRepository.listChannels(missingParentIds);
const parents = await this.channelRepository.channelData.listChannels(missingParentIds);
for (const parent of parents) {
lookup.set(parent.id.toString(), parent);
}
@@ -393,7 +393,7 @@ export class GuildSearchService {
threadParents: Map<string, Channel>,
): Promise<void> {
const threadsByGuild = new Map<string, Array<Channel>>();
for (const thread of await this.channelRepository.listChannels([...threadIds])) {
for (const thread of await this.channelRepository.channelData.listChannels([...threadIds])) {
const parent = thread.parentId === null ? undefined : parentById.get(thread.parentId.toString());
if (!thread.isThread() || !parent || thread.guildId === null || parent.guildId !== thread.guildId) continue;
const threads = threadsByGuild.get(thread.guildId.toString());
@@ -438,7 +438,7 @@ export class GuildSearchService {
await mapWithConcurrency(guildIds, GUILD_FANOUT_CONCURRENCY, async (guildId) => {
const [guildData, guildChannels, viewableChannels] = await Promise.all([
this.gatewayService.getGuildData({guildId, userId}),
this.channelRepository
this.channelRepository.channelData
.listGuildChannels(guildId, 'enrolled')
.then((channels) =>
channels.filter(
@@ -3,7 +3,7 @@
import type {ApiContext} from '@app/api/ApiContext';
import type {EmojiID, GuildID, RoleID, StickerID, UserID} from '@app/api/BrandedTypes';
import {createChannelID, createWebhookID} from '@app/api/BrandedTypes';
import type {IChannelRepository} from '@app/api/channel/IChannelRepository';
import type {ChannelRepository} from '@app/api/channel/ChannelRepository';
import type {ChannelService} from '@app/api/channel/services/ChannelService';
import {mapThreadToResponse} from '@app/api/channel/services/thread/ThreadMappers';
import {everEnabled, isTainted, type ThreadViewer, viewerActive} from '@app/api/experiment/ChannelThreadsGate';
@@ -19,7 +19,7 @@ import {
THREAD_AUDIT_LOG_ACTION_TYPES,
} from '@app/api/guild/GuildAuditLogEntryMapper';
import type {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService';
import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate';
import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository';
import {GuildChannelService} from '@app/api/guild/services/GuildChannelService';
import {GuildContentService} from '@app/api/guild/services/GuildContentService';
import {GuildDataService} from '@app/api/guild/services/GuildDataService';
@@ -38,9 +38,9 @@ import type {LimitConfigService} from '@app/api/limits/LimitConfigService';
import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware';
import type {GuildAuditLog} from '@app/api/models/GuildAuditLog';
import type {Webhook} from '@app/api/models/Webhook';
import type {IUserRepository} from '@app/api/user/IUserRepository';
import type {UserRepository} from '@app/api/user/repositories/UserRepository';
import {getCachedUserPartialResponses} from '@app/api/user/UserCacheHelpers';
import type {IWebhookRepository} from '@app/api/webhook/IWebhookRepository';
import type {WebhookRepository} from '@app/api/webhook/WebhookRepository';
import {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType';
import {Permissions} from '@fluxer/constants/src/ChannelConstants';
import {GuildFeatures} from '@fluxer/constants/src/GuildConstants';
@@ -99,25 +99,25 @@ export class GuildService {
public readonly content: GuildContentService;
public readonly channels: GuildChannelService;
public readonly search: GuildSearchService;
private readonly guildRepository: IGuildRepositoryAggregate;
private readonly guildRepository: GuildRepository;
private readonly cacheService: ICacheService;
private readonly userCacheService: UserCacheService;
private readonly webhookRepository: IWebhookRepository;
private readonly webhookRepository: WebhookRepository;
private readonly guildAuditLogService: GuildAuditLogService;
private readonly gatewayService: IGatewayService;
private readonly userRepository: IUserRepository;
private readonly userRepository: UserRepository;
constructor(
apiContext: ApiContext,
guildRepository: IGuildRepositoryAggregate,
private readonly channelRepository: IChannelRepository,
guildRepository: GuildRepository,
private readonly channelRepository: ChannelRepository,
inviteRepository: InviteRepository,
channelService: ChannelService,
userCacheService: UserCacheService,
entityAssetService: EntityAssetService,
avatarService: AvatarService,
assetDeletionQueue: IAssetDeletionQueue,
webhookRepository: IWebhookRepository,
webhookRepository: WebhookRepository,
guildAuditLogService: GuildAuditLogService,
limitConfigService: LimitConfigService,
) {
@@ -218,7 +218,7 @@ export class GuildService {
): Promise<GuildResponse> {
const {guildId, requestCache} = params;
const {guild, previousFeatures, updatedFeatures} = await this.withGuildUpdateLock(guildId, requestCache, () =>
this.data.updateGuild(params, auditLogReason),
this.data.operations.updateGuild(params, auditLogReason),
);
if (
previousFeatures.has(GuildFeatures.TEXT_CHANNEL_FLEXIBLE_NAMES) &&
@@ -258,7 +258,7 @@ export class GuildService {
async getEmojiMetadata(emojiId: EmojiID): Promise<GuildEmojiMetadataResponse> {
const emoji = await this.guildRepository.getEmojiById(emojiId);
if (!emoji) throw new UnknownGuildEmojiError();
const guild = await this.data.getGuildSystem(emoji.guildId);
const guild = await this.data.operations.getGuildSystem(emoji.guildId);
return {
id: emoji.id.toString(),
guild_id: guild.id.toString(),
@@ -271,7 +271,7 @@ export class GuildService {
async getStickerMetadata(stickerId: StickerID): Promise<GuildStickerMetadataResponse> {
const sticker = await this.guildRepository.getStickerById(stickerId);
if (!sticker) throw new UnknownGuildStickerError();
const guild = await this.data.getGuildSystem(sticker.guildId);
const guild = await this.data.operations.getGuildSystem(sticker.guildId);
return {
id: sticker.id.toString(),
guild_id: guild.id.toString(),
@@ -421,7 +421,7 @@ export class GuildService {
);
const channels =
candidateIds.length > 0
? await this.channelRepository.listChannels(candidateIds.map((id) => createChannelID(BigInt(id))))
? await this.channelRepository.channelData.listChannels(candidateIds.map((id) => createChannelID(BigInt(id))))
: [];
const gatedIds = new Set(
channels
@@ -435,7 +435,7 @@ export class GuildService {
const channelIds = [...new Set(webhooks.flatMap((webhook) => (webhook.channelId ? [webhook.channelId] : [])))];
if (channelIds.length === 0) return webhooks;
const forumIds = new Set(
(await this.channelRepository.listChannels(channelIds))
(await this.channelRepository.channelData.listChannels(channelIds))
.filter((channel) => channel.isThreadOnly())
.map((channel) => channel.id),
);
@@ -454,12 +454,14 @@ export class GuildService {
.map((id) => createChannelID(BigInt(id)));
if (threadIds.length === 0) return [];
const [channels, states, stats] = await Promise.all([
this.channelRepository.listChannels(threadIds),
this.channelRepository.channelData.listChannels(threadIds),
this.channelRepository.threads.getStates(threadIds),
this.channelRepository.threads.getStatsMany(threadIds),
]);
const stateById = new Map(states.map((state) => [state.threadId.toString(), state]));
const parents = await this.channelRepository.listChannels([...new Set(states.map((state) => state.parentId))]);
const parents = await this.channelRepository.channelData.listChannels([
...new Set(states.map((state) => state.parentId)),
]);
const parentTypeById = new Map(parents.map((parent) => [parent.id.toString(), parent.type]));
return channels.flatMap((channel) => {
const state = stateById.get(channel.id.toString());
@@ -14,7 +14,7 @@ import {
resolveThreadPermissionMode,
shouldMaskThreadPermissionBits,
} from '@app/api/guild/services/ThreadPermissionBits';
import type {IUserRepository} from '@app/api/user/IUserRepository';
import type {UserRepository} from '@app/api/user/repositories/UserRepository';
import {
applyProtectedOverwriteBits,
applyProtectedRolePermissions,
@@ -212,7 +212,7 @@ describe('guild MFA enforcement', () => {
it('treats MANAGE_THREADS as elevated', async () => {
const userRepository = {
findUnique: async () => ({authenticatorTypes: new Set()}),
} as unknown as IUserRepository;
} as unknown as UserRepository;
const enforce = await createGuildMfaEnforcer({
userRepository,
guildData: {mfa_level: GuildMFALevel.ELEVATED, owner_id: '1'},
@@ -3,7 +3,7 @@
import type {ChannelID, EmojiID, GuildID, RoleID, StickerID, UserID} from '@app/api/BrandedTypes';
import {createChannelID, createRoleID, createUserID} from '@app/api/BrandedTypes';
import {mapChannelToResponse} from '@app/api/channel/ChannelMappers';
import type {IChannelRepository} from '@app/api/channel/IChannelRepository';
import type {ChannelRepository} from '@app/api/channel/ChannelRepository';
import {
buildThreadParentPatch,
loadThreadParentConfig,
@@ -21,7 +21,7 @@ import {
} from '@app/api/experiment/ChannelThreadsGate';
import type {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService';
import type {GuildAuditLogChange} from '@app/api/guild/GuildAuditLogTypes';
import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate';
import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository';
import {ChannelHelpers, type ChannelReorderOperation} from '@app/api/guild/services/channel/ChannelHelpers';
import {resolveProtectedBitActor} from '@app/api/guild/services/ThreadPermissionBits';
import type {IGatewayService} from '@app/api/infrastructure/IGatewayService';
@@ -64,8 +64,8 @@ import type {ICacheService} from '@pkgs/cache/src/ICacheService';
export class ChannelOperationsService {
constructor(
private readonly channelRepository: IChannelRepository,
private readonly guildRepository: IGuildRepositoryAggregate,
private readonly channelRepository: ChannelRepository,
private readonly guildRepository: GuildRepository,
private readonly userCacheService: UserCacheService,
private readonly gatewayService: IGatewayService,
private readonly cacheService: ICacheService,
@@ -86,7 +86,7 @@ export class ChannelOperationsService {
auditLogReason?: string | null,
): Promise<ChannelResponse> {
await this.ensureGuildHasCapacity(params.guildId);
const channels = await this.channelRepository.listGuildChannels(params.guildId, 'enrolled');
const channels = await this.channelRepository.channelData.listGuildChannels(params.guildId, 'enrolled');
const parentId = params.data.parent_id ? createChannelID(params.data.parent_id) : null;
const parentChannel = this.validateParentCategory({
parentId,
@@ -212,7 +212,7 @@ export class ChannelOperationsService {
const requestedContentWarningText =
trimmedContentWarningText && trimmedContentWarningText.length > 0 ? trimmedContentWarningText : null;
const channelId = createChannelID(await this.snowflakeService.generate());
const channel = await this.channelRepository.upsert({
const channel = await this.channelRepository.channelData.upsert({
channel_id: channelId,
guild_id: params.guildId,
type: params.data.type,
@@ -289,7 +289,7 @@ export class ChannelOperationsService {
async sanitizeTextChannelNames(params: {guildId: GuildID; requestCache: RequestCache}): Promise<void> {
const {guildId, requestCache} = params;
const channels = await this.channelRepository.listGuildChannels(guildId, 'enrolled');
const channels = await this.channelRepository.channelData.listGuildChannels(guildId, 'enrolled');
let hasChanges = false;
const updatedChannels: Array<Channel> = [];
for (const channel of channels) {
@@ -305,7 +305,7 @@ export class ChannelOperationsService {
updatedChannels.push(channel);
continue;
}
const updated = await this.channelRepository.upsert({
const updated = await this.channelRepository.channelData.upsert({
...channel.toRow(),
name: normalized,
});
@@ -372,7 +372,7 @@ export class ChannelOperationsService {
}
private async listPositionableChannels(guildId: GuildID, hideThreadOnly: boolean): Promise<Array<Channel>> {
const channels = await this.channelRepository.listGuildChannels(guildId, 'enrolled');
const channels = await this.channelRepository.channelData.listGuildChannels(guildId, 'enrolled');
return hideThreadOnly ? channels.filter((ch) => !ch.isThreadOnly()) : channels;
}
@@ -461,9 +461,9 @@ export class ChannelOperationsService {
clientFeatures?: ReadonlySet<string>;
viewer?: ThreadViewer;
}): Promise<void> {
const parent = await this.channelRepository.findUnique(params.parentId);
const parent = await this.channelRepository.channelData.findUnique(params.parentId);
if (!parent || parent.guildId !== params.guildId || parent.type !== ChannelTypes.GUILD_CATEGORY) return;
const child = await this.channelRepository.findUnique(params.channelId);
const child = await this.channelRepository.channelData.findUnique(params.channelId);
if (!child || child.guildId !== params.guildId) return;
const userPermissions = await this.gatewayService.getUserPermissions({
guildId: params.guildId,
@@ -508,7 +508,7 @@ export class ChannelOperationsService {
const existingAllow = child.permissionOverwrites.get(targetId)?.allow ?? 0n;
if ((incoming.allow & ~existingAllow & ~userPermissions) !== 0n) throw new MissingPermissionsError();
}
await this.channelRepository.upsert({
await this.channelRepository.channelData.upsert({
...child.toRow(),
permission_overwrites: new Map(
Array.from(incomingOverwrites.entries()).map(([targetId, overwrite]) => [
@@ -525,7 +525,7 @@ export class ChannelOperationsService {
requestCache: RequestCache;
}): Promise<void> {
const {guildId, operation, requestCache} = params;
const allChannels = await this.channelRepository.listGuildChannels(guildId, 'enrolled');
const allChannels = await this.channelRepository.channelData.listGuildChannels(guildId, 'enrolled');
const planResult = computeGuildChannelReorderPlan({channels: allChannels, operation});
if (!planResult.ok) {
this.throwReorderPlanError(planResult.code, operation);
@@ -561,14 +561,14 @@ export class ChannelOperationsService {
const currentParent = channel.parentId ?? null;
if (channel.position !== desiredPosition || currentParent !== desiredParent) {
updatePromises.push(
this.channelRepository
this.channelRepository.channelData
.upsert({...channel.toRow(), position: desiredPosition, parent_id: desiredParent})
.then(() => {}),
);
}
}
await Promise.all(updatePromises);
const updatedChannels = await this.channelRepository.listGuildChannels(guildId, 'enrolled');
const updatedChannels = await this.channelRepository.channelData.listGuildChannels(guildId, 'enrolled');
await this.dispatchChannelUpdateBulk({guildId, channels: updatedChannels, requestCache});
}
@@ -3,7 +3,7 @@
import {createEmojiID, type EmojiID, type GuildID, type UserID} from '@app/api/BrandedTypes';
import {getContentMessage} from '@app/api/content_i18n/ContentI18n';
import {mapGuildEmojisWithUsersToResponse, mapGuildEmojiToResponse} from '@app/api/guild/GuildModel';
import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate';
import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository';
import type {ContentHelpers} from '@app/api/guild/services/content/ContentHelpers';
import type {ExpressionAssetPurger} from '@app/api/guild/services/content/ExpressionAssetPurger';
import type {AvatarService} from '@app/api/infrastructure/AvatarService';
@@ -29,7 +29,7 @@ import type {GuildEmojiResponse, GuildEmojiWithUserResponse} from '@fluxer/schem
export class EmojiService {
constructor(
private readonly guildRepository: IGuildRepositoryAggregate,
private readonly guildRepository: GuildRepository,
private readonly userCacheService: UserCacheService,
private readonly gatewayService: IGatewayService,
private readonly avatarService: AvatarService,
@@ -3,7 +3,7 @@
import {createStickerID, type GuildID, type StickerID, type UserID} from '@app/api/BrandedTypes';
import {getContentMessage} from '@app/api/content_i18n/ContentI18n';
import {mapGuildStickersWithUsersToResponse, mapGuildStickerToResponse} from '@app/api/guild/GuildModel';
import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate';
import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository';
import type {ContentHelpers} from '@app/api/guild/services/content/ContentHelpers';
import type {ExpressionAssetPurger} from '@app/api/guild/services/content/ExpressionAssetPurger';
import type {AvatarService} from '@app/api/infrastructure/AvatarService';
@@ -33,7 +33,7 @@ import type {
export class StickerService {
constructor(
private readonly guildRepository: IGuildRepositoryAggregate,
private readonly guildRepository: GuildRepository,
private readonly userCacheService: UserCacheService,
private readonly gatewayService: IGatewayService,
private readonly avatarService: AvatarService,
@@ -9,7 +9,7 @@ import {createGuildMfaEnforcer} from '@app/api/guild/services/GuildMfaEnforcemen
import type {IGatewayService} from '@app/api/infrastructure/IGatewayService';
import {Logger} from '@app/api/Logger';
import type {Guild} from '@app/api/models/Guild';
import type {IUserRepository} from '@app/api/user/IUserRepository';
import type {UserRepository} from '@app/api/user/repositories/UserRepository';
import {serializeGuildForAudit as serializeGuildForAuditUtil} from '@app/api/utils/AuditSerializationUtils';
import {requirePermission} from '@app/api/utils/PermissionUtils';
import type {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType';
@@ -26,7 +26,7 @@ export class GuildDataHelpers {
constructor(
private readonly gatewayService: IGatewayService,
private readonly guildAuditLogService: GuildAuditLogService,
private readonly userRepository: IUserRepository,
private readonly userRepository: UserRepository,
) {}
private readonly guildRepository = new GuildRepository();
@@ -2,7 +2,7 @@
import type {ChannelID, GuildID, RoleID, UserID} from '@app/api/BrandedTypes';
import {createChannelID, createGuildID, createRoleID, guildIdToRoleId} from '@app/api/BrandedTypes';
import type {IChannelRepository} from '@app/api/channel/IChannelRepository';
import type {ChannelRepository} from '@app/api/channel/ChannelRepository';
import {
type ChannelFollowerRemovalCopyMode,
scheduleDeletedChannelFollowerRemoval,
@@ -14,8 +14,8 @@ import type {PermissionOverwrite} from '@app/api/database/types/ChannelTypes';
import type {GuildRow} from '@app/api/database/types/GuildTypes';
import {guildActive, isTainted} from '@app/api/experiment/ChannelThreadsGate';
import {mapGuildToGuildResponse, mapGuildToPartialResponse} from '@app/api/guild/GuildModel';
import type {IGuildDiscoveryRepository} from '@app/api/guild/repositories/GuildDiscoveryRepository';
import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate';
import type {GuildDiscoveryRepository} from '@app/api/guild/repositories/GuildDiscoveryRepository';
import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository';
import type {GuildDataHelpers} from '@app/api/guild/services/data/GuildDataHelpers';
import {contentModerationService} from '@app/api/infrastructure/ContentModerationService';
import type {EntityAssetService, PreparedAssetUpload} from '@app/api/infrastructure/EntityAssetService';
@@ -35,11 +35,11 @@ import type {GuildDiscoveryContext} from '@app/api/search/guild/GuildSearchSeria
import {deleteChannelMessageSearchDocuments} from '@app/api/search/MessageSearchIndexCleanup';
import {deleteThreadSearchDocuments} from '@app/api/search/thread/ThreadSearchService';
import {Channels, ChannelsByGuild, GuildMembers, GuildMembersByUserId, GuildRoles, Guilds} from '@app/api/Tables';
import type {IUserRepository} from '@app/api/user/IUserRepository';
import type {UserRepository} from '@app/api/user/repositories/UserRepository';
import {mapUserSettingsToResponse} from '@app/api/user/UserMappers';
import {addGuildToUncategorizedFolder, removeGuildFromUserFolders} from '@app/api/user/utils/GuildFolderUtils';
import {mapWithConcurrency} from '@app/api/utils/ConcurrencyUtils';
import type {IWebhookRepository} from '@app/api/webhook/IWebhookRepository';
import type {WebhookRepository} from '@app/api/webhook/WebhookRepository';
import {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType';
import {ALL_PERMISSIONS, ChannelTypes, DEFAULT_PERMISSIONS, Permissions} from '@fluxer/constants/src/ChannelConstants';
import {DiscoveryApplicationStatus} from '@fluxer/constants/src/DiscoveryConstants';
@@ -185,18 +185,18 @@ function isSystemChannelType(type: number): boolean {
export class GuildOperationsService {
constructor(
private readonly guildRepository: IGuildRepositoryAggregate,
private readonly channelRepository: IChannelRepository,
private readonly guildRepository: GuildRepository,
private readonly channelRepository: ChannelRepository,
private readonly inviteRepository: InviteRepository,
private readonly channelService: ChannelService,
private readonly gatewayService: IGatewayService,
private readonly entityAssetService: EntityAssetService,
private readonly userRepository: IUserRepository,
private readonly userRepository: UserRepository,
private readonly snowflakeService: ISnowflakeService,
private readonly webhookRepository: IWebhookRepository,
private readonly webhookRepository: WebhookRepository,
private readonly helpers: GuildDataHelpers,
private readonly limitConfigService: LimitConfigService,
private readonly discoveryRepository: IGuildDiscoveryRepository,
private readonly discoveryRepository: GuildDiscoveryRepository,
) {}
async getGuild({userId, guildId}: {userId: UserID; guildId: GuildID}): Promise<GuildResponse> {
@@ -521,7 +521,7 @@ export class GuildOperationsService {
if (data.afk_channel_id !== undefined) {
if (data.afk_channel_id) {
const afkChannelId = createChannelID(data.afk_channel_id);
const afkChannel = await this.channelRepository.findUnique(afkChannelId);
const afkChannel = await this.channelRepository.channelData.findUnique(afkChannelId);
if (!afkChannel || afkChannel.guildId !== guildId) {
throw InputValidationError.fromCode('afk_channel_id', ValidationErrorCodes.AFK_CHANNEL_MUST_BE_IN_GUILD);
}
@@ -539,7 +539,7 @@ export class GuildOperationsService {
if (data.system_channel_id !== undefined) {
if (data.system_channel_id) {
const systemChannelId = createChannelID(data.system_channel_id);
const systemChannel = await this.channelRepository.findUnique(systemChannelId);
const systemChannel = await this.channelRepository.channelData.findUnique(systemChannelId);
if (!systemChannel || systemChannel.guildId !== guildId) {
throw InputValidationError.fromCode(
'system_channel_id',
@@ -768,7 +768,7 @@ export class GuildOperationsService {
if (!guild) {
throw new UnknownGuildError();
}
const channels = await this.channelRepository.listGuildChannels(guildId, 'complete');
const channels = await this.channelRepository.channelData.listGuildChannels(guildId, 'complete');
for (const channel of channels) {
await scheduleDeletedChannelFollowerRemoval({channel, crossposts: this.channelRepository.crossposts, copyMode});
}
@@ -805,7 +805,7 @@ export class GuildOperationsService {
for (const channel of channels) {
await this.channelService.attachments.purgeChannelAttachments(channel);
}
await Promise.all(channels.map((channel) => this.channelRepository.deleteAllChannelMessages(channel.id)));
await Promise.all(channels.map((channel) => this.channelRepository.messages.deleteAllChannelMessages(channel.id)));
await Promise.all(
channels.map((channel) => deleteChannelMessageSearchDocuments(channel.id, {context: {source: 'guild_delete'}})),
);
@@ -815,7 +815,7 @@ export class GuildOperationsService {
}
if (threadIds.length > 0) {
const threadChannels = new Map(
(await this.channelRepository.listChannels(threadIds)).map((channel) => [channel.id, channel]),
(await this.channelRepository.channelData.listChannels(threadIds)).map((channel) => [channel.id, channel]),
);
await mapWithConcurrency(threadIds, THREAD_PURGE_CONCURRENCY, async (threadId) => {
const thread = threadChannels.get(threadId);
@@ -2,9 +2,9 @@
import type {GuildID, UserID} from '@app/api/BrandedTypes';
import {mapGuildToGuildResponse} from '@app/api/guild/GuildModel';
import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate';
import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository';
import type {GuildDataHelpers} from '@app/api/guild/services/data/GuildDataHelpers';
import type {IUserRepository} from '@app/api/user/IUserRepository';
import type {UserRepository} from '@app/api/user/repositories/UserRepository';
import {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType';
import {MissingAccessError} from '@fluxer/errors/src/domains/core/MissingAccessError';
import {MissingPermissionsError} from '@fluxer/errors/src/domains/core/MissingPermissionsError';
@@ -17,8 +17,8 @@ const OWNERSHIP_AUDIT_KEYS: ReadonlySet<string> = new Set(['owner_id']);
export class GuildOwnershipService {
constructor(
private readonly guildRepository: IGuildRepositoryAggregate,
private readonly userRepository: IUserRepository,
private readonly guildRepository: GuildRepository,
private readonly userRepository: UserRepository,
private readonly helpers: GuildDataHelpers,
) {}
@@ -7,7 +7,7 @@ import {
type UserID,
vanityCodeToInviteCode,
} from '@app/api/BrandedTypes';
import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate';
import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository';
import type {GuildDataHelpers} from '@app/api/guild/services/data/GuildDataHelpers';
import type {InviteRepository} from '@app/api/invite/InviteRepository';
import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware';
@@ -23,7 +23,7 @@ const VANITY_AUDIT_KEYS: ReadonlySet<string> = new Set(['vanity_url_code']);
export class GuildVanityService {
constructor(
private readonly guildRepository: IGuildRepositoryAggregate,
private readonly guildRepository: GuildRepository,
private readonly inviteRepository: InviteRepository,
private readonly helpers: GuildDataHelpers,
) {}
@@ -3,7 +3,7 @@
import type {GuildID, RoleID, UserID} from '@app/api/BrandedTypes';
import {createGuildMfaEnforcer} from '@app/api/guild/services/GuildMfaEnforcement';
import type {IGatewayService} from '@app/api/infrastructure/IGatewayService';
import type {IUserRepository} from '@app/api/user/IUserRepository';
import type {UserRepository} from '@app/api/user/repositories/UserRepository';
import {MissingAccessError} from '@fluxer/errors/src/domains/core/MissingAccessError';
import {MissingPermissionsError} from '@fluxer/errors/src/domains/core/MissingPermissionsError';
import type {GuildResponse} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas';
@@ -20,7 +20,7 @@ interface GuildAuth {
export class GuildMemberAuthService {
constructor(
private readonly gatewayService: IGatewayService,
private readonly userRepository: IUserRepository,
private readonly userRepository: UserRepository,
) {}
async getGuildAuthenticated({userId, guildId}: {userId: UserID; guildId: GuildID}): Promise<GuildAuth> {
@@ -10,7 +10,7 @@ import type {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService';
import type {GuildAuditLogChange} from '@app/api/guild/GuildAuditLogTypes';
import {resolveMaxGuildMembersLimit} from '@app/api/guild/GuildMemberLimitUtils';
import {mapGuildMemberToResponse} from '@app/api/guild/GuildModel';
import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate';
import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository';
import type {GuildMemberAuthService} from '@app/api/guild/services/member/GuildMemberAuthService';
import type {GuildMemberEventService} from '@app/api/guild/services/member/GuildMemberEventService';
import type {GuildMemberSearchIndexService} from '@app/api/guild/services/member/GuildMemberSearchIndexService';
@@ -32,7 +32,7 @@ import type {User} from '@app/api/models/User';
import type {UserGuildSettings} from '@app/api/models/UserGuildSettings';
import type {UserSettings} from '@app/api/models/UserSettings';
import {assertAccountNotLimited} from '@app/api/user/AccountLimit';
import type {IUserRepository} from '@app/api/user/IUserRepository';
import type {UserRepository} from '@app/api/user/repositories/UserRepository';
import {dispatchUserGuildSettingsUpdate} from '@app/api/user/UserGuildSettingsThreadView';
import {isProfileSubstringExempt} from '@app/api/user/UserHelpers';
import {mapUserSettingsToResponse} from '@app/api/user/UserMappers';
@@ -145,12 +145,12 @@ function joinSourceName(type: JoinSourceType): JoinSource {
export class GuildMemberOperationsService {
constructor(
private readonly guildRepository: IGuildRepositoryAggregate,
private readonly guildRepository: GuildRepository,
private readonly channelService: ChannelService,
private readonly userCacheService: UserCacheService,
private readonly gatewayService: IGatewayService,
private readonly entityAssetService: EntityAssetService,
private readonly userRepository: IUserRepository,
private readonly userRepository: UserRepository,
private readonly rateLimitService: IRateLimitService,
private readonly authService: GuildMemberAuthService,
private readonly validationService: GuildMemberValidationService,
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {GuildID, RoleID, UserID} from '@app/api/BrandedTypes';
import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate';
import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository';
import type {GuildMemberAuthService} from '@app/api/guild/services/member/GuildMemberAuthService';
import type {GuildMemberValidationService} from '@app/api/guild/services/member/GuildMemberValidationService';
import type {IGatewayService} from '@app/api/infrastructure/IGatewayService';
@@ -10,7 +10,7 @@ import {UnknownGuildMemberError} from '@fluxer/errors/src/domains/guild/UnknownG
export class GuildMemberRoleService {
constructor(
private readonly guildRepository: IGuildRepositoryAggregate,
private readonly guildRepository: GuildRepository,
private readonly gatewayService: IGatewayService,
private readonly authService: GuildMemberAuthService,
private readonly validationService: GuildMemberValidationService,
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {GuildID, UserID} from '@app/api/BrandedTypes';
import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate';
import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository';
import {Logger} from '@app/api/Logger';
import type {GuildMember} from '@app/api/models/GuildMember';
import type {User} from '@app/api/models/User';
@@ -19,11 +19,7 @@ function getGuildMemberIndexService(options: GuildMemberSearchIndexOptions = {})
}
export class GuildMemberSearchIndexService {
async updateUserMembers(
user: User,
guildIds: Array<GuildID>,
guildRepository: IGuildRepositoryAggregate,
): Promise<void> {
async updateUserMembers(user: User, guildIds: Array<GuildID>, guildRepository: GuildRepository): Promise<void> {
if (guildIds.length === 0) return;
const guilds = await guildRepository.listGuilds(guildIds);
const indexedGuilds = guilds.filter((guild) => guild.membersIndexedAt != null);
@@ -3,9 +3,9 @@
import type {GuildID, RoleID, UserID} from '@app/api/BrandedTypes';
import {guildIdToRoleId} from '@app/api/BrandedTypes';
import {isIpBanExempt} from '@app/api/ban/IpBanExemptions';
import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate';
import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository';
import type {GuildMember} from '@app/api/models/GuildMember';
import type {IUserRepository} from '@app/api/user/IUserRepository';
import type {UserRepository} from '@app/api/user/repositories/UserRepository';
import {Permissions} from '@fluxer/constants/src/ChannelConstants';
import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes';
import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError';
@@ -24,8 +24,8 @@ function ensureNotEveryoneRole(roleId: RoleID, guildId: GuildID, path: string):
export class GuildMemberValidationService {
constructor(
private readonly guildRepository: IGuildRepositoryAggregate,
private readonly userRepository: IUserRepository,
private readonly guildRepository: GuildRepository,
private readonly userRepository: UserRepository,
) {}
async validateAndGetRoleIds(params: {
@@ -11,7 +11,7 @@ import {
import type {GuildAuditLogRow} from '@app/api/database/types/GuildTypes';
import {mapGuildAuditLogEntry} from '@app/api/guild/GuildAuditLogEntryMapper';
import {GuildAuditLogService} from '@app/api/guild/GuildAuditLogService';
import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate';
import type {GuildRepository} from '@app/api/guild/repositories/GuildRepository';
import type {IGatewayService} from '@app/api/infrastructure/IGatewayService';
import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService';
import {ChannelPermissionOverwrite} from '@app/api/models/ChannelPermissionOverwrite';
@@ -29,19 +29,6 @@ const MEMBER_ID = createUserID(1420000000000000002n);
const ROLE_ID = createRoleID(1420000000000000003n);
const CHANNEL_ID = createChannelID(1420000000000000004n);
const SKIPPABLE_ACTIONS = [
AuditLogActionType.GUILD_UPDATE,
AuditLogActionType.CHANNEL_UPDATE,
AuditLogActionType.CHANNEL_OVERWRITE_UPDATE,
AuditLogActionType.MEMBER_UPDATE,
AuditLogActionType.MEMBER_ROLE_UPDATE,
AuditLogActionType.MEMBER_MOVE,
AuditLogActionType.ROLE_UPDATE,
AuditLogActionType.WEBHOOK_UPDATE,
AuditLogActionType.EMOJI_UPDATE,
AuditLogActionType.STICKER_UPDATE,
];
function createService(roleNames: Map<string, string> = new Map()) {
let nextLogId = 1420000000000001000n;
const createAuditLog = vi.fn(async (row: GuildAuditLogRow) => new GuildAuditLog(row));
@@ -55,7 +42,7 @@ function createService(roleNames: Map<string, string> = new Map()) {
const dispatchGuild = vi.fn().mockResolvedValue(undefined);
const addJob = vi.fn().mockResolvedValue(undefined);
const service = new GuildAuditLogService(
{createAuditLog, batchDeleteAndCreateAuditLogs, getRole} as unknown as IGuildRepositoryAggregate,
{createAuditLog, batchDeleteAndCreateAuditLogs, getRole} as unknown as GuildRepository,
{generate: vi.fn(async () => nextLogId++)} as unknown as ISnowflakeService,
{addJob} as unknown as IWorkerService<WorkerTaskName>,
{dispatchGuild} as unknown as IGatewayService,
@@ -175,11 +162,11 @@ describe('GuildAuditLogService dispatch', () => {
});
describe('GuildAuditLogBuilder.commit', () => {
it.each(SKIPPABLE_ACTIONS)('skips action %i with empty changes', async (actionType) => {
it('skips an update with empty changes', async () => {
const {service, createAuditLog, dispatchGuild} = createService();
const result = await service
.createBuilder(GUILD_ID, ACTOR_ID)
.withAction(actionType, MEMBER_ID.toString())
.withAction(AuditLogActionType.MEMBER_UPDATE, MEMBER_ID.toString())
.withReason('ignored reason')
.withMetadata({role_name: 'Moderators'})
.withChanges([])
@@ -189,31 +176,6 @@ describe('GuildAuditLogBuilder.commit', () => {
expect(dispatchGuild).not.toHaveBeenCalled();
});
it.each(SKIPPABLE_ACTIONS)('skips action %i without changes', async (actionType) => {
const {service, createAuditLog, dispatchGuild} = createService();
const result = await service
.createBuilder(GUILD_ID, ACTOR_ID)
.withAction(actionType, MEMBER_ID.toString())
.commit();
expect(result).toBeNull();
expect(createAuditLog).not.toHaveBeenCalled();
expect(dispatchGuild).not.toHaveBeenCalled();
});
it('skips a guild update that only changes internal keys', async () => {
const {service, createAuditLog} = createService();
const result = await service
.createBuilder(GUILD_ID, ACTOR_ID)
.withAction(AuditLogActionType.GUILD_UPDATE, GUILD_ID.toString())
.withChanges([
{key: 'member_count', old_value: 1, new_value: 2},
{key: 'splash_width', old_value: null, new_value: 1920},
])
.commit();
expect(result).toBeNull();
expect(createAuditLog).not.toHaveBeenCalled();
});
it('records a kick without changes', async () => {
const {service, createAuditLog, dispatchGuild} = createService();
const result = await service
@@ -227,18 +189,6 @@ describe('GuildAuditLogBuilder.commit', () => {
expect(writtenRow(createAuditLog).changes).toBeNull();
expect(dispatchGuild).toHaveBeenCalledTimes(1);
});
it('records a webhook create with empty changes', async () => {
const {service, createAuditLog, dispatchGuild} = createService();
const result = await service
.createBuilder(GUILD_ID, ACTOR_ID)
.withAction(AuditLogActionType.WEBHOOK_CREATE, '1420000000000000005')
.withChanges([])
.commit();
expect(result).not.toBeNull();
expect(createAuditLog).toHaveBeenCalledTimes(1);
expect(dispatchGuild).toHaveBeenCalledTimes(1);
});
});
describe('GuildAuditLogService.recordPermissionOverwriteDiff', () => {
@@ -202,7 +202,7 @@ describe('Guild Channel Management', () => {
expect.objectContaining({path: 'rtc_p2p', code: ValidationErrorCodes.GUILD_FEATURE_NOT_TOGGLEABLE}),
);
}
const stored = await getChannelRepository().findUnique(createChannelID(BigInt(voiceChannel.id)));
const stored = await getChannelRepository().channelData.findUnique(createChannelID(BigInt(voiceChannel.id)));
expect(stored?.rtcP2p).toBe(false);
await setVoiceP2pConfig({enabled: true, included_user_ids: [account.userId]});
await createBuilder(harness, account.token)
@@ -225,7 +225,7 @@ describe('Guild Channel Management', () => {
.body({type: ChannelTypes.GUILD_TEXT, rtc_p2p: true})
.execute();
expect(data.rtc_p2p).toBeUndefined();
const stored = await getChannelRepository().findUnique(createChannelID(BigInt(textChannel.id)));
const stored = await getChannelRepository().channelData.findUnique(createChannelID(BigInt(textChannel.id)));
expect(stored?.rtcP2p).toBe(false);
});
test('should require MANAGE_CHANNELS to update rtc_p2p', async () => {
@@ -242,7 +242,7 @@ describe('Guild Channel Management', () => {
.body({type: ChannelTypes.GUILD_VOICE, rtc_p2p: true})
.expect(HTTP_STATUS.FORBIDDEN)
.execute();
const stored = await getChannelRepository().findUnique(createChannelID(BigInt(voiceChannel.id)));
const stored = await getChannelRepository().channelData.findUnique(createChannelID(BigInt(voiceChannel.id)));
expect(stored?.rtcP2p).toBe(false);
});
test('should clamp bitrate to 96000 without an audio bitrate feature', async () => {
@@ -227,7 +227,10 @@ describe('Guild Channel Positions', () => {
{id: coopVoice.id, parent_id: coopGames.id},
]);
const channelRepository = new ChannelRepository();
const channelsBeforeMove = await channelRepository.listGuildChannels(createGuildID(BigInt(guild.id)), 'enrolled');
const channelsBeforeMove = await channelRepository.channelData.listGuildChannels(
createGuildID(BigInt(guild.id)),
'enrolled',
);
const storedCoopText = channelsBeforeMove.find((channel) => channel.id.toString() === coopText.id);
const storedCoopVoice = channelsBeforeMove.find((channel) => channel.id.toString() === coopVoice.id);
expect(storedCoopText).toBeDefined();
@@ -235,8 +238,8 @@ describe('Guild Channel Positions', () => {
if (!storedCoopText || !storedCoopVoice) {
return;
}
await channelRepository.upsert({...storedCoopVoice.toRow(), position: 1});
await channelRepository.upsert({...storedCoopText.toRow(), position: 2});
await channelRepository.channelData.upsert({...storedCoopVoice.toRow(), position: 1});
await channelRepository.channelData.upsert({...storedCoopText.toRow(), position: 2});
await updateChannelPositions(harness, account.token, guild.id, [
{
id: frontDoor.id,
@@ -318,7 +318,7 @@ describe('thread permission bits across guild modes', () => {
.execute();
const channelId = createChannelID(BigInt(channel.id));
const stored = async () => {
const overwrites = (await new ChannelRepository().findUnique(channelId))!.permissionOverwrites;
const overwrites = (await new ChannelRepository().channelData.findUnique(channelId))!.permissionOverwrites;
return {
everyone: overwrites.get(createRoleID(BigInt(guild.id))),
target: overwrites.get(createRoleID(BigInt(target.id))),
@@ -358,9 +358,14 @@ describe('thread permission bits across guild modes', () => {
.execute();
const repository = new ChannelRepository();
const before = await repository.channelData.countGuildChannels(guildId);
const source = (await repository.findUnique(createChannelID(BigInt(text.id))))!;
const source = (await repository.channelData.findUnique(createChannelID(BigInt(text.id))))!;
const forumId = createChannelID(BigInt(text.id) + 1n);
await repository.upsert({...source.toRow(), channel_id: forumId, type: ChannelTypes.GUILD_FORUM, name: 'forum'});
await repository.channelData.upsert({
...source.toRow(),
channel_id: forumId,
type: ChannelTypes.GUILD_FORUM,
name: 'forum',
});
expect(await repository.channelData.countGuildChannels(guildId)).toBe(before + 1);
await createBuilder(harness, owner.token)
.delete(`/channels/${forumId}`)
@@ -383,9 +388,14 @@ describe('thread permission bits across guild modes', () => {
.body({name: 'text', type: ChannelTypes.GUILD_TEXT, parent_id: category.id})
.execute();
const repository = new ChannelRepository();
const source = (await repository.findUnique(createChannelID(BigInt(text.id))))!;
const source = (await repository.channelData.findUnique(createChannelID(BigInt(text.id))))!;
const forumId = createChannelID(BigInt(text.id) + 1n);
await repository.upsert({...source.toRow(), channel_id: forumId, type: ChannelTypes.GUILD_FORUM, name: 'forum'});
await repository.channelData.upsert({
...source.toRow(),
channel_id: forumId,
type: ChannelTypes.GUILD_FORUM,
name: 'forum',
});
expect(await fetchOne<GuildThreadStateRow>(FETCH_MARKER, {guild_id: guildId})).not.toBeNull();
await setChannelThreadsConfig({enabled: false});
const dispatchSpy = vi.spyOn(NoopGatewayService.prototype, 'dispatchGuild');
@@ -398,12 +408,12 @@ describe('thread permission bits across guild modes', () => {
.patch(`/channels/${category.id}`)
.body({permission_overwrites: [{id: guild.id, type: 0, allow: '0', deny: Permissions.VIEW_CHANNEL.toString()}]})
.execute();
expect((await repository.findUnique(forumId))?.permissionOverwrites.size).toBe(1);
expect((await repository.channelData.findUnique(forumId))?.permissionOverwrites.size).toBe(1);
expect(updatedIds()).toContain(text.id);
expect(updatedIds()).not.toContain(forumId.toString());
dispatchSpy.mockClear();
await createBuilder(harness, owner.token).delete(`/channels/${category.id}`).expect(204).execute();
expect((await repository.findUnique(forumId))?.parentId).toBeNull();
expect((await repository.channelData.findUnique(forumId))?.parentId).toBeNull();
expect(updatedIds()).toContain(text.id);
expect(updatedIds()).not.toContain(forumId.toString());
} finally {
@@ -429,7 +439,7 @@ describe('thread permission bits across guild modes', () => {
const channelId = createChannelID(BigInt(channel.id));
const everyoneId = createRoleID(BigInt(guild.id));
const storedAllow = async () =>
(await new ChannelRepository().findUnique(channelId))?.permissionOverwrites.get(everyoneId)?.allow;
(await new ChannelRepository().channelData.findUnique(channelId))?.permissionOverwrites.get(everyoneId)?.allow;
await createBuilder(harness, owner.token)
.patch(`/channels/${channel.id}`)
.body({permission_overwrites: [{id: guild.id, type: 0, allow: Permissions.VIEW_CHANNEL.toString(), deny: '0'}]})
@@ -480,7 +490,7 @@ describe('thread permission bits across guild modes', () => {
.body([{id: child.id, parent_id: category.id, lock_permissions: true}])
.expect(204);
await (capable ? builder.header(FEATURES, CAPABLE) : builder).execute();
const stored = await new ChannelRepository().findUnique(createChannelID(BigInt(child.id)));
const stored = await new ChannelRepository().channelData.findUnique(createChannelID(BigInt(child.id)));
return stored?.permissionOverwrites.get(everyoneId)?.allow;
};
expect(await moveAndRead(true)).toBe(Permissions.SEND_MESSAGES | inThreads);