hackchat-engine 1.1.14 → 1.1.16

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') {
@@ -442,6 +440,17 @@ class Client extends EventEmitter {
442
440
 
443
441
  this.ws.send(payload);
444
442
  }
443
+
444
+ /**
445
+ * Unsubscribe from a channel
446
+ * @param {string} channel The channel to leave
447
+ */
448
+ leave(channel) {
449
+ this.ws.send({
450
+ cmd: OPCodes.LEAVE,
451
+ channel,
452
+ });
453
+ }
445
454
  }
446
455
 
447
456
  export default Client;
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 {
@@ -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.14"
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.16"
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,21 @@ class User {
262
259
  * @type {string}
263
260
  */
264
261
  updateUser(data) {
262
+ if (data.channel) {
263
+ this.channels.add(data.channel);
264
+ }
265
+
266
+ if (data.trip) this.usertrip = data.trip;
267
+ if (data.hash) this.userhash = data.hash;
268
+
265
269
  this.username = data.nick;
266
270
  this.userlevel = data.uType;
267
- this.bot = data.isBot;
271
+ this.botting = data.isBot;
268
272
  this.nickColor = data.color;
269
273
  this.flair = data.flair;
270
274
  this.permissionLevel = data.level;
271
275
  }
272
276
 
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
277
  /**
285
278
  * Sets this user either online or offline
286
279
  * @param {Message} message This user's latest message
@@ -302,15 +295,16 @@ class User {
302
295
 
303
296
  /**
304
297
  * Send chat text using this user's channel
298
+ * @param {string} channel The specific channel to send to
305
299
  * @param {string} text Text to send
306
300
  * @returns {boolean}
307
301
  */
308
- sendMessage(text) {
302
+ sendMessage(channel, text) {
309
303
  try {
310
304
  return new Promise((resolve) => {
311
305
  this.client.ws.send({
312
306
  cmd: OPCodes.CHAT,
313
- channel: this.channel, // @todo Multichannel
307
+ channel,
314
308
  text,
315
309
  });
316
310
  resolve(this);
@@ -322,17 +316,17 @@ class User {
322
316
 
323
317
  /**
324
318
  * Send a whisper through this users channel to the user
319
+ * @param {string} channel The specific channel to send to
325
320
  * @param {string} text Text to send
326
- * @todo Finish whisper functions on server
327
321
  * @returns {Promise}
328
322
  */
329
- sendWhisper(text) {
323
+ sendWhisper(channel, text) {
330
324
  try {
331
325
  return new Promise((resolve) => {
332
326
  this.client.ws.send({
333
327
  cmd: OPCodes.WHISPER,
334
- channel: this.channel, // @todo Multichannel
335
- userid: this.userid,
328
+ channel,
329
+ nick: this.username, /* @todo legacy should be this.userid */
336
330
  text,
337
331
  });
338
332
  resolve(this);
@@ -344,6 +338,8 @@ class User {
344
338
 
345
339
  /**
346
340
  * Send `invite` event to this user
341
+ * @param {string} channelName Channel name to use
342
+ * @param {string} to Username as string
347
343
  * @returns {void}
348
344
  */
349
345
  sendInvite(channel, to = '') {
@@ -362,6 +358,8 @@ class User {
362
358
 
363
359
  /**
364
360
  * Send `kick` operation to the server
361
+ * @param {string} channelName Channel name to use
362
+ * @param {string} to Username as string
365
363
  * @returns {void}
366
364
  */
367
365
  kick(channel, to = '') {
@@ -380,6 +378,7 @@ class User {
380
378
 
381
379
  /**
382
380
  * Send `ban` operation to the server
381
+ * @param {string} channelName Channel name to use
383
382
  * @returns {void}
384
383
  */
385
384
  ban(channel) {
@@ -392,6 +391,7 @@ class User {
392
391
 
393
392
  /**
394
393
  * Send `mute` operation to the server
394
+ * @param {string} channelName Channel name to use
395
395
  * @returns {void}
396
396
  */
397
397
  mute(channel) {
@@ -404,6 +404,7 @@ class User {
404
404
 
405
405
  /**
406
406
  * Send `unmute` operation to the server
407
+ * @param {string} channelName Channel name to use
407
408
  * @returns {void}
408
409
  */
409
410
  unmute(channel) {
@@ -414,6 +415,15 @@ class User {
414
415
  });
415
416
  }
416
417
 
418
+ /**
419
+ * Is user in channel
420
+ * @param {string} channelName Channel name to use
421
+ * @returns {boolean}
422
+ */
423
+ inChannel(channelName) {
424
+ return this.channels.has(channelName);
425
+ }
426
+
417
427
  /**
418
428
  * When referenced as a string, output this users name instead of an object type
419
429
  * @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}
package/util/Constants.js CHANGED
@@ -60,6 +60,7 @@ export const Status = {
60
60
  export const OPCodes = {
61
61
  SESSION: 'session',
62
62
  JOIN: 'join',
63
+ LEAVE: 'leave',
63
64
  CHAT: 'chat',
64
65
  INVITE: 'invite',
65
66
  CHANGE_NICK: 'changenick',
@@ -85,9 +85,9 @@ class SocketHandler extends EventEmitter {
85
85
  /**
86
86
  * Store new client session id when assigned
87
87
  */
88
- this.client.on(Events.SESSION, (data) =>
89
- this.sessionID = data.token,
90
- );
88
+ this.client.on(Events.SESSION, (data) => {
89
+ this.sessionID = data.token;
90
+ });
91
91
 
92
92
  // Initiate connection
93
93
  this.connect(gateway);
@@ -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
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));