Skip to content

Commit

Permalink
feat: automatic setup service
Browse files Browse the repository at this point in the history
Signed-off-by: Ar Rakin <rakinar2@onesoftnet.eu.org>
  • Loading branch information
virtual-designer committed Sep 20, 2024
1 parent 78aa832 commit 71b87b1
Show file tree
Hide file tree
Showing 11 changed files with 687 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"estree",
"extbuilds",
"fargs",
"finishable",
"httpcat",
"httpdog",
"kickable",
Expand Down Expand Up @@ -98,6 +99,7 @@
"Unbans",
"undici",
"Unmutes",
"uuidv",
"xnor"
],
"material-icon-theme.folders.associations": {
Expand Down
2 changes: 2 additions & 0 deletions src/framework/typescript/commands/Context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export type ContextOf<T extends Command<ContextType>> =
: never;
export type AnyContext = ContextOf<AnyCommand>;

export type ContextReplyOptions = Parameters<Context["reply"]>[0];

abstract class Context<T extends CommandMessage = CommandMessage> {
public readonly commandName: string;
public readonly commandMessage: T;
Expand Down
128 changes: 128 additions & 0 deletions src/framework/typescript/widgets/Wizard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import type Context from "@framework/commands/Context";
import type { ContextReplyOptions } from "@framework/commands/Context";
import WizardButtonBuilder from "@framework/widgets/WizardButtonBuilder";
import type WizardManager from "@framework/widgets/WizardManager";
import type { ButtonInteraction } from "discord.js";
import {
ActionRowBuilder,
ButtonStyle,
type AnyComponentBuilder,
type Awaitable,
type Message,
type MessageEditOptions
} from "discord.js";
import { v4 as uuidv4 } from "uuid";

abstract class Wizard {
private readonly manager: WizardManager;
private readonly context: Context;
private message: Message | null = null;
private handlers: Record<string, string> = {};
private readonly id = uuidv4();
protected readonly inactivityTimeout: number = 300000; /* 5 minutes */
private timeout: Timer | null = null;
protected readonly states: ContextReplyOptions[] = [];

public constructor(manager: WizardManager, context: Context) {
this.context = context;
this.manager = manager;
}

protected button(customId: string): WizardButtonBuilder {
return new WizardButtonBuilder()
.setCustomId(`w::${this.id}::${customId}`)
.setStyle(ButtonStyle.Secondary);
}

protected row<T extends AnyComponentBuilder>(components: T[]) {
return new ActionRowBuilder<T>().addComponents(...components);
}

protected abstract render(): Awaitable<ContextReplyOptions>;

public async update(): Promise<void> {
const options = this.states.at(-1) ?? (await this.render());

this.handlers = {};

if (typeof options === "object" && "components" in options) {
for (const component of options.components ?? []) {
if (component instanceof ActionRowBuilder) {
for (const button of component.components) {
if (button instanceof WizardButtonBuilder) {
this.handlers[button.customId] = button.handler;
}
}
}
}
}

if (!this.message) {
this.message = await this.context.reply(options);
this.manager.register(this.id, this);
this.timeout = setTimeout(() => this.dispose(), this.inactivityTimeout);
} else {
await this.message.edit(options as MessageEditOptions);
}
}

protected pushState(options: ContextReplyOptions) {
this.states.push(options);
}

protected popState() {
return this.states.pop();
}

protected async revertState(interaction?: ButtonInteraction) {
const state = this.popState();

if (interaction) {
await interaction.deferUpdate();
}

if (state) {
await this.update();
}

return state;
}

public async dispatch(interaction: ButtonInteraction, customId: string) {
const handler = this.handlers[customId];

if (this.timeout) {
clearTimeout(this.timeout);
}

this.timeout = setTimeout(() => this.dispose(), this.inactivityTimeout);

if (handler in this && typeof this[handler as keyof this] === "function") {
const result = await (
this as unknown as Record<
string,
(
interaction: ButtonInteraction,
customId: string
) => Awaitable<ContextReplyOptions>
>
)[handler].call(this, interaction, customId);

if (result) {
this.pushState(result);

if (!interaction.deferred) {
await interaction.deferUpdate();
}

await interaction.message.edit(result as MessageEditOptions);
}
}
}

public dispose() {
this.manager.dispose(this.id);
}
}

export default Wizard;
26 changes: 26 additions & 0 deletions src/framework/typescript/widgets/WizardButtonBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ButtonBuilder } from "discord.js";

class WizardButtonBuilder extends ButtonBuilder {
private _customId?: string;
private _handler?: string;

public override setCustomId(customId: string): this {
this._customId = customId;
return super.setCustomId(customId);
}

public get customId(): string {
return this._customId!;
}

public setHandler(handler: string): this {
this._handler = handler;
return this;
}

public get handler(): string {
return this._handler!;
}
}

export default WizardButtonBuilder;
20 changes: 20 additions & 0 deletions src/framework/typescript/widgets/WizardManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { HasApplication } from "@framework/types/HasApplication";
import type Wizard from "@framework/widgets/Wizard";

class WizardManager extends HasApplication {
private readonly wizards: Map<string, Wizard> = new Map();

public register(name: string, wizard: Wizard): void {
this.wizards.set(name, wizard);
}

public get(name: string): Wizard | undefined {
return this.wizards.get(name);
}

public dispose(name: string): void {
this.wizards.delete(name);
}
}

export default WizardManager;
2 changes: 2 additions & 0 deletions src/main/typescript/core/DiscordKernel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ class DiscordKernel extends Kernel {
"@services/SurveyService",
"@services/ShellService",
"@services/AuthService",
"@services/WizardManagerService",
"@services/GuildSetupService",
"@services/SnippetManagerService",
"@services/TranslationService",
"@services/SystemUpdateService",
Expand Down
53 changes: 53 additions & 0 deletions src/main/typescript/events/guild/GuildCreateEventListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* This file is part of SudoBot.
*
* Copyright (C) 2021, 2022, 2023, 2024 OSN Developers.
*
* SudoBot is free software; you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* SudoBot 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with SudoBot. If not, see <https://www.gnu.org/licenses/>.
*/

import { Inject } from "@framework/container/Inject";
import EventListener from "@framework/events/EventListener";
import { Logger } from "@framework/log/Logger";
import { Events } from "@framework/types/ClientEvents";
import type { Guild } from "discord.js";
import type Client from "../../core/Client";
import ConfigurationManager from "../../services/ConfigurationManager";

class GuildCreateEventListener extends EventListener<Events.GuildCreate, Client> {
public override readonly name = Events.GuildCreate;

@Inject()
public readonly configManager!: ConfigurationManager;

@Inject()
public readonly logger!: Logger;

public override async execute(guild: Guild) {
this.logger.info(`Joined a guild: ${guild.name} (${guild.id})`);

if (!this.configManager.config[guild.id]) {
this.logger.info(`Auto-configuring guild: ${guild.id}`);
this.configManager.autoConfigure(guild.id);

await this.configManager.write({
system: false,
guild: true
});
await this.configManager.load();
}
}
}

export default GuildCreateEventListener;
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { Inject } from "@framework/container/Inject";
import EventListener from "@framework/events/EventListener";
import { Events } from "@framework/types/ClientEvents";
import type VerificationService from "@main/automod/VerificationService";
import type WizardManagerService from "@main/services/WizardManagerService";
import { Interaction } from "discord.js";
import type CommandManager from "../../services/CommandManager";

Expand All @@ -33,13 +34,17 @@ class InteractionCreateEventListener extends EventListener<Events.InteractionCre
@Inject("verificationService")
private readonly verificationService!: VerificationService;

@Inject("wizardManagerService")
private readonly wizardManagerService!: WizardManagerService;

public override async execute(interaction: Interaction): Promise<void> {
if (interaction.isCommand()) {
await this.commandManager.runCommandFromInteraction(interaction);
}

if (interaction.isButton()) {
await this.verificationService.onInteractionCreate(interaction);
this.wizardManagerService.onInteractionCreate(interaction);
}
}
}
Expand Down
Loading

0 comments on commit 71b87b1

Please sign in to comment.