Skip to content

Commit b8e36b2

Browse files
committed
feat: migration notices
1 parent db013af commit b8e36b2

3 files changed

Lines changed: 89 additions & 3 deletions

File tree

packages/bot/src/events/ready.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,40 @@
1+
import { clearInterval, setInterval } from 'node:timers';
12
import type { Client } from 'discord.js';
2-
import { Events } from 'discord.js';
3+
import { ActivityType, Events } from 'discord.js';
34
import { singleton } from 'tsyringe';
45
import type { Event } from '../struct/Event.js';
56
import { JobManager } from '../struct/JobManager.js';
67
import { logger } from '../util/logger.js';
8+
import { MIGRATION_STATUS_TEXT } from '../util/migrationNotice.js';
9+
10+
const PRESENCE_REFRESH_INTERVAL = 60 * 60 * 1_000;
711

812
@singleton()
913
export default class implements Event<typeof Events.ClientReady> {
1014
public readonly name = Events.ClientReady;
1115

16+
private presenceRefreshInterval: NodeJS.Timeout | null = null;
17+
1218
public constructor(private readonly jobManager: JobManager) {}
1319

1420
public async handle(client: Client<true>) {
1521
logger.info(`Ready as ${client.user.tag} (${client.user.id})`);
22+
23+
// See ChatSift/ChatSift#313 -- temporary, remove alongside `util/migrationNotice.ts` post-cutover.
24+
this.applyMigrationPresence(client);
25+
if (this.presenceRefreshInterval) {
26+
clearInterval(this.presenceRefreshInterval);
27+
}
28+
29+
this.presenceRefreshInterval = setInterval(() => this.applyMigrationPresence(client), PRESENCE_REFRESH_INTERVAL);
30+
1631
await this.jobManager.register();
1732
await this.jobManager.start();
1833
}
34+
35+
private applyMigrationPresence(client: Client<true>): void {
36+
client.user.setPresence({
37+
activities: [{ name: 'Custom Status', type: ActivityType.Custom, state: MIGRATION_STATUS_TEXT }],
38+
});
39+
}
1940
}

packages/bot/src/util/handleThreadManagement.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
import i18next from 'i18next';
3131
import { container } from 'tsyringe';
3232
import { getSortedMemberRolesString } from './getSortedMemberRoles.js';
33+
import { buildMigrationNoticeEmbed } from './migrationNotice.js';
3334

3435
const promptTags = async (
3536
input: ChatInputCommandInteraction | ContextMenuCommandInteraction | Message,
@@ -198,6 +199,11 @@ export async function openThread(
198199
});
199200
}
200201

202+
// The cutover notice leads, so it's the first thing staff see on opening a brand new thread -- the owner
203+
// announcement DMs never reach the moderators actually working the queue. Temporary; delete this line (and
204+
// `util/migrationNotice.ts`) once the new bot is live. See ChatSift/ChatSift#313.
205+
const embeds = [buildMigrationNoticeEmbed(), embed];
206+
201207
let startMessageOptions: GuildForumThreadCreateOptions | MessageCreateOptions;
202208
if (modmail.type === ChannelType.GuildForum) {
203209
const tags = modmail.availableTags.filter((tag) => !tag.moderated);
@@ -208,11 +214,11 @@ export async function openThread(
208214

209215
startMessageOptions = {
210216
name: `${member.user.username}-${member.user.discriminator}`,
211-
message: { embeds: [embed] },
217+
message: { embeds },
212218
appliedTags: tag ? [tag.id] : [],
213219
};
214220
} else {
215-
startMessageOptions = { embeds: [embed] };
221+
startMessageOptions = { embeds };
216222
}
217223

218224
if (isMessage) {
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Colors, EmbedBuilder, time, TimestampStyles } from 'discord.js';
2+
3+
/**
4+
* Temporary cutover comms for the move to the new ticket-based ModMail (ChatSift/ChatSift#313).
5+
*
6+
* This whole file and its two call sites (`events/ready.ts` for the bot status, `util/handleThreadManagement.ts`
7+
* for the in-thread notice) are meant to be deleted once the new bot is live -- everything here is hardcoded
8+
* on purpose rather than plumbed through `struct/Env.ts`, since it has a known expiry date.
9+
*
10+
* The owner announcement DMs (`scripts/announce-modmail-migration.mjs` over in ChatSift/ChatSift) only reach 21
11+
* guild owners; the moderators who actually run the bot day to day never see them, which is what the in-thread
12+
* notice is for.
13+
*/
14+
15+
/**
16+
* Start of the freeze window, in seconds: Mon 2026-08-24 18:00 EEST, i.e. `2026-08-24T15:00:00Z`.
17+
*
18+
* The announcement DMs are generated from the same instant (`MIGRATION_START_ISO=2026-08-24T15:00:00Z` passed to
19+
* the script named above) -- if this ever moves, both have to move together, or owners and moderators end up
20+
* looking at two different dates.
21+
*/
22+
export const MIGRATION_START_TS = 1_787_583_600;
23+
24+
/**
25+
* End of the freeze window and the point the migration itself runs: 48h after {@link MIGRATION_START_TS}, i.e.
26+
* `2026-08-26T15:00:00Z`. Same 48h arithmetic the announcement script does.
27+
*/
28+
export const FREEZE_END_TS = MIGRATION_START_TS + 48 * 60 * 60;
29+
30+
/**
31+
* Custom status shown on the bot's profile. Discord doesn't render `<t:...>` markup in a presence, so unlike the
32+
* embed below this has to spell the date out.
33+
*/
34+
export const MIGRATION_STATUS_TEXT = '⚠️ New ModMail on Aug 24 — 48h thread freeze';
35+
36+
/**
37+
* Posted as the first embed of every newly opened thread, ahead of the usual "who is this" info embed. Yellow
38+
* rather than the info embed's `NotQuiteBlack` so the two don't read as one block.
39+
*/
40+
export function buildMigrationNoticeEmbed(): EmbedBuilder {
41+
const start = time(MIGRATION_START_TS, TimestampStyles.LongDateTime);
42+
const startRelative = time(MIGRATION_START_TS, TimestampStyles.RelativeTime);
43+
const freezeEnd = time(FREEZE_END_TS, TimestampStyles.LongDateTime);
44+
45+
return new EmbedBuilder()
46+
.setColor(Colors.Yellow)
47+
.setTitle('⚠️ ModMail is moving to a new system')
48+
.setDescription(
49+
[
50+
`On **${start}** (${startRelative}), ModMail switches to a new ticket-based system with a full web dashboard.`,
51+
'',
52+
'• For **48 hours** from that moment, **no new threads can be opened**. Threads already open keep working as normal.',
53+
`• At **${freezeEnd}**, we force-close every open thread and migrate your full history over — nothing gets left behind.`,
54+
"• After that you're live on the new ModMail. **Configuring a panel on the dashboard will be mandatory** to keep using the bot.",
55+
'',
56+
'Questions? Join the support server: https://discord.gg/tgZ2pSgXXv',
57+
].join('\n'),
58+
);
59+
}

0 commit comments

Comments
 (0)