highrise.bot 1.0.3 → 1.1.1

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.3",
3
+ "version": "1.1.1",
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,200 +1,88 @@
1
1
  class AwaitClass {
2
2
  constructor(ws) {
3
- if (!ws) {
4
- throw new Error('ws instance is required');
5
- }
3
+ if (!ws) throw new Error('ws instance is required');
6
4
  this.ws = ws;
7
- this._pendingAwaiters = new Map();
5
+ this._pendingAwaiters = {
6
+ chat: new Map(),
7
+ whisper: new Map(),
8
+ direct: new Map(),
9
+ tip: new Map(),
10
+ movement: new Map()
11
+ };
8
12
  }
9
13
 
10
14
  chat(filter = () => true, timeout = 30000, maxToCollect = 1, uniqueUsers = false) {
11
- if (!this._validateParameters('chat', filter, timeout, maxToCollect)) {
12
- return Promise.resolve([]);
13
- }
14
-
15
15
  return this._createAwaiter('chat', filter, timeout, maxToCollect, uniqueUsers);
16
16
  }
17
17
 
18
18
  whisper(filter = () => true, timeout = 30000, maxToCollect = 1, uniqueUsers = false) {
19
- if (!this._validateParameters('whisper', filter, timeout, maxToCollect)) {
20
- return Promise.resolve([]);
21
- }
22
-
23
19
  return this._createAwaiter('whisper', filter, timeout, maxToCollect, uniqueUsers);
24
20
  }
25
21
 
26
22
  direct(filter = () => true, timeout = 30000, maxToCollect = 1, uniqueUsers = false) {
27
- if (!this._validateParameters('direct', filter, timeout, maxToCollect)) {
28
- return Promise.resolve([]);
29
- }
30
23
  return this._createAwaiter('direct', filter, timeout, maxToCollect, uniqueUsers);
31
24
  }
32
25
 
33
26
  tip(filter = () => true, timeout = 30000, maxToCollect = 1, uniqueUsers = false) {
34
- if (!this._validateParameters('tip', filter, timeout, maxToCollect)) {
35
- return Promise.resolve([]);
36
- }
37
27
  return this._createAwaiter('tip', filter, timeout, maxToCollect, uniqueUsers);
38
28
  }
39
29
 
40
30
  movement(filter = () => true, timeout = 30000, maxToCollect = 1, uniqueUsers = false) {
41
- if (!this._validateParameters('movement', filter, timeout, maxToCollect)) {
42
- return Promise.resolve([]);
43
- }
44
31
  return this._createAwaiter('movement', filter, timeout, maxToCollect, uniqueUsers);
45
32
  }
46
33
 
47
- _validateParameters(method, filter, timeout, maxToCollect) {
48
- try {
49
- if (typeof filter !== 'function') {
50
- throw new TypeError(`Filter must be a function`);
51
- }
52
-
53
- if (typeof timeout !== 'number') {
54
- throw new TypeError(`Timeout must be a number`);
55
- }
56
-
57
- if (timeout < 0) {
58
- throw new RangeError(`Timeout cannot be negative`);
59
- }
60
-
61
- if (typeof maxToCollect !== 'number') {
62
- throw new TypeError(`maxToCollect must be a number`);
63
- }
64
-
65
- if (!Number.isInteger(maxToCollect)) {
66
- throw new TypeError(`maxToCollect must be an integer`);
67
- }
68
-
69
- if (maxToCollect < 1) {
70
- throw new RangeError(`maxToCollect must be at least 1`);
71
- }
72
-
73
- if (maxToCollect > 1000) {
74
- throw new RangeError(`maxToCollect cannot exceed 1000`);
75
- }
76
-
77
- if (!this.ws.isConnected || !this.ws.isConnected()) {
78
- throw new Error(`WebSocket is not connected to Highrise`);
79
- }
80
-
81
- return true;
82
-
83
- } catch (error) {
84
- if (error instanceof TypeError) {
85
- this.ws._logger.error(`await.${method}`, `TypeError: ${error.message}`, { timeout, maxToCollect }, error);
86
- } else if (error instanceof RangeError) {
87
- this.ws._logger.error(`await.${method}`, `RangeError: ${error.message}`, { timeout, maxToCollect }, error);
88
- } else {
89
- this.ws._logger.error(`await.${method}`, error.message, { timeout, maxToCollect }, error);
90
- }
91
- return false;
34
+ _createAwaiter(eventType, filter, timeout, maxToCollect, uniqueUsers) {
35
+ if (typeof filter !== 'function' || typeof timeout !== 'number' || timeout < 0 ||
36
+ !Number.isInteger(maxToCollect) || maxToCollect < 1 || maxToCollect > 1000) {
37
+ return Promise.resolve([]);
92
38
  }
93
- }
94
39
 
95
- _createAwaiter(eventType, filter, timeout, maxToCollect, uniqueUsers = false) {
96
- return new Promise((resolve, reject) => {
40
+ return new Promise((resolve) => {
97
41
  const awaiterId = Symbol('awaiter');
98
42
  const collected = [];
99
- const seenUsers = new Set();
100
- let timeoutId;
43
+ const seenUsers = uniqueUsers ? new Set() : null;
101
44
 
102
45
  const cleanup = () => {
103
46
  clearTimeout(timeoutId);
104
- this._pendingAwaiters.delete(awaiterId);
47
+ this._pendingAwaiters[eventType].delete(awaiterId);
105
48
  };
106
49
 
107
50
  const awaiter = {
108
- eventType,
109
51
  filter,
110
52
  maxToCollect,
111
53
  collected,
112
- uniqueUsers,
113
54
  seenUsers,
114
55
  resolve: (results) => {
115
56
  cleanup();
116
57
  resolve(results);
117
- },
118
- reject: (error) => {
119
- cleanup();
120
- reject(error);
121
58
  }
122
59
  };
123
60
 
124
- this._pendingAwaiters.set(awaiterId, awaiter);
61
+ this._pendingAwaiters[eventType].set(awaiterId, awaiter);
125
62
 
126
- if (timeout > 0) {
127
- timeoutId = setTimeout(() => {
128
- awaiter.resolve(collected);
129
- }, timeout);
130
- }
63
+ const timeoutId = timeout > 0 ? setTimeout(() => awaiter.resolve(collected), timeout) : null;
131
64
  });
132
65
  }
133
66
 
134
67
  _processEvent(eventType, ...eventArgs) {
135
- for (const [awaiterId, awaiter] of this._pendingAwaiters) {
136
- if (awaiter.eventType === eventType) {
137
- try {
138
- this._validateEventArguments(eventType, eventArgs);
68
+ const awaiters = this._pendingAwaiters[eventType];
69
+ if (!awaiters) return;
139
70
 
140
- if (awaiter.uniqueUsers && eventArgs[0]?.id) {
141
- if (awaiter.seenUsers.has(eventArgs[0].id)) {
142
- continue;
143
- }
144
- awaiter.seenUsers.add(eventArgs[0].id);
145
- }
146
-
147
- if (awaiter.filter(...eventArgs)) {
148
- awaiter.collected.push(eventArgs);
71
+ for (const awaiter of awaiters.values()) {
72
+ try {
73
+ if (awaiter.seenUsers && eventArgs[0]?.id) {
74
+ if (awaiter.seenUsers.has(eventArgs[0].id)) continue;
75
+ awaiter.seenUsers.add(eventArgs[0].id);
76
+ }
149
77
 
150
- if (awaiter.collected.length >= awaiter.maxToCollect) {
151
- awaiter.resolve(awaiter.collected);
152
- }
78
+ if (awaiter.filter(...eventArgs)) {
79
+ awaiter.collected.push(eventArgs);
80
+ if (awaiter.collected.length >= awaiter.maxToCollect) {
81
+ awaiter.resolve(awaiter.collected);
153
82
  }
154
- } catch (error) {
155
- this.ws._logger.error(`await.${eventType}`, `Filter error: ${error.message}`)
156
83
  }
157
- }
158
- }
159
- }
160
-
161
- _validateEventArguments(eventType, eventArgs) {
162
- const validators = {
163
- chat: (args) => {
164
- if (args.length !== 2) throw new TypeError('Chat event requires 2 arguments (user, message)');
165
- if (!args[0] || typeof args[0].id !== 'string') throw new TypeError('Invalid user object in chat event');
166
- if (typeof args[1] !== 'string') throw new TypeError('Invalid message in chat event');
167
- },
168
- whisper: (args) => {
169
- if (args.length !== 2) throw new TypeError('Whisper event requires 2 arguments (user, message)');
170
- if (!args[0] || typeof args[0].id !== 'string') throw new TypeError('Invalid user object in whisper event');
171
- if (typeof args[1] !== 'string') throw new TypeError('Invalid message in whisper event');
172
- },
173
- direct: (args) => {
174
- if (args.length !== 3) throw new TypeError('Direct event requires 3 arguments (user, message, conversation)');
175
- if (!args[0] || typeof args[0].id !== 'string') throw new TypeError('Invalid user object in direct event');
176
- if (typeof args[1] !== 'string') throw new TypeError('Invalid message in direct event');
177
- if (!args[2] || typeof args[2].id !== 'string') throw new TypeError('Invalid conversation object in direct event');
178
- },
179
- tip: (args) => {
180
- if (args.length !== 3) throw new TypeError('Tip event requires 3 arguments (sender, receiver, currency)');
181
- if (!args[0] || typeof args[0].id !== 'string') throw new TypeError('Invalid sender in tip event');
182
- if (!args[1] || typeof args[1].id !== 'string') throw new TypeError('Invalid receiver in tip event');
183
- if (!args[2] || typeof args[2].amount !== 'number') throw new TypeError('Invalid currency in tip event');
184
- },
185
- movement: (args) => {
186
- if (args.length !== 3) throw new TypeError('Movement event requires 3 arguments (user, position, anchor)');
187
- if (!args[0] || typeof args[0].id !== 'string') throw new TypeError('Invalid user object in movement event');
188
- if (!args[1] || typeof args[1].x !== 'number') throw new TypeError('Invalid position in movement event');
189
- if (args[2] !== null && (!args[2] || typeof args[2].entity_id !== 'string')) throw new TypeError('Invalid anchor in movement event');
190
- }
191
- };
192
-
193
- if (validators[eventType]) {
194
- try {
195
- validators[eventType](eventArgs);
196
84
  } catch (error) {
197
- this.ws._logger.error(`await.${eventType}`, error.message)
85
+ // Filter error - continue to next awaiter
198
86
  }
199
87
  }
200
88
  }
@@ -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,391 @@
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
+ return true
292
+ }
293
+
294
+ _roleCheck(bot, user, module) {
295
+ const commandRoles = module.roles
296
+ if (!commandRoles) return true
297
+
298
+ return commandRoles.some(requiredRole => {
299
+ const hasRequiredRole = bot.utils.roles.hasRole(requiredRole, user.id)
300
+ return hasRequiredRole
301
+ })
302
+ }
303
+
304
+ async _executeCommand(user, module, context) {
305
+ const eligible = this._roleCheck(this.bot, user, module);
306
+ if (eligible) {
307
+ try {
308
+ const proceed = await this._runPreMiddleware(this.bot, context)
309
+ if (!proceed) return
310
+
311
+ const result = await module.execute(this.bot, context);
312
+ await this._runPostMiddleware(this.bot, context, result ?? { success: true });
313
+ } catch (error) {
314
+ this.logger.error(`Command ${module.filePath}`, error.message)
315
+ }
316
+ }
317
+ }
318
+
319
+ preMiddleware(name, fn) {
320
+ const status = this._validateMiddleware(name, fn)
321
+
322
+ if (!status) return
323
+
324
+ if (this.preMiddlewares.has(name)) {
325
+ this.logger.warn(`Pre middleware "${name}" already exists and will be overwritten`)
326
+ }
327
+
328
+ this.preMiddlewares.set(name, { name, fn, type: 'Pre' })
329
+ }
330
+
331
+ postMiddleware(name, fn) {
332
+ const status = this._validateMiddleware(name, fn)
333
+
334
+ if (!status) return
335
+
336
+ if (this.postMiddlewares.has(name)) {
337
+ this.logger.warn(`Post middleware "${name}" already exists and will be overwritten`)
338
+ }
339
+
340
+ this.postMiddlewares.set(name, { name, fn, type: 'Post' })
341
+ }
342
+
343
+ async registerCommand(module) {
344
+ const warns = this._validateModule(module)
345
+ if (warns.length > 0) {
346
+ this._buildValidationWarning(module, warns)
347
+ return false
348
+ }
349
+
350
+ const { name, aliases } = module
351
+
352
+ if (this.commands.has(name)) {
353
+ return false
354
+ }
355
+
356
+ if (aliases) this._registerAliases(name, aliases)
357
+
358
+ this.commands.set(name, module)
359
+ return true
360
+ }
361
+
362
+ async unRegisterCommand(commandName) {
363
+ if (!this.commands.has(commandName)) {
364
+ return false
365
+ }
366
+
367
+ const module = this.commands.get(commandName)
368
+
369
+ if (module.aliases) {
370
+ module.aliases.forEach(alias => this.aliases.delete(alias))
371
+ }
372
+
373
+ return this.commands.delete(commandName)
374
+ }
375
+
376
+
377
+ async handle(commandName, context = {}) {
378
+ if (!this._validateContext(context)) {
379
+ return;
380
+ }
381
+
382
+ const module = this.commands.get(commandName) ||
383
+ this.commands.get(this.aliases.get(commandName));
384
+
385
+ if (!module) return;
386
+
387
+ await this._executeCommand(context.user, module, context);
388
+ }
389
+ }
390
+
391
+ module.exports = CommandHandler
@@ -17,6 +17,7 @@ class MessageHandler {
17
17
  setImmediate(() => {
18
18
  try {
19
19
  const message = JSON.parse(data.toString());
20
+
20
21
  if (this.validateMessageStructure(message)) {
21
22
  this._processMessage(message);
22
23
  } else {
@@ -37,7 +37,7 @@ class RolesManager {
37
37
  this._startWebApiSync();
38
38
  this._startAutoSave();
39
39
  this.initialized = true;
40
- this.logger.success('RolesManager', 'Ready! Auto-syncing every 5 minutes.');
40
+ this.logger.success('RolesManager', 'Ready! Auto-syncing every 5 minutes.');
41
41
  } catch (error) {
42
42
  this.logger.error('RolesManager', 'Setup failed', error);
43
43
  this._createCustomRoles();
@@ -131,8 +131,8 @@ class RolesManager {
131
131
  }
132
132
 
133
133
  _startWebApiSync() {
134
- this.syncWithWebApi()
135
- setInterval(() => this.syncWithWebApi(), this.config.webapiSyncInterval);
134
+ // this.syncWithWebApi()
135
+ // setInterval(() => this.syncWithWebApi(), this.config.webapiSyncInterval);
136
136
  }
137
137
 
138
138
  createRole(roleName) {
@@ -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,36 @@ 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;
832
+ args?: string[];
833
+ message?: string;
834
+ conversation?: Conversation
835
+ /** Additional properties */
836
+ [key: string]: any;
837
+ }
838
+
839
+ /**
840
+ * Command module definition
841
+ */
842
+ interface CommandModule {
843
+ /** Unique command name */
844
+ name: string;
845
+ /** Optional command description */
846
+ description?: string;
847
+ /** Required roles for command access */
848
+ roles?: string[];
849
+ /** Command execution function */
850
+ execute: (bot: Highrise, context: CommandContext) => Promise<any>;
851
+ /** Alternative names for the command */
852
+ aliases?: string[];
853
+ /** Additional properties */
854
+ [key: string]: any;
855
+ }
856
+
759
857
  interface Job {
760
858
  id: string;
761
859
  interval: number;
@@ -3265,7 +3363,7 @@ declare class AwaitClass {
3265
3363
  * );
3266
3364
  * ```
3267
3365
  */
3268
- movement(filter?: MovementFilter, timeout?: number, maxToCollect?: number, uniqueUsers?: boolean): Promise<[User, Position, AnchorPosition | null][] | []>;
3366
+ movement(filter?: MovementFilter, timeout?: number, maxToCollect?: number, uniqueUsers?: boolean): Promise<[User, Position | null, AnchorPosition | null][] | []>;
3269
3367
  }
3270
3368
 
3271
3369
  declare class WebApi {
@@ -4511,7 +4609,150 @@ declare class Logger {
4511
4609
  debug(method: string, message: string, data?: any): void;
4512
4610
  }
4513
4611
 
4612
+
4613
+
4614
+ /**
4615
+ * Handles bot commands with support for modules, aliases, and middleware
4616
+ */
4617
+ declare class CommandHandler {
4618
+ /**
4619
+ * Creates a new command handler
4620
+ * @param bot - Highrise Bot instance
4621
+ * @param relativeDir - Directory containing command modules
4622
+ * @param options - Configuration options
4623
+ */
4624
+ constructor(
4625
+ bot: Highrise,
4626
+ relativeDir: string,
4627
+ options?: {
4628
+ /** Auto-load command modules on initialization (default: true) */
4629
+ autoLoad?: boolean;
4630
+ }
4631
+ );
4632
+
4633
+ /**
4634
+ * Handles a command execution
4635
+ * @param commandName - Name or alias of the command to execute
4636
+ * @param context - Execution context with user information
4637
+ * @example
4638
+ * const [command, ...args] = message.trim().split(/\s+/);
4639
+ * let context = {
4640
+ * args: args,
4641
+ * user
4642
+ * }
4643
+ *
4644
+ * const roomCommand = bot.utils.command(`./src/commands/`)
4645
+ *
4646
+ * roomCommand.handle(command, context)
4647
+ */
4648
+ handle(commandName: string, context?: CommandContext): Promise<void>;
4649
+
4650
+ /**
4651
+ * Registers middleware to run before command execution
4652
+ *
4653
+ * Use cases:
4654
+ * - Authentication/authorization checks (return false to deny access)
4655
+ * - Input validation and sanitization
4656
+ * - Rate limiting and cooldown enforcement
4657
+ * - Logging access attempts
4658
+ * - Setting up command prerequisites
4659
+ *
4660
+ * Return values:
4661
+ * - `true` or `undefined`: Command continues to execute normally
4662
+ * - `false`: Command execution is halted immediately
4663
+ * - Thrown errors: Caught by middleware handler, command execution halted
4664
+ *
4665
+ * @param name - Middleware identifier for reference and debugging
4666
+ * @param fn - Middleware function that receives bot instance and context
4667
+ * @example
4668
+ * const roomCommand = bot.utils.command(`./src/commands`)
4669
+ *
4670
+ * roomCommand.preMiddleware('auth', async (bot, context) => {
4671
+ * if (!context.user.isAdmin) {
4672
+ * return false; // Prevent command execution
4673
+ * }
4674
+ * return true;
4675
+ * });
4676
+ */
4677
+ preMiddleware(name: string, fn: preMiddlewareFunction): void;
4678
+
4679
+ /**
4680
+ * Registers middleware to run after command execution
4681
+ *
4682
+ * Use cases:
4683
+ * - Processing command results and responses
4684
+ * - Logging successful command outcomes
4685
+ * - Analytics and metrics collection
4686
+ * - Cleanup operations
4687
+ * - Audit trail maintenance
4688
+ * - Error handling for command results
4689
+ *
4690
+ * Return values:
4691
+ * - Any value (`true`, `false`, `undefined`): No impact on execution flow
4692
+ * (post-middleware always runs after command completes)
4693
+ * - Thrown errors: Caught by middleware handler, doesn't affect original command result
4694
+ *
4695
+ * @param name - Middleware identifier for reference and debugging
4696
+ * @param fn - Middleware function that processes bot, context, and command result
4697
+ * The result parameter contains the output returned by the executed command
4698
+ * @example
4699
+ * const roomCommand = bot.utils.command(`./src/commands`)
4700
+ *
4701
+ * roomCommand.postMiddleware('log', async (bot, context, result) => {
4702
+ * console.log(`Command executed: ${result?.success}`);
4703
+ * });
4704
+ */
4705
+ postMiddleware(name: string, fn: postMiddlewareFunction): void;
4706
+
4707
+ /**
4708
+ * Registers a command module
4709
+ * @param module - Command configuration
4710
+ * @returns Success status
4711
+ * @example
4712
+ * const success = handler.register({
4713
+ * name: 'ping',
4714
+ * description: 'Responds with pong',
4715
+ * execute: async (bot, ctx) => {
4716
+ * return { message: 'pong' };
4717
+ * }
4718
+ * });
4719
+ */
4720
+ register(module: CommandModule): boolean;
4721
+
4722
+ /**
4723
+ * Unregisters a command by name
4724
+ * @param commandName - Name of command to remove
4725
+ * @returns Success status
4726
+ * @example
4727
+ * const success = handler.unregister('ping');
4728
+ */
4729
+ unregister(commandName: string): boolean;
4730
+ }
4731
+
4732
+ /**
4733
+ * Function type for creating CommandHandler instances
4734
+ */
4735
+ type CommandHandlerFactory = (
4736
+ relativeDir: string,
4737
+ options?: {
4738
+ /** Auto-load command modules on initialization (default: true) */
4739
+ autoLoad?: boolean;
4740
+ }
4741
+ ) => CommandHandler;
4742
+
4514
4743
  declare class BotUtils {
4744
+ /**
4745
+ * Factory function to create CommandHandler instances
4746
+ * @param relativeDir - Directory containing command modules
4747
+ * @param options - Configuration options
4748
+ * @returns New CommandHandler instance
4749
+ * @example
4750
+ * const command = bot.utils.command('./src/commands/room');
4751
+ * await command.handle('help', { user: { id: '123', username: 'john' } });
4752
+ */
4753
+ command: CommandHandlerFactory;
4754
+
4755
+ // ... other properties remain the same ...
4515
4756
  /**
4516
4757
  * Logger instance for structured, color-coded logging throughout the application
4517
4758
  * Provides different log levels (SUCCESS, ERROR, WARN, INFO, DEBUG) with timestamps and method tracking