Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement registerUI #18

Merged
merged 54 commits into from
Aug 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
6a56f7f
Add UUID npm package
rob-3 Jul 23, 2021
3349e53
Add UI module and registerUI() method
rob-3 Jul 24, 2021
58959ea
Fix bug with link buttons
rob-3 Jul 24, 2021
28cbe93
Merge branch 'main' into dev/button-rework2
rob-3 Jul 28, 2021
fa5c6fc
Use an object for run() parameters
rob-3 Jul 28, 2021
6c7a8c5
Inject registerUI function directly
rob-3 Jul 28, 2021
447bfc9
Update docs
rob-3 Jul 28, 2021
1734879
Remove FIXME
rob-3 Jul 28, 2021
12c0397
Fix bug with this binding
rob-3 Jul 28, 2021
924e7f7
Remove needless dependency on Client
rob-3 Jul 28, 2021
263b634
Reformat code
rob-3 Jul 28, 2021
904910d
Extract normalization code to function
rob-3 Jul 28, 2021
afcfd0a
Convert to idiomatic OOP style
rob-3 Jul 28, 2021
b31b393
Simplify ButtonHandler arguments
rob-3 Jul 28, 2021
cbcc9f6
Improve docs
rob-3 Jul 28, 2021
74283e6
Add validation for row size constraints
rob-3 Jul 28, 2021
4577a98
Improve typing of ButtonOptions
rob-3 Jul 28, 2021
cda6aae
Use Omit with LinkButtonOptions as well
rob-3 Jul 28, 2021
3c681df
Format code
rob-3 Jul 28, 2021
605cedd
Fix lint error
rob-3 Jul 28, 2021
5702170
bump discord.js
suneettipirneni Jul 31, 2021
2352402
Inject guild into commands
rob-3 Jul 31, 2021
97c61be
Add support for select menus
rob-3 Jul 31, 2021
1ce08dd
Extract validation to functions
rob-3 Jul 31, 2021
65784e5
Reorganize UI module
rob-3 Jul 31, 2021
cec8af0
Merge button types to a single class
rob-3 Jul 31, 2021
069a012
Simplify typings
rob-3 Jul 31, 2021
ca92a11
Refactor to remove use of class
rob-3 Jul 31, 2021
30e3e3c
Move toDiscordComponent to UI.ts
rob-3 Jul 31, 2021
4fded03
Move isSelectMenuOptions
rob-3 Jul 31, 2021
0a15681
Rename DispatchSelectMenu.ts
rob-3 Jul 31, 2021
6473eb2
Rename toComponents to toDiscordUI
rob-3 Jul 31, 2021
68b07c9
Simplify exports
rob-3 Jul 31, 2021
97cd9e9
Move toDiscordComponent higher in file
rob-3 Jul 31, 2021
9b981e3
Merge branch 'main' into registerUI
rob-3 Jul 31, 2021
660f65b
Use label as value in select options by default
rob-3 Aug 1, 2021
f6793c0
Remove GuildMember casts and fix typing problem
rob-3 Aug 1, 2021
7cd39e9
Allow for destructuring of interaction methods
rob-3 Aug 1, 2021
07ec7b9
Extract button and select handler logic to module
rob-3 Aug 1, 2021
40b7240
Remove guild argument
rob-3 Aug 1, 2021
fe9265c
Fix this binding issue for real
rob-3 Aug 1, 2021
b0669f0
Rename UIComponent types
rob-3 Aug 1, 2021
bd19207
Remove old FIXME
rob-3 Aug 1, 2021
bcc18f2
Format code
rob-3 Aug 1, 2021
33eea0a
Clean up registerInteractionListener
rob-3 Aug 1, 2021
e8f7e48
Add docstring to registerInteractionListener
rob-3 Aug 1, 2021
512fe49
Refactor to inject listener maps
rob-3 Aug 1, 2021
7b1c6f1
Create compile time checks for valid UI layout
rob-3 Aug 2, 2021
8fcb3bb
Remove unnecessary runtime checks
rob-3 Aug 2, 2021
b58f18e
Improve hover types in IDE with Simplify<T>
rob-3 Aug 2, 2021
b84174f
Add some tests
rob-3 Aug 2, 2021
a984f73
Merge branch 'main' into registerUI
rob-3 Aug 2, 2021
0e354f8
Inject registerMessageFilters into commands
rob-3 Aug 2, 2021
0595e09
Merge branch 'main' into registerUI
rob-3 Aug 3, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
242 changes: 207 additions & 35 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@
"@types/jest": "^26.0.24",
"@types/lodash": "^4.14.171",
"@types/node": "^16.3.1",
"@types/uuid": "^8.3.1",
"@typescript-eslint/eslint-plugin": "^4.28.2",
"@typescript-eslint/parser": "^4.28.2",
"babel-jest": "^27.0.6",
"discord.js": "^13.0.0-dev.t1627776267.00c2bf8",
"discord.js": "^13.0.0-dev.t1627778678.74fc23b",
"eslint": "^7.30.0",
"jest": "^27.0.6",
"lodash": "^4.17.21",
Expand All @@ -44,7 +45,8 @@
"typescript": "^4.3.5"
},
"dependencies": {
"dotenv": "^10.0.0"
"dotenv": "^10.0.0",
"uuid": "^8.3.2"
},
"files": [
"/dist"
Expand Down
3 changes: 0 additions & 3 deletions src/ButtonHandler.ts

This file was deleted.

73 changes: 20 additions & 53 deletions src/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,27 @@ import discord, {
ApplicationCommandPermissionData,
ClientOptions,
Collection,
CommandInteraction,
Guild,
GuildApplicationCommandPermissionData,
Message,
Snowflake,
} from 'discord.js';
import { dispatch } from './dispatch';
import { loadStructures } from './loaders';
import { isEqual } from 'lodash';
import { Command, isCommand } from './Command';
import { loadStructures } from './loaders';
import { registerInteractionListener } from './registerInteractionListener';
import { toData } from './utils/command';
import { isEqual } from 'lodash';
import { MessageFilter, runMessageFilters } from './messageFilters';

export default class Client extends discord.Client {

private commands = new Collection<string, Command>();
private messageFilters: MessageFilter[] = [];

/**
* Handles commands for the bot.
*/
constructor(options: ClientOptions) {
super(options);
}
}

async syncCommands(commands: Command[]): Promise<void> {

if (!this.isReady()) {
throw new Error('This must be used after the client is ready.');
}
Expand All @@ -40,9 +34,10 @@ export default class Client extends discord.Client {
}

// Fetch the commands from the server.
const rawCommands = process.env.NODE_ENV === 'development' ?
await this.guilds.cache.get(process.env.GUILD_ID)?.commands.fetch() :
await this.application.commands.fetch();
const rawCommands =
process.env.NODE_ENV === 'development'
? await this.guilds.cache.get(process.env.GUILD_ID)?.commands.fetch()
: await this.application.commands.fetch();

if (!rawCommands) {
throw new Error('Could not fetch remote commands!');
Expand All @@ -51,10 +46,10 @@ export default class Client extends discord.Client {
// Normalize all of the commands.

const appCommands = new Collection<string, ApplicationCommandData>();
rawCommands.map(toData).forEach(data => appCommands.set(data.name, data));
rawCommands.map(toData).forEach((data) => appCommands.set(data.name, data));

const clientCommands = new Collection<string, ApplicationCommandData>();
commands.map(toData).forEach(data => clientCommands.set(data.name, data));
commands.map(toData).forEach((data) => clientCommands.set(data.name, data));

// Helper for whenever there's a diff.
const push = async () => {
Expand All @@ -65,17 +60,19 @@ export default class Client extends discord.Client {

// If the length is not the same it's obvious that the commands aren't the same.
if (appCommands.size !== clientCommands.size) {
console.log({ appCommands: appCommands.size, commands: clientCommands.size });
console.log({
appCommands: appCommands.size,
commands: clientCommands.size,
});
await push();
return;
}


// Calculate if theres a diff between the local and remote commands.
const diff = !appCommands.every(appCommand => {
const diff = !appCommands.every((appCommand) => {
// Get the name, and get the corresponding command with the same name.
const clientCommand = clientCommands.get(appCommand.name);

// Check if the commands are equal.
return isEqual(clientCommand, appCommand);
});
Expand All @@ -89,9 +86,7 @@ export default class Client extends discord.Client {
await push();
}

async pushCommands(
appCommands: ApplicationCommandData[],
): Promise<void> {
async pushCommands(appCommands: ApplicationCommandData[]): Promise<void> {
let guild: Guild | undefined = undefined;
if (process.env.GUILD_ID) {
guild = this.guilds.cache.get(process.env.GUILD_ID);
Expand All @@ -100,9 +95,7 @@ export default class Client extends discord.Client {
}

if (!guild) {
throw new Error(
'Guild is not initialized, check your GUILD_ID.'
);
throw new Error('Guild is not initialized, check your GUILD_ID.');
}

let pushedCommands: ApplicationCommand[] | undefined;
Expand Down Expand Up @@ -134,12 +127,6 @@ export default class Client extends discord.Client {
await guild.commands.permissions.set({ fullPermissions });
}

registerMessageFilters(filters: MessageFilter[]): void {
this.messageFilters.push(...filters);
this.on('messageUpdate', async (_, message) => await runMessageFilters(message as Message, this.messageFilters));
this.on('messageCreate', async (message) => await runMessageFilters(message, this.messageFilters));
}

/**
* Registers the commands to be used by this client.
* @param dir The directory to load commands from.
Expand All @@ -149,7 +136,7 @@ export default class Client extends discord.Client {
// Load all of the commands in.
const commands = await loadStructures(dir, isCommand, recursive);

commands.forEach(command => {
commands.forEach((command) => {
this.commands.set(command.name, command);
});

Expand All @@ -162,27 +149,7 @@ export default class Client extends discord.Client {
// If we get here the client is already ready, so we'll register immediately.
await this.syncCommands(commands);
}

// Enable dispatcher.
this.on('interactionCreate', (interaction) => {
if (interaction instanceof CommandInteraction) {
dispatch(interaction, commands);
}

// FIXME figure out a button/select menu api that
/*
if (interaction instanceof ButtonInteraction) {
const handler = client.buttonListeners.get(interaction.customId);

if (!handler) {
return;
}

// Run handler.
handler(interaction);
}
*/
});
registerInteractionListener(this, commands, new Map(), new Map(), []);
}
}

Expand Down
25 changes: 21 additions & 4 deletions src/Command.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import {
ApplicationCommandOptionData,
CommandInteraction,
MessageActionRow,
Snowflake,
} from 'discord.js';
import { MessageFilter } from './messageFilters';
import { UI } from './UI';

export type PermissionHandler = (interaction: CommandInteraction) => boolean | string | Promise<string | boolean>;
export type PermissionHandler = (
interaction: CommandInteraction
) => boolean | string | Promise<string | boolean>;

/**
* Represents a the blueprint for a slash commands.
Expand All @@ -26,10 +31,22 @@ export interface Command {
readonly options?: ApplicationCommandOptionData[];

/**
* The function that gets executed after the command
* is invoked.
* The function that gets executed after the command is invoked.
* @param args
* @param args.interaction Interaction object from discord.js
* @param args.registerUI **Must be called at most once per message!**
* Generates a discord.js compatible UI from Dispatch components.
* @param args.registerMessageFilters Registers a callback that receives all
* messages and deletes a message if the callback returns false
*/
run(interaction: CommandInteraction): Promise<void> | void;
run({
interaction,
registerUI,
}: {
interaction: CommandInteraction;
registerUI: (ui: UI) => MessageActionRow[];
registerMessageFilters: (filters: MessageFilter[]) => void;
}): Promise<void> | void;

/**
* The static role permissions for this command.
Expand Down
35 changes: 35 additions & 0 deletions src/UI/Button.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {
ButtonInteraction,
MessageButtonOptions,
MessageButtonStyle,
} from 'discord.js';
import { Simplify } from '../utils/Simplify';
import { UIComponent } from './UI';

export type ButtonHandler = (
interaction: ButtonInteraction
) => void | Promise<void>;

export type Button = Simplify<
Omit<MessageButtonOptions, 'customId' | 'type'> & {
style: Simplify<Exclude<MessageButtonStyle, 'LINK'>>;
onClick: ButtonHandler;
}
>;

export type LinkButton = Simplify<
Omit<MessageButtonOptions, 'customId' | 'type'> & {
style: 'LINK';
url: string;
}
>;

export function isLinkButton(options: UIComponent): options is LinkButton {
return 'url' in options && 'style' in options && options.style === 'LINK';
}

export function isRegularButton(
options: UIComponent
): options is Button {
return 'onClick' in options && 'style' in options;
}
28 changes: 28 additions & 0 deletions src/UI/SelectMenu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {
MessageSelectMenuOptions,
MessageSelectOptionData,
SelectMenuInteraction,
} from 'discord.js';
import { Simplify } from '../utils/Simplify';
import { UIComponent } from './UI';

export type SelectOption = Simplify<
Omit<MessageSelectOptionData, 'value'> & {
value?: string;
}
>;

export type SelectMenu = Simplify<
Omit<MessageSelectMenuOptions, 'customId' | 'options' | 'type'> & {
onSelect: SelectMenuHandler;
options: SelectOption[];
}
>;

export type SelectMenuHandler = (
interaction: SelectMenuInteraction
) => void | Promise<void>;

export function isSelectMenu(options: UIComponent): options is SelectMenu {
return 'onSelect' in options;
}
Loading