UNPKG

13.6 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const tslib_1 = require("tslib");
4const fs_1 = tslib_1.__importDefault(require("fs"));
5const path_1 = tslib_1.__importDefault(require("path"));
6const FacebookDeviceId_1 = tslib_1.__importDefault(require("./FacebookDeviceId"));
7const HttpApi_1 = tslib_1.__importDefault(require("./http/HttpApi"));
8const MqttApi_1 = tslib_1.__importDefault(require("./mqtt/MqttApi"));
9const Thread_1 = require("./types/Thread");
10const User_1 = require("./types/User");
11const debug_1 = tslib_1.__importDefault(require("debug"));
12const Message_1 = require("./types/Message");
13const parseDeltaEvent_1 = tslib_1.__importDefault(require("./types/events/parseDeltaEvent"));
14const events_1 = tslib_1.__importDefault(require("events"));
15const Errors_1 = require("./types/Errors");
16const Payloads = tslib_1.__importStar(require("./mqtt/payloads"));
17const debugLog = debug_1.default('fblib');
18// 🥖
19/**
20 * Main client class
21 */
22class Client extends events_1.default {
23 constructor(options = { selfListen: false, session: null }) {
24 super();
25 this.seqId = '';
26 this.loggedIn = false;
27 this.options = options;
28 this.mqttApi = new MqttApi_1.default();
29 this.httpApi = new HttpApi_1.default();
30 let session = options.session;
31 if (!session) {
32 session = { tokens: null, deviceId: null };
33 }
34 if (options.deviceId) {
35 session.deviceId = options.deviceId;
36 }
37 if (!session.deviceId) {
38 const deviceId = FacebookDeviceId_1.default();
39 session.deviceId = deviceId;
40 this.httpApi.deviceId = deviceId.deviceId;
41 }
42 if (session.tokens) {
43 this.httpApi.token = session.tokens.access_token;
44 }
45 this.session = session;
46 }
47 login(email, password) {
48 return tslib_1.__awaiter(this, void 0, void 0, function* () {
49 // trim to check for spaces (which are truthy)
50 if (this.loggedIn)
51 throw new Error('Already logged in!');
52 if (!email || typeof email !== 'string' || !email.trim() ||
53 !password || typeof password !== 'string' || !password.trim())
54 throw new Error('Wrong username/password!');
55 yield this.doLogin(email, password);
56 this.loggedIn = true;
57 });
58 }
59 doLogin(login, password) {
60 return new Promise((resolve, reject) => tslib_1.__awaiter(this, void 0, void 0, function* () {
61 if (!this.session.tokens) {
62 let tokens;
63 try {
64 tokens = yield this.httpApi.auth(login, password);
65 }
66 catch (err) {
67 return reject(err);
68 }
69 this.httpApi.token = tokens.access_token;
70 this.session.tokens = tokens;
71 }
72 this.mqttApi.on('publish', (publish) => tslib_1.__awaiter(this, void 0, void 0, function* () {
73 debugLog(publish.topic);
74 if (publish.topic === '/send_message_response') {
75 const response = JSON.parse(publish.data.toString('utf8'));
76 debugLog(response);
77 this.mqttApi.emit('sentMessage:' + response.msgid, response);
78 }
79 if (publish.topic === '/t_ms')
80 this.handleMS(publish.data.toString('utf8'));
81 }));
82 this.mqttApi.on('connected', () => tslib_1.__awaiter(this, void 0, void 0, function* () {
83 let viewer;
84 try {
85 ({ viewer } = yield this.httpApi.querySeqId());
86 }
87 catch (err) {
88 return reject(err);
89 }
90 const seqId = viewer.message_threads.sync_sequence_id;
91 this.seqId = seqId;
92 resolve();
93 if (!this.session.tokens.syncToken) {
94 yield this.createQueue(seqId);
95 return;
96 }
97 yield this.createQueue(seqId);
98 }));
99 try {
100 yield this.mqttApi.connect(this.session.tokens, this.session.deviceId);
101 }
102 catch (err) {
103 return reject(err);
104 }
105 }));
106 }
107 getSession() {
108 return this.session;
109 }
110 sendMessage(threadId, message, options) {
111 return this.mqttApi.sendMessage(threadId, message, options);
112 }
113 /**
114 * Indicate that the user is currently present in the conversation.
115 * Only relevant for non-group conversations
116 */
117 sendPresenceState(recipientUserId, present) {
118 return tslib_1.__awaiter(this, void 0, void 0, function* () {
119 const payload = new Payloads.PresenceState(recipientUserId, present);
120 return this.mqttApi.sendPublish(payload.getTopic(), yield Payloads.encodePayload(payload));
121 });
122 }
123 /**
124 * Send "User is typing" message.
125 * In a non-group conversation, sendPresenceState() must be called first.
126 */
127 sendTypingState(threadOrRecipientUserId, present) {
128 return tslib_1.__awaiter(this, void 0, void 0, function* () {
129 const payload = new Payloads.TypingState(this.session.tokens.uid, present, threadOrRecipientUserId);
130 return this.mqttApi.sendPublish(payload.getTopic(), yield Payloads.encodePayload(payload));
131 });
132 }
133 /**
134 * Mark a message as read.
135 */
136 sendReadReceipt(message) {
137 return tslib_1.__awaiter(this, void 0, void 0, function* () {
138 const payload = new Payloads.ReadReceipt(message);
139 return this.mqttApi.sendPublish(payload.getTopic(), yield Payloads.encodePayload(payload));
140 });
141 }
142 getThreadList(count) {
143 return tslib_1.__awaiter(this, void 0, void 0, function* () {
144 const threads = yield this.httpApi.threadListQuery(count);
145 return threads.viewer.message_threads.nodes.map(Thread_1.parseThread);
146 });
147 }
148 sendAttachmentFile(threadId, attachmentPath, extension) {
149 if (!fs_1.default.existsSync(attachmentPath))
150 throw new Errors_1.AttachmentNotFoundError(attachmentPath);
151 const stream = fs_1.default.createReadStream(attachmentPath);
152 if (!extension)
153 extension = path_1.default.parse(attachmentPath).ext;
154 const length = fs_1.default.statSync(attachmentPath).size.toString();
155 return this.httpApi.sendImage(stream, extension, this.session.tokens.uid, threadId, length);
156 }
157 sendAttachmentStream(threadId, extension, attachment) {
158 return this.httpApi.sendImage(attachment, extension, this.session.tokens.uid, threadId);
159 }
160 getAttachmentURL(messageId, attachmentId) {
161 return tslib_1.__awaiter(this, void 0, void 0, function* () {
162 const attachment = yield this.httpApi.getAttachment(messageId, attachmentId);
163 if (!attachment.redirect_uri)
164 throw new Errors_1.AttachmentURLMissingError(attachment);
165 return attachment.redirect_uri;
166 });
167 }
168 getAttachmentInfo(messageId, attachmentId) {
169 return this.httpApi.getAttachment(messageId, attachmentId);
170 }
171 getStickerURL(stickerId) {
172 return tslib_1.__awaiter(this, void 0, void 0, function* () {
173 const sticker = yield this.httpApi.getSticker(stickerId);
174 return sticker[stickerId.toString()].thread_image.uri;
175 });
176 }
177 getThreadInfo(threadId) {
178 return tslib_1.__awaiter(this, void 0, void 0, function* () {
179 const res = yield this.httpApi.threadQuery(threadId);
180 const thread = res[threadId];
181 if (!thread)
182 return null;
183 return Thread_1.parseThread(thread);
184 });
185 }
186 getUserInfo(userId) {
187 return tslib_1.__awaiter(this, void 0, void 0, function* () {
188 const res = yield this.httpApi.userQuery(userId);
189 const user = res[userId];
190 if (!user)
191 return null;
192 return User_1.parseUser(user);
193 });
194 }
195 getMessages(threadId, count) {
196 return tslib_1.__awaiter(this, void 0, void 0, function* () {
197 const res = yield this.httpApi.threadMessagesQuery(threadId, count);
198 const thread = res[threadId];
199 if (!thread)
200 return null;
201 return thread.messages.nodes.map(message => Message_1.parseThreadMessage(threadId, message));
202 });
203 }
204 createQueue(seqId) {
205 return tslib_1.__awaiter(this, void 0, void 0, function* () {
206 // sync_api_version 3: You receive /t_ms payloads as json
207 // sync_api_version 10: You receiove /t_ms payloads as thrift,
208 // and connectQueue() does not have to be called.
209 // Note that connectQueue() should always use 10 instead.
210 const obj = ({
211 initial_titan_sequence_id: seqId,
212 delta_batch_size: 125,
213 device_params: {
214 image_sizes: {
215 0: '4096x4096',
216 4: '312x312',
217 1: '768x768',
218 2: '420x420',
219 3: '312x312'
220 },
221 animated_image_format: 'WEBP,GIF',
222 animated_image_sizes: {
223 0: '4096x4096',
224 4: '312x312',
225 1: '768x768',
226 2: '420x420',
227 3: '312x312'
228 }
229 },
230 entity_fbid: this.session.tokens.uid,
231 sync_api_version: 3,
232 encoding: 'JSON',
233 queue_params: {
234 // Array of numbers -> Some bitwise encoding scheme -> base64. Numbers range from 0 to 67
235 // Decides what type of /t_ms delta messages you get. Flags unknown, copy-pasted from app.
236 client_delta_sync_bitmask: 'Amvr2dBlf7PNgA',
237 graphql_query_hashes: {
238 xma_query_id: '306810703252313'
239 },
240 graphql_query_params: {
241 306810703252313: {
242 xma_id: '<ID>',
243 small_preview_width: 624,
244 small_preview_height: 312,
245 large_preview_width: 1536,
246 large_preview_height: 768,
247 full_screen_width: 4096,
248 full_screen_height: 4096,
249 blur: 0.0,
250 nt_context: {
251 styles_id: 'fe1fd5357bb40c81777dc915dfbd6aa4',
252 pixel_ratio: 3.0
253 }
254 }
255 }
256 }
257 });
258 yield this.mqttApi.sendPublish('/messenger_sync_create_queue', JSON.stringify(obj));
259 });
260 }
261 connectQueue(seqId) {
262 return tslib_1.__awaiter(this, void 0, void 0, function* () {
263 // If createQueue() uses sync_api_version 10, this does not need to be called, and you will not receive json payloads.
264 // If this does not use sync_api_version 10, you will not receive all messages (e.g. reactions )
265 // Send the thrift-equivalent payload to /t_ms_gd and you will receive mostly thrift-encoded payloads instead.
266 const obj = {
267 delta_batch_size: 125,
268 max_deltas_able_to_process: 1250,
269 sync_api_version: 10,
270 encoding: 'JSON',
271 last_seq_id: seqId,
272 sync_token: this.session.tokens.syncToken
273 };
274 yield this.mqttApi.sendPublish('/messenger_sync_get_diffs', JSON.stringify(obj));
275 });
276 }
277 handleMS(ms) {
278 return tslib_1.__awaiter(this, void 0, void 0, function* () {
279 let data;
280 try {
281 data = JSON.parse(ms.replace('\u0000', ''));
282 }
283 catch (err) {
284 console.error('Error while parsing the following message:');
285 console.error(ms);
286 return;
287 }
288 // Handled on queue creation
289 if (data.syncToken) {
290 this.session.tokens.syncToken = data.syncToken;
291 yield this.connectQueue(this.seqId);
292 return;
293 }
294 if (!data.deltas || !Array.isArray(data.deltas))
295 return;
296 data.deltas.forEach(delta => {
297 debugLog(delta);
298 this.handleMessage(delta);
299 });
300 });
301 }
302 handleMessage(event) {
303 if (event.deltaNewMessage) {
304 const message = Message_1.parseDeltaMessage(event.deltaNewMessage);
305 if (!message || message.authorId === this.session.tokens.uid && !this.options.selfListen)
306 return;
307 this.emit('message', message);
308 }
309 const deltaEvent = parseDeltaEvent_1.default(event);
310 if (!deltaEvent)
311 return;
312 this.emit('event', deltaEvent);
313 // @ts-ignore TypeScript somehow doesn't recognize that EventType is compatible with the properties defined in ClientEvents
314 this.emit(deltaEvent.type, deltaEvent.event);
315 }
316}
317exports.default = Client;
318//# sourceMappingURL=Client.js.map
\No newline at end of file