Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion packages/bot/src/events/ready.ts
Original file line number Diff line number Diff line change
@@ -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<typeof Events.ClientReady> {
public readonly name = Events.ClientReady;

private presenceRefreshInterval: NodeJS.Timeout | null = null;

public constructor(private readonly jobManager: JobManager) {}

public async handle(client: Client<true>) {
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<true>): void {
client.user.setPresence({
activities: [{ name: 'Custom Status', type: ActivityType.Custom, state: MIGRATION_STATUS_TEXT }],
});
}
}
10 changes: 8 additions & 2 deletions packages/bot/src/util/handleThreadManagement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand Down
59 changes: 59 additions & 0 deletions packages/bot/src/util/migrationNotice.ts
Original file line number Diff line number Diff line change
@@ -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 `<t:...>` 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'),
);
}
Loading