UNPKG

8.29 kBPlain TextView Raw
1import fs from 'fs'
2import path from 'path'
3import makeDeviceId from './FacebookDeviceId'
4import HttpApi from './http/HttpApi'
5import MqttApi from './mqtt/MqttApi'
6import Session from './types/Session'
7import Thread, { parseThread } from './types/Thread'
8import User, { parseUser } from './types/User'
9import debug from 'debug'
10import { Readable } from 'stream'
11import { PublishPacket } from './mqtt/messages/Publish'
12import Message, { parseThreadMessage } from './types/Message'
13import parseDeltaMessage from './types/message/parseDeltaMessage'
14import parseDeltaEvent from './types/events/parseDeltaEvent'
15import EventEmitter from 'events'
16import { AttachmentNotFoundError, AttachmentURLMissingError } from './types/Errors'
17import StrictEventEmitter from 'strict-event-emitter-types'
18import ClientEvents from './ClientEvents'
19
20const debugLog = debug('fblib')
21
22export interface ClientOptions {
23 selfListen?: boolean
24 session?: Session
25}
26
27type ClientEmitter = StrictEventEmitter<EventEmitter, ClientEvents>
28
29// 🥖
30/**
31 * Main client class
32 */
33export 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 // trim to check for spaces (which are truthy)
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 // Handled on queue creation
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 // @ts-ignore TypeScript somehow doesn't recognize that EventType is compatible with the properties defined in ClientEvents
272 this.emit(deltaEvent.type, deltaEvent.event)
273 }
274}