@eryxenx/insta-chat 2.0.11 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -0
- package/DOCS.md +53 -11
- package/README.md +83 -3
- package/index.d.ts +103 -0
- package/index.js +6 -0
- package/package.json +2 -2
- package/src/api/realtime/listen.js +3 -1
- package/src/api/users/getProfileBio.js +33 -0
- package/src/api/users/getProfilePictureV2.js +54 -0
- package/src/api/users/getSelfInfo.js +27 -0
- package/src/api/users/getUserFriendship.js +42 -0
- package/src/api/users/getUserID.js +31 -0
- package/src/api/users/getUserInfoV2.js +62 -0
- package/src/api/users/getUserProfile.js +26 -0
- package/src/api.js +116 -2
- package/src/app/InstaClient.js +23 -0
- package/src/app/createFcaClient.js +13 -1
- package/src/constants.js +10 -1
- package/src/methods/inbox.js +82 -1
- package/src/methods/user.js +180 -4
- package/src/profileParse.js +250 -0
- package/src/realtime/bridge.js +76 -0
- package/src/realtime/client.js +376 -0
- package/src/realtime/listener.js +86 -0
- package/src/realtime/mqttot.js +609 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* insta-chat - Unofficial Instagram Chat API
|
|
3
|
+
* Copyright (c) 2026 EryXenX (https://github.com/EryXenX)
|
|
4
|
+
* Licensed under the Apache License, Version 2.0. See LICENSE and NOTICE.
|
|
5
|
+
* Original author credit must be retained in all copies and derivatives.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// The realtime listener: it opens the MQTT connection, subscribes to the Direct
|
|
9
|
+
// message-sync stream and feeds decoded messages to a callback as they arrive,
|
|
10
|
+
// instead of the inbox poller's interval. It is push, so a bot replies as soon
|
|
11
|
+
// as Instagram delivers the message.
|
|
12
|
+
//
|
|
13
|
+
// Real-time is best-effort. Instagram can drop the socket, refuse the iris
|
|
14
|
+
// subscription, or the process can run on a host that blocks the broker, so the
|
|
15
|
+
// listener reports failure instead of pretending to be connected; listen() keeps
|
|
16
|
+
// the poller as its fallback.
|
|
17
|
+
|
|
18
|
+
"use strict";
|
|
19
|
+
|
|
20
|
+
const { RealtimeClient } = require("./client");
|
|
21
|
+
const { rawEventFromMessageSync } = require("./bridge");
|
|
22
|
+
|
|
23
|
+
class RealtimeListener {
|
|
24
|
+
constructor(context, options = {}) {
|
|
25
|
+
this.context = context || {};
|
|
26
|
+
this.options = options;
|
|
27
|
+
this.connected = false;
|
|
28
|
+
this._client = null;
|
|
29
|
+
this._callback = null;
|
|
30
|
+
this._stopped = false;
|
|
31
|
+
this._subscribed = false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_emitError(err) {
|
|
35
|
+
if (typeof this.options.onError === "function") this.options.onError(err);
|
|
36
|
+
else if (this._callback) this._callback(err, null);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_handleMessageSync(event) {
|
|
40
|
+
if (this._stopped || !this._callback) return;
|
|
41
|
+
const raw = rawEventFromMessageSync(event, this.context.userId);
|
|
42
|
+
if (raw) this._callback(null, raw);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_resolveCursor() {
|
|
46
|
+
const opts = this.options;
|
|
47
|
+
if (opts.seqId != null && opts.snapshotAtMs != null) {
|
|
48
|
+
return { seqId: opts.seqId, snapshotAtMs: opts.snapshotAtMs };
|
|
49
|
+
}
|
|
50
|
+
if (typeof opts.getRealtimeState === "function") {
|
|
51
|
+
return Promise.resolve(opts.getRealtimeState()).then((state) => state || null);
|
|
52
|
+
}
|
|
53
|
+
return Promise.resolve(null);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async start(callback) {
|
|
57
|
+
this._callback = callback;
|
|
58
|
+
this._client = new RealtimeClient(this.context, this.options);
|
|
59
|
+
this._client.on("message", (event) => this._handleMessageSync(event));
|
|
60
|
+
this._client.on("error", (err) => this._emitError(err));
|
|
61
|
+
this._client.on("disconnected", () => {
|
|
62
|
+
this.connected = false;
|
|
63
|
+
if (!this._stopped) this._emitError(new Error("Realtime MQTT connection closed"));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await this._client.connect();
|
|
67
|
+
this.connected = true;
|
|
68
|
+
|
|
69
|
+
const cursor = await this._resolveCursor();
|
|
70
|
+
if (cursor && cursor.seqId != null && cursor.snapshotAtMs != null) {
|
|
71
|
+
await this._client.irisSubscribe(Number(cursor.seqId) || 0, Number(cursor.snapshotAtMs));
|
|
72
|
+
this._subscribed = true;
|
|
73
|
+
} else if (typeof this.options.onReady === "function") {
|
|
74
|
+
this.options.onReady(this);
|
|
75
|
+
}
|
|
76
|
+
return this;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
stop() {
|
|
80
|
+
this._stopped = true;
|
|
81
|
+
this.connected = false;
|
|
82
|
+
if (this._client) this._client.disconnect();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { RealtimeListener };
|
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* insta-chat - Unofficial Instagram Chat API
|
|
3
|
+
* Copyright (c) 2026 EryXenX (https://github.com/EryXenX)
|
|
4
|
+
* Licensed under the Apache License, Version 2.0. See LICENSE and NOTICE.
|
|
5
|
+
* Original author credit must be retained in all copies and derivatives.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// The MQTToT wire format Instagram's realtime/push brokers speak. It is MQTT
|
|
9
|
+
// 3.1.1 with a Facebook twist: the CONNECT payload is a Thrift compact struct
|
|
10
|
+
// (MQTToTConnection) instead of the spec's UTF-8 fields, and publish payloads
|
|
11
|
+
// are zlib-compressed. This module is a dependency-free codec (Thrift compact
|
|
12
|
+
// protocol + MQTT packets) plus a TLS transport, so the realtime client needs
|
|
13
|
+
// nothing beyond Node's net/tls/zlib.
|
|
14
|
+
|
|
15
|
+
"use strict";
|
|
16
|
+
|
|
17
|
+
const tls = require("tls");
|
|
18
|
+
const zlib = require("zlib");
|
|
19
|
+
|
|
20
|
+
const ThriftTypes = {
|
|
21
|
+
STOP: 0x00,
|
|
22
|
+
TRUE: 0x01,
|
|
23
|
+
FALSE: 0x02,
|
|
24
|
+
BYTE: 0x03,
|
|
25
|
+
INT_16: 0x04,
|
|
26
|
+
INT_32: 0x05,
|
|
27
|
+
INT_64: 0x06,
|
|
28
|
+
DOUBLE: 0x07,
|
|
29
|
+
BINARY: 0x08,
|
|
30
|
+
LIST: 0x09,
|
|
31
|
+
SET: 0x0a,
|
|
32
|
+
MAP: 0x0b,
|
|
33
|
+
STRUCT: 0x0c,
|
|
34
|
+
BOOLEAN: 0xa1,
|
|
35
|
+
LIST_INT_32: (0x05 << 8) | 0x09,
|
|
36
|
+
LIST_BINARY: (0x08 << 8) | 0x09,
|
|
37
|
+
MAP_BINARY_BINARY: (0x88 << 8) | 0x0b
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Thrift compact integers are zigzag + LEB128. Values above 2^53 (appId, a
|
|
41
|
+
// 64-bit userId) need BigInt, so the varint writer always works in BigInt.
|
|
42
|
+
function zigzag(value, bits) {
|
|
43
|
+
const v = typeof value === "bigint" ? value : BigInt(value);
|
|
44
|
+
return (v << 1n) ^ (v >> BigInt(bits - 1));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function unzigzag(value) {
|
|
48
|
+
const v = typeof value === "bigint" ? value : BigInt(value);
|
|
49
|
+
return (v >> 1n) ^ -(v & 1n);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class ThriftWriter {
|
|
53
|
+
constructor() {
|
|
54
|
+
this.buffer = [];
|
|
55
|
+
this._fieldStack = [];
|
|
56
|
+
this._field = 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
writeStop() {
|
|
60
|
+
this.buffer.push(ThriftTypes.STOP);
|
|
61
|
+
if (this._fieldStack.length) this._field = this._fieldStack.pop();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
writeField(field, fieldType) {
|
|
65
|
+
const delta = field - this._field;
|
|
66
|
+
const thriftType = fieldType & 0x0f;
|
|
67
|
+
if (delta > 0 && delta <= 15) {
|
|
68
|
+
this.buffer.push((delta << 4) | thriftType);
|
|
69
|
+
} else {
|
|
70
|
+
this.buffer.push(thriftType);
|
|
71
|
+
this.writeVarint(zigzag(field, 16));
|
|
72
|
+
}
|
|
73
|
+
this._field = field;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
writeVarint(value) {
|
|
77
|
+
let v = typeof value === "bigint" ? value : BigInt(value);
|
|
78
|
+
if (v < 0n) v &= 0xffffffffffffffffn;
|
|
79
|
+
for (;;) {
|
|
80
|
+
if ((v & ~0x7fn) === 0n) {
|
|
81
|
+
this.buffer.push(Number(v));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
this.buffer.push(Number((v & 0x7fn) | 0x80n));
|
|
85
|
+
v >>= 7n;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
writeStringDirect(value) {
|
|
90
|
+
const raw = Buffer.from(String(value), "utf8");
|
|
91
|
+
this.writeVarint(raw.length);
|
|
92
|
+
for (const b of raw) this.buffer.push(b);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
writeString(field, value) {
|
|
96
|
+
this.writeField(field, ThriftTypes.BINARY);
|
|
97
|
+
this.writeStringDirect(value);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
writeBool(field, value) {
|
|
101
|
+
this.writeField(field, value ? ThriftTypes.TRUE : ThriftTypes.FALSE);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
writeInt(field, value, bits) {
|
|
105
|
+
const fieldType = { 8: ThriftTypes.BYTE, 16: ThriftTypes.INT_16, 32: ThriftTypes.INT_32, 64: ThriftTypes.INT_64 }[bits];
|
|
106
|
+
this.writeField(field, fieldType);
|
|
107
|
+
if (bits === 8) {
|
|
108
|
+
this.buffer.push(Number(BigInt(value) & 0xffn));
|
|
109
|
+
} else {
|
|
110
|
+
this.writeVarint(zigzag(value, bits));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
pushStruct(field) {
|
|
115
|
+
this.writeField(field, ThriftTypes.STRUCT);
|
|
116
|
+
this._fieldStack.push(this._field);
|
|
117
|
+
this._field = 0;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
class ThriftReader {
|
|
122
|
+
constructor(data) {
|
|
123
|
+
this.data = data;
|
|
124
|
+
this.pos = 0;
|
|
125
|
+
this._fieldStack = [];
|
|
126
|
+
this._field = 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
readStruct(descriptors) {
|
|
130
|
+
const result = {};
|
|
131
|
+
while (this.pos < this.data.length) {
|
|
132
|
+
const fieldType = this.readField();
|
|
133
|
+
if (fieldType === ThriftTypes.STOP) {
|
|
134
|
+
if (this._fieldStack.length) this._field = this._fieldStack.pop();
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
const descriptor = findDescriptor(descriptors, this._field, fieldType);
|
|
138
|
+
const value = this.readValue(fieldType, descriptor);
|
|
139
|
+
if (descriptor) result[descriptor.name] = value;
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
readField() {
|
|
145
|
+
const byte = this.readByte();
|
|
146
|
+
if (byte === ThriftTypes.STOP) return ThriftTypes.STOP;
|
|
147
|
+
const delta = (byte & 0xf0) >> 4;
|
|
148
|
+
if (delta) this._field += delta;
|
|
149
|
+
else this._field = Number(unzigzag(this.readVarint()));
|
|
150
|
+
return byte & 0x0f;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
readValue(fieldType, descriptor) {
|
|
154
|
+
if (fieldType === ThriftTypes.TRUE) return true;
|
|
155
|
+
if (fieldType === ThriftTypes.FALSE) return false;
|
|
156
|
+
if (fieldType === ThriftTypes.BYTE) return this.readBytes(1).readInt8(0);
|
|
157
|
+
if (fieldType === ThriftTypes.INT_16 || fieldType === ThriftTypes.INT_32 || fieldType === ThriftTypes.INT_64) {
|
|
158
|
+
return Number(unzigzag(this.readVarint()));
|
|
159
|
+
}
|
|
160
|
+
if (fieldType === ThriftTypes.BINARY) return this.readBytes(Number(this.readVarint())).toString("utf8");
|
|
161
|
+
if (fieldType === ThriftTypes.LIST) return this.readList();
|
|
162
|
+
if (fieldType === ThriftTypes.MAP) return this.readMap();
|
|
163
|
+
if (fieldType === ThriftTypes.STRUCT) {
|
|
164
|
+
this._fieldStack.push(this._field);
|
|
165
|
+
this._field = 0;
|
|
166
|
+
return this.readStruct(descriptor ? descriptor.children : []);
|
|
167
|
+
}
|
|
168
|
+
throw new Error(`Unsupported thrift type: ${fieldType}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
readList() {
|
|
172
|
+
const header = this.readByte();
|
|
173
|
+
let size = header >> 4;
|
|
174
|
+
const itemType = header & 0x0f;
|
|
175
|
+
if (size === 0x0f) size = Number(this.readVarint());
|
|
176
|
+
const items = [];
|
|
177
|
+
for (let i = 0; i < size; i++) items.push(this.readValue(itemType, null));
|
|
178
|
+
return items;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
readMap() {
|
|
182
|
+
const size = Number(this.readVarint());
|
|
183
|
+
if (!size) return {};
|
|
184
|
+
const itemTypes = this.readByte();
|
|
185
|
+
const keyType = (itemTypes & 0xf0) >> 4;
|
|
186
|
+
const valueType = itemTypes & 0x0f;
|
|
187
|
+
const result = {};
|
|
188
|
+
for (let i = 0; i < size; i++) {
|
|
189
|
+
const key = this.readValue(keyType, null);
|
|
190
|
+
result[key] = this.readValue(valueType, null);
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
readByte() {
|
|
196
|
+
const value = this.data[this.pos];
|
|
197
|
+
this.pos += 1;
|
|
198
|
+
return value;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
readBytes(size) {
|
|
202
|
+
const value = this.data.slice(this.pos, this.pos + Number(size));
|
|
203
|
+
this.pos += Number(size);
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
readVarint() {
|
|
208
|
+
let shift = 0n;
|
|
209
|
+
let result = 0n;
|
|
210
|
+
while (this.pos < this.data.length) {
|
|
211
|
+
const byte = this.readByte();
|
|
212
|
+
result |= BigInt(byte & 0x7f) << shift;
|
|
213
|
+
if ((byte & 0x80) === 0) break;
|
|
214
|
+
shift += 7n;
|
|
215
|
+
}
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function findDescriptor(descriptors, field, fieldType) {
|
|
221
|
+
for (const descriptor of descriptors) {
|
|
222
|
+
if (descriptor.field !== field) continue;
|
|
223
|
+
const descriptorType = descriptor.type & 0x0f;
|
|
224
|
+
if (descriptorType === fieldType) return descriptor;
|
|
225
|
+
if (descriptor.type === ThriftTypes.BOOLEAN && (fieldType === ThriftTypes.TRUE || fieldType === ThriftTypes.FALSE)) return descriptor;
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function writeThriftStruct(writer, data, descriptors) {
|
|
231
|
+
const byName = {};
|
|
232
|
+
for (const d of descriptors) byName[d.name] = d;
|
|
233
|
+
for (const [name, value] of Object.entries(data)) {
|
|
234
|
+
if (value === null || value === undefined) continue;
|
|
235
|
+
const descriptor = byName[name];
|
|
236
|
+
if (!descriptor) continue;
|
|
237
|
+
const thriftType = descriptor.type & 0xff;
|
|
238
|
+
if (thriftType === ThriftTypes.BOOLEAN) writer.writeBool(descriptor.field, !!value);
|
|
239
|
+
else if (thriftType === ThriftTypes.BYTE) writer.writeInt(descriptor.field, value, 8);
|
|
240
|
+
else if (thriftType === ThriftTypes.INT_16) writer.writeInt(descriptor.field, value, 16);
|
|
241
|
+
else if (thriftType === ThriftTypes.INT_32) writer.writeInt(descriptor.field, value, 32);
|
|
242
|
+
else if (thriftType === ThriftTypes.INT_64) writer.writeInt(descriptor.field, value, 64);
|
|
243
|
+
else if (thriftType === ThriftTypes.BINARY) writer.writeString(descriptor.field, value);
|
|
244
|
+
else if (thriftType === ThriftTypes.STRUCT) {
|
|
245
|
+
writer.pushStruct(descriptor.field);
|
|
246
|
+
writeThriftStruct(writer, value, descriptor.children);
|
|
247
|
+
writer.writeStop();
|
|
248
|
+
} else if (thriftType === ThriftTypes.LIST) {
|
|
249
|
+
writer.writeField(descriptor.field, ThriftTypes.LIST);
|
|
250
|
+
writeThriftList(writer, descriptor.type >> 8, value);
|
|
251
|
+
} else if (thriftType === ThriftTypes.MAP) {
|
|
252
|
+
writeThriftMap(writer, descriptor.field, value);
|
|
253
|
+
} else {
|
|
254
|
+
throw new Error(`Unsupported thrift type: ${descriptor.type}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function writeThriftList(writer, itemType, values) {
|
|
260
|
+
const size = values.length;
|
|
261
|
+
if (size < 0x0f) writer.buffer.push((size << 4) | itemType);
|
|
262
|
+
else {
|
|
263
|
+
writer.buffer.push(0xf0 | itemType);
|
|
264
|
+
writer.writeVarint(size);
|
|
265
|
+
}
|
|
266
|
+
for (const value of values) {
|
|
267
|
+
if (itemType === ThriftTypes.INT_32) writer.writeVarint(zigzag(value, 32));
|
|
268
|
+
else if (itemType === ThriftTypes.BINARY) writer.writeStringDirect(value);
|
|
269
|
+
else throw new Error(`Unsupported thrift list type: ${itemType}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function writeThriftMap(writer, field, values) {
|
|
274
|
+
writer.writeField(field, ThriftTypes.MAP);
|
|
275
|
+
const keys = Object.keys(values);
|
|
276
|
+
writer.writeVarint(keys.length);
|
|
277
|
+
if (!keys.length) return;
|
|
278
|
+
writer.buffer.push((ThriftTypes.BINARY << 4) | ThriftTypes.BINARY);
|
|
279
|
+
for (const key of keys) {
|
|
280
|
+
writer.writeStringDirect(key);
|
|
281
|
+
writer.writeStringDirect(values[key]);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function writeThriftObject(data, descriptors) {
|
|
286
|
+
const writer = new ThriftWriter();
|
|
287
|
+
writeThriftStruct(writer, data, descriptors);
|
|
288
|
+
writer.writeStop();
|
|
289
|
+
return Buffer.from(writer.buffer);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// The CONNECT payload: a Thrift compact MQTToTConnection struct.
|
|
293
|
+
const CONNECTION_DESCRIPTORS = [
|
|
294
|
+
{ name: "clientIdentifier", field: 1, type: ThriftTypes.BINARY, children: [] },
|
|
295
|
+
{ name: "willTopic", field: 2, type: ThriftTypes.BINARY, children: [] },
|
|
296
|
+
{ name: "willMessage", field: 3, type: ThriftTypes.BINARY, children: [] },
|
|
297
|
+
{
|
|
298
|
+
name: "clientInfo",
|
|
299
|
+
field: 4,
|
|
300
|
+
type: ThriftTypes.STRUCT,
|
|
301
|
+
children: [
|
|
302
|
+
{ name: "userId", field: 1, type: ThriftTypes.INT_64, children: [] },
|
|
303
|
+
{ name: "userAgent", field: 2, type: ThriftTypes.BINARY, children: [] },
|
|
304
|
+
{ name: "clientCapabilities", field: 3, type: ThriftTypes.INT_64, children: [] },
|
|
305
|
+
{ name: "endpointCapabilities", field: 4, type: ThriftTypes.INT_64, children: [] },
|
|
306
|
+
{ name: "publishFormat", field: 5, type: ThriftTypes.INT_32, children: [] },
|
|
307
|
+
{ name: "noAutomaticForeground", field: 6, type: ThriftTypes.BOOLEAN, children: [] },
|
|
308
|
+
{ name: "makeUserAvailableInForeground", field: 7, type: ThriftTypes.BOOLEAN, children: [] },
|
|
309
|
+
{ name: "deviceId", field: 8, type: ThriftTypes.BINARY, children: [] },
|
|
310
|
+
{ name: "isInitiallyForeground", field: 9, type: ThriftTypes.BOOLEAN, children: [] },
|
|
311
|
+
{ name: "networkType", field: 10, type: ThriftTypes.INT_32, children: [] },
|
|
312
|
+
{ name: "networkSubtype", field: 11, type: ThriftTypes.INT_32, children: [] },
|
|
313
|
+
{ name: "clientMqttSessionId", field: 12, type: ThriftTypes.INT_64, children: [] },
|
|
314
|
+
{ name: "clientIpAddress", field: 13, type: ThriftTypes.BINARY, children: [] },
|
|
315
|
+
{ name: "subscribeTopics", field: 14, type: ThriftTypes.LIST_INT_32, children: [] },
|
|
316
|
+
{ name: "clientType", field: 15, type: ThriftTypes.BINARY, children: [] },
|
|
317
|
+
{ name: "appId", field: 16, type: ThriftTypes.INT_64, children: [] },
|
|
318
|
+
{ name: "overrideNectarLogging", field: 17, type: ThriftTypes.BOOLEAN, children: [] },
|
|
319
|
+
{ name: "connectTokenHash", field: 18, type: ThriftTypes.BINARY, children: [] },
|
|
320
|
+
{ name: "regionPreference", field: 19, type: ThriftTypes.BINARY, children: [] },
|
|
321
|
+
{ name: "deviceSecret", field: 20, type: ThriftTypes.BINARY, children: [] },
|
|
322
|
+
{ name: "clientStack", field: 21, type: ThriftTypes.BYTE, children: [] },
|
|
323
|
+
{ name: "fbnsConnectionKey", field: 22, type: ThriftTypes.INT_64, children: [] },
|
|
324
|
+
{ name: "fbnsConnectionSecret", field: 23, type: ThriftTypes.BINARY, children: [] },
|
|
325
|
+
{ name: "fbnsDeviceId", field: 24, type: ThriftTypes.BINARY, children: [] },
|
|
326
|
+
{ name: "fbnsDeviceSecret", field: 25, type: ThriftTypes.BINARY, children: [] },
|
|
327
|
+
{ name: "anotherUnknown", field: 26, type: ThriftTypes.INT_64, children: [] }
|
|
328
|
+
]
|
|
329
|
+
},
|
|
330
|
+
{ name: "password", field: 5, type: ThriftTypes.BINARY, children: [] },
|
|
331
|
+
{ name: "getDiffsRequests", field: 6, type: ThriftTypes.LIST_BINARY, children: [] },
|
|
332
|
+
{ name: "zeroRatingTokenHash", field: 9, type: ThriftTypes.BINARY, children: [] },
|
|
333
|
+
{ name: "appSpecificInfo", field: 10, type: ThriftTypes.MAP_BINARY_BINARY, children: [] }
|
|
334
|
+
];
|
|
335
|
+
|
|
336
|
+
class MQTToTConnection {
|
|
337
|
+
constructor({ clientIdentifier, clientInfo, password, appSpecificInfo }) {
|
|
338
|
+
this.clientIdentifier = clientIdentifier;
|
|
339
|
+
this.clientInfo = clientInfo;
|
|
340
|
+
this.password = password;
|
|
341
|
+
this.appSpecificInfo = appSpecificInfo || null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
toThrift() {
|
|
345
|
+
const payload = {
|
|
346
|
+
clientIdentifier: this.clientIdentifier,
|
|
347
|
+
clientInfo: this.clientInfo,
|
|
348
|
+
password: this.password
|
|
349
|
+
};
|
|
350
|
+
if (this.appSpecificInfo) payload.appSpecificInfo = this.appSpecificInfo;
|
|
351
|
+
return writeThriftObject(payload, CONNECTION_DESCRIPTORS);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function compressPayload(data) {
|
|
356
|
+
return zlib.deflateSync(data, { level: 9 });
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Publish payloads are zlib streams (0x78 ...). Anything else is left as-is.
|
|
360
|
+
function tryDecompressPayload(data) {
|
|
361
|
+
if (!data || !data.length || data[0] !== 0x78) return data;
|
|
362
|
+
try {
|
|
363
|
+
return zlib.inflateSync(data);
|
|
364
|
+
} catch (_) {
|
|
365
|
+
return data;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function writeConnectPacket(connection, keepAlive = 20) {
|
|
370
|
+
const payload = compressPayload(connection.toThrift());
|
|
371
|
+
const header = Buffer.concat([
|
|
372
|
+
writeUtf8("MQTToT"),
|
|
373
|
+
Buffer.from([3, 0xc2]),
|
|
374
|
+
uint16BE(keepAlive)
|
|
375
|
+
]);
|
|
376
|
+
const remaining = Buffer.concat([header, payload]);
|
|
377
|
+
return Buffer.concat([Buffer.from([0x10]), encodeRemainingLength(remaining.length), remaining]);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function writePublishPacket(topic, payload, qos = 1, packetId = 1) {
|
|
381
|
+
if (qos !== 0 && qos !== 1) throw new Error("Only QoS 0 and QoS 1 are supported");
|
|
382
|
+
let body = writeUtf8(String(topic));
|
|
383
|
+
if (qos) body = Buffer.concat([body, uint16BE(packetId)]);
|
|
384
|
+
body = Buffer.concat([body, payload]);
|
|
385
|
+
return Buffer.concat([Buffer.from([0x30 | (qos << 1)]), encodeRemainingLength(body.length), body]);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function writeSubscribePacket(topic, packetId = 1, qos = 1) {
|
|
389
|
+
const body = Buffer.concat([uint16BE(packetId), writeUtf8(String(topic)), Buffer.from([qos])]);
|
|
390
|
+
return Buffer.concat([Buffer.from([0x82]), encodeRemainingLength(body.length), body]);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function writePingreqPacket() {
|
|
394
|
+
return Buffer.from([0xc0, 0x00]);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function writeDisconnectPacket() {
|
|
398
|
+
return Buffer.from([0xe0, 0x00]);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function decodePacket(packet) {
|
|
402
|
+
const packetTypeId = packet[0] >> 4;
|
|
403
|
+
const flags = packet[0] & 0x0f;
|
|
404
|
+
const { value: remainingLength, offset } = decodeRemainingLength(packet, 1);
|
|
405
|
+
const body = packet.slice(offset, offset + remainingLength);
|
|
406
|
+
if (packetTypeId === 1) {
|
|
407
|
+
const { value: protocolName, offset: pos } = readUtf8(body, 0);
|
|
408
|
+
return {
|
|
409
|
+
packetType: "connect",
|
|
410
|
+
protocolName,
|
|
411
|
+
protocolLevel: body[pos],
|
|
412
|
+
connectFlags: body[pos + 1],
|
|
413
|
+
keepAlive: body.readUInt16BE(pos + 2),
|
|
414
|
+
payload: body.slice(pos + 4)
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
if (packetTypeId === 2) {
|
|
418
|
+
return { packetType: "connack", returnCode: body[1], payload: body.slice(2) };
|
|
419
|
+
}
|
|
420
|
+
if (packetTypeId === 3) {
|
|
421
|
+
const { value: topic, offset: pos } = readUtf8(body, 0);
|
|
422
|
+
const qos = (flags >> 1) & 0x03;
|
|
423
|
+
let packetId = null;
|
|
424
|
+
let at = pos;
|
|
425
|
+
if (qos) {
|
|
426
|
+
packetId = body.readUInt16BE(at);
|
|
427
|
+
at += 2;
|
|
428
|
+
}
|
|
429
|
+
return { packetType: "publish", topic, qos, packetId, payload: body.slice(at) };
|
|
430
|
+
}
|
|
431
|
+
if (packetTypeId === 12) return { packetType: "pingreq", payload: body };
|
|
432
|
+
if (packetTypeId === 13) return { packetType: "pingresp", payload: body };
|
|
433
|
+
if (packetTypeId === 14) return { packetType: "disconnect", payload: body };
|
|
434
|
+
return { packetType: String(packetTypeId), payload: body };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function parseJsonPayload(payload) {
|
|
438
|
+
return JSON.parse(tryDecompressPayload(payload).toString("utf8"));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function uint16BE(value) {
|
|
442
|
+
const buf = Buffer.alloc(2);
|
|
443
|
+
buf.writeUInt16BE(value & 0xffff, 0);
|
|
444
|
+
return buf;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function writeUtf8(value) {
|
|
448
|
+
const raw = Buffer.from(value, "utf8");
|
|
449
|
+
return Buffer.concat([uint16BE(raw.length), raw]);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function readUtf8(data, offset) {
|
|
453
|
+
const size = data.readUInt16BE(offset);
|
|
454
|
+
offset += 2;
|
|
455
|
+
return { value: data.slice(offset, offset + size).toString("utf8"), offset: offset + size };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function encodeRemainingLength(value) {
|
|
459
|
+
const encoded = [];
|
|
460
|
+
for (;;) {
|
|
461
|
+
let byte = value % 128;
|
|
462
|
+
value = Math.floor(value / 128);
|
|
463
|
+
if (value) byte |= 0x80;
|
|
464
|
+
encoded.push(byte);
|
|
465
|
+
if (!value) return Buffer.from(encoded);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function decodeRemainingLength(data, offset) {
|
|
470
|
+
let multiplier = 1;
|
|
471
|
+
let value = 0;
|
|
472
|
+
let byte;
|
|
473
|
+
do {
|
|
474
|
+
byte = data[offset];
|
|
475
|
+
offset += 1;
|
|
476
|
+
value += (byte & 127) * multiplier;
|
|
477
|
+
multiplier *= 128;
|
|
478
|
+
} while (byte & 128);
|
|
479
|
+
return { value, offset };
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// A TLS transport that frames MQTT packets. `recvPacket()` resolves with one
|
|
483
|
+
// complete packet; the realtime client drives it in a loop. Node's socket is
|
|
484
|
+
// asynchronous, so unlike the reference (blocking) transport this buffers and
|
|
485
|
+
// hands out packets as they arrive.
|
|
486
|
+
class SocketMQTToTTransport {
|
|
487
|
+
constructor(host, port = 443, { timeout = 30000 } = {}) {
|
|
488
|
+
this.host = host;
|
|
489
|
+
this.port = port;
|
|
490
|
+
this.timeout = timeout;
|
|
491
|
+
this.socket = null;
|
|
492
|
+
this._buffer = Buffer.alloc(0);
|
|
493
|
+
this._waiters = [];
|
|
494
|
+
this._closed = false;
|
|
495
|
+
this._error = null;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
connect() {
|
|
499
|
+
return new Promise((resolve, reject) => {
|
|
500
|
+
const socket = tls.connect({
|
|
501
|
+
host: this.host,
|
|
502
|
+
port: this.port,
|
|
503
|
+
servername: this.host,
|
|
504
|
+
minVersion: "TLSv1.2"
|
|
505
|
+
});
|
|
506
|
+
const onError = (err) => { cleanup(); reject(err); };
|
|
507
|
+
const onConnect = () => { cleanup(); this._attach(socket); resolve(); };
|
|
508
|
+
const cleanup = () => {
|
|
509
|
+
socket.removeListener("error", onError);
|
|
510
|
+
socket.removeListener("secureConnect", onConnect);
|
|
511
|
+
};
|
|
512
|
+
socket.setTimeout(this.timeout, () => socket.destroy(new Error("MQTT socket timeout")));
|
|
513
|
+
socket.once("error", onError);
|
|
514
|
+
socket.once("secureConnect", onConnect);
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
_attach(socket) {
|
|
519
|
+
this.socket = socket;
|
|
520
|
+
socket.on("data", (chunk) => {
|
|
521
|
+
this._buffer = Buffer.concat([this._buffer, chunk]);
|
|
522
|
+
this._drain();
|
|
523
|
+
});
|
|
524
|
+
socket.on("error", (err) => this._fail(err));
|
|
525
|
+
socket.on("close", () => this._fail(new Error("MQTT socket closed")));
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
_drain() {
|
|
529
|
+
while (this._waiters.length) {
|
|
530
|
+
const packet = this._tryRead();
|
|
531
|
+
if (!packet) break;
|
|
532
|
+
this._waiters.shift().resolve(packet);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
_tryRead() {
|
|
537
|
+
if (this._buffer.length < 2) return null;
|
|
538
|
+
let multiplier = 1;
|
|
539
|
+
let size = 0;
|
|
540
|
+
let offset = 1;
|
|
541
|
+
let byte;
|
|
542
|
+
do {
|
|
543
|
+
if (offset >= this._buffer.length) return null;
|
|
544
|
+
byte = this._buffer[offset];
|
|
545
|
+
size += (byte & 0x7f) * multiplier;
|
|
546
|
+
multiplier *= 128;
|
|
547
|
+
offset += 1;
|
|
548
|
+
} while (byte & 0x80);
|
|
549
|
+
const total = offset + size;
|
|
550
|
+
if (this._buffer.length < total) return null;
|
|
551
|
+
const packet = this._buffer.slice(0, total);
|
|
552
|
+
this._buffer = this._buffer.slice(total);
|
|
553
|
+
return packet;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
recvPacket() {
|
|
557
|
+
const packet = this._tryRead();
|
|
558
|
+
if (packet) return Promise.resolve(packet);
|
|
559
|
+
if (this._closed) return Promise.reject(this._error || new Error("MQTT socket closed"));
|
|
560
|
+
return new Promise((resolve, reject) => this._waiters.push({ resolve, reject }));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
send(packet) {
|
|
564
|
+
if (!this.socket) return Promise.reject(new Error("MQTT transport is not connected"));
|
|
565
|
+
return new Promise((resolve, reject) => {
|
|
566
|
+
this.socket.write(packet, (err) => (err ? reject(err) : resolve()));
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
disconnect() {
|
|
571
|
+
if (!this.socket) return;
|
|
572
|
+
const socket = this.socket;
|
|
573
|
+
this.socket = null;
|
|
574
|
+
try { socket.end(); } catch (_) {}
|
|
575
|
+
try { socket.destroy(); } catch (_) {}
|
|
576
|
+
this._fail(new Error("MQTT transport closed"));
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
_fail(err) {
|
|
580
|
+
if (this._closed && this._error) return;
|
|
581
|
+
this._closed = true;
|
|
582
|
+
this._error = err;
|
|
583
|
+
for (const waiter of this._waiters) waiter.reject(err);
|
|
584
|
+
this._waiters = [];
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
module.exports = {
|
|
589
|
+
ThriftTypes,
|
|
590
|
+
ThriftWriter,
|
|
591
|
+
ThriftReader,
|
|
592
|
+
MQTToTConnection,
|
|
593
|
+
CONNECTION_DESCRIPTORS,
|
|
594
|
+
SocketMQTToTTransport,
|
|
595
|
+
compressPayload,
|
|
596
|
+
tryDecompressPayload,
|
|
597
|
+
writeConnectPacket,
|
|
598
|
+
writePublishPacket,
|
|
599
|
+
writeSubscribePacket,
|
|
600
|
+
writePingreqPacket,
|
|
601
|
+
writeDisconnectPacket,
|
|
602
|
+
decodePacket,
|
|
603
|
+
parseJsonPayload,
|
|
604
|
+
writeThriftObject,
|
|
605
|
+
writeThriftStruct,
|
|
606
|
+
readThriftObject: (data, descriptors) => new ThriftReader(data).readStruct(descriptors),
|
|
607
|
+
zigzag,
|
|
608
|
+
unzigzag
|
|
609
|
+
};
|