├── .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 | version 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 | screen shot 2016-11-04 at 14 36 00 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 | --------------------------------------------------------------------------------