Skip to content

Commit 7f7aa30

Browse files
author
hyper-neutrino
committed
improve ban command input
1 parent 41a2dab commit 7f7aa30

File tree

4 files changed

+130
-14
lines changed

4 files changed

+130
-14
lines changed

Diff for: src/commandDetails/admin/ban.ts

+27-8
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import {
99
import { banUser } from '../../components/admin';
1010
import { vars } from '../../config';
1111
import { DEFAULT_EMBED_COLOUR } from '../../utils/embeds.js';
12-
import { pluralize } from '../../utils/pluralize';
12+
import { DurationStyle, formatDuration } from '../../utils/formatDuration.js';
13+
import { parseDuration } from '../../utils/parseDuration.js';
1314
import { CodeyUserError } from './../../codeyUserError';
1415

1516
const NOTIF_CHANNEL_ID: string = vars.NOTIF_CHANNEL_ID;
@@ -35,10 +36,22 @@ const banExecuteCommand: SapphireMessageExecuteType = async (client, messageFrom
3536
'please enter a valid reason why you are banning the user.',
3637
);
3738
}
38-
const days = <number>args['days'];
39+
const duration = parseDuration(<string>args['duration']);
40+
41+
if (duration === null) {
42+
throw new CodeyUserError(
43+
messageFromUser,
44+
'please enter a valid duration (e.g. 7d, 3h, 1h30m).',
45+
);
46+
}
47+
48+
if (duration > 7 * 24 * 60 * 60 * 1000) {
49+
throw new CodeyUserError(messageFromUser, 'cannot purge more than 7 days of messages.');
50+
}
51+
3952
// get Guild object corresponding to server
4053
const guild = await client.guilds.fetch(vars.TARGET_GUILD_ID);
41-
if (await banUser(guild, user, reason, days)) {
54+
if (await banUser(guild, user, reason, duration)) {
4255
const mod = getUserFromMessage(messageFromUser);
4356
const banEmbed = new EmbedBuilder()
4457
.setTitle('Ban')
@@ -52,14 +65,19 @@ const banExecuteCommand: SapphireMessageExecuteType = async (client, messageFrom
5265
{ name: 'Reason', value: reason },
5366
{
5467
name: 'Messages Purged',
55-
value: !days ? 'None' : `Past ${days} ${pluralize('day', days)}`,
68+
value: !duration ? 'None' : `Past ${formatDuration(duration, DurationStyle.Blank)}`,
5669
},
5770
]);
5871
(client.channels.cache.get(NOTIF_CHANNEL_ID) as TextChannel).send({
5972
embeds: [banEmbed],
6073
});
6174
return `Successfully banned user ${user.tag} (id: ${user.id}) ${
62-
days ? `and deleted their messages in the past ${days} ${pluralize('day', days)} ` : ``
75+
duration
76+
? `and deleted their messages in the past ${formatDuration(
77+
duration,
78+
DurationStyle.Blank,
79+
)} `
80+
: ``
6381
}for the following reason: ${reason}`;
6482
} else {
6583
throw new CodeyUserError(
@@ -98,9 +116,10 @@ export const banCommandDetails: CodeyCommandDetails = {
98116
required: true,
99117
},
100118
{
101-
name: 'days',
102-
description: "Messages in last 'days' days from user are deleted. Default is 0 days.",
103-
type: CodeyCommandOptionType.INTEGER,
119+
name: 'duration',
120+
description:
121+
'Messages within the specified time (e.g. 1 day, 2h30m) from user are deleted. Default 0d, max 7d.',
122+
type: CodeyCommandOptionType.STRING,
104123
required: false,
105124
},
106125
],

Diff for: src/components/admin.ts

+11-6
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
import { Guild, User } from 'discord.js';
22
import { vars } from '../config';
33
import { logger } from '../logger/default';
4-
import { pluralize } from '../utils/pluralize';
4+
import { DurationStyle, formatDuration } from '../utils/formatDuration.js';
55

66
const MOD_USER_ID_FOR_BAN_APPEAL: string = vars.MOD_USER_ID_FOR_BAN_APPEAL;
77

88
/* Make ban message */
9-
const makeBanMessage = (reason: string, days?: number): string =>
9+
const makeBanMessage = (reason: string, duration?: number): string =>
1010
`
1111
Uh oh, you have been banned from the UW Computer Science Club server ${
12-
days ? `and your messages in the past ${days} ${pluralize('day', days)} have been deleted ` : ''
12+
duration
13+
? `and your messages in the past ${formatDuration(
14+
duration,
15+
DurationStyle.Blank,
16+
)} have been deleted `
17+
: ''
1318
}for the following reason:
1419
1520
> ${reason}
@@ -25,12 +30,12 @@ export const banUser = async (
2530
guild: Guild,
2631
user: User,
2732
reason: string,
28-
days?: number,
33+
duration?: number,
2934
): Promise<boolean> => {
3035
let isSuccessful = false;
3136
try {
3237
try {
33-
await user.send(makeBanMessage(reason, days));
38+
await user.send(makeBanMessage(reason, duration));
3439
} catch (err) {
3540
logger.error({
3641
event: "Can't send message to user not in server",
@@ -39,7 +44,7 @@ export const banUser = async (
3944
}
4045
await guild.members.ban(user, {
4146
reason: reason,
42-
deleteMessageSeconds: days == null ? 0 : days * 86400,
47+
deleteMessageSeconds: duration === undefined ? 0 : Math.floor(duration / 1000),
4348
});
4449
isSuccessful = true;
4550
} catch (err) {

Diff for: src/utils/formatDuration.ts

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* Formats a duration in milliseconds into English text.
3+
* @param duration - The duration in milliseconds
4+
* @param style - The style to format the duration in
5+
* @returns The formatted duration
6+
*/
7+
export const formatDuration = (duration: number, style = DurationStyle.For): string => {
8+
if (duration === Infinity) return 'indefinitely';
9+
10+
duration = Math.round(duration / 1000);
11+
12+
if (duration < 0) {
13+
const core = _formatDuration(-duration);
14+
if (style === DurationStyle.Blank) return `negative ${core}`;
15+
if (style === DurationStyle.For) return `for negative ${core}`;
16+
if (style === DurationStyle.Until) return `until ${core} ago`;
17+
}
18+
19+
if (duration === 0) {
20+
if (style === DurationStyle.Blank) return 'no time';
21+
if (style === DurationStyle.For) return 'for no time';
22+
if (style === DurationStyle.Until) return 'until right now';
23+
}
24+
25+
const core = _formatDuration(duration);
26+
if (style === DurationStyle.Blank) return core;
27+
if (style === DurationStyle.For) return `for ${core}`;
28+
if (style === DurationStyle.Until) return `until ${core} from now`;
29+
30+
return '??';
31+
};
32+
33+
function _formatDuration(duration: number): string {
34+
if (duration === Infinity) return 'indefinitely';
35+
36+
const parts: string[] = [];
37+
38+
for (const [name, scale] of formatTimescales) {
39+
if (duration >= scale) {
40+
const amount = Math.floor(duration / scale);
41+
duration %= scale;
42+
43+
parts.push(`${amount} ${name}${amount === 1 ? '' : 's'}`);
44+
}
45+
}
46+
47+
return parts.join(' ');
48+
}
49+
50+
export enum DurationStyle {
51+
Blank,
52+
For,
53+
Until,
54+
}
55+
56+
const formatTimescales: [string, number][] = [
57+
['day', 86400],
58+
['hour', 3600],
59+
['minute', 60],
60+
['second', 1],
61+
];

Diff for: src/utils/parseDuration.ts

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* Parses English text into a duration in milliseconds. Works for long for (1 day,
3+
* 3 weeks 2 hours) and short form (1d, 3w2h).
4+
*
5+
* @param text - The text to parse
6+
* @returns The duration in milliseconds or null if invalid
7+
*/
8+
export const parseDuration = (text: string): number | null => {
9+
const match = text.match(
10+
/^(\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*)?$/,
11+
);
12+
13+
if (!match) return null;
14+
15+
let duration = 0;
16+
17+
for (const [index, scale] of parseTimescales) {
18+
const submatch = match[index]?.match(/\d+/);
19+
if (submatch) duration += parseInt(submatch[0]) * scale;
20+
}
21+
22+
return duration;
23+
};
24+
25+
const parseTimescales = [
26+
[1, 604800000],
27+
[3, 86400000],
28+
[5, 3600000],
29+
[8, 60000],
30+
[11, 1000],
31+
];

0 commit comments

Comments
 (0)