Jonell01 commited on
Commit
aa0eab9
·
verified ·
1 Parent(s): de4c237

Upload 3 files

Browse files
includes/login/DOCS.md ADDED
@@ -0,0 +1,1948 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Documentation
2
+ ### You can use callback or .then() .catch() or async/await
3
+
4
+ * [`login(credentials, options, [callback])`](#logincredentials-options-callback) ⇒ <code>Promise</code>
5
+ * [`api.addExternalModule(moduleObj)`](#apiaddexternalmodulemoduleobj)
6
+ * [`api.addUserToGroup(userID, threadID, [callback])`](#apiaddusertogroupuserid-threadid-callback) ⇒ <code>Promise</code>
7
+ * [`api.changeAdminStatus(threadID, adminIDs, adminStatus, [callback])`](#apichangeadminstatusthreadid-adminids-adminstatus-callback) ⇒ <code>Promise</code>
8
+ * [`api.changeArchivedStatus(threadOrThreads, archive, [callback])`](#apichangearchivedstatusthreadorthreads-archive-callback) ⇒ <code>Promise</code>
9
+ * [`api.changeBlockedStatus(userID, block, [callback])`](#apichangeblockedstatususerid-block-callback) ⇒ <code>Promise</code>
10
+ * [`api.changeGroupImage(image, threadID, [callback])`](#apichangegroupimageimage-threadid-callback) ⇒ <code>Promise</code>
11
+ * [`api.changeNickname(nickname, threadID, participantID, [callback])`](#apichangenicknamenickname-threadid-participantid-callback) ⇒ <code>Promise</code>
12
+ * [`api.changeThreadColor(color, threadID, [callback])`](#apichangethreadcolorcolor-threadid-callback) ⇒ <code>Promise</code>
13
+ * [`api.changeThreadEmoji(emoji, threadID, [callback])`](#apichangethreademojiemoji-threadid-callback) ⇒ <code>Promise</code>
14
+ * [`api.createNewGroup(participantIDs, groupTitle, [callback])`](#apicreatenewgroupparticipantids-grouptitle-callback) ⇒ <code>Promise</code>
15
+ * [`api.createPoll(title, threadID, options, [callback]) (*temporary deprecated because Facebook is updating this feature*)`](#apicreatepolltitle-threadid-options-callback-temporary-deprecated-because-facebook-is-updating-this-feature) ⇒ <code>Promise</code>
16
+ * [`api.deleteMessage(messageOrMessages, [callback])`](#apideletemessagemessageormessages-callback) ⇒ <code>Promise</code>
17
+ * [`api.deleteThread(threadOrThreads, [callback])`](#apideletethreadthreadorthreads-callback) ⇒ <code>Promise</code>
18
+ * [`api.forwardAttachment(attachmentID, userOrUsers, [callback])`](#apiforwardattachmentattachmentid-userorusers-callback) ⇒ <code>Promise</code>
19
+ * [`api.getAppState()`](#apigetappstate) ⇒ <code>Array</code>
20
+ * [`api.getCurrentUserID()`](#apigetcurrentuserid) ⇒ <code>string</code>
21
+ * [`api.getEmojiUrl(c, size, pixelRatio)`](#apigetemojiurlc-size-pixelratio) ⇒ <code>string</code>
22
+ * [`api.getFriendsList([callback])`](#apigetfriendslistcallback) ⇒ <code>Promise</code>
23
+ * [`api.getMessage(threadID, messageID, [callback])`](#apigetmessagethreadid-messageid-callback) ⇒ <code>Promise</code>
24
+ * [`api.getThreadHistory(threadID, amount, timestamp, [callback])`](#apigetthreadhistorythreadid-amount-timestamp-callback) ⇒ <code>Promise</code>
25
+ * [`api.getThreadInfo(threadIDs, [callback])`](#apigetthreadinfothreadids-callback) ⇒ <code>Promise</code>
26
+ * [`api.getThreadList(limit, timestamp, tags, [callback])`](#apigetthreadlistlimit-timestamp-tags-callback) ⇒ <code>Promise</code>
27
+ * [`api.getThreadPictures(threadID, offset, limit, [callback])`](#apigetthreadpicturesthreadid-offset-limit-callback) ⇒ <code>Promise</code>
28
+ * [`api.getUserID(name, [callback])`](#apigetuseridname-callback) ⇒ <code>Promise</code>
29
+ * [`api.getUserInfo(ids, [callback])`](#apigetuserinfoids-callback) ⇒ <code>Promise</code>
30
+ * [`api.handleMessageRequest(threadID, accept, [callback])`](#apihandlemessagerequestthreadid-accept-callback) ⇒ <code>Promise</code>
31
+ * [`api.httpGet(url, form, [customHeader], [callback], [notAPI])`](#apihttpgeturl-form-customheader-callback-notapi) ⇒ <code>Promise</code>
32
+ * [`api.httpPost(url, form, [customHeader], [callback], [notAPI])`](#apihttpposturl-form-customheader-callback-notapi) ⇒ <code>Promise</code>
33
+ * [`api.httpPostFormData(url, form, [customHeader], [callback], [notAPI])`](#apihttppostformdataurl-form-customheader-callback-notapi) ⇒ <code>Promise</code>
34
+ * [~~`api.listen([callback])`~~](#apilistencallback) ⇒ <code>Promise</code>
35
+ * [`api.listenMqtt([callback])`](#apilistenmqttcallback) ⇒ <code>Promise</code>
36
+ * [`api.logout([callback])`](#apilogoutcallback) ⇒ <code>Promise</code>
37
+ * [`api.markAsDelivered(threadID, messageID, [callback]`](#apimarkasdeliveredthreadid-messageid-callback) ⇒ <code>Promise</code>
38
+ * [`api.markAsRead(threadID, [read, [callback]])`](#apimarkasreadthreadid-read-callback) ⇒ <code>Promise</code>
39
+ * [`api.markAsReadAll([callback])`](#apimarkasreadallcallback) ⇒ <code>Promise</code>
40
+ * [`api.markAsSeen([seenTimestamp], [callback])`](#apimarkasseenseentimestamp-callback) ⇒ <code>Promise</code>
41
+ * [`api.muteThread(threadID, muteSeconds, [callback])`](#apimutethreadthreadid-muteseconds-callback) ⇒ <code>Promise</code>
42
+ * [`api.removeUserFromGroup(userID, threadID, [callback])`](#apiremoveuserfromgroupuserid-threadid-callback) ⇒ <code>Promise</code>
43
+ * [`api.resolvePhotoUrl(photoID, [callback])`](#apiresolvephotourlphotoid-callback) ⇒ <code>Promise</code>
44
+ * [`api.searchForThread(name, [callback])`](#apisearchforthreadname-callback)
45
+ * [`api.sendMessage(message, threadID, [callback], messageID)`](#apisendmessagemessage-threadid-callback-messageid) ⇒ <code>Promise</code>
46
+ * [`api.sendTypingIndicator(threadID, [callback])`](#apisendtypingindicatorthreadid-callback) ⇒ <code>Promise</code>
47
+ * [`api.setMessageReaction(reaction, messageID, [callback], [forceCustomReaction])`](#apisetmessagereactionreaction-messageid-callback-forcecustomreaction) ⇒ <code>Promise</code>
48
+ * [`api.setOptions(options)`](#apisetoptionsoptions) ⇒ <code>Promise</code>
49
+ * [`api.setPostReaction(postID, type, [callback])`](#apisetpostreactionpostid-type-callback) ⇒ <code>Promise</code>
50
+ * [`api.setTitle(newTitle, threadID, [callback])`](#apisettitlenewtitle-threadid-callback) ⇒ <code>Promise</code>
51
+ * [`api.threadColors`](#apithreadcolors) ⇒ <code>Object</code>
52
+ * [`api.unsendMessage(messageID, [callback])`](#apiunsendmessagemessageid-callback) ⇒ <code>Promise</code>
53
+ * [`api.uploadAttachment(attachments, [callback])`](#apiuploadattachmentattachments-callback) ⇒ <code>Promise</code>
54
+
55
+ ---------------------------------------
56
+
57
+ ### Password safety
58
+
59
+ **Read this** before you _copy+paste_ examples from below.
60
+
61
+ You should not store Facebook password in your scripts.
62
+ There are few good reasons:
63
+ * People who are standing behind you may look at your "code" and get your password if it is on the screen
64
+ * Backups of source files may be readable by someone else. "_There is nothing secret in my code, why should I ever password protect my backups_"
65
+ * You can't push your code to Github (or any onther service) without removing your password from the file. Remember: Even if you undo your accidential commit with password, Git doesn't delete it, that commit is just not used but is still readable by everybody.
66
+ * If you change your password in the future (maybe it leaked because _someone_ stored password in source file… oh… well…) you will have to change every occurrence in your scripts
67
+
68
+ Preferred method is to have `login.js` that saves `AppState` to a file and then use that file from all your scripts.
69
+ This way you can put password in your code for a minute, login to facebook and then remove it.
70
+
71
+ If you want to be even more safe: _login.js_ can get password with `require("readline")` or with environment variables like this:
72
+ ```js
73
+ var credentials = {
74
+ email: process.env.FB_EMAIL,
75
+ password: process.env.FB_PASSWORD
76
+ }
77
+ ```
78
+ ```bash
79
+ FB_EMAIL="john.doe@example.com"
80
+ FB_PASSWORD="MySuperHardP@ssw0rd"
81
+ nodejs login.js
82
+ ```
83
+
84
+ ---------------------------------------
85
+
86
+ <a name="login"></a>
87
+ ### login(credentials, options, [callback])
88
+
89
+ This function is returned by `require(...)` and is the main entry point to the API.
90
+
91
+ It allows the user to log into facebook given the right credentials.
92
+
93
+ Return a Promise that will resolve if logged in successfully, or reject if failed to login. (will not resolve or reject if callback is supplied!)
94
+
95
+ If `callback` is supplied:
96
+
97
+ * `callback` will be called with a `null` object (for potential errors) and with an object containing all the available functions if logged in successfully.
98
+
99
+ * `callback` will be called with an error object if failed to login.
100
+
101
+ If `login-approval` error was thrown: Inside error object is `continue` function, you can call that function with 2FA code. The behaviour of this function depends on how you call `login` with:
102
+
103
+ * If `callback` is not supplied (using `Promise`), this function will return a `Promise` that behaves like `Promise` received from `login`.
104
+
105
+ * If `callback` is supplied, this function will still return a `Promise`, but it will not resolve. Instead, the result is called to `callback`.
106
+
107
+ __Arguments__
108
+
109
+ * `credentials`: An object containing the fields `email` and `password` used to login, __*or*__ an object containing the field `appState`.
110
+ * `options`: An object representing options to use when logging in (as described in [api.setOptions](#setOptions)).
111
+ * `callback(err, api)`: A callback called when login is done (successful or not). `err` is an object containing a field `error`.
112
+ ---
113
+
114
+ <h1><b>Now login with account and password is no longer available, <a href="#loginWithAppstate">use appState</a> login instead.</b></h1>
115
+
116
+
117
+ ~~__Example (Email & Password)__: (it is no longer usable, please use [this](#loginWithAppstate) alternative method)~~
118
+
119
+ ```js
120
+ const login = require("fb-chat-api-temp");
121
+
122
+ login({email: "FB_EMAIL", password: "FB_PASSWORD"}, (err, api) => {
123
+ if(err) return console.error(err);
124
+ // Here you can use the api
125
+ });
126
+ ```
127
+
128
+ ~~__Example (Email & Password then save appState to file)__: (it is no longer usable, please use [this](#loginWithAppstate) alternative method)~~
129
+
130
+ ```js
131
+ const fs = require("fs-extra");
132
+ const login = require("fb-chat-api-temp");
133
+
134
+ login({email: "FB_EMAIL", password: "FB_PASSWORD"}, (err, api) => {
135
+ if(err) return console.error(err);
136
+
137
+ fs.writeFileSync('appstate.json', JSON.stringify(api.getAppState()));
138
+ });
139
+ ```
140
+
141
+
142
+ ~~__Login Approvals (2-Factor Auth)__: When you try to login with Login Approvals enabled, your callback will be called with an error `'login-approval'` that has a `continue` function that accepts the approval code as a `string` or a `number`. (it is no longer usable, please use [this](#loginWithAppstate) alternative method)~~
143
+
144
+ __Example__:
145
+
146
+ ```js
147
+ const login = require("fb-chat-api-temp");
148
+ const readline = require("readline");
149
+
150
+ var rl = readline.createInterface({
151
+ input: process.stdin,
152
+ output: process.stdout
153
+ });
154
+
155
+ const obj = {email: "FB_EMAIL", password: "FB_PASSWORD"};
156
+ login(obj, (err, api) => {
157
+ if(err) {
158
+ switch (err.error) {
159
+ case 'login-approval':
160
+ console.log('Enter code > ');
161
+ rl.on('line', (line) => {
162
+ err.continue(line);
163
+ rl.close();
164
+ });
165
+ break;
166
+ default:
167
+ console.error(err);
168
+ }
169
+ return;
170
+ }
171
+
172
+ // Logged in!
173
+ });
174
+ ```
175
+
176
+ __Review Recent Login__: Sometimes Facebook will ask you to review your recent logins. This means you've recently logged in from a unrecognized location. This will will result in the callback being called with an error `'review-recent-login'` by default. If you wish to automatically approve all recent logins, you can set the option `forceLogin` to `true` in the `loginOptions`.
177
+
178
+
179
+
180
+ <a name="loginWithAppstate"></a>
181
+ #### __Example (AppState loaded from file)__: You can get fbstate using [this](https://github.com/ntkhang03/c3c-fbstate) extension
182
+
183
+ ```js
184
+ const fs = require("fs-extra");
185
+ const login = require("fb-chat-api-temp");
186
+
187
+ login({appState: JSON.parse(fs.readFileSync('appstate.json', 'utf8'))}, (err, api) => {
188
+ if(err) return console.error(err);
189
+ // Here you can use the api
190
+ });
191
+ ```
192
+
193
+
194
+ ---------------------------------------
195
+
196
+ <a name="apiaddexternalmodulemoduleobj"></a>
197
+ ### api.addExternalModule(moduleObj)
198
+
199
+ This function is used to add external modules to the api object, each module is a function that takes 3 arguments: `defaultFuncs`, `api`, `ctx` and returns a function that will be added to the api object.
200
+
201
+ Example:
202
+ ```js
203
+ api.addExternalModule({
204
+ "example": function(defaultFuncs, api, ctx) {
205
+ return function () {
206
+ console.log("globalOptions", ctx.globalOptions);
207
+ };
208
+ }
209
+ });
210
+ ```
211
+
212
+ ---------------------------------------
213
+
214
+ <a name="addUserToGroup"></a>
215
+ ### api.addUserToGroup(userID, threadID, [callback])
216
+
217
+ Adds a user (or array of users) to a group chat.
218
+
219
+ __Arguments__
220
+
221
+ * `userID`: User ID or array of user IDs.
222
+ * `threadID`: Group chat ID.
223
+ * `callback(err)`: A callback called when the query is done (either with an error or with no arguments).
224
+
225
+ __Example__
226
+
227
+ ```js
228
+ api.addUserToGroup("1234567890", "0987654321", (err) => {
229
+ if(err)
230
+ return console.error(err);
231
+ console.log("Added user to group.");
232
+ });
233
+ ```
234
+
235
+ ---------------------------------------
236
+
237
+ <a name="changeAdminStatus"></a>
238
+ ### api.changeAdminStatus(threadID, adminIDs, adminStatus, [callback])
239
+
240
+ Given a adminID, or an array of adminIDs, will set the admin status of the user(s) to `adminStatus`.
241
+
242
+ __Arguments__
243
+ * `threadID`: ID of a group chat (can't use in one-to-one conversations)
244
+ * `adminIDs`: The id(s) of users you wish to admin/unadmin (string or an array).
245
+ * `adminStatus`: Boolean indicating whether the user(s) should be promoted to admin (`true`) or demoted to a regular user (`false`).
246
+ * `callback(err)`: A callback called when the query is done (either with an error or null).
247
+
248
+ __Example__
249
+
250
+ ```js
251
+ const fs = require("fs-extra");
252
+ const login = require("fb-chat-api-temp");
253
+
254
+ login({appState: JSON.parse(fs.readFileSync('appstate.json', 'utf8'))}, (err, api) => {
255
+ if (err) return console.error(err);
256
+
257
+ let threadID = "0000000000000000";
258
+ let newAdmins = ["111111111111111", "222222222222222"];
259
+ api.changeAdminStatus(threadID, newAdmins, true, editAdminsCallback);
260
+
261
+ let adminToRemove = "333333333333333";
262
+ api.changeAdminStatus(threadID, adminToRemove, false, editAdminsCallback);
263
+
264
+ });
265
+
266
+ function editAdminsCallback(err) {
267
+ if (err) return console.error(err);
268
+ }
269
+
270
+ ```
271
+
272
+ ---------------------------------------
273
+
274
+ <a name="changeArchivedStatus"></a>
275
+ ### api.changeArchivedStatus(threadOrThreads, archive, [callback])
276
+
277
+ Given a threadID, or an array of threadIDs, will set the archive status of the threads to `archive`. Archiving a thread will hide it from the logged-in user's inbox until the next time a message is sent or received.
278
+
279
+ __Arguments__
280
+ * `threadOrThreads`: The id(s) of the threads you wish to archive/unarchive.
281
+ * `archive`: Boolean indicating the new archive status to assign to the thread(s).
282
+ * `callback(err)`: A callback called when the query is done (either with an error or null).
283
+
284
+ __Example__
285
+
286
+ ```js
287
+ const fs = require("fs-extra");
288
+ const login = require("fb-chat-api-temp");
289
+
290
+ login({appState: JSON.parse(fs.readFileSync('appstate.json', 'utf8'))}, (err, api) => {
291
+ if(err) return console.error(err);
292
+
293
+ api.changeArchivedStatus("000000000000000", true, (err) => {
294
+ if(err) return console.error(err);
295
+ });
296
+ });
297
+ ```
298
+
299
+ ---------------------------------------
300
+
301
+ <a name="changeBlockedStatus"></a>
302
+ ### api.changeBlockedStatus(userID, block, [callback])
303
+
304
+ Prevents a user from privately contacting you. (Messages in a group chat will still be seen by both parties).
305
+
306
+ __Arguments__
307
+
308
+ * `userID`: User ID.
309
+ * `block`: Boolean indicating whether to block or unblock the user (true for block).
310
+ * `callback(err)`: A callback called when the query is done (either with an error or with no arguments).
311
+
312
+ ---------------------------------------
313
+
314
+ <a name="changeGroupImage"></a>
315
+ ### api.changeGroupImage(image, threadID, [callback])
316
+
317
+ Will change the group chat's image to the given image.
318
+
319
+ __Arguments__
320
+ * `image`: File stream of image.
321
+ * `threadID`: String representing the ID of the thread.
322
+ * `callback(err)`: A callback called when the change is done (either with an error or null).
323
+
324
+ __Example__
325
+
326
+ ```js
327
+ const fs = require("fs-extra");
328
+ const login = require("fb-chat-api-temp");
329
+
330
+ login({appState: JSON.parse(fs.readFileSync('appstate.json', 'utf8'))}, (err, api) => {
331
+ if(err) return console.error(err);
332
+
333
+ api.changeGroupImage(fs.createReadStream("./avatar.png"), "000000000000000", (err) => {
334
+ if(err) return console.error(err);
335
+ });
336
+ });
337
+ ```
338
+
339
+ ---------------------------------------
340
+
341
+ <a name="changeNickname"></a>
342
+ ### api.changeNickname(nickname, threadID, participantID, [callback])
343
+
344
+ Will change the thread user nickname to the one provided.
345
+
346
+ __Arguments__
347
+ * `nickname`: String containing a nickname. Leave empty to reset nickname.
348
+ * `threadID`: String representing the ID of the thread.
349
+ * `participantID`: String representing the ID of the user.
350
+ * `callback(err)`: An optional callback called when the change is done (either with an error or null).
351
+
352
+ __Example__
353
+
354
+ ```js
355
+ const fs = require("fs-extra");
356
+ const login = require("fb-chat-api-temp");
357
+
358
+ login({appState: JSON.parse(fs.readFileSync('appstate.json', 'utf8'))}, (err, api) => {
359
+ if(err) return console.error(err);
360
+
361
+ api.changeNickname("Example", "000000000000000", "000000000000000", (err) => {
362
+ if(err) return console.error(err);
363
+ });
364
+ });
365
+ ```
366
+
367
+ ---------------------------------------
368
+
369
+ <a name="changeThreadColor"></a>
370
+ ### api.changeThreadColor(color, threadID, [callback])
371
+
372
+ Will change the thread color to the given hex string color ("#0000ff"). Set it
373
+ to empty string if you want the default.
374
+
375
+ Note: the color needs to start with a "#".
376
+
377
+ __Arguments__
378
+ * `color`: String representing a theme ID (a list of theme ID can be found at `api.threadColors`).
379
+ * `threadID`: String representing the ID of the thread.
380
+ * `callback(err)`: A callback called when the change is done (either with an error or null).
381
+
382
+ __Example__
383
+
384
+ ```js
385
+ const fs = require("fs-extra");
386
+ const login = require("fb-chat-api-temp");
387
+
388
+ login({appState: JSON.parse(fs.readFileSync('appstate.json', 'utf8'))}, (err, api) => {
389
+ if(err) return console.error(err);
390
+
391
+ api.changeThreadColor("#0000ff", "000000000000000", (err) => {
392
+ if(err) return console.error(err);
393
+ });
394
+ });
395
+ ```
396
+
397
+ ---------------------------------------
398
+
399
+ <a name="changeThreadEmoji"></a>
400
+ ### api.changeThreadEmoji(emoji, threadID, [callback])
401
+
402
+ Will change the thread emoji to the one provided.
403
+
404
+ Note: The UI doesn't play nice with all emoji.
405
+
406
+ __Arguments__
407
+ * `emoji`: String containing a single emoji character.
408
+ * `threadID`: String representing the ID of the thread.
409
+ * `callback(err)`: A callback called when the change is done (either with an error or null).
410
+
411
+ __Example__
412
+
413
+ ```js
414
+ const fs = require("fs-extra");
415
+ const login = require("fb-chat-api-temp");
416
+
417
+ login({appState: JSON.parse(fs.readFileSync('appstate.json', 'utf8'))}, (err, api) => {
418
+ if(err) return console.error(err);
419
+
420
+ api.changeThreadEmoji("💯", "000000000000000", (err) => {
421
+ if(err) return console.error(err);
422
+ });
423
+ });
424
+ ```
425
+
426
+ ---------------------------------------
427
+
428
+ <a name="createNewGroup"></a>
429
+ ### api.createNewGroup(participantIDs, groupTitle, [callback])
430
+
431
+ Create a new group chat.
432
+
433
+ __Arguments__
434
+ * `participantIDs`: An array containing participant IDs. (*Length must be >= 2*)
435
+ * `groupTitle`: The title of the new group chat.
436
+ * `callback(err, threadID)`: A callback called when created.
437
+
438
+ ---------------------------------------
439
+
440
+ <a name="createPoll"></a>
441
+ ### api.createPoll(title, threadID, options, [callback]) (*temporary deprecated because Facebook is updating this feature*)
442
+
443
+ Creates a poll with the specified title and optional poll options, which can also be initially selected by the logged-in user.
444
+
445
+ __Arguments__
446
+ * `title`: String containing a title for the poll.
447
+ * `threadID`: String representing the ID of the thread.
448
+ * `options`: An optional `string : bool` dictionary to specify initial poll options and their initial states (selected/not selected), respectively.
449
+ * `callback(err)`: An optional callback called when the poll is posted (either with an error or null) - can omit the `options` parameter and use this as the third parameter if desired.
450
+
451
+ __Example__
452
+
453
+ ```js
454
+ const fs = require("fs-extra");
455
+ const login = require("fb-chat-api-temp");
456
+
457
+ login({appState: JSON.parse(fs.readFileSync('appstate.json', 'utf8'))}, (err, api) => {
458
+ if(err) return console.error(err);
459
+
460
+ api.createPoll("Example Poll", "000000000000000", {
461
+ "Option 1": false,
462
+ "Option 2": true
463
+ }, (err) => {
464
+ if(err) return console.error(err);
465
+ });
466
+ });
467
+ ```
468
+
469
+ ---------------------------------------
470
+
471
+ <a name="deleteMessage"></a>
472
+ ### api.deleteMessage(messageOrMessages, [callback])
473
+
474
+ Takes a messageID or an array of messageIDs and deletes the corresponding message.
475
+
476
+ __Arguments__
477
+ * `messageOrMessages`: A messageID string or messageID string array
478
+ * `callback(err)`: A callback called when the query is done (either with an error or null).
479
+
480
+ __Example__
481
+ ```js
482
+ const fs = require("fs-extra");
483
+ const login = require("fb-chat-api-temp");
484
+
485
+ login({appState: JSON.parse(fs.readFileSync('appstate.json', 'utf8'))}, (err, api) => {
486
+ if(err) return console.error(err);
487
+
488
+ api.listen((err, message) => {
489
+ if(message.body) {
490
+ api.sendMessage(message.body, message.threadID, (err, messageInfo) => {
491
+ if(err) return console.error(err);
492
+
493
+ api.deleteMessage(messageInfo.messageID);
494
+ });
495
+ }
496
+ });
497
+ });
498
+ ```
499
+
500
+ ---------------------------------------
501
+
502
+ <a name="deleteThread"></a>
503
+ ### api.deleteThread(threadOrThreads, [callback])
504
+
505
+ Given a threadID, or an array of threadIDs, will delete the threads from your account. Note that this does *not* remove the messages from Facebook's servers - anyone who hasn't deleted the thread can still view all of the messages.
506
+
507
+ __Arguments__
508
+
509
+ * `threadOrThreads` - The id(s) of the threads you wish to remove from your account.
510
+ * `callback(err)` - A callback called when the operation is done, maybe with an object representing an error.
511
+
512
+ __Example__
513
+
514
+ ```js
515
+ const fs = require("fs-extra");
516
+ const login = require("fb-chat-api-temp");
517
+
518
+ login({appState: JSON.parse(fs.readFileSync('appstate.json', 'utf8'))}, (err, api) => {
519
+ if(err) return console.error(err);
520
+
521
+ api.deleteThread("000000000000000", (err) => {
522
+ if(err) return console.error(err);
523
+ });
524
+ });
525
+ ```
526
+
527
+ ---------------------------------------
528
+
529
+ <a name="forwardAttachment"></a>
530
+ ### api.forwardAttachment(attachmentID, userOrUsers, [callback])
531
+
532
+ Forwards corresponding attachment to given userID or to every user from an array of userIDs
533
+
534
+ __Arguments__
535
+ * `attachmentID`: The ID field in the attachment object. Recorded audio cannot be forwarded.
536
+ * `userOrUsers`: A userID string or usersID string array
537
+ * `callback(err)`: A callback called when the query is done (either with an error or null).
538
+
539
+ ---------------------------------------
540
+
541
+ <a name="getAppState"></a>
542
+ ### api.getAppState()
543
+
544
+ Returns current appState which can be saved to a file or stored in a variable.
545
+
546
+ ---------------------------------------
547
+
548
+ <a name="getCurrentUserID"></a>
549
+ ### api.getCurrentUserID()
550
+
551
+ Returns the currently logged-in user's Facebook user ID.
552
+
553
+ ---------------------------------------
554
+
555
+ <a name="getEmojiUrl"></a>
556
+ ### api.getEmojiUrl(c, size, pixelRatio)
557
+
558
+ Returns the URL to a Facebook Messenger-style emoji image asset.
559
+
560
+ __note__: This function will return a URL regardless of whether the image at the URL actually exists.
561
+ This can happen if, for example, Messenger does not have an image asset for the requested emoji.
562
+
563
+ __Arguments__
564
+
565
+ * `c` - The emoji character
566
+ * `size` - The width and height of the emoji image; supported sizes are 32, 64, and 128
567
+ * `pixelRatio` - The pixel ratio of the emoji image; supported ratios are '1.0' and '1.5' (default is '1.0')
568
+
569
+ __Example__
570
+
571
+ ```js
572
+ const fs = require("fs-extra");
573
+ const login = require("fb-chat-api-temp");
574
+
575
+ login({appState: JSON.parse(fs.readFileSync('appstate.json', 'utf8'))}, (err, api) => {
576
+ if(err) return console.error(err);
577
+
578
+ // Prints https://static.xx.fbcdn.net/images/emoji.php/v8/z9c/1.0/128/1f40d.png
579
+ console.log('Snake emoji, 128px (128x128 with pixel ratio of 1.0');
580
+ console.log(api.getEmojiUrl('\ud83d\udc0d', 128));
581
+
582
+ // Prints https://static.xx.fbcdn.net/images/emoji.php/v8/ze1/1.5/128/1f40d.png
583
+ console.log('Snake emoji, 192px (128x128 with pixel ratio of 1.5');
584
+ console.log(api.getEmojiUrl('\ud83d\udc0d', 128, '1.5'));
585
+ });
586
+ ```
587
+
588
+ ---------------------------------------
589
+
590
+ <a name="getFriendsList"></a>
591
+ ### api.getFriendsList([callback])
592
+
593
+ Returns an array of objects with some information about your friends.
594
+
595
+ __Arguments__
596
+
597
+ * `callback(err, arr)` - A callback called when the query is done (either with an error or with an confirmation object). `arr` is an array of objects with the following fields: `alternateName`, `firstName`, `gender`, `userID`, `isFriend`, `fullName`, `profilePicture`, `type`, `profileUrl`, `vanity`, `isBirthday`.
598
+
599
+ __Example__
600
+
601
+ ```js
602
+ const fs = require("fs-extra");
603
+ const login = require("fb-chat-api-temp");
604
+
605
+ login({appState: JSON.parse(fs.readFileSync('appstate.json', 'utf8'))}, (err, api) => {
606
+ if(err) return console.error(err);
607
+
608
+ api.getFriendsList((err, data) => {
609
+ if(err) return console.error(err);
610
+
611
+ console.log(data.length);
612
+ });
613
+ });
614
+ ```
615
+
616
+ ---------------------------------------
617
+
618
+ <a name="getMessage"></a>
619
+ ### api.getMessage(threadID, messageID, [callback])
620
+
621
+ Returns message data from messageID
622
+
623
+ __Arguments__
624
+
625
+ * `threadID`: The ID of the thread you want to get the message from.
626
+ * `messageID`: The ID of the message you want to get.
627
+ * `callback(err, data)` - A callback called when the query is done.
628
+
629
+ ---------------------------------------
630
+ <a name="getThreadHistory"></a>
631
+ ### api.getThreadHistory(threadID, amount, timestamp, [callback])
632
+
633
+ Takes a threadID, number of messages, a timestamp, and a callback.
634
+
635
+ __note__: if you're getting a 500 error, it's possible that you're requesting too many messages. Try reducing that number and see if that works.
636
+
637
+ __Arguments__
638
+ * `threadID`: A threadID corresponding to the target chat
639
+ * `amount`: The amount of messages to *request*
640
+ * `timestamp`: Used to described the time of the most recent message to load. If timestamp is `undefined`, facebook will load the most recent messages.
641
+ * `callback(error, history)`: If error is null, history will contain an array of message objects.
642
+
643
+ __Example__
644
+
645
+ To load 50 messages at a time, we can use `undefined` as the timestamp to retrieve the most recent messages and use the timestamp of the earliest message to load the next 50.
646
+
647
+ ```js
648
+ var timestamp = undefined;
649
+
650
+ function loadNextThreadHistory(api){
651
+ api.getThreadHistory(threadID, 50, timestamp, (err, history) => {
652
+ if(err) return console.error(err);
653
+
654
+ /*
655
+ Since the timestamp is from a previous loaded message,
656
+ that message will be included in this history so we can discard it unless it is the first load.
657
+ */
658
+ if(timestamp != undefined) history.pop();
659
+
660
+ /*
661
+ Handle message history
662
+ */
663
+
664
+ timestamp = history[0].timestamp;
665
+ });
666
+ }
667
+ ```
668
+
669
+ ---------------------------------------
670
+
671
+ <a name="getThreadInfo"></a>
672
+ ### api.getThreadInfo(threadIDs, [callback])
673
+
674
+ Takes a threadID and a callback. Works for both single-user and group threads.
675
+
676
+ __Arguments__
677
+ * `threadIDs`: Either a string/number for one ID or an array of strings/numbers for a batched query.
678
+ * `callback(err, info)`: If `err` is `null`, `info` will return
679
+ <!-- * in nghiêng chữ -->
680
+ *if `threadIDs` is an array of IDs, `info` will be an array of objects with the following fields:*
681
+ ```js
682
+ {
683
+ "4000000000000000": {
684
+ threadID: "4000000000000000",
685
+ threadName: "Thread Name",
686
+ // ...
687
+ // thread info
688
+ },
689
+ "5000000000000000": {
690
+ threadID: "5000000000000000",
691
+ threadName: "Thread Name",
692
+ // ...
693
+ // thread info
694
+ },
695
+ // ...
696
+ }
697
+ ```
698
+
699
+ *if `threadIDs` is a single ID, `info` will be an object with the following fields:*
700
+ ```js
701
+ {
702
+ threadID: "4000000000000000",
703
+ threadName: "Thread Name",
704
+ // ...
705
+ // thread info
706
+ }
707
+ ```
708
+
709
+ `info` will contain the following fields:
710
+
711
+ | Key | Description |
712
+ | :---------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
713
+ | threadID | ID of the thread |
714
+ | participantIDs | Array of user IDs in the thread |
715
+ | threadName | Name of the thread. Usually the name of the user. In group chats, this will be empty if the name of the group chat is unset. |
716
+ | userInfo | An array contains info of members, which has the same structure as [`getUserInfo`](#getUserInfo), but add a key `id`, contain ID of member currently at. |
717
+ | nicknames | Map of nicknames for members of the thread. If there are no nicknames set, this will be `null`. Can be of the form: <ul><li>`{'123456789': "nickname"}`</ul></li> |
718
+ | unreadCount | Number of unread messages |
719
+ | messageCount | Number of messages |
720
+ | imageSrc | URL to the group chat photo. `null` if unset or a 1-1 thread. |
721
+ | timestamp | Timestamp of last activity |
722
+ | muteUntil | Timestamp at which the thread will no longer be muted. The timestamp will be -1 if the thread is muted indefinitely or null if the thread is not muted. |
723
+ | isGroup | boolean, true if this thread is a group thread (more than 2 participants). |
724
+ | isSubscribed | |
725
+ | folder | The folder that the thread is in. Can be one of: `INBOX`, `ARCHIVED`, `PENDING` or `OTHER` |
726
+ | isArchived | True if the thread is archived, false if not |
727
+ | cannotReplyReason | If you cannot reply to this thread, this will be a string stating why. Otherwise it will be null. |
728
+ | lastReadTimestamp | Timestamp of the last message that is marked as 'read' by the current user. |
729
+ | emoji | Object with key 'emoji' whose value is the emoji unicode character. Null if unset. |
730
+ | color | String form of the custom color in hexadecimal form. |
731
+ | adminIDs | Array of user IDs of the admins of the thread. Empty array if unset. Can be of the form:<ul><li>`[{ id: '123456789' }]`</li></ul> |
732
+ | approvalMode | `true` or `false`, used to check if this group requires admin approval to add users |
733
+ | approvalQueue | Array of object that has the following keys: <ul><li>`inviterID`: ID of the user invited the person to the group</li><li>`requesterID`: ID of the person waiting to be approved</li><li>`timestamp`: Request timestamp</li></ul> |
734
+ | inviteLink | Invite link for the thread. <ul><li>`enable`: `true` if the invite link is enabled, `false` if it is disabled</li> <li>`link`: Invite link</li></ul> |
735
+
736
+ `accountType` is one of the following:
737
+ - `"User"`
738
+ - `"Page"`
739
+ - `"UnavailableMessagingActor"`
740
+ - `"ReducedMessagingActor"`
741
+
742
+ ---------------------------------------
743
+
744
+ <a name="getThreadList"></a>
745
+ ### api.getThreadList(limit, timestamp, tags, [callback])
746
+
747
+ Returns information about the user's threads.
748
+
749
+ __Arguments__
750
+
751
+ * `limit`: Limit the number of threads to fetch.
752
+ * `timestamp`: Request threads *before* this date. `null` means *now*
753
+ * `tags`: An array describing which folder to fetch. It should be one of these:
754
+ - `["INBOX"]` *(same as `[]`)*
755
+ - `["ARCHIVED"]`
756
+ - `["PENDING"]`
757
+ - `["OTHER"]`
758
+ - `["INBOX", "unread"]`
759
+ - `["ARCHIVED", "unread"]`
760
+ - `["PENDING", "unread"]`
761
+ - `["OTHER", "unread"]`
762
+
763
+ *if you find something new, let us know*
764
+
765
+ * `callback(err, list)`: Callback called when the query is done (either with an error or with a proper result). `list` is an *array* with objects with the following properties same structure as [`getThreadInfo`](#getThreadInfo)
766
+
767
+ `accountType` is one of the following:
768
+ - `"User"`
769
+ - `"Page"`
770
+ - `"UnavailableMessagingActor"`
771
+ - `"ReducedMessagingActor"`
772
+
773
+ (*there might be more*)
774
+
775
+ <table>
776
+ <tr>
777
+ <th>Account type</th>
778
+ <th>Key</th>
779
+ <th>Description</th>
780
+ </tr>
781
+ <tr>
782
+ <td rowspan="12"><code>"User"</code></td>
783
+ <td>userID</td>
784
+ <td>ID of user</td>
785
+ </tr>
786
+ <tr>
787
+ <td>name</td>
788
+ <td>Full name of user</td>
789
+ </tr>
790
+ <tr>
791
+ <td>shortName</td>
792
+ <td>Short name of user (most likely first name)</td>
793
+ </tr>
794
+ <tr>
795
+ <td>gender</td>
796
+ <td>Either
797
+ <code>"MALE"</code>,
798
+ <code>"FEMALE"</code>,
799
+ <code>"NEUTER"</code> or
800
+ <code>"UNKNOWN"</code>
801
+ </td>
802
+ </tr>
803
+ <tr>
804
+ <td>url</td>
805
+ <td>URL of the user's Facebook profile</td>
806
+ </tr>
807
+ <tr>
808
+ <td>profilePicture</td>
809
+ <td>URL of the profile picture</td>
810
+ </tr>
811
+ <tr>
812
+ <td>username</td>
813
+ <td>Username of user or
814
+ <code>null</code>
815
+ </td>
816
+ </tr>
817
+ <tr>
818
+ <td>isViewerFriend</td>
819
+ <td>Is the user a friend of you?</td>
820
+ </tr>
821
+ <tr>
822
+ <td>isMessengerUser</td>
823
+ <td>Does the user use Messenger?</td>
824
+ </tr>
825
+ <tr>
826
+ <td>isVerified</td>
827
+ <td>Is the user verified? (Little blue tick mark)</td>
828
+ </tr>
829
+ <tr>
830
+ <td>isMessageBlockedByViewer</td>
831
+ <td>Is the user blocking messages from you?</td>
832
+ </tr>
833
+ <tr>
834
+ <td>isViewerCoworker</td>
835
+ <td>Is the user your coworker?
836
+ </td>
837
+ </tr>
838
+
839
+ <tr>
840
+ <td rowspan="10"><code>"Page"</code></td>
841
+ <td>userID</td>
842
+ <td>ID of the page</td>
843
+ </tr>
844
+ <tr>
845
+ <td>name</td>
846
+ <td>Name of the fanpage</td>
847
+ </tr>
848
+ <tr>
849
+ <td>url</td>
850
+ <td>URL of the fanpage</td>
851
+ </tr>
852
+ <tr>
853
+ <td>profilePicture</td>
854
+ <td>URL of the profile picture</td>
855
+ </tr>
856
+ <tr>
857
+ <td>username</td>
858
+ <td>Username of user or
859
+ <code>null</code>
860
+ </td>
861
+ </tr>
862
+ <tr>
863
+ <td>acceptsMessengerUserFeedback</td>
864
+ <td></td>
865
+ </tr>
866
+ <tr>
867
+ <td>isMessengerUser</td>
868
+ <td>Does the fanpage use Messenger?</td>
869
+ </tr>
870
+ <tr>
871
+ <td>isVerified</td>
872
+ <td>Is the fanpage verified? (Little blue tick mark)</td>
873
+ </tr>
874
+ <tr>
875
+ <td>isMessengerPlatformBot</td>
876
+ <td>Is the fanpage a bot</td>
877
+ </tr>
878
+ <tr>
879
+ <td>isMessageBlockedByViewer</td>
880
+ <td>Is the fanpage blocking messages from you?</td>
881
+ </tr>
882
+
883
+ <tr>
884
+ <td rowspan="7"><code>"ReducedMessagingActor"</code><br />(account requres verification,<br />messages are hidden)</td>
885
+ <td>userID</td>
886
+ <td>ID of the user</td>
887
+ </tr>
888
+ <tr>
889
+ <td>name</td>
890
+ <td>Name of the user</td>
891
+ </tr>
892
+ <tr>
893
+ <td>url</td>
894
+ <td>
895
+ <code>null</code>
896
+ </td>
897
+ </tr>
898
+ <tr>
899
+ <td>profilePicture</td>
900
+ <td>URL of the default Facebook profile picture</td>
901
+ </tr>
902
+ <tr>
903
+ <td>username</td>
904
+ <td>Username of user</td>
905
+ </td>
906
+ </tr>
907
+ <tr>
908
+ <td>acceptsMessengerUserFeedback</td>
909
+ <td></td>
910
+ </tr>
911
+ <tr>
912
+ <td>isMessageBlockedByViewer</td>
913
+ <td>Is the user blocking messages from you?</td>
914
+ </tr>
915
+ <tr>
916
+ <td rowspan="7"><code>"UnavailableMessagingActor"</code><br />(account disabled/removed)</td>
917
+ <td>userID</td>
918
+ <td>ID of the user</td>
919
+ </tr>
920
+ <tr>
921
+ <td>name</td>
922
+ <td><em>Facebook User</em> in user's language</td>
923
+ </tr>
924
+ <tr>
925
+ <td>url</td>
926
+ <td><code>null</code></td>
927
+ </tr>
928
+ <tr>
929
+ <td>profilePicture</td>
930
+ <td>URL of the default **male** Facebook profile picture</td>
931
+ </tr>
932
+ <tr>
933
+ <td>username</td>
934
+ <td><code>null</code></td>
935
+ </tr>
936
+ <tr>
937
+ <td>acceptsMessengerUserFeedback</td>
938
+ <td></td>
939
+ </tr>
940
+ <tr>
941
+ <td>isMessageBlockedByViewer</td>
942
+ <td>Is the user blocking messages from you?</td>
943
+ </tr>
944
+ </table>
945
+
946
+
947
+ In a case that some account type is not supported, we return just this *(but you can't rely on it)* and log a warning to the console:
948
+
949
+ | Key | Description |
950
+ | ----------- | --------------------- |
951
+ | accountType | type, can be anything |
952
+ | userID | ID of the account |
953
+ | name | Name of the account |
954
+
955
+
956
+ ---------------------------------------
957
+
958
+ <a name="getThreadPictures"></a>
959
+ ### api.getThreadPictures(threadID, offset, limit, [callback])
960
+
961
+ Returns pictures sent in the thread.
962
+
963
+ __Arguments__
964
+
965
+ * `threadID`: A threadID corresponding to the target chat
966
+ * `offset`: Start index of picture to retrieve, where 0 is the most recent picture
967
+ * `limit`: Number of pictures to get, incrementing from the offset index
968
+ * `callback(err, arr)`: A callback called when the query is done (either with an error or with an confirmation object). `arr` is an array of objects with `uri`, `width`, and `height`.
969
+
970
+ ---------------------------------------
971
+
972
+ <a name="getUserID"></a>
973
+ ### api.getUserID(name, [callback])
974
+
975
+ Given the full name or vanity name of a Facebook user, event, page, group or app, the call will perform a Facebook Graph search and return all corresponding IDs (order determined by Facebook).
976
+
977
+ __Arguments__
978
+
979
+ * `name` - A string being the name of the item you're looking for.
980
+ * `callback(err, obj)` - A callback called when the search is done (either with an error or with the resulting object). `obj` is an array which contains all of the items that facebook graph search found, ordered by "importance". Each item in the array has the following properties: `userID`,`photoUrl`,`indexRank`, `name`, `isVerified`, `profileUrl`, `category`, `score`, `type` (type is generally user, group, page, event or app).
981
+
982
+ __Example__
983
+
984
+ ```js
985
+ const fs = require("fs-extra");
986
+ const login = require("fb-chat-api-temp");
987
+
988
+ login({appState: JSON.parse(fs.readFileSync('appstate.json', 'utf8'))}, (err, api) => {
989
+ if(err) return console.error(err);
990
+
991
+ api.getUserID("Marc Zuckerbot", (err, data) => {
992
+ if(err) return console.error(err);
993
+
994
+ // Send the message to the best match (best by Facebook's criteria)
995
+ var msg = "Hello!"
996
+ var threadID = data[0].userID;
997
+ api.sendMessage(msg, threadID);
998
+ });
999
+ });
1000
+ ```
1001
+
1002
+ ---------------------------------------
1003
+
1004
+ <a name="getUserInfo"></a>
1005
+ ### api.getUserInfo(ids, [callback])
1006
+
1007
+ Will get some information about the given users.
1008
+
1009
+ __Arguments__
1010
+
1011
+ * `ids` - Either a string/number for one ID or an array of strings/numbers for a batched query.
1012
+ * `callback(err, obj)` - A callback called when the query is done (either with an error or with an confirmation object). `obj` is a mapping from userId to another object containing the following properties:
1013
+
1014
+ | Key | Description |
1015
+ | ------------- | ------------------------------------------------------------------ |
1016
+ | name | Name of the user |
1017
+ | firstName | First name of the user |
1018
+ | vanity | the username of the user if any |
1019
+ | thumbSrc | URL of the profile picture |
1020
+ | profileUrl | URL of the profile |
1021
+ | gender | the gender of the user, with 1 is Female, 2 is Male, 0 is unknown |
1022
+ | type | type is generally user, group, page, event or app |
1023
+ | isFriend | is the user a friend of the current user, either `true` or `false` |
1024
+ | isBirthday | is the user having a birthday today, either `true` or `false` |
1025
+ | searchTokens | an array of strings that can be used to search for the user |
1026
+ | alternateName | |
1027
+
1028
+ __Example__
1029
+
1030
+ ```js
1031
+ const fs = require("fs-extra");
1032
+ const login = require("fb-chat-api-temp");
1033
+
1034
+ login({appState: JSON.parse(fs.readFileSync('appstate.json', 'utf8'))}, (err, api) => {
1035
+ if(err) return console.error(err);
1036
+
1037
+ api.getUserInfo([1, 2, 3, 4], (err, ret) => {
1038
+ if(err) return console.error(err);
1039
+
1040
+ for(var prop in ret) {
1041
+ if(ret.hasOwnProperty(prop) && ret[prop].isBirthday) {
1042
+ api.sendMessage("Happy birthday :)", prop);
1043
+ }
1044
+ }
1045
+ });
1046
+ });
1047
+ ```
1048
+
1049
+ ---------------------------------------
1050
+
1051
+ <a name="threadColors"></a>
1052
+ ### api.threadColors
1053
+
1054
+ A dictionary mapping names of all currently valid thread themes to their theme ID that are accepted by [`api.changeThreadColor`](#changeThreadColor). These themes, listed below, are the ones present in the palette UI used for selecting thread themes on the Messenger client.
1055
+ <details>
1056
+ <summary><b>List of colors:</b></summary>
1057
+ <br>
1058
+
1059
+ - MessengerBlue: `196241301102133`
1060
+ - Viking: `1928399724138152`
1061
+ - GoldenPoppy: `174636906462322`
1062
+ - RadicalRed: `2129984390566328`
1063
+ - Shocking: `2058653964378557`
1064
+ - FreeSpeechGreen: `2136751179887052`
1065
+ - Pumpkin: `175615189761153`
1066
+ - LightCoral: `980963458735625`
1067
+ - MediumSlateBlue: `234137870477637`
1068
+ - DeepSkyBlue: `2442142322678320`
1069
+ - BrilliantRose: `169463077092846`
1070
+ - DefaultBlue: `196241301102133`
1071
+ - HotPink: `169463077092846`
1072
+ - AquaBlue: `2442142322678320`
1073
+ - BrightPurple: `234137870477637`
1074
+ - CoralPink: `980963458735625`
1075
+ - Orange: `175615189761153`
1076
+ - Green: `2136751179887052`
1077
+ - LavenderPurple: `2058653964378557`
1078
+ - Red: `2129984390566328`
1079
+ - Yellow: `174636906462322`
1080
+ - TealBlue: `1928399724138152`
1081
+ - Aqua: `417639218648241`
1082
+ - Mango: `930060997172551`
1083
+ - Berry: `164535220883264`
1084
+ - Citrus: `370940413392601`
1085
+ - Candy: `205488546921017`
1086
+ - Earth: `1833559466821043`
1087
+ - Support: `365557122117011`
1088
+ - Music: `339021464972092`
1089
+ - Pride: `1652456634878319`
1090
+ - DoctorStrange: `538280997628317`
1091
+ - LoFi: `1060619084701625`
1092
+ - Sky: `3190514984517598`
1093
+ - LunarNewYear: `357833546030778`
1094
+ - Celebration: `627144732056021`
1095
+ - Chill: `390127158985345`
1096
+ - StrangerThings: `1059859811490132`
1097
+ - Dune: `1455149831518874`
1098
+ - Care: `275041734441112`
1099
+ - Astrology: `3082966625307060`
1100
+ - JBalvin: `184305226956268`
1101
+ - Birthday: `621630955405500`
1102
+ - Cottagecore: `539927563794799`
1103
+ - Ocean: `736591620215564`
1104
+ - Love: `741311439775765`
1105
+ - TieDye: `230032715012014`
1106
+ - Monochrome: `788274591712841`
1107
+ - Default: `3259963564026002`
1108
+ - Rocket: `582065306070020`
1109
+ - Berry2: `724096885023603`
1110
+ - Candy2: `624266884847972`
1111
+ - Unicorn: `273728810607574`
1112
+ - Tropical: `262191918210707`
1113
+ - Maple: `2533652183614000`
1114
+ - Sushi: `909695489504566`
1115
+ - Citrus2: `557344741607350`
1116
+ - Lollipop: `280333826736184`
1117
+ - Shadow: `271607034185782`
1118
+ - Rose: `1257453361255152`
1119
+ - Lavender: `571193503540759`
1120
+ - Tulip: `2873642949430623`
1121
+ - Classic: `3273938616164733`
1122
+ - Peach: `3022526817824329`
1123
+ - Honey: `672058580051520`
1124
+ - Kiwi: `3151463484918004`
1125
+ - Grape: `193497045377796`
1126
+ - NonBinary: `737761000603635`
1127
+ - ~~StarWars: `809305022860427`~~ (Facebook removed it.)
1128
+ </details>
1129
+
1130
+ ---------------------------------------
1131
+
1132
+ <a name="handleMessageRequest"></a>
1133
+ ### api.handleMessageRequest(threadID, accept, [callback])
1134
+
1135
+ Accept or ignore message request(s) with thread id `threadID`.
1136
+
1137
+ __Arguments__
1138
+
1139
+ * `threadID`: A threadID or array of threadIDs corresponding to the target thread(s). Can be numbers or strings.
1140
+ * `accept`: Boolean indicating the new status to assign to the message request(s); true for inbox, false to others.
1141
+ * `callback(err)`: A callback called when the query is done (with an error or with null).
1142
+
1143
+ ---------------------------------------
1144
+
1145
+ <a name="httpGet"></a>
1146
+ ### api.httpGet(url, form, [customHeader], [callback], [notAPI])
1147
+
1148
+ Get data from a URL with method GET.
1149
+
1150
+ __Arguments__
1151
+
1152
+ * `url`: A string being the URL to get data from.
1153
+ * `form`: An object containing the form data to send.
1154
+ * `customHeader`: An object containing custom headers to send.
1155
+ * `callback(err, data)`: A callback called when the query is done (either with an error or with the resulting data).
1156
+ * `notAPI`: A boolean indicating whether the request is made from the API or not (default: false, meaning it's from the API).
1157
+
1158
+ ---------------------------------------
1159
+
1160
+ <a name="httpPost"></a>
1161
+ ### api.httpPost(url, form, [customHeader], [callback], [notAPI])
1162
+
1163
+ Get data from a URL with method POST.
1164
+
1165
+ __Arguments__
1166
+
1167
+ * Similar to [`api.httpGet`](#httpGet).
1168
+
1169
+ ---------------------------------------
1170
+
1171
+ <a name="httpPostFormData"></a>
1172
+ ### api.httpPostFormData(url, form, [customHeader], [callback], [notAPI])
1173
+
1174
+ Post form data to a URL.
1175
+
1176
+ __Arguments__
1177
+
1178
+ * Similar to [`api.httpGet`](#httpGet).
1179
+
1180
+ ---------------------------------------
1181
+
1182
+ <a name="listen"></a>
1183
+ ### ~~api.listen([callback])~~
1184
+ <a name="listenMqtt"></a>
1185
+ ### api.listenMqtt([callback])
1186
+
1187
+ Will call `callback` when a new message is received on this account.
1188
+ By default this won't receive events (joining/leaving a chat, title change etc...) but it can be activated with `api.setOptions({listenEvents: true})`. This will by default ignore messages sent by the current account, you can enable listening to your own messages with `api.setOptions({selfListen: true})`. This returns an `EventEmitter` that contains function `stopListening` that will stop the `listen` loop and is guaranteed to prevent any future calls to the callback given to `listen`. An immediate call to `stopListening` when an error occurs will prevent the listen function to continue.
1189
+
1190
+ If `callback` is not defined, or isn't a `Function`, you can listen to messages with event `message` and `error` from `EventEmitter` returned by this function.
1191
+
1192
+ __Arguments__
1193
+
1194
+ - `callback(error, message)`: A callback called every time the logged-in account receives a new message.
1195
+
1196
+ <a name="message"></a>
1197
+ __Message__
1198
+
1199
+ The message object will contain different fields based on its type (as determined by its `type` field). By default, the only type that will be listened for is `message`. If enabled through [setOptions](#setOptions), the message object may alternatively represent an event e.g. a read receipt. The available event types are as follows:
1200
+
1201
+ <table>
1202
+ <tr>
1203
+ <th>Event Type</th>
1204
+ <th>Field</th>
1205
+ <th>Description</th>
1206
+ </tr>
1207
+ <tr>
1208
+ <td rowspan="9">
1209
+ <code>"message"</code><br />
1210
+ A message was sent to a thread.
1211
+ </td>
1212
+ <td><code>attachments</code></td>
1213
+ <td>An array of attachments to the message. Attachments vary in type, see the attachments table below.</td>
1214
+ </tr>
1215
+ <tr>
1216
+ <td><code>body</code></td>
1217
+ <td>The string corresponding to the message that was just received.</td>
1218
+ </tr>
1219
+ <tr>
1220
+ <td><code>isGroup</code></td>
1221
+ <td>boolean, true if this thread is a group thread (more than 2 participants).</td>
1222
+ </tr>
1223
+ <tr>
1224
+ <td><code>mentions</code></td>
1225
+ <td>An object containing people mentioned/tagged in the message in the format { id: name }</td>
1226
+ </tr>
1227
+ <tr>
1228
+ <td><code>messageID</code></td>
1229
+ <td>A string representing the message ID.</td>
1230
+ </tr>
1231
+ <tr>
1232
+ <td><code>senderID</code></td>
1233
+ <td>The id of the person who sent the message in the chat with threadID.</td>
1234
+ </tr>
1235
+ <tr>
1236
+ <td><code>threadID</code></td>
1237
+ <td>The threadID representing the thread in which the message was sent.</td>
1238
+ </tr>
1239
+ <tr>
1240
+ <td><code>isUnread</code></td>
1241
+ <td>Boolean representing whether or not the message was read.</td>
1242
+ </tr>
1243
+ <tr>
1244
+ <td><code>type</code></td>
1245
+ <td>For this event type, this will always be the string <code>"message"</code>.</td>
1246
+ </tr>
1247
+ <tr>
1248
+ <td rowspan="6">
1249
+ <code>"event"</code><br />
1250
+ An event occurred within a thread. Note that receiving this event type needs to be enabled with `api.setOptions({ listenEvents: true })`
1251
+ </td>
1252
+ <td><code>author</code></td>
1253
+ <td>The person who performed the event.</td>
1254
+ </tr>
1255
+ <tr>
1256
+ <td><code>logMessageBody</code></td>
1257
+ <td>String printed in the chat.</td>
1258
+ </tr>
1259
+ <tr>
1260
+ <td><code>logMessageData</code></td>
1261
+ <td>Data relevant to the event.</td>
1262
+ </tr>
1263
+ <tr>
1264
+ <td><code>logMessageType</code></td>
1265
+ <td>String representing the type of event (<code>log:subscribe</code>, <code>log:unsubscribe</code>, <code>log:thread-name</code>, <code>log:thread-color</code>, <code>log:thread-icon</code>, <code>log:user-nickname</code>)</td>
1266
+ </tr>
1267
+ <tr>
1268
+ <td><code>threadID</code></td>
1269
+ <td>The threadID representing the thread in which the message was sent.</td>
1270
+ </tr>
1271
+ <tr>
1272
+ <td><code>type</code></td>
1273
+ <td>For this event type, this will always be the string <code>"event"</code>.</td>
1274
+ </tr>
1275
+ <tr>
1276
+ <td rowspan="5">
1277
+ <code>"typ"</code><br />
1278
+ A user in a thread is typing. Note that receiving this event type needs to be enabled with `api.setOptions({ listenTyping: true })`
1279
+ </td>
1280
+ <td><code>from</code></td>
1281
+ <td>ID of the user who started/stopped typing.</td>
1282
+ </tr>
1283
+ <tr>
1284
+ <td><code>fromMobile</code></td>
1285
+ <td>Boolean representing whether or not the person's using a mobile device to type.</td>
1286
+ </tr>
1287
+ <tr>
1288
+ <td><code>isTyping</code></td>
1289
+ <td>Boolean representing whether or not a person started typing.</td>
1290
+ </tr>
1291
+ <tr>
1292
+ <td><code>threadID</code></td>
1293
+ <td>The threadID representing the thread in which a user is typing.</td>
1294
+ </tr>
1295
+ <tr>
1296
+ <td><code>type</code></td>
1297
+ <td>For this event type, this will always be the string <code>"typ"</code>.</td>
1298
+ </tr>
1299
+ <tr>
1300
+ <td rowspan="3">
1301
+ <code>"read"</code><br />
1302
+ The current API user has read a message.
1303
+ </td>
1304
+ <td><code>threadID</code></td>
1305
+ <td>The threadID representing the thread in which the message was sent.</td>
1306
+ </tr>
1307
+ <tr>
1308
+ <td><code>time</code></td>
1309
+ <td>The time at which the user read the message.</td>
1310
+ </tr>
1311
+ <tr>
1312
+ <td><code>type</code></td>
1313
+ <td>For this event type, this will always be the string <code>"read"</code>.</td>
1314
+ </tr>
1315
+ <tr>
1316
+ <td rowspan="4">
1317
+ <code>"read_receipt"</code><br />
1318
+ A user within a thread has seen a message sent by the API user.
1319
+ </td>
1320
+ <td><code>reader</code></td>
1321
+ <td>ID of the user who just read the message.</td>
1322
+ </tr>
1323
+ <tr>
1324
+ <td><code>threadID</code></td>
1325
+ <td>The thread in which the message was read.</td>
1326
+ </tr>
1327
+ <tr>
1328
+ <td><code>time</code></td>
1329
+ <td>The time at which the reader read the message.</td>
1330
+ </tr>
1331
+ <tr>
1332
+ <td><code>type</code></td>
1333
+ <td>For this event type, this will always be the string <code>"read_receipt"</code>.</td>
1334
+ </tr>
1335
+ <tr>
1336
+ <td rowspan="8">
1337
+ <code>"message_reaction"</code><br />
1338
+ A user has sent a reaction to a message.
1339
+ </td>
1340
+ <td><code>messageID</code></td>
1341
+ <td>The ID of the message</td>
1342
+ </tr>
1343
+ <tr>
1344
+ <td><code>offlineThreadingID</code></td>
1345
+ <td>The offline message ID</td>
1346
+ </tr>
1347
+ <tr>
1348
+ <td><code>reaction</code></td>
1349
+ <td>Contains reaction emoji</td>
1350
+ </tr>
1351
+ <tr>
1352
+ <td><code>senderID</code></td>
1353
+ <td>ID of the author the message, where has been reaction added</td>
1354
+ </tr>
1355
+ <tr>
1356
+ <td><code>threadID</code></td>
1357
+ <td>ID of the thread where the message has been sent</td>
1358
+ </tr>
1359
+ <tr>
1360
+ <td><code>timestamp</code></td>
1361
+ <td>Unix Timestamp (in miliseconds) when the reaction was sent</td>
1362
+ </tr>
1363
+ <tr>
1364
+ <td><code>type</code></td>
1365
+ <td>For this event type, this will always be the string <code>"message_reaction"</code>.</td>
1366
+ </tr>
1367
+ <tr>
1368
+ <td><code>userID</code></td>
1369
+ <td>ID of the reaction sender</td>
1370
+ </tr>
1371
+ <tr>
1372
+ <td rowspan="4"><a name="presence"></a>
1373
+ <code>"presence"</code><br />
1374
+ The online status of the user's friends. Note that receiving this event type needs to be enabled with <code>api.setOptions({ updatePresence: true })</code>
1375
+ </td>
1376
+ <td><code>statuses</code></td>
1377
+ <td>The online status of the user. <code>0</code> means the user is idle (away for 2 minutes) and <code>2</code> means the user is online (we don't know what 1 or above 2 means...).</td>
1378
+ </tr>
1379
+ <tr>
1380
+ <td><code>timestamp</code></td>
1381
+ <td>The time when the user was last online.</td>
1382
+ </tr>
1383
+ <tr>
1384
+ <td><code>type</code></td>
1385
+ <td>For this event type, this will always be the string <code>"presence"</code>.</td>
1386
+ </tr>
1387
+ <tr>
1388
+ <td><code>userID</code></td>
1389
+ <td>The ID of the user whose status this packet is describing.</td>
1390
+ </tr>
1391
+ <tr>
1392
+ <td rowspan="5">
1393
+ <code>"message_unsend"</code><br />
1394
+ A revoke message request for a message from a thread was received.
1395
+ </td>
1396
+ <td><code>threadID</code></td>
1397
+ <td>The threadID representing the thread in which the revoke message request was received.</td>
1398
+ </tr>
1399
+ <tr>
1400
+ <td><code>senderID</code></td>
1401
+ <td>The id of the person who request to revoke message on threadID.</td>
1402
+ </tr>
1403
+ <tr>
1404
+ <td><code>messageID</code></td>
1405
+ <td>A string representing the message ID that the person request to revoke message want to.</td>
1406
+ </tr>
1407
+ <tr>
1408
+ <td><code>deletionTimestamp</code></td>
1409
+ <td>The time when the request was sent.</td>
1410
+ </tr>
1411
+ <tr>
1412
+ <td><code>type</code></td>
1413
+ <td>For this event type, this will always be the string <code>"message_unsend"</code>.</td>
1414
+ </tr>
1415
+ <tr>
1416
+ <td rowspan="10">
1417
+ <code>"message_reply"</code><br />
1418
+ A reply message was sent to a thread.
1419
+ </td>
1420
+ <td><code>attachments</code></td>
1421
+ <td>An array of attachments to the message. Attachments vary in type, see the attachments table below.</td>
1422
+ </tr>
1423
+ <tr>
1424
+ <td><code>body</code></td>
1425
+ <td>The string corresponding to the message that was just received.</td>
1426
+ </tr>
1427
+ <tr>
1428
+ <td><code>isGroup</code></td>
1429
+ <td>boolean, true if this thread is a group thread (more than 2 participants).</td>
1430
+ </tr>
1431
+ <tr>
1432
+ <td><code>mentions</code></td>
1433
+ <td>An object containing people mentioned/tagged in the message in the format { id: name }</td>
1434
+ </tr>
1435
+ <tr>
1436
+ <td><code>messageID</code></td>
1437
+ <td>A string representing the message ID.</td>
1438
+ </tr>
1439
+ <tr>
1440
+ <td><code>senderID</code></td>
1441
+ <td>The id of the person who sent the message in the chat with threadID.</td>
1442
+ </tr>
1443
+ <tr>
1444
+ <td><code>threadID</code></td>
1445
+ <td>The threadID representing the thread in which the message was sent.</td>
1446
+ </tr>
1447
+ <tr>
1448
+ <td><code>isUnread</code></td>
1449
+ <td>Boolean representing whether or not the message was read.</td>
1450
+ </tr>
1451
+ <tr>
1452
+ <td><code>type</code></td>
1453
+ <td>For this event type, this will always be the string <code>"message_reply"</code>.</td>
1454
+ </tr>
1455
+ <tr>
1456
+ <td><code>messageReply</code></td>
1457
+ <td>An object represent a message being replied. Content inside is the same like a normal <code>"message"</code> event.</td>
1458
+ </tr>
1459
+ </table>
1460
+
1461
+ __Attachments__
1462
+
1463
+ Similar to how messages can vary based on their `type`, so too can the `attachments` within `"message"` events. Each attachment will consist of an object of one of the following types:
1464
+
1465
+ | Attachment Type | Fields |
1466
+ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
1467
+ | `"sticker"` | `ID`, `url`, `packID`, `spriteUrl`, `spriteUrl2x`, `width`, `height`, `caption`, `description`, `frameCount`, `frameRate`, `framesPerRow`, `framesPerCol` |
1468
+ | `"file"` | `ID`, `filename`, `url`, `isMalicious`, `contentType` |
1469
+ | `"photo"` | `ID`, `filename`, `thumbnailUrl`, `previewUrl`, `previewWidth`, `previewHeight`, `largePreviewUrl`, `largePreviewWidth`, `largePreviewHeight` |
1470
+ | `"animated_image"` | `ID`, `filename`, `previewUrl`, `previewWidth`, `previewHeight`, `url`, `width`, `height` |
1471
+ | `"video"` | `ID`, `filename`, `previewUrl`, `previewWidth`, `previewHeight`, `url`, `width`, `height`, `duration`, `videoType` |
1472
+ | `"audio"` | `ID`, `filename`, `audioType`, `duration`, `url`, `isVoiceMail` |
1473
+ | `"location"` | `ID`, `latitude`, `longitude`, `image`, `width`, `height`, `url`, `address` |
1474
+ | `"share"` | `ID`, `url`, `title`, `description`, `source`, `image`, `width`, `height`, `playable`, `duration`, `playableUrl`, `subattachments`, `properties` |
1475
+
1476
+ __Example__
1477
+
1478
+ ```js
1479
+ const fs = require("fs-extra");
1480
+ const login = require("fb-chat-api-temp");
1481
+
1482
+ // Simple echo bot. He'll repeat anything that you say.
1483
+ // Will stop when you say '/stop'
1484
+
1485
+ login({appState: JSON.parse(fs.readFileSync('appstate.json', 'utf8'))}, (err, api) => {
1486
+ if(err) return console.error(err);
1487
+
1488
+ api.setOptions({listenEvents: true});
1489
+
1490
+ var listenEmitter = api.listen((err, event) => {
1491
+ if(err) return console.error(err);
1492
+
1493
+ switch (event.type) {
1494
+ case "message":
1495
+ if(event.body === '/stop') {
1496
+ api.sendMessage("Goodbye...", event.threadID);
1497
+ return listenEmitter.stopListening();
1498
+ }
1499
+ api.markAsRead(event.threadID, (err) => {
1500
+ if(err) console.log(err);
1501
+ });
1502
+ api.sendMessage("TEST BOT: " + event.body, event.threadID);
1503
+ break;
1504
+ case "event":
1505
+ console.log(event);
1506
+ break;
1507
+ }
1508
+ });
1509
+ });
1510
+ ```
1511
+
1512
+ ---------------------------------------
1513
+
1514
+ <a name="logout"></a>
1515
+ ### api.logout([callback])
1516
+
1517
+ Logs out the current user.
1518
+
1519
+ __Arguments__
1520
+
1521
+ * `callback(err)`: A callback called when the query is done (either with an error or with null).
1522
+
1523
+ ---------------------------------------
1524
+
1525
+ <a name="markAsDelivered"></a>
1526
+ ### api.markAsDelivered(threadID, messageID, [callback]])
1527
+
1528
+ Given a threadID and a messageID will mark that message as delivered. If a message is marked as delivered that tells facebook servers that it was recieved.
1529
+
1530
+ You can also mark new messages as delivered automatically. This is enabled by default. See [api.setOptions](#setOptions).
1531
+
1532
+ __Arguments__
1533
+
1534
+ * `threadID` - The id of the thread in which you want to mark the message as delivered.
1535
+ * `messageID` - The id of the message want to mark as delivered.
1536
+ * `callback(err)` - A callback called when the operation is done maybe with an object representing an error.
1537
+
1538
+ __Example__
1539
+
1540
+ ```js
1541
+ const fs = require("fs-extra");
1542
+ const login = require("facebook-chat-api");
1543
+
1544
+ login({appState: JSON.parse(fs.readFileSync('appstate.json', 'utf8'))}, (err, api) => {
1545
+ if(err) return console.error(err);
1546
+
1547
+ api.listen((err, message) => {
1548
+ if(err) return console.error(err);
1549
+
1550
+ // Marks messages as delivered immediately after they're received
1551
+ api.markAsDelivered(message.threadID, message.messageID);
1552
+ });
1553
+ });
1554
+ ```
1555
+
1556
+ ---------------------------------------
1557
+
1558
+ <a name="markAsRead"></a>
1559
+ ### api.markAsRead(threadID, [read, [callback]])
1560
+
1561
+ Given a threadID will mark all the unread messages in a thread as read. Facebook will take a couple of seconds to show that you've read the messages.
1562
+
1563
+ You can also mark new messages as read automatically. See [api.setOptions](#setOptions). But be careful, this will make your account getting banned, especially when receiving *HUGE* amount of messages.
1564
+
1565
+ __Arguments__
1566
+
1567
+ * `threadID` - The id of the thread in which you want to mark the messages as read.
1568
+ * `read` - An optional boolean where `true` means to mark the message as being "read" and `false` means to mark the message as being "unread".
1569
+ * `callback(err)` - A callback called when the operation is done maybe with an object representing an error.
1570
+
1571
+ __Example__
1572
+
1573
+ ```js
1574
+ const fs = require("fs-extra");
1575
+ const login = require("fb-chat-api-temp");
1576
+
1577
+ login({appState: JSON.parse(fs.readFileSync('appstate.json', 'utf8'))}, (err, api) => {
1578
+ if(err) return console.error(err);
1579
+
1580
+ api.listen((err, message) => {
1581
+ if(err) return console.error(err);
1582
+
1583
+ // Marks messages as read immediately after they're received
1584
+ api.markAsRead(message.threadID);
1585
+ });
1586
+ });
1587
+ ```
1588
+
1589
+ ---------------------------------------
1590
+
1591
+ <a name="markAsReadAll"></a>
1592
+ ### api.markAsReadAll([callback])
1593
+
1594
+ This function will mark all of messages in your inbox readed.
1595
+
1596
+ ---------------------------------------
1597
+
1598
+ <a name="markAsSeen"></a>
1599
+ ### api.markAsSeen([seenTimestamp], [callback])
1600
+
1601
+ This function will mark your entire inbox as seen (don't be confused with read!).
1602
+
1603
+ ---------------------------------------
1604
+
1605
+ <a name="muteThread"></a>
1606
+ ### api.muteThread(threadID, muteSeconds, [callback])
1607
+
1608
+ Mute a chat for a period of time, or unmute a chat.
1609
+
1610
+ __Arguments__
1611
+
1612
+ * `threadID` - The ID of the chat you want to mute.
1613
+ * `muteSeconds` - Mute the chat for this amount of seconds. Use `0` to unmute a chat. Use '-1' to mute a chat indefinitely.
1614
+ * `callback(err)` - A callback called when the operation is done maybe with an object representing an error.
1615
+
1616
+ __Example__
1617
+
1618
+ ```js
1619
+ const fs = require("fs-extra");
1620
+ const login = require("fb-chat-api-temp");
1621
+
1622
+ login({appState: JSON.parse(fs.readFileSync('appstate.json', 'utf8'))}, (err, api) => {
1623
+ if(err) return console.error(err);
1624
+
1625
+ api.listen((err, message) => {
1626
+ if(err) return console.error(err);
1627
+
1628
+ // Mute all incoming chats for one minute
1629
+ api.muteThread(message.threadID, 60);
1630
+ });
1631
+ });
1632
+ ```
1633
+
1634
+ ---------------------------------------
1635
+
1636
+ <a name="removeUserFromGroup"></a>
1637
+ ### api.removeUserFromGroup(userID, threadID, [callback])
1638
+
1639
+ Removes a user from a group chat.
1640
+
1641
+ __Arguments__
1642
+
1643
+ * `userID`: User ID.
1644
+ * `threadID`: Group chat ID.
1645
+ * `callback(err)`: A callback called when the query is done (either with an error or with no arguments).
1646
+
1647
+ ---------------------------------------
1648
+
1649
+ <a name="resolvePhotoUrl"></a>
1650
+ ### api.resolvePhotoUrl(photoID, [callback])
1651
+
1652
+ Resolves the URL to the full-size photo, given its ID. This function is useful for retrieving the full-size photo URL
1653
+ of image attachments in messages, returned by [`api.getThreadHistory`](#getThreadHistory).
1654
+
1655
+ __Arguments__
1656
+
1657
+ * `photoID`: Photo ID.
1658
+ * `callback(err, url)`: A callback called when the query is done (either with an error or with the photo's URL). `url` is a string with the photo's URL.
1659
+
1660
+ ---------------------------------------
1661
+
1662
+ <a name="searchForThread"></a>
1663
+ ### api.searchForThread(name, [callback])
1664
+
1665
+ > This part is outdated.
1666
+ > see #396
1667
+
1668
+ Takes a chat title (thread name) and returns matching results as a formatted threads array (ordered according to Facebook).
1669
+
1670
+ __Arguments__
1671
+ * `name`: A messageID string or messageID string array
1672
+ * `callback(err, obj)`: A callback called when the query is done (either with an error or a thread object). The object passed in the callback has the following shape: `threadID`, <del>`participants`</del>, `participantIDs`, `formerParticipants`, `name`, `nicknames`, `snippet`, `snippetHasAttachment`, `snippetAttachments`, `snippetSender`, `unreadCount`, `messageCount`, `imageSrc`, `timestamp`, `serverTimestamp`, `muteSettings`, `isCanonicalUser`, `isCanonical`, `canonicalFbid`, `isSubscribed`, `rootMessageThreadingID`, `folder`, `isArchived`, `recipientsLoadable`, `hasEmailParticipant`, `readOnly`, `canReply`, `composerEnabled`, `blockedParticipants`, `lastMessageID`
1673
+
1674
+ ---------------------------------------
1675
+
1676
+ <a name="sendMessage"></a>
1677
+ ### api.sendMessage(message, threadID, [callback], messageID)
1678
+
1679
+ Sends the given message to the threadID.
1680
+
1681
+ __Arguments__
1682
+
1683
+ * `message`: A string (for backward compatibility) or a message object as described below.
1684
+ * `threadID`: A string, number, or array representing a thread. It happens to be someone's userID in the case of a one to one conversation or an array of userIDs when starting a new group chat.
1685
+ * `callback(err, messageInfo)`: (Optional) A callback called when sending the message is done (either with an error or with an confirmation object). `messageInfo` contains the `threadID` where the message was sent and a `messageID`, as well as the `timestamp` of the message.
1686
+ * `messageID`: (Optional) A string representing a message you want to reply.
1687
+
1688
+ __Message Object__:
1689
+
1690
+ Various types of message can be sent:
1691
+ * *Regular:* set field `body` to the desired message as a string.
1692
+ * *Sticker:* set a field `sticker` to the desired sticker ID.
1693
+ * *File or image:* Set field `attachment` to a readable stream or an array of readable streams.
1694
+ * *URL:* set a field `url` to the desired URL.
1695
+ * *Emoji:* set field `emoji` to the desired emoji as a string and set field `emojiSize` with size of the emoji (`small`, `medium`, `large`)
1696
+ * *Mentions:* set field `mentions` to an array of objects. Objects should have the `tag` field set to the text that should be highlighted in the mention. The object should have an `id` field, where the `id` is the user id of the person being mentioned. The instance of `tag` that is highlighted is determined through indexOf, an optional `fromIndex`
1697
+ can be passed in to specify the start index to start searching for the `tag` text
1698
+ in `body` (default=0). (See below for an example.)
1699
+ * *Location:* set field `location` to an object with `latitude` and `longitude` fields. Optionally set field `current` of the `location` object to true to indicate the location is the user’s current location. Otherwise the location will be sent as a pinned location.
1700
+
1701
+ Note that a message can only be a regular message (which can be empty) and optionally one of the following: a sticker, an attachment or a url.
1702
+
1703
+ __Tip__: to find your own ID, you can look inside the cookies. The `userID` is under the name `c_user`.
1704
+
1705
+ __Example (Basic Message)__
1706
+ ```js
1707
+ const fs = require("fs-extra");
1708
+ const login = require("fb-chat-api-temp");
1709
+
1710
+ login({appState: JSON.parse(fs.readFileSync('appstate.json', 'utf8'))}, (err, api) => {
1711
+ if(err) return console.error(err);
1712
+
1713
+ var yourID = "000000000000000";
1714
+ var msg = {body: "Hey!"};
1715
+ api.sendMessage(msg, yourID);
1716
+ });
1717
+ ```
1718
+
1719
+ __Example (File upload)__
1720
+ ```js
1721
+ const fs = require("fs-extra");
1722
+ const login = require("fb-chat-api-temp");
1723
+
1724
+ login({appState: JSON.parse(fs.readFileSync('appstate.json', 'utf8'))}, (err, api) => {
1725
+ if(err) return console.error(err);
1726
+
1727
+ // This example uploads an image called image.jpg
1728
+ var yourID = "000000000000000";
1729
+ var msg = {
1730
+ body: "Hey!",
1731
+ attachment: fs.createReadStream(__dirname + '/image.jpg')
1732
+ }
1733
+ api.sendMessage(msg, yourID);
1734
+ });
1735
+ ```
1736
+
1737
+ __Example (Mention)__
1738
+ ```js
1739
+ const login = require("fb-chat-api-temp");
1740
+
1741
+ login({email: "EMAIL", password: "PASSWORD"}, (err, api) => {
1742
+ if(err) return console.error(err);
1743
+
1744
+ api.listen((err, message) => {
1745
+ if (message && message.body) {
1746
+ // Getting the actual sender name from ID involves calling
1747
+ // `api.getThreadInfo` and `api.getUserInfo`
1748
+ api.sendMessage({
1749
+ body: 'Hello @Sender! @Sender!',
1750
+ mentions: [{
1751
+ tag: '@Sender',
1752
+ id: message.senderID,
1753
+ fromIndex: 9, // Highlight the second occurrence of @Sender
1754
+ }],
1755
+ }, message.threadID);
1756
+ }
1757
+ });
1758
+ });
1759
+ ```
1760
+
1761
+ __Example (Location)__
1762
+ ```js
1763
+ const login = require("fb-chat-api-temp");
1764
+ login({email: "EMAIL", password: "PASSWORD"}, (err, api) => {
1765
+ if(err) return console.error(err);
1766
+ var yourID = "000000000000000";
1767
+ const msg = {
1768
+ location: { latitude: 48.858093, longitude: 2.294694, current: true },
1769
+ };
1770
+ api.sendMessage(msg, yourID);
1771
+ });
1772
+ ```
1773
+
1774
+ ---------------------------------------
1775
+
1776
+ <a name="sendTypingIndicator"></a>
1777
+ ### api.sendTypingIndicator(threadID, [callback])
1778
+
1779
+ Sends a "USERNAME is typing" indicator to other members of the thread indicated by `threadID`. This indication will disappear after 30 second or when the `end` function is called. The `end` function is returned by `api.sendTypingIndicator`.
1780
+
1781
+ __Arguments__
1782
+
1783
+ * `threadID`: Group chat ID.
1784
+ * `callback(err)`: A callback called when the query is done (with an error or with null).
1785
+
1786
+ ---------------------------------------
1787
+
1788
+ <a name="setMessageReaction"></a>
1789
+ ### api.setMessageReaction(reaction, messageID, [callback], forceCustomReaction)
1790
+
1791
+ Sets reaction on message
1792
+
1793
+ __Arguments__
1794
+
1795
+ * `reaction`: A string containing either an emoji, an emoji in unicode, or an emoji shortcut (see list of supported emojis below). The string can be left empty ("") in order to remove a reaction.
1796
+ * `messageID`: A string representing the message ID.
1797
+ * `callback(err)`: A callback called when sending the reaction is done.
1798
+ * `forceCustomReaction`: Forcing the use of an emoji for setting reaction **(WARNING: NOT TESTED, YOU SHOULD NOT USE THIS AT ALL, UNLESS YOU'RE TESTING A NEW EMOJI)**
1799
+
1800
+ __Supported Emojis__
1801
+
1802
+ | Emoji | Text | Unicode | Shortcuts |
1803
+ | ----- | ---- | -------------- | --------------------------- |
1804
+ | 😍 | `😍` | `\uD83D\uDE0D` | `:love:`, `:heart_eyes:` |
1805
+ | 😆 | `😆` | `\uD83D\uDE06` | `:haha:`, `:laughing:` |
1806
+ | 😮 | `😮` | `\uD83D\uDE2E` | `:wow:`, `:open_mouth:` |
1807
+ | 😢 | `😢` | `\uD83D\uDE22` | `:sad:`, `:cry:` |
1808
+ | 😠 | `😠` | `\uD83D\uDE20` | `:angry:` |
1809
+ | 👍 | `👍` | `\uD83D\uDC4D` | `:like:`, `:thumbsup:` |
1810
+ | 👎 | `👎` | `\uD83D\uDC4E` | `:dislike:`, `:thumbsdown:` |
1811
+ | ❤ | `❤` | `\u2764` | `:heart:` |
1812
+ | 💗 | `💗` | `\uD83D\uDC97` | `:glowingheart:` |
1813
+
1814
+ ---------------------------------------
1815
+
1816
+ <a name="setOptions"></a>
1817
+ ### api.setOptions(options)
1818
+
1819
+ Sets various configurable options for the api.
1820
+
1821
+ __Arguments__
1822
+
1823
+ * `options` - An object containing the new values for the options that you want
1824
+ to set. If the value for an option is unspecified, it is unchanged. The following options are possible.
1825
+ - `logLevel`: The desired logging level as determined by npmlog. Choose
1826
+ from either `"silly"`, `"verbose"`, `"info"`, `"http"`, `"warn"`, `"error"`, or `"silent"`.
1827
+ - `selfListen`: (Default `false`) Set this to `true` if you want your api
1828
+ to receive messages from its own account. This is to be used with
1829
+ caution, as it can result in loops (a simple echo bot will send messages
1830
+ forever).
1831
+ - `listenEvents`: (Default `false`) Will make [api.listen](#listen) also handle events (look at api.listen for more details).
1832
+ - `pageID`: (Default empty) Makes [api.listen](#listen) only receive messages through the page specified by that ID. Also makes [api.sendMessage](#sendMessage) send from the page.
1833
+ - `updatePresence`: (Default `false`) Will make [api.listen](#listen) also return `presence` ([api.listen](#presence) for more details).
1834
+ - `forceLogin`: (Default `false`) Will automatically approve of any recent logins and continue with the login process.
1835
+ - `userAgent`: (Default `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/600.3.18 (KHTML, like Gecko) Version/8.0.3 Safari/600.3.18`) The desired simulated User Agent.
1836
+ - `autoMarkDelivery`: (Default `true`) Will automatically mark new messages as delivered. See [api.markAsDelivered](#markAsDelivered).
1837
+ - `autoMarkRead`: (Default `false`) Will automatically mark new messages as read/seen. See [api.markAsRead](#markAsRead).
1838
+ - `proxy`: (Default empty) Set this to proxy server address to use proxy. Note: Only HTTP Proxies which support CONNECT method is supported.
1839
+ - `online`: (Default `true`) Set account's online state.
1840
+
1841
+ __Example__
1842
+
1843
+ ```js
1844
+ const fs = require("fs-extra");
1845
+ const login = require("fb-chat-api-temp");
1846
+
1847
+ // Simple echo bot. This will send messages forever.
1848
+
1849
+ login({appState: JSON.parse(fs.readFileSync('appstate.json', 'utf8'))}, (err, api) => {
1850
+ if(err) return console.error(err);
1851
+
1852
+ api.setOptions({
1853
+ selfListen: true,
1854
+ logLevel: "silent"
1855
+ });
1856
+
1857
+ api.listen((err, message) => {
1858
+ if(err) return console.error(err);
1859
+
1860
+ // Ignore empty messages (photos etc.)
1861
+ if (typeof message.body === "string") {
1862
+ api.sendMessage(message.body, message.threadID);
1863
+ }
1864
+ });
1865
+ });
1866
+ ```
1867
+
1868
+ ---------------------------------------
1869
+
1870
+ <a name="setPostReaction"></a>
1871
+ ### api.setPostReaction(postID, type, [callback])
1872
+ __Arguments__
1873
+
1874
+ * `postID`: id of the post to react.
1875
+ * `type`: A string reaction type or key reaction.
1876
+ * `callback(err, obj)`: A callback called when the query is done.
1877
+
1878
+ | Key | Reaction Type |
1879
+ | --- | ------------- |
1880
+ | 0 | unlike |
1881
+ | 1 | like |
1882
+ | 2 | heart |
1883
+ | 16 | love |
1884
+ | 4 | haha |
1885
+ | 3 | wow |
1886
+ | 7 | sad |
1887
+ | 8 | angry |
1888
+
1889
+ ---------------------------------------
1890
+
1891
+ <a name="setTitle"></a>
1892
+ ### api.setTitle(newTitle, threadID, [callback])
1893
+
1894
+ Sets the title of the group chat with thread id `threadID` to `newTitle`.
1895
+
1896
+ Note: This will not work if the thread id corresponds to a single-user chat or if the bot is not in the group chat.
1897
+
1898
+ __Arguments__
1899
+
1900
+ * `newTitle`: A string representing the new title.
1901
+ * `threadID`: A string or number representing a thread. It happens to be someone's userID in the case of a one to one conversation.
1902
+ * `callback(err, obj)` - A callback called when sending the message is done (either with an error or with an confirmation object). `obj` contains only the threadID where the message was sent.
1903
+
1904
+ ---------------------------------------
1905
+
1906
+ <a name="unsendMessage"></a>
1907
+ ### api.unsendMessage(messageID, [callback])
1908
+
1909
+ Revokes a message from anyone that could see that message with `messageID`
1910
+
1911
+ Note: This will only work if the message is sent by you and was sent less than 10 minutes ago.
1912
+
1913
+ __Arguments__
1914
+
1915
+ * `messageID`: Message ID you want to unsend.
1916
+ * `callback(err)`: A callback called when the query is done (with an error or with null).
1917
+
1918
+ ---------------------------------------
1919
+
1920
+ <a name="uploadAttachments"></a>
1921
+ ### api.uploadAttachment(attachments, [callback])
1922
+ This function is used to upload attachments to Facebook. It is used internally by [api.sendMessage](#apisendmessagemessage-threadid-callback-messageid).
1923
+
1924
+ __Arguments__
1925
+ <!-- readable stream or an array of readable streams. -->
1926
+ * `attachments`: A readable stream or an array of readable streams to upload.
1927
+ * `callback(err, info)`: A callback called when the upload is done (either with an error or with the uploaded file info).
1928
+
1929
+ __Example__
1930
+
1931
+ ```js
1932
+ const fs = require("fs-extra");
1933
+ const login = require("fb-chat-api-temp");
1934
+ login({appState: JSON.parse(fs.readFileSync('appstate.json', 'utf8'))}, (err, api) => {
1935
+ if (err)
1936
+ return console.error(err);
1937
+
1938
+ // Send a local file as a stream
1939
+ const iamgeOne = fs.createReadStream(__dirname + '/image.jpg');
1940
+ const iamgeTwo = fs.createReadStream(__dirname + '/image2.jpg');
1941
+
1942
+ api.uploadAttachment([iamgeOne, iamgeTwo], (err, attachments) => {
1943
+ if(err)
1944
+ return console.error(err);
1945
+
1946
+ console.log(attachments);
1947
+ });
1948
+ });
includes/login/index.js ADDED
@@ -0,0 +1,392 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("./utils");
4
+ const log = require("npmlog");
5
+
6
+ const checkVerified = null;
7
+
8
+ const defaultLogRecordSize = 100;
9
+ log.maxRecordSize = defaultLogRecordSize;
10
+
11
+ function setOptions(globalOptions, options) {
12
+ Object.keys(options).map(function (key) {
13
+ switch (key) {
14
+ case 'online':
15
+ globalOptions.online = Boolean(options.online);
16
+ break;
17
+ case 'logLevel':
18
+ log.level = options.logLevel;
19
+ globalOptions.logLevel = options.logLevel;
20
+ break;
21
+ case 'logRecordSize':
22
+ log.maxRecordSize = options.logRecordSize;
23
+ globalOptions.logRecordSize = options.logRecordSize;
24
+ break;
25
+ case 'selfListen':
26
+ globalOptions.selfListen = Boolean(options.selfListen);
27
+ break;
28
+ case 'selfListenEvent':
29
+ globalOptions.selfListenEvent = options.selfListenEvent;
30
+ break;
31
+ case 'listenEvents':
32
+ globalOptions.listenEvents = Boolean(options.listenEvents);
33
+ break;
34
+ case 'pageID':
35
+ globalOptions.pageID = options.pageID.toString();
36
+ break;
37
+ case 'updatePresence':
38
+ globalOptions.updatePresence = Boolean(options.updatePresence);
39
+ break;
40
+ case 'forceLogin':
41
+ globalOptions.forceLogin = Boolean(options.forceLogin);
42
+ break;
43
+ case 'userAgent':
44
+ globalOptions.userAgent = options.userAgent;
45
+ break;
46
+ case 'autoMarkDelivery':
47
+ globalOptions.autoMarkDelivery = Boolean(options.autoMarkDelivery);
48
+ break;
49
+ case 'autoMarkRead':
50
+ globalOptions.autoMarkRead = Boolean(options.autoMarkRead);
51
+ break;
52
+ case 'listenTyping':
53
+ globalOptions.listenTyping = Boolean(options.listenTyping);
54
+ break;
55
+ case 'proxy':
56
+ if (typeof options.proxy != "string") {
57
+ delete globalOptions.proxy;
58
+ utils.setProxy();
59
+ } else {
60
+ globalOptions.proxy = options.proxy;
61
+ utils.setProxy(globalOptions.proxy);
62
+ }
63
+ break;
64
+ case 'autoReconnect':
65
+ globalOptions.autoReconnect = Boolean(options.autoReconnect);
66
+ break;
67
+ case 'emitReady':
68
+ globalOptions.emitReady = Boolean(options.emitReady);
69
+ break;
70
+ default:
71
+ log.warn("setOptions", "Unrecognized option given to setOptions: " + key);
72
+ break;
73
+ }
74
+ });
75
+ }
76
+
77
+ function buildAPI(globalOptions, html, jar) {
78
+ const maybeCookie = jar.getCookies("https://www.facebook.com").filter(function (val) {
79
+ return val.cookieString().split("=")[0] === "c_user";
80
+ });
81
+
82
+ const objCookie = jar.getCookies("https://www.facebook.com").reduce(function (obj, val) {
83
+ obj[val.cookieString().split("=")[0]] = val.cookieString().split("=")[1];
84
+ return obj;
85
+ }, {});
86
+
87
+ if (maybeCookie.length === 0) {
88
+ throw { error: "Error retrieving userID. This can be caused by a lot of things, including getting blocked by Facebook for logging in from an unknown location. Try logging in with a browser to verify." };
89
+ }
90
+
91
+ if (html.indexOf("/checkpoint/block/?next") > -1) {
92
+ log.warn("login", "Checkpoint detected. Please log in with a browser to verify.");
93
+ }
94
+
95
+ const userID = maybeCookie[0].cookieString().split("=")[1].toString();
96
+ const i_userID = objCookie.i_user || null;
97
+ log.info("login", `Logged in as ${userID}`);
98
+
99
+ try {
100
+ clearInterval(checkVerified);
101
+ } catch (_) { }
102
+
103
+ const clientID = (Math.random() * 2147483648 | 0).toString(16);
104
+
105
+
106
+ const oldFBMQTTMatch = html.match(/irisSeqID:"(.+?)",appID:219994525426954,endpoint:"(.+?)"/);
107
+ let mqttEndpoint = null;
108
+ let region = null;
109
+ let irisSeqID = null;
110
+ let noMqttData = null;
111
+
112
+ if (oldFBMQTTMatch) {
113
+ irisSeqID = oldFBMQTTMatch[1];
114
+ mqttEndpoint = oldFBMQTTMatch[2];
115
+ region = new URL(mqttEndpoint).searchParams.get("region").toUpperCase();
116
+ log.info("login", `Got this account's message region: ${region}`);
117
+ } else {
118
+ const newFBMQTTMatch = html.match(/{"app_id":"219994525426954","endpoint":"(.+?)","iris_seq_id":"(.+?)"}/);
119
+ if (newFBMQTTMatch) {
120
+ irisSeqID = newFBMQTTMatch[2];
121
+ mqttEndpoint = newFBMQTTMatch[1].replace(/\\\//g, "/");
122
+ region = new URL(mqttEndpoint).searchParams.get("region").toUpperCase();
123
+ log.info("login", `Got this account's message region: ${region}`);
124
+ } else {
125
+ const legacyFBMQTTMatch = html.match(/(\["MqttWebConfig",\[\],{fbid:")(.+?)(",appID:219994525426954,endpoint:")(.+?)(",pollingEndpoint:")(.+?)(3790])/);
126
+ if (legacyFBMQTTMatch) {
127
+ mqttEndpoint = legacyFBMQTTMatch[4];
128
+ region = new URL(mqttEndpoint).searchParams.get("region").toUpperCase();
129
+ log.warn("login", `Cannot get sequence ID with new RegExp. Fallback to old RegExp (without seqID)...`);
130
+ log.info("login", `Got this account's message region: ${region}`);
131
+ log.info("login", `[Unused] Polling endpoint: ${legacyFBMQTTMatch[6]}`);
132
+ } else {
133
+ log.warn("login", "Cannot get MQTT region & sequence ID.");
134
+ noMqttData = html;
135
+ }
136
+ }
137
+ }
138
+
139
+ // All data available to api functions
140
+ const ctx = {
141
+ userID: userID,
142
+ i_userID: i_userID,
143
+ jar: jar,
144
+ clientID: clientID,
145
+ globalOptions: globalOptions,
146
+ loggedIn: true,
147
+ access_token: 'NONE',
148
+ clientMutationId: 0,
149
+ mqttClient: undefined,
150
+ lastSeqId: irisSeqID,
151
+ syncToken: undefined,
152
+ wsReqNumber: 0,
153
+ wsTaskNumber: 0,
154
+ mqttEndpoint,
155
+ region,
156
+ firstListen: true
157
+ };
158
+
159
+ const api = {
160
+ setOptions: setOptions.bind(null, globalOptions),
161
+ getAppState: function getAppState() {
162
+ const appState = utils.getAppState(jar);
163
+ // filter duplicate
164
+ return appState.filter((item, index, self) => self.findIndex((t) => { return t.key === item.key; }) === index);
165
+ }
166
+ };
167
+
168
+ if (noMqttData) {
169
+ api["htmlData"] = noMqttData;
170
+ }
171
+
172
+ const apiFuncNames = [
173
+ 'addExternalModule',
174
+ 'addUserToGroup',
175
+ 'changeAdminStatus',
176
+ 'changeArchivedStatus',
177
+ 'changeAvatar',
178
+ 'changeBio',
179
+ 'changeBlockedStatus',
180
+ 'changeGroupImage',
181
+ 'changeNickname',
182
+ 'changeThreadColor',
183
+ 'changeThreadEmoji',
184
+ 'createNewGroup',
185
+ 'createPoll',
186
+ 'deleteMessage',
187
+ 'deleteThread',
188
+ 'forwardAttachment',
189
+ 'getCurrentUserID',
190
+ 'getEmojiUrl',
191
+ 'getFriendsList',
192
+ 'getMessage',
193
+ 'getThreadHistory',
194
+ 'getThreadInfo',
195
+ 'getThreadList',
196
+ 'getThreadPictures',
197
+ 'getUserID',
198
+ 'getUserInfo',
199
+ 'handleMessageRequest',
200
+ 'listenMqtt',
201
+ 'logout',
202
+ 'markAsDelivered',
203
+ 'markAsRead',
204
+ 'markAsReadAll',
205
+ 'markAsSeen',
206
+ 'muteThread',
207
+ 'refreshFb_dtsg',
208
+ 'removeUserFromGroup',
209
+ 'resolvePhotoUrl',
210
+ 'searchForThread',
211
+ 'sendMessage',
212
+ 'sendTypingIndicator',
213
+ 'setMessageReaction',
214
+ 'setPostReaction',
215
+ 'setTitle',
216
+ 'threadColors',
217
+ 'unsendMessage',
218
+ 'unfriend',
219
+ 'editMessage',
220
+ 'shareContact',
221
+ 'sendComment',
222
+
223
+ // HTTP
224
+ 'httpGet',
225
+ 'httpPost',
226
+ 'httpPostFormData',
227
+
228
+ 'uploadAttachment'
229
+ ];
230
+
231
+ const defaultFuncs = utils.makeDefaults(html, i_userID || userID, ctx);
232
+
233
+ // Load all api functions in a loop
234
+ apiFuncNames.map(function (v) {
235
+ api[v] = require('./src/' + v)(defaultFuncs, api, ctx);
236
+ });
237
+
238
+ //Removing original `listen` that uses pull.
239
+ //Map it to listenMqtt instead for backward compatibly.
240
+ api.listen = api.listenMqtt;
241
+
242
+ return [ctx, defaultFuncs, api];
243
+ }
244
+
245
+ // Helps the login
246
+ function loginHelper(appState, email, password, globalOptions, callback, prCallback) {
247
+ let mainPromise = null;
248
+ const jar = utils.getJar();
249
+
250
+ // If we're given an appState we loop through it and save each cookie
251
+ // back into the jar.
252
+ if (appState) {
253
+ // check and convert cookie to appState
254
+ if (utils.getType(appState) === 'Array' && appState.some(c => c.name)) {
255
+ appState = appState.map(c => {
256
+ c.key = c.name;
257
+ delete c.name;
258
+ return c;
259
+ });
260
+ }
261
+ else if (utils.getType(appState) === 'String') {
262
+ const arrayAppState = [];
263
+ appState.split(';').forEach(c => {
264
+ const [key, value] = c.split('=');
265
+
266
+ arrayAppState.push({
267
+ key: (key || "").trim(),
268
+ value: (value || "").trim(),
269
+ domain: "facebook.com",
270
+ path: "/",
271
+ expires: new Date().getTime() + 1000 * 60 * 60 * 24 * 365
272
+ });
273
+ });
274
+ appState = arrayAppState;
275
+ }
276
+
277
+ appState.map(function (c) {
278
+ const str = c.key + "=" + c.value + "; expires=" + c.expires + "; domain=" + c.domain + "; path=" + c.path + ";";
279
+ jar.setCookie(str, "http://" + c.domain);
280
+ });
281
+
282
+ // Load the main page.
283
+ mainPromise = utils
284
+ .get('https://www.facebook.com/', jar, null, globalOptions, { noRef: true })
285
+ .then(utils.saveCookies(jar));
286
+ } else {
287
+ if (email) {
288
+ throw { error: "Currently, the login method by email and password is no longer supported, please use the login method by appState" };
289
+ }
290
+ else {
291
+ throw { error: "No appState given." };
292
+ }
293
+ }
294
+
295
+ let ctx = null;
296
+ let _defaultFuncs = null;
297
+ let api = null;
298
+
299
+ mainPromise = mainPromise
300
+ .then(function (res) {
301
+ // Hacky check for the redirection that happens on some ISPs, which doesn't return statusCode 3xx
302
+ const reg = /<meta http-equiv="refresh" content="0;url=([^"]+)[^>]+>/;
303
+ const redirect = reg.exec(res.body);
304
+ if (redirect && redirect[1]) {
305
+ return utils
306
+ .get(redirect[1], jar, null, globalOptions)
307
+ .then(utils.saveCookies(jar));
308
+ }
309
+ return res;
310
+ })
311
+ .then(function (res) {
312
+ const html = res.body;
313
+ const stuff = buildAPI(globalOptions, html, jar);
314
+ ctx = stuff[0];
315
+ _defaultFuncs = stuff[1];
316
+ api = stuff[2];
317
+ return res;
318
+ });
319
+
320
+ // given a pageID we log in as a page
321
+ if (globalOptions.pageID) {
322
+ mainPromise = mainPromise
323
+ .then(function () {
324
+ return utils
325
+ .get('https://www.facebook.com/' + ctx.globalOptions.pageID + '/messages/?section=messages&subsection=inbox', ctx.jar, null, globalOptions);
326
+ })
327
+ .then(function (resData) {
328
+ let url = utils.getFrom(resData.body, 'window.location.replace("https:\\/\\/www.facebook.com\\', '");').split('\\').join('');
329
+ url = url.substring(0, url.length - 1);
330
+
331
+ return utils
332
+ .get('https://www.facebook.com' + url, ctx.jar, null, globalOptions);
333
+ });
334
+ }
335
+
336
+ // At the end we call the callback or catch an exception
337
+ mainPromise
338
+ .then(function () {
339
+ log.info("login", 'Done logging in.');
340
+ return callback(null, api);
341
+ })
342
+ .catch(function (e) {
343
+ log.error("login", e.error || e);
344
+ callback(e);
345
+ });
346
+ }
347
+
348
+ function login(loginData, options, callback) {
349
+ if (utils.getType(options) === 'Function' || utils.getType(options) === 'AsyncFunction') {
350
+ callback = options;
351
+ options = {};
352
+ }
353
+
354
+ const globalOptions = {
355
+ selfListen: false,
356
+ selfListenEvent: false,
357
+ listenEvents: false,
358
+ listenTyping: false,
359
+ updatePresence: false,
360
+ forceLogin: false,
361
+ autoMarkDelivery: true,
362
+ autoMarkRead: false,
363
+ autoReconnect: true,
364
+ logRecordSize: defaultLogRecordSize,
365
+ online: true,
366
+ emitReady: false,
367
+ userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/600.3.18 (KHTML, like Gecko) Version/8.0.3 Safari/600.3.18"
368
+ };
369
+
370
+ setOptions(globalOptions, options);
371
+
372
+ let prCallback = null;
373
+ if (utils.getType(callback) !== "Function" && utils.getType(callback) !== "AsyncFunction") {
374
+ let rejectFunc = null;
375
+ let resolveFunc = null;
376
+ var returnPromise = new Promise(function (resolve, reject) {
377
+ resolveFunc = resolve;
378
+ rejectFunc = reject;
379
+ });
380
+ prCallback = function (error, api) {
381
+ if (error) {
382
+ return rejectFunc(error);
383
+ }
384
+ return resolveFunc(api);
385
+ };
386
+ callback = prCallback;
387
+ }
388
+ loginHelper(loginData.appState, loginData.email, loginData.password, globalOptions, callback, prCallback);
389
+ return returnPromise;
390
+ }
391
+
392
+ module.exports = login;
includes/login/utils.js ADDED
@@ -0,0 +1,1544 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* eslint-disable no-prototype-builtins */
2
+ "use strict";
3
+
4
+ let request = promisifyPromise(require("request").defaults({ jar: true, proxy: process.env.FB_PROXY }));
5
+ const stream = require("stream");
6
+ const log = require("npmlog");
7
+ const querystring = require("querystring");
8
+ const url = require("url");
9
+
10
+ class CustomError extends Error {
11
+ constructor(obj) {
12
+ if (typeof obj === 'string')
13
+ obj = { message: obj };
14
+ if (typeof obj !== 'object' || obj === null)
15
+ throw new TypeError('Object required');
16
+ obj.message ? super(obj.message) : super();
17
+ Object.assign(this, obj);
18
+ }
19
+ }
20
+
21
+ function callbackToPromise(func) {
22
+ return function (...args) {
23
+ return new Promise((resolve, reject) => {
24
+ func(...args, (err, data) => {
25
+ if (err)
26
+ reject(err);
27
+ else
28
+ resolve(data);
29
+ });
30
+ });
31
+ };
32
+ }
33
+
34
+ function isHasCallback(func) {
35
+ if (typeof func !== "function")
36
+ return false;
37
+ return func.toString().split("\n")[0].match(/(callback|cb)\s*\)/) !== null;
38
+ }
39
+
40
+ // replace for bluebird.promisify (but this only applies best to the `request` package)
41
+ function promisifyPromise(promise) {
42
+ const keys = Object.keys(promise);
43
+ let promise_;
44
+ if (
45
+ typeof promise === "function"
46
+ && isHasCallback(promise)
47
+ )
48
+ promise_ = callbackToPromise(promise);
49
+ else
50
+ promise_ = promise;
51
+
52
+ for (const key of keys) {
53
+ if (!promise[key]?.toString)
54
+ continue;
55
+
56
+ if (
57
+ typeof promise[key] === "function"
58
+ && isHasCallback(promise[key])
59
+ ) {
60
+ promise_[key] = callbackToPromise(promise[key]);
61
+ }
62
+ else {
63
+ promise_[key] = promise[key];
64
+ }
65
+ }
66
+
67
+ return promise_;
68
+ }
69
+
70
+ // replace for bluebird.delay
71
+ function delay(ms) {
72
+ return new Promise(resolve => setTimeout(resolve, ms));
73
+ }
74
+
75
+ // replace for bluebird.try
76
+ function tryPromise(tryFunc) {
77
+ return new Promise((resolve, reject) => {
78
+ try {
79
+ resolve(tryFunc());
80
+ } catch (error) {
81
+ reject(error);
82
+ }
83
+ });
84
+ }
85
+
86
+ function setProxy(url) {
87
+ if (typeof url == "undefined")
88
+ return request = promisifyPromise(require("request").defaults({
89
+ jar: true
90
+ }));
91
+ return request = promisifyPromise(require("request").defaults({
92
+ jar: true,
93
+ proxy: url
94
+ }));
95
+ }
96
+
97
+ function getHeaders(url, options, ctx, customHeader) {
98
+ const headers = {
99
+ "Content-Type": "application/x-www-form-urlencoded",
100
+ Referer: "https://www.facebook.com/",
101
+ Host: url.replace("https://", "").split("/")[0],
102
+ Origin: "https://www.facebook.com",
103
+ "User-Agent": options.userAgent,
104
+ Connection: "keep-alive",
105
+ "sec-fetch-site": "same-origin"
106
+ };
107
+ if (customHeader) {
108
+ Object.assign(headers, customHeader);
109
+ }
110
+ if (ctx && ctx.region) {
111
+ headers["X-MSGR-Region"] = ctx.region;
112
+ }
113
+
114
+ return headers;
115
+ }
116
+
117
+ function isReadableStream(obj) {
118
+ return (
119
+ obj instanceof stream.Stream &&
120
+ (getType(obj._read) === "Function" ||
121
+ getType(obj._read) === "AsyncFunction") &&
122
+ getType(obj._readableState) === "Object"
123
+ );
124
+ }
125
+
126
+ function get(url, jar, qs, options, ctx) {
127
+ // I'm still confused about this
128
+ if (getType(qs) === "Object") {
129
+ for (const prop in qs) {
130
+ if (qs.hasOwnProperty(prop) && getType(qs[prop]) === "Object") {
131
+ qs[prop] = JSON.stringify(qs[prop]);
132
+ }
133
+ }
134
+ }
135
+ const op = {
136
+ headers: getHeaders(url, options, ctx),
137
+ timeout: 60000,
138
+ qs: qs,
139
+ url: url,
140
+ method: "GET",
141
+ jar: jar,
142
+ gzip: true
143
+ };
144
+
145
+ return request(op).then(function (res) {
146
+ return Array.isArray(res) ? res[0] : res;
147
+ });
148
+ }
149
+
150
+ function post(url, jar, form, options, ctx, customHeader) {
151
+ const op = {
152
+ headers: getHeaders(url, options, ctx, customHeader),
153
+ timeout: 60000,
154
+ url: url,
155
+ method: "POST",
156
+ form: form,
157
+ jar: jar,
158
+ gzip: true
159
+ };
160
+
161
+ return request(op).then(function (res) {
162
+ return Array.isArray(res) ? res[0] : res;
163
+ });
164
+ }
165
+
166
+ function postFormData(url, jar, form, qs, options, ctx) {
167
+ const headers = getHeaders(url, options, ctx);
168
+ headers["Content-Type"] = "multipart/form-data";
169
+ const op = {
170
+ headers: headers,
171
+ timeout: 60000,
172
+ url: url,
173
+ method: "POST",
174
+ formData: form,
175
+ qs: qs,
176
+ jar: jar,
177
+ gzip: true
178
+ };
179
+
180
+ return request(op).then(function (res) {
181
+ return Array.isArray(res) ? res[0] : res;
182
+ });
183
+ }
184
+
185
+ function padZeros(val, len) {
186
+ val = String(val);
187
+ len = len || 2;
188
+ while (val.length < len) val = "0" + val;
189
+ return val;
190
+ }
191
+
192
+ function generateThreadingID(clientID) {
193
+ const k = Date.now();
194
+ const l = Math.floor(Math.random() * 4294967295);
195
+ const m = clientID;
196
+ return "<" + k + ":" + l + "-" + m + "@mail.projektitan.com>";
197
+ }
198
+
199
+ function binaryToDecimal(data) {
200
+ let ret = "";
201
+ while (data !== "0") {
202
+ let end = 0;
203
+ let fullName = "";
204
+ let i = 0;
205
+ for (; i < data.length; i++) {
206
+ end = 2 * end + parseInt(data[i], 10);
207
+ if (end >= 10) {
208
+ fullName += "1";
209
+ end -= 10;
210
+ }
211
+ else {
212
+ fullName += "0";
213
+ }
214
+ }
215
+ ret = end.toString() + ret;
216
+ data = fullName.slice(fullName.indexOf("1"));
217
+ }
218
+ return ret;
219
+ }
220
+
221
+ function generateOfflineThreadingID() {
222
+ const ret = Date.now();
223
+ const value = Math.floor(Math.random() * 4294967295);
224
+ const str = ("0000000000000000000000" + value.toString(2)).slice(-22);
225
+ const msgs = ret.toString(2) + str;
226
+ return binaryToDecimal(msgs);
227
+ }
228
+
229
+ let h;
230
+ const i = {};
231
+ const j = {
232
+ _: "%",
233
+ A: "%2",
234
+ B: "000",
235
+ C: "%7d",
236
+ D: "%7b%22",
237
+ E: "%2c%22",
238
+ F: "%22%3a",
239
+ G: "%2c%22ut%22%3a1",
240
+ H: "%2c%22bls%22%3a",
241
+ I: "%2c%22n%22%3a%22%",
242
+ J: "%22%3a%7b%22i%22%3a0%7d",
243
+ K: "%2c%22pt%22%3a0%2c%22vis%22%3a",
244
+ L: "%2c%22ch%22%3a%7b%22h%22%3a%22",
245
+ M: "%7b%22v%22%3a2%2c%22time%22%3a1",
246
+ N: ".channel%22%2c%22sub%22%3a%5b",
247
+ O: "%2c%22sb%22%3a1%2c%22t%22%3a%5b",
248
+ P: "%2c%22ud%22%3a100%2c%22lc%22%3a0",
249
+ Q: "%5d%2c%22f%22%3anull%2c%22uct%22%3a",
250
+ R: ".channel%22%2c%22sub%22%3a%5b1%5d",
251
+ S: "%22%2c%22m%22%3a0%7d%2c%7b%22i%22%3a",
252
+ T: "%2c%22blc%22%3a1%2c%22snd%22%3a1%2c%22ct%22%3a",
253
+ U: "%2c%22blc%22%3a0%2c%22snd%22%3a1%2c%22ct%22%3a",
254
+ V: "%2c%22blc%22%3a0%2c%22snd%22%3a0%2c%22ct%22%3a",
255
+ W: "%2c%22s%22%3a0%2c%22blo%22%3a0%7d%2c%22bl%22%3a%7b%22ac%22%3a",
256
+ X: "%2c%22ri%22%3a0%7d%2c%22state%22%3a%7b%22p%22%3a0%2c%22ut%22%3a1",
257
+ Y:
258
+ "%2c%22pt%22%3a0%2c%22vis%22%3a1%2c%22bls%22%3a0%2c%22blc%22%3a0%2c%22snd%22%3a1%2c%22ct%22%3a",
259
+ Z:
260
+ "%2c%22sb%22%3a1%2c%22t%22%3a%5b%5d%2c%22f%22%3anull%2c%22uct%22%3a0%2c%22s%22%3a0%2c%22blo%22%3a0%7d%2c%22bl%22%3a%7b%22ac%22%3a"
261
+ };
262
+ (function () {
263
+ const l = [];
264
+ for (const m in j) {
265
+ i[j[m]] = m;
266
+ l.push(j[m]);
267
+ }
268
+ l.reverse();
269
+ h = new RegExp(l.join("|"), "g");
270
+ })();
271
+
272
+ function presenceEncode(str) {
273
+ return encodeURIComponent(str)
274
+ .replace(/([_A-Z])|%../g, function (m, n) {
275
+ return n ? "%" + n.charCodeAt(0).toString(16) : m;
276
+ })
277
+ .toLowerCase()
278
+ .replace(h, function (m) {
279
+ return i[m];
280
+ });
281
+ }
282
+
283
+ // eslint-disable-next-line no-unused-vars
284
+ function presenceDecode(str) {
285
+ return decodeURIComponent(
286
+ str.replace(/[_A-Z]/g, function (m) {
287
+ return j[m];
288
+ })
289
+ );
290
+ }
291
+
292
+ function generatePresence(userID) {
293
+ const time = Date.now();
294
+ return (
295
+ "E" +
296
+ presenceEncode(
297
+ JSON.stringify({
298
+ v: 3,
299
+ time: parseInt(time / 1000, 10),
300
+ user: userID,
301
+ state: {
302
+ ut: 0,
303
+ t2: [],
304
+ lm2: null,
305
+ uct2: time,
306
+ tr: null,
307
+ tw: Math.floor(Math.random() * 4294967295) + 1,
308
+ at: time
309
+ },
310
+ ch: {
311
+ ["p_" + userID]: 0
312
+ }
313
+ })
314
+ )
315
+ );
316
+ }
317
+
318
+ function generateAccessiblityCookie() {
319
+ const time = Date.now();
320
+ return encodeURIComponent(
321
+ JSON.stringify({
322
+ sr: 0,
323
+ "sr-ts": time,
324
+ jk: 0,
325
+ "jk-ts": time,
326
+ kb: 0,
327
+ "kb-ts": time,
328
+ hcm: 0,
329
+ "hcm-ts": time
330
+ })
331
+ );
332
+ }
333
+
334
+ function getGUID() {
335
+ /** @type {number} */
336
+ let sectionLength = Date.now();
337
+ /** @type {string} */
338
+ const id = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
339
+ /** @type {number} */
340
+ const r = Math.floor((sectionLength + Math.random() * 16) % 16);
341
+ /** @type {number} */
342
+ sectionLength = Math.floor(sectionLength / 16);
343
+ /** @type {string} */
344
+ const _guid = (c == "x" ? r : (r & 7) | 8).toString(16);
345
+ return _guid;
346
+ });
347
+ return id;
348
+ }
349
+
350
+ function getExtension(original_extension, fullFileName = "") {
351
+ if (original_extension) {
352
+ return original_extension;
353
+ }
354
+ else {
355
+ const extension = fullFileName.split(".").pop();
356
+ if (extension === fullFileName) {
357
+ return "";
358
+ }
359
+ else {
360
+ return extension;
361
+ }
362
+ }
363
+ }
364
+
365
+ function _formatAttachment(attachment1, attachment2) {
366
+ // TODO: THIS IS REALLY BAD
367
+ // This is an attempt at fixing Facebook's inconsistencies. Sometimes they give us
368
+ // two attachment objects, but sometimes only one. They each contain part of the
369
+ // data that you'd want so we merge them for convenience.
370
+ // Instead of having a bunch of if statements guarding every access to image_data,
371
+ // we set it to empty object and use the fact that it'll return undefined.
372
+ const fullFileName = attachment1.filename;
373
+ const fileSize = Number(attachment1.fileSize || 0);
374
+ const durationVideo = attachment1.genericMetadata ? Number(attachment1.genericMetadata.videoLength) : undefined;
375
+ const durationAudio = attachment1.genericMetadata ? Number(attachment1.genericMetadata.duration) : undefined;
376
+ const mimeType = attachment1.mimeType;
377
+
378
+ attachment2 = attachment2 || { id: "", image_data: {} };
379
+ attachment1 = attachment1.mercury || attachment1;
380
+ let blob = attachment1.blob_attachment || attachment1.sticker_attachment;
381
+ let type =
382
+ blob && blob.__typename ? blob.__typename : attachment1.attach_type;
383
+ if (!type && attachment1.sticker_attachment) {
384
+ type = "StickerAttachment";
385
+ blob = attachment1.sticker_attachment;
386
+ }
387
+ else if (!type && attachment1.extensible_attachment) {
388
+ if (
389
+ attachment1.extensible_attachment.story_attachment &&
390
+ attachment1.extensible_attachment.story_attachment.target &&
391
+ attachment1.extensible_attachment.story_attachment.target.__typename &&
392
+ attachment1.extensible_attachment.story_attachment.target.__typename === "MessageLocation"
393
+ ) {
394
+ type = "MessageLocation";
395
+ }
396
+ else {
397
+ type = "ExtensibleAttachment";
398
+ }
399
+
400
+ blob = attachment1.extensible_attachment;
401
+ }
402
+ // TODO: Determine whether "sticker", "photo", "file" etc are still used
403
+ // KEEP IN SYNC WITH getThreadHistory
404
+ switch (type) {
405
+ case "sticker":
406
+ return {
407
+ type: "sticker",
408
+ ID: attachment1.metadata.stickerID.toString(),
409
+ url: attachment1.url,
410
+
411
+ packID: attachment1.metadata.packID.toString(),
412
+ spriteUrl: attachment1.metadata.spriteURI,
413
+ spriteUrl2x: attachment1.metadata.spriteURI2x,
414
+ width: attachment1.metadata.width,
415
+ height: attachment1.metadata.height,
416
+
417
+ caption: attachment2.caption,
418
+ description: attachment2.description,
419
+
420
+ frameCount: attachment1.metadata.frameCount,
421
+ frameRate: attachment1.metadata.frameRate,
422
+ framesPerRow: attachment1.metadata.framesPerRow,
423
+ framesPerCol: attachment1.metadata.framesPerCol,
424
+
425
+ stickerID: attachment1.metadata.stickerID.toString(), // @Legacy
426
+ spriteURI: attachment1.metadata.spriteURI, // @Legacy
427
+ spriteURI2x: attachment1.metadata.spriteURI2x // @Legacy
428
+ };
429
+ case "file":
430
+ return {
431
+ type: "file",
432
+ ID: attachment2.id.toString(),
433
+ fullFileName: fullFileName,
434
+ filename: attachment1.name,
435
+ fileSize: fileSize,
436
+ original_extension: getExtension(attachment1.original_extension, fullFileName),
437
+ mimeType: mimeType,
438
+ url: attachment1.url,
439
+
440
+ isMalicious: attachment2.is_malicious,
441
+ contentType: attachment2.mime_type,
442
+
443
+ name: attachment1.name // @Legacy
444
+ };
445
+ case "photo":
446
+ return {
447
+ type: "photo",
448
+ ID: attachment1.metadata.fbid.toString(),
449
+ filename: attachment1.fileName,
450
+ fullFileName: fullFileName,
451
+ fileSize: fileSize,
452
+ original_extension: getExtension(attachment1.original_extension, fullFileName),
453
+ mimeType: mimeType,
454
+ thumbnailUrl: attachment1.thumbnail_url,
455
+
456
+ previewUrl: attachment1.preview_url,
457
+ previewWidth: attachment1.preview_width,
458
+ previewHeight: attachment1.preview_height,
459
+
460
+ largePreviewUrl: attachment1.large_preview_url,
461
+ largePreviewWidth: attachment1.large_preview_width,
462
+ largePreviewHeight: attachment1.large_preview_height,
463
+
464
+ url: attachment1.metadata.url, // @Legacy
465
+ width: attachment1.metadata.dimensions.split(",")[0], // @Legacy
466
+ height: attachment1.metadata.dimensions.split(",")[1], // @Legacy
467
+ name: fullFileName // @Legacy
468
+ };
469
+ case "animated_image":
470
+ return {
471
+ type: "animated_image",
472
+ ID: attachment2.id.toString(),
473
+ filename: attachment2.filename,
474
+ fullFileName: fullFileName,
475
+ original_extension: getExtension(attachment2.original_extension, fullFileName),
476
+ mimeType: mimeType,
477
+
478
+ previewUrl: attachment1.preview_url,
479
+ previewWidth: attachment1.preview_width,
480
+ previewHeight: attachment1.preview_height,
481
+
482
+ url: attachment2.image_data.url,
483
+ width: attachment2.image_data.width,
484
+ height: attachment2.image_data.height,
485
+
486
+ name: attachment1.name, // @Legacy
487
+ facebookUrl: attachment1.url, // @Legacy
488
+ thumbnailUrl: attachment1.thumbnail_url, // @Legacy
489
+ rawGifImage: attachment2.image_data.raw_gif_image, // @Legacy
490
+ rawWebpImage: attachment2.image_data.raw_webp_image, // @Legacy
491
+ animatedGifUrl: attachment2.image_data.animated_gif_url, // @Legacy
492
+ animatedGifPreviewUrl: attachment2.image_data.animated_gif_preview_url, // @Legacy
493
+ animatedWebpUrl: attachment2.image_data.animated_webp_url, // @Legacy
494
+ animatedWebpPreviewUrl: attachment2.image_data.animated_webp_preview_url // @Legacy
495
+ };
496
+ case "share":
497
+ return {
498
+ type: "share",
499
+ ID: attachment1.share.share_id.toString(),
500
+ url: attachment2.href,
501
+
502
+ title: attachment1.share.title,
503
+ description: attachment1.share.description,
504
+ source: attachment1.share.source,
505
+
506
+ image: attachment1.share.media.image,
507
+ width: attachment1.share.media.image_size.width,
508
+ height: attachment1.share.media.image_size.height,
509
+ playable: attachment1.share.media.playable,
510
+ duration: attachment1.share.media.duration,
511
+
512
+ subattachments: attachment1.share.subattachments,
513
+ properties: {},
514
+
515
+ animatedImageSize: attachment1.share.media.animated_image_size, // @Legacy
516
+ facebookUrl: attachment1.share.uri, // @Legacy
517
+ target: attachment1.share.target, // @Legacy
518
+ styleList: attachment1.share.style_list // @Legacy
519
+ };
520
+ case "video":
521
+ return {
522
+ type: "video",
523
+ ID: attachment1.metadata.fbid.toString(),
524
+ filename: attachment1.name,
525
+ fullFileName: fullFileName,
526
+ original_extension: getExtension(attachment1.original_extension, fullFileName),
527
+ mimeType: mimeType,
528
+ duration: durationVideo,
529
+
530
+ previewUrl: attachment1.preview_url,
531
+ previewWidth: attachment1.preview_width,
532
+ previewHeight: attachment1.preview_height,
533
+
534
+ url: attachment1.url,
535
+ width: attachment1.metadata.dimensions.width,
536
+ height: attachment1.metadata.dimensions.height,
537
+
538
+ videoType: "unknown",
539
+
540
+ thumbnailUrl: attachment1.thumbnail_url // @Legacy
541
+ };
542
+ case "error":
543
+ return {
544
+ type: "error",
545
+
546
+ // Save error attachments because we're unsure of their format,
547
+ // and whether there are cases they contain something useful for debugging.
548
+ attachment1: attachment1,
549
+ attachment2: attachment2
550
+ };
551
+ case "MessageImage":
552
+ return {
553
+ type: "photo",
554
+ ID: blob.legacy_attachment_id,
555
+ filename: blob.filename,
556
+ fullFileName: fullFileName,
557
+ fileSize: fileSize,
558
+ original_extension: getExtension(blob.original_extension, fullFileName),
559
+ mimeType: mimeType,
560
+ thumbnailUrl: blob.thumbnail.uri,
561
+
562
+ previewUrl: blob.preview.uri,
563
+ previewWidth: blob.preview.width,
564
+ previewHeight: blob.preview.height,
565
+
566
+ largePreviewUrl: blob.large_preview.uri,
567
+ largePreviewWidth: blob.large_preview.width,
568
+ largePreviewHeight: blob.large_preview.height,
569
+
570
+ url: blob.large_preview.uri, // @Legacy
571
+ width: blob.original_dimensions.x, // @Legacy
572
+ height: blob.original_dimensions.y, // @Legacy
573
+ name: blob.filename // @Legacy
574
+ };
575
+ case "MessageAnimatedImage":
576
+ return {
577
+ type: "animated_image",
578
+ ID: blob.legacy_attachment_id,
579
+ filename: blob.filename,
580
+ fullFileName: fullFileName,
581
+ original_extension: getExtension(blob.original_extension, fullFileName),
582
+ mimeType: mimeType,
583
+
584
+ previewUrl: blob.preview_image.uri,
585
+ previewWidth: blob.preview_image.width,
586
+ previewHeight: blob.preview_image.height,
587
+
588
+ url: blob.animated_image.uri,
589
+ width: blob.animated_image.width,
590
+ height: blob.animated_image.height,
591
+
592
+ thumbnailUrl: blob.preview_image.uri, // @Legacy
593
+ name: blob.filename, // @Legacy
594
+ facebookUrl: blob.animated_image.uri, // @Legacy
595
+ rawGifImage: blob.animated_image.uri, // @Legacy
596
+ animatedGifUrl: blob.animated_image.uri, // @Legacy
597
+ animatedGifPreviewUrl: blob.preview_image.uri, // @Legacy
598
+ animatedWebpUrl: blob.animated_image.uri, // @Legacy
599
+ animatedWebpPreviewUrl: blob.preview_image.uri // @Legacy
600
+ };
601
+ case "MessageVideo":
602
+ return {
603
+ type: "video",
604
+ ID: blob.legacy_attachment_id,
605
+ filename: blob.filename,
606
+ fullFileName: fullFileName,
607
+ original_extension: getExtension(blob.original_extension, fullFileName),
608
+ fileSize: fileSize,
609
+ duration: durationVideo,
610
+ mimeType: mimeType,
611
+
612
+ previewUrl: blob.large_image.uri,
613
+ previewWidth: blob.large_image.width,
614
+ previewHeight: blob.large_image.height,
615
+
616
+ url: blob.playable_url,
617
+ width: blob.original_dimensions.x,
618
+ height: blob.original_dimensions.y,
619
+
620
+ videoType: blob.video_type.toLowerCase(),
621
+
622
+ thumbnailUrl: blob.large_image.uri // @Legacy
623
+ };
624
+ case "MessageAudio":
625
+ return {
626
+ type: "audio",
627
+ ID: blob.url_shimhash,
628
+ filename: blob.filename,
629
+ fullFileName: fullFileName,
630
+ fileSize: fileSize,
631
+ duration: durationAudio,
632
+ original_extension: getExtension(blob.original_extension, fullFileName),
633
+ mimeType: mimeType,
634
+
635
+ audioType: blob.audio_type,
636
+ url: blob.playable_url,
637
+
638
+ isVoiceMail: blob.is_voicemail
639
+ };
640
+ case "StickerAttachment":
641
+ case "Sticker":
642
+ return {
643
+ type: "sticker",
644
+ ID: blob.id,
645
+ url: blob.url,
646
+
647
+ packID: blob.pack ? blob.pack.id : null,
648
+ spriteUrl: blob.sprite_image,
649
+ spriteUrl2x: blob.sprite_image_2x,
650
+ width: blob.width,
651
+ height: blob.height,
652
+
653
+ caption: blob.label,
654
+ description: blob.label,
655
+
656
+ frameCount: blob.frame_count,
657
+ frameRate: blob.frame_rate,
658
+ framesPerRow: blob.frames_per_row,
659
+ framesPerCol: blob.frames_per_column,
660
+
661
+ stickerID: blob.id, // @Legacy
662
+ spriteURI: blob.sprite_image, // @Legacy
663
+ spriteURI2x: blob.sprite_image_2x // @Legacy
664
+ };
665
+ case "MessageLocation":
666
+ var urlAttach = blob.story_attachment.url;
667
+ var mediaAttach = blob.story_attachment.media;
668
+
669
+ var u = querystring.parse(url.parse(urlAttach).query).u;
670
+ var where1 = querystring.parse(url.parse(u).query).where1;
671
+ var address = where1.split(", ");
672
+
673
+ var latitude;
674
+ var longitude;
675
+
676
+ try {
677
+ latitude = Number.parseFloat(address[0]);
678
+ longitude = Number.parseFloat(address[1]);
679
+ } catch (err) {
680
+ /* empty */
681
+ }
682
+
683
+ var imageUrl;
684
+ var width;
685
+ var height;
686
+
687
+ if (mediaAttach && mediaAttach.image) {
688
+ imageUrl = mediaAttach.image.uri;
689
+ width = mediaAttach.image.width;
690
+ height = mediaAttach.image.height;
691
+ }
692
+
693
+ return {
694
+ type: "location",
695
+ ID: blob.legacy_attachment_id,
696
+ latitude: latitude,
697
+ longitude: longitude,
698
+ image: imageUrl,
699
+ width: width,
700
+ height: height,
701
+ url: u || urlAttach,
702
+ address: where1,
703
+
704
+ facebookUrl: blob.story_attachment.url, // @Legacy
705
+ target: blob.story_attachment.target, // @Legacy
706
+ styleList: blob.story_attachment.style_list // @Legacy
707
+ };
708
+ case "ExtensibleAttachment":
709
+ return {
710
+ type: "share",
711
+ ID: blob.legacy_attachment_id,
712
+ url: blob.story_attachment.url,
713
+
714
+ title: blob.story_attachment.title_with_entities.text,
715
+ description:
716
+ blob.story_attachment.description &&
717
+ blob.story_attachment.description.text,
718
+ source: blob.story_attachment.source
719
+ ? blob.story_attachment.source.text
720
+ : null,
721
+
722
+ image:
723
+ blob.story_attachment.media &&
724
+ blob.story_attachment.media.image &&
725
+ blob.story_attachment.media.image.uri,
726
+ width:
727
+ blob.story_attachment.media &&
728
+ blob.story_attachment.media.image &&
729
+ blob.story_attachment.media.image.width,
730
+ height:
731
+ blob.story_attachment.media &&
732
+ blob.story_attachment.media.image &&
733
+ blob.story_attachment.media.image.height,
734
+ playable:
735
+ blob.story_attachment.media &&
736
+ blob.story_attachment.media.is_playable,
737
+ duration:
738
+ blob.story_attachment.media &&
739
+ blob.story_attachment.media.playable_duration_in_ms,
740
+ playableUrl:
741
+ blob.story_attachment.media == null
742
+ ? null
743
+ : blob.story_attachment.media.playable_url,
744
+
745
+ subattachments: blob.story_attachment.subattachments,
746
+ properties: blob.story_attachment.properties.reduce(function (obj, cur) {
747
+ obj[cur.key] = cur.value.text;
748
+ return obj;
749
+ }, {}),
750
+
751
+ facebookUrl: blob.story_attachment.url, // @Legacy
752
+ target: blob.story_attachment.target, // @Legacy
753
+ styleList: blob.story_attachment.style_list // @Legacy
754
+ };
755
+ case "MessageFile":
756
+ return {
757
+ type: "file",
758
+ ID: blob.message_file_fbid,
759
+ fullFileName: fullFileName,
760
+ filename: blob.filename,
761
+ fileSize: fileSize,
762
+ mimeType: blob.mimetype,
763
+ original_extension: blob.original_extension || fullFileName.split(".").pop(),
764
+
765
+ url: blob.url,
766
+ isMalicious: blob.is_malicious,
767
+ contentType: blob.content_type,
768
+
769
+ name: blob.filename
770
+ };
771
+ default:
772
+ throw new Error(
773
+ "unrecognized attach_file of type " +
774
+ type +
775
+ "`" +
776
+ JSON.stringify(attachment1, null, 4) +
777
+ " attachment2: " +
778
+ JSON.stringify(attachment2, null, 4) +
779
+ "`"
780
+ );
781
+ }
782
+ }
783
+
784
+ function formatAttachment(attachments, attachmentIds, attachmentMap, shareMap) {
785
+ attachmentMap = shareMap || attachmentMap;
786
+ return attachments
787
+ ? attachments.map(function (val, i) {
788
+ if (
789
+ !attachmentMap ||
790
+ !attachmentIds ||
791
+ !attachmentMap[attachmentIds[i]]
792
+ ) {
793
+ return _formatAttachment(val);
794
+ }
795
+ return _formatAttachment(val, attachmentMap[attachmentIds[i]]);
796
+ })
797
+ : [];
798
+ }
799
+
800
+ function formatDeltaMessage(m) {
801
+ const md = m.delta.messageMetadata;
802
+
803
+ const mdata =
804
+ m.delta.data === undefined
805
+ ? []
806
+ : m.delta.data.prng === undefined
807
+ ? []
808
+ : JSON.parse(m.delta.data.prng);
809
+ const m_id = mdata.map(u => u.i);
810
+ const m_offset = mdata.map(u => u.o);
811
+ const m_length = mdata.map(u => u.l);
812
+ const mentions = {};
813
+ for (let i = 0; i < m_id.length; i++) {
814
+ mentions[m_id[i]] = m.delta.body.substring(
815
+ m_offset[i],
816
+ m_offset[i] + m_length[i]
817
+ );
818
+ }
819
+ return {
820
+ type: "message",
821
+ senderID: formatID(md.actorFbId.toString()),
822
+ body: m.delta.body || "",
823
+ threadID: formatID(
824
+ (md.threadKey.threadFbId || md.threadKey.otherUserFbId).toString()
825
+ ),
826
+ messageID: md.messageId,
827
+ attachments: (m.delta.attachments || []).map(v => _formatAttachment(v)),
828
+ mentions: mentions,
829
+ timestamp: md.timestamp,
830
+ isGroup: !!md.threadKey.threadFbId,
831
+ participantIDs: m.delta.participants || (md.cid ? md.cid.canonicalParticipantFbids : []) || []
832
+ };
833
+ }
834
+
835
+ function formatID(id) {
836
+ if (id != undefined && id != null) {
837
+ return id.replace(/(fb)?id[:.]/, "");
838
+ }
839
+ else {
840
+ return id;
841
+ }
842
+ }
843
+
844
+ function formatMessage(m) {
845
+ const originalMessage = m.message ? m.message : m;
846
+ const obj = {
847
+ type: "message",
848
+ senderName: originalMessage.sender_name,
849
+ senderID: formatID(originalMessage.sender_fbid.toString()),
850
+ participantNames: originalMessage.group_thread_info
851
+ ? originalMessage.group_thread_info.participant_names
852
+ : [originalMessage.sender_name.split(" ")[0]],
853
+ participantIDs: originalMessage.group_thread_info
854
+ ? originalMessage.group_thread_info.participant_ids.map(function (v) {
855
+ return formatID(v.toString());
856
+ })
857
+ : [formatID(originalMessage.sender_fbid)],
858
+ body: originalMessage.body || "",
859
+ threadID: formatID(
860
+ (
861
+ originalMessage.thread_fbid || originalMessage.other_user_fbid
862
+ ).toString()
863
+ ),
864
+ threadName: originalMessage.group_thread_info
865
+ ? originalMessage.group_thread_info.name
866
+ : originalMessage.sender_name,
867
+ location: originalMessage.coordinates ? originalMessage.coordinates : null,
868
+ messageID: originalMessage.mid
869
+ ? originalMessage.mid.toString()
870
+ : originalMessage.message_id,
871
+ attachments: formatAttachment(
872
+ originalMessage.attachments,
873
+ originalMessage.attachmentIds,
874
+ originalMessage.attachment_map,
875
+ originalMessage.share_map
876
+ ),
877
+ timestamp: originalMessage.timestamp,
878
+ timestampAbsolute: originalMessage.timestamp_absolute,
879
+ timestampRelative: originalMessage.timestamp_relative,
880
+ timestampDatetime: originalMessage.timestamp_datetime,
881
+ tags: originalMessage.tags,
882
+ reactions: originalMessage.reactions ? originalMessage.reactions : [],
883
+ isUnread: originalMessage.is_unread
884
+ };
885
+
886
+ if (m.type === "pages_messaging")
887
+ obj.pageID = m.realtime_viewer_fbid.toString();
888
+ obj.isGroup = obj.participantIDs.length > 2;
889
+
890
+ return obj;
891
+ }
892
+
893
+ function formatEvent(m) {
894
+ const originalMessage = m.message ? m.message : m;
895
+ let logMessageType = originalMessage.log_message_type;
896
+ let logMessageData;
897
+ if (logMessageType === "log:generic-admin-text") {
898
+ logMessageData = originalMessage.log_message_data.untypedData;
899
+ logMessageType = getAdminTextMessageType(
900
+ originalMessage.log_message_data.message_type
901
+ );
902
+ }
903
+ else {
904
+ logMessageData = originalMessage.log_message_data;
905
+ }
906
+
907
+ return Object.assign(formatMessage(originalMessage), {
908
+ type: "event",
909
+ logMessageType: logMessageType,
910
+ logMessageData: logMessageData,
911
+ logMessageBody: originalMessage.log_message_body
912
+ });
913
+ }
914
+
915
+ function formatHistoryMessage(m) {
916
+ switch (m.action_type) {
917
+ case "ma-type:log-message":
918
+ return formatEvent(m);
919
+ default:
920
+ return formatMessage(m);
921
+ }
922
+ }
923
+
924
+ // Get a more readable message type for AdminTextMessages
925
+ function getAdminTextMessageType(type) {
926
+ switch (type) {
927
+ case "change_thread_theme":
928
+ return "log:thread-color";
929
+ case "change_thread_icon":
930
+ case "change_thread_quick_reaction":
931
+ return "log:thread-icon";
932
+ case "change_thread_nickname":
933
+ return "log:user-nickname";
934
+ case "change_thread_admins":
935
+ return "log:thread-admins";
936
+ case "group_poll":
937
+ return "log:thread-poll";
938
+ case "change_thread_approval_mode":
939
+ return "log:thread-approval-mode";
940
+ case "messenger_call_log":
941
+ case "participant_joined_group_call":
942
+ return "log:thread-call";
943
+ default:
944
+ return type;
945
+ }
946
+ }
947
+
948
+ function formatDeltaEvent(m) {
949
+ let logMessageType;
950
+ let logMessageData;
951
+
952
+ // log:thread-color => {theme_color}
953
+ // log:user-nickname => {participant_id, nickname}
954
+ // log:thread-icon => {thread_icon}
955
+ // log:thread-name => {name}
956
+ // log:subscribe => {addedParticipants - [Array]}
957
+ // log:unsubscribe => {leftParticipantFbId}
958
+
959
+ switch (m.class) {
960
+ case "AdminTextMessage":
961
+ logMessageData = m.untypedData;
962
+ logMessageType = getAdminTextMessageType(m.type);
963
+ break;
964
+ case "ThreadName":
965
+ logMessageType = "log:thread-name";
966
+ logMessageData = { name: m.name };
967
+ break;
968
+ case "ParticipantsAddedToGroupThread":
969
+ logMessageType = "log:subscribe";
970
+ logMessageData = { addedParticipants: m.addedParticipants };
971
+ break;
972
+ case "ParticipantLeftGroupThread":
973
+ logMessageType = "log:unsubscribe";
974
+ logMessageData = { leftParticipantFbId: m.leftParticipantFbId };
975
+ break;
976
+ case "ApprovalQueue":
977
+ logMessageType = "log:approval-queue";
978
+ logMessageData = {
979
+ approvalQueue: {
980
+ action: m.action,
981
+ recipientFbId: m.recipientFbId,
982
+ requestSource: m.requestSource,
983
+ ...m.messageMetadata
984
+ }
985
+ };
986
+ }
987
+
988
+ return {
989
+ type: "event",
990
+ threadID: formatID(
991
+ (
992
+ m.messageMetadata.threadKey.threadFbId ||
993
+ m.messageMetadata.threadKey.otherUserFbId
994
+ ).toString()
995
+ ),
996
+ messageID: m.messageMetadata.messageId.toString(),
997
+ logMessageType: logMessageType,
998
+ logMessageData: logMessageData,
999
+ logMessageBody: m.messageMetadata.adminText,
1000
+ timestamp: m.messageMetadata.timestamp,
1001
+ author: m.messageMetadata.actorFbId,
1002
+ participantIDs: (m.participants || []).map(p => p.toString())
1003
+ };
1004
+ }
1005
+
1006
+ function formatTyp(event) {
1007
+ return {
1008
+ isTyping: !!event.st,
1009
+ from: event.from.toString(),
1010
+ threadID: formatID(
1011
+ (event.to || event.thread_fbid || event.from).toString()
1012
+ ),
1013
+ // When receiving typ indication from mobile, `from_mobile` isn't set.
1014
+ // If it is, we just use that value.
1015
+ fromMobile: event.hasOwnProperty("from_mobile") ? event.from_mobile : true,
1016
+ userID: (event.realtime_viewer_fbid || event.from).toString(),
1017
+ type: "typ"
1018
+ };
1019
+ }
1020
+
1021
+ function formatDeltaReadReceipt(delta) {
1022
+ // otherUserFbId seems to be used as both the readerID and the threadID in a 1-1 chat.
1023
+ // In a group chat actorFbId is used for the reader and threadFbId for the thread.
1024
+ return {
1025
+ reader: (delta.threadKey.otherUserFbId || delta.actorFbId).toString(),
1026
+ time: delta.actionTimestampMs,
1027
+ threadID: formatID(
1028
+ (delta.threadKey.otherUserFbId || delta.threadKey.threadFbId).toString()
1029
+ ),
1030
+ type: "read_receipt"
1031
+ };
1032
+ }
1033
+
1034
+ function formatReadReceipt(event) {
1035
+ return {
1036
+ reader: event.reader.toString(),
1037
+ time: event.time,
1038
+ threadID: formatID((event.thread_fbid || event.reader).toString()),
1039
+ type: "read_receipt"
1040
+ };
1041
+ }
1042
+
1043
+ function formatRead(event) {
1044
+ return {
1045
+ threadID: formatID(
1046
+ (
1047
+ (event.chat_ids && event.chat_ids[0]) ||
1048
+ (event.thread_fbids && event.thread_fbids[0])
1049
+ ).toString()
1050
+ ),
1051
+ time: event.timestamp,
1052
+ type: "read"
1053
+ };
1054
+ }
1055
+
1056
+ function getFrom(str, startToken, endToken) {
1057
+ const start = str.indexOf(startToken) + startToken.length;
1058
+ if (start < startToken.length) return "";
1059
+
1060
+ const lastHalf = str.substring(start);
1061
+ const end = lastHalf.indexOf(endToken);
1062
+ if (end === -1) {
1063
+ throw new Error(
1064
+ "Could not find endTime `" + endToken + "` in the given string."
1065
+ );
1066
+ }
1067
+ return lastHalf.substring(0, end);
1068
+ }
1069
+
1070
+ function makeParsable(html) {
1071
+ const withoutForLoop = html.replace(/for\s*\(\s*;\s*;\s*\)\s*;\s*/, "");
1072
+
1073
+ // (What the fuck FB, why windows style newlines?)
1074
+ // So sometimes FB will send us base multiple objects in the same response.
1075
+ // They're all valid JSON, one after the other, at the top level. We detect
1076
+ // that and make it parse-able by JSON.parse.
1077
+ // Ben - July 15th 2017
1078
+ //
1079
+ // It turns out that Facebook may insert random number of spaces before
1080
+ // next object begins (issue #616)
1081
+ // rav_kr - 2018-03-19
1082
+ const maybeMultipleObjects = withoutForLoop.split(/\}\r\n *\{/);
1083
+ if (maybeMultipleObjects.length === 1) return maybeMultipleObjects;
1084
+
1085
+ return "[" + maybeMultipleObjects.join("},{") + "]";
1086
+ }
1087
+
1088
+ function arrToForm(form) {
1089
+ return arrayToObject(
1090
+ form,
1091
+ function (v) {
1092
+ return v.name;
1093
+ },
1094
+ function (v) {
1095
+ return v.val;
1096
+ }
1097
+ );
1098
+ }
1099
+
1100
+ function arrayToObject(arr, getKey, getValue) {
1101
+ return arr.reduce(function (acc, val) {
1102
+ acc[getKey(val)] = getValue(val);
1103
+ return acc;
1104
+ }, {});
1105
+ }
1106
+
1107
+ function getSignatureID() {
1108
+ return Math.floor(Math.random() * 2147483648).toString(16);
1109
+ }
1110
+
1111
+ function generateTimestampRelative() {
1112
+ const d = new Date();
1113
+ return d.getHours() + ":" + padZeros(d.getMinutes());
1114
+ }
1115
+
1116
+ function makeDefaults(html, userID, ctx) {
1117
+ let reqCounter = 1;
1118
+ const fb_dtsg = getFrom(html, 'name="fb_dtsg" value="', '"');
1119
+
1120
+ // @Hack Ok we've done hacky things, this is definitely on top 5.
1121
+ // We totally assume the object is flat and try parsing until a }.
1122
+ // If it works though it's cool because we get a bunch of extra data things.
1123
+ //
1124
+ // Update: we don't need this. Leaving it in in case we ever do.
1125
+ // Ben - July 15th 2017
1126
+
1127
+ // var siteData = getFrom(html, "[\"SiteData\",[],", "},");
1128
+ // try {
1129
+ // siteData = JSON.parse(siteData + "}");
1130
+ // } catch(e) {
1131
+ // log.warn("makeDefaults", "Couldn't parse SiteData. Won't have access to some variables.");
1132
+ // siteData = {};
1133
+ // }
1134
+
1135
+ let ttstamp = "2";
1136
+ for (let i = 0; i < fb_dtsg.length; i++) {
1137
+ ttstamp += fb_dtsg.charCodeAt(i);
1138
+ }
1139
+ const revision = getFrom(html, 'revision":', ",");
1140
+
1141
+ function mergeWithDefaults(obj) {
1142
+ // @TODO This is missing a key called __dyn.
1143
+ // After some investigation it seems like __dyn is some sort of set that FB
1144
+ // calls BitMap. It seems like certain responses have a "define" key in the
1145
+ // res.jsmods arrays. I think the code iterates over those and calls `set`
1146
+ // on the bitmap for each of those keys. Then it calls
1147
+ // bitmap.toCompressedString() which returns what __dyn is.
1148
+ //
1149
+ // So far the API has been working without this.
1150
+ //
1151
+ // Ben - July 15th 2017
1152
+ const newObj = {
1153
+ __user: userID,
1154
+ __req: (reqCounter++).toString(36),
1155
+ __rev: revision,
1156
+ __a: 1,
1157
+ // __af: siteData.features,
1158
+ fb_dtsg: ctx.fb_dtsg ? ctx.fb_dtsg : fb_dtsg,
1159
+ jazoest: ctx.ttstamp ? ctx.ttstamp : ttstamp
1160
+ // __spin_r: siteData.__spin_r,
1161
+ // __spin_b: siteData.__spin_b,
1162
+ // __spin_t: siteData.__spin_t,
1163
+ };
1164
+
1165
+ // @TODO this is probably not needed.
1166
+ // Ben - July 15th 2017
1167
+ // if (siteData.be_key) {
1168
+ // newObj[siteData.be_key] = siteData.be_mode;
1169
+ // }
1170
+ // if (siteData.pkg_cohort_key) {
1171
+ // newObj[siteData.pkg_cohort_key] = siteData.pkg_cohort;
1172
+ // }
1173
+
1174
+ if (!obj) return newObj;
1175
+
1176
+ for (const prop in obj) {
1177
+ if (obj.hasOwnProperty(prop)) {
1178
+ if (!newObj[prop]) {
1179
+ newObj[prop] = obj[prop];
1180
+ }
1181
+ }
1182
+ }
1183
+
1184
+ return newObj;
1185
+ }
1186
+
1187
+ function postWithDefaults(url, jar, form, ctxx, customHeader = {}) {
1188
+ return post(url, jar, mergeWithDefaults(form), ctx.globalOptions, ctxx || ctx, customHeader);
1189
+ }
1190
+
1191
+ function getWithDefaults(url, jar, qs, ctxx, customHeader = {}) {
1192
+ return get(url, jar, mergeWithDefaults(qs), ctx.globalOptions, ctxx || ctx, customHeader);
1193
+ }
1194
+
1195
+ function postFormDataWithDefault(url, jar, form, qs, ctxx) {
1196
+ return postFormData(
1197
+ url,
1198
+ jar,
1199
+ mergeWithDefaults(form),
1200
+ mergeWithDefaults(qs),
1201
+ ctx.globalOptions,
1202
+ ctxx || ctx
1203
+ );
1204
+ }
1205
+
1206
+ return {
1207
+ get: getWithDefaults,
1208
+ post: postWithDefaults,
1209
+ postFormData: postFormDataWithDefault
1210
+ };
1211
+ }
1212
+
1213
+ function parseAndCheckLogin(ctx, defaultFuncs, retryCount, sourceCall) {
1214
+ if (retryCount == undefined) {
1215
+ retryCount = 0;
1216
+ }
1217
+ if (sourceCall == undefined) {
1218
+ try {
1219
+ throw new Error();
1220
+ }
1221
+ catch (e) {
1222
+ sourceCall = e;
1223
+ }
1224
+ }
1225
+ return function (data) {
1226
+ return tryPromise(function () {
1227
+ log.verbose("parseAndCheckLogin", data.body);
1228
+ if (data.statusCode >= 500 && data.statusCode < 600) {
1229
+ if (retryCount >= 5) {
1230
+ throw new CustomError({
1231
+ message: "Request retry failed. Check the `res` and `statusCode` property on this error.",
1232
+ statusCode: data.statusCode,
1233
+ res: data.body,
1234
+ error: "Request retry failed. Check the `res` and `statusCode` property on this error.",
1235
+ sourceCall: sourceCall
1236
+ });
1237
+ }
1238
+ retryCount++;
1239
+ const retryTime = Math.floor(Math.random() * 5000);
1240
+ log.warn(
1241
+ "parseAndCheckLogin",
1242
+ "Got status code " +
1243
+ data.statusCode +
1244
+ " - " +
1245
+ retryCount +
1246
+ ". attempt to retry in " +
1247
+ retryTime +
1248
+ " milliseconds..."
1249
+ );
1250
+ const url =
1251
+ data.request.uri.protocol +
1252
+ "//" +
1253
+ data.request.uri.hostname +
1254
+ data.request.uri.pathname;
1255
+ if (
1256
+ data.request.headers["Content-Type"].split(";")[0] ===
1257
+ "multipart/form-data"
1258
+ ) {
1259
+ return delay(retryTime)
1260
+ .then(function () {
1261
+ return defaultFuncs.postFormData(
1262
+ url,
1263
+ ctx.jar,
1264
+ data.request.formData,
1265
+ {}
1266
+ );
1267
+ })
1268
+ .then(parseAndCheckLogin(ctx, defaultFuncs, retryCount, sourceCall));
1269
+ }
1270
+ else {
1271
+ return delay(retryTime)
1272
+ .then(function () {
1273
+ return defaultFuncs.post(url, ctx.jar, data.request.formData);
1274
+ })
1275
+ .then(parseAndCheckLogin(ctx, defaultFuncs, retryCount, sourceCall));
1276
+ }
1277
+ }
1278
+ if (data.statusCode !== 200)
1279
+ throw new CustomError({
1280
+ message: "parseAndCheckLogin got status code: " + data.statusCode + ". Bailing out of trying to parse response.",
1281
+ statusCode: data.statusCode,
1282
+ res: data.body,
1283
+ error: "parseAndCheckLogin got status code: " + data.statusCode + ". Bailing out of trying to parse response.",
1284
+ sourceCall: sourceCall
1285
+ });
1286
+
1287
+ let res = null;
1288
+ try {
1289
+ res = JSON.parse(makeParsable(data.body));
1290
+ } catch (e) {
1291
+ throw new CustomError({
1292
+ message: "JSON.parse error. Check the `detail` property on this error.",
1293
+ detail: e,
1294
+ res: data.body,
1295
+ error: "JSON.parse error. Check the `detail` property on this error.",
1296
+ sourceCall: sourceCall
1297
+ });
1298
+ }
1299
+
1300
+ // In some cases the response contains only a redirect URL which should be followed
1301
+ if (res.redirect && data.request.method === "GET") {
1302
+ return defaultFuncs
1303
+ .get(res.redirect, ctx.jar)
1304
+ .then(parseAndCheckLogin(ctx, defaultFuncs, undefined, sourceCall));
1305
+ }
1306
+
1307
+ // TODO: handle multiple cookies?
1308
+ if (
1309
+ res.jsmods &&
1310
+ res.jsmods.require &&
1311
+ Array.isArray(res.jsmods.require[0]) &&
1312
+ res.jsmods.require[0][0] === "Cookie"
1313
+ ) {
1314
+ res.jsmods.require[0][3][0] = res.jsmods.require[0][3][0].replace(
1315
+ "_js_",
1316
+ ""
1317
+ );
1318
+ const cookie = formatCookie(res.jsmods.require[0][3], "facebook");
1319
+ const cookie2 = formatCookie(res.jsmods.require[0][3], "messenger");
1320
+ ctx.jar.setCookie(cookie, "https://www.facebook.com");
1321
+ ctx.jar.setCookie(cookie2, "https://www.messenger.com");
1322
+ }
1323
+
1324
+ // On every request we check if we got a DTSG and we mutate the context so that we use the latest
1325
+ // one for the next requests.
1326
+ if (res.jsmods && Array.isArray(res.jsmods.require)) {
1327
+ const arr = res.jsmods.require;
1328
+ for (const i in arr) {
1329
+ if (arr[i][0] === "DTSG" && arr[i][1] === "setToken") {
1330
+ ctx.fb_dtsg = arr[i][3][0];
1331
+
1332
+ // Update ttstamp since that depends on fb_dtsg
1333
+ ctx.ttstamp = "2";
1334
+ for (let j = 0; j < ctx.fb_dtsg.length; j++) {
1335
+ ctx.ttstamp += ctx.fb_dtsg.charCodeAt(j);
1336
+ }
1337
+ }
1338
+ }
1339
+ }
1340
+
1341
+ if (res.error === 1357001) {
1342
+ throw new CustomError({
1343
+ message: "Facebook blocked login. Please visit https://facebook.com and check your account.",
1344
+ error: "Not logged in.",
1345
+ res: res,
1346
+ statusCode: data.statusCode,
1347
+ sourceCall: sourceCall
1348
+ });
1349
+ }
1350
+ return res;
1351
+ });
1352
+ };
1353
+ }
1354
+
1355
+ function checkLiveCookie(ctx, defaultFuncs) {
1356
+ return defaultFuncs
1357
+ .get("https://m.facebook.com/me", ctx.jar)
1358
+ .then(function (res) {
1359
+ if (res.body.indexOf(ctx.i_userID || ctx.userID) === -1) {
1360
+ throw new CustomError({
1361
+ message: "Not logged in.",
1362
+ error: "Not logged in."
1363
+ });
1364
+ }
1365
+ return true;
1366
+ });
1367
+ }
1368
+
1369
+ function saveCookies(jar) {
1370
+ return function (res) {
1371
+ const cookies = res.headers["set-cookie"] || [];
1372
+ cookies.forEach(function (c) {
1373
+ if (c.indexOf(".facebook.com") > -1) {
1374
+ jar.setCookie(c, "https://www.facebook.com");
1375
+ }
1376
+ const c2 = c.replace(/domain=\.facebook\.com/, "domain=.messenger.com");
1377
+ jar.setCookie(c2, "https://www.messenger.com");
1378
+ });
1379
+ return res;
1380
+ };
1381
+ }
1382
+
1383
+ const NUM_TO_MONTH = [
1384
+ "Jan",
1385
+ "Feb",
1386
+ "Mar",
1387
+ "Apr",
1388
+ "May",
1389
+ "Jun",
1390
+ "Jul",
1391
+ "Aug",
1392
+ "Sep",
1393
+ "Oct",
1394
+ "Nov",
1395
+ "Dec"
1396
+ ];
1397
+ const NUM_TO_DAY = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
1398
+ function formatDate(date) {
1399
+ let d = date.getUTCDate();
1400
+ d = d >= 10 ? d : "0" + d;
1401
+ let h = date.getUTCHours();
1402
+ h = h >= 10 ? h : "0" + h;
1403
+ let m = date.getUTCMinutes();
1404
+ m = m >= 10 ? m : "0" + m;
1405
+ let s = date.getUTCSeconds();
1406
+ s = s >= 10 ? s : "0" + s;
1407
+ return (
1408
+ NUM_TO_DAY[date.getUTCDay()] +
1409
+ ", " +
1410
+ d +
1411
+ " " +
1412
+ NUM_TO_MONTH[date.getUTCMonth()] +
1413
+ " " +
1414
+ date.getUTCFullYear() +
1415
+ " " +
1416
+ h +
1417
+ ":" +
1418
+ m +
1419
+ ":" +
1420
+ s +
1421
+ " GMT"
1422
+ );
1423
+ }
1424
+
1425
+ function formatCookie(arr, url) {
1426
+ return (
1427
+ arr[0] + "=" + arr[1] + "; Path=" + arr[3] + "; Domain=" + url + ".com"
1428
+ );
1429
+ }
1430
+
1431
+ function formatThread(data) {
1432
+ return {
1433
+ threadID: formatID(data.thread_fbid.toString()),
1434
+ participants: data.participants.map(formatID),
1435
+ participantIDs: data.participants.map(formatID),
1436
+ name: data.name,
1437
+ nicknames: data.custom_nickname,
1438
+ snippet: data.snippet,
1439
+ snippetAttachments: data.snippet_attachments,
1440
+ snippetSender: formatID((data.snippet_sender || "").toString()),
1441
+ unreadCount: data.unread_count,
1442
+ messageCount: data.message_count,
1443
+ imageSrc: data.image_src,
1444
+ timestamp: data.timestamp,
1445
+ serverTimestamp: data.server_timestamp, // what is this?
1446
+ muteUntil: data.mute_until,
1447
+ isCanonicalUser: data.is_canonical_user,
1448
+ isCanonical: data.is_canonical,
1449
+ isSubscribed: data.is_subscribed,
1450
+ folder: data.folder,
1451
+ isArchived: data.is_archived,
1452
+ recipientsLoadable: data.recipients_loadable,
1453
+ hasEmailParticipant: data.has_email_participant,
1454
+ readOnly: data.read_only,
1455
+ canReply: data.can_reply,
1456
+ cannotReplyReason: data.cannot_reply_reason,
1457
+ lastMessageTimestamp: data.last_message_timestamp,
1458
+ lastReadTimestamp: data.last_read_timestamp,
1459
+ lastMessageType: data.last_message_type,
1460
+ emoji: data.custom_like_icon,
1461
+ color: data.custom_color,
1462
+ adminIDs: data.admin_ids,
1463
+ threadType: data.thread_type
1464
+ };
1465
+ }
1466
+
1467
+ function getType(obj) {
1468
+ return Object.prototype.toString.call(obj).slice(8, -1);
1469
+ }
1470
+
1471
+ function formatProxyPresence(presence, userID) {
1472
+ if (presence.lat === undefined || presence.p === undefined) return null;
1473
+ return {
1474
+ type: "presence",
1475
+ timestamp: presence.lat * 1000,
1476
+ userID: userID,
1477
+ statuses: presence.p
1478
+ };
1479
+ }
1480
+
1481
+ function formatPresence(presence, userID) {
1482
+ return {
1483
+ type: "presence",
1484
+ timestamp: presence.la * 1000,
1485
+ userID: userID,
1486
+ statuses: presence.a
1487
+ };
1488
+ }
1489
+
1490
+ function decodeClientPayload(payload) {
1491
+ /*
1492
+ Special function which Client using to "encode" clients JSON payload
1493
+ */
1494
+ return JSON.parse(String.fromCharCode.apply(null, payload));
1495
+ }
1496
+
1497
+ function getAppState(jar) {
1498
+ return jar
1499
+ .getCookies("https://www.facebook.com")
1500
+ .concat(jar.getCookies("https://facebook.com"))
1501
+ .concat(jar.getCookies("https://www.messenger.com"));
1502
+ }
1503
+ module.exports = {
1504
+ CustomError,
1505
+ isReadableStream,
1506
+ get,
1507
+ post,
1508
+ postFormData,
1509
+ generateThreadingID,
1510
+ generateOfflineThreadingID,
1511
+ getGUID,
1512
+ getFrom,
1513
+ makeParsable,
1514
+ arrToForm,
1515
+ getSignatureID,
1516
+ getJar: request.jar,
1517
+ generateTimestampRelative,
1518
+ makeDefaults,
1519
+ parseAndCheckLogin,
1520
+ saveCookies,
1521
+ getType,
1522
+ _formatAttachment,
1523
+ formatHistoryMessage,
1524
+ formatID,
1525
+ formatMessage,
1526
+ formatDeltaEvent,
1527
+ formatDeltaMessage,
1528
+ formatProxyPresence,
1529
+ formatPresence,
1530
+ formatTyp,
1531
+ formatDeltaReadReceipt,
1532
+ formatCookie,
1533
+ formatThread,
1534
+ formatReadReceipt,
1535
+ formatRead,
1536
+ generatePresence,
1537
+ generateAccessiblityCookie,
1538
+ formatDate,
1539
+ decodeClientPayload,
1540
+ getAppState,
1541
+ getAdminTextMessageType,
1542
+ setProxy,
1543
+ checkLiveCookie
1544
+ };