Compare commits

...

1 Commits

Author SHA1 Message Date
D3vision
f288293fd5 adding tracking functionality to kick command 2023-12-15 21:23:12 +01:00
13 changed files with 416 additions and 230 deletions

View File

@ -332,6 +332,7 @@ export type InfractionArguments = {
} }
export type AdditionalInfractionData = { export type AdditionalInfractionData = {
track?: boolean;
muteType?: MuteType, muteType?: MuteType,
roles?: Role[] roles?: Role[]
roleIds?: Snowflake[], roleIds?: Snowflake[],

10
@types/Events.d.ts vendored
View File

@ -1,10 +1,17 @@
import { Component } from '../src/client/interfaces/index.ts'; import { Component, Infraction } from '../src/client/interfaces/index.ts';
import { ClientEvents as CE, InvalidRequestWarningData, RateLimitData, ResponseLike } from 'discord.js'; import { ClientEvents as CE, InvalidRequestWarningData, RateLimitData, ResponseLike } from 'discord.js';
import { ExtendedGuildBan, ExtendedMessage } from './Client.ts'; import { ExtendedGuildBan, ExtendedMessage } from './Client.ts';
import { APIRequest } from '@discordjs/rest'; import { APIRequest } from '@discordjs/rest';
import { AutomodErrorProps, LinkFilterWarnProps, LogErrorProps, MissingPermsProps, UtilityErrorProps, WordWatcherErrorProps } from './Moderation.js'; import { AutomodErrorProps, LinkFilterWarnProps, LogErrorProps, MissingPermsProps, UtilityErrorProps, WordWatcherErrorProps } from './Moderation.js';
type ComponentUpdate = { component: Component, type: 'ENABLE' | 'DISABLE' | 'LOAD' | 'UNLOAD' | 'RELOAD' } type ComponentUpdate = { component: Component, type: 'ENABLE' | 'DISABLE' | 'LOAD' | 'UNLOAD' | 'RELOAD' }
export type RejoinLogEntry = {
userId: string;
guildId: string;
caseId: string;
notify: string;
}
export interface ClientEvents extends CE { export interface ClientEvents extends CE {
componentUpdate: [data: ComponentUpdate], componentUpdate: [data: ComponentUpdate],
rateLimit: [rateLimitInfo: RateLimitData]; rateLimit: [rateLimitInfo: RateLimitData];
@ -20,4 +27,5 @@ export interface ClientEvents extends CE {
utilityError: [UtilityErrorProps]; utilityError: [UtilityErrorProps];
linkFilterWarn: [LinkFilterWarnProps]; linkFilterWarn: [LinkFilterWarnProps];
filterMissingPermissions: [MissingPermsProps]; filterMissingPermissions: [MissingPermsProps];
infraction: [Infraction];
} }

3
@types/Guild.d.ts vendored
View File

@ -21,6 +21,7 @@ import {
PermissionSettings, PermissionSettings,
ProtectionSettings, ProtectionSettings,
RaidprotectionSettings, RaidprotectionSettings,
RejoinSettings,
SelfroleSettings, SelfroleSettings,
SilenceSettings, SilenceSettings,
StaffSettings, StaffSettings,
@ -63,6 +64,7 @@ export type GuildSettingTypes =
| WordFilterSettings | WordFilterSettings
| WordWatcherSettings | WordWatcherSettings
| LocaleSettings | LocaleSettings
| RejoinSettings
export type PartialGuildSettings = Partial<GuildSettings> export type PartialGuildSettings = Partial<GuildSettings>
// { // {
@ -101,6 +103,7 @@ export type GuildSettings = {
invitefilter: InviteFilterSettings, invitefilter: InviteFilterSettings,
mentionfilter: MentionFilterSettings, mentionfilter: MentionFilterSettings,
raidprotection: RaidprotectionSettings, raidprotection: RaidprotectionSettings,
rejoin: RejoinSettings
} }
export type PermissionSet = { export type PermissionSet = {

View File

@ -193,3 +193,8 @@ export type LocaleSettings = {
export type RaidprotectionSettings = { export type RaidprotectionSettings = {
// //
} & Setting } & Setting
export type RejoinSettings = {
channel: string | null,
message: string | null
} & Setting

View File

@ -76,7 +76,7 @@
] ]
}, },
"mariadb": { "mariadb": {
"load": false, "load": true,
"tables": [] "tables": []
} }
}, },

View File

@ -595,6 +595,11 @@ class DiscordClient extends Client
return this.#storageManager.mongodb; return this.#storageManager.mongodb;
} }
get mariadb ()
{
return this.#storageManager.mariadb;
}
get moderation () get moderation ()
{ {
return this.#moderationManager; return this.#moderationManager;

View File

@ -1,4 +1,4 @@
import { CommandParams } from '../../../../../@types/Client.js'; import { CommandOptionType, CommandParams } from '../../../../../@types/Client.js';
import DiscordClient from '../../../DiscordClient.js'; import DiscordClient from '../../../DiscordClient.js';
import { Kick } from '../../../infractions/index.js'; import { Kick } from '../../../infractions/index.js';
import { ModerationCommand } from '../../../interfaces/index.js'; import { ModerationCommand } from '../../../interfaces/index.js';
@ -13,7 +13,16 @@ class KickCommand extends ModerationCommand
name: 'kick', name: 'kick',
description: 'Kick people.', description: 'Kick people.',
moduleName: 'moderation', moduleName: 'moderation',
options: [], options: [
{
name: 'track',
description: 'Whether to ping you if the user rejoins',
type: CommandOptionType.BOOLEAN,
flag: true,
valueOptional: true,
defaultValue: false
}
],
guildOnly: true, guildOnly: true,
showUsage: true, showUsage: true,
memberPermissions: [ 'KickMembers' ], memberPermissions: [ 'KickMembers' ],
@ -21,11 +30,14 @@ class KickCommand extends ModerationCommand
}); });
} }
async execute (invoker: InvokerWrapper<true>, { users, ...args }: CommandParams) async execute (invoker: InvokerWrapper<true>, { users, track, ...args }: CommandParams)
{ {
const wrappers = await Promise.all(users!.asUsers.map(user => invoker.guild.memberWrapper(user))); const wrappers = await Promise.all(users!.asUsers.map(user => invoker.guild.memberWrapper(user)));
return this.client.moderation.handleInfraction(Kick, invoker, { return this.client.moderation.handleInfraction(Kick, invoker, {
targets: wrappers.filter(Boolean) as MemberWrapper[], targets: wrappers.filter(Boolean) as MemberWrapper[],
data: {
track: track?.asBool
},
args args
}); });
} }

View File

@ -9,6 +9,8 @@ import { AttachmentData, MessageLogEntry } from '../../../../@types/Guild.js';
import { stripIndents } from 'common-tags'; import { stripIndents } from 'common-tags';
import moment from 'moment'; import moment from 'moment';
import { inspect } from 'util'; import { inspect } from 'util';
import Infraction from '../../interfaces/Infraction.js';
import { RejoinLogEntry } from '../../../../@types/Events.js';
/* eslint-disable no-labels */ /* eslint-disable no-labels */
const CONSTANTS: { const CONSTANTS: {
@ -60,7 +62,9 @@ class GuildLogger extends Observer
[ 'guildMemberUpdate', this.memberUpdate.bind(this) ], [ 'guildMemberUpdate', this.memberUpdate.bind(this) ],
[ 'threadCreate', this.threadCreate.bind(this) ], [ 'threadCreate', this.threadCreate.bind(this) ],
[ 'threadDelete', this.threadDelete.bind(this) ], [ 'threadDelete', this.threadDelete.bind(this) ],
[ 'threadUpdate', this.threadUpdate.bind(this) ] [ 'threadUpdate', this.threadUpdate.bind(this) ],
[ 'infraction', this.infraction.bind(this) ],
[ 'guildMemberAdd', this.rejoinTracking.bind(this) ],
]; ];
if (!process.env.MODERATION_WEHBHOOK_ID || !process.env.MODERATION_WEHBHOOK_TOKEN) if (!process.env.MODERATION_WEHBHOOK_ID || !process.env.MODERATION_WEHBHOOK_TOKEN)
@ -924,6 +928,60 @@ class GuildLogger extends Observer
}; };
await logChannel.send({ embeds: [ embed ] }); await logChannel.send({ embeds: [ embed ] });
} }
async infraction (inf: Infraction)
{
if (inf.type !== 'KICK')
return;
const { guild: wrapper } = inf;
const settings = await wrapper.settings();
const setting = settings.rejoin;
if (!setting.channel || !setting.enabled)
return;
if (!inf.targetId || !inf.guildId || !inf.id)
throw new Error('Missing ID for target, guild, infraction');
const notifyArray = [];
if (inf.data.track)
notifyArray.push(inf.executorId);
await this.client.mariadb.query('INSERT INTO `kickLog` (`userId`, `guildId`, `caseId`, `notify`) VALUES (?);', [[ inf.targetId, inf.guildId, inf.id, JSON.stringify(notifyArray) ]]);
}
async rejoinTracking (member: ExtendedGuildMember)
{
const { guildWrapper: wrapper } = member;
const settings = await wrapper.settings();
const setting = settings.rejoin;
if (!setting.channel || !setting.enabled)
return;
const logChannel = await wrapper.resolveChannel<TextChannel>(setting.channel);
if (!logChannel)
return;
const missing = logChannel.permissionsFor(this.client.user!)?.missing([ 'ViewChannel', 'SendMessages' ]);
if (!missing || missing.length)
return this.client.emit('logError', { guild: wrapper, logger: 'rejoinLogger', reason: 'REJOINLOG_NO_PERMS', params: { missing: missing?.join(', ') ?? 'ALL' } });
const [ result ] = await this.client.mariadb.query('DELETE FROM `kickLog` WHERE `guildId` = ? AND `userId` = ? RETURNING *;', [ member.guild.id, member.id ]) as RejoinLogEntry[];
if (!result)
return;
const infraction = await this.client.mongodb.infractions.findOne({ id: result.caseId });
let { message } = setting;
message = this._replaceTags(message!, member);
const notifyArray = JSON.parse(result.notify);
if (notifyArray.length > 0)
{
const notifyString = notifyArray.map((id: string) => `<@${id}>`).join(', ');
message = notifyString + '\n' + message;
}
message += (infraction ? `\n**Reason:** \`${infraction.reason}\`` : '') + `\n**Case:** \`${result.caseId.split(':')[1]}\``;
await this.client.rateLimiter.queueSend(logChannel, message);
}
} }
export default GuildLogger; export default GuildLogger;

View File

@ -0,0 +1,83 @@
import { TextChannel } from 'discord.js';
import { CommandOptionType, CommandParams } from '../../../../../@types/Client.js';
import { RejoinSettings } from '../../../../../@types/Settings.js';
import DiscordClient from '../../../DiscordClient.js';
import Setting from '../../../interfaces/Setting.js';
import InvokerWrapper from '../../wrappers/InvokerWrapper.js';
import CommandError from '../../../interfaces/CommandError.js';
class Rejoin extends Setting
{
constructor (client: DiscordClient)
{
super(client, {
name: 'rejoin',
description: 'Configure if and where the bot will send a message when a kicked user rejoins the server',
display: 'Rejoin Logging',
moduleName: 'logging',
default: {
enabled: false,
channel: null,
message: 'The user **{mention}** ({id}) has rejoined the server after being kicked.',
},
definitions: {
enabled: 'BOOLEAN',
channel: 'GUILD_TEXT',
message: 'STRING'
},
commandOptions: [
{
name: 'enabled',
description: 'Toggle logging on or off',
type: CommandOptionType.BOOLEAN,
flag: true,
valueOptional: true,
defaultValue: true
},
{
name: 'channel',
description: 'Channel in which to output logs',
type: CommandOptionType.TEXT_CHANNEL
},
{
name: 'message',
description: 'Message to send when a user rejoins',
type: CommandOptionType.STRING
}
]
});
}
async execute (invoker: InvokerWrapper, opts: CommandParams, setting: RejoinSettings)
{
const channel = opts.channel?.asChannel;
const message = opts.message?.asString;
const index = 'SETTING_SUCCESS_ALT';
if (opts.enabled)
setting.enabled = opts.enabled.asBool;
if (opts.channel)
{
if (!(channel instanceof TextChannel))
throw new CommandError(invoker, { index: 'ERR_INVALID_CHANNEL_TYPE' });
const perms = channel.permissionsFor(this.client.user!);
const missingPerms = perms?.missing([ 'ViewChannel', 'SendMessages' ]);
if (!missingPerms || missingPerms.length)
return {
error: true,
index: 'ERR_CHANNEL_PERMS',
params: { channel: channel.name, perms: missingPerms?.join(', ') ?? 'ALL' }
};
setting.channel = channel.id;
}
if (opts.message)
setting.message = message ?? null;
return { index };
}
}
export default Rejoin;

View File

@ -235,6 +235,8 @@ class Infraction
if (this.#data.roles) if (this.#data.roles)
delete this.#data.roles; delete this.#data.roles;
this.client.emit('infraction', this);
return this.save(); return this.save();
} }

View File

@ -187,7 +187,7 @@ class MariaDBProvider extends Provider
{ {
const result = await new Promise<T[] | FieldInfo[] | undefined>((resolve, reject) => const result = await new Promise<T[] | FieldInfo[] | undefined>((resolve, reject) =>
{ {
const q = connection.query({ timeout, sql: query }, [ values ], (err, results, fields) => const q = connection.query({ timeout, sql: query }, values, (err, results, fields) =>
{ {
if (err) if (err)
reject(err); reject(err);

View File

@ -163,6 +163,11 @@ User ID: {id}
Bot missing permissions in nickname log channel Bot missing permissions in nickname log channel
Missing: {missing} Missing: {missing}
//Rejoin Tracking Logs
[REJOINLOG_NO_PERMS]
Bot missing permissions in rejoin log channel
Missing: {missing}
//Bulk Delete Logs //Bulk Delete Logs
[BULK_DELETE_TITLE] [BULK_DELETE_TITLE]
Bulk message delete log [{embedNr}] in **#{channel}** Bulk message delete log [{embedNr}] in **#{channel}**

View File

@ -42,3 +42,7 @@ Configure member nickname logging for your server.
[SETTING_VOICE_HELP] [SETTING_VOICE_HELP]
Configure logging of voice joins and leaves for your server. Configure logging of voice joins and leaves for your server.
//Rejoin logs
[SETTING_REJOIN_HELP]
Configure if and where a message is sent when a user who has been kicked rejoins the server.