// SPDX-License-Identifier: AGPL-3.0-or-later export {type RecordedJob, RecordingWorkerService} from '@app/api/test/RecordingWorkerService'; import {createTestAccount, type TestAccount} from '@app/api/auth/tests/AuthTestUtils'; import {createChannelID, createGuildID, createMessageID} from '@app/api/BrandedTypes'; import {createApiContext} from '@app/api/CreateApiContext'; import {ChannelRepository} from '@app/api/channel/ChannelRepository'; import type {ChannelService} from '@app/api/channel/services/ChannelService'; import {publishedEditRateLimitIdentifier} from '@app/api/channel/services/message/CrosspostPropagation'; import {crosspostChannelRateLimitIdentifier} from '@app/api/channel/services/message/MessageCrosspostService'; import { acceptInvite, addMemberRole, createChannel, createChannelInvite, createGuild, createRole, } from '@app/api/channel/tests/ChannelTestUtils'; import type {GuildRow} from '@app/api/database/types/GuildTypes'; import {GuildRepository} from '@app/api/guild/repositories/GuildRepository'; import {DisabledLiveKitService} from '@app/api/infrastructure/DisabledLiveKitService'; import {InMemoryVoiceRoomStore} from '@app/api/infrastructure/InMemoryVoiceRoomStore'; import {ensureSessionStarted} from '@app/api/message/tests/MessageTestUtils'; import {createGuildStackServices, type GuildStackServices} from '@app/api/middleware/GuildStackServiceFactory'; import {getVoiceAvailabilityService} from '@app/api/middleware/ServiceRegistry'; import { getAssetDeletionQueue, getAttachmentUploadTraceRepository, getAvatarService, getChannelRepository, getEmbedService, getEntityAssetService, getFavoriteMemeRepository, getGuildAuditLogService, getGuildRepository, getInviteRepository, getLimitConfigService, getPurgeQueue, getRateLimitService, getReadStateService, getStorageService, getUserCacheService, getUserRepository, getVirusScanServiceInstance, getWebhookRepository, } from '@app/api/middleware/ServiceSingletons'; import type {Message} from '@app/api/models/Message'; import type {ApiTestHarness} from '@app/api/test/ApiTestHarness'; import {createBuilder} from '@app/api/test/TestRequestBuilder'; import { CROSSPOST_CHANNEL_RATE_LIMIT, PUBLISHED_MESSAGE_EDIT_RATE_LIMIT, } from '@fluxer/constants/src/AnnouncementConstants'; import {ChannelTypes, Permissions} from '@fluxer/constants/src/ChannelConstants'; import type {ChannelResponse} from '@fluxer/schema/src/domains/channel/ChannelSchemas'; import type {GuildResponse} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas'; import type {MessageResponse} from '@fluxer/schema/src/domains/message/MessageResponseSchemas'; export interface AnnouncementWorld { owner: TestAccount; author: TestAccount; mod: TestAccount; member: TestAccount; guild: GuildResponse; announcement: ChannelResponse; text: ChannelResponse; modRoleId: string; } export async function createAnnouncementWorld(harness: ApiTestHarness): Promise { const owner = await createTestAccount(harness); const author = await createTestAccount(harness); const mod = await createTestAccount(harness); const member = await createTestAccount(harness); const guild = await createGuild(harness, owner.token, 'Announcements Source'); const announcement = await createChannel(harness, owner.token, guild.id, 'news', ChannelTypes.GUILD_ANNOUNCEMENT); const text = await createChannel(harness, owner.token, guild.id, 'chat', ChannelTypes.GUILD_TEXT); const invite = await createChannelInvite(harness, owner.token, announcement.id); for (const account of [author, mod, member]) { await acceptInvite(harness, account.token, invite.code); await ensureSessionStarted(harness, account.token); } await ensureSessionStarted(harness, owner.token); const modRole = await createRole(harness, owner.token, guild.id, { name: 'Moderator', permissions: Permissions.MANAGE_MESSAGES.toString(), }); await addMemberRole(harness, owner.token, guild.id, mod.userId, modRole.id); return {owner, author, mod, member, guild, announcement, text, modRoleId: modRole.id}; } export function crosspostRequest(harness: ApiTestHarness, token: string, channelId: string, messageId: string) { return createBuilder(harness, token).post(`/channels/${channelId}/messages/${messageId}/crosspost`); } export async function publish( harness: ApiTestHarness, token: string, channelId: string, messageId: string, ): Promise { return crosspostRequest(harness, token, channelId, messageId).expect(200).execute(); } export function editMessage( harness: ApiTestHarness, token: string, channelId: string, messageId: string, body: Record, ) { return createBuilder(harness, token) .patch(`/channels/${channelId}/messages/${messageId}`) .body(body); } export async function readMessageRow(channelId: string, messageId: string): Promise { return new ChannelRepository().messages.getMessage( createChannelID(BigInt(channelId)), createMessageID(BigInt(messageId)), ); } export async function writeMessageRow(message: Message, patch: Partial>): Promise { await new ChannelRepository().messages.upsertMessage({...message.toRow(), ...patch}, message.toRow()); } export async function listCrosspostSources(channelId: string): Promise> { const ids = await new ChannelRepository().crossposts.listSourcesByChannel(createChannelID(BigInt(channelId)), { limit: 100, }); return ids.map((id) => id.toString()); } export async function patchGuildRow(guildId: string, patch: Partial): Promise { const repository = new GuildRepository(); const id = createGuildID(BigInt(guildId)); const existing = await repository.findUnique(id); if (!existing) { throw new Error(`guild ${guildId} not found`); } await repository.upsertPartial(id, patch, existing.toRow()); } export async function addGuildFeature(guildId: string, feature: string): Promise { const existing = await new GuildRepository().findUnique(createGuildID(BigInt(guildId))); const features = new Set(existing?.features ?? []); features.add(feature); await patchGuildRow(guildId, {features}); } export async function channelBudgetRemaining(channelId: string): Promise { const result = await getRateLimitService().peekLimit({ identifier: crosspostChannelRateLimitIdentifier(createChannelID(BigInt(channelId))), ...CROSSPOST_CHANNEL_RATE_LIMIT, }); return result.remaining; } export async function publishedEditBudgetRemaining(messageId: string): Promise { const result = await getRateLimitService().peekLimit({ identifier: publishedEditRateLimitIdentifier(createMessageID(BigInt(messageId))), ...PUBLISHED_MESSAGE_EDIT_RATE_LIMIT, }); return result.remaining; } export interface StallGate { reached: Promise; release: () => void; } export function createStallGate(): StallGate & {hit: () => Promise} { let markReached: () => void = () => {}; let release: () => void = () => {}; const reached = new Promise((resolve) => { markReached = resolve; }); const released = new Promise((resolve) => { release = resolve; }); return { reached, release, hit: async () => { markReached(); await released; }, }; } export function createTestGuildStack(): GuildStackServices { return createGuildStackServices({ apiContext: createApiContext(), channelRepository: getChannelRepository(), userRepository: getUserRepository(), guildRepository: getGuildRepository(), inviteRepository: getInviteRepository(), webhookRepository: getWebhookRepository(), favoriteMemeRepository: getFavoriteMemeRepository(), avatarService: getAvatarService(), entityAssetService: getEntityAssetService(), assetDeletionQueue: getAssetDeletionQueue(), userCacheService: getUserCacheService(), limitConfigService: getLimitConfigService(), embedService: getEmbedService(), readStateService: getReadStateService(), storageService: getStorageService(), attachmentUploadTraceRepository: getAttachmentUploadTraceRepository(), virusScanService: getVirusScanServiceInstance(), purgeQueue: getPurgeQueue(), guildAuditLogService: getGuildAuditLogService(), voiceRoomStore: new InMemoryVoiceRoomStore(), liveKitService: new DisabledLiveKitService(), voiceAvailabilityService: getVoiceAvailabilityService(), }); } export function createTestChannelService(): ChannelService { return createTestGuildStack().channelService; } export {sendChannelMessage as postMessage} from '@app/api/channel/tests/ChannelTestUtils';