highrise.bot 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "highrise.bot",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Unofficial JavaScript SDK for the Highrise platform. Feature-complete WebSocket client with TypeScript support, built for production environments.",
5
5
  "keywords": [
6
6
  "highrise.bot",
@@ -1,7 +1,8 @@
1
1
  const RoleManager = require("highrise.bot/src/classes/Managers/RoleManager");
2
2
  const CooldownManager = require("highrise.bot/src/classes/Managers/CooldownManager");
3
3
  const DanceFloor = require("highrise.bot/src/classes/Managers/DanceFloorManagers");
4
- const { Logger } = require("highrise.bot/src/classes/Managers/Helpers/LoggerManager")
4
+ const { Logger } = require("highrise.bot/src/classes/Managers/Helpers/LoggerManager");
5
+ const CommandHandler = require("highrise.bot/src/classes/Handlers/CommandHandler");
5
6
 
6
7
  class Utils {
7
8
  constructor(bot, options) {
@@ -14,7 +15,7 @@ class Utils {
14
15
  dataDir: './Json/permissions',
15
16
  filename: 'roles.json',
16
17
  customRoles: options.customRoles || [],
17
-
18
+
18
19
  });
19
20
  }
20
21
 
@@ -72,6 +73,10 @@ class Utils {
72
73
  return result;
73
74
  }
74
75
 
76
+ command(relativeDir, options = {}) {
77
+ return new CommandHandler(this.bot, relativeDir, options);
78
+ };
79
+
75
80
  }
76
81
 
77
82
  module.exports = Utils
@@ -0,0 +1,396 @@
1
+ const fsp = require('fs').promises
2
+ const path = require('path')
3
+
4
+ class CommandHandler {
5
+ constructor(bot, relativeDir, options = {}) {
6
+ this.options = {
7
+ autoLoad: options.autoLoad || true
8
+ }
9
+
10
+ this.bot = bot
11
+ this.logger = bot.utils.logger
12
+ this.dir = path.resolve(relativeDir);
13
+
14
+ this.commands = new Map() // name -> module
15
+ this.aliases = new Map() // alias -> commandName
16
+
17
+ this.preMiddlewares = new Map()
18
+ this.postMiddlewares = new Map()
19
+
20
+ this.errorHeader = `CommandHandler (${this.dir})`
21
+ this.handlerError = (...args) => this.logger.error(this.errorHeader, args)
22
+
23
+ if (this.options.autoLoad) this._initialize()
24
+ }
25
+
26
+ async _initialize() {
27
+ const files = await this._getFiles()
28
+ const modules = await this._loadModules(files)
29
+ const result = await this._registerCommands(modules)
30
+ this.logger.success(this.errorHeader, `initializing Commands | Success: ${result.success} - Failed: ${result.failed}`)
31
+ }
32
+
33
+ async _getFiles() {
34
+ try {
35
+ const files = await fsp.readdir(this.dir, {
36
+ recursive: true,
37
+ withFileTypes: true
38
+ });
39
+
40
+ return files
41
+ .filter(file => file.name.endsWith('.js'))
42
+ } catch (error) {
43
+ return this._handleGetFilesError(this.dir, error);
44
+ }
45
+ }
46
+
47
+ _handleGetFilesError(dir, error) {
48
+ if (error.code === 'ENOENT') {
49
+ this.logger.warn(`CommandHandler`, `Directory does not exist: ${dir}\n`);
50
+ return [];
51
+ } else if (error.code === 'EACCES') {
52
+ this.logger.warn(`CommandHandler`, `Access failed to directory: ${dir}\n`);
53
+ return [];
54
+ } else {
55
+ this.logger.error(`CommandHandler`, `Error reading directory: ${dir}`, error.message);
56
+ return []
57
+ }
58
+ }
59
+
60
+ async _loadModules(files) {
61
+ let modules = []
62
+
63
+ for (const file of files) {
64
+ const modulePath = path.join(file.parentPath, file.name);
65
+
66
+ if (require.cache[modulePath]) {
67
+ delete require.cache[modulePath];
68
+ }
69
+
70
+ try {
71
+ let module = require(modulePath)
72
+ Object.assign(module, {
73
+ filePath: modulePath
74
+ })
75
+
76
+ modules.push(module)
77
+ } catch (error) {
78
+ const causePart = this._dissectSyntaxErrorStack(error)
79
+ const message = this._buildSyntaxErrorMessage(file, causePart)
80
+ this.logger.error(`CommandHandler - Loading Modules`, message)
81
+ }
82
+ }
83
+
84
+ return modules
85
+ }
86
+
87
+ _dissectSyntaxErrorStack(error) {
88
+ const stack = error.stack
89
+ const parts = stack.split('\n')
90
+ const causePart = parts.slice(0, 3).join('\n')
91
+ return causePart
92
+ }
93
+
94
+ _buildSyntaxErrorMessage(fileName, causePart) {
95
+ let message = `Error: Caught a syntax error in ${fileName}:\n`
96
+ message += `Cause:\n ${causePart}`
97
+ return message
98
+ }
99
+
100
+ _buildValidationWarning(module, warns) {
101
+ let warningMessage = `Error Registering command "${module.filePath}":\n`
102
+ for (const warn of warns) {
103
+ warningMessage += warn.concat('\n')
104
+ }
105
+
106
+ this.logger.warn(`CommandHandler - Module Register`, warningMessage.trim())
107
+ }
108
+
109
+ _validateModule(module) {
110
+ let warn = []
111
+
112
+ if (typeof module !== 'object' || module === null) {
113
+ warn.push(`module must be an object`)
114
+ return warn;
115
+ }
116
+
117
+ if (('name' in module)) {
118
+ if (typeof module.name !== 'string' || module.name.length === 0) {
119
+ warn.push(`module.name must be a non-empty string`)
120
+ }
121
+ } else {
122
+ warn.push(`module.name is required`)
123
+ }
124
+
125
+ if ('description' in module) {
126
+ if (typeof module.description !== 'string' || module.description.length === 0) {
127
+ warn.push(`module.description must be a non-empty string`)
128
+ }
129
+ }
130
+
131
+ if (('roles' in module)) {
132
+ if (!Array.isArray(module.roles)) {
133
+ warn.push(`module.roles must be an array`)
134
+ } else if (!module.roles.every(role => typeof role === 'string' && role.length !== 0)) {
135
+ warn.push(`module.roles elements must be only non-empty strings`)
136
+ }
137
+ }
138
+
139
+ if (('execute' in module)) {
140
+ if (typeof module.execute !== 'function') {
141
+ warn.push(`module.execute must be a function`)
142
+ }
143
+ } else {
144
+ warn.push(`module.execute is required`)
145
+ }
146
+
147
+ if ('aliases' in module) {
148
+ if (!Array.isArray(module.aliases)) {
149
+ warn.push(`module.aliases must be an array`)
150
+ } else if (!module.aliases.every(alias => typeof alias === 'string' && alias.length !== 0)) {
151
+ warn.push(`module.aliases elements must be only non-empty strings`)
152
+ }
153
+ }
154
+
155
+ return warn;
156
+ }
157
+
158
+ _registerAliases(commandName, aliases) {
159
+ let success = 0
160
+ let failed = 0
161
+
162
+ for (const alias of aliases) {
163
+ const status = this._registerAlias(commandName, alias)
164
+ status ? success++ : failed++
165
+ }
166
+
167
+ return { success, failed }
168
+ }
169
+
170
+ _validateAlias(alias) {
171
+ if (this.aliases.has(alias)) {
172
+ const existingCommand = this.aliases.get(alias)
173
+ this.handlerError(`alias "${alias}" is already set to command "${existingCommand}"`)
174
+ return false
175
+ }
176
+
177
+ if (this.commands.has(alias)) {
178
+ this.handlerError(`alias "${alias}" conflicts with existing command name`)
179
+ return false
180
+ }
181
+
182
+ return true
183
+ }
184
+
185
+ _registerAlias(commandName, alias) {
186
+ const isValid = this._validateAlias(alias)
187
+ if (!isValid) return false
188
+
189
+ this.aliases.set(alias, commandName)
190
+ return true
191
+ }
192
+
193
+ async _registerCommands(modules) {
194
+ let success = 0
195
+ let failed = 0
196
+
197
+ for (const module of modules) {
198
+ const status = await this.registerCommand(module)
199
+ status ? success++ : failed++
200
+ }
201
+
202
+ return { success, failed }
203
+ }
204
+
205
+ async _unRegisterCommands(modules) {
206
+ let success = 0
207
+ let failed = 0
208
+
209
+ for (const module of modules) {
210
+ const status = this.unRegisterCommand(module.name)
211
+ status ? success++ : failed++
212
+ }
213
+
214
+ return { success, failed }
215
+ }
216
+
217
+ _getPreMiddlewares() {
218
+ return this.preMiddlewares.entries()
219
+ }
220
+
221
+ _getPostMiddlewares() {
222
+ return this.postMiddlewares.entries()
223
+ }
224
+
225
+ async _middlewareErrorHandler(middleware, bot, context, result = null) {
226
+ try {
227
+ const output = await middleware.fn(bot, context, result);
228
+ return output !== false;
229
+ } catch (error) {
230
+ this.logger.error(`${middleware.type} Middleware - ${middleware.name}`, error.message)
231
+ return false
232
+ }
233
+ }
234
+
235
+ async _runPreMiddleware(bot, context) {
236
+ const pres = this._getPreMiddlewares()
237
+ for (const [_, pre] of pres) {
238
+ const proceed = await this._middlewareErrorHandler(pre, bot, context)
239
+ if (!proceed) return false
240
+ }
241
+ return true;
242
+ }
243
+
244
+ async _runPostMiddleware(bot, context, result) {
245
+ const posts = this._getPostMiddlewares()
246
+ for (const [_, post] of posts) {
247
+ await this._middlewareErrorHandler(post, bot, context, result)
248
+ }
249
+ }
250
+
251
+ _validateMiddleware(name, fn) {
252
+ if (!name || typeof name !== 'string') {
253
+ this.handlerError(`name must be a non-empty string`)
254
+ return false
255
+ }
256
+
257
+ if (!fn || typeof fn !== 'function') {
258
+ this.handlerError(`fn must be a function`)
259
+ return false
260
+ }
261
+
262
+ return true
263
+ }
264
+
265
+ _validateContext(context) {
266
+ if (!('user' in context)) {
267
+ this.handlerError(`context.user must be provided.`)
268
+ return false
269
+ }
270
+
271
+ if (typeof context.user !== 'object') {
272
+ this.handlerError(`context.user must be an object.`)
273
+ return false
274
+ }
275
+
276
+ if (!('id' in context.user)) {
277
+ this.handlerError(`context.user.id must be provided.`)
278
+ return false
279
+ }
280
+
281
+ if (typeof context.user.id !== 'string' || context.user.id.trim() === '') {
282
+ this.handlerError(`context.user.id must be a non-empty string.`)
283
+ return false
284
+ }
285
+
286
+ if (!('username' in context.user)) {
287
+ this.handlerError(`context.user.username must be provided.`)
288
+ return false
289
+ }
290
+
291
+ if (typeof context.user.username !== 'string' || context.user.username.trim() === '') {
292
+ this.handlerError(`context.user.username must be a non-empty string.`)
293
+ return false
294
+ }
295
+
296
+ return true
297
+ }
298
+
299
+ _roleCheck(bot, user, module) {
300
+ const commandRoles = module.roles
301
+ if (!commandRoles) return true
302
+
303
+ return commandRoles.some(requiredRole => {
304
+ const hasRequiredRole = bot.utils.roles.hasRole(requiredRole, user.id)
305
+ return hasRequiredRole
306
+ })
307
+ }
308
+
309
+ async _executeCommand(user, module, context) {
310
+ const eligible = this._roleCheck(this.bot, user, module);
311
+ if (eligible) {
312
+ try {
313
+ const proceed = await this._runPreMiddleware(this.bot, context)
314
+ if (!proceed) return
315
+
316
+ const result = await module.execute(this.bot, context);
317
+ await this._runPostMiddleware(this.bot, context, result ?? { success: true });
318
+ } catch (error) {
319
+ this.logger.error(`Command ${module.filePath}`, error.message)
320
+ }
321
+ }
322
+ }
323
+
324
+ preMiddleware(name, fn) {
325
+ const status = this._validateMiddleware(name, fn)
326
+
327
+ if (!status) return
328
+
329
+ if (this.preMiddlewares.has(name)) {
330
+ this.logger.warn(`Pre middleware "${name}" already exists and will be overwritten`)
331
+ }
332
+
333
+ this.preMiddlewares.set(name, { name, fn, type: 'Pre' })
334
+ }
335
+
336
+ postMiddleware(name, fn) {
337
+ const status = this._validateMiddleware(name, fn)
338
+
339
+ if (!status) return
340
+
341
+ if (this.postMiddlewares.has(name)) {
342
+ this.logger.warn(`Post middleware "${name}" already exists and will be overwritten`)
343
+ }
344
+
345
+ this.postMiddlewares.set(name, { name, fn, type: 'Post' })
346
+ }
347
+
348
+ async registerCommand(module) {
349
+ const warns = this._validateModule(module)
350
+ if (warns.length > 0) {
351
+ this._buildValidationWarning(module, warns)
352
+ return false
353
+ }
354
+
355
+ const { name, aliases } = module
356
+
357
+ if (this.commands.has(name)) {
358
+ return false
359
+ }
360
+
361
+ if (aliases) this._registerAliases(name, aliases)
362
+
363
+ this.commands.set(name, module)
364
+ return true
365
+ }
366
+
367
+ async unRegisterCommand(commandName) {
368
+ if (!this.commands.has(commandName)) {
369
+ return false
370
+ }
371
+
372
+ const module = this.commands.get(commandName)
373
+
374
+ if (module.aliases) {
375
+ module.aliases.forEach(alias => this.aliases.delete(alias))
376
+ }
377
+
378
+ return this.commands.delete(commandName)
379
+ }
380
+
381
+
382
+ async handle(commandName, context = {}) {
383
+ if (!this._validateContext(context)) {
384
+ return;
385
+ }
386
+
387
+ const module = this.commands.get(commandName) ||
388
+ this.commands.get(this.aliases.get(commandName));
389
+
390
+ if (!module) return;
391
+
392
+ await this._executeCommand(context.user, module, context);
393
+ }
394
+ }
395
+
396
+ module.exports = CommandHandler
@@ -153,8 +153,76 @@ type TipFilter = (sender: Sender, receiver: Receiver, currency: Currency) => boo
153
153
  */
154
154
  type MovementFilter = (user: User, position: Position | null, anchor: AnchorPosition | null) => boolean;
155
155
 
156
+ /**
157
+ * Function type for middleware that executes BEFORE command execution
158
+ *
159
+ * Use cases:
160
+ * - Authentication/authorization checks (return false to deny access)
161
+ * - Input validation and sanitization
162
+ * - Rate limiting and cooldown enforcement
163
+ * - Logging access attempts
164
+ * - Setting up command prerequisites
165
+ *
166
+ * Return values:
167
+ * - `true` or `undefined`: Command continues to execute normally
168
+ * - `false`: Command execution is halted immediately
169
+ * - Thrown errors: Caught by middleware handler, command execution halted
170
+ *
171
+ * @param bot - Highrise bot instance for API interactions
172
+ * @param context - Command execution context containing user data and parameters
173
+ * @returns Boolean indicating whether to proceed with command execution
174
+ *
175
+ * @example
176
+ * // Admin-only command protection
177
+ * preMiddleware('admin-check', async (bot, context) => {
178
+ * const result = await bot.room.privilege.isModerator(context.user.id);
179
+ * if (!result.success || !result.isModerator) {
180
+ * await bot.whisper.send(context.user.id, 'Admin access required');
181
+ * return false; // Block command execution
182
+ * }
183
+ * return true; // Allow command to run
184
+ * })
185
+ */
186
+ type preMiddlewareFunction = (bot: Highrise, context: CommandContext) => boolean | void;
156
187
 
157
- interface User {
188
+ /**
189
+ * Function type for middleware that executes AFTER command execution
190
+ *
191
+ * Use cases:
192
+ * - Processing command results and responses
193
+ * - Logging successful command outcomes
194
+ * - Analytics and metrics collection
195
+ * - Cleanup operations
196
+ * - Audit trail maintenance
197
+ * - Error handling for command results
198
+ *
199
+ * Return values:
200
+ * - Any value (`true`, `false`, `undefined`): No impact on execution flow
201
+ * (post-middleware always runs after command completes)
202
+ * - Thrown errors: Caught by middleware handler, doesn't affect original command result
203
+ *
204
+ * @param bot - Highrise bot instance for API interactions
205
+ * @param context - Command execution context containing user data and parameters
206
+ * @param result - Output returned by the executed command (may be undefined)
207
+ * @returns (Optional) No functional impact on execution flow
208
+ *
209
+ * @example
210
+ * // Command usage analytics
211
+ * postMiddleware('analytics', async (bot, context, result) => {
212
+ * // Log successful commands to channel
213
+ * if (result?.success) {
214
+ * await bot.channel.send([
215
+ * 'command-execution',
216
+ * context.commandName
217
+ * ], JSON.stringify({
218
+ * userId: context.user.id,
219
+ * command: context.commandName,
220
+ * timestamp: Date.now()
221
+ * }));
222
+ * }
223
+ * })
224
+ */
225
+ type postMiddlewareFunction = (bot: Highrise, context: CommandContext, result?: any) => boolean | void;interface User {
158
226
  id: string;
159
227
  username: string;
160
228
  }
@@ -756,6 +824,34 @@ interface CooldownManagerStats {
756
824
  memoryUsage: CooldownMemoryUsage;
757
825
  }
758
826
 
827
+ /**
828
+ * Context passed to command handlers containing user and additional data
829
+ */
830
+ interface CommandContext {
831
+ user: User; // Using SDK's User type
832
+ args?: string[];
833
+ message?: string;
834
+ [key: string]: any;
835
+ }
836
+
837
+ /**
838
+ * Command module definition
839
+ */
840
+ interface CommandModule {
841
+ /** Unique command name */
842
+ name: string;
843
+ /** Optional command description */
844
+ description?: string;
845
+ /** Required roles for command access */
846
+ roles?: string[];
847
+ /** Command execution function */
848
+ execute: (bot: Highrise, context: CommandContext) => Promise<any>;
849
+ /** Alternative names for the command */
850
+ aliases?: string[];
851
+ /** Additional properties */
852
+ [key: string]: any;
853
+ }
854
+
759
855
  interface Job {
760
856
  id: string;
761
857
  interval: number;
@@ -4511,7 +4607,150 @@ declare class Logger {
4511
4607
  debug(method: string, message: string, data?: any): void;
4512
4608
  }
4513
4609
 
4610
+
4611
+
4612
+ /**
4613
+ * Handles bot commands with support for modules, aliases, and middleware
4614
+ */
4615
+ declare class CommandHandler {
4616
+ /**
4617
+ * Creates a new command handler
4618
+ * @param bot - Highrise Bot instance
4619
+ * @param relativeDir - Directory containing command modules
4620
+ * @param options - Configuration options
4621
+ */
4622
+ constructor(
4623
+ bot: Highrise,
4624
+ relativeDir: string,
4625
+ options?: {
4626
+ /** Auto-load command modules on initialization (default: true) */
4627
+ autoLoad?: boolean;
4628
+ }
4629
+ );
4630
+
4631
+ /**
4632
+ * Handles a command execution
4633
+ * @param commandName - Name or alias of the command to execute
4634
+ * @param context - Execution context with user information
4635
+ * @example
4636
+ * const [command, ...args] = message.trim().split(/\s+/);
4637
+ * let context = {
4638
+ * args: args,
4639
+ * user
4640
+ * }
4641
+ *
4642
+ * const roomCommand = bot.utils.command(`./src/commands/`)
4643
+ *
4644
+ * roomCommand.handle(command, context)
4645
+ */
4646
+ handle(commandName: string, context?: CommandContext): Promise<void>;
4647
+
4648
+ /**
4649
+ * Registers middleware to run before command execution
4650
+ *
4651
+ * Use cases:
4652
+ * - Authentication/authorization checks (return false to deny access)
4653
+ * - Input validation and sanitization
4654
+ * - Rate limiting and cooldown enforcement
4655
+ * - Logging access attempts
4656
+ * - Setting up command prerequisites
4657
+ *
4658
+ * Return values:
4659
+ * - `true` or `undefined`: Command continues to execute normally
4660
+ * - `false`: Command execution is halted immediately
4661
+ * - Thrown errors: Caught by middleware handler, command execution halted
4662
+ *
4663
+ * @param name - Middleware identifier for reference and debugging
4664
+ * @param fn - Middleware function that receives bot instance and context
4665
+ * @example
4666
+ * const roomCommand = bot.utils.command(`./src/commands`)
4667
+ *
4668
+ * roomCommand.preMiddleware('auth', async (bot, context) => {
4669
+ * if (!context.user.isAdmin) {
4670
+ * return false; // Prevent command execution
4671
+ * }
4672
+ * return true;
4673
+ * });
4674
+ */
4675
+ preMiddleware(name: string, fn: preMiddlewareFunction): void;
4676
+
4677
+ /**
4678
+ * Registers middleware to run after command execution
4679
+ *
4680
+ * Use cases:
4681
+ * - Processing command results and responses
4682
+ * - Logging successful command outcomes
4683
+ * - Analytics and metrics collection
4684
+ * - Cleanup operations
4685
+ * - Audit trail maintenance
4686
+ * - Error handling for command results
4687
+ *
4688
+ * Return values:
4689
+ * - Any value (`true`, `false`, `undefined`): No impact on execution flow
4690
+ * (post-middleware always runs after command completes)
4691
+ * - Thrown errors: Caught by middleware handler, doesn't affect original command result
4692
+ *
4693
+ * @param name - Middleware identifier for reference and debugging
4694
+ * @param fn - Middleware function that processes bot, context, and command result
4695
+ * The result parameter contains the output returned by the executed command
4696
+ * @example
4697
+ * const roomCommand = bot.utils.command(`./src/commands`)
4698
+ *
4699
+ * roomCommand.postMiddleware('log', async (bot, context, result) => {
4700
+ * console.log(`Command executed: ${result?.success}`);
4701
+ * });
4702
+ */
4703
+ postMiddleware(name: string, fn: postMiddlewareFunction): void;
4704
+
4705
+ /**
4706
+ * Registers a command module
4707
+ * @param module - Command configuration
4708
+ * @returns Success status
4709
+ * @example
4710
+ * const success = handler.register({
4711
+ * name: 'ping',
4712
+ * description: 'Responds with pong',
4713
+ * execute: async (bot, ctx) => {
4714
+ * return { message: 'pong' };
4715
+ * }
4716
+ * });
4717
+ */
4718
+ register(module: CommandModule): boolean;
4719
+
4720
+ /**
4721
+ * Unregisters a command by name
4722
+ * @param commandName - Name of command to remove
4723
+ * @returns Success status
4724
+ * @example
4725
+ * const success = handler.unregister('ping');
4726
+ */
4727
+ unregister(commandName: string): boolean;
4728
+ }
4729
+
4730
+ /**
4731
+ * Function type for creating CommandHandler instances
4732
+ */
4733
+ type CommandHandlerFactory = (
4734
+ relativeDir: string,
4735
+ options?: {
4736
+ /** Auto-load command modules on initialization (default: true) */
4737
+ autoLoad?: boolean;
4738
+ }
4739
+ ) => CommandHandler;
4740
+
4514
4741
  declare class BotUtils {
4742
+ /**
4743
+ * Factory function to create CommandHandler instances
4744
+ * @param relativeDir - Directory containing command modules
4745
+ * @param options - Configuration options
4746
+ * @returns New CommandHandler instance
4747
+ * @example
4748
+ * const command = bot.utils.command('./src/commands/room');
4749
+ * await command.handle('help', { user: { id: '123', username: 'john' } });
4750
+ */
4751
+ command: CommandHandlerFactory;
4752
+
4753
+ // ... other properties remain the same ...
4515
4754
  /**
4516
4755
  * Logger instance for structured, color-coded logging throughout the application
4517
4756
  * Provides different log levels (SUCCESS, ERROR, WARN, INFO, DEBUG) with timestamps and method tracking