Skip to content
This repository has been archived by the owner on Aug 21, 2024. It is now read-only.

Commit

Permalink
Updated interactions subsystem.
Browse files Browse the repository at this point in the history
  • Loading branch information
Hatry1337 committed Mar 27, 2022
1 parent 15fedd3 commit 1260d2d
Show file tree
Hide file tree
Showing 17 changed files with 328 additions and 313 deletions.
20 changes: 8 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ You can find documentation here: https://rainbowbot.xyz/docs and more practical

## Installation

```console
```zsh
npm install rainbowbot-core
```

Expand Down Expand Up @@ -58,7 +58,7 @@ This is the example of RainbowBOT Core Module:
```ts
import Discord from "discord.js";
import { SlashCommandBuilder } from "@discordjs/builders";
import { Colors, Module, ModuleManager, Utils } from "rainbowbot-core";
import { Colors, Module, RainbowBOT, Utils } from "rainbowbot-core";

export default class MyModule extends Module{
public Name: string = "MyModule";
Expand All @@ -67,22 +67,18 @@ export default class MyModule extends Module{
public Category: string = "Info";
public Author: string = "Thomasss#9258";

constructor(Controller: ModuleManager, UUID: string) {
super(Controller, UUID);
constructor(bot: RainbowBOT, UUID: string) {
super(bot, UUID);
this.SlashCommands.push(
new SlashCommandBuilder()
.setName("helloworld")
this.bot.interactions.createCommand("helloworld", this.bot.moduleGlobalLoading ? undefined : this.bot.masterGuildId)
.setDescription(this.Description)
) as SlashCommandBuilder
.onExecute(this.Run.bind(this))
.commit()
);
}

public Test(interaction: Discord.CommandInteraction){
return interaction.commandName.toLowerCase() === "helloworld";
}

public Run(interaction: Discord.CommandInteraction){
return new Promise<Discord.Message | void>(async (resolve, reject) => {
return new Promise<void>(async (resolve, reject) => {
return resolve(await interaction.reply("Hello and Welcome to RainbowBOT Core!").catch(reject));
});
}
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "rainbowbot-core",
"version": "3.5.2",
"version": "3.6.0",
"description": "RainbowBOT Core",
"license": "MIT",
"author": {
Expand All @@ -24,7 +24,7 @@
"build": "tsc",
"docs": "npx typedoc",
"prepublish": "npm run build",
"test": "npx jest"
"test": "npx jest --setupFiles dotenv/config"
},
"dependencies": {
"@discordjs/builders": "^0.11.0",
Expand Down
104 changes: 49 additions & 55 deletions src/EventManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ declare interface EventManager {
on(event: 'GuildMemberAdd', listener: (guild: Guild, user: User, member: Discord.GuildMember) => void): this;
on(event: 'GuildMemberAdd', listener: (guild: Guild, user: User, member: Discord.GuildMember | Discord.PartialGuildMember) => void): this;
once(event: 'Initialized', listener: () => void): this;
once(event: 'Exit', listener: () => void): this;
once(event: 'Stop', listener: () => void): this;
on(event: 'Error', listener: (error: any, fatal: boolean) => void): this;
}

class EventManager extends EventEmitter{
Expand All @@ -31,8 +32,8 @@ class EventManager extends EventEmitter{
this.bot.client.on( "voiceStateUpdate", this.onVoiceStateUpdate.bind(this));
this.bot.client.on( "guildMemberAdd", this.onGuildMemberAdd.bind(this));
this.bot.client.on( "guildMemberRemove", this.onGuildMemberRemove.bind(this));
this.bot.client.on( "interactionCreate", this.onInteractionCreate.bind(this));

this.on( "Error", this.onError.bind(this));
process.on( "SIGINT", this.onExit.bind(this));
process.on( "SIGTERM", this.onExit.bind(this));
}
Expand All @@ -45,67 +46,60 @@ class EventManager extends EventEmitter{
this.buttonSubscriptions.delete(cus_id);
}

private async onInteractionCreate(interaction: Discord.Interaction){
if(interaction.isCommand()){
await this.bot.modules.FindAndRun(interaction).catch(err => GlobalLogger.root.error("Error Running Module:", err));
return;
}
if(interaction.isButton()){
for(let e of this.buttonSubscriptions.entries()){
if(interaction.customId === e[0]){
await e[1](interaction).catch(err => GlobalLogger.root.error("Error Executing Button Callback:", err));
}
}
return;
private async onError(err: any, fatal: boolean){
if(fatal){
logger.fatal("Fatal Error Occured:", err);
await this.bot.stop();
process.exit(-1);
}else{
logger.info("Error Occured:", err);
}
}

private async onceReady(){
logger.info(`Loggined In! (${this.bot.client.user?.tag})`);

await sequelize().sync({force: false});
logger.info(`Fetching system user..`);
await this.bot.users.updateAssociations();

let sys = await this.bot.users.fetchOne(this.bot.users.idFromDiscordId(this.bot.client.user!.id) || -1, true);
if(!sys){
logger.info(`No system user. Creating new one..`);
sys = await this.bot.users.createFromDiscord(this.bot.client.user!, "Admin");
logger.info(`Created system user. ID: ${sys.id}`);
}
logger.info(`Database Synchronized.`);

logger.info("Running guilds caching...");
let cachec = await this.bot.CacheGuilds(true);
logger.info(`Cached ${cachec} guilds.`);

logger.info(`Running Modules Initialization...`);
await this.bot.modules.data.loadFromStorage();
await this.bot.config.get("amogus", "sus"); // This is for underlying data container fetching (container is sus lmfao)

let inic = await this.bot.modules.Init().catch(err => GlobalLogger.root.error("[Ready Event] Error intializing modules:", err));
if(!inic && inic !== 0){
logger.fatal("Fatal error occured. Can't load modules.");
}else{
logger.info(`Initialized ${inic} Module Initializers.`);
logger.info(`BOT Fully ready! Enjoy =)`);
this.bot.isReady = true;
this.emit("Initialized");
try {
logger.info(`Loggined In! (${this.bot.client.user?.tag})`);

await sequelize().sync({force: false});
logger.info(`Fetching system user..`);
await this.bot.users.updateAssociations();

let sys = await this.bot.users.fetchOne(this.bot.users.idFromDiscordId(this.bot.client.user!.id) || -1, true);
if(!sys){
logger.info(`No system user. Creating new one..`);
sys = await this.bot.users.createFromDiscord(this.bot.client.user!, "Admin");
logger.info(`Created system user. ID: ${sys.id}`);
}
logger.info(`Database Synchronized.`);

logger.info("Running guilds caching...");
let cachec = await this.bot.CacheGuilds(true);
logger.info(`Cached ${cachec} guilds.`);

logger.info(`Running Modules Initialization...`);
await this.bot.modules.data.loadFromStorage();
await this.bot.config.get("amogus", "sus"); // This is for underlying data container fetching (container is sus lmfao)

let inic = await this.bot.modules.Init().catch(err => GlobalLogger.root.error("[Ready Event] Error intializing modules:", err));
if(!inic && inic !== 0){
logger.fatal("Fatal error occured. Can't load modules.");
}else{
logger.info(`Initialized ${inic} Module Initializers.`);

logger.info(`Uploading slash commands...`);
await this.bot.interactions.updateSlashCommands();

logger.info(`BOT Fully ready! Enjoy =)`);
this.bot.isReady = true;
this.emit("Initialized");
}
} catch (error) {
this.emit("Error", error, true);
}
}

private async onExit(){
this.emit("Exit");
logger.info(`Accepted exit signal. Running graceful exit task.`);
await this.bot.modules.UnloadAllModules();
logger.info(`Commands unloaded. Stopping Database Updates.`);
this.bot.users.stopUpdating();
await this.bot.users.syncStorage();
logger.info(`Database Updates stopped. Destroying client.`);
this.bot.client.destroy();
logger.info(`Clinet destroyed. Disconnecting Database.`);
await sequelize().close();
logger.info(`Graceful exit task successfully finished.`);
await this.bot.stop();
process.exit(0);
}

Expand Down
6 changes: 2 additions & 4 deletions src/GuildManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@ export default class GuildManager{
this.timer = setInterval(async () => {
await this.syncStorage();
}, 5 * 60 * 1000);
}

public stopUpdating(){
clearInterval(this.timer);
this.bot.events.once("Stop", () => { clearInterval(this.timer); });
}

public createFromDiscord(dGuild: Discord.Guild, group: string = "default"){
Expand Down Expand Up @@ -152,6 +149,7 @@ export default class GuildManager{
await this.syncCacheEntry(user, g, t);
}
await t.commit();
return resolve();
}).catch(reject);
});
}
Expand Down
167 changes: 167 additions & 0 deletions src/InteractionsManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import Discord from "discord.js";
import { SlashCommandBuilder } from "@discordjs/builders";
import { GlobalLogger } from "./GlobalLogger";
import RainbowBOT from "./RainbowBOT";
import crypto from "crypto";
import { Routes } from "discord-api-types/rest/v9";
import { User } from ".";

export type ButtonInteractionCallback = (interaction: Discord.ButtonInteraction) => Promise<void>;
export type CommandInteractionCallback = (interaction: Discord.CommandInteraction, user: User) => Promise<void>;

export class InteractiveButton extends Discord.MessageButton {
private clickCallback?: ButtonInteractionCallback;
public lastInteraction?: Discord.ButtonInteraction;

constructor(readonly uuid: string){
super();
this.setCustomId(uuid);
}

public onClick(callback: ButtonInteractionCallback){
this.clickCallback = callback;
return this;
}

public async _clicked(interaction: Discord.ButtonInteraction){
this.lastInteraction = interaction;
if(this.clickCallback){
await this.clickCallback(interaction);
}
}
}

export class InteractiveCommand extends SlashCommandBuilder{
public isUpdated: boolean = true;
public isPushed: boolean = false;
private execCallback?: CommandInteractionCallback;
public lastInteraction?: Discord.CommandInteraction;

constructor(name: string, readonly forGuildId?: string){
super();
this.setName(name);
}

public onExecute(callback: CommandInteractionCallback){
this.execCallback = callback;
return this;
}

public async _exec(interaction: Discord.CommandInteraction, user: User){
this.lastInteraction = interaction;
if(this.execCallback){
await this.execCallback(interaction, user);
}
}

public commit(){
this.isUpdated = false;
return this;
}
}

export default class InteractionsManager{
private interactiveButtonsRegistry: Map<string, InteractiveButton> = new Map;
private interactiveCommandsRegistry: Map<string, InteractiveCommand> = new Map;
private updateTimer: NodeJS.Timeout;

constructor(public bot: RainbowBOT) {
this.updateTimer = setInterval(this.updateSlashCommands.bind(this), 20000);
this.bot.client.on("interactionCreate", this.onInteractionCreate.bind(this));
this.bot.events.once("Stop", () => { clearInterval(this.updateTimer); });
}

public createCommand(name: string, forGuildId?: string){
if(this.interactiveCommandsRegistry.has(name)){
throw new Error("This command already exists.");
}
let cmd = new InteractiveCommand(name, forGuildId);
this.interactiveCommandsRegistry.set(name, cmd);
return cmd;
}

public getCommand(name: string){
return this.interactiveCommandsRegistry.get(name);
}

public createButton(){
let button = new InteractiveButton(crypto.randomUUID() + "-rbc-ibtn");
this.interactiveButtonsRegistry.set(button.uuid, button);
return button;
}

public getButton(uuid: string){
return this.interactiveButtonsRegistry.get(uuid);
}

public async updateSlashCommands(){
let cmds = Array.from(this.interactiveCommandsRegistry.values()).filter(c => !c.isUpdated);
if(cmds.length === 0) return;

for(let c of cmds){
if(c.forGuildId){
if(c.isPushed){
await this.bot.rest.patch(
Routes.applicationGuildCommands(this.bot.client.application!.id, c.forGuildId),
{ body: c.toJSON() },
).catch(err => GlobalLogger.root.error("Error Updating Guild Slash Command:", err));
c.isUpdated = true;
return;
}else{
await this.bot.rest.post(
Routes.applicationGuildCommands(this.bot.client.application!.id, c.forGuildId),
{ body: c.toJSON() },
).catch(err => GlobalLogger.root.error("Error Pushing Guild Slash Command:", err));
c.isUpdated = true;
c.isPushed = true;
return;
}
}else{
if(c.isPushed){
await this.bot.rest.patch(
Routes.applicationCommands(this.bot.client.application!.id),
{ body: c.toJSON() },
).catch(err => GlobalLogger.root.error("Error Updating Global Slash Command:", err));
c.isUpdated = true;
return;
}else{
await this.bot.rest.post(
Routes.applicationCommands(this.bot.client.application!.id),
{ body: c.toJSON() },
).catch(err => GlobalLogger.root.error("Error Pushing Global Slash Command:", err));
c.isUpdated = true;
c.isPushed = true;
return;
}
}
}
}

private async onInteractionCreate(interaction: Discord.Interaction){
if(interaction.isCommand()){
let user_id = this.bot.users.idFromDiscordId(interaction.user.id);
let user: User | null = null;
if(user_id){
user = await this.bot.users.fetchOne(user_id);
}
if(!user){
user = await this.bot.users.createFromDiscord(interaction.user);
}
let cmd = Array.from(this.interactiveCommandsRegistry.values()).find(c => c.name === interaction.commandName);

if(!cmd){
GlobalLogger.root.warn(`Fired "${interaction.commandName}" command but InteractiveCommand not found.`);
return;
}
return await cmd._exec(interaction, user!).catch(err => GlobalLogger.root.error("Command Callback Error:", err));;
}
if(interaction.isButton()){
for(let btn of this.interactiveButtonsRegistry.entries()){
if(interaction.customId === btn[0]){
return await btn[1]._clicked(interaction).catch(err => GlobalLogger.root.error("Button Callback Error:", err));
}
}
return;
}
}
}
3 changes: 2 additions & 1 deletion src/ModuleDataManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ export default class ModuleDataManager{
private timer: NodeJS.Timeout;
constructor(public bot: RainbowBOT){
this.timer = setInterval(async () => {
await this.syncStorage().catch(err => GlobalLogger.root.error("PrivateDataContainerManager AutoSync Error:", err));
await this.syncStorage().catch(err => GlobalLogger.root.error("ModuleDataManager AutoSync Error:", err));
}, 5 * 60 * 1000);
this.bot.events.once("Stop", () => { clearInterval(this.timer); });
}

public getContainer(uuid: string) {
Expand Down
Loading

0 comments on commit 1260d2d

Please sign in to comment.