aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--config/Config.ts214
-rw-r--r--config/example-options.ts2
-rw-r--r--src/bot.ts2
-rw-r--r--src/commands/info/help.ts13
-rw-r--r--src/commands/info/links.ts11
-rw-r--r--src/commands/utilities/wolframAlpha.ts13
-rw-r--r--src/lib/common/Sentry.ts6
-rw-r--r--src/lib/utils/BushClientUtils.ts46
-rw-r--r--src/lib/utils/BushLogger.ts3
-rw-r--r--src/lib/utils/BushUtils.ts6
-rw-r--r--src/listeners/message/directMessage.ts12
11 files changed, 266 insertions, 62 deletions
diff --git a/config/Config.ts b/config/Config.ts
index 7504658..4aae10a 100644
--- a/config/Config.ts
+++ b/config/Config.ts
@@ -1,15 +1,52 @@
import type { Snowflake } from 'discord.js';
-export class Config {
+/**
+ * Different options for the bot.
+ */
+export class Config implements ConfigOptions {
+ /**
+ * Credentials for various services that the bot depends on.
+ */
public credentials: Credentials;
+
+ /**
+ * The environment that the bot is operating under.
+ */
public environment: Environment;
+
+ /**
+ * Bot developers.
+ */
public owners: Snowflake[];
+
+ /**
+ * A string that needs to come before text commands.
+ */
public prefix: string;
+
+ /**
+ * Various discord channels that logs will be sent in.
+ */
public channels: Channels;
+
+ /**
+ * Options for the Postgres database connection.
+ */
public db: DataBase;
+
+ /**
+ * Options for what events to log.
+ */
public logging: Logging;
+
+ /**
+ * Information regarding the bot's support server.
+ */
public supportGuild: SupportGuild;
+ /**
+ * @param options The options
+ */
public constructor(options: ConfigOptions) {
this.credentials = options.credentials;
this.environment = options.environment;
@@ -32,6 +69,8 @@ export class Config {
return this.credentials.betaToken;
case 'development':
return this.credentials.devToken;
+ default:
+ throw new TypeError(`Unexpected environment: "${this.environment}"`);
}
}
@@ -57,52 +96,179 @@ export class Config {
}
}
+/**
+ * The options to be provided to the {@link Config} class.
+ */
export interface ConfigOptions {
+ /**
+ * Credentials for various services that the bot depends on.
+ */
credentials: Credentials;
+
+ /**
+ * The environment that the bot is operating under.
+ */
environment: Environment;
+
+ /**
+ * Bot developers.
+ */
owners: Snowflake[];
+
+ /**
+ * A string that needs to come before text commands.
+ */
prefix: string;
+
+ /**
+ * Various discord channels that logs will be sent in.
+ */
channels: Channels;
+
+ /**
+ * Options for the Postgres database connection.
+ */
db: DataBase;
+
+ /**
+ * Options for what events to log.
+ */
logging: Logging;
+
+ /**
+ * Information regarding the bot's support server.
+ */
supportGuild: SupportGuild;
}
-interface Credentials {
+/**
+ * Credentials for various services that the bot depends on.
+ */
+export interface Credentials {
+ /**
+ * The discord bot token - used when in a 'production' environment.
+ */
token: string;
+
+ /**
+ * The discord bot token - used when in a 'beta' environment.
+ */
betaToken: string;
+
+ /**
+ * The discord bot token - used when in a 'development' environment.
+ */
devToken: string;
- hypixelApiKey: string;
- wolframAlphaAppId: string;
- imgurClientId: string;
- imgurClientSecret: string;
- sentryDsn: string;
- perspectiveApiKey: string;
-}
-type Environment = 'production' | 'beta' | 'development';
+ /**
+ * Api Key for the Hypixel Minecraft Java server.
+ * @see {@link https://api.hypixel.net/#section/Authentication/ApiKey}
+ */
+ hypixelApiKey: string | null;
+
+ /**
+ * The app id for an API Application for WorlframAlpha
+ * @see {@link https://products.wolframalpha.com/api/}
+ */
+ wolframAlphaAppId: string | null;
-interface Channels {
- log: Snowflake;
- error: Snowflake;
- dm: Snowflake;
- servers: Snowflake;
+ /**
+ * The client id for Imgur's API
+ * @see {@link https://apidocs.imgur.com/#authorization-and-oauth}
+ */
+ imgurClientId: string | null;
+
+ /**
+ * The client secret for Imgur's API
+ * @see {@link https://apidocs.imgur.com/#authorization-and-oauth}
+ */
+ imgurClientSecret: string | null;
+
+ /**
+ * The sentry DSN (Data Source Name) for error reporting
+ * @see {@link https://docs.sentry.io/product/sentry-basics/dsn-explainer/}
+ */
+ sentryDsn: string | null;
+
+ /**
+ * The Perspective API Key
+ * @see {@link https://perspectiveapi.com/}
+ */
+ perspectiveApiKey: string | null;
}
-interface DataBase {
+/**
+ * The possible environments that the bot can be running in.
+ */
+export type Environment = 'production' | 'beta' | 'development';
+
+/**
+ * Various discord channels that logs will be sent in.
+ */
+export type Channels = {
+ /**
+ * The id of a channel to send logging messages in,
+ * use an empty string for no channel to be used.
+ */
+ [Key in ConfigChannelKey]: Snowflake | '';
+};
+
+/**
+ * The type of information to be sent in the configured channel.
+ */
+export type ConfigChannelKey = 'log' | 'error' | 'dm' | 'servers';
+
+/**
+ * Options for the Postgres database connection.
+ */
+export interface DataBase {
+ /**
+ * The host of the database.
+ */
host: string;
+
+ /**
+ * The port of the database.
+ */
port: number;
+
+ /**
+ * The username which is used to authenticate against the database.
+ */
username: string;
+
+ /**
+ * The password which is used to authenticate against the database.
+ */
password: string;
}
-interface Logging {
- db: boolean;
- verbose: boolean;
- info: boolean;
-}
+/**
+ * Options for what events to log.
+ */
+export type Logging = {
+ /**
+ * Whether or not to log database queries, verbose logs, or informational logs
+ */
+ [Key in LoggingType]: boolean;
+};
-interface SupportGuild {
- id: Snowflake;
- invite: string;
+/**
+ * The logging level that can be changed.
+ */
+export type LoggingType = 'db' | 'verbose' | 'info';
+
+/**
+ * Information regarding the bot's support server.
+ */
+export interface SupportGuild {
+ /**
+ * The id of the support server.
+ */
+ id: Snowflake | null;
+
+ /**
+ * An invite link to the support server.
+ */
+ invite: string | null;
}
diff --git a/config/example-options.ts b/config/example-options.ts
index 1289c8d..43b805e 100644
--- a/config/example-options.ts
+++ b/config/example-options.ts
@@ -9,7 +9,7 @@ export default new Config({
wolframAlphaAppId: '[APP_ID]',
imgurClientId: '[CLIENT_ID]',
imgurClientSecret: '[CLIENT_SECRET]',
- sentryDsn: 'SENTRY_DSN',
+ sentryDsn: '[SENTRY_DSN]',
perspectiveApiKey: '[PERSPECTIVE_API_KEY]'
},
environment: 'development',
diff --git a/src/bot.ts b/src/bot.ts
index 7d8327e..d7b5ad9 100644
--- a/src/bot.ts
+++ b/src/bot.ts
@@ -10,7 +10,7 @@ const { Sentry } = await import('./lib/common/Sentry.js');
const { BushClient } = await import('./lib/index.js');
const isDry = process.argv.includes('dry');
-if (!isDry) new Sentry(dirname(fileURLToPath(import.meta.url)) || process.cwd());
+if (!isDry && config.credentials.sentryDsn !== null) new Sentry(dirname(fileURLToPath(import.meta.url)) || process.cwd(), config);
BushClient.extendStructures();
const client = new BushClient(config);
if (!isDry) await client.dbPreInit();
diff --git a/src/commands/info/help.ts b/src/commands/info/help.ts
index ec079e7..d8d91d5 100644
--- a/src/commands/info/help.ts
+++ b/src/commands/info/help.ts
@@ -211,14 +211,17 @@ export default class HelpCommand extends BushCommand {
private addLinks(message: CommandMessage | SlashMessage) {
const row = new ActionRowBuilder<ButtonBuilder>();
+ const config = this.client.config;
- if (!this.client.config.isDevelopment && !this.client.guilds.cache.some((guild) => guild.ownerId === message.author.id)) {
+ if (!config.isDevelopment && !this.client.guilds.cache.some((guild) => guild.ownerId === message.author.id)) {
row.addComponents(new ButtonBuilder({ style: ButtonStyle.Link, label: 'Invite Me', url: invite(this.client) }));
}
- if (!this.client.guilds.cache.get(this.client.config.supportGuild.id)?.members.cache.has(message.author.id)) {
- row.addComponents(
- new ButtonBuilder({ style: ButtonStyle.Link, label: 'Support Server', url: this.client.config.supportGuild.invite })
- );
+ if (
+ config.supportGuild.id &&
+ config.supportGuild.invite &&
+ !this.client.guilds.cache.get(config.supportGuild.id)?.members.cache.has(message.author.id)
+ ) {
+ row.addComponents(new ButtonBuilder({ style: ButtonStyle.Link, label: 'Support Server', url: config.supportGuild.invite }));
}
if (packageDotJSON?.repository)
row.addComponents(new ButtonBuilder({ style: ButtonStyle.Link, label: 'GitHub', url: packageDotJSON.repository }));
diff --git a/src/commands/info/links.ts b/src/commands/info/links.ts
index 3671c6c..3c7add2 100644
--- a/src/commands/info/links.ts
+++ b/src/commands/info/links.ts
@@ -24,10 +24,13 @@ export default class LinksCommand extends BushCommand {
if (!this.client.config.isDevelopment || message.author.isOwner()) {
buttonRow.addComponents(new ButtonBuilder({ style: ButtonStyle.Link, label: 'Invite Me', url: invite(this.client) }));
}
- buttonRow.addComponents(
- new ButtonBuilder({ style: ButtonStyle.Link, label: 'Support Server', url: this.client.config.supportGuild.invite }),
- new ButtonBuilder({ style: ButtonStyle.Link, label: 'GitHub', url: packageDotJSON.repository })
- );
+ const supportInvite = this.client.config.supportGuild.invite;
+
+ if (supportInvite) {
+ buttonRow.addComponents(new ButtonBuilder({ style: ButtonStyle.Link, label: 'Support Server', url: supportInvite }));
+ }
+
+ buttonRow.addComponents(new ButtonBuilder({ style: ButtonStyle.Link, label: 'GitHub', url: packageDotJSON.repository }));
return await message.util.reply({ content: 'Here are some useful links:', components: [buttonRow] });
}
}
diff --git a/src/commands/utilities/wolframAlpha.ts b/src/commands/utilities/wolframAlpha.ts
index 3cd0653..bac9f58 100644
--- a/src/commands/utilities/wolframAlpha.ts
+++ b/src/commands/utilities/wolframAlpha.ts
@@ -54,8 +54,17 @@ export default class WolframAlphaCommand extends BushCommand {
) {
if (message.util.isSlashMessage(message)) await message.interaction.deferReply();
- args.image && void message.util.reply({ content: `${emojis.loading} Loading...`, embeds: [] });
- const waApi = WolframAlphaAPI(this.client.config.credentials.wolframAlphaAppId);
+ const appId = this.client.config.credentials.wolframAlphaAppId;
+
+ if (appId === null || appId === '' || appId === '[APP_ID]')
+ return message.util.reply(
+ message.author.isSuperUser()
+ ? `${emojis.error} The 'wolframAlphaAppId' credential isn't set so this command cannot be used.`
+ : `${emojis.error} Sorry, this command is unavailable.`
+ );
+
+ if (args.image) void message.util.reply({ content: `${emojis.loading} Loading...`, embeds: [] });
+ const waApi = WolframAlphaAPI(appId);
const decodedEmbed = new EmbedBuilder().addFields({
name: '📥 Input',
diff --git a/src/lib/common/Sentry.ts b/src/lib/common/Sentry.ts
index 34bc06f..2792203 100644
--- a/src/lib/common/Sentry.ts
+++ b/src/lib/common/Sentry.ts
@@ -1,10 +1,12 @@
import { RewriteFrames } from '@sentry/integrations';
import * as SentryNode from '@sentry/node';
import { Integrations } from '@sentry/node';
-import config from '../../../config/options.js';
+import type { Config } from '../../../config/Config.js';
export class Sentry {
- public constructor(rootdir: string) {
+ public constructor(rootdir: string, config: Config) {
+ if (config.credentials.sentryDsn === null) throw TypeError('sentryDsn cannot be null');
+
SentryNode.init({
dsn: config.credentials.sentryDsn,
environment: config.environment,
diff --git a/src/lib/utils/BushClientUtils.ts b/src/lib/utils/BushClientUtils.ts
index 44a08ef..af49803 100644
--- a/src/lib/utils/BushClientUtils.ts
+++ b/src/lib/utils/BushClientUtils.ts
@@ -1,11 +1,13 @@
import assert from 'assert';
import {
cleanCodeBlockContent,
+ DMChannel,
escapeCodeBlock,
GuildMember,
Message,
+ PartialDMChannel,
Routes,
- TextChannel,
+ TextBasedChannel,
ThreadMember,
User,
type APIMessage,
@@ -15,6 +17,7 @@ import {
} from 'discord.js';
import got from 'got';
import _ from 'lodash';
+import { ConfigChannelKey } from '../../../config/Config.js';
import CommandErrorListener from '../../listeners/commands/commandError.js';
import { BushInspectOptions } from '../common/typings/BushInspectOptions.js';
import { CodeBlockLang } from '../common/typings/CodeBlockLang.js';
@@ -146,15 +149,16 @@ export class BushClientUtils {
* @returns The readable version of the key or the original key if there isn't a mapping.
*/
#mapCredential(key: string): string {
- const mapping = {
- token: 'Main Token',
- devToken: 'Dev Token',
- betaToken: 'Beta Token',
- hypixelApiKey: 'Hypixel Api Key',
- wolframAlphaAppId: 'Wolfram|Alpha App ID',
- dbPassword: 'Database Password'
- };
- return mapping[key as keyof typeof mapping] || key;
+ return (
+ {
+ token: 'Main Token',
+ devToken: 'Dev Token',
+ betaToken: 'Beta Token',
+ hypixelApiKey: 'Hypixel Api Key',
+ wolframAlphaAppId: 'Wolfram|Alpha App ID',
+ dbPassword: 'Database Password'
+ }[key] ?? key
+ );
}
/**
@@ -167,6 +171,7 @@ export class BushClientUtils {
const credential = { ...this.client.config.credentials, dbPassword: this.client.config.db.password }[
credentialName as keyof typeof this.client.config.credentials
];
+ if (credential === null || credential === '') continue;
const replacement = this.#mapCredential(credentialName);
const escapeRegex = /[.*+?^${}()|[\]\\]/g;
text = text.replace(new RegExp(credential.toString().replace(escapeRegex, '\\$&'), 'g'), `[${replacement} Omitted]`);
@@ -456,11 +461,24 @@ export class BushClientUtils {
}
/**
- * Gets a a configured channel as a TextChannel.
- * @channel The channel to retrieve.
+ * Resolves a channel from the config and ensures it is a non-dm-based-text-channel.
+ * @param channel The channel to retrieve.
*/
- public async getConfigChannel(channel: keyof Client['config']['channels']): Promise<TextChannel> {
- return (await this.client.channels.fetch(this.client.config.channels[channel])) as unknown as TextChannel;
+ public async getConfigChannel(
+ channel: ConfigChannelKey
+ ): Promise<Exclude<TextBasedChannel, DMChannel | PartialDMChannel> | null> {
+ const channels = this.client.config.channels;
+ if (!(channel in channels))
+ throw new TypeError(`Invalid channel provided (${channel}), must be one of ${Object.keys(channels).join(' ')}`);
+
+ const channelId = channels[channel];
+ if (channelId === '') return null;
+
+ const res = await this.client.channels.fetch(channelId);
+
+ if (!res?.isTextBased() || res.isDMBased()) return null;
+
+ return res;
}
}
diff --git a/src/lib/utils/BushLogger.ts b/src/lib/utils/BushLogger.ts
index 3cfd860..5c98760 100644
--- a/src/lib/utils/BushLogger.ts
+++ b/src/lib/utils/BushLogger.ts
@@ -155,6 +155,7 @@ export class BushLogger {
*/
public async channelLog(message: SendMessageType): Promise<Message | null> {
const channel = await this.client.utils.getConfigChannel('log');
+ if (channel === null) return null;
return await channel.send(message).catch(() => null);
}
@@ -322,5 +323,3 @@ export class BushLogger {
await this.channelLog({ embeds: [embed] }).catch(() => {});
}
}
-
-/** @typedef {PartialTextBasedChannelFields} vscodeDontDeleteMyImportTy */
diff --git a/src/lib/utils/BushUtils.ts b/src/lib/utils/BushUtils.ts
index 059d001..e3539a1 100644
--- a/src/lib/utils/BushUtils.ts
+++ b/src/lib/utils/BushUtils.ts
@@ -11,7 +11,7 @@ import {
} from '#lib';
import { humanizeDuration as humanizeDurationMod } from '@notenoughupdates/humanize-duration';
import assert from 'assert';
-import { exec } from 'child_process';
+import cp from 'child_process';
import deepLock from 'deep-lock';
import { Util as AkairoUtil } from 'discord-akairo';
import {
@@ -43,13 +43,15 @@ export function capitalize(text: string): string {
return text.charAt(0).toUpperCase() + text.slice(1);
}
+export const exec = promisify(cp.exec);
+
/**
* Runs a shell command and gives the output
* @param command The shell command to run
* @returns The stdout and stderr of the shell command
*/
export async function shell(command: string): Promise<{ stdout: string; stderr: string }> {
- return await promisify(exec)(command);
+ return await exec(command);
}
/**
diff --git a/src/listeners/message/directMessage.ts b/src/listeners/message/directMessage.ts
index 91733a5..7278e63 100644
--- a/src/listeners/message/directMessage.ts
+++ b/src/listeners/message/directMessage.ts
@@ -14,7 +14,11 @@ export default class DirectMessageListener extends BushListener {
if (message.channel.type === ChannelType.DM) {
if (!(message.author.id == this.client.user!.id) && message.author.bot) return;
if (this.client.cache.global.blacklistedUsers.includes(message.author.id)) return;
- const dmLogEmbed = new EmbedBuilder().setTimestamp().setFooter({ text: `User ID • ${message.channel.recipientId}` });
+
+ const dmLogEmbed = new EmbedBuilder()
+ .setTimestamp()
+ .setFooter({ text: `User ID • ${message.channel.recipientId}` })
+ .setDescription(`**DM:**\n${message.content}`);
if (message.author.id != this.client.user!.id) {
dmLogEmbed
@@ -22,7 +26,6 @@ export default class DirectMessageListener extends BushListener {
name: `From: ${message.author.username}`,
iconURL: `${message.author.displayAvatarURL()}`
})
- .setDescription(`**DM:**\n${message}`)
.setColor(colors.blue);
} else {
dmLogEmbed
@@ -30,9 +33,7 @@ export default class DirectMessageListener extends BushListener {
name: `To: ${message.channel.recipient?.username}`,
iconURL: `${message.channel.recipient?.displayAvatarURL()}`
})
- .setDescription(`**DM:**\n${message}`)
- .setColor(colors.red)
- .setTimestamp();
+ .setColor(colors.red);
}
if (message.attachments.filter((a) => typeof a.size == 'number').size == 1) {
dmLogEmbed.setImage(message.attachments.filter((a) => typeof a.size == 'number').first()!.proxyURL);
@@ -40,6 +41,7 @@ export default class DirectMessageListener extends BushListener {
dmLogEmbed.addFields({ name: 'Attachments', value: message.attachments.map((a) => a.proxyURL).join('\n') });
}
const dmChannel = await this.client.utils.getConfigChannel('dm');
+ if (dmChannel === null) return;
await dmChannel.send({ embeds: [dmLogEmbed] });
}
}