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 +1 -1
- package/src/classes/Actions/Awaiter.js +30 -142
- package/src/classes/Actions/Utils.js +7 -2
- package/src/classes/Handlers/CommandHandler.js +391 -0
- package/src/classes/Managers/Networking/MessageHandler.js +1 -0
- package/src/classes/Managers/RoleManager.js +3 -3
- package/typings/index.d.ts +243 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "highrise.bot",
|
|
3
|
-
"version": "1.
|
|
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 =
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
try {
|
|
138
|
-
this._validateEventArguments(eventType, eventArgs);
|
|
68
|
+
const awaiters = this._pendingAwaiters[eventType];
|
|
69
|
+
if (!awaiters) return;
|
|
139
70
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
|
@@ -37,7 +37,7 @@ class RolesManager {
|
|
|
37
37
|
this._startWebApiSync();
|
|
38
38
|
this._startAutoSave();
|
|
39
39
|
this.initialized = true;
|
|
40
|
-
this.logger.success('RolesManager', '
|
|
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) {
|
package/typings/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|