From 34a726ff0db461a51cf452f3b0ef29e0ef962407 Mon Sep 17 00:00:00 2001 From: ianpike Date: Wed, 11 Mar 2026 23:07:40 -0400 Subject: [PATCH 1/3] Delay voice first-join notice until no mod present, DM first then channel --- .../components/voice-first-join-notice.ts | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/src/modules/tccpp/components/voice-first-join-notice.ts b/src/modules/tccpp/components/voice-first-join-notice.ts index 9806336d..ce60006a 100644 --- a/src/modules/tccpp/components/voice-first-join-notice.ts +++ b/src/modules/tccpp/components/voice-first-join-notice.ts @@ -21,6 +21,7 @@ export default class VoiceFirstJoinNotice extends BotComponent { wheatley_roles.voice, wheatley_roles.no_voice, wheatley_roles.server_booster, + wheatley_roles.voice_moderator, ); private database = this.wheatley.database.create_proxy<{ voice_first_join_notice: voice_first_join_notice_entry; @@ -51,6 +52,24 @@ export default class VoiceFirstJoinNotice extends BotComponent { return; } const member = new_state.member; + const channel = new_state.channel; + if (!channel) { + return; + } + + // Only record or notify on first join when no voice mod or ban-capable mod is present. + // If one is present, the user can be helped immediately; we delay the notice until they + // join with no moderator present. + const has_voice_mod_or_ban_moderator = [...channel.members.values()].some( + m => + m.id !== member.id && + (m.roles.cache.has(this.roles.voice_moderator.id) || + m.permissions.has(Discord.PermissionFlagsBits.BanMembers)), + ); + if (has_voice_mod_or_ban_moderator) { + return; + } + const res = await this.database.voice_first_join_notice.updateOne( { guild: new_state.guild.id, user: member.id }, { @@ -76,18 +95,20 @@ export default class VoiceFirstJoinNotice extends BotComponent { ) { return; } - const channel = new_state.channel; - if (!channel) { - return; + const message = + "new users are suppressed by default to protect our voice channels. " + + "You will be able to speak when joining a channel with a voice moderator present. " + + "Stick around and you will eventually be granted permanent voice access. " + + "__Please do not ping voice moderators to be unsupressed or for the voice role.__"; + // Try DM first; fall back to the voice channel if the user has DMs disabled. + // DM's will pierce the veil of a users inbox and is far more likely to be read. + try { + await member.send({ content: message }); + } catch { + await channel.send({ + content: `<@${member.id}> ` + message, + allowedMentions: { users: [member.id] }, + }); } - await channel.send({ - content: - `<@${member.id}> ` + - "new users are suppressed by default to protect our voice channels. " + - "You will be able to speak when joining a channel with a voice moderator present. " + - "Stick around and you will eventually be granted permanent voice access. " + - "__Please do not ping voice moderators to be unsupressed or for the voice role.__", - allowedMentions: { users: [member.id] }, - }); } } From 9c05f6ab30490ee145db0ccbf78b33d8201c9e90 Mon Sep 17 00:00:00 2001 From: ianpike Date: Wed, 11 Mar 2026 23:15:17 -0400 Subject: [PATCH 2/3] Only record voice join after notice sent --- .../components/voice-first-join-notice.ts | 50 +++++++++++-------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/src/modules/tccpp/components/voice-first-join-notice.ts b/src/modules/tccpp/components/voice-first-join-notice.ts index ce60006a..1d27a2cd 100644 --- a/src/modules/tccpp/components/voice-first-join-notice.ts +++ b/src/modules/tccpp/components/voice-first-join-notice.ts @@ -69,22 +69,6 @@ export default class VoiceFirstJoinNotice extends BotComponent { if (has_voice_mod_or_ban_moderator) { return; } - - const res = await this.database.voice_first_join_notice.updateOne( - { guild: new_state.guild.id, user: member.id }, - { - $setOnInsert: { - guild: new_state.guild.id, - user: member.id, - first_seen_at: new Date(), - first_channel: new_state.channelId, - }, - }, - { upsert: true }, - ); - if (res.upsertedCount === 0) { - return; - } if ( member.roles.cache.has(this.roles.voice.id) || member.roles.cache.has(this.roles.no_voice.id) || @@ -102,13 +86,39 @@ export default class VoiceFirstJoinNotice extends BotComponent { "__Please do not ping voice moderators to be unsupressed or for the voice role.__"; // Try DM first; fall back to the voice channel if the user has DMs disabled. // DM's will pierce the veil of a users inbox and is far more likely to be read. + let sent = false; try { await member.send({ content: message }); + sent = true; } catch { - await channel.send({ - content: `<@${member.id}> ` + message, - allowedMentions: { users: [member.id] }, - }); + try { + await channel.send({ + content: `<@${member.id}> ` + message, + allowedMentions: { users: [member.id] }, + }); + sent = true; + } catch { + sent = false; + } + } + if (!sent) { + return; + } + + const res = await this.database.voice_first_join_notice.updateOne( + { guild: new_state.guild.id, user: member.id }, + { + $setOnInsert: { + guild: new_state.guild.id, + user: member.id, + first_seen_at: new Date(), + first_channel: new_state.channelId, + }, + }, + { upsert: true }, + ); + if (res.upsertedCount === 0) { + return; } } } From 4bf0a1377c1b5b0dda00bd8942c80cf3e8b0db54 Mon Sep 17 00:00:00 2001 From: ianpike Date: Mon, 20 Jul 2026 14:36:59 -0400 Subject: [PATCH 3/3] Clean up the code and apply requested changes --- .../components/voice-first-join-notice.ts | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/modules/tccpp/components/voice-first-join-notice.ts b/src/modules/tccpp/components/voice-first-join-notice.ts index 1d27a2cd..37ea881f 100644 --- a/src/modules/tccpp/components/voice-first-join-notice.ts +++ b/src/modules/tccpp/components/voice-first-join-notice.ts @@ -57,9 +57,9 @@ export default class VoiceFirstJoinNotice extends BotComponent { return; } - // Only record or notify on first join when no voice mod or ban-capable mod is present. - // If one is present, the user can be helped immediately; we delay the notice until they - // join with no moderator present. + // Only record or notify when no voice mod or ban-capable mod is present. + // If one is present the user can be helped immediately, + // so we delay the notice until they join with no moderator present. const has_voice_mod_or_ban_moderator = [...channel.members.values()].some( m => m.id !== member.id && @@ -79,33 +79,34 @@ export default class VoiceFirstJoinNotice extends BotComponent { ) { return; } + const already_notified = await this.database.voice_first_join_notice.findOne({ + guild: new_state.guild.id, + user: member.id, + }); + if (already_notified) { + return; + } const message = "new users are suppressed by default to protect our voice channels. " + "You will be able to speak when joining a channel with a voice moderator present. " + "Stick around and you will eventually be granted permanent voice access. " + - "__Please do not ping voice moderators to be unsupressed or for the voice role.__"; - // Try DM first; fall back to the voice channel if the user has DMs disabled. - // DM's will pierce the veil of a users inbox and is far more likely to be read. - let sent = false; + "__Please do not ping voice moderators to be unsuppressed or for the voice role.__"; + // Try a DM first since it is far more likely to be read + // fall back to the voice channel if the user has DMs disabled. try { await member.send({ content: message }); - sent = true; } catch { try { await channel.send({ content: `<@${member.id}> ` + message, allowedMentions: { users: [member.id] }, }); - sent = true; } catch { - sent = false; + // Neither delivery worked, leave the user unrecorded so the next join retries + return; } } - if (!sent) { - return; - } - - const res = await this.database.voice_first_join_notice.updateOne( + await this.database.voice_first_join_notice.updateOne( { guild: new_state.guild.id, user: member.id }, { $setOnInsert: { @@ -117,8 +118,5 @@ export default class VoiceFirstJoinNotice extends BotComponent { }, { upsert: true }, ); - if (res.upsertedCount === 0) { - return; - } } }