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
29 changed files with 948 additions and 402 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

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

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

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

@ -27,20 +27,47 @@ class DebugCommand extends Command
name: 'debug',
restricted: true,
moduleName: 'developer',
options: [
{
name: [ 'enable', 'disable' ],
type: CommandOptionType.SUB_COMMAND,
options: [
{ name: 'guild', required: true },
{ name: 'enabled', required: true, type: CommandOptionType.BOOLEAN }
]
},
{
name: 'list',
type: CommandOptionType.SUB_COMMAND,
}
]
});
}
async execute (_invoker: InvokerWrapper, options: CommandParams)
async execute (invoker: InvokerWrapper, options: CommandParams)
{
const { subcommand } = invoker;
if (!subcommand)
throw new Error('Missing subcommand');
if ([ 'enable', 'disable' ].includes(subcommand.name))
{
const guildId = options.guild!.asString;
return this.enableDebug(guildId, options.enabled?.asBool);
return this.toggleDebug(guildId, subcommand.name === 'enable');
}
async enableDebug (guildId: string, enabled = false)
if (subcommand.name === 'list')
return this.listDebugGuilds();
}
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

@ -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];
/**
* Check whether the user is banned from using the bot
*/
async botBanned (): Promise<boolean>
{
const data = await this.fetchData();
return data?.banned ?? false;
}
// 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 } }
// );
async setBotBan (value = true)
{
if (!this.#data)
await this.fetchData();
this.#data!.banned = value;
await this.updateData({ banned: value });
}
// 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)
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(
{ guildId: this.id },
{ $set: data }
);
this.#settings = {
...this.#settings,
...data
};
this.#storageLog(`Database Update (guild:${this.id}).`);
await this.#client.mongodb.users.updateOne({ id: this.id }, { $set: data });
this.#data = { ...this.#data, ...data } as UserData;
this.#storageLog('Data update');
}
catch (error)
catch (err)
{
this.#storageError(error as Error);
const error = err as Error;
this.#storageError(error);
}
return true;
}
get defaultConfig ()
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 ()

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.