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

@ -192,4 +192,9 @@ export type LocaleSettings = {
export type RaidprotectionSettings = { export type RaidprotectionSettings = {
// //
} & Setting
export type RejoinSettings = {
channel: string | null,
message: string | null
} & Setting } & 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

@ -1,180 +1,185 @@
// Message logs // Message logs
[MSGLOG_DELETE_TITLE] [MSGLOG_DELETE_TITLE]
{emoji_trash} {author}'s message was deleted in #{channel} {emoji_trash} {author}'s message was deleted in #{channel}
[MSGLOG_NOCONTENT] [MSGLOG_NOCONTENT]
**__NO TEXT CONTENT__** **__NO TEXT CONTENT__**
[MSGLOG_REPLY] [MSGLOG_REPLY]
Message was in reply to user {tag} ({id}): Message was in reply to user {tag} ({id}):
[MSGLOG_REPLY_VALUE] [MSGLOG_REPLY_VALUE]
**[Jump]({link})** **[Jump]({link})**
{content} {content}
[MSGLOG_REPLY_NOCONTENT] [MSGLOG_REPLY_NOCONTENT]
**__Missing content.__** **__Missing content.__**
[MSGLOG_FILTERED] [MSGLOG_FILTERED]
The message was filtered: The message was filtered:
[MSGLOG_FILTERED_VALUE_WORD] [MSGLOG_FILTERED_VALUE_WORD]
Filter: **{matcher}** Filter: **{matcher}**
Matched: **{match}** Matched: **{match}**
[MSGLOG_FILTERED_VALUE_INVITE] [MSGLOG_FILTERED_VALUE_INVITE]
Filter: **{matcher}** Filter: **{matcher}**
Matched: **{match}** Matched: **{match}**
[MSGLOG_FILTERED_VALUE_LINK] [MSGLOG_FILTERED_VALUE_LINK]
Filter: **{matcher}** Filter: **{matcher}**
Matched: **{match}** Matched: **{match}**
[MSGLOG_FILTERED_VALUE_MENTION] [MSGLOG_FILTERED_VALUE_MENTION]
Filter: **{filter}** Filter: **{filter}**
Amount: **{amount}** Amount: **{amount}**
[MSGLOG_FILTERED_VALUE_WORDWATCHER] [MSGLOG_FILTERED_VALUE_WORDWATCHER]
Filter: **{filter}** Filter: **{filter}**
Action: **{action}** Action: **{action}**
[MSGLOG_FILTERED_SANCTIONED] [MSGLOG_FILTERED_SANCTIONED]
__User was sanctioned.__ {emoji_hammer} __User was sanctioned.__ {emoji_hammer}
[MSGLOG_DELETE_FOOTER] [MSGLOG_DELETE_FOOTER]
Message ID: {msgID} | User ID: {userID} Message ID: {msgID} | User ID: {userID}
[MSGLOG_EDIT_TITLE] [MSGLOG_EDIT_TITLE]
{emoji_note} {author} edited their message in #{channel} {emoji_note} {author} edited their message in #{channel}
[MSGLOG_EDIT_FOOTER] [MSGLOG_EDIT_FOOTER]
Message ID: {msgID} | User ID: {userID} Message ID: {msgID} | User ID: {userID}
[MSGLOG_EDIT_OLD] [MSGLOG_EDIT_OLD]
Old message Old message
[MSGLOG_EDIT_NEW] [MSGLOG_EDIT_NEW]
New message New message
[MSGLOG_EDIT_OLD_CUTOFF] [MSGLOG_EDIT_OLD_CUTOFF]
The original message was cut off at 1024 characters. The original message was cut off at 1024 characters.
[MSGLOG_EDIT_NEW_CUTOFF] [MSGLOG_EDIT_NEW_CUTOFF]
The new message is cut off at 1024 characters. The new message is cut off at 1024 characters.
[MSGLOG_EDIT_JUMP] [MSGLOG_EDIT_JUMP]
**[Jump to message](https://discord.com/channels/{guild}/{channel}/{message})** **[Jump to message](https://discord.com/channels/{guild}/{channel}/{message})**
[MSGLOG_PINNED_TITLE] [MSGLOG_PINNED_TITLE]
{emoji_pin} {author}'s message was {pinned} in #{channel} {emoji_pin} {author}'s message was {pinned} in #{channel}
[PIN_TOGGLE] [PIN_TOGGLE]
switch({toggle}) { switch({toggle}) {
case true: case true:
'pinned'; 'pinned';
break; break;
default: default:
'unpinned'; 'unpinned';
break; break;
} }
[THREAD_SWITCH] [THREAD_SWITCH]
switch('{type}') { switch('{type}') {
case 'CREATE': case 'CREATE':
'created'; 'created';
break; break;
case 'DELETE': case 'DELETE':
'deleted'; 'deleted';
break; break;
case 'ARCHIVE': case 'ARCHIVE':
'archived'; 'archived';
break; break;
case 'UNARCHIVE': case 'UNARCHIVE':
'unarchived'; 'unarchived';
break; break;
} }
[MSGLOG_THREAD_TITLE] [MSGLOG_THREAD_TITLE]
A thread was {action} in #{channel} A thread was {action} in #{channel}
[MSGLOG_THREAD_DESC_DELETE] [MSGLOG_THREAD_DESC_DELETE]
**Thread:** {name} ({id}) **Thread:** {name} ({id})
**Owner:** {owner} **Owner:** {owner}
**Deleted by:** {actor} **Deleted by:** {actor}
[MSGLOG_THREAD_DESC_CREATE] [MSGLOG_THREAD_DESC_CREATE]
**Thread:** {name} <#{id}> **Thread:** {name} <#{id}>
**Owner:** {owner} **Owner:** {owner}
[MSGLOG_THREAD_DESC_ARCHIVE] [MSGLOG_THREAD_DESC_ARCHIVE]
**Thread:** {name} <#{id}> **Thread:** {name} <#{id}>
**Owner:** {owner} **Owner:** {owner}
**Archived by:** {actor} **Archived by:** {actor}
[MSGLOG_THREAD_DESC_UNARCHIVE] [MSGLOG_THREAD_DESC_UNARCHIVE]
**Thread:** {name} <#{id}> **Thread:** {name} <#{id}>
**Owner:** {owner} **Owner:** {owner}
**Unarchived by:** {actor} **Unarchived by:** {actor}
[MSGLOG_THREAD_FOOTER] [MSGLOG_THREAD_FOOTER]
Channel ID: {channelId} • Thread ID: {threadId} • Owner ID: {ownerId} Channel ID: {channelId} • Thread ID: {threadId} • Owner ID: {ownerId}
[MSGLOG_THREAD_LOCKED] [MSGLOG_THREAD_LOCKED]
**Thread was locked** **Thread was locked**
[MSGLOG_NO_PERMS] [MSGLOG_NO_PERMS]
Bot missing permissions in message log channel Bot missing permissions in message log channel
Missing: {missing} Missing: {missing}
[MSGLOG_NO_HOOK] [MSGLOG_NO_HOOK]
Webhook missing in message log channel. Webhook missing in message log channel.
Reassign the message log channel to have the bot recreate the webhook. Reassign the message log channel to have the bot recreate the webhook.
//Voice Logs //Voice Logs
[VCLOG_JOIN] [VCLOG_JOIN]
{nickname} **{tag}** ({id}) joined channel {emoji_voice-channel} **{newChannel}**. {nickname} **{tag}** ({id}) joined channel {emoji_voice-channel} **{newChannel}**.
[VCLOG_SWITCH] [VCLOG_SWITCH]
{nickname} **{tag}** ({id}) switched from {emoji_voice-channel} **{oldChannel}** to {emoji_voice-channel} **{newChannel}**. {nickname} **{tag}** ({id}) switched from {emoji_voice-channel} **{oldChannel}** to {emoji_voice-channel} **{newChannel}**.
[VCLOG_LEAVE] [VCLOG_LEAVE]
{nickname} **{tag}** ({id}) left channel {emoji_voice-channel} **{oldChannel}** {nickname} **{tag}** ({id}) left channel {emoji_voice-channel} **{oldChannel}**
[VCLOG_NO_PERMS] [VCLOG_NO_PERMS]
Bot missing permissions in voice join/leave log channel Bot missing permissions in voice join/leave log channel
Missing: {missing} Missing: {missing}
[MEMBERLOG_NO_PERMS] [MEMBERLOG_NO_PERMS]
Bot missing permissions in member log channel Bot missing permissions in member log channel
Missing: {missing} Missing: {missing}
//Nickname Logs //Nickname Logs
[NICKLOG_TITLE] [NICKLOG_TITLE]
{user} updated their nickname {user} updated their nickname
[NICKLOG_DESCRIPTION] [NICKLOG_DESCRIPTION]
**Old nickname:** `{oldNick}` **Old nickname:** `{oldNick}`
**New nickname:** `{newNick}` **New nickname:** `{newNick}`
[NICKLOG_FOOTER] [NICKLOG_FOOTER]
User ID: {id} User ID: {id}
[NICKLOG_NO_PERMS] [NICKLOG_NO_PERMS]
Bot missing permissions in nickname log channel Bot missing permissions in nickname log channel
Missing: {missing} Missing: {missing}
//Bulk Delete Logs //Rejoin Tracking Logs
[BULK_DELETE_TITLE] [REJOINLOG_NO_PERMS]
Bulk message delete log [{embedNr}] in **#{channel}** Bot missing permissions in rejoin log channel
Missing: {missing}
[BULK_DELETE_FOOTER]
Showing messages {rangeStart}-{rangeEnd} of {total} messages //Bulk Delete Logs
[BULK_DELETE_TITLE]
[BULK_DELETE_NO_CONTENT] Bulk message delete log [{embedNr}] in **#{channel}**
No content.
[BULK_DELETE_FOOTER]
[BULK_DELETE_ATTACHMENTS] Showing messages {rangeStart}-{rangeEnd} of {total} messages
Message attachments:
[BULK_DELETE_NO_CONTENT]
[BULK_DELETE_ATTACHMENTS_VALUE] No content.
[BULK_DELETE_ATTACHMENTS]
Message attachments:
[BULK_DELETE_ATTACHMENTS_VALUE]
{links} {links}

View File

@ -1,44 +1,48 @@
[SETTING_MESSAGES_HELP] [SETTING_MESSAGES_HELP]
Configure message logging for your server. Configure message logging for your server.
Message logging utilizes webhooks for bulk message deletions, so if you wish to log those, make sure the bot has the `ManageWebhooks` permission in the logging channel! Message logging utilizes webhooks for bulk message deletions, so if you wish to log those, make sure the bot has the `ManageWebhooks` permission in the logging channel!
If you've given the permission retroactively, make sure to reconfigure the channel to ensure the bot creates the webhook. If you've given the permission retroactively, make sure to reconfigure the channel to ensure the bot creates the webhook.
[SETTING_MEMBERS_HELP] [SETTING_MEMBERS_HELP]
Configure member logging for your server. Configure member logging for your server.
**Usable tags:** **Usable tags:**
`{mention}` - mentions the user `{mention}` - mentions the user
`{tag}` - username#discriminator `{tag}` - username#discriminator
`{username}` - username `{username}` - username
`{guildsize}` - member count of the server `{guildsize}` - member count of the server
`{guildname}` - name of the server `{guildname}` - name of the server
`{accage}` - age of the account `{accage}` - age of the account
`{id}` - ID of the account `{id}` - ID of the account
[SETTING_DMINFRACTION_HELP] [SETTING_DMINFRACTION_HELP]
Configure if messages get sent to users after they're infracted. Configure if messages get sent to users after they're infracted.
Setup custom messages and customize what infractions you want to send to the user. Setup custom messages and customize what infractions you want to send to the user.
[SETTING_DMINFRACTION_VALID] [SETTING_DMINFRACTION_VALID]
Valid choices are **{valid}**. Valid choices are **{valid}**.
// Memberlogs // Memberlogs
[SETTING_MEMBERLOG_JOIN] [SETTING_MEMBERLOG_JOIN]
》 Join Message 》 Join Message
[SETTING_MEMBERLOG_LEAVE] [SETTING_MEMBERLOG_LEAVE]
》 Leave Message 》 Leave Message
[SETTING_ERRORS_HELP] [SETTING_ERRORS_HELP]
Log issues that arise due to configuration. Log issues that arise due to configuration.
Most common outputs will be from automod. Most common outputs will be from automod.
[SETTING_MODERATION_HELP] [SETTING_MODERATION_HELP]
Define the channel to which moderation logs are sent. Define the channel to which moderation logs are sent.
The setting also allows you to exclude/include types of actions from being logged in the channel. The setting also allows you to exclude/include types of actions from being logged in the channel.
[SETTING_NICKNAMES_HELP] [SETTING_NICKNAMES_HELP]
Configure member nickname logging for your server. 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.