highrise.bot 1.0.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/LICENSE +21 -0
- package/README.md +86 -0
- package/index.js +13 -0
- package/package.json +29 -0
- package/src/classes/Actions/Awaiter.js +203 -0
- package/src/classes/Actions/Channel.js +327 -0
- package/src/classes/Actions/Direct.js +390 -0
- package/src/classes/Actions/Inventory.js +193 -0
- package/src/classes/Actions/Music.js +243 -0
- package/src/classes/Actions/Outfit.js +175 -0
- package/src/classes/Actions/Player.js +604 -0
- package/src/classes/Actions/Public.js +143 -0
- package/src/classes/Actions/Room.js +495 -0
- package/src/classes/Actions/Utils.js +77 -0
- package/src/classes/Actions/lib/AudioStreaming.js +695 -0
- package/src/classes/Caches/MovementCache.js +364 -0
- package/src/classes/Handlers/AxiosErrorHandler.js +68 -0
- package/src/classes/Handlers/ErrorHandler.js +65 -0
- package/src/classes/Handlers/EventHandlers.js +193 -0
- package/src/classes/Handlers/WebSocketHandlers.js +126 -0
- package/src/classes/Managers/CooldownManager.js +516 -0
- package/src/classes/Managers/DanceFloorManagers.js +609 -0
- package/src/classes/Managers/Helpers/CleanupManager.js +130 -0
- package/src/classes/Managers/Helpers/HighriseError.js +107 -0
- package/src/classes/Managers/Helpers/HighriseResponse.js +33 -0
- package/src/classes/Managers/Helpers/LoggerManager.js +171 -0
- package/src/classes/Managers/Helpers/MetricsManager.js +83 -0
- package/src/classes/Managers/Networking/ConnectionManager.js +253 -0
- package/src/classes/Managers/Networking/EventsManager.js +64 -0
- package/src/classes/Managers/Networking/KeepAliveManager.js +58 -0
- package/src/classes/Managers/Networking/MessageHandler.js +123 -0
- package/src/classes/Managers/Networking/Request.js +323 -0
- package/src/classes/Managers/RoleManager.js +322 -0
- package/src/classes/WebApi/Category/Grab.js +98 -0
- package/src/classes/WebApi/Category/Item.js +347 -0
- package/src/classes/WebApi/Category/Post.js +154 -0
- package/src/classes/WebApi/Category/Room.js +137 -0
- package/src/classes/WebApi/Category/User.js +88 -0
- package/src/classes/WebApi/webapi.js +52 -0
- package/src/constants/ErrorConstants.js +109 -0
- package/src/constants/TypesConstants.js +91 -0
- package/src/constants/WebSocketConstants.js +78 -0
- package/src/core/Highrise.js +192 -0
- package/src/core/HighriseWebsocket.js +242 -0
- package/src/utils/ConvertSvgToPng.js +51 -0
- package/src/utils/Job.js +130 -0
- package/src/utils/ModelPool.js +160 -0
- package/src/utils/Models.js +128 -0
- package/src/utils/versionCheck.js +27 -0
- package/src/validators/ConfigValidator.js +195 -0
- package/src/validators/ConnectionValidator.js +65 -0
- package/typings/index.d.ts +5042 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Highrise Bot SDK
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Highrise Bot, Highrise JavaScript SDK, Highrise TypeScript SDK, Highrise WebSocket API, Highrise Bot Framework, Highrise Bots, Highrise Development Tools
|
|
3
|
+
-->
|
|
4
|
+
|
|
5
|
+
# Highrise Bot SDK: Unofficial JavaScript/TypeScript SDK for Building Highrise Bots
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
## Build Advanced Highrise Bots with JavaScript
|
|
13
|
+
|
|
14
|
+
Highrise Bot SDK (`highrise.bot`) is the premier **unofficial TypeScript/JavaScript SDK** for creating **powerful automation bots** on the Highrise social platform. Whether you're building **chat moderators, game assistants, or interactive experiences**, our SDK provides **enterprise-grade reliability** with production-ready features.
|
|
15
|
+
|
|
16
|
+
### Core Capabilities
|
|
17
|
+
- **Real-time WebSocket Connection** - Stable connection with auto-reconnect
|
|
18
|
+
- **Complete Bot API Coverage** - All official Highrise bot endpoints implemented
|
|
19
|
+
- **Advanced Caching System** - Efficient user position tracking and spatial queries and more upcoming
|
|
20
|
+
- **Comprehensive Event Handling** - All major Highrise events supported
|
|
21
|
+
- **Web API Integration** - Access to user, room, item, post, grab data
|
|
22
|
+
|
|
23
|
+
### Performance Highlights
|
|
24
|
+
- **86%+ Memory Reduction** in position tracking using binary encoding
|
|
25
|
+
- **Sub-millisecond** spatial queries for user location
|
|
26
|
+
- **Automatic resource cleanup** and connection management
|
|
27
|
+
- **Object pooling** for reduced garbage collection
|
|
28
|
+
- **98% Memory leak free**
|
|
29
|
+
|
|
30
|
+
# Installation
|
|
31
|
+
```bash
|
|
32
|
+
npm install highrise.bot
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
# Quick Start
|
|
36
|
+
|
|
37
|
+
```javascript
|
|
38
|
+
const { Highrise } = require('highrise.bot');
|
|
39
|
+
|
|
40
|
+
// Create bot instance
|
|
41
|
+
const bot = new Highrise(
|
|
42
|
+
['ChatEvent', 'UserJoinedEvent', 'UserMovedEvent'], // Events to listen for
|
|
43
|
+
{
|
|
44
|
+
LoggerOptions: {
|
|
45
|
+
showTimestamp: true,
|
|
46
|
+
showMethodName: true,
|
|
47
|
+
colors: true
|
|
48
|
+
},
|
|
49
|
+
autoReconnect: true,
|
|
50
|
+
reconnectDelay: 5000
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Event handlers
|
|
55
|
+
bot.on('Ready', (metadata) => {
|
|
56
|
+
console.log(`Bot connected to room: ${metadata.room.room_name}`);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
bot.on('Chat', async (user, message) => {
|
|
60
|
+
if (message === '!hello') {
|
|
61
|
+
await bot.message.send(`Hello ${user.username}! 👋`);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
bot.on('UserJoined', async (user, position) => {
|
|
66
|
+
await bot.message.send(`Welcome to the room, ${user.username}! 🎉`);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Connect to Highrise
|
|
70
|
+
bot.login('your_64_character_bot_token', 'your_24_character_room_id');
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Documentation
|
|
74
|
+
|
|
75
|
+
For complete API documentation, check the TypeScript definitions in `index.d.ts` or visit our documentation site (will be available soon).
|
|
76
|
+
|
|
77
|
+
## Issues and Support
|
|
78
|
+
|
|
79
|
+
Found a bug or need help? Please send a message to me in highrise discord server @oqs0_ with:
|
|
80
|
+
- sdk version
|
|
81
|
+
- Error logs
|
|
82
|
+
- Steps to reproduce
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT License - Copyright (c) 2025 Yahya Ahmed
|
package/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/// <reference types="./typings/index.d.ts" />
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Highrise Bot SDK
|
|
6
|
+
* A HIghrise WebSocket SDK for Highrise bots
|
|
7
|
+
*
|
|
8
|
+
* @copyright 2025 Yahya Ahmed
|
|
9
|
+
* @license MIT
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
exports.Highrise = require('highrise.js/src/core/Highrise')
|
|
13
|
+
exports.WebApi = require('highrise.js/src/classes/WebApi/webapi').WebApi
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "highrise.bot",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Unofficial JavaScript SDK for the Highrise platform. Feature-complete WebSocket client with TypeScript support, built for production environments.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"highrise.bot",
|
|
7
|
+
"highrise",
|
|
8
|
+
"websocket",
|
|
9
|
+
"sdk",
|
|
10
|
+
"bot",
|
|
11
|
+
"typescript",
|
|
12
|
+
"spatial",
|
|
13
|
+
"cache",
|
|
14
|
+
"js"
|
|
15
|
+
],
|
|
16
|
+
"main": "./index.js",
|
|
17
|
+
"types": "./typings/index.d.ts",
|
|
18
|
+
"author": "Yahya Ahmed eheaahmed2000@gmail.com",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"type": "commonjs",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"axios": "^1.13.2",
|
|
23
|
+
"chalk": "^4.1.2",
|
|
24
|
+
"colors": "^1.4.0",
|
|
25
|
+
"sharp": "^0.34.5",
|
|
26
|
+
"user-agents": "^1.1.669",
|
|
27
|
+
"ws": "^8.18.3"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
class AwaitClass {
|
|
2
|
+
constructor(ws) {
|
|
3
|
+
if (!ws) {
|
|
4
|
+
throw new Error('ws instance is required');
|
|
5
|
+
}
|
|
6
|
+
this.ws = ws;
|
|
7
|
+
this._pendingAwaiters = new Map();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
chat(filter = () => true, timeout = 30000, maxToCollect = 1, uniqueUsers = false) {
|
|
11
|
+
if (!this._validateParameters('chat', filter, timeout, maxToCollect)) {
|
|
12
|
+
return Promise.resolve([]);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return this._createAwaiter('chat', filter, timeout, maxToCollect, uniqueUsers);
|
|
16
|
+
}
|
|
17
|
+
|
|
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
|
+
return this._createAwaiter('whisper', filter, timeout, maxToCollect, uniqueUsers);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
direct(filter = () => true, timeout = 30000, maxToCollect = 1, uniqueUsers = false) {
|
|
27
|
+
if (!this._validateParameters('direct', filter, timeout, maxToCollect)) {
|
|
28
|
+
return Promise.resolve([]);
|
|
29
|
+
}
|
|
30
|
+
return this._createAwaiter('direct', filter, timeout, maxToCollect, uniqueUsers);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
tip(filter = () => true, timeout = 30000, maxToCollect = 1, uniqueUsers = false) {
|
|
34
|
+
if (!this._validateParameters('tip', filter, timeout, maxToCollect)) {
|
|
35
|
+
return Promise.resolve([]);
|
|
36
|
+
}
|
|
37
|
+
return this._createAwaiter('tip', filter, timeout, maxToCollect, uniqueUsers);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
movement(filter = () => true, timeout = 30000, maxToCollect = 1, uniqueUsers = false) {
|
|
41
|
+
if (!this._validateParameters('movement', filter, timeout, maxToCollect)) {
|
|
42
|
+
return Promise.resolve([]);
|
|
43
|
+
}
|
|
44
|
+
return this._createAwaiter('movement', filter, timeout, maxToCollect, uniqueUsers);
|
|
45
|
+
}
|
|
46
|
+
|
|
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;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
_createAwaiter(eventType, filter, timeout, maxToCollect, uniqueUsers = false) {
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
const awaiterId = Symbol('awaiter');
|
|
98
|
+
const collected = [];
|
|
99
|
+
const seenUsers = new Set();
|
|
100
|
+
let timeoutId;
|
|
101
|
+
|
|
102
|
+
const cleanup = () => {
|
|
103
|
+
clearTimeout(timeoutId);
|
|
104
|
+
this._pendingAwaiters.delete(awaiterId);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const awaiter = {
|
|
108
|
+
eventType,
|
|
109
|
+
filter,
|
|
110
|
+
maxToCollect,
|
|
111
|
+
collected,
|
|
112
|
+
uniqueUsers,
|
|
113
|
+
seenUsers,
|
|
114
|
+
resolve: (results) => {
|
|
115
|
+
cleanup();
|
|
116
|
+
resolve(results);
|
|
117
|
+
},
|
|
118
|
+
reject: (error) => {
|
|
119
|
+
cleanup();
|
|
120
|
+
reject(error);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
this._pendingAwaiters.set(awaiterId, awaiter);
|
|
125
|
+
|
|
126
|
+
if (timeout > 0) {
|
|
127
|
+
timeoutId = setTimeout(() => {
|
|
128
|
+
awaiter.resolve(collected);
|
|
129
|
+
}, timeout);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
_processEvent(eventType, ...eventArgs) {
|
|
135
|
+
for (const [awaiterId, awaiter] of this._pendingAwaiters) {
|
|
136
|
+
if (awaiter.eventType === eventType) {
|
|
137
|
+
try {
|
|
138
|
+
this._validateEventArguments(eventType, eventArgs);
|
|
139
|
+
|
|
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);
|
|
149
|
+
|
|
150
|
+
if (awaiter.collected.length >= awaiter.maxToCollect) {
|
|
151
|
+
awaiter.resolve(awaiter.collected);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
this.ws._logger.error(`await.${eventType}`, `Filter error: ${error.message}`)
|
|
156
|
+
}
|
|
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
|
+
} catch (error) {
|
|
197
|
+
this.ws._logger.error(`await.${eventType}`, error.message)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
module.exports = AwaitClass;
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const ErrorConstants = require('highrise.js/src/constants/ErrorConstants');
|
|
3
|
+
|
|
4
|
+
class ChannelClass {
|
|
5
|
+
constructor(bot) {
|
|
6
|
+
this.bot = bot;
|
|
7
|
+
this.logger = bot.logger;
|
|
8
|
+
|
|
9
|
+
this._messageHistory = [];
|
|
10
|
+
this._maxHistory = 500;
|
|
11
|
+
this._listeners = new Map();
|
|
12
|
+
this._tagIndex = new Map();
|
|
13
|
+
|
|
14
|
+
this.HighriseError = bot.HighriseError;
|
|
15
|
+
this.HighriseResponse = bot.HighriseResponse;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async send(message, tags = []) {
|
|
19
|
+
let storedMessage
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
if (typeof message !== 'string' || message.trim().length === 0) {
|
|
23
|
+
return this.HighriseError.invalidMessage(message).toResult();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const normalizedTags = this._normalizeTags(tags);
|
|
27
|
+
|
|
28
|
+
const payload = {
|
|
29
|
+
_type: 'ChannelRequest',
|
|
30
|
+
message: message,
|
|
31
|
+
tags: normalizedTags,
|
|
32
|
+
rid: crypto.randomUUID()
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const response = await this.bot._fireAndForget.send(payload);
|
|
36
|
+
|
|
37
|
+
if (response.success) {
|
|
38
|
+
storedMessage = {
|
|
39
|
+
id: crypto.randomUUID(),
|
|
40
|
+
message,
|
|
41
|
+
tags: normalizedTags,
|
|
42
|
+
timestamp: Date.now(),
|
|
43
|
+
senderId: this.bot.info.user?.id || 'unknown',
|
|
44
|
+
sent: true
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
this._storeMessage(storedMessage);
|
|
48
|
+
|
|
49
|
+
return this.HighriseResponse.success('message', storedMessage);
|
|
50
|
+
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
} catch (error) {
|
|
54
|
+
return new this.HighriseError(
|
|
55
|
+
ErrorConstants.CHANNEL.LISTENER_ERROR,
|
|
56
|
+
`Failed to send channel message: ${error.message}`,
|
|
57
|
+
{ storedMessage }
|
|
58
|
+
).toResult();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
on(tags, callback, options = {}) {
|
|
63
|
+
let listener
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
if (!callback || typeof callback !== 'function') {
|
|
67
|
+
return new this.HighriseError(
|
|
68
|
+
ErrorConstants.VALIDATION.INVALID_CHANNEL_CALLBACK,
|
|
69
|
+
`callback must be a function`
|
|
70
|
+
).toResult();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const normalizedTags = this._normalizeTags(Array.isArray(tags) ? tags : [tags]);
|
|
74
|
+
|
|
75
|
+
const listenerId = crypto.randomUUID();
|
|
76
|
+
listener = {
|
|
77
|
+
id: listenerId,
|
|
78
|
+
callback,
|
|
79
|
+
tags: normalizedTags,
|
|
80
|
+
options: {
|
|
81
|
+
once: options.once || false,
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
this._listeners.set(listenerId, listener);
|
|
86
|
+
|
|
87
|
+
normalizedTags.forEach(tag => {
|
|
88
|
+
if (!this._tagIndex.has(tag)) {
|
|
89
|
+
this._tagIndex.set(tag, new Set());
|
|
90
|
+
}
|
|
91
|
+
this._tagIndex.get(tag).add(listenerId);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return this.HighriseResponse.success('listener', {
|
|
95
|
+
id: listenerId,
|
|
96
|
+
tags: normalizedTags}
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
} catch (error) {
|
|
100
|
+
return new this.HighriseError(
|
|
101
|
+
ErrorConstants.CHANNEL.LISTENER_ERROR,
|
|
102
|
+
`Failed to register listener: ${error.message}`,
|
|
103
|
+
{ listener }
|
|
104
|
+
).toResult();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
once(tags, callback) {
|
|
109
|
+
return this.on(tags, callback, { once: true });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
off(listenerId) {
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const listener = this._listeners.get(listenerId);
|
|
116
|
+
if (!listener) {
|
|
117
|
+
return new this.HighriseError(
|
|
118
|
+
ErrorConstants.NOT_FOUND.LISTENER_NOT_FOUND,
|
|
119
|
+
`Listener '${listenerId ? `${listenerId.substring(0, 12)}...` : 'EMPTY'}' Not found to be removed.`
|
|
120
|
+
).toResult();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
listener.tags.forEach(tag => {
|
|
124
|
+
const set = this._tagIndex.get(tag);
|
|
125
|
+
if (set) {
|
|
126
|
+
set.delete(listenerId);
|
|
127
|
+
if (set.size === 0) {
|
|
128
|
+
this._tagIndex.delete(tag);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
this._listeners.delete(listenerId);
|
|
134
|
+
|
|
135
|
+
return this.HighriseResponse.success('listenerId', listenerId);
|
|
136
|
+
|
|
137
|
+
} catch (error) {
|
|
138
|
+
return new this.HighriseError(
|
|
139
|
+
ErrorConstants.CHANNEL.LISTENER_ERROR,
|
|
140
|
+
`Failed to remove listener: ${error.message}`,
|
|
141
|
+
{ listenerId }
|
|
142
|
+
).toResult();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
offAll(tags) {
|
|
147
|
+
const normalizedTags = this._normalizeTags(Array.isArray(tags) ? tags : [tags]);
|
|
148
|
+
|
|
149
|
+
const listenersToRemove = new Set();
|
|
150
|
+
|
|
151
|
+
normalizedTags.forEach(tag => {
|
|
152
|
+
const listeners = this._tagIndex.get(tag);
|
|
153
|
+
if (listeners) {
|
|
154
|
+
listeners.forEach(listenerId => listenersToRemove.add(listenerId));
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
let removed = 0;
|
|
159
|
+
let failed = {
|
|
160
|
+
count: 0,
|
|
161
|
+
errors: []
|
|
162
|
+
}
|
|
163
|
+
listenersToRemove.forEach(listenerId => {
|
|
164
|
+
const result = this.off(listenerId);
|
|
165
|
+
if (result.success) {
|
|
166
|
+
removed++;
|
|
167
|
+
} else {
|
|
168
|
+
failed.errors.push(result.error)
|
|
169
|
+
failed.count++
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return this.HighriseResponse.success('removed', {
|
|
174
|
+
count: removed,
|
|
175
|
+
failed,
|
|
176
|
+
tags: normalizedTags
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
query(filter = {}) {
|
|
181
|
+
const {
|
|
182
|
+
tags = [],
|
|
183
|
+
since = 0,
|
|
184
|
+
until = Date.now(),
|
|
185
|
+
limit = 50
|
|
186
|
+
} = filter;
|
|
187
|
+
|
|
188
|
+
const normalizedTags = this._normalizeTags(Array.isArray(tags) ? tags : [tags]);
|
|
189
|
+
|
|
190
|
+
const results = [];
|
|
191
|
+
|
|
192
|
+
for (const msg of this._messageHistory) {
|
|
193
|
+
if (this._matchesFilter(msg, { tags: normalizedTags, since, until })) {
|
|
194
|
+
results.push(msg);
|
|
195
|
+
if (results.length >= limit) break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return this.HighriseResponse.success('query', {
|
|
200
|
+
count: results.length,
|
|
201
|
+
messages: results,
|
|
202
|
+
filter
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
stats() {
|
|
207
|
+
return this.HighriseResponse.success('stats', {
|
|
208
|
+
totalMessages: this._messageHistory.length,
|
|
209
|
+
activeListeners: this._listeners.size,
|
|
210
|
+
indexedTags: this._tagIndex.size
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
storeIncomingMessage(senderId, message, tags) {
|
|
215
|
+
try {
|
|
216
|
+
const normalizedTags = this._normalizeTags(tags);
|
|
217
|
+
|
|
218
|
+
const storedMsg = {
|
|
219
|
+
id: crypto.randomUUID(),
|
|
220
|
+
message,
|
|
221
|
+
tags: normalizedTags,
|
|
222
|
+
timestamp: Date.now(),
|
|
223
|
+
senderId,
|
|
224
|
+
sent: false
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
this._storeMessage(storedMsg);
|
|
228
|
+
this._triggerListeners(storedMsg);
|
|
229
|
+
|
|
230
|
+
return this.HighriseResponse.success('stored', {
|
|
231
|
+
id: storedMsg.id,
|
|
232
|
+
senderId: storedMsg.senderId,
|
|
233
|
+
tags: storedMsg.tags,
|
|
234
|
+
timestamp: storedMsg.timestamp
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
} catch (error) {
|
|
238
|
+
this.logger.error('ChannelManager', 'Failed to store incoming message', { error: error.message });
|
|
239
|
+
|
|
240
|
+
return new this.HighriseError(
|
|
241
|
+
ErrorConstants.CHANNEL.LISTENER_ERROR,
|
|
242
|
+
'Failed to store incoming message',
|
|
243
|
+
{ error: error.message }
|
|
244
|
+
).toResult();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
_triggerListeners(message) {
|
|
249
|
+
const matchedListeners = new Map();
|
|
250
|
+
|
|
251
|
+
message.tags.forEach(tag => {
|
|
252
|
+
const listeners = this._tagIndex.get(tag);
|
|
253
|
+
if (listeners) {
|
|
254
|
+
listeners.forEach(listenerId => {
|
|
255
|
+
const listener = this._listeners.get(listenerId);
|
|
256
|
+
if (listener && !matchedListeners.has(listenerId)) {
|
|
257
|
+
const hasAllTags = listener.tags.every(t => message.tags.includes(t));
|
|
258
|
+
if (hasAllTags) {
|
|
259
|
+
matchedListeners.set(listenerId, listener);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
matchedListeners.forEach((listener, listenerId) => {
|
|
267
|
+
try {
|
|
268
|
+
listener.callback(message);
|
|
269
|
+
|
|
270
|
+
if (listener.options.once) {
|
|
271
|
+
this.off(listenerId);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
} catch (error) {
|
|
275
|
+
this.logger.error('ChannelManager', 'Listener callback error', { listenerId, error: error.message });
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
_normalizeTags(tags) {
|
|
281
|
+
const normalized = tags
|
|
282
|
+
.map(tag => {
|
|
283
|
+
if (typeof tag !== 'string') return '';
|
|
284
|
+
return tag.toLowerCase()
|
|
285
|
+
.trim()
|
|
286
|
+
.replace(/[^a-z0-9_:.-]/g, '-')
|
|
287
|
+
.replace(/-+/g, '-')
|
|
288
|
+
.substring(0, 50);
|
|
289
|
+
})
|
|
290
|
+
.filter(tag => tag.length > 0)
|
|
291
|
+
.filter((tag, index, self) => self.indexOf(tag) === index)
|
|
292
|
+
.sort();
|
|
293
|
+
|
|
294
|
+
return normalized.length > 0 ? normalized : ['global'];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
_storeMessage(message) {
|
|
298
|
+
this._messageHistory.unshift(message);
|
|
299
|
+
|
|
300
|
+
if (this._messageHistory.length > this._maxHistory) {
|
|
301
|
+
this._messageHistory.splice(this._maxHistory);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
_matchesFilter(message, filter) {
|
|
306
|
+
const { tags = [], since = 0, until = Date.now() } = filter;
|
|
307
|
+
|
|
308
|
+
if (message.timestamp < since || message.timestamp > until) {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (tags.length > 0) {
|
|
313
|
+
const hasAllTags = tags.every(tag => message.tags.includes(tag));
|
|
314
|
+
if (!hasAllTags) return false;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
clear() {
|
|
321
|
+
this._messageHistory = [];
|
|
322
|
+
this._listeners.clear();
|
|
323
|
+
this._tagIndex.clear();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
module.exports = { ChannelClass };
|