From 960fde527d39a21f6fc9549c8c7c56a2f796e3c8 Mon Sep 17 00:00:00 2001 From: CartucheraSB Date: Sat, 23 May 2026 04:19:55 -0300 Subject: [PATCH 1/2] fix: remove blacklist roles on automatic ban expiry --- src/utils/automaticUnbans.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/utils/automaticUnbans.ts b/src/utils/automaticUnbans.ts index f7e8499..6a3621e 100644 --- a/src/utils/automaticUnbans.ts +++ b/src/utils/automaticUnbans.ts @@ -14,7 +14,17 @@ export async function automaticUnban(ban: Bans) { await sendDm(userId, moderationMessages.banLiftedDm({ expired: true })) const guild = await getGuild() - const username = (await guild.members.fetch(userId))?.displayName ?? userId + // Added member fetch to remove blacklisted roles when unbanning a user whose ban has expired. + const member = await guild.members.fetch(userId).catch(() => null) + const username = member?.displayName ?? userId + + if (member) { + await Promise.all([ + member.roles.remove('1354296037094854788'), + member.roles.remove('1344793211146600530'), + ]) +} + // log ban removal const embedType = createEmbedType( From 3f38ac71e0a3cc332fb7cbadf772c4992b834ebe Mon Sep 17 00:00:00 2001 From: CartucheraSB Date: Sun, 24 May 2026 01:43:34 -0300 Subject: [PATCH 2/2] feat: add remove-manual-user subcommand to bypass large db query on autocomplete --- src/commands/superCommands/strike.ts | 48 ++++++++++++--- src/utils/Autocompletions.ts | 89 ++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 7 deletions(-) diff --git a/src/commands/superCommands/strike.ts b/src/commands/superCommands/strike.ts index 2cedc92..eda6454 100644 --- a/src/commands/superCommands/strike.ts +++ b/src/commands/superCommands/strike.ts @@ -7,7 +7,7 @@ import { import giveStrike from '../moderation/playerModeration/giveStrike' import removeStrike from '../moderation/playerModeration/removeStrike' -import { strikeAutocomplete } from '../../utils/Autocompletions' +import { strikeAutocomplete, strikeAutocompleteByUser } from '../../utils/Autocompletions' export default { data: new SlashCommandBuilder() @@ -86,18 +86,52 @@ export default { .setRequired(false) .setMaxLength(500), ), + ) + .addSubcommand((sub) => + sub + .setName('remove-manual-user') + .setDescription('[HELPER] Remove strike(s) from a user without user name autocomplete') + .addStringOption((option) => + option + .setName('user') + .setDescription('The user to remove strike(s) from') + .setRequired(true), + ) + .addStringOption((option) => + option + .setName('strike') + .setDescription('Strike to remove') + .setRequired(true) + .setAutocomplete(true) + .setMaxLength(500), + ) + .addStringOption((option) => + option + .setName('reason') + .setDescription('Reason for removing strike') + .setRequired(false) + .setMaxLength(500), + ), ), + + async execute(interaction: ChatInputCommandInteraction) { - if (interaction.options.getSubcommand() === 'give') { - await giveStrike.execute(interaction) - } else if (interaction.options.getSubcommand() === 'remove') { - await removeStrike.execute(interaction) - } - }, + if (interaction.options.getSubcommand() === 'give') { + await giveStrike.execute(interaction) + } else if (interaction.options.getSubcommand() === 'remove') { + await removeStrike.execute(interaction) + } else if (interaction.options.getSubcommand() === 'remove-manual-user') { + await removeStrike.execute(interaction) + } +}, async autocomplete(interaction: AutocompleteInteraction) { + if (interaction.options.getSubcommand() === 'remove-manual-user') { + await strikeAutocompleteByUser(interaction) + } else { await strikeAutocomplete(interaction) + } }, } // this supercommand should only be usable by helper+ diff --git a/src/utils/Autocompletions.ts b/src/utils/Autocompletions.ts index bdb6fc3..a76087e 100644 --- a/src/utils/Autocompletions.ts +++ b/src/utils/Autocompletions.ts @@ -165,6 +165,95 @@ export async function strikeAutocomplete(interaction: AutocompleteInteraction) { } } +export async function strikeAutocompleteByUser(interaction: AutocompleteInteraction) { +try { + const focused = interaction.options.getFocused(true) + const name = focused.name + const value = String(focused.value ?? '') + + // only handles the strike field since user is a plain mention + if (name === 'strike') { + const raw = interaction.options.getString('user') ?? '' + const selectedUser = raw.match(/^<@!?(\d+)>$/)?.[1] ?? raw + + if (!selectedUser) { + await interaction.respond([ + { name: 'select a user first', value: 'select_user_first' }, + ]) + return + } + // query directly by user ID — bypassing getAllStrikedUsers call which makes autocomplete fail + const strikes = await strikeUtils.getUserStrikes(selectedUser) + + if (!strikes || strikes.length === 0) { + await interaction.respond([ + { name: 'This user has no strikes', value: 'no_strikes_found' }, + ]) + return + } + + const issuerIds = [ + ...new Set(strikes.map((s: any) => s.issued_by_id).filter(Boolean)), + ] + + const issuers = await Promise.all( + issuerIds.map((id: string) => fetchUserSafe(id)), + ) + + const issuerMap = new Map() + issuerIds.forEach((id, i) => + issuerMap.set(id, issuers[i]?.username ?? id), + ) + const qraw = value.trim() + const q = qraw.toLowerCase() + let filtered = strikes as any[] + + // same filtering logic as original + const m = q.match(prefixed) + if (m) { + const key = m[1].toLowerCase() + const val = m[2].trim() + if (key === 'id') { + filtered = strikes.filter((s) => String(s.id).includes(val)) + } else if (key === 'reason') { + const v = val.toLowerCase() + filtered = strikes.filter((s) => + (s.reason || '').toLowerCase().includes(v), + ) + } + } else if (digits.test(q)) { + filtered = strikes.filter((s) => String(s.id).includes(q)) + } else if (q.length > 0) { + filtered = strikes.filter((s) => { + const issuer = (issuerMap.get(s.issued_by_id) || '').toLowerCase() + const reason = (s.reason || '').toLowerCase() + const ref = (s.reference || '').toLowerCase() + return issuer.includes(q) || reason.includes(q) || ref.includes(q) + }) + } + const choices = filtered.slice(0, 25).map((s) => { + const issuer = issuerMap.get(s.issued_by_id) || s.issued_by_id + const refLabel = s.reference || 'no channel' + const issued = s.issued_at ? new Date(s.issued_at) : null + const stamp = issued ? issued.toISOString().slice(0, 10) : '' + const label = `#${s.id} by ${issuer} · ${ell(s.reason || '', 40)} · ${refLabel} · ${stamp}` + return { name: ell(label, 100), value: String(s.id) } + }) + + await interaction.respond( + choices.length ? choices : [{ name: 'no matches', value: 'none' }], + ) + return + } + + await interaction.respond([]) + } catch (err) { + console.error('autocomplete error:', err) + if (!interaction.responded) await interaction.respond([]) + } +} + + const BAN_REASON_PRESETS = [ 'Repeated offenses', 'Severe harassment',