From b8e36b26348248666922a020a4619c99b9ccd442 Mon Sep 17 00:00:00 2001 From: didinele Date: Tue, 11 Aug 2026 17:44:23 +0300 Subject: [PATCH] feat: migration notices --- packages/bot/src/events/ready.ts | 23 +++++++- .../bot/src/util/handleThreadManagement.ts | 10 +++- packages/bot/src/util/migrationNotice.ts | 59 +++++++++++++++++++ 3 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 packages/bot/src/util/migrationNotice.ts diff --git a/packages/bot/src/events/ready.ts b/packages/bot/src/events/ready.ts index 46b3814..3b90849 100644 --- a/packages/bot/src/events/ready.ts +++ b/packages/bot/src/events/ready.ts @@ -1,19 +1,40 @@ +import { clearInterval, setInterval } from 'node:timers'; import type { Client } from 'discord.js'; -import { Events } from 'discord.js'; +import { ActivityType, Events } from 'discord.js'; import { singleton } from 'tsyringe'; import type { Event } from '../struct/Event.js'; import { JobManager } from '../struct/JobManager.js'; import { logger } from '../util/logger.js'; +import { MIGRATION_STATUS_TEXT } from '../util/migrationNotice.js'; + +const PRESENCE_REFRESH_INTERVAL = 60 * 60 * 1_000; @singleton() export default class implements Event { public readonly name = Events.ClientReady; + private presenceRefreshInterval: NodeJS.Timeout | null = null; + public constructor(private readonly jobManager: JobManager) {} public async handle(client: Client) { logger.info(`Ready as ${client.user.tag} (${client.user.id})`); + + // See ChatSift/ChatSift#313 -- temporary, remove alongside `util/migrationNotice.ts` post-cutover. + this.applyMigrationPresence(client); + if (this.presenceRefreshInterval) { + clearInterval(this.presenceRefreshInterval); + } + + this.presenceRefreshInterval = setInterval(() => this.applyMigrationPresence(client), PRESENCE_REFRESH_INTERVAL); + await this.jobManager.register(); await this.jobManager.start(); } + + private applyMigrationPresence(client: Client): void { + client.user.setPresence({ + activities: [{ name: 'Custom Status', type: ActivityType.Custom, state: MIGRATION_STATUS_TEXT }], + }); + } } diff --git a/packages/bot/src/util/handleThreadManagement.ts b/packages/bot/src/util/handleThreadManagement.ts index fe2ea26..e2892ea 100644 --- a/packages/bot/src/util/handleThreadManagement.ts +++ b/packages/bot/src/util/handleThreadManagement.ts @@ -30,6 +30,7 @@ import { import i18next from 'i18next'; import { container } from 'tsyringe'; import { getSortedMemberRolesString } from './getSortedMemberRoles.js'; +import { buildMigrationNoticeEmbed } from './migrationNotice.js'; const promptTags = async ( input: ChatInputCommandInteraction | ContextMenuCommandInteraction | Message, @@ -198,6 +199,11 @@ export async function openThread( }); } + // The cutover notice leads, so it's the first thing staff see on opening a brand new thread -- the owner + // announcement DMs never reach the moderators actually working the queue. Temporary; delete this line (and + // `util/migrationNotice.ts`) once the new bot is live. See ChatSift/ChatSift#313. + const embeds = [buildMigrationNoticeEmbed(), embed]; + let startMessageOptions: GuildForumThreadCreateOptions | MessageCreateOptions; if (modmail.type === ChannelType.GuildForum) { const tags = modmail.availableTags.filter((tag) => !tag.moderated); @@ -208,11 +214,11 @@ export async function openThread( startMessageOptions = { name: `${member.user.username}-${member.user.discriminator}`, - message: { embeds: [embed] }, + message: { embeds }, appliedTags: tag ? [tag.id] : [], }; } else { - startMessageOptions = { embeds: [embed] }; + startMessageOptions = { embeds }; } if (isMessage) { diff --git a/packages/bot/src/util/migrationNotice.ts b/packages/bot/src/util/migrationNotice.ts new file mode 100644 index 0000000..9b19048 --- /dev/null +++ b/packages/bot/src/util/migrationNotice.ts @@ -0,0 +1,59 @@ +import { Colors, EmbedBuilder, time, TimestampStyles } from 'discord.js'; + +/** + * Temporary cutover comms for the move to the new ticket-based ModMail (ChatSift/ChatSift#313). + * + * This whole file and its two call sites (`events/ready.ts` for the bot status, `util/handleThreadManagement.ts` + * for the in-thread notice) are meant to be deleted once the new bot is live -- everything here is hardcoded + * on purpose rather than plumbed through `struct/Env.ts`, since it has a known expiry date. + * + * The owner announcement DMs (`scripts/announce-modmail-migration.mjs` over in ChatSift/ChatSift) only reach 21 + * guild owners; the moderators who actually run the bot day to day never see them, which is what the in-thread + * notice is for. + */ + +/** + * Start of the freeze window, in seconds: Mon 2026-08-24 18:00 EEST, i.e. `2026-08-24T15:00:00Z`. + * + * The announcement DMs are generated from the same instant (`MIGRATION_START_ISO=2026-08-24T15:00:00Z` passed to + * the script named above) -- if this ever moves, both have to move together, or owners and moderators end up + * looking at two different dates. + */ +export const MIGRATION_START_TS = 1_787_583_600; + +/** + * End of the freeze window and the point the migration itself runs: 48h after {@link MIGRATION_START_TS}, i.e. + * `2026-08-26T15:00:00Z`. Same 48h arithmetic the announcement script does. + */ +export const FREEZE_END_TS = MIGRATION_START_TS + 48 * 60 * 60; + +/** + * Custom status shown on the bot's profile. Discord doesn't render `` markup in a presence, so unlike the + * embed below this has to spell the date out. + */ +export const MIGRATION_STATUS_TEXT = '⚠️ New ModMail on Aug 24 — 48h thread freeze'; + +/** + * Posted as the first embed of every newly opened thread, ahead of the usual "who is this" info embed. Yellow + * rather than the info embed's `NotQuiteBlack` so the two don't read as one block. + */ +export function buildMigrationNoticeEmbed(): EmbedBuilder { + const start = time(MIGRATION_START_TS, TimestampStyles.LongDateTime); + const startRelative = time(MIGRATION_START_TS, TimestampStyles.RelativeTime); + const freezeEnd = time(FREEZE_END_TS, TimestampStyles.LongDateTime); + + return new EmbedBuilder() + .setColor(Colors.Yellow) + .setTitle('⚠️ ModMail is moving to a new system') + .setDescription( + [ + `On **${start}** (${startRelative}), ModMail switches to a new ticket-based system with a full web dashboard.`, + '', + '• For **48 hours** from that moment, **no new threads can be opened**. Threads already open keep working as normal.', + `• At **${freezeEnd}**, we force-close every open thread and migrate your full history over — nothing gets left behind.`, + "• After that you're live on the new ModMail. **Configuring a panel on the dashboard will be mandatory** to keep using the bot.", + '', + 'Questions? Join the support server: https://discord.gg/tgZ2pSgXXv', + ].join('\n'), + ); +}