├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── DOCS.md
├── LICENSE-MIT
├── README.md
├── index.js
├── package.json
├── src
├── addExternalModule.js
├── addUserToGroup.js
├── changeAdminStatus.js
├── changeArchivedStatus.js
├── changeAvatar.js
├── changeBio.js
├── changeBlockedStatus.js
├── changeGroupImage.js
├── changeNickname.js
├── changeThreadColor.js
├── changeThreadEmoji.js
├── createNewGroup.js
├── createPoll.js
├── deleteMessage.js
├── deleteThread.js
├── forwardAttachment.js
├── getCurrentUserID.js
├── getEmojiUrl.js
├── getFriendsList.js
├── getMessage.js
├── getThreadHistory.js
├── getThreadInfo.js
├── getThreadList.js
├── getThreadPictures.js
├── getUserID.js
├── getUserInfo.js
├── handleFriendRequest.js
├── handleMessageRequest.js
├── httpGet.js
├── httpPost.js
├── httpPostFormData.js
├── listenMqtt.js
├── logout.js
├── markAsDelivered.js
├── markAsRead.js
├── markAsReadAll.js
├── markAsSeen.js
├── muteThread.js
├── refreshFb_dtsg.js
├── removeUserFromGroup.js
├── resolvePhotoUrl.js
├── searchForThread.js
├── sendMessage.js
├── sendMessageMqtt.js
├── sendTypingIndicator.js
├── setMessageReaction.js
├── setPostReaction.js
├── setTitle.js
├── threadColors.js
├── unfriend.js
├── unsendMessage.js
└── uploadAttachment.js
├── test
├── data
│ ├── shareAttach.js
│ └── test.txt
├── example-config.json
├── test-page.js
└── test.js
└── utils.js
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | node_modules
3 | test/appstate.json
4 | test/test-config.json
5 | package-lock.json
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "6"
4 |
5 | script:
6 | - npm run lint
7 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 | Too lazy to write changelog, sorry! (will write changelog in the next release, through.)
--------------------------------------------------------------------------------
/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Avery, Benjamin, David, Maude
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This repo is a fork from main repo and will usually have new features bundled faster than main repo (and maybe bundle some bugs, too).
2 | See main repo [here](https://github.com/Schmavery/facebook-chat-api).
3 |
4 | # Unofficial Facebook Chat API
5 |
6 |
7 | Facebook now has an official API for chat bots [here](https://developers.facebook.com/docs/messenger-platform).
8 |
9 | This API is the only way to automate chat functionalities on a user account. We do this by emulating the browser. This means doing the exact same GET/POST requests and tricking Facebook into thinking we're accessing the website normally. Because we're doing it this way, this API won't work with an auth token but requires the credentials of a Facebook account.
10 |
11 | _Disclaimer_: We are not responsible if your account gets banned for spammy activities such as sending lots of messages to people you don't know, sending messages very quickly, sending spammy looking URLs, logging in and out very quickly... Be responsible Facebook citizens.
12 |
13 | See [below](#projects-using-this-api) for projects using this API.
14 |
15 | See the [full changelog](/CHANGELOG.md) for release details.
16 |
17 | ## Install
18 | ~~If you just want to use fb-chat-api, you should use this command:~~
19 |
20 | ~~It will download `fb-chat-api` from NPM~~
21 |
(Not available in NPM)
22 |
23 |
24 | ### Bleeding edge
25 | If you want to use bleeding edge (directly from github) to test new features or submit bug report, this is the command for you:
26 | ```bash
27 | npm install ntkhang03/fb-chat-api-temp
28 | ```
29 | ### Note
30 | **Currently, this repo is not available on NPM. Please use the bleeding edge version.**
31 |
32 | ## Testing your bots
33 | ~~If you want to test your bots without creating another account on Facebook, you can use [Facebook Whitehat Accounts](https://www.facebook.com/whitehat/accounts/).~~ (Facebook has removed this feature.)
34 |
35 | ## Example Usage
36 | ### Note
37 | **Currently, login with credentials is not available. You need to create a file named `appstate.json` to save your login state. You can use [c3c-fbstate](https://github.com/c3cbot/c3c-fbstate) to get fbstate.json (appstate.json)**
38 |
39 | ```javascript
40 | const login = require("fb-chat-api-temp");
41 |
42 | // Create simple echo bot
43 | login({email: "FB_EMAIL", password: "FB_PASSWORD"}, (err, api) => {
44 | if(err) return console.error(err);
45 |
46 | api.listen((err, message) => {
47 | api.sendMessage(message.body, message.threadID);
48 | });
49 | });
50 | ```
51 |
52 | Or use `appstate.json` to save your login state:
53 |
54 | ```javascript
55 | const login = require("fb-chat-api-temp");
56 | const fs = require("fs");
57 |
58 | // Create simple echo bot
59 | login({appState: JSON.parse(fs.readFileSync('appstate.json', 'utf8'))}, (err, api) => {
60 | if(err) return console.error(err);
61 | console.log("Logged in!");
62 |
63 | api.listen((err, message) => {
64 | api.sendMessage(message.body, message.threadID);
65 | });
66 | });
67 | ```
68 |
69 | Result:
70 |
71 |
72 |
73 |
74 | ## Documentation
75 |
76 | You can see it [here](DOCS.md).
77 |
78 | ## Main Functionality
79 |
80 | ### Sending a message
81 | #### api.sendMessage(message, threadID, [callback], [messageID])
82 |
83 | Various types of message can be sent:
84 | * *Regular:* set field `body` to the desired message as a string.
85 | * *Sticker:* set a field `sticker` to the desired sticker ID.
86 | * *File or image:* Set field `attachment` to a readable stream or an array of readable streams.
87 | * *URL:* set a field `url` to the desired URL.
88 | * *Emoji:* set field `emoji` to the desired emoji as a string and set field `emojiSize` with size of the emoji (`small`, `medium`, `large`)
89 |
90 | 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.
91 |
92 | __Tip__: to find your own ID, you can look inside the cookies. The `userID` is under the name `c_user`.
93 |
94 | __Example (Basic Message)__
95 | ```js
96 | const login = require("fb-chat-api-temp");
97 |
98 | login({email: "FB_EMAIL", password: "FB_PASSWORD"}, (err, api) => {
99 | if(err) return console.error(err);
100 |
101 | var yourID = "000000000000000";
102 | var msg = "Hey!";
103 | api.sendMessage(msg, yourID);
104 | });
105 | ```
106 |
107 | __Example (File upload)__
108 | ```js
109 | const login = require("fb-chat-api-temp");
110 |
111 | login({email: "FB_EMAIL", password: "FB_PASSWORD"}, (err, api) => {
112 | if(err) return console.error(err);
113 |
114 | // Note this example uploads an image called image.jpg
115 | var yourID = "000000000000000";
116 | var msg = {
117 | body: "Hey!",
118 | attachment: fs.createReadStream(__dirname + '/image.jpg')
119 | }
120 | api.sendMessage(msg, yourID);
121 | });
122 | ```
123 |
124 | ------------------------------------
125 | ### Saving session.
126 |
127 | To avoid logging in every time you should save AppState (cookies etc.) to a file, then you can use it without having password in your scripts.
128 |
129 | __Example__
130 |
131 | ```js
132 | const fs = require("fs");
133 | const login = require("fb-chat-api-temp");
134 |
135 | var credentials = {email: "FB_EMAIL", password: "FB_PASSWORD"};
136 |
137 | login(credentials, (err, api) => {
138 | if(err) return console.error(err);
139 |
140 | fs.writeFileSync('appstate.json', JSON.stringify(api.getAppState()));
141 | });
142 | ```
143 |
144 | Alternative: Use [c3c-fbstate](https://github.com/lequanglam/c3c-fbstate) to get fbstate.json (appstate.json)
145 |
146 | ------------------------------------
147 |
148 | ### Listening to a chat
149 | #### api.listenMqtt(callback)
150 |
151 | Listen watches for messages sent in a chat. 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})`.
152 |
153 | __Example__
154 |
155 | ```js
156 | const fs = require("fs");
157 | const login = require("fb-chat-api-temp");
158 |
159 | // Simple echo bot. It will repeat everything that you say.
160 | // Will stop when you say '/stop'
161 | login({appState: JSON.parse(fs.readFileSync('appstate.json', 'utf8'))}, (err, api) => {
162 | if(err) return console.error(err);
163 |
164 | api.setOptions({listenEvents: true});
165 |
166 | var stopListening = api.listenMqtt((err, event) => {
167 | if(err) return console.error(err);
168 |
169 | api.markAsRead(event.threadID, (err) => {
170 | if(err) console.error(err);
171 | });
172 |
173 | switch(event.type) {
174 | case "message":
175 | if(event.body === '/stop') {
176 | api.sendMessage("Goodbye…", event.threadID);
177 | return stopListening();
178 | }
179 | api.sendMessage("TEST BOT: " + event.body, event.threadID);
180 | break;
181 | case "event":
182 | console.log(event);
183 | break;
184 | }
185 | });
186 | });
187 | ```
188 |
189 | ## FAQS
190 |
191 | 1. How do I run tests?
192 | > For tests, create a `test-config.json` file that resembles `example-config.json` and put it in the `test` directory. From the root >directory, run `npm test`.
193 |
194 | 2. Why doesn't `sendMessage` always work when I'm logged in as a page?
195 | > Pages can't start conversations with users directly; this is to prevent pages from spamming users.
196 |
197 | 3. What do I do when `login` doesn't work?
198 | > First check that you can login to Facebook using the website. If login approvals are enabled, you might be logging in incorrectly. For how to handle login approvals, read our docs on [`login`](DOCS.md#login).
199 |
200 | 4. How can I avoid logging in every time? Can I log into a previous session?
201 | > We support caching everything relevant for you to bypass login. `api.getAppState()` returns an object that you can save and pass into login as `{appState: mySavedAppState}` instead of the credentials object. If this fails, your session has expired.
202 |
203 | 5. Do you support sending messages as a page?
204 | > Yes, set the pageID option on login (this doesn't work if you set it using api.setOptions, it affects the login process).
205 | > ```js
206 | > login(credentials, {pageID: "000000000000000"}, (err, api) => { … }
207 | > ```
208 |
209 | 6. I'm getting some crazy weird syntax error like `SyntaxError: Unexpected token [`!!!
210 | > Please try to update your version of node.js before submitting an issue of this nature. We like to use new language features.
211 |
212 | 7. I don't want all of these logging messages!
213 | > You can use `api.setOptions` to silence the logging. You get the `api` object from `login` (see example above). Do
214 | > ```js
215 | > api.setOptions({
216 | > logLevel: "silent"
217 | > });
218 | > ```
219 |
220 |
221 | ## Projects using this API:
222 | - [c3c](https://github.com/lequanglam/c3c) - A bot that can be customizable using plugins. Support Facebook & Discord.
223 | - [GOAT BOT 🐐](https://github.com/ntkhang03/Goat-Bot) - A bot chat Messenger can be customizable using scripts. Support .
224 |
225 | ## Projects using this API (original repository, facebook-chat-api):
226 |
227 | - [Messer](https://github.com/mjkaufer/Messer) - Command-line messaging for Facebook Messenger
228 | - [messen](https://github.com/tomquirk/messen) - Rapidly build Facebook Messenger apps in Node.js
229 | - [Concierge](https://github.com/concierge/Concierge) - Concierge is a highly modular, easily extensible general purpose chat bot with a built in package manager
230 | - [Marc Zuckerbot](https://github.com/bsansouci/marc-zuckerbot) - Facebook chat bot
231 | - [Marc Thuckerbot](https://github.com/bsansouci/lisp-bot) - Programmable lisp bot
232 | - [MarkovsInequality](https://github.com/logicx24/MarkovsInequality) - Extensible chat bot adding useful functions to Facebook Messenger
233 | - [AllanBot](https://github.com/AllanWang/AllanBot-Public) - Extensive module that combines the facebook api with firebase to create numerous functions; no coding experience is required to implement this.
234 | - [Larry Pudding Dog Bot](https://github.com/Larry850806/facebook-chat-bot) - A facebook bot you can easily customize the response
235 | - [fbash](https://github.com/avikj/fbash) - Run commands on your computer's terminal over Facebook Messenger
236 | - [Klink](https://github.com/KeNt178/klink) - This Chrome extension will 1-click share the link of your active tab over Facebook Messenger
237 | - [Botyo](https://github.com/ivkos/botyo) - Modular bot designed for group chat rooms on Facebook
238 | - [matrix-puppet-facebook](https://github.com/matrix-hacks/matrix-puppet-facebook) - A facebook bridge for [matrix](https://matrix.org)
239 | - [facebot](https://github.com/Weetbix/facebot) - A facebook bridge for Slack.
240 | - [Botium](https://github.com/codeforequity-at/botium-core) - The Selenium for Chatbots
241 | - [Messenger-CLI](https://github.com/AstroCB/Messenger-CLI) - A command-line interface for sending and receiving messages through Facebook Messenger.
242 | - [AssumeZero-Bot](https://github.com/AstroCB/AssumeZero-Bot) – A highly customizable Facebook Messenger bot for group chats.
243 | - [Miscord](https://github.com/Bjornskjald/miscord) - An easy-to-use Facebook bridge for Discord.
244 | - [chat-bridge](https://github.com/rexx0520/chat-bridge) - A Messenger, Telegram and IRC chat bridge.
245 | - [messenger-auto-reply](https://gitlab.com/theSander/messenger-auto-reply) - An auto-reply service for Messenger.
246 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("./utils");
4 | const log = require("npmlog");
5 |
6 | let 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 | mqttEndpoint,
153 | region,
154 | firstListen: true
155 | };
156 |
157 | const api = {
158 | setOptions: setOptions.bind(null, globalOptions),
159 | getAppState: function getAppState() {
160 | const appState = utils.getAppState(jar);
161 | // filter duplicate
162 | return appState.filter((item, index, self) => self.findIndex((t) => { return t.key === item.key }) === index);
163 | }
164 | };
165 |
166 | if (noMqttData) {
167 | api["htmlData"] = noMqttData;
168 | }
169 |
170 | const apiFuncNames = [
171 | 'addExternalModule',
172 | 'addUserToGroup',
173 | 'changeAdminStatus',
174 | 'changeArchivedStatus',
175 | 'changeAvatar',
176 | 'changeBio',
177 | 'changeBlockedStatus',
178 | 'changeGroupImage',
179 | 'changeNickname',
180 | 'changeThreadColor',
181 | 'changeThreadEmoji',
182 | 'createNewGroup',
183 | 'createPoll',
184 | 'deleteMessage',
185 | 'deleteThread',
186 | 'forwardAttachment',
187 | 'getCurrentUserID',
188 | 'getEmojiUrl',
189 | 'getFriendsList',
190 | 'getMessage',
191 | 'getThreadHistory',
192 | 'getThreadInfo',
193 | 'getThreadList',
194 | 'getThreadPictures',
195 | 'getUserID',
196 | 'getUserInfo',
197 | 'handleMessageRequest',
198 | 'listenMqtt',
199 | 'logout',
200 | 'markAsDelivered',
201 | 'markAsRead',
202 | 'markAsReadAll',
203 | 'markAsSeen',
204 | 'muteThread',
205 | 'refreshFb_dtsg',
206 | 'removeUserFromGroup',
207 | 'resolvePhotoUrl',
208 | 'searchForThread',
209 | 'sendMessage',
210 | 'sendTypingIndicator',
211 | 'setMessageReaction',
212 | 'setPostReaction',
213 | 'setTitle',
214 | 'threadColors',
215 | 'unsendMessage',
216 | 'unfriend',
217 |
218 | // HTTP
219 | 'httpGet',
220 | 'httpPost',
221 | 'httpPostFormData',
222 |
223 | 'uploadAttachment'
224 | ];
225 |
226 | const defaultFuncs = utils.makeDefaults(html, i_userID || userID, ctx);
227 |
228 | // Load all api functions in a loop
229 | apiFuncNames.map(function (v) {
230 | api[v] = require('./src/' + v)(defaultFuncs, api, ctx);
231 | });
232 |
233 | //Removing original `listen` that uses pull.
234 | //Map it to listenMqtt instead for backward compatibly.
235 | api.listen = api.listenMqtt;
236 |
237 | return [ctx, defaultFuncs, api];
238 | }
239 |
240 | // Helps the login
241 | function loginHelper(appState, email, password, globalOptions, callback, prCallback) {
242 | let mainPromise = null;
243 | const jar = utils.getJar();
244 |
245 | // If we're given an appState we loop through it and save each cookie
246 | // back into the jar.
247 | if (appState) {
248 | // check and convert cookie to appState
249 | if (utils.getType(appState) === 'Array' && appState.some(c => c.name)) {
250 | appState = appState.map(c => {
251 | c.key = c.name;
252 | delete c.name;
253 | return c;
254 | })
255 | }
256 | else if (utils.getType(appState) === 'String') {
257 | const arrayAppState = [];
258 | appState.split(';').forEach(c => {
259 | const [key, value] = c.split('=');
260 |
261 | arrayAppState.push({
262 | key: (key || "").trim(),
263 | value: (value || "").trim(),
264 | domain: "facebook.com",
265 | path: "/",
266 | expires: new Date().getTime() + 1000 * 60 * 60 * 24 * 365
267 | });
268 | });
269 | appState = arrayAppState;
270 | }
271 |
272 | appState.map(function (c) {
273 | const str = c.key + "=" + c.value + "; expires=" + c.expires + "; domain=" + c.domain + "; path=" + c.path + ";";
274 | jar.setCookie(str, "http://" + c.domain);
275 | });
276 |
277 | // Load the main page.
278 | mainPromise = utils
279 | .get('https://www.facebook.com/', jar, null, globalOptions, { noRef: true })
280 | .then(utils.saveCookies(jar));
281 | } else {
282 | if (email) {
283 | throw { error: "Currently, the login method by email and password is no longer supported, please use the login method by appState" };
284 | }
285 | else {
286 | throw { error: "No appState given." };
287 | }
288 | }
289 |
290 | let ctx = null;
291 | let _defaultFuncs = null;
292 | let api = null;
293 |
294 | mainPromise = mainPromise
295 | .then(function (res) {
296 | // Hacky check for the redirection that happens on some ISPs, which doesn't return statusCode 3xx
297 | const reg = /]+>/;
298 | const redirect = reg.exec(res.body);
299 | if (redirect && redirect[1]) {
300 | return utils
301 | .get(redirect[1], jar, null, globalOptions)
302 | .then(utils.saveCookies(jar));
303 | }
304 | return res;
305 | })
306 | .then(function (res) {
307 | const html = res.body;
308 | const stuff = buildAPI(globalOptions, html, jar);
309 | ctx = stuff[0];
310 | _defaultFuncs = stuff[1];
311 | api = stuff[2];
312 | return res;
313 | });
314 |
315 | // given a pageID we log in as a page
316 | if (globalOptions.pageID) {
317 | mainPromise = mainPromise
318 | .then(function () {
319 | return utils
320 | .get('https://www.facebook.com/' + ctx.globalOptions.pageID + '/messages/?section=messages&subsection=inbox', ctx.jar, null, globalOptions);
321 | })
322 | .then(function (resData) {
323 | let url = utils.getFrom(resData.body, 'window.location.replace("https:\\/\\/www.facebook.com\\', '");').split('\\').join('');
324 | url = url.substring(0, url.length - 1);
325 |
326 | return utils
327 | .get('https://www.facebook.com' + url, ctx.jar, null, globalOptions);
328 | });
329 | }
330 |
331 | // At the end we call the callback or catch an exception
332 | mainPromise
333 | .then(function () {
334 | log.info("login", 'Done logging in.');
335 | return callback(null, api);
336 | })
337 | .catch(function (e) {
338 | log.error("login", e.error || e);
339 | callback(e);
340 | });
341 | }
342 |
343 | function login(loginData, options, callback) {
344 | if (utils.getType(options) === 'Function' || utils.getType(options) === 'AsyncFunction') {
345 | callback = options;
346 | options = {};
347 | }
348 |
349 | const globalOptions = {
350 | selfListen: false,
351 | selfListenEvent: false,
352 | listenEvents: false,
353 | listenTyping: false,
354 | updatePresence: false,
355 | forceLogin: false,
356 | autoMarkDelivery: true,
357 | autoMarkRead: false,
358 | autoReconnect: true,
359 | logRecordSize: defaultLogRecordSize,
360 | online: true,
361 | emitReady: false,
362 | 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"
363 | };
364 |
365 | setOptions(globalOptions, options);
366 |
367 | let prCallback = null;
368 | if (utils.getType(callback) !== "Function" && utils.getType(callback) !== "AsyncFunction") {
369 | let rejectFunc = null;
370 | let resolveFunc = null;
371 | var returnPromise = new Promise(function (resolve, reject) {
372 | resolveFunc = resolve;
373 | rejectFunc = reject;
374 | });
375 | prCallback = function (error, api) {
376 | if (error) {
377 | return rejectFunc(error);
378 | }
379 | return resolveFunc(api);
380 | };
381 | callback = prCallback;
382 | }
383 | loginHelper(loginData.appState, loginData.email, loginData.password, globalOptions, callback, prCallback);
384 | return returnPromise;
385 | }
386 |
387 | module.exports = login;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fb-chat-api-temp",
3 | "version": "1.0.4",
4 | "description": "A Facebook chat API that doesn't rely on XMPP",
5 | "scripts": {
6 | "test": "mocha",
7 | "lint": "./node_modules/.bin/eslint **.js",
8 | "prettier": "prettier utils.js src/* --write"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "https://github.com/ntkhang03/fb-chat-api-temp.git"
13 | },
14 | "keywords": [
15 | "facebook",
16 | "chat",
17 | "api",
18 | "fca",
19 | "fb-chat-api",
20 | "fb-chat-api-temp"
21 | ],
22 | "bugs": {
23 | "url": "https://github.com/ntkhang03/fb-chat-api-temp/issues"
24 | },
25 | "author": "Avery, David, Maude, Benjamin, UIRI, NTKhang (rebuild)",
26 | "license": "MIT",
27 | "dependencies": {
28 | "https-proxy-agent": "^4.0.0",
29 | "mqtt": "^3.0.0",
30 | "npmlog": "^1.2.0",
31 | "request": "^2.53.0",
32 | "websocket-stream": "^5.5.0"
33 | },
34 | "engines": {
35 | "node": ">=10.x"
36 | },
37 | "devDependencies": {
38 | "eslint": "^7.5.0",
39 | "mocha": "^7.0.1",
40 | "prettier": "^1.11.1"
41 | },
42 | "eslintConfig": {
43 | "env": {
44 | "commonjs": true,
45 | "es2021": true,
46 | "node": true
47 | },
48 | "extends": "eslint:recommended",
49 | "parserOptions": {
50 | "ecmaVersion": 13
51 | },
52 | "rules": {
53 | "no-prototype-builtins": 0,
54 | "no-unused-vars": 1,
55 | "comma-dangle": 1,
56 | "no-redeclare": 0,
57 | "prefer-const": 1,
58 | "no-useless-escape": 0,
59 | "no-mixed-spaces-and-tabs": 0,
60 | "semi": 1,
61 | "no-useless-catch": 0,
62 | "no-empty": 0,
63 | "use-isnan": 0,
64 | "no-extra-semi": 1,
65 | "no-async-promise-executor": 0,
66 | "no-unreachable": 1,
67 | "valid-typeof": 0,
68 | "no-case-declarations": 0
69 | }
70 | }
71 | }
--------------------------------------------------------------------------------
/src/addExternalModule.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 |
5 | module.exports = function (defaultFuncs, api, ctx) {
6 | return function addExternalModule(moduleObj) {
7 | if (utils.getType(moduleObj) == "Object") {
8 | for (const apiName in moduleObj) {
9 | if (utils.getType(moduleObj[apiName]) == "Function") {
10 | api[apiName] = moduleObj[apiName](defaultFuncs, api, ctx);
11 | } else {
12 | throw new Error(`Item "${apiName}" in moduleObj must be a function, not ${utils.getType(moduleObj[apiName])}!`);
13 | }
14 | }
15 | } else {
16 | throw new Error(`moduleObj must be an object, not ${utils.getType(moduleObj)}!`);
17 | }
18 | };
19 | };
20 |
--------------------------------------------------------------------------------
/src/addUserToGroup.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | return function addUserToGroup(userID, threadID, callback) {
8 | let resolveFunc = function () { };
9 | let rejectFunc = function () { };
10 | const returnPromise = new Promise(function (resolve, reject) {
11 | resolveFunc = resolve;
12 | rejectFunc = reject;
13 | });
14 |
15 | if (
16 | !callback &&
17 | (utils.getType(threadID) === "Function" ||
18 | utils.getType(threadID) === "AsyncFunction")
19 | ) {
20 | throw new utils.CustomError({ error: "please pass a threadID as a second argument." });
21 | }
22 |
23 | if (!callback) {
24 | callback = function (err) {
25 | if (err) {
26 | return rejectFunc(err);
27 | }
28 | resolveFunc();
29 | };
30 | }
31 |
32 | if (
33 | utils.getType(threadID) !== "Number" &&
34 | utils.getType(threadID) !== "String"
35 | ) {
36 | throw new utils.CustomError({
37 | error:
38 | "ThreadID should be of type Number or String and not " +
39 | utils.getType(threadID) +
40 | "."
41 | });
42 | }
43 |
44 | if (utils.getType(userID) !== "Array") {
45 | userID = [userID];
46 | }
47 |
48 | const messageAndOTID = utils.generateOfflineThreadingID();
49 | const form = {
50 | client: "mercury",
51 | action_type: "ma-type:log-message",
52 | author: "fbid:" + (ctx.i_userID || ctx.userID),
53 | thread_id: "",
54 | timestamp: Date.now(),
55 | timestamp_absolute: "Today",
56 | timestamp_relative: utils.generateTimestampRelative(),
57 | timestamp_time_passed: "0",
58 | is_unread: false,
59 | is_cleared: false,
60 | is_forward: false,
61 | is_filtered_content: false,
62 | is_filtered_content_bh: false,
63 | is_filtered_content_account: false,
64 | is_spoof_warning: false,
65 | source: "source:chat:web",
66 | "source_tags[0]": "source:chat",
67 | log_message_type: "log:subscribe",
68 | status: "0",
69 | offline_threading_id: messageAndOTID,
70 | message_id: messageAndOTID,
71 | threading_id: utils.generateThreadingID(ctx.clientID),
72 | manual_retry_cnt: "0",
73 | thread_fbid: threadID
74 | };
75 |
76 | for (let i = 0; i < userID.length; i++) {
77 | if (
78 | utils.getType(userID[i]) !== "Number" &&
79 | utils.getType(userID[i]) !== "String"
80 | ) {
81 | throw new utils.CustomError({
82 | error:
83 | "Elements of userID should be of type Number or String and not " +
84 | utils.getType(userID[i]) +
85 | "."
86 | });
87 | }
88 |
89 | form["log_message_data[added_participants][" + i + "]"] =
90 | "fbid:" + userID[i];
91 | }
92 |
93 | defaultFuncs
94 | .post("https://www.facebook.com/messaging/send/", ctx.jar, form)
95 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
96 | .then(function (resData) {
97 | if (!resData) {
98 | throw new utils.CustomError({ error: "Add to group failed." });
99 | }
100 | if (resData.error) {
101 | throw new utils.CustomError(resData);
102 | }
103 |
104 | return callback();
105 | })
106 | .catch(function (err) {
107 | log.error("addUserToGroup", err);
108 | return callback(err);
109 | });
110 |
111 | return returnPromise;
112 | };
113 | };
114 |
--------------------------------------------------------------------------------
/src/changeAdminStatus.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | return function changeAdminStatus(threadID, adminIDs, adminStatus, callback) {
8 | if (utils.getType(threadID) !== "String") {
9 | throw new utils.CustomError({ error: "changeAdminStatus: threadID must be a string" });
10 | }
11 |
12 | if (utils.getType(adminIDs) === "String") {
13 | adminIDs = [adminIDs];
14 | }
15 |
16 | if (utils.getType(adminIDs) !== "Array") {
17 | throw new utils.CustomError({ error: "changeAdminStatus: adminIDs must be an array or string" });
18 | }
19 |
20 | if (utils.getType(adminStatus) !== "Boolean") {
21 | throw new utils.CustomError({ error: "changeAdminStatus: adminStatus must be a string" });
22 | }
23 |
24 | let resolveFunc = function () { };
25 | let rejectFunc = function () { };
26 | const returnPromise = new Promise(function (resolve, reject) {
27 | resolveFunc = resolve;
28 | rejectFunc = reject;
29 | });
30 |
31 | if (!callback) {
32 | callback = function (err) {
33 | if (err) {
34 | return rejectFunc(err);
35 | }
36 | resolveFunc();
37 | };
38 | }
39 |
40 | if (utils.getType(callback) !== "Function" && utils.getType(callback) !== "AsyncFunction") {
41 | throw new utils.CustomError({ error: "changeAdminStatus: callback is not a function" });
42 | }
43 |
44 | const form = {
45 | "thread_fbid": threadID
46 | };
47 |
48 | let i = 0;
49 | for (const u of adminIDs) {
50 | form[`admin_ids[${i++}]`] = u;
51 | }
52 | form["add"] = adminStatus;
53 |
54 | defaultFuncs
55 | .post("https://www.facebook.com/messaging/save_admins/?dpr=1", ctx.jar, form)
56 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
57 | .then(function (resData) {
58 | if (resData.error) {
59 | switch (resData.error) {
60 | case 1976004:
61 | throw new utils.CustomError({ error: "Cannot alter admin status: you are not an admin.", rawResponse: resData });
62 | case 1357031:
63 | throw new utils.CustomError({ error: "Cannot alter admin status: this thread is not a group chat.", rawResponse: resData });
64 | default:
65 | throw new utils.CustomError({ error: "Cannot alter admin status: unknown error.", rawResponse: resData });
66 | }
67 | }
68 |
69 | callback();
70 | })
71 | .catch(function (err) {
72 | log.error("changeAdminStatus", err);
73 | return callback(err);
74 | });
75 |
76 | return returnPromise;
77 | };
78 | };
79 |
80 |
--------------------------------------------------------------------------------
/src/changeArchivedStatus.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | return function changeArchivedStatus(threadOrThreads, archive, callback) {
8 | let resolveFunc = function () { };
9 | let rejectFunc = function () { };
10 | const returnPromise = new Promise(function (resolve, reject) {
11 | resolveFunc = resolve;
12 | rejectFunc = reject;
13 | });
14 |
15 | if (!callback) {
16 | callback = function (err) {
17 | if (err) {
18 | return rejectFunc(err);
19 | }
20 | resolveFunc();
21 | };
22 | }
23 |
24 | const form = {};
25 |
26 | if (utils.getType(threadOrThreads) === "Array") {
27 | for (let i = 0; i < threadOrThreads.length; i++) {
28 | form["ids[" + threadOrThreads[i] + "]"] = archive;
29 | }
30 | } else {
31 | form["ids[" + threadOrThreads + "]"] = archive;
32 | }
33 |
34 | defaultFuncs
35 | .post(
36 | "https://www.facebook.com/ajax/mercury/change_archived_status.php",
37 | ctx.jar,
38 | form
39 | )
40 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
41 | .then(function (resData) {
42 | if (resData.error) {
43 | throw resData;
44 | }
45 |
46 | return callback();
47 | })
48 | .catch(function (err) {
49 | log.error("changeArchivedStatus", err);
50 | return callback(err);
51 | });
52 |
53 | return returnPromise;
54 | };
55 | };
56 |
--------------------------------------------------------------------------------
/src/changeAvatar.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | function handleUpload(image, callback) {
8 | const uploads = [];
9 |
10 | const form = {
11 | profile_id: ctx.i_userID || ctx.userID,
12 | photo_source: 57,
13 | av: ctx.i_userID || ctx.userID,
14 | file: image
15 | };
16 |
17 | uploads.push(
18 | defaultFuncs
19 | .postFormData(
20 | "https://www.facebook.com/profile/picture/upload/",
21 | ctx.jar,
22 | form,
23 | {}
24 | )
25 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
26 | .then(function (resData) {
27 | if (resData.error) {
28 | throw resData;
29 | }
30 | return resData;
31 | })
32 | );
33 |
34 | // resolve all promises
35 | Promise
36 | .all(uploads)
37 | .then(function (resData) {
38 | callback(null, resData);
39 | })
40 | .catch(function (err) {
41 | log.error("handleUpload", err);
42 | return callback(err);
43 | });
44 | }
45 |
46 | return function changeAvatar(image, caption = "", timestamp = null, callback) {
47 | let resolveFunc = function () { };
48 | let rejectFunc = function () { };
49 | const returnPromise = new Promise(function (resolve, reject) {
50 | resolveFunc = resolve;
51 | rejectFunc = reject;
52 | });
53 |
54 | if (!timestamp && utils.getType(caption) === "Number") {
55 | timestamp = caption;
56 | caption = "";
57 | }
58 |
59 | if (!timestamp && !callback && (utils.getType(caption) == "Function" || utils.getType(caption) == "AsyncFunction")) {
60 | callback = caption;
61 | caption = "";
62 | timestamp = null;
63 | }
64 |
65 | if (!callback) callback = function (err, data) {
66 | if (err) {
67 | return rejectFunc(err);
68 | }
69 | resolveFunc(data);
70 | };
71 |
72 | if (!utils.isReadableStream(image))
73 | return callback("Image is not a readable stream");
74 |
75 | handleUpload(image, function (err, payload) {
76 | if (err) {
77 | return callback(err);
78 | }
79 |
80 | const form = {
81 | av: ctx.i_userID || ctx.userID,
82 | fb_api_req_friendly_name: "ProfileCometProfilePictureSetMutation",
83 | fb_api_caller_class: "RelayModern",
84 | doc_id: "5066134240065849",
85 | variables: JSON.stringify({
86 | input: {
87 | caption,
88 | existing_photo_id: payload[0].payload.fbid,
89 | expiration_time: timestamp,
90 | profile_id: ctx.i_userID || ctx.userID,
91 | profile_pic_method: "EXISTING",
92 | profile_pic_source: "TIMELINE",
93 | scaled_crop_rect: {
94 | height: 1,
95 | width: 1,
96 | x: 0,
97 | y: 0
98 | },
99 | skip_cropping: true,
100 | actor_id: ctx.i_userID || ctx.userID,
101 | client_mutation_id: Math.round(Math.random() * 19).toString()
102 | },
103 | isPage: false,
104 | isProfile: true,
105 | scale: 3
106 | })
107 | };
108 |
109 | defaultFuncs
110 | .post("https://www.facebook.com/api/graphql/", ctx.jar, form)
111 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
112 | .then(function (resData) {
113 | if (resData.errors) {
114 | throw resData;
115 | }
116 | return callback(null, resData[0].data.profile_picture_set);
117 | })
118 | .catch(function (err) {
119 | log.error("changeAvatar", err);
120 | return callback(err);
121 | });
122 | });
123 |
124 | return returnPromise;
125 | };
126 | };
127 |
--------------------------------------------------------------------------------
/src/changeBio.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | return function changeBio(bio, publish, callback) {
8 | let resolveFunc = function () { };
9 | let rejectFunc = function () { };
10 | const returnPromise = new Promise(function (resolve, reject) {
11 | resolveFunc = resolve;
12 | rejectFunc = reject;
13 | });
14 |
15 | if (!callback) {
16 | if (utils.getType(publish) == "Function" || utils.getType(publish) == "AsyncFunction") {
17 | callback = publish;
18 | } else {
19 | callback = function (err) {
20 | if (err) {
21 | return rejectFunc(err);
22 | }
23 | resolveFunc();
24 | };
25 | }
26 | }
27 |
28 | if (utils.getType(publish) != "Boolean") {
29 | publish = false;
30 | }
31 |
32 | if (utils.getType(bio) != "String") {
33 | bio = "";
34 | publish = false;
35 | }
36 |
37 | const form = {
38 | fb_api_caller_class: "RelayModern",
39 | fb_api_req_friendly_name: "ProfileCometSetBioMutation",
40 | // This doc_is is valid as of May 23, 2020
41 | doc_id: "2725043627607610",
42 | variables: JSON.stringify({
43 | input: {
44 | bio: bio,
45 | publish_bio_feed_story: publish,
46 | actor_id: ctx.i_userID || ctx.userID,
47 | client_mutation_id: Math.round(Math.random() * 1024).toString()
48 | },
49 | hasProfileTileViewID: false,
50 | profileTileViewID: null,
51 | scale: 1
52 | }),
53 | av: ctx.i_userID || ctx.userID
54 | };
55 |
56 | defaultFuncs
57 | .post(
58 | "https://www.facebook.com/api/graphql/",
59 | ctx.jar,
60 | form
61 | )
62 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
63 | .then(function (resData) {
64 | if (resData.errors) {
65 | throw resData;
66 | }
67 |
68 | return callback();
69 | })
70 | .catch(function (err) {
71 | log.error("changeBio", err);
72 | return callback(err);
73 | });
74 |
75 | return returnPromise;
76 | };
77 | };
78 |
--------------------------------------------------------------------------------
/src/changeBlockedStatus.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | return function changeBlockedStatus(userID, block, callback) {
8 | let resolveFunc = function () { };
9 | let rejectFunc = function () { };
10 | const returnPromise = new Promise(function (resolve, reject) {
11 | resolveFunc = resolve;
12 | rejectFunc = reject;
13 | });
14 |
15 | if (!callback) {
16 | callback = function (err) {
17 | if (err) {
18 | return rejectFunc(err);
19 | }
20 | resolveFunc();
21 | };
22 | }
23 |
24 | defaultFuncs
25 | .post(
26 | `https://www.facebook.com/messaging/${block ? "" : "un"}block_messages/`,
27 | ctx.jar,
28 | {
29 | fbid: userID
30 | }
31 | )
32 | .then(utils.saveCookies(ctx.jar))
33 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
34 | .then(function (resData) {
35 | if (resData.error) {
36 | throw resData;
37 | }
38 |
39 | return callback();
40 | })
41 | .catch(function (err) {
42 | log.error("changeBlockedStatus", err);
43 | return callback(err);
44 | });
45 | return returnPromise;
46 | };
47 | };
48 |
--------------------------------------------------------------------------------
/src/changeGroupImage.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | function handleUpload(image, callback) {
8 | const uploads = [];
9 |
10 | const form = {
11 | images_only: "true",
12 | "attachment[]": image
13 | };
14 |
15 | uploads.push(
16 | defaultFuncs
17 | .postFormData(
18 | "https://upload.facebook.com/ajax/mercury/upload.php",
19 | ctx.jar,
20 | form,
21 | {}
22 | )
23 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
24 | .then(function (resData) {
25 | if (resData.error) {
26 | throw resData;
27 | }
28 |
29 | return resData.payload.metadata[0];
30 | })
31 | );
32 |
33 | // resolve all promises
34 | Promise
35 | .all(uploads)
36 | .then(function (resData) {
37 | callback(null, resData);
38 | })
39 | .catch(function (err) {
40 | log.error("handleUpload", err);
41 | return callback(err);
42 | });
43 | }
44 |
45 | return function changeGroupImage(image, threadID, callback) {
46 | if (
47 | !callback &&
48 | (utils.getType(threadID) === "Function" ||
49 | utils.getType(threadID) === "AsyncFunction")
50 | ) {
51 | throw { error: "please pass a threadID as a second argument." };
52 | }
53 |
54 | if (!utils.isReadableStream(image)) {
55 | throw { error: "please pass a readable stream as a first argument." };
56 | }
57 |
58 | let resolveFunc = function () { };
59 | let rejectFunc = function () { };
60 | const returnPromise = new Promise(function (resolve, reject) {
61 | resolveFunc = resolve;
62 | rejectFunc = reject;
63 | });
64 |
65 | if (!callback) {
66 | callback = function (err) {
67 | if (err) {
68 | return rejectFunc(err);
69 | }
70 | resolveFunc();
71 | };
72 | }
73 |
74 | const messageAndOTID = utils.generateOfflineThreadingID();
75 | const form = {
76 | client: "mercury",
77 | action_type: "ma-type:log-message",
78 | author: "fbid:" + (ctx.i_userID || ctx.userID),
79 | author_email: "",
80 | ephemeral_ttl_mode: "0",
81 | is_filtered_content: false,
82 | is_filtered_content_account: false,
83 | is_filtered_content_bh: false,
84 | is_filtered_content_invalid_app: false,
85 | is_filtered_content_quasar: false,
86 | is_forward: false,
87 | is_spoof_warning: false,
88 | is_unread: false,
89 | log_message_type: "log:thread-image",
90 | manual_retry_cnt: "0",
91 | message_id: messageAndOTID,
92 | offline_threading_id: messageAndOTID,
93 | source: "source:chat:web",
94 | "source_tags[0]": "source:chat",
95 | status: "0",
96 | thread_fbid: threadID,
97 | thread_id: "",
98 | timestamp: Date.now(),
99 | timestamp_absolute: "Today",
100 | timestamp_relative: utils.generateTimestampRelative(),
101 | timestamp_time_passed: "0"
102 | };
103 |
104 | handleUpload(image, function (err, payload) {
105 | if (err) {
106 | return callback(err);
107 | }
108 |
109 | form["thread_image_id"] = payload[0]["image_id"];
110 | form["thread_id"] = threadID;
111 |
112 | defaultFuncs
113 | .post("https://www.facebook.com/messaging/set_thread_image/", ctx.jar, form)
114 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
115 | .then(function (resData) {
116 | // check for errors here
117 |
118 | if (resData.error) {
119 | throw resData;
120 | }
121 |
122 | return callback();
123 | })
124 | .catch(function (err) {
125 | log.error("changeGroupImage", err);
126 | return callback(err);
127 | });
128 | });
129 |
130 | return returnPromise;
131 | };
132 | };
133 |
--------------------------------------------------------------------------------
/src/changeNickname.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | return function changeNickname(nickname, threadID, participantID, callback) {
8 | let resolveFunc = function () { };
9 | let rejectFunc = function () { };
10 | const returnPromise = new Promise(function (resolve, reject) {
11 | resolveFunc = resolve;
12 | rejectFunc = reject;
13 | });
14 | if (!callback) {
15 | callback = function (err) {
16 | if (err) {
17 | return rejectFunc(err);
18 | }
19 | resolveFunc();
20 | };
21 | }
22 |
23 | const form = {
24 | nickname: nickname,
25 | participant_id: participantID,
26 | thread_or_other_fbid: threadID
27 | };
28 |
29 | defaultFuncs
30 | .post(
31 | "https://www.facebook.com/messaging/save_thread_nickname/?source=thread_settings&dpr=1",
32 | ctx.jar,
33 | form
34 | )
35 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
36 | .then(function (resData) {
37 | if (resData.error === 1545014) {
38 | throw { error: "Trying to change nickname of user isn't in thread" };
39 | }
40 | if (resData.error === 1357031) {
41 | throw {
42 | error:
43 | "Trying to change user nickname of a thread that doesn't exist. Have at least one message in the thread before trying to change the user nickname."
44 | };
45 | }
46 | if (resData.error) {
47 | throw resData;
48 | }
49 |
50 | return callback();
51 | })
52 | .catch(function (err) {
53 | log.error("changeNickname", err);
54 | return callback(err);
55 | });
56 |
57 | return returnPromise;
58 | };
59 | };
60 |
--------------------------------------------------------------------------------
/src/changeThreadColor.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | return function changeThreadColor(color, threadID, callback) {
8 | let resolveFunc = function () { };
9 | let rejectFunc = function () { };
10 | const returnPromise = new Promise(function (resolve, reject) {
11 | resolveFunc = resolve;
12 | rejectFunc = reject;
13 | });
14 |
15 | if (!callback) {
16 | callback = function (err) {
17 | if (err) {
18 | return rejectFunc(err);
19 | }
20 | resolveFunc(err);
21 | };
22 | }
23 |
24 | if (!isNaN(color)) {
25 | color = color.toString();
26 | }
27 | const validatedColor = color !== null ? color.toLowerCase() : color; // API only accepts lowercase letters in hex string
28 |
29 | const form = {
30 | dpr: 1,
31 | queries: JSON.stringify({
32 | o0: {
33 | //This doc_id is valid as of January 31, 2020
34 | doc_id: "1727493033983591",
35 | query_params: {
36 | data: {
37 | actor_id: ctx.i_userID || ctx.userID,
38 | client_mutation_id: "0",
39 | source: "SETTINGS",
40 | theme_id: validatedColor,
41 | thread_id: threadID
42 | }
43 | }
44 | }
45 | })
46 | };
47 |
48 | defaultFuncs
49 | .post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
50 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
51 | .then(function (resData) {
52 | if (resData[resData.length - 1].error_results > 0) {
53 | throw new utils.CustomError(resData[0].o0.errors);
54 | }
55 |
56 | return callback();
57 | })
58 | .catch(function (err) {
59 | log.error("changeThreadColor", err);
60 | return callback(err);
61 | });
62 |
63 | return returnPromise;
64 | };
65 | };
66 |
--------------------------------------------------------------------------------
/src/changeThreadEmoji.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | return function changeThreadEmoji(emoji, threadID, callback) {
8 | let resolveFunc = function () { };
9 | let rejectFunc = function () { };
10 | const returnPromise = new Promise(function (resolve, reject) {
11 | resolveFunc = resolve;
12 | rejectFunc = reject;
13 | });
14 |
15 | if (!callback) {
16 | callback = function (err) {
17 | if (err) {
18 | return rejectFunc(err);
19 | }
20 | resolveFunc();
21 | };
22 | }
23 | const form = {
24 | emoji_choice: emoji,
25 | thread_or_other_fbid: threadID
26 | };
27 |
28 | defaultFuncs
29 | .post(
30 | "https://www.facebook.com/messaging/save_thread_emoji/?source=thread_settings&__pc=EXP1%3Amessengerdotcom_pkg",
31 | ctx.jar,
32 | form
33 | )
34 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
35 | .then(function (resData) {
36 | if (resData.error === 1357031) {
37 | throw {
38 | error:
39 | "Trying to change emoji of a chat that doesn't exist. Have at least one message in the thread before trying to change the emoji."
40 | };
41 | }
42 | if (resData.error) {
43 | throw resData;
44 | }
45 |
46 | return callback();
47 | })
48 | .catch(function (err) {
49 | log.error("changeThreadEmoji", err);
50 | return callback(err);
51 | });
52 |
53 | return returnPromise;
54 | };
55 | };
56 |
--------------------------------------------------------------------------------
/src/createNewGroup.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | return function createNewGroup(participantIDs, groupTitle, callback) {
8 | if (utils.getType(groupTitle) == "Function") {
9 | callback = groupTitle;
10 | groupTitle = null;
11 | }
12 |
13 | if (utils.getType(participantIDs) !== "Array") {
14 | throw { error: "createNewGroup: participantIDs should be an array." };
15 | }
16 |
17 | if (participantIDs.length < 2) {
18 | throw { error: "createNewGroup: participantIDs should have at least 2 IDs." };
19 | }
20 |
21 | let resolveFunc = function () { };
22 | let rejectFunc = function () { };
23 | const returnPromise = new Promise(function (resolve, reject) {
24 | resolveFunc = resolve;
25 | rejectFunc = reject;
26 | });
27 |
28 | if (!callback) {
29 | callback = function (err, threadID) {
30 | if (err) {
31 | return rejectFunc(err);
32 | }
33 | resolveFunc(threadID);
34 | };
35 | }
36 |
37 | const pids = [];
38 | for (const n in participantIDs) {
39 | pids.push({
40 | fbid: participantIDs[n]
41 | });
42 | }
43 | pids.push({ fbid: ctx.i_userID || ctx.userID });
44 |
45 | const form = {
46 | fb_api_caller_class: "RelayModern",
47 | fb_api_req_friendly_name: "MessengerGroupCreateMutation",
48 | av: ctx.i_userID || ctx.userID,
49 | //This doc_id is valid as of January 11th, 2020
50 | doc_id: "577041672419534",
51 | variables: JSON.stringify({
52 | input: {
53 | entry_point: "jewel_new_group",
54 | actor_id: ctx.i_userID || ctx.userID,
55 | participants: pids,
56 | client_mutation_id: Math.round(Math.random() * 1024).toString(),
57 | thread_settings: {
58 | name: groupTitle,
59 | joinable_mode: "PRIVATE",
60 | thread_image_fbid: null
61 | }
62 | }
63 | })
64 | };
65 |
66 | defaultFuncs
67 | .post(
68 | "https://www.facebook.com/api/graphql/",
69 | ctx.jar,
70 | form
71 | )
72 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
73 | .then(function (resData) {
74 | if (resData.errors) {
75 | throw resData;
76 | }
77 | return callback(null, resData.data.messenger_group_thread_create.thread.thread_key.thread_fbid);
78 | })
79 | .catch(function (err) {
80 | log.error("createNewGroup", err);
81 | return callback(err);
82 | });
83 |
84 | return returnPromise;
85 | };
86 | };
87 |
--------------------------------------------------------------------------------
/src/createPoll.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | return function createPoll(title, threadID, options, callback) {
8 | let resolveFunc = function () { };
9 | let rejectFunc = function () { };
10 | const returnPromise = new Promise(function (resolve, reject) {
11 | resolveFunc = resolve;
12 | rejectFunc = reject;
13 | });
14 |
15 | if (!callback) {
16 | if (utils.getType(options) == "Function") {
17 | callback = options;
18 | options = null;
19 | } else {
20 | callback = function (err) {
21 | if (err) {
22 | return rejectFunc(err);
23 | }
24 | resolveFunc();
25 | };
26 | }
27 | }
28 | if (!options) {
29 | options = {}; // Initial poll options are optional
30 | }
31 |
32 | const form = {
33 | target_id: threadID,
34 | question_text: title
35 | };
36 |
37 | // Set fields for options (and whether they are selected initially by the posting user)
38 | let ind = 0;
39 | for (const opt in options) {
40 | // eslint-disable-next-line no-prototype-builtins
41 | if (options.hasOwnProperty(opt)) {
42 | form["option_text_array[" + ind + "]"] = opt;
43 | form["option_is_selected_array[" + ind + "]"] = options[opt]
44 | ? "1"
45 | : "0";
46 | ind++;
47 | }
48 | }
49 |
50 | defaultFuncs
51 | .post(
52 | "https://www.facebook.com/messaging/group_polling/create_poll/?dpr=1",
53 | ctx.jar,
54 | form
55 | )
56 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
57 | .then(function (resData) {
58 | if (resData.payload.status != "success") {
59 | throw resData;
60 | }
61 |
62 | return callback();
63 | })
64 | .catch(function (err) {
65 | log.error("createPoll", err);
66 | return callback(err);
67 | });
68 |
69 | return returnPromise;
70 | };
71 | };
72 |
--------------------------------------------------------------------------------
/src/deleteMessage.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | return function deleteMessage(messageOrMessages, callback) {
8 | let resolveFunc = function () { };
9 | let rejectFunc = function () { };
10 | const returnPromise = new Promise(function (resolve, reject) {
11 | resolveFunc = resolve;
12 | rejectFunc = reject;
13 | });
14 | if (!callback) {
15 | callback = function (err) {
16 | if (err) {
17 | return rejectFunc(err);
18 | }
19 | resolveFunc();
20 | };
21 | }
22 |
23 | const form = {
24 | client: "mercury"
25 | };
26 |
27 | if (utils.getType(messageOrMessages) !== "Array") {
28 | messageOrMessages = [messageOrMessages];
29 | }
30 |
31 | for (let i = 0; i < messageOrMessages.length; i++) {
32 | form["message_ids[" + i + "]"] = messageOrMessages[i];
33 | }
34 |
35 | defaultFuncs
36 | .post(
37 | "https://www.facebook.com/ajax/mercury/delete_messages.php",
38 | ctx.jar,
39 | form
40 | )
41 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
42 | .then(function (resData) {
43 | if (resData.error) {
44 | throw resData;
45 | }
46 |
47 | return callback();
48 | })
49 | .catch(function (err) {
50 | log.error("deleteMessage", err);
51 | return callback(err);
52 | });
53 |
54 | return returnPromise;
55 | };
56 | };
57 |
--------------------------------------------------------------------------------
/src/deleteThread.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | return function deleteThread(threadOrThreads, callback) {
8 | let resolveFunc = function () { };
9 | let rejectFunc = function () { };
10 | const returnPromise = new Promise(function (resolve, reject) {
11 | resolveFunc = resolve;
12 | rejectFunc = reject;
13 | });
14 | if (!callback) {
15 | callback = function (err) {
16 | if (err) {
17 | return rejectFunc(err);
18 | }
19 | resolveFunc();
20 | };
21 | }
22 |
23 | const form = {
24 | client: "mercury"
25 | };
26 |
27 | if (utils.getType(threadOrThreads) !== "Array") {
28 | threadOrThreads = [threadOrThreads];
29 | }
30 |
31 | for (let i = 0; i < threadOrThreads.length; i++) {
32 | form["ids[" + i + "]"] = threadOrThreads[i];
33 | }
34 |
35 | defaultFuncs
36 | .post(
37 | "https://www.facebook.com/ajax/mercury/delete_thread.php",
38 | ctx.jar,
39 | form
40 | )
41 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
42 | .then(function (resData) {
43 | if (resData.error) {
44 | throw resData;
45 | }
46 |
47 | return callback();
48 | })
49 | .catch(function (err) {
50 | log.error("deleteThread", err);
51 | return callback(err);
52 | });
53 |
54 | return returnPromise;
55 | };
56 | };
57 |
--------------------------------------------------------------------------------
/src/forwardAttachment.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | return function forwardAttachment(attachmentID, userOrUsers, callback) {
8 | let resolveFunc = function () { };
9 | let rejectFunc = function () { };
10 | const returnPromise = new Promise(function (resolve, reject) {
11 | resolveFunc = resolve;
12 | rejectFunc = reject;
13 | });
14 | if (!callback) {
15 | callback = function (err) {
16 | if (err) {
17 | return rejectFunc(err);
18 | }
19 | resolveFunc();
20 | };
21 | }
22 |
23 | const form = {
24 | attachment_id: attachmentID
25 | };
26 |
27 | if (utils.getType(userOrUsers) !== "Array") {
28 | userOrUsers = [userOrUsers];
29 | }
30 |
31 | const timestamp = Math.floor(Date.now() / 1000);
32 |
33 | for (let i = 0; i < userOrUsers.length; i++) {
34 | //That's good, the key of the array is really timestmap in seconds + index
35 | //Probably time when the attachment will be sent?
36 | form["recipient_map[" + (timestamp + i) + "]"] = userOrUsers[i];
37 | }
38 |
39 | defaultFuncs
40 | .post(
41 | "https://www.facebook.com/mercury/attachments/forward/",
42 | ctx.jar,
43 | form
44 | )
45 | .then(utils.parseAndCheckLogin(ctx.jar, defaultFuncs))
46 | .then(function (resData) {
47 | if (resData.error) {
48 | throw resData;
49 | }
50 |
51 | return callback();
52 | })
53 | .catch(function (err) {
54 | log.error("forwardAttachment", err);
55 | return callback(err);
56 | });
57 |
58 | return returnPromise;
59 | };
60 | };
61 |
--------------------------------------------------------------------------------
/src/getCurrentUserID.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | module.exports = function (defaultFuncs, api, ctx) {
4 | return function getCurrentUserID() {
5 | return ctx.i_userID || ctx.userID;
6 | };
7 | };
8 |
--------------------------------------------------------------------------------
/src/getEmojiUrl.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const util = require("util");
4 |
5 | module.exports = function () {
6 | return function getEmojiUrl(c, size, pixelRatio) {
7 | /*
8 | Resolves Facebook Messenger emoji image asset URL for an emoji character.
9 | Supported sizes are 32, 64, and 128.
10 | Supported pixel ratios are '1.0' and '1.5' (possibly more; haven't tested)
11 | */
12 | const baseUrl = "https://static.xx.fbcdn.net/images/emoji.php/v8/z%s/%s";
13 | pixelRatio = pixelRatio || "1.0";
14 |
15 | const ending = util.format(
16 | "%s/%s/%s.png",
17 | pixelRatio,
18 | size,
19 | c.codePointAt(0).toString(16)
20 | );
21 | let base = 317426846;
22 | for (let i = 0; i < ending.length; i++) {
23 | base = (base << 5) - base + ending.charCodeAt(i);
24 | }
25 |
26 | const hashed = (base & 255).toString(16);
27 | return util.format(baseUrl, hashed, ending);
28 | };
29 | };
30 |
--------------------------------------------------------------------------------
/src/getFriendsList.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | // [almost] copy pasted from one of FB's minified file (GenderConst)
7 | const GENDERS = {
8 | 0: "unknown",
9 | 1: "female_singular",
10 | 2: "male_singular",
11 | 3: "female_singular_guess",
12 | 4: "male_singular_guess",
13 | 5: "mixed",
14 | 6: "neuter_singular",
15 | 7: "unknown_singular",
16 | 8: "female_plural",
17 | 9: "male_plural",
18 | 10: "neuter_plural",
19 | 11: "unknown_plural"
20 | };
21 |
22 | function formatData(obj) {
23 | return Object.keys(obj).map(function (key) {
24 | const user = obj[key];
25 | return {
26 | alternateName: user.alternateName,
27 | firstName: user.firstName,
28 | gender: GENDERS[user.gender],
29 | userID: utils.formatID(user.id.toString()),
30 | isFriend: user.is_friend != null && user.is_friend ? true : false,
31 | fullName: user.name,
32 | profilePicture: user.thumbSrc,
33 | type: user.type,
34 | profileUrl: user.uri,
35 | vanity: user.vanity,
36 | isBirthday: !!user.is_birthday
37 | };
38 | });
39 | }
40 |
41 | module.exports = function (defaultFuncs, api, ctx) {
42 | return function getFriendsList(callback) {
43 | let resolveFunc = function () { };
44 | let rejectFunc = function () { };
45 | const returnPromise = new Promise(function (resolve, reject) {
46 | resolveFunc = resolve;
47 | rejectFunc = reject;
48 | });
49 |
50 | if (!callback) {
51 | callback = function (err, friendList) {
52 | if (err) {
53 | return rejectFunc(err);
54 | }
55 | resolveFunc(friendList);
56 | };
57 | }
58 |
59 | defaultFuncs
60 | .postFormData(
61 | "https://www.facebook.com/chat/user_info_all",
62 | ctx.jar,
63 | {},
64 | { viewer: ctx.i_userID || ctx.userID }
65 | )
66 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
67 | .then(function (resData) {
68 | if (!resData) {
69 | throw { error: "getFriendsList returned empty object." };
70 | }
71 | if (resData.error) {
72 | throw resData;
73 | }
74 | callback(null, formatData(resData.payload));
75 | })
76 | .catch(function (err) {
77 | log.error("getFriendsList", err);
78 | return callback(err);
79 | });
80 |
81 | return returnPromise;
82 | };
83 | };
84 |
--------------------------------------------------------------------------------
/src/getThreadHistory.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 |
7 | function getExtension(original_extension, filename = "") {
8 | if (original_extension) {
9 | return original_extension;
10 | }
11 | else {
12 | const extension = filename.split(".").pop();
13 | if (extension === filename) {
14 | return "";
15 | }
16 | else {
17 | return extension;
18 | }
19 | }
20 | }
21 |
22 | function formatAttachmentsGraphQLResponse(attachment) {
23 | switch (attachment.__typename) {
24 | case "MessageImage":
25 | return {
26 | type: "photo",
27 | ID: attachment.legacy_attachment_id,
28 | filename: attachment.filename,
29 | original_extension: getExtension(attachment.original_extension, attachment.filename),
30 | thumbnailUrl: attachment.thumbnail.uri,
31 |
32 | previewUrl: attachment.preview.uri,
33 | previewWidth: attachment.preview.width,
34 | previewHeight: attachment.preview.height,
35 |
36 | largePreviewUrl: attachment.large_preview.uri,
37 | largePreviewHeight: attachment.large_preview.height,
38 | largePreviewWidth: attachment.large_preview.width,
39 |
40 | // You have to query for the real image. See below.
41 | url: attachment.large_preview.uri, // @Legacy
42 | width: attachment.large_preview.width, // @Legacy
43 | height: attachment.large_preview.height, // @Legacy
44 | name: attachment.filename, // @Legacy
45 |
46 | // @Undocumented
47 | attributionApp: attachment.attribution_app
48 | ? {
49 | attributionAppID: attachment.attribution_app.id,
50 | name: attachment.attribution_app.name,
51 | logo: attachment.attribution_app.square_logo
52 | }
53 | : null
54 |
55 | // @TODO No idea what this is, should we expose it?
56 | // Ben - July 15th 2017
57 | // renderAsSticker: attachment.render_as_sticker,
58 |
59 | // This is _not_ the real URI, this is still just a large preview.
60 | // To get the URL we'll need to support a POST query to
61 | //
62 | // https://www.facebook.com/webgraphql/query/
63 | //
64 | // With the following query params:
65 | //
66 | // query_id:728987990612546
67 | // variables:{"id":"100009069356507","photoID":"10213724771692996"}
68 | // dpr:1
69 | //
70 | // No special form though.
71 | };
72 | case "MessageAnimatedImage":
73 | return {
74 | type: "animated_image",
75 | ID: attachment.legacy_attachment_id,
76 | filename: attachment.filename,
77 | original_extension: getExtension(attachment.original_extension, attachment.filename),
78 |
79 | previewUrl: attachment.preview_image.uri,
80 | previewWidth: attachment.preview_image.width,
81 | previewHeight: attachment.preview_image.height,
82 |
83 | url: attachment.animated_image.uri,
84 | width: attachment.animated_image.width,
85 | height: attachment.animated_image.height,
86 |
87 | thumbnailUrl: attachment.preview_image.uri, // @Legacy
88 | name: attachment.filename, // @Legacy
89 | facebookUrl: attachment.animated_image.uri, // @Legacy
90 | rawGifImage: attachment.animated_image.uri, // @Legacy
91 | animatedGifUrl: attachment.animated_image.uri, // @Legacy
92 | animatedGifPreviewUrl: attachment.preview_image.uri, // @Legacy
93 | animatedWebpUrl: attachment.animated_image.uri, // @Legacy
94 | animatedWebpPreviewUrl: attachment.preview_image.uri, // @Legacy
95 |
96 | // @Undocumented
97 | attributionApp: attachment.attribution_app
98 | ? {
99 | attributionAppID: attachment.attribution_app.id,
100 | name: attachment.attribution_app.name,
101 | logo: attachment.attribution_app.square_logo
102 | }
103 | : null
104 | };
105 | case "MessageVideo":
106 | return {
107 | type: "video",
108 | ID: attachment.legacy_attachment_id,
109 | filename: attachment.filename,
110 | original_extension: getExtension(attachment.original_extension, attachment.filename),
111 | duration: attachment.playable_duration_in_ms,
112 |
113 | thumbnailUrl: attachment.large_image.uri, // @Legacy
114 |
115 | previewUrl: attachment.large_image.uri,
116 | previewWidth: attachment.large_image.width,
117 | previewHeight: attachment.large_image.height,
118 |
119 | url: attachment.playable_url,
120 | width: attachment.original_dimensions.x,
121 | height: attachment.original_dimensions.y,
122 |
123 | videoType: attachment.video_type.toLowerCase()
124 | };
125 | case "MessageFile":
126 | return {
127 | type: "file",
128 | ID: attachment.message_file_fbid,
129 | filename: attachment.filename,
130 | original_extension: getExtension(attachment.original_extension, attachment.filename),
131 |
132 | url: attachment.url,
133 | isMalicious: attachment.is_malicious,
134 | contentType: attachment.content_type,
135 |
136 | name: attachment.filename, // @Legacy
137 | mimeType: "", // @Legacy
138 | fileSize: -1 // @Legacy
139 | };
140 | case "MessageAudio":
141 | return {
142 | type: "audio",
143 | ID: attachment.url_shimhash, // Not fowardable
144 | filename: attachment.filename,
145 | original_extension: getExtension(attachment.original_extension, attachment.filename),
146 |
147 | duration: attachment.playable_duration_in_ms,
148 | audioType: attachment.audio_type,
149 | url: attachment.playable_url,
150 |
151 | isVoiceMail: attachment.is_voicemail
152 | };
153 | default:
154 | return {
155 | error: "Don't know about attachment type " + attachment.__typename
156 | };
157 | }
158 | }
159 |
160 | function formatExtensibleAttachment(attachment) {
161 | if (attachment.story_attachment) {
162 | return {
163 | type: "share",
164 | ID: attachment.legacy_attachment_id,
165 | url: attachment.story_attachment.url,
166 |
167 | title: attachment.story_attachment.title_with_entities.text,
168 | description:
169 | attachment.story_attachment.description &&
170 | attachment.story_attachment.description.text,
171 | source:
172 | attachment.story_attachment.source == null
173 | ? null
174 | : attachment.story_attachment.source.text,
175 |
176 | image:
177 | attachment.story_attachment.media == null
178 | ? null
179 | : attachment.story_attachment.media.animated_image == null &&
180 | attachment.story_attachment.media.image == null
181 | ? null
182 | : (
183 | attachment.story_attachment.media.animated_image ||
184 | attachment.story_attachment.media.image
185 | ).uri,
186 | width:
187 | attachment.story_attachment.media == null
188 | ? null
189 | : attachment.story_attachment.media.animated_image == null &&
190 | attachment.story_attachment.media.image == null
191 | ? null
192 | : (
193 | attachment.story_attachment.media.animated_image ||
194 | attachment.story_attachment.media.image
195 | ).width,
196 | height:
197 | attachment.story_attachment.media == null
198 | ? null
199 | : attachment.story_attachment.media.animated_image == null &&
200 | attachment.story_attachment.media.image == null
201 | ? null
202 | : (
203 | attachment.story_attachment.media.animated_image ||
204 | attachment.story_attachment.media.image
205 | ).height,
206 | playable:
207 | attachment.story_attachment.media == null
208 | ? null
209 | : attachment.story_attachment.media.is_playable,
210 | duration:
211 | attachment.story_attachment.media == null
212 | ? null
213 | : attachment.story_attachment.media.playable_duration_in_ms,
214 | playableUrl:
215 | attachment.story_attachment.media == null
216 | ? null
217 | : attachment.story_attachment.media.playable_url,
218 |
219 | subattachments: attachment.story_attachment.subattachments,
220 |
221 | // Format example:
222 | //
223 | // [{
224 | // key: "width",
225 | // value: { text: "1280" }
226 | // }]
227 | //
228 | // That we turn into:
229 | //
230 | // {
231 | // width: "1280"
232 | // }
233 | //
234 | properties: attachment.story_attachment.properties.reduce(function (
235 | obj,
236 | cur
237 | ) {
238 | obj[cur.key] = cur.value.text;
239 | return obj;
240 | },
241 | {}),
242 |
243 | // Deprecated fields
244 | animatedImageSize: "", // @Legacy
245 | facebookUrl: "", // @Legacy
246 | styleList: "", // @Legacy
247 | target: "", // @Legacy
248 | thumbnailUrl:
249 | attachment.story_attachment.media == null
250 | ? null
251 | : attachment.story_attachment.media.animated_image == null &&
252 | attachment.story_attachment.media.image == null
253 | ? null
254 | : (
255 | attachment.story_attachment.media.animated_image ||
256 | attachment.story_attachment.media.image
257 | ).uri, // @Legacy
258 | thumbnailWidth:
259 | attachment.story_attachment.media == null
260 | ? null
261 | : attachment.story_attachment.media.animated_image == null &&
262 | attachment.story_attachment.media.image == null
263 | ? null
264 | : (
265 | attachment.story_attachment.media.animated_image ||
266 | attachment.story_attachment.media.image
267 | ).width, // @Legacy
268 | thumbnailHeight:
269 | attachment.story_attachment.media == null
270 | ? null
271 | : attachment.story_attachment.media.animated_image == null &&
272 | attachment.story_attachment.media.image == null
273 | ? null
274 | : (
275 | attachment.story_attachment.media.animated_image ||
276 | attachment.story_attachment.media.image
277 | ).height // @Legacy
278 | };
279 | } else {
280 | return { error: "Don't know what to do with extensible_attachment." };
281 | }
282 | }
283 |
284 | function formatReactionsGraphQL(reaction) {
285 | return {
286 | reaction: reaction.reaction,
287 | userID: reaction.user.id
288 | };
289 | }
290 |
291 | function formatEventData(event) {
292 | if (event == null) {
293 | return {};
294 | }
295 |
296 | switch (event.__typename) {
297 | case "ThemeColorExtensibleMessageAdminText":
298 | return {
299 | color: event.theme_color
300 | };
301 | case "ThreadNicknameExtensibleMessageAdminText":
302 | return {
303 | nickname: event.nickname,
304 | participantID: event.participant_id
305 | };
306 | case "ThreadIconExtensibleMessageAdminText":
307 | return {
308 | threadIcon: event.thread_icon
309 | };
310 | case "InstantGameUpdateExtensibleMessageAdminText":
311 | return {
312 | gameID: (event.game == null ? null : event.game.id),
313 | update_type: event.update_type,
314 | collapsed_text: event.collapsed_text,
315 | expanded_text: event.expanded_text,
316 | instant_game_update_data: event.instant_game_update_data
317 | };
318 | case "GameScoreExtensibleMessageAdminText":
319 | return {
320 | game_type: event.game_type
321 | };
322 | case "RtcCallLogExtensibleMessageAdminText":
323 | return {
324 | event: event.event,
325 | is_video_call: event.is_video_call,
326 | server_info_data: event.server_info_data
327 | };
328 | case "GroupPollExtensibleMessageAdminText":
329 | return {
330 | event_type: event.event_type,
331 | total_count: event.total_count,
332 | question: event.question
333 | };
334 | case "AcceptPendingThreadExtensibleMessageAdminText":
335 | return {
336 | accepter_id: event.accepter_id,
337 | requester_id: event.requester_id
338 | };
339 | case "ConfirmFriendRequestExtensibleMessageAdminText":
340 | return {
341 | friend_request_recipient: event.friend_request_recipient,
342 | friend_request_sender: event.friend_request_sender
343 | };
344 | case "AddContactExtensibleMessageAdminText":
345 | return {
346 | contact_added_id: event.contact_added_id,
347 | contact_adder_id: event.contact_adder_id
348 | };
349 | case "AdExtensibleMessageAdminText":
350 | return {
351 | ad_client_token: event.ad_client_token,
352 | ad_id: event.ad_id,
353 | ad_preferences_link: event.ad_preferences_link,
354 | ad_properties: event.ad_properties
355 | };
356 | // never data
357 | case "ParticipantJoinedGroupCallExtensibleMessageAdminText":
358 | case "ThreadEphemeralTtlModeExtensibleMessageAdminText":
359 | case "StartedSharingVideoExtensibleMessageAdminText":
360 | case "LightweightEventCreateExtensibleMessageAdminText":
361 | case "LightweightEventNotifyExtensibleMessageAdminText":
362 | case "LightweightEventNotifyBeforeEventExtensibleMessageAdminText":
363 | case "LightweightEventUpdateTitleExtensibleMessageAdminText":
364 | case "LightweightEventUpdateTimeExtensibleMessageAdminText":
365 | case "LightweightEventUpdateLocationExtensibleMessageAdminText":
366 | case "LightweightEventDeleteExtensibleMessageAdminText":
367 | return {};
368 | default:
369 | return {
370 | error: "Don't know what to with event data type " + event.__typename
371 | };
372 | }
373 | }
374 |
375 | function formatMessagesGraphQLResponse(data) {
376 | const messageThread = data.o0.data.message_thread;
377 | const threadID = messageThread.thread_key.thread_fbid
378 | ? messageThread.thread_key.thread_fbid
379 | : messageThread.thread_key.other_user_id;
380 |
381 | const messages = messageThread.messages.nodes.map(function (d) {
382 | switch (d.__typename) {
383 | case "UserMessage":
384 | // Give priority to stickers. They're seen as normal messages but we've
385 | // been considering them as attachments.
386 | var maybeStickerAttachment;
387 | if (d.sticker) {
388 | maybeStickerAttachment = [
389 | {
390 | type: "sticker",
391 | ID: d.sticker.id,
392 | url: d.sticker.url,
393 |
394 | packID: d.sticker.pack ? d.sticker.pack.id : null,
395 | spriteUrl: d.sticker.sprite_image,
396 | spriteUrl2x: d.sticker.sprite_image_2x,
397 | width: d.sticker.width,
398 | height: d.sticker.height,
399 |
400 | caption: d.snippet, // Not sure what the heck caption was.
401 | description: d.sticker.label, // Not sure about this one either.
402 |
403 | frameCount: d.sticker.frame_count,
404 | frameRate: d.sticker.frame_rate,
405 | framesPerRow: d.sticker.frames_per_row,
406 | framesPerCol: d.sticker.frames_per_col,
407 |
408 | stickerID: d.sticker.id, // @Legacy
409 | spriteURI: d.sticker.sprite_image, // @Legacy
410 | spriteURI2x: d.sticker.sprite_image_2x // @Legacy
411 | }
412 | ];
413 | }
414 |
415 | var mentionsObj = {};
416 | if (d.message !== null) {
417 | d.message.ranges.forEach(e => {
418 | mentionsObj[e.entity.id] = d.message.text.substr(e.offset, e.length);
419 | });
420 | }
421 |
422 | return {
423 | type: "message",
424 | attachments: maybeStickerAttachment
425 | ? maybeStickerAttachment
426 | : d.blob_attachments && d.blob_attachments.length > 0
427 | ? d.blob_attachments.map(formatAttachmentsGraphQLResponse)
428 | : d.extensible_attachment
429 | ? [formatExtensibleAttachment(d.extensible_attachment)]
430 | : [],
431 | body: d.message !== null ? d.message.text : '',
432 | isGroup: messageThread.thread_type === "GROUP",
433 | messageID: d.message_id,
434 | senderID: d.message_sender.id,
435 | threadID: threadID,
436 | timestamp: d.timestamp_precise,
437 |
438 | mentions: mentionsObj,
439 | isUnread: d.unread,
440 |
441 | // New
442 | messageReactions: d.message_reactions
443 | ? d.message_reactions.map(formatReactionsGraphQL)
444 | : null,
445 | isSponsored: d.is_sponsored,
446 | snippet: d.snippet
447 | };
448 | case "ThreadNameMessage":
449 | return {
450 | type: "event",
451 | messageID: d.message_id,
452 | threadID: threadID,
453 | isGroup: messageThread.thread_type === "GROUP",
454 | senderID: d.message_sender.id,
455 | timestamp: d.timestamp_precise,
456 | eventType: "change_thread_name",
457 | snippet: d.snippet,
458 | eventData: {
459 | threadName: d.thread_name
460 | },
461 |
462 | // @Legacy
463 | author: d.message_sender.id,
464 | logMessageType: "log:thread-name",
465 | logMessageData: { name: d.thread_name }
466 | };
467 | case "ThreadImageMessage":
468 | return {
469 | type: "event",
470 | messageID: d.message_id,
471 | threadID: threadID,
472 | isGroup: messageThread.thread_type === "GROUP",
473 | senderID: d.message_sender.id,
474 | timestamp: d.timestamp_precise,
475 | eventType: "change_thread_image",
476 | snippet: d.snippet,
477 | eventData:
478 | d.image_with_metadata == null
479 | ? {} /* removed image */
480 | : {
481 | /* image added */
482 | threadImage: {
483 | attachmentID: d.image_with_metadata.legacy_attachment_id,
484 | width: d.image_with_metadata.original_dimensions.x,
485 | height: d.image_with_metadata.original_dimensions.y,
486 | url: d.image_with_metadata.preview.uri
487 | }
488 | },
489 |
490 | // @Legacy
491 | logMessageType: "log:thread-icon",
492 | logMessageData: {
493 | thread_icon: d.image_with_metadata
494 | ? d.image_with_metadata.preview.uri
495 | : null
496 | }
497 | };
498 | case "ParticipantLeftMessage":
499 | return {
500 | type: "event",
501 | messageID: d.message_id,
502 | threadID: threadID,
503 | isGroup: messageThread.thread_type === "GROUP",
504 | senderID: d.message_sender.id,
505 | timestamp: d.timestamp_precise,
506 | eventType: "remove_participants",
507 | snippet: d.snippet,
508 | eventData: {
509 | // Array of IDs.
510 | participantsRemoved: d.participants_removed.map(function (p) {
511 | return p.id;
512 | })
513 | },
514 |
515 | // @Legacy
516 | logMessageType: "log:unsubscribe",
517 | logMessageData: {
518 | leftParticipantFbId: d.participants_removed.map(function (p) {
519 | return p.id;
520 | })
521 | }
522 | };
523 | case "ParticipantsAddedMessage":
524 | return {
525 | type: "event",
526 | messageID: d.message_id,
527 | threadID: threadID,
528 | isGroup: messageThread.thread_type === "GROUP",
529 | senderID: d.message_sender.id,
530 | timestamp: d.timestamp_precise,
531 | eventType: "add_participants",
532 | snippet: d.snippet,
533 | eventData: {
534 | // Array of IDs.
535 | participantsAdded: d.participants_added.map(function (p) {
536 | return p.id;
537 | })
538 | },
539 |
540 | // @Legacy
541 | logMessageType: "log:subscribe",
542 | logMessageData: {
543 | addedParticipants: d.participants_added.map(function (p) {
544 | return p.id;
545 | })
546 | }
547 | };
548 | case "VideoCallMessage":
549 | return {
550 | type: "event",
551 | messageID: d.message_id,
552 | threadID: threadID,
553 | isGroup: messageThread.thread_type === "GROUP",
554 | senderID: d.message_sender.id,
555 | timestamp: d.timestamp_precise,
556 | eventType: "video_call",
557 | snippet: d.snippet,
558 |
559 | // @Legacy
560 | logMessageType: "other"
561 | };
562 | case "VoiceCallMessage":
563 | return {
564 | type: "event",
565 | messageID: d.message_id,
566 | threadID: threadID,
567 | isGroup: messageThread.thread_type === "GROUP",
568 | senderID: d.message_sender.id,
569 | timestamp: d.timestamp_precise,
570 | eventType: "voice_call",
571 | snippet: d.snippet,
572 |
573 | // @Legacy
574 | logMessageType: "other"
575 | };
576 | case "GenericAdminTextMessage":
577 | return {
578 | type: "event",
579 | messageID: d.message_id,
580 | threadID: threadID,
581 | isGroup: messageThread.thread_type === "GROUP",
582 | senderID: d.message_sender.id,
583 | timestamp: d.timestamp_precise,
584 | snippet: d.snippet,
585 | eventType: d.extensible_message_admin_text_type.toLowerCase(),
586 | eventData: formatEventData(d.extensible_message_admin_text),
587 |
588 | // @Legacy
589 | logMessageType: utils.getAdminTextMessageType(
590 | d.extensible_message_admin_text_type
591 | ),
592 | logMessageData: d.extensible_message_admin_text // Maybe different?
593 | };
594 | default:
595 | return { error: "Don't know about message type " + d.__typename };
596 | }
597 | });
598 | return messages;
599 | }
600 |
601 | module.exports = function (defaultFuncs, api, ctx) {
602 | return function getThreadHistoryGraphQL(
603 | threadID,
604 | amount,
605 | timestamp,
606 | callback
607 | ) {
608 | let resolveFunc = function () { };
609 | let rejectFunc = function () { };
610 | const returnPromise = new Promise(function (resolve, reject) {
611 | resolveFunc = resolve;
612 | rejectFunc = reject;
613 | });
614 |
615 | if (!callback) {
616 | callback = function (err, data) {
617 | if (err) {
618 | return rejectFunc(err);
619 | }
620 | resolveFunc(data);
621 | };
622 | }
623 |
624 | // `queries` has to be a string. I couldn't tell from the dev console. This
625 | // took me a really long time to figure out. I deserve a cookie for this.
626 | const form = {
627 | "av": ctx.globalOptions.pageID,
628 | queries: JSON.stringify({
629 | o0: {
630 | // This doc_id was valid on February 2nd 2017.
631 | doc_id: "1498317363570230",
632 | query_params: {
633 | id: threadID,
634 | message_limit: amount,
635 | load_messages: 1,
636 | load_read_receipts: false,
637 | before: timestamp
638 | }
639 | }
640 | })
641 | };
642 |
643 | defaultFuncs
644 | .post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
645 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
646 | .then(function (resData) {
647 | if (resData.error) {
648 | throw resData;
649 | }
650 | // This returns us an array of things. The last one is the success /
651 | // failure one.
652 | // @TODO What do we do in this case?
653 | if (resData[resData.length - 1].error_results !== 0) {
654 | throw new Error("There was an error_result.");
655 | }
656 |
657 | callback(null, formatMessagesGraphQLResponse(resData[0]));
658 | })
659 | .catch(function (err) {
660 | log.error("getThreadHistoryGraphQL", err);
661 | return callback(err);
662 | });
663 |
664 | return returnPromise;
665 | };
666 | };
667 |
--------------------------------------------------------------------------------
/src/getThreadInfo.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | function formatEventReminders(reminder) {
7 | return {
8 | reminderID: reminder.id,
9 | eventCreatorID: reminder.lightweight_event_creator.id,
10 | time: reminder.time,
11 | eventType: reminder.lightweight_event_type.toLowerCase(),
12 | locationName: reminder.location_name,
13 | // @TODO verify this
14 | locationCoordinates: reminder.location_coordinates,
15 | locationPage: reminder.location_page,
16 | eventStatus: reminder.lightweight_event_status.toLowerCase(),
17 | note: reminder.note,
18 | repeatMode: reminder.repeat_mode.toLowerCase(),
19 | eventTitle: reminder.event_title,
20 | triggerMessage: reminder.trigger_message,
21 | secondsToNotifyBefore: reminder.seconds_to_notify_before,
22 | allowsRsvp: reminder.allows_rsvp,
23 | relatedEvent: reminder.related_event,
24 | members: reminder.event_reminder_members.edges.map(function (member) {
25 | return {
26 | memberID: member.node.id,
27 | state: member.guest_list_state.toLowerCase()
28 | };
29 | })
30 | };
31 | }
32 |
33 | function formatThreadGraphQLResponse(data) {
34 | if (data.errors)
35 | return data.errors;
36 | const messageThread = data.message_thread;
37 | if (!messageThread)
38 | return null;
39 | const threadID = messageThread.thread_key.thread_fbid
40 | ? messageThread.thread_key.thread_fbid
41 | : messageThread.thread_key.other_user_id;
42 |
43 | // Remove me
44 | const lastM = messageThread.last_message;
45 | const snippetID =
46 | lastM &&
47 | lastM.nodes &&
48 | lastM.nodes[0] &&
49 | lastM.nodes[0].message_sender &&
50 | lastM.nodes[0].message_sender.messaging_actor
51 | ? lastM.nodes[0].message_sender.messaging_actor.id
52 | : null;
53 | const snippetText =
54 | lastM && lastM.nodes && lastM.nodes[0] ? lastM.nodes[0].snippet : null;
55 | const lastR = messageThread.last_read_receipt;
56 | const lastReadTimestamp =
57 | lastR && lastR.nodes && lastR.nodes[0] && lastR.nodes[0].timestamp_precise
58 | ? lastR.nodes[0].timestamp_precise
59 | : null;
60 |
61 | return {
62 | threadID: threadID,
63 | threadName: messageThread.name,
64 | participantIDs: messageThread.all_participants.edges.map(d => d.node.messaging_actor.id),
65 | userInfo: messageThread.all_participants.edges.map(d => ({
66 | id: d.node.messaging_actor.id,
67 | name: d.node.messaging_actor.name,
68 | firstName: d.node.messaging_actor.short_name,
69 | vanity: d.node.messaging_actor.username,
70 | url: d.node.messaging_actor.url,
71 | thumbSrc: d.node.messaging_actor.big_image_src.uri,
72 | profileUrl: d.node.messaging_actor.big_image_src.uri,
73 | gender: d.node.messaging_actor.gender,
74 | type: d.node.messaging_actor.__typename,
75 | isFriend: d.node.messaging_actor.is_viewer_friend,
76 | isBirthday: !!d.node.messaging_actor.is_birthday //not sure?
77 | })),
78 | unreadCount: messageThread.unread_count,
79 | messageCount: messageThread.messages_count,
80 | timestamp: messageThread.updated_time_precise,
81 | muteUntil: messageThread.mute_until,
82 | isGroup: messageThread.thread_type == "GROUP",
83 | isSubscribed: messageThread.is_viewer_subscribed,
84 | isArchived: messageThread.has_viewer_archived,
85 | folder: messageThread.folder,
86 | cannotReplyReason: messageThread.cannot_reply_reason,
87 | eventReminders: messageThread.event_reminders
88 | ? messageThread.event_reminders.nodes.map(formatEventReminders)
89 | : null,
90 | emoji: messageThread.customization_info
91 | ? messageThread.customization_info.emoji
92 | : null,
93 | color:
94 | messageThread.customization_info &&
95 | messageThread.customization_info.outgoing_bubble_color
96 | ? messageThread.customization_info.outgoing_bubble_color.slice(2)
97 | : null,
98 | threadTheme: messageThread.thread_theme,
99 | nicknames:
100 | messageThread.customization_info &&
101 | messageThread.customization_info.participant_customizations
102 | ? messageThread.customization_info.participant_customizations.reduce(
103 | function (res, val) {
104 | if (val.nickname) res[val.participant_id] = val.nickname;
105 | return res;
106 | },
107 | {}
108 | )
109 | : {},
110 | adminIDs: messageThread.thread_admins,
111 | approvalMode: Boolean(messageThread.approval_mode),
112 | approvalQueue: messageThread.group_approval_queue.nodes.map(a => ({
113 | inviterID: a.inviter.id,
114 | requesterID: a.requester.id,
115 | timestamp: a.request_timestamp,
116 | request_source: a.request_source // @Undocumented
117 | })),
118 |
119 | // @Undocumented
120 | reactionsMuteMode: messageThread.reactions_mute_mode.toLowerCase(),
121 | mentionsMuteMode: messageThread.mentions_mute_mode.toLowerCase(),
122 | isPinProtected: messageThread.is_pin_protected,
123 | relatedPageThread: messageThread.related_page_thread,
124 |
125 | // @Legacy
126 | name: messageThread.name,
127 | snippet: snippetText,
128 | snippetSender: snippetID,
129 | snippetAttachments: [],
130 | serverTimestamp: messageThread.updated_time_precise,
131 | imageSrc: messageThread.image ? messageThread.image.uri : null,
132 | isCanonicalUser: messageThread.is_canonical_neo_user,
133 | isCanonical: messageThread.thread_type != "GROUP",
134 | recipientsLoadable: true,
135 | hasEmailParticipant: false,
136 | readOnly: false,
137 | canReply: messageThread.cannot_reply_reason == null,
138 | lastMessageTimestamp: messageThread.last_message
139 | ? messageThread.last_message.timestamp_precise
140 | : null,
141 | lastMessageType: "message",
142 | lastReadTimestamp: lastReadTimestamp,
143 | threadType: messageThread.thread_type == "GROUP" ? 2 : 1,
144 |
145 | // update in Wed, 13 Jul 2022 19:41:12 +0700
146 | inviteLink: {
147 | enable: messageThread.joinable_mode ? messageThread.joinable_mode.mode == 1 : false,
148 | link: messageThread.joinable_mode ? messageThread.joinable_mode.link : null
149 | }
150 | };
151 | }
152 |
153 | module.exports = function (defaultFuncs, api, ctx) {
154 | return function getThreadInfoGraphQL(threadID, callback) {
155 | let resolveFunc = function () { };
156 | let rejectFunc = function () { };
157 | const returnPromise = new Promise(function (resolve, reject) {
158 | resolveFunc = resolve;
159 | rejectFunc = reject;
160 | });
161 |
162 | if (utils.getType(callback) != "Function" && utils.getType(callback) != "AsyncFunction") {
163 | callback = function (err, data) {
164 | if (err) {
165 | return rejectFunc(err);
166 | }
167 | resolveFunc(data);
168 | };
169 | }
170 |
171 | if (utils.getType(threadID) !== "Array") {
172 | threadID = [threadID];
173 | }
174 |
175 | let form = {};
176 | // `queries` has to be a string. I couldn't tell from the dev console. This
177 | // took me a really long time to figure out. I deserve a cookie for this.
178 | threadID.map(function (t, i) {
179 | form["o" + i] = {
180 | doc_id: "3449967031715030",
181 | query_params: {
182 | id: t,
183 | message_limit: 0,
184 | load_messages: false,
185 | load_read_receipts: false,
186 | before: null
187 | }
188 | };
189 | });
190 |
191 | form = {
192 | queries: JSON.stringify(form),
193 | batch_name: "MessengerGraphQLThreadFetcher"
194 | };
195 |
196 | defaultFuncs
197 | .post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
198 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
199 | .then(function (resData) {
200 |
201 | if (resData.error) {
202 | throw resData;
203 | }
204 | // This returns us an array of things. The last one is the success /
205 | // failure one.
206 | // @TODO What do we do in this case?
207 | // if (resData[resData.length - 1].error_results !== 0) {
208 | // throw resData[0].o0.errors[0];
209 | // }
210 | // if (!resData[0].o0.data.message_thread) {
211 | // throw new Error("can't find this thread");
212 | // }
213 | const threadInfos = {};
214 | for (let i = resData.length - 2; i >= 0; i--) {
215 | const threadInfo = formatThreadGraphQLResponse(resData[i][Object.keys(resData[i])[0]].data);
216 | threadInfos[threadInfo?.threadID || threadID[threadID.length - 1 - i]] = threadInfo;
217 | }
218 | if (Object.values(threadInfos).length == 1) {
219 | callback(null, Object.values(threadInfos)[0]);
220 | }
221 | else {
222 | callback(null, threadInfos);
223 | }
224 | })
225 | .catch(function (err) {
226 | log.error("getThreadInfoGraphQL", err);
227 | return callback(err);
228 | });
229 |
230 | return returnPromise;
231 | };
232 | };
233 |
--------------------------------------------------------------------------------
/src/getThreadList.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | function formatEventReminders(reminder) {
7 | return {
8 | reminderID: reminder.id,
9 | eventCreatorID: reminder.lightweight_event_creator.id,
10 | time: reminder.time,
11 | eventType: reminder.lightweight_event_type.toLowerCase(),
12 | locationName: reminder.location_name,
13 | // @TODO verify this
14 | locationCoordinates: reminder.location_coordinates,
15 | locationPage: reminder.location_page,
16 | eventStatus: reminder.lightweight_event_status.toLowerCase(),
17 | note: reminder.note,
18 | repeatMode: reminder.repeat_mode.toLowerCase(),
19 | eventTitle: reminder.event_title,
20 | triggerMessage: reminder.trigger_message,
21 | secondsToNotifyBefore: reminder.seconds_to_notify_before,
22 | allowsRsvp: reminder.allows_rsvp,
23 | relatedEvent: reminder.related_event,
24 | members: reminder.event_reminder_members.edges.map(function (member) {
25 | return {
26 | memberID: member.node.id,
27 | state: member.guest_list_state.toLowerCase()
28 | };
29 | })
30 | };
31 | }
32 |
33 | function formatThreadGraphQLResponse(messageThread) {
34 | const threadID = messageThread.thread_key.thread_fbid
35 | ? messageThread.thread_key.thread_fbid
36 | : messageThread.thread_key.other_user_id;
37 |
38 | // Remove me
39 | const lastM = messageThread.last_message;
40 | const snippetID =
41 | lastM &&
42 | lastM.nodes &&
43 | lastM.nodes[0] &&
44 | lastM.nodes[0].message_sender &&
45 | lastM.nodes[0].message_sender.messaging_actor
46 | ? lastM.nodes[0].message_sender.messaging_actor.id
47 | : null;
48 | const snippetText =
49 | lastM && lastM.nodes && lastM.nodes[0] ? lastM.nodes[0].snippet : null;
50 | const lastR = messageThread.last_read_receipt;
51 | const lastReadTimestamp =
52 | lastR && lastR.nodes && lastR.nodes[0] && lastR.nodes[0].timestamp_precise
53 | ? lastR.nodes[0].timestamp_precise
54 | : null;
55 |
56 | return {
57 | threadID: threadID,
58 | threadName: messageThread.name,
59 | participantIDs: messageThread.all_participants.edges.map(d => d.node.messaging_actor.id),
60 | userInfo: messageThread.all_participants.edges.map(d => ({
61 | id: d.node.messaging_actor.id,
62 | name: d.node.messaging_actor.name,
63 | firstName: d.node.messaging_actor.short_name,
64 | vanity: d.node.messaging_actor.username,
65 | url: d.node.messaging_actor.url,
66 | thumbSrc: d.node.messaging_actor.big_image_src.uri,
67 | profileUrl: d.node.messaging_actor.big_image_src.uri,
68 | gender: d.node.messaging_actor.gender,
69 | type: d.node.messaging_actor.__typename,
70 | isFriend: d.node.messaging_actor.is_viewer_friend,
71 | isBirthday: !!d.node.messaging_actor.is_birthday //not sure?
72 | })),
73 | unreadCount: messageThread.unread_count,
74 | messageCount: messageThread.messages_count,
75 | timestamp: messageThread.updated_time_precise,
76 | muteUntil: messageThread.mute_until,
77 | isGroup: messageThread.thread_type == "GROUP",
78 | isSubscribed: messageThread.is_viewer_subscribed,
79 | isArchived: messageThread.has_viewer_archived,
80 | folder: messageThread.folder,
81 | cannotReplyReason: messageThread.cannot_reply_reason,
82 | eventReminders: messageThread.event_reminders
83 | ? messageThread.event_reminders.nodes.map(formatEventReminders)
84 | : null,
85 | emoji: messageThread.customization_info
86 | ? messageThread.customization_info.emoji
87 | : null,
88 | color:
89 | messageThread.customization_info &&
90 | messageThread.customization_info.outgoing_bubble_color
91 | ? messageThread.customization_info.outgoing_bubble_color.slice(2)
92 | : null,
93 | threadTheme: messageThread.thread_theme,
94 | nicknames:
95 | messageThread.customization_info &&
96 | messageThread.customization_info.participant_customizations
97 | ? messageThread.customization_info.participant_customizations.reduce(
98 | function (res, val) {
99 | if (val.nickname) res[val.participant_id] = val.nickname;
100 | return res;
101 | },
102 | {}
103 | )
104 | : {},
105 | adminIDs: messageThread.thread_admins,
106 | approvalMode: Boolean(messageThread.approval_mode),
107 | approvalQueue: messageThread.group_approval_queue.nodes.map(a => ({
108 | inviterID: a.inviter.id,
109 | requesterID: a.requester.id,
110 | timestamp: a.request_timestamp,
111 | request_source: a.request_source // @Undocumented
112 | })),
113 |
114 | // @Undocumented
115 | reactionsMuteMode: messageThread.reactions_mute_mode.toLowerCase(),
116 | mentionsMuteMode: messageThread.mentions_mute_mode.toLowerCase(),
117 | isPinProtected: messageThread.is_pin_protected,
118 | relatedPageThread: messageThread.related_page_thread,
119 |
120 | // @Legacy
121 | name: messageThread.name,
122 | snippet: snippetText,
123 | snippetSender: snippetID,
124 | snippetAttachments: [],
125 | serverTimestamp: messageThread.updated_time_precise,
126 | imageSrc: messageThread.image ? messageThread.image.uri : null,
127 | isCanonicalUser: messageThread.is_canonical_neo_user,
128 | isCanonical: messageThread.thread_type != "GROUP",
129 | recipientsLoadable: true,
130 | hasEmailParticipant: false,
131 | readOnly: false,
132 | canReply: messageThread.cannot_reply_reason == null,
133 | lastMessageTimestamp: messageThread.last_message
134 | ? messageThread.last_message.timestamp_precise
135 | : null,
136 | lastMessageType: "message",
137 | lastReadTimestamp: lastReadTimestamp,
138 | threadType: messageThread.thread_type == "GROUP" ? 2 : 1,
139 |
140 | // update in Wed, 13 Jul 2022 19:41:12 +0700
141 | inviteLink: {
142 | enable: messageThread.joinable_mode ? messageThread.joinable_mode.mode == 1 : false,
143 | link: messageThread.joinable_mode ? messageThread.joinable_mode.link : null
144 | }
145 | };
146 | }
147 |
148 | function formatThreadList(data) {
149 | // console.log(JSON.stringify(data.find(t => t.thread_key.thread_fbid === "5095817367161431"), null, 2));
150 | return data.map(t => formatThreadGraphQLResponse(t));
151 | }
152 |
153 | module.exports = function (defaultFuncs, api, ctx) {
154 | return function getThreadList(limit, timestamp, tags, callback) {
155 | if (!callback && (utils.getType(tags) === "Function" || utils.getType(tags) === "AsyncFunction")) {
156 | callback = tags;
157 | tags = [""];
158 | }
159 | if (utils.getType(limit) !== "Number" || !Number.isInteger(limit) || limit <= 0) {
160 | throw new utils.CustomError({ error: "getThreadList: limit must be a positive integer" });
161 | }
162 | if (utils.getType(timestamp) !== "Null" &&
163 | (utils.getType(timestamp) !== "Number" || !Number.isInteger(timestamp))) {
164 | throw new utils.CustomError({ error: "getThreadList: timestamp must be an integer or null" });
165 | }
166 | if (utils.getType(tags) === "String") {
167 | tags = [tags];
168 | }
169 | if (utils.getType(tags) !== "Array") {
170 | throw new utils.CustomError({
171 | error: "getThreadList: tags must be an array",
172 | message: "getThreadList: tags must be an array"
173 | });
174 | }
175 |
176 | let resolveFunc = function () { };
177 | let rejectFunc = function () { };
178 | const returnPromise = new Promise(function (resolve, reject) {
179 | resolveFunc = resolve;
180 | rejectFunc = reject;
181 | });
182 |
183 | if (utils.getType(callback) !== "Function" && utils.getType(callback) !== "AsyncFunction") {
184 | callback = function (err, data) {
185 | if (err) {
186 | return rejectFunc(err);
187 | }
188 | resolveFunc(data);
189 | };
190 | }
191 |
192 | const form = {
193 | "av": ctx.i_userID || ctx.userID,
194 | "queries": JSON.stringify({
195 | "o0": {
196 | // This doc_id was valid on 2020-07-20
197 | // "doc_id": "3336396659757871",
198 | "doc_id": "3426149104143726",
199 | "query_params": {
200 | "limit": limit + (timestamp ? 1 : 0),
201 | "before": timestamp,
202 | "tags": tags,
203 | "includeDeliveryReceipts": true,
204 | "includeSeqID": false
205 | }
206 | }
207 | }),
208 | "batch_name": "MessengerGraphQLThreadlistFetcher"
209 | };
210 |
211 | defaultFuncs
212 | .post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
213 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
214 | .then((resData) => {
215 | if (resData[resData.length - 1].error_results > 0) {
216 | throw new utils.CustomError(resData[0].o0.errors);
217 | }
218 |
219 | if (resData[resData.length - 1].successful_results === 0) {
220 | throw new utils.CustomError({ error: "getThreadList: there was no successful_results", res: resData });
221 | }
222 |
223 | // When we ask for threads using timestamp from the previous request,
224 | // we are getting the last thread repeated as the first thread in this response.
225 | // .shift() gets rid of it
226 | // It is also the reason for increasing limit by 1 when timestamp is set
227 | // this way user asks for 10 threads, we are asking for 11,
228 | // but after removing the duplicated one, it is again 10
229 | if (timestamp) {
230 | resData[0].o0.data.viewer.message_threads.nodes.shift();
231 | }
232 | callback(null, formatThreadList(resData[0].o0.data.viewer.message_threads.nodes));
233 | })
234 | .catch((err) => {
235 | log.error("getThreadList", err);
236 | return callback(err);
237 | });
238 |
239 | return returnPromise;
240 | };
241 | };
242 |
--------------------------------------------------------------------------------
/src/getThreadPictures.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | return function getThreadPictures(threadID, offset, limit, callback) {
8 | let resolveFunc = function () { };
9 | let rejectFunc = function () { };
10 | const returnPromise = new Promise(function (resolve, reject) {
11 | resolveFunc = resolve;
12 | rejectFunc = reject;
13 | });
14 |
15 | if (!callback) {
16 | callback = function (err, friendList) {
17 | if (err) {
18 | return rejectFunc(err);
19 | }
20 | resolveFunc(friendList);
21 | };
22 | }
23 |
24 | let form = {
25 | thread_id: threadID,
26 | offset: offset,
27 | limit: limit
28 | };
29 |
30 | defaultFuncs
31 | .post(
32 | "https://www.facebook.com/ajax/messaging/attachments/sharedphotos.php",
33 | ctx.jar,
34 | form
35 | )
36 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
37 | .then(function (resData) {
38 | if (resData.error) {
39 | throw resData;
40 | }
41 | return Promise.all(
42 | resData.payload.imagesData.map(function (image) {
43 | form = {
44 | thread_id: threadID,
45 | image_id: image.fbid
46 | };
47 | return defaultFuncs
48 | .post(
49 | "https://www.facebook.com/ajax/messaging/attachments/sharedphotos.php",
50 | ctx.jar,
51 | form
52 | )
53 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
54 | .then(function (resData) {
55 | if (resData.error) {
56 | throw resData;
57 | }
58 | // the response is pretty messy
59 | const queryThreadID =
60 | resData.jsmods.require[0][3][1].query_metadata.query_path[0]
61 | .message_thread;
62 | const imageData =
63 | resData.jsmods.require[0][3][1].query_results[queryThreadID]
64 | .message_images.edges[0].node.image2;
65 | return imageData;
66 | });
67 | })
68 | );
69 | })
70 | .then(function (resData) {
71 | callback(null, resData);
72 | })
73 | .catch(function (err) {
74 | log.error("Error in getThreadPictures", err);
75 | callback(err);
76 | });
77 | return returnPromise;
78 | };
79 | };
80 |
--------------------------------------------------------------------------------
/src/getUserID.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | function formatData(data) {
7 | return {
8 | userID: utils.formatID(data.uid.toString()),
9 | photoUrl: data.photo,
10 | indexRank: data.index_rank,
11 | name: data.text,
12 | isVerified: data.is_verified,
13 | profileUrl: data.path,
14 | category: data.category,
15 | score: data.score,
16 | type: data.type
17 | };
18 | }
19 |
20 | module.exports = function (defaultFuncs, api, ctx) {
21 | return function getUserID(name, callback) {
22 | let resolveFunc = function () { };
23 | let rejectFunc = function () { };
24 | const returnPromise = new Promise(function (resolve, reject) {
25 | resolveFunc = resolve;
26 | rejectFunc = reject;
27 | });
28 |
29 | if (!callback) {
30 | callback = function (err, friendList) {
31 | if (err) {
32 | return rejectFunc(err);
33 | }
34 | resolveFunc(friendList);
35 | };
36 | }
37 |
38 | const form = {
39 | value: name.toLowerCase(),
40 | viewer: ctx.i_userID || ctx.userID,
41 | rsp: "search",
42 | context: "search",
43 | path: "/home.php",
44 | request_id: utils.getGUID()
45 | };
46 |
47 | defaultFuncs
48 | .get("https://www.facebook.com/ajax/typeahead/search.php", ctx.jar, form)
49 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
50 | .then(function (resData) {
51 | if (resData.error) {
52 | throw resData;
53 | }
54 |
55 | const data = resData.payload.entries;
56 |
57 | callback(null, data.map(formatData));
58 | })
59 | .catch(function (err) {
60 | log.error("getUserID", err);
61 | return callback(err);
62 | });
63 |
64 | return returnPromise;
65 | };
66 | };
67 |
--------------------------------------------------------------------------------
/src/getUserInfo.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | function formatData(data) {
7 | const retObj = {};
8 |
9 | for (const prop in data) {
10 | // eslint-disable-next-line no-prototype-builtins
11 | if (data.hasOwnProperty(prop)) {
12 | const innerObj = data[prop];
13 | retObj[prop] = {
14 | name: innerObj.name,
15 | firstName: innerObj.firstName,
16 | vanity: innerObj.vanity,
17 | thumbSrc: innerObj.thumbSrc,
18 | profileUrl: innerObj.uri,
19 | gender: innerObj.gender,
20 | type: innerObj.type,
21 | isFriend: innerObj.is_friend,
22 | isBirthday: !!innerObj.is_birthday,
23 | searchTokens: innerObj.searchTokens,
24 | alternateName: innerObj.alternateName
25 | };
26 | }
27 | }
28 |
29 | return retObj;
30 | }
31 |
32 | module.exports = function (defaultFuncs, api, ctx) {
33 | return function getUserInfo(id, callback) {
34 | let resolveFunc = function () { };
35 | let rejectFunc = function () { };
36 | const returnPromise = new Promise(function (resolve, reject) {
37 | resolveFunc = resolve;
38 | rejectFunc = reject;
39 | });
40 |
41 | if (!callback) {
42 | callback = function (err, friendList) {
43 | if (err) {
44 | return rejectFunc(err);
45 | }
46 | resolveFunc(friendList);
47 | };
48 | }
49 |
50 | if (utils.getType(id) !== "Array") {
51 | id = [id];
52 | }
53 |
54 | const form = {};
55 | id.map(function (v, i) {
56 | form["ids[" + i + "]"] = v;
57 | });
58 | defaultFuncs
59 | .post("https://www.facebook.com/chat/user_info/", ctx.jar, form)
60 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
61 | .then(function (resData) {
62 | if (resData.error) {
63 | throw resData;
64 | }
65 | return callback(null, formatData(resData.payload.profiles));
66 | })
67 | .catch(function (err) {
68 | log.error("getUserInfo", err);
69 | return callback(err);
70 | });
71 |
72 | return returnPromise;
73 | };
74 | };
75 |
--------------------------------------------------------------------------------
/src/handleFriendRequest.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | return function handleFriendRequest(userID, accept, callback) {
8 | if (utils.getType(accept) !== "Boolean") {
9 | throw {
10 | error: "Please pass a boolean as a second argument."
11 | };
12 | }
13 |
14 | let resolveFunc = function () { };
15 | let rejectFunc = function () { };
16 | const returnPromise = new Promise(function (resolve, reject) {
17 | resolveFunc = resolve;
18 | rejectFunc = reject;
19 | });
20 |
21 | if (!callback) {
22 | callback = function (err, friendList) {
23 | if (err) {
24 | return rejectFunc(err);
25 | }
26 | resolveFunc(friendList);
27 | };
28 | }
29 |
30 | const form = {
31 | viewer_id: ctx.i_userID || ctx.userID,
32 | "frefs[0]": "jwl",
33 | floc: "friend_center_requests",
34 | ref: "/reqs.php",
35 | action: (accept ? "confirm" : "reject")
36 | };
37 |
38 | defaultFuncs
39 | .post(
40 | "https://www.facebook.com/requests/friends/ajax/",
41 | ctx.jar,
42 | form
43 | )
44 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
45 | .then(function (resData) {
46 | if (resData.payload.err) {
47 | throw {
48 | err: resData.payload.err
49 | };
50 | }
51 |
52 | return callback();
53 | })
54 | .catch(function (err) {
55 | log.error("handleFriendRequest", err);
56 | return callback(err);
57 | });
58 |
59 | return returnPromise;
60 | };
61 | };
62 |
--------------------------------------------------------------------------------
/src/handleMessageRequest.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | return function handleMessageRequest(threadID, accept, callback) {
8 | if (utils.getType(accept) !== "Boolean") {
9 | throw {
10 | error: "Please pass a boolean as a second argument."
11 | };
12 | }
13 |
14 | let resolveFunc = function () { };
15 | let rejectFunc = function () { };
16 | const returnPromise = new Promise(function (resolve, reject) {
17 | resolveFunc = resolve;
18 | rejectFunc = reject;
19 | });
20 |
21 | if (!callback) {
22 | callback = function (err, friendList) {
23 | if (err) {
24 | return rejectFunc(err);
25 | }
26 | resolveFunc(friendList);
27 | };
28 | }
29 |
30 | const form = {
31 | client: "mercury"
32 | };
33 |
34 | if (utils.getType(threadID) !== "Array") {
35 | threadID = [threadID];
36 | }
37 |
38 | const messageBox = accept ? "inbox" : "other";
39 |
40 | for (let i = 0; i < threadID.length; i++) {
41 | form[messageBox + "[" + i + "]"] = threadID[i];
42 | }
43 |
44 | defaultFuncs
45 | .post(
46 | "https://www.facebook.com/ajax/mercury/move_thread.php",
47 | ctx.jar,
48 | form
49 | )
50 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
51 | .then(function (resData) {
52 | if (resData.error) {
53 | throw resData;
54 | }
55 |
56 | return callback();
57 | })
58 | .catch(function (err) {
59 | log.error("handleMessageRequest", err);
60 | return callback(err);
61 | });
62 |
63 | return returnPromise;
64 | };
65 | };
66 |
--------------------------------------------------------------------------------
/src/httpGet.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | return function httpGet(url, form, customHeader, callback, notAPI) {
8 | let resolveFunc = function () { };
9 | let rejectFunc = function () { };
10 |
11 | const returnPromise = new Promise(function (resolve, reject) {
12 | resolveFunc = resolve;
13 | rejectFunc = reject;
14 | });
15 |
16 | if (utils.getType(form) == "Function" || utils.getType(form) == "AsyncFunction") {
17 | callback = form;
18 | form = {};
19 | }
20 |
21 | if (utils.getType(customHeader) == "Function" || utils.getType(customHeader) == "AsyncFunction") {
22 | callback = customHeader;
23 | customHeader = {};
24 | }
25 |
26 | customHeader = customHeader || {};
27 |
28 | callback = callback || function (err, data) {
29 | if (err) return rejectFunc(err);
30 | resolveFunc(data);
31 | };
32 |
33 | if (notAPI) {
34 | utils
35 | .get(url, ctx.jar, form, ctx.globalOptions, ctx, customHeader)
36 | .then(function (resData) {
37 | callback(null, resData.body.toString());
38 | })
39 | .catch(function (err) {
40 | log.error("httpGet", err);
41 | return callback(err);
42 | });
43 | } else {
44 | defaultFuncs
45 | .get(url, ctx.jar, form, null, customHeader)
46 | .then(function (resData) {
47 | callback(null, resData.body.toString());
48 | })
49 | .catch(function (err) {
50 | log.error("httpGet", err);
51 | return callback(err);
52 | });
53 | }
54 |
55 | return returnPromise;
56 | };
57 | };
58 |
--------------------------------------------------------------------------------
/src/httpPost.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | return function httpPost(url, form, customHeader, callback, notAPI) {
8 | let resolveFunc = function () { };
9 | let rejectFunc = function () { };
10 |
11 | const returnPromise = new Promise(function (resolve, reject) {
12 | resolveFunc = resolve;
13 | rejectFunc = reject;
14 | });
15 |
16 | if (utils.getType(form) == "Function" || utils.getType(form) == "AsyncFunction") {
17 | callback = form;
18 | form = {};
19 | }
20 |
21 | if (utils.getType(customHeader) == "Function" || utils.getType(customHeader) == "AsyncFunction") {
22 | callback = customHeader;
23 | customHeader = {};
24 | }
25 |
26 | customHeader = customHeader || {};
27 |
28 | callback = callback || function (err, data) {
29 | if (err) return rejectFunc(err);
30 | resolveFunc(data);
31 | };
32 |
33 | if (notAPI) {
34 | utils
35 | .post(url, ctx.jar, form, ctx.globalOptions, ctx, customHeader)
36 | .then(function (resData) {
37 | callback(null, resData.body.toString());
38 | })
39 | .catch(function (err) {
40 | log.error("httpPost", err);
41 | return callback(err);
42 | });
43 | } else {
44 | defaultFuncs
45 | .post(url, ctx.jar, form, {}, customHeader)
46 | .then(function (resData) {
47 | callback(null, resData.body.toString());
48 | })
49 | .catch(function (err) {
50 | log.error("httpPost", err);
51 | return callback(err);
52 | });
53 | }
54 |
55 | return returnPromise;
56 | };
57 | };
58 |
--------------------------------------------------------------------------------
/src/httpPostFormData.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 |
7 | module.exports = function (defaultFuncs, api, ctx) {
8 | return function httpPostFormData(url, form, customHeader, callback, notAPI) {
9 | let resolveFunc = function () { };
10 | let rejectFunc = function () { };
11 |
12 | const returnPromise = new Promise(function (resolve, reject) {
13 | resolveFunc = resolve;
14 | rejectFunc = reject;
15 | });
16 |
17 | if (utils.getType(form) == "Function" || utils.getType(form) == "AsyncFunction") {
18 | callback = form;
19 | form = {};
20 | }
21 |
22 | if (utils.getType(customHeader) == "Function" || utils.getType(customHeader) == "AsyncFunction") {
23 | callback = customHeader;
24 | customHeader = {};
25 | }
26 |
27 | customHeader = customHeader || {};
28 |
29 | if (utils.getType(callback) == "Boolean") {
30 | notAPI = callback;
31 | callback = null;
32 | }
33 |
34 | callback = callback || function (err, data) {
35 | if (err) return rejectFunc(err);
36 | resolveFunc(data);
37 | };
38 |
39 | if (notAPI) {
40 | utils
41 | .postFormData(url, ctx.jar, form, ctx.globalOptions, ctx, customHeader)
42 | .then(function (resData) {
43 | callback(null, resData.body.toString());
44 | })
45 | .catch(function (err) {
46 | log.error("httpPostFormData", err);
47 | return callback(err);
48 | });
49 | } else {
50 | defaultFuncs
51 | .postFormData(url, ctx.jar, form, null, customHeader)
52 | .then(function (resData) {
53 | callback(null, resData.body.toString());
54 | })
55 | .catch(function (err) {
56 | log.error("httpPostFormData", err);
57 | return callback(err);
58 | });
59 | }
60 |
61 | return returnPromise;
62 | };
63 | };
64 |
--------------------------------------------------------------------------------
/src/logout.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | return function logout(callback) {
8 | let resolveFunc = function () { };
9 | let rejectFunc = function () { };
10 | const returnPromise = new Promise(function (resolve, reject) {
11 | resolveFunc = resolve;
12 | rejectFunc = reject;
13 | });
14 |
15 | if (!callback) {
16 | callback = function (err, friendList) {
17 | if (err) {
18 | return rejectFunc(err);
19 | }
20 | resolveFunc(friendList);
21 | };
22 | }
23 |
24 | const form = {
25 | pmid: "0"
26 | };
27 |
28 | defaultFuncs
29 | .post(
30 | "https://www.facebook.com/bluebar/modern_settings_menu/?help_type=364455653583099&show_contextual_help=1",
31 | ctx.jar,
32 | form
33 | )
34 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
35 | .then(function (resData) {
36 | const elem = resData.jsmods.instances[0][2][0].filter(function (v) {
37 | return v.value === "logout";
38 | })[0];
39 |
40 | const html = resData.jsmods.markup.filter(function (v) {
41 | return v[0] === elem.markup.__m;
42 | })[0][1].__html;
43 |
44 | const form = {
45 | fb_dtsg: utils.getFrom(html, '"fb_dtsg" value="', '"'),
46 | ref: utils.getFrom(html, '"ref" value="', '"'),
47 | h: utils.getFrom(html, '"h" value="', '"')
48 | };
49 |
50 | return defaultFuncs
51 | .post("https://www.facebook.com/logout.php", ctx.jar, form)
52 | .then(utils.saveCookies(ctx.jar));
53 | })
54 | .then(function (res) {
55 | if (!res.headers) {
56 | throw { error: "An error occurred when logging out." };
57 | }
58 |
59 | return defaultFuncs
60 | .get(res.headers.location, ctx.jar)
61 | .then(utils.saveCookies(ctx.jar));
62 | })
63 | .then(function () {
64 | ctx.loggedIn = false;
65 | log.info("logout", "Logged out successfully.");
66 | callback();
67 | })
68 | .catch(function (err) {
69 | log.error("logout", err);
70 | return callback(err);
71 | });
72 |
73 | return returnPromise;
74 | };
75 | };
76 |
--------------------------------------------------------------------------------
/src/markAsDelivered.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | return function markAsDelivered(threadID, messageID, callback) {
8 | let resolveFunc = function () { };
9 | let rejectFunc = function () { };
10 | const returnPromise = new Promise(function (resolve, reject) {
11 | resolveFunc = resolve;
12 | rejectFunc = reject;
13 | });
14 |
15 | if (!callback) {
16 | callback = function (err, friendList) {
17 | if (err) {
18 | return rejectFunc(err);
19 | }
20 | resolveFunc(friendList);
21 | };
22 | }
23 |
24 | if (!threadID || !messageID) {
25 | return callback("Error: messageID or threadID is not defined");
26 | }
27 |
28 | const form = {};
29 |
30 | form["message_ids[0]"] = messageID;
31 | form["thread_ids[" + threadID + "][0]"] = messageID;
32 |
33 | defaultFuncs
34 | .post(
35 | "https://www.facebook.com/ajax/mercury/delivery_receipts.php",
36 | ctx.jar,
37 | form
38 | )
39 | .then(utils.saveCookies(ctx.jar))
40 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
41 | .then(function (resData) {
42 | if (resData.error) {
43 | throw resData;
44 | }
45 |
46 | return callback();
47 | })
48 | .catch(function (err) {
49 | log.error("markAsDelivered", err);
50 | if (utils.getType(err) == "Object" && err.error === "Not logged in.") {
51 | ctx.loggedIn = false;
52 | }
53 | return callback(err);
54 | });
55 |
56 | return returnPromise;
57 | };
58 | };
59 |
--------------------------------------------------------------------------------
/src/markAsRead.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | return async function markAsRead(threadID, read, callback) {
8 | if (utils.getType(read) === 'Function' || utils.getType(read) === 'AsyncFunction') {
9 | callback = read;
10 | read = true;
11 | }
12 | if (read == undefined) {
13 | read = true;
14 | }
15 |
16 | if (!callback) {
17 | callback = () => { };
18 | }
19 |
20 | const form = {};
21 |
22 | if (typeof ctx.globalOptions.pageID !== 'undefined') {
23 | form["source"] = "PagesManagerMessagesInterface";
24 | form["request_user_id"] = ctx.globalOptions.pageID;
25 | form["ids[" + threadID + "]"] = read;
26 | form["watermarkTimestamp"] = new Date().getTime();
27 | form["shouldSendReadReceipt"] = true;
28 | form["commerce_last_message_type"] = "";
29 | //form["titanOriginatedThreadId"] = utils.generateThreadingID(ctx.clientID);
30 |
31 | let resData;
32 | try {
33 | resData = await (
34 | defaultFuncs
35 | .post(
36 | "https://www.facebook.com/ajax/mercury/change_read_status.php",
37 | ctx.jar,
38 | form
39 | )
40 | .then(utils.saveCookies(ctx.jar))
41 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
42 | );
43 | } catch (e) {
44 | callback(e);
45 | return e;
46 | }
47 |
48 | if (resData.error) {
49 | const err = resData.error;
50 | log.error("markAsRead", err);
51 | if (utils.getType(err) == "Object" && err.error === "Not logged in.") {
52 | ctx.loggedIn = false;
53 | }
54 | callback(err);
55 | return err;
56 | }
57 |
58 | callback();
59 | return null;
60 | } else {
61 | try {
62 | if (ctx.mqttClient) {
63 | const err = await new Promise(r => ctx.mqttClient.publish("/mark_thread", JSON.stringify({
64 | threadID,
65 | mark: "read",
66 | state: read
67 | }), { qos: 1, retain: false }, r));
68 | if (err) throw err;
69 | } else {
70 | throw {
71 | error: "You can only use this function after you start listening."
72 | };
73 | }
74 | } catch (e) {
75 | callback(e);
76 | return e;
77 | }
78 | }
79 | };
80 | };
81 |
--------------------------------------------------------------------------------
/src/markAsReadAll.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | return function markAsReadAll(callback) {
8 | let resolveFunc = function () { };
9 | let rejectFunc = function () { };
10 | const returnPromise = new Promise(function (resolve, reject) {
11 | resolveFunc = resolve;
12 | rejectFunc = reject;
13 | });
14 |
15 | if (!callback) {
16 | callback = function (err, friendList) {
17 | if (err) {
18 | return rejectFunc(err);
19 | }
20 | resolveFunc(friendList);
21 | };
22 | }
23 |
24 | const form = {
25 | folder: 'inbox'
26 | };
27 |
28 | defaultFuncs
29 | .post(
30 | "https://www.facebook.com/ajax/mercury/mark_folder_as_read.php",
31 | ctx.jar,
32 | form
33 | )
34 | .then(utils.saveCookies(ctx.jar))
35 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
36 | .then(function (resData) {
37 | if (resData.error) {
38 | throw resData;
39 | }
40 |
41 | return callback();
42 | })
43 | .catch(function (err) {
44 | log.error("markAsReadAll", err);
45 | return callback(err);
46 | });
47 |
48 | return returnPromise;
49 | };
50 | };
--------------------------------------------------------------------------------
/src/markAsSeen.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | return function markAsRead(seen_timestamp, callback) {
8 | if (utils.getType(seen_timestamp) == "Function" ||
9 | utils.getType(seen_timestamp) == "AsyncFunction") {
10 | callback = seen_timestamp;
11 | seen_timestamp = Date.now();
12 | }
13 |
14 | let resolveFunc = function () { };
15 | let rejectFunc = function () { };
16 | const returnPromise = new Promise(function (resolve, reject) {
17 | resolveFunc = resolve;
18 | rejectFunc = reject;
19 | });
20 |
21 | if (!callback) {
22 | callback = function (err, friendList) {
23 | if (err) {
24 | return rejectFunc(err);
25 | }
26 | resolveFunc(friendList);
27 | };
28 | }
29 |
30 | const form = {
31 | seen_timestamp: seen_timestamp
32 | };
33 |
34 | defaultFuncs
35 | .post(
36 | "https://www.facebook.com/ajax/mercury/mark_seen.php",
37 | ctx.jar,
38 | form
39 | )
40 | .then(utils.saveCookies(ctx.jar))
41 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
42 | .then(function (resData) {
43 | if (resData.error) {
44 | throw resData;
45 | }
46 |
47 | return callback();
48 | })
49 | .catch(function (err) {
50 | log.error("markAsSeen", err);
51 | if (utils.getType(err) == "Object" && err.error === "Not logged in.") {
52 | ctx.loggedIn = false;
53 | }
54 | return callback(err);
55 | });
56 |
57 | return returnPromise;
58 | };
59 | };
60 |
--------------------------------------------------------------------------------
/src/muteThread.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | // muteSecond: -1=permanent mute, 0=unmute, 60=one minute, 3600=one hour, etc.
8 | return function muteThread(threadID, muteSeconds, callback) {
9 | let resolveFunc = function () { };
10 | let rejectFunc = function () { };
11 | const returnPromise = new Promise(function (resolve, reject) {
12 | resolveFunc = resolve;
13 | rejectFunc = reject;
14 | });
15 |
16 | if (!callback) {
17 | callback = function (err, friendList) {
18 | if (err) {
19 | return rejectFunc(err);
20 | }
21 | resolveFunc(friendList);
22 | };
23 | }
24 |
25 | const form = {
26 | thread_fbid: threadID,
27 | mute_settings: muteSeconds
28 | };
29 |
30 | defaultFuncs
31 | .post(
32 | "https://www.facebook.com/ajax/mercury/change_mute_thread.php",
33 | ctx.jar,
34 | form
35 | )
36 | .then(utils.saveCookies(ctx.jar))
37 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
38 | .then(function (resData) {
39 | if (resData.error) {
40 | throw resData;
41 | }
42 |
43 | return callback();
44 | })
45 | .catch(function (err) {
46 | log.error("muteThread", err);
47 | return callback(err);
48 | });
49 |
50 | return returnPromise;
51 | };
52 | };
53 |
--------------------------------------------------------------------------------
/src/refreshFb_dtsg.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | /**
8 | * Refreshes the fb_dtsg and jazoest values.
9 | * @param {Function} callback
10 | * @returns {Promise}
11 | * @description if you don't update the value of fb_dtsg and jazoest for a long time an error "Please try closing and re-opening your browser window" will appear
12 | * @description you should refresh it every 48h or less
13 | */
14 | return function refreshFb_dtsg(obj, callback) {
15 | let resolveFunc = function () { };
16 | let rejectFunc = function () { };
17 | const returnPromise = new Promise(function (resolve, reject) {
18 | resolveFunc = resolve;
19 | rejectFunc = reject;
20 | });
21 |
22 | if (utils.getType(obj) === "Function" || utils.getType(obj) === "AsyncFunction") {
23 | callback = obj;
24 | obj = {};
25 | }
26 |
27 | if (!obj) {
28 | obj = {};
29 | }
30 |
31 | if (utils.getType(obj) !== "Object") {
32 | throw new utils.CustomError("the first parameter must be an object or a callback function");
33 | }
34 |
35 | if (!callback) {
36 | callback = function (err, friendList) {
37 | if (err) {
38 | return rejectFunc(err);
39 | }
40 | resolveFunc(friendList);
41 | };
42 | }
43 |
44 | if (Object.keys(obj).length == 0) {
45 | utils
46 | .get('https://m.facebook.com/', ctx.jar, null, ctx.globalOptions, { noRef: true })
47 | .then(function (resData) {
48 | const html = resData.body;
49 | const fb_dtsg = utils.getFrom(html, 'name="fb_dtsg" value="', '"');
50 | const jazoest = utils.getFrom(html, 'name="jazoest" value="', '"');
51 | if (!fb_dtsg) {
52 | throw new utils.CustomError("Could not find fb_dtsg in HTML after requesting https://www.facebook.com/");
53 | }
54 | ctx.fb_dtsg = fb_dtsg;
55 | ctx.jazoest = jazoest;
56 | callback(null, {
57 | data: {
58 | fb_dtsg: fb_dtsg,
59 | jazoest: jazoest
60 | },
61 | message: "refreshed fb_dtsg and jazoest"
62 | });
63 | })
64 | .catch(function (err) {
65 | log.error("refreshFb_dtsg", err);
66 | return callback(err);
67 | });
68 | }
69 | else {
70 | Object.keys(obj).forEach(function (key) {
71 | ctx[key] = obj[key];
72 | });
73 | callback(null, {
74 | data: obj,
75 | message: "refreshed " + Object.keys(obj).join(", ")
76 | });
77 | }
78 |
79 | return returnPromise;
80 | };
81 | };
82 |
--------------------------------------------------------------------------------
/src/removeUserFromGroup.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | return function removeUserFromGroup(userID, threadID, callback) {
8 | if (
9 | !callback &&
10 | (utils.getType(threadID) === "Function" ||
11 | utils.getType(threadID) === "AsyncFunction")
12 | ) {
13 | throw { error: "please pass a threadID as a second argument." };
14 | }
15 | if (
16 | utils.getType(threadID) !== "Number" &&
17 | utils.getType(threadID) !== "String"
18 | ) {
19 | throw {
20 | error:
21 | "threadID should be of type Number or String and not " +
22 | utils.getType(threadID) +
23 | "."
24 | };
25 | }
26 | if (
27 | utils.getType(userID) !== "Number" &&
28 | utils.getType(userID) !== "String"
29 | ) {
30 | throw {
31 | error:
32 | "userID should be of type Number or String and not " +
33 | utils.getType(userID) +
34 | "."
35 | };
36 | }
37 |
38 | let resolveFunc = function () { };
39 | let rejectFunc = function () { };
40 | const returnPromise = new Promise(function (resolve, reject) {
41 | resolveFunc = resolve;
42 | rejectFunc = reject;
43 | });
44 |
45 | if (!callback) {
46 | callback = function (err, friendList) {
47 | if (err) {
48 | return rejectFunc(err);
49 | }
50 | resolveFunc(friendList);
51 | };
52 | }
53 |
54 | const form = {
55 | uid: userID,
56 | tid: threadID
57 | };
58 |
59 | defaultFuncs
60 | .post("https://www.facebook.com/chat/remove_participants", ctx.jar, form)
61 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
62 | .then(function (resData) {
63 | if (!resData) {
64 | throw { error: "Remove from group failed." };
65 | }
66 | if (resData.error) {
67 | throw resData;
68 | }
69 |
70 | return callback();
71 | })
72 | .catch(function (err) {
73 | log.error("removeUserFromGroup", err);
74 | return callback(err);
75 | });
76 |
77 | return returnPromise;
78 | };
79 | };
80 |
--------------------------------------------------------------------------------
/src/resolvePhotoUrl.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | return function resolvePhotoUrl(photoID, callback) {
8 | let resolveFunc = function () { };
9 | let rejectFunc = function () { };
10 | const returnPromise = new Promise(function (resolve, reject) {
11 | resolveFunc = resolve;
12 | rejectFunc = reject;
13 | });
14 |
15 | if (!callback) {
16 | callback = function (err, friendList) {
17 | if (err) {
18 | return rejectFunc(err);
19 | }
20 | resolveFunc(friendList);
21 | };
22 | }
23 |
24 | defaultFuncs
25 | .get("https://www.facebook.com/mercury/attachments/photo", ctx.jar, {
26 | photo_id: photoID
27 | })
28 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
29 | .then(resData => {
30 | if (resData.error) {
31 | throw resData;
32 | }
33 |
34 | const photoUrl = resData.jsmods.require[0][3][0];
35 |
36 | return callback(null, photoUrl);
37 | })
38 | .catch(err => {
39 | log.error("resolvePhotoUrl", err);
40 | return callback(err);
41 | });
42 |
43 | return returnPromise;
44 | };
45 | };
46 |
--------------------------------------------------------------------------------
/src/searchForThread.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 |
5 | module.exports = function (defaultFuncs, api, ctx) {
6 | return function searchForThread(name, callback) {
7 | let resolveFunc = function () { };
8 | let rejectFunc = function () { };
9 | const returnPromise = new Promise(function (resolve, reject) {
10 | resolveFunc = resolve;
11 | rejectFunc = reject;
12 | });
13 |
14 | if (!callback) {
15 | callback = function (err, friendList) {
16 | if (err) {
17 | return rejectFunc(err);
18 | }
19 | resolveFunc(friendList);
20 | };
21 | }
22 |
23 | const tmpForm = {
24 | client: "web_messenger",
25 | query: name,
26 | offset: 0,
27 | limit: 21,
28 | index: "fbid"
29 | };
30 |
31 | defaultFuncs
32 | .post(
33 | "https://www.facebook.com/ajax/mercury/search_threads.php",
34 | ctx.jar,
35 | tmpForm
36 | )
37 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
38 | .then(function (resData) {
39 | if (resData.error) {
40 | throw resData;
41 | }
42 | if (!resData.payload.mercury_payload.threads) {
43 | return callback({ error: "Could not find thread `" + name + "`." });
44 | }
45 | return callback(
46 | null,
47 | resData.payload.mercury_payload.threads.map(utils.formatThread)
48 | );
49 | });
50 |
51 | return returnPromise;
52 | };
53 | };
54 |
--------------------------------------------------------------------------------
/src/sendMessage.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | const allowedProperties = {
7 | attachment: true,
8 | url: true,
9 | sticker: true,
10 | emoji: true,
11 | emojiSize: true,
12 | body: true,
13 | mentions: true,
14 | location: true
15 | };
16 |
17 | module.exports = function (defaultFuncs, api, ctx) {
18 | function uploadAttachment(attachments, callback) {
19 | const uploads = [];
20 |
21 | // create an array of promises
22 | for (let i = 0; i < attachments.length; i++) {
23 | if (!utils.isReadableStream(attachments[i])) {
24 | throw {
25 | error:
26 | "Attachment should be a readable stream and not " +
27 | utils.getType(attachments[i]) +
28 | "."
29 | };
30 | }
31 |
32 | const form = {
33 | upload_1024: attachments[i],
34 | voice_clip: "true"
35 | };
36 |
37 | uploads.push(
38 | defaultFuncs
39 | .postFormData(
40 | "https://upload.facebook.com/ajax/mercury/upload.php",
41 | ctx.jar,
42 | form,
43 | {}
44 | )
45 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
46 | .then(function (resData) {
47 | if (resData.error) {
48 | throw resData;
49 | }
50 |
51 | // We have to return the data unformatted unless we want to change it
52 | // back in sendMessage.
53 | return resData.payload.metadata[0];
54 | })
55 | );
56 | }
57 |
58 | // resolve all promises
59 | Promise
60 | .all(uploads)
61 | .then(function (resData) {
62 | callback(null, resData);
63 | })
64 | .catch(function (err) {
65 | log.error("uploadAttachment", err);
66 | return callback(err);
67 | });
68 | }
69 |
70 | function getUrl(url, callback) {
71 | const form = {
72 | image_height: 960,
73 | image_width: 960,
74 | uri: url
75 | };
76 |
77 | defaultFuncs
78 | .post(
79 | "https://www.facebook.com/message_share_attachment/fromURI/",
80 | ctx.jar,
81 | form
82 | )
83 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
84 | .then(function (resData) {
85 | if (resData.error) {
86 | return callback(resData);
87 | }
88 |
89 | if (!resData.payload) {
90 | return callback({ error: "Invalid url" });
91 | }
92 |
93 | callback(null, resData.payload.share_data.share_params);
94 | })
95 | .catch(function (err) {
96 | log.error("getUrl", err);
97 | return callback(err);
98 | });
99 | }
100 |
101 | function sendContent(form, threadID, isSingleUser, messageAndOTID, callback) {
102 | // There are three cases here:
103 | // 1. threadID is of type array, where we're starting a new group chat with users
104 | // specified in the array.
105 | // 2. User is sending a message to a specific user.
106 | // 3. No additional form params and the message goes to an existing group chat.
107 | if (utils.getType(threadID) === "Array") {
108 | for (let i = 0; i < threadID.length; i++) {
109 | form["specific_to_list[" + i + "]"] = "fbid:" + threadID[i];
110 | }
111 | form["specific_to_list[" + threadID.length + "]"] = "fbid:" + (ctx.i_userID || ctx.userID);
112 | form["client_thread_id"] = "root:" + messageAndOTID;
113 | log.info("sendMessage", "Sending message to multiple users: " + threadID);
114 | } else {
115 | // This means that threadID is the id of a user, and the chat
116 | // is a single person chat
117 | if (isSingleUser) {
118 | form["specific_to_list[0]"] = "fbid:" + threadID;
119 | form["specific_to_list[1]"] = "fbid:" + (ctx.i_userID || ctx.userID);
120 | form["other_user_fbid"] = threadID;
121 | } else {
122 | form["thread_fbid"] = threadID;
123 | }
124 | }
125 |
126 | if (ctx.globalOptions.pageID) {
127 | form["author"] = "fbid:" + ctx.globalOptions.pageID;
128 | form["specific_to_list[1]"] = "fbid:" + ctx.globalOptions.pageID;
129 | form["creator_info[creatorID]"] = ctx.i_userID || ctx.userID;
130 | form["creator_info[creatorType]"] = "direct_admin";
131 | form["creator_info[labelType]"] = "sent_message";
132 | form["creator_info[pageID]"] = ctx.globalOptions.pageID;
133 | form["request_user_id"] = ctx.globalOptions.pageID;
134 | form["creator_info[profileURI]"] =
135 | "https://www.facebook.com/profile.php?id=" + (ctx.i_userID || ctx.userID);
136 | }
137 |
138 | defaultFuncs
139 | .post("https://www.facebook.com/messaging/send/", ctx.jar, form)
140 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
141 | .then(function (resData) {
142 | if (!resData) {
143 | return callback({ error: "Send message failed." });
144 | }
145 |
146 | if (resData.error) {
147 | if (resData.error === 1545012) {
148 | log.warn(
149 | "sendMessage",
150 | "Got error 1545012. This might mean that you're not part of the conversation " +
151 | threadID
152 | );
153 | }
154 | else {
155 | log.error("sendMessage", resData);
156 | }
157 | return callback(null, resData);
158 | }
159 |
160 | const messageInfo = resData.payload.actions.reduce(function (p, v) {
161 | return (
162 | {
163 | threadID: v.thread_fbid,
164 | messageID: v.message_id,
165 | timestamp: v.timestamp
166 | } || p
167 | );
168 | }, null);
169 |
170 | return callback(null, messageInfo);
171 | })
172 | .catch(function (err) {
173 | log.error("sendMessage", err);
174 | if (utils.getType(err) == "Object" && err.error === "Not logged in.") {
175 | ctx.loggedIn = false;
176 | }
177 | return callback(err);
178 | });
179 | }
180 |
181 | function send(form, threadID, messageAndOTID, callback, isGroup) {
182 | // We're doing a query to this to check if the given id is the id of
183 | // a user or of a group chat. The form will be different depending
184 | // on that.
185 | if (utils.getType(threadID) === "Array") {
186 | sendContent(form, threadID, false, messageAndOTID, callback);
187 | } else {
188 | if (utils.getType(isGroup) != "Boolean") {
189 | // Removed the use of api.getUserInfo() in the old version to reduce account lockout
190 | sendContent(form, threadID, threadID.toString().length < 16, messageAndOTID, callback);
191 | } else {
192 | sendContent(form, threadID, !isGroup, messageAndOTID, callback);
193 | }
194 | }
195 | }
196 |
197 | function handleUrl(msg, form, callback, cb) {
198 | if (msg.url) {
199 | form["shareable_attachment[share_type]"] = "100";
200 | getUrl(msg.url, function (err, params) {
201 | if (err) {
202 | return callback(err);
203 | }
204 |
205 | form["shareable_attachment[share_params]"] = params;
206 | cb();
207 | });
208 | } else {
209 | cb();
210 | }
211 | }
212 |
213 | function handleLocation(msg, form, callback, cb) {
214 | if (msg.location) {
215 | if (msg.location.latitude == null || msg.location.longitude == null) {
216 | return callback({ error: "location property needs both latitude and longitude" });
217 | }
218 |
219 | form["location_attachment[coordinates][latitude]"] = msg.location.latitude;
220 | form["location_attachment[coordinates][longitude]"] = msg.location.longitude;
221 | form["location_attachment[is_current_location]"] = !!msg.location.current;
222 | }
223 |
224 | cb();
225 | }
226 |
227 | function handleSticker(msg, form, callback, cb) {
228 | if (msg.sticker) {
229 | form["sticker_id"] = msg.sticker;
230 | }
231 | cb();
232 | }
233 |
234 | function handleEmoji(msg, form, callback, cb) {
235 | if (msg.emojiSize != null && msg.emoji == null) {
236 | return callback({ error: "emoji property is empty" });
237 | }
238 | if (msg.emoji) {
239 | if (msg.emojiSize == null) {
240 | msg.emojiSize = "medium";
241 | }
242 | if (
243 | msg.emojiSize != "small" &&
244 | msg.emojiSize != "medium" &&
245 | msg.emojiSize != "large"
246 | ) {
247 | return callback({ error: "emojiSize property is invalid" });
248 | }
249 | if (form["body"] != null && form["body"] != "") {
250 | return callback({ error: "body is not empty" });
251 | }
252 | form["body"] = msg.emoji;
253 | form["tags[0]"] = "hot_emoji_size:" + msg.emojiSize;
254 | }
255 | cb();
256 | }
257 |
258 | function handleAttachment(msg, form, callback, cb) {
259 | if (msg.attachment) {
260 | form["image_ids"] = [];
261 | form["gif_ids"] = [];
262 | form["file_ids"] = [];
263 | form["video_ids"] = [];
264 | form["audio_ids"] = [];
265 |
266 | if (utils.getType(msg.attachment) !== "Array") {
267 | msg.attachment = [msg.attachment];
268 | }
269 |
270 | uploadAttachment(msg.attachment, function (err, files) {
271 | if (err) {
272 | return callback(err);
273 | }
274 |
275 | files.forEach(function (file) {
276 | const key = Object.keys(file);
277 | const type = key[0]; // image_id, file_id, etc
278 | form["" + type + "s"].push(file[type]); // push the id
279 | });
280 | cb();
281 | });
282 | } else {
283 | cb();
284 | }
285 | }
286 |
287 | function handleMention(msg, form, callback, cb) {
288 | if (msg.mentions) {
289 | for (let i = 0; i < msg.mentions.length; i++) {
290 | const mention = msg.mentions[i];
291 |
292 | const tag = mention.tag;
293 | if (typeof tag !== "string") {
294 | return callback({ error: "Mention tags must be strings." });
295 | }
296 |
297 | const offset = msg.body.indexOf(tag, mention.fromIndex || 0);
298 |
299 | if (offset < 0) {
300 | log.warn(
301 | "handleMention",
302 | 'Mention for "' + tag + '" not found in message string.'
303 | );
304 | }
305 |
306 | if (mention.id == null) {
307 | log.warn("handleMention", "Mention id should be non-null.");
308 | }
309 |
310 | const id = mention.id || 0;
311 | form["profile_xmd[" + i + "][offset]"] = offset;
312 | form["profile_xmd[" + i + "][length]"] = tag.length;
313 | form["profile_xmd[" + i + "][id]"] = id;
314 | form["profile_xmd[" + i + "][type]"] = "p";
315 | }
316 | }
317 | cb();
318 | }
319 |
320 | return function sendMessage(msg, threadID, callback, replyToMessage, isGroup) {
321 | typeof isGroup == "undefined" ? isGroup = null : "";
322 | if (
323 | !callback &&
324 | (utils.getType(threadID) === "Function" ||
325 | utils.getType(threadID) === "AsyncFunction")
326 | ) {
327 | return threadID({ error: "Pass a threadID as a second argument." });
328 | }
329 | if (
330 | !replyToMessage &&
331 | utils.getType(callback) === "String"
332 | ) {
333 | replyToMessage = callback;
334 | callback = function () { };
335 | }
336 |
337 | let resolveFunc = function () { };
338 | let rejectFunc = function () { };
339 | const returnPromise = new Promise(function (resolve, reject) {
340 | resolveFunc = resolve;
341 | rejectFunc = reject;
342 | });
343 |
344 | if (!callback) {
345 | callback = function (err, friendList) {
346 | if (err) {
347 | return rejectFunc(err);
348 | }
349 | resolveFunc(friendList);
350 | };
351 | }
352 |
353 | const msgType = utils.getType(msg);
354 | const threadIDType = utils.getType(threadID);
355 | const messageIDType = utils.getType(replyToMessage);
356 |
357 | if (msgType !== "String" && msgType !== "Object") {
358 | return callback({
359 | error:
360 | "Message should be of type string or object and not " + msgType + "."
361 | });
362 | }
363 |
364 | // Changing this to accomodate an array of users
365 | if (
366 | threadIDType !== "Array" &&
367 | threadIDType !== "Number" &&
368 | threadIDType !== "String"
369 | ) {
370 | return callback({
371 | error:
372 | "ThreadID should be of type number, string, or array and not " +
373 | threadIDType +
374 | "."
375 | });
376 | }
377 |
378 | if (replyToMessage && messageIDType !== 'String') {
379 | return callback({
380 | error:
381 | "MessageID should be of type string and not " +
382 | threadIDType +
383 | "."
384 | });
385 | }
386 |
387 | if (msgType === "String") {
388 | msg = { body: msg };
389 | }
390 |
391 | const disallowedProperties = Object.keys(msg).filter(
392 | prop => !allowedProperties[prop]
393 | );
394 | if (disallowedProperties.length > 0) {
395 | return callback({
396 | error: "Dissallowed props: `" + disallowedProperties.join(", ") + "`"
397 | });
398 | }
399 |
400 | const messageAndOTID = utils.generateOfflineThreadingID();
401 |
402 | const form = {
403 | client: "mercury",
404 | action_type: "ma-type:user-generated-message",
405 | author: "fbid:" + (ctx.i_userID || ctx.userID),
406 | timestamp: Date.now(),
407 | timestamp_absolute: "Today",
408 | timestamp_relative: utils.generateTimestampRelative(),
409 | timestamp_time_passed: "0",
410 | is_unread: false,
411 | is_cleared: false,
412 | is_forward: false,
413 | is_filtered_content: false,
414 | is_filtered_content_bh: false,
415 | is_filtered_content_account: false,
416 | is_filtered_content_quasar: false,
417 | is_filtered_content_invalid_app: false,
418 | is_spoof_warning: false,
419 | source: "source:chat:web",
420 | "source_tags[0]": "source:chat",
421 | body: msg.body ? msg.body.toString() : "",
422 | html_body: false,
423 | ui_push_phase: "V3",
424 | status: "0",
425 | offline_threading_id: messageAndOTID,
426 | message_id: messageAndOTID,
427 | threading_id: utils.generateThreadingID(ctx.clientID),
428 | "ephemeral_ttl_mode:": "0",
429 | manual_retry_cnt: "0",
430 | has_attachment: !!(msg.attachment || msg.url || msg.sticker),
431 | signatureID: utils.getSignatureID(),
432 | replied_to_message_id: replyToMessage
433 | };
434 |
435 | handleLocation(msg, form, callback, () =>
436 | handleSticker(msg, form, callback, () =>
437 | handleAttachment(msg, form, callback, () =>
438 | handleUrl(msg, form, callback, () =>
439 | handleEmoji(msg, form, callback, () =>
440 | handleMention(msg, form, callback, () =>
441 | send(form, threadID, messageAndOTID, callback, isGroup)
442 | )
443 | )
444 | )
445 | )
446 | )
447 | );
448 |
449 | return returnPromise;
450 | };
451 | };
452 |
--------------------------------------------------------------------------------
/src/sendMessageMqtt.js:
--------------------------------------------------------------------------------
1 | var utils = require("../utils");
2 | var log = require("npmlog");
3 | var bluebird = require("bluebird");
4 |
5 | module.exports = function (defaultFuncs, api, ctx) {
6 | function uploadAttachment(attachments, callback) {
7 | callback = callback || function () { };
8 | var uploads = [];
9 |
10 | // create an array of promises
11 | for (var i = 0; i < attachments.length; i++) {
12 | if (!utils.isReadableStream(attachments[i])) {
13 | throw {
14 | error:
15 | "Attachment should be a readable stream and not " +
16 | utils.getType(attachments[i]) +
17 | "."
18 | };
19 | }
20 |
21 | var form = {
22 | upload_1024: attachments[i],
23 | voice_clip: "true"
24 | };
25 |
26 | uploads.push(
27 | defaultFuncs
28 | .postFormData(
29 | "https://upload.facebook.com/ajax/mercury/upload.php",
30 | ctx.jar,
31 | form,
32 | {}
33 | )
34 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
35 | .then(function (resData) {
36 | if (resData.error) {
37 | throw resData;
38 | }
39 |
40 | // We have to return the data unformatted unless we want to change it
41 | // back in sendMessage.
42 | return resData.payload.metadata[0];
43 | })
44 | );
45 | }
46 |
47 | // resolve all promises
48 | bluebird
49 | .all(uploads)
50 | .then(function (resData) {
51 | callback(null, resData);
52 | })
53 | .catch(function (err) {
54 | log.error("uploadAttachment", err);
55 | return callback(err);
56 | });
57 | }
58 |
59 | let variance = 0;
60 | const epoch_id = () => Math.floor(Date.now() * (4194304 + (variance = (variance + 0.1) % 5)));
61 | const emojiSizes = {
62 | small: 1,
63 | medium: 2,
64 | large: 3
65 | };
66 |
67 | function handleEmoji(msg, form, callback, cb) {
68 | if (msg.emojiSize != null && msg.emoji == null) {
69 | return callback({ error: "emoji property is empty" });
70 | }
71 | if (msg.emoji) {
72 | if (!msg.emojiSize) {
73 | msg.emojiSize = "small";
74 | }
75 | if (
76 | msg.emojiSize !== "small" &&
77 | msg.emojiSize !== "medium" &&
78 | msg.emojiSize !== "large" &&
79 | (isNaN(msg.emojiSize) || msg.emojiSize < 1 || msg.emojiSize > 3)
80 | ) {
81 | return callback({ error: "emojiSize property is invalid" });
82 | }
83 |
84 | form.payload.tasks[0].payload.send_type = 1;
85 | form.payload.tasks[0].payload.text = msg.emoji;
86 | form.payload.tasks[0].payload.hot_emoji_size = !isNaN(msg.emojiSize) ? msg.emojiSize : emojiSizes[msg.emojiSize];
87 | }
88 | cb();
89 | }
90 |
91 | function handleSticker(msg, form, callback, cb) {
92 | if (msg.sticker) {
93 | form.payload.tasks[0].payload.send_type = 2;
94 | form.payload.tasks[0].payload.sticker_id = msg.sticker;
95 | }
96 | cb();
97 | }
98 |
99 | function handleAttachment(msg, form, callback, cb) {
100 | if (msg.attachment) {
101 | form.payload.tasks[0].payload.send_type = 3;
102 | form.payload.tasks[0].payload.attachment_fbids = [];
103 | if (form.payload.tasks[0].payload.text == "")
104 | form.payload.tasks[0].payload.text = null;
105 | if (utils.getType(msg.attachment) !== "Array") {
106 | msg.attachment = [msg.attachment];
107 | }
108 |
109 | uploadAttachment(msg.attachment, function (err, files) {
110 | if (err) {
111 | return callback(err);
112 | }
113 |
114 | files.forEach(function (file) {
115 | var key = Object.keys(file);
116 | var type = key[0]; // image_id, file_id, etc
117 | form.payload.tasks[0].payload.attachment_fbids.push(file[type]); // push the id
118 | });
119 | cb();
120 | });
121 | } else {
122 | cb();
123 | }
124 | }
125 |
126 |
127 | function handleMention(msg, form, callback, cb) {
128 | if (msg.mentions) {
129 | form.payload.tasks[0].payload.send_type = 1;
130 |
131 | const arrayIds = [];
132 | const arrayOffsets = [];
133 | const arrayLengths = [];
134 | const mention_types = [];
135 |
136 | for (let i = 0; i < msg.mentions.length; i++) {
137 | const mention = msg.mentions[i];
138 |
139 | const tag = mention.tag;
140 | if (typeof tag !== "string") {
141 | return callback({ error: "Mention tags must be strings." });
142 | }
143 |
144 | const offset = msg.body.indexOf(tag, mention.fromIndex || 0);
145 |
146 | if (offset < 0) {
147 | log.warn(
148 | "handleMention",
149 | 'Mention for "' + tag + '" not found in message string.'
150 | );
151 | }
152 |
153 | if (mention.id == null) {
154 | log.warn("handleMention", "Mention id should be non-null.");
155 | }
156 |
157 | const id = mention.id || 0;
158 | arrayIds.push(id);
159 | arrayOffsets.push(offset);
160 | arrayLengths.push(tag.length);
161 | mention_types.push("p");
162 | }
163 |
164 | form.payload.tasks[0].payload.mention_data = {
165 | mention_ids: arrayIds.join(","),
166 | mention_offsets: arrayOffsets.join(","),
167 | mention_lengths: arrayLengths.join(","),
168 | mention_types: mention_types.join(",")
169 | };
170 | }
171 | cb();
172 | }
173 |
174 | function handleLocation(msg, form, callback, cb) {
175 | // this is not working yet
176 | if (msg.location) {
177 | if (msg.location.latitude == null || msg.location.longitude == null) {
178 | return callback({ error: "location property needs both latitude and longitude" });
179 | }
180 |
181 | form.payload.tasks[0].payload.send_type = 1;
182 | form.payload.tasks[0].payload.location_data = {
183 | coordinates: {
184 | latitude: msg.location.latitude,
185 | longitude: msg.location.longitude
186 | },
187 | is_current_location: !!msg.location.current,
188 | is_live_location: !!msg.location.live
189 | };
190 | }
191 |
192 | cb();
193 | }
194 |
195 | function send(form, threadID, callback, replyToMessage) {
196 | if (replyToMessage) {
197 | form.payload.tasks[0].payload.reply_metadata = {
198 | reply_source_id: replyToMessage,
199 | reply_source_type: 1,
200 | reply_type: 0
201 | };
202 | }
203 | const mqttClient = ctx.mqttClient;
204 | form.payload.tasks.forEach((task) => {
205 | task.payload = JSON.stringify(task.payload);
206 | });
207 | form.payload = JSON.stringify(form.payload);
208 | console.log(global.jsonStringifyColor(form, null, 2));
209 |
210 | return mqttClient.publish("/ls_req", JSON.stringify(form), function (err, data) {
211 | if (err) {
212 | console.error('Error publishing message: ', err);
213 | callback(err);
214 | } else {
215 | console.log('Message published successfully with data: ', data);
216 | callback(null, data);
217 | }
218 | });
219 | }
220 |
221 | return function sendMessageMqtt(msg, threadID, callback, replyToMessage) {
222 | if (
223 | !callback &&
224 | (utils.getType(threadID) === "Function" ||
225 | utils.getType(threadID) === "AsyncFunction")
226 | ) {
227 | return threadID({ error: "Pass a threadID as a second argument." });
228 | }
229 | if (
230 | !replyToMessage &&
231 | utils.getType(callback) === "String"
232 | ) {
233 | replyToMessage = callback;
234 | callback = function () { };
235 | }
236 |
237 |
238 | if (!callback) {
239 | callback = function (err, friendList) {
240 | };
241 | }
242 |
243 | var msgType = utils.getType(msg);
244 | var threadIDType = utils.getType(threadID);
245 | var messageIDType = utils.getType(replyToMessage);
246 |
247 | if (msgType !== "String" && msgType !== "Object") {
248 | return callback({
249 | error:
250 | "Message should be of type string or object and not " + msgType + "."
251 | });
252 | }
253 |
254 | if (msgType === "String") {
255 | msg = { body: msg };
256 | }
257 |
258 | const timestamp = Date.now();
259 | // get full date time
260 | const epoch = timestamp << 22;
261 | //const otid = epoch + 0; // TODO replace with randomInt(0, 2**22)
262 | const otid = epoch + Math.floor(Math.random() * 4194304);
263 |
264 | const form = {
265 | app_id: "2220391788200892",
266 | payload: {
267 | tasks: [
268 | {
269 | label: "46",
270 | payload: {
271 | thread_id: threadID.toString(),
272 | otid: otid.toString(),
273 | source: 0,
274 | send_type: 1,
275 | sync_group: 1,
276 | text: msg.body != null && msg.body != undefined ? msg.body.toString() : "",
277 | initiating_source: 1,
278 | skip_url_preview_gen: 0
279 | },
280 | queue_name: threadID.toString(),
281 | task_id: 0,
282 | failure_count: null
283 | },
284 | {
285 | label: "21",
286 | payload: {
287 | thread_id: threadID.toString(),
288 | last_read_watermark_ts: Date.now(),
289 | sync_group: 1
290 | },
291 | queue_name: threadID.toString(),
292 | task_id: 1,
293 | failure_count: null
294 | }
295 | ],
296 | epoch_id: epoch_id(),
297 | version_id: "6120284488008082",
298 | data_trace_id: null
299 | },
300 | request_id: 1,
301 | type: 3
302 | };
303 |
304 | handleEmoji(msg, form, callback, function () {
305 | handleLocation(msg, form, callback, function () {
306 | handleMention(msg, form, callback, function () {
307 | handleSticker(msg, form, callback, function () {
308 | handleAttachment(msg, form, callback, function () {
309 | send(form, threadID, callback, replyToMessage);
310 | });
311 | });
312 | });
313 | });
314 | });
315 | };
316 | };
--------------------------------------------------------------------------------
/src/sendTypingIndicator.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | function makeTypingIndicator(typ, threadID, callback, isGroup) {
8 | const form = {
9 | typ: +typ,
10 | to: "",
11 | source: "mercury-chat",
12 | thread: threadID
13 | };
14 |
15 | // Check if thread is a single person chat or a group chat
16 | // More info on this is in api.sendMessage
17 | if (utils.getType(isGroup) == "Boolean") {
18 | if (!isGroup) {
19 | form.to = threadID;
20 | }
21 | defaultFuncs
22 | .post("https://www.facebook.com/ajax/messaging/typ.php", ctx.jar, form)
23 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
24 | .then(function (resData) {
25 | if (resData.error) {
26 | throw resData;
27 | }
28 |
29 | return callback();
30 | })
31 | .catch(function (err) {
32 | log.error("sendTypingIndicator", err);
33 | if (utils.getType(err) == "Object" && err.error === "Not logged in") {
34 | ctx.loggedIn = false;
35 | }
36 | return callback(err);
37 | });
38 | } else {
39 | api.getUserInfo(threadID, function (err, res) {
40 | if (err) {
41 | return callback(err);
42 | }
43 |
44 | // If id is single person chat
45 | if (Object.keys(res).length > 0) {
46 | form.to = threadID;
47 | }
48 |
49 | defaultFuncs
50 | .post("https://www.facebook.com/ajax/messaging/typ.php", ctx.jar, form)
51 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
52 | .then(function (resData) {
53 | if (resData.error) {
54 | throw resData;
55 | }
56 |
57 | return callback();
58 | })
59 | .catch(function (err) {
60 | log.error("sendTypingIndicator", err);
61 | if (utils.getType(err) == "Object" && err.error === "Not logged in.") {
62 | ctx.loggedIn = false;
63 | }
64 | return callback(err);
65 | });
66 | });
67 | }
68 | }
69 |
70 | return function sendTypingIndicator(threadID, callback, isGroup) {
71 | if (
72 | utils.getType(callback) !== "Function" &&
73 | utils.getType(callback) !== "AsyncFunction"
74 | ) {
75 | if (callback) {
76 | log.warn(
77 | "sendTypingIndicator",
78 | "callback is not a function - ignoring."
79 | );
80 | }
81 | callback = () => { };
82 | }
83 |
84 | makeTypingIndicator(true, threadID, callback, isGroup);
85 |
86 | return function end(cb) {
87 | if (
88 | utils.getType(cb) !== "Function" &&
89 | utils.getType(cb) !== "AsyncFunction"
90 | ) {
91 | if (cb) {
92 | log.warn(
93 | "sendTypingIndicator",
94 | "callback is not a function - ignoring."
95 | );
96 | }
97 | cb = () => { };
98 | }
99 |
100 | makeTypingIndicator(false, threadID, cb, isGroup);
101 | };
102 | };
103 | };
104 |
--------------------------------------------------------------------------------
/src/setMessageReaction.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | return function setMessageReaction(reaction, messageID, callback, forceCustomReaction) {
8 | let resolveFunc = function () { };
9 | let rejectFunc = function () { };
10 | const returnPromise = new Promise(function (resolve, reject) {
11 | resolveFunc = resolve;
12 | rejectFunc = reject;
13 | });
14 |
15 | if (!callback) {
16 | callback = function (err, friendList) {
17 | if (err) {
18 | return rejectFunc(err);
19 | }
20 | resolveFunc(friendList);
21 | };
22 | }
23 |
24 | switch (reaction) {
25 | case "\uD83D\uDE0D": //:heart_eyes:
26 | case "\uD83D\uDE06": //:laughing:
27 | case "\uD83D\uDE2E": //:open_mouth:
28 | case "\uD83D\uDE22": //:cry:
29 | case "\uD83D\uDE20": //:angry:
30 | case "\uD83D\uDC4D": //:thumbsup:
31 | case "\uD83D\uDC4E": //:thumbsdown:
32 | case "\u2764": //:heart:
33 | case "\uD83D\uDC97": //:glowingheart:
34 | case "":
35 | //valid
36 | break;
37 | case ":heart_eyes:":
38 | case ":love:":
39 | reaction = "\uD83D\uDE0D";
40 | break;
41 | case ":laughing:":
42 | case ":haha:":
43 | reaction = "\uD83D\uDE06";
44 | break;
45 | case ":open_mouth:":
46 | case ":wow:":
47 | reaction = "\uD83D\uDE2E";
48 | break;
49 | case ":cry:":
50 | case ":sad:":
51 | reaction = "\uD83D\uDE22";
52 | break;
53 | case ":angry:":
54 | reaction = "\uD83D\uDE20";
55 | break;
56 | case ":thumbsup:":
57 | case ":like:":
58 | reaction = "\uD83D\uDC4D";
59 | break;
60 | case ":thumbsdown:":
61 | case ":dislike:":
62 | reaction = "\uD83D\uDC4E";
63 | break;
64 | case ":heart:":
65 | reaction = "\u2764";
66 | break;
67 | case ":glowingheart:":
68 | reaction = "\uD83D\uDC97";
69 | break;
70 | default:
71 | if (forceCustomReaction) {
72 | break;
73 | }
74 | return callback({ error: "Reaction is not a valid emoji." });
75 | }
76 |
77 | const variables = {
78 | data: {
79 | client_mutation_id: ctx.clientMutationId++,
80 | actor_id: ctx.i_userID || ctx.userID,
81 | action: reaction == "" ? "REMOVE_REACTION" : "ADD_REACTION",
82 | message_id: messageID,
83 | reaction: reaction
84 | }
85 | };
86 |
87 | const qs = {
88 | doc_id: "1491398900900362",
89 | variables: JSON.stringify(variables),
90 | dpr: 1
91 | };
92 |
93 | defaultFuncs
94 | .postFormData(
95 | "https://www.facebook.com/webgraphql/mutation/",
96 | ctx.jar,
97 | {},
98 | qs
99 | )
100 | .then(utils.parseAndCheckLogin(ctx.jar, defaultFuncs))
101 | .then(function (resData) {
102 | if (!resData) {
103 | throw { error: "setReaction returned empty object." };
104 | }
105 | if (resData.error) {
106 | throw resData;
107 | }
108 | callback(null);
109 | })
110 | .catch(function (err) {
111 | log.error("setReaction", err);
112 | return callback(err);
113 | });
114 |
115 | return returnPromise;
116 | };
117 | };
118 |
--------------------------------------------------------------------------------
/src/setPostReaction.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fix by NTKhang
3 | * update as Thursday, 10 February 2022
4 | * do not remove the author name to get more updates
5 | */
6 |
7 | "use strict";
8 |
9 | const utils = require("../utils");
10 | const log = require("npmlog");
11 |
12 | function formatData(resData) {
13 | return {
14 | viewer_feedback_reaction_info: resData.feedback_react.feedback.viewer_feedback_reaction_info,
15 | supported_reactions: resData.feedback_react.feedback.supported_reactions,
16 | top_reactions: resData.feedback_react.feedback.top_reactions.edges,
17 | reaction_count: resData.feedback_react.feedback.reaction_count
18 | };
19 | }
20 |
21 | module.exports = function (defaultFuncs, api, ctx) {
22 | return function setPostReaction(postID, type, callback) {
23 | let resolveFunc = function () { };
24 | let rejectFunc = function () { };
25 | const returnPromise = new Promise(function (resolve, reject) {
26 | resolveFunc = resolve;
27 | rejectFunc = reject;
28 | });
29 |
30 | if (!callback) {
31 | if (utils.getType(type) === "Function" || utils.getType(type) === "AsyncFunction") {
32 | callback = type;
33 | type = 0;
34 | }
35 | else {
36 | callback = function (err, data) {
37 | if (err) {
38 | return rejectFunc(err);
39 | }
40 | resolveFunc(data);
41 | };
42 | }
43 | }
44 |
45 | const map = {
46 | unlike: 0,
47 | like: 1,
48 | heart: 2,
49 | love: 16,
50 | haha: 4,
51 | wow: 3,
52 | sad: 7,
53 | angry: 8
54 | };
55 |
56 | if (utils.getType(type) !== "Number" && utils.getType(type) === "String") {
57 | type = map[type.toLowerCase()];
58 | }
59 |
60 | if (utils.getType(type) !== "Number" && utils.getType(type) !== "String") {
61 | throw {
62 | error: "setPostReaction: Invalid reaction type"
63 | };
64 | }
65 |
66 | if (type != 0 && !type) {
67 | throw {
68 | error: "setPostReaction: Invalid reaction type"
69 | };
70 | }
71 |
72 | const form = {
73 | av: ctx.i_userID || ctx.userID,
74 | fb_api_caller_class: "RelayModern",
75 | fb_api_req_friendly_name: "CometUFIFeedbackReactMutation",
76 | doc_id: "4769042373179384",
77 | variables: JSON.stringify({
78 | input: {
79 | actor_id: ctx.i_userID || ctx.userID,
80 | feedback_id: (new Buffer("feedback:" + postID)).toString("base64"),
81 | feedback_reaction: type,
82 | feedback_source: "OBJECT",
83 | is_tracking_encrypted: true,
84 | tracking: [],
85 | session_id: "f7dd50dd-db6e-4598-8cd9-561d5002b423",
86 | client_mutation_id: Math.round(Math.random() * 19).toString()
87 | },
88 | useDefaultActor: false,
89 | scale: 3
90 | })
91 | };
92 |
93 | defaultFuncs
94 | .post("https://www.facebook.com/api/graphql/", ctx.jar, form)
95 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
96 | .then(function (resData) {
97 | if (resData.errors) {
98 | throw resData;
99 | }
100 | return callback(null, formatData(resData.data));
101 | })
102 | .catch(function (err) {
103 | log.error("setPostReaction", err);
104 | return callback(err);
105 | });
106 |
107 | return returnPromise;
108 | };
109 | };
--------------------------------------------------------------------------------
/src/setTitle.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | return function setTitle(newTitle, threadID, callback) {
8 | if (
9 | !callback &&
10 | (utils.getType(threadID) === "Function" ||
11 | utils.getType(threadID) === "AsyncFunction")
12 | ) {
13 | throw { error: "please pass a threadID as a second argument." };
14 | }
15 |
16 | let resolveFunc = function () { };
17 | let rejectFunc = function () { };
18 | const returnPromise = new Promise(function (resolve, reject) {
19 | resolveFunc = resolve;
20 | rejectFunc = reject;
21 | });
22 |
23 | if (!callback) {
24 | callback = function (err, friendList) {
25 | if (err) {
26 | return rejectFunc(err);
27 | }
28 | resolveFunc(friendList);
29 | };
30 | }
31 |
32 | const messageAndOTID = utils.generateOfflineThreadingID();
33 | const form = {
34 | client: "mercury",
35 | action_type: "ma-type:log-message",
36 | author: "fbid:" + (ctx.i_userID || ctx.userID),
37 | author_email: "",
38 | coordinates: "",
39 | timestamp: Date.now(),
40 | timestamp_absolute: "Today",
41 | timestamp_relative: utils.generateTimestampRelative(),
42 | timestamp_time_passed: "0",
43 | is_unread: false,
44 | is_cleared: false,
45 | is_forward: false,
46 | is_filtered_content: false,
47 | is_spoof_warning: false,
48 | source: "source:chat:web",
49 | "source_tags[0]": "source:chat",
50 | status: "0",
51 | offline_threading_id: messageAndOTID,
52 | message_id: messageAndOTID,
53 | threading_id: utils.generateThreadingID(ctx.clientID),
54 | manual_retry_cnt: "0",
55 | thread_fbid: threadID,
56 | thread_name: newTitle,
57 | thread_id: threadID,
58 | log_message_type: "log:thread-name"
59 | };
60 |
61 | defaultFuncs
62 | .post("https://www.facebook.com/messaging/set_thread_name/", ctx.jar, form)
63 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
64 | .then(function (resData) {
65 | if (resData.error && resData.error === 1545012) {
66 | throw { error: "Cannot change chat title: Not member of chat." };
67 | }
68 |
69 | if (resData.error && resData.error === 1545003) {
70 | throw { error: "Cannot set title of single-user chat." };
71 | }
72 |
73 | if (resData.error) {
74 | throw resData;
75 | }
76 |
77 | return callback();
78 | })
79 | .catch(function (err) {
80 | log.error("setTitle", err);
81 | return callback(err);
82 | });
83 |
84 | return returnPromise;
85 | };
86 | };
87 |
--------------------------------------------------------------------------------
/src/threadColors.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | module.exports = function (_defaultFuncs, _api, _ctx) {
4 | // Currently the only colors that can be passed to api.changeThreadColor(); may change if Facebook adds more
5 | return {
6 | //Old hex colors.
7 | ////MessengerBlue: null,
8 | ////Viking: "#44bec7",
9 | ////GoldenPoppy: "#ffc300",
10 | ////RadicalRed: "#fa3c4c",
11 | ////Shocking: "#d696bb",
12 | ////PictonBlue: "#6699cc",
13 | ////FreeSpeechGreen: "#13cf13",
14 | ////Pumpkin: "#ff7e29",
15 | ////LightCoral: "#e68585",
16 | ////MediumSlateBlue: "#7646ff",
17 | ////DeepSkyBlue: "#20cef5",
18 | ////Fern: "#67b868",
19 | ////Cameo: "#d4a88c",
20 | ////BrilliantRose: "#ff5ca1",
21 | ////BilobaFlower: "#a695c7"
22 |
23 | //#region This part is for backward compatibly
24 | //trying to match the color one-by-one. kill me plz
25 | MessengerBlue: "196241301102133", //DefaultBlue
26 | Viking: "1928399724138152", //TealBlue
27 | GoldenPoppy: "174636906462322", //Yellow
28 | RadicalRed: "2129984390566328", //Red
29 | Shocking: "2058653964378557", //LavenderPurple
30 | FreeSpeechGreen: "2136751179887052", //Green
31 | Pumpkin: "175615189761153", //Orange
32 | LightCoral: "980963458735625", //CoralPink
33 | MediumSlateBlue: "234137870477637", //BrightPurple
34 | DeepSkyBlue: "2442142322678320", //AquaBlue
35 | BrilliantRose: "169463077092846", //HotPink
36 | //i've tried my best, everything else can't be mapped. (or is it?) -UIRI 2020
37 | //#endregion
38 |
39 | DefaultBlue: "196241301102133",
40 | HotPink: "169463077092846",
41 | AquaBlue: "2442142322678320",
42 | BrightPurple: "234137870477637",
43 | CoralPink: "980963458735625",
44 | Orange: "175615189761153",
45 | Green: "2136751179887052",
46 | LavenderPurple: "2058653964378557",
47 | Red: "2129984390566328",
48 | Yellow: "174636906462322",
49 | TealBlue: "1928399724138152",
50 | Aqua: "417639218648241",
51 | Mango: "930060997172551",
52 | Berry: "164535220883264",
53 | Citrus: "370940413392601",
54 | Candy: "205488546921017",
55 |
56 | /**
57 | * July 06, 2022
58 | * added by @NTKhang
59 | */
60 | Earth: "1833559466821043",
61 | Support: "365557122117011",
62 | Music: "339021464972092",
63 | Pride: "1652456634878319",
64 | DoctorStrange: "538280997628317",
65 | LoFi: "1060619084701625",
66 | Sky: "3190514984517598",
67 | LunarNewYear: "357833546030778",
68 | Celebration: "627144732056021",
69 | Chill: "390127158985345",
70 | StrangerThings: "1059859811490132",
71 | Dune: "1455149831518874",
72 | Care: "275041734441112",
73 | Astrology: "3082966625307060",
74 | JBalvin: "184305226956268",
75 | Birthday: "621630955405500",
76 | Cottagecore: "539927563794799",
77 | Ocean: "736591620215564",
78 | Love: "741311439775765",
79 | TieDye: "230032715012014",
80 | Monochrome: "788274591712841",
81 | Default: "3259963564026002",
82 | Rocket: "582065306070020",
83 | Berry2: "724096885023603",
84 | Candy2: "624266884847972",
85 | Unicorn: "273728810607574",
86 | Tropical: "262191918210707",
87 | Maple: "2533652183614000",
88 | Sushi: "909695489504566",
89 | Citrus2: "557344741607350",
90 | Lollipop: "280333826736184",
91 | Shadow: "271607034185782",
92 | Rose: "1257453361255152",
93 | Lavender: "571193503540759",
94 | Tulip: "2873642949430623",
95 | Classic: "3273938616164733",
96 | Peach: "3022526817824329",
97 | Honey: "672058580051520",
98 | Kiwi: "3151463484918004",
99 | Grape: "193497045377796",
100 |
101 | /**
102 | * July 15, 2022
103 | * added by @NTKhang
104 | */
105 | NonBinary: "737761000603635",
106 |
107 | /**
108 | * November 25, 2022
109 | * added by @NTKhang
110 | */
111 | ThankfulForFriends: "1318983195536293",
112 | Transgender: "504518465021637",
113 | TaylorSwift: "769129927636836",
114 | NationalComingOutDay: "788102625833584",
115 | Autumn: "822549609168155",
116 | Cyberpunk2077: "780962576430091",
117 |
118 | /**
119 | * May 13, 2023
120 | */
121 | MothersDay: "1288506208402340",
122 | APAHM: "121771470870245",
123 | Parenthood: "810978360551741",
124 | StarWars: "1438011086532622",
125 | GuardianOfTheGalaxy: "101275642962533",
126 | Bloom: "158263147151440",
127 | BubbleTea: "195296273246380",
128 | Basketball: "6026716157422736",
129 | ElephantsAndFlowers: "693996545771691"
130 | };
131 | };
132 |
--------------------------------------------------------------------------------
/src/unfriend.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | return function unfriend(userID, callback) {
8 | let resolveFunc = function () { };
9 | let rejectFunc = function () { };
10 | const returnPromise = new Promise(function (resolve, reject) {
11 | resolveFunc = resolve;
12 | rejectFunc = reject;
13 | });
14 |
15 | if (!callback) {
16 | callback = function (err, friendList) {
17 | if (err) {
18 | return rejectFunc(err);
19 | }
20 | resolveFunc(friendList);
21 | };
22 | }
23 |
24 | const form = {
25 | uid: userID,
26 | unref: "bd_friends_tab",
27 | floc: "friends_tab",
28 | "nctr[_mod]": "pagelet_timeline_app_collection_" + (ctx.i_userID || ctx.userID) + ":2356318349:2"
29 | };
30 |
31 | defaultFuncs
32 | .post(
33 | "https://www.facebook.com/ajax/profile/removefriendconfirm.php",
34 | ctx.jar,
35 | form
36 | )
37 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
38 | .then(function (resData) {
39 | if (resData.error) {
40 | throw resData;
41 | }
42 |
43 | return callback(null, true);
44 | })
45 | .catch(function (err) {
46 | log.error("unfriend", err);
47 | return callback(err);
48 | });
49 |
50 | return returnPromise;
51 | };
52 | };
53 |
--------------------------------------------------------------------------------
/src/unsendMessage.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../utils");
4 | const log = require("npmlog");
5 |
6 | module.exports = function (defaultFuncs, api, ctx) {
7 | return function unsendMessage(messageID, callback) {
8 | let resolveFunc = function () { };
9 | let rejectFunc = function () { };
10 | const returnPromise = new Promise(function (resolve, reject) {
11 | resolveFunc = resolve;
12 | rejectFunc = reject;
13 | });
14 |
15 | if (!callback) {
16 | callback = function (err, friendList) {
17 | if (err) {
18 | return rejectFunc(err);
19 | }
20 | resolveFunc(friendList);
21 | };
22 | }
23 |
24 | const form = {
25 | message_id: messageID
26 | };
27 |
28 | defaultFuncs
29 | .post(
30 | "https://www.facebook.com/messaging/unsend_message/",
31 | ctx.jar,
32 | form
33 | )
34 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
35 | .then(function (resData) {
36 | if (resData.error) {
37 | throw resData;
38 | }
39 |
40 | return callback();
41 | })
42 | .catch(function (err) {
43 | log.error("unsendMessage", err);
44 | return callback(err);
45 | });
46 |
47 | return returnPromise;
48 | };
49 | };
50 |
--------------------------------------------------------------------------------
/src/uploadAttachment.js:
--------------------------------------------------------------------------------
1 | const utils = require("../utils");
2 | const log = require("npmlog");
3 |
4 | module.exports = function (defaultFuncs, api, ctx) {
5 | function upload(attachments, callback) {
6 | callback = callback || function () { };
7 | const uploads = [];
8 |
9 | // create an array of promises
10 | for (let i = 0; i < attachments.length; i++) {
11 | if (!utils.isReadableStream(attachments[i])) {
12 | throw {
13 | error:
14 | "Attachment should be a readable stream and not " +
15 | utils.getType(attachments[i]) +
16 | "."
17 | };
18 | }
19 |
20 | const form = {
21 | upload_1024: attachments[i],
22 | voice_clip: "true"
23 | };
24 |
25 | uploads.push(
26 | defaultFuncs
27 | .postFormData(
28 | "https://upload.facebook.com/ajax/mercury/upload.php",
29 | ctx.jar,
30 | form,
31 | {}
32 | )
33 | .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
34 | .then(function (resData) {
35 | if (resData.error) {
36 | throw resData;
37 | }
38 |
39 | // We have to return the data unformatted unless we want to change it
40 | // back in sendMessage.
41 | return resData.payload.metadata[0];
42 | })
43 | );
44 | }
45 |
46 | // resolve all promises
47 | Promise
48 | .all(uploads)
49 | .then(function (resData) {
50 | callback(null, resData);
51 | })
52 | .catch(function (err) {
53 | log.error("uploadAttachment", err);
54 | return callback(err);
55 | });
56 | }
57 |
58 | return function uploadAttachment(attachments, callback) {
59 | if (
60 | !attachments &&
61 | !utils.isReadableStream(attachments) &&
62 | !utils.getType(attachments) === "Array" &&
63 | (utils.getType(attachments) === "Array" && !attachments.length)
64 | )
65 | throw { error: "Please pass an attachment or an array of attachments." };
66 |
67 | let resolveFunc = function () { };
68 | let rejectFunc = function () { };
69 | const returnPromise = new Promise(function (resolve, reject) {
70 | resolveFunc = resolve;
71 | rejectFunc = reject;
72 | });
73 |
74 | if (!callback) {
75 | callback = function (err, info) {
76 | if (err) {
77 | return rejectFunc(err);
78 | }
79 | resolveFunc(info);
80 | };
81 | }
82 |
83 | if (utils.getType(attachments) !== "Array")
84 | attachments = [attachments];
85 |
86 | upload(attachments, (err, info) => {
87 | if (err) {
88 | return callback(err);
89 | }
90 | callback(null, info);
91 | });
92 |
93 | return returnPromise;
94 | };
95 | };
--------------------------------------------------------------------------------
/test/data/shareAttach.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "delta": {
3 | "attachments": [
4 | {
5 | "fbid": "1522004821162174",
6 | "id": "1522004821162174",
7 | "mercury": {
8 | "app_attribution": null,
9 | "attach_type": "share",
10 | "name": null,
11 | "url": null,
12 | "rel": null,
13 | "preview_url": null,
14 | "preview_width": null,
15 | "preview_height": null,
16 | "large_preview_url": null,
17 | "large_preview_width": null,
18 | "large_preview_height": null,
19 | "icon_type": null,
20 | "metadata": null,
21 | "thumbnail_url": null,
22 | "share": {
23 | "description": null,
24 | "media": {
25 | "animated_image": null,
26 | "animated_image_size": {
27 | "height": null,
28 | "width": null
29 | },
30 | "image": null,
31 | "image_size": {
32 | "height": null,
33 | "width": null
34 | },
35 | "duration": null,
36 | "playable": null,
37 | "source": null
38 | },
39 | "source": "Dimon - testing",
40 | "style_list": [
41 | "business_message_items", "fallback"
42 | ],
43 | "title": "search engines",
44 | "properties": null,
45 | "uri": null,
46 | "subattachments": [],
47 | "deduplication_key": "abcde",
48 | "action_links": [],
49 | "share_id": "1522004821162174",
50 | "target": {
51 | "call_to_actions": [],
52 | "items": [
53 | {
54 | "id": "629934437209008",
55 | "name": "search engines",
56 | "desc": "",
57 | "thumb_url": null,
58 | "item_url": null,
59 | "title": "search engines",
60 | "text": "",
61 | "source": null,
62 | "metalines": {
63 | "metaline_1": "click to get redirected",
64 | "metaline_2": null,
65 | "metaline_3": null
66 | },
67 | "location": 12314,
68 | "category": 69,
69 | "call_to_actions": [
70 | {
71 | "action_link": "http://l.facebook.com/l.php?u=http%3A%2F%2Fgoogle.com%2F&h=ATNziCq_-6I3ZPYwwLluFdCrWMEwLLKvokFlXdEdS4LD2Lzsv2cR2SJYffJcDYBfB092Xeq8oRdftJk4husEYVduH24RnlP3HvVQOkOrciXDs2M7TkWYyNLBelvJ2Fc-mw8pbGy5NslGf_fkZ_A",
72 | "action_type": 2,
73 | "id": "FFD=",
74 | "title": "Google",
75 | "link_target_ids": [629934437209008],
76 | "is_mutable_by_server": false,
77 | "should_show_user_confirmation": false,
78 | "confirmation_title": null,
79 | "confirmation_message": null,
80 | "confirmation_continue_label": null,
81 | "confirmation_cancel_label": null,
82 | "payment_metadata": {
83 | "total_price": null,
84 | "payment_module_config": null
85 | },
86 | "is_disabled": false
87 | }, {
88 | "action_link": "http://l.facebook.com/l.php?u=http%3A%2F%2Fyahoo.com%2F&h=ATNIuTf7iDGP5xXTWOAdhaGhRFfDf4eS09t_G9CrR0MDiBKpqtCDzPf_9y5Bq7TXMgmo6RttztsgeO0ReSc0PDvJDTa1fLMMK2CjrpkqC91_m-yaMXfeQ4aI6MbhZrOPnK3YFnQP4XvRx3N1udE",
89 | "action_type": 2,
90 | "id": "CDE=",
91 | "title": "Yahoo",
92 | "link_target_ids": [629934437209008],
93 | "is_mutable_by_server": false,
94 | "should_show_user_confirmation": false,
95 | "confirmation_title": null,
96 | "confirmation_message": null,
97 | "confirmation_continue_label": null,
98 | "confirmation_cancel_label": null,
99 | "payment_metadata": {
100 | "total_price": null,
101 | "payment_module_config": null
102 | },
103 | "is_disabled": false
104 | }, {
105 | "action_link": "http://l.facebook.com/l.php?u=http%3A%2F%2Fbing.com%2F&h=ATMoMijAt6Da6WWIQ679DhZyZizWdxAViWwyl-RjKobFUG_x8GmB8LD6pPa3KP5K1-QTL9vuaFwjqB0itaMFWk4VwQ9uh56JgnbFnAo4qM_CrQufgLeHwwCnWSCnZt8IzYT4y6YULLLFA5bL1H4",
106 | "action_type": 2,
107 | "id": "ABC=",
108 | "title": "Bing",
109 | "link_target_ids": [629934437209008],
110 | "is_mutable_by_server": false,
111 | "should_show_user_confirmation": false,
112 | "confirmation_title": null,
113 | "confirmation_message": null,
114 | "confirmation_continue_label": null,
115 | "confirmation_cancel_label": null,
116 | "payment_metadata": {
117 | "total_price": null,
118 | "payment_module_config": null
119 | },
120 | "is_disabled": false
121 | }
122 | ]
123 | }
124 | ],
125 | "location": 132145,
126 | "category": 69,
127 | "message": "Aaa: search engines"
128 | }
129 | }
130 | },
131 | "otherUserFbIds": ["1521994257829897"],
132 | "titanType": 1
133 | }
134 | ],
135 | "messageMetadata": {
136 | "actorFbId": "1345",
137 | "messageId": "mid.12345:asdv",
138 | "offlineThreadingId": "1345v1345",
139 | "tags": ["source:messenger:commerce"],
140 | "threadKey": {
141 | "otherUserFbId": "13451345"
142 | },
143 | "timestamp": "1487078180265"
144 | }
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/test/data/test.txt:
--------------------------------------------------------------------------------
1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut placerat risus massa, eu rutrum massa tempus id. Aenean aliquet turpis at risus gravida, id scelerisque sem vestibulum. Aliquam erat volutpat. Pellentesque ut justo a sapien fringilla tincidunt ornare ac arcu. Nam non finibus turpis, eget tincidunt turpis. Morbi sed tempus leo. Aliquam ut nunc sed ante efficitur tristique et sed eros.
2 |
3 | In eu tincidunt libero, eget tincidunt mauris. Donec ultrices placerat tincidunt. Sed ultrices neque dui, id viverra ante porta sed. Suspendisse tincidunt malesuada finibus. Ut cursus dolor sem, eu mattis lectus euismod a. In porttitor maximus lacus, eget volutpat mauris pretium at. Nulla consequat ipsum id enim fermentum feugiat. Fusce convallis bibendum massa ac viverra.
4 |
5 | Sed a vehicula diam, et sollicitudin nunc. Quisque nec libero sit amet nibh fringilla pretium at vel massa. In enim dolor, euismod sed sapien id, accumsan tempus lacus. Aenean dapibus nulla at libero ultricies, id sagittis erat pretium. Nam iaculis tellus est, lobortis lacinia dui egestas vitae. Phasellus elementum quis lectus nec tincidunt. Ut gravida vestibulum ipsum ut cursus.
6 |
7 | Mauris quam est, dignissim sed quam at, vulputate scelerisque purus. Maecenas tortor turpis, venenatis non purus et, finibus venenatis augue. Etiam et fringilla enim. Suspendisse a leo sed ex aliquet feugiat vitae nec magna. Vestibulum id massa in orci dictum ultricies. Vestibulum vitae leo sed lacus tempor dapibus. Cras viverra lorem sit amet magna imperdiet sodales. In sollicitudin ex sed feugiat commodo. Maecenas ac arcu tristique quam euismod ultrices quis et mi. Nulla lacinia sit amet lacus nec ultrices. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; In hac habitasse platea dictumst. Curabitur vehicula, enim at vulputate bibendum, lorem tortor pellentesque massa, nec aliquam lacus mi ac libero. In vel nibh in ante facilisis tristique. Aliquam sapien purus, lobortis quis ultricies nec, dictum a turpis. Aenean pharetra congue lacus, id cursus erat fringilla congue.
8 |
--------------------------------------------------------------------------------
/test/example-config.json:
--------------------------------------------------------------------------------
1 | // Instructions: Copy this file to test-config.json, fill in your test data
2 | // and remove all comments (JSON doesn't support comments).
3 | // Run the test with `npm test` after installing the devDependencies (mocha).
4 | {
5 | // Test user login information
6 | "user" : {
7 | "id" : "00000000000000",
8 | "email" : "example@test.com",
9 | "password" : "qwerty"
10 | },
11 | // Array of at least 2 other user IDs (not the same as the test user)
12 | "userIDs" : [
13 | "11111111111111",
14 | "22222222222222"
15 | ],
16 | // Id of page to which test user is an admin
17 | "pageID": "3333333333333"
18 | }
19 |
--------------------------------------------------------------------------------
/test/test-page.js:
--------------------------------------------------------------------------------
1 | var login = require('../index.js');
2 | var fs = require('fs');
3 | var assert = require('assert');
4 |
5 | var conf = JSON.parse(process.env.testconfig || fs.readFileSync('test/test-config.json', 'utf8'));
6 | var credentials = {
7 | email: conf.user.email,
8 | password: conf.user.password,
9 | };
10 |
11 | var userIDs = conf.userIDs;
12 |
13 | var options = {
14 | selfListen: true,
15 | listenEvents: true,
16 | logLevel: "silent",
17 | pageID: conf.pageID
18 | };
19 | var getType = require('../utils').getType;
20 |
21 | var userID = conf.user.id;
22 |
23 | var groupChatID;
24 | var groupChatName;
25 |
26 | function checkErr(done){
27 | return function(err) {
28 | if (err) done(err);
29 | };
30 | }
31 |
32 | // describe('Login As Page:', function() {
33 | // var api = null;
34 | // process.on('SIGINT', () => api && !api.logout() && console.log("Logged out :)"));
35 | // var tests = [];
36 | // var stopListening;
37 | // this.timeout(20000);
38 |
39 | // function listen(done, matcher) {
40 | // tests.push({matcher:matcher, done:done});
41 | // }
42 |
43 | // before(function(done) {
44 | // login(credentials, options, function (err, localAPI) {
45 | // if(err) return done(err);
46 |
47 | // assert(localAPI);
48 | // api = localAPI;
49 | // stopListening = api.listen(function (err, msg) {
50 | // if (err) throw err;
51 | // // Removes matching function and calls corresponding done
52 | // tests = tests.filter(function(test) {
53 | // return !(test.matcher(msg) && (test.done() || true));
54 | // });
55 | // });
56 |
57 | // done();
58 | // });
59 | // });
60 |
61 | // it('should login without error', function (){
62 | // assert(api);
63 | // });
64 |
65 | // it('should get the right user ID', function (){
66 | // assert(userID == api.getCurrentUserID());
67 | // });
68 |
69 | // it('should send text message object (user)', function (done){
70 | // var body = "text-msg-obj-" + Date.now();
71 | // listen(done, msg =>
72 | // msg.type === 'message' &&
73 | // msg.body === body &&
74 | // msg.isGroup === false
75 | // );
76 | // api.sendMessage({body: body}, userID, checkErr(done));
77 | // });
78 |
79 | // it('should send sticker message object (user)', function (done){
80 | // var stickerID = '767334526626290';
81 | // listen(done, msg =>
82 | // msg.type === 'message' &&
83 | // msg.attachments.length > 0 &&
84 | // msg.attachments[0].type === 'sticker' &&
85 | // msg.attachments[0].stickerID === stickerID &&
86 | // msg.isGroup === false
87 | // );
88 | // api.sendMessage({sticker: stickerID}, userID, checkErr(done));
89 | // });
90 |
91 | // it('should send basic string (user)', function (done){
92 | // var body = "basic-str-" + Date.now();
93 | // listen(done, msg =>
94 | // msg.type === 'message' &&
95 | // msg.body === body &&
96 | // msg.isGroup === false
97 | // );
98 | // api.sendMessage(body, userID, checkErr(done));
99 | // });
100 |
101 | // it('should send typing indicator', function (done) {
102 | // var stopType = api.sendTypingIndicator(userID, function(err) {
103 | // checkErr(done)(err);
104 | // stopType();
105 | // done();
106 | // });
107 | // });
108 |
109 | // it('should get the right user info', function (done) {
110 | // api.getUserInfo(userID, function(err, data) {
111 | // checkErr(done)(err);
112 | // var user = data[userID];
113 | // assert(user.name);
114 | // assert(user.firstName);
115 | // assert(user.vanity !== null);
116 | // assert(user.profileUrl);
117 | // assert(user.gender);
118 | // assert(user.type);
119 | // assert(!user.isFriend);
120 | // done();
121 | // });
122 | // });
123 |
124 | // it('should get the list of friends', function (done) {
125 | // api.getFriendsList(function(err, data) {
126 | // checkErr(done)(err);
127 | // assert(getType(data) === "Array");
128 | // data.map(function(v) {parseInt(v);});
129 | // done();
130 | // });
131 | // });
132 |
133 | // it('should log out', function (done) {
134 | // api.logout(done);
135 | // });
136 |
137 | // after(function (){
138 | // if (stopListening) stopListening();
139 | // });
140 | // });
141 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | var login = require('../index.js');
2 | var fs = require('fs');
3 | var assert = require('assert');
4 |
5 | var conf = JSON.parse(process.env.testconfig || fs.readFileSync('test/test-config.json', 'utf8')); console.debug({conf});
6 | var appState = fs.existsSync('test/appstate.json') ? JSON.parse(fs.readFileSync('test/appstate.json', 'utf8')) : null;
7 | var credentials = appState ? {appState } : {
8 | email: conf.user.email,
9 | password: conf.user.password,
10 | };
11 |
12 | var userIDs = conf.userIDs;
13 |
14 | var options = { selfListen: true, listenEvents: true, logLevel: "silent"};
15 | var pageOptions = {logLevel: 'silent', pageID: conf.pageID};
16 | var getType = require('../utils').getType;
17 | var formatDeltaMessage = require('../utils').formatDeltaMessage;
18 | var shareAttachmentFixture = require('./data/shareAttach');
19 |
20 | var userID = conf.user.id;
21 |
22 | var groupChatID;
23 | var groupChatName;
24 |
25 | function checkErr(done){
26 | return function(err) {
27 | if (err) done(err);
28 | };
29 | }
30 |
31 | describe('Login:', function() {
32 | var api = null;
33 | process.on('SIGINT', () => api && !api.logout() && console.log("Logged out :)"));
34 | var tests = [];
35 | var stopListening;
36 | this.timeout(20000);
37 |
38 | function listen(done, matcher) {
39 | tests.push({matcher:matcher, done:done});
40 | }
41 |
42 | before(function(done) {
43 | console.debug({credentials});
44 | login(credentials, options, function (err, localAPI) {
45 | if(err) return done(err);
46 |
47 | assert(localAPI);
48 | api = localAPI;
49 | stopListening = api.listen(function (err, msg) {
50 | if (err) throw err;
51 | if (msg.type === "message") {
52 | assert(msg.senderID && !isNaN(msg.senderID));
53 | assert(msg.threadID && !isNaN(msg.threadID));
54 | assert(msg.timestamp && !isNaN(msg.timestamp));
55 | assert(msg.messageID != null && msg.messageID.length > 0);
56 | assert(msg.body != null || msg.attachments.length > 0);
57 | }
58 | // Removes matching function and calls corresponding done
59 | tests = tests.filter(function(test) {
60 | return !(test.matcher(msg) && (test.done() || true));
61 | });
62 | });
63 |
64 | done();
65 | });
66 | });
67 |
68 | it('should login without error', function (){
69 | assert(api);
70 | });
71 |
72 | it('should get the current user ID', function (){
73 | assert(userID === api.getCurrentUserID());
74 | });
75 |
76 | it('should send text message object (user)', function (done){
77 | var body = "text-msg-obj-" + Date.now();
78 | listen(done, msg =>
79 | msg.type === 'message' &&
80 | msg.body === body &&
81 | msg.isGroup === false
82 | );
83 | api.sendMessage({body: body}, userID, checkErr(done));
84 | });
85 |
86 | it('should send sticker message object (user)', function (done){
87 | var stickerID = '767334526626290';
88 | listen(done, msg =>
89 | msg.type === 'message' &&
90 | msg.attachments.length > 0 &&
91 | msg.attachments[0].type === 'sticker' &&
92 | msg.attachments[0].stickerID === stickerID &&
93 | msg.isGroup === false
94 | );
95 | api.sendMessage({sticker: stickerID}, userID, checkErr(done));
96 | });
97 |
98 | it('should send basic string (user)', function (done){
99 | var body = "basic-str-" + Date.now();
100 | listen(done, msg =>
101 | msg.type === 'message' &&
102 | msg.body === body &&
103 | msg.isGroup === false
104 | );
105 | api.sendMessage(body, userID, checkErr(done));
106 | });
107 |
108 | it('should get thread info (user)', function (done){
109 | api.getThreadInfo(userID, (err, info) => {
110 | if (err) done(err);
111 |
112 | assert(info.participantIDs != null && info.participantIDs.length > 0);
113 | assert(!info.participantIDs.some(isNaN));
114 | assert(!info.participantIDs.some(v => v.length == 0));
115 | assert(info.name != null);
116 | assert(info.messageCount != null && !isNaN(info.messageCount));
117 | assert(info.hasOwnProperty('emoji'));
118 | assert(info.hasOwnProperty('nicknames'));
119 | assert(info.hasOwnProperty('color'));
120 | done();
121 | });
122 | });
123 |
124 |
125 | it('should get the history of the chat (user)', function (done) {
126 | api.getThreadHistory(userID, 5, null, function(err, data) {
127 | checkErr(done)(err);
128 | assert(getType(data) === "Array");
129 | assert(data.every(function(v) {return getType(v) == "Object";}));
130 | done();
131 | });
132 | });
133 |
134 | it('should get the history of the chat (user) (graphql)', function (done) {
135 | api.getThreadHistoryGraphQL(userID, 5, null, function(err, data) {
136 | checkErr(done)(err);
137 | assert(getType(data) === "Array");
138 | assert(data.every(function(v) {return getType(v) == "Object";}));
139 | done();
140 | });
141 | });
142 |
143 | it('should create a chat', function (done){
144 | var body = "new-chat-" + Date.now();
145 | var inc = 0;
146 |
147 | function doneHack(){
148 | if (inc === 1) return done();
149 | inc++;
150 | }
151 |
152 | listen(doneHack, msg =>
153 | msg.type === 'message' && msg.body === body
154 | );
155 | api.sendMessage(body, userIDs, function(err, info){
156 | checkErr(done)(err);
157 | groupChatID = info.threadID;
158 | doneHack();
159 | });
160 | });
161 |
162 | it('should send text message object (group)', function (done){
163 | var body = "text-msg-obj-" + Date.now();
164 | listen(done, msg =>
165 | msg.type === 'message' &&
166 | msg.body === body &&
167 | msg.isGroup === true
168 | );
169 | api.sendMessage({body: body}, groupChatID, function(err, info){
170 | checkErr(done)(err);
171 | assert(groupChatID === info.threadID);
172 | });
173 | });
174 |
175 | it('should send basic string (group)', function (done){
176 | var body = "basic-str-" + Date.now();
177 | listen(done, msg =>
178 | msg.type === 'message' &&
179 | msg.body === body &&
180 | msg.isGroup === true
181 | );
182 | api.sendMessage(body, groupChatID, function(err, info) {
183 | checkErr(done)(err);
184 | assert(groupChatID === info.threadID);
185 | });
186 | });
187 |
188 | it('should send sticker message object (group)', function (done){
189 | var stickerID = '767334526626290';
190 | listen(done, function (msg) {
191 | return msg.type === 'message' &&
192 | msg.attachments.length > 0 &&
193 | msg.attachments[0].type === 'sticker' &&
194 | msg.attachments[0].stickerID === stickerID;
195 | });
196 | api.sendMessage({sticker: stickerID}, groupChatID, function (err, info) {
197 | assert(groupChatID === info.threadID);
198 | checkErr(done)(err);
199 | });
200 | });
201 |
202 | it('should send an attachment with a body (group)', function (done){
203 | var body = "attach-" + Date.now();
204 | var attach = [];
205 | attach.push(fs.createReadStream("test/data/test.txt"));
206 | attach.push(fs.createReadStream("test/data/test.png"));
207 | listen(done, function (msg) {
208 | return msg.type === 'message' && msg.body === body;
209 | });
210 | api.sendMessage({attachment: attach, body: body}, groupChatID, function(err, info){
211 | checkErr(done)(err);
212 | assert(groupChatID === info.threadID);
213 | });
214 | });
215 |
216 | it('should get the history of the chat (group)', function (done) {
217 | api.getThreadHistory(groupChatID, 5, null, function(err, data) {
218 | checkErr(done)(err);
219 | assert(getType(data) === "Array");
220 | assert(data.every(function(v) {return getType(v) == "Object";}));
221 | done();
222 | });
223 | });
224 |
225 | it('should get the history of the chat (group) (graphql)', function (done) {
226 | api.getThreadHistoryGraphQL(groupChatID, 5, null, function(err, data) {
227 | checkErr(done)(err);
228 | assert(getType(data) === "Array");
229 | assert(data.every(function(v) {return getType(v) == "Object";}));
230 | done();
231 | });
232 | });
233 |
234 |
235 | it('should change chat title', function (done){
236 | var title = 'test-chat-title-' + Date.now();
237 | listen(done, function (msg) {
238 | return msg.type === 'event' &&
239 | msg.logMessageType === 'log:thread-name' &&
240 | msg.logMessageData.name === title;
241 | });
242 | groupChatName = title;
243 | api.setTitle(title, groupChatID, checkErr(done));
244 | });
245 |
246 | it('should kick user', function (done) {
247 | var id = userIDs[0];
248 | listen(done, function (msg) {
249 | return msg.type === 'event' &&
250 | msg.logMessageType === 'log:unsubscribe' &&
251 | msg.logMessageData.leftParticipantFbId === id;
252 | });
253 | api.removeUserFromGroup(id, groupChatID, checkErr(done));
254 | });
255 |
256 | it('should add user', function (done) {
257 | var id = userIDs[0];
258 | listen(done, function (msg) {
259 | return (msg.type === 'event' &&
260 | msg.logMessageType === 'log:subscribe' &&
261 | msg.logMessageData.addedParticipants.length > 0 &&
262 | msg.logMessageData.addedParticipants[0].userFbId === id);
263 | });
264 | // TODO: we don't check for errors inside this because FB changed and
265 | // returns an error, even though we receive the event that the user was
266 | // added
267 | api.addUserToGroup(id, groupChatID, function() {});
268 | });
269 |
270 | xit('should get thread info (group)', function (done){
271 | api.getThreadInfo(groupChatID, (err, info) => {
272 | if (err) done(err);
273 |
274 | assert(info.participantIDs != null && info.participantIDs.length > 0);
275 | assert(!info.participantIDs.some(isNaN));
276 | assert(!info.participantIDs.some(v => v.length == 0));
277 | assert(info.name != null);
278 | assert(info.messageCount != null && !isNaN(info.messageCount));
279 | assert(info.hasOwnProperty('emoji'));
280 | assert(info.hasOwnProperty('nicknames'));
281 | assert(info.hasOwnProperty('color'));
282 | done();
283 | });
284 | });
285 |
286 | it('should retrieve a list of threads', function (done) {
287 | api.getThreadList(0, 20, function(err, res) {
288 | checkErr(done)(err);
289 |
290 | // This checks to see if the group chat we just made
291 | // is in the list... it should be.
292 | assert(res.some(function (v) {
293 | return (
294 | v.threadID === groupChatID &&
295 | userIDs.every(function (val) {
296 | return v.participants.indexOf(val) > -1;
297 | }) &&
298 | v.name === groupChatName
299 | );
300 | }));
301 | done();
302 | });
303 | });
304 |
305 | it('should mark as read', function (done){
306 | api.markAsRead(groupChatID, done);
307 | });
308 |
309 | it('should send typing indicator', function (done) {
310 | var stopType = api.sendTypingIndicator(groupChatID, function(err) {
311 | checkErr(done)(err);
312 | stopType();
313 | done();
314 | });
315 | });
316 |
317 |
318 | it('should get the right user info', function (done) {
319 | api.getUserInfo(userID, function(err, data) {
320 | checkErr(done)(err);
321 | var user = data[userID];
322 | assert(user.name);
323 | assert(user.firstName);
324 | assert(user.vanity !== null);
325 | assert(user.profileUrl);
326 | assert(user.gender);
327 | assert(user.type);
328 | assert(!user.isFriend);
329 | done();
330 | });
331 | });
332 |
333 | it('should get the user ID', function(done) {
334 | api.getUserInfo(userIDs[0], function(err, data) {
335 | checkErr(done)(err);
336 | var user = data[userIDs[0]];
337 | api.getUserID(user.name, function(err, data) {
338 | checkErr(done)(err);
339 | assert(getType(data) === "Array");
340 | assert(data.some(function(val) {
341 | return val.userID === userIDs[0];
342 | }));
343 | done();
344 | });
345 | });
346 | });
347 |
348 | it('should get the list of friends', function (done) {
349 | api.getFriendsList(function(err, data) {
350 | try {
351 | checkErr(done)(err);
352 | assert(getType(data) === "Array");
353 | data.map(v => {
354 | assert(getType(v.firstName) === "String");
355 | assert(getType(v.gender) === "String");
356 | assert(getType(v.userID) === "String");
357 | assert(getType(v.isFriend) === "Boolean");
358 | assert(getType(v.fullName) === "String");
359 | assert(getType(v.profilePicture) === "String");
360 | assert(getType(v.type) === "String");
361 | assert(v.hasOwnProperty("profileUrl")); // This can be null if the account is disabled
362 | assert(getType(v.isBirthday) === "Boolean");
363 | })
364 | done();
365 | } catch(e){
366 | done(e);
367 | }
368 | });
369 | });
370 |
371 | it('should parse share attachment correctly', function () {
372 | var formatted = formatDeltaMessage(shareAttachmentFixture);
373 | assert(formatted.attachments[0].type === "share");
374 | assert(formatted.attachments[0].title === "search engines");
375 | assert(formatted.attachments[0].target.items[0].name === "search engines");
376 | assert(formatted.attachments[0].target.items[0].call_to_actions.length === 3);
377 | assert(formatted.attachments[0].target.items[0].call_to_actions[0].title === "Google");
378 | });
379 |
380 | it('should log out', function (done) {
381 | api.logout(done);
382 | });
383 |
384 | after(function (){
385 | if (stopListening) stopListening();
386 | });
387 | });
388 |
--------------------------------------------------------------------------------