Skip to content
This repository was archived by the owner on Jan 13, 2025. It is now read-only.

Commit

Permalink
refactor: log differences instead of key (#5)
Browse files Browse the repository at this point in the history
* refactor(CommandRegistryHandler): return differences array instead

* refactor: return differences array instead of string

* fix: bug fixes
  • Loading branch information
ijsKoud authored Dec 6, 2022
1 parent 17e15fd commit e23f834
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 67 deletions.
193 changes: 126 additions & 67 deletions src/lib/handlers/CommandRegistryHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import {
} from "discord.js";
import { bold } from "colorette";
import _ from "lodash";
import { Command, IgloClient, InteractionHandlerError } from "@snowcrystals/iglo";
import type { DifferenceData, Differences } from "../types.js";
import type { IgloClient } from "../Client.js";
import { InteractionHandlerError, Command } from "../index.js";

export class CommandRegistry {
public constructor(public client: IgloClient) {}
Expand All @@ -41,9 +43,10 @@ export class CommandRegistry {
try {
if (different.discord) {
this.client.logger.debug(
`(CommandRegistry): Updating a new command with name: ${bold(different.command.command.name)} because ${bold(
different.command.result
)} was different.`
`(CommandRegistry): Updating a new command with name: ${bold(
different.command.command.name
)} the following as different:`,
...different.command.differences
);
await this.updateCommand(different.discord, different.command.command);
} else {
Expand Down Expand Up @@ -88,29 +91,29 @@ export class CommandRegistry {
{
command: {
command: Command;
result: string;
differences: Differences;
};
discord?: ApplicationCommand;
},
void,
unknown
> {
const [registered, unregistered] = commands.partition((cmd) => Boolean(discord.find((dCmd) => dCmd.name === cmd.name)));
const different = registered
.map((cmd) => {
const result = this.isDifferent(discord.find((dCmd) => dCmd.name === cmd.name)!, cmd);
return {
result,
command: cmd
};
})
.filter((res) => Boolean(res.result));
const different = registered.map((cmd) => {
const discordCmd = discord.find((dCmd) => dCmd.name === cmd.name)!;
const differences = this.isDifferent(discordCmd, cmd);

return {
differences,
command: cmd
};
});

// first return the commands which aren't registered yet
for (const command of [...unregistered.values()]) {
const obj = {
command: {
result: "",
differences: [],
command
}
};
Expand All @@ -120,11 +123,13 @@ export class CommandRegistry {

// after that return the commands which are registered
for (const command of [...different.values()]) {
if (!command.differences.length) continue;

const obj = {
discord: discord.find((dCmd) => dCmd.name === command.command.name)!,
command: command as {
command: Command;
result: string;
differences: Differences;
}
};

Expand All @@ -137,84 +142,123 @@ export class CommandRegistry {
* @param discord the Discord ApplicationCommand
* @param command the bot Command
*/
private isDifferent(discord: ApplicationCommand, command: Command): string | null {
if (!_.isEqual(discord.nameLocalizations, command.nameLocalizations ?? null)) return "nameLocalizations";
if (!_.isEqual(discord.descriptionLocalizations ?? {}, command.descriptions)) return "descriptionLocalizations";
if (discord.description !== command.description) return "description";

if (discord.dmPermission !== command.permissions.dm) return "dmPermission";
if (!_.isEqual(discord.defaultMemberPermissions, command.permissions.default ? new PermissionsBitField(command.permissions.default) : null))
return "defaultMemberPermissions";
private isDifferent(discord: ApplicationCommand, command: Command): Differences {
const differences: Differences = [];

// Check NameLocalizations
differences.push(...this.checkLocalization(discord.nameLocalizations ?? {}, command.nameLocalizations ?? {}, "nameLocalizations"));
// Check NameLocalizations
differences.push(...this.checkLocalization(discord.descriptionLocalizations ?? {}, command.descriptions ?? {}, "descriptionLocalizations"));
// Check description
if (discord.description !== command.description)
differences.push({ key: "description", expected: command.description, received: discord.description });

// check DM Permissions
if (discord.dmPermission !== command.permissions.dm)
differences.push({ key: "dmPermission", expected: command.permissions.dm, received: discord.dmPermission });
// Check member Permissions
const discordPermissions = discord.defaultMemberPermissions;
const commandPermissions = command.permissions.default ? new PermissionsBitField(command.permissions.default) : null;
if (discordPermissions && !commandPermissions)
differences.push({ key: "defaultMemberPermissions", expected: null, received: discordPermissions.bitfield });
else if (!discordPermissions && commandPermissions)
differences.push({ key: "defaultMemberPermissions", expected: commandPermissions.bitfield, received: null });
else if (discordPermissions && commandPermissions && !_.isEqual(discordPermissions, commandPermissions))
differences.push({ key: "defaultMemberPermissions", expected: commandPermissions.bitfield, received: discordPermissions.bitfield });

const optionsRes = this.optionsAreDifferent(discord.options, command.options);
if (optionsRes) return `options(${optionsRes})`;
differences.push(...optionsRes);

return null;
return differences;
}

private optionsAreDifferent(discord: ApplicationCommandOption[], command: ApplicationCommandOption[]): string | null {
// Expected options but received no options
if (!discord.length && command.length) return "amount";
// Expected no options but received options
if (discord.length && !command.length) return "amount";
private optionsAreDifferent(discord: ApplicationCommandOption[], command: ApplicationCommandOption[]): Differences {
const differences: Differences = [];

// Check Options length
if (command.length !== discord.length) differences.push({ key: "options", expected: command.length, received: discord.length });

// Check to see if commands here include options which aren't at Discord
if (command.some((opt) => !discord.find((o) => o.name === opt.name))) return "data-notEqual";
const missing = command.filter((opt) => !discord.find((o) => o.name === opt.name));
differences.push(...missing.map<DifferenceData>((opt) => ({ key: `option[${opt.name}]`, expected: "object", received: "undefined" })));
// Check to see if commands at Discord include options which aren't here
if (discord.some((opt) => !command.find((o) => o.name === opt.name))) return "data-notEqual";
const existing = discord.filter((opt) => !command.find((o) => o.name === opt.name));
differences.push(...existing.map<DifferenceData>((opt) => ({ key: `option[${opt.name}]`, expected: "undefined", received: "object" })));

// Map over all options to see if they are the same
let index = 0;
for (const option of command) {
const existingOption = discord.find((opt) => opt.name === option.name);
const optionsRes = this.isOptionDifferent(existingOption, option);
if (optionsRes) return optionsRes;
if (!existingOption) continue; // Ignore missing options, already addressed above.

const optionsRes = this.isOptionDifferent(existingOption, option, index);
if (optionsRes) differences.push(...optionsRes);

index++;
}

return null;
return differences;
}

private isOptionDifferent(discord: ApplicationCommandOption | undefined, command: ApplicationCommandOption): string | null {
// Expected DiscordOption but received undefined
if (!discord) return "undefined";

// check the name localizations
if (!_.isEqual(discord.nameLocalizations, command.nameLocalizations ?? null)) return "nameLocalizations";
// check the description localizations
if (!_.isEqual(discord.descriptionLocalizations, command.descriptionLocalizations ?? null)) return "descriptionLocalizations";
// check the description
if (discord.description !== command.description) return "description";
private isOptionDifferent(discord: ApplicationCommandOption, command: ApplicationCommandOption, index: number): Differences {
const differences: Differences = [];
const optionIndex = `option[${index}]`;

// Check NameLocalizations
differences.push(
...this.checkLocalization(discord.nameLocalizations ?? {}, command.nameLocalizations ?? {}, `${optionIndex}.nameLocalizations`)
);
// Check NameLocalizations
differences.push(
...this.checkLocalization(
discord.descriptionLocalizations ?? {},
command.descriptionLocalizations ?? {},
`${optionIndex}.descriptionLocalizations`
)
);
// Check description
if (discord.description !== command.description)
differences.push({ key: `${optionIndex}.description`, expected: command.description, received: discord.description });

// Check the type
if (discord.type !== command.type) return "type";
if (discord.type !== command.type)
differences.push({
key: `${optionIndex}.type`,
expected: ApplicationCommandOptionType[command.type],
received: ApplicationCommandOptionType[discord.type]
});
// check if autocomplete is different
if ((discord.autocomplete ?? false) !== (command.autocomplete ?? false)) return "autocomplete";
if ((discord.autocomplete ?? false) !== (command.autocomplete ?? false))
differences.push({ key: `${optionIndex}.autocomplete`, expected: command.autocomplete, received: discord.autocomplete });

// check if required
if ("required" in discord) {
const cmd = command as typeof discord;
if ((discord.required ?? false) !== (cmd.required ?? false)) return "required";
if ((discord.required ?? false) !== (cmd.required ?? false))
differences.push({ key: `${optionIndex}.required`, expected: cmd.required, received: discord.required });
}

switch (discord.type) {
case ApplicationCommandOptionType.SubcommandGroup:
case ApplicationCommandOptionType.Subcommand: {
const disc = discord as ApplicationCommandSubGroup;
const cmd = command as ApplicationCommandSubGroup;
return this.optionsAreDifferent(disc.options ?? [], cmd.options ?? []);
}
case ApplicationCommandOptionType.Subcommand:
{
const disc = discord as ApplicationCommandSubGroup;
const cmd = command as ApplicationCommandSubGroup;
const optRes = this.optionsAreDifferent(disc.options ?? [], cmd.options ?? []);
differences.push(...optRes);
}
break;
case ApplicationCommandOptionType.String:
{
const cmd = command as ApplicationCommandStringOption | ApplicationCommandAutocompleteStringOption;
const disc = discord as ApplicationCommandStringOption | ApplicationCommandAutocompleteStringOption;
if (!_.isEqual(disc.maxLength, cmd.maxLength)) return "maxLength";
if (!_.isEqual(disc.minLength, cmd.minLength)) return "minLength";
if (!_.isEqual(disc.maxLength, cmd.maxLength))
differences.push({ key: `${optionIndex}.maxLength`, expected: cmd.maxLength, received: disc.maxLength });
if (!_.isEqual(disc.minLength, cmd.minLength))
differences.push({ key: `${optionIndex}.minLength`, expected: cmd.minLength, received: disc.minLength });

// Expected choices but got undefined (and reversed)
// @ts-ignore Discord data returns undefined if no choices are passed, we should do the same with local data
cmd.choices ??= undefined;
if ("choices" in disc && !("choices" in cmd)) return "choices";
if (!("choices" in disc) && "choices" in cmd) return "choices";

if ("choices" in disc && "choices" in cmd) {
const discChoices = disc.choices ?? [];
const cmdChoices = cmd.choices ?? [];
Expand All @@ -231,8 +275,10 @@ export class CommandRegistry {
};

// one of the items has more than expected
if (discChoices.length !== cmdChoices.length) return "choices-amount";
if (cmdChoices.some(someChoice)) return "choices-notEqual";
if (discChoices.length !== cmdChoices.length)
differences.push({ key: `${optionIndex}.choices.length`, expected: cmdChoices.length, received: discChoices.length });
if (cmdChoices.some(someChoice))
differences.push({ key: `${optionIndex}.choices`, expected: cmdChoices, received: discChoices });
}
}
break;
Expand All @@ -241,24 +287,37 @@ export class CommandRegistry {
const disc = discord as ApplicationCommandChannelOption;
const cmd = command as ApplicationCommandChannelOption;

if (disc.channelTypes?.length !== cmd.channelTypes?.length) return "channel-amount";
if (!_.isEqual(disc.channelTypes, cmd.channelTypes)) return "channel-channelTypes";
if (disc.channelTypes?.length !== cmd.channelTypes?.length || !_.isEqual(cmd.channelTypes, disc.channelTypes))
differences.push({ key: `${optionIndex}.channelTypes`, expected: cmd.channelTypes, received: disc.channelTypes });
}
break;
case ApplicationCommandOptionType.Integer:
case ApplicationCommandOptionType.Number:
{
const cmd = command as ApplicationCommandNumericOption | ApplicationCommandAutocompleteNumericOption;
const disc = discord as ApplicationCommandNumericOption | ApplicationCommandAutocompleteNumericOption;
if (!_.isEqual(disc.maxValue, cmd.maxValue)) return "int-maxValue";
if (!_.isEqual(disc.minValue, cmd.minValue)) return "int-minValue";
if (!_.isEqual(disc.maxValue, cmd.maxValue))
differences.push({ key: `${optionIndex}.maxValue`, expected: cmd.maxValue, received: disc.maxValue });
if (!_.isEqual(disc.minValue, cmd.minValue))
differences.push({ key: `${optionIndex}.minValue`, expected: cmd.minValue, received: disc.minValue });
}
break;
default:
break;
}

return null;
return differences;
}

private checkLocalization(discord: Record<string, string | null>, command: Record<string, string | null>, objectKey: string): Differences {
const differences: Differences = [];
Object.keys(command).forEach((key) => {
const received = discord[key];
const expected = command[key];
if (!received || received !== expected) differences.push({ key: objectKey, expected, received });
});

return differences;
}

private async updateCommand(discord: ApplicationCommand, command: Command) {
Expand Down
8 changes: 8 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,11 @@ export interface IgloClientOptions {
interactions: string;
};
}

export interface DifferenceData {
received: any;
expected: any;
key: string;
}

export type Differences = DifferenceData[];

1 comment on commit e23f834

@ijsKoud
Copy link
Member Author

@ijsKoud ijsKoud commented on e23f834 Dec 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ijsblokjeee[bot] release v1.2.0

Please sign in to comment.