From fa32071d4ef4c386d74c6ae6be1ed2950d527be7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Susa=C3=B1a?= Date: Fri, 21 Jul 2023 15:27:38 -0400 Subject: [PATCH] feat(helpers): Permissions util --- packages/helpers/src/Collector.ts | 34 ++--- packages/helpers/src/Permissions.ts | 132 ++++++++++++++++++ packages/helpers/src/Utils.ts | 9 +- .../commands/contextMenu/ContextCommand.ts | 16 +-- packages/helpers/src/commands/index.ts | 6 +- .../src/commands/slash/SlashCommand.ts | 16 +-- .../src/commands/slash/SlashCommandOption.ts | 36 ++--- packages/helpers/src/components/ActionRow.ts | 10 +- .../helpers/src/components/BaseComponent.ts | 2 +- .../helpers/src/components/MessageButton.ts | 4 +- packages/helpers/src/components/SelectMenu.ts | 8 +- packages/helpers/src/components/TextInput.ts | 6 +- packages/helpers/src/components/index.ts | 10 +- packages/helpers/src/index.ts | 9 +- 14 files changed, 212 insertions(+), 86 deletions(-) create mode 100644 packages/helpers/src/Permissions.ts diff --git a/packages/helpers/src/Collector.ts b/packages/helpers/src/Collector.ts index 3315627..f612ffc 100644 --- a/packages/helpers/src/Collector.ts +++ b/packages/helpers/src/Collector.ts @@ -1,7 +1,7 @@ -import { MakeRequired, Options } from '@biscuitland/common'; -import { type Session, Handler } from '@biscuitland/core'; -import { GatewayEvents } from '@biscuitland/ws'; -import { EventEmitter } from 'node:events'; +import { MakeRequired, Options } from "@biscuitland/common"; +import { Handler, type Session } from "@biscuitland/core"; +import { GatewayEvents } from "@biscuitland/ws"; +import { EventEmitter } from "node:events"; interface CollectorOptions { event: `${E}`; @@ -13,19 +13,19 @@ interface CollectorOptions { export const DEFAULT_OPTIONS = { filter: () => true, - max: -1 + max: -1, }; export enum CollectorStatus { Idle = 0, Started = 1, - Ended = 2 + Ended = 2, } export class EventCollector extends EventEmitter { collected = new Set[0]>(); status: CollectorStatus = CollectorStatus.Idle; - options: MakeRequired, 'filter' | 'max'>; + options: MakeRequired, "filter" | "max">; private timeout: NodeJS.Timeout | null = null; constructor(readonly session: Session, rawOptions: CollectorOptions) { @@ -36,24 +36,24 @@ export class EventCollector extends EventEmitter start() { this.session.setMaxListeners(this.session.getMaxListeners() + 1); this.session.on(this.options.event, (...args: unknown[]) => this.collect(...(args as Parameters))); - this.timeout = setTimeout(() => this.stop('time'), this.options.idle ?? this.options.time); + this.timeout = setTimeout(() => this.stop("time"), this.options.idle ?? this.options.time); } private collect(...args: Parameters) { if (this.options.filter?.(...args)) { this.collected.add(args[0]); - this.emit('collect', ...args); + this.emit("collect", ...args); } if (this.options.idle) { if (this.timeout) clearTimeout(this.timeout); - this.timeout = setTimeout(() => this.stop('time'), this.options.idle); + this.timeout = setTimeout(() => this.stop("time"), this.options.idle); } - if (this.collected.size >= this.options.max!) this.stop('max'); + if (this.collected.size >= this.options.max!) this.stop("max"); } - stop(reason = 'User stopped') { + stop(reason = "User stopped") { if (this.status === CollectorStatus.Ended) return; if (this.timeout) clearTimeout(this.timeout); @@ -62,17 +62,17 @@ export class EventCollector extends EventEmitter this.session.setMaxListeners(this.session.getMaxListeners() - 1); this.status = CollectorStatus.Ended; - this.emit('end', reason, this.collected); + this.emit("end", reason, this.collected); } - on(event: 'collect', listener: (...args: Parameters) => unknown): this; - on(event: 'end', listener: (reason: string | null | undefined, collected: Set[0]>) => void): this; + on(event: "collect", listener: (...args: Parameters) => unknown): this; + on(event: "end", listener: (reason: string | null | undefined, collected: Set[0]>) => void): this; on(event: string, listener: unknown): this { return super.on(event, listener as () => unknown); } - once(event: 'collect', listener: (...args: Parameters) => unknown): this; - once(event: 'end', listener: (reason: string | null | undefined, collected: Set[0]>) => void): this; + once(event: "collect", listener: (...args: Parameters) => unknown): this; + once(event: "end", listener: (reason: string | null | undefined, collected: Set[0]>) => void): this; once(event: string, listener: unknown): this { return super.once(event, listener as () => unknown); } diff --git a/packages/helpers/src/Permissions.ts b/packages/helpers/src/Permissions.ts new file mode 100644 index 0000000..d9a0856 --- /dev/null +++ b/packages/helpers/src/Permissions.ts @@ -0,0 +1,132 @@ +import { PermissionFlagsBits } from "@biscuitland/common"; + +export type PermissionsStrings = keyof typeof PermissionFlagsBits; +export type PermissionResolvable = bigint | PermissionsStrings | PermissionsStrings[] | PermissionsStrings | PermissionsStrings[]; + +export class Permissions { + /** Stores a reference to BitwisePermissionFlags */ + static Flags = PermissionFlagsBits; + + /** Falsy; Stores the lack of permissions*/ + static None = 0n; + + /** Stores all entity permissions */ + bitfield: bigint; + + /** + * Wheter to grant all other permissions to the administrator + * **Not to get confused with Permissions#admin** + */ + __admin__ = true; + + constructor(bitfield: PermissionResolvable) { + this.bitfield = Permissions.resolve(bitfield); + } + + /** Wheter the bitfield has the administrator flag */ + get admin(): boolean { + return this.has(Permissions.Flags.Administrator); + } + + get array(): PermissionsStrings[] { + // unsafe cast, do not edit + const permissions = Object.keys(Permissions.Flags) as PermissionsStrings[]; + return permissions.filter((bit) => this.has(bit)); + } + + add(...bits: PermissionResolvable[]): this { + let reduced = 0n; + for (const bit of bits) { + reduced |= Permissions.resolve(bit); + } + this.bitfield |= reduced; + return this; + } + + remove(...bits: PermissionResolvable[]): this { + let reduced = 0n; + for (const bit of bits) { + reduced |= Permissions.resolve(bit); + } + this.bitfield &= ~reduced; + return this; + } + + has(bit: PermissionResolvable): boolean { + const bbit = Permissions.resolve(bit); + + if (this.__admin__ && this.bitfield & BigInt(Permissions.Flags.Administrator)) { + return true; + } + + return (this.bitfield & bbit) === bbit; + } + + any(bit: PermissionResolvable): boolean { + const bbit = Permissions.resolve(bit); + + if (this.__admin__ && this.bitfield & BigInt(Permissions.Flags.Administrator)) { + return true; + } + + return (this.bitfield & bbit) !== Permissions.None; + } + + equals(bit: PermissionResolvable): boolean { + return !!(this.bitfield & Permissions.resolve(bit)); + } + + /** Gets all permissions */ + static get All(): bigint { + let reduced = 0n; + for (const key in PermissionFlagsBits) { + const perm = PermissionFlagsBits[key]; + + reduced = reduced | perm; + } + return reduced; + } + + static resolve(bit: PermissionResolvable): bigint { + switch (typeof bit) { + case "bigint": + return bit; + case "number": + return BigInt(bit); + case "string": + return BigInt(Permissions.Flags[bit]); + case "object": + return Permissions.resolve( + bit + .map((p) => (typeof p === "string" ? BigInt(Permissions.Flags[p]) : BigInt(p))) + .reduce((acc, cur) => acc | cur, Permissions.None), + ); + default: + throw new TypeError(`Cannot resolve permission: ${bit}`); + } + } + + static sum(permissions: (bigint | number)[]) { + return permissions.reduce((y, x) => BigInt(y) | BigInt(x), Permissions.None); + } + + static reduce(permissions: PermissionResolvable[]): Permissions { + const solved = permissions.map(Permissions.resolve); + + return new Permissions(solved.reduce((y, x) => y | x, Permissions.None)); + } + + *[Symbol.iterator]() { + yield* this.array; + } + + valueOf() { + return this.bitfield; + } + + toJSON(): { fields: string[] } { + const fields = Object.keys(Permissions.Flags).filter((bit) => typeof bit === "number" && this.has(bit)); + + return { fields }; + } +} diff --git a/packages/helpers/src/Utils.ts b/packages/helpers/src/Utils.ts index c7dbd56..df0589b 100644 --- a/packages/helpers/src/Utils.ts +++ b/packages/helpers/src/Utils.ts @@ -1,4 +1,4 @@ -import { APIMessageActionRowComponent, APIModalActionRowComponent, ComponentType, PermissionFlagsBits } from '@biscuitland/common'; +import { APIMessageActionRowComponent, APIModalActionRowComponent, ComponentType } from "@biscuitland/common"; import { ChannelSelectMenu, MentionableSelectMenu, @@ -6,9 +6,9 @@ import { ModalTextInput, RoleSelectMenu, StringSelectMenu, - UserSelectMenu -} from './components'; -import { BaseComponent } from './components/BaseComponent'; + UserSelectMenu, +} from "./components"; +import { BaseComponent } from "./components/BaseComponent"; export function createComponent(data: APIMessageActionRowComponent): HelperComponents; export function createComponent(data: APIModalActionRowComponent): HelperComponents; @@ -36,7 +36,6 @@ export function createComponent(data: HelperComponents | APIMessageActionRowComp } } -export type PermissionsStrings = `${keyof typeof PermissionFlagsBits}`; export type OptionValuesLength = { max: number; min: number }; export type MessageSelectMenus = RoleSelectMenu | UserSelectMenu | StringSelectMenu | ChannelSelectMenu | MentionableSelectMenu; export type MessageComponents = MessageButton | MessageSelectMenus; diff --git a/packages/helpers/src/commands/contextMenu/ContextCommand.ts b/packages/helpers/src/commands/contextMenu/ContextCommand.ts index 3f8b8f7..a60239d 100644 --- a/packages/helpers/src/commands/contextMenu/ContextCommand.ts +++ b/packages/helpers/src/commands/contextMenu/ContextCommand.ts @@ -1,11 +1,5 @@ -import { - LocalizationMap, - ApplicationCommandType, - Permissions, - PermissionFlagsBits, - RESTPostAPIContextMenuApplicationCommandsJSONBody -} from '@biscuitland/common'; -import { PermissionsStrings } from '../../Utils'; +import { ApplicationCommandType, LocalizationMap, RESTPostAPIContextMenuApplicationCommandsJSONBody } from "@biscuitland/common"; +import { PermissionResolvable, Permissions } from "../../Permissions"; export type ContextCommandType = ApplicationCommandType.Message | ApplicationCommandType.User; @@ -14,7 +8,7 @@ export class ContextCommand { name_localizations?: LocalizationMap; type: ContextCommandType = undefined!; default_permission: boolean | undefined = undefined; - default_member_permissions: Permissions | null | undefined = undefined; + default_member_permissions: string | undefined = undefined; dm_permission: boolean | undefined = undefined; setName(name: string): this { @@ -38,8 +32,8 @@ export class ContextCommand { return this; } - setDefautlMemberPermissions(permissions: PermissionsStrings[]): this { - this.default_member_permissions = `$${permissions.reduce((y, x) => y | PermissionFlagsBits[x], 0n)}`; + setDefautlMemberPermissions(permissions: PermissionResolvable[]): this { + this.default_member_permissions = Permissions.reduce(permissions).bitfield.toString(); return this; } diff --git a/packages/helpers/src/commands/index.ts b/packages/helpers/src/commands/index.ts index 4e4a865..973a0bb 100644 --- a/packages/helpers/src/commands/index.ts +++ b/packages/helpers/src/commands/index.ts @@ -1,3 +1,3 @@ -export * from './slash/SlashCommand'; -export * from './slash/SlashCommandOption'; -export * from './contextMenu/ContextCommand'; +export * from "./contextMenu/ContextCommand"; +export * from "./slash/SlashCommand"; +export * from "./slash/SlashCommandOption"; diff --git a/packages/helpers/src/commands/slash/SlashCommand.ts b/packages/helpers/src/commands/slash/SlashCommand.ts index 803b5ac..ced21cc 100644 --- a/packages/helpers/src/commands/slash/SlashCommand.ts +++ b/packages/helpers/src/commands/slash/SlashCommand.ts @@ -1,7 +1,7 @@ -import { ApplicationCommandType, PermissionFlagsBits, RESTPostAPIChatInputApplicationCommandsJSONBody } from '@biscuitland/common'; -import { AllSlashOptions, SlashSubcommandGroupOption, SlashSubcommandOption } from './SlashCommandOption'; -import { PermissionsStrings } from '../../Utils'; -import { Mixin } from 'ts-mixer'; +import { ApplicationCommandType, RESTPostAPIChatInputApplicationCommandsJSONBody } from "@biscuitland/common"; +import { Mixin } from "ts-mixer"; +import { PermissionResolvable, Permissions } from "../../Permissions"; +import { AllSlashOptions, SlashSubcommandGroupOption, SlashSubcommandOption } from "./SlashCommandOption"; class SlashCommandB { constructor(public data: Partial = {}) {} @@ -16,8 +16,8 @@ class SlashCommandB { return this; } - setDefautlMemberPermissions(permissions: PermissionsStrings[]): this { - this.data.default_member_permissions = `$${permissions.reduce((y, x) => y | PermissionFlagsBits[x], 0n)}`; + setDefautlMemberPermissions(permissions: PermissionResolvable[]): this { + this.data.default_member_permissions = Permissions.reduce(permissions).bitfield.toString(); return this; } @@ -33,7 +33,7 @@ class SlashCommandB { return this; } - addRawOption(option: ReturnType) { + addRawOption(option: ReturnType) { this.data.options ??= []; // @ts-expect-error discord-api-types bad typing, again this.data.options.push(option); @@ -42,7 +42,7 @@ class SlashCommandB { toJSON(): RESTPostAPIChatInputApplicationCommandsJSONBody { return { ...this.data, - type: ApplicationCommandType.ChatInput + type: ApplicationCommandType.ChatInput, } as RESTPostAPIChatInputApplicationCommandsJSONBody & { type: ApplicationCommandType.ChatInput; }; diff --git a/packages/helpers/src/commands/slash/SlashCommandOption.ts b/packages/helpers/src/commands/slash/SlashCommandOption.ts index 7cf490e..39f6ffd 100644 --- a/packages/helpers/src/commands/slash/SlashCommandOption.ts +++ b/packages/helpers/src/commands/slash/SlashCommandOption.ts @@ -1,26 +1,26 @@ import { - APIApplicationCommandOption, - APIApplicationCommandOptionChoice, - ApplicationCommandOptionType, + APIApplicationCommandIntegerOption as AACIO, + APIApplicationCommandNumberOption as AACNO, + APIApplicationCommandSubcommandOption as AACSCO, + APIApplicationCommandSubcommandGroupOption as AACSGO, APIApplicationCommandStringOption as AACSO, + APIApplicationCommandAttachmentOption, + APIApplicationCommandBooleanOption, + APIApplicationCommandChannelOption, + APIApplicationCommandMentionableOption, + APIApplicationCommandOption, APIApplicationCommandOptionBase, + APIApplicationCommandOptionChoice, + APIApplicationCommandRoleOption, + APIApplicationCommandUserOption, + ApplicationCommandOptionType, + ChannelType, LocalizationMap, RestToKeys, TypeArray, When, - APIApplicationCommandNumberOption as AACNO, - APIApplicationCommandIntegerOption as AACIO, - APIApplicationCommandSubcommandGroupOption as AACSGO, - APIApplicationCommandSubcommandOption as AACSCO, - APIApplicationCommandUserOption, - APIApplicationCommandChannelOption, - ChannelType, - APIApplicationCommandRoleOption, - APIApplicationCommandMentionableOption, - APIApplicationCommandAttachmentOption, - APIApplicationCommandBooleanOption -} from '@biscuitland/common'; -import { OptionValuesLength } from '../../'; +} from "@biscuitland/common"; +import { OptionValuesLength } from "../../"; export type SlashBaseOptionTypes = | Exclude @@ -53,7 +53,7 @@ export abstract class SlashBaseOption { return this; } - addLocalizations(locals: RestToKeys<[LocalizationMap, 'name', 'description']>): this { + addLocalizations(locals: RestToKeys<[LocalizationMap, "name", "description"]>): this { this.data.name_localizations = locals.name; this.data.description_localizations = locals.description; return this; @@ -333,7 +333,7 @@ export class SlashSubcommandGroupOption extends SlashBaseOption) { + addRawOption(option: ReturnType) { this.data.options ??= []; this.data.options.push(option); } diff --git a/packages/helpers/src/components/ActionRow.ts b/packages/helpers/src/components/ActionRow.ts index 50b0eed..379c1ae 100644 --- a/packages/helpers/src/components/ActionRow.ts +++ b/packages/helpers/src/components/ActionRow.ts @@ -1,6 +1,6 @@ -import { APIActionRowComponent, APIMessageActionRowComponent, ComponentType, TypeArray } from '@biscuitland/common'; -import { MessageComponents, createComponent } from '../Utils'; -import { BaseComponent } from './BaseComponent'; +import { APIActionRowComponent, APIMessageActionRowComponent, ComponentType, TypeArray } from "@biscuitland/common"; +import { MessageComponents, createComponent } from "../Utils"; +import { BaseComponent } from "./BaseComponent"; export class MessageActionRow extends BaseComponent> { constructor({ components, ...data }: Partial>) { @@ -22,7 +22,7 @@ export class MessageActionRow extends BaseComponent toJSON(): APIActionRowComponent { return { ...this.data, - components: this.components.map((c) => c.toJSON()) - } as APIActionRowComponent>; + components: this.components.map((c) => c.toJSON()), + } as APIActionRowComponent>; } } diff --git a/packages/helpers/src/components/BaseComponent.ts b/packages/helpers/src/components/BaseComponent.ts index c88dcf9..2bb8f09 100644 --- a/packages/helpers/src/components/BaseComponent.ts +++ b/packages/helpers/src/components/BaseComponent.ts @@ -1,4 +1,4 @@ -import { APIBaseComponent, ComponentType } from '@biscuitland/common'; +import { APIBaseComponent, ComponentType } from "@biscuitland/common"; export abstract class BaseComponent> = APIBaseComponent,> { constructor(public data: Partial) {} diff --git a/packages/helpers/src/components/MessageButton.ts b/packages/helpers/src/components/MessageButton.ts index 9c03f36..98310b8 100644 --- a/packages/helpers/src/components/MessageButton.ts +++ b/packages/helpers/src/components/MessageButton.ts @@ -1,5 +1,5 @@ -import { APIButtonComponentBase, APIMessageComponentEmoji, ButtonStyle, ComponentType, When } from '@biscuitland/common'; -import { BaseComponent } from './BaseComponent'; +import { APIButtonComponentBase, APIMessageComponentEmoji, ButtonStyle, ComponentType, When } from "@biscuitland/common"; +import { BaseComponent } from "./BaseComponent"; export type ButtonStylesForID = Exclude; diff --git a/packages/helpers/src/components/SelectMenu.ts b/packages/helpers/src/components/SelectMenu.ts index 56c90a1..0a49894 100644 --- a/packages/helpers/src/components/SelectMenu.ts +++ b/packages/helpers/src/components/SelectMenu.ts @@ -9,10 +9,10 @@ import { APIUserSelectComponent, ChannelType, ComponentType, - TypeArray -} from '@biscuitland/common'; -import { BaseComponent } from './BaseComponent'; -import { OptionValuesLength } from '..'; + TypeArray, +} from "@biscuitland/common"; +import { OptionValuesLength } from ".."; +import { BaseComponent } from "./BaseComponent"; class SelectMenu { setCustomId(id: string): this { diff --git a/packages/helpers/src/components/TextInput.ts b/packages/helpers/src/components/TextInput.ts index 10a8778..abdb4e3 100644 --- a/packages/helpers/src/components/TextInput.ts +++ b/packages/helpers/src/components/TextInput.ts @@ -1,6 +1,6 @@ -import { APITextInputComponent, ComponentType, TextInputStyle } from '@biscuitland/common'; -import { BaseComponent } from './BaseComponent'; -import { OptionValuesLength } from '..'; +import { APITextInputComponent, ComponentType, TextInputStyle } from "@biscuitland/common"; +import { OptionValuesLength } from ".."; +import { BaseComponent } from "./BaseComponent"; export class ModalTextInput extends BaseComponent { constructor(data: Partial = {}) { diff --git a/packages/helpers/src/components/index.ts b/packages/helpers/src/components/index.ts index 371ac8d..554cf5e 100644 --- a/packages/helpers/src/components/index.ts +++ b/packages/helpers/src/components/index.ts @@ -1,5 +1,5 @@ -export * from './ActionRow'; -export * from './MessageButton'; -export * from './SelectMenu'; -export * from './TextInput'; -export * from './BaseComponent'; +export * from "./ActionRow"; +export * from "./BaseComponent"; +export * from "./MessageButton"; +export * from "./SelectMenu"; +export * from "./TextInput"; diff --git a/packages/helpers/src/index.ts b/packages/helpers/src/index.ts index bbb9a93..fd99747 100644 --- a/packages/helpers/src/index.ts +++ b/packages/helpers/src/index.ts @@ -1,4 +1,5 @@ -export * from './MessageEmbed'; -export * from './components/index'; -export * from './Utils'; -export * from './commands'; +export * from "./MessageEmbed"; +export * from "./Permissions"; +export * from "./Utils"; +export * from "./commands"; +export * from "./components";