hackchat-engine 1.1.15 → 1.1.17

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/.eslintrc.json CHANGED
@@ -1,27 +1,27 @@
1
- {
2
- "env": {
3
- "browser": true,
4
- "es6": true,
5
- "node": true
6
- },
7
- "extends": [
8
- "airbnb-base"
9
- ],
10
- "globals": {
11
- "Atomics": "readonly",
12
- "SharedArrayBuffer": "readonly"
13
- },
14
- "parserOptions": {
15
- "ecmaVersion": 2018,
16
- "sourceType": "module"
17
- },
18
- "rules": {
19
- "import/extensions": [
20
- "error",
21
- "ignorePackages",
22
- {
23
- "js": "always"
24
- }
25
- ]
26
- }
27
- }
1
+ {
2
+ "env": {
3
+ "browser": true,
4
+ "es6": true,
5
+ "node": true
6
+ },
7
+ "extends": [
8
+ "airbnb-base"
9
+ ],
10
+ "globals": {
11
+ "Atomics": "readonly",
12
+ "SharedArrayBuffer": "readonly"
13
+ },
14
+ "parserOptions": {
15
+ "ecmaVersion": 2018,
16
+ "sourceType": "module"
17
+ },
18
+ "rules": {
19
+ "import/extensions": [
20
+ "error",
21
+ "ignorePackages",
22
+ {
23
+ "js": "always"
24
+ }
25
+ ]
26
+ }
27
+ }
package/Client.js CHANGED
@@ -150,9 +150,6 @@ class Client extends EventEmitter {
150
150
  throw new Error(Errors.INVALID_CHANNEL);
151
151
  }
152
152
 
153
- // @todo: this will be changed with the multichannel patch
154
- this.channel = channel;
155
-
156
153
  this.ws.send({
157
154
  cmd: OPCodes.JOIN,
158
155
  nick: name,
@@ -411,13 +408,13 @@ class Client extends EventEmitter {
411
408
  * Send `updateMessage` operation to the server
412
409
  * @param {string} customId The customId of the target message
413
410
  * @param {string} text The text to apply
414
- * @param {string} [mode='overwrite'] The update mode:
415
- * 'overwrite', 'append', 'prepend', or 'complete'
411
+ * @param {string} channel Target channel
412
+ * @param {string} [mode='overwrite|append|prepend|complete'] The update mode
416
413
  */
417
- updateMessage(customId, text, mode = 'overwrite') {
414
+ updateMessage(customId, text, channel, mode = 'overwrite') {
418
415
  this.ws.send({
419
416
  cmd: OPCodes.UPDATE_MESSAGE,
420
- channel: this.channel, // @todo Multichannel
417
+ channel,
421
418
  customId,
422
419
  text,
423
420
  mode,
@@ -427,11 +424,12 @@ class Client extends EventEmitter {
427
424
  /**
428
425
  * Request the public wallet address of a specific user
429
426
  * @param {number|string} target The userid (number) or nickname (string) of the user
427
+ * @param {string} channel Target channel
430
428
  */
431
- getWallet(target) {
429
+ getWallet(target, channel) {
432
430
  const payload = {
433
431
  cmd: OPCodes.GET_WALLET,
434
- channel: this.channel, // @todo Multichannel
432
+ channel,
435
433
  };
436
434
 
437
435
  if (typeof target === 'number') {
@@ -450,7 +448,7 @@ class Client extends EventEmitter {
450
448
  leave(channel) {
451
449
  this.ws.send({
452
450
  cmd: OPCodes.LEAVE,
453
- channel: channel || this.channel, // @todo Multichannel
451
+ channel,
454
452
  });
455
453
  }
456
454
  }
package/README.md CHANGED
@@ -1,181 +1,174 @@
1
- # hackchat-engine
2
-
3
- A modern, NodeJS and browser-friendly ES6 WebSocket library for interacting with [hack.chat](https://hack.chat) servers. This engine provides an object-oriented interface, automatic keep-alives, and structured event handling.
4
-
5
- ## Installation
6
-
7
- ### Prerequisites
8
-
9
- * **Node.js 14+** (This package uses ES Modules)
10
- * **NPM 6+**
11
-
12
- ### Install
13
-
14
- ```bash
15
- npm install hackchat-engine
16
-
17
- ```
18
-
19
- ---
20
-
21
- ## Quick Start
22
-
23
- This library uses **ES Modules**. You must use `import` syntax.
24
-
25
- ```javascript
26
- import { Client } from 'hackchat-engine';
27
-
28
- const botName = 'MyBot';
29
- const botPass = 'secretPassword';
30
- const channel = 'programming';
31
-
32
- // Initialize the client
33
- const client = new Client({
34
- debug: true
35
- });
36
-
37
- // Triggered when the WebSocket connects to the gateway
38
- client.on('connected', () => {
39
- console.log('Connected to server!');
40
- client.ws.send({ cmd: 'getchannels' });
41
- client.join(botName, botPass, channel);
42
- });
43
-
44
- // Triggered when the server assigns a session ID
45
- client.on('session', (session) => {
46
- console.log(`Session ID: ${session.sessionID}`);
47
- });
48
-
49
- // Triggered when the client successfully joins the channel
50
- client.on('channelJoined', (data) => {
51
- console.log(`Joined channel: ${data.channel}`);
52
- client.say(channel, 'Hello world! I am a bot.');
53
- });
54
-
55
- // Triggered on every new message
56
- client.on('message', (message) => {
57
- // Ignore our own messages
58
- if (message.user.isMine) return;
59
-
60
- console.log(`[${message.channel}] ${message.user.name}: ${message.content}`);
61
-
62
- if (message.content === '!ping') {
63
- // Helper method to reply directly to the channel
64
- message.reply('Pong!');
65
- }
66
- });
67
-
68
- ```
69
-
70
- ---
71
-
72
- ## Configuration
73
-
74
- You can pass an options object to the `Client` constructor to customize the connection.
75
-
76
- ```javascript
77
- const client = new Client({
78
- // Connection details
79
- ws: {
80
- gateway: 'wss://hack.chat/chat-ws', // Default
81
- // gateway: 'ws://127.0.0.1:6060/' // For local development
82
- },
83
-
84
- // Set to true to see internal logs and raw packets
85
- debug: false,
86
-
87
- // If true, identifies as a bot to the server (default: true)
88
- isBot: true,
89
- });
90
-
91
- ```
92
-
93
- ---
94
-
95
- ## Events
96
-
97
- The client extends `EventEmitter`. You can listen for the following events:
98
-
99
- | Event | Payload | Description |
100
- | --- | --- | --- |
101
- | `connected` | `Client` | Socket connection established. |
102
- | `session` | `SessionStruct` | Server has assigned a session ID. |
103
- | `channelJoined` | `Object` | Client has successfully joined a channel and received the user list. |
104
- | `message` | `MessageStruct` | A new message was received. |
105
- | `userJoined` | `UserStruct` | A user joined the channel. |
106
- | `userLeft` | `UserStruct` | A user left the channel. |
107
- | `userUpdate` | `UserStruct` | A user changed their flair, color, or status. |
108
- | `whisper` | `WhisperStruct` | Received a private whisper. |
109
- | `invite` | `InviteStruct` | Received an invitation to another channel. |
110
- | `emote` | `EmoteStruct` | A user sent a `/me` emote. |
111
- | `warning` | `WarningStruct` | Server sent a warning (e.g., rate limit). |
112
- | `gotCaptcha` | `CaptchaStruct` | Server requires a captcha solution. |
113
- | `hackAttempt` | `HackAttemptStruct` | A client upgrade request attempt. |
114
- | `updateMessage` | `UpdateMessageStruct` | A previously sent message was edited or deleted. |
115
-
116
- ---
117
-
118
- ## Key Methods
119
-
120
- ### Core
121
-
122
- * **`client.join(nick, password, channel)`**: Authenticate and join a specific channel.
123
- * **`client.say(channel, text)`**: Send a message to a channel.
124
- * **`client.changeColor(hex)`**: Change your nickname color (e.g., `CCCCCC`).
125
- * **`client.changeUsername(newNick)`**: Request a nickname change.
126
-
127
- ### Moderation
128
-
129
- * **`client.kick(channel, user)`**: Kick a user (requires permissions).
130
- * **`client.ban(channel, user)`**: Ban a user (requires permissions).
131
- * **`client.enableCaptcha(channel)`**: Turn on captcha for the channel.
132
- * **`client.lockChannel(channel)`**: Lock the channel (admin only).
133
-
134
- ### Interactions
135
-
136
- The `MessageStruct` (passed in the `message` event) has helper methods:
137
-
138
- * **`message.reply(text)`**: Automatically replies to the channel the message originated from.
139
-
140
- ### Message Manipulation
141
-
142
- * **`client.updateMessage(customId, text, mode)`**: Edit a message you previously sent.
143
- * `mode`: `'overwrite'`, `'append'`, `'prepend'`, or `'complete'` (delete).
144
-
145
-
146
-
147
- ---
148
-
149
- ## Advanced Usage
150
-
151
- ### Handling Custom Commands
152
-
153
- If the server implements custom opcodes that are not natively handled by the engine, you can register a listener for them:
154
-
155
- ```javascript
156
- client.onCommand('customEvent', (payload) => {
157
- console.log('Received custom packet:', payload);
158
- });
159
-
160
- ```
161
-
162
- ### Accessing Users
163
-
164
- Users are stored in an extended Map for easy access.
165
-
166
- ```javascript
167
- // Get a user by ID
168
- const user = client.users.get(12345);
169
-
170
- // Find a user by name
171
- const user = client.users.find(u => u.username === 'Admin');
172
-
173
- if (user) {
174
- console.log(`${user.username} is currently ${user.isOnline ? 'Online' : 'Offline'}`);
175
- }
176
-
177
- ```
178
-
179
- ## License
180
-
181
- MIT
1
+ # hackchat-engine
2
+
3
+ A modern, NodeJS and browser-friendly ES6 WebSocket library for interacting with [hack.chat](https://hack.chat) servers. This engine provides an object-oriented interface, automatic keep-alives, and structured event handling.
4
+
5
+ ## Installation
6
+
7
+ ### Prerequisites
8
+
9
+ * **Node.js 14+** (This package uses ES Modules)
10
+ * **NPM 6+**
11
+
12
+ ### Install
13
+
14
+ ```bash
15
+ npm install hackchat-engine
16
+
17
+ ```
18
+
19
+ ---
20
+
21
+ ## Quick Start
22
+
23
+ This library uses **ES Modules**. You must use `import` syntax.
24
+
25
+ ```javascript
26
+ import { Client, Events } from 'hackchat-engine';
27
+
28
+ const botName = 'MyBot';
29
+ const botPass = 'secretPassword';
30
+ const channel = 'programming';
31
+
32
+ // initialize the client
33
+ const client = new Client({
34
+ debug: false,
35
+ });
36
+
37
+ // triggered when the server establishes a session
38
+ client.on(Events.SESSION, (session) => {
39
+ console.log(`Session established! Token: ${session.token}`);
40
+ // join the channel once the session is ready
41
+ client.join(botName, botPass, channel);
42
+ });
43
+
44
+ // triggered when the client successfully joins the channel
45
+ client.on(Events.ONLINE_SET, (data) => {
46
+ client.say(data.channel, 'Hello world! I am a bot.');
47
+ });
48
+
49
+ // triggered on every new message
50
+ client.on(Events.NEW_MESSAGE, (message) => {
51
+ if (message.user.username === botName) return;
52
+
53
+ if (message.content === '!ping') {
54
+ client.say(message.channel, 'Pong!');
55
+ }
56
+ });
57
+
58
+ ```
59
+
60
+ For a more in-depth example:
61
+
62
+ **[./example.js](./example.js)**
63
+ ---
64
+
65
+ ## Configuration
66
+
67
+ You can pass an options object to the `Client` constructor to customize the connection.
68
+
69
+ ```javascript
70
+ const client = new Client({
71
+ // Connection details
72
+ ws: {
73
+ gateway: 'wss://hack.chat/chat-ws', // Default
74
+ // gateway: 'ws://127.0.0.1:6060/' // For local development
75
+ },
76
+
77
+ // Set to true to see internal logs and raw packets (default: false)
78
+ debug: false,
79
+
80
+ // If true, identifies as a bot to the server (default: true)
81
+ isBot: true,
82
+ });
83
+
84
+ ```
85
+
86
+ ---
87
+
88
+ ## Events
89
+
90
+ The client extends `EventEmitter`. You can listen for the following events:
91
+
92
+ | Event | Payload | Description |
93
+ | --- | --- | --- |
94
+ | `connected` | `Client` | Socket connection established. |
95
+ | `session` | `SessionStruct` | Server has assigned a session ID. |
96
+ | `channelJoined` | `Object` | Client has successfully joined a channel and received the user list. |
97
+ | `message` | `MessageStruct` | A new message was received. |
98
+ | `userJoined` | `UserStruct` | A user joined the channel. |
99
+ | `userLeft` | `UserStruct` | A user left the channel. |
100
+ | `userUpdate` | `UserStruct` | A user changed their flair, color, or status. |
101
+ | `whisper` | `WhisperStruct` | Received a private whisper. |
102
+ | `invite` | `InviteStruct` | Received an invitation to another channel. |
103
+ | `emote` | `EmoteStruct` | A user sent a `/me` emote. |
104
+ | `warning` | `WarningStruct` | Server sent a warning (e.g., rate limit). |
105
+ | `gotCaptcha` | `CaptchaStruct` | Server requires a captcha solution. |
106
+ | `hackAttempt` | `HackAttemptStruct` | A client upgrade request attempt. |
107
+ | `updateMessage` | `UpdateMessageStruct` | A previously sent message was edited or deleted. |
108
+
109
+ ---
110
+
111
+ ## Key Methods
112
+
113
+ ### Core
114
+
115
+ * **`client.join(nick, password, channel)`**: Authenticate and join a specific channel.
116
+ * **`client.say(channel, text)`**: Send a message to a channel.
117
+ * **`client.changeColor(hex)`**: Change your nickname color (e.g., `CCCCCC`).
118
+ * **`client.changeUsername(newNick)`**: Request a nickname change.
119
+
120
+ ### Moderation
121
+
122
+ * **`client.kick(channel, user)`**: Kick a user (requires permissions).
123
+ * **`client.ban(channel, user)`**: Ban a user (requires permissions).
124
+ * **`client.enableCaptcha(channel)`**: Turn on captcha for the channel.
125
+ * **`client.lockChannel(channel)`**: Lock the channel (admin only).
126
+
127
+ ### Interactions
128
+
129
+ The `MessageStruct` (passed in the `message` event) has helper methods:
130
+
131
+ * **`message.reply(text)`**: Automatically replies to the channel the message originated from.
132
+
133
+ ### Message Manipulation
134
+
135
+ * **`client.updateMessage(customId, text, mode)`**: Edit a message you previously sent.
136
+ * `mode`: `'overwrite'`, `'append'`, `'prepend'`, or `'complete'` (delete).
137
+
138
+
139
+
140
+ ---
141
+
142
+ ## Advanced Usage
143
+
144
+ ### Handling Custom Commands
145
+
146
+ If the server implements custom opcodes that are not natively handled by the engine, you can register a listener for them:
147
+
148
+ ```javascript
149
+ client.onCommand('customEvent', (payload) => {
150
+ console.log('Received custom packet:', payload);
151
+ });
152
+
153
+ ```
154
+
155
+ ### Accessing Users
156
+
157
+ Users are stored in an extended Map for easy access.
158
+
159
+ ```javascript
160
+ // Get a user by ID
161
+ const user = client.users.get(12345);
162
+
163
+ // Find a user by name
164
+ const user = client.users.find(u => u.username === 'Admin');
165
+
166
+ if (user) {
167
+ console.log(`${user.username} is currently ${user.isOnline ? 'Online' : 'Offline'}`);
168
+ }
169
+
170
+ ```
171
+
172
+ ## License
173
+
174
+ MIT
@@ -4,8 +4,6 @@ import UserStruct from '../structures/UserStruct.js';
4
4
 
5
5
  /**
6
6
  * This class handles an incoming `onlineSet` event from the server
7
- * @todo Change user handling for multi-channel patch. What if we removed
8
- * this event entirely and funneled everything through the `join` event?
9
7
  * @private
10
8
  */
11
9
  class OnlineSet extends AbstractEvent {
@@ -16,20 +14,39 @@ class OnlineSet extends AbstractEvent {
16
14
  */
17
15
  handle(data) {
18
16
  const { client } = this;
17
+ const incomingChannel = data.channel;
19
18
 
20
- /**
21
- * Patch user array into online users
22
- * @todo This will be changed with the multi-channel patch
23
- * @legacy
24
- */
25
- data.users.map((userData) => {
19
+ data.users.forEach((userData) => {
26
20
  if (userData.isme) {
27
21
  if (!client.myUser) {
22
+ // eslint-disable-next-line no-param-reassign
23
+ userData.channel = incomingChannel;
28
24
  client.myUser = new ClientStruct(client, userData);
25
+ } else {
26
+ if (client.myUser.channels) {
27
+ client.myUser.channels.add(incomingChannel);
28
+ }
29
+
30
+ client.myUser.updateUser(userData);
29
31
  }
30
32
  }
31
33
 
32
- return client.users.set(userData.userid, new UserStruct(client, userData));
34
+ const existingUser = client.users.get(userData.userid);
35
+
36
+ if (existingUser) {
37
+ if (existingUser.channels) {
38
+ existingUser.channels.add(incomingChannel);
39
+ }
40
+
41
+ existingUser.updateUser(userData);
42
+ existingUser.online = true;
43
+ } else {
44
+ // eslint-disable-next-line no-param-reassign
45
+ userData.channel = incomingChannel;
46
+
47
+ const newUser = new UserStruct(client, userData);
48
+ client.users.set(userData.userid, newUser);
49
+ }
33
50
  });
34
51
  }
35
52
  }
package/events/Session.js CHANGED
@@ -3,7 +3,6 @@ import SessionStruct from '../structures/SessionStruct.js';
3
3
 
4
4
  /**
5
5
  * This class handles an incoming `session` event from the server
6
- * @todo Implement session restore
7
6
  * @private
8
7
  */
9
8
  class Session extends AbstractEvent {
@@ -14,7 +14,13 @@ class UpdateUser extends AbstractEvent {
14
14
  const { client } = this;
15
15
  const user = client.users.get(data.userid);
16
16
 
17
- user.updateUser(data);
17
+ if (user) {
18
+ user.updateUser(data);
19
+ }
20
+
21
+ if (client.myUser && client.myUser.userid === data.userid) {
22
+ client.myUser.updateUser(data);
23
+ }
18
24
 
19
25
  return user;
20
26
  }
@@ -13,17 +13,17 @@ class UserJoin extends AbstractEvent {
13
13
  */
14
14
  handle(data) {
15
15
  const { client } = this;
16
- // Check if user is already stored
17
16
  const user = client.users.get(data.userid);
18
17
 
19
- // If stored, set them to online
20
18
  if (user) {
21
- user.toggleOnline(data.channel);
19
+ user.channels.add(data.channel);
20
+ user.online = true;
21
+ user.channel = data.channel;
22
22
  return user;
23
23
  }
24
24
 
25
- // If not, then add them to the records
26
25
  const newUser = new User(client, data);
26
+ newUser.channel = data.channel;
27
27
  client.users.set(data.userid, newUser);
28
28
  return newUser;
29
29
  }
@@ -14,7 +14,13 @@ class UserLeave extends AbstractEvent {
14
14
  const { client } = this;
15
15
  const user = client.users.get(data.userid);
16
16
 
17
- user.toggleOnline(data.channel);
17
+ if (user) {
18
+ user.channels.delete(data.channel);
19
+
20
+ if (user.channels.size === 0) {
21
+ user.online = false;
22
+ }
23
+ }
18
24
 
19
25
  return user;
20
26
  }
package/example.js ADDED
@@ -0,0 +1,261 @@
1
+ /* eslint-disable no-console */
2
+
3
+ // remember to npm install hackchat-engine
4
+ // import { Client, Events } from 'hackchat-engine'; <-- use this one
5
+ // import { Client as HcClient, Events as HcEvents } from 'hackchat-engine'; <-- or this one
6
+ import { Client, Events } from './index.js'; // <-- not this one
7
+
8
+ const CONFIG = {
9
+ gateway: 'wss://hack.chat/chat-ws',
10
+ botName: 'ExampleBot',
11
+ password: 'botpassword',
12
+ homeChannel: 'test',
13
+ };
14
+
15
+ // init
16
+ let sessionToken = null;
17
+
18
+ const client = new Client({
19
+ ws: { gateway: CONFIG.gateway },
20
+ debug: false, // set to true to see internal debug logs
21
+ session: sessionToken,
22
+ });
23
+
24
+ console.log(`Starting ${CONFIG.botName}`);
25
+
26
+ /**
27
+ * 1. Connection & session events
28
+ */
29
+
30
+ /**
31
+ * Fired when the WebSocket successfully opens
32
+ */
33
+ client.on(Events.CONNECTED, () => {
34
+ console.log('[Connection] WebSocket connected. Negotiating session...');
35
+ });
36
+
37
+ /**
38
+ * Fired when the server provides a session token
39
+ * This is the "login success" event
40
+ */
41
+ client.on(Events.SESSION, (payload) => {
42
+ if (payload.token) {
43
+ sessionToken = payload.token;
44
+ console.log(`[Session] Token received: ${payload.token.substring(0, 10)}...`);
45
+ }
46
+
47
+ // join the home channel once we have a session
48
+ console.log(`[System] Joining ${CONFIG.homeChannel}...`);
49
+ client.join(CONFIG.botName, CONFIG.password, CONFIG.homeChannel);
50
+ });
51
+
52
+ /**
53
+ * Fired when the client is fully ready
54
+ */
55
+ client.on(Events.READY, () => {
56
+ console.log('[System] Client is Ready');
57
+ });
58
+
59
+ /**
60
+ * Fired when the socket disconnects
61
+ */
62
+ client.on(Events.DISCONNECT, (event) => {
63
+ console.log(`[Connection] Disconnected. Code: ${event.code}`);
64
+ });
65
+
66
+ /**
67
+ * Fired when the client attempts to auto-reconnect
68
+ */
69
+ client.on(Events.RECONNECTING, () => {
70
+ console.log('[Connection] Connection lost. Attempting to reconnect...');
71
+ });
72
+
73
+ /**
74
+ * 2. Channel & user state events
75
+ */
76
+
77
+ /**
78
+ * Fired when the client successfully joins a channel
79
+ * Contains the channel name and the initial list of users
80
+ */
81
+ client.on(Events.ONLINE_SET, (payload) => {
82
+ console.log(`[Channel] Joined '${payload.channel}'. Users: ${payload.users.length}`);
83
+
84
+ // optional: announce arrival
85
+ client.say(payload.channel, `Hello! I am ${CONFIG.botName}.`);
86
+ });
87
+
88
+ /**
89
+ * Fired when a user joins a channel
90
+ */
91
+ client.on(Events.USER_JOIN, (user) => {
92
+ console.log(`[Presence] ${user.username} joined ${user.channel}.`);
93
+ });
94
+
95
+ /**
96
+ * Fired when a user leaves a channel
97
+ */
98
+ client.on(Events.USER_LEAVE, (user) => {
99
+ console.log(`[Presence] ${user.username} left ${user.channel}.`);
100
+ });
101
+
102
+ /**
103
+ * Fired when a user updates their details (change nick, color, trip, etc)
104
+ */
105
+ client.on(Events.USER_UPDATE, (user) => {
106
+ console.log(`[Presence] User updated: ${user.username} (Trip: ${user.trip || 'None'})`);
107
+ });
108
+
109
+ /**
110
+ * Fired when the server sends the list of public channels
111
+ */
112
+ client.on(Events.PUB_CHANS, (payload) => {
113
+ console.log(`[Info] Received public channel list: ${payload.list.length} channels.`);
114
+ });
115
+
116
+ /**
117
+ * Fired when the server sends an informational message
118
+ */
119
+ client.on(Events.CHANNEL_INFO, (info) => {
120
+ console.log(`[Info] Server sez: "${info.text}" in ${info.channel}`);
121
+ });
122
+
123
+ /**
124
+ * 3. Chat & interaction events
125
+ */
126
+
127
+ /**
128
+ * Fired on standard chat messages
129
+ */
130
+ client.on(Events.NEW_MESSAGE, (msg) => {
131
+ // ignore our own messages
132
+ if (msg.user.username === CONFIG.botName) return;
133
+
134
+ console.log(`[Chat][${msg.channel}] ${msg.user.username}: ${msg.content}`);
135
+
136
+ // example command handling
137
+ if (msg.content === '!ping') {
138
+ client.say(msg.channel, `@${msg.user.username} Pong!`);
139
+ }
140
+ });
141
+
142
+ /**
143
+ * Fired when a user performs an emote
144
+ */
145
+ client.on(Events.CHANNEL_EMOTE, (emote) => {
146
+ console.log(`[Emote][${emote.channel}] * ${emote.user.username} ${emote.content}`);
147
+ });
148
+
149
+ /**
150
+ * Fired when the bot receives a private whisper
151
+ */
152
+ client.on(Events.CHANNEL_WHISPER, (msg) => {
153
+ console.log(`[Whisper] ${msg.from.username} -> Me: ${msg.content}`);
154
+
155
+ // auto-reply example
156
+ msg.reply(`I received your whisper: "${msg.content}"`);
157
+ });
158
+
159
+ /**
160
+ * Fired when the bot is invited to another channel
161
+ */
162
+ client.on(Events.CHANNEL_INVITE, (invite) => {
163
+ console.log(`[Invite] ${invite.from.username} invited us to '${invite.targetChannel}'`);
164
+ // client.join(CONFIG.botName, CONFIG.password, invite.targetChannel);
165
+ });
166
+
167
+ /**
168
+ * Fired when a message is edited/updated (e.g. overwriting the last line)
169
+ */
170
+ client.on(Events.UPDATE_MESSAGE, (update) => {
171
+ console.log(`[Edit] ${update.user.username} updated a message in ${update.channel}. Mode: ${update.mode}`);
172
+ });
173
+
174
+ /**
175
+ * 4. Security & system events
176
+ */
177
+
178
+ /**
179
+ * Fired when the server issues a warning (e.g., spamming too fast)
180
+ */
181
+ client.on(Events.CHANNEL_WARN, (warning) => {
182
+ console.warn(`[Warning] ID: ${warning.id} | Text: ${warning.text}`);
183
+ });
184
+
185
+ /**
186
+ * Fired when the server requires a CAPTCHA to join/chat
187
+ */
188
+ client.on(Events.CHANNEL_CAPTCHA, (captcha) => {
189
+ console.log(`[Captcha] Captcha required in ${captcha.channel}`);
190
+ console.log(captcha.text); // prints the ascii art
191
+ });
192
+
193
+ /**
194
+ * Fired when a 3rd party code suggestion arrives
195
+ */
196
+ client.on(Events.HACK_ATTEMPT, (suggestion) => {
197
+ console.warn(`[Extension] 3rd party code suggested from ${suggestion.fromNick}, library: ${suggestion.lib}`);
198
+ });
199
+
200
+ /**
201
+ * Fired on internal errors
202
+ */
203
+ client.on(Events.ERROR, (err) => {
204
+ console.error('[Error]', err);
205
+ });
206
+
207
+ /**
208
+ * Fired on non-critical internal warnings
209
+ */
210
+ client.on(Events.WARN, (msg) => {
211
+ console.warn('[System Warn]', msg);
212
+ });
213
+
214
+ /**
215
+ * Fired for debug logs (if debug: true is set in Client options)
216
+ * Uncomment to see verbose logs
217
+ */
218
+ /*
219
+ client.on(Events.DEBUG, (msg) => {
220
+ console.log('[Debug]', msg);
221
+ });
222
+ */
223
+
224
+ /**
225
+ * 5. Web3 / wallet events
226
+ */
227
+
228
+ /**
229
+ * Fired when a wallet address lookup (getWallet) returns data
230
+ */
231
+ client.on(Events.WALLET_INFO, (info) => {
232
+ console.log(`[Wallet] User: ${info.nick}, Address: ${info.address}`);
233
+ });
234
+
235
+ /**
236
+ * Fired when the server requests the client to Sign-In With Solana (SIWS)
237
+ */
238
+ client.on(Events.SIGN_MESSAGE, (payload) => {
239
+ console.log(`[Web3] Sign Message Request. Wallet: ${payload.wallet}`);
240
+ console.log(`[Web3] Message to sign: "${payload.message}"`);
241
+ // logic to sign message with private key would go here
242
+ // @todo show reply flow
243
+ });
244
+
245
+ /**
246
+ * Fired when the server requests the client to sign a transaction
247
+ */
248
+ client.on(Events.SIGN_TRANSACTION, (payload) => {
249
+ console.log(`[Web3] Sign Transaction Request. Type: ${payload.type}`);
250
+ console.log(`[Web3] TX Payload: ${payload.tx.substring(0, 20)}...`);
251
+ // @todo show reply flow
252
+ });
253
+
254
+ /**
255
+ * Process cleanup
256
+ */
257
+ process.on('SIGINT', () => {
258
+ console.log('\n[System] Disconnecting...');
259
+ client.destroy();
260
+ process.exit(0);
261
+ });
package/index.js CHANGED
@@ -1,2 +1,3 @@
1
- // eslint-disable-next-line import/prefer-default-export
1
+ /* eslint-disable import/prefer-default-export */
2
2
  export { default as Client } from './Client.js';
3
+ export { Events } from './util/Constants.js';
package/package.json CHANGED
@@ -1,63 +1,63 @@
1
- {
2
- "_from": "hackchat-engine@^1.0.9",
3
- "_inBundle": false,
4
- "_integrity": "sha512-uxyU4HBAs8L4hQktdtvTP/3drbJHQvjkeQDdonglAffHIPtZ1pgU9W/ScNXNCFEyqDAo/QSVikfXp0wMmLtzeQ==",
5
- "_location": "/hackchat-engine",
6
- "_phantomChildren": {},
7
- "_requested": {
8
- "type": "range",
9
- "registry": true,
10
- "raw": "hackchat-engine@^1.0.9",
11
- "name": "hackchat-engine",
12
- "escapedName": "hackchat-engine",
13
- "rawSpec": "^1.0.9",
14
- "saveSpec": null,
15
- "fetchSpec": "^1.0.9"
16
- },
17
- "_requiredBy": [
18
- "#USER",
19
- "/"
20
- ],
21
- "_resolved": "https://registry.npmjs.org/hackchat-engine/-/hackchat-engine-1.0.9.tgz",
22
- "_shasum": "536ea142dbfca44d3853c5767b7797c303642cdb",
23
- "_spec": "hackchat-engine@^1.0.9",
24
- "_where": "C:\\Users\\Owner\\Documents\\GitHub\\hcClientBeta",
25
- "author": {
26
- "name": "Marzavec"
27
- },
28
- "bugs": {
29
- "url": "https://github.com/hack-chat/hackchat-engine/issues"
30
- },
31
- "bundleDependencies": [],
32
- "dependencies": {
33
- "node-fetch": "^2.6.0",
34
- "ws": "^7.2.0"
35
- },
36
- "deprecated": false,
37
- "description": "hack.chat chat engine",
38
- "devDependencies": {
39
- "eslint": "^7.12.1",
40
- "eslint-config-airbnb-base": "^14.2.0",
41
- "eslint-plugin-import": "^2.22.1"
42
- },
43
- "homepage": "https://github.com/hack-chat/hackchat-engine#readme",
44
- "keywords": [
45
- "hack.chat",
46
- "chat",
47
- "client"
48
- ],
49
- "type": "module",
50
- "license": "WTFPL",
51
- "main": "index.js",
52
- "name": "hackchat-engine",
53
- "repository": {
54
- "type": "git",
55
- "url": "git+https://github.com/hack-chat/hackchat-engine.git"
56
- },
57
- "scripts": {
58
- "lint": "eslint --ignore-path .gitignore .",
59
- "lintfix": "eslint --fix --ignore-path .gitignore .",
60
- "test": "echo \"Error: no test specified\" && exit 1"
61
- },
62
- "version": "1.1.15"
63
- }
1
+ {
2
+ "_from": "hackchat-engine@^1.0.9",
3
+ "_inBundle": false,
4
+ "_integrity": "sha512-uxyU4HBAs8L4hQktdtvTP/3drbJHQvjkeQDdonglAffHIPtZ1pgU9W/ScNXNCFEyqDAo/QSVikfXp0wMmLtzeQ==",
5
+ "_location": "/hackchat-engine",
6
+ "_phantomChildren": {},
7
+ "_requested": {
8
+ "type": "range",
9
+ "registry": true,
10
+ "raw": "hackchat-engine@^1.0.9",
11
+ "name": "hackchat-engine",
12
+ "escapedName": "hackchat-engine",
13
+ "rawSpec": "^1.0.9",
14
+ "saveSpec": null,
15
+ "fetchSpec": "^1.0.9"
16
+ },
17
+ "_requiredBy": [
18
+ "#USER",
19
+ "/"
20
+ ],
21
+ "_resolved": "https://registry.npmjs.org/hackchat-engine/-/hackchat-engine-1.0.9.tgz",
22
+ "_shasum": "536ea142dbfca44d3853c5767b7797c303642cdb",
23
+ "_spec": "hackchat-engine@^1.0.9",
24
+ "_where": "C:\\Users\\Owner\\Documents\\GitHub\\hcClientBeta",
25
+ "author": {
26
+ "name": "Marzavec"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/hack-chat/hackchat-engine/issues"
30
+ },
31
+ "bundleDependencies": [],
32
+ "dependencies": {
33
+ "node-fetch": "^2.6.0",
34
+ "ws": "^7.2.0"
35
+ },
36
+ "deprecated": false,
37
+ "description": "hack.chat chat engine",
38
+ "devDependencies": {
39
+ "eslint": "^7.12.1",
40
+ "eslint-config-airbnb-base": "^14.2.0",
41
+ "eslint-plugin-import": "^2.22.1"
42
+ },
43
+ "homepage": "https://github.com/hack-chat/hackchat-engine#readme",
44
+ "keywords": [
45
+ "hack.chat",
46
+ "chat",
47
+ "client"
48
+ ],
49
+ "type": "module",
50
+ "license": "WTFPL",
51
+ "main": "index.js",
52
+ "name": "hackchat-engine",
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "git+https://github.com/hack-chat/hackchat-engine.git"
56
+ },
57
+ "scripts": {
58
+ "lint": "eslint --ignore-path .gitignore .",
59
+ "lintfix": "eslint --fix --ignore-path .gitignore .",
60
+ "test": "echo \"Error: no test specified\" && exit 1"
61
+ },
62
+ "version": "1.1.17"
63
+ }
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * This class handles parsing of the data of a `session` event
3
- * @todo Implement session restore
4
3
  */
5
4
  class SessionStruct {
6
5
  constructor(client, data) {
@@ -37,7 +37,16 @@ class User {
37
37
  * @type {string}
38
38
  * @legacy Will be array or set with multichannel patch
39
39
  */
40
- this.userchannel = data.channel || 'unknown';
40
+ this.channel = data.channel || 'unknown';
41
+
42
+ /**
43
+ * The channels this user is in
44
+ * @type {string}
45
+ */
46
+ this.channels = new Set();
47
+ if (data.channel) {
48
+ this.channels.add(data.channel);
49
+ }
41
50
 
42
51
  /**
43
52
  * This users name
@@ -71,10 +80,9 @@ class User {
71
80
 
72
81
  /**
73
82
  * Whether or not the user is a bot
74
- * @todo
75
83
  * @type {boolean}
76
84
  */
77
- this.bot = data.isBot || false;
85
+ this.botting = data.isBot || false;
78
86
 
79
87
  /**
80
88
  * The Message object of the last message sent by the user, if one was sent
@@ -128,15 +136,6 @@ class User {
128
136
  return this.userid;
129
137
  }
130
138
 
131
- /**
132
- * User name
133
- * @type {string}
134
- * @readonly
135
- */
136
- get channel() {
137
- return this.userchannel;
138
- }
139
-
140
139
  /**
141
140
  * User name
142
141
  * @type {string}
@@ -167,7 +166,6 @@ class User {
167
166
  /**
168
167
  * User level , returns 'user', 'mod', 'admin'
169
168
  * @type {number}
170
- * @todo Change from string to numeric levels
171
169
  * @readonly
172
170
  */
173
171
  get level() {
@@ -186,11 +184,10 @@ class User {
186
184
  /**
187
185
  * Bot status
188
186
  * @type {boolean}
189
- * @todo
190
187
  * @readonly
191
188
  */
192
189
  get isBot() {
193
- return this.bot;
190
+ return this.botting;
194
191
  }
195
192
 
196
193
  /**
@@ -262,25 +259,22 @@ class User {
262
259
  * @type {string}
263
260
  */
264
261
  updateUser(data) {
262
+ if (data.channel) {
263
+ this.channels.add(data.channel);
264
+ this.channel = data.channel;
265
+ }
266
+
267
+ if (typeof data.trip !== 'undefined') this.usertrip = data.trip;
268
+ if (typeof data.hash !== 'undefined') this.userhash = data.hash;
269
+
265
270
  this.username = data.nick;
266
271
  this.userlevel = data.uType;
267
- this.bot = data.isBot;
272
+ this.botting = data.isBot;
268
273
  this.nickColor = data.color;
269
274
  this.flair = data.flair;
270
275
  this.permissionLevel = data.level;
271
276
  }
272
277
 
273
- /**
274
- * Sets this user either online or offline
275
- * @param {string} channel Channel that the user joined or left
276
- * @returns {boolean}
277
- */
278
- // @todo Multichannel
279
- toggleOnline(channel) { // eslint-disable-line no-unused-vars
280
- this.online = !this.online;
281
- return this.online;
282
- }
283
-
284
278
  /**
285
279
  * Sets this user either online or offline
286
280
  * @param {Message} message This user's latest message
@@ -302,15 +296,16 @@ class User {
302
296
 
303
297
  /**
304
298
  * Send chat text using this user's channel
299
+ * @param {string} channel The specific channel to send to
305
300
  * @param {string} text Text to send
306
301
  * @returns {boolean}
307
302
  */
308
- sendMessage(text) {
303
+ sendMessage(channel, text) {
309
304
  try {
310
305
  return new Promise((resolve) => {
311
306
  this.client.ws.send({
312
307
  cmd: OPCodes.CHAT,
313
- channel: this.channel, // @todo Multichannel
308
+ channel,
314
309
  text,
315
310
  });
316
311
  resolve(this);
@@ -322,17 +317,17 @@ class User {
322
317
 
323
318
  /**
324
319
  * Send a whisper through this users channel to the user
320
+ * @param {string} channel The specific channel to send to
325
321
  * @param {string} text Text to send
326
- * @todo Finish whisper functions on server
327
322
  * @returns {Promise}
328
323
  */
329
- sendWhisper(text) {
324
+ sendWhisper(channel, text) {
330
325
  try {
331
326
  return new Promise((resolve) => {
332
327
  this.client.ws.send({
333
328
  cmd: OPCodes.WHISPER,
334
- channel: this.channel, // @todo Multichannel
335
- userid: this.userid,
329
+ channel,
330
+ nick: this.username, /* @todo legacy should be this.userid */
336
331
  text,
337
332
  });
338
333
  resolve(this);
@@ -344,6 +339,8 @@ class User {
344
339
 
345
340
  /**
346
341
  * Send `invite` event to this user
342
+ * @param {string} channelName Channel name to use
343
+ * @param {string} to Username as string
347
344
  * @returns {void}
348
345
  */
349
346
  sendInvite(channel, to = '') {
@@ -362,6 +359,8 @@ class User {
362
359
 
363
360
  /**
364
361
  * Send `kick` operation to the server
362
+ * @param {string} channelName Channel name to use
363
+ * @param {string} to Username as string
365
364
  * @returns {void}
366
365
  */
367
366
  kick(channel, to = '') {
@@ -380,6 +379,7 @@ class User {
380
379
 
381
380
  /**
382
381
  * Send `ban` operation to the server
382
+ * @param {string} channelName Channel name to use
383
383
  * @returns {void}
384
384
  */
385
385
  ban(channel) {
@@ -392,6 +392,7 @@ class User {
392
392
 
393
393
  /**
394
394
  * Send `mute` operation to the server
395
+ * @param {string} channelName Channel name to use
395
396
  * @returns {void}
396
397
  */
397
398
  mute(channel) {
@@ -404,6 +405,7 @@ class User {
404
405
 
405
406
  /**
406
407
  * Send `unmute` operation to the server
408
+ * @param {string} channelName Channel name to use
407
409
  * @returns {void}
408
410
  */
409
411
  unmute(channel) {
@@ -414,6 +416,15 @@ class User {
414
416
  });
415
417
  }
416
418
 
419
+ /**
420
+ * Is user in channel
421
+ * @param {string} channelName Channel name to use
422
+ * @returns {boolean}
423
+ */
424
+ inChannel(channelName) {
425
+ return this.channels.has(channelName);
426
+ }
427
+
417
428
  /**
418
429
  * When referenced as a string, output this users name instead of an object type
419
430
  * @returns {string}
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * This class handles parsing of the data of a `warning` event
3
- * @todo Warnings will be numeric events in the future
4
3
  */
5
4
  class WarningStruct {
6
5
  /**
@@ -32,8 +31,6 @@ class WarningStruct {
32
31
 
33
32
  /**
34
33
  * Text included with warning
35
- * @todo Common warnings will be made integer based to allow i18n
36
- * This will be removed
37
34
  * @legacy
38
35
  * @type {string}
39
36
  */
@@ -41,7 +38,6 @@ class WarningStruct {
41
38
 
42
39
  /**
43
40
  * Channel the warning was issued from
44
- * @todo Touches multi-channel patch
45
41
  * @type {string}
46
42
  */
47
43
  this.channel = data.channel;
@@ -27,6 +27,12 @@ class WhisperStruct {
27
27
  * @returns {void}
28
28
  */
29
29
  setup(data) {
30
+ /**
31
+ * The channel the event occured in
32
+ * @type {User}
33
+ */
34
+ this.channel = data.channel;
35
+
30
36
  /**
31
37
  * The user who sent the whisper
32
38
  * @type {User}
@@ -26,6 +26,7 @@ import WalletInfoHandler from './handlers/WalletInfoHandler.js';
26
26
 
27
27
  const BeforeReadyWhitelist = [
28
28
  WSEvents.SESSION,
29
+ WSEvents.ONLINE_SET,
29
30
  WSEvents.PUB_CHANS,
30
31
  ];
31
32
 
@@ -116,10 +117,12 @@ class PacketRouter {
116
117
  * @returns {void}
117
118
  */
118
119
  processQueue() {
119
- this.queue.forEach((element, index) => {
120
- this.route(this.queue[index], true);
121
- this.queue.splice(index, 1);
122
- });
120
+ while (this.queue.length > 0) {
121
+ const packet = this.queue.shift();
122
+ if (packet) {
123
+ this.route(packet, true);
124
+ }
125
+ }
123
126
  }
124
127
 
125
128
  /**
@@ -24,6 +24,7 @@ class OnlineSetHandler extends AbstractHandler {
24
24
  client.emit(Events.ONLINE_SET, {
25
25
  client,
26
26
  channel: packet.channel,
27
+ users: packet.users,
27
28
  });
28
29
 
29
30
  // Emit debug info
@@ -1,5 +1,5 @@
1
1
  import AbstractHandler from './AbstractHandler.js';
2
- import { Events } from '../../../util/Constants.js';
2
+ import { Events, Status } from '../../../util/Constants.js';
3
3
 
4
4
  /**
5
5
  * Handles a new chat message that has been broadcast within a channel
@@ -15,19 +15,21 @@ class SessionHandler extends AbstractHandler {
15
15
  const { client } = this.packetRouter;
16
16
 
17
17
  client.readyAt = new Date();
18
- const t = client.setTimeout(() => {
19
- client.ws.connection.triggerReady();
20
- }, 1);
21
18
 
22
- client.once('ready', () => {
23
- client.setMaxListeners(10);
24
- client.clearTimeout(t);
25
- });
19
+ if (client.status !== Status.READY) {
20
+ const t = client.setTimeout(() => {
21
+ client.ws.connection.triggerReady();
22
+ }, 1);
23
+
24
+ client.once(Events.READY, () => {
25
+ client.clearTimeout(t);
26
+ });
27
+ }
26
28
 
27
29
  const response = client.events.Session.handle(packet);
28
30
 
29
31
  /**
30
- * Emitted when a message is sent in a channel
32
+ * Emitted when a session packet is received
31
33
  * @event Client#session
32
34
  * @param {SessionStruct} session Full session details
33
35
  */
package/test.js DELETED
@@ -1,45 +0,0 @@
1
- /* eslint no-console: 0 */
2
- import Client from './Client.js';
3
-
4
- const testName = 'testBot';
5
- const testPass = 'testBot';
6
- const testChannel = 'programming';
7
-
8
- const hcClient = new Client({
9
- ws: {
10
- gateway: 'ws://127.0.0.1:6060/',
11
- },
12
- debug: true,
13
- });
14
-
15
- hcClient.on('connected', () => console.log('Connected!'));
16
-
17
- hcClient.on('session', (payload) => {
18
- console.log(payload);
19
- hcClient.join(testName, testPass, testChannel);
20
- });
21
-
22
- hcClient.on('channelJoined', (payload) => {
23
- console.log(payload);
24
- hcClient.say(testChannel, 'Bep boop i r bot');
25
- });
26
-
27
- hcClient.on('debug', (payload) => console.log(payload));
28
-
29
- hcClient.on('userJoined', (payload) => console.log(payload));
30
-
31
- hcClient.on('userLeft', (payload) => console.log(payload));
32
-
33
- hcClient.on('warning', (payload) => console.log(payload));
34
-
35
- hcClient.on('gotCaptcha', (payload) => console.log(payload));
36
-
37
- hcClient.on('information', (payload) => console.log(payload));
38
-
39
- hcClient.on('emote', (payload) => console.log(payload));
40
-
41
- hcClient.on('invite', (payload) => console.log(payload));
42
-
43
- hcClient.on('whisper', (payload) => console.log(payload));
44
-
45
- hcClient.on('message', (payload) => console.log(payload));