Compare commits
24 Commits
main
...
rejoin-log
Author | SHA1 | Date | |
---|---|---|---|
f3c0c1308c | |||
c037a8abe9 | |||
9119316438 | |||
79f7c00670 | |||
d963cf3db5 | |||
51ec1189c0 | |||
390c0ee840 | |||
8199399323 | |||
34923bee9e | |||
dc2602724c | |||
fa45aeab42 | |||
ccb5ce3000 | |||
c081cdc1bf | |||
2c4c4702b4 | |||
cc3ca2580d | |||
96b459a8aa | |||
d637a197b2 | |||
de26cdec44 | |||
2a43ad7feb | |||
a3e6793632 | |||
5f07fd3e15 | |||
c5c304d5d9 | |||
bc467e4dd9 | |||
|
f288293fd5 |
@ -26,6 +26,9 @@
|
||||
"rules": {
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-misused-promises": ["error", {
|
||||
// "checksVoidReturn": false
|
||||
}],
|
||||
"accessor-pairs": "warn",
|
||||
"array-callback-return": "warn",
|
||||
"array-bracket-newline": [
|
||||
|
@ -42,7 +42,7 @@ import { InvokerWrapper, MemberWrapper, UserWrapper } from '../src/client/compon
|
||||
import GuildWrapper from '../src/client/components/wrappers/GuildWrapper.js';
|
||||
import DiscordClient from '../src/client/DiscordClient.js';
|
||||
import { CommandOption, Inhibitor } from '../src/client/interfaces/index.js';
|
||||
import { MuteType } from './Settings.js';
|
||||
import { MuteType, TextCommandsSettings } from './Settings.js';
|
||||
import { ClientEvents } from './Events.js';
|
||||
import { FilterResult } from './Utils.js';
|
||||
import { GuildSettingTypes } from './Guild.js';
|
||||
@ -114,6 +114,7 @@ export type ObserverOptions = {
|
||||
export type InhibitorOptions = {
|
||||
name?: string,
|
||||
guild?: boolean
|
||||
// Higher numbers come first
|
||||
priority?: number,
|
||||
silent?: boolean
|
||||
} & Partial<ComponentOptions>
|
||||
@ -348,6 +349,7 @@ export type InfractionArguments = {
|
||||
}
|
||||
|
||||
export type AdditionalInfractionData = {
|
||||
track?: boolean;
|
||||
muteType?: MuteType,
|
||||
roles?: Role[]
|
||||
roleIds?: Snowflake[],
|
||||
@ -501,4 +503,15 @@ export declare interface ExtendedVoiceState extends VoiceState {
|
||||
|
||||
export declare interface ExtendedInvite extends Invite {
|
||||
guildWrapper?: GuildWrapper
|
||||
}
|
||||
|
||||
export type EntitySettings = {
|
||||
[key: string]: unknown,
|
||||
textcommands: TextCommandsSettings
|
||||
}
|
||||
|
||||
export type EntityData = {
|
||||
id: Snowflake,
|
||||
banned?: boolean,
|
||||
settings?: EntitySettings
|
||||
}
|
10
@types/Events.d.ts
vendored
10
@types/Events.d.ts
vendored
@ -14,13 +14,20 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
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 { ExtendedGuildBan, ExtendedMessage } from './Client.ts';
|
||||
import { APIRequest } from '@discordjs/rest';
|
||||
import { AutomodErrorProps, LinkFilterWarnProps, LogErrorProps, MissingPermsProps, UtilityErrorProps, WordWatcherErrorProps } from './Moderation.js';
|
||||
|
||||
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 {
|
||||
componentUpdate: [data: ComponentUpdate],
|
||||
rateLimit: [rateLimitInfo: RateLimitData];
|
||||
@ -36,4 +43,5 @@ export interface ClientEvents extends CE {
|
||||
utilityError: [UtilityErrorProps];
|
||||
linkFilterWarn: [LinkFilterWarnProps];
|
||||
filterMissingPermissions: [MissingPermsProps];
|
||||
infraction: [Infraction];
|
||||
}
|
8
@types/Guild.d.ts
vendored
8
@types/Guild.d.ts
vendored
@ -37,6 +37,7 @@ import {
|
||||
PermissionSettings,
|
||||
ProtectionSettings,
|
||||
RaidprotectionSettings,
|
||||
RejoinSettings,
|
||||
SelfroleSettings,
|
||||
SilenceSettings,
|
||||
StaffSettings,
|
||||
@ -48,6 +49,7 @@ import {
|
||||
WordWatcherSettings
|
||||
} from './Settings.js';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { EntityData, EntitySettings } from './Client.ts';
|
||||
|
||||
export type GuildSettingTypes =
|
||||
| AutomodSettings
|
||||
@ -79,6 +81,7 @@ export type GuildSettingTypes =
|
||||
| WordFilterSettings
|
||||
| WordWatcherSettings
|
||||
| LocaleSettings
|
||||
| RejoinSettings
|
||||
|
||||
export type PartialGuildSettings = Partial<GuildSettings>
|
||||
// {
|
||||
@ -117,7 +120,8 @@ export type GuildSettings = {
|
||||
invitefilter: InviteFilterSettings,
|
||||
mentionfilter: MentionFilterSettings,
|
||||
raidprotection: RaidprotectionSettings,
|
||||
}
|
||||
rejoin: RejoinSettings
|
||||
} & EntitySettings
|
||||
|
||||
export type PermissionSet = {
|
||||
global: string[],
|
||||
@ -143,7 +147,7 @@ export type GuildData = {
|
||||
modlogs?: boolean,
|
||||
settings?: boolean
|
||||
}
|
||||
}
|
||||
} & EntityData
|
||||
|
||||
export type CallbackData = {
|
||||
type: string,
|
||||
|
@ -17,11 +17,6 @@
|
||||
import { Snowflake } from 'discord.js';
|
||||
import { InfractionType, SettingAction } from './Client.js';
|
||||
|
||||
export type UserSettings = {
|
||||
prefix?: string,
|
||||
locale?: string
|
||||
}
|
||||
|
||||
export type Setting = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[key: string]: any
|
||||
@ -212,4 +207,9 @@ export type LocaleSettings = {
|
||||
|
||||
export type RaidprotectionSettings = {
|
||||
//
|
||||
} & Setting
|
||||
|
||||
export type RejoinSettings = {
|
||||
channel: string | null,
|
||||
message: string | null
|
||||
} & Setting
|
34
@types/User.d.ts
vendored
Normal file
34
@types/User.d.ts
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
// Galactic - Discord moderation bot
|
||||
// Copyright (C) 2024 Navy.gif
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import { EntityData } from './Client.ts';
|
||||
import { LocaleSettings, TextCommandsSettings } from './Settings.ts';
|
||||
|
||||
export type UserSettingTypes =
|
||||
| LocaleSettings
|
||||
| TextCommandsSettings
|
||||
|
||||
export type UserSettings = {
|
||||
[key: string]: UserSettingTypes
|
||||
textcommands: TextCommandsSettings,
|
||||
locale: LocaleSettings
|
||||
} & EntitySettings
|
||||
|
||||
export type UserData = {
|
||||
[key: string]: unknown
|
||||
settings?: UserSettings,
|
||||
debug?: boolean
|
||||
} & EntityData
|
@ -18,6 +18,8 @@ Internal/technical documentation will be in the source files adjacent to the cod
|
||||
- MariaDB (TBD)
|
||||
- RabbitMQ (maybe, TBD)
|
||||
|
||||
A Docker compose file is available for convenience for setting up databases for development or personal deployment.
|
||||
|
||||
### Running
|
||||
- Install the dependencies: `yarn install`
|
||||
- Run the bot: `yarn start`
|
||||
|
47
docker-compose.yml
Normal file
47
docker-compose.yml
Normal file
@ -0,0 +1,47 @@
|
||||
# Compose file for the bot's DB stack
|
||||
# Modify as necessary
|
||||
# TODO: Setup Traefik labels
|
||||
|
||||
services:
|
||||
mongo:
|
||||
image: mongo
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- mongo-data:/data/db
|
||||
ports:
|
||||
- 27017:27017
|
||||
environment:
|
||||
MONGO_INITDB_ROOT_USERNAME: root
|
||||
MONGO_INITDB_ROOT_PASSWORD: I_am_ROOT
|
||||
|
||||
maria:
|
||||
image: mariadb
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- maria-data:/var/lib/mysql
|
||||
ports:
|
||||
- 3306:3306
|
||||
environment:
|
||||
MARIADB_ROOT_PASSWORD: I_am_ROOT
|
||||
|
||||
# Web interfaces for interacting with the databases
|
||||
adminer:
|
||||
image: adminer
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 8080:8080
|
||||
|
||||
mongo-express:
|
||||
image: mongo-express
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 8081:8081
|
||||
environment:
|
||||
ME_CONFIG_MONGODB_ADMINUSERNAME: root
|
||||
ME_CONFIG_MONGODB_ADMINPASSWORD: I_am_ROOT
|
||||
ME_CONFIG_MONGODB_URL: mongodb://root:I_am_ROOT@mongo:27017/
|
||||
ME_CONFIG_BASICAUTH: false
|
||||
|
||||
volumes:
|
||||
mongo-data:
|
||||
maria-data:
|
@ -84,7 +84,7 @@
|
||||
]
|
||||
},
|
||||
"mariadb": {
|
||||
"load": false,
|
||||
"load": true,
|
||||
"tables": []
|
||||
}
|
||||
},
|
||||
|
@ -235,6 +235,7 @@ class DiscordClient extends Client
|
||||
this.#logger.error(`Unhandled rejection:\n${err?.stack || err}`);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
process.on('message', this.#handleMessage.bind(this));
|
||||
process.on('SIGINT', () => this.logger.info('Received SIGINT'));
|
||||
process.on('SIGTERM', () => this.logger.info('Received SIGTERM'));
|
||||
@ -524,6 +525,7 @@ class DiscordClient extends Client
|
||||
return this.localeLoader.format(language, index, params, code);
|
||||
}
|
||||
|
||||
// TODO: Combine these
|
||||
async getGuildWrapper (id: string)
|
||||
{
|
||||
if (this.#guildWrappers.has(id))
|
||||
@ -551,6 +553,11 @@ class DiscordClient extends Client
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
async getGuildWrappers (ids: string[])
|
||||
{
|
||||
return (await Promise.all(ids.map(id => this.getGuildWrapper(id)))).filter(entry => entry !== null);
|
||||
}
|
||||
|
||||
getUserWrapper(resolveable: UserResolveable, fetch?: false): UserWrapper | null;
|
||||
getUserWrapper(resolveable: UserResolveable, fetch?: true): Promise<UserWrapper | null>;
|
||||
getUserWrapper (resolveable: UserResolveable, fetch = true)
|
||||
@ -582,6 +589,16 @@ class DiscordClient extends Client
|
||||
});
|
||||
}
|
||||
|
||||
getUserWrappers(resolveables: UserResolveable[], fetch?: true): Promise<UserWrapper[]>;
|
||||
getUserWrappers(resolveables: UserResolveable[], fetch?: false): UserWrapper[];
|
||||
getUserWrappers (resolveables: UserResolveable[], fetch = true): Promise<UserWrapper[]> | UserWrapper[]
|
||||
{
|
||||
if (!fetch)
|
||||
return resolveables.map(r => this.getUserWrapper(r, false)).filter(e => e !== null) as UserWrapper[];
|
||||
return Promise.all(resolveables.map(resolveable => this.getUserWrapper(resolveable)))
|
||||
.then(entries => entries.filter(e => e !== null)) as Promise<UserWrapper[]>;
|
||||
}
|
||||
|
||||
async fetchInvite (invite: InviteResolvable, opts?: ClientFetchInviteOptions)
|
||||
{
|
||||
const code = DataResolver.resolveInviteCode(invite);
|
||||
@ -639,6 +656,11 @@ class DiscordClient extends Client
|
||||
return this.#storageManager.mongodb;
|
||||
}
|
||||
|
||||
get mariadb ()
|
||||
{
|
||||
return this.#storageManager.mariadb;
|
||||
}
|
||||
|
||||
get moderation ()
|
||||
{
|
||||
return this.#moderationManager;
|
||||
|
71
src/client/components/commands/developer/Botban.ts
Normal file
71
src/client/components/commands/developer/Botban.ts
Normal file
@ -0,0 +1,71 @@
|
||||
// Galactic - Discord moderation bot
|
||||
// Copyright (C) 2024 Navy.gif
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import { CommandOptionType, CommandParams } from '../../../../../@types/Client.js';
|
||||
import { Command } from '../../../interfaces/index.js';
|
||||
import InvokerWrapper from '../../wrappers/InvokerWrapper.js';
|
||||
import DiscordClient from '../../../DiscordClient.js';
|
||||
|
||||
class BotbanCommand extends Command
|
||||
{
|
||||
|
||||
constructor (client: DiscordClient)
|
||||
{
|
||||
super(client, {
|
||||
name: 'botban',
|
||||
aliases: [ 'bban' ],
|
||||
restricted: true,
|
||||
moduleName: 'developer',
|
||||
options: [
|
||||
{ name: 'users', type: CommandOptionType.USERS },
|
||||
{ name: 'guild', type: CommandOptionType.STRING },
|
||||
{ name: 'unban', type: CommandOptionType.BOOLEAN, flag: true, defaultValue: true, valueOptional: true }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
async execute (_invoker: InvokerWrapper, { users, guild, unban }: CommandParams)
|
||||
{
|
||||
let out = 'Revoked bot access from:\n';
|
||||
if (users)
|
||||
{
|
||||
const ids = users.asUsers.map(u => u.id);
|
||||
const wrappers = await this.client.getUserWrappers(ids);
|
||||
out += '**Users**\n';
|
||||
for (const wrapper of wrappers)
|
||||
{
|
||||
out += `\t${wrapper.displayName}\n`;
|
||||
await wrapper.setBotBan(!unban?.asBool);
|
||||
}
|
||||
}
|
||||
if (guild)
|
||||
{
|
||||
const ids = guild.asString.split(' ');
|
||||
const wrappers = await this.client.getGuildWrappers(ids);
|
||||
out += '**Servers**\n';
|
||||
for (const wrapper of wrappers)
|
||||
{
|
||||
out += `\t${wrapper}`;
|
||||
await wrapper.setBotBan(!unban?.asBool);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default BotbanCommand;
|
@ -28,19 +28,46 @@ class DebugCommand extends Command
|
||||
restricted: true,
|
||||
moduleName: 'developer',
|
||||
options: [
|
||||
{ name: 'guild', required: true },
|
||||
{ name: 'enabled', required: true, type: CommandOptionType.BOOLEAN }
|
||||
{
|
||||
name: [ 'enable', 'disable' ],
|
||||
type: CommandOptionType.SUB_COMMAND,
|
||||
options: [
|
||||
{ name: 'guild', required: true },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'list',
|
||||
type: CommandOptionType.SUB_COMMAND,
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
async execute (_invoker: InvokerWrapper, options: CommandParams)
|
||||
async execute (invoker: InvokerWrapper, options: CommandParams)
|
||||
{
|
||||
const guildId = options.guild!.asString;
|
||||
return this.enableDebug(guildId, options.enabled?.asBool);
|
||||
const { subcommand } = invoker;
|
||||
if (!subcommand)
|
||||
throw new Error('Missing subcommand');
|
||||
|
||||
if ([ 'enable', 'disable' ].includes(subcommand.name))
|
||||
{
|
||||
const guildId = options.guild!.asString;
|
||||
return this.toggleDebug(guildId, subcommand.name === 'enable');
|
||||
}
|
||||
|
||||
if (subcommand.name === 'list')
|
||||
return this.listDebugGuilds();
|
||||
}
|
||||
|
||||
async enableDebug (guildId: string, enabled = false)
|
||||
async listDebugGuilds ()
|
||||
{
|
||||
const data = await this.client.mongodb.guilds.find({ debug: true });
|
||||
const ids = data.map(entry => entry.id ?? entry.guildId);
|
||||
const guilds = await this.client.getGuildWrappers(ids);
|
||||
return guilds.map(guild => `**${guild.name}**: ${guild.id}`).join('\n');
|
||||
}
|
||||
|
||||
async toggleDebug (guildId: string, enabled = false)
|
||||
{
|
||||
let output = '';
|
||||
if (this.client.shard)
|
||||
|
@ -14,7 +14,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import { CommandParams } from '../../../../../@types/Client.js';
|
||||
import { CommandOptionType, CommandParams } from '../../../../../@types/Client.js';
|
||||
import DiscordClient from '../../../DiscordClient.js';
|
||||
import { Kick } from '../../../infractions/index.js';
|
||||
import { ModerationCommand } from '../../../interfaces/index.js';
|
||||
@ -29,7 +29,16 @@ class KickCommand extends ModerationCommand
|
||||
name: 'kick',
|
||||
description: 'Kick people.',
|
||||
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,
|
||||
showUsage: true,
|
||||
memberPermissions: [ 'KickMembers' ],
|
||||
@ -37,11 +46,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)));
|
||||
return this.client.moderation.handleInfraction(Kick, invoker, {
|
||||
targets: wrappers.filter(Boolean) as MemberWrapper[],
|
||||
data: {
|
||||
track: track?.asBool
|
||||
},
|
||||
args
|
||||
});
|
||||
}
|
||||
|
47
src/client/components/inhibitors/Banned.ts
Normal file
47
src/client/components/inhibitors/Banned.ts
Normal file
@ -0,0 +1,47 @@
|
||||
// Galactic - Discord moderation bot
|
||||
// Copyright (C) 2024 Navy.gif
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import { InhibitorResponse } from '../../../../@types/Client.js';
|
||||
import Util from '../../../utilities/Util.js';
|
||||
import DiscordClient from '../../DiscordClient.js';
|
||||
import Inhibitor from '../../interfaces/Inhibitor.js';
|
||||
import InvokerWrapper from '../wrappers/InvokerWrapper.js';
|
||||
|
||||
class Banned extends Inhibitor
|
||||
{
|
||||
constructor (client: DiscordClient)
|
||||
{
|
||||
super(client, {
|
||||
name: 'banned',
|
||||
priority: 10,
|
||||
silent: true
|
||||
});
|
||||
}
|
||||
|
||||
async execute (invoker: InvokerWrapper): Promise<InhibitorResponse>
|
||||
{
|
||||
const user = await invoker.userWrapper();
|
||||
const { guild } = invoker;
|
||||
if (user && await user.botBanned())
|
||||
return super._fail({ noun: Util.capitalise(invoker.format('NOUN_USER')) });
|
||||
if (guild && await guild.botBanned())
|
||||
return super._fail({ noun: Util.capitalise(invoker.format('NOUN_GUILD')) });
|
||||
return super._succeed();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Banned;
|
@ -24,7 +24,7 @@ class ChannelIgnore extends Inhibitor
|
||||
{
|
||||
super(client, {
|
||||
name: 'channelIgnore',
|
||||
priority: 9,
|
||||
priority: 8,
|
||||
guild: true,
|
||||
silent: true
|
||||
});
|
||||
|
@ -25,7 +25,7 @@ class ClientPermissions extends Inhibitor
|
||||
{
|
||||
super(client, {
|
||||
name: 'clientPermissions',
|
||||
priority: 10,
|
||||
priority: 9,
|
||||
guarded: true,
|
||||
guild: true
|
||||
});
|
||||
|
@ -287,7 +287,7 @@ class CommandHandler extends Observer
|
||||
{
|
||||
if (!(invoker.command instanceof SettingsCommand))
|
||||
invoker.command.error(now);
|
||||
this.logger.error(`\n[${invoker.type.toUpperCase()}] Command ${debugstr} errored:\nGuild: ${invoker.inGuild() ? invoker.guild?.name : 'dms'} (${invoker.guild?.id || ''})\nOptions:\n${Object.keys(options).map((key) => `[${key}: ${options[key].asString} (${options[key].rawValue})]`).join('\n')}\n${error.stack || error}`);
|
||||
this.logger.error(`\n[${invoker.type.toUpperCase()}] Command "${debugstr}" errored:\nGuild: ${invoker.inGuild() ? invoker.guild?.name : 'dms'} (${invoker.guild?.id || ''})\nOptions:\n${Object.keys(options).map((key) => `[${key}: ${options[key].asString} (${options[key].rawValue})]`).join('\n')}\n${error.stack || error}`);
|
||||
this._generateError(invoker, { type: 'commandHandler' });
|
||||
}
|
||||
return;
|
||||
|
@ -25,6 +25,8 @@ import { AttachmentData, MessageLogEntry } from '../../../../@types/Guild.js';
|
||||
import { stripIndents } from 'common-tags';
|
||||
import moment from 'moment';
|
||||
import { inspect } from 'util';
|
||||
import Infraction from '../../interfaces/Infraction.js';
|
||||
import { RejoinLogEntry } from '../../../../@types/Events.js';
|
||||
|
||||
/* eslint-disable no-labels */
|
||||
const CONSTANTS: {
|
||||
@ -81,7 +83,9 @@ class GuildLogger extends Observer
|
||||
[ 'guildMemberUpdate', this.memberUpdate.bind(this) ],
|
||||
[ 'threadCreate', this.threadCreate.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)
|
||||
@ -955,6 +959,60 @@ class GuildLogger extends Observer
|
||||
};
|
||||
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;
|
83
src/client/components/settings/logging/Rejoin.ts
Normal file
83
src/client/components/settings/logging/Rejoin.ts
Normal 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;
|
40
src/client/components/wrappers/EntityWrapper.ts
Normal file
40
src/client/components/wrappers/EntityWrapper.ts
Normal file
@ -0,0 +1,40 @@
|
||||
// Galactic - Discord moderation bot
|
||||
// Copyright (C) 2024 Navy.gif
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// import { LoggerClient } from '@navy.gif/logger';
|
||||
// import DiscordClient from '../../DiscordClient.js';
|
||||
// import { EntityData } from '../../../../@types/Client.js';
|
||||
|
||||
// // TODO: Consider refactoring Guild and User wrappers to have a common ancestor
|
||||
// class EntityWrapper<DataType extends EntityData>
|
||||
// {
|
||||
// #client: DiscordClient;
|
||||
// #data!: DataType;
|
||||
// #logger: LoggerClient;
|
||||
// constructor (client: DiscordClient)
|
||||
// {
|
||||
// this.#client = client;
|
||||
// this.#logger = client.createLogger(this, { name: this.constructor.name });
|
||||
// }
|
||||
|
||||
// async banned ()
|
||||
// {
|
||||
// return this.#data.banned;
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// export default EntityWrapper;
|
@ -26,7 +26,7 @@ import {
|
||||
} from '../../../../@types/Guild.js';
|
||||
import DiscordClient from '../../DiscordClient.js';
|
||||
|
||||
const configVersion = '3.slash.2';
|
||||
const configVersion = '3.slash.3';
|
||||
|
||||
import {
|
||||
Guild,
|
||||
@ -45,6 +45,7 @@ import {
|
||||
import MemberWrapper from './MemberWrapper.js';
|
||||
import { FilterUtil, Util } from '../../../utilities/index.js';
|
||||
import { LoggerClient } from '@navy.gif/logger';
|
||||
import { ObjectId } from 'mongodb';
|
||||
|
||||
class GuildWrapper
|
||||
{
|
||||
@ -104,15 +105,16 @@ class GuildWrapper
|
||||
{
|
||||
if (this.#data)
|
||||
return this.#data;
|
||||
const data = await this.#client.mongodb.guilds.findOne<GuildData>({ guildId: this.id });
|
||||
|
||||
const data = await this.#client.mongodb.guilds.findOne({ $or: [{ guildId: this.id }, { id: this.id }] });
|
||||
if (!data)
|
||||
{
|
||||
this.#data = {};
|
||||
this.#data = { id: this.id };
|
||||
return this.#data;
|
||||
}
|
||||
if (data._version === '3.slash')
|
||||
{
|
||||
const oldSettings = data as GuildSettings;
|
||||
const oldSettings = data as unknown as GuildSettings;
|
||||
const keys = Object.keys(this.defaultConfig);
|
||||
const settings: PartialGuildSettings = {};
|
||||
for (const key of keys)
|
||||
@ -125,6 +127,13 @@ class GuildWrapper
|
||||
await this.#client.mongodb.guilds.deleteOne({ guildId: this.id });
|
||||
await this.#client.mongodb.guilds.updateOne({ guildId: this.id }, { $set: data });
|
||||
}
|
||||
if (data._version === '3.slash.2')
|
||||
{
|
||||
data.id = data.guildId as string;
|
||||
delete data.guildId;
|
||||
data._version = configVersion; // 3.slash.3
|
||||
await this.#client.mongodb.guilds.updateOne({ _id: data._id as ObjectId }, { $set: data });
|
||||
}
|
||||
this.#data = data;
|
||||
return data;
|
||||
}
|
||||
@ -136,10 +145,8 @@ class GuildWrapper
|
||||
return this.#settings;
|
||||
|
||||
const data = await this.fetchData();
|
||||
// eslint-disable-next-line prefer-const
|
||||
const {
|
||||
settings,
|
||||
// _imported
|
||||
} = data;
|
||||
const { defaultConfig } = this;
|
||||
|
||||
@ -168,7 +175,7 @@ class GuildWrapper
|
||||
return this.#settings;
|
||||
}
|
||||
|
||||
async updateData (data: GuildData)
|
||||
async updateData (data: Partial<GuildData>)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -194,6 +201,22 @@ class GuildWrapper
|
||||
} as GuildSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if guild has been banned from using the bot
|
||||
*/
|
||||
async botBanned ()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
async setBotBan (value = true)
|
||||
{
|
||||
if (!this.#data)
|
||||
await this.fetchData();
|
||||
this.#data.banned = value;
|
||||
await this.updateData({ banned: value });
|
||||
}
|
||||
|
||||
async permissions ()
|
||||
{
|
||||
if (this.#permissions)
|
||||
|
@ -16,171 +16,104 @@
|
||||
|
||||
import { ImageURLOptions, MessageCreateOptions, MessagePayload, User } from 'discord.js';
|
||||
import DiscordClient from '../../DiscordClient.js';
|
||||
import { UserSettings } from '../../../../@types/Settings.js';
|
||||
import { LoggerClient } from '@navy.gif/logger';
|
||||
import { UserData, UserSettings } from '../../../../@types/User.js';
|
||||
|
||||
class UserWrapper
|
||||
{
|
||||
#client: DiscordClient;
|
||||
#user: User;
|
||||
|
||||
#settings: UserSettings;
|
||||
// #points: {
|
||||
// [key: string]: {
|
||||
// expirations: Expiry[],
|
||||
// points: number
|
||||
// }
|
||||
// };
|
||||
|
||||
#data!: UserData;
|
||||
#logger: LoggerClient;
|
||||
#settings: UserSettings;
|
||||
|
||||
constructor (client: DiscordClient, user: User)
|
||||
{
|
||||
this.#client = client;
|
||||
this.#user = user;
|
||||
this.#logger = client.createLogger({ name: `User: ${user.id}` });
|
||||
}
|
||||
|
||||
this.#settings = {};
|
||||
// this.#points = {};
|
||||
async fetchData ()
|
||||
{
|
||||
if (this.#data)
|
||||
return this.#data;
|
||||
|
||||
const data = await this.#client.mongodb.users.findOne({ id: this.id });
|
||||
if (!data)
|
||||
{
|
||||
this.#data = { id: this.id };
|
||||
return this.#data;
|
||||
}
|
||||
|
||||
this.#data = data;
|
||||
return data;
|
||||
}
|
||||
|
||||
async settings (forceFetch = false)
|
||||
{
|
||||
if (this.#settings && !forceFetch)
|
||||
return this.#settings;
|
||||
if (this.#data && !forceFetch)
|
||||
return this.#data.settings;
|
||||
|
||||
const data = await this.fetchData();
|
||||
const { settings } = data;
|
||||
const { defaultConfig } = this;
|
||||
|
||||
const settings = await this.#client.mongodb.users.findOne({ userId: this.id });
|
||||
if (settings)
|
||||
this.#settings = { ...this.defaultConfig, ...settings };
|
||||
else
|
||||
this.#settings = { userId: this.id, ...this.defaultConfig };
|
||||
{
|
||||
const keys = Object.keys(settings);
|
||||
for (const key of keys)
|
||||
{
|
||||
defaultConfig[key] = { ...defaultConfig[key], [key]: settings[key] };
|
||||
}
|
||||
}
|
||||
|
||||
this.#settings = defaultConfig;
|
||||
return this.#settings;
|
||||
}
|
||||
|
||||
// async fetchPoints (guild: GuildWrapper)
|
||||
// {
|
||||
// let index = this.#points[guild.id];
|
||||
// if (index)
|
||||
// return Promise.resolve(index);
|
||||
// this.#points[guild.id] = {
|
||||
// expirations: [],
|
||||
// points: 0
|
||||
// };
|
||||
// index = this.#points[guild.id];
|
||||
|
||||
// const filter = {
|
||||
// guild: guild.id,
|
||||
// target: this.id,
|
||||
// resolved: false,
|
||||
// // points: { $gte: 0 },
|
||||
// // $or: [{ expiration: 0 }, { expiration: { $gte: now } }]
|
||||
// };
|
||||
// const find = await this.#client.mongodb.infractions.find(
|
||||
// filter,
|
||||
// { projection: { id: 1, points: 1, expiration: 1 } }
|
||||
// );
|
||||
|
||||
// if (find.length)
|
||||
// {
|
||||
// for (const { points: p, expiration, id } of find)
|
||||
// {
|
||||
// let points = p;
|
||||
// // Imported cases may have false or null
|
||||
// if (typeof points !== 'number')
|
||||
// points = 0;
|
||||
// if (expiration > 0)
|
||||
// {
|
||||
// index.expirations.push({ points, expiration, id });
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// index.points += points;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return index;
|
||||
// }
|
||||
|
||||
// async editPoints (guild: GuildWrapper, { id, diff, expiration }: {id: string, diff: number, expiration: unknown })
|
||||
// {
|
||||
// const points = await this.fetchPoints(guild);
|
||||
// if (expiration)
|
||||
// {
|
||||
// const expiry = points.expirations.find((exp) => exp.id === id) as Expiry;
|
||||
// expiry.points += diff;
|
||||
// }
|
||||
// else
|
||||
// points.points += diff;
|
||||
// return this.totalPoints(guild);
|
||||
// }
|
||||
|
||||
// async editExpiration (guild: GuildWrapper, { id, expiration, points }: { id: string, expiration: number, points: number })
|
||||
// {
|
||||
// const index = await this.fetchPoints(guild);
|
||||
// const i = index.expirations.findIndex((exp) => exp.id === id);
|
||||
// if (i > -1)
|
||||
// index.expirations[i].expiration = expiration;
|
||||
// else
|
||||
// {
|
||||
// index.points -= points;
|
||||
// index.expirations.push({ id, points, expiration });
|
||||
// }
|
||||
// return this.totalPoints(guild);
|
||||
// }
|
||||
|
||||
// async totalPoints (guild: GuildWrapper, point?: { id: string, points: number, expiration: number, timestamp: number })
|
||||
// { // point = { points: x, expiration: x, timestamp: x}
|
||||
// const index = await this.fetchPoints(guild);
|
||||
// const now = Date.now();
|
||||
|
||||
// if (point)
|
||||
// {
|
||||
// if (point.expiration > 0)
|
||||
// {
|
||||
// index.expirations.push({ id: point.id, points: point.points, expiration: point.expiration + point.timestamp });
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// index.points += point.points;
|
||||
// }
|
||||
// }
|
||||
|
||||
// let expirationPoints = index.expirations.map((e) =>
|
||||
// {
|
||||
// if (e.expiration >= now)
|
||||
// return e.points;
|
||||
// return 0;
|
||||
// });
|
||||
|
||||
// if (expirationPoints.length === 0)
|
||||
// expirationPoints = [ 0 ];
|
||||
// return expirationPoints.reduce((p, v) => p + v) + index.points;
|
||||
// }
|
||||
|
||||
async updateSettings (data: UserSettings)
|
||||
{ // Update property (upsert true) - updateOne
|
||||
if (!this.#settings)
|
||||
await this.settings();
|
||||
try
|
||||
{
|
||||
await this.#client.mongodb.users.updateOne(
|
||||
{ guildId: this.id },
|
||||
{ $set: data }
|
||||
);
|
||||
this.#settings = {
|
||||
...this.#settings,
|
||||
...data
|
||||
};
|
||||
this.#storageLog(`Database Update (guild:${this.id}).`);
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
this.#storageError(error as Error);
|
||||
}
|
||||
return true;
|
||||
/**
|
||||
* Check whether the user is banned from using the bot
|
||||
*/
|
||||
async botBanned (): Promise<boolean>
|
||||
{
|
||||
const data = await this.fetchData();
|
||||
return data?.banned ?? false;
|
||||
}
|
||||
|
||||
get defaultConfig ()
|
||||
async setBotBan (value = true)
|
||||
{
|
||||
if (!this.#data)
|
||||
await this.fetchData();
|
||||
this.#data!.banned = value;
|
||||
await this.updateData({ banned: value });
|
||||
}
|
||||
|
||||
async updateSettings (settings: Partial<UserSettings>)
|
||||
{
|
||||
if (!this.#data?.settings)
|
||||
await this.settings();
|
||||
|
||||
await this.updateData({ settings: settings as UserSettings });
|
||||
}
|
||||
|
||||
async updateData (data: Partial<UserData>)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this.#client.mongodb.users.updateOne({ id: this.id }, { $set: data });
|
||||
this.#data = { ...this.#data, ...data } as UserData;
|
||||
this.#storageLog('Data update');
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
const error = err as Error;
|
||||
this.#storageError(error);
|
||||
}
|
||||
}
|
||||
|
||||
get defaultConfig (): UserSettings
|
||||
{
|
||||
return JSON.parse(JSON.stringify(this.#client.defaultConfig('USER')));
|
||||
}
|
||||
@ -242,14 +175,114 @@ class UserWrapper
|
||||
|
||||
get prefix ()
|
||||
{
|
||||
return this.#settings.prefix;
|
||||
return this.#data?.settings.textcommands.prefix;
|
||||
}
|
||||
|
||||
get locale ()
|
||||
{
|
||||
return this.#settings.locale;
|
||||
return this.#data?.settings.locale.language;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default UserWrapper;
|
||||
export default UserWrapper;
|
||||
|
||||
// async fetchPoints (guild: GuildWrapper)
|
||||
// {
|
||||
// let index = this.#points[guild.id];
|
||||
// if (index)
|
||||
// return Promise.resolve(index);
|
||||
// this.#points[guild.id] = {
|
||||
// expirations: [],
|
||||
// points: 0
|
||||
// };
|
||||
// index = this.#points[guild.id];
|
||||
|
||||
// const filter = {
|
||||
// guild: guild.id,
|
||||
// target: this.id,
|
||||
// resolved: false,
|
||||
// // points: { $gte: 0 },
|
||||
// // $or: [{ expiration: 0 }, { expiration: { $gte: now } }]
|
||||
// };
|
||||
// const find = await this.#client.mongodb.infractions.find(
|
||||
// filter,
|
||||
// { projection: { id: 1, points: 1, expiration: 1 } }
|
||||
// );
|
||||
|
||||
// if (find.length)
|
||||
// {
|
||||
// for (const { points: p, expiration, id } of find)
|
||||
// {
|
||||
// let points = p;
|
||||
// // Imported cases may have false or null
|
||||
// if (typeof points !== 'number')
|
||||
// points = 0;
|
||||
// if (expiration > 0)
|
||||
// {
|
||||
// index.expirations.push({ points, expiration, id });
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// index.points += points;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return index;
|
||||
// }
|
||||
|
||||
// async editPoints (guild: GuildWrapper, { id, diff, expiration }: {id: string, diff: number, expiration: unknown })
|
||||
// {
|
||||
// const points = await this.fetchPoints(guild);
|
||||
// if (expiration)
|
||||
// {
|
||||
// const expiry = points.expirations.find((exp) => exp.id === id) as Expiry;
|
||||
// expiry.points += diff;
|
||||
// }
|
||||
// else
|
||||
// points.points += diff;
|
||||
// return this.totalPoints(guild);
|
||||
// }
|
||||
|
||||
// async editExpiration (guild: GuildWrapper, { id, expiration, points }: { id: string, expiration: number, points: number })
|
||||
// {
|
||||
// const index = await this.fetchPoints(guild);
|
||||
// const i = index.expirations.findIndex((exp) => exp.id === id);
|
||||
// if (i > -1)
|
||||
// index.expirations[i].expiration = expiration;
|
||||
// else
|
||||
// {
|
||||
// index.points -= points;
|
||||
// index.expirations.push({ id, points, expiration });
|
||||
// }
|
||||
// return this.totalPoints(guild);
|
||||
// }
|
||||
|
||||
// async totalPoints (guild: GuildWrapper, point?: { id: string, points: number, expiration: number, timestamp: number })
|
||||
// { // point = { points: x, expiration: x, timestamp: x}
|
||||
// const index = await this.fetchPoints(guild);
|
||||
// const now = Date.now();
|
||||
|
||||
// if (point)
|
||||
// {
|
||||
// if (point.expiration > 0)
|
||||
// {
|
||||
// index.expirations.push({ id: point.id, points: point.points, expiration: point.expiration + point.timestamp });
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// index.points += point.points;
|
||||
// }
|
||||
// }
|
||||
|
||||
// let expirationPoints = index.expirations.map((e) =>
|
||||
// {
|
||||
// if (e.expiration >= now)
|
||||
// return e.points;
|
||||
// return 0;
|
||||
// });
|
||||
|
||||
// if (expirationPoints.length === 0)
|
||||
// expirationPoints = [ 0 ];
|
||||
// return expirationPoints.reduce((p, v) => p + v) + index.points;
|
||||
// }
|
@ -264,12 +264,14 @@ class Infraction
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the role structures as they will cause problems when serialising for database
|
||||
if (this.#data.roles)
|
||||
delete this.#data.roles;
|
||||
|
||||
if (this.#duration)
|
||||
await this.#client.moderation.handleTimedInfraction(this.json);
|
||||
|
||||
/* LMAOOOO PLEASE DONT JUDGE ME */
|
||||
if (this.#data.roles)
|
||||
delete this.#data.roles;
|
||||
this.client.emit('infraction', this);
|
||||
|
||||
return this.save();
|
||||
}
|
||||
|
@ -208,7 +208,7 @@ class MariaDBProvider extends Provider
|
||||
{
|
||||
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)
|
||||
reject(err);
|
||||
|
@ -17,8 +17,8 @@
|
||||
import { CallbackInfo } from '../../../../@types/CallbackManager.js';
|
||||
import { InfractionJSON } from '../../../../@types/Client.js';
|
||||
import { AttachmentData, GuildData, GuildPermissions, MessageLogEntry, RoleCacheEntry, WebhookEntry, WordWatcherEntry } from '../../../../@types/Guild.js';
|
||||
import { UserSettings } from '../../../../@types/Settings.js';
|
||||
import { MongoDBOptions } from '../../../../@types/Storage.js';
|
||||
import { UserData } from '../../../../@types/User.js';
|
||||
import DiscordClient from '../../DiscordClient.js';
|
||||
import { MongodbTable } from '../interfaces/index.js';
|
||||
import Provider from '../interfaces/Provider.js';
|
||||
@ -142,7 +142,7 @@ class MongoDBProvider extends Provider
|
||||
|
||||
get users ()
|
||||
{
|
||||
return this.tables.users as MongodbTable<UserSettings>;
|
||||
return this.tables.users as MongodbTable<UserData>;
|
||||
}
|
||||
|
||||
get roleCache ()
|
||||
|
@ -26,4 +26,7 @@ The command **{command}** requires the __bot__ to have permissions to use.
|
||||
The command **{command}** can only be run by developers.
|
||||
|
||||
[INHIBITOR_GUILDONLY_ERROR]
|
||||
The command **{command}** is only available in servers.
|
||||
The command **{command}** is only available in servers.
|
||||
|
||||
[INHIBITOR_BANNED_ERROR]
|
||||
{noun} has been banned from using the bot.
|
5
src/localization/en_gb/general/en_gb_nouns.lang
Normal file
5
src/localization/en_gb/general/en_gb_nouns.lang
Normal file
@ -0,0 +1,5 @@
|
||||
[NOUN_USER]
|
||||
user
|
||||
|
||||
[NOUN_GUILD]
|
||||
server
|
@ -1,180 +1,185 @@
|
||||
// Message logs
|
||||
[MSGLOG_DELETE_TITLE]
|
||||
{emoji_trash} {author}'s message was deleted in #{channel}
|
||||
|
||||
[MSGLOG_NOCONTENT]
|
||||
**__NO TEXT CONTENT__**
|
||||
|
||||
[MSGLOG_REPLY]
|
||||
Message was in reply to user {tag} ({id}):
|
||||
|
||||
[MSGLOG_REPLY_VALUE]
|
||||
**[Jump]({link})**
|
||||
{content}
|
||||
|
||||
[MSGLOG_REPLY_NOCONTENT]
|
||||
**__Missing content.__**
|
||||
|
||||
[MSGLOG_FILTERED]
|
||||
The message was filtered:
|
||||
|
||||
[MSGLOG_FILTERED_VALUE_WORD]
|
||||
Filter: **{matcher}**
|
||||
Matched: **{match}**
|
||||
|
||||
[MSGLOG_FILTERED_VALUE_INVITE]
|
||||
Filter: **{matcher}**
|
||||
Matched: **{match}**
|
||||
|
||||
[MSGLOG_FILTERED_VALUE_LINK]
|
||||
Filter: **{matcher}**
|
||||
Matched: **{match}**
|
||||
|
||||
[MSGLOG_FILTERED_VALUE_MENTION]
|
||||
Filter: **{filter}**
|
||||
Amount: **{amount}**
|
||||
|
||||
[MSGLOG_FILTERED_VALUE_WORDWATCHER]
|
||||
Filter: **{filter}**
|
||||
Action: **{action}**
|
||||
|
||||
[MSGLOG_FILTERED_SANCTIONED]
|
||||
__User was sanctioned.__ {emoji_hammer}
|
||||
|
||||
[MSGLOG_DELETE_FOOTER]
|
||||
Message ID: {msgID} | User ID: {userID}
|
||||
|
||||
[MSGLOG_EDIT_TITLE]
|
||||
{emoji_note} {author} edited their message in #{channel}
|
||||
|
||||
[MSGLOG_EDIT_FOOTER]
|
||||
Message ID: {msgID} | User ID: {userID}
|
||||
|
||||
[MSGLOG_EDIT_OLD]
|
||||
Old message
|
||||
|
||||
[MSGLOG_EDIT_NEW]
|
||||
New message
|
||||
|
||||
[MSGLOG_EDIT_OLD_CUTOFF]
|
||||
The original message was cut off at 1024 characters.
|
||||
|
||||
[MSGLOG_EDIT_NEW_CUTOFF]
|
||||
The new message is cut off at 1024 characters.
|
||||
|
||||
[MSGLOG_EDIT_JUMP]
|
||||
**[Jump to message](https://discord.com/channels/{guild}/{channel}/{message})**
|
||||
|
||||
[MSGLOG_PINNED_TITLE]
|
||||
{emoji_pin} {author}'s message was {pinned} in #{channel}
|
||||
|
||||
[PIN_TOGGLE]
|
||||
switch({toggle}) {
|
||||
case true:
|
||||
'pinned';
|
||||
break;
|
||||
default:
|
||||
'unpinned';
|
||||
break;
|
||||
}
|
||||
|
||||
[THREAD_SWITCH]
|
||||
switch('{type}') {
|
||||
case 'CREATE':
|
||||
'created';
|
||||
break;
|
||||
case 'DELETE':
|
||||
'deleted';
|
||||
break;
|
||||
case 'ARCHIVE':
|
||||
'archived';
|
||||
break;
|
||||
case 'UNARCHIVE':
|
||||
'unarchived';
|
||||
break;
|
||||
}
|
||||
|
||||
[MSGLOG_THREAD_TITLE]
|
||||
A thread was {action} in #{channel}
|
||||
|
||||
[MSGLOG_THREAD_DESC_DELETE]
|
||||
**Thread:** {name} ({id})
|
||||
**Owner:** {owner}
|
||||
**Deleted by:** {actor}
|
||||
|
||||
[MSGLOG_THREAD_DESC_CREATE]
|
||||
**Thread:** {name} <#{id}>
|
||||
**Owner:** {owner}
|
||||
|
||||
[MSGLOG_THREAD_DESC_ARCHIVE]
|
||||
**Thread:** {name} <#{id}>
|
||||
**Owner:** {owner}
|
||||
**Archived by:** {actor}
|
||||
|
||||
[MSGLOG_THREAD_DESC_UNARCHIVE]
|
||||
**Thread:** {name} <#{id}>
|
||||
**Owner:** {owner}
|
||||
**Unarchived by:** {actor}
|
||||
|
||||
[MSGLOG_THREAD_FOOTER]
|
||||
Channel ID: {channelId} • Thread ID: {threadId} • Owner ID: {ownerId}
|
||||
|
||||
[MSGLOG_THREAD_LOCKED]
|
||||
**Thread was locked**
|
||||
|
||||
[MSGLOG_NO_PERMS]
|
||||
Bot missing permissions in message log channel
|
||||
Missing: {missing}
|
||||
|
||||
[MSGLOG_NO_HOOK]
|
||||
Webhook missing in message log channel.
|
||||
Reassign the message log channel to have the bot recreate the webhook.
|
||||
|
||||
//Voice Logs
|
||||
[VCLOG_JOIN]
|
||||
{nickname} **{tag}** ({id}) joined channel {emoji_voice-channel} **{newChannel}**.
|
||||
|
||||
[VCLOG_SWITCH]
|
||||
{nickname} **{tag}** ({id}) switched from {emoji_voice-channel} **{oldChannel}** to {emoji_voice-channel} **{newChannel}**.
|
||||
|
||||
[VCLOG_LEAVE]
|
||||
{nickname} **{tag}** ({id}) left channel {emoji_voice-channel} **{oldChannel}**
|
||||
|
||||
[VCLOG_NO_PERMS]
|
||||
Bot missing permissions in voice join/leave log channel
|
||||
Missing: {missing}
|
||||
|
||||
[MEMBERLOG_NO_PERMS]
|
||||
Bot missing permissions in member log channel
|
||||
Missing: {missing}
|
||||
|
||||
//Nickname Logs
|
||||
[NICKLOG_TITLE]
|
||||
{user} updated their nickname
|
||||
|
||||
[NICKLOG_DESCRIPTION]
|
||||
**Old nickname:** `{oldNick}`
|
||||
**New nickname:** `{newNick}`
|
||||
|
||||
[NICKLOG_FOOTER]
|
||||
User ID: {id}
|
||||
|
||||
[NICKLOG_NO_PERMS]
|
||||
Bot missing permissions in nickname log channel
|
||||
Missing: {missing}
|
||||
|
||||
//Bulk Delete Logs
|
||||
[BULK_DELETE_TITLE]
|
||||
Bulk message delete log [{embedNr}] in **#{channel}**
|
||||
|
||||
[BULK_DELETE_FOOTER]
|
||||
Showing messages {rangeStart}-{rangeEnd} of {total} messages
|
||||
|
||||
[BULK_DELETE_NO_CONTENT]
|
||||
No content.
|
||||
|
||||
[BULK_DELETE_ATTACHMENTS]
|
||||
Message attachments:
|
||||
|
||||
[BULK_DELETE_ATTACHMENTS_VALUE]
|
||||
// Message logs
|
||||
[MSGLOG_DELETE_TITLE]
|
||||
{emoji_trash} {author}'s message was deleted in #{channel}
|
||||
|
||||
[MSGLOG_NOCONTENT]
|
||||
**__NO TEXT CONTENT__**
|
||||
|
||||
[MSGLOG_REPLY]
|
||||
Message was in reply to user {tag} ({id}):
|
||||
|
||||
[MSGLOG_REPLY_VALUE]
|
||||
**[Jump]({link})**
|
||||
{content}
|
||||
|
||||
[MSGLOG_REPLY_NOCONTENT]
|
||||
**__Missing content.__**
|
||||
|
||||
[MSGLOG_FILTERED]
|
||||
The message was filtered:
|
||||
|
||||
[MSGLOG_FILTERED_VALUE_WORD]
|
||||
Filter: **{matcher}**
|
||||
Matched: **{match}**
|
||||
|
||||
[MSGLOG_FILTERED_VALUE_INVITE]
|
||||
Filter: **{matcher}**
|
||||
Matched: **{match}**
|
||||
|
||||
[MSGLOG_FILTERED_VALUE_LINK]
|
||||
Filter: **{matcher}**
|
||||
Matched: **{match}**
|
||||
|
||||
[MSGLOG_FILTERED_VALUE_MENTION]
|
||||
Filter: **{filter}**
|
||||
Amount: **{amount}**
|
||||
|
||||
[MSGLOG_FILTERED_VALUE_WORDWATCHER]
|
||||
Filter: **{filter}**
|
||||
Action: **{action}**
|
||||
|
||||
[MSGLOG_FILTERED_SANCTIONED]
|
||||
__User was sanctioned.__ {emoji_hammer}
|
||||
|
||||
[MSGLOG_DELETE_FOOTER]
|
||||
Message ID: {msgID} | User ID: {userID}
|
||||
|
||||
[MSGLOG_EDIT_TITLE]
|
||||
{emoji_note} {author} edited their message in #{channel}
|
||||
|
||||
[MSGLOG_EDIT_FOOTER]
|
||||
Message ID: {msgID} | User ID: {userID}
|
||||
|
||||
[MSGLOG_EDIT_OLD]
|
||||
Old message
|
||||
|
||||
[MSGLOG_EDIT_NEW]
|
||||
New message
|
||||
|
||||
[MSGLOG_EDIT_OLD_CUTOFF]
|
||||
The original message was cut off at 1024 characters.
|
||||
|
||||
[MSGLOG_EDIT_NEW_CUTOFF]
|
||||
The new message is cut off at 1024 characters.
|
||||
|
||||
[MSGLOG_EDIT_JUMP]
|
||||
**[Jump to message](https://discord.com/channels/{guild}/{channel}/{message})**
|
||||
|
||||
[MSGLOG_PINNED_TITLE]
|
||||
{emoji_pin} {author}'s message was {pinned} in #{channel}
|
||||
|
||||
[PIN_TOGGLE]
|
||||
switch({toggle}) {
|
||||
case true:
|
||||
'pinned';
|
||||
break;
|
||||
default:
|
||||
'unpinned';
|
||||
break;
|
||||
}
|
||||
|
||||
[THREAD_SWITCH]
|
||||
switch('{type}') {
|
||||
case 'CREATE':
|
||||
'created';
|
||||
break;
|
||||
case 'DELETE':
|
||||
'deleted';
|
||||
break;
|
||||
case 'ARCHIVE':
|
||||
'archived';
|
||||
break;
|
||||
case 'UNARCHIVE':
|
||||
'unarchived';
|
||||
break;
|
||||
}
|
||||
|
||||
[MSGLOG_THREAD_TITLE]
|
||||
A thread was {action} in #{channel}
|
||||
|
||||
[MSGLOG_THREAD_DESC_DELETE]
|
||||
**Thread:** {name} ({id})
|
||||
**Owner:** {owner}
|
||||
**Deleted by:** {actor}
|
||||
|
||||
[MSGLOG_THREAD_DESC_CREATE]
|
||||
**Thread:** {name} <#{id}>
|
||||
**Owner:** {owner}
|
||||
|
||||
[MSGLOG_THREAD_DESC_ARCHIVE]
|
||||
**Thread:** {name} <#{id}>
|
||||
**Owner:** {owner}
|
||||
**Archived by:** {actor}
|
||||
|
||||
[MSGLOG_THREAD_DESC_UNARCHIVE]
|
||||
**Thread:** {name} <#{id}>
|
||||
**Owner:** {owner}
|
||||
**Unarchived by:** {actor}
|
||||
|
||||
[MSGLOG_THREAD_FOOTER]
|
||||
Channel ID: {channelId} • Thread ID: {threadId} • Owner ID: {ownerId}
|
||||
|
||||
[MSGLOG_THREAD_LOCKED]
|
||||
**Thread was locked**
|
||||
|
||||
[MSGLOG_NO_PERMS]
|
||||
Bot missing permissions in message log channel
|
||||
Missing: {missing}
|
||||
|
||||
[MSGLOG_NO_HOOK]
|
||||
Webhook missing in message log channel.
|
||||
Reassign the message log channel to have the bot recreate the webhook.
|
||||
|
||||
//Voice Logs
|
||||
[VCLOG_JOIN]
|
||||
{nickname} **{tag}** ({id}) joined channel {emoji_voice-channel} **{newChannel}**.
|
||||
|
||||
[VCLOG_SWITCH]
|
||||
{nickname} **{tag}** ({id}) switched from {emoji_voice-channel} **{oldChannel}** to {emoji_voice-channel} **{newChannel}**.
|
||||
|
||||
[VCLOG_LEAVE]
|
||||
{nickname} **{tag}** ({id}) left channel {emoji_voice-channel} **{oldChannel}**
|
||||
|
||||
[VCLOG_NO_PERMS]
|
||||
Bot missing permissions in voice join/leave log channel
|
||||
Missing: {missing}
|
||||
|
||||
[MEMBERLOG_NO_PERMS]
|
||||
Bot missing permissions in member log channel
|
||||
Missing: {missing}
|
||||
|
||||
//Nickname Logs
|
||||
[NICKLOG_TITLE]
|
||||
{user} updated their nickname
|
||||
|
||||
[NICKLOG_DESCRIPTION]
|
||||
**Old nickname:** `{oldNick}`
|
||||
**New nickname:** `{newNick}`
|
||||
|
||||
[NICKLOG_FOOTER]
|
||||
User ID: {id}
|
||||
|
||||
[NICKLOG_NO_PERMS]
|
||||
Bot missing permissions in nickname log channel
|
||||
Missing: {missing}
|
||||
|
||||
//Rejoin Tracking Logs
|
||||
[REJOINLOG_NO_PERMS]
|
||||
Bot missing permissions in rejoin log channel
|
||||
Missing: {missing}
|
||||
|
||||
//Bulk Delete Logs
|
||||
[BULK_DELETE_TITLE]
|
||||
Bulk message delete log [{embedNr}] in **#{channel}**
|
||||
|
||||
[BULK_DELETE_FOOTER]
|
||||
Showing messages {rangeStart}-{rangeEnd} of {total} messages
|
||||
|
||||
[BULK_DELETE_NO_CONTENT]
|
||||
No content.
|
||||
|
||||
[BULK_DELETE_ATTACHMENTS]
|
||||
Message attachments:
|
||||
|
||||
[BULK_DELETE_ATTACHMENTS_VALUE]
|
||||
{links}
|
@ -1,44 +1,48 @@
|
||||
[SETTING_MESSAGES_HELP]
|
||||
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!
|
||||
If you've given the permission retroactively, make sure to reconfigure the channel to ensure the bot creates the webhook.
|
||||
|
||||
[SETTING_MEMBERS_HELP]
|
||||
Configure member logging for your server.
|
||||
|
||||
**Usable tags:**
|
||||
`{mention}` - mentions the user
|
||||
`{tag}` - username#discriminator
|
||||
`{username}` - username
|
||||
`{guildsize}` - member count of the server
|
||||
`{guildname}` - name of the server
|
||||
`{accage}` - age of the account
|
||||
`{id}` - ID of the account
|
||||
|
||||
[SETTING_DMINFRACTION_HELP]
|
||||
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.
|
||||
|
||||
[SETTING_DMINFRACTION_VALID]
|
||||
Valid choices are **{valid}**.
|
||||
|
||||
// Memberlogs
|
||||
[SETTING_MEMBERLOG_JOIN]
|
||||
》 Join Message
|
||||
|
||||
[SETTING_MEMBERLOG_LEAVE]
|
||||
》 Leave Message
|
||||
|
||||
[SETTING_ERRORS_HELP]
|
||||
Log issues that arise due to configuration.
|
||||
Most common outputs will be from automod.
|
||||
|
||||
[SETTING_MODERATION_HELP]
|
||||
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.
|
||||
|
||||
[SETTING_NICKNAMES_HELP]
|
||||
Configure member nickname logging for your server.
|
||||
|
||||
[SETTING_VOICE_HELP]
|
||||
Configure logging of voice joins and leaves for your server.
|
||||
[SETTING_MESSAGES_HELP]
|
||||
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!
|
||||
If you've given the permission retroactively, make sure to reconfigure the channel to ensure the bot creates the webhook.
|
||||
|
||||
[SETTING_MEMBERS_HELP]
|
||||
Configure member logging for your server.
|
||||
|
||||
**Usable tags:**
|
||||
`{mention}` - mentions the user
|
||||
`{tag}` - username#discriminator
|
||||
`{username}` - username
|
||||
`{guildsize}` - member count of the server
|
||||
`{guildname}` - name of the server
|
||||
`{accage}` - age of the account
|
||||
`{id}` - ID of the account
|
||||
|
||||
[SETTING_DMINFRACTION_HELP]
|
||||
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.
|
||||
|
||||
[SETTING_DMINFRACTION_VALID]
|
||||
Valid choices are **{valid}**.
|
||||
|
||||
// Memberlogs
|
||||
[SETTING_MEMBERLOG_JOIN]
|
||||
》 Join Message
|
||||
|
||||
[SETTING_MEMBERLOG_LEAVE]
|
||||
》 Leave Message
|
||||
|
||||
[SETTING_ERRORS_HELP]
|
||||
Log issues that arise due to configuration.
|
||||
Most common outputs will be from automod.
|
||||
|
||||
[SETTING_MODERATION_HELP]
|
||||
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.
|
||||
|
||||
[SETTING_NICKNAMES_HELP]
|
||||
Configure member nickname logging for your server.
|
||||
|
||||
[SETTING_VOICE_HELP]
|
||||
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.
|
Loading…
Reference in New Issue
Block a user