mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
feat(threads): thread and forum API, admin, schemas and docs (#3251)
This commit is contained in:
1 parent
5f4295e399
commit
3f4160b138
495 files changed
+39232
-1215
No files matched your search
@@ -12,6 +12,7 @@ import {getContentMessage} from '@app/api/content_i18n/ContentI18n';
|
||||
import {BatchBuilder} from '@app/api/database/CassandraQueryExecution';
|
||||
import type {PermissionOverwrite} from '@app/api/database/types/ChannelTypes';
|
||||
import type {GuildRow} from '@app/api/database/types/GuildTypes';
|
||||
import {guildActive, isTainted} from '@app/api/experiment/ChannelThreadsGate';
|
||||
import {mapGuildToGuildResponse, mapGuildToPartialResponse} from '@app/api/guild/GuildModel';
|
||||
import type {IGuildDiscoveryRepository} from '@app/api/guild/repositories/GuildDiscoveryRepository';
|
||||
import type {IGuildRepositoryAggregate} from '@app/api/guild/repositories/IGuildRepositoryAggregate';
|
||||
@@ -26,15 +27,18 @@ import type {LimitConfigService} from '@app/api/limits/LimitConfigService';
|
||||
import {resolveLimitSafe} from '@app/api/limits/LimitConfigUtils';
|
||||
import {createLimitMatchContext} from '@app/api/limits/LimitMatchContextBuilder';
|
||||
import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware';
|
||||
import {getKVThreadAutoArchiveQueue} from '@app/api/middleware/ServiceSingletons';
|
||||
import {Guild} from '@app/api/models/Guild';
|
||||
import type {User} from '@app/api/models/User';
|
||||
import {getGuildSearchService} from '@app/api/SearchFactory';
|
||||
import type {GuildDiscoveryContext} from '@app/api/search/guild/GuildSearchSerializer';
|
||||
import {deleteChannelMessageSearchDocuments} from '@app/api/search/MessageSearchIndexCleanup';
|
||||
import {deleteThreadSearchDocuments} from '@app/api/search/thread/ThreadSearchService';
|
||||
import {Channels, ChannelsByGuild, GuildMembers, GuildMembersByUserId, GuildRoles, Guilds} from '@app/api/Tables';
|
||||
import type {IUserRepository} from '@app/api/user/IUserRepository';
|
||||
import {mapUserSettingsToResponse} from '@app/api/user/UserMappers';
|
||||
import {addGuildToUncategorizedFolder, removeGuildFromUserFolders} from '@app/api/user/utils/GuildFolderUtils';
|
||||
import {mapWithConcurrency} from '@app/api/utils/ConcurrencyUtils';
|
||||
import type {IWebhookRepository} from '@app/api/webhook/IWebhookRepository';
|
||||
import {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType';
|
||||
import {ALL_PERMISSIONS, ChannelTypes, DEFAULT_PERMISSIONS, Permissions} from '@fluxer/constants/src/ChannelConstants';
|
||||
@@ -56,6 +60,11 @@ import {
|
||||
VOICE_CHANNEL_CONNECTION_LIMIT_DEFAULT,
|
||||
VOICE_CHANNEL_USER_LIMIT_MAX,
|
||||
} from '@fluxer/constants/src/LimitConstants';
|
||||
import {
|
||||
DEFAULT_THREAD_PERMISSIONS,
|
||||
THREAD_AWARE_ALL_PERMISSIONS,
|
||||
THREAD_PERMISSIONS,
|
||||
} from '@fluxer/constants/src/ThreadPermissionUtils';
|
||||
import {DEFAULT_GUILD_FOLDER_ICON} from '@fluxer/constants/src/UserConstants';
|
||||
import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes';
|
||||
import {AccessDeniedError} from '@fluxer/errors/src/domains/core/AccessDeniedError';
|
||||
@@ -166,6 +175,7 @@ function setsEqual(a: ReadonlySet<string>, b: ReadonlySet<string>): boolean {
|
||||
}
|
||||
|
||||
const TEMPLATE_AFK_TIMEOUT_MIN_SECONDS = 60;
|
||||
const THREAD_PURGE_CONCURRENCY = 8;
|
||||
const TEMPLATE_AFK_TIMEOUT_MAX_SECONDS = 3600;
|
||||
const THE_OTHER_PLATFORM_GUILD_STAGE_VOICE_CHANNEL_TYPE = 13;
|
||||
|
||||
@@ -413,6 +423,9 @@ export class GuildOperationsService {
|
||||
);
|
||||
batch.addPrepared(GuildMembersByUserId.insert({user_id: user.id, guild_id: guildId}));
|
||||
await batch.execute();
|
||||
if (guildActive(guildId)) {
|
||||
await this.channelRepository.threads.ensureGuildMarker(guildId, {permsSeededAt: new Date()});
|
||||
}
|
||||
const guild = new Guild(guildData);
|
||||
await this.gatewayService.startGuild(guildId);
|
||||
await this.gatewayService.joinGuild({userId: user.id, guildId});
|
||||
@@ -755,7 +768,7 @@ export class GuildOperationsService {
|
||||
if (!guild) {
|
||||
throw new UnknownGuildError();
|
||||
}
|
||||
const channels = await this.channelRepository.listGuildChannels(guildId);
|
||||
const channels = await this.channelRepository.listGuildChannels(guildId, 'complete');
|
||||
for (const channel of channels) {
|
||||
await scheduleDeletedChannelFollowerRemoval({channel, crossposts: this.channelRepository.crossposts, copyMode});
|
||||
}
|
||||
@@ -788,6 +801,7 @@ export class GuildOperationsService {
|
||||
await Promise.all(invites.map((invite) => this.inviteRepository.delete(invite.code)));
|
||||
const webhooks = await this.webhookRepository.listByGuild(guildId);
|
||||
await Promise.all(webhooks.map((webhook) => this.webhookRepository.delete(webhook.id)));
|
||||
const threadIds = await this.channelRepository.threads.listGuildThreadIds(guildId, {parents: channels});
|
||||
for (const channel of channels) {
|
||||
await this.channelService.attachments.purgeChannelAttachments(channel);
|
||||
}
|
||||
@@ -799,6 +813,24 @@ export class GuildOperationsService {
|
||||
if (discoveryRow) {
|
||||
await this.discoveryRepository.deleteByGuildId(guildId, discoveryRow.status, discoveryRow.applied_at);
|
||||
}
|
||||
if (threadIds.length > 0) {
|
||||
const threadChannels = new Map(
|
||||
(await this.channelRepository.listChannels(threadIds)).map((channel) => [channel.id, channel]),
|
||||
);
|
||||
await mapWithConcurrency(threadIds, THREAD_PURGE_CONCURRENCY, async (threadId) => {
|
||||
const thread = threadChannels.get(threadId);
|
||||
if (thread) {
|
||||
await deleteChannelMessageSearchDocuments(threadId, {context: {source: 'guild_delete'}});
|
||||
await this.channelService.attachments.purgeChannelAttachments(thread);
|
||||
}
|
||||
await this.channelRepository.threads.purgeThread(threadId);
|
||||
});
|
||||
await deleteThreadSearchDocuments(threadIds);
|
||||
}
|
||||
if (await isTainted(guildId, {fresh: true})) {
|
||||
await this.channelRepository.threads.purgeGuild(guildId);
|
||||
await getKVThreadAutoArchiveQueue().removeGuild(guildId);
|
||||
}
|
||||
await this.guildRepository.delete(guildId, guild.ownerId);
|
||||
await this.gatewayService.stopGuild(guildId);
|
||||
const guildSearchService = getGuildSearchService();
|
||||
@@ -899,7 +931,7 @@ export class GuildOperationsService {
|
||||
guild_id: guildId,
|
||||
role_id: guildIdToRoleId(guildId),
|
||||
name: '@everyone',
|
||||
permissions: DEFAULT_PERMISSIONS,
|
||||
permissions: guildActive(guildId) ? DEFAULT_PERMISSIONS | DEFAULT_THREAD_PERMISSIONS : DEFAULT_PERMISSIONS,
|
||||
position: 0,
|
||||
hoist_position: null,
|
||||
color: 0,
|
||||
@@ -975,9 +1007,19 @@ export class GuildOperationsService {
|
||||
'[GuildOperationsService] Template import skipped unsupported channel types',
|
||||
);
|
||||
}
|
||||
const threadsActive = guildActive(guildId);
|
||||
const permissionMask = threadsActive ? THREAD_AWARE_ALL_PERMISSIONS : ALL_PERMISSIONS;
|
||||
const templateEveryonePermissions =
|
||||
this.parseTemplatePermissionBitfield(
|
||||
everyoneRole?.permissions_new ?? everyoneRole?.permissions,
|
||||
permissionMask,
|
||||
) || DEFAULT_PERMISSIONS;
|
||||
const everyonePermissions =
|
||||
this.parseTemplatePermissionBitfield(everyoneRole?.permissions_new ?? everyoneRole?.permissions) ||
|
||||
DEFAULT_PERMISSIONS;
|
||||
threadsActive &&
|
||||
(templateEveryonePermissions & THREAD_PERMISSIONS) === 0n &&
|
||||
(templateEveryonePermissions & Permissions.SEND_MESSAGES) !== 0n
|
||||
? templateEveryonePermissions | DEFAULT_THREAD_PERMISSIONS
|
||||
: templateEveryonePermissions;
|
||||
batch.addPrepared(
|
||||
GuildRoles.insert({
|
||||
guild_id: guildId,
|
||||
@@ -1005,7 +1047,7 @@ export class GuildOperationsService {
|
||||
guild_id: guildId,
|
||||
role_id: roleId,
|
||||
name: role.name,
|
||||
permissions: this.parseTemplatePermissionBitfield(role.permissions_new ?? role.permissions),
|
||||
permissions: this.parseTemplatePermissionBitfield(role.permissions_new ?? role.permissions, permissionMask),
|
||||
position: nextRolePosition,
|
||||
hoist_position: null,
|
||||
color: role.color ?? 0,
|
||||
@@ -1033,8 +1075,8 @@ export class GuildOperationsService {
|
||||
if (overwrite.type !== 0) continue;
|
||||
const mappedRoleId = roleIdMap.get(this.getTemplateEntityKey(overwrite.id));
|
||||
if (!mappedRoleId) continue;
|
||||
const allow = this.parseTemplatePermissionBitfield(overwrite.allow);
|
||||
const deny = this.parseTemplatePermissionBitfield(overwrite.deny);
|
||||
const allow = this.parseTemplatePermissionBitfield(overwrite.allow, permissionMask);
|
||||
const deny = this.parseTemplatePermissionBitfield(overwrite.deny, permissionMask);
|
||||
permissionOverwrites.set(mappedRoleId, {
|
||||
type: overwrite.type,
|
||||
allow_: allow,
|
||||
@@ -1140,10 +1182,10 @@ export class GuildOperationsService {
|
||||
return {systemChannelId};
|
||||
}
|
||||
|
||||
private parseTemplatePermissionBitfield(value: string | number | undefined): bigint {
|
||||
private parseTemplatePermissionBitfield(value: string | number | undefined, mask: bigint): bigint {
|
||||
const valueToParse = value ?? '0';
|
||||
try {
|
||||
return BigInt(valueToParse) & ALL_PERMISSIONS;
|
||||
return BigInt(valueToParse) & mask;
|
||||
} catch {
|
||||
throw new GuildTemplateInvalidError();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user