webmaxsocket 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +300 -16
- package/config/example.json +6 -0
- package/example-ios.js +186 -0
- package/example-sms.js +131 -0
- package/example-token.js +100 -0
- package/example.js +2 -0
- package/index.js +2 -0
- package/lib/client.js +510 -54
- package/lib/opcodes.js +3 -1
- package/lib/socketTransport.js +296 -0
- package/lib/userAgent.js +8 -4
- package/package.json +16 -3
package/lib/opcodes.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Opcodes для протокола Max API
|
|
3
|
-
* Портировано из PyMax
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
5
|
const Opcode = {
|
|
@@ -10,6 +9,8 @@ const Opcode = {
|
|
|
10
9
|
LOG: 5,
|
|
11
10
|
SESSION_INIT: 6,
|
|
12
11
|
PROFILE: 16,
|
|
12
|
+
AUTH_REQUEST: 17,
|
|
13
|
+
AUTH: 18,
|
|
13
14
|
LOGIN: 19,
|
|
14
15
|
LOGOUT: 20,
|
|
15
16
|
SYNC: 21,
|
|
@@ -40,6 +41,7 @@ const Opcode = {
|
|
|
40
41
|
NOTIF_MESSAGE: 128,
|
|
41
42
|
NOTIF_CHAT: 135,
|
|
42
43
|
NOTIF_ATTACH: 136,
|
|
44
|
+
NOTIF_MSG_DELETE: 154,
|
|
43
45
|
NOTIF_MSG_REACTIONS_CHANGED: 155,
|
|
44
46
|
MSG_REACTION: 178,
|
|
45
47
|
MSG_CANCEL_REACTION: 179,
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Socket transport для Max API (api.oneme.ru:443)
|
|
3
|
+
* Бинарный протокол msgpack с LZ4 компрессией
|
|
4
|
+
* Используется для IOS/DESKTOP/ANDROID устройств
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const tls = require('tls');
|
|
8
|
+
const { encode: msgpackEncode, decode: msgpackDecode } = require('@msgpack/msgpack');
|
|
9
|
+
const lz4Binding = require('lz4/lib/binding.js');
|
|
10
|
+
const { v4: uuidv4 } = require('uuid');
|
|
11
|
+
const { Opcode, getOpcodeName } = require('./opcodes');
|
|
12
|
+
const { UserAgentPayload } = require('./userAgent');
|
|
13
|
+
|
|
14
|
+
const HOST = 'api.oneme.ru';
|
|
15
|
+
const PORT = 443;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Формирует бинарный пакет
|
|
19
|
+
* Header: 1b ver, 2b cmd, 1b seq, 2b opcode, 4b payload_len
|
|
20
|
+
*/
|
|
21
|
+
function packPacket(ver, cmd, seq, opcode, payload) {
|
|
22
|
+
const payloadBuf = payload ? Buffer.from(msgpackEncode(payload)) : Buffer.alloc(0);
|
|
23
|
+
const payloadLen = payloadBuf.length & 0xFFFFFF;
|
|
24
|
+
const buf = Buffer.allocUnsafe(10 + payloadBuf.length);
|
|
25
|
+
buf.writeUInt8(ver, 0);
|
|
26
|
+
buf.writeUInt16BE(cmd, 1);
|
|
27
|
+
buf.writeUInt8(seq % 256, 3);
|
|
28
|
+
buf.writeUInt16BE(opcode, 4);
|
|
29
|
+
buf.writeUInt32BE(payloadLen, 6);
|
|
30
|
+
if (payloadBuf.length) payloadBuf.copy(buf, 10);
|
|
31
|
+
return buf;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Парсит ответ. compFlag: raw LZ4 block.
|
|
36
|
+
*/
|
|
37
|
+
function unpackPacket(data) {
|
|
38
|
+
if (data.length < 10) return null;
|
|
39
|
+
const ver = data.readUInt8(0);
|
|
40
|
+
const cmd = data.readUInt16BE(1);
|
|
41
|
+
const seq = data.readUInt8(3);
|
|
42
|
+
const opcode = data.readUInt16BE(4);
|
|
43
|
+
const packedLen = data.readUInt32BE(6);
|
|
44
|
+
const compFlag = packedLen >> 24;
|
|
45
|
+
const payloadLength = packedLen & 0xFFFFFF;
|
|
46
|
+
let payload = null;
|
|
47
|
+
|
|
48
|
+
if (payloadLength > 0 && data.length >= 10 + payloadLength) {
|
|
49
|
+
const payloadBytes = Buffer.from(data.subarray(10, 10 + payloadLength));
|
|
50
|
+
try {
|
|
51
|
+
if (compFlag !== 0) {
|
|
52
|
+
const out = Buffer.alloc(Math.max(payloadLength * 20, 256 * 1024));
|
|
53
|
+
const n = lz4Binding.uncompress(payloadBytes, out);
|
|
54
|
+
if (n > 0) payload = msgpackDecode(out.subarray(0, n));
|
|
55
|
+
} else {
|
|
56
|
+
payload = msgpackDecode(payloadBytes);
|
|
57
|
+
}
|
|
58
|
+
} catch (_) {
|
|
59
|
+
payload = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { ver, cmd, seq, opcode, payload };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readExactlyFromBuffer(transport, n) {
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
const tryResolve = () => {
|
|
69
|
+
if (transport._recvBuffer.length >= n) {
|
|
70
|
+
const result = transport._recvBuffer.subarray(0, n);
|
|
71
|
+
transport._recvBuffer = transport._recvBuffer.subarray(n);
|
|
72
|
+
resolve(Buffer.from(result));
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
};
|
|
77
|
+
if (tryResolve()) return;
|
|
78
|
+
|
|
79
|
+
const onData = () => {
|
|
80
|
+
if (tryResolve()) transport.socket.removeListener('data', onData);
|
|
81
|
+
};
|
|
82
|
+
transport.socket.on('data', onData);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
class MaxSocketTransport {
|
|
87
|
+
constructor(options = {}) {
|
|
88
|
+
this.host = options.host || HOST;
|
|
89
|
+
this.port = options.port || PORT;
|
|
90
|
+
this.deviceId = options.deviceId || uuidv4();
|
|
91
|
+
this.deviceType = options.deviceType || 'IOS';
|
|
92
|
+
this.ua = options.ua || options.headerUserAgent ||
|
|
93
|
+
'Mozilla/5.0 (iPhone15,2; CPU iPhone OS 18_6_2 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/602.1.50';
|
|
94
|
+
this.debug = options.debug || false;
|
|
95
|
+
|
|
96
|
+
this.socket = null;
|
|
97
|
+
this.seq = 0;
|
|
98
|
+
this.ver = 11;
|
|
99
|
+
this.pending = new Map();
|
|
100
|
+
this._recvBuffer = Buffer.alloc(0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
_log(...args) {
|
|
104
|
+
if (this.debug) console.log('[Socket]', ...args);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
connect() {
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
const opts = {
|
|
110
|
+
host: this.host,
|
|
111
|
+
port: this.port,
|
|
112
|
+
servername: this.host,
|
|
113
|
+
rejectUnauthorized: true
|
|
114
|
+
};
|
|
115
|
+
const sock = tls.connect(opts, () => {
|
|
116
|
+
this.socket = sock;
|
|
117
|
+
this._recvBuffer = Buffer.alloc(0);
|
|
118
|
+
sock.on('data', (chunk) => {
|
|
119
|
+
this._recvBuffer = Buffer.concat([this._recvBuffer, chunk]);
|
|
120
|
+
});
|
|
121
|
+
this._log('Connected to', this.host + ':' + this.port);
|
|
122
|
+
this._startRecvLoop();
|
|
123
|
+
resolve();
|
|
124
|
+
});
|
|
125
|
+
sock.on('error', reject);
|
|
126
|
+
sock.setKeepAlive(true);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
_makeMessage(opcode, payload, cmd = 0) {
|
|
131
|
+
this.seq++;
|
|
132
|
+
return {
|
|
133
|
+
ver: this.ver,
|
|
134
|
+
cmd,
|
|
135
|
+
seq: this.seq,
|
|
136
|
+
opcode,
|
|
137
|
+
payload: payload || {}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
_startRecvLoop() {
|
|
142
|
+
const readNext = async () => {
|
|
143
|
+
if (!this.socket || this.socket.destroyed) return;
|
|
144
|
+
try {
|
|
145
|
+
const header = await readExactlyFromBuffer(this, 10);
|
|
146
|
+
const packedLen = header.readUInt32BE(6);
|
|
147
|
+
const payloadLen = packedLen & 0xFFFFFF;
|
|
148
|
+
let payloadData = Buffer.alloc(0);
|
|
149
|
+
if (payloadLen > 0) {
|
|
150
|
+
payloadData = await readExactlyFromBuffer(this, payloadLen);
|
|
151
|
+
}
|
|
152
|
+
const packet = unpackPacket(Buffer.concat([header, payloadData]));
|
|
153
|
+
if (!packet) return readNext();
|
|
154
|
+
|
|
155
|
+
const seqKey = packet.seq % 256;
|
|
156
|
+
const payloads = Array.isArray(packet.payload) ? packet.payload : [packet.payload];
|
|
157
|
+
for (const p of payloads) {
|
|
158
|
+
const data = { ...packet, payload: p };
|
|
159
|
+
const pending = this.pending.get(seqKey);
|
|
160
|
+
if (pending) {
|
|
161
|
+
this.pending.delete(seqKey);
|
|
162
|
+
pending.resolve(data);
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
if (this.onNotification) this.onNotification(data);
|
|
166
|
+
}
|
|
167
|
+
} catch (e) {
|
|
168
|
+
if (this.socket && !this.socket.destroyed) {
|
|
169
|
+
this._log('Recv error:', e.message);
|
|
170
|
+
}
|
|
171
|
+
for (const [, p] of this.pending) p.reject(e);
|
|
172
|
+
this.pending.clear();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
readNext();
|
|
176
|
+
};
|
|
177
|
+
readNext();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async sendAndWait(opcode, payload, cmd = 0, timeout = 20000) {
|
|
181
|
+
if (!this.socket || this.socket.destroyed) throw new Error('Socket not connected');
|
|
182
|
+
|
|
183
|
+
const msg = this._makeMessage(opcode, payload, cmd);
|
|
184
|
+
const seqKey = msg.seq % 256;
|
|
185
|
+
const packet = packPacket(msg.ver, msg.cmd, msg.seq, msg.opcode, msg.payload);
|
|
186
|
+
|
|
187
|
+
let pendingRef;
|
|
188
|
+
const promise = new Promise((resolve, reject) => {
|
|
189
|
+
const t = setTimeout(() => {
|
|
190
|
+
const p = this.pending.get(seqKey);
|
|
191
|
+
if (p) {
|
|
192
|
+
this.pending.delete(seqKey);
|
|
193
|
+
p.reject(new Error(`Timeout waiting for opcode ${getOpcodeName(opcode)}`));
|
|
194
|
+
}
|
|
195
|
+
}, timeout);
|
|
196
|
+
pendingRef = {
|
|
197
|
+
resolve: (data) => { clearTimeout(t); resolve(data); },
|
|
198
|
+
reject: (err) => { clearTimeout(t); reject(err); }
|
|
199
|
+
};
|
|
200
|
+
this.pending.set(seqKey, pendingRef);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
this.socket.write(packet, (err) => {
|
|
204
|
+
if (err) {
|
|
205
|
+
const p = this.pending.get(seqKey);
|
|
206
|
+
if (p) {
|
|
207
|
+
this.pending.delete(seqKey);
|
|
208
|
+
p.reject(err);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const result = await promise;
|
|
214
|
+
if (result.payload && result.payload.error) {
|
|
215
|
+
const errMsg = result.payload.localizedMessage || result.payload.error?.message || JSON.stringify(result.payload.error);
|
|
216
|
+
throw new Error(errMsg);
|
|
217
|
+
}
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async handshake(userAgentPayload) {
|
|
222
|
+
const ua = userAgentPayload || new UserAgentPayload({
|
|
223
|
+
deviceType: this.deviceType,
|
|
224
|
+
headerUserAgent: this.ua,
|
|
225
|
+
appVersion: '25.12.14',
|
|
226
|
+
osVersion: '18.6.2',
|
|
227
|
+
deviceName: 'Safari',
|
|
228
|
+
screen: '390x844 3.0x'
|
|
229
|
+
});
|
|
230
|
+
const payload = {
|
|
231
|
+
deviceId: this.deviceId,
|
|
232
|
+
userAgent: typeof ua.toJSON === 'function' ? ua.toJSON() : ua
|
|
233
|
+
};
|
|
234
|
+
const resp = await this.sendAndWait(Opcode.SESSION_INIT, payload);
|
|
235
|
+
this._log('Handshake OK');
|
|
236
|
+
return resp;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async requestCode(phone, language = 'ru') {
|
|
240
|
+
const payload = { phone, type: 'START_AUTH', language };
|
|
241
|
+
const data = await this.sendAndWait(Opcode.AUTH_REQUEST, payload);
|
|
242
|
+
return data.payload?.token || null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async sendCode(tempToken, code) {
|
|
246
|
+
const payload = {
|
|
247
|
+
token: tempToken,
|
|
248
|
+
verifyCode: code,
|
|
249
|
+
authTokenType: 'CHECK_CODE'
|
|
250
|
+
};
|
|
251
|
+
const data = await this.sendAndWait(Opcode.AUTH, payload);
|
|
252
|
+
return data.payload;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async sync(token, userAgentJson) {
|
|
256
|
+
const payload = {
|
|
257
|
+
interactive: true,
|
|
258
|
+
token,
|
|
259
|
+
chatsSync: 0,
|
|
260
|
+
contactsSync: 0,
|
|
261
|
+
presenceSync: 0,
|
|
262
|
+
draftsSync: 0,
|
|
263
|
+
chatsCount: 40
|
|
264
|
+
};
|
|
265
|
+
if (userAgentJson) payload.userAgent = userAgentJson;
|
|
266
|
+
const data = await this.sendAndWait(Opcode.LOGIN, payload);
|
|
267
|
+
return data.payload;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async getChats(marker = 0) {
|
|
271
|
+
const data = await this.sendAndWait(Opcode.CHATS_LIST, { marker });
|
|
272
|
+
return data.payload?.chats || [];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async getHistory(chatId, from = Date.now(), backward = 200, forward = 0) {
|
|
276
|
+
const data = await this.sendAndWait(Opcode.CHAT_HISTORY, {
|
|
277
|
+
chatId,
|
|
278
|
+
from,
|
|
279
|
+
forward,
|
|
280
|
+
backward,
|
|
281
|
+
getMessages: true
|
|
282
|
+
});
|
|
283
|
+
return data.payload?.messages || [];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async close() {
|
|
287
|
+
if (this.socket) {
|
|
288
|
+
this.socket.destroy();
|
|
289
|
+
this.socket = null;
|
|
290
|
+
}
|
|
291
|
+
for (const [, p] of this.pending) p.reject(new Error('Connection closed'));
|
|
292
|
+
this.pending.clear();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
module.exports = { MaxSocketTransport, packPacket, unpackPacket, HOST, PORT };
|
package/lib/userAgent.js
CHANGED
|
@@ -42,10 +42,11 @@ function randomInt(min, max) {
|
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* Создает UserAgent пейлоад для Max API
|
|
45
|
+
* Поддерживает WEB, IOS и кастомные профили (для token auth)
|
|
45
46
|
*/
|
|
46
47
|
class UserAgentPayload {
|
|
47
48
|
constructor(options = {}) {
|
|
48
|
-
this.deviceType = 'WEB';
|
|
49
|
+
this.deviceType = options.deviceType || 'WEB';
|
|
49
50
|
this.locale = options.locale || 'ru';
|
|
50
51
|
this.deviceLocale = options.deviceLocale || 'ru';
|
|
51
52
|
this.osVersion = options.osVersion || randomChoice(OS_VERSIONS);
|
|
@@ -54,15 +55,16 @@ class UserAgentPayload {
|
|
|
54
55
|
this.appVersion = options.appVersion || '25.12.14';
|
|
55
56
|
this.screen = options.screen || randomChoice(SCREEN_SIZES);
|
|
56
57
|
this.timezone = options.timezone || randomChoice(TIMEZONES);
|
|
57
|
-
this.clientSessionId = options.clientSessionId
|
|
58
|
-
this.buildNumber = options.buildNumber
|
|
58
|
+
this.clientSessionId = options.clientSessionId ?? randomInt(1, 15);
|
|
59
|
+
this.buildNumber = options.buildNumber ?? 0x97CB;
|
|
60
|
+
this.release = options.release;
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
/**
|
|
62
64
|
* Преобразует в объект для отправки (camelCase ключи)
|
|
63
65
|
*/
|
|
64
66
|
toJSON() {
|
|
65
|
-
|
|
67
|
+
const obj = {
|
|
66
68
|
deviceType: this.deviceType,
|
|
67
69
|
locale: this.locale,
|
|
68
70
|
deviceLocale: this.deviceLocale,
|
|
@@ -75,6 +77,8 @@ class UserAgentPayload {
|
|
|
75
77
|
clientSessionId: this.clientSessionId,
|
|
76
78
|
buildNumber: this.buildNumber,
|
|
77
79
|
};
|
|
80
|
+
if (this.release !== undefined) obj.release = this.release;
|
|
81
|
+
return obj;
|
|
78
82
|
}
|
|
79
83
|
}
|
|
80
84
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webmaxsocket",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Node.js client for Max Messenger with QR code authentication",
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "Node.js client for Max Messenger with QR code and token authentication",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"start": "node example.js",
|
|
8
|
-
"example": "node example.js"
|
|
8
|
+
"example": "node example.js",
|
|
9
|
+
"example:token": "node example-token.js",
|
|
10
|
+
"example:sms": "node example-sms.js",
|
|
11
|
+
"example:ios": "node example-ios.js"
|
|
9
12
|
},
|
|
10
13
|
"keywords": [
|
|
11
14
|
"max",
|
|
@@ -18,6 +21,7 @@
|
|
|
18
21
|
"websocket",
|
|
19
22
|
"qr-code",
|
|
20
23
|
"qr-auth",
|
|
24
|
+
"token-auth",
|
|
21
25
|
"bot",
|
|
22
26
|
"nodejs",
|
|
23
27
|
"commonjs"
|
|
@@ -33,18 +37,27 @@
|
|
|
33
37
|
},
|
|
34
38
|
"homepage": "https://github.com/Tellarion/webmaxsocket#readme",
|
|
35
39
|
"dependencies": {
|
|
40
|
+
"@msgpack/msgpack": "^3.1.3",
|
|
41
|
+
"lz4": "^0.6.5",
|
|
36
42
|
"qrcode-terminal": "^0.12.0",
|
|
37
43
|
"uuid": "^9.0.0",
|
|
38
44
|
"ws": "^8.18.0"
|
|
39
45
|
},
|
|
46
|
+
"optionalDependencies": {
|
|
47
|
+
"lz4": "^0.6.5"
|
|
48
|
+
},
|
|
40
49
|
"devDependencies": {},
|
|
41
50
|
"engines": {
|
|
42
51
|
"node": ">=14.0.0"
|
|
43
52
|
},
|
|
44
53
|
"files": [
|
|
45
54
|
"lib/",
|
|
55
|
+
"config/",
|
|
46
56
|
"index.js",
|
|
47
57
|
"example.js",
|
|
58
|
+
"example-token.js",
|
|
59
|
+
"example-sms.js",
|
|
60
|
+
"example-ios.js",
|
|
48
61
|
"README.md"
|
|
49
62
|
]
|
|
50
63
|
}
|