feat: add announcement channels, publishing and following (#3090)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-01 16:57:21 +02:00
1 parent 376afd2ad6
commit 7e4d5137f8
381 files changed
+45650 -14076

No files matched your search

@@ -3,6 +3,10 @@
import type {ChannelID, GuildID, RoleID, UserID} from '@app/api/BrandedTypes';
import {createChannelID, createGuildID, createRoleID, guildIdToRoleId} from '@app/api/BrandedTypes';
import type {IChannelRepository} from '@app/api/channel/IChannelRepository';
import {
type ChannelFollowerRemovalCopyMode,
scheduleDeletedChannelFollowerRemoval,
} from '@app/api/channel/services/ChannelFollowers';
import type {ChannelService} from '@app/api/channel/services/ChannelService';
import {getContentMessage} from '@app/api/content_i18n/ContentI18n';
import {BatchBuilder} from '@app/api/database/CassandraQueryExecution';
@@ -162,9 +166,12 @@ function setsEqual(a: ReadonlySet<string>, b: ReadonlySet<string>): boolean {
const TEMPLATE_AFK_TIMEOUT_MIN_SECONDS = 60;
const TEMPLATE_AFK_TIMEOUT_MAX_SECONDS = 3600;
const THE_OTHER_PLATFORM_GUILD_ANNOUNCEMENT_CHANNEL_TYPE = 5;
const THE_OTHER_PLATFORM_GUILD_STAGE_VOICE_CHANNEL_TYPE = 13;
function isSystemChannelType(type: number): boolean {
return type === ChannelTypes.GUILD_TEXT || type === ChannelTypes.GUILD_ANNOUNCEMENT;
}
export class GuildOperationsService {
constructor(
private readonly guildRepository: IGuildRepositoryAggregate,
@@ -525,7 +532,7 @@ export class GuildOperationsService {
ValidationErrorCodes.SYSTEM_CHANNEL_MUST_BE_IN_GUILD,
);
}
if (systemChannel.type !== ChannelTypes.GUILD_TEXT) {
if (!isSystemChannelType(systemChannel.type)) {
throw InputValidationError.fromCode('system_channel_id', ValidationErrorCodes.SYSTEM_CHANNEL_MUST_BE_TEXT);
}
patch.system_channel_id = systemChannelId;
@@ -735,18 +742,22 @@ export class GuildOperationsService {
if (!guildData || guildData.owner_id !== user.id.toString()) {
throw new MissingPermissionsError();
}
await this.performGuildDeletion(guildId);
await this.performGuildDeletion(guildId, 'source_deleted');
}
async deleteGuildById(guildId: GuildID): Promise<void> {
await this.performGuildDeletion(guildId);
await this.performGuildDeletion(guildId, 'purge');
}
private async performGuildDeletion(guildId: GuildID): Promise<void> {
private async performGuildDeletion(guildId: GuildID, copyMode: ChannelFollowerRemovalCopyMode): Promise<void> {
const guild = await this.guildRepository.findUnique(guildId);
if (!guild) {
throw new UnknownGuildError();
}
const channels = await this.channelRepository.listGuildChannels(guildId);
for (const channel of channels) {
await scheduleDeletedChannelFollowerRemoval({channel, crossposts: this.channelRepository.crossposts, copyMode});
}
const members = await this.guildRepository.listMembers(guildId);
await this.gatewayService.dispatchGuild({
guildId,
@@ -776,12 +787,13 @@ 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 channels = await this.channelRepository.listGuildChannels(guildId);
for (const channel of channels) {
await this.channelService.attachments.purgeChannelAttachments(channel);
}
await Promise.all(channels.map((channel) => this.channelRepository.deleteAllChannelMessages(channel.id)));
await Promise.all(
channels.map((channel) => deleteChannelMessageSearchDocuments(channel.id, {context: {source: 'guild_delete'}})),
);
await Promise.all(channels.map((channel) => this.channelService.attachments.purgeChannelAttachments(channel)));
const discoveryRow = await this.discoveryRepository.findByGuildId(guildId);
if (discoveryRow) {
await this.discoveryRepository.deleteByGuildId(guildId, discoveryRow.status, discoveryRow.applied_at);
@@ -1032,7 +1044,7 @@ export class GuildOperationsService {
permissionOverwrites = null;
}
}
if (fluxerType === ChannelTypes.GUILD_TEXT && !firstTextChannelId) {
if (isSystemChannelType(fluxerType) && !firstTextChannelId) {
firstTextChannelId = channelId;
}
batch.addPrepared(
@@ -1078,7 +1090,7 @@ export class GuildOperationsService {
if (template.system_channel_id != null) {
const templateSystemChannelKey = this.getTemplateEntityKey(template.system_channel_id);
const templateSystemChannelType = channelTypeMap.get(templateSystemChannelKey);
if (templateSystemChannelType === ChannelTypes.GUILD_TEXT) {
if (templateSystemChannelType !== undefined && isSystemChannelType(templateSystemChannelType)) {
systemChannelId = channelIdMap.get(templateSystemChannelKey) ?? null;
}
}
@@ -1151,14 +1163,12 @@ export class GuildOperationsService {
private mapOtherPlatformTemplateChannelTypeToFluxer(channelType: number): number | null {
if (
channelType === ChannelTypes.GUILD_TEXT ||
channelType === ChannelTypes.GUILD_ANNOUNCEMENT ||
channelType === ChannelTypes.GUILD_VOICE ||
channelType === ChannelTypes.GUILD_CATEGORY
) {
return channelType;
}
if (channelType === THE_OTHER_PLATFORM_GUILD_ANNOUNCEMENT_CHANNEL_TYPE) {
return ChannelTypes.GUILD_TEXT;
}
if (channelType === THE_OTHER_PLATFORM_GUILD_STAGE_VOICE_CHANNEL_TYPE) {
return ChannelTypes.GUILD_VOICE;
}