Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add temporary bans & fix unmute not unmuting #916

Merged
merged 5 commits into from
Dec 24, 2024
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
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
Loading