@somehiddenkey/discord-command-utils 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,157 @@
1
+ # DiscordUtilsLibrary
2
+ The purpose of this Library is to hide and abstract many of the common aspects that comes with coding discord bots in javascript. The main focus is configurations as well as registering all types of commands and interactions between different guilds.
3
+ ## Installation
4
+ ### SSH Installation
5
+ Use this if your GitHub account already has SSH access -> requires SSH key set up
6
+ ```bash
7
+ npm install git+ssh://git@github.com/studytogether-discord/DiscordUtilsLibrary.git
8
+ ```
9
+
10
+ ### HTTPS Installation
11
+ If you want to use HTTPS instead, you must use a GitHub Personal Access Token (classic) with repo permissions (replace "TOKEN" with your PAT)
12
+ ```bash
13
+ npm install "https://TOKEN@github.com/studytogether-discord/DiscordUtilsLibrary.git"
14
+ ```
15
+
16
+ ### Libary Import
17
+ Then import it in your code
18
+ ```c#
19
+ import { Something } from "DiscordUtilsLibrary";
20
+ // or
21
+ const { Something } = require("DiscordUtilsLibrary");
22
+ ```
23
+
24
+ ## Configurations
25
+ We will use 3 different kinds of configurations:
26
+ - local : settings specific to this specific bot like it's prefix etc
27
+ - global : information used by all bot instances
28
+ - secret : environment variables that contain all the secrets of your bot instance
29
+ ### Local configuration
30
+ Loads the local configuration of that specific bot. It only contains public configs, no secrets, and may thus be included in the git tracing. Load a new instance of your local config through the following code:
31
+ ```js
32
+ export const localConfig = LocalConfig.load("./configs/local_config.json", Environments.DEV);
33
+ ```
34
+
35
+ Your local configuration JSON file should look like this:
36
+ ```json
37
+ {
38
+ "production": {
39
+ "prefix": "!",
40
+ "client_id": "123456789123456789",
41
+ "local": {
42
+ "any_local_configuration": "any values"
43
+ },
44
+ "rest_options" : {
45
+ "version": "10",
46
+ "rejectOnRateLimit": ["/channels"]
47
+ }
48
+ },
49
+ "development": {
50
+ "prefix": "!",
51
+ "client_id": "123456789123456789",
52
+ "local": {
53
+ "any_local_configuration": "any values"
54
+ },
55
+ "rest_options" : {
56
+ "version": "10",
57
+ "rejectOnRateLimit": ["/channels"]
58
+ }
59
+ },
60
+ "activity": {
61
+ "name": "the communities",
62
+ "type": 0
63
+ }
64
+ }
65
+ ```
66
+ - `client_id` : ID of your bot user
67
+ - `rest_options` : Must be of type [Partial<RESTOptions>](https://discord.js.org/docs/packages/rest/2.6.0/RESTOptions:Interface)
68
+ - `activity` : Must be of type [ActivitiesOptions](https://discord.js.org/docs/packages/discord.js/14.25.1/ActivitiesOptions:Interface)
69
+
70
+ ### Global configuration
71
+ Loads the global configuration that contains information related to all bot instances, like snowflake IDs of channels, categories, guild, etc. Load a new instance of your global config through the following code:
72
+ ```js
73
+ export const globalConfig = GlobalConfig.load("../global_config.json", Environments.DEV);
74
+ ```
75
+ It contains the IDs of the following sections: guilds, categories, channels, log channels and roles. These can be used as constant values throughout your code, making sure that if an ID ever changes, that all bot instances would take the new updated value by simply updating the json once.
76
+
77
+ ### Secrets configuration
78
+ Loads the environment variables that contain all secrets for that bot, like your bot token and database credentials. It goes without saying that these actual values may never be git traced. Load new instance of your secret config through the following code:
79
+ ```js
80
+ import { config } from 'dotenv';
81
+
82
+ config();
83
+ export const secretConfig = new SecretConfig(process.env);
84
+ ```
85
+ Your .env file should look like this:
86
+ ```bash
87
+ NODE_ENV="production"
88
+
89
+ TOKEN="someDiscordBotToken"
90
+
91
+ DATABASE__0__HOST="localhost"
92
+ DATABASE__0__USER="userName"
93
+ DATABASE__0__NAME="databaseName"
94
+ DATABASE__0__PASSWORD="passwordValue"
95
+ ```
96
+ Please take the following into account:
97
+ - `NODE_ENV` must be of one of the following values:
98
+ - `"development"` : Uses your development configurations for local testing
99
+ - `"production"` : Uses your production configurations for PRD env. Do not use for local tests, it won't work
100
+ - If you use multiple databases, prefix each one with another index, starting at `0`
101
+
102
+ You can retrieve the various database configurations with the following code:
103
+ ```js
104
+ export const db_connection_0 = knex(secretConfig.DatabaseConfig[0]);
105
+ ```
106
+ You can initiate the bot with the following code:
107
+ ```js
108
+ bot.login(secretConfig.Token);
109
+ ```
110
+ ## Interactions
111
+ ### Register Interaction
112
+ You can register new interaction callbacks based on a set of factors:
113
+ - the scope : type `InteractionScope` -- in which server is this interaction called (main, tutor, communities, dm, etc)
114
+ - the type : type `InteractionType` -- is it a button, a modal, a slash command, a message, etc
115
+ - the unique id: custom unique ID per interaction of this type
116
+
117
+ An example for a slash command called in the main server:
118
+ ```js
119
+ Interaction.OnSlashCommand(
120
+ "ping",
121
+ InteractionScope.Main,
122
+ async (interaction) => await interaction.reply("pong")
123
+ );
124
+ ```
125
+ The same exists for:
126
+ - Button : `OnButton`
127
+ - Selection menu : `OnSelectMenu`
128
+ - Message : `OnMessageCommand`
129
+ - Modal submission : `OnModal`
130
+ ### Call Interaction
131
+ All interactions get stored in a so-called `InteractionContainer` that still needs to be called:
132
+ ```js
133
+ const interactionContainer = new InteractionContainer(localConfig, globalConfig);
134
+
135
+ bot.on(Events.MessageCreate, async (message) => {
136
+ await interactionContainer.call_message_interaction(message, e => consola.error(e))
137
+ });
138
+
139
+ bot.on(Events.InteractionCreate, async (interaction) =>
140
+ await interactionContainer.call_interaction(message, e => consola.error(e))
141
+ );
142
+ ```
143
+ ### Register slash commands to REST
144
+ For slash commands to work, you need to register them to a bot. There are two kinds:
145
+ - Guild commands : a slash command that will only be registered in one specific guild
146
+ - Public commands : a slash command available in all guilds that have this bot
147
+
148
+ You can add a set of slash commands through the following abstraction:
149
+ ```js
150
+ const rest = interactionContainer.Rest
151
+ .add([firstSlashCommand, secondSlashCommand], localConfig.guilds.tutor)
152
+ .add([firstSlashCommand, secondSlashCommand], localConfig.guilds.community)
153
+ .add([somePublicSlashCommand])
154
+ .register(secretConfig.token);
155
+ ```
156
+ ## Utils
157
+ Contains various check functions to verify the access level of a user as well as useful enums for scoping, community modlevels, etc
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@somehiddenkey/discord-command-utils",
3
+ "version": "1.0.1",
4
+ "description": "A utility library for building Discord bot commands using discord.js",
5
+ "author": {
6
+ "email": "k3y.throwaway@gmail.com",
7
+ "name": "Key"
8
+ },
9
+ "license": "ISC",
10
+ "type": "module",
11
+ "main": "index.js",
12
+ "scripts": {
13
+ "test": "echo \"Error: no test specified\" && exit 1"
14
+ },
15
+ "private": false,
16
+ "keywords": [],
17
+ "dependencies": {
18
+ "consola": "^3.4.2",
19
+ "discord.js": "^14.25.1"
20
+ }
21
+ }
@@ -0,0 +1,8 @@
1
+ class ConfigError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = "ConfigError";
5
+ }
6
+ }
7
+
8
+ export default ConfigError;
@@ -0,0 +1,265 @@
1
+ /**
2
+ * @typedef {string} Snowflake
3
+ */
4
+
5
+ import fs from 'fs';
6
+ import ConfigError from "./ConfigError";
7
+ import consola from 'consola';
8
+ import { InteractionScope } from "../Utils/enums";
9
+
10
+ export default class GlobalConfig {
11
+ /** @type {Guilds} */
12
+ guilds;
13
+
14
+ /** @type {Categories} */
15
+ categories;
16
+
17
+ /** @type {Channels} */
18
+ channels;
19
+
20
+ /** @type {Logs} */
21
+ logs;
22
+
23
+ /** @type {Roles} */
24
+ roles;
25
+
26
+ constructor(data) {
27
+ if (!data)
28
+ throw new ConfigError('No data initialized for GlobalConfig');
29
+
30
+ this.guilds = new Guilds(data.guilds);
31
+ this.categories = new Categories(data.categories);
32
+ this.channels = new Channels(data.channels);
33
+ this.logs = new Logs(data.logs);
34
+ this.roles = new Roles(data.roles);
35
+ }
36
+
37
+ /**
38
+ * Loads the JSON file and returns a GlobalConfig instance.
39
+ * @param {string} path
40
+ * @param {string} environment
41
+ * @returns {GlobalConfig}
42
+ */
43
+ static async load(path, environment) {
44
+ var json
45
+ try {
46
+ const raw = fs.readFileSync(path, { encoding: 'utf8', flag: 'r' });
47
+ json = JSON.parse(raw);
48
+ } catch (error) {
49
+ consola.error(`Failed to load GlobalConfig from path: ${path}`, error);
50
+ throw error;
51
+ }
52
+ if (environment == "production")
53
+ return new GlobalConfig(json.production);
54
+ else
55
+ return new GlobalConfig(json.development);
56
+ }
57
+ }
58
+
59
+
60
+ /* -----------------------
61
+ * Nested Classes
62
+ * ----------------------- */
63
+
64
+ class Guilds {
65
+ /** @type {Snowflake} */
66
+ main;
67
+ /** @type {Snowflake} */
68
+ community;
69
+ /** @type {Snowflake} */
70
+ tutor;
71
+ /** @type {Snowflake} */
72
+ staff;
73
+
74
+ constructor(data) {
75
+ Object.assign(this, data);
76
+ }
77
+
78
+ /**
79
+ * @param {Snowflake} guild_id
80
+ * @returns {InteractionScope} scope
81
+ */
82
+ get_scope(guild_id) {
83
+ switch(guild_id) {
84
+ case this.main:
85
+ return InteractionScope.Main;
86
+ case this.community:
87
+ return InteractionScope.Community;
88
+ case this.tutor:
89
+ return InteractionScope.Tutor;
90
+ case this.staff:
91
+ return InteractionScope.Staff;
92
+ default:
93
+ return InteractionScope.OtherGuild;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * @param {InteractionScope} scope
99
+ * @returns {Snowflake} guild_id
100
+ */
101
+ get_guild_id(scope) {
102
+ switch(scope) {
103
+ case InteractionScope.Main:
104
+ return this.main;
105
+ case InteractionScope.Community:
106
+ return this.community;
107
+ case InteractionScope.Tutor:
108
+ return this.tutor;
109
+ case InteractionScope.Staff:
110
+ return this.staff;
111
+ default:
112
+ return null;
113
+ }
114
+ }
115
+ }
116
+
117
+ class Categories {
118
+ /** @type {Snowflake} */
119
+ chat;
120
+ /** @type {[Snowflake]} */
121
+ custom_rooms;
122
+ /** @type {[Snowflake]} */
123
+ verified_com;
124
+ /** @type {Snowflake} */
125
+ chill;
126
+ /** @type {Snowflake} */
127
+ events;
128
+ /** @type {Snowflake} */
129
+ music;
130
+ /** @type {Snowflake} */
131
+ timer_25;
132
+ /** @type {Snowflake} */
133
+ timer_50;
134
+ /** @type {Snowflake} */
135
+ screen_cam;
136
+ /** @type {Snowflake} */
137
+ staff;
138
+ /** @type {Snowflake} */
139
+ subjects;
140
+ /** @type {Snowflake} */
141
+ subjects_col;
142
+ /** @type {Snowflake} */
143
+ subjects_uni;
144
+ /** @type {Snowflake} */
145
+ to_do;
146
+
147
+ /** @type {[Snowflake]} */
148
+ custom_study_rooms;
149
+ /** @type {[Snowflake]} */
150
+ study_rooms;
151
+
152
+ constructor(data) {
153
+ Object.assign(this, data);
154
+ this.custom_study_rooms = data.custom_rooms.concat(data.verified_com);
155
+ this.study_rooms = this.custom_study_rooms.concat([data.music, data.timer_25, data.timer_50, data.screen_cam]);
156
+ }
157
+ }
158
+
159
+ class Channels {
160
+ /** @type {Snowflake} */
161
+ timer_control;
162
+ /** @type {Snowflake} */
163
+ accountability;
164
+ /** @type {Snowflake} */
165
+ break_afk;
166
+ /** @type {Snowflake} */
167
+ create_room;
168
+ /** @type {Snowflake} */
169
+ commands;
170
+ /** @type {Snowflake} */
171
+ general;
172
+ /** @type {Snowflake} */
173
+ international;
174
+ /** @type {Snowflake} */
175
+ introductions;
176
+ /** @type {Snowflake} */
177
+ mod_commands;
178
+ /** @type {Snowflake} */
179
+ support_commands;
180
+ /** @type {Snowflake} */
181
+ session_goals;
182
+ /** @type {Snowflake} */
183
+ silent_study;
184
+ /** @type {Snowflake} */
185
+ welcome;
186
+
187
+ constructor(data) {
188
+ Object.assign(this, data);
189
+ }
190
+ }
191
+
192
+ class Logs {
193
+ /** @type {Snowflake} */
194
+ error;
195
+ /** @type {Snowflake} */
196
+ votekick;
197
+ /** @type {Snowflake} */
198
+ room_commands;
199
+ /** @type {Snowflake} */
200
+ room_admin;
201
+ /** @type {Snowflake} */
202
+ invites;
203
+ /** @type {Snowflake} */
204
+ command;
205
+ /** @type {Snowflake} */
206
+ communities;
207
+
208
+ constructor(data) {
209
+ Object.assign(this, data);
210
+ }
211
+ }
212
+
213
+ class Roles {
214
+ /** @type {Snowflake} */
215
+ cuckoo_ping;
216
+ /** @type {Snowflake} */
217
+ developer
218
+ /** @type {Snowflake} */
219
+ forest_ping;
220
+ /** @type {Snowflake} */
221
+ helper;
222
+ /** @type {Snowflake} */
223
+ member;
224
+ /** @type {Snowflake} */
225
+ music;
226
+ /** @type {Snowflake} */
227
+ muted;
228
+ /** @type {Snowflake} */
229
+ no_pc;
230
+ /** @type {Snowflake} */
231
+ screen_cam;
232
+ /** @type {Snowflake} */
233
+ studying;
234
+ /** @type {Snowflake} */
235
+ timer_25;
236
+ /** @type {Snowflake} */
237
+ timer_50;
238
+ /** @type {Snowflake} */
239
+ tutor;
240
+ /** @type {Snowflake} */
241
+ verified_com;
242
+ /** @type {Snowflake} */
243
+ water_ping;
244
+ /** @type {Snowflake} */
245
+ deepfocus;
246
+ /** @type {Snowflake} */
247
+ sr_staff;
248
+ /** @type {Snowflake} */
249
+ staff;
250
+ /** @type {Snowflake} */
251
+ jr_staff;
252
+ /** @type {Snowflake} */
253
+ st_team;
254
+ /** @type {Snowflake} */
255
+ coordinator;
256
+
257
+ /** @type {[Snowflake]} */
258
+ all_staff;
259
+
260
+
261
+ constructor(data) {
262
+ Object.assign(this, data);
263
+ this.all_staff = [this.jr_staff, this.sr_staff, this.st_team, this.coordinator];
264
+ }
265
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @typedef {string} Snowflake
3
+ */
4
+
5
+ import { RESTOptions } from '@discordjs/rest';
6
+ import { ActivitiesOptions } from 'discord.js';
7
+
8
+ export default class LocalConfig {
9
+ /** @type {string?} */
10
+ prefix;
11
+ /** @type {Snowflake} */
12
+ clientId;
13
+
14
+ /** @type {object} */
15
+ local;
16
+ /** @type {Partial<RESTOptions>} */
17
+ restOptions;
18
+ /** @type {ActivitiesOptions} */
19
+ activity;
20
+
21
+ constructor(data) {
22
+ this.prefix = data.prefix;
23
+ this.clientId = data.client_id;
24
+ this.local = data.local || {};
25
+ this.restOptions = data.rest_options || {};
26
+ this.activity = data.activity || {};
27
+
28
+ if(!this.client_id)
29
+ throw new ConfigError('No client_id initialized for LocalConfig');
30
+ }
31
+
32
+ /**
33
+ * Loads the JSON file and returns a GlobalConfig instance.
34
+ * @param {string} path
35
+ * @param {string} environment
36
+ * @returns {GlobalConfig}
37
+ */
38
+ static async load(path, environment) {
39
+ var json
40
+ try {
41
+ const raw = fs.readFileSync(path, { encoding: 'utf8', flag: 'r' });
42
+ json = JSON.parse(raw);
43
+ } catch (error) {
44
+ consola.error(`Failed to load GlobalConfig from path: ${path}`, error);
45
+ throw error;
46
+ }
47
+ if (environment == "production")
48
+ return new GlobalConfig({...json.production || json.activity});
49
+ else
50
+ return new GlobalConfig({...json.development || json.activity});
51
+ }
52
+ }
@@ -0,0 +1,51 @@
1
+ import consola from "consola";
2
+ import ConfigError from "./ConfigError";
3
+
4
+ /**
5
+ * @typedef {{client: string, connection: {host: string, user: string, password: string, database: string}}} DatabaseConfig
6
+ */
7
+
8
+ export default class SecretConfig {
9
+ /** @type {string} */
10
+ token;
11
+
12
+ /** @type {string} */
13
+ environment;
14
+
15
+ /** @type {[DatabaseConfig]} */
16
+ databaseConfig;
17
+
18
+ constructor(env) {
19
+ if (!env)
20
+ throw new ConfigError('env is required to initialize SecretConfig');
21
+
22
+ this.environment = env.NODE_ENV;
23
+ if (!this.environment)
24
+ throw new ConfigError('NODE_ENV is required to initialize SecretConfig');
25
+ if (!['production', 'development', 'testing'].includes(this.environment))
26
+ throw new ConfigError('Unrecognized NODE_ENV: '+this.environment);
27
+ this.token = env.TOKEN;
28
+
29
+ var databaseConfigs = [];
30
+ for (let index = 0, section = "DATABASE__"+index; !!env[section+"__NAME"]; index++) {
31
+ try {
32
+ let db_config = {
33
+ client: 'mysql',
34
+ connection: {
35
+ host: env[section+"__HOST"],
36
+ user: env[section+"__USER"],
37
+ password: env[section+"__PASSWORD"],
38
+ database: env[section+"__NAME"],
39
+ }
40
+ }
41
+
42
+ databaseConfigs.push(db_config);
43
+ consola.start(`Loaded database config for section: ${section}`);
44
+ } catch (error) {
45
+ consola.error(`Failed to load database config for section: ${section}`, error);
46
+ throw error;
47
+ }
48
+ }
49
+ this.databaseConfig = databaseConfigs;
50
+ }
51
+ }
@@ -0,0 +1,34 @@
1
+ import { Interaction as DiscordBaseInteraction, MessageFlags } from "discord.js";
2
+
3
+ export default class CommandError extends Error {
4
+ constructor(message) {
5
+ super(message);
6
+ this.name = "DiscordCommandError";
7
+ }
8
+
9
+ /**
10
+ * @param {DiscordBaseInteraction} interaction
11
+ * @returns {Promise<any>}
12
+ */
13
+ error_as_message(interaction){
14
+ return interaction.reply(this.message).then(msg => setTimeout(() => msg.delete(), 5000)).catch(() => {});
15
+ }
16
+
17
+ /**
18
+ * @param {DiscordBaseInteraction} interaction
19
+ * @returns {Promise<any>}
20
+ */
21
+ error_as_command(interaction){
22
+ return interaction
23
+ .editReply({
24
+ content: this.message,
25
+ flags: MessageFlags.Ephemeral,
26
+ })
27
+ .catch(() =>
28
+ interaction.reply({
29
+ content: this.message,
30
+ flags: MessageFlags.Ephemeral,
31
+ }))
32
+ .catch(() => {});
33
+ }
34
+ }
@@ -0,0 +1,96 @@
1
+ import {
2
+ Interaction as DiscordBaseInteraction,
3
+ OmitPartialGroupDMChannel,
4
+ Message,
5
+ ModalSubmitInteraction,
6
+ ButtonInteraction,
7
+ AnySelectMenuInteraction,
8
+ ChatInputCommandInteraction,
9
+ CacheType
10
+ } from 'discord.js';
11
+ import InteractionContainer from "./InteractionContainer";
12
+ import { InteractionType, InteractionScope } from "../Utils/enums";
13
+
14
+ /**
15
+ * @typedef { (interaction: DiscordBaseInteraction<CacheType>) => Promise<any> } InteractionFunction
16
+ * @typedef { (message: OmitPartialGroupDMChannel<Message<boolean>>) => Promise<any> } InteractionMsgFunction
17
+ * @typedef { string } InteractionID
18
+ */
19
+
20
+ export default class Interaction {
21
+ /** @type {string} */
22
+ id;
23
+ /** @type {InteractionType} */
24
+ type;
25
+ /** @type {InteractionScope} */
26
+ scope;
27
+ /** @type {InteractionFunction} */
28
+ interaction_command_function;
29
+
30
+ constructor(custom_id, type, scope, interaction_command_function) {
31
+ this.id = custom_id;
32
+ this.scope = scope;
33
+ this.type = type;
34
+ this.interaction_command_function = interaction_command_function;
35
+ }
36
+
37
+ /**
38
+ * Command wrapper
39
+ * @param {InteractionID} custom_id
40
+ * @param {InteractionType} type
41
+ * @param {InteractionScope|[InteractionScope]} scope
42
+ * @param {InteractionFunction} target
43
+ */
44
+ static On(custom_id, type, scope, target) {
45
+ if(Array.isArray(scope))
46
+ scope.forEach(s => InteractionContainer.add(new Interaction(custom_id, s, type, target)));
47
+ else;
48
+ InteractionContainer.add(new Interaction(custom_id, scope, type, target));
49
+ return custom_id;
50
+ }
51
+
52
+ /**
53
+ * @param {InteractionID} custom_id
54
+ * @param {InteractionScope|[InteractionScope]} scope
55
+ * @param {(interaction: ChatInputCommandInteraction) => Promise<any>} target
56
+ */
57
+ static OnSlashCommand(custom_id, scope, target) {
58
+ return Interaction.On(custom_id, InteractionType.Command, scope, target)
59
+ }
60
+
61
+ /**
62
+ * @param {InteractionID} custom_id
63
+ * @param {InteractionScope|[InteractionScope]} scope
64
+ * @param {(interaction: ButtonInteraction) => Promise<any>} target
65
+ */
66
+ static OnButton(custom_id, scope, target) {
67
+ return Interaction.On(custom_id, InteractionType.Button, scope, target)
68
+ }
69
+
70
+ /**
71
+ * @param {InteractionID} custom_id
72
+ * @param {(interaction: AnySelectMenuInteraction) => Promise<any>} target
73
+ * @param {InteractionScope|[InteractionScope]} scope
74
+ */
75
+ static OnSelectMenu(custom_id, scope, target) {
76
+ return Interaction.On(custom_id, InteractionType.SelectMenu, scope, target)
77
+ }
78
+
79
+ /**
80
+ * @param {InteractionID} custom_id
81
+ * @param {InteractionMsgFunction} target
82
+ * @param {InteractionScope|[InteractionScope]} scope
83
+ */
84
+ static OnMessageCommand(custom_id, scope, target) {
85
+ return Interaction.On(custom_id, InteractionType.Message, scope, target)
86
+ }
87
+
88
+ /**
89
+ * @param {InteractionID} custom_id
90
+ * @param {(interaction: ModalSubmitInteraction) => Promise<any>} target
91
+ * @param {InteractionScope|[InteractionScope]} scope
92
+ */
93
+ static OnModal(custom_id, scope, target) {
94
+ return Interaction.On(custom_id, InteractionType.Modal, scope, target)
95
+ }
96
+ }
@@ -0,0 +1,151 @@
1
+ import {
2
+ Interaction as DiscordBaseInteraction,
3
+ OmitPartialGroupDMChannel,
4
+ Message,
5
+ CacheType
6
+ } from 'discord.js';
7
+ import Interaction from "./Interaction";
8
+ import { InteractionScope, InteractionType } from "../Utils/enums";
9
+ import GlobalConfig from "../Configs/GlobalConfig";
10
+ import LocalConfig from "../Configs/LocalConfig";
11
+ import consola from "consola";
12
+ import CommandError from "./CommandError";
13
+ import RestCommands from './RestCommands';
14
+
15
+ /**
16
+ * @typedef { (interaction: DiscordBaseInteraction<CacheType>) => Promise<any> } InteractionFunction
17
+ * @typedef { (message: OmitPartialGroupDMChannel<Message<boolean>>) => Promise<any> } InteractionMsgFunction
18
+ * @typedef { string } InteractionID
19
+ */
20
+
21
+ export default class InteractionContainer {
22
+ /** @type {Map<InteractionID, Interaction>} */
23
+ static #interaction_container = new Map();
24
+
25
+ /** @type {GlobalConfig} */
26
+ #global_config;
27
+ /** @type {LocalConfig} */
28
+ #local_config;
29
+
30
+ /** @type {RestCommands} */
31
+ Rest = new RestCommands(this.#global_config, this.#local_config);
32
+
33
+ /**
34
+ * @param {InteractionType} type
35
+ * @param {InteractionScope} scope
36
+ * @param {string} id
37
+ * @returns {InteractionID}
38
+ */
39
+ static #make_key(type, scope, id) {
40
+ return `${type}__${scope}__${id}`;
41
+ }
42
+
43
+ /**
44
+ * @param {InteractionType} type
45
+ * @param {InteractionScope} scope
46
+ * @param {string} id
47
+ * @param {Interaction} base_interaction
48
+ */
49
+ static add(base_interaction) {
50
+ const key = InteractionContainer.#make_key(base_interaction.type, base_interaction.scope, base_interaction.id);
51
+ InteractionContainer.#interaction_container.set(key, base_interaction);
52
+ }
53
+
54
+ /**
55
+ * @param {InteractionType} type
56
+ * @param {InteractionScope} scope
57
+ * @param {string} id
58
+ * @returns {Interaction?}
59
+ */
60
+ static get(type, scope, id) {
61
+ return InteractionContainer.#interaction_container.get(InteractionContainer.#make_key(type, scope, id));
62
+ }
63
+
64
+ constructor(local_config, global_config) {
65
+ this.#local_config = local_config;
66
+ this.#global_config = global_config;
67
+ }
68
+
69
+ /**
70
+ * @param {DiscordBaseInteraction} interaction
71
+ * @param { (error: Error) => Promise<any> } on_error
72
+ * @returns {Promise<any>}
73
+ */
74
+ call_message_interaction(interaction, on_error = async () => {}) {
75
+ if (interaction.author.bot) return;
76
+
77
+ const scope =
78
+ (interaction.channel.type === ChannelType.DM) ?
79
+ InteractionScope.DM : this.#global_config.guilds.get_scope(interaction.guild.id);
80
+
81
+ const customId = interaction.content.slice(this.#local_config.prefix?.length)
82
+
83
+ return InteractionContainer
84
+ .#call(InteractionType.Message, scope, customId, interaction)
85
+ .catch(error => {
86
+ if (error instanceof CommandError)
87
+ return error.error_as_message()
88
+ else
89
+ return interaction
90
+ .reply("something went wrong")
91
+ .then(msg => setTimeout(() => msg.delete(), 5000))
92
+ .catch(() => {})
93
+ .then(() => on_error(error));
94
+ });
95
+ }
96
+
97
+ /**
98
+ * @param {DiscordBaseInteraction} interaction
99
+ * @param { (error: Error) => Promise<any> } on_error
100
+ * @returns {Promise<any>}
101
+ */
102
+ call_interaction(interaction, on_error = async () => {}) {
103
+ const scope =
104
+ (interaction.channel.type === ChannelType.DM) ?
105
+ InteractionScope.DM : this.#global_config.guilds.get_scope(interaction.guild.id);
106
+
107
+ const customId = (interaction.commandName || interaction.customId)?.split(/\?(.+)/, 2)
108
+
109
+ var type;
110
+ if (interaction.isChatInputCommand())
111
+ type = InteractionType.Command;
112
+ else if (interaction.isButton())
113
+ type = InteractionType.Button;
114
+ else if (interaction.isAnySelectMenu())
115
+ type = InteractionType.SelectMenu;
116
+ else if (interaction.isModalSubmit())
117
+ type = InteractionType.Modal;
118
+ else
119
+ return;
120
+
121
+ return InteractionContainer
122
+ .#call(type, scope, customId, interaction)
123
+ .catch(error => {
124
+ if (error instanceof CommandError)
125
+ return error.error_as_command(interaction)
126
+ else
127
+ return interaction
128
+ .editReply("something went wrong")
129
+ .catch(() => interaction.reply("something went wrong"))
130
+ .then(() => on_error(error))
131
+ .then();
132
+ });
133
+ }
134
+
135
+ /**
136
+ *
137
+ * @param {InteractionType} type
138
+ * @param {InteractionScope} scope
139
+ * @param {InteractionID} customId
140
+ * @param {DiscordBaseInteraction} interaction
141
+ * @returns {Promise<any>}
142
+ * @throws {CommandError}
143
+ */
144
+ static #call(type, scope, customId, interaction) {
145
+ const interaction_command = InteractionContainer.get(type, scope, customId?.[0]);
146
+ if(!interaction_command)
147
+ return consola.error(`No interaction found for id ${customId}`);
148
+
149
+ return interaction_command.interaction_command_function.apply(null, [interaction, ...customId.slice(1)]);
150
+ }
151
+ }
@@ -0,0 +1,45 @@
1
+ import { InteractionScope } from "../Utils/enums";
2
+ import GlobalConfig from "../Configs/GlobalConfig";
3
+ import LocalConfig from "../Configs/LocalConfig";
4
+ import { REST } from "discord.js";
5
+
6
+ export default class RestCommands {
7
+ #commands = new Map();
8
+ #global_commands = [];
9
+ /** @type {GlobalConfig} */
10
+ #global_config;
11
+ /** @type {LocalConfig} */
12
+ #local_config;
13
+
14
+ constructor(global_config, local_config) {
15
+ this.#global_config = global_config;
16
+ this.#local_config = local_config;
17
+ }
18
+
19
+ add(commands, scope) {
20
+ if(!!scope)
21
+ this.#commands.set(scope, commands);
22
+ else
23
+ this.#global_commands = commands;
24
+
25
+ return this;
26
+ }
27
+
28
+ async register(secret_token){
29
+ const rest = new REST(this.#local_config.restOptions).setToken(secret_token);
30
+
31
+ for(const [scope, commands] of this.#commands.entries()) {
32
+ await rest.put(Routes.applicationGuildCommands(this.#local_config.clientId, this.#global_config.guilds.get_guild_id(scope)), {
33
+ body: commands,
34
+ });
35
+ }
36
+
37
+ if(this.#global_commands.length > 0) {
38
+ await rest.put(Routes.applicationCommands(this.#local_config.clientId), {
39
+ body: this.#global_commands,
40
+ });
41
+ }
42
+
43
+ return rest;
44
+ }
45
+ }
@@ -0,0 +1,44 @@
1
+ import { BaseInteraction } from "discord.js";
2
+ import { Modlevel } from "./enums";
3
+
4
+
5
+ /**
6
+ * @param {BaseInteraction} interaction
7
+ * @returns {Boolean}
8
+ */
9
+ export function check_is_staff(interaction) {
10
+ return interaction.member.permissions.has(PermissionsBitField.Flags.ModerateMembers)
11
+ }
12
+
13
+ /**
14
+ * @param {BaseInteraction} interaction
15
+ * @returns {Boolean}
16
+ */
17
+ export function check_can_ban(interaction) {
18
+ return interaction.member.permissions.has(PermissionsBitField.Flags.BanMembers)
19
+ }
20
+
21
+ /**
22
+ * @param {Modlevel} mod_level
23
+ * @param {Modlevel} required_mod_level
24
+ * @returns {Boolean}
25
+ */
26
+ export function check_required_mod_level(mod_level, required_mod_level){
27
+ switch(required_mod_level){
28
+ case null:
29
+ return true;
30
+ case undefined:
31
+ return true;
32
+ case Modlevel.Member:
33
+ if(mod_level == Modlevel.Member)
34
+ return true;
35
+ case Modlevel.Admin:
36
+ if(mod_level == Modlevel.Admin)
37
+ return true;
38
+ case Modlevel.Owner:
39
+ if(mod_level == Modlevel.Owner)
40
+ return true;
41
+ default:
42
+ return false
43
+ }
44
+ }
@@ -0,0 +1,32 @@
1
+ /** @enum { String } */
2
+ export const Modlevel = {
3
+ Owner: "owner",
4
+ Admin: "admin",
5
+ Member: "member"
6
+ };
7
+
8
+ /** @enum { String } */
9
+ export const InteractionScope = {
10
+ Main: "main",
11
+ Community: "comms",
12
+ Tutor: "tutor",
13
+ Staff: "staff",
14
+ OtherGuild: "guild",
15
+ DM: "dm"
16
+ };
17
+
18
+ /** @enum { String } */
19
+ export const InteractionType = {
20
+ Command: "CIP",
21
+ Button: "BTN",
22
+ SelectMenu: "ASM",
23
+ Message: "MSG",
24
+ Modal: "MDL"
25
+ };
26
+
27
+ /** @enum { String } */
28
+ export const Environments = {
29
+ DEV: "development",
30
+ TST: "testing",
31
+ PRD: "production"
32
+ };
package/src/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export {default as LocalConfig} from "./Configs/LocalConfig.js";
2
+ export {default as GlobalConfig} from "./Configs/GlobalConfig.js";
3
+ export {default as SecretConfig} from "./Configs/SecretConfig.js";
4
+
5
+ export {default as InteractionContainer} from "./Interactions/InteractionContainer.js";
6
+ export {default as Interaction} from "./Interactions/Interaction.js";
7
+ export {default as CommandError} from "./Interactions/CommandError.js";
8
+
9
+ export * from "./Utils/checks.js";
10
+ export * from "./Utils/enums.js";