feat(helpers): Permissions util

This commit is contained in:
Marcos Susaña 2023-07-21 15:27:38 -04:00
parent 490898a56f
commit fa32071d4e
14 changed files with 212 additions and 86 deletions

View File

@ -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<E extends keyof GatewayEvents> {
event: `${E}`;
@ -13,19 +13,19 @@ interface CollectorOptions<E extends keyof GatewayEvents> {
export const DEFAULT_OPTIONS = {
filter: () => true,
max: -1
max: -1,
};
export enum CollectorStatus {
Idle = 0,
Started = 1,
Ended = 2
Ended = 2,
}
export class EventCollector<E extends keyof GatewayEvents> extends EventEmitter {
collected = new Set<Parameters<Handler[E]>[0]>();
status: CollectorStatus = CollectorStatus.Idle;
options: MakeRequired<CollectorOptions<E>, 'filter' | 'max'>;
options: MakeRequired<CollectorOptions<E>, "filter" | "max">;
private timeout: NodeJS.Timeout | null = null;
constructor(readonly session: Session, rawOptions: CollectorOptions<E>) {
@ -36,24 +36,24 @@ export class EventCollector<E extends keyof GatewayEvents> extends EventEmitter
start() {
this.session.setMaxListeners(this.session.getMaxListeners() + 1);
this.session.on(this.options.event, (...args: unknown[]) => this.collect(...(args as Parameters<Handler[E]>)));
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<Handler[E]>) {
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<E extends keyof GatewayEvents> 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<Handler[E]>) => unknown): this;
on(event: 'end', listener: (reason: string | null | undefined, collected: Set<Parameters<Handler[E]>[0]>) => void): this;
on(event: "collect", listener: (...args: Parameters<Handler[E]>) => unknown): this;
on(event: "end", listener: (reason: string | null | undefined, collected: Set<Parameters<Handler[E]>[0]>) => void): this;
on(event: string, listener: unknown): this {
return super.on(event, listener as () => unknown);
}
once(event: 'collect', listener: (...args: Parameters<Handler[E]>) => unknown): this;
once(event: 'end', listener: (reason: string | null | undefined, collected: Set<Parameters<Handler[E]>[0]>) => void): this;
once(event: "collect", listener: (...args: Parameters<Handler[E]>) => unknown): this;
once(event: "end", listener: (reason: string | null | undefined, collected: Set<Parameters<Handler[E]>[0]>) => void): this;
once(event: string, listener: unknown): this {
return super.once(event, listener as () => unknown);
}

View File

@ -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 };
}
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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";

View File

@ -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<RESTPostAPIChatInputApplicationCommandsJSONBody> = {}) {}
@ -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<AllSlashOptions['toJSON']>) {
addRawOption(option: ReturnType<AllSlashOptions["toJSON"]>) {
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;
};

View File

@ -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<APIApplicationCommandOption, AACSO | AACNO | AACIO | AACSCO>
@ -53,7 +53,7 @@ export abstract class SlashBaseOption<DataType extends SlashBaseOptionTypes> {
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<APIApplicationCo
return this;
}
addRawOption(option: ReturnType<SlashSubcommandOption['toJSON']>) {
addRawOption(option: ReturnType<SlashSubcommandOption["toJSON"]>) {
this.data.options ??= [];
this.data.options.push(option);
}

View File

@ -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<T extends MessageComponents> extends BaseComponent<APIActionRowComponent<APIMessageActionRowComponent>> {
constructor({ components, ...data }: Partial<APIActionRowComponent<APIMessageActionRowComponent>>) {
@ -22,7 +22,7 @@ export class MessageActionRow<T extends MessageComponents> extends BaseComponent
toJSON(): APIActionRowComponent<APIMessageActionRowComponent> {
return {
...this.data,
components: this.components.map((c) => c.toJSON())
} as APIActionRowComponent<ReturnType<T['toJSON']>>;
components: this.components.map((c) => c.toJSON()),
} as APIActionRowComponent<ReturnType<T["toJSON"]>>;
}
}

View File

@ -1,4 +1,4 @@
import { APIBaseComponent, ComponentType } from '@biscuitland/common';
import { APIBaseComponent, ComponentType } from "@biscuitland/common";
export abstract class BaseComponent<TYPE extends Partial<APIBaseComponent<ComponentType>> = APIBaseComponent<ComponentType>,> {
constructor(public data: Partial<TYPE>) {}

View File

@ -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<ButtonStyle, ButtonStyle.Link>;

View File

@ -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<Select extends APISelectMenuComponent = APISelectMenuComponent,> extends BaseComponent<Select> {
setCustomId(id: string): this {

View File

@ -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<APITextInputComponent> {
constructor(data: Partial<APITextInputComponent> = {}) {

View File

@ -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";

View File

@ -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";