feat(handler) moving and organizing files, refactor context api

This commit is contained in:
Jacob Nguyen
2022-04-04 17:40:51 -05:00
parent ecf07fe9ce
commit e21508ca4f
14 changed files with 120 additions and 131 deletions

12
package-lock.json generated
View File

@@ -6879,9 +6879,9 @@
}
},
"node_modules/minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
"dev": true
},
"node_modules/minimist-options": {
@@ -14100,9 +14100,9 @@
}
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
"dev": true
},
"minimist-options": {

View File

@@ -6,12 +6,14 @@ import Context from '../structures/context';
import type Wrapper from '../structures/wrapper';
import * as Files from '../utilities/readFile';
import { filterTap } from './observableHandling';
import { filter } from 'rxjs';
export const onInteractionCreate = ( wrapper : Wrapper ) => {
const { client } = wrapper;
(<Observable<Interaction>> fromEvent(client, 'interactionCreate'))
.pipe(
.pipe(
filter( i => i.inGuild() ),
concatMap ( interaction => {
if (interaction.isChatInputCommand()) {
return of(Files.Commands.get(interaction.commandName))
@@ -26,8 +28,7 @@ export const onInteractionCreate = ( wrapper : Wrapper ) => {
return of(Files.ContextMenuUser.get(interaction.commandName))
.pipe(
filterTap(CommandType.MENU_USER, mod => {
const ctx = Context.wrap(interaction);
mod.execute(ctx);
mod.execute(interaction);
}),
);
}
@@ -35,8 +36,7 @@ export const onInteractionCreate = ( wrapper : Wrapper ) => {
return of(Files.ContextMenuMsg.get(interaction.commandName))
.pipe(
filterTap(CommandType.MENU_MSG, mod => {
const ctx = Context.wrap(interaction);
mod.execute(ctx);
mod.execute(interaction);
}),
);
}
@@ -44,8 +44,7 @@ export const onInteractionCreate = ( wrapper : Wrapper ) => {
return of(Files.Buttons.get(interaction.customId))
.pipe(
filterTap(CommandType.BUTTON, mod => {
const ctx = Context.wrap(interaction);
mod.execute(ctx);
mod.execute(interaction);
})
);
}
@@ -53,8 +52,7 @@ export const onInteractionCreate = ( wrapper : Wrapper ) => {
return of(Files.SelectMenus.get(interaction.customId))
.pipe(
filterTap(CommandType.MENU_SELECT, mod => {
const ctx = Context.wrap(interaction);
mod.execute(ctx);
mod.execute(interaction);
})
);
}

View File

@@ -1,4 +1,4 @@
import type { Message } from 'discord.js';
import type { ChatInputCommandInteraction, Message } from 'discord.js';
import { fromEvent, Observable, of, concatMap } from 'rxjs';
import { CommandType } from '../sern';
import Context from '../structures/context';
@@ -15,14 +15,14 @@ export const onMessageCreate = (wrapper : Wrapper) => {
concatMap ( m => {
const [ prefix, ...data ] = fmt(m, defaultPrefix);
const posMod = Files.Commands.get(prefix) ?? Files.Alias.get(prefix);
const ctx = Context.wrap(m);
return of( posMod )
.pipe (
filterTap(CommandType.TEXT, mod => {
const ctx = Context.wrap(m);
mod.execute(ctx, ['text', data]);
})
);
);
})
).subscribe ({
error(e) {

View File

@@ -2,7 +2,7 @@ import type { Awaitable, Message } from 'discord.js';
import type { CommandType } from '../sern';
import type { Module } from '../structures/structxports';
import { Observable, throwError } from 'rxjs';
import type { ModuleDefs } from '../structures/commands/moduleHandler';
import type { ModuleDefs } from '../structures/modules/commands/moduleHandler';
import { SernError } from '../structures/errors';
import { isNotFromBot, isNotFromDM } from '../utilities/messageHelpers';
@@ -16,7 +16,7 @@ export function filterTap<T extends keyof ModuleDefs>(
return (src : Observable<Module|undefined>) =>
new Observable<Module|undefined>( subscriber => {
return src.subscribe({
next(modul ) {
next(modul) {
if(match(modul, cmdType)) {
const asModT = <ModuleDefs[T]> modul;
tap(asModT);
@@ -31,9 +31,9 @@ export function filterTap<T extends keyof ModuleDefs>(
error: (e) => subscriber.error(e),
complete: () => subscriber.complete()
});
});
}
export function ignoreNonBot(prefix : string) {
return (src : Observable<Message>) =>
new Observable<Message>(subscriber => {

View File

@@ -1,13 +1,13 @@
import { concatMap, first, from, fromEvent, pipe, tap } from 'rxjs';
import { first, from, fromEvent } from 'rxjs';
import { basename } from 'path';
import * as Files from '../utilities/readFile';
import type Wrapper from '../structures/wrapper';
import type { Module } from '../structures/structxports';
import type { HandlerCallback, ModuleHandlers, ModuleStates, ModuleType } from '../structures/commands/moduleHandler';
import type { HandlerCallback, ModuleHandlers, ModuleStates, ModuleType } from '../structures/modules/commands/moduleHandler';
import { CommandType } from '../sern';
export const onReady = ( wrapper : Wrapper ) => {
const { client, init, commands, } = wrapper;
const { client, init, commands } = wrapper;
fromEvent(client, 'ready')
.pipe(first())
.subscribe(() => {
@@ -54,7 +54,7 @@ function setCommands ( { mod, absPath } : { mod : Module, absPath : string } ) {
registerModules(name, mod);
}
async function createCommandCache(
function createCommandCache(
arr: {mod: Module, absPath: string}[]
) {
from(arr).subscribe ( setCommands );

View File

@@ -27,6 +27,8 @@ function eventObserver(client: Client, events: DiscordEvent[] ) {
fromEvent(client, event, cb).subscribe();
});
}
/**
* @enum { number };
*/
@@ -40,3 +42,4 @@ export enum CommandType {
BOTH = 0b000011,
ANY = 0b111111
}

View File

@@ -1,53 +0,0 @@
import type { ApplicationCommandOptionData, Awaitable, ButtonInteraction, ChatInputCommandInteraction, ContextMenuCommandInteraction, Interaction, MessageContextMenuCommandInteraction, SelectMenuInteraction } from 'discord.js';
import type { Args, Override } from '../../../types/handler';
import type { CommandType } from '../../sern';
import type Context from '../context';
type executeSlash = { execute : (ctx : Context<ChatInputCommandInteraction>, args: Args) => Awaitable<void> };
export interface BaseModule {
name? : string;
description : string;
execute: (ctx: Context<Interaction>, args: Args) => Awaitable<void>;
}
export type TextCommand = {
type : CommandType.TEXT;
alias : string[] | [],
} & BaseModule;
export type SlashCommand = {
type : CommandType.SLASH;
options : ApplicationCommandOptionData[] | [],
} & Override<BaseModule, executeSlash>;
export type BothCommand = {
type : CommandType.BOTH;
alias : string[] | [];
options : ApplicationCommandOptionData[] | [],
} & Override<BaseModule, executeSlash>;
export type ContextMenuUser = {
type : CommandType.MENU_USER;
} & Override<BaseModule, { execute : ( ctx: Context<ContextMenuCommandInteraction> ) => Awaitable<void> }>;
export type ContextMenuMsg = {
type : CommandType.MENU_MSG;
} & Override<BaseModule, { execute : ( ctx: Context<MessageContextMenuCommandInteraction> ) => Awaitable<void> }>;
export type ButtonCommand = {
type : CommandType.BUTTON;
} & Override<BaseModule, { execute : (ctx : Context<ButtonInteraction> ) => Awaitable<void> }>;
export type SelectMenuCommand = {
type : CommandType.MENU_SELECT;
} & Override<BaseModule, { execute : (ctx : Context<SelectMenuInteraction> ) => Awaitable<void> }>;
export type Module =
TextCommand
| SlashCommand
| BothCommand
| ContextMenuUser
| ContextMenuMsg
| ButtonCommand
| SelectMenuCommand;

View File

@@ -1,9 +1,8 @@
import type { APIInteractionGuildMember } from 'discord-api-types/v9';
import type {
Awaitable,
ChatInputCommandInteraction,
Guild,
GuildMember,
Interaction,
Message,
Snowflake,
TextBasedChannel,
@@ -18,21 +17,22 @@ function firstSome<T>(...args : Option<T>[]) : Nullish<T> {
}
return null;
}
export default class Context<I extends Interaction = Interaction> {
export default class Context {
private constructor(
private oMsg: Option<Message> = None,
private oInterac: Option<I> = None
private oInterac: Option<ChatInputCommandInteraction> = None
) {
this.oMsg = oMsg;
this.oInterac = oInterac;
}
static wrap<I extends Interaction = Interaction>(wrappable: I|Message) : Context<I> {
static wrap(
wrappable: ChatInputCommandInteraction|Message
) : Context {
if ( 'token' in wrappable ) {
return new Context<I>( None, Some(wrappable));
return new Context( None, Some(wrappable));
}
return new Context<I>(Some(wrappable), None);
return new Context(Some(wrappable), None);
}
public isEmpty() {
return this.oMsg.none && this.oInterac.none;
@@ -44,18 +44,6 @@ export default class Context<I extends Interaction = Interaction> {
return this.oInterac.unwrap();
}
/**
* maps a general Context<I> to Context<B>
* if interaction is None return Context.empty()
*/
public mapInteraction<B extends Interaction = Interaction>(
cb : ( ctx: I ) => Context<B>
) : Context<B> {
if (this.oInterac.none) return new Context();
return cb(this.oInterac.val);
}
public get id() : Snowflake {
return firstSome(
this.oInterac.map( i => i.id),
@@ -68,11 +56,11 @@ export default class Context<I extends Interaction = Interaction> {
this.oInterac.map(i => i.channel)
);
}
public get user(): Nullish<User> {
public get user(): User {
return firstSome(
this.oMsg.map(m => m.author),
this.oInterac.map(i => i.user)
);
)!;
}
public get createdTimestamp() : number {
return firstSome(
@@ -81,56 +69,51 @@ export default class Context<I extends Interaction = Interaction> {
)!;
}
public get guild() : Nullish<Guild> {
public get guild() : Guild {
return firstSome(
this.oMsg.map(m => m.guild),
this.oMsg.map(m => m.guild!),
this.oInterac.map(i => i.guild)
);
)!;
}
public get guildId() : Nullish<Snowflake> {
public get guildId() : Snowflake {
return firstSome(
this.oMsg.map(m => m.guildId),
this.oInterac.map(i => i.guildId)
);
)!;
}
public get member() : Nullish<GuildMember | APIInteractionGuildMember> {
public get member() : Nullish<GuildMember> {
return firstSome(
this.oMsg.map(m => m.member),
this.oInterac.map(i => i.member)
this.oMsg.andThen(m => Some(m.member!)),
this.oInterac.andThen(i => i.inCachedGuild() ? Some(i.member) : None)
);
}
/*
* Returns the underlying Context but allows for doing other operations
*/
public on (
onInteraction : ( interaction : I ) => Awaitable<void>,
onMsg? : (message : Message) => Awaitable<void>
): Context<I> {
public onInteraction(
onInteraction : ( interaction : ChatInputCommandInteraction ) => Awaitable<void>,
): Context {
this.oInterac.map(onInteraction);
this.oMsg.map(m => onMsg?.(m));
return this;
}
public extractInteraction<T>(
extract : (interaction : I) => T
public onMessage(
onMessage : ( message : Message ) => Awaitable<void>
): Context {
this.oMsg.map( onMessage );
return this;
}
public takeInteractionValue<T>(
extract : (interaction : ChatInputCommandInteraction) => T
): Nullish<T> {
if(this.oInterac.none) return null;
return extract(this.oInterac.val);
}
public extractMessage<T>(
extract : (message: Message) => T
): Nullish<T> {
public takeMessageValue<T>(
extract : (message: Message) => T
): Nullish<T> {
if(this.oMsg.none) return null;
return extract(this.oMsg.val);
}
// extract either
public extractEither<T,V>(
i : (interaction : I) => T,
m : (message : Message ) => V
) {
const iExtract = this.extractMessage(m);
const mExtract = this.extractInteraction(i);
return iExtract ?? mExtract;
}
}

View File

@@ -0,0 +1,45 @@
import type { ApplicationCommandOptionData, Awaitable, ButtonInteraction, ContextMenuCommandInteraction, MessageContextMenuCommandInteraction, SelectMenuInteraction } from 'discord.js';
import type { Override } from '../../../../types/handler';
import type { CommandType } from '../../../sern';
import type { BaseModule } from '../module';
//possible refactoring to interfaces and not types
export type TextCommand = {
type : CommandType.TEXT;
alias : string[] | [],
} & BaseModule;
export type SlashCommand = {
type : CommandType.SLASH;
options : ApplicationCommandOptionData[] | [],
} & BaseModule;
export type BothCommand = {
type : CommandType.BOTH;
alias : string[] | [];
options : ApplicationCommandOptionData[] | [],
} & BaseModule;
export type ContextMenuUser = {
type : CommandType.MENU_USER;
} & Override<BaseModule, { execute : ( ctx: ContextMenuCommandInteraction ) => Awaitable<void> }>;
export type ContextMenuMsg = {
type : CommandType.MENU_MSG;
} & Override<BaseModule, { execute : ( ctx: MessageContextMenuCommandInteraction ) => Awaitable<void> }>;
export type ButtonCommand = {
type : CommandType.BUTTON;
} & Override<BaseModule, { execute : (ctx :ButtonInteraction ) => Awaitable<void> }>;
export type SelectMenuCommand = {
type : CommandType.MENU_SELECT;
} & Override<BaseModule, { execute : (ctx : SelectMenuInteraction ) => Awaitable<void> }>;
export type Module =
TextCommand
| SlashCommand
| BothCommand
| ContextMenuUser
| ContextMenuMsg
| ButtonCommand
| SelectMenuCommand;

View File

@@ -1,4 +1,4 @@
import { CommandType } from '../../sern';
import { CommandType } from '../../../sern';
import type { TextCommand, BothCommand, ButtonCommand, SlashCommand, ContextMenuMsg, ContextMenuUser, SelectMenuCommand } from './module';
//https://stackoverflow.com/questions/64092736/alternative-to-switch-statement-for-typescript-discriminated-union

View File

@@ -0,0 +1,13 @@
import type { Awaitable, ChatInputCommandInteraction, Interaction } from "discord.js";
import type { Args } from "../../..";
import type Context from "../context";
export interface BaseModule {
name? : string;
description : string;
execute: (ctx: Context, args: Args) => Awaitable<void>;
}

View File

@@ -1,5 +1,5 @@
import Context from './context';
import type { SlashCommand, TextCommand, BothCommand, Module } from './commands/module';
import type { SlashCommand, TextCommand, BothCommand, Module } from '../structures/modules/commands/module';
import type Wrapper from './wrapper';
export {

View File

@@ -14,7 +14,6 @@ interface Wrapper {
readonly client: Client;
readonly defaultPrefix: string;
readonly commands: string;
readonly components : string;
init?: (handler: Wrapper) => void;
readonly events? : DiscordEvent[];
}

View File

@@ -1,6 +1,6 @@
import { readdirSync, statSync } from 'fs';
import { join } from 'path';
import type { Module } from '../structures/commands/module';
import type { Module } from '../structures/modules/commands/module';
import { SernError } from '../structures/errors';
//We can look into lazily loading modules once everything is set
@@ -11,6 +11,7 @@ export const Alias = new Map<string, Module>();
export const Buttons = new Map<string, Module>();
export const SelectMenus = new Map<string, Module>();
// Courtesy @Townsy45
function readPath(dir: string, arrayOfFiles: string[] = []): string[] {
try {