refactor: squash branch changes
This commit is contained in:
@@ -140,6 +140,12 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
|
||||
|
||||
kv: {
|
||||
url: master.internal.kv,
|
||||
mode: ((master.internal as {kv_mode?: string}).kv_mode ?? 'standalone') as 'standalone' | 'cluster',
|
||||
clusterNodes:
|
||||
(master.internal as {kv_cluster_nodes?: Array<{host: string; port: number}>}).kv_cluster_nodes ?? [],
|
||||
clusterNatMap:
|
||||
(master.internal as {kv_cluster_nat_map?: Record<string, {host: string; port: number}>}).kv_cluster_nat_map ??
|
||||
{},
|
||||
},
|
||||
|
||||
nats: {
|
||||
@@ -240,8 +246,13 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
|
||||
defaultRegion: master.integrations.voice.default_region,
|
||||
},
|
||||
search: {
|
||||
engine: ((master.integrations.search as {engine?: string}).engine ?? 'meilisearch') as
|
||||
| 'meilisearch'
|
||||
| 'elasticsearch',
|
||||
url: master.integrations.search.url,
|
||||
apiKey: master.integrations.search.api_key,
|
||||
username: (master.integrations.search as {username?: string}).username ?? '',
|
||||
password: (master.integrations.search as {password?: string}).password ?? '',
|
||||
},
|
||||
stripe: {
|
||||
enabled: master.integrations.stripe.enabled,
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
|
||||
import {Config} from '@fluxer/api/src/Config';
|
||||
import {ElasticsearchSearchProvider} from '@fluxer/api/src/infrastructure/ElasticsearchSearchProvider';
|
||||
import {MeilisearchSearchProvider} from '@fluxer/api/src/infrastructure/MeilisearchSearchProvider';
|
||||
import {NullSearchProvider} from '@fluxer/api/src/infrastructure/NullSearchProvider';
|
||||
import {Logger} from '@fluxer/api/src/Logger';
|
||||
@@ -34,6 +35,29 @@ import {DEFAULT_SEARCH_CLIENT_TIMEOUT_MS} from '@fluxer/constants/src/Timeouts';
|
||||
let searchProvider: ISearchProvider | null = null;
|
||||
|
||||
export function createSearchProvider(): ISearchProvider {
|
||||
const engine = Config.search.engine ?? 'meilisearch';
|
||||
|
||||
if (engine === 'elasticsearch') {
|
||||
if (!Config.search.apiKey && !Config.search.username) {
|
||||
Logger.warn('Elasticsearch credentials are not configured; search will be unavailable');
|
||||
return new NullSearchProvider();
|
||||
}
|
||||
|
||||
Logger.info({url: Config.search.url}, 'Using Elasticsearch for search');
|
||||
return new ElasticsearchSearchProvider({
|
||||
config: {
|
||||
node: Config.search.url,
|
||||
auth: Config.search.apiKey
|
||||
? {apiKey: Config.search.apiKey}
|
||||
: Config.search.username
|
||||
? {username: Config.search.username, password: Config.search.password}
|
||||
: undefined,
|
||||
requestTimeoutMs: DEFAULT_SEARCH_CLIENT_TIMEOUT_MS,
|
||||
},
|
||||
logger: Logger,
|
||||
});
|
||||
}
|
||||
|
||||
if (!Config.search.apiKey) {
|
||||
Logger.warn('Search API key is not configured; search will be unavailable');
|
||||
return new NullSearchProvider();
|
||||
|
||||
@@ -27,7 +27,7 @@ import {ScheduledMessage} from '@fluxer/api/src/models/ScheduledMessage';
|
||||
import type {User} from '@fluxer/api/src/models/User';
|
||||
import {withBusinessSpan} from '@fluxer/api/src/telemetry/BusinessSpans';
|
||||
import type {ScheduledMessageRepository} from '@fluxer/api/src/user/repositories/ScheduledMessageRepository';
|
||||
import {ManagedTraits} from '@fluxer/constants/src/ManagedTraits';
|
||||
import {UserFlags} from '@fluxer/constants/src/UserConstants';
|
||||
import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes';
|
||||
import {FeatureTemporarilyDisabledError} from '@fluxer/errors/src/domains/core/FeatureTemporarilyDisabledError';
|
||||
import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError';
|
||||
@@ -134,7 +134,7 @@ export class ScheduledMessageService {
|
||||
private async upsertScheduledMessage(params: UpdateScheduleParams): Promise<ScheduledMessage> {
|
||||
const {user, channelId, data, scheduledLocalAt, timezone} = params;
|
||||
|
||||
if (!user.traits.has(ManagedTraits.MESSAGE_SCHEDULING)) {
|
||||
if ((user.flags & UserFlags.STAFF) === 0n) {
|
||||
throw new FeatureTemporarilyDisabledError();
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,7 @@
|
||||
|
||||
import {ensureSessionStarted} from '@fluxer/api/src/message/tests/MessageTestUtils';
|
||||
import type {ApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
|
||||
import {createBuilder} from '@fluxer/api/src/test/TestRequestBuilder';
|
||||
import {ManagedTraits} from '@fluxer/constants/src/ManagedTraits';
|
||||
import {createBuilder, createBuilderWithoutAuth} from '@fluxer/api/src/test/TestRequestBuilder';
|
||||
import type {MessageResponse} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
|
||||
import type {ScheduledMessageResponseSchema} from '@fluxer/schema/src/domains/message/ScheduledMessageSchemas';
|
||||
import type {z} from 'zod';
|
||||
@@ -45,14 +44,8 @@ export async function createGuildChannel(
|
||||
return json as {id: string};
|
||||
}
|
||||
|
||||
export async function enableMessageSchedulingForGuild(harness: ApiTestHarness, guildId: string): Promise<void> {
|
||||
await createBuilder<void>(harness, '')
|
||||
.post(`/test/guilds/${guildId}/features`)
|
||||
.body({
|
||||
add_features: [ManagedTraits.MESSAGE_SCHEDULING],
|
||||
})
|
||||
.expect(200)
|
||||
.execute();
|
||||
export async function grantStaffAccess(harness: ApiTestHarness, userId: string): Promise<void> {
|
||||
await createBuilderWithoutAuth(harness).patch(`/test/users/${userId}/flags`).body({flags: 1}).execute();
|
||||
}
|
||||
|
||||
export async function scheduleMessage(
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import {createTestAccount} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {
|
||||
createGuildChannel,
|
||||
enableMessageSchedulingForGuild,
|
||||
grantStaffAccess,
|
||||
scheduleMessage,
|
||||
} from '@fluxer/api/src/channel/tests/ScheduledMessageTestUtils';
|
||||
import {createGuild} from '@fluxer/api/src/guild/tests/GuildTestUtils';
|
||||
@@ -29,7 +29,7 @@ import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/Ap
|
||||
import {createBuilder} from '@fluxer/api/src/test/TestRequestBuilder';
|
||||
import {beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
|
||||
describe('Scheduled message trait gating', () => {
|
||||
describe('Scheduled message staff gating', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -40,7 +40,7 @@ describe('Scheduled message trait gating', () => {
|
||||
await harness.reset();
|
||||
});
|
||||
|
||||
it('rejects scheduling message before trait enabled', async () => {
|
||||
it('rejects scheduling message before staff flag granted', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'scheduled-flag');
|
||||
const channel = await createGuildChannel(harness, owner.token, guild.id, 'scheduled-channel');
|
||||
@@ -56,7 +56,7 @@ describe('Scheduled message trait gating', () => {
|
||||
.expect(403)
|
||||
.execute();
|
||||
|
||||
await enableMessageSchedulingForGuild(harness, guild.id);
|
||||
await grantStaffAccess(harness, owner.userId);
|
||||
|
||||
const scheduled = await scheduleMessage(harness, channel.id, owner.token, 'enabled now');
|
||||
expect(scheduled.id).toBeDefined();
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import {createTestAccount} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {
|
||||
createGuildChannel,
|
||||
enableMessageSchedulingForGuild,
|
||||
grantStaffAccess,
|
||||
scheduleMessage,
|
||||
} from '@fluxer/api/src/channel/tests/ScheduledMessageTestUtils';
|
||||
import {createGuild} from '@fluxer/api/src/guild/tests/GuildTestUtils';
|
||||
@@ -49,7 +49,7 @@ describe('Scheduled message validation', () => {
|
||||
it('rejects scheduling message with past time', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'sched-validation-past');
|
||||
await enableMessageSchedulingForGuild(harness, guild.id);
|
||||
await grantStaffAccess(harness, owner.userId);
|
||||
const channel = await createGuildChannel(harness, owner.token, guild.id, 'test');
|
||||
await ensureSessionStarted(harness, owner.token);
|
||||
|
||||
@@ -76,7 +76,7 @@ describe('Scheduled message validation', () => {
|
||||
it('rejects scheduling message exceeding 30 days', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'sched-validation-30day');
|
||||
await enableMessageSchedulingForGuild(harness, guild.id);
|
||||
await grantStaffAccess(harness, owner.userId);
|
||||
const channel = await createGuildChannel(harness, owner.token, guild.id, 'test');
|
||||
await ensureSessionStarted(harness, owner.token);
|
||||
|
||||
@@ -103,7 +103,7 @@ describe('Scheduled message validation', () => {
|
||||
it('rejects scheduling message with invalid timezone', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'sched-validation-tz');
|
||||
await enableMessageSchedulingForGuild(harness, guild.id);
|
||||
await grantStaffAccess(harness, owner.userId);
|
||||
const channel = await createGuildChannel(harness, owner.token, guild.id, 'test');
|
||||
await ensureSessionStarted(harness, owner.token);
|
||||
|
||||
@@ -130,7 +130,7 @@ describe('Scheduled message validation', () => {
|
||||
it('accepts scheduling message at 30 day boundary', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'sched-validation-boundary');
|
||||
await enableMessageSchedulingForGuild(harness, guild.id);
|
||||
await grantStaffAccess(harness, owner.userId);
|
||||
const channel = await createGuildChannel(harness, owner.token, guild.id, 'test');
|
||||
|
||||
const futureTime = new Date(Date.now() + 29 * 24 * 60 * 60 * 1000 + 23 * 60 * 60 * 1000).toISOString();
|
||||
|
||||
@@ -21,9 +21,9 @@ import {createTestAccount} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {
|
||||
createChannelInvite,
|
||||
createGuildChannel,
|
||||
enableMessageSchedulingForGuild,
|
||||
getChannelMessages,
|
||||
getScheduledMessage,
|
||||
grantStaffAccess,
|
||||
joinGuild,
|
||||
messageFromAuthorContains,
|
||||
removeGuildMember,
|
||||
@@ -49,7 +49,7 @@ describe('Scheduled message worker lifecycle', () => {
|
||||
it('delivers scheduled message when permissions remain', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'scheduled-messages');
|
||||
await enableMessageSchedulingForGuild(harness, guild.id);
|
||||
await grantStaffAccess(harness, owner.userId);
|
||||
const channel = await createGuildChannel(harness, owner.token, guild.id, 'scheduled');
|
||||
|
||||
const content = 'scheduled message goes through';
|
||||
@@ -67,7 +67,7 @@ describe('Scheduled message worker lifecycle', () => {
|
||||
it('reschedules pending message before worker execution', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'scheduled-messages');
|
||||
await enableMessageSchedulingForGuild(harness, guild.id);
|
||||
await grantStaffAccess(harness, owner.userId);
|
||||
const channel = await createGuildChannel(harness, owner.token, guild.id, 'scheduled');
|
||||
|
||||
const content = 'scheduled message initial content';
|
||||
@@ -106,7 +106,7 @@ describe('Scheduled message worker lifecycle', () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const member = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'scheduled-messages');
|
||||
await enableMessageSchedulingForGuild(harness, guild.id);
|
||||
await grantStaffAccess(harness, member.userId);
|
||||
const channel = await createGuildChannel(harness, owner.token, guild.id, 'scheduled');
|
||||
|
||||
const invite = await createChannelInvite(harness, owner.token, channel.id);
|
||||
|
||||
@@ -21,8 +21,8 @@ import {createTestAccount} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {
|
||||
createChannelInvite,
|
||||
createGuildChannel,
|
||||
enableMessageSchedulingForGuild,
|
||||
getScheduledMessages,
|
||||
grantStaffAccess,
|
||||
joinGuild,
|
||||
removeGuildMember,
|
||||
scheduleMessage,
|
||||
@@ -47,7 +47,7 @@ describe('Scheduled messages list invalid entry', () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const member = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'scheduled-invalid');
|
||||
await enableMessageSchedulingForGuild(harness, guild.id);
|
||||
await grantStaffAccess(harness, member.userId);
|
||||
const channel = await createGuildChannel(harness, owner.token, guild.id, 'scheduled-invalid');
|
||||
|
||||
const invite = await createChannelInvite(harness, owner.token, channel.id);
|
||||
|
||||
@@ -21,8 +21,8 @@ import {createTestAccount} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {
|
||||
cancelScheduledMessage,
|
||||
createGuildChannel,
|
||||
enableMessageSchedulingForGuild,
|
||||
getScheduledMessages,
|
||||
grantStaffAccess,
|
||||
scheduleMessage,
|
||||
} from '@fluxer/api/src/channel/tests/ScheduledMessageTestUtils';
|
||||
import {createGuild} from '@fluxer/api/src/guild/tests/GuildTestUtils';
|
||||
@@ -43,7 +43,7 @@ describe('Scheduled messages list lifecycle', () => {
|
||||
it('lists scheduled messages and removes after cancel', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'scheduled-list');
|
||||
await enableMessageSchedulingForGuild(harness, guild.id);
|
||||
await grantStaffAccess(harness, owner.userId);
|
||||
const channel = await createGuildChannel(harness, owner.token, guild.id, 'scheduled-list');
|
||||
|
||||
const content = 'list scheduled';
|
||||
|
||||
@@ -36,6 +36,9 @@ export interface APIConfig {
|
||||
|
||||
kv: {
|
||||
url: string;
|
||||
mode: 'standalone' | 'cluster';
|
||||
clusterNodes: Array<{host: string; port: number}>;
|
||||
clusterNatMap: Record<string, {host: string; port: number}>;
|
||||
};
|
||||
|
||||
nats: {
|
||||
@@ -149,8 +152,11 @@ export interface APIConfig {
|
||||
};
|
||||
|
||||
search: {
|
||||
engine: 'meilisearch' | 'elasticsearch';
|
||||
url: string;
|
||||
apiKey: string;
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
stripe: {
|
||||
|
||||
@@ -36,7 +36,6 @@ import type {RequestCache} from '@fluxer/api/src/middleware/RequestCacheMiddlewa
|
||||
import type {Guild} from '@fluxer/api/src/models/Guild';
|
||||
import type {GuildMember} from '@fluxer/api/src/models/GuildMember';
|
||||
import type {User} from '@fluxer/api/src/models/User';
|
||||
import type {GuildManagedTraitService} from '@fluxer/api/src/traits/GuildManagedTraitService';
|
||||
import type {IUserRepository} from '@fluxer/api/src/user/IUserRepository';
|
||||
import type {IWebhookRepository} from '@fluxer/api/src/webhook/IWebhookRepository';
|
||||
import type {GuildCreateRequest, GuildUpdateRequest} from '@fluxer/schema/src/domains/guild/GuildRequestSchemas';
|
||||
@@ -64,7 +63,6 @@ export class GuildDataService {
|
||||
private readonly webhookRepository: IWebhookRepository,
|
||||
private readonly guildAuditLogService: GuildAuditLogService,
|
||||
private readonly limitConfigService: LimitConfigService,
|
||||
private readonly guildManagedTraitService?: GuildManagedTraitService,
|
||||
) {
|
||||
this.helpers = new GuildDataHelpers(this.gatewayService, this.guildAuditLogService);
|
||||
|
||||
@@ -81,7 +79,6 @@ export class GuildDataService {
|
||||
this.helpers,
|
||||
this.limitConfigService,
|
||||
new GuildDiscoveryRepository(),
|
||||
this.guildManagedTraitService,
|
||||
);
|
||||
|
||||
this.vanityService = new GuildVanityService(this.guildRepository, this.inviteRepository, this.helpers);
|
||||
|
||||
@@ -34,7 +34,6 @@ import type {UserCacheService} from '@fluxer/api/src/infrastructure/UserCacheSer
|
||||
import type {LimitConfigService} from '@fluxer/api/src/limits/LimitConfigService';
|
||||
import type {RequestCache} from '@fluxer/api/src/middleware/RequestCacheMiddleware';
|
||||
import type {GuildMember} from '@fluxer/api/src/models/GuildMember';
|
||||
import type {GuildManagedTraitService} from '@fluxer/api/src/traits/GuildManagedTraitService';
|
||||
import type {IUserRepository} from '@fluxer/api/src/user/IUserRepository';
|
||||
import {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType';
|
||||
import {UnknownGuildMemberError} from '@fluxer/errors/src/domains/guild/UnknownGuildMemberError';
|
||||
@@ -62,7 +61,6 @@ export class GuildMemberService {
|
||||
rateLimitService: IRateLimitService,
|
||||
private readonly guildAuditLogService: GuildAuditLogService,
|
||||
limitConfigService: LimitConfigService,
|
||||
guildManagedTraitService?: GuildManagedTraitService,
|
||||
) {
|
||||
this.userRepository = userRepository;
|
||||
this.authService = new GuildMemberAuthService(gatewayService);
|
||||
@@ -82,7 +80,6 @@ export class GuildMemberService {
|
||||
this.validationService,
|
||||
this.guildAuditLogService,
|
||||
limitConfigService,
|
||||
guildManagedTraitService,
|
||||
this.searchIndexService,
|
||||
);
|
||||
this.roleService = new GuildMemberRoleService(
|
||||
|
||||
@@ -47,7 +47,6 @@ import type {GuildAuditLog} from '@fluxer/api/src/models/GuildAuditLog';
|
||||
import type {GuildMember} from '@fluxer/api/src/models/GuildMember';
|
||||
import type {User} from '@fluxer/api/src/models/User';
|
||||
import type {Webhook} from '@fluxer/api/src/models/Webhook';
|
||||
import type {GuildManagedTraitService} from '@fluxer/api/src/traits/GuildManagedTraitService';
|
||||
import type {IUserRepository} from '@fluxer/api/src/user/IUserRepository';
|
||||
import {getCachedUserPartialResponses} from '@fluxer/api/src/user/UserCacheHelpers';
|
||||
import type {IWebhookRepository} from '@fluxer/api/src/webhook/IWebhookRepository';
|
||||
@@ -169,7 +168,6 @@ export class GuildService {
|
||||
webhookRepository: IWebhookRepository,
|
||||
guildAuditLogService: GuildAuditLogService,
|
||||
limitConfigService: LimitConfigService,
|
||||
guildManagedTraitService?: GuildManagedTraitService,
|
||||
) {
|
||||
this.gatewayService = gatewayService;
|
||||
this.guildRepository = guildRepository;
|
||||
@@ -189,7 +187,6 @@ export class GuildService {
|
||||
webhookRepository,
|
||||
guildAuditLogService,
|
||||
limitConfigService,
|
||||
guildManagedTraitService,
|
||||
);
|
||||
this.members = new GuildMemberService(
|
||||
guildRepository,
|
||||
@@ -201,7 +198,6 @@ export class GuildService {
|
||||
rateLimitService,
|
||||
guildAuditLogService,
|
||||
limitConfigService,
|
||||
guildManagedTraitService,
|
||||
);
|
||||
this.roles = new GuildRoleService(
|
||||
guildRepository,
|
||||
|
||||
@@ -50,11 +50,9 @@ import {
|
||||
} from '@fluxer/api/src/Tables';
|
||||
import {withBusinessSpan} from '@fluxer/api/src/telemetry/BusinessSpans';
|
||||
import {withSpan} from '@fluxer/api/src/telemetry/Tracing';
|
||||
import type {GuildManagedTraitService} from '@fluxer/api/src/traits/GuildManagedTraitService';
|
||||
import type {IUserRepository} from '@fluxer/api/src/user/IUserRepository';
|
||||
import {mapUserSettingsToResponse} from '@fluxer/api/src/user/UserMappers';
|
||||
import {removeGuildFromUserFolders} from '@fluxer/api/src/user/utils/GuildFolderUtils';
|
||||
import {areFeatureSetsEqual} from '@fluxer/api/src/utils/featureUtils';
|
||||
import type {IWebhookRepository} from '@fluxer/api/src/webhook/IWebhookRepository';
|
||||
import {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType';
|
||||
import {ChannelTypes, DEFAULT_PERMISSIONS, Permissions} from '@fluxer/constants/src/ChannelConstants';
|
||||
@@ -106,7 +104,6 @@ export class GuildOperationsService {
|
||||
private readonly helpers: GuildDataHelpers,
|
||||
private readonly limitConfigService: LimitConfigService,
|
||||
private readonly discoveryRepository: IGuildDiscoveryRepository,
|
||||
private readonly guildManagedTraitService?: GuildManagedTraitService,
|
||||
) {}
|
||||
|
||||
async getGuild({userId, guildId}: {userId: UserID; guildId: GuildID}): Promise<GuildResponse> {
|
||||
@@ -663,9 +660,7 @@ export class GuildOperationsService {
|
||||
sanitizedSystemChannelFlags = data.system_channel_flags & SUPPORTED_SYSTEM_CHANNEL_FLAGS;
|
||||
}
|
||||
|
||||
const previousFeatures = new Set(currentGuild.features);
|
||||
let updatedFeatures = currentGuild.features;
|
||||
let featuresChanged = false;
|
||||
if (data.features !== undefined) {
|
||||
const newFeatures = new Set(currentGuild.features);
|
||||
|
||||
@@ -692,7 +687,6 @@ export class GuildOperationsService {
|
||||
}
|
||||
|
||||
updatedFeatures = newFeatures;
|
||||
featuresChanged = !areFeatureSetsEqual(previousFeatures, updatedFeatures);
|
||||
}
|
||||
|
||||
let messageHistoryCutoff: Date | null | undefined;
|
||||
@@ -764,14 +758,6 @@ export class GuildOperationsService {
|
||||
Logger.error({error, guildId}, 'Failed to commit asset changes after successful guild update');
|
||||
}
|
||||
|
||||
if (featuresChanged && this.guildManagedTraitService) {
|
||||
await this.guildManagedTraitService.reconcileTraitsForGuildFeatureChange({
|
||||
guildId,
|
||||
previousFeatures,
|
||||
newFeatures: updatedFeatures,
|
||||
});
|
||||
}
|
||||
|
||||
await this.helpers.dispatchGuildUpdate(updatedGuild);
|
||||
|
||||
const guildSearchService = getGuildSearchService();
|
||||
|
||||
@@ -43,7 +43,6 @@ import type {GuildMember} from '@fluxer/api/src/models/GuildMember';
|
||||
import type {User} from '@fluxer/api/src/models/User';
|
||||
import type {UserGuildSettings} from '@fluxer/api/src/models/UserGuildSettings';
|
||||
import type {UserSettings} from '@fluxer/api/src/models/UserSettings';
|
||||
import type {GuildManagedTraitService} from '@fluxer/api/src/traits/GuildManagedTraitService';
|
||||
import type {IUserRepository} from '@fluxer/api/src/user/IUserRepository';
|
||||
import {mapUserGuildSettingsToResponse, mapUserSettingsToResponse} from '@fluxer/api/src/user/UserMappers';
|
||||
import {removeGuildFromUserFolders} from '@fluxer/api/src/user/utils/GuildFolderUtils';
|
||||
@@ -117,7 +116,6 @@ export class GuildMemberOperationsService {
|
||||
private readonly validationService: GuildMemberValidationService,
|
||||
private readonly guildAuditLogService: GuildAuditLogService,
|
||||
private readonly limitConfigService: LimitConfigService,
|
||||
private readonly guildManagedTraitService?: GuildManagedTraitService,
|
||||
private readonly searchIndexService?: GuildMemberSearchIndexService,
|
||||
) {}
|
||||
|
||||
@@ -373,10 +371,6 @@ export class GuildMemberOperationsService {
|
||||
});
|
||||
}
|
||||
|
||||
if (guild && this.guildManagedTraitService) {
|
||||
await this.guildManagedTraitService.reconcileTraitsForGuildLeave({guild, userId});
|
||||
}
|
||||
|
||||
await this.gatewayService.leaveGuild({userId: targetId, guildId});
|
||||
succeeded = true;
|
||||
} finally {
|
||||
@@ -502,13 +496,6 @@ export class GuildMemberOperationsService {
|
||||
void this.searchIndexService.indexMember(guildMember, user);
|
||||
}
|
||||
|
||||
if (this.guildManagedTraitService) {
|
||||
await this.guildManagedTraitService.ensureTraitsForGuildJoin({
|
||||
guild,
|
||||
user,
|
||||
});
|
||||
}
|
||||
|
||||
if (sendJoinMessage && !(guild.systemChannelFlags & SystemChannelFlags.SUPPRESS_JOIN_NOTIFICATIONS)) {
|
||||
await this.channelService.sendJoinSystemMessage({guildId, userId, requestCache});
|
||||
}
|
||||
|
||||
@@ -440,6 +440,7 @@ describe('Guild Channel Positions', () => {
|
||||
expect(frontDoorIndex).toBeGreaterThan(milsimsIndex);
|
||||
expect(frontDoorIndex).toBeLessThan(coopGamesIndex);
|
||||
});
|
||||
|
||||
test('should reject text channels being positioned below voice channels via preceding_sibling_id', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Test Guild');
|
||||
|
||||
124
packages/api/src/infrastructure/ElasticsearchSearchProvider.tsx
Normal file
124
packages/api/src/infrastructure/ElasticsearchSearchProvider.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {ILogger} from '@fluxer/api/src/ILogger';
|
||||
import {ElasticsearchAuditLogSearchService} from '@fluxer/api/src/search/elasticsearch/ElasticsearchAuditLogSearchService';
|
||||
import {ElasticsearchGuildMemberSearchService} from '@fluxer/api/src/search/elasticsearch/ElasticsearchGuildMemberSearchService';
|
||||
import {ElasticsearchGuildSearchService} from '@fluxer/api/src/search/elasticsearch/ElasticsearchGuildSearchService';
|
||||
import {ElasticsearchMessageSearchService} from '@fluxer/api/src/search/elasticsearch/ElasticsearchMessageSearchService';
|
||||
import {ElasticsearchReportSearchService} from '@fluxer/api/src/search/elasticsearch/ElasticsearchReportSearchService';
|
||||
import {ElasticsearchUserSearchService} from '@fluxer/api/src/search/elasticsearch/ElasticsearchUserSearchService';
|
||||
import type {IAuditLogSearchService} from '@fluxer/api/src/search/IAuditLogSearchService';
|
||||
import type {IGuildMemberSearchService} from '@fluxer/api/src/search/IGuildMemberSearchService';
|
||||
import type {IGuildSearchService} from '@fluxer/api/src/search/IGuildSearchService';
|
||||
import type {IMessageSearchService} from '@fluxer/api/src/search/IMessageSearchService';
|
||||
import type {IReportSearchService} from '@fluxer/api/src/search/IReportSearchService';
|
||||
import type {ISearchProvider} from '@fluxer/api/src/search/ISearchProvider';
|
||||
import type {IUserSearchService} from '@fluxer/api/src/search/IUserSearchService';
|
||||
import {
|
||||
createElasticsearchClient,
|
||||
type ElasticsearchClientConfig,
|
||||
} from '@fluxer/elasticsearch_search/src/ElasticsearchClient';
|
||||
|
||||
export interface ElasticsearchSearchProviderOptions {
|
||||
config: ElasticsearchClientConfig;
|
||||
logger: ILogger;
|
||||
}
|
||||
|
||||
export class ElasticsearchSearchProvider implements ISearchProvider {
|
||||
private readonly logger: ILogger;
|
||||
private readonly config: ElasticsearchClientConfig;
|
||||
|
||||
private messageService: ElasticsearchMessageSearchService | null = null;
|
||||
private guildService: ElasticsearchGuildSearchService | null = null;
|
||||
private userService: ElasticsearchUserSearchService | null = null;
|
||||
private reportService: ElasticsearchReportSearchService | null = null;
|
||||
private auditLogService: ElasticsearchAuditLogSearchService | null = null;
|
||||
private guildMemberService: ElasticsearchGuildMemberSearchService | null = null;
|
||||
|
||||
constructor(options: ElasticsearchSearchProviderOptions) {
|
||||
this.logger = options.logger;
|
||||
this.config = options.config;
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
const client = createElasticsearchClient(this.config);
|
||||
|
||||
this.messageService = new ElasticsearchMessageSearchService({client});
|
||||
this.guildService = new ElasticsearchGuildSearchService({client});
|
||||
this.userService = new ElasticsearchUserSearchService({client});
|
||||
this.reportService = new ElasticsearchReportSearchService({client});
|
||||
this.auditLogService = new ElasticsearchAuditLogSearchService({client});
|
||||
this.guildMemberService = new ElasticsearchGuildMemberSearchService({client});
|
||||
|
||||
await Promise.all([
|
||||
this.messageService.initialize(),
|
||||
this.guildService.initialize(),
|
||||
this.userService.initialize(),
|
||||
this.reportService.initialize(),
|
||||
this.auditLogService.initialize(),
|
||||
this.guildMemberService.initialize(),
|
||||
]);
|
||||
|
||||
this.logger.info({node: this.config.node}, 'ElasticsearchSearchProvider initialised');
|
||||
}
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
const services = [
|
||||
this.messageService,
|
||||
this.guildService,
|
||||
this.userService,
|
||||
this.reportService,
|
||||
this.auditLogService,
|
||||
this.guildMemberService,
|
||||
];
|
||||
await Promise.all(services.filter((s) => s != null).map((s) => s.shutdown()));
|
||||
|
||||
this.messageService = null;
|
||||
this.guildService = null;
|
||||
this.userService = null;
|
||||
this.reportService = null;
|
||||
this.auditLogService = null;
|
||||
this.guildMemberService = null;
|
||||
}
|
||||
|
||||
getMessageSearchService(): IMessageSearchService | null {
|
||||
return this.messageService;
|
||||
}
|
||||
|
||||
getGuildSearchService(): IGuildSearchService | null {
|
||||
return this.guildService;
|
||||
}
|
||||
|
||||
getUserSearchService(): IUserSearchService | null {
|
||||
return this.userService;
|
||||
}
|
||||
|
||||
getReportSearchService(): IReportSearchService | null {
|
||||
return this.reportService;
|
||||
}
|
||||
|
||||
getAuditLogSearchService(): IAuditLogSearchService | null {
|
||||
return this.auditLogService;
|
||||
}
|
||||
|
||||
getGuildMemberSearchService(): IGuildMemberSearchService | null {
|
||||
return this.guildMemberService;
|
||||
}
|
||||
}
|
||||
@@ -135,7 +135,6 @@ import {SearchService} from '@fluxer/api/src/search/SearchService';
|
||||
import {StripeService} from '@fluxer/api/src/stripe/StripeService';
|
||||
import {TenorService} from '@fluxer/api/src/tenor/TenorService';
|
||||
import {ThemeService} from '@fluxer/api/src/theme/ThemeService';
|
||||
import {GuildManagedTraitService} from '@fluxer/api/src/traits/GuildManagedTraitService';
|
||||
import type {HonoEnv} from '@fluxer/api/src/types/HonoEnv';
|
||||
import {EmailChangeRepository} from '@fluxer/api/src/user/repositories/auth/EmailChangeRepository';
|
||||
import {PasswordChangeRepository} from '@fluxer/api/src/user/repositories/auth/PasswordChangeRepository';
|
||||
@@ -375,12 +374,6 @@ export const ServiceMiddleware = createMiddleware<HonoEnv>(async (ctx, next) =>
|
||||
const themeService = new ThemeService(storageService);
|
||||
const csamEvidenceRetentionService = new CsamEvidenceRetentionService(storageService);
|
||||
const gatewayService = getGatewayService();
|
||||
const guildManagedTraitService = new GuildManagedTraitService({
|
||||
userRepository,
|
||||
guildRepository,
|
||||
gatewayService,
|
||||
userCacheService,
|
||||
});
|
||||
const alertService = getAlertService();
|
||||
const workerService = getWorkerService();
|
||||
const botMfaMirrorService = new BotMfaMirrorService(applicationRepository, userRepository, gatewayService);
|
||||
@@ -511,7 +504,6 @@ export const ServiceMiddleware = createMiddleware<HonoEnv>(async (ctx, next) =>
|
||||
webhookRepository,
|
||||
guildAuditLogService,
|
||||
limitConfigService,
|
||||
guildManagedTraitService,
|
||||
);
|
||||
|
||||
const discoveryRepository = new GuildDiscoveryRepository();
|
||||
@@ -963,7 +955,6 @@ export const ServiceMiddleware = createMiddleware<HonoEnv>(async (ctx, next) =>
|
||||
ctx.set('csamEvidenceRetentionService', csamEvidenceRetentionService);
|
||||
ctx.set('instanceConfigRepository', instanceConfigRepository);
|
||||
ctx.set('limitConfigService', limitConfigService);
|
||||
ctx.set('guildManagedTraitService', guildManagedTraitService);
|
||||
ctx.set('errorI18nService', errorI18nService);
|
||||
|
||||
const ncmecReporter = new NcmecReporter({config: createNcmecApiConfig(), fetch});
|
||||
|
||||
@@ -54,6 +54,9 @@ export function getKVClient(): IKVProvider {
|
||||
if (!_kvClient) {
|
||||
_kvClient = new KVClient({
|
||||
url: Config.kv.url,
|
||||
mode: Config.kv.mode,
|
||||
clusterNodes: Config.kv.clusterNodes,
|
||||
clusterNatMap: Config.kv.clusterNatMap,
|
||||
});
|
||||
}
|
||||
return _kvClient;
|
||||
|
||||
@@ -51,7 +51,7 @@ import {
|
||||
MAX_INSTALLED_PACKS_NON_PREMIUM,
|
||||
MAX_PACK_EXPRESSIONS,
|
||||
} from '@fluxer/constants/src/LimitConstants';
|
||||
import {ManagedTraits} from '@fluxer/constants/src/ManagedTraits';
|
||||
import {UserFlags} from '@fluxer/constants/src/UserConstants';
|
||||
import {FeatureAccessError} from '@fluxer/errors/src/domains/core/FeatureAccessError';
|
||||
import {FeatureTemporarilyDisabledError} from '@fluxer/errors/src/domains/core/FeatureTemporarilyDisabledError';
|
||||
import {UnknownGuildEmojiError} from '@fluxer/errors/src/domains/guild/UnknownGuildEmojiError';
|
||||
@@ -83,7 +83,7 @@ export class PackService {
|
||||
|
||||
private async requireExpressionPackAccess(userId: UserID): Promise<void> {
|
||||
const user = await this.userRepository.findUnique(userId);
|
||||
if (!user || !user.traits.has(ManagedTraits.EXPRESSION_PACKS)) {
|
||||
if (!user || (user.flags & UserFlags.STAFF) === 0n) {
|
||||
throw new FeatureTemporarilyDisabledError();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,9 @@
|
||||
|
||||
import {createTestAccount} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {
|
||||
createGuild,
|
||||
createPack,
|
||||
enableExpressionPacksForGuild,
|
||||
grantPremium,
|
||||
grantStaffAccess,
|
||||
installPack,
|
||||
listPacks,
|
||||
revokePremium,
|
||||
@@ -45,13 +44,13 @@ describe('Pack Premium Requirements', () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
|
||||
test('user without expression_packs trait cannot list packs', async () => {
|
||||
test('user without staff flag cannot list packs', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
|
||||
await createBuilder(harness, account.token).get('/packs').expect(HTTP_STATUS.FORBIDDEN).execute();
|
||||
});
|
||||
|
||||
test('user with trait but no premium cannot create pack', async () => {
|
||||
test('user with staff flag but no premium cannot create pack', async () => {
|
||||
const {account} = await setupNonPremiumPackTestAccount(harness);
|
||||
|
||||
await createBuilder(harness, account.token)
|
||||
@@ -61,7 +60,7 @@ describe('Pack Premium Requirements', () => {
|
||||
.execute();
|
||||
});
|
||||
|
||||
test('premium user with trait can create emoji pack', async () => {
|
||||
test('premium user with staff flag can create emoji pack', async () => {
|
||||
const {account} = await setupPackTestAccount(harness);
|
||||
|
||||
const pack = await createPack(harness, account.token, 'emoji', {name: 'My Emoji Pack'});
|
||||
@@ -71,7 +70,7 @@ describe('Pack Premium Requirements', () => {
|
||||
expect(pack.type).toBe('emoji');
|
||||
});
|
||||
|
||||
test('premium user with trait can create sticker pack', async () => {
|
||||
test('premium user with staff flag can create sticker pack', async () => {
|
||||
const {account} = await setupPackTestAccount(harness);
|
||||
|
||||
const pack = await createPack(harness, account.token, 'sticker', {name: 'My Sticker Pack'});
|
||||
@@ -120,7 +119,7 @@ describe('Pack Premium Requirements', () => {
|
||||
expect(installed).toBeTruthy();
|
||||
});
|
||||
|
||||
test('user without trait cannot access pack endpoints', async () => {
|
||||
test('user without staff flag cannot access pack endpoints', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
|
||||
await createBuilder(harness, account.token).get('/packs').expect(HTTP_STATUS.FORBIDDEN).execute();
|
||||
@@ -132,10 +131,9 @@ describe('Pack Premium Requirements', () => {
|
||||
.execute();
|
||||
});
|
||||
|
||||
test('user gains trait when joining guild with expression_packs feature', async () => {
|
||||
test('user gains staff flag and can access expression packs', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'Feature Guild');
|
||||
await enableExpressionPacksForGuild(harness, guild.id);
|
||||
await grantStaffAccess(harness, owner.userId);
|
||||
await grantPremium(harness, owner.userId);
|
||||
|
||||
const dashboard = await listPacks(harness, owner.token);
|
||||
@@ -155,7 +153,7 @@ describe('Pack Premium Requirements', () => {
|
||||
expect(dashboard.sticker.installed_limit).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('pack limits show zero for non-premium user with trait', async () => {
|
||||
test('pack limits show zero for non-premium user with staff flag', async () => {
|
||||
const {account} = await setupNonPremiumPackTestAccount(harness);
|
||||
|
||||
const dashboard = await listPacks(harness, account.token);
|
||||
|
||||
@@ -23,7 +23,6 @@ import {createTestAccount, type TestAccount} from '@fluxer/api/src/auth/tests/Au
|
||||
import type {ApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
|
||||
import {HTTP_STATUS} from '@fluxer/api/src/test/TestConstants';
|
||||
import {createBuilder, createBuilderWithoutAuth} from '@fluxer/api/src/test/TestRequestBuilder';
|
||||
import {ManagedTraits} from '@fluxer/constants/src/ManagedTraits';
|
||||
import type {
|
||||
GuildEmojiResponse,
|
||||
GuildEmojiWithUserResponse,
|
||||
@@ -54,13 +53,8 @@ export async function createGuild(harness: ApiTestHarness, token: string, name:
|
||||
return createBuilder<GuildResponse>(harness, token).post('/guilds').body({name}).expect(HTTP_STATUS.OK).execute();
|
||||
}
|
||||
|
||||
export async function enableExpressionPacksForGuild(harness: ApiTestHarness, guildId: string): Promise<void> {
|
||||
await createBuilderWithoutAuth(harness)
|
||||
.post(`/test/guilds/${guildId}/features`)
|
||||
.body({
|
||||
add_features: [ManagedTraits.EXPRESSION_PACKS],
|
||||
})
|
||||
.execute();
|
||||
export async function grantStaffAccess(harness: ApiTestHarness, userId: string): Promise<void> {
|
||||
await createBuilderWithoutAuth(harness).patch(`/test/users/${userId}/flags`).body({flags: 1}).execute();
|
||||
}
|
||||
|
||||
export async function grantPremium(harness: ApiTestHarness, userId: string): Promise<void> {
|
||||
@@ -90,7 +84,7 @@ export async function setupPackTestAccount(harness: ApiTestHarness): Promise<{
|
||||
}> {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Pack Test Guild');
|
||||
await enableExpressionPacksForGuild(harness, guild.id);
|
||||
await grantStaffAccess(harness, account.userId);
|
||||
await grantPremium(harness, account.userId);
|
||||
return {account, guild};
|
||||
}
|
||||
@@ -101,7 +95,7 @@ export async function setupNonPremiumPackTestAccount(harness: ApiTestHarness): P
|
||||
}> {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Pack Test Guild');
|
||||
await enableExpressionPacksForGuild(harness, guild.id);
|
||||
await grantStaffAccess(harness, account.userId);
|
||||
return {account, guild};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {AdminAuditLog} from '@fluxer/api/src/admin/IAdminRepository';
|
||||
import {convertToSearchableAuditLog} from '@fluxer/api/src/search/auditlog/AuditLogSearchSerializer';
|
||||
import {ElasticsearchSearchServiceBase} from '@fluxer/api/src/search/elasticsearch/ElasticsearchSearchServiceBase';
|
||||
import type {IAuditLogSearchService} from '@fluxer/api/src/search/IAuditLogSearchService';
|
||||
import {
|
||||
ElasticsearchAuditLogAdapter,
|
||||
type ElasticsearchAuditLogAdapterOptions,
|
||||
} from '@fluxer/elasticsearch_search/src/adapters/ElasticsearchAuditLogAdapter';
|
||||
import type {SearchResult as SchemaSearchResult} from '@fluxer/schema/src/contracts/search/SearchAdapterTypes';
|
||||
import type {AuditLogSearchFilters, SearchableAuditLog} from '@fluxer/schema/src/contracts/search/SearchDocumentTypes';
|
||||
|
||||
export interface ElasticsearchAuditLogSearchServiceOptions extends ElasticsearchAuditLogAdapterOptions {}
|
||||
|
||||
export class ElasticsearchAuditLogSearchService
|
||||
extends ElasticsearchSearchServiceBase<AuditLogSearchFilters, SearchableAuditLog, ElasticsearchAuditLogAdapter>
|
||||
implements IAuditLogSearchService
|
||||
{
|
||||
constructor(options: ElasticsearchAuditLogSearchServiceOptions) {
|
||||
super(new ElasticsearchAuditLogAdapter({client: options.client}));
|
||||
}
|
||||
|
||||
async indexAuditLog(log: AdminAuditLog): Promise<void> {
|
||||
await this.indexDocument(convertToSearchableAuditLog(log));
|
||||
}
|
||||
|
||||
async indexAuditLogs(logs: Array<AdminAuditLog>): Promise<void> {
|
||||
if (logs.length === 0) return;
|
||||
await this.indexDocuments(logs.map(convertToSearchableAuditLog));
|
||||
}
|
||||
|
||||
async updateAuditLog(log: AdminAuditLog): Promise<void> {
|
||||
await this.updateDocument(convertToSearchableAuditLog(log));
|
||||
}
|
||||
|
||||
async deleteAuditLog(logId: bigint): Promise<void> {
|
||||
await this.deleteDocument(logId.toString());
|
||||
}
|
||||
|
||||
async deleteAuditLogs(logIds: Array<bigint>): Promise<void> {
|
||||
await this.deleteDocuments(logIds.map((id) => id.toString()));
|
||||
}
|
||||
|
||||
searchAuditLogs(
|
||||
query: string,
|
||||
filters: AuditLogSearchFilters,
|
||||
options?: {limit?: number; offset?: number},
|
||||
): Promise<SchemaSearchResult<SearchableAuditLog>> {
|
||||
return this.search(query, filters, options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {GuildID, UserID} from '@fluxer/api/src/BrandedTypes';
|
||||
import type {GuildMember} from '@fluxer/api/src/models/GuildMember';
|
||||
import type {User} from '@fluxer/api/src/models/User';
|
||||
import {ElasticsearchSearchServiceBase} from '@fluxer/api/src/search/elasticsearch/ElasticsearchSearchServiceBase';
|
||||
import {convertToSearchableGuildMember} from '@fluxer/api/src/search/guild_member/GuildMemberSearchSerializer';
|
||||
import type {IGuildMemberSearchService} from '@fluxer/api/src/search/IGuildMemberSearchService';
|
||||
import {
|
||||
ElasticsearchGuildMemberAdapter,
|
||||
type ElasticsearchGuildMemberAdapterOptions,
|
||||
} from '@fluxer/elasticsearch_search/src/adapters/ElasticsearchGuildMemberAdapter';
|
||||
import type {SearchResult as SchemaSearchResult} from '@fluxer/schema/src/contracts/search/SearchAdapterTypes';
|
||||
import type {
|
||||
GuildMemberSearchFilters,
|
||||
SearchableGuildMember,
|
||||
} from '@fluxer/schema/src/contracts/search/SearchDocumentTypes';
|
||||
|
||||
const DEFAULT_LIMIT = 25;
|
||||
|
||||
function toSearchOptions(options?: {limit?: number; offset?: number}): {limit?: number; offset?: number} {
|
||||
return {
|
||||
limit: options?.limit ?? DEFAULT_LIMIT,
|
||||
offset: options?.offset ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
export interface ElasticsearchGuildMemberSearchServiceOptions extends ElasticsearchGuildMemberAdapterOptions {}
|
||||
|
||||
export class ElasticsearchGuildMemberSearchService
|
||||
extends ElasticsearchSearchServiceBase<
|
||||
GuildMemberSearchFilters,
|
||||
SearchableGuildMember,
|
||||
ElasticsearchGuildMemberAdapter
|
||||
>
|
||||
implements IGuildMemberSearchService
|
||||
{
|
||||
constructor(options: ElasticsearchGuildMemberSearchServiceOptions) {
|
||||
super(new ElasticsearchGuildMemberAdapter({client: options.client}));
|
||||
}
|
||||
|
||||
async indexMember(member: GuildMember, user: User): Promise<void> {
|
||||
await this.indexDocument(convertToSearchableGuildMember(member, user));
|
||||
}
|
||||
|
||||
async indexMembers(members: Array<{member: GuildMember; user: User}>): Promise<void> {
|
||||
if (members.length === 0) return;
|
||||
await this.indexDocuments(members.map(({member, user}) => convertToSearchableGuildMember(member, user)));
|
||||
}
|
||||
|
||||
async updateMember(member: GuildMember, user: User): Promise<void> {
|
||||
await this.updateDocument(convertToSearchableGuildMember(member, user));
|
||||
}
|
||||
|
||||
async deleteMember(guildId: GuildID, userId: UserID): Promise<void> {
|
||||
await this.deleteDocument(`${guildId}_${userId}`);
|
||||
}
|
||||
|
||||
async deleteGuildMembers(guildId: GuildID): Promise<void> {
|
||||
const guildIdString = guildId.toString();
|
||||
while (true) {
|
||||
const result = await this.search('', {guildId: guildIdString}, {limit: 1000, offset: 0});
|
||||
if (result.hits.length === 0) {
|
||||
return;
|
||||
}
|
||||
await this.deleteDocuments(result.hits.map((hit) => hit.id));
|
||||
}
|
||||
}
|
||||
|
||||
searchMembers(
|
||||
query: string,
|
||||
filters: GuildMemberSearchFilters,
|
||||
options?: {limit?: number; offset?: number},
|
||||
): Promise<SchemaSearchResult<SearchableGuildMember>> {
|
||||
return this.search(query, filters, toSearchOptions(options));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {GuildID} from '@fluxer/api/src/BrandedTypes';
|
||||
import type {Guild} from '@fluxer/api/src/models/Guild';
|
||||
import {ElasticsearchSearchServiceBase} from '@fluxer/api/src/search/elasticsearch/ElasticsearchSearchServiceBase';
|
||||
import {convertToSearchableGuild, type GuildDiscoveryContext} from '@fluxer/api/src/search/guild/GuildSearchSerializer';
|
||||
import type {IGuildSearchService} from '@fluxer/api/src/search/IGuildSearchService';
|
||||
import {
|
||||
ElasticsearchGuildAdapter,
|
||||
type ElasticsearchGuildAdapterOptions,
|
||||
} from '@fluxer/elasticsearch_search/src/adapters/ElasticsearchGuildAdapter';
|
||||
import type {SearchResult as SchemaSearchResult} from '@fluxer/schema/src/contracts/search/SearchAdapterTypes';
|
||||
import type {GuildSearchFilters, SearchableGuild} from '@fluxer/schema/src/contracts/search/SearchDocumentTypes';
|
||||
|
||||
export interface ElasticsearchGuildSearchServiceOptions extends ElasticsearchGuildAdapterOptions {}
|
||||
|
||||
export class ElasticsearchGuildSearchService
|
||||
extends ElasticsearchSearchServiceBase<GuildSearchFilters, SearchableGuild, ElasticsearchGuildAdapter>
|
||||
implements IGuildSearchService
|
||||
{
|
||||
constructor(options: ElasticsearchGuildSearchServiceOptions) {
|
||||
super(new ElasticsearchGuildAdapter({client: options.client}));
|
||||
}
|
||||
|
||||
async indexGuild(guild: Guild, discovery?: GuildDiscoveryContext): Promise<void> {
|
||||
await this.indexDocument(convertToSearchableGuild(guild, discovery));
|
||||
}
|
||||
|
||||
async indexGuilds(guilds: Array<Guild>): Promise<void> {
|
||||
if (guilds.length === 0) return;
|
||||
await this.indexDocuments(guilds.map((g) => convertToSearchableGuild(g)));
|
||||
}
|
||||
|
||||
async updateGuild(guild: Guild, discovery?: GuildDiscoveryContext): Promise<void> {
|
||||
await this.updateDocument(convertToSearchableGuild(guild, discovery));
|
||||
}
|
||||
|
||||
async deleteGuild(guildId: GuildID): Promise<void> {
|
||||
await this.deleteDocument(guildId.toString());
|
||||
}
|
||||
|
||||
async deleteGuilds(guildIds: Array<GuildID>): Promise<void> {
|
||||
await this.deleteDocuments(guildIds.map((id) => id.toString()));
|
||||
}
|
||||
|
||||
searchGuilds(
|
||||
query: string,
|
||||
filters: GuildSearchFilters,
|
||||
options?: {limit?: number; offset?: number},
|
||||
): Promise<SchemaSearchResult<SearchableGuild>> {
|
||||
return this.search(query, filters, options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {GuildID, MessageID, UserID} from '@fluxer/api/src/BrandedTypes';
|
||||
import type {Message} from '@fluxer/api/src/models/Message';
|
||||
import {ElasticsearchSearchServiceBase} from '@fluxer/api/src/search/elasticsearch/ElasticsearchSearchServiceBase';
|
||||
import type {IMessageSearchService} from '@fluxer/api/src/search/IMessageSearchService';
|
||||
import {convertToSearchableMessage} from '@fluxer/api/src/search/message/MessageSearchSerializer';
|
||||
import {
|
||||
ElasticsearchMessageAdapter,
|
||||
type ElasticsearchMessageAdapterOptions,
|
||||
} from '@fluxer/elasticsearch_search/src/adapters/ElasticsearchMessageAdapter';
|
||||
import type {SearchResult as SchemaSearchResult} from '@fluxer/schema/src/contracts/search/SearchAdapterTypes';
|
||||
import type {MessageSearchFilters, SearchableMessage} from '@fluxer/schema/src/contracts/search/SearchDocumentTypes';
|
||||
|
||||
const MESSAGE_DELETE_BATCH_SIZE = 1000;
|
||||
const DEFAULT_HITS_PER_PAGE = 25;
|
||||
|
||||
function toSearchOptions(options?: {hitsPerPage?: number; page?: number}): {limit?: number; offset?: number} {
|
||||
return {
|
||||
limit: options?.hitsPerPage,
|
||||
offset: options?.page ? (options.page - 1) * (options.hitsPerPage ?? DEFAULT_HITS_PER_PAGE) : 0,
|
||||
};
|
||||
}
|
||||
|
||||
export interface ElasticsearchMessageSearchServiceOptions extends ElasticsearchMessageAdapterOptions {}
|
||||
|
||||
export class ElasticsearchMessageSearchService
|
||||
extends ElasticsearchSearchServiceBase<MessageSearchFilters, SearchableMessage, ElasticsearchMessageAdapter>
|
||||
implements IMessageSearchService
|
||||
{
|
||||
constructor(options: ElasticsearchMessageSearchServiceOptions) {
|
||||
super(new ElasticsearchMessageAdapter({client: options.client}));
|
||||
}
|
||||
|
||||
async indexMessage(message: Message, authorIsBot?: boolean): Promise<void> {
|
||||
await this.indexDocument(convertToSearchableMessage(message, authorIsBot));
|
||||
}
|
||||
|
||||
async indexMessages(messages: Array<Message>, authorBotMap?: Map<UserID, boolean>): Promise<void> {
|
||||
if (messages.length === 0) {
|
||||
return;
|
||||
}
|
||||
await this.indexDocuments(
|
||||
messages.map((message) => {
|
||||
const isBot = message.authorId ? (authorBotMap?.get(message.authorId) ?? false) : false;
|
||||
return convertToSearchableMessage(message, isBot);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async updateMessage(message: Message, authorIsBot?: boolean): Promise<void> {
|
||||
await this.updateDocument(convertToSearchableMessage(message, authorIsBot));
|
||||
}
|
||||
|
||||
async deleteMessage(messageId: MessageID): Promise<void> {
|
||||
await this.deleteDocument(messageId.toString());
|
||||
}
|
||||
|
||||
async deleteMessages(messageIds: Array<MessageID>): Promise<void> {
|
||||
await this.deleteDocuments(messageIds.map((id) => id.toString()));
|
||||
}
|
||||
|
||||
async deleteGuildMessages(guildId: GuildID): Promise<void> {
|
||||
const guildIdString = guildId.toString();
|
||||
while (true) {
|
||||
const result = await this.search('', {guildId: guildIdString}, {limit: MESSAGE_DELETE_BATCH_SIZE, offset: 0});
|
||||
if (result.hits.length === 0) {
|
||||
return;
|
||||
}
|
||||
await this.deleteDocuments(result.hits.map((hit) => hit.id));
|
||||
}
|
||||
}
|
||||
|
||||
searchMessages(
|
||||
query: string,
|
||||
filters: MessageSearchFilters,
|
||||
options?: {hitsPerPage?: number; page?: number},
|
||||
): Promise<SchemaSearchResult<SearchableMessage>> {
|
||||
return this.search(query, filters, toSearchOptions(options));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {GuildID, MessageID, ReportID, UserID} from '@fluxer/api/src/BrandedTypes';
|
||||
import type {IARSubmission} from '@fluxer/api/src/report/IReportRepository';
|
||||
import {ElasticsearchSearchServiceBase} from '@fluxer/api/src/search/elasticsearch/ElasticsearchSearchServiceBase';
|
||||
import type {IReportSearchService} from '@fluxer/api/src/search/IReportSearchService';
|
||||
import {convertToSearchableReport} from '@fluxer/api/src/search/report/ReportSearchSerializer';
|
||||
import {
|
||||
ElasticsearchReportAdapter,
|
||||
type ElasticsearchReportAdapterOptions,
|
||||
} from '@fluxer/elasticsearch_search/src/adapters/ElasticsearchReportAdapter';
|
||||
import type {SearchResult as SchemaSearchResult} from '@fluxer/schema/src/contracts/search/SearchAdapterTypes';
|
||||
import type {ReportSearchFilters, SearchableReport} from '@fluxer/schema/src/contracts/search/SearchDocumentTypes';
|
||||
|
||||
export interface ElasticsearchReportSearchServiceOptions extends ElasticsearchReportAdapterOptions {}
|
||||
|
||||
export class ElasticsearchReportSearchService
|
||||
extends ElasticsearchSearchServiceBase<ReportSearchFilters, SearchableReport, ElasticsearchReportAdapter>
|
||||
implements IReportSearchService
|
||||
{
|
||||
constructor(options: ElasticsearchReportSearchServiceOptions) {
|
||||
super(new ElasticsearchReportAdapter({client: options.client}));
|
||||
}
|
||||
|
||||
async indexReport(report: IARSubmission): Promise<void> {
|
||||
await this.indexDocument(convertToSearchableReport(report));
|
||||
}
|
||||
|
||||
async indexReports(reports: Array<IARSubmission>): Promise<void> {
|
||||
if (reports.length === 0) return;
|
||||
await this.indexDocuments(reports.map(convertToSearchableReport));
|
||||
}
|
||||
|
||||
async updateReport(report: IARSubmission): Promise<void> {
|
||||
await this.updateDocument(convertToSearchableReport(report));
|
||||
}
|
||||
|
||||
async deleteReport(reportId: ReportID): Promise<void> {
|
||||
await this.deleteDocument(reportId.toString());
|
||||
}
|
||||
|
||||
async deleteReports(reportIds: Array<ReportID>): Promise<void> {
|
||||
await this.deleteDocuments(reportIds.map((id) => id.toString()));
|
||||
}
|
||||
|
||||
searchReports(
|
||||
query: string,
|
||||
filters: ReportSearchFilters,
|
||||
options?: {limit?: number; offset?: number},
|
||||
): Promise<SchemaSearchResult<SearchableReport>> {
|
||||
return this.search(query, filters, options);
|
||||
}
|
||||
|
||||
listReportsByReporter(
|
||||
reporterId: UserID,
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
): Promise<SchemaSearchResult<SearchableReport>> {
|
||||
return this.searchReports('', {reporterId: reporterId.toString()}, {limit, offset});
|
||||
}
|
||||
|
||||
listReportsByStatus(status: number, limit?: number, offset?: number): Promise<SchemaSearchResult<SearchableReport>> {
|
||||
return this.searchReports('', {status}, {limit, offset});
|
||||
}
|
||||
|
||||
listReportsByType(
|
||||
reportType: number,
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
): Promise<SchemaSearchResult<SearchableReport>> {
|
||||
return this.searchReports('', {reportType}, {limit, offset});
|
||||
}
|
||||
|
||||
listReportsByReportedUser(
|
||||
reportedUserId: UserID,
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
): Promise<SchemaSearchResult<SearchableReport>> {
|
||||
return this.searchReports('', {reportedUserId: reportedUserId.toString()}, {limit, offset});
|
||||
}
|
||||
|
||||
listReportsByReportedGuild(
|
||||
reportedGuildId: GuildID,
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
): Promise<SchemaSearchResult<SearchableReport>> {
|
||||
return this.searchReports('', {reportedGuildId: reportedGuildId.toString()}, {limit, offset});
|
||||
}
|
||||
|
||||
listReportsByReportedMessage(
|
||||
reportedMessageId: MessageID,
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
): Promise<SchemaSearchResult<SearchableReport>> {
|
||||
return this.searchReports('', {reportedMessageId: reportedMessageId.toString()}, {limit, offset});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {trackSearchTask} from '@fluxer/api/src/search/SearchTaskTracker';
|
||||
import type {ISearchAdapter, SearchOptions, SearchResult} from '@fluxer/schema/src/contracts/search/SearchAdapterTypes';
|
||||
|
||||
export abstract class ElasticsearchSearchServiceBase<
|
||||
TFilters,
|
||||
TDocument,
|
||||
TAdapter extends ISearchAdapter<TFilters, TDocument>,
|
||||
> {
|
||||
protected readonly adapter: TAdapter;
|
||||
|
||||
protected constructor(adapter: TAdapter) {
|
||||
this.adapter = adapter;
|
||||
}
|
||||
|
||||
initialize(): Promise<void> {
|
||||
return this.adapter.initialize();
|
||||
}
|
||||
|
||||
shutdown(): Promise<void> {
|
||||
return this.adapter.shutdown();
|
||||
}
|
||||
|
||||
isAvailable(): boolean {
|
||||
return this.adapter.isAvailable();
|
||||
}
|
||||
|
||||
indexDocument(doc: TDocument): Promise<void> {
|
||||
return trackSearchTask(this.adapter.indexDocument(doc));
|
||||
}
|
||||
|
||||
indexDocuments(docs: Array<TDocument>): Promise<void> {
|
||||
return trackSearchTask(this.adapter.indexDocuments(docs));
|
||||
}
|
||||
|
||||
updateDocument(doc: TDocument): Promise<void> {
|
||||
return trackSearchTask(this.adapter.updateDocument(doc));
|
||||
}
|
||||
|
||||
deleteDocument(id: string): Promise<void> {
|
||||
return trackSearchTask(this.adapter.deleteDocument(id));
|
||||
}
|
||||
|
||||
deleteDocuments(ids: Array<string>): Promise<void> {
|
||||
return trackSearchTask(this.adapter.deleteDocuments(ids));
|
||||
}
|
||||
|
||||
deleteAllDocuments(): Promise<void> {
|
||||
return trackSearchTask(this.adapter.deleteAllDocuments());
|
||||
}
|
||||
|
||||
search(query: string, filters: TFilters, options?: SearchOptions): Promise<SearchResult<TDocument>> {
|
||||
return this.adapter.search(query, filters, options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {UserID} from '@fluxer/api/src/BrandedTypes';
|
||||
import type {User} from '@fluxer/api/src/models/User';
|
||||
import {ElasticsearchSearchServiceBase} from '@fluxer/api/src/search/elasticsearch/ElasticsearchSearchServiceBase';
|
||||
import type {IUserSearchService} from '@fluxer/api/src/search/IUserSearchService';
|
||||
import {convertToSearchableUser} from '@fluxer/api/src/search/user/UserSearchSerializer';
|
||||
import {
|
||||
ElasticsearchUserAdapter,
|
||||
type ElasticsearchUserAdapterOptions,
|
||||
} from '@fluxer/elasticsearch_search/src/adapters/ElasticsearchUserAdapter';
|
||||
import type {SearchResult as SchemaSearchResult} from '@fluxer/schema/src/contracts/search/SearchAdapterTypes';
|
||||
import type {SearchableUser, UserSearchFilters} from '@fluxer/schema/src/contracts/search/SearchDocumentTypes';
|
||||
|
||||
export interface ElasticsearchUserSearchServiceOptions extends ElasticsearchUserAdapterOptions {}
|
||||
|
||||
export class ElasticsearchUserSearchService
|
||||
extends ElasticsearchSearchServiceBase<UserSearchFilters, SearchableUser, ElasticsearchUserAdapter>
|
||||
implements IUserSearchService
|
||||
{
|
||||
constructor(options: ElasticsearchUserSearchServiceOptions) {
|
||||
super(new ElasticsearchUserAdapter({client: options.client}));
|
||||
}
|
||||
|
||||
async indexUser(user: User): Promise<void> {
|
||||
await this.indexDocument(convertToSearchableUser(user));
|
||||
}
|
||||
|
||||
async indexUsers(users: Array<User>): Promise<void> {
|
||||
if (users.length === 0) return;
|
||||
await this.indexDocuments(users.map(convertToSearchableUser));
|
||||
}
|
||||
|
||||
async updateUser(user: User): Promise<void> {
|
||||
await this.updateDocument(convertToSearchableUser(user));
|
||||
}
|
||||
|
||||
async deleteUser(userId: UserID): Promise<void> {
|
||||
await this.deleteDocument(userId.toString());
|
||||
}
|
||||
|
||||
async deleteUsers(userIds: Array<UserID>): Promise<void> {
|
||||
await this.deleteDocuments(userIds.map((id) => id.toString()));
|
||||
}
|
||||
|
||||
searchUsers(
|
||||
query: string,
|
||||
filters: UserSearchFilters,
|
||||
options?: {limit?: number; offset?: number},
|
||||
): Promise<SchemaSearchResult<SearchableUser>> {
|
||||
return this.search(query, filters, options);
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ export interface ApiTestHarness {
|
||||
}
|
||||
|
||||
export interface CreateApiTestHarnessOptions {
|
||||
search?: 'disabled' | 'meilisearch';
|
||||
search?: 'disabled' | 'meilisearch' | 'elasticsearch';
|
||||
}
|
||||
|
||||
export async function createApiTestHarness(options: CreateApiTestHarnessOptions = {}): Promise<ApiTestHarness> {
|
||||
|
||||
@@ -58,7 +58,6 @@ import {getKVClient} from '@fluxer/api/src/middleware/ServiceRegistry';
|
||||
import {OAuth2TokenRepository} from '@fluxer/api/src/oauth/repositories/OAuth2TokenRepository';
|
||||
import {IpAuthorizationTokens, OAuth2AccessTokensByUser} from '@fluxer/api/src/Tables';
|
||||
import {resetTestHarnessState} from '@fluxer/api/src/test/TestHarnessReset';
|
||||
import type {GuildManagedTraitService} from '@fluxer/api/src/traits/GuildManagedTraitService';
|
||||
import type {HonoApp, HonoEnv} from '@fluxer/api/src/types/HonoEnv';
|
||||
import {AuthSessionRepository} from '@fluxer/api/src/user/repositories/auth/AuthSessionRepository';
|
||||
import {ScheduledMessageRepository} from '@fluxer/api/src/user/repositories/ScheduledMessageRepository';
|
||||
@@ -67,7 +66,6 @@ import {UserRepository} from '@fluxer/api/src/user/repositories/UserRepository';
|
||||
import {processUserDeletion} from '@fluxer/api/src/user/services/UserDeletionService';
|
||||
import {UserHarvestRepository} from '@fluxer/api/src/user/UserHarvestRepository';
|
||||
import {getExpiryBucket} from '@fluxer/api/src/utils/AttachmentDecay';
|
||||
import {areFeatureSetsEqual} from '@fluxer/api/src/utils/featureUtils';
|
||||
import {ScheduledMessageExecutor} from '@fluxer/api/src/worker/executors/ScheduledMessageExecutor';
|
||||
import {processExpiredAttachments} from '@fluxer/api/src/worker/tasks/ExpireAttachments';
|
||||
import {processInactivityDeletionsCore} from '@fluxer/api/src/worker/tasks/ProcessInactivityDeletions';
|
||||
@@ -75,7 +73,6 @@ import {setWorkerDependencies} from '@fluxer/api/src/worker/WorkerContext';
|
||||
import {initializeWorkerDependencies} from '@fluxer/api/src/worker/WorkerDependencies';
|
||||
import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants';
|
||||
import {MAX_GUILD_MEMBERS_VERY_LARGE_GUILD} from '@fluxer/constants/src/LimitConstants';
|
||||
import {isManagedTrait} from '@fluxer/constants/src/ManagedTraits';
|
||||
import {SuspiciousActivityFlags, UserFlags} from '@fluxer/constants/src/UserConstants';
|
||||
import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes';
|
||||
import type {IEmailService} from '@fluxer/email/src/IEmailService';
|
||||
@@ -108,19 +105,6 @@ import * as BucketUtils from '@fluxer/snowflake/src/SnowflakeBuckets';
|
||||
import type {Context} from 'hono';
|
||||
import {seconds} from 'itty-time';
|
||||
|
||||
function differenceSet<T>(base: Iterable<T>, comparator: Iterable<T>): Set<T> {
|
||||
const comparatorSet = new Set(comparator);
|
||||
const result = new Set<T>();
|
||||
|
||||
for (const entry of base) {
|
||||
if (!comparatorSet.has(entry)) {
|
||||
result.add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const TEST_EMAIL_ENDPOINT = '/test/emails';
|
||||
const TEST_AUTH_HEADER = 'x-test-token';
|
||||
const MAX_TEST_PRIVATE_CHANNELS = 1000;
|
||||
@@ -1093,7 +1077,6 @@ export function TestHarnessController(app: HonoApp) {
|
||||
throw new UnknownGuildError();
|
||||
}
|
||||
|
||||
const previousFeatures = guild.features ? new Set(guild.features) : null;
|
||||
const newFeatures = new Set(guild.features);
|
||||
|
||||
if (Array.isArray(addFeatures)) {
|
||||
@@ -1112,66 +1095,12 @@ export function TestHarnessController(app: HonoApp) {
|
||||
}
|
||||
}
|
||||
|
||||
const featuresChanged = !areFeatureSetsEqual(previousFeatures, newFeatures);
|
||||
const guildRow = guild.toRow();
|
||||
await guildRepository.upsert({
|
||||
...guildRow,
|
||||
features: newFeatures,
|
||||
});
|
||||
|
||||
const guildManagedTraitService = ctx.get('guildManagedTraitService') as GuildManagedTraitService | undefined;
|
||||
if (featuresChanged) {
|
||||
if (guildManagedTraitService) {
|
||||
try {
|
||||
await guildManagedTraitService.reconcileTraitsForGuildFeatureChange({
|
||||
guildId,
|
||||
previousFeatures,
|
||||
newFeatures,
|
||||
});
|
||||
} catch (error) {
|
||||
Logger.error(
|
||||
{error, guildId: guildId.toString()},
|
||||
'Failed to reconcile managed traits after test harness guild feature update',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const userRepository = ctx.get('userRepository');
|
||||
const userCacheService = ctx.get('userCacheService');
|
||||
const members = await guildRepository.listMembers(guildId);
|
||||
|
||||
const addedTraits = differenceSet(new Set(addFeatures || []), new Set(previousFeatures || []));
|
||||
const removedTraits = differenceSet(new Set(previousFeatures || []), new Set(addFeatures || []));
|
||||
|
||||
for (const member of members) {
|
||||
const user = await userRepository.findUnique(member.userId);
|
||||
if (!user) continue;
|
||||
|
||||
const updatedTraits = new Set(user.traits);
|
||||
let changed = false;
|
||||
|
||||
for (const trait of addedTraits) {
|
||||
if (isManagedTrait(trait) && !updatedTraits.has(trait)) {
|
||||
updatedTraits.add(trait);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (const trait of removedTraits) {
|
||||
if (updatedTraits.has(trait)) {
|
||||
updatedTraits.delete(trait);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
const traitValue = updatedTraits.size > 0 ? new Set(updatedTraits) : null;
|
||||
await userRepository.patchUpsert(member.userId, {traits: traitValue}, user.toRow());
|
||||
await userCacheService.invalidateUserCache(member.userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.json({
|
||||
success: true,
|
||||
features: Array.from(newFeatures),
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {GuildID, UserID} from '@fluxer/api/src/BrandedTypes';
|
||||
import type {IGuildRepositoryAggregate} from '@fluxer/api/src/guild/repositories/IGuildRepositoryAggregate';
|
||||
import type {IGatewayService} from '@fluxer/api/src/infrastructure/IGatewayService';
|
||||
import type {UserCacheService} from '@fluxer/api/src/infrastructure/UserCacheService';
|
||||
import {Logger} from '@fluxer/api/src/Logger';
|
||||
import type {Guild} from '@fluxer/api/src/models/Guild';
|
||||
import type {User} from '@fluxer/api/src/models/User';
|
||||
import type {IUserRepository} from '@fluxer/api/src/user/IUserRepository';
|
||||
import {BaseUserUpdatePropagator} from '@fluxer/api/src/user/services/BaseUserUpdatePropagator';
|
||||
import {isManagedTrait} from '@fluxer/constants/src/ManagedTraits';
|
||||
|
||||
interface GuildManagedTraitServiceDeps {
|
||||
userRepository: IUserRepository;
|
||||
guildRepository: IGuildRepositoryAggregate;
|
||||
gatewayService: IGatewayService;
|
||||
userCacheService: UserCacheService;
|
||||
}
|
||||
|
||||
export class GuildManagedTraitService {
|
||||
private readonly updatePropagator: BaseUserUpdatePropagator;
|
||||
|
||||
constructor(private readonly deps: GuildManagedTraitServiceDeps) {
|
||||
this.updatePropagator = new BaseUserUpdatePropagator({
|
||||
userCacheService: deps.userCacheService,
|
||||
gatewayService: deps.gatewayService,
|
||||
});
|
||||
}
|
||||
|
||||
async ensureTraitsForGuildJoin({guild, user}: {guild: Guild; user: User}): Promise<void> {
|
||||
const managedTraits = this.getManagedTraitsFromIterable(guild.features);
|
||||
if (managedTraits.size === 0) return;
|
||||
|
||||
const updatedTraits = new Set(user.traits);
|
||||
let changed = false;
|
||||
for (const trait of managedTraits) {
|
||||
if (!updatedTraits.has(trait)) {
|
||||
updatedTraits.add(trait);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.updateUserTraits(user.id, updatedTraits, user);
|
||||
}
|
||||
|
||||
async reconcileTraitsForGuildLeave({guild, userId}: {guild: Guild; userId: UserID}): Promise<void> {
|
||||
const managedTraits = this.getManagedTraitsFromIterable(guild.features);
|
||||
if (managedTraits.size === 0) return;
|
||||
|
||||
await this.removeTraitsIfNoProviders(userId, managedTraits, guild.id);
|
||||
}
|
||||
|
||||
async reconcileTraitsForGuildFeatureChange(params: {
|
||||
guildId: GuildID;
|
||||
previousFeatures: Iterable<string> | null | undefined;
|
||||
newFeatures: Iterable<string> | null | undefined;
|
||||
}): Promise<void> {
|
||||
const previousTraits = this.getManagedTraitsFromIterable(params.previousFeatures);
|
||||
const newTraits = this.getManagedTraitsFromIterable(params.newFeatures);
|
||||
|
||||
const addedTraits = GuildManagedTraitService.difference(newTraits, previousTraits);
|
||||
const removedTraits = GuildManagedTraitService.difference(previousTraits, newTraits);
|
||||
|
||||
if (addedTraits.size === 0 && removedTraits.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const members = await this.deps.guildRepository.listMembers(params.guildId);
|
||||
|
||||
for (const member of members) {
|
||||
try {
|
||||
if (addedTraits.size > 0) {
|
||||
await this.ensureTraitSetForUser(member.userId, addedTraits);
|
||||
}
|
||||
if (removedTraits.size > 0) {
|
||||
await this.removeTraitsIfNoProviders(member.userId, removedTraits, params.guildId);
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(
|
||||
{
|
||||
guildId: params.guildId.toString(),
|
||||
userId: member.userId.toString(),
|
||||
error,
|
||||
},
|
||||
'Failed to reconcile managed traits for guild member',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureTraitSetForUser(userId: UserID, traits: Set<string>): Promise<void> {
|
||||
const user = await this.deps.userRepository.findUnique(userId);
|
||||
if (!user) return;
|
||||
|
||||
const updatedTraits = new Set(user.traits);
|
||||
let changed = false;
|
||||
for (const trait of traits) {
|
||||
if (!updatedTraits.has(trait)) {
|
||||
updatedTraits.add(trait);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.updateUserTraits(userId, updatedTraits, user);
|
||||
}
|
||||
|
||||
private async updateUserTraits(
|
||||
userId: UserID,
|
||||
traits: Set<string>,
|
||||
existingUser?: User | null,
|
||||
): Promise<User | null> {
|
||||
const user = existingUser ?? (await this.deps.userRepository.findUnique(userId));
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (GuildManagedTraitService.areSetsEqual(user.traits, traits)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const traitValue = traits.size > 0 ? new Set(traits) : null;
|
||||
const updatedUser = await this.deps.userRepository.patchUpsert(userId, {traits: traitValue}, user.toRow());
|
||||
await this.updatePropagator.dispatchUserUpdate(updatedUser);
|
||||
return updatedUser;
|
||||
}
|
||||
|
||||
private async collectManagedTraitsFromGuilds(
|
||||
guildIds: Array<GuildID>,
|
||||
options?: {excludeGuildId?: GuildID},
|
||||
): Promise<Set<string>> {
|
||||
const traits = new Set<string>();
|
||||
for (const guildId of guildIds) {
|
||||
if (options?.excludeGuildId && guildId === options.excludeGuildId) {
|
||||
continue;
|
||||
}
|
||||
const guild = await this.deps.guildRepository.findUnique(guildId);
|
||||
if (!guild) continue;
|
||||
for (const trait of this.getManagedTraitsFromIterable(guild.features)) {
|
||||
traits.add(trait);
|
||||
}
|
||||
}
|
||||
return traits;
|
||||
}
|
||||
|
||||
private async removeTraitsIfNoProviders(
|
||||
userId: UserID,
|
||||
traits: Set<string>,
|
||||
excludeGuildId?: GuildID,
|
||||
): Promise<void> {
|
||||
if (traits.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const remainingGuildIds = await this.deps.userRepository.getUserGuildIds(userId);
|
||||
const remainingTraits = await this.collectManagedTraitsFromGuilds(remainingGuildIds, {
|
||||
excludeGuildId,
|
||||
});
|
||||
|
||||
const user = await this.deps.userRepository.findUnique(userId);
|
||||
if (!user) return;
|
||||
|
||||
const updatedTraits = new Set(user.traits);
|
||||
let changed = false;
|
||||
for (const trait of traits) {
|
||||
if (updatedTraits.has(trait) && !remainingTraits.has(trait)) {
|
||||
updatedTraits.delete(trait);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.updateUserTraits(userId, updatedTraits, user);
|
||||
}
|
||||
|
||||
private getManagedTraitsFromIterable(features: Iterable<string> | null | undefined): Set<string> {
|
||||
const traits = new Set<string>();
|
||||
if (!features) {
|
||||
return traits;
|
||||
}
|
||||
|
||||
for (const feature of features) {
|
||||
if (feature && isManagedTrait(feature)) {
|
||||
traits.add(feature);
|
||||
}
|
||||
}
|
||||
|
||||
return traits;
|
||||
}
|
||||
|
||||
private static difference(base: Set<string>, comparator: Set<string>): Set<string> {
|
||||
const result = new Set<string>();
|
||||
for (const entry of base) {
|
||||
if (!comparator.has(entry)) {
|
||||
result.add(entry);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static areSetsEqual(a: Set<string>, b: Set<string>): boolean {
|
||||
if (a.size !== b.size) {
|
||||
return false;
|
||||
}
|
||||
for (const entry of a) {
|
||||
if (!b.has(entry)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,6 @@ import type {RpcService} from '@fluxer/api/src/rpc/RpcService';
|
||||
import type {SearchService} from '@fluxer/api/src/search/SearchService';
|
||||
import type {StripeService} from '@fluxer/api/src/stripe/StripeService';
|
||||
import type {ThemeService} from '@fluxer/api/src/theme/ThemeService';
|
||||
import type {GuildManagedTraitService} from '@fluxer/api/src/traits/GuildManagedTraitService';
|
||||
import type {IUserRepository} from '@fluxer/api/src/user/IUserRepository';
|
||||
import type {EmailChangeService} from '@fluxer/api/src/user/services/EmailChangeService';
|
||||
import type {PasswordChangeService} from '@fluxer/api/src/user/services/PasswordChangeService';
|
||||
@@ -154,7 +153,6 @@ export interface HonoEnv {
|
||||
alertService: AlertService;
|
||||
discoveryService: IGuildDiscoveryService;
|
||||
guildService: GuildService;
|
||||
guildManagedTraitService: GuildManagedTraitService;
|
||||
packService: PackService;
|
||||
packRequestService: PackRequestService;
|
||||
packRepository: PackRepository;
|
||||
|
||||
@@ -38,61 +38,91 @@ export class VoiceDataInitializer {
|
||||
|
||||
try {
|
||||
const repository = new VoiceRepository();
|
||||
|
||||
const serverId = `${defaultRegion.id}-server-1`;
|
||||
const livekitEndpoint = this.resolveLivekitEndpoint();
|
||||
const existingRegions = await repository.listRegions();
|
||||
if (existingRegions.length > 0) {
|
||||
|
||||
if (existingRegions.length === 0) {
|
||||
Logger.info('[VoiceDataInitializer] Creating default voice region from config...');
|
||||
await repository.createRegion({
|
||||
id: defaultRegion.id,
|
||||
name: defaultRegion.name,
|
||||
emoji: defaultRegion.emoji,
|
||||
latitude: defaultRegion.latitude,
|
||||
longitude: defaultRegion.longitude,
|
||||
isDefault: true,
|
||||
restrictions: {
|
||||
vipOnly: false,
|
||||
requiredGuildFeatures: new Set(),
|
||||
allowedGuildIds: new Set(),
|
||||
allowedUserIds: new Set(),
|
||||
},
|
||||
});
|
||||
Logger.info(`[VoiceDataInitializer] Created region: ${defaultRegion.name} (${defaultRegion.id})`);
|
||||
|
||||
await repository.createServer({
|
||||
regionId: defaultRegion.id,
|
||||
serverId,
|
||||
endpoint: livekitEndpoint,
|
||||
apiKey: livekitApiKey,
|
||||
apiSecret: livekitApiSecret,
|
||||
isActive: true,
|
||||
restrictions: {
|
||||
vipOnly: false,
|
||||
requiredGuildFeatures: new Set(),
|
||||
allowedGuildIds: new Set(),
|
||||
allowedUserIds: new Set(),
|
||||
},
|
||||
});
|
||||
|
||||
Logger.info(`[VoiceDataInitializer] Created server: ${serverId} -> ${livekitEndpoint}`);
|
||||
Logger.info('[VoiceDataInitializer] Successfully created default voice region');
|
||||
return;
|
||||
}
|
||||
|
||||
const configuredRegion = await repository.getRegion(defaultRegion.id);
|
||||
if (!configuredRegion) {
|
||||
Logger.info(
|
||||
`[VoiceDataInitializer] ${existingRegions.length} voice region(s) already exist, skipping default region creation`,
|
||||
`[VoiceDataInitializer] ${existingRegions.length} voice region(s) already exist and configured default region (${defaultRegion.id}) is absent, skipping config sync`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.info('[VoiceDataInitializer] Creating default voice region from config...');
|
||||
const configuredServer = await repository.getServer(defaultRegion.id, serverId);
|
||||
if (!configuredServer) {
|
||||
Logger.info(
|
||||
`[VoiceDataInitializer] Region ${defaultRegion.id} exists but server ${serverId} is absent, skipping config sync`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const livekitEndpoint =
|
||||
Config.voice.url ||
|
||||
(() => {
|
||||
const protocol = new URL(Config.endpoints.apiPublic).protocol.slice(0, -1) === 'https' ? 'wss' : 'ws';
|
||||
return `${protocol}://${new URL(Config.endpoints.apiPublic).hostname}/livekit`;
|
||||
})();
|
||||
const endpointChanged = configuredServer.endpoint !== livekitEndpoint;
|
||||
const apiKeyChanged = configuredServer.apiKey !== livekitApiKey;
|
||||
const apiSecretChanged = configuredServer.apiSecret !== livekitApiSecret;
|
||||
if (!endpointChanged && !apiKeyChanged && !apiSecretChanged) {
|
||||
Logger.info(`[VoiceDataInitializer] Default voice server ${serverId} already matches config credentials`);
|
||||
return;
|
||||
}
|
||||
|
||||
await repository.createRegion({
|
||||
id: defaultRegion.id,
|
||||
name: defaultRegion.name,
|
||||
emoji: defaultRegion.emoji,
|
||||
latitude: defaultRegion.latitude,
|
||||
longitude: defaultRegion.longitude,
|
||||
isDefault: true,
|
||||
restrictions: {
|
||||
vipOnly: false,
|
||||
requiredGuildFeatures: new Set(),
|
||||
allowedGuildIds: new Set(),
|
||||
allowedUserIds: new Set(),
|
||||
},
|
||||
});
|
||||
Logger.info(`[VoiceDataInitializer] Created region: ${defaultRegion.name} (${defaultRegion.id})`);
|
||||
|
||||
const serverId = `${defaultRegion.id}-server-1`;
|
||||
|
||||
await repository.createServer({
|
||||
regionId: defaultRegion.id,
|
||||
serverId,
|
||||
await repository.upsertServer({
|
||||
...configuredServer,
|
||||
endpoint: livekitEndpoint,
|
||||
apiKey: livekitApiKey,
|
||||
apiSecret: livekitApiSecret,
|
||||
isActive: true,
|
||||
restrictions: {
|
||||
vipOnly: false,
|
||||
requiredGuildFeatures: new Set(),
|
||||
allowedGuildIds: new Set(),
|
||||
allowedUserIds: new Set(),
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
Logger.info(`[VoiceDataInitializer] Created server: ${serverId} -> ${livekitEndpoint}`);
|
||||
Logger.info('[VoiceDataInitializer] Successfully created default voice region');
|
||||
Logger.info(`[VoiceDataInitializer] Synced default voice server ${serverId} credentials from config`);
|
||||
} catch (error) {
|
||||
Logger.error({error}, '[VoiceDataInitializer] Failed to create default voice region');
|
||||
Logger.error({error}, '[VoiceDataInitializer] Failed to initialise config-managed voice topology');
|
||||
}
|
||||
}
|
||||
|
||||
private resolveLivekitEndpoint(): string {
|
||||
if (Config.voice.url) {
|
||||
return Config.voice.url;
|
||||
}
|
||||
|
||||
const protocol = new URL(Config.endpoints.apiPublic).protocol.slice(0, -1) === 'https' ? 'wss' : 'ws';
|
||||
return `${protocol}://${new URL(Config.endpoints.apiPublic).hostname}/livekit`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,10 @@ import {
|
||||
createGuildEmoji,
|
||||
createWebhook,
|
||||
deleteWebhook,
|
||||
enableExpressionPacksForGuild,
|
||||
executeWebhook,
|
||||
getChannelMessage,
|
||||
grantCreateExpressionsPermission,
|
||||
grantStaffAccess,
|
||||
sendChannelMessage,
|
||||
} from '@fluxer/api/src/webhook/tests/WebhookTestUtils';
|
||||
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
|
||||
@@ -54,7 +54,7 @@ describe('Webhook compare to regular user', () => {
|
||||
const emojiGuild = await createGuild(harness, user.token, 'Emoji Source Guild');
|
||||
const emojiGuildId = emojiGuild.id;
|
||||
|
||||
await enableExpressionPacksForGuild(harness, emojiGuildId);
|
||||
await grantStaffAccess(harness, user.userId);
|
||||
await grantCreateExpressionsPermission(harness, user.token, emojiGuildId);
|
||||
|
||||
const emoji = await createGuildEmoji(harness, user.token, emojiGuildId, 'compare');
|
||||
|
||||
@@ -25,9 +25,9 @@ import {
|
||||
createGuildEmojiWithFile,
|
||||
createWebhook,
|
||||
deleteWebhook,
|
||||
enableExpressionPacksForGuild,
|
||||
executeWebhook,
|
||||
grantCreateExpressionsPermission,
|
||||
grantStaffAccess,
|
||||
} from '@fluxer/api/src/webhook/tests/WebhookTestUtils';
|
||||
import {beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
|
||||
@@ -48,7 +48,7 @@ describe('Webhook emoji bypass', () => {
|
||||
const guildId = guild.id;
|
||||
const channelId = guild.system_channel_id!;
|
||||
|
||||
await enableExpressionPacksForGuild(harness, guildId);
|
||||
await grantStaffAccess(harness, user.userId);
|
||||
await grantCreateExpressionsPermission(harness, user.token, guildId);
|
||||
|
||||
const emoji = await createGuildEmoji(harness, user.token, guildId, 'external');
|
||||
@@ -112,7 +112,7 @@ describe('Webhook emoji bypass', () => {
|
||||
const guildId = guild.id;
|
||||
const channelId = guild.system_channel_id!;
|
||||
|
||||
await enableExpressionPacksForGuild(harness, guildId);
|
||||
await grantStaffAccess(harness, user.userId);
|
||||
await grantCreateExpressionsPermission(harness, user.token, guildId);
|
||||
|
||||
const emoji = await createGuildEmoji(harness, user.token, guildId, 'wait_emoji');
|
||||
|
||||
@@ -21,7 +21,6 @@ import {createMultipartFormData} from '@fluxer/api/src/channel/tests/AttachmentT
|
||||
import {getPngDataUrl, VALID_PNG_BASE64} from '@fluxer/api/src/emoji/tests/EmojiTestUtils';
|
||||
import type {ApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
|
||||
import {createBuilder, createBuilderWithoutAuth} from '@fluxer/api/src/test/TestRequestBuilder';
|
||||
import {ManagedTraits} from '@fluxer/constants/src/ManagedTraits';
|
||||
import type {GuildEmojiWithUserResponse} from '@fluxer/schema/src/domains/guild/GuildEmojiSchemas';
|
||||
import type {MessageResponse} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
|
||||
import type {WebhookResponse, WebhookTokenResponse} from '@fluxer/schema/src/domains/webhook/WebhookSchemas';
|
||||
@@ -215,13 +214,8 @@ export async function getChannelMessage(
|
||||
return createBuilder<MessageResponse>(harness, token).get(`/channels/${channelId}/messages/${messageId}`).execute();
|
||||
}
|
||||
|
||||
export async function enableExpressionPacksForGuild(harness: ApiTestHarness, guildId: string): Promise<void> {
|
||||
return createBuilderWithoutAuth<void>(harness)
|
||||
.post(`/test/guilds/${guildId}/features`)
|
||||
.body({
|
||||
add_features: [ManagedTraits.EXPRESSION_PACKS],
|
||||
})
|
||||
.execute();
|
||||
export async function grantStaffAccess(harness: ApiTestHarness, userId: string): Promise<void> {
|
||||
await createBuilderWithoutAuth(harness).patch(`/test/users/${userId}/flags`).body({flags: 1}).execute();
|
||||
}
|
||||
|
||||
const CREATE_EXPRESSIONS = 0x08000000n;
|
||||
|
||||
@@ -322,7 +322,6 @@ export async function initializeWorkerDependencies(snowflakeService: SnowflakeSe
|
||||
webhookRepository,
|
||||
guildAuditLogService,
|
||||
limitConfigService,
|
||||
undefined,
|
||||
);
|
||||
const inviteService = new InviteService(
|
||||
inviteRepository,
|
||||
|
||||
Reference in New Issue
Block a user