mirror of
https://github.com/tiennm99/FBcount.git
synced 2026-09-04 16:16:19 +00:00
Init
This commit is contained in:
+6
@@ -0,0 +1,6 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "6"
|
||||
|
||||
script:
|
||||
- npm run lint
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
# Changelog
|
||||
## 1.6.1 (unreleased)
|
||||
* [New] [changeAdminStatus](/DOCS.md#changeAdminStatus): Function to add/remove admins in group threads ([#659](https://github.com/Schmavery/facebook-chat-api/pull/659))
|
||||
|
||||
## 1.5.0
|
||||
* [Fix] Logging in using login approvals (2FA) now work ([#548](https://github.com/Schmavery/facebook-chat-api/pull/548))
|
||||
* [New] [listen](/DOCS.md#listen): Adds field `mentions` that stores an array of ids of users tagged in the message ([#510](https://github.com/Schmavery/facebook-chat-api/pull/510))
|
||||
* [Breaking change] [getThreadList](/DOCS.md#getThreadList): Removes deprecated fields in returned object, adds some new fields ([#488](https://github.com/Schmavery/facebook-chat-api/pull/488))
|
||||
* [Breaking change] [getThreadInfo](/DOCS.md#getThreadInfo): Removes deprecated fields in returned object, adds some new fields ([#488](https://github.com/Schmavery/facebook-chat-api/pull/488))
|
||||
* [New] [getEmojiUrl](/DOCS.md#getEmojiUrl): Adds utility function for getting the image URL for a Messenger-style emoji ([#477](https://github.com/Schmavery/facebook-chat-api/pull/477))
|
||||
* [Breaking change] [changeThreadColor](/DOCS.md#changeThreadColor): Due to Facebook backend changes, the thread color can no longer be set to an arbitrary hex value. Color validation and `api.threadColors` have been added to facilitate this change ([#512](https://github.com/Schmavery/facebook-chat-api/pull/512))
|
||||
* [getThreadHistory](/DOCS.md#getThreadHistory): Fix crash when the author of an old message is no longer available ([#521](https://github.com/Schmavery/facebook-chat-api/pull/521))
|
||||
* [New] [message](/DOCS.md#message): Adds field `isUnread` that represents whether or not the message was read ([#519](https://github.com/Schmavery/facebook-chat-api/pull/519))
|
||||
|
||||
## 1.4.0 (2017-04-28)
|
||||
* [Breaking change] [getThreadHistory](/DOCS.md#getThreadHistory): update parameters - no more start & end params; replaced with amount ([#453](https://github.com/Schmavery/facebook-chat-api/pull/453))
|
||||
* [New] [setMessageReaction](/DOCS.md#setMessageReaction): set reaction on message ([#427](https://github.com/Schmavery/facebook-chat-api/pull/427), [#437](https://github.com/Schmavery/facebook-chat-api/pull/437), [#445](https://github.com/Schmavery/facebook-chat-api/pull/445))
|
||||
* [New] [forwardAttachment](/DOCS.md#forwardAttachment): send attachment to array of users ([#435](https://github.com/Schmavery/facebook-chat-api/pull/435))
|
||||
* [setMessage](/DOCS.md#sendMessage): added Mentions field ([#460](https://github.com/Schmavery/facebook-chat-api/pull/460))
|
||||
* [sendTypingIndicator](/DOCS.md#sendTypingIndicator): make callback optional ([#457](https://github.com/Schmavery/facebook-chat-api/pull/457))
|
||||
* [getUserID](/DOCS.md#getUserID): returns all results instead of just users; callback obj array now includes a type (generally user, group, page, event or app) ([#459](https://github.com/Schmavery/facebook-chat-api/pull/459))
|
||||
* [Internal] Async function support ([#425](https://github.com/Schmavery/facebook-chat-api/pull/425))
|
||||
|
||||
## 1.3.0 (2017-04-01)
|
||||
* [New] [changeBlockedStatus](/DOCS.md#changeBlockedStatus): combines blockUser & unblockUser functions ([#369](https://github.com/Schmavery/facebook-chat-api/pull/369))
|
||||
* [changeThreadColor](/DOCS.md#changeThreadColor): callback is now optional ([#367](https://github.com/Schmavery/facebook-chat-api/pull/367))
|
||||
* [Internal] [getThreadHistory](/DOCS.md#getThreadHistory): callback arrays will be error filtered before returned ([#360](https://github.com/Schmavery/facebook-chat-api/pull/360))
|
||||
|
||||
## 1.2.0 (2016-08-18)
|
||||
* [New] [muteThread](/DOCS.md#muteThread): mute a chat for a period of time, or unmute a chat ([#295](https://github.com/Schmavery/facebook-chat-api/pull/295))
|
||||
* [New] [handleMessageRequest](/DOCS.md#handleMessageRequest): accept or ignore message request(s) ([#301](https://github.com/Schmavery/facebook-chat-api/pull/301))
|
||||
* [getThreadList](/DOCS.md#getThreadList): optional type argument; can be 'inbox', 'pending', or 'archived'. Inbox is default ([#301](https://github.com/Schmavery/facebook-chat-api/pull/301))
|
||||
+1686
File diff suppressed because it is too large
Load Diff
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Avery, Benjamin, David, Maude
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
# Unofficial Facebook Chat API
|
||||
<a href="https://www.npmjs.com/package/facebook-chat-api"><img alt="npm version" src="https://img.shields.io/npm/v/facebook-chat-api.svg?style=flat-square"></a>
|
||||
<a href="https://www.npmjs.com/package/facebook-chat-api"><img src="https://img.shields.io/npm/dm/facebook-chat-api.svg?style=flat-square" alt="npm downloads"></a>
|
||||
[](https://github.com/prettier/prettier)
|
||||
|
||||
Facebook now has an official API for chat bots [here](https://developers.facebook.com/docs/messenger-platform).
|
||||
|
||||
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.
|
||||
|
||||
_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.
|
||||
|
||||
See [below](#projects-using-this-api) for projects using this API.
|
||||
|
||||
See the [full changelog](/CHANGELOG.md) for release details.
|
||||
|
||||
## Install
|
||||
If you just want to use facebook-chat-api, you should use this command:
|
||||
```bash
|
||||
npm install facebook-chat-api
|
||||
```
|
||||
It will download facebook-chat-api from NPM repositories
|
||||
|
||||
### Bleeding edge
|
||||
If you want to use bleeding edge (directly from github) to test new features or submit bug report, this is the command for you:
|
||||
```bash
|
||||
npm install Schmavery/facebook-chat-api
|
||||
```
|
||||
|
||||
## Testing your bots
|
||||
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/).
|
||||
|
||||
## Example Usage
|
||||
```javascript
|
||||
const login = require("facebook-chat-api");
|
||||
|
||||
// Create simple echo bot
|
||||
login({email: "FB_EMAIL", password: "FB_PASSWORD"}, (err, api) => {
|
||||
if(err) return console.error(err);
|
||||
|
||||
api.listen((err, message) => {
|
||||
api.sendMessage(message.body, message.threadID);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
<img width="517" alt="screen shot 2016-11-04 at 14 36 00" src="https://cloud.githubusercontent.com/assets/4534692/20023545/f8c24130-a29d-11e6-9ef7-47568bdbc1f2.png">
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
* [`login`](DOCS.md#login)
|
||||
* [`api.addUserToGroup`](DOCS.md#addUserToGroup)
|
||||
* [`api.changeAdminStatus`](DOCS.md#changeAdminStatus)
|
||||
* [`api.changeArchivedStatus`](DOCS.md#changeArchivedStatus)
|
||||
* [`api.changeBlockedStatus`](DOCS.md#changeBlockedStatus)
|
||||
* [`api.changeGroupImage`](DOCS.md#changeGroupImage)
|
||||
* [`api.changeNickname`](DOCS.md#changeNickname)
|
||||
* [`api.changeThreadColor`](DOCS.md#changeThreadColor)
|
||||
* [`api.changeThreadEmoji`](DOCS.md#changeThreadEmoji)
|
||||
* [`api.createPoll`](DOCS.md#createPoll)
|
||||
* [`api.deleteMessage`](DOCS.md#deleteMessage)
|
||||
* [`api.deleteThread`](DOCS.md#deleteThread)
|
||||
* [`api.forwardAttachment`](DOCS.md#forwardAttachment)
|
||||
* [`api.getAppState`](DOCS.md#getAppState)
|
||||
* [`api.getCurrentUserID`](DOCS.md#getCurrentUserID)
|
||||
* [`api.getFriendsList`](DOCS.md#getFriendsList)
|
||||
* [`api.getThreadHistory`](DOCS.md#getThreadHistory)
|
||||
* [`api.getThreadInfo`](DOCS.md#getThreadInfo)
|
||||
* [`api.getThreadList`](DOCS.md#getThreadList)
|
||||
* [`api.getThreadPictures`](DOCS.md#getThreadPictures)
|
||||
* [`api.getUserID`](DOCS.md#getUserID)
|
||||
* [`api.getUserInfo`](DOCS.md#getUserInfo)
|
||||
* [`api.handleMessageRequest`](DOCS.md#handleMessageRequest)
|
||||
* [`api.listen`](DOCS.md#listen)
|
||||
* [`api.logout`](DOCS.md#logout)
|
||||
* [`api.markAsRead`](DOCS.md#markAsRead)
|
||||
* [`api.markAsReadAll`](DOCS.md#markAsReadAll)
|
||||
* [`api.muteThread`](DOCS.md#muteThread)
|
||||
* [`api.removeUserFromGroup`](DOCS.md#removeUserFromGroup)
|
||||
* [`api.resolvePhotoUrl`](DOCS.md#resolvePhotoUrl)
|
||||
* [`api.searchForThread`](DOCS.md#searchForThread)
|
||||
* [`api.sendMessage`](DOCS.md#sendMessage)
|
||||
* [`api.sendTypingIndicator`](DOCS.md#sendTypingIndicator)
|
||||
* [`api.setMessageReaction`](DOCS.md#setMessageReaction)
|
||||
* [`api.setOptions`](DOCS.md#setOptions)
|
||||
* [`api.setTitle`](DOCS.md#setTitle)
|
||||
* [`api.unsendMessage`](DOCS.md#unsendMessage)
|
||||
|
||||
## Main Functionality
|
||||
|
||||
### Sending a message
|
||||
#### api.sendMessage(message, threadID[, callback][, messageID])
|
||||
|
||||
Various types of message can be sent:
|
||||
* *Regular:* set field `body` to the desired message as a string.
|
||||
* *Sticker:* set a field `sticker` to the desired sticker ID.
|
||||
* *File or image:* Set field `attachment` to a readable stream or an array of readable streams.
|
||||
* *URL:* set a field `url` to the desired URL.
|
||||
* *Emoji:* set field `emoji` to the desired emoji as a string and set field `emojiSize` with size of the emoji (`small`, `medium`, `large`)
|
||||
|
||||
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.
|
||||
|
||||
__Tip__: to find your own ID, you can look inside the cookies. The `userID` is under the name `c_user`.
|
||||
|
||||
__Example (Basic Message)__
|
||||
```js
|
||||
const login = require("facebook-chat-api");
|
||||
|
||||
login({email: "FB_EMAIL", password: "FB_PASSWORD"}, (err, api) => {
|
||||
if(err) return console.error(err);
|
||||
|
||||
var yourID = "000000000000000";
|
||||
var msg = "Hey!";
|
||||
api.sendMessage(msg, yourID);
|
||||
});
|
||||
```
|
||||
|
||||
__Example (File upload)__
|
||||
```js
|
||||
const login = require("facebook-chat-api");
|
||||
|
||||
login({email: "FB_EMAIL", password: "FB_PASSWORD"}, (err, api) => {
|
||||
if(err) return console.error(err);
|
||||
|
||||
// Note this example uploads an image called image.jpg
|
||||
var yourID = "000000000000000";
|
||||
var msg = {
|
||||
body: "Hey!",
|
||||
attachment: fs.createReadStream(__dirname + '/image.jpg')
|
||||
}
|
||||
api.sendMessage(msg, yourID);
|
||||
});
|
||||
```
|
||||
|
||||
------------------------------------
|
||||
### Saving session.
|
||||
|
||||
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.
|
||||
|
||||
__Example__
|
||||
|
||||
```js
|
||||
const fs = require("fs");
|
||||
const login = require("facebook-chat-api");
|
||||
|
||||
var credentials = {email: "FB_EMAIL", password: "FB_PASSWORD"};
|
||||
|
||||
login(credentials, (err, api) => {
|
||||
if(err) return console.error(err);
|
||||
|
||||
fs.writeFileSync('appstate.json', JSON.stringify(api.getAppState()));
|
||||
});
|
||||
```
|
||||
|
||||
------------------------------------
|
||||
|
||||
### Listening to a chat
|
||||
#### api.listen(callback)
|
||||
|
||||
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})`.
|
||||
|
||||
__Example__
|
||||
|
||||
```js
|
||||
const fs = require("fs");
|
||||
const login = require("facebook-chat-api");
|
||||
|
||||
// Simple echo bot. It will repeat everything that you say.
|
||||
// Will stop when you say '/stop'
|
||||
login({appState: JSON.parse(fs.readFileSync('appstate.json', 'utf8'))}, (err, api) => {
|
||||
if(err) return console.error(err);
|
||||
|
||||
api.setOptions({listenEvents: true});
|
||||
|
||||
var stopListening = api.listen((err, event) => {
|
||||
if(err) return console.error(err);
|
||||
|
||||
api.markAsRead(event.threadID, (err) => {
|
||||
if(err) console.error(err);
|
||||
});
|
||||
|
||||
switch(event.type) {
|
||||
case "message":
|
||||
if(event.body === '/stop') {
|
||||
api.sendMessage("Goodbye…", event.threadID);
|
||||
return stopListening();
|
||||
}
|
||||
api.sendMessage("TEST BOT: " + event.body, event.threadID);
|
||||
break;
|
||||
case "event":
|
||||
console.log(event);
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## FAQS
|
||||
|
||||
1. How do I run tests?
|
||||
> 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`.
|
||||
|
||||
2. Why doesn't `sendMessage` always work when I'm logged in as a page?
|
||||
> Pages can't start conversations with users directly; this is to prevent pages from spamming users.
|
||||
|
||||
3. What do I do when `login` doesn't work?
|
||||
> 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).
|
||||
|
||||
4. How can I avoid logging in every time? Can I log into a previous session?
|
||||
> 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.
|
||||
|
||||
5. Do you support sending messages as a page?
|
||||
> Yes, set the pageID option on login (this doesn't work if you set it using api.setOptions, it affects the login process).
|
||||
> ```js
|
||||
> login(credentials, {pageID: "000000000000000"}, (err, api) => { … }
|
||||
> ```
|
||||
|
||||
6. I'm getting some crazy weird syntax error like `SyntaxError: Unexpected token [`!!!
|
||||
> Please try to update your version of node.js before submitting an issue of this nature. We like to use new language features.
|
||||
|
||||
7. I don't want all of these logging messages!
|
||||
> You can use `api.setOptions` to silence the logging. You get the `api` object from `login` (see example above). Do
|
||||
> ```js
|
||||
> api.setOptions({
|
||||
> logLevel: "silent"
|
||||
> });
|
||||
> ```
|
||||
|
||||
## Projects using this API
|
||||
|
||||
- [Messer](https://github.com/mjkaufer/Messer) - Command-line messaging for Facebook Messenger
|
||||
- [messen](https://github.com/tomquirk/messen) - Rapidly build Facebook Messenger apps in Node.js
|
||||
- [Concierge](https://github.com/concierge/Concierge) - Concierge is a highly modular, easily extensible general purpose chat bot with a built in package manager
|
||||
- [Marc Zuckerbot](https://github.com/bsansouci/marc-zuckerbot) - Facebook chat bot
|
||||
- [Marc Thuckerbot](https://github.com/bsansouci/lisp-bot) - Programmable lisp bot
|
||||
- [MarkovsInequality](https://github.com/logicx24/MarkovsInequality) - Extensible chat bot adding useful functions to Facebook Messenger
|
||||
- [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.
|
||||
- [Larry Pudding Dog Bot](https://github.com/Larry850806/facebook-chat-bot) - A facebook bot you can easily customize the response
|
||||
- [fbash](https://github.com/avikj/fbash) - Run commands on your computer's terminal over Facebook Messenger
|
||||
- [Klink](https://github.com/KeNt178/klink) - This Chrome extension will 1-click share the link of your active tab over Facebook Messenger
|
||||
- [Botyo](https://github.com/ivkos/botyo) - Modular bot designed for group chat rooms on Facebook
|
||||
- [matrix-puppet-facebook](https://github.com/matrix-hacks/matrix-puppet-facebook) - A facebook bridge for [matrix](https://matrix.org)
|
||||
- [facebot](https://github.com/Weetbix/facebot) - A facebook bridge for Slack.
|
||||
- [Botium](https://github.com/codeforequity-at/botium-core) - The Selenium for Chatbots
|
||||
- [Messenger-CLI](https://github.com/AstroCB/Messenger-CLI) - A command-line interface for sending and receiving messages through Facebook Messenger.
|
||||
- [AssumeZero-Bot](https://github.com/AstroCB/AssumeZero-Bot) – A highly customizable Facebook Messenger bot for group chats.
|
||||
- [Miscord](https://github.com/Bjornskjald/miscord) - An easy-to-use Facebook bridge for Discord.
|
||||
- [chat-bridge](https://github.com/rexx0520/chat-bridge) - A Messenger, Telegram and IRC chat bridge.
|
||||
- [messenger-auto-reply](https://gitlab.com/theSander/messenger-auto-reply) - An auto-reply service for Messenger.
|
||||
+483
@@ -0,0 +1,483 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("./utils");
|
||||
var cheerio = require("cheerio");
|
||||
var log = require("npmlog");
|
||||
|
||||
var defaultLogRecordSize = 100;
|
||||
log.maxRecordSize = defaultLogRecordSize;
|
||||
|
||||
function setOptions(globalOptions, options) {
|
||||
Object.keys(options).map(function(key) {
|
||||
switch (key) {
|
||||
case 'logLevel':
|
||||
log.level = options.logLevel;
|
||||
globalOptions.logLevel = options.logLevel;
|
||||
break;
|
||||
case 'logRecordSize':
|
||||
log.maxRecordSize = options.logRecordSize;
|
||||
globalOptions.logRecordSize = options.logRecordSize;
|
||||
break;
|
||||
case 'selfListen':
|
||||
globalOptions.selfListen = options.selfListen;
|
||||
break;
|
||||
case 'listenEvents':
|
||||
globalOptions.listenEvents = options.listenEvents;
|
||||
break;
|
||||
case 'pageID':
|
||||
globalOptions.pageID = options.pageID.toString();
|
||||
break;
|
||||
case 'updatePresence':
|
||||
globalOptions.updatePresence = options.updatePresence;
|
||||
break;
|
||||
case 'forceLogin':
|
||||
globalOptions.forceLogin = options.forceLogin;
|
||||
break;
|
||||
case 'userAgent':
|
||||
globalOptions.userAgent = options.userAgent;
|
||||
break;
|
||||
case 'autoMarkDelivery':
|
||||
globalOptions.autoMarkDelivery = options.autoMarkDelivery;
|
||||
break;
|
||||
case 'autoMarkRead':
|
||||
globalOptions.autoMarkRead = options.autoMarkRead;
|
||||
break;
|
||||
default:
|
||||
log.warn("setOptions", "Unrecognized option given to setOptions: " + key);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function buildAPI(globalOptions, html, jar) {
|
||||
var maybeCookie = jar.getCookies("https://www.facebook.com").filter(function(val) {
|
||||
return val.cookieString().split("=")[0] === "c_user";
|
||||
});
|
||||
|
||||
if(maybeCookie.length === 0) {
|
||||
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."};
|
||||
}
|
||||
|
||||
var userID = maybeCookie[0].cookieString().split("=")[1].toString();
|
||||
log.info("login", "Logged in");
|
||||
|
||||
var clientID = (Math.random() * 2147483648 | 0).toString(16);
|
||||
|
||||
// All data available to api functions
|
||||
var ctx = {
|
||||
userID: userID,
|
||||
jar: jar,
|
||||
clientID: clientID,
|
||||
globalOptions: globalOptions,
|
||||
loggedIn: true,
|
||||
access_token: 'NONE',
|
||||
clientMutationId: 0
|
||||
};
|
||||
|
||||
var api = {
|
||||
setOptions: setOptions.bind(null, globalOptions),
|
||||
getAppState: function getAppState() {
|
||||
return utils.getAppState(jar);
|
||||
},
|
||||
};
|
||||
|
||||
const apiFuncNames = [
|
||||
'addUserToGroup',
|
||||
'changeAdminStatus',
|
||||
'changeArchivedStatus',
|
||||
'changeBlockedStatus',
|
||||
'changeGroupImage',
|
||||
'changeNickname',
|
||||
'changeThreadColor',
|
||||
'changeThreadEmoji',
|
||||
'createPoll',
|
||||
'deleteMessage',
|
||||
'deleteThread',
|
||||
'forwardAttachment',
|
||||
'getCurrentUserID',
|
||||
'getEmojiUrl',
|
||||
'getFriendsList',
|
||||
'getThreadHistory',
|
||||
'getThreadInfo',
|
||||
'getThreadList',
|
||||
'getThreadPictures',
|
||||
'getUserID',
|
||||
'getUserInfo',
|
||||
'handleMessageRequest',
|
||||
'listen',
|
||||
'listenMqtt',
|
||||
'logout',
|
||||
'markAsDelivered',
|
||||
'markAsRead',
|
||||
'markAsReadAll',
|
||||
'muteThread',
|
||||
'removeUserFromGroup',
|
||||
'resolvePhotoUrl',
|
||||
'searchForThread',
|
||||
'sendMessage',
|
||||
'sendTypingIndicator',
|
||||
'setMessageReaction',
|
||||
'setTitle',
|
||||
'threadColors',
|
||||
'unsendMessage',
|
||||
|
||||
// Deprecated features
|
||||
"getThreadListDeprecated",
|
||||
'getThreadHistoryDeprecated',
|
||||
'getThreadInfoDeprecated',
|
||||
];
|
||||
|
||||
var defaultFuncs = utils.makeDefaults(html, userID, ctx);
|
||||
|
||||
// Load all api functions in a loop
|
||||
apiFuncNames.map(function(v) {
|
||||
api[v] = require('./src/' + v)(defaultFuncs, api, ctx);
|
||||
});
|
||||
|
||||
return [ctx, defaultFuncs, api];
|
||||
}
|
||||
|
||||
function makeLogin(jar, email, password, loginOptions, callback) {
|
||||
return function(res) {
|
||||
var html = res.body;
|
||||
var $ = cheerio.load(html);
|
||||
var arr = [];
|
||||
|
||||
// This will be empty, but just to be sure we leave it
|
||||
$("#login_form input").map(function(i, v){
|
||||
arr.push({val: $(v).val(), name: $(v).attr("name")});
|
||||
});
|
||||
|
||||
arr = arr.filter(function(v) {
|
||||
return v.val && v.val.length;
|
||||
});
|
||||
|
||||
var form = utils.arrToForm(arr);
|
||||
form.lsd = utils.getFrom(html, "[\"LSD\",[],{\"token\":\"", "\"}");
|
||||
form.lgndim = Buffer.from("{\"w\":1440,\"h\":900,\"aw\":1440,\"ah\":834,\"c\":24}").toString('base64');
|
||||
form.email = email;
|
||||
form.pass = password;
|
||||
form.default_persistent = '0';
|
||||
form.lgnrnd = utils.getFrom(html, "name=\"lgnrnd\" value=\"", "\"");
|
||||
form.locale = 'en_US';
|
||||
form.timezone = '240';
|
||||
form.lgnjs = ~~(Date.now() / 1000);
|
||||
|
||||
|
||||
// Getting cookies from the HTML page... (kill me now plz)
|
||||
// we used to get a bunch of cookies in the headers of the response of the
|
||||
// request, but FB changed and they now send those cookies inside the JS.
|
||||
// They run the JS which then injects the cookies in the page.
|
||||
// The "solution" is to parse through the html and find those cookies
|
||||
// which happen to be conveniently indicated with a _js_ in front of their
|
||||
// variable name.
|
||||
//
|
||||
// ---------- Very Hacky Part Starts -----------------
|
||||
var willBeCookies = html.split("\"_js_");
|
||||
willBeCookies.slice(1).map(function(val) {
|
||||
var cookieData = JSON.parse("[\"" + utils.getFrom(val, "", "]") + "]");
|
||||
jar.setCookie(utils.formatCookie(cookieData, "facebook"), "https://www.facebook.com");
|
||||
});
|
||||
// ---------- Very Hacky Part Ends -----------------
|
||||
|
||||
log.info("login", "Logging in...");
|
||||
return utils
|
||||
.post("https://www.facebook.com/login.php?login_attempt=1&lwv=110", jar, form, loginOptions)
|
||||
.then(utils.saveCookies(jar))
|
||||
.then(function(res) {
|
||||
var headers = res.headers;
|
||||
if (!headers.location) {
|
||||
throw {error: "Wrong username/password."};
|
||||
}
|
||||
|
||||
// This means the account has login approvals turned on.
|
||||
if (headers.location.indexOf('https://www.facebook.com/checkpoint/') > -1) {
|
||||
log.info("login", "You have login approvals turned on.");
|
||||
var nextURL = 'https://www.facebook.com/checkpoint/?next=https%3A%2F%2Fwww.facebook.com%2Fhome.php';
|
||||
|
||||
return utils
|
||||
.get(headers.location, jar, null, loginOptions)
|
||||
.then(utils.saveCookies(jar))
|
||||
.then(function(res) {
|
||||
var html = res.body;
|
||||
// Make the form in advance which will contain the fb_dtsg and nh
|
||||
var $ = cheerio.load(html);
|
||||
var arr = [];
|
||||
$("form input").map(function(i, v){
|
||||
arr.push({val: $(v).val(), name: $(v).attr("name")});
|
||||
});
|
||||
|
||||
arr = arr.filter(function(v) {
|
||||
return v.val && v.val.length;
|
||||
});
|
||||
|
||||
var form = utils.arrToForm(arr);
|
||||
if (html.indexOf("checkpoint/?next") > -1) {
|
||||
throw {
|
||||
error: 'login-approval',
|
||||
continue: function(code) {
|
||||
form.approvals_code = code;
|
||||
form['submit[Continue]'] = 'Continue';
|
||||
return utils
|
||||
.post(nextURL, jar, form, loginOptions)
|
||||
.then(utils.saveCookies(jar))
|
||||
.then(function() {
|
||||
// Use the same form (safe I hope)
|
||||
form.name_action_selected = 'save_device';
|
||||
|
||||
return utils
|
||||
.post(nextURL, jar, form, loginOptions)
|
||||
.then(utils.saveCookies(jar));
|
||||
})
|
||||
.then(function(res) {
|
||||
var headers = res.headers;
|
||||
if (!headers.location && res.body.indexOf('Review Recent Login') > -1) {
|
||||
throw {error: "Something went wrong with login approvals."};
|
||||
}
|
||||
|
||||
var appState = utils.getAppState(jar);
|
||||
|
||||
// Simply call loginHelper because all it needs is the jar
|
||||
// and will then complete the login process
|
||||
return loginHelper(appState, email, password, loginOptions, callback);
|
||||
})
|
||||
.catch(function(err) {
|
||||
callback(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
} else {
|
||||
if (!loginOptions.forceLogin) {
|
||||
throw {error: "Couldn't login. Facebook might have blocked this account. Please login with a browser or enable the option 'forceLogin' and try again."};
|
||||
}
|
||||
if (html.indexOf("Suspicious Login Attempt") > -1) {
|
||||
form['submit[This was me]'] = "This was me";
|
||||
} else {
|
||||
form['submit[This Is Okay]'] = "This Is Okay";
|
||||
}
|
||||
|
||||
return utils
|
||||
.post(nextURL, jar, form, loginOptions)
|
||||
.then(utils.saveCookies(jar))
|
||||
.then(function() {
|
||||
// Use the same form (safe I hope)
|
||||
form.name_action_selected = 'save_device';
|
||||
|
||||
return utils
|
||||
.post(nextURL, jar, form, loginOptions)
|
||||
.then(utils.saveCookies(jar));
|
||||
})
|
||||
.then(function(res) {
|
||||
var headers = res.headers;
|
||||
|
||||
if (!headers.location && res.body.indexOf('Review Recent Login') > -1) {
|
||||
throw {error: "Something went wrong with review recent login."};
|
||||
}
|
||||
|
||||
var appState = utils.getAppState(jar);
|
||||
|
||||
// Simply call loginHelper because all it needs is the jar
|
||||
// and will then complete the login process
|
||||
return loginHelper(appState, email, password, loginOptions, callback);
|
||||
})
|
||||
.catch(function(e) {
|
||||
callback(e);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return utils
|
||||
.get('https://www.facebook.com/', jar, null, loginOptions)
|
||||
.then(utils.saveCookies(jar));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Helps the login
|
||||
function loginHelper(appState, email, password, globalOptions, callback) {
|
||||
var mainPromise = null;
|
||||
var jar = utils.getJar();
|
||||
|
||||
// If we're given an appState we loop through it and save each cookie
|
||||
// back into the jar.
|
||||
if(appState) {
|
||||
appState.map(function(c) {
|
||||
var str = c.key + "=" + c.value + "; expires=" + c.expires + "; domain=" + c.domain + "; path=" + c.path + ";";
|
||||
jar.setCookie(str, "http://" + c.domain);
|
||||
});
|
||||
|
||||
// Load the main page.
|
||||
mainPromise = utils
|
||||
.get('https://www.facebook.com/', jar, null, globalOptions)
|
||||
.then(utils.saveCookies(jar));
|
||||
} else {
|
||||
// Open the main page, then we login with the given credentials and finally
|
||||
// load the main page again (it'll give us some IDs that we need)
|
||||
mainPromise = utils
|
||||
.get("https://www.facebook.com/", null, null, globalOptions)
|
||||
.then(utils.saveCookies(jar))
|
||||
.then(makeLogin(jar, email, password, globalOptions, callback))
|
||||
.then(function() {
|
||||
return utils
|
||||
.get('https://www.facebook.com/', jar, null, globalOptions)
|
||||
.then(utils.saveCookies(jar));
|
||||
});
|
||||
}
|
||||
|
||||
var ctx = null;
|
||||
var defaultFuncs = null;
|
||||
var api = null;
|
||||
|
||||
mainPromise = mainPromise
|
||||
.then(function(res) {
|
||||
// Hacky check for the redirection that happens on some ISPs, which doesn't return statusCode 3xx
|
||||
var reg = /<meta http-equiv="refresh" content="0;url=([^"]+)[^>]+>/;
|
||||
var redirect = reg.exec(res.body);
|
||||
if (redirect && redirect[1]) {
|
||||
return utils
|
||||
.get(redirect[1], jar, null, globalOptions)
|
||||
.then(utils.saveCookies(jar));
|
||||
}
|
||||
return res;
|
||||
})
|
||||
.then(function(res) {
|
||||
var html = res.body;
|
||||
var stuff = buildAPI(globalOptions, html, jar);
|
||||
ctx = stuff[0];
|
||||
defaultFuncs = stuff[1];
|
||||
api = stuff[2];
|
||||
return res;
|
||||
})
|
||||
.then(function() {
|
||||
var form = {
|
||||
reason: 6
|
||||
};
|
||||
log.info("login", 'Request to reconnect');
|
||||
return defaultFuncs
|
||||
.get("https://www.facebook.com/ajax/presence/reconnect.php", ctx.jar, form)
|
||||
.then(utils.saveCookies(ctx.jar));
|
||||
})
|
||||
.then(function(_res) {
|
||||
log.info("login", 'Request to pull 1');
|
||||
var form = {
|
||||
channel : 'p_' + ctx.userID,
|
||||
seq : 0,
|
||||
partition : -2,
|
||||
clientid : ctx.clientID,
|
||||
viewer_uid : ctx.userID,
|
||||
uid : ctx.userID,
|
||||
state : 'active',
|
||||
idle : 0,
|
||||
cap : 8,
|
||||
msgs_recv: 0
|
||||
};
|
||||
var presence = utils.generatePresence(ctx.userID);
|
||||
ctx.jar.setCookie("presence=" + presence + "; path=/; domain=.facebook.com; secure", "https://www.facebook.com");
|
||||
ctx.jar.setCookie("presence=" + presence + "; path=/; domain=.messenger.com; secure", "https://www.messenger.com");
|
||||
ctx.jar.setCookie("locale=en_US; path=/; domain=.facebook.com; secure", "https://www.facebook.com");
|
||||
ctx.jar.setCookie("locale=en_US; path=/; domain=.messenger.com; secure", "https://www.messenger.com");
|
||||
ctx.jar.setCookie("a11y=" + utils.generateAccessiblityCookie() + "; path=/; domain=.facebook.com; secure", "https://www.facebook.com");
|
||||
|
||||
return utils
|
||||
.get("https://0-edge-chat.facebook.com/pull", ctx.jar, form, globalOptions)
|
||||
.then(utils.saveCookies(ctx.jar))
|
||||
.then(function(res) {
|
||||
var ret = null;
|
||||
try {
|
||||
ret = JSON.parse(utils.makeParsable(res.body));
|
||||
} catch(e) {
|
||||
throw {error: "Error inside first pull request. Received HTML instead of JSON. Logging in inside a browser might help fix this."};
|
||||
}
|
||||
|
||||
return ret;
|
||||
});
|
||||
})
|
||||
.then(function(resData) {
|
||||
if (resData.t !== 'lb') throw {error: "Bad response from pull 1"};
|
||||
|
||||
var form = {
|
||||
channel : 'p_' + ctx.userID,
|
||||
seq : 0,
|
||||
partition : -2,
|
||||
clientid : ctx.clientID,
|
||||
viewer_uid : ctx.userID,
|
||||
uid : ctx.userID,
|
||||
state : 'active',
|
||||
idle : 0,
|
||||
cap : 8,
|
||||
msgs_recv:0,
|
||||
sticky_token: resData.lb_info.sticky,
|
||||
sticky_pool: resData.lb_info.pool,
|
||||
};
|
||||
|
||||
log.info("login", "Request to pull 2");
|
||||
return utils
|
||||
.get("https://0-edge-chat.facebook.com/pull", ctx.jar, form, globalOptions)
|
||||
.then(utils.saveCookies(ctx.jar));
|
||||
})
|
||||
.then(function() {
|
||||
var form = {
|
||||
'client' : 'mercury',
|
||||
'folders[0]': 'inbox',
|
||||
'last_action_timestamp' : '0'
|
||||
};
|
||||
log.info("login", "Request to thread_sync");
|
||||
|
||||
return defaultFuncs
|
||||
.post("https://www.facebook.com/ajax/mercury/thread_sync.php", ctx.jar, form, globalOptions)
|
||||
.then(utils.saveCookies(ctx.jar));
|
||||
});
|
||||
|
||||
// given a pageID we log in as a page
|
||||
if (globalOptions.pageID) {
|
||||
mainPromise = mainPromise
|
||||
.then(function() {
|
||||
return utils
|
||||
.get('https://www.facebook.com/' + ctx.globalOptions.pageID + '/messages/?section=messages&subsection=inbox', ctx.jar, null, globalOptions);
|
||||
})
|
||||
.then(function(resData) {
|
||||
var url = utils.getFrom(resData.body, 'window.location.replace("https:\\/\\/www.facebook.com\\', '");').split('\\').join('');
|
||||
url = url.substring(0, url.length - 1);
|
||||
|
||||
return utils
|
||||
.get('https://www.facebook.com' + url, ctx.jar, null, globalOptions);
|
||||
});
|
||||
}
|
||||
|
||||
// At the end we call the callback or catch an exception
|
||||
mainPromise
|
||||
.then(function() {
|
||||
log.info("login", 'Done logging in.');
|
||||
return callback(null, api);
|
||||
})
|
||||
.catch(function(e) {
|
||||
log.error("login", e.error || e);
|
||||
callback(e);
|
||||
});
|
||||
}
|
||||
|
||||
function login(loginData, options, callback) {
|
||||
if(utils.getType(options) === 'Function' || utils.getType(options) === 'AsyncFunction') {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
var globalOptions = {
|
||||
selfListen: false,
|
||||
listenEvents: false,
|
||||
updatePresence: false,
|
||||
forceLogin: false,
|
||||
autoMarkDelivery: true,
|
||||
autoMarkRead: false,
|
||||
logRecordSize: defaultLogRecordSize,
|
||||
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"
|
||||
};
|
||||
|
||||
setOptions(globalOptions, options);
|
||||
|
||||
loginHelper(loginData.appState, loginData.email, loginData.password, globalOptions, callback);
|
||||
}
|
||||
|
||||
module.exports = login;
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"_from": "facebook-chat-api",
|
||||
"_id": "facebook-chat-api@1.7.0",
|
||||
"_inBundle": false,
|
||||
"_integrity": "sha512-Ib/pNz+WPHHoScTzlJcc3xu88is+QL6tA8KgrXRoCt0/fY4obmz3xW3p0A97VKVxsTQSm9yfQer207o2DZy7IQ==",
|
||||
"_location": "/facebook-chat-api",
|
||||
"_phantomChildren": {},
|
||||
"_requested": {
|
||||
"type": "tag",
|
||||
"registry": true,
|
||||
"raw": "facebook-chat-api",
|
||||
"name": "facebook-chat-api",
|
||||
"escapedName": "facebook-chat-api",
|
||||
"rawSpec": "",
|
||||
"saveSpec": null,
|
||||
"fetchSpec": "latest"
|
||||
},
|
||||
"_requiredBy": [
|
||||
"#USER",
|
||||
"/"
|
||||
],
|
||||
"_resolved": "https://registry.npmjs.org/facebook-chat-api/-/facebook-chat-api-1.7.0.tgz",
|
||||
"_shasum": "7b3da252afeb812f3988e3ff73b046748e1ea1f5",
|
||||
"_spec": "facebook-chat-api",
|
||||
"_where": "/home/miti99/Dropbox/Workspace/FBcount/Nodejs",
|
||||
"author": {
|
||||
"name": "Avery, David, Maude, Benjamin"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/Schmavery/facebook-chat-api/issues"
|
||||
},
|
||||
"bundleDependencies": false,
|
||||
"dependencies": {
|
||||
"bluebird": "^2.9.27",
|
||||
"cheerio": "^0.22.0",
|
||||
"mqtt": "^3.0.0",
|
||||
"npmlog": "^1.2.0",
|
||||
"request": "^2.53.0"
|
||||
},
|
||||
"deprecated": false,
|
||||
"description": "Facebook chat API that doesn't rely on XMPP. Will NOT be deprecated April 30th 2015.",
|
||||
"devDependencies": {
|
||||
"eslint": "^4.19.1",
|
||||
"mocha": "^7.0.1",
|
||||
"prettier": "^1.11.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.x"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"env": {
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"no-unused-vars": [
|
||||
1,
|
||||
{
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"homepage": "https://github.com/Schmavery/facebook-chat-api#readme",
|
||||
"keywords": [
|
||||
"facebook",
|
||||
"chat",
|
||||
"api"
|
||||
],
|
||||
"license": "MIT",
|
||||
"name": "facebook-chat-api",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/Schmavery/facebook-chat-api.git"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint **.js",
|
||||
"prettier": "prettier utils.js src/* --write",
|
||||
"test": "mocha"
|
||||
},
|
||||
"version": "1.7.0"
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function addUserToGroup(userID, threadID, callback) {
|
||||
if (
|
||||
!callback &&
|
||||
(utils.getType(threadID) === "Function" ||
|
||||
utils.getType(threadID) === "AsyncFunction")
|
||||
) {
|
||||
throw { error: "please pass a threadID as a second argument." };
|
||||
}
|
||||
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
if (
|
||||
utils.getType(threadID) !== "Number" &&
|
||||
utils.getType(threadID) !== "String"
|
||||
) {
|
||||
throw {
|
||||
error:
|
||||
"ThreadID should be of type Number or String and not " +
|
||||
utils.getType(threadID) +
|
||||
"."
|
||||
};
|
||||
}
|
||||
|
||||
if (utils.getType(userID) !== "Array") {
|
||||
userID = [userID];
|
||||
}
|
||||
|
||||
var messageAndOTID = utils.generateOfflineThreadingID();
|
||||
var form = {
|
||||
client: "mercury",
|
||||
action_type: "ma-type:log-message",
|
||||
author: "fbid:" + ctx.userID,
|
||||
thread_id: "",
|
||||
timestamp: Date.now(),
|
||||
timestamp_absolute: "Today",
|
||||
timestamp_relative: utils.generateTimestampRelative(),
|
||||
timestamp_time_passed: "0",
|
||||
is_unread: false,
|
||||
is_cleared: false,
|
||||
is_forward: false,
|
||||
is_filtered_content: false,
|
||||
is_filtered_content_bh: false,
|
||||
is_filtered_content_account: false,
|
||||
is_spoof_warning: false,
|
||||
source: "source:chat:web",
|
||||
"source_tags[0]": "source:chat",
|
||||
log_message_type: "log:subscribe",
|
||||
status: "0",
|
||||
offline_threading_id: messageAndOTID,
|
||||
message_id: messageAndOTID,
|
||||
threading_id: utils.generateThreadingID(ctx.clientID),
|
||||
manual_retry_cnt: "0",
|
||||
thread_fbid: threadID
|
||||
};
|
||||
|
||||
for (var i = 0; i < userID.length; i++) {
|
||||
if (
|
||||
utils.getType(userID[i]) !== "Number" &&
|
||||
utils.getType(userID[i]) !== "String"
|
||||
) {
|
||||
throw {
|
||||
error:
|
||||
"Elements of userID should be of type Number or String and not " +
|
||||
utils.getType(userID[i]) +
|
||||
"."
|
||||
};
|
||||
}
|
||||
|
||||
form["log_message_data[added_participants][" + i + "]"] =
|
||||
"fbid:" + userID[i];
|
||||
}
|
||||
|
||||
defaultFuncs
|
||||
.post("https://www.facebook.com/messaging/send/", ctx.jar, form)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (!resData) {
|
||||
throw { error: "Add to group failed." };
|
||||
}
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("addUserToGroup", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
"use strict";
|
||||
|
||||
const utils = require("../utils");
|
||||
const log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function changeAdminStatus(threadID, adminIDs, adminStatus, callback) {
|
||||
if (utils.getType(threadID) !== "String") {
|
||||
throw {error: "changeAdminStatus: threadID must be a string"};
|
||||
}
|
||||
|
||||
if (utils.getType(adminIDs) === "String") {
|
||||
adminIDs = [adminIDs];
|
||||
}
|
||||
|
||||
if (utils.getType(adminIDs) !== "Array") {
|
||||
throw {error: "changeAdminStatus: adminIDs must be an array or string"};
|
||||
}
|
||||
|
||||
if (utils.getType(adminStatus) !== "Boolean") {
|
||||
throw {error: "changeAdminStatus: adminStatus must be a string"};
|
||||
}
|
||||
|
||||
if (!callback) {
|
||||
callback = () => {};
|
||||
}
|
||||
|
||||
if (utils.getType(callback) !== "Function" && utils.getType(callback) !== "AsyncFunction") {
|
||||
throw {error: "changeAdminStatus: callback is not a function"};
|
||||
}
|
||||
|
||||
let form = {
|
||||
"thread_fbid": threadID,
|
||||
};
|
||||
|
||||
let i = 0;
|
||||
for (let u of adminIDs) {
|
||||
form[`admin_ids[${i++}]`] = u
|
||||
}
|
||||
form["add"] = adminStatus;
|
||||
|
||||
defaultFuncs
|
||||
.post("https://www.facebook.com/messaging/save_admins/?dpr=1", ctx.jar, form)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
switch (resData.error) {
|
||||
case 1976004:
|
||||
throw { error: "Cannot alter admin status: you are not an admin.", rawResponse: resData };
|
||||
case 1357031:
|
||||
throw { error: "Cannot alter admin status: this thread is not a group chat.", rawResponse: resData };
|
||||
default:
|
||||
throw { error: "Cannot alter admin status: unknown error.", rawResponse: resData };
|
||||
}
|
||||
}
|
||||
|
||||
callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("changeAdminStatus", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function changeArchivedStatus(threadOrThreads, archive, callback) {
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
var form = {};
|
||||
|
||||
if (utils.getType(threadOrThreads) === "Array") {
|
||||
for (var i = 0; i < threadOrThreads.length; i++) {
|
||||
form["ids[" + threadOrThreads[i] + "]"] = archive;
|
||||
}
|
||||
} else {
|
||||
form["ids[" + threadOrThreads + "]"] = archive;
|
||||
}
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/mercury/change_archived_status.php",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("changeArchivedStatus", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function changeBlockedStatus(userID, block, callback) {
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
if (block) {
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/nfx/block_messages/?thread_fbid=" +
|
||||
userID +
|
||||
"&location=www_chat_head",
|
||||
ctx.jar,
|
||||
{}
|
||||
)
|
||||
.then(utils.saveCookies(ctx.jar))
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com" +
|
||||
/action="(.+?)"+?/
|
||||
.exec(resData.jsmods.markup[0][1].__html)[1]
|
||||
.replace(/&/g, "&"),
|
||||
ctx.jar,
|
||||
{}
|
||||
)
|
||||
.then(utils.saveCookies(ctx.jar))
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(_resData) {
|
||||
if (_resData.error) {
|
||||
throw _resData;
|
||||
}
|
||||
return callback();
|
||||
});
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("changeBlockedStatus", err);
|
||||
return callback(err);
|
||||
});
|
||||
} else {
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/nfx/messenger_undo_block.php?story_location=messenger&context=%7B%22reportable_ent_token%22%3A%22" +
|
||||
userID +
|
||||
"%22%2C%22initial_action_name%22%3A%22BLOCK_MESSAGES%22%7D&",
|
||||
ctx.jar,
|
||||
{}
|
||||
)
|
||||
.then(utils.saveCookies(ctx.jar))
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("changeBlockedStatus", err);
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
var bluebird = require("bluebird");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
function handleUpload(image, callback) {
|
||||
var uploads = [];
|
||||
|
||||
var form = {
|
||||
images_only: "true",
|
||||
"attachment[]": image
|
||||
};
|
||||
|
||||
uploads.push(
|
||||
defaultFuncs
|
||||
.postFormData(
|
||||
"https://upload.facebook.com/ajax/mercury/upload.php",
|
||||
ctx.jar,
|
||||
form,
|
||||
{}
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return resData.payload.metadata[0];
|
||||
})
|
||||
);
|
||||
|
||||
// resolve all promises
|
||||
bluebird
|
||||
.all(uploads)
|
||||
.then(function(resData) {
|
||||
callback(null, resData);
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("handleUpload", err);
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
return function changeGroupImage(image, threadID, callback) {
|
||||
if (
|
||||
!callback &&
|
||||
(utils.getType(threadID) === "Function" ||
|
||||
utils.getType(threadID) === "AsyncFunction")
|
||||
) {
|
||||
throw { error: "please pass a threadID as a second argument." };
|
||||
}
|
||||
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
var messageAndOTID = utils.generateOfflineThreadingID();
|
||||
var form = {
|
||||
client: "mercury",
|
||||
action_type: "ma-type:log-message",
|
||||
author: "fbid:" + ctx.userID,
|
||||
author_email: "",
|
||||
ephemeral_ttl_mode: "0",
|
||||
is_filtered_content: false,
|
||||
is_filtered_content_account: false,
|
||||
is_filtered_content_bh: false,
|
||||
is_filtered_content_invalid_app: false,
|
||||
is_filtered_content_quasar: false,
|
||||
is_forward: false,
|
||||
is_spoof_warning: false,
|
||||
is_unread: false,
|
||||
log_message_type: "log:thread-image",
|
||||
manual_retry_cnt: "0",
|
||||
message_id: messageAndOTID,
|
||||
offline_threading_id: messageAndOTID,
|
||||
source: "source:chat:web",
|
||||
"source_tags[0]": "source:chat",
|
||||
status: "0",
|
||||
thread_fbid: threadID,
|
||||
thread_id: "",
|
||||
timestamp: Date.now(),
|
||||
timestamp_absolute: "Today",
|
||||
timestamp_relative: utils.generateTimestampRelative(),
|
||||
timestamp_time_passed: "0"
|
||||
};
|
||||
|
||||
handleUpload(image, function(err, payload) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
form["thread_image_id"] = payload[0]["image_id"];
|
||||
form["thread_id"] = threadID;
|
||||
|
||||
defaultFuncs
|
||||
.post("https://www.facebook.com/messaging/set_thread_image/", ctx.jar, form)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
// check for errors here
|
||||
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("changeGroupImage", err);
|
||||
return callback(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function changeNickname(nickname, threadID, participantID, callback) {
|
||||
callback = callback || function() {};
|
||||
|
||||
var form = {
|
||||
nickname: nickname,
|
||||
participant_id: participantID,
|
||||
thread_or_other_fbid: threadID
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/messaging/save_thread_nickname/?source=thread_settings&dpr=1",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error === 1545014) {
|
||||
throw { error: "Trying to change nickname of user isn't in thread" };
|
||||
}
|
||||
if (resData.error === 1357031) {
|
||||
throw {
|
||||
error:
|
||||
"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."
|
||||
};
|
||||
}
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("changeNickname", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function changeThreadColor(color, threadID, callback) {
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
var validatedColor = color !== null ? color.toLowerCase() : color; // API only accepts lowercase letters in hex string
|
||||
var colorList = Object.keys(api.threadColors).map(function(name) {
|
||||
return api.threadColors[name];
|
||||
});
|
||||
if (!colorList.includes(validatedColor)) {
|
||||
throw {
|
||||
error:
|
||||
"The color you are trying to use is not a valid thread color. Use api.threadColors to find acceptable values."
|
||||
};
|
||||
}
|
||||
|
||||
var form = {
|
||||
color_choice: validatedColor,
|
||||
thread_or_other_fbid: threadID
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/messaging/save_thread_color/?source=thread_settings&dpr=1",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error === 1357031) {
|
||||
throw {
|
||||
error:
|
||||
"Trying to change colors of a chat that doesn't exist. Have at least one message in the thread before trying to change the colors."
|
||||
};
|
||||
}
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("changeThreadColor", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function changeThreadEmoji(emoji, threadID, callback) {
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
var form = {
|
||||
emoji_choice: emoji,
|
||||
thread_or_other_fbid: threadID
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/messaging/save_thread_emoji/?source=thread_settings&__pc=EXP1%3Amessengerdotcom_pkg",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error === 1357031) {
|
||||
throw {
|
||||
error:
|
||||
"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."
|
||||
};
|
||||
}
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("changeThreadEmoji", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function createPoll(title, threadID, options, callback) {
|
||||
if (!callback) {
|
||||
if (utils.getType(options) == "Function") {
|
||||
callback = options;
|
||||
} else {
|
||||
callback = function() {};
|
||||
}
|
||||
}
|
||||
if (!options) {
|
||||
options = {}; // Initial poll options are optional
|
||||
}
|
||||
|
||||
var form = {
|
||||
target_id: threadID,
|
||||
question_text: title
|
||||
};
|
||||
|
||||
// Set fields for options (and whether they are selected initially by the posting user)
|
||||
var ind = 0;
|
||||
for (var opt in options) {
|
||||
if (options.hasOwnProperty(opt)) {
|
||||
form["option_text_array[" + ind + "]"] = opt;
|
||||
form["option_is_selected_array[" + ind + "]"] = options[opt]
|
||||
? "1"
|
||||
: "0";
|
||||
ind++;
|
||||
}
|
||||
}
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/messaging/group_polling/create_poll/?dpr=1",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.payload.status != "success") {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("createPoll", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function deleteMessage(messageOrMessages, callback) {
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
var form = {
|
||||
client: "mercury"
|
||||
};
|
||||
|
||||
if (utils.getType(messageOrMessages) !== "Array") {
|
||||
messageOrMessages = [messageOrMessages];
|
||||
}
|
||||
|
||||
for (var i = 0; i < messageOrMessages.length; i++) {
|
||||
form["message_ids[" + i + "]"] = messageOrMessages[i];
|
||||
}
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/mercury/delete_messages.php",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("deleteMessage", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function deleteThread(threadOrThreads, callback) {
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
var form = {
|
||||
client: "mercury"
|
||||
};
|
||||
|
||||
if (utils.getType(threadOrThreads) !== "Array") {
|
||||
threadOrThreads = [threadOrThreads];
|
||||
}
|
||||
|
||||
for (var i = 0; i < threadOrThreads.length; i++) {
|
||||
form["ids[" + i + "]"] = threadOrThreads[i];
|
||||
}
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/mercury/delete_thread.php",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("deleteThread", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function forwardAttachment(attachmentID, userOrUsers, callback) {
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
var form = {
|
||||
attachment_id: attachmentID
|
||||
};
|
||||
|
||||
if (utils.getType(userOrUsers) !== "Array") {
|
||||
userOrUsers = [userOrUsers];
|
||||
}
|
||||
|
||||
var timestamp = Math.floor(Date.now() / 1000);
|
||||
|
||||
for (var i = 0; i < userOrUsers.length; i++) {
|
||||
//That's good, the key of the array is really timestmap in seconds + index
|
||||
//Probably time when the attachment will be sent?
|
||||
form["recipient_map[" + (timestamp + i) + "]"] = userOrUsers[i];
|
||||
}
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/mercury/attachments/forward/",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx.jar, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback(null);
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("forwardAttachment", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function getCurrentUserID() {
|
||||
return ctx.userID;
|
||||
};
|
||||
};
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
"use strict";
|
||||
|
||||
const util = require("util");
|
||||
|
||||
module.exports = function() {
|
||||
return function getEmojiUrl(c, size, pixelRatio) {
|
||||
/*
|
||||
Resolves Facebook Messenger emoji image asset URL for an emoji character.
|
||||
Supported sizes are 32, 64, and 128.
|
||||
Supported pixel ratios are '1.0' and '1.5' (possibly more; haven't tested)
|
||||
*/
|
||||
const baseUrl = "https://static.xx.fbcdn.net/images/emoji.php/v8/z%s/%s";
|
||||
pixelRatio = pixelRatio || "1.0";
|
||||
|
||||
let ending = util.format(
|
||||
"%s/%s/%s.png",
|
||||
pixelRatio,
|
||||
size,
|
||||
c.codePointAt(0).toString(16)
|
||||
);
|
||||
let base = 317426846;
|
||||
for (let i = 0; i < ending.length; i++) {
|
||||
base = (base << 5) - base + ending.charCodeAt(i);
|
||||
}
|
||||
|
||||
let hashed = (base & 255).toString(16);
|
||||
return util.format(baseUrl, hashed, ending);
|
||||
};
|
||||
};
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
"use strict";
|
||||
|
||||
var cheerio = require("cheerio");
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
// [almost] copy pasted from one of FB's minified file (GenderConst)
|
||||
var GENDERS = {
|
||||
0: "unknown",
|
||||
1: "female_singular",
|
||||
2: "male_singular",
|
||||
3: "female_singular_guess",
|
||||
4: "male_singular_guess",
|
||||
5: "mixed",
|
||||
6: "neuter_singular",
|
||||
7: "unknown_singular",
|
||||
8: "female_plural",
|
||||
9: "male_plural",
|
||||
10: "neuter_plural",
|
||||
11: "unknown_plural"
|
||||
};
|
||||
|
||||
function formatData(obj) {
|
||||
return Object.keys(obj).map(function(key) {
|
||||
var user = obj[key];
|
||||
return {
|
||||
alternateName: user.alternateName,
|
||||
firstName: user.firstName,
|
||||
gender: GENDERS[user.gender],
|
||||
userID: utils.formatID(user.id.toString()),
|
||||
isFriend: user.is_friend != null && user.is_friend ? true : false,
|
||||
fullName: user.name,
|
||||
profilePicture: user.thumbSrc,
|
||||
type: user.type,
|
||||
profileUrl: user.uri,
|
||||
vanity: user.vanity,
|
||||
isBirthday: !!user.is_birthday
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function getFriendsList(callback) {
|
||||
if (!callback) {
|
||||
throw { error: "getFriendsList: need callback" };
|
||||
}
|
||||
|
||||
defaultFuncs
|
||||
.postFormData(
|
||||
"https://www.facebook.com/chat/user_info_all",
|
||||
ctx.jar,
|
||||
{},
|
||||
{ viewer: ctx.userID }
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (!resData) {
|
||||
throw { error: "getFriendsList returned empty object." };
|
||||
}
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
callback(null, formatData(resData.payload));
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("getFriendsList", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+632
@@ -0,0 +1,632 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
function formatAttachmentsGraphQLResponse(attachment) {
|
||||
switch (attachment.__typename) {
|
||||
case "MessageImage":
|
||||
return {
|
||||
type: "photo",
|
||||
ID: attachment.legacy_attachment_id,
|
||||
filename: attachment.filename,
|
||||
thumbnailUrl: attachment.thumbnail.uri,
|
||||
|
||||
previewUrl: attachment.preview.uri,
|
||||
previewWidth: attachment.preview.width,
|
||||
previewHeight: attachment.preview.height,
|
||||
|
||||
largePreviewUrl: attachment.large_preview.uri,
|
||||
largePreviewHeight: attachment.large_preview.height,
|
||||
largePreviewWidth: attachment.large_preview.width,
|
||||
|
||||
// You have to query for the real image. See below.
|
||||
url: attachment.large_preview.uri, // @Legacy
|
||||
width: attachment.large_preview.width, // @Legacy
|
||||
height: attachment.large_preview.height, // @Legacy
|
||||
name: attachment.filename, // @Legacy
|
||||
|
||||
// @Undocumented
|
||||
attributionApp: attachment.attribution_app
|
||||
? {
|
||||
attributionAppID: attachment.attribution_app.id,
|
||||
name: attachment.attribution_app.name,
|
||||
logo: attachment.attribution_app.square_logo
|
||||
}
|
||||
: null
|
||||
|
||||
// @TODO No idea what this is, should we expose it?
|
||||
// Ben - July 15th 2017
|
||||
// renderAsSticker: attachment.render_as_sticker,
|
||||
|
||||
// This is _not_ the real URI, this is still just a large preview.
|
||||
// To get the URL we'll need to support a POST query to
|
||||
//
|
||||
// https://www.facebook.com/webgraphql/query/
|
||||
//
|
||||
// With the following query params:
|
||||
//
|
||||
// query_id:728987990612546
|
||||
// variables:{"id":"100009069356507","photoID":"10213724771692996"}
|
||||
// dpr:1
|
||||
//
|
||||
// No special form though.
|
||||
};
|
||||
case "MessageAnimatedImage":
|
||||
return {
|
||||
type: "animated_image",
|
||||
ID: attachment.legacy_attachment_id,
|
||||
filename: attachment.filename,
|
||||
|
||||
previewUrl: attachment.preview_image.uri,
|
||||
previewWidth: attachment.preview_image.width,
|
||||
previewHeight: attachment.preview_image.height,
|
||||
|
||||
url: attachment.animated_image.uri,
|
||||
width: attachment.animated_image.width,
|
||||
height: attachment.animated_image.height,
|
||||
|
||||
thumbnailUrl: attachment.preview_image.uri, // @Legacy
|
||||
name: attachment.filename, // @Legacy
|
||||
facebookUrl: attachment.animated_image.uri, // @Legacy
|
||||
rawGifImage: attachment.animated_image.uri, // @Legacy
|
||||
animatedGifUrl: attachment.animated_image.uri, // @Legacy
|
||||
animatedGifPreviewUrl: attachment.preview_image.uri, // @Legacy
|
||||
animatedWebpUrl: attachment.animated_image.uri, // @Legacy
|
||||
animatedWebpPreviewUrl: attachment.preview_image.uri, // @Legacy
|
||||
|
||||
// @Undocumented
|
||||
attributionApp: attachment.attribution_app
|
||||
? {
|
||||
attributionAppID: attachment.attribution_app.id,
|
||||
name: attachment.attribution_app.name,
|
||||
logo: attachment.attribution_app.square_logo
|
||||
}
|
||||
: null
|
||||
};
|
||||
case "MessageVideo":
|
||||
return {
|
||||
type: "video",
|
||||
filename: attachment.filename,
|
||||
ID: attachment.legacy_attachment_id,
|
||||
|
||||
thumbnailUrl: attachment.large_image.uri, // @Legacy
|
||||
|
||||
previewUrl: attachment.large_image.uri,
|
||||
previewWidth: attachment.large_image.width,
|
||||
previewHeight: attachment.large_image.height,
|
||||
|
||||
url: attachment.playable_url,
|
||||
width: attachment.original_dimensions.x,
|
||||
height: attachment.original_dimensions.y,
|
||||
|
||||
duration: attachment.playable_duration_in_ms,
|
||||
videoType: attachment.video_type.toLowerCase()
|
||||
};
|
||||
break;
|
||||
case "MessageFile":
|
||||
return {
|
||||
type: "file",
|
||||
filename: attachment.filename,
|
||||
ID: attachment.message_file_fbid,
|
||||
|
||||
url: attachment.url,
|
||||
isMalicious: attachment.is_malicious,
|
||||
contentType: attachment.content_type,
|
||||
|
||||
name: attachment.filename, // @Legacy
|
||||
mimeType: "", // @Legacy
|
||||
fileSize: -1 // @Legacy
|
||||
};
|
||||
case "MessageAudio":
|
||||
return {
|
||||
type: "audio",
|
||||
filename: attachment.filename,
|
||||
ID: attachment.url_shimhash, // Not fowardable
|
||||
|
||||
audioType: attachment.audio_type,
|
||||
duration: attachment.playable_duration_in_ms,
|
||||
url: attachment.playable_url,
|
||||
|
||||
isVoiceMail: attachment.is_voicemail
|
||||
};
|
||||
default:
|
||||
return {
|
||||
error: "Don't know about attachment type " + attachment.__typename
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function formatExtensibleAttachment(attachment) {
|
||||
if (attachment.story_attachment) {
|
||||
return {
|
||||
type: "share",
|
||||
ID: attachment.legacy_attachment_id,
|
||||
url: attachment.story_attachment.url,
|
||||
|
||||
title: attachment.story_attachment.title_with_entities.text,
|
||||
description:
|
||||
attachment.story_attachment.description &&
|
||||
attachment.story_attachment.description.text,
|
||||
source:
|
||||
attachment.story_attachment.source == null
|
||||
? null
|
||||
: attachment.story_attachment.source.text,
|
||||
|
||||
image:
|
||||
attachment.story_attachment.media == null
|
||||
? null
|
||||
: attachment.story_attachment.media.animated_image == null &&
|
||||
attachment.story_attachment.media.image == null
|
||||
? null
|
||||
: (
|
||||
attachment.story_attachment.media.animated_image ||
|
||||
attachment.story_attachment.media.image
|
||||
).uri,
|
||||
width:
|
||||
attachment.story_attachment.media == null
|
||||
? null
|
||||
: attachment.story_attachment.media.animated_image == null &&
|
||||
attachment.story_attachment.media.image == null
|
||||
? null
|
||||
: (
|
||||
attachment.story_attachment.media.animated_image ||
|
||||
attachment.story_attachment.media.image
|
||||
).width,
|
||||
height:
|
||||
attachment.story_attachment.media == null
|
||||
? null
|
||||
: attachment.story_attachment.media.animated_image == null &&
|
||||
attachment.story_attachment.media.image == null
|
||||
? null
|
||||
: (
|
||||
attachment.story_attachment.media.animated_image ||
|
||||
attachment.story_attachment.media.image
|
||||
).height,
|
||||
playable:
|
||||
attachment.story_attachment.media == null
|
||||
? null
|
||||
: attachment.story_attachment.media.is_playable,
|
||||
duration:
|
||||
attachment.story_attachment.media == null
|
||||
? null
|
||||
: attachment.story_attachment.media.playable_duration_in_ms,
|
||||
playableUrl:
|
||||
attachment.story_attachment.media == null
|
||||
? null
|
||||
: attachment.story_attachment.media.playable_url,
|
||||
|
||||
subattachments: attachment.story_attachment.subattachments,
|
||||
|
||||
// Format example:
|
||||
//
|
||||
// [{
|
||||
// key: "width",
|
||||
// value: { text: "1280" }
|
||||
// }]
|
||||
//
|
||||
// That we turn into:
|
||||
//
|
||||
// {
|
||||
// width: "1280"
|
||||
// }
|
||||
//
|
||||
properties: attachment.story_attachment.properties.reduce(function(
|
||||
obj,
|
||||
cur
|
||||
) {
|
||||
obj[cur.key] = cur.value.text;
|
||||
return obj;
|
||||
},
|
||||
{}),
|
||||
|
||||
// Deprecated fields
|
||||
animatedImageSize: "", // @Legacy
|
||||
facebookUrl: "", // @Legacy
|
||||
styleList: "", // @Legacy
|
||||
target: "", // @Legacy
|
||||
thumbnailUrl:
|
||||
attachment.story_attachment.media == null
|
||||
? null
|
||||
: attachment.story_attachment.media.animated_image == null &&
|
||||
attachment.story_attachment.media.image == null
|
||||
? null
|
||||
: (
|
||||
attachment.story_attachment.media.animated_image ||
|
||||
attachment.story_attachment.media.image
|
||||
).uri, // @Legacy
|
||||
thumbnailWidth:
|
||||
attachment.story_attachment.media == null
|
||||
? null
|
||||
: attachment.story_attachment.media.animated_image == null &&
|
||||
attachment.story_attachment.media.image == null
|
||||
? null
|
||||
: (
|
||||
attachment.story_attachment.media.animated_image ||
|
||||
attachment.story_attachment.media.image
|
||||
).width, // @Legacy
|
||||
thumbnailHeight:
|
||||
attachment.story_attachment.media == null
|
||||
? null
|
||||
: attachment.story_attachment.media.animated_image == null &&
|
||||
attachment.story_attachment.media.image == null
|
||||
? null
|
||||
: (
|
||||
attachment.story_attachment.media.animated_image ||
|
||||
attachment.story_attachment.media.image
|
||||
).height // @Legacy
|
||||
};
|
||||
} else {
|
||||
return { error: "Don't know what to do with extensible_attachment." };
|
||||
}
|
||||
}
|
||||
|
||||
function formatReactionsGraphQL(reaction) {
|
||||
return {
|
||||
reaction: reaction.reaction,
|
||||
userID: reaction.user.id
|
||||
};
|
||||
}
|
||||
|
||||
function formatEventData(event) {
|
||||
if (event == null) {
|
||||
return {};
|
||||
}
|
||||
|
||||
switch (event.__typename) {
|
||||
case "ThemeColorExtensibleMessageAdminText":
|
||||
return {
|
||||
color: event.theme_color
|
||||
};
|
||||
case "ThreadNicknameExtensibleMessageAdminText":
|
||||
return {
|
||||
nickname: event.nickname,
|
||||
participantID: event.participant_id
|
||||
};
|
||||
case "ThreadIconExtensibleMessageAdminText":
|
||||
return {
|
||||
threadIcon: event.thread_icon
|
||||
};
|
||||
case "InstantGameUpdateExtensibleMessageAdminText":
|
||||
return {
|
||||
gameID: (event.game == null ? null : event.game.id),
|
||||
update_type: event.update_type,
|
||||
collapsed_text: event.collapsed_text,
|
||||
expanded_text: event.expanded_text,
|
||||
instant_game_update_data: event.instant_game_update_data
|
||||
};
|
||||
case "GameScoreExtensibleMessageAdminText":
|
||||
return {
|
||||
game_type: event.game_type
|
||||
};
|
||||
case "RtcCallLogExtensibleMessageAdminText":
|
||||
return {
|
||||
event: event.event,
|
||||
is_video_call: event.is_video_call,
|
||||
server_info_data: event.server_info_data
|
||||
};
|
||||
case "GroupPollExtensibleMessageAdminText":
|
||||
return {
|
||||
event_type: event.event_type,
|
||||
total_count: event.total_count,
|
||||
question: event.question
|
||||
};
|
||||
case "AcceptPendingThreadExtensibleMessageAdminText":
|
||||
return {
|
||||
accepter_id: event.accepter_id,
|
||||
requester_id: event.requester_id
|
||||
};
|
||||
case "ConfirmFriendRequestExtensibleMessageAdminText":
|
||||
return {
|
||||
friend_request_recipient: event.friend_request_recipient,
|
||||
friend_request_sender: event.friend_request_sender
|
||||
};
|
||||
case "AddContactExtensibleMessageAdminText":
|
||||
return {
|
||||
contact_added_id: event.contact_added_id,
|
||||
contact_adder_id: event.contact_adder_id
|
||||
};
|
||||
case "AdExtensibleMessageAdminText":
|
||||
return {
|
||||
ad_client_token: event.ad_client_token,
|
||||
ad_id: event.ad_id,
|
||||
ad_preferences_link: event.ad_preferences_link,
|
||||
ad_properties: event.ad_properties
|
||||
};
|
||||
// never data
|
||||
case "ParticipantJoinedGroupCallExtensibleMessageAdminText":
|
||||
case "ThreadEphemeralTtlModeExtensibleMessageAdminText":
|
||||
case "StartedSharingVideoExtensibleMessageAdminText":
|
||||
case "LightweightEventCreateExtensibleMessageAdminText":
|
||||
case "LightweightEventNotifyExtensibleMessageAdminText":
|
||||
case "LightweightEventNotifyBeforeEventExtensibleMessageAdminText":
|
||||
case "LightweightEventUpdateTitleExtensibleMessageAdminText":
|
||||
case "LightweightEventUpdateTimeExtensibleMessageAdminText":
|
||||
case "LightweightEventUpdateLocationExtensibleMessageAdminText":
|
||||
case "LightweightEventDeleteExtensibleMessageAdminText":
|
||||
return {};
|
||||
default:
|
||||
return {
|
||||
error: "Don't know what to with event data type " + event.__typename
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function formatMessagesGraphQLResponse(data) {
|
||||
var messageThread = data.o0.data.message_thread;
|
||||
var threadID = messageThread.thread_key.thread_fbid
|
||||
? messageThread.thread_key.thread_fbid
|
||||
: messageThread.thread_key.other_user_id;
|
||||
|
||||
var messages = messageThread.messages.nodes.map(function(d) {
|
||||
switch (d.__typename) {
|
||||
case "UserMessage":
|
||||
// Give priority to stickers. They're seen as normal messages but we've
|
||||
// been considering them as attachments.
|
||||
var maybeStickerAttachment;
|
||||
if (d.sticker) {
|
||||
maybeStickerAttachment = [
|
||||
{
|
||||
type: "sticker",
|
||||
ID: d.sticker.id,
|
||||
url: d.sticker.url,
|
||||
|
||||
packID: d.sticker.pack.id,
|
||||
spriteUrl: d.sticker.sprite_image,
|
||||
spriteUrl2x: d.sticker.sprite_image_2x,
|
||||
width: d.sticker.width,
|
||||
height: d.sticker.height,
|
||||
|
||||
caption: d.snippet, // Not sure what the heck caption was.
|
||||
description: d.sticker.label, // Not sure about this one either.
|
||||
|
||||
frameCount: d.sticker.frame_count,
|
||||
frameRate: d.sticker.frame_rate,
|
||||
framesPerRow: d.sticker.frames_per_row,
|
||||
framesPerCol: d.sticker.frames_per_col,
|
||||
|
||||
stickerID: d.sticker.id, // @Legacy
|
||||
spriteURI: d.sticker.sprite_image, // @Legacy
|
||||
spriteURI2x: d.sticker.sprite_image_2x // @Legacy
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
var mentionsObj = {};
|
||||
if (d.message !== null) {
|
||||
d.message.ranges.forEach(e => {
|
||||
mentionsObj[e.entity.id] = d.message.text.substr(e.offset, e.length);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
type: "message",
|
||||
attachments: maybeStickerAttachment
|
||||
? maybeStickerAttachment
|
||||
: d.blob_attachments && d.blob_attachments.length > 0
|
||||
? d.blob_attachments.map(formatAttachmentsGraphQLResponse)
|
||||
: d.extensible_attachment
|
||||
? [formatExtensibleAttachment(d.extensible_attachment)]
|
||||
: [],
|
||||
body: d.message !== null ? d.message.text : '',
|
||||
isGroup: messageThread.thread_type === "GROUP",
|
||||
messageID: d.message_id,
|
||||
senderID: d.message_sender.id,
|
||||
threadID: threadID,
|
||||
timestamp: d.timestamp_precise,
|
||||
|
||||
mentions: mentionsObj,
|
||||
isUnread: d.unread,
|
||||
|
||||
// New
|
||||
messageReactions: d.message_reactions
|
||||
? d.message_reactions.map(formatReactionsGraphQL)
|
||||
: null,
|
||||
isSponsored: d.is_sponsored,
|
||||
snippet: d.snippet
|
||||
};
|
||||
case "ThreadNameMessage":
|
||||
return {
|
||||
type: "event",
|
||||
messageID: d.message_id,
|
||||
threadID: threadID,
|
||||
isGroup: messageThread.thread_type === "GROUP",
|
||||
senderID: d.message_sender.id,
|
||||
timestamp: d.timestamp_precise,
|
||||
eventType: "change_thread_name",
|
||||
snippet: d.snippet,
|
||||
eventData: {
|
||||
threadName: d.thread_name
|
||||
},
|
||||
|
||||
// @Legacy
|
||||
author: d.message_sender.id,
|
||||
logMessageType: "log:thread-name",
|
||||
logMessageData: { name: d.thread_name }
|
||||
};
|
||||
case "ThreadImageMessage":
|
||||
return {
|
||||
type: "event",
|
||||
messageID: d.message_id,
|
||||
threadID: threadID,
|
||||
isGroup: messageThread.thread_type === "GROUP",
|
||||
senderID: d.message_sender.id,
|
||||
timestamp: d.timestamp_precise,
|
||||
eventType: "change_thread_image",
|
||||
snippet: d.snippet,
|
||||
eventData:
|
||||
d.image_with_metadata == null
|
||||
? {} /* removed image */
|
||||
: {
|
||||
/* image added */
|
||||
threadImage: {
|
||||
attachmentID: d.image_with_metadata.legacy_attachment_id,
|
||||
width: d.image_with_metadata.original_dimensions.x,
|
||||
height: d.image_with_metadata.original_dimensions.y,
|
||||
url: d.image_with_metadata.preview.uri
|
||||
}
|
||||
},
|
||||
|
||||
// @Legacy
|
||||
logMessageType: "log:thread-icon",
|
||||
logMessageData: {
|
||||
thread_icon: d.image_with_metadata
|
||||
? d.image_with_metadata.preview.uri
|
||||
: null
|
||||
}
|
||||
};
|
||||
case "ParticipantLeftMessage":
|
||||
return {
|
||||
type: "event",
|
||||
messageID: d.message_id,
|
||||
threadID: threadID,
|
||||
isGroup: messageThread.thread_type === "GROUP",
|
||||
senderID: d.message_sender.id,
|
||||
timestamp: d.timestamp_precise,
|
||||
eventType: "remove_participants",
|
||||
snippet: d.snippet,
|
||||
eventData: {
|
||||
// Array of IDs.
|
||||
participantsRemoved: d.participants_removed.map(function(p) {
|
||||
return p.id;
|
||||
})
|
||||
},
|
||||
|
||||
// @Legacy
|
||||
logMessageType: "log:unsubscribe",
|
||||
logMessageData: {
|
||||
leftParticipantFbId: d.participants_removed.map(function(p) {
|
||||
return p.id;
|
||||
})
|
||||
}
|
||||
};
|
||||
case "ParticipantsAddedMessage":
|
||||
return {
|
||||
type: "event",
|
||||
messageID: d.message_id,
|
||||
threadID: threadID,
|
||||
isGroup: messageThread.thread_type === "GROUP",
|
||||
senderID: d.message_sender.id,
|
||||
timestamp: d.timestamp_precise,
|
||||
eventType: "add_participants",
|
||||
snippet: d.snippet,
|
||||
eventData: {
|
||||
// Array of IDs.
|
||||
participantsAdded: d.participants_added.map(function(p) {
|
||||
return p.id;
|
||||
})
|
||||
},
|
||||
|
||||
// @Legacy
|
||||
logMessageType: "log:subscribe",
|
||||
logMessageData: {
|
||||
addedParticipants: d.participants_added.map(function(p) {
|
||||
return p.id;
|
||||
})
|
||||
}
|
||||
};
|
||||
case "VideoCallMessage":
|
||||
return {
|
||||
type: "event",
|
||||
messageID: d.message_id,
|
||||
threadID: threadID,
|
||||
isGroup: messageThread.thread_type === "GROUP",
|
||||
senderID: d.message_sender.id,
|
||||
timestamp: d.timestamp_precise,
|
||||
eventType: "video_call",
|
||||
snippet: d.snippet,
|
||||
|
||||
// @Legacy
|
||||
logMessageType: "other"
|
||||
};
|
||||
case "VoiceCallMessage":
|
||||
return {
|
||||
type: "event",
|
||||
messageID: d.message_id,
|
||||
threadID: threadID,
|
||||
isGroup: messageThread.thread_type === "GROUP",
|
||||
senderID: d.message_sender.id,
|
||||
timestamp: d.timestamp_precise,
|
||||
eventType: "voice_call",
|
||||
snippet: d.snippet,
|
||||
|
||||
// @Legacy
|
||||
logMessageType: "other"
|
||||
};
|
||||
case "GenericAdminTextMessage":
|
||||
return {
|
||||
type: "event",
|
||||
messageID: d.message_id,
|
||||
threadID: threadID,
|
||||
isGroup: messageThread.thread_type === "GROUP",
|
||||
senderID: d.message_sender.id,
|
||||
timestamp: d.timestamp_precise,
|
||||
snippet: d.snippet,
|
||||
eventType: d.extensible_message_admin_text_type.toLowerCase(),
|
||||
eventData: formatEventData(d.extensible_message_admin_text),
|
||||
|
||||
// @Legacy
|
||||
logMessageType: utils.getAdminTextMessageType(
|
||||
d.extensible_message_admin_text_type
|
||||
),
|
||||
logMessageData: d.extensible_message_admin_text // Maybe different?
|
||||
};
|
||||
default:
|
||||
return { error: "Don't know about message type " + d.__typename };
|
||||
}
|
||||
});
|
||||
return messages;
|
||||
}
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function getThreadHistoryGraphQL(
|
||||
threadID,
|
||||
amount,
|
||||
timestamp,
|
||||
callback
|
||||
) {
|
||||
if (!callback) {
|
||||
throw { error: "getThreadHistoryGraphQL: need callback" };
|
||||
}
|
||||
|
||||
// `queries` has to be a string. I couldn't tell from the dev console. This
|
||||
// took me a really long time to figure out. I deserve a cookie for this.
|
||||
var form = {
|
||||
"av": ctx.globalOptions.pageID,
|
||||
queries: JSON.stringify({
|
||||
o0: {
|
||||
// This doc_id was valid on February 2nd 2017.
|
||||
doc_id: "1498317363570230",
|
||||
query_params: {
|
||||
id: threadID,
|
||||
message_limit: amount,
|
||||
load_messages: 1,
|
||||
load_read_receipts: false,
|
||||
before: timestamp
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
// This returns us an array of things. The last one is the success /
|
||||
// failure one.
|
||||
// @TODO What do we do in this case?
|
||||
if (resData[resData.length - 1].error_results !== 0) {
|
||||
throw new Error("well darn there was an error_result");
|
||||
}
|
||||
|
||||
callback(null, formatMessagesGraphQLResponse(resData[0]));
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("getThreadHistoryGraphQL", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function getThreadHistory(threadID, amount, timestamp, callback) {
|
||||
if (!callback) {
|
||||
throw { error: "getThreadHistory: need callback" };
|
||||
}
|
||||
|
||||
var form = {
|
||||
client: "mercury"
|
||||
};
|
||||
|
||||
api.getUserInfo(threadID, function(err, res) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
var key = Object.keys(res).length > 0 ? "user_ids" : "thread_fbids";
|
||||
form["messages[" + key + "][" + threadID + "][offset]"] = 0;
|
||||
form["messages[" + key + "][" + threadID + "][timestamp]"] = timestamp;
|
||||
form["messages[" + key + "][" + threadID + "][limit]"] = amount;
|
||||
|
||||
if (ctx.globalOptions.pageID)
|
||||
form.request_user_id = ctx.globalOptions.pageID;
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/mercury/thread_info.php",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
} else if (!resData.payload) {
|
||||
throw { error: "Could not retrieve thread history." };
|
||||
}
|
||||
|
||||
// Asking for message history from a thread with no message history
|
||||
// will return undefined for actions here
|
||||
if (!resData.payload.actions) {
|
||||
resData.payload.actions = [];
|
||||
}
|
||||
|
||||
var userIDs = {};
|
||||
resData.payload.actions.forEach(function(v) {
|
||||
userIDs[v.author.split(":").pop()] = "";
|
||||
});
|
||||
|
||||
api.getUserInfo(Object.keys(userIDs), function(err, data) {
|
||||
if (err) return callback(err); //callback({error: "Could not retrieve user information in getThreadHistory."});
|
||||
|
||||
resData.payload.actions.forEach(function(v) {
|
||||
var sender = data[v.author.split(":").pop()];
|
||||
if (sender) v.sender_name = sender.name;
|
||||
else v.sender_name = "Facebook User";
|
||||
v.sender_fbid = v.author;
|
||||
delete v.author;
|
||||
});
|
||||
|
||||
callback(
|
||||
null,
|
||||
resData.payload.actions.map(utils.formatHistoryMessage)
|
||||
);
|
||||
});
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("getThreadHistory", err);
|
||||
return callback(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
function formatEventReminders(reminder) {
|
||||
return {
|
||||
reminderID: reminder.id,
|
||||
eventCreatorID: reminder.lightweight_event_creator.id,
|
||||
time: reminder.time,
|
||||
eventType: reminder.lightweight_event_type.toLowerCase(),
|
||||
locationName: reminder.location_name,
|
||||
// @TODO verify this
|
||||
locationCoordinates: reminder.location_coordinates,
|
||||
locationPage: reminder.location_page,
|
||||
eventStatus: reminder.lightweight_event_status.toLowerCase(),
|
||||
note: reminder.note,
|
||||
repeatMode: reminder.repeat_mode.toLowerCase(),
|
||||
eventTitle: reminder.event_title,
|
||||
triggerMessage: reminder.trigger_message,
|
||||
secondsToNotifyBefore: reminder.seconds_to_notify_before,
|
||||
allowsRsvp: reminder.allows_rsvp,
|
||||
relatedEvent: reminder.related_event,
|
||||
members: reminder.event_reminder_members.edges.map(function(member) {
|
||||
return {
|
||||
memberID: member.node.id,
|
||||
state: member.guest_list_state.toLowerCase()
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
function formatThreadGraphQLResponse(data) {
|
||||
var messageThread = data.o0.data.message_thread;
|
||||
var threadID = messageThread.thread_key.thread_fbid
|
||||
? messageThread.thread_key.thread_fbid
|
||||
: messageThread.thread_key.other_user_id;
|
||||
|
||||
// Remove me
|
||||
var lastM = messageThread.last_message;
|
||||
var snippetID =
|
||||
lastM &&
|
||||
lastM.nodes &&
|
||||
lastM.nodes[0] &&
|
||||
lastM.nodes[0].message_sender &&
|
||||
lastM.nodes[0].message_sender.messaging_actor
|
||||
? lastM.nodes[0].message_sender.messaging_actor.id
|
||||
: null;
|
||||
var snippetText =
|
||||
lastM && lastM.nodes && lastM.nodes[0] ? lastM.nodes[0].snippet : null;
|
||||
var lastR = messageThread.last_read_receipt;
|
||||
var lastReadTimestamp =
|
||||
lastR && lastR.nodes && lastR.nodes[0] && lastR.nodes[0].timestamp_precise
|
||||
? lastR.nodes[0].timestamp_precise
|
||||
: null;
|
||||
|
||||
return {
|
||||
threadID: threadID,
|
||||
threadName: messageThread.name,
|
||||
participantIDs: messageThread.all_participants.nodes.map(function(d) {
|
||||
return d.messaging_actor.id;
|
||||
}),
|
||||
unreadCount: messageThread.unread_count,
|
||||
messageCount: messageThread.messages_count,
|
||||
timestamp: messageThread.updated_time_precise,
|
||||
muteUntil: messageThread.mute_until,
|
||||
isGroup: messageThread.thread_type == "GROUP",
|
||||
isSubscribed: messageThread.is_viewer_subscribed,
|
||||
isArchived: messageThread.has_viewer_archived,
|
||||
folder: messageThread.folder,
|
||||
cannotReplyReason: messageThread.cannot_reply_reason,
|
||||
eventReminders: messageThread.event_reminders
|
||||
? messageThread.event_reminders.nodes.map(formatEventReminders)
|
||||
: null,
|
||||
emoji: messageThread.customization_info
|
||||
? messageThread.customization_info.emoji
|
||||
: null,
|
||||
color:
|
||||
messageThread.customization_info &&
|
||||
messageThread.customization_info.outgoing_bubble_color
|
||||
? messageThread.customization_info.outgoing_bubble_color.slice(2)
|
||||
: null,
|
||||
nicknames:
|
||||
messageThread.customization_info &&
|
||||
messageThread.customization_info.participant_customizations
|
||||
? messageThread.customization_info.participant_customizations.reduce(
|
||||
function(res, val) {
|
||||
if (val.nickname) res[val.participant_id] = val.nickname;
|
||||
return res;
|
||||
},
|
||||
{}
|
||||
)
|
||||
: {},
|
||||
adminIDs: messageThread.thread_admins,
|
||||
|
||||
// @Undocumented
|
||||
topEmojis: messageThread.top_emojis,
|
||||
reactionsMuteMode: messageThread.reactions_mute_mode.toLowerCase(),
|
||||
mentionsMuteMode: messageThread.mentions_mute_mode.toLowerCase(),
|
||||
isPinProtected: messageThread.is_pin_protected,
|
||||
relatedPageThread: messageThread.related_page_thread,
|
||||
|
||||
// @Legacy
|
||||
name: messageThread.name,
|
||||
snippet: snippetText,
|
||||
snippetSender: snippetID,
|
||||
snippetAttachments: [],
|
||||
serverTimestamp: messageThread.updated_time_precise,
|
||||
imageSrc: messageThread.image ? messageThread.image.uri : null,
|
||||
isCanonicalUser: messageThread.is_canonical_neo_user,
|
||||
isCanonical: messageThread.thread_type != "GROUP",
|
||||
recipientsLoadable: true,
|
||||
hasEmailParticipant: false,
|
||||
readOnly: false,
|
||||
canReply: messageThread.cannot_reply_reason == null,
|
||||
lastMessageTimestamp: messageThread.last_message
|
||||
? messageThread.last_message.timestamp_precise
|
||||
: null,
|
||||
lastMessageType: "message",
|
||||
lastReadTimestamp: lastReadTimestamp,
|
||||
threadType: messageThread.thread_type == "GROUP" ? 2 : 1
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function getThreadInfoGraphQL(threadID, callback) {
|
||||
if (!callback) {
|
||||
throw { error: "getThreadInfoGraphQL: need callback" };
|
||||
}
|
||||
|
||||
// `queries` has to be a string. I couldn't tell from the dev console. This
|
||||
// took me a really long time to figure out. I deserve a cookie for this.
|
||||
var form = {
|
||||
queries: JSON.stringify({
|
||||
o0: {
|
||||
// This doc_id is valid as of February 1st, 2018.
|
||||
doc_id: "1498317363570230",
|
||||
query_params: {
|
||||
id: threadID,
|
||||
message_limit: 0,
|
||||
load_messages: 0,
|
||||
load_read_receipts: false,
|
||||
before: null
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
// This returns us an array of things. The last one is the success /
|
||||
// failure one.
|
||||
// @TODO What do we do in this case?
|
||||
if (resData[resData.length - 1].error_results !== 0) {
|
||||
throw new Error("well darn there was an error_result");
|
||||
}
|
||||
|
||||
callback(null, formatThreadGraphQLResponse(resData[0]));
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("getThreadInfoGraphQL", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function getThreadInfo(threadID, callback) {
|
||||
if (!callback) callback = function() {};
|
||||
|
||||
var form = {
|
||||
client: "mercury"
|
||||
};
|
||||
|
||||
api.getUserInfo(threadID, function(err, userRes) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
var key = Object.keys(userRes).length > 0 ? "user_ids" : "thread_fbids";
|
||||
form["threads[" + key + "][0]"] = threadID;
|
||||
|
||||
if (ctx.globalOptions.pageId)
|
||||
form.request_user_id = ctx.globalOptions.pageId;
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/mercury/thread_info.php",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
} else if (!resData.payload) {
|
||||
throw {
|
||||
error: "Could not retrieve thread Info."
|
||||
};
|
||||
}
|
||||
var threadData = resData.payload.threads[0];
|
||||
var userData = userRes[threadID];
|
||||
|
||||
if (threadData == null) {
|
||||
throw {
|
||||
error: "ThreadData is null"
|
||||
};
|
||||
}
|
||||
|
||||
threadData.name =
|
||||
userData != null && userData.name != null
|
||||
? userData.name
|
||||
: threadData.name;
|
||||
threadData.image_src =
|
||||
userData != null && userData.thumbSrc != null
|
||||
? userData.thumbSrc
|
||||
: threadData.image_src;
|
||||
|
||||
callback(null, utils.formatThread(threadData));
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("getThreadInfo", err);
|
||||
return callback(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
"use strict";
|
||||
|
||||
const utils = require("../utils");
|
||||
const log = require("npmlog");
|
||||
|
||||
function createProfileUrl(url, username, id) {
|
||||
if (url) return url;
|
||||
return "https://www.facebook.com/" + (username || utils.formatID(id.toString()));
|
||||
}
|
||||
|
||||
function formatParticipants(participants) {
|
||||
return participants.nodes.map((p)=>{
|
||||
p = p.messaging_actor;
|
||||
switch (p["__typename"]) {
|
||||
case "User":
|
||||
return {
|
||||
accountType: p["__typename"],
|
||||
userID: utils.formatID(p.id.toString()), // do we need .toString()? when it is not a string?
|
||||
name: p.name,
|
||||
shortName: p.short_name,
|
||||
gender: p.gender,
|
||||
url: p.url, // how about making it profileURL
|
||||
profilePicture: p.big_image_src.uri,
|
||||
username: (p.username||null),
|
||||
// TODO: maybe better names for these?
|
||||
isViewerFriend: p.is_viewer_friend, // true/false
|
||||
isMessengerUser: p.is_messenger_user, // true/false
|
||||
isVerified: p.is_verified, // true/false
|
||||
isMessageBlockedByViewer: p.is_message_blocked_by_viewer, // true/false
|
||||
isViewerCoworker: p.is_viewer_coworker, // true/false
|
||||
isEmployee: p.is_employee // null? when it is something other? can someone check?
|
||||
};
|
||||
case "Page":
|
||||
return {
|
||||
accountType: p["__typename"],
|
||||
userID: utils.formatID(p.id.toString()), // or maybe... pageID?
|
||||
name: p.name,
|
||||
url: p.url,
|
||||
profilePicture: p.big_image_src.uri,
|
||||
username: (p.username||null),
|
||||
// uhm... better names maybe?
|
||||
acceptsMessengerUserFeedback: p.accepts_messenger_user_feedback, // true/false
|
||||
isMessengerUser: p.is_messenger_user, // true/false
|
||||
isVerified: p.is_verified, // true/false
|
||||
isMessengerPlatformBot: p.is_messenger_platform_bot, // true/false
|
||||
isMessageBlockedByViewer: p.is_message_blocked_by_viewer, // true/false
|
||||
};
|
||||
case "ReducedMessagingActor":
|
||||
return {
|
||||
accountType: p["__typename"],
|
||||
userID: utils.formatID(p.id.toString()),
|
||||
name: p.name,
|
||||
url: createProfileUrl(p.url, p.username, p.id), // in this case p.url is null all the time
|
||||
profilePicture: p.big_image_src.uri, // in this case it is default facebook photo, we could determine gender using it
|
||||
username: (p.username||null), // maybe we could use it to generate profile URL?
|
||||
isMessageBlockedByViewer: p.is_message_blocked_by_viewer, // true/false
|
||||
};
|
||||
case "UnavailableMessagingActor":
|
||||
return {
|
||||
accountType: p["__typename"],
|
||||
userID: utils.formatID(p.id.toString()),
|
||||
name: p.name, // "Facebook User" in user's language
|
||||
url: createProfileUrl(p.url, p.username, p.id), // in this case p.url is null all the time
|
||||
profilePicture: p.big_image_src.uri, // default male facebook photo
|
||||
username: (p.username||null), // maybe we could use it to generate profile URL?
|
||||
isMessageBlockedByViewer: p.is_message_blocked_by_viewer, // true/false
|
||||
};
|
||||
default:
|
||||
log.warn("getThreadList", "Found participant with unsupported typename. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues\n" + JSON.stringify(p, null, 2));
|
||||
return {
|
||||
accountType: p["__typename"],
|
||||
userID: utils.formatID(p.id.toString()),
|
||||
name: p.name || `[unknown ${p["__typename"]}]`, // probably it will always be something... but fallback to [unknown], just in case
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// "FF8C0077" -> "8C0077"
|
||||
function formatColor(color) {
|
||||
if (color && color.match(/^(?:[0-9a-fA-F]{8})$/g)) {
|
||||
return color.slice(2);
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
function getThreadName(t) {
|
||||
if (t.name || t.thread_key.thread_fbid) return t.name;
|
||||
|
||||
for (let p of t.all_participants.nodes) {
|
||||
if (p.messaging_actor.id === t.thread_key.other_user_id) return p.messaging_actor.name;
|
||||
}
|
||||
}
|
||||
|
||||
function mapNicknames(customizationInfo) {
|
||||
return (customizationInfo && customizationInfo.participant_customizations) ? customizationInfo.participant_customizations.map(u => {
|
||||
return {
|
||||
"userID": u.participant_id,
|
||||
"nickname": u.nickname
|
||||
};
|
||||
}):[];
|
||||
}
|
||||
|
||||
function formatThreadList(data) {
|
||||
return data.map(t => {
|
||||
let lastMessageNode = (t.last_message&&t.last_message.nodes&&t.last_message.nodes.length>0)?t.last_message.nodes[0]:null;
|
||||
return {
|
||||
threadID: t.thread_key?utils.formatID(t.thread_key.thread_fbid || t.thread_key.other_user_id):null, // shall never be null
|
||||
name: getThreadName(t),
|
||||
unreadCount: t.unread_count,
|
||||
messageCount: t.messages_count,
|
||||
imageSrc: t.image?t.image.uri:null,
|
||||
emoji: t.customization_info?t.customization_info.emoji:null,
|
||||
color: formatColor(t.customization_info?t.customization_info.outgoing_bubble_color:null),
|
||||
nicknames: mapNicknames(t.customization_info),
|
||||
muteUntil: t.mute_until,
|
||||
participants: formatParticipants(t.all_participants),
|
||||
adminIDs: t.thread_admins.map(a => a.id),
|
||||
folder: t.folder,
|
||||
isGroup: t.thread_type === "GROUP",
|
||||
// rtc_call_data: t.rtc_call_data, // TODO: format and document this
|
||||
// isPinProtected: t.is_pin_protected, // feature from future? always false (2018-04-04)
|
||||
customizationEnabled: t.customization_enabled, // false for ONE_TO_ONE with Page or ReducedMessagingActor
|
||||
participantAddMode: t.participant_add_mode_as_string, // "ADD" if "GROUP" and null if "ONE_TO_ONE"
|
||||
montageThread: t.montage_thread?Buffer.from(t.montage_thread.id,"base64").toString():null, // base64 encoded string "message_thread:0000000000000000"
|
||||
// it is not userID nor any other ID known to me...
|
||||
// can somebody inspect it? where is it used?
|
||||
// probably Messenger Day uses it
|
||||
reactionsMuteMode: t.reactions_mute_mode,
|
||||
mentionsMuteMode: t.mentions_mute_mode,
|
||||
isArchived: t.has_viewer_archived,
|
||||
isSubscribed: t.is_viewer_subscribed,
|
||||
timestamp: t.updated_time_precise, // in miliseconds
|
||||
// isCanonicalUser: t.is_canonical_neo_user, // is it always false?
|
||||
// TODO: how about putting snippet in another object? current implementation does not handle every possibile message type etc.
|
||||
snippet: lastMessageNode?lastMessageNode.snippet:null,
|
||||
snippetAttachments: lastMessageNode?lastMessageNode.extensible_attachment:null, // TODO: not sure if it works
|
||||
snippetSender: lastMessageNode?utils.formatID((lastMessageNode.message_sender.messaging_actor.id || "").toString()):null,
|
||||
lastMessageTimestamp: lastMessageNode?lastMessageNode.timestamp_precise:null, // timestamp in miliseconds
|
||||
lastReadTimestamp: (t.last_read_receipt&&t.last_read_receipt.nodes.length>0)
|
||||
? (t.last_read_receipt.nodes[0]?t.last_read_receipt.nodes[0].timestamp_precise:null)
|
||||
: null, // timestamp in miliseconds
|
||||
cannotReplyReason: t.cannot_reply_reason, // TODO: inspect possible values
|
||||
|
||||
// @Legacy
|
||||
participantIDs: formatParticipants(t.all_participants).map(participant => participant.userID),
|
||||
threadType: t.thread_type === "GROUP" ? 2 : 1 // "GROUP" or "ONE_TO_ONE"
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function getThreadList(limit, timestamp, tags, callback) {
|
||||
if (!callback && (utils.getType(tags) === "Function" || utils.getType(tags) === "AsyncFunction")) {
|
||||
callback = tags;
|
||||
tags = [""];
|
||||
}
|
||||
if (utils.getType(limit) !== "Number" || !Number.isInteger(limit) || limit <= 0) {
|
||||
throw {error: "getThreadList: limit must be a positive integer"};
|
||||
}
|
||||
if (utils.getType(timestamp) !== "Null" &&
|
||||
(utils.getType(timestamp) !== "Number" || !Number.isInteger(timestamp))) {
|
||||
throw {error: "getThreadList: timestamp must be an integer or null"};
|
||||
}
|
||||
if (utils.getType(tags) === "String") {
|
||||
tags = [tags];
|
||||
}
|
||||
if (utils.getType(tags) !== "Array") {
|
||||
throw {error: "getThreadList: tags must be an array"};
|
||||
}
|
||||
if (utils.getType(callback) !== "Function" && utils.getType(callback) !== "AsyncFunction") {
|
||||
throw {error: "getThreadList: need callback"};
|
||||
}
|
||||
|
||||
const form = {
|
||||
"av": ctx.globalOptions.pageID,
|
||||
"queries": JSON.stringify({
|
||||
"o0": {
|
||||
// This doc_id was valid on 2018-04-04.
|
||||
"doc_id": "1349387578499440",
|
||||
"query_params": {
|
||||
"limit": limit+(timestamp?1:0),
|
||||
"before": timestamp,
|
||||
"tags": tags,
|
||||
"includeDeliveryReceipts": true,
|
||||
"includeSeqID": false
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then((resData) => {
|
||||
if (resData[resData.length - 1].error_results > 0) {
|
||||
throw resData[0].o0.errors;
|
||||
}
|
||||
|
||||
if (resData[resData.length - 1].successful_results === 0) {
|
||||
throw {error: "getThreadList: there was no successful_results", res: resData};
|
||||
}
|
||||
|
||||
// When we ask for threads using timestamp from the previous request,
|
||||
// we are getting the last thread repeated as the first thread in this response.
|
||||
// .shift() gets rid of it
|
||||
// It is also the reason for increasing limit by 1 when timestamp is set
|
||||
// this way user asks for 10 threads, we are asking for 11,
|
||||
// but after removing the duplicated one, it is again 10
|
||||
if (timestamp) {
|
||||
resData[0].o0.data.viewer.message_threads.nodes.shift();
|
||||
}
|
||||
callback(null, formatThreadList(resData[0].o0.data.viewer.message_threads.nodes));
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error("getThreadList", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function getThreadList(start, end, type, callback) {
|
||||
if (utils.getType(callback) === "Undefined") {
|
||||
if (utils.getType(end) !== "Number") {
|
||||
throw {
|
||||
error: "Please pass a number as a second argument."
|
||||
};
|
||||
} else if (
|
||||
utils.getType(type) === "Function" ||
|
||||
utils.getType(type) === "AsyncFunction"
|
||||
) {
|
||||
callback = type;
|
||||
type = "inbox"; //default to inbox
|
||||
} else if (utils.getType(type) !== "String") {
|
||||
throw {
|
||||
error:
|
||||
"Please pass a String as a third argument. Your options are: inbox, pending, and archived"
|
||||
};
|
||||
} else {
|
||||
throw {
|
||||
error: "getThreadList: need callback"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (type === "archived") {
|
||||
type = "action:archived";
|
||||
} else if (type !== "inbox" && type !== "pending" && type !== "other") {
|
||||
throw {
|
||||
error:
|
||||
"type can only be one of the following: inbox, pending, archived, other"
|
||||
};
|
||||
}
|
||||
|
||||
if (end <= start) end = start + 20;
|
||||
|
||||
var form = {
|
||||
client: "mercury"
|
||||
};
|
||||
|
||||
form[type + "[offset]"] = start;
|
||||
form[type + "[limit]"] = end - start;
|
||||
|
||||
if (ctx.globalOptions.pageID) {
|
||||
form.request_user_id = ctx.globalOptions.pageID;
|
||||
}
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/mercury/threadlist_info.php",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
log.verbose("getThreadList", JSON.stringify(resData.payload.threads));
|
||||
return callback(
|
||||
null,
|
||||
(resData.payload.threads || []).map(utils.formatThread)
|
||||
);
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("getThreadList", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function getThreadPictures(threadID, offset, limit, callback) {
|
||||
if (!callback) {
|
||||
throw { error: "getThreadPictures: need callback" };
|
||||
}
|
||||
|
||||
var form = {
|
||||
thread_id: threadID,
|
||||
offset: offset,
|
||||
limit: limit
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/messaging/attachments/sharedphotos.php",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
return Promise.all(
|
||||
resData.payload.imagesData.map(function(image) {
|
||||
form = {
|
||||
thread_id: threadID,
|
||||
image_id: image.fbid
|
||||
};
|
||||
return defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/messaging/attachments/sharedphotos.php",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
// the response is pretty messy
|
||||
var queryThreadID =
|
||||
resData.jsmods.require[0][3][1].query_metadata.query_path[0]
|
||||
.message_thread;
|
||||
var imageData =
|
||||
resData.jsmods.require[0][3][1].query_results[queryThreadID]
|
||||
.message_images.edges[0].node.image2;
|
||||
return imageData;
|
||||
});
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(function(resData) {
|
||||
callback(null, resData);
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("Error in getThreadPictures", err);
|
||||
callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
function formatData(data) {
|
||||
return {
|
||||
userID: utils.formatID(data.uid.toString()),
|
||||
photoUrl: data.photo,
|
||||
indexRank: data.index_rank,
|
||||
name: data.text,
|
||||
isVerified: data.is_verified,
|
||||
profileUrl: data.path,
|
||||
category: data.category,
|
||||
score: data.score,
|
||||
type: data.type
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function getUserID(name, callback) {
|
||||
if (!callback) {
|
||||
throw { error: "getUserID: need callback" };
|
||||
}
|
||||
|
||||
var form = {
|
||||
value: name.toLowerCase(),
|
||||
viewer: ctx.userID,
|
||||
rsp: "search",
|
||||
context: "search",
|
||||
path: "/home.php",
|
||||
request_id: utils.getGUID()
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.get("https://www.facebook.com/ajax/typeahead/search.php", ctx.jar, form)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
var data = resData.payload.entries;
|
||||
|
||||
callback(null, data.map(formatData));
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("getUserID", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
function formatData(data) {
|
||||
var retObj = {};
|
||||
|
||||
for (var prop in data) {
|
||||
if (data.hasOwnProperty(prop)) {
|
||||
var innerObj = data[prop];
|
||||
retObj[prop] = {
|
||||
name: innerObj.name,
|
||||
firstName: innerObj.firstName,
|
||||
vanity: innerObj.vanity,
|
||||
thumbSrc: innerObj.thumbSrc,
|
||||
profileUrl: innerObj.uri,
|
||||
gender: innerObj.gender,
|
||||
type: innerObj.type,
|
||||
isFriend: innerObj.is_friend,
|
||||
isBirthday: !!innerObj.is_birthday
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return retObj;
|
||||
}
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function getUserInfo(id, callback) {
|
||||
if (!callback) {
|
||||
throw { error: "getUserInfo: need callback" };
|
||||
}
|
||||
|
||||
if (utils.getType(id) !== "Array") {
|
||||
id = [id];
|
||||
}
|
||||
|
||||
var form = {};
|
||||
id.map(function(v, i) {
|
||||
form["ids[" + i + "]"] = v;
|
||||
});
|
||||
defaultFuncs
|
||||
.post("https://www.facebook.com/chat/user_info/", ctx.jar, form)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
return callback(null, formatData(resData.payload.profiles));
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("getUserInfo", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function handleMessageRequest(threadID, accept, callback) {
|
||||
if (utils.getType(accept) !== "Boolean") {
|
||||
throw {
|
||||
error: "Please pass a boolean as a second argument."
|
||||
};
|
||||
}
|
||||
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
var form = {
|
||||
client: "mercury"
|
||||
};
|
||||
|
||||
if (utils.getType(threadID) !== "Array") {
|
||||
threadID = [threadID];
|
||||
}
|
||||
|
||||
var messageBox = accept ? "inbox" : "other";
|
||||
|
||||
for (var i = 0; i < threadID.length; i++) {
|
||||
form[messageBox + "[" + i + "]"] = threadID[i];
|
||||
}
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/mercury/move_thread.php",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("handleMessageRequest", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+553
@@ -0,0 +1,553 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
var msgsRecv = 0;
|
||||
var identity = function() {};
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
var currentlyRunning = null;
|
||||
var globalCallback = identity;
|
||||
|
||||
var stopListening = function() {
|
||||
globalCallback = identity;
|
||||
if (currentlyRunning) {
|
||||
clearTimeout(currentlyRunning);
|
||||
currentlyRunning = null;
|
||||
}
|
||||
};
|
||||
|
||||
var prev = Date.now();
|
||||
var tmpPrev = Date.now();
|
||||
var lastSync = Date.now();
|
||||
|
||||
var form = {
|
||||
channel: "p_" + ctx.userID,
|
||||
seq: "0",
|
||||
partition: "-2",
|
||||
clientid: ctx.clientID,
|
||||
viewer_uid: ctx.userID,
|
||||
uid: ctx.userID,
|
||||
state: "active",
|
||||
idle: 0,
|
||||
cap: "8",
|
||||
msgs_recv: msgsRecv,
|
||||
qp: "y",
|
||||
pws: "fresh"
|
||||
};
|
||||
|
||||
if (ctx.globalOptions.pageID) {
|
||||
form.aiq = ctx.globalOptions.pageID + ",0";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an object maybe representing an event. Handles events it wants to handle
|
||||
* and returns true if it did handle an event (and called the globalCallback).
|
||||
* Returns false otherwise.
|
||||
*/
|
||||
function handleMessagingEvents(event) {
|
||||
switch (event.event) {
|
||||
// "read_receipt" event triggers when other people read the user's messages.
|
||||
case "read_receipt":
|
||||
var fmtMsg;
|
||||
try {
|
||||
fmtMsg = utils.formatReadReceipt(event);
|
||||
} catch (err) {
|
||||
globalCallback({
|
||||
error: "Problem parsing message object. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues.",
|
||||
detail: err,
|
||||
res: event,
|
||||
type: "parse_error"
|
||||
});
|
||||
return true;
|
||||
}
|
||||
globalCallback(null, fmtMsg);
|
||||
return true;
|
||||
// "read event" triggers when the user read other people's messages.
|
||||
case "read":
|
||||
var fmtMsg;
|
||||
try {
|
||||
fmtMsg = utils.formatRead(event);
|
||||
} catch (err) {
|
||||
globalCallback({
|
||||
error: "Problem parsing message object. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues.",
|
||||
detail: err,
|
||||
res: event,
|
||||
type: "parse_error"
|
||||
});
|
||||
return true;
|
||||
}
|
||||
globalCallback(null, fmtMsg);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var serverNumber = "0";
|
||||
|
||||
function listen(servern) {
|
||||
if (currentlyRunning == null || !ctx.loggedIn) {
|
||||
return;
|
||||
}
|
||||
|
||||
form.idle = ~~(Date.now() / 1000) - prev;
|
||||
prev = ~~(Date.now() / 1000);
|
||||
var presence = utils.generatePresence(ctx.userID);
|
||||
ctx.jar.setCookie(
|
||||
"presence=" + presence + "; path=/; domain=.facebook.com; secure",
|
||||
"https://www.facebook.com"
|
||||
);
|
||||
defaultFuncs
|
||||
.get(
|
||||
"https://" + serverNumber + "-edge-chat.facebook.com/pull",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
var now = Date.now();
|
||||
log.info("listen", "Got answer in " + (now - tmpPrev));
|
||||
tmpPrev = now;
|
||||
if (resData && resData.t === "lb") {
|
||||
form.sticky_token = resData.lb_info.sticky;
|
||||
form.sticky_pool = resData.lb_info.pool;
|
||||
}
|
||||
|
||||
if (resData && resData.t === "fullReload") {
|
||||
form.seq = resData.seq;
|
||||
delete form.sticky_pool;
|
||||
delete form.sticky_token;
|
||||
var form4 = {
|
||||
lastSync: ~~(lastSync / 1000)
|
||||
};
|
||||
defaultFuncs
|
||||
.get("https://www.facebook.com/notifications/sync/", ctx.jar, form4)
|
||||
.then(utils.saveCookies(ctx.jar))
|
||||
.then(function() {
|
||||
lastSync = Date.now();
|
||||
var form = {
|
||||
client: "mercury",
|
||||
"folders[0]": "inbox",
|
||||
last_action_timestamp: ~~(Date.now() - 60)
|
||||
};
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/mercury/thread_sync.php",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(function() {
|
||||
currentlyRunning = setTimeout(listen, 1000);
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (resData.ms) {
|
||||
msgsRecv += resData.ms.length;
|
||||
var atLeastOne = false;
|
||||
resData.ms
|
||||
.sort(function(a, b) {
|
||||
return a.timestamp - b.timestamp;
|
||||
})
|
||||
.forEach(function parsePackets(v) {
|
||||
switch (v.type) {
|
||||
// TODO: 'ttyp' was used before. It changed to 'typ'. We're keeping
|
||||
// both for now but we should remove 'ttyp' at some point.
|
||||
case "ttyp":
|
||||
case "typ":
|
||||
if (
|
||||
!ctx.globalOptions.listenEvents ||
|
||||
(!ctx.globalOptions.selfListen &&
|
||||
v.from.toString() === ctx.userID)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
var fmtMsg;
|
||||
try {
|
||||
fmtMsg = utils.formatTyp(v);
|
||||
} catch (err) {
|
||||
return globalCallback({
|
||||
error: "Problem parsing message object. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues.",
|
||||
detail: err,
|
||||
res: v,
|
||||
type: "parse_error"
|
||||
});
|
||||
}
|
||||
return globalCallback(null, fmtMsg);
|
||||
case "chatproxy-presence":
|
||||
// TODO: what happens when you're logged in as a page?
|
||||
if (!ctx.globalOptions.updatePresence) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.loggedIn) {
|
||||
for (var userID in v.buddyList) {
|
||||
var formattedPresence;
|
||||
try {
|
||||
formattedPresence = utils.formatProxyPresence(
|
||||
v.buddyList[userID],
|
||||
userID
|
||||
);
|
||||
} catch (err) {
|
||||
return globalCallback({
|
||||
error: "Problem parsing message object. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues.",
|
||||
detail: err,
|
||||
res: v.buddyList[userID],
|
||||
type: "parse_error"
|
||||
});
|
||||
}
|
||||
|
||||
if (formattedPresence != null) {
|
||||
globalCallback(null, formattedPresence);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
case "buddylist_overlay":
|
||||
// TODO: what happens when you're logged in as a page?
|
||||
if (!ctx.globalOptions.updatePresence) {
|
||||
return;
|
||||
}
|
||||
// There should be only one key inside overlay
|
||||
Object.keys(v.overlay).map(function(userID) {
|
||||
var formattedPresence;
|
||||
try {
|
||||
formattedPresence = utils.formatPresence(
|
||||
v.overlay[userID],
|
||||
userID
|
||||
);
|
||||
} catch (err) {
|
||||
return globalCallback({
|
||||
error: "Problem parsing message object. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues.",
|
||||
detail: err,
|
||||
res: v.overlay[userID],
|
||||
type: "parse_error"
|
||||
});
|
||||
}
|
||||
if (ctx.loggedIn) {
|
||||
return globalCallback(null, formattedPresence);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "delta":
|
||||
if (v.delta.class == "NewMessage") {
|
||||
if (ctx.globalOptions.pageID &&
|
||||
ctx.globalOptions.pageID != v.queue
|
||||
)
|
||||
return;
|
||||
|
||||
(function resolveAttachmentUrl(i) {
|
||||
if (i == v.delta.attachments.length) {
|
||||
var fmtMsg;
|
||||
try {
|
||||
fmtMsg = utils.formatDeltaMessage(v);
|
||||
} catch (err) {
|
||||
return globalCallback({
|
||||
error: "Problem parsing message object. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues.",
|
||||
detail: err,
|
||||
res: v,
|
||||
type: "parse_error"
|
||||
});
|
||||
}
|
||||
return !ctx.globalOptions.selfListen &&
|
||||
fmtMsg.senderID === ctx.userID ?
|
||||
undefined :
|
||||
globalCallback(null, fmtMsg);
|
||||
} else {
|
||||
if (
|
||||
v.delta.attachments[i].mercury.attach_type == "photo"
|
||||
) {
|
||||
api.resolvePhotoUrl(
|
||||
v.delta.attachments[i].fbid,
|
||||
(err, url) => {
|
||||
if (!err)
|
||||
v.delta.attachments[
|
||||
i
|
||||
].mercury.metadata.url = url;
|
||||
return resolveAttachmentUrl(i + 1);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return resolveAttachmentUrl(i + 1);
|
||||
}
|
||||
}
|
||||
})(0);
|
||||
break;
|
||||
}
|
||||
|
||||
if (v.delta.class == "ClientPayload") {
|
||||
var clientPayload = utils.decodeClientPayload(
|
||||
v.delta.payload
|
||||
);
|
||||
if (clientPayload && clientPayload.deltas) {
|
||||
for (var i in clientPayload.deltas) {
|
||||
var delta = clientPayload.deltas[i];
|
||||
if (delta.deltaMessageReaction && !!ctx.globalOptions.listenEvents) {
|
||||
globalCallback(null, {
|
||||
type: "message_reaction",
|
||||
threadID: delta.deltaMessageReaction.threadKey
|
||||
.threadFbId ?
|
||||
delta.deltaMessageReaction.threadKey.threadFbId : delta.deltaMessageReaction.threadKey
|
||||
.otherUserFbId,
|
||||
messageID: delta.deltaMessageReaction.messageId,
|
||||
reaction: delta.deltaMessageReaction.reaction,
|
||||
senderID: delta.deltaMessageReaction.senderId,
|
||||
userID: delta.deltaMessageReaction.userId,
|
||||
timestamp: v.ofd_ts
|
||||
});
|
||||
} else if (delta.deltaRecallMessageData && !!ctx.globalOptions.listenEvents) {
|
||||
globalCallback(null, {
|
||||
type: "message_unsend",
|
||||
threadID: delta.deltaRecallMessageData.threadKey.threadFbId ?
|
||||
delta.deltaRecallMessageData.threadKey.threadFbId : delta.deltaRecallMessageData.threadKey
|
||||
.otherUserFbId,
|
||||
messageID: delta.deltaRecallMessageData.messageID,
|
||||
senderID: delta.deltaRecallMessageData.senderID,
|
||||
deletionTimestamp: delta.deltaRecallMessageData.deletionTimestamp,
|
||||
timestamp: v.ofd_ts
|
||||
});
|
||||
} else if (delta.deltaMessageReply) {
|
||||
//Mention block - #1
|
||||
var mdata =
|
||||
delta.deltaMessageReply.message.data === undefined ? [] :
|
||||
delta.deltaMessageReply.message.data.prng === undefined ? [] :
|
||||
JSON.parse(delta.deltaMessageReply.message.data.prng);
|
||||
var m_id = mdata.map(u => u.i);
|
||||
var m_offset = mdata.map(u => u.o);
|
||||
var m_length = mdata.map(u => u.l);
|
||||
|
||||
var mentions = {};
|
||||
|
||||
for (var i = 0; i < m_id.length; i++) {
|
||||
mentions[m_id[i]] = delta.deltaMessageReply.message.body.substring(
|
||||
m_offset[i],
|
||||
m_offset[i] + m_length[i]
|
||||
);
|
||||
}
|
||||
//Mention block - 1#
|
||||
//Mention block - #2
|
||||
var mdata =
|
||||
delta.deltaMessageReply.repliedToMessage.data === undefined ? [] :
|
||||
delta.deltaMessageReply.repliedToMessage.data.prng === undefined ? [] :
|
||||
JSON.parse(delta.deltaMessageReply.repliedToMessage.data.prng);
|
||||
var m_id = mdata.map(u => u.i);
|
||||
var m_offset = mdata.map(u => u.o);
|
||||
var m_length = mdata.map(u => u.l);
|
||||
|
||||
var rmentions = {};
|
||||
|
||||
for (var i = 0; i < m_id.length; i++) {
|
||||
rmentions[m_id[i]] = delta.deltaMessageReply.repliedToMessage.body.substring(
|
||||
m_offset[i],
|
||||
m_offset[i] + m_length[i]
|
||||
);
|
||||
}
|
||||
//Mention block - 2#
|
||||
|
||||
globalCallback(null, {
|
||||
type: "message_reply",
|
||||
threadID: delta.deltaMessageReply.message.messageMetadata.threadKey.threadFbId ?
|
||||
delta.deltaMessageReply.message.messageMetadata.threadKey.threadFbId : delta.deltaMessageReply.message.messageMetadata.threadKey
|
||||
.otherUserFbId,
|
||||
messageID: delta.deltaMessageReply.message.messageMetadata.messageId,
|
||||
senderID: delta.deltaMessageReply.message.messageMetadata.actorFbId,
|
||||
attachments: delta.deltaMessageReply.message.attachments.map(function(att) {
|
||||
var mercury = JSON.parse(att.mercuryJSON);
|
||||
Object.assign(att, mercury);
|
||||
return att;
|
||||
}).map(att => utils._formatAttachment(att)),
|
||||
body: delta.deltaMessageReply.message.body || "",
|
||||
isGroup: !!delta.deltaMessageReply.message.messageMetadata.threadKey.threadFbId,
|
||||
mentions: mentions,
|
||||
timestamp: v.ofd_ts,
|
||||
messageReply: {
|
||||
threadID: delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey.threadFbId ?
|
||||
delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey.threadFbId : delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey
|
||||
.otherUserFbId,
|
||||
messageID: delta.deltaMessageReply.repliedToMessage.messageMetadata.messageId,
|
||||
senderID: delta.deltaMessageReply.repliedToMessage.messageMetadata.actorFbId,
|
||||
attachments: delta.deltaMessageReply.repliedToMessage.attachments.map(function(att) {
|
||||
var mercury = JSON.parse(att.mercuryJSON);
|
||||
Object.assign(att, mercury);
|
||||
return att;
|
||||
}).map(att => utils._formatAttachment(att)),
|
||||
body: delta.deltaMessageReply.repliedToMessage.body || "",
|
||||
isGroup: !!delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey.threadFbId,
|
||||
mentions: rmentions
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (v.delta.class !== "NewMessage" &&
|
||||
!ctx.globalOptions.listenEvents
|
||||
)
|
||||
return;
|
||||
switch (v.delta.class) {
|
||||
case "ReadReceipt":
|
||||
var fmtMsg;
|
||||
try {
|
||||
fmtMsg = utils.formatDeltaReadReceipt(v.delta);
|
||||
} catch (err) {
|
||||
return globalCallback({
|
||||
error: "Problem parsing message object. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues.",
|
||||
detail: err,
|
||||
res: v.delta,
|
||||
type: "parse_error"
|
||||
});
|
||||
}
|
||||
return globalCallback(null, fmtMsg);
|
||||
case "AdminTextMessage":
|
||||
switch (v.delta.type) {
|
||||
case "change_thread_theme":
|
||||
case "change_thread_nickname":
|
||||
case "change_thread_icon":
|
||||
break;
|
||||
case "group_poll":
|
||||
var fmtMsg;
|
||||
try {
|
||||
fmtMsg = utils.formatDeltaEvent(v.delta);
|
||||
} catch (err) {
|
||||
return globalCallback({
|
||||
error: "Problem parsing message object. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues.",
|
||||
detail: err,
|
||||
res: v.delta,
|
||||
type: "parse_error"
|
||||
});
|
||||
}
|
||||
return globalCallback(null, fmtMsg);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
case "ThreadName":
|
||||
case "ParticipantsAddedToGroupThread":
|
||||
case "ParticipantLeftGroupThread":
|
||||
var formattedEvent;
|
||||
try {
|
||||
formattedEvent = utils.formatDeltaEvent(v.delta);
|
||||
} catch (err) {
|
||||
return globalCallback({
|
||||
error: "Problem parsing message object. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues.",
|
||||
detail: err,
|
||||
res: v.delta,
|
||||
type: "parse_error"
|
||||
});
|
||||
}
|
||||
return (!ctx.globalOptions.selfListen &&
|
||||
formattedEvent.author.toString() === ctx.userID) ||
|
||||
!ctx.loggedIn ?
|
||||
undefined :
|
||||
globalCallback(null, formattedEvent);
|
||||
}
|
||||
|
||||
break;
|
||||
case "messaging":
|
||||
if (handleMessagingEvents(v)) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "pages_messaging":
|
||||
if (
|
||||
!ctx.globalOptions.pageID ||
|
||||
v.event !== "deliver" ||
|
||||
(!ctx.globalOptions.selfListen &&
|
||||
(v.message.sender_fbid.toString() === ctx.userID ||
|
||||
v.message.sender_fbid.toString() ===
|
||||
ctx.globalOptions.pageID)) ||
|
||||
v.realtime_viewer_fbid.toString() !==
|
||||
ctx.globalOptions.pageID
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
atLeastOne = true;
|
||||
if (ctx.loggedIn) {
|
||||
var fmtMsg;
|
||||
try {
|
||||
fmtMsg = utils.formatMessage(v);
|
||||
} catch (err) {
|
||||
return globalCallback({
|
||||
error: "Problem parsing message object. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues.",
|
||||
detail: err,
|
||||
res: v,
|
||||
type: "parse_error"
|
||||
});
|
||||
}
|
||||
return globalCallback(null, fmtMsg);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
if (atLeastOne) {
|
||||
// Send deliveryReceipt notification to the server
|
||||
var formDeliveryReceipt = {};
|
||||
|
||||
resData.ms
|
||||
.filter(function(v) {
|
||||
return (
|
||||
v.message &&
|
||||
v.message.mid &&
|
||||
v.message.sender_fbid.toString() !== ctx.userID
|
||||
);
|
||||
})
|
||||
.forEach(function(val, i) {
|
||||
formDeliveryReceipt["[" + i + "]"] = val.message.mid;
|
||||
});
|
||||
|
||||
// If there's at least one, we do the post request
|
||||
if (formDeliveryReceipt["[0]"]) {
|
||||
defaultFuncs.post(
|
||||
"https://www.facebook.com/ajax/mercury/delivery_receipts.php",
|
||||
ctx.jar,
|
||||
formDeliveryReceipt
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (resData.seq) {
|
||||
form.seq = resData.seq;
|
||||
}
|
||||
if (resData.tr) {
|
||||
form.traceid = resData.tr;
|
||||
}
|
||||
if (currentlyRunning) {
|
||||
currentlyRunning = setTimeout(listen, Math.random() * 200 + 50);
|
||||
}
|
||||
return;
|
||||
})
|
||||
.catch(function(err) {
|
||||
if (err.code === "ETIMEDOUT") {
|
||||
log.info("listen", "Suppressed timeout error.");
|
||||
} else if (err.code === "EAI_AGAIN") {
|
||||
serverNumber = (~~(Math.random() * 6)).toString();
|
||||
} else {
|
||||
log.error("listen", err);
|
||||
globalCallback(err);
|
||||
}
|
||||
if (currentlyRunning) {
|
||||
currentlyRunning = setTimeout(listen, Math.random() * 200 + 50);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return function(callback) {
|
||||
globalCallback = callback;
|
||||
|
||||
if (!currentlyRunning) {
|
||||
currentlyRunning = setTimeout(listen, Math.random() * 200 + 50, callback);
|
||||
}
|
||||
|
||||
return stopListening;
|
||||
};
|
||||
};
|
||||
+551
@@ -0,0 +1,551 @@
|
||||
/* eslint-disable no-redeclare */
|
||||
"use strict";
|
||||
var fbconnect = require("./mqtt/fbconnect");
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
var identity = function () {};
|
||||
var mqttClient = undefined;
|
||||
|
||||
var lastSeqId = 0;
|
||||
var syncToken;
|
||||
|
||||
//Don't really know what this does but I think it's for the active state
|
||||
var chatOn = true;
|
||||
var foreground = false;
|
||||
|
||||
var topics = [
|
||||
"/t_ms",
|
||||
"/thread_typing",
|
||||
"/orca_typing_notifications",
|
||||
"/orca_presence",
|
||||
"/legacy_web",
|
||||
"/br_sr",
|
||||
"/sr_res",
|
||||
"/webrtc",
|
||||
"/onevc",
|
||||
"/notify_disconnect",
|
||||
"/inbox",
|
||||
"/mercury",
|
||||
"/messaging_events",
|
||||
"/orca_message_notifications",
|
||||
"/pp",
|
||||
"/webrtc_response",
|
||||
]
|
||||
|
||||
function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
||||
var sessionID = Math.floor(Math.random() * 9007199254740991) + 1;
|
||||
var username = {
|
||||
u: ctx.userID,
|
||||
s: sessionID,
|
||||
chat_on: chatOn,
|
||||
fg: foreground,
|
||||
d: utils.getGUID(),
|
||||
ct: "websocket",
|
||||
//App id from facebook
|
||||
aid: "219994525426954",
|
||||
mqtt_sid: "",
|
||||
cp: 3,
|
||||
ecp: 10,
|
||||
st: topics,
|
||||
pm: [],
|
||||
dc: "",
|
||||
no_auto_fg: true,
|
||||
gas: null
|
||||
};
|
||||
var cookies = ctx.jar.getCookies("https://www.facebook.com").join("; ");
|
||||
|
||||
//Region could be changed for better ping. (Region atn: Southeast Asia, region ash: West US, prob) (Don't really know if we need it).
|
||||
//// var host = 'wss://edge-chat.facebook.com/chat?region=atn&sid=' + sessionID;
|
||||
var host = 'wss://edge-chat.facebook.com/chat?sid=' + sessionID;
|
||||
|
||||
var options = {
|
||||
clientId: "mqttwsclient",
|
||||
protocolId: 'MQIsdp',
|
||||
protocolVersion: 3,
|
||||
username: JSON.stringify(username),
|
||||
clean: true,
|
||||
wsOptions: {
|
||||
'headers': {
|
||||
'Cookie': cookies,
|
||||
'Origin': 'https://www.facebook.com',
|
||||
'User-Agent': ctx.globalOptions.userAgent,
|
||||
'Referer': 'https://www.facebook.com',
|
||||
'Host': 'edge-chat.facebook.com'
|
||||
},
|
||||
origin: 'https://www.facebook.com',
|
||||
protocolVersion: 13
|
||||
}
|
||||
};
|
||||
|
||||
mqttClient = fbconnect.connect(host, options);
|
||||
|
||||
mqttClient.on('error', function(err) {
|
||||
log.error(err);
|
||||
mqttClient.end();
|
||||
globalCallback("Connection refused: Server unavailable", null);
|
||||
});
|
||||
|
||||
mqttClient.on('connect', function() {
|
||||
var topic;
|
||||
var queue = {
|
||||
sync_api_version: 10,
|
||||
max_deltas_able_to_process: 1000,
|
||||
delta_batch_size: 500,
|
||||
encoding: "JSON",
|
||||
entity_fbid: ctx.userID,
|
||||
};
|
||||
|
||||
if(ctx.globalOptions.pageID) {
|
||||
queue.entity_fbid = ctx.globalOptions.pageID;
|
||||
}
|
||||
|
||||
if(syncToken) {
|
||||
topic = "/messenger_sync_get_diffs";
|
||||
queue.last_seq_id = lastSeqId;
|
||||
queue.sync_token = syncToken;
|
||||
} else {
|
||||
topic = "/messenger_sync_create_queue";
|
||||
queue.initial_titan_sequence_id = lastSeqId;
|
||||
queue.device_params = null;
|
||||
}
|
||||
|
||||
mqttClient.publish(topic, JSON.stringify(queue), {qos: 1, retain: false})
|
||||
});
|
||||
|
||||
mqttClient.on('message', function(topic, message, packet) {
|
||||
var jsonMessage = JSON.parse(message);
|
||||
if(topic === "/t_ms") {
|
||||
if(jsonMessage.firstDeltaSeqId && jsonMessage.syncToken) {
|
||||
lastSeqId = jsonMessage.firstDeltaSeqId;
|
||||
syncToken = jsonMessage.syncToken;
|
||||
}
|
||||
|
||||
if(jsonMessage.lastIssuedSeqId) {
|
||||
lastSeqId = parseInt(jsonMessage.lastIssuedSeqId);
|
||||
}
|
||||
|
||||
if(jsonMessage.queueEntityId && ctx.globalOptions.pageID &&
|
||||
ctx.globalOptions.pageID != jsonMessage.queueEntityId) {
|
||||
return;
|
||||
}
|
||||
|
||||
//If it contains more than 1 delta
|
||||
for (var i in jsonMessage.deltas) {
|
||||
var delta = jsonMessage.deltas[i];
|
||||
parseDelta(defaultFuncs, api, ctx, globalCallback, { "delta": delta });
|
||||
}
|
||||
} else if (topic === "/thread_typing" || topic === "/orca_typing_notifications") {
|
||||
var typ = {
|
||||
type: "typ",
|
||||
isTyping: !!jsonMessage.state,
|
||||
from: jsonMessage.sender_fbid.toString(),
|
||||
threadID: utils.formatID((jsonMessage.thread || jsonMessage.sender_fbid).toString())
|
||||
};
|
||||
(function () { globalCallback(null, typ); })();
|
||||
} else if (topic === "/orca_presence") {
|
||||
if (!ctx.globalOptions.updatePresence) {
|
||||
for (var i in jsonMessage.list) {
|
||||
var data = jsonMessage.list[i];
|
||||
var userID = data["u"];
|
||||
|
||||
var presence = {
|
||||
type: "presence",
|
||||
userID: userID.toString(),
|
||||
//Convert to ms
|
||||
timestamp: data["l"] * 1000,
|
||||
statuses: data["p"]
|
||||
};
|
||||
(function () { globalCallback(null, presence); })();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
mqttClient.on('close', function() {
|
||||
// client.end();
|
||||
});
|
||||
}
|
||||
|
||||
function parseDelta(defaultFuncs, api, ctx, globalCallback, v) {
|
||||
if(v.delta.class == "NewMessage") {
|
||||
(function resolveAttachmentUrl(i) {
|
||||
if (i == v.delta.attachments.length) {
|
||||
var fmtMsg;
|
||||
try {
|
||||
fmtMsg = utils.formatDeltaMessage(v);
|
||||
} catch (err) {
|
||||
return globalCallback({
|
||||
error: "Problem parsing message object. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues.",
|
||||
detail: err,
|
||||
res: v,
|
||||
type: "parse_error"
|
||||
});
|
||||
}
|
||||
if (fmtMsg) {
|
||||
if (ctx.globalOptions.autoMarkDelivery) {
|
||||
markDelivery(ctx, api, fmtMsg.threadID, fmtMsg.messageID);
|
||||
}
|
||||
}
|
||||
return !ctx.globalOptions.selfListen &&
|
||||
fmtMsg.senderID === ctx.userID ?
|
||||
undefined :
|
||||
(function () { globalCallback(null, fmtMsg); })();
|
||||
} else {
|
||||
if (
|
||||
v.delta.attachments[i].mercury.attach_type == "photo"
|
||||
) {
|
||||
api.resolvePhotoUrl(
|
||||
v.delta.attachments[i].fbid,
|
||||
(err, url) => {
|
||||
if (!err)
|
||||
v.delta.attachments[
|
||||
i
|
||||
].mercury.metadata.url = url;
|
||||
return resolveAttachmentUrl(i + 1);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return resolveAttachmentUrl(i + 1);
|
||||
}
|
||||
}
|
||||
})(0);
|
||||
}
|
||||
|
||||
if (v.delta.class == "ClientPayload") {
|
||||
var clientPayload = utils.decodeClientPayload(
|
||||
v.delta.payload
|
||||
);
|
||||
if (clientPayload && clientPayload.deltas) {
|
||||
for (var i in clientPayload.deltas) {
|
||||
var delta = clientPayload.deltas[i];
|
||||
if (delta.deltaMessageReaction && !!ctx.globalOptions.listenEvents) {
|
||||
(function () { globalCallback(null, {
|
||||
type: "message_reaction",
|
||||
threadID: (delta.deltaMessageReaction.threadKey
|
||||
.threadFbId ?
|
||||
delta.deltaMessageReaction.threadKey.threadFbId : delta.deltaMessageReaction.threadKey
|
||||
.otherUserFbId).toString(),
|
||||
messageID: delta.deltaMessageReaction.messageId,
|
||||
reaction: delta.deltaMessageReaction.reaction,
|
||||
senderID: delta.deltaMessageReaction.senderId.toString(),
|
||||
userID: delta.deltaMessageReaction.userId.toString()
|
||||
}); })();
|
||||
} else if (delta.deltaRecallMessageData && !!ctx.globalOptions.listenEvents) {
|
||||
(function () { globalCallback(null, {
|
||||
type: "message_unsend",
|
||||
threadID: (delta.deltaRecallMessageData.threadKey.threadFbId ?
|
||||
delta.deltaRecallMessageData.threadKey.threadFbId : delta.deltaRecallMessageData.threadKey
|
||||
.otherUserFbId).toString(),
|
||||
messageID: delta.deltaRecallMessageData.messageID,
|
||||
senderID: delta.deltaRecallMessageData.senderID.toString(),
|
||||
deletionTimestamp: delta.deltaRecallMessageData.deletionTimestamp,
|
||||
timestamp: delta.deltaRecallMessageData.timestamp
|
||||
}); })();
|
||||
} else if (delta.deltaMessageReply) {
|
||||
//Mention block - #1
|
||||
var mdata =
|
||||
delta.deltaMessageReply.message === undefined ? [] :
|
||||
delta.deltaMessageReply.message.data === undefined ? [] :
|
||||
delta.deltaMessageReply.message.data.prng === undefined ? [] :
|
||||
JSON.parse(delta.deltaMessageReply.message.data.prng);
|
||||
var m_id = mdata.map(u => u.i);
|
||||
var m_offset = mdata.map(u => u.o);
|
||||
var m_length = mdata.map(u => u.l);
|
||||
|
||||
var mentions = {};
|
||||
|
||||
for (var i = 0; i < m_id.length; i++) {
|
||||
mentions[m_id[i]] = (delta.deltaMessageReply.message.body || "").substring(
|
||||
m_offset[i],
|
||||
m_offset[i] + m_length[i]
|
||||
);
|
||||
}
|
||||
//Mention block - 1#
|
||||
var callbackToReturn = {
|
||||
type: "message_reply",
|
||||
threadID: (delta.deltaMessageReply.message.messageMetadata.threadKey.threadFbId ?
|
||||
delta.deltaMessageReply.message.messageMetadata.threadKey.threadFbId : delta.deltaMessageReply.message.messageMetadata.threadKey
|
||||
.otherUserFbId).toString(),
|
||||
messageID: delta.deltaMessageReply.message.messageMetadata.messageId,
|
||||
senderID: delta.deltaMessageReply.message.messageMetadata.actorFbId.toString(),
|
||||
attachments: delta.deltaMessageReply.message.attachments.map(function (att) {
|
||||
var mercury = JSON.parse(att.mercuryJSON);
|
||||
Object.assign(att, mercury);
|
||||
return att;
|
||||
}).map(att => {
|
||||
var x;
|
||||
try {
|
||||
x = utils._formatAttachment(att);
|
||||
} catch (ex) {
|
||||
x = att;
|
||||
x.error = ex;
|
||||
x.type = "unknown";
|
||||
}
|
||||
return x;
|
||||
}),
|
||||
body: delta.deltaMessageReply.message.body || "",
|
||||
isGroup: !!delta.deltaMessageReply.message.messageMetadata.threadKey.threadFbId,
|
||||
mentions: mentions,
|
||||
timestamp: delta.deltaMessageReply.message.messageMetadata.timestamp,
|
||||
};
|
||||
|
||||
if (delta.deltaMessageReply.repliedToMessage) {
|
||||
//Mention block - #2
|
||||
mdata =
|
||||
delta.deltaMessageReply.repliedToMessage === undefined ? [] :
|
||||
delta.deltaMessageReply.repliedToMessage.data === undefined ? [] :
|
||||
delta.deltaMessageReply.repliedToMessage.data.prng === undefined ? [] :
|
||||
JSON.parse(delta.deltaMessageReply.repliedToMessage.data.prng);
|
||||
m_id = mdata.map(u => u.i);
|
||||
m_offset = mdata.map(u => u.o);
|
||||
m_length = mdata.map(u => u.l);
|
||||
|
||||
var rmentions = {};
|
||||
|
||||
for (var i = 0; i < m_id.length; i++) {
|
||||
rmentions[m_id[i]] = (delta.deltaMessageReply.repliedToMessage.body || "").substring(
|
||||
m_offset[i],
|
||||
m_offset[i] + m_length[i]
|
||||
);
|
||||
}
|
||||
//Mention block - 2#
|
||||
callbackToReturn.messageReply = {
|
||||
threadID: (delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey.threadFbId ?
|
||||
delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey.threadFbId : delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey
|
||||
.otherUserFbId).toString(),
|
||||
messageID: delta.deltaMessageReply.repliedToMessage.messageMetadata.messageId,
|
||||
senderID: delta.deltaMessageReply.repliedToMessage.messageMetadata.actorFbId.toString(),
|
||||
attachments: delta.deltaMessageReply.repliedToMessage.attachments.map(function (att) {
|
||||
var mercury = JSON.parse(att.mercuryJSON);
|
||||
Object.assign(att, mercury);
|
||||
return att;
|
||||
}).map(att => {
|
||||
var x;
|
||||
try {
|
||||
x = utils._formatAttachment(att);
|
||||
} catch (ex) {
|
||||
x = att;
|
||||
x.error = ex;
|
||||
x.type = "unknown";
|
||||
}
|
||||
return x;
|
||||
}),
|
||||
body: delta.deltaMessageReply.repliedToMessage.body || "",
|
||||
isGroup: !!delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey.threadFbId,
|
||||
mentions: rmentions,
|
||||
timestamp: delta.deltaMessageReply.repliedToMessage.messageMetadata.timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
if (ctx.globalOptions.autoMarkDelivery) {
|
||||
markDelivery(ctx, api, callbackToReturn.threadID, callbackToReturn.messageID);
|
||||
}
|
||||
|
||||
return !ctx.globalOptions.selfListen &&
|
||||
callbackToReturn.senderID === ctx.userID ?
|
||||
undefined :
|
||||
(function () { globalCallback(null, callbackToReturn); })();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (v.delta.class !== "NewMessage" &&
|
||||
!ctx.globalOptions.listenEvents
|
||||
)
|
||||
return;
|
||||
|
||||
switch (v.delta.class) {
|
||||
case "ReadReceipt":
|
||||
var fmtMsg;
|
||||
try {
|
||||
fmtMsg = utils.formatDeltaReadReceipt(v.delta);
|
||||
} catch (err) {
|
||||
return globalCallback({
|
||||
error: "Problem parsing message object. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues.",
|
||||
detail: err,
|
||||
res: v.delta,
|
||||
type: "parse_error"
|
||||
});
|
||||
}
|
||||
return (function () { globalCallback(null, fmtMsg); })();
|
||||
case "AdminTextMessage":
|
||||
switch (v.delta.type) {
|
||||
case "change_thread_theme":
|
||||
case "change_thread_nickname":
|
||||
case "change_thread_icon":
|
||||
break;
|
||||
case "group_poll":
|
||||
var fmtMsg;
|
||||
try {
|
||||
fmtMsg = utils.formatDeltaEvent(v.delta);
|
||||
} catch (err) {
|
||||
return globalCallback({
|
||||
error: "Problem parsing message object. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues.",
|
||||
detail: err,
|
||||
res: v.delta,
|
||||
type: "parse_error"
|
||||
});
|
||||
}
|
||||
return (function () { globalCallback(null, fmtMsg); })();
|
||||
default:
|
||||
return;
|
||||
}
|
||||
break;
|
||||
//For group images
|
||||
case "ForcedFetch":
|
||||
if (!v.delta.threadKey) return;
|
||||
var mid = v.delta.messageId;
|
||||
var tid = v.delta.threadKey.threadFbId;
|
||||
if (mid && tid) {
|
||||
const form = {
|
||||
"av": ctx.globalOptions.pageID,
|
||||
"queries": JSON.stringify({
|
||||
"o0": {
|
||||
//This doc_id is valid as of ? (prob January 18, 2020)
|
||||
"doc_id": "1768656253222505",
|
||||
"query_params": {
|
||||
"thread_and_message_id": {
|
||||
"thread_id": tid.toString(),
|
||||
"message_id": mid.toString(),
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then((resData) => {
|
||||
if (resData[resData.length - 1].error_results > 0) {
|
||||
throw resData[0].o0.errors;
|
||||
}
|
||||
|
||||
if (resData[resData.length - 1].successful_results === 0) {
|
||||
throw { error: "forcedFetch: there was no successful_results", res: resData };
|
||||
}
|
||||
|
||||
var fetchData = resData[0].o0.data.message;
|
||||
|
||||
if (fetchData && fetchData.__typename === "ThreadImageMessage") {
|
||||
(!ctx.globalOptions.selfListen &&
|
||||
fetchData.message_sender.id.toString() === ctx.userID) ||
|
||||
!ctx.loggedIn ?
|
||||
undefined :
|
||||
(function () { globalCallback(null, {
|
||||
type: "change_thread_image",
|
||||
threadID: utils.formatID(tid.toString()),
|
||||
snippet: fetchData.snippet,
|
||||
timestamp: fetchData.timestamp_precise,
|
||||
author: fetchData.message_sender.id,
|
||||
image: {
|
||||
attachmentID: fetchData.image_with_metadata.legacy_attachment_id,
|
||||
width: fetchData.image_with_metadata.original_dimensions.x,
|
||||
height: fetchData.image_with_metadata.original_dimensions.y,
|
||||
url: fetchData.image_with_metadata.preview.uri
|
||||
}
|
||||
}); })();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error("forcedFetch", err);
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "ThreadName":
|
||||
case "ParticipantsAddedToGroupThread":
|
||||
case "ParticipantLeftGroupThread":
|
||||
var formattedEvent;
|
||||
try {
|
||||
formattedEvent = utils.formatDeltaEvent(v.delta);
|
||||
} catch (err) {
|
||||
return globalCallback({
|
||||
error: "Problem parsing message object. Please open an issue at https://github.com/Schmavery/facebook-chat-api/issues.",
|
||||
detail: err,
|
||||
res: v.delta,
|
||||
type: "parse_error"
|
||||
});
|
||||
}
|
||||
return (!ctx.globalOptions.selfListen &&
|
||||
formattedEvent.author.toString() === ctx.userID) ||
|
||||
!ctx.loggedIn ?
|
||||
undefined :
|
||||
(function () { globalCallback(null, formattedEvent); })();
|
||||
}
|
||||
}
|
||||
|
||||
function markDelivery(ctx, api, threadID, messageID) {
|
||||
if (threadID && messageID) {
|
||||
api.markAsDelivered(threadID, messageID, (err) => {
|
||||
if (err) {
|
||||
log.error(err);
|
||||
} else {
|
||||
if (ctx.globalOptions.autoMarkRead) {
|
||||
api.markAsRead(threadID, (err) => {
|
||||
if (err) {
|
||||
log.error(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function (defaultFuncs, api, ctx) {
|
||||
var globalCallback = identity;
|
||||
return function (callback) {
|
||||
globalCallback = callback;
|
||||
|
||||
//Same request as getThreadList
|
||||
const form = {
|
||||
"av": ctx.globalOptions.pageID,
|
||||
"queries": JSON.stringify({
|
||||
"o0": {
|
||||
"doc_id": "1349387578499440",
|
||||
"query_params": {
|
||||
"limit": 1,
|
||||
"before": null,
|
||||
"tags": ["INBOX"],
|
||||
"includeDeliveryReceipts": false,
|
||||
"includeSeqID": true
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then((resData) => {
|
||||
if (resData && resData.length > 0 && resData[resData.length - 1].error_results > 0) {
|
||||
throw resData[0].o0.errors;
|
||||
}
|
||||
|
||||
if (resData[resData.length - 1].successful_results === 0) {
|
||||
throw { error: "getSeqId: there was no successful_results", res: resData };
|
||||
}
|
||||
|
||||
if (resData[0].o0.data.viewer.message_threads.sync_sequence_id) {
|
||||
lastSeqId = resData[0].o0.data.viewer.message_threads.sync_sequence_id;
|
||||
listenMqtt(defaultFuncs, api, ctx, globalCallback);
|
||||
}
|
||||
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error("getSeqId", err);
|
||||
return callback(err);
|
||||
});
|
||||
|
||||
var stopListening = function () {
|
||||
globalCallback = identity;
|
||||
mqttClient.end();
|
||||
};
|
||||
|
||||
return stopListening;
|
||||
};
|
||||
};
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function logout(callback) {
|
||||
callback = callback || function() {};
|
||||
|
||||
var form = {
|
||||
pmid: "0"
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/bluebar/modern_settings_menu/?help_type=364455653583099&show_contextual_help=1",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
var elem = resData.jsmods.instances[0][2][0].filter(function(v) {
|
||||
return v.value === "logout";
|
||||
})[0];
|
||||
|
||||
var html = resData.jsmods.markup.filter(function(v) {
|
||||
return v[0] === elem.markup.__m;
|
||||
})[0][1].__html;
|
||||
|
||||
var form = {
|
||||
fb_dtsg: utils.getFrom(html, '"fb_dtsg" value="', '"'),
|
||||
ref: utils.getFrom(html, '"ref" value="', '"'),
|
||||
h: utils.getFrom(html, '"h" value="', '"')
|
||||
};
|
||||
|
||||
return defaultFuncs
|
||||
.post("https://www.facebook.com/logout.php", ctx.jar, form)
|
||||
.then(utils.saveCookies(ctx.jar));
|
||||
})
|
||||
.then(function(res) {
|
||||
if (!res.headers) {
|
||||
throw { error: "An error occurred when logging out." };
|
||||
}
|
||||
|
||||
return defaultFuncs
|
||||
.get(res.headers.location, ctx.jar)
|
||||
.then(utils.saveCookies(ctx.jar));
|
||||
})
|
||||
.then(function() {
|
||||
ctx.loggedIn = false;
|
||||
log.info("logout", "Logged out successfully.");
|
||||
callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("logout", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function (defaultFuncs, api, ctx) {
|
||||
return function markAsDelivered(threadID, messageID, callback) {
|
||||
if (!callback) {
|
||||
callback = function () { };
|
||||
}
|
||||
|
||||
if (!threadID || !messageID) {
|
||||
return callback("Error: messageID or threadID is not defined");
|
||||
}
|
||||
|
||||
var form = {};
|
||||
|
||||
form["message_ids[0]"] = messageID;
|
||||
form["thread_ids[" + threadID + "][0]"] = messageID;
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/mercury/delivery_receipts.php",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.saveCookies(ctx.jar))
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function (resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function (err) {
|
||||
log.error("markAsDelivered", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function markAsRead(threadID, read, callback) {
|
||||
if (utils.getType(read) === 'Function' || utils.getType(read) === 'AsyncFunction') {
|
||||
callback = read;
|
||||
read = true;
|
||||
}
|
||||
if (read == undefined) {
|
||||
read = true;
|
||||
}
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
var form = {};
|
||||
|
||||
if (typeof ctx.globalOptions.pageID !== 'undefined') {
|
||||
form["source"] = "PagesManagerMessagesInterface";
|
||||
form["request_user_id"] = ctx.globalOptions.pageID;
|
||||
}
|
||||
|
||||
form["ids[" + threadID + "]"] = read;
|
||||
form["watermarkTimestamp"] = new Date().getTime();
|
||||
form["shouldSendReadReceipt"] = true;
|
||||
form["commerce_last_message_type"] = "non_ad";
|
||||
form["titanOriginatedThreadId"] = utils.generateThreadingID(ctx.clientID);
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/mercury/change_read_status.php",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.saveCookies(ctx.jar))
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("markAsRead", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function markAsReadAll(callback) {
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
var form = {
|
||||
folder: 'inbox'
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/mercury/mark_folder_as_read.php",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.saveCookies(ctx.jar))
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("markAsReadAll", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
//From MQTT.js (https://github.com/mqttjs/MQTT.js)
|
||||
'use strict';
|
||||
|
||||
var mqtt = require('mqtt');
|
||||
var url = require('url');
|
||||
var xtend = require('xtend');
|
||||
var protocols = {};
|
||||
protocols.ws = require('./fbws');
|
||||
protocols.wss = require('./fbws');
|
||||
|
||||
/**
|
||||
* Parse the auth attribute and merge username and password in the options object.
|
||||
*
|
||||
* @param {Object} [opts] option object
|
||||
*/
|
||||
function parseAuthOptions(opts) {
|
||||
var matches;
|
||||
if(opts.auth) {
|
||||
matches = opts.auth.match(/^(.+):(.+)$/);
|
||||
if(matches) {
|
||||
opts.username = matches[1];
|
||||
opts.password = matches[2];
|
||||
} else {
|
||||
opts.username = opts.auth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* connect - connect to an MQTT broker.
|
||||
*
|
||||
* @param {String} [brokerUrl] - url of the broker, optional
|
||||
* @param {Object} opts - see MqttClient#constructor
|
||||
*/
|
||||
function connect(brokerUrl, opts) {
|
||||
if((typeof brokerUrl === 'object') && !opts) {
|
||||
opts = brokerUrl;
|
||||
brokerUrl = null;
|
||||
}
|
||||
|
||||
opts = opts || {};
|
||||
|
||||
if(brokerUrl) {
|
||||
var parsed = url.parse(brokerUrl, true);
|
||||
if(parsed.port != null) {
|
||||
parsed.port = Number(parsed.port);
|
||||
}
|
||||
|
||||
opts = xtend(parsed, opts);
|
||||
|
||||
if(opts.protocol === null) {
|
||||
throw new Error('Missing protocol');
|
||||
}
|
||||
opts.protocol = opts.protocol.replace(/:$/, '');
|
||||
}
|
||||
|
||||
// merge in the auth options if supplied
|
||||
parseAuthOptions(opts);
|
||||
|
||||
// support clientId passed in the query string of the url
|
||||
if(opts.query && typeof opts.query.clientId === 'string') {
|
||||
opts.clientId = opts.query.clientId;
|
||||
}
|
||||
|
||||
if(opts.cert && opts.key) {
|
||||
if(opts.protocol) {
|
||||
if(['mqtts', 'wss'].indexOf(opts.protocol) === -1) {
|
||||
switch(opts.protocol) {
|
||||
case 'mqtt':
|
||||
opts.protocol = 'mqtts';
|
||||
break;
|
||||
case 'ws':
|
||||
opts.protocol = 'wss';
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unknown protocol for secure connection: "' + opts.protocol + '"!');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// don't know what protocol he want to use, mqtts or wss
|
||||
throw new Error('Missing secure protocol key');
|
||||
}
|
||||
}
|
||||
|
||||
if(!protocols[opts.protocol]) {
|
||||
var isSecure = ['mqtts', 'wss'].indexOf(opts.protocol) !== -1;
|
||||
opts.protocol = [
|
||||
'mqtt',
|
||||
'mqtts',
|
||||
'ws',
|
||||
'wss'
|
||||
].filter(function(key, index) {
|
||||
if(isSecure && index % 2 === 0) {
|
||||
// Skip insecure protocols when requesting a secure one.
|
||||
return false;
|
||||
}
|
||||
return (typeof protocols[key] === 'function');
|
||||
})[0];
|
||||
}
|
||||
|
||||
if(opts.clean === false && !opts.clientId) {
|
||||
throw new Error('Missing clientId for unclean clients');
|
||||
}
|
||||
|
||||
if(opts.protocol) {
|
||||
opts.defaultProtocol = opts.protocol;
|
||||
}
|
||||
|
||||
function wrapper(client) {
|
||||
if(opts.servers) {
|
||||
if(!client._reconnectCount || client._reconnectCount === opts.servers.length) {
|
||||
client._reconnectCount = 0;
|
||||
}
|
||||
|
||||
opts.host = opts.servers[client._reconnectCount].host;
|
||||
opts.port = opts.servers[client._reconnectCount].port;
|
||||
opts.protocol = (!opts.servers[client._reconnectCount].protocol ? opts.defaultProtocol : opts.servers[client._reconnectCount].protocol);
|
||||
opts.hostname = opts.host;
|
||||
|
||||
client._reconnectCount++;
|
||||
}
|
||||
|
||||
return protocols[opts.protocol](client, opts);
|
||||
}
|
||||
|
||||
return new mqtt.Client(wrapper, opts);
|
||||
}
|
||||
|
||||
module.exports = connect;
|
||||
module.exports.connect = connect;
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
//From MQTT.js (https://github.com/mqttjs/MQTT.js)
|
||||
'use strict';
|
||||
|
||||
var websocket = require('websocket-stream');
|
||||
var urlModule = require('url');
|
||||
var WSS_OPTIONS = [
|
||||
'rejectUnauthorized',
|
||||
'ca',
|
||||
'cert',
|
||||
'key',
|
||||
'pfx',
|
||||
'passphrase'
|
||||
];
|
||||
function buildUrl(opts, client) {
|
||||
var url = opts.protocol + '://' + opts.hostname + ':' + opts.port + opts.path;
|
||||
if (typeof (opts.transformWsUrl) === 'function') {
|
||||
url = opts.transformWsUrl(url, opts, client);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function setDefaultOpts(opts) {
|
||||
if (!opts.hostname) {
|
||||
opts.hostname = 'localhost';
|
||||
}
|
||||
if (!opts.port) {
|
||||
if (opts.protocol === 'wss') {
|
||||
opts.port = 443;
|
||||
} else {
|
||||
opts.port = 80;
|
||||
}
|
||||
}
|
||||
if (!opts.path) {
|
||||
opts.path = '/';
|
||||
}
|
||||
|
||||
if (!opts.wsOptions) {
|
||||
opts.wsOptions = {};
|
||||
}
|
||||
if (opts.protocol === 'wss') {
|
||||
// Add cert/key/ca etc options
|
||||
WSS_OPTIONS.forEach(function (prop) {
|
||||
if (opts.hasOwnProperty(prop) && !opts.wsOptions.hasOwnProperty(prop)) {
|
||||
opts.wsOptions[prop] = opts[prop];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createWebSocket(client, opts) {
|
||||
var websocketSubProtocol =
|
||||
(opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3)
|
||||
? 'mqttv3.1'
|
||||
: 'mqtt';
|
||||
|
||||
setDefaultOpts(opts);
|
||||
var url = buildUrl(opts, client);
|
||||
return websocket(url, undefined, opts.wsOptions);
|
||||
}
|
||||
|
||||
function buildBuilder(client, opts) {
|
||||
return createWebSocket(client, opts);
|
||||
}
|
||||
|
||||
module.exports = buildBuilder;
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
// muteSecond: -1=permanent mute, 0=unmute, 60=one minute, 3600=one hour, etc.
|
||||
return function muteThread(threadID, muteSeconds, callback) {
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
var form = {
|
||||
thread_fbid: threadID,
|
||||
mute_settings: muteSeconds
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/mercury/change_mute_thread.php",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.saveCookies(ctx.jar))
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("muteThread", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function removeUserFromGroup(userID, threadID, callback) {
|
||||
if (
|
||||
!callback &&
|
||||
(utils.getType(threadID) === "Function" ||
|
||||
utils.getType(threadID) === "AsyncFunction")
|
||||
) {
|
||||
throw { error: "please pass a threadID as a second argument." };
|
||||
}
|
||||
if (
|
||||
utils.getType(threadID) !== "Number" &&
|
||||
utils.getType(threadID) !== "String"
|
||||
) {
|
||||
throw {
|
||||
error:
|
||||
"threadID should be of type Number or String and not " +
|
||||
utils.getType(threadID) +
|
||||
"."
|
||||
};
|
||||
}
|
||||
if (
|
||||
utils.getType(userID) !== "Number" &&
|
||||
utils.getType(userID) !== "String"
|
||||
) {
|
||||
throw {
|
||||
error:
|
||||
"userID should be of type Number or String and not " +
|
||||
utils.getType(userID) +
|
||||
"."
|
||||
};
|
||||
}
|
||||
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
var form = {
|
||||
uid: userID,
|
||||
tid: threadID
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post("https://www.facebook.com/chat/remove_participants", ctx.jar, form)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (!resData) {
|
||||
throw { error: "Remove from group failed." };
|
||||
}
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("removeUserFromGroup", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function resolvePhotoUrl(photoID, callback) {
|
||||
if (!callback) {
|
||||
throw { error: "resolvePhotoUrl: need callback" };
|
||||
}
|
||||
|
||||
defaultFuncs
|
||||
.get("https://www.facebook.com/mercury/attachments/photo", ctx.jar, {
|
||||
photo_id: photoID
|
||||
})
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(resData => {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
var photoUrl = resData.jsmods.require[0][3][0];
|
||||
|
||||
return callback(null, photoUrl);
|
||||
})
|
||||
.catch(err => {
|
||||
log.error("resolvePhotoUrl", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function searchForThread(name, callback) {
|
||||
if (!callback) {
|
||||
throw { error: "searchForThread: need callback" };
|
||||
}
|
||||
|
||||
var tmpForm = {
|
||||
client: "web_messenger",
|
||||
query: name,
|
||||
offset: 0,
|
||||
limit: 21,
|
||||
index: "fbid"
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/ajax/mercury/search_threads.php",
|
||||
ctx.jar,
|
||||
tmpForm
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
if (!resData.payload.mercury_payload.threads) {
|
||||
return callback({ error: "Could not find thread `" + name + "`." });
|
||||
}
|
||||
return callback(
|
||||
null,
|
||||
resData.payload.mercury_payload.threads.map(utils.formatThread)
|
||||
);
|
||||
});
|
||||
};
|
||||
};
|
||||
+420
@@ -0,0 +1,420 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
var bluebird = require("bluebird");
|
||||
|
||||
var allowedProperties = {
|
||||
attachment: true,
|
||||
url: true,
|
||||
sticker: true,
|
||||
emoji: true,
|
||||
emojiSize: true,
|
||||
body: true,
|
||||
mentions: true
|
||||
};
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
function uploadAttachment(attachments, callback) {
|
||||
var uploads = [];
|
||||
|
||||
// create an array of promises
|
||||
for (var i = 0; i < attachments.length; i++) {
|
||||
if (!utils.isReadableStream(attachments[i])) {
|
||||
throw {
|
||||
error:
|
||||
"Attachment should be a readable stream and not " +
|
||||
utils.getType(attachments[i]) +
|
||||
"."
|
||||
};
|
||||
}
|
||||
|
||||
var form = {
|
||||
upload_1024: attachments[i],
|
||||
voice_clip: "true"
|
||||
};
|
||||
|
||||
uploads.push(
|
||||
defaultFuncs
|
||||
.postFormData(
|
||||
"https://upload.facebook.com/ajax/mercury/upload.php",
|
||||
ctx.jar,
|
||||
form,
|
||||
{}
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
// We have to return the data unformatted unless we want to change it
|
||||
// back in sendMessage.
|
||||
return resData.payload.metadata[0];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// resolve all promises
|
||||
bluebird
|
||||
.all(uploads)
|
||||
.then(function(resData) {
|
||||
callback(null, resData);
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("uploadAttachment", err);
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
function getUrl(url, callback) {
|
||||
var form = {
|
||||
image_height: 960,
|
||||
image_width: 960,
|
||||
uri: url
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/message_share_attachment/fromURI/",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
return callback(resData);
|
||||
}
|
||||
|
||||
if (!resData.payload) {
|
||||
return callback({ error: "Invalid url" });
|
||||
}
|
||||
|
||||
callback(null, resData.payload.share_data.share_params);
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("getUrl", err);
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
function sendContent(form, threadID, isSingleUser, messageAndOTID, callback) {
|
||||
// There are three cases here:
|
||||
// 1. threadID is of type array, where we're starting a new group chat with users
|
||||
// specified in the array.
|
||||
// 2. User is sending a message to a specific user.
|
||||
// 3. No additional form params and the message goes to an existing group chat.
|
||||
if (utils.getType(threadID) === "Array") {
|
||||
for (var i = 0; i < threadID.length; i++) {
|
||||
form["specific_to_list[" + i + "]"] = "fbid:" + threadID[i];
|
||||
}
|
||||
form["specific_to_list[" + threadID.length + "]"] = "fbid:" + ctx.userID;
|
||||
form["client_thread_id"] = "root:" + messageAndOTID;
|
||||
log.info("sendMessage", "Sending message to multiple users: " + threadID);
|
||||
} else {
|
||||
// This means that threadID is the id of a user, and the chat
|
||||
// is a single person chat
|
||||
if (isSingleUser) {
|
||||
form["specific_to_list[0]"] = "fbid:" + threadID;
|
||||
form["specific_to_list[1]"] = "fbid:" + ctx.userID;
|
||||
form["other_user_fbid"] = threadID;
|
||||
} else {
|
||||
form["thread_fbid"] = threadID;
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.globalOptions.pageID) {
|
||||
form["author"] = "fbid:" + ctx.globalOptions.pageID;
|
||||
form["specific_to_list[1]"] = "fbid:" + ctx.globalOptions.pageID;
|
||||
form["creator_info[creatorID]"] = ctx.userID;
|
||||
form["creator_info[creatorType]"] = "direct_admin";
|
||||
form["creator_info[labelType]"] = "sent_message";
|
||||
form["creator_info[pageID]"] = ctx.globalOptions.pageID;
|
||||
form["request_user_id"] = ctx.globalOptions.pageID;
|
||||
form["creator_info[profileURI]"] =
|
||||
"https://www.facebook.com/profile.php?id=" + ctx.userID;
|
||||
}
|
||||
|
||||
defaultFuncs
|
||||
.post("https://www.facebook.com/messaging/send/", ctx.jar, form)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (!resData) {
|
||||
return callback({ error: "Send message failed." });
|
||||
}
|
||||
|
||||
if (resData.error) {
|
||||
if (resData.error === 1545012) {
|
||||
log.warn(
|
||||
"sendMessage",
|
||||
"Got error 1545012. This might mean that you're not part of the conversation " +
|
||||
threadID
|
||||
);
|
||||
}
|
||||
return callback(resData);
|
||||
}
|
||||
|
||||
var messageInfo = resData.payload.actions.reduce(function(p, v) {
|
||||
return (
|
||||
{
|
||||
threadID: v.thread_fbid,
|
||||
messageID: v.message_id,
|
||||
timestamp: v.timestamp
|
||||
} || p
|
||||
);
|
||||
}, null);
|
||||
|
||||
return callback(null, messageInfo);
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("sendMessage", err);
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
function send(form, threadID, messageAndOTID, callback) {
|
||||
// We're doing a query to this to check if the given id is the id of
|
||||
// a user or of a group chat. The form will be different depending
|
||||
// on that.
|
||||
if (utils.getType(threadID) === "Array") {
|
||||
sendContent(form, threadID, false, messageAndOTID, callback);
|
||||
} else {
|
||||
api.getUserInfo(threadID, function(err, res) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
sendContent(
|
||||
form,
|
||||
threadID,
|
||||
Object.keys(res).length > 0,
|
||||
messageAndOTID,
|
||||
callback
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleUrl(msg, form, callback, cb) {
|
||||
if (msg.url) {
|
||||
form["shareable_attachment[share_type]"] = "100";
|
||||
getUrl(msg.url, function(err, params) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
form["shareable_attachment[share_params]"] = params;
|
||||
cb();
|
||||
});
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
|
||||
function handleSticker(msg, form, callback, cb) {
|
||||
if (msg.sticker) {
|
||||
form["sticker_id"] = msg.sticker;
|
||||
}
|
||||
cb();
|
||||
}
|
||||
|
||||
function handleEmoji(msg, form, callback, cb) {
|
||||
if (msg.emojiSize != null && msg.emoji == null) {
|
||||
return callback({ error: "emoji property is empty" });
|
||||
}
|
||||
if (msg.emoji) {
|
||||
if (msg.emojiSize == null) {
|
||||
msg.emojiSize = "medium";
|
||||
}
|
||||
if (
|
||||
msg.emojiSize != "small" &&
|
||||
msg.emojiSize != "medium" &&
|
||||
msg.emojiSize != "large"
|
||||
) {
|
||||
return callback({ error: "emojiSize property is invalid" });
|
||||
}
|
||||
if (form["body"] != null && form["body"] != "") {
|
||||
return callback({ error: "body is not empty" });
|
||||
}
|
||||
form["body"] = msg.emoji;
|
||||
form["tags[0]"] = "hot_emoji_size:" + msg.emojiSize;
|
||||
}
|
||||
cb();
|
||||
}
|
||||
|
||||
function handleAttachment(msg, form, callback, cb) {
|
||||
if (msg.attachment) {
|
||||
form["image_ids"] = [];
|
||||
form["gif_ids"] = [];
|
||||
form["file_ids"] = [];
|
||||
form["video_ids"] = [];
|
||||
form["audio_ids"] = [];
|
||||
|
||||
if (utils.getType(msg.attachment) !== "Array") {
|
||||
msg.attachment = [msg.attachment];
|
||||
}
|
||||
|
||||
uploadAttachment(msg.attachment, function(err, files) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
files.forEach(function(file) {
|
||||
var key = Object.keys(file);
|
||||
var type = key[0]; // image_id, file_id, etc
|
||||
form["" + type + "s"].push(file[type]); // push the id
|
||||
});
|
||||
cb();
|
||||
});
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
|
||||
function handleMention(msg, form, callback, cb) {
|
||||
if (msg.mentions) {
|
||||
for (let i = 0; i < msg.mentions.length; i++) {
|
||||
const mention = msg.mentions[i];
|
||||
|
||||
const tag = mention.tag;
|
||||
if (typeof tag !== "string") {
|
||||
return callback({ error: "Mention tags must be strings." });
|
||||
}
|
||||
|
||||
const offset = msg.body.indexOf(tag, mention.fromIndex || 0);
|
||||
|
||||
if (offset < 0) {
|
||||
log.warn(
|
||||
"handleMention",
|
||||
'Mention for "' + tag + '" not found in message string.'
|
||||
);
|
||||
}
|
||||
|
||||
if (mention.id == null) {
|
||||
log.warn("handleMention", "Mention id should be non-null.");
|
||||
}
|
||||
|
||||
const id = mention.id || 0;
|
||||
form["profile_xmd[" + i + "][offset]"] = offset;
|
||||
form["profile_xmd[" + i + "][length]"] = tag.length;
|
||||
form["profile_xmd[" + i + "][id]"] = id;
|
||||
form["profile_xmd[" + i + "][type]"] = "p";
|
||||
}
|
||||
}
|
||||
cb();
|
||||
}
|
||||
|
||||
return function sendMessage(msg, threadID, callback, replyToMessage) {
|
||||
if (
|
||||
!callback &&
|
||||
(utils.getType(threadID) === "Function" ||
|
||||
utils.getType(threadID) === "AsyncFunction")
|
||||
) {
|
||||
return callback({ error: "Pass a threadID as a second argument." });
|
||||
}
|
||||
if (
|
||||
!replyToMessage &&
|
||||
utils.getType(callback) === "String"
|
||||
) {
|
||||
replyToMessage = callback;
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
var msgType = utils.getType(msg);
|
||||
var threadIDType = utils.getType(threadID);
|
||||
var messageIDType = utils.getType(replyToMessage);
|
||||
|
||||
if (msgType !== "String" && msgType !== "Object") {
|
||||
return callback({
|
||||
error:
|
||||
"Message should be of type string or object and not " + msgType + "."
|
||||
});
|
||||
}
|
||||
|
||||
// Changing this to accomodate an array of users
|
||||
if (
|
||||
threadIDType !== "Array" &&
|
||||
threadIDType !== "Number" &&
|
||||
threadIDType !== "String"
|
||||
) {
|
||||
return callback({
|
||||
error:
|
||||
"ThreadID should be of type number, string, or array and not " +
|
||||
threadIDType +
|
||||
"."
|
||||
});
|
||||
}
|
||||
|
||||
if (replyToMessage && messageIDType !== 'String') {
|
||||
return callback({
|
||||
error:
|
||||
"MessageID should be of type string and not " +
|
||||
threadIDType +
|
||||
"."
|
||||
});
|
||||
}
|
||||
|
||||
if (msgType === "String") {
|
||||
msg = { body: msg };
|
||||
}
|
||||
|
||||
var disallowedProperties = Object.keys(msg).filter(
|
||||
prop => !allowedProperties[prop]
|
||||
);
|
||||
if (disallowedProperties.length > 0) {
|
||||
return callback({
|
||||
error: "Dissallowed props: `" + disallowedProperties.join(", ") + "`"
|
||||
});
|
||||
}
|
||||
|
||||
var messageAndOTID = utils.generateOfflineThreadingID();
|
||||
|
||||
var form = {
|
||||
client: "mercury",
|
||||
action_type: "ma-type:user-generated-message",
|
||||
author: "fbid:" + ctx.userID,
|
||||
timestamp: Date.now(),
|
||||
timestamp_absolute: "Today",
|
||||
timestamp_relative: utils.generateTimestampRelative(),
|
||||
timestamp_time_passed: "0",
|
||||
is_unread: false,
|
||||
is_cleared: false,
|
||||
is_forward: false,
|
||||
is_filtered_content: false,
|
||||
is_filtered_content_bh: false,
|
||||
is_filtered_content_account: false,
|
||||
is_filtered_content_quasar: false,
|
||||
is_filtered_content_invalid_app: false,
|
||||
is_spoof_warning: false,
|
||||
source: "source:chat:web",
|
||||
"source_tags[0]": "source:chat",
|
||||
body: msg.body ? msg.body.toString() : "",
|
||||
html_body: false,
|
||||
ui_push_phase: "V3",
|
||||
status: "0",
|
||||
offline_threading_id: messageAndOTID,
|
||||
message_id: messageAndOTID,
|
||||
threading_id: utils.generateThreadingID(ctx.clientID),
|
||||
"ephemeral_ttl_mode:": "0",
|
||||
manual_retry_cnt: "0",
|
||||
has_attachment: !!(msg.attachment || msg.url || msg.sticker),
|
||||
signatureID: utils.getSignatureID(),
|
||||
replied_to_message_id: replyToMessage
|
||||
};
|
||||
|
||||
handleSticker(msg, form, callback, () =>
|
||||
handleAttachment(msg, form, callback, () =>
|
||||
handleUrl(msg, form, callback, () =>
|
||||
handleEmoji(msg, form, callback, () =>
|
||||
handleMention(msg, form, callback, () =>
|
||||
send(form, threadID, messageAndOTID, callback)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
};
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
function makeTypingIndicator(typ, threadID, callback) {
|
||||
var form = {
|
||||
typ: +typ,
|
||||
to: "",
|
||||
source: "mercury-chat",
|
||||
thread: threadID
|
||||
};
|
||||
|
||||
// Check if thread is a single person chat or a group chat
|
||||
// More info on this is in api.sendMessage
|
||||
api.getUserInfo(threadID, function(err, res) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// If id is single person chat
|
||||
if (Object.keys(res).length > 0) {
|
||||
form.to = threadID;
|
||||
}
|
||||
|
||||
defaultFuncs
|
||||
.post("https://www.facebook.com/ajax/messaging/typ.php", ctx.jar, form)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("sendTypingIndicator", err);
|
||||
return callback(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return function sendTypingIndicator(threadID, callback) {
|
||||
if (
|
||||
utils.getType(callback) !== "Function" &&
|
||||
utils.getType(callback) !== "AsyncFunction"
|
||||
) {
|
||||
if (callback) {
|
||||
log.warn(
|
||||
"sendTypingIndicator",
|
||||
"callback is not a function - ignoring."
|
||||
);
|
||||
}
|
||||
callback = () => {};
|
||||
}
|
||||
|
||||
makeTypingIndicator(true, threadID, callback);
|
||||
|
||||
return function end(cb) {
|
||||
if (
|
||||
utils.getType(cb) !== "Function" &&
|
||||
utils.getType(cb) !== "AsyncFunction"
|
||||
) {
|
||||
if (cb) {
|
||||
log.warn(
|
||||
"sendTypingIndicator",
|
||||
"callback is not a function - ignoring."
|
||||
);
|
||||
}
|
||||
cb = () => {};
|
||||
}
|
||||
|
||||
makeTypingIndicator(false, threadID, cb);
|
||||
};
|
||||
};
|
||||
};
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function setMessageReaction(reaction, messageID, callback) {
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
switch (reaction) {
|
||||
case "\uD83D\uDE0D": //:heart_eyes:
|
||||
case "\uD83D\uDE06": //:laughing:
|
||||
case "\uD83D\uDE2E": //:open_mouth:
|
||||
case "\uD83D\uDE22": //:cry:
|
||||
case "\uD83D\uDE20": //:angry:
|
||||
case "\uD83D\uDC4D": //:thumbsup:
|
||||
case "\uD83D\uDC4E": //:thumbsdown:
|
||||
case "":
|
||||
//valid
|
||||
break;
|
||||
case ":heart_eyes:":
|
||||
case ":love:":
|
||||
reaction = "\uD83D\uDE0D";
|
||||
break;
|
||||
case ":laughing:":
|
||||
case ":haha:":
|
||||
reaction = "\uD83D\uDE06";
|
||||
break;
|
||||
case ":open_mouth:":
|
||||
case ":wow:":
|
||||
reaction = "\uD83D\uDE2E";
|
||||
break;
|
||||
case ":cry:":
|
||||
case ":sad:":
|
||||
reaction = "\uD83D\uDE22";
|
||||
break;
|
||||
case ":angry:":
|
||||
reaction = "\uD83D\uDE20";
|
||||
break;
|
||||
case ":thumbsup:":
|
||||
case ":like:":
|
||||
reaction = "\uD83D\uDC4D";
|
||||
break;
|
||||
case ":thumbsdown:":
|
||||
case ":dislike:":
|
||||
reaction = "\uD83D\uDC4E";
|
||||
break;
|
||||
default:
|
||||
return callback({ error: "Reaction is not a valid emoji." });
|
||||
break;
|
||||
}
|
||||
|
||||
var variables = {
|
||||
data: {
|
||||
client_mutation_id: ctx.clientMutationId++,
|
||||
actor_id: ctx.userID,
|
||||
action: reaction == "" ? "REMOVE_REACTION" : "ADD_REACTION",
|
||||
message_id: messageID,
|
||||
reaction: reaction
|
||||
}
|
||||
};
|
||||
|
||||
var qs = {
|
||||
doc_id: "1491398900900362",
|
||||
variables: JSON.stringify(variables),
|
||||
dpr: 1
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.postFormData(
|
||||
"https://www.facebook.com/webgraphql/mutation/",
|
||||
ctx.jar,
|
||||
{},
|
||||
qs
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx.jar, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (!resData) {
|
||||
throw { error: "setReaction returned empty object." };
|
||||
}
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
callback(null);
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("setReaction", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function setTitle(newTitle, threadID, callback) {
|
||||
if (
|
||||
!callback &&
|
||||
(utils.getType(threadID) === "Function" ||
|
||||
utils.getType(threadID) === "AsyncFunction")
|
||||
) {
|
||||
throw { error: "please pass a threadID as a second argument." };
|
||||
}
|
||||
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
var messageAndOTID = utils.generateOfflineThreadingID();
|
||||
var form = {
|
||||
client: "mercury",
|
||||
action_type: "ma-type:log-message",
|
||||
author: "fbid:" + ctx.userID,
|
||||
thread_id: "",
|
||||
author_email: "",
|
||||
coordinates: "",
|
||||
timestamp: Date.now(),
|
||||
timestamp_absolute: "Today",
|
||||
timestamp_relative: utils.generateTimestampRelative(),
|
||||
timestamp_time_passed: "0",
|
||||
is_unread: false,
|
||||
is_cleared: false,
|
||||
is_forward: false,
|
||||
is_filtered_content: false,
|
||||
is_spoof_warning: false,
|
||||
source: "source:chat:web",
|
||||
"source_tags[0]": "source:chat",
|
||||
status: "0",
|
||||
offline_threading_id: messageAndOTID,
|
||||
message_id: messageAndOTID,
|
||||
threading_id: utils.generateThreadingID(ctx.clientID),
|
||||
manual_retry_cnt: "0",
|
||||
thread_fbid: threadID,
|
||||
thread_name: newTitle,
|
||||
thread_id: threadID,
|
||||
log_message_type: "log:thread-name"
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post("https://www.facebook.com/messaging/set_thread_name/", ctx.jar, form)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error && resData.error === 1545012) {
|
||||
throw { error: "Cannot change chat title: Not member of chat." };
|
||||
}
|
||||
|
||||
if (resData.error && resData.error === 1545003) {
|
||||
throw { error: "Cannot set title of single-user chat." };
|
||||
}
|
||||
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("setTitle", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
// Currently the only colors that can be passed to api.changeThreadColor(); may change if Facebook adds more
|
||||
return {
|
||||
MessengerBlue: null,
|
||||
Viking: "#44bec7",
|
||||
GoldenPoppy: "#ffc300",
|
||||
RadicalRed: "#fa3c4c",
|
||||
Shocking: "#d696bb",
|
||||
PictonBlue: "#6699cc",
|
||||
FreeSpeechGreen: "#13cf13",
|
||||
Pumpkin: "#ff7e29",
|
||||
LightCoral: "#e68585",
|
||||
MediumSlateBlue: "#7646ff",
|
||||
DeepSkyBlue: "#20cef5",
|
||||
Fern: "#67b868",
|
||||
Cameo: "#d4a88c",
|
||||
BrilliantRose: "#ff5ca1",
|
||||
BilobaFlower: "#a695c7"
|
||||
};
|
||||
};
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
"use strict";
|
||||
|
||||
var utils = require("../utils");
|
||||
var log = require("npmlog");
|
||||
|
||||
module.exports = function(defaultFuncs, api, ctx) {
|
||||
return function unsendMessage(messageID, callback) {
|
||||
if (!callback) {
|
||||
callback = function() {};
|
||||
}
|
||||
|
||||
var form = {
|
||||
message_id: messageID
|
||||
};
|
||||
|
||||
defaultFuncs
|
||||
.post(
|
||||
"https://www.facebook.com/messaging/unsend_message/",
|
||||
ctx.jar,
|
||||
form
|
||||
)
|
||||
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
||||
.then(function(resData) {
|
||||
if (resData.error) {
|
||||
throw resData;
|
||||
}
|
||||
|
||||
return callback();
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error("unsendMessage", err);
|
||||
return callback(err);
|
||||
});
|
||||
};
|
||||
};
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
module.exports = {
|
||||
"delta": {
|
||||
"attachments": [
|
||||
{
|
||||
"fbid": "1522004821162174",
|
||||
"id": "1522004821162174",
|
||||
"mercury": {
|
||||
"app_attribution": null,
|
||||
"attach_type": "share",
|
||||
"name": null,
|
||||
"url": null,
|
||||
"rel": null,
|
||||
"preview_url": null,
|
||||
"preview_width": null,
|
||||
"preview_height": null,
|
||||
"large_preview_url": null,
|
||||
"large_preview_width": null,
|
||||
"large_preview_height": null,
|
||||
"icon_type": null,
|
||||
"metadata": null,
|
||||
"thumbnail_url": null,
|
||||
"share": {
|
||||
"description": null,
|
||||
"media": {
|
||||
"animated_image": null,
|
||||
"animated_image_size": {
|
||||
"height": null,
|
||||
"width": null
|
||||
},
|
||||
"image": null,
|
||||
"image_size": {
|
||||
"height": null,
|
||||
"width": null
|
||||
},
|
||||
"duration": null,
|
||||
"playable": null,
|
||||
"source": null
|
||||
},
|
||||
"source": "Dimon - testing",
|
||||
"style_list": [
|
||||
"business_message_items", "fallback"
|
||||
],
|
||||
"title": "search engines",
|
||||
"properties": null,
|
||||
"uri": null,
|
||||
"subattachments": [],
|
||||
"deduplication_key": "abcde",
|
||||
"action_links": [],
|
||||
"share_id": "1522004821162174",
|
||||
"target": {
|
||||
"call_to_actions": [],
|
||||
"items": [
|
||||
{
|
||||
"id": "629934437209008",
|
||||
"name": "search engines",
|
||||
"desc": "",
|
||||
"thumb_url": null,
|
||||
"item_url": null,
|
||||
"title": "search engines",
|
||||
"text": "",
|
||||
"source": null,
|
||||
"metalines": {
|
||||
"metaline_1": "click to get redirected",
|
||||
"metaline_2": null,
|
||||
"metaline_3": null
|
||||
},
|
||||
"location": 12314,
|
||||
"category": 69,
|
||||
"call_to_actions": [
|
||||
{
|
||||
"action_link": "http://l.facebook.com/l.php?u=http%3A%2F%2Fgoogle.com%2F&h=ATNziCq_-6I3ZPYwwLluFdCrWMEwLLKvokFlXdEdS4LD2Lzsv2cR2SJYffJcDYBfB092Xeq8oRdftJk4husEYVduH24RnlP3HvVQOkOrciXDs2M7TkWYyNLBelvJ2Fc-mw8pbGy5NslGf_fkZ_A",
|
||||
"action_type": 2,
|
||||
"id": "FFD=",
|
||||
"title": "Google",
|
||||
"link_target_ids": [629934437209008],
|
||||
"is_mutable_by_server": false,
|
||||
"should_show_user_confirmation": false,
|
||||
"confirmation_title": null,
|
||||
"confirmation_message": null,
|
||||
"confirmation_continue_label": null,
|
||||
"confirmation_cancel_label": null,
|
||||
"payment_metadata": {
|
||||
"total_price": null,
|
||||
"payment_module_config": null
|
||||
},
|
||||
"is_disabled": false
|
||||
}, {
|
||||
"action_link": "http://l.facebook.com/l.php?u=http%3A%2F%2Fyahoo.com%2F&h=ATNIuTf7iDGP5xXTWOAdhaGhRFfDf4eS09t_G9CrR0MDiBKpqtCDzPf_9y5Bq7TXMgmo6RttztsgeO0ReSc0PDvJDTa1fLMMK2CjrpkqC91_m-yaMXfeQ4aI6MbhZrOPnK3YFnQP4XvRx3N1udE",
|
||||
"action_type": 2,
|
||||
"id": "CDE=",
|
||||
"title": "Yahoo",
|
||||
"link_target_ids": [629934437209008],
|
||||
"is_mutable_by_server": false,
|
||||
"should_show_user_confirmation": false,
|
||||
"confirmation_title": null,
|
||||
"confirmation_message": null,
|
||||
"confirmation_continue_label": null,
|
||||
"confirmation_cancel_label": null,
|
||||
"payment_metadata": {
|
||||
"total_price": null,
|
||||
"payment_module_config": null
|
||||
},
|
||||
"is_disabled": false
|
||||
}, {
|
||||
"action_link": "http://l.facebook.com/l.php?u=http%3A%2F%2Fbing.com%2F&h=ATMoMijAt6Da6WWIQ679DhZyZizWdxAViWwyl-RjKobFUG_x8GmB8LD6pPa3KP5K1-QTL9vuaFwjqB0itaMFWk4VwQ9uh56JgnbFnAo4qM_CrQufgLeHwwCnWSCnZt8IzYT4y6YULLLFA5bL1H4",
|
||||
"action_type": 2,
|
||||
"id": "ABC=",
|
||||
"title": "Bing",
|
||||
"link_target_ids": [629934437209008],
|
||||
"is_mutable_by_server": false,
|
||||
"should_show_user_confirmation": false,
|
||||
"confirmation_title": null,
|
||||
"confirmation_message": null,
|
||||
"confirmation_continue_label": null,
|
||||
"confirmation_cancel_label": null,
|
||||
"payment_metadata": {
|
||||
"total_price": null,
|
||||
"payment_module_config": null
|
||||
},
|
||||
"is_disabled": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"location": 132145,
|
||||
"category": 69,
|
||||
"message": "Aaa: search engines"
|
||||
}
|
||||
}
|
||||
},
|
||||
"otherUserFbIds": ["1521994257829897"],
|
||||
"titanType": 1
|
||||
}
|
||||
],
|
||||
"messageMetadata": {
|
||||
"actorFbId": "1345",
|
||||
"messageId": "mid.12345:asdv",
|
||||
"offlineThreadingId": "1345v1345",
|
||||
"tags": ["source:messenger:commerce"],
|
||||
"threadKey": {
|
||||
"otherUserFbId": "13451345"
|
||||
},
|
||||
"timestamp": "1487078180265"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
+7
@@ -0,0 +1,7 @@
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
// Instructions: Copy this file to test-config.json, fill in your test data
|
||||
// and remove all comments (JSON doesn't support comments).
|
||||
// Run the test with `npm test` after installing the devDependencies (mocha).
|
||||
{
|
||||
// Test user login information
|
||||
"user" : {
|
||||
"id" : "00000000000000",
|
||||
"email" : "example@test.com",
|
||||
"password" : "qwerty"
|
||||
},
|
||||
// Array of at least 2 other user IDs (not the same as the test user)
|
||||
"userIDs" : [
|
||||
"11111111111111",
|
||||
"22222222222222"
|
||||
],
|
||||
// Id of page to which test user is an admin
|
||||
"pageID": "3333333333333"
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
var login = require('../index.js');
|
||||
var fs = require('fs');
|
||||
var assert = require('assert');
|
||||
|
||||
var conf = JSON.parse(process.env.testconfig || fs.readFileSync('test/test-config.json', 'utf8'));
|
||||
var credentials = {
|
||||
email: conf.user.email,
|
||||
password: conf.user.password,
|
||||
};
|
||||
|
||||
var userIDs = conf.userIDs;
|
||||
|
||||
var options = {
|
||||
selfListen: true,
|
||||
listenEvents: true,
|
||||
logLevel: "silent",
|
||||
pageID: conf.pageID
|
||||
};
|
||||
var getType = require('../utils').getType;
|
||||
|
||||
var userID = conf.user.id;
|
||||
|
||||
var groupChatID;
|
||||
var groupChatName;
|
||||
|
||||
function checkErr(done){
|
||||
return function(err) {
|
||||
if (err) done(err);
|
||||
};
|
||||
}
|
||||
|
||||
// describe('Login As Page:', function() {
|
||||
// var api = null;
|
||||
// process.on('SIGINT', () => api && !api.logout() && console.log("Logged out :)"));
|
||||
// var tests = [];
|
||||
// var stopListening;
|
||||
// this.timeout(20000);
|
||||
|
||||
// function listen(done, matcher) {
|
||||
// tests.push({matcher:matcher, done:done});
|
||||
// }
|
||||
|
||||
// before(function(done) {
|
||||
// login(credentials, options, function (err, localAPI) {
|
||||
// if(err) return done(err);
|
||||
|
||||
// assert(localAPI);
|
||||
// api = localAPI;
|
||||
// stopListening = api.listen(function (err, msg) {
|
||||
// if (err) throw err;
|
||||
// // Removes matching function and calls corresponding done
|
||||
// tests = tests.filter(function(test) {
|
||||
// return !(test.matcher(msg) && (test.done() || true));
|
||||
// });
|
||||
// });
|
||||
|
||||
// done();
|
||||
// });
|
||||
// });
|
||||
|
||||
// it('should login without error', function (){
|
||||
// assert(api);
|
||||
// });
|
||||
|
||||
// it('should get the right user ID', function (){
|
||||
// assert(userID == api.getCurrentUserID());
|
||||
// });
|
||||
|
||||
// it('should send text message object (user)', function (done){
|
||||
// var body = "text-msg-obj-" + Date.now();
|
||||
// listen(done, msg =>
|
||||
// msg.type === 'message' &&
|
||||
// msg.body === body &&
|
||||
// msg.isGroup === false
|
||||
// );
|
||||
// api.sendMessage({body: body}, userID, checkErr(done));
|
||||
// });
|
||||
|
||||
// it('should send sticker message object (user)', function (done){
|
||||
// var stickerID = '767334526626290';
|
||||
// listen(done, msg =>
|
||||
// msg.type === 'message' &&
|
||||
// msg.attachments.length > 0 &&
|
||||
// msg.attachments[0].type === 'sticker' &&
|
||||
// msg.attachments[0].stickerID === stickerID &&
|
||||
// msg.isGroup === false
|
||||
// );
|
||||
// api.sendMessage({sticker: stickerID}, userID, checkErr(done));
|
||||
// });
|
||||
|
||||
// it('should send basic string (user)', function (done){
|
||||
// var body = "basic-str-" + Date.now();
|
||||
// listen(done, msg =>
|
||||
// msg.type === 'message' &&
|
||||
// msg.body === body &&
|
||||
// msg.isGroup === false
|
||||
// );
|
||||
// api.sendMessage(body, userID, checkErr(done));
|
||||
// });
|
||||
|
||||
// it('should send typing indicator', function (done) {
|
||||
// var stopType = api.sendTypingIndicator(userID, function(err) {
|
||||
// checkErr(done)(err);
|
||||
// stopType();
|
||||
// done();
|
||||
// });
|
||||
// });
|
||||
|
||||
// it('should get the right user info', function (done) {
|
||||
// api.getUserInfo(userID, function(err, data) {
|
||||
// checkErr(done)(err);
|
||||
// var user = data[userID];
|
||||
// assert(user.name);
|
||||
// assert(user.firstName);
|
||||
// assert(user.vanity !== null);
|
||||
// assert(user.profileUrl);
|
||||
// assert(user.gender);
|
||||
// assert(user.type);
|
||||
// assert(!user.isFriend);
|
||||
// done();
|
||||
// });
|
||||
// });
|
||||
|
||||
// it('should get the list of friends', function (done) {
|
||||
// api.getFriendsList(function(err, data) {
|
||||
// checkErr(done)(err);
|
||||
// assert(getType(data) === "Array");
|
||||
// data.map(function(v) {parseInt(v);});
|
||||
// done();
|
||||
// });
|
||||
// });
|
||||
|
||||
// it('should log out', function (done) {
|
||||
// api.logout(done);
|
||||
// });
|
||||
|
||||
// after(function (){
|
||||
// if (stopListening) stopListening();
|
||||
// });
|
||||
// });
|
||||
+385
@@ -0,0 +1,385 @@
|
||||
var login = require('../index.js');
|
||||
var fs = require('fs');
|
||||
var assert = require('assert');
|
||||
|
||||
var conf = JSON.parse(process.env.testconfig || fs.readFileSync('test/test-config.json', 'utf8'));
|
||||
var credentials = {
|
||||
email: conf.user.email,
|
||||
password: conf.user.password,
|
||||
};
|
||||
|
||||
var userIDs = conf.userIDs;
|
||||
|
||||
var options = { selfListen: true, listenEvents: true, logLevel: "silent"};
|
||||
var pageOptions = {logLevel: 'silent', pageID: conf.pageID};
|
||||
var getType = require('../utils').getType;
|
||||
var formatDeltaMessage = require('../utils').formatDeltaMessage;
|
||||
var shareAttachmentFixture = require('./data/shareAttach');
|
||||
|
||||
var userID = conf.user.id;
|
||||
|
||||
var groupChatID;
|
||||
var groupChatName;
|
||||
|
||||
function checkErr(done){
|
||||
return function(err) {
|
||||
if (err) done(err);
|
||||
};
|
||||
}
|
||||
|
||||
describe('Login:', function() {
|
||||
var api = null;
|
||||
process.on('SIGINT', () => api && !api.logout() && console.log("Logged out :)"));
|
||||
var tests = [];
|
||||
var stopListening;
|
||||
this.timeout(20000);
|
||||
|
||||
function listen(done, matcher) {
|
||||
tests.push({matcher:matcher, done:done});
|
||||
}
|
||||
|
||||
before(function(done) {
|
||||
login(credentials, options, function (err, localAPI) {
|
||||
if(err) return done(err);
|
||||
|
||||
assert(localAPI);
|
||||
api = localAPI;
|
||||
stopListening = api.listen(function (err, msg) {
|
||||
if (err) throw err;
|
||||
if (msg.type === "message") {
|
||||
assert(msg.senderID && !isNaN(msg.senderID));
|
||||
assert(msg.threadID && !isNaN(msg.threadID));
|
||||
assert(msg.timestamp && !isNaN(msg.timestamp));
|
||||
assert(msg.messageID != null && msg.messageID.length > 0);
|
||||
assert(msg.body != null || msg.attachments.length > 0);
|
||||
}
|
||||
// Removes matching function and calls corresponding done
|
||||
tests = tests.filter(function(test) {
|
||||
return !(test.matcher(msg) && (test.done() || true));
|
||||
});
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should login without error', function (){
|
||||
assert(api);
|
||||
});
|
||||
|
||||
it('should get the current user ID', function (){
|
||||
assert(userID === api.getCurrentUserID());
|
||||
});
|
||||
|
||||
it('should send text message object (user)', function (done){
|
||||
var body = "text-msg-obj-" + Date.now();
|
||||
listen(done, msg =>
|
||||
msg.type === 'message' &&
|
||||
msg.body === body &&
|
||||
msg.isGroup === false
|
||||
);
|
||||
api.sendMessage({body: body}, userID, checkErr(done));
|
||||
});
|
||||
|
||||
it('should send sticker message object (user)', function (done){
|
||||
var stickerID = '767334526626290';
|
||||
listen(done, msg =>
|
||||
msg.type === 'message' &&
|
||||
msg.attachments.length > 0 &&
|
||||
msg.attachments[0].type === 'sticker' &&
|
||||
msg.attachments[0].stickerID === stickerID &&
|
||||
msg.isGroup === false
|
||||
);
|
||||
api.sendMessage({sticker: stickerID}, userID, checkErr(done));
|
||||
});
|
||||
|
||||
it('should send basic string (user)', function (done){
|
||||
var body = "basic-str-" + Date.now();
|
||||
listen(done, msg =>
|
||||
msg.type === 'message' &&
|
||||
msg.body === body &&
|
||||
msg.isGroup === false
|
||||
);
|
||||
api.sendMessage(body, userID, checkErr(done));
|
||||
});
|
||||
|
||||
it('should get thread info (user)', function (done){
|
||||
api.getThreadInfo(userID, (err, info) => {
|
||||
if (err) done(err);
|
||||
|
||||
assert(info.participantIDs != null && info.participantIDs.length > 0);
|
||||
assert(!info.participantIDs.some(isNaN));
|
||||
assert(!info.participantIDs.some(v => v.length == 0));
|
||||
assert(info.name != null);
|
||||
assert(info.messageCount != null && !isNaN(info.messageCount));
|
||||
assert(info.hasOwnProperty('emoji'));
|
||||
assert(info.hasOwnProperty('nicknames'));
|
||||
assert(info.hasOwnProperty('color'));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should get the history of the chat (user)', function (done) {
|
||||
api.getThreadHistory(userID, 5, null, function(err, data) {
|
||||
checkErr(done)(err);
|
||||
assert(getType(data) === "Array");
|
||||
assert(data.every(function(v) {return getType(v) == "Object";}));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should get the history of the chat (user) (graphql)', function (done) {
|
||||
api.getThreadHistoryGraphQL(userID, 5, null, function(err, data) {
|
||||
checkErr(done)(err);
|
||||
assert(getType(data) === "Array");
|
||||
assert(data.every(function(v) {return getType(v) == "Object";}));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a chat', function (done){
|
||||
var body = "new-chat-" + Date.now();
|
||||
var inc = 0;
|
||||
|
||||
function doneHack(){
|
||||
if (inc === 1) return done();
|
||||
inc++;
|
||||
}
|
||||
|
||||
listen(doneHack, msg =>
|
||||
msg.type === 'message' && msg.body === body
|
||||
);
|
||||
api.sendMessage(body, userIDs, function(err, info){
|
||||
checkErr(done)(err);
|
||||
groupChatID = info.threadID;
|
||||
doneHack();
|
||||
});
|
||||
});
|
||||
|
||||
it('should send text message object (group)', function (done){
|
||||
var body = "text-msg-obj-" + Date.now();
|
||||
listen(done, msg =>
|
||||
msg.type === 'message' &&
|
||||
msg.body === body &&
|
||||
msg.isGroup === true
|
||||
);
|
||||
api.sendMessage({body: body}, groupChatID, function(err, info){
|
||||
checkErr(done)(err);
|
||||
assert(groupChatID === info.threadID);
|
||||
});
|
||||
});
|
||||
|
||||
it('should send basic string (group)', function (done){
|
||||
var body = "basic-str-" + Date.now();
|
||||
listen(done, msg =>
|
||||
msg.type === 'message' &&
|
||||
msg.body === body &&
|
||||
msg.isGroup === true
|
||||
);
|
||||
api.sendMessage(body, groupChatID, function(err, info) {
|
||||
checkErr(done)(err);
|
||||
assert(groupChatID === info.threadID);
|
||||
});
|
||||
});
|
||||
|
||||
it('should send sticker message object (group)', function (done){
|
||||
var stickerID = '767334526626290';
|
||||
listen(done, function (msg) {
|
||||
return msg.type === 'message' &&
|
||||
msg.attachments.length > 0 &&
|
||||
msg.attachments[0].type === 'sticker' &&
|
||||
msg.attachments[0].stickerID === stickerID;
|
||||
});
|
||||
api.sendMessage({sticker: stickerID}, groupChatID, function (err, info) {
|
||||
assert(groupChatID === info.threadID);
|
||||
checkErr(done)(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should send an attachment with a body (group)', function (done){
|
||||
var body = "attach-" + Date.now();
|
||||
var attach = [];
|
||||
attach.push(fs.createReadStream("test/data/test.txt"));
|
||||
attach.push(fs.createReadStream("test/data/test.png"));
|
||||
listen(done, function (msg) {
|
||||
return msg.type === 'message' && msg.body === body;
|
||||
});
|
||||
api.sendMessage({attachment: attach, body: body}, groupChatID, function(err, info){
|
||||
checkErr(done)(err);
|
||||
assert(groupChatID === info.threadID);
|
||||
});
|
||||
});
|
||||
|
||||
it('should get the history of the chat (group)', function (done) {
|
||||
api.getThreadHistory(groupChatID, 5, null, function(err, data) {
|
||||
checkErr(done)(err);
|
||||
assert(getType(data) === "Array");
|
||||
assert(data.every(function(v) {return getType(v) == "Object";}));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should get the history of the chat (group) (graphql)', function (done) {
|
||||
api.getThreadHistoryGraphQL(groupChatID, 5, null, function(err, data) {
|
||||
checkErr(done)(err);
|
||||
assert(getType(data) === "Array");
|
||||
assert(data.every(function(v) {return getType(v) == "Object";}));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should change chat title', function (done){
|
||||
var title = 'test-chat-title-' + Date.now();
|
||||
listen(done, function (msg) {
|
||||
return msg.type === 'event' &&
|
||||
msg.logMessageType === 'log:thread-name' &&
|
||||
msg.logMessageData.name === title;
|
||||
});
|
||||
groupChatName = title;
|
||||
api.setTitle(title, groupChatID, checkErr(done));
|
||||
});
|
||||
|
||||
it('should kick user', function (done) {
|
||||
var id = userIDs[0];
|
||||
listen(done, function (msg) {
|
||||
return msg.type === 'event' &&
|
||||
msg.logMessageType === 'log:unsubscribe' &&
|
||||
msg.logMessageData.leftParticipantFbId === id;
|
||||
});
|
||||
api.removeUserFromGroup(id, groupChatID, checkErr(done));
|
||||
});
|
||||
|
||||
it('should add user', function (done) {
|
||||
var id = userIDs[0];
|
||||
listen(done, function (msg) {
|
||||
return (msg.type === 'event' &&
|
||||
msg.logMessageType === 'log:subscribe' &&
|
||||
msg.logMessageData.addedParticipants.length > 0 &&
|
||||
msg.logMessageData.addedParticipants[0].userFbId === id);
|
||||
});
|
||||
// TODO: we don't check for errors inside this because FB changed and
|
||||
// returns an error, even though we receive the event that the user was
|
||||
// added
|
||||
api.addUserToGroup(id, groupChatID, function() {});
|
||||
});
|
||||
|
||||
xit('should get thread info (group)', function (done){
|
||||
api.getThreadInfo(groupChatID, (err, info) => {
|
||||
if (err) done(err);
|
||||
|
||||
assert(info.participantIDs != null && info.participantIDs.length > 0);
|
||||
assert(!info.participantIDs.some(isNaN));
|
||||
assert(!info.participantIDs.some(v => v.length == 0));
|
||||
assert(info.name != null);
|
||||
assert(info.messageCount != null && !isNaN(info.messageCount));
|
||||
assert(info.hasOwnProperty('emoji'));
|
||||
assert(info.hasOwnProperty('nicknames'));
|
||||
assert(info.hasOwnProperty('color'));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should retrieve a list of threads', function (done) {
|
||||
api.getThreadList(0, 20, function(err, res) {
|
||||
checkErr(done)(err);
|
||||
|
||||
// This checks to see if the group chat we just made
|
||||
// is in the list... it should be.
|
||||
assert(res.some(function (v) {
|
||||
return (
|
||||
v.threadID === groupChatID &&
|
||||
userIDs.every(function (val) {
|
||||
return v.participants.indexOf(val) > -1;
|
||||
}) &&
|
||||
v.name === groupChatName
|
||||
);
|
||||
}));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should mark as read', function (done){
|
||||
api.markAsRead(groupChatID, done);
|
||||
});
|
||||
|
||||
it('should send typing indicator', function (done) {
|
||||
var stopType = api.sendTypingIndicator(groupChatID, function(err) {
|
||||
checkErr(done)(err);
|
||||
stopType();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should get the right user info', function (done) {
|
||||
api.getUserInfo(userID, function(err, data) {
|
||||
checkErr(done)(err);
|
||||
var user = data[userID];
|
||||
assert(user.name);
|
||||
assert(user.firstName);
|
||||
assert(user.vanity !== null);
|
||||
assert(user.profileUrl);
|
||||
assert(user.gender);
|
||||
assert(user.type);
|
||||
assert(!user.isFriend);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should get the user ID', function(done) {
|
||||
api.getUserInfo(userIDs[0], function(err, data) {
|
||||
checkErr(done)(err);
|
||||
var user = data[userIDs[0]];
|
||||
api.getUserID(user.name, function(err, data) {
|
||||
checkErr(done)(err);
|
||||
assert(getType(data) === "Array");
|
||||
assert(data.some(function(val) {
|
||||
return val.userID === userIDs[0];
|
||||
}));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should get the list of friends', function (done) {
|
||||
api.getFriendsList(function(err, data) {
|
||||
try {
|
||||
checkErr(done)(err);
|
||||
assert(getType(data) === "Array");
|
||||
data.map(v => {
|
||||
assert(getType(v.firstName) === "String");
|
||||
assert(getType(v.gender) === "String");
|
||||
assert(getType(v.userID) === "String");
|
||||
assert(getType(v.isFriend) === "Boolean");
|
||||
assert(getType(v.fullName) === "String");
|
||||
assert(getType(v.profilePicture) === "String");
|
||||
assert(getType(v.type) === "String");
|
||||
assert(v.hasOwnProperty("profileUrl")); // This can be null if the account is disabled
|
||||
assert(getType(v.isBirthday) === "Boolean");
|
||||
})
|
||||
done();
|
||||
} catch(e){
|
||||
done(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse share attachment correctly', function () {
|
||||
var formatted = formatDeltaMessage(shareAttachmentFixture);
|
||||
assert(formatted.attachments[0].type === "share");
|
||||
assert(formatted.attachments[0].title === "search engines");
|
||||
assert(formatted.attachments[0].target.items[0].name === "search engines");
|
||||
assert(formatted.attachments[0].target.items[0].call_to_actions.length === 3);
|
||||
assert(formatted.attachments[0].target.items[0].call_to_actions[0].title === "Google");
|
||||
});
|
||||
|
||||
it('should log out', function (done) {
|
||||
api.logout(done);
|
||||
});
|
||||
|
||||
after(function (){
|
||||
if (stopListening) stopListening();
|
||||
});
|
||||
});
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
"{
|
||||
\"user\" : {
|
||||
\"id\" : \"100009069356507\",
|
||||
\"email\" : \"mark.zuckerbot@gmail.com\",
|
||||
\"password\" : \"NwHgLmuGQUgzjZ5GYKZHv9Fvx7F5wSU9;;;\"
|
||||
},
|
||||
\"userIDs\" : [
|
||||
\"100001056938824\",
|
||||
\"100009563449169\",
|
||||
\"1216678154\"
|
||||
],
|
||||
\"pageID\": \"1439610092996093\"
|
||||
}"
|
||||
+1281
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user