aboutsummaryrefslogtreecommitdiff
path: root/src/lib/extensions
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/extensions')
-rw-r--r--src/lib/extensions/discord-akairo/BushClient.ts14
-rw-r--r--src/lib/extensions/discord-akairo/BushClientUtil.ts106
-rw-r--r--src/lib/extensions/discord.js/BushGuildMember.ts62
3 files changed, 155 insertions, 27 deletions
diff --git a/src/lib/extensions/discord-akairo/BushClient.ts b/src/lib/extensions/discord-akairo/BushClient.ts
index 6911573..ed5c90a 100644
--- a/src/lib/extensions/discord-akairo/BushClient.ts
+++ b/src/lib/extensions/discord-akairo/BushClient.ts
@@ -216,7 +216,11 @@ export class BushClient extends AkairoClient {
loaders[loader].loadAll();
await this.logger.success('Startup', `Successfully loaded <<${loader}>>.`, false);
} catch (e) {
- await this.logger.error('Startup', `Unable to load loader <<${loader}>> with error:\n${e?.stack}`, false);
+ await this.logger.error(
+ 'Startup',
+ `Unable to load loader <<${loader}>> with error:\n${typeof e === 'object' ? e?.stack : e}`,
+ false
+ );
}
}
await this.dbPreInit();
@@ -237,8 +241,12 @@ export class BushClient extends AkairoClient {
Models.StickyRole.initModel(this.db);
await this.db.sync({ alter: true }); // Sync all tables to fix everything if updated
await this.console.success('Startup', `Successfully connected to <<database>>.`, false);
- } catch (error) {
- await this.console.error('Startup', `Failed to connect to <<database>> with error:\n` + error?.stack, false);
+ } catch (e) {
+ await this.console.error(
+ 'Startup',
+ `Failed to connect to <<database>> with error:\n` + typeof e === 'object' ? e?.stack : e,
+ false
+ );
}
}
diff --git a/src/lib/extensions/discord-akairo/BushClientUtil.ts b/src/lib/extensions/discord-akairo/BushClientUtil.ts
index 9289598..5a22efc 100644
--- a/src/lib/extensions/discord-akairo/BushClientUtil.ts
+++ b/src/lib/extensions/discord-akairo/BushClientUtil.ts
@@ -24,8 +24,9 @@ import {
WebhookEditMessageOptions
} from 'discord.js';
import got from 'got';
+import humanizeDuration from 'humanize-duration';
import { promisify } from 'util';
-import { Global, Guild, ModLog, ModLogType } from '../../models';
+import { Ban, Global, Guild, ModLog, ModLogType, Mute, PunishmentRole } from '../../models';
import { BushCache } from '../../utils/BushCache';
import { BushConstants } from '../../utils/BushConstants';
import { BushGuildResolvable } from '../discord.js/BushCommandInteraction';
@@ -302,7 +303,7 @@ export class BushClientUtil extends ClientUtil {
});
const filter = (interaction: ButtonInteraction) =>
interaction.customID.startsWith('paginate_') && interaction.message == msg;
- const collector = msg.createMessageComponentInteractionCollector({ filter, time: 300000 });
+ const collector = msg.createMessageComponentCollector({ filter, time: 300000 });
collector.on('collect', async (interaction: MessageComponentInteraction) => {
if (interaction.user.id == message.author.id || this.client.config.owners.includes(interaction.user.id)) {
switch (interaction.customID) {
@@ -391,7 +392,7 @@ export class BushClientUtil extends ClientUtil {
updateOptions();
const msg = await message.util.reply(options as MessageOptions & { split?: false });
const filter = (interaction: ButtonInteraction) => interaction.customID == 'paginate__stop' && interaction.message == msg;
- const collector = msg.createMessageComponentInteractionCollector({ filter, time: 300000 });
+ const collector = msg.createMessageComponentCollector({ filter, time: 300000 });
collector.on('collect', async (interaction: MessageComponentInteraction) => {
if (interaction.user.id == message.author.id || this.client.config.owners.includes(interaction.user.id)) {
await interaction.deferUpdate().catch(() => undefined);
@@ -530,23 +531,36 @@ export class BushClientUtil extends ClientUtil {
return newArray;
}
- public parseDuration(content: string): { duration: number; contentWithoutTime: string } {
+ public parseDuration(content: string, remove = true): { duration: number; contentWithoutTime: string } {
if (!content) return { duration: 0, contentWithoutTime: null };
- let duration = 0,
- contentWithoutTime = content;
+ let duration = 0;
+ // Try to reduce false positives by requiring a space before the duration, this makes sure it still matches if it is
+ // in the beginning of the argument
+ let contentWithoutTime = ` ${content}`;
- const regexString = Object.entries(BushConstants.TimeUnits)
- .map(([name, { label }]) => String.raw`(?:(?<${name}>-?(?:\d+)?\.?\d+) *${label})?`)
- .join('\\s*');
- const match = new RegExp(`^${regexString}$`, 'im').exec(content);
- if (!match) return null;
+ for (const unit in BushConstants.TimeUnits) {
+ const regex = BushConstants.TimeUnits[unit].match;
+ const match = regex.exec(contentWithoutTime);
+ const value = Number(match?.groups?.[unit] || 0);
+ // this.client.console.debug(unit + ': ' + value);
+ duration += value * BushConstants.TimeUnits[unit].value;
- for (const key in match.groups) {
- contentWithoutTime = contentWithoutTime.replace(match.groups[key], '');
- const value = Number(match.groups[key] || 0);
- duration += value * BushConstants.TimeUnits[key].value;
+ if (remove) contentWithoutTime = contentWithoutTime.replace(regex, '');
+ // this.client.console.debug(contentWithoutTime);
}
+ //^(?:(?<years>-?(?:\d+)?\.?\d+) *(?:years?|y))?\s*(?:(?<months>-?(?:\d+)?\.?\d+) *(?:months?|mon|mo?))?\s*(?:(?<weeks>-?(?:\d+)?\.?\d+) *(?:weeks?|w))?\s*(?:(?<days>-?(?:\d+)?\.?\d+) *(?:days?|d))?\s*(?:(?<hours>-?(?:\d+)?\.?\d+) *(?:hours?|hrs?|h))?\s*(?:(?<minutes>-?(?:\d+)?\\.?\\d+) *(?:minutes?|mins?))?\s*(?:(?<seconds>-?(?:\d+)?\\.?\d+) *(?:seconds?|secs?|s))?\s*(?:(?<milliseconds>-?(?:\d+)?\.?\d+) *(?:milliseconds?|msecs?|ms))?$
+ // const regexString = Object.entries(BushConstants.TimeUnits)
+ // .map(([name, { label }]) => String.raw`(?: (?<${name}>-?(?:\d+)?\.?\d+) *${label})`)
+ // .join(' |');
+ // const match = new RegExp(`^${regexString}$`, 'img').exec(' ' + content + ' ');
+ // if (!match) return null;
+ // console.
+ // const contentWithoutTime = content.replace(new RegExp(`^${regexString}$`, 'img'), '');
+ // for (const key in match.groups) {
+ // const value = Number(match.groups[key] || 0);
+ // duration += value * BushConstants.TimeUnits[key].value;
+ // }
return { duration, contentWithoutTime };
}
@@ -555,11 +569,20 @@ export class BushClientUtil extends ClientUtil {
* Checks if a moderator can perform a moderation action on another user.
* @param moderator - The person trying to perform the action.
* @param victim - The person getting punished.
+ * @param checkModerator - Whether or not to check if the victim is a moderator.
*/
- public moderatorCanModerateUser(moderator: BushGuildMember, victim: BushGuildMember): boolean {
- throw 'not implemented';
+ public moderationPermissionCheck(
+ moderator: BushGuildMember,
+ victim: BushGuildMember,
+ checkModerator = true
+ ): true | 'user hierarchy' | 'client hierarchy' | 'moderator' | 'self' {
if (moderator.guild.id !== victim.guild.id) throw 'wtf';
- if (moderator.guild.ownerID === moderator.id) return true;
+ const isOwner = moderator.guild.ownerID === moderator.id;
+ if (moderator.id === victim.id) return 'self';
+ if (moderator.roles.highest.position <= victim.roles.highest.position && !isOwner) return 'user hierarchy';
+ if (victim.roles.highest.position >= victim.guild.me.roles.highest.position) return 'client hierarchy';
+ if (checkModerator && victim.permissions.has('MANAGE_MESSAGES')) return 'moderator';
+ return true;
}
public async createModLogEntry(options: {
@@ -569,10 +592,11 @@ export class BushClientUtil extends ClientUtil {
reason: string;
duration: number;
guild: BushGuildResolvable;
- }): Promise<void> {
+ }): Promise<ModLog> {
const user = this.client.users.resolveID(options.user);
const moderator = this.client.users.resolveID(options.moderator);
const guild = this.client.guilds.resolveID(options.guild);
+ const duration = options.duration || null;
// If guild does not exist create it so the modlog can reference a guild.
await Guild.findOrCreate({
@@ -589,9 +613,49 @@ export class BushClientUtil extends ClientUtil {
user,
moderator,
reason: options.reason,
- duration: options.duration,
+ duration: duration,
guild
});
- await modLogEntry.save();
+ return modLogEntry.save().catch((err) => {
+ this.client.console.error('createModLogEntry', err);
+ return null;
+ });
+ }
+
+ public async createPunishmentEntry(options: {
+ type: 'mute' | 'ban' | 'role';
+ user: BushGuildMemberResolvable;
+ duration: number;
+ guild: BushGuildResolvable;
+ modlog: string;
+ }): Promise<Mute | Ban | PunishmentRole> {
+ let dbModel: typeof Mute | typeof Ban | typeof PunishmentRole;
+ switch (options.type) {
+ case 'mute':
+ dbModel = Mute;
+ break;
+ case 'ban':
+ dbModel = Ban;
+ break;
+ case 'role':
+ dbModel = PunishmentRole;
+ break;
+ default:
+ throw 'choose a valid punishment entry type';
+ }
+
+ const expires = options.duration ? new Date(new Date().getTime() + options.duration) : null;
+ const user = this.client.users.resolveID(options.user);
+ const guild = this.client.guilds.resolveID(options.guild);
+
+ const entry = dbModel.build({ user, guild, expires, modlog: options.modlog });
+ return await entry.save().catch((err) => {
+ this.client.console.error('createPunishmentEntry', err);
+ return null;
+ });
+ }
+
+ public humanizeDuration(duration: number): string {
+ return humanizeDuration(duration, { language: 'en', maxDecimalPoints: 2 });
}
}
diff --git a/src/lib/extensions/discord.js/BushGuildMember.ts b/src/lib/extensions/discord.js/BushGuildMember.ts
index 59dc777..2fefcdd 100644
--- a/src/lib/extensions/discord.js/BushGuildMember.ts
+++ b/src/lib/extensions/discord.js/BushGuildMember.ts
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { GuildMember } from 'discord.js';
+import { ModLogType } from '../../models';
import { BushClient, BushUserResolvable } from '../discord-akairo/BushClient';
import { BushGuild } from './BushGuild';
import { BushUser } from './BushUser';
@@ -7,7 +8,6 @@ import { BushUser } from './BushUser';
interface BushPunishmentOptions {
reason?: string;
moderator: BushUserResolvable;
- createModLogEntry?: boolean;
}
interface BushTimedPunishmentOptions extends BushPunishmentOptions {
@@ -18,7 +18,16 @@ type PunishmentResponse = 'success';
type WarnResponse = PunishmentResponse;
-type MuteResponse = PunishmentResponse | 'no mute role';
+type MuteResponse =
+ | PunishmentResponse
+ | 'missing permissions'
+ | 'no mute role'
+ | 'invalid mute role'
+ | 'mute role not manageable'
+ | 'error giving mute role'
+ | 'error creating modlog entry'
+ | 'error creating mute entry'
+ | 'failed to dm';
type UnmuteResponse = PunishmentResponse;
@@ -44,7 +53,54 @@ export class BushGuildMember extends GuildMember {
}
public async mute(options: BushTimedPunishmentOptions): Promise<MuteResponse> {
- throw 'not implemented';
+ //checks
+ if (!this.guild.me.permissions.has('MANAGE_ROLES')) return 'missing permissions';
+ const muteRoleID = await this.guild.getSetting('muteRole');
+ if (!muteRoleID) return 'no mute role';
+ const muteRole = this.guild.roles.cache.get(muteRoleID);
+ if (!muteRole) return 'invalid mute role';
+ if (muteRole.position >= this.guild.me.roles.highest.position || muteRole.managed) return 'mute role not manageable';
+
+ //add role
+ const success = await this.roles.add(muteRole).catch(() => null);
+ if (!success) return 'error giving mute role';
+
+ //add modlog entry
+ const modlog = await this.client.util
+ .createModLogEntry({
+ type: options.duration ? ModLogType.TEMP_MUTE : ModLogType.PERM_MUTE,
+ user: this,
+ moderator: options.moderator,
+ reason: options.reason,
+ duration: options.duration,
+ guild: this.guild
+ })
+ .catch(() => null);
+ if (!modlog) return 'error creating modlog entry';
+
+ // add punishment entry so they can be unmuted later
+ const mute = await this.client.util
+ .createPunishmentEntry({
+ type: 'mute',
+ user: this,
+ guild: this.guild,
+ duration: options.duration,
+ modlog: modlog.id
+ })
+ .catch(() => null);
+ if (!mute) return 'error creating mute entry';
+
+ //dm user
+ const ending = this.guild.getSetting('punishmentEnding');
+ const dmSuccess = await this.send({
+ content: `You have been muted ${
+ options.duration ? 'for ' + this.client.util.humanizeDuration(options.duration) : 'permanently'
+ } in **${this.guild}** for **${options.reason || 'No reason provided'}**.${ending ? `\n\n${ending}` : ''}`
+ }).catch(() => null);
+
+ if (!dmSuccess) return 'failed to dm';
+
+ return 'success';
}
public async unmute(options: BushPunishmentOptions): Promise<UnmuteResponse> {