Skip to content

Commit dab7d9f

Browse files
committed
Add interactive modal verifications
Added modal interaction support. Members are auto verified on join, if they previously verified. Interaction failure messages are now ephemeral. Bot now initializes managers one by one, to prevent race conditions. Closes #65
1 parent 7ea7c8e commit dab7d9f

File tree

10 files changed

+245
-45
lines changed

10 files changed

+245
-45
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@
2222
"homepage": "https://github.com/jafrilli/acm-bot#readme",
2323
"dependencies": {
2424
"@discordjs/builders": "^0.12.0",
25-
"@discordjs/rest": "^0.1.0-canary.0",
25+
"@discordjs/rest": "^0.4.1",
2626
"@google-cloud/firestore": "^4.13.0",
2727
"axios": "^0.21.4",
2828
"body-parser": "^1.19.0",
2929
"discord-api-types": "^0.23.1",
30-
"discord.js": "^13.6.0",
30+
"discord.js": "^13.7.0",
3131
"express": "^4.17.1",
3232
"is-url": "^1.2.4",
3333
"leeks.js": "^0.0.8",

src/api/bot.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export interface Config {
2727
slashCommandPath: string;
2828
cmCommandPath: string;
2929
buttonPath: string;
30+
modalPath: string;
3031
eventPath: string;
3132
endpointPath: string;
3233
sentryDNS: string;
@@ -87,17 +88,18 @@ export default class Bot extends Client {
8788
this,
8889
config.slashCommandPath,
8990
config.cmCommandPath,
90-
config.buttonPath
91+
config.buttonPath,
92+
config.modalPath,
9193
),
9294
error: new ErrorManager(this),
95+
database: new DatabaseManager(this, config),
96+
firestore: new FirestoreManager(this),
9397
verification: new VerificationManager(this),
9498
event: new EventManager(this, config.eventPath),
9599
indicator: new IndicatorManager(this),
96-
database: new DatabaseManager(this, config),
97100
scheduler: new ScheduleManager(this),
98101
circle: new CircleManager(this),
99102
rero: new ReactionRoleManager(this),
100-
firestore: new FirestoreManager(this),
101103
resolve: new ResolveManager(this),
102104
express: new ExpressManager(this, config.endpointPath),
103105
points: new PointsManager(this),
@@ -110,9 +112,9 @@ export default class Bot extends Client {
110112
async start(): Promise<void> {
111113
await this.login(this.config.token);
112114
this.logger.info("Initializing managers...");
113-
Object.entries(this.managers).forEach(([k, v]) => {
114-
v.init();
115-
});
115+
for (const manager of Object.values(this.managers)) {
116+
await manager.init();
117+
}
116118
this.logger.info("Bot started.");
117119
}
118120
}

src/api/interaction/modal.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import {
2+
ApplicationCommandPermissionData,
3+
ModalSubmitInteraction,
4+
Interaction,
5+
} from "discord.js";
6+
import { SlashCommandBuilder } from "@discordjs/builders";
7+
import CustomInteraction, {
8+
InteractionConfig,
9+
InteractionContext,
10+
} from "./interaction";
11+
12+
export interface ModalInteractionConfig extends InteractionConfig {}
13+
14+
export interface ModalInteractionContext extends InteractionContext {
15+
interaction: ModalSubmitInteraction;
16+
}
17+
18+
/**
19+
* No need to register modals, but we do need a way to handle the callbacks
20+
*/
21+
export default abstract class CustomModalInteraction extends CustomInteraction {
22+
protected constructor(config: ModalInteractionConfig) {
23+
super(config);
24+
}
25+
26+
/**
27+
* Match to customId, either by direct comparison or regex or something else.
28+
* If it matches, expect handleInteraction to be called afterwards.
29+
* @param customId
30+
*/
31+
public abstract matchCustomId(customId: string): boolean;
32+
33+
/**
34+
* Perform actions for handling the interaction
35+
*/
36+
public abstract handleInteraction(context: ModalInteractionContext): any;
37+
}

src/event/guildmemberadd.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ export default class GuildMemberAddEvent extends Event {
88
}
99

1010
public async emit(bot: Bot, member: GuildMember) {
11+
// If member previously verified, restore their member role
12+
await bot.managers.verification.handleMemberJoin(member);
13+
1114
const embed = new MessageEmbed({
1215
title: `**Welcome to the ACM Discord Server!** 🎉`,
1316
author: {

src/event/messagecreate.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,5 @@ export default class MessageCreateEvent extends Event {
99

1010
public async emit(bot: Bot, msg: Message): Promise<void> {
1111
await bot.managers.command.handle(msg);
12-
await bot.managers.verification.handle(msg);
1312
}
1413
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const bot: Bot = new Bot({
1515
slashCommandPath: path.join(process.cwd(), "dist", "interaction", "command"),
1616
cmCommandPath: path.join(process.cwd(), "dist", "interaction", "contextmenu"),
1717
buttonPath: path.join(process.cwd(), "dist", "interaction", "button"),
18+
modalPath: path.join(process.cwd(), "dist", "interaction", "modal"),
1819
eventPath: path.join(process.cwd(), "dist", "event"),
1920
endpointPath: path.join(process.cwd(), "dist", "endpoint"),
2021
responseFormat: settings.responseFormat,
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import CustomButtonInteraction, {
2+
ButtonInteractionContext
3+
} from "../../api/interaction/button";
4+
5+
export default class VerificationButton extends CustomButtonInteraction {
6+
public constructor() {
7+
super({
8+
name: "verification-button",
9+
});
10+
}
11+
12+
public matchCustomId(customId: string) {
13+
return customId === "verification-button";
14+
}
15+
16+
public async handleInteraction({
17+
bot,
18+
interaction,
19+
}: ButtonInteractionContext): Promise<void> {
20+
await bot.managers.verification.handleVerificationRequest(interaction);
21+
}
22+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import CustomModalInteraction, {
2+
ModalInteractionContext,
3+
} from "../../api/interaction/modal";
4+
5+
export default class VerificationModal extends CustomModalInteraction {
6+
public constructor() {
7+
super({
8+
name: "verification-modal",
9+
});
10+
}
11+
12+
public matchCustomId(customId: string) {
13+
return customId === "verification-modal";
14+
}
15+
16+
public async handleInteraction({
17+
bot,
18+
interaction,
19+
}: ModalInteractionContext): Promise<void> {
20+
// Forward to circle handler
21+
await bot.managers.verification.handleVerificationSubmit(interaction);
22+
}
23+
}

src/util/manager/interaction.ts

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
import {
2-
ButtonInteraction,
3-
Collection,
4-
CommandInteraction,
5-
ContextMenuInteraction,
62
GuildApplicationCommandPermissionData,
73
Interaction,
84
} from "discord.js";
9-
import path from "path";
105
import Bot from "../../api/bot";
116
import Manager from "../../api/manager";
127
import BaseInteraction from "../../api/interaction/interaction";
@@ -15,28 +10,32 @@ import { Routes } from "discord-api-types/v9";
1510
import SlashCommand from "../../api/interaction/slashcommand";
1611
import CustomButtonInteraction from "../../api/interaction/button";
1712
import ContextMenuCommand from "../../api/interaction/contextmenucommand";
18-
import { ApplicationCommandType } from "discord-api-types";
13+
import CustomModalInteraction from "../../api/interaction/modal";
1914

2015
export default class InteractionManager extends Manager {
2116
// private readonly interactionPath = process.cwd() + "/dist/interaction/";
2217
private slashCommandPath: string;
2318
private cmCommandPath: string;
2419
private buttonPath: string;
20+
private modalPath: string;
2521

2622
private slashCommands: Map<string, SlashCommand> = new Map();
2723
private cmCommands: Map<string, ContextMenuCommand> = new Map();
2824
private buttons: Map<string, CustomButtonInteraction> = new Map();
25+
private modals: Map<string, CustomModalInteraction> = new Map();
2926

3027
constructor(
3128
bot: Bot,
3229
slashCommandPath: string,
3330
cmCommandPath: string,
34-
buttonPath: string
31+
buttonPath: string,
32+
modalPath: string,
3533
) {
3634
super(bot);
3735
this.slashCommandPath = slashCommandPath;
3836
this.cmCommandPath = cmCommandPath;
3937
this.buttonPath = buttonPath;
38+
this.modalPath = modalPath;
4039
}
4140

4241
/**
@@ -45,7 +44,7 @@ export default class InteractionManager extends Manager {
4544
public init() {
4645
// this.loadInteractionHandlers();
4746
this.registerSlashAndContextMenuCommands();
48-
this.registerButtons();
47+
this.registerButtonsAndModals();
4948
}
5049

5150
/**
@@ -65,6 +64,10 @@ export default class InteractionManager extends Manager {
6564
handler = [...this.buttons.values()].find((x) =>
6665
x.matchCustomId(interaction.customId)
6766
) as BaseInteraction;
67+
} else if (interaction.isModalSubmit()) {
68+
handler = [...this.modals.values()].find((x) =>
69+
x.matchCustomId(interaction.customId)
70+
) as BaseInteraction;
6871
} else return;
6972

7073
// Return if not found
@@ -74,9 +77,10 @@ export default class InteractionManager extends Manager {
7477
try {
7578
await handler.handleInteraction({ bot: this.bot, interaction });
7679
} catch (e: any) {
77-
await interaction.reply(
78-
"Command execution failed. Please contact a bot maintainer..."
79-
);
80+
await interaction.reply({
81+
content: "Command execution failed. Please contact a bot maintainer...",
82+
ephemeral: true
83+
});
8084
// Don't throw and let the bot handle this as an unhandled rejection. Instead,
8185
// take initiative to handle it as an error so we can see the trace.
8286
await this.bot.managers.error.handleErr(e);
@@ -161,16 +165,22 @@ export default class InteractionManager extends Manager {
161165
}
162166
}
163167

164-
private async registerButtons() {
168+
private async registerButtonsAndModals() {
165169
try {
166170
// Dynamically load source files
167171
this.buttons = new Map(
168172
DynamicLoader.loadClasses(this.buttonPath).map((sc) => [sc.name, sc])
169173
);
170-
171174
for (const btn of this.buttons.keys()) {
172175
this.bot.logger.info(`Loaded button '${btn}'`);
173176
}
177+
178+
this.modals = new Map(
179+
DynamicLoader.loadClasses(this.modalPath).map((sc) => [sc.name, sc])
180+
);
181+
for (const mdl of this.modals.keys()) {
182+
this.bot.logger.info(`Loaded modal '${mdl}'`);
183+
}
174184
} catch (error: any) {
175185
await this.bot.managers.error.handleErr(error);
176186
}

0 commit comments

Comments
 (0)