Compare commits

...

24 Commits

Author SHA1 Message Date
f3c0c1308c Merge branch 'development' into rejoin-logger 2024-09-30 18:06:13 +03:00
c037a8abe9 Update readme 2024-09-30 17:59:56 +03:00
9119316438 Refactor how wrappers store data
Added botban inhibitor
Added docker compose for bootstrapping databases
2024-09-30 17:56:24 +03:00
79f7c00670 Fix order of operations 2024-07-28 18:51:07 +03:00
d963cf3db5 Merge main 2024-07-26 17:32:29 +03:00
51ec1189c0 Added quotes to ping command 2024-07-26 17:11:13 +03:00
390c0ee840 upgrade dependencies 2024-07-26 17:11:01 +03:00
8199399323 Include past contributor list 2024-07-26 17:10:48 +03:00
34923bee9e Merge 2024-07-20 14:48:44 +03:00
dc2602724c Merge branch 'development' into alpha 2024-07-20 14:44:45 +03:00
fa45aeab42 Add link to repo in help command 2024-07-20 14:42:16 +03:00
ccb5ce3000 Add jsdoc comments 2024-07-19 21:35:30 +03:00
c081cdc1bf Update readme 2024-07-19 21:30:01 +03:00
2c4c4702b4 upgrade yarn 2024-07-19 21:29:55 +03:00
cc3ca2580d Merge branch 'development' of https://git.corgi.wtf/galactic/galactic-bot into development 2024-07-19 19:59:50 +03:00
96b459a8aa Add license and copyright attribution 2024-07-18 20:06:49 +03:00
d637a197b2 Add README.md 2024-07-12 15:47:20 +02:00
de26cdec44 More debug logging, added dev command to enable debug logging in specific guild (#9)
Reviewed-on: #9
Co-authored-by: Navy.gif <navydotgif@gmail.com>
Co-committed-by: Navy.gif <navydotgif@gmail.com>
2024-07-12 15:28:29 +02:00
2a43ad7feb More debug logging, added dev command to enable debug logging in specific guild 2024-07-12 16:25:13 +03:00
a3e6793632 Various fixes
Some checks failed
/ Tests (push) Has been cancelled
Hopefully prevent danging child processes
Reduce unnecessary db calls in moderation manager
2024-04-01 14:01:30 +03:00
5f07fd3e15 Eval command tweaks 2024-04-01 13:47:28 +03:00
c5c304d5d9 Refactor poll command 2024-04-01 13:47:17 +03:00
bc467e4dd9 Define test workflow 2024-04-01 13:27:47 +03:00
D3vision
f288293fd5 adding tracking functionality to kick command 2023-12-15 21:23:12 +01:00
33 changed files with 2623 additions and 2037 deletions

View File

@ -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": [

View File

@ -1,3 +1,7 @@
compressionLevel: mixed
enableGlobalCache: false
nodeLinker: node-modules
npmRegistryServer: "https://registry.corgi.wtf"

View File

@ -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[],
@ -502,3 +504,14 @@ 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
View File

@ -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
View File

@ -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,

View File

@ -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
@ -213,3 +208,8 @@ 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
View 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

View File

@ -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`
@ -41,3 +43,7 @@ New feature branch -> development (local bots) -> alpha (QA) -> beta (GalacticTe
- Linter
- Build test
- Unit tests
# Contributors
**Nolan** - https://github.com/noolaan
Major framework contributions to V3 development.

47
docker-compose.yml Normal file
View 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:

View File

@ -11,7 +11,8 @@
"prefix": "!",
"developers": [
"132777808362471424",
"187613017733726210"
"187613017733726210",
"132620781791346688"
],
"developmentMode": false,
"libraryOptions": {
@ -35,7 +36,11 @@
"GuildMessageReactions",
"DirectMessages"
],
"invalidRequestWarningInterval": 500
"invalidRequestWarningInterval": 500,
"rest": {
"timeout": 30000,
"retries": 5
}
},
"invite": "https://discord.gg/WDCTKGp",
"slashCommands": {
@ -79,7 +84,7 @@
]
},
"mariadb": {
"load": false,
"load": true,
"tables": []
}
},

View File

@ -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;

View 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;

View File

@ -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)

View File

@ -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
});
}

View File

@ -17,6 +17,7 @@
import { SlashCommand } from '../../../interfaces/index.js';
import DiscordClient from '../../../DiscordClient.js';
import InvokerWrapper from '../../wrappers/InvokerWrapper.js';
import Quotes from '../../../../constants/Quotes.js';
class PingCommand extends SlashCommand
{
@ -37,7 +38,10 @@ class PingCommand extends SlashCommand
const { ping } = this.client.ws;
const number = (ping / 40);
const repeat = number > 1 ? number : 1;
return invoker.reply(`P${'o'.repeat(repeat)}ng! \`${ping}ms\``, { emoji: 'success' });
const index = Math.floor(Quotes.length * Math.random());
const [ quote, author ] = Quotes[index];
return invoker.reply(`P${'o'.repeat(repeat)}ng! \`${ping}ms\`\n\n> ${quote.replaceAll('\n', '\n> ')}\n\\- *${author}*`, { emoji: 'success' });
}
}

View 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;

View File

@ -24,7 +24,7 @@ class ChannelIgnore extends Inhibitor
{
super(client, {
name: 'channelIgnore',
priority: 9,
priority: 8,
guild: true,
silent: true
});

View File

@ -25,7 +25,7 @@ class ClientPermissions extends Inhibitor
{
super(client, {
name: 'clientPermissions',
priority: 10,
priority: 9,
guarded: true,
guild: true
});

View File

@ -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;

View File

@ -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;

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

@ -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;

View File

@ -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)

View File

@ -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;
// 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;
// }

View File

@ -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();
}

View File

@ -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);

View File

@ -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 ()

23
src/constants/Quotes.ts Normal file
View File

@ -0,0 +1,23 @@
export default [
['Pacifism is objectively pro-fascist. This is elementary common sense. If you hamper the war effort of one side, you automatically help out that of the other. Nor is there any real way of remaining outside such a war as the present one. In practice, \'he that is not with me is against me\'.', 'George Orwell'],
['If people are good only because they fear punishment, and hope for reward, then we are a sorry lot indeed.', 'Albert Einstein'],
['The ultimate tragedy is not the oppression and cruelty by the bad people but the silence over that by the good people.', 'Martin Luther King Jr.'],
['The Three Laws of Robotics are:\n**1.** A robot may not injure a human being or, through inaction, allow a human being to come to harm.\n**2.** A robot must obey the orders given it by human beings except where such orders would conflict with the First Law.\n**3.** A robot must protect its own existence as long as such protection does not conflict with the First or Second Laws.', 'Isaac Asimov'],
['That\'s why they call it the American Dream, because you have to be asleep to believe it.', 'George Carlin'],
['Think of how stupid the average person is, and realize half of them are stupider than that.', 'George Carlin'],
['In another life, I would have really liked just doing laundry and taxes with you.', 'Waymond Wang (Everything Everywhere All at Once)'],
['I\'m not upset that you lied to me, I\'m upset that I can\'t trust you again.', 'Friedrich Nietzsche'],
['Religion is the impotence of the human mind to deal with occurrences it cannot understand.', 'Karl Marx'],
['He wears a mask, and his face grows to fit it.', 'George Orwell'],
['Darkness cannot drive out darkness: only light can do that. Hate cannot drive out hate: only love can do that.', 'Martin Luther King Jr.'],
['Without music, life would be a mistake.', 'Friedrich Nietzsche'],
['People don\'t get what they deserve. They just get what they get. There\'s nothing any of us can do about it.', 'Gregory House (House)'],
['Rational arguments don\'t usually work on religious people. Otherwise there would be no religious people.', 'Gregory House (House)'],
['It\'s never lupus', 'Gregory House (House)'],
['Almost dying changes nothing. Dying changes everything.', 'Gregory House (House)'],
['Do not go gentle into that good night,\nOld age should burn and rave at close of day;\nRage, rage against the dying of the light.', 'Dylan Thomas'],
['Mankind was born on Earth. It was never meant to die here.','Joseph Cooper (Interstellar)'],
['Once you\'re a parent, you\'re the ghost of your children\'s future.','Joseph Cooper (Interstellar)'],
['When we deny the EVIL within ourselves, we dehumanize ourselves, and we deprive ourselves not only of our own destiny but of any possibility of dealing with the EVIL of others.','J. Robert Oppenheimer'],
['Are you not entertained? Are you not entertained? Is this not why you are here?', 'Maximus Decimus Meridius (Gladiator)']
]

View File

@ -27,3 +27,6 @@ The command **{command}** can only be run by developers.
[INHIBITOR_GUILDONLY_ERROR]
The command **{command}** is only available in servers.
[INHIBITOR_BANNED_ERROR]
{noun} has been banned from using the bot.

View File

@ -0,0 +1,5 @@
[NOUN_USER]
user
[NOUN_GUILD]
server

View File

@ -163,6 +163,11 @@ User ID: {id}
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}**

View File

@ -42,3 +42,7 @@ 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.

3262
yarn.lock

File diff suppressed because it is too large Load Diff