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

improve ban command input #528

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
35 changes: 27 additions & 8 deletions src/commandDetails/admin/ban.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
import { banUser } from '../../components/admin';
import { vars } from '../../config';
import { DEFAULT_EMBED_COLOUR } from '../../utils/embeds.js';
import { pluralize } from '../../utils/pluralize';
import { DurationStyle, formatDuration } from '../../utils/formatDuration.js';
import { parseDuration } from '../../utils/parseDuration.js';
import { CodeyUserError } from './../../codeyUserError';

const NOTIF_CHANNEL_ID: string = vars.NOTIF_CHANNEL_ID;
Expand All @@ -35,10 +36,22 @@ const banExecuteCommand: SapphireMessageExecuteType = async (client, messageFrom
'please enter a valid reason why you are banning the user.',
);
}
const days = <number>args['days'];
const duration = parseDuration(<string>args['duration']);

if (duration === null) {
throw new CodeyUserError(
messageFromUser,
'please enter a valid duration (e.g. 7d, 3h, 1h30m).',
);
}

if (duration > 7 * 24 * 60 * 60 * 1000) {
throw new CodeyUserError(messageFromUser, 'cannot purge more than 7 days of messages.');
}

// get Guild object corresponding to server
const guild = await client.guilds.fetch(vars.TARGET_GUILD_ID);
if (await banUser(guild, user, reason, days)) {
if (await banUser(guild, user, reason, duration)) {
const mod = getUserFromMessage(messageFromUser);
const banEmbed = new EmbedBuilder()
.setTitle('Ban')
Expand All @@ -52,14 +65,19 @@ const banExecuteCommand: SapphireMessageExecuteType = async (client, messageFrom
{ name: 'Reason', value: reason },
{
name: 'Messages Purged',
value: !days ? 'None' : `Past ${days} ${pluralize('day', days)}`,
value: !duration ? 'None' : `Past ${formatDuration(duration, DurationStyle.Blank)}`,
},
]);
(client.channels.cache.get(NOTIF_CHANNEL_ID) as TextChannel).send({
embeds: [banEmbed],
});
return `Successfully banned user ${user.tag} (id: ${user.id}) ${
days ? `and deleted their messages in the past ${days} ${pluralize('day', days)} ` : ``
duration
? `and deleted their messages in the past ${formatDuration(
duration,
DurationStyle.Blank,
)} `
: ``
}for the following reason: ${reason}`;
} else {
throw new CodeyUserError(
Expand Down Expand Up @@ -98,9 +116,10 @@ export const banCommandDetails: CodeyCommandDetails = {
required: true,
},
{
name: 'days',
description: "Messages in last 'days' days from user are deleted. Default is 0 days.",
type: CodeyCommandOptionType.INTEGER,
name: 'duration',
description:
'Messages within the specified time (e.g. 1 day, 2h30m) from user are deleted. Default 0d, max 7d.',
type: CodeyCommandOptionType.STRING,
required: false,
},
],
Expand Down
17 changes: 11 additions & 6 deletions src/components/admin.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { Guild, User } from 'discord.js';
import { vars } from '../config';
import { logger } from '../logger/default';
import { pluralize } from '../utils/pluralize';
import { DurationStyle, formatDuration } from '../utils/formatDuration.js';

const MOD_USER_ID_FOR_BAN_APPEAL: string = vars.MOD_USER_ID_FOR_BAN_APPEAL;

/* Make ban message */
const makeBanMessage = (reason: string, days?: number): string =>
const makeBanMessage = (reason: string, duration?: number): string =>
`
Uh oh, you have been banned from the UW Computer Science Club server ${
days ? `and your messages in the past ${days} ${pluralize('day', days)} have been deleted ` : ''
duration
? `and your messages in the past ${formatDuration(
duration,
DurationStyle.Blank,
)} have been deleted `
: ''
}for the following reason:

> ${reason}
Expand All @@ -25,12 +30,12 @@ export const banUser = async (
guild: Guild,
user: User,
reason: string,
days?: number,
duration?: number,
): Promise<boolean> => {
let isSuccessful = false;
try {
try {
await user.send(makeBanMessage(reason, days));
await user.send(makeBanMessage(reason, duration));
} catch (err) {
logger.error({
event: "Can't send message to user not in server",
Expand All @@ -39,7 +44,7 @@ export const banUser = async (
}
await guild.members.ban(user, {
reason: reason,
deleteMessageSeconds: days == null ? 0 : days * 86400,
deleteMessageSeconds: duration === undefined ? 0 : Math.floor(duration / 1000),
});
isSuccessful = true;
} catch (err) {
Expand Down
61 changes: 61 additions & 0 deletions src/utils/formatDuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Formats a duration in milliseconds into English text.
* @param duration - The duration in milliseconds
* @param style - The style to format the duration in
* @returns The formatted duration
*/
export const formatDuration = (duration: number, style = DurationStyle.For): string => {
if (duration === Infinity) return 'indefinitely';

duration = Math.round(duration / 1000);

if (duration < 0) {
const core = _formatDuration(-duration);
if (style === DurationStyle.Blank) return `negative ${core}`;
if (style === DurationStyle.For) return `for negative ${core}`;
if (style === DurationStyle.Until) return `until ${core} ago`;
}

if (duration === 0) {
if (style === DurationStyle.Blank) return 'no time';
if (style === DurationStyle.For) return 'for no time';
if (style === DurationStyle.Until) return 'until right now';
}

const core = _formatDuration(duration);
if (style === DurationStyle.Blank) return core;
if (style === DurationStyle.For) return `for ${core}`;
if (style === DurationStyle.Until) return `until ${core} from now`;

return '??';
};

function _formatDuration(duration: number): string {
if (duration === Infinity) return 'indefinitely';

const parts: string[] = [];

for (const [name, scale] of formatTimescales) {
if (duration >= scale) {
const amount = Math.floor(duration / scale);
duration %= scale;

parts.push(`${amount} ${name}${amount === 1 ? '' : 's'}`);
}
}

return parts.join(' ');
}

export enum DurationStyle {
Blank,
For,
Until,
}

const formatTimescales: [string, number][] = [
['day', 86400],
['hour', 3600],
['minute', 60],
['second', 1],
];
31 changes: 31 additions & 0 deletions src/utils/parseDuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Parses English text into a duration in milliseconds. Works for long for (1 day,
* 3 weeks 2 hours) and short form (1d, 3w2h).
*
* @param text - The text to parse
* @returns The duration in milliseconds or null if invalid
*/
export const parseDuration = (text: string): number | null => {
const match = text.match(
/^(\d+\s*w(eeks?)?\s*)?(\d+\s*d(ays?)?\s*)?(\d+\s*h((ou)?rs?)?\s*)?(\d+\s*m(in(ute)?s?)?\s*)?(\d+\s*s(ec(ond)?s?)?\s*)?$/,
);

if (!match) return null;

let duration = 0;

for (const [index, scale] of parseTimescales) {
const submatch = match[index]?.match(/\d+/);
if (submatch) duration += parseInt(submatch[0]) * scale;
}

return duration;
};

const parseTimescales = [
[1, 604800000],
[3, 86400000],
[5, 3600000],
[8, 60000],
[11, 1000],
];
Loading