1 | import fs from 'fs'
|
2 | import path from 'path'
|
3 | import makeDeviceId from './FacebookDeviceId'
|
4 | import HttpApi from './http/HttpApi'
|
5 | import MqttApi from './mqtt/MqttApi'
|
6 | import Session from './types/Session'
|
7 | import Thread, { parseThread } from './types/Thread'
|
8 | import User, { parseUser } from './types/User'
|
9 | import debug from 'debug'
|
10 | import { Readable } from 'stream'
|
11 | import { PublishPacket } from './mqtt/messages/Publish'
|
12 | import Message, { parseThreadMessage } from './types/Message'
|
13 | import parseDeltaMessage from './types/message/parseDeltaMessage'
|
14 | import parseDeltaEvent from './types/events/parseDeltaEvent'
|
15 | import EventEmitter from 'events'
|
16 | import { AttachmentNotFoundError, AttachmentURLMissingError } from './types/Errors'
|
17 | import StrictEventEmitter from 'strict-event-emitter-types'
|
18 | import ClientEvents from './ClientEvents'
|
19 |
|
20 | const debugLog = debug('fblib')
|
21 |
|
22 | export interface ClientOptions {
|
23 | selfListen?: boolean
|
24 | session?: Session
|
25 | }
|
26 |
|
27 | type ClientEmitter = StrictEventEmitter<EventEmitter, ClientEvents>
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 | export default class Client extends (EventEmitter as { new(): ClientEmitter }) {
|
34 | private mqttApi: MqttApi
|
35 | private httpApi: HttpApi
|
36 | private readonly session: Session | null
|
37 | private seqId = ''
|
38 | loggedIn: boolean = false
|
39 | private options: ClientOptions
|
40 |
|
41 | constructor (options: ClientOptions = { selfListen: false, session: null }) {
|
42 | super()
|
43 | this.mqttApi = new MqttApi()
|
44 | this.httpApi = new HttpApi()
|
45 |
|
46 | let session = options.session
|
47 | if (!session) {
|
48 | session = { tokens: null, deviceId: null }
|
49 | }
|
50 |
|
51 | if (!session.deviceId) {
|
52 | const deviceId = makeDeviceId()
|
53 | session.deviceId = deviceId
|
54 | this.httpApi.deviceId = deviceId.deviceId
|
55 | }
|
56 |
|
57 | if (session.tokens) {
|
58 | this.httpApi.token = session.tokens.access_token
|
59 | }
|
60 |
|
61 | this.session = session
|
62 | }
|
63 |
|
64 | async login (email: string, password: string) {
|
65 |
|
66 | if (this.loggedIn) throw new Error('Already logged in!')
|
67 | if (
|
68 | !email || typeof email !== 'string' || !email.trim() ||
|
69 | !password || typeof password !== 'string' || !password.trim()
|
70 | ) throw new Error('Wrong username/password!')
|
71 | await this.doLogin(email, password)
|
72 | this.loggedIn = true
|
73 | }
|
74 |
|
75 | private doLogin (login: string, password: string) {
|
76 | return new Promise(async (resolve, reject) => {
|
77 | if (!this.session.tokens) {
|
78 | let tokens
|
79 | try {
|
80 | tokens = await this.httpApi.auth(login, password)
|
81 | } catch (err) {
|
82 | return reject(err)
|
83 | }
|
84 | this.httpApi.token = tokens.access_token
|
85 | this.session.tokens = tokens
|
86 | }
|
87 |
|
88 | this.mqttApi.on('publish', async (publish: PublishPacket) => {
|
89 | debugLog(publish.topic)
|
90 | if (publish.topic === '/send_message_response') {
|
91 | const response = JSON.parse(publish.data.toString('utf8'))
|
92 | debugLog(response)
|
93 | this.mqttApi.emit('sentMessage:' + response.msgid, response)
|
94 | }
|
95 | if (publish.topic === '/t_ms') this.handleMS(publish.data.toString('utf8'))
|
96 | })
|
97 |
|
98 | this.mqttApi.on('connected', async () => {
|
99 | let viewer
|
100 | try {
|
101 | ({ viewer } = await this.httpApi.querySeqId())
|
102 | } catch (err) {
|
103 | return reject(err)
|
104 | }
|
105 | const seqId = viewer.message_threads.sync_sequence_id
|
106 | this.seqId = seqId
|
107 | resolve()
|
108 | if (!this.session.tokens.syncToken) {
|
109 | await this.createQueue(seqId)
|
110 | return
|
111 | }
|
112 |
|
113 | await this.createQueue(seqId)
|
114 | })
|
115 |
|
116 | try {
|
117 | await this.mqttApi.connect(
|
118 | this.session.tokens,
|
119 | this.session.deviceId
|
120 | )
|
121 | } catch (err) {
|
122 | return reject(err)
|
123 | }
|
124 | })
|
125 | }
|
126 |
|
127 | getSession () {
|
128 | return this.session
|
129 | }
|
130 |
|
131 | sendMessage (threadId: string, message: string) {
|
132 | return this.mqttApi.sendMessage(threadId, message)
|
133 | }
|
134 |
|
135 | getThreadList = async (count: number): Promise<Thread[]> => {
|
136 | const threads = await this.httpApi.threadListQuery(count)
|
137 | return threads.viewer.message_threads.nodes.map(parseThread)
|
138 | }
|
139 |
|
140 | sendAttachmentFile (threadId: string, attachmentPath: string, extension?: string) {
|
141 | if (!fs.existsSync(attachmentPath)) throw new AttachmentNotFoundError(attachmentPath)
|
142 | const stream = fs.createReadStream(attachmentPath)
|
143 | if (!extension) extension = path.parse(attachmentPath).ext
|
144 | const length = fs.statSync(attachmentPath).size.toString()
|
145 | return this.httpApi.sendImage(stream, extension, this.session.tokens.uid, threadId, length)
|
146 | }
|
147 |
|
148 | sendAttachmentStream (threadId: string, extension: string, attachment: Readable) {
|
149 | return this.httpApi.sendImage(attachment, extension, this.session.tokens.uid, threadId)
|
150 | }
|
151 |
|
152 | async getAttachmentURL (messageId: string, attachmentId: string): Promise<string> {
|
153 | const attachment = await this.httpApi.getAttachment(messageId, attachmentId)
|
154 | if (!attachment.redirect_uri) throw new AttachmentURLMissingError(attachment)
|
155 | return attachment.redirect_uri
|
156 | }
|
157 |
|
158 | getAttachmentInfo (messageId: string, attachmentId: string) {
|
159 | return this.httpApi.getAttachment(messageId, attachmentId)
|
160 | }
|
161 |
|
162 | async getStickerURL (stickerId: number): Promise<string> {
|
163 | const sticker = await this.httpApi.getSticker(stickerId)
|
164 | return sticker[stickerId.toString()].thread_image.uri
|
165 | }
|
166 |
|
167 | async getThreadInfo (threadId: string): Promise<Thread> {
|
168 | const res = await this.httpApi.threadQuery(threadId)
|
169 | const thread = res[threadId]
|
170 | if (!thread) return null
|
171 | return parseThread(thread)
|
172 | }
|
173 |
|
174 | async getUserInfo (userId: string): Promise<User> {
|
175 | const res = await this.httpApi.userQuery(userId)
|
176 | const user = res[userId]
|
177 | if (!user) return null
|
178 | return parseUser(user)
|
179 | }
|
180 |
|
181 | async getMessages (threadId: string, count: number): Promise<Message> {
|
182 | const res = await this.httpApi.threadMessagesQuery(threadId, count)
|
183 | const thread = res[threadId]
|
184 | if (!thread) return null
|
185 | return thread.messages.nodes.map(message => parseThreadMessage(threadId, message))
|
186 | }
|
187 |
|
188 | private async createQueue (seqId: number) {
|
189 | const obj = {
|
190 | delta_batch_size: 125,
|
191 | max_deltas_able_to_process: 1250,
|
192 | sync_api_version: 3,
|
193 | encoding: 'JSON',
|
194 |
|
195 | initial_titan_sequence_id: seqId,
|
196 | device_id: this.session.deviceId.deviceId,
|
197 | entity_fbid: this.session.tokens.uid,
|
198 |
|
199 | queue_params: {
|
200 | buzz_on_deltas_enabled: 'false',
|
201 | graphql_query_hashes: {
|
202 | xma_query_id: '10153919431161729'
|
203 | },
|
204 |
|
205 | graphql_query_params: {
|
206 | '10153919431161729': {
|
207 | xma_id: '<ID>'
|
208 | }
|
209 | }
|
210 | }
|
211 | }
|
212 |
|
213 | await this.mqttApi.sendPublish(
|
214 | '/messenger_sync_create_queue',
|
215 | JSON.stringify(obj)
|
216 | )
|
217 | }
|
218 |
|
219 | private async connectQueue (seqId) {
|
220 | const obj = {
|
221 | delta_batch_size: 125,
|
222 | max_deltas_able_to_process: 1250,
|
223 | sync_api_version: 3,
|
224 | encoding: 'JSON',
|
225 |
|
226 | last_seq_id: seqId,
|
227 | sync_token: this.session.tokens.syncToken
|
228 | }
|
229 |
|
230 | await this.mqttApi.sendPublish(
|
231 | '/messenger_sync_get_diffs',
|
232 | JSON.stringify(obj)
|
233 | )
|
234 | }
|
235 |
|
236 | private async handleMS (ms: string) {
|
237 | let data
|
238 | try {
|
239 | data = JSON.parse(ms.replace('\u0000', ''))
|
240 | } catch (err) {
|
241 | console.error('Error while parsing the following message:')
|
242 | console.error(ms)
|
243 | return
|
244 | }
|
245 |
|
246 |
|
247 | if (data.syncToken) {
|
248 | this.session.tokens.syncToken = data.syncToken
|
249 | await this.connectQueue(this.seqId)
|
250 | return
|
251 | }
|
252 |
|
253 | if (!data.deltas || !Array.isArray(data.deltas)) return
|
254 |
|
255 | data.deltas.forEach(delta => {
|
256 | debugLog(delta)
|
257 | this.handleMessage(delta)
|
258 | })
|
259 | }
|
260 |
|
261 | handleMessage (event: any) {
|
262 | if (event.deltaNewMessage) {
|
263 | const message = parseDeltaMessage(event.deltaNewMessage)
|
264 | if (!message || message.authorId === this.session.tokens.uid && !this.options.selfListen) return
|
265 | this.emit('message', message)
|
266 | }
|
267 |
|
268 | const deltaEvent = parseDeltaEvent(event)
|
269 | if (!deltaEvent) return
|
270 | this.emit('event', deltaEvent)
|
271 |
|
272 | this.emit(deltaEvent.type, deltaEvent.event)
|
273 | }
|
274 | }
|