Skip to content

Commit

Permalink
Add temporary bans & fix unmute not unmuting (#916)
Browse files Browse the repository at this point in the history
* Add temporary bans to UI, timed unbanning to timer, alter user table to include ban end date

* Input validation for timeout & ban. Better formatting for them too. 

* Fixed months not working.

---------

Co-authored-by: theimperious1 <[email protected]>
  • Loading branch information
theimperious1 and theimperious1 authored Dec 24, 2024
1 parent b2c986c commit c298154
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 36 deletions.
88 changes: 54 additions & 34 deletions src/discord/commands/guild/d.moderate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import { stripIndents } from 'common-tags';
import { user_action_type, user_actions, users } from '@prisma/client';
import moment from 'moment';
import { SlashCommand } from '../../@types/commandDef';
import { parseDuration } from '../../../global/utils/parseDuration';
import { parseDuration, validateDurationInput } from '../../../global/utils/parseDuration';
import commandContext from '../../utils/context'; // eslint-disable-line
import { getDiscordMember } from '../../utils/guildMemberLookup';
import { embedTemplate } from '../../utils/embedTemplate';
Expand Down Expand Up @@ -1387,50 +1387,56 @@ export async function moderate(
}

// Process duration time for ban and timeouts
let duration = 0 as null | number;
let banEndTime = null;
let actionDuration = 0 as null | number;
let durationStr = '';
if (isTimeout(command)) {
// log.debug(F, 'Parsing timeout duration');
let durationVal = modalInt.fields.getTextInputValue('duration');
if (durationVal === '') durationVal = '7 days';
// log.debug(F, `durationVal: ${durationVal}`);
if (durationVal.length === 1) {
// If the input is a single number, assume it's days
duration = parseInt(durationVal, 10);
if (Number.isNaN(duration)) {
return { content: 'Timeout must be a number!' };
}
if (duration < 0 || duration > 7) {
return { content: 'Timeout must be between 0 and 7 days!' };
}
durationVal = `${duration} days`;
if (durationVal === '') durationVal = '7d';

if (!validateDurationInput(durationVal)) {
return {
content: 'Timeout duration must include at least one of seconds, minutes, hours, days, or a week. For example: 5d 5h 5m 5s, 1w or 5d.',
};
}

duration = await parseDuration(durationVal);
if (duration && (duration < 0 || duration > 7 * 24 * 60 * 60 * 1000)) {
return { content: 'Timeout must be between 0 and 7 days!!' };
actionDuration = await parseDuration(durationVal);
if (actionDuration && (actionDuration < 0 || actionDuration > 7 * 24 * 60 * 60 * 1000)) {
return { content: 'Timeout must be between 0 and 7 days!' };
}

// convert the milliseconds into a human readable string
const humanTime = msToHuman(duration);
const humanTime = msToHuman(actionDuration);

durationStr = `for ${humanTime}. It will expire ${time(new Date(Date.now() + duration), 'R')}`;
durationStr = ` for ${humanTime}. It will expire ${time(new Date(Date.now() + actionDuration), 'R')}`;
// log.debug(F, `duration: ${duration}`);
}
if (isFullBan(command)) {
// If the command is ban, then the input value exists, so pull that and try to parse it as an int
let dayInput = parseInt(modalInt.fields.getTextInputValue('days'), 10);
const durationVal = modalInt.fields.getTextInputValue('ban_duration');

// If no input was provided, default to 0 days
if (Number.isNaN(dayInput)) dayInput = 0;
if (durationVal !== '') {
let tempBanDuration = parseInt(durationVal, 10);

// If the input is a string, or outside the bounds, tell the user and return
if (dayInput && (dayInput < 0 || dayInput > 7)) {
return { content: 'Ban days must be at least 0 and at most 7!' };
}
if (Number.isNaN(tempBanDuration)) {
return { content: 'Ban duration must be a number!' };
}

// Get the millisecond value of the input
duration = await parseDuration(`${dayInput} days`);
if (!validateDurationInput(durationVal)) {
return {
content: 'Ban duration must include at least one of seconds, minutes, hours, days, weeks, months, or years. For example: 1yr 1M 1w 1d 1h 1m 1s',
};
}

tempBanDuration = await parseDuration(durationVal);
if (tempBanDuration && tempBanDuration < 0) {
return { content: 'Ban duration must be at least 1 second!' };
}

durationStr = `${time(new Date(Date.now() + tempBanDuration), 'R')}`;
const currentTime = new Date();
banEndTime = tempBanDuration !== 0 ? new Date(currentTime.getTime() + tempBanDuration) : null;
}
}

// Display all properties we're going to use
Expand All @@ -1440,7 +1446,7 @@ export async function moderate(
targetId: ${targetId}
internalNote: ${internalNote}
description: ${description}
duration: ${duration}
duration: ${actionDuration}
durationStr: ${durationStr}
`);

Expand Down Expand Up @@ -1532,10 +1538,12 @@ export async function moderate(

if (command === 'FULL_BAN') {
internalNote += `\n **Actioned by:** ${actor.displayName}`;
internalNote += `\n **Ban Ends:** ${durationStr || 'Never'}`;
}

let actionData = {
user_id: targetData.id,
target_discord_id: targetData.discord_id,
guild_id: actor.guild.id,
type: command.includes('UN-') ? command.slice(3) : command,
ban_evasion_related_user: null as string | null,
Expand All @@ -1553,7 +1561,7 @@ export async function moderate(
if (isBan(command)) {
if (isFullBan(command) || isUnderban(command) || isBanEvasion(command)) {
targetData.removed_at = new Date();
const deleteMessageValue = duration ?? 0;
const deleteMessageValue = actionDuration ?? 0;
try {
if (deleteMessageValue > 0 && targetMember) {
// log.debug(F, `I am deleting ${deleteMessageValue} days of messages!`);
Expand All @@ -1568,6 +1576,10 @@ export async function moderate(

try {
targetObj = await buttonInt.guild.bans.create(targetId, { deleteMessageSeconds: deleteMessageValue / 1000, reason: internalNote ?? noReason });
// Set ban duration if present
if (banEndTime !== null) {
actionData.expires_at = banEndTime;
}
} catch (err) {
log.error(F, `Error: ${err}`);
}
Expand Down Expand Up @@ -1617,11 +1629,12 @@ export async function moderate(
}
actionData.repealed_at = new Date();
actionData.repealed_by = actorData.id;
actionData.expires_at = null;
} else if (isTimeout(command)) {
if (targetMember) {
actionData.expires_at = new Date(Date.now() + (duration as number));
actionData.expires_at = new Date(Date.now() + (actionDuration as number));
try {
await targetMember.timeout(duration, internalNote ?? noReason);
await targetMember.timeout(actionDuration, internalNote ?? noReason);
} catch (err) {
log.error(F, `Error: ${err}`);
}
Expand Down Expand Up @@ -1649,7 +1662,7 @@ export async function moderate(
actionData.repealed_by = actorData.id;

try {
await targetMember.timeout(0, internalNote ?? noReason);
await targetMember.timeout(null, internalNote ?? noReason);
// log.debug(F, `I untimeout ${target.displayName} because\n '${internalNote}'!`);
} catch (err) {
log.error(F, `Error: ${err}`);
Expand Down Expand Up @@ -1943,6 +1956,13 @@ export async function modModal(
.setPlaceholder('4 days 3hrs 2 mins 30 seconds (Max 7 days, Default 0 days)')
.setRequired(false)
.setCustomId('days')));
modal.addComponents(new ActionRowBuilder<TextInputBuilder>()
.addComponents(new TextInputBuilder()
.setLabel('How long should they be banned for?')
.setStyle(TextInputStyle.Short)
.setPlaceholder('365 days (Empty = Permanent)')
.setRequired(false)
.setCustomId('ban_duration')));
}

// When the modal is opened, disable the button on the embed
Expand Down
9 changes: 7 additions & 2 deletions src/global/utils/parseDuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default parseDuration;
export async function parseDuration(duration:string):Promise<number> {
// Those code inspired by https://gist.github.com/substanc3-dev/306bb4d04b2aad3a5d019052b1a0dec0
// This is super cool, thanks a lot!
const supported = 'smhdwmoy';
const supported = 'smMhdwmoy';
const numbers = '0123456789';
let stage = 1;
let idx = 0;
Expand Down Expand Up @@ -53,7 +53,7 @@ export async function parseDuration(duration:string):Promise<number> {
if (c === 'h') {
timeValue += tempNumber * 60 * 60 * 1000;
}
if (c === 'mo') {
if (c === 'M') {
timeValue += tempNumber * 30 * 24 * 60 * 60 * 1000;
}
if (c === 'm') {
Expand Down Expand Up @@ -81,3 +81,8 @@ export async function parseDuration(duration:string):Promise<number> {
}
return timeValue;
}

export const validateDurationInput = (input: string): boolean => {
const regex = /^(\d+(yr|M|w|d|h|m|s)\s?)+$/;
return regex.test(input.trim());
};
44 changes: 44 additions & 0 deletions src/global/utils/timer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,49 @@ async function checkMoodle() { // eslint-disable-line
// }
}

async function undoExpiredBans() {
const expiredBans = await db.user_actions.findMany({
where: {
expires_at: {
not: null, // Ensure the ban duration is set (i.e., not null, indicating a ban exists)
lte: new Date(), // Fetch users whose ban duration is in the past (expired bans)
},
type: 'FULL_BAN',
},
});

if (expiredBans.length > 0) {
// Get the tripsit guild
const tripsitGuild = await global.discordClient.guilds.fetch(env.DISCORD_GUILD_ID);
expiredBans.forEach(async activeBan => {
// Check if the reminder is ready to be triggered
if (activeBan.target_discord_id !== null) {
const user = await global.discordClient.users.fetch(activeBan.target_discord_id);
if (user) {
// Unban them
try {
await tripsitGuild.bans.remove(user, 'Temporary ban expired');
log.info(F, `Temporary ban for ${activeBan.target_discord_id} has expired and been lifted!`);
} catch (err) {
// If this error is ever encountered then something in our flow is probably wrong. This should never happen.
log.error(F, `Failed to remove temporary ban on ${activeBan.target_discord_id}. Likely already unbanned.`);
} finally {
// Reset expires_at flag to null
await db.user_actions.update({
where: {
id: activeBan.id,
},
data: {
expires_at: null, // Reset the ban duration to null
},
});
}
}
}
});
}
}

async function checkEvery(
callback: () => Promise<void>,
interval: number,
Expand Down Expand Up @@ -1161,6 +1204,7 @@ async function runTimer() {
{ callback: checkMoodle, interval: env.NODE_ENV === 'production' ? seconds60 : seconds5 },
// { callback: checkLpm, interval: env.NODE_ENV === 'production' ? seconds10 : seconds5 },
{ callback: updateDb, interval: env.NODE_ENV === 'production' ? hours24 : hours48 },
{ callback: undoExpiredBans, interval: env.NODE_ENV === 'production' ? hours24 : seconds10 },
];

timers.forEach(timer => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "user_actions" ADD COLUMN "target_discord_id" TEXT;
1 change: 1 addition & 0 deletions src/prisma/tripbot/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ model rpg_inventory {
model user_actions {
id String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
user_id String @db.Uuid
target_discord_id String?
guild_id String @default("179641883222474752")
type user_action_type
ban_evasion_related_user String? @db.Uuid
Expand Down

0 comments on commit c298154

Please sign in to comment.