# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import requests
import urllib
from uuid import uuid1
from random import choice
from bs4 import BeautifulSoup as bs
from mimetypes import guess_type
from collections import OrderedDict
from ._util import *
from .models import *
from . import _graphql
from ._state import State
import time
import json
try:
from urllib.parse import urlparse, parse_qs
except ImportError:
from urlparse import urlparse, parse_qs
ACONTEXT = {
"action_history": [
{"surface": "messenger_chat_tab", "mechanism": "messenger_composer"}
]
}
[docs]class Client(object):
"""A client for the Facebook Chat (Messenger).
This is the main class of ``fbchat``, which contains all the methods you use to
interact with Facebook. You can extend this class, and overwrite the ``on`` methods,
to provide custom event handling (mainly useful while listening).
"""
listening = False
"""Whether the client is listening. Used when creating an external event loop to determine when to stop listening"""
@property
def ssl_verify(self):
"""Verify ssl certificate, set to False to allow debugging with a proxy."""
# TODO: Deprecate this
return self._state._session.verify
@ssl_verify.setter
def ssl_verify(self, value):
self._state._session.verify = value
@property
def uid(self):
"""The ID of the client.
Can be used as ``thread_id``. See :ref:`intro_threads` for more info.
"""
return self._uid
def __init__(
self,
email,
password,
user_agent=None,
max_tries=5,
session_cookies=None,
logging_level=logging.INFO,
):
"""Initialize and log in the client.
:param email: Facebook ``email``, ``id`` or ``phone number``
:param password: Facebook account password
:param user_agent: Custom user agent to use when sending requests. If `None`, user agent will be chosen from a premade list
:param max_tries: Maximum number of times to try logging in
:param session_cookies: Cookies from a previous session (Will default to login if these are invalid)
:param logging_level: Configures the `logging level <https://docs.python.org/3/library/logging.html#logging-levels>`_. Defaults to ``logging.INFO``
:type max_tries: int
:type session_cookies: dict
:type logging_level: int
:raises: FBchatException on failed login
"""
self._sticky, self._pool = (None, None)
self._seq = "0"
self._client_id = hex(int(random() * 2 ** 31))[2:]
self._default_thread_id = None
self._default_thread_type = None
self._pull_channel = 0
self._markAlive = True
self._buddylist = dict()
handler.setLevel(logging_level)
# If session cookies aren't set, not properly loaded or gives us an invalid session, then do the login
if (
not session_cookies
or not self.setSession(session_cookies, user_agent=user_agent)
or not self.isLoggedIn()
):
self.login(email, password, max_tries, user_agent=user_agent)
"""
INTERNAL REQUEST METHODS
"""
def _generatePayload(self, query):
if not query:
query = {}
query.update(self._state.get_params())
return query
def _do_refresh(self):
# TODO: Raise the error instead, and make the user do the refresh manually
# It may be a bad idea to do this in an exception handler, if you have a better method, please suggest it!
log.warning("Refreshing state and resending request")
self._state = State.from_session(session=self._state._session)
def _get(self, url, query=None, error_retries=3):
payload = self._generatePayload(query)
r = self._state._session.get(prefix_url(url), params=payload)
content = check_request(r)
j = to_json(content)
try:
handle_payload_error(j)
except FBchatPleaseRefresh:
if error_retries > 0:
self._do_refresh()
return self._get(url, query=query, error_retries=error_retries - 1)
raise
return j
def _post(self, url, query=None, files=None, as_graphql=False, error_retries=3):
payload = self._generatePayload(query)
r = self._state._session.post(prefix_url(url), data=payload, files=files)
content = check_request(r)
try:
if as_graphql:
return _graphql.response_to_json(content)
else:
j = to_json(content)
# TODO: Remove this, and move it to _payload_post instead
# We can't yet, since errors raised in here need to be caught below
handle_payload_error(j)
return j
except FBchatPleaseRefresh:
if error_retries > 0:
self._do_refresh()
return self._post(
url,
query=query,
files=files,
as_graphql=as_graphql,
error_retries=error_retries - 1,
)
raise
def _payload_post(self, url, data, files=None):
j = self._post(url, data, files=files)
try:
return j["payload"]
except (KeyError, TypeError):
raise FBchatException("Missing payload: {}".format(j))
[docs] def graphql_requests(self, *queries):
"""
:param queries: Zero or more dictionaries
:type queries: dict
:raises: FBchatException if request failed
:return: A tuple containing json graphql queries
:rtype: tuple
"""
data = {
"method": "GET",
"response_format": "json",
"queries": _graphql.queries_to_json(*queries),
}
return tuple(self._post("/api/graphqlbatch/", data, as_graphql=True))
[docs] def graphql_request(self, query):
"""
Shorthand for ``graphql_requests(query)[0]``
:raises: FBchatException if request failed
"""
return self.graphql_requests(query)[0]
"""
END INTERNAL REQUEST METHODS
"""
"""
LOGIN METHODS
"""
[docs] def isLoggedIn(self):
"""
Sends a request to Facebook to check the login status
:return: True if the client is still logged in
:rtype: bool
"""
return self._state.is_logged_in()
[docs] def getSession(self):
"""Retrieves session cookies
:return: A dictionay containing session cookies
:rtype: dict
"""
return self._state.get_cookies()
[docs] def setSession(self, session_cookies, user_agent=None):
"""Loads session cookies
:param session_cookies: A dictionay containing session cookies
:type session_cookies: dict
:return: False if ``session_cookies`` does not contain proper cookies
:rtype: bool
"""
try:
# Load cookies into current session
state = State.from_cookies(session_cookies, user_agent=user_agent)
except Exception as e:
log.exception("Failed loading session")
return False
uid = state.get_user_id()
if uid is None:
log.warning("Could not find c_user cookie")
return False
self._state = state
self._uid = uid
return True
[docs] def login(self, email, password, max_tries=5, user_agent=None):
"""
Uses ``email`` and ``password`` to login the user (If the user is already logged in, this will do a re-login)
:param email: Facebook ``email`` or ``id`` or ``phone number``
:param password: Facebook account password
:param max_tries: Maximum number of times to try logging in
:type max_tries: int
:raises: FBchatException on failed login
"""
self.onLoggingIn(email=email)
if max_tries < 1:
raise FBchatUserError("Cannot login: max_tries should be at least one")
if not (email and password):
raise FBchatUserError("Email and password not set")
for i in range(1, max_tries + 1):
try:
state = State.login(email, password, user_agent=user_agent)
uid = state.get_user_id()
if uid is None:
raise FBchatException("Could not find user id")
except Exception:
if i >= max_tries:
raise
log.exception("Attempt #{} failed, retrying".format(i))
time.sleep(1)
else:
self._state = state
self._uid = uid
self.onLoggedIn(email=email)
break
[docs] def logout(self):
"""
Safely logs out the client
:return: True if the action was successful
:rtype: bool
"""
if self._state.logout():
self._state = None
self._uid = None
return True
return False
"""
END LOGIN METHODS
"""
"""
DEFAULT THREAD METHODS
"""
def _getThread(self, given_thread_id=None, given_thread_type=None):
"""
Checks if thread ID is given, checks if default is set and returns correct values
:raises ValueError: If thread ID is not given and there is no default
:return: Thread ID and thread type
:rtype: tuple
"""
if given_thread_id is None:
if self._default_thread_id is not None:
return self._default_thread_id, self._default_thread_type
else:
raise ValueError("Thread ID is not set")
else:
return given_thread_id, given_thread_type
[docs] def setDefaultThread(self, thread_id, thread_type):
"""
Sets default thread to send messages to
:param thread_id: User/Group ID to default to. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_threads`
:type thread_type: ThreadType
"""
self._default_thread_id = thread_id
self._default_thread_type = thread_type
[docs] def resetDefaultThread(self):
"""Resets default thread"""
self.setDefaultThread(None, None)
"""
END DEFAULT THREAD METHODS
"""
"""
FETCH METHODS
"""
def _forcedFetch(self, thread_id, mid):
params = {"thread_and_message_id": {"thread_id": thread_id, "message_id": mid}}
return self.graphql_request(_graphql.from_doc_id("1768656253222505", params))
[docs] def fetchThreads(self, thread_location, before=None, after=None, limit=None):
"""
Get all threads in thread_location.
Threads will be sorted from newest to oldest.
:param thread_location: ThreadLocation: INBOX, PENDING, ARCHIVED or OTHER
:param before: Fetch only thread before this epoch (in ms) (default all threads)
:param after: Fetch only thread after this epoch (in ms) (default all threads)
:param limit: The max. amount of threads to fetch (default all threads)
:return: :class:`Thread` objects
:rtype: list
:raises: FBchatException if request failed
"""
threads = []
last_thread_timestamp = None
while True:
# break if limit is exceeded
if limit and len(threads) >= limit:
break
# fetchThreadList returns at max 20 threads before last_thread_timestamp (included)
candidates = self.fetchThreadList(
before=last_thread_timestamp, thread_location=thread_location
)
if len(candidates) > 1:
threads += candidates[1:]
else: # End of threads
break
last_thread_timestamp = threads[-1].last_message_timestamp
# FB returns a sorted list of threads
if (before is not None and int(last_thread_timestamp) > before) or (
after is not None and int(last_thread_timestamp) < after
):
break
# Return only threads between before and after (if set)
if before is not None or after is not None:
for t in threads:
last_message_timestamp = int(t.last_message_timestamp)
if (before is not None and last_message_timestamp > before) or (
after is not None and last_message_timestamp < after
):
threads.remove(t)
if limit and len(threads) > limit:
return threads[:limit]
return threads
[docs] def fetchAllUsersFromThreads(self, threads):
"""
Get all users involved in threads.
:param threads: Thread: List of threads to check for users
:return: :class:`User` objects
:rtype: list
:raises: FBchatException if request failed
"""
users = []
users_to_fetch = [] # It's more efficient to fetch all users in one request
for thread in threads:
if thread.type == ThreadType.USER:
if thread.uid not in [user.uid for user in users]:
users.append(thread)
elif thread.type == ThreadType.GROUP:
for user_id in thread.participants:
if (
user_id not in [user.uid for user in users]
and user_id not in users_to_fetch
):
users_to_fetch.append(user_id)
for user_id, user in self.fetchUserInfo(*users_to_fetch).items():
users.append(user)
return users
[docs] def fetchAllUsers(self):
"""
Gets all users the client is currently chatting with
:return: :class:`User` objects
:rtype: list
:raises: FBchatException if request failed
"""
data = {"viewer": self._uid}
j = self._payload_post("/chat/user_info_all", data)
users = []
for data in j.values():
if data["type"] in ["user", "friend"]:
if data["id"] in ["0", 0]:
# Skip invalid users
continue
users.append(User._from_all_fetch(data))
return users
[docs] def searchForUsers(self, name, limit=10):
"""
Find and get user by his/her name
:param name: Name of the user
:param limit: The max. amount of users to fetch
:return: :class:`User` objects, ordered by relevance
:rtype: list
:raises: FBchatException if request failed
"""
params = {"search": name, "limit": limit}
j = self.graphql_request(_graphql.from_query(_graphql.SEARCH_USER, params))
return [User._from_graphql(node) for node in j[name]["users"]["nodes"]]
[docs] def searchForPages(self, name, limit=10):
"""
Find and get page by its name
:param name: Name of the page
:return: :class:`Page` objects, ordered by relevance
:rtype: list
:raises: FBchatException if request failed
"""
params = {"search": name, "limit": limit}
j = self.graphql_request(_graphql.from_query(_graphql.SEARCH_PAGE, params))
return [Page._from_graphql(node) for node in j[name]["pages"]["nodes"]]
[docs] def searchForGroups(self, name, limit=10):
"""
Find and get group thread by its name
:param name: Name of the group thread
:param limit: The max. amount of groups to fetch
:return: :class:`Group` objects, ordered by relevance
:rtype: list
:raises: FBchatException if request failed
"""
params = {"search": name, "limit": limit}
j = self.graphql_request(_graphql.from_query(_graphql.SEARCH_GROUP, params))
return [Group._from_graphql(node) for node in j["viewer"]["groups"]["nodes"]]
[docs] def searchForThreads(self, name, limit=10):
"""
Find and get a thread by its name
:param name: Name of the thread
:param limit: The max. amount of groups to fetch
:return: :class:`User`, :class:`Group` and :class:`Page` objects, ordered by relevance
:rtype: list
:raises: FBchatException if request failed
"""
params = {"search": name, "limit": limit}
j = self.graphql_request(_graphql.from_query(_graphql.SEARCH_THREAD, params))
rtn = []
for node in j[name]["threads"]["nodes"]:
if node["__typename"] == "User":
rtn.append(User._from_graphql(node))
elif node["__typename"] == "MessageThread":
# MessageThread => Group thread
rtn.append(Group._from_graphql(node))
elif node["__typename"] == "Page":
rtn.append(Page._from_graphql(node))
elif node["__typename"] == "Group":
# We don't handle Facebook "Groups"
pass
else:
log.warning(
"Unknown type {} in {}".format(repr(node["__typename"]), node)
)
return rtn
[docs] def search(self, query, fetch_messages=False, thread_limit=5, message_limit=5):
"""
Searches for messages in all threads
:param query: Text to search for
:param fetch_messages: Whether to fetch :class:`Message` objects or IDs only
:param thread_limit: Max. number of threads to retrieve
:param message_limit: Max. number of messages to retrieve
:type thread_limit: int
:type message_limit: int
:return: Dictionary with thread IDs as keys and iterables to get messages as values
:rtype: typing.Dict[str, typing.Iterable]
:raises: FBchatException if request failed
"""
data = {"query": query, "snippetLimit": thread_limit}
j = self._payload_post("/ajax/mercury/search_snippets.php?dpr=1", data)
result = j["search_snippets"][query]
if not result:
return {}
if fetch_messages:
search_method = self.searchForMessages
else:
search_method = self.searchForMessageIDs
return {
thread_id: search_method(query, limit=message_limit, thread_id=thread_id)
for thread_id in result
}
def _fetchInfo(self, *ids):
data = {"ids[{}]".format(i): _id for i, _id in enumerate(ids)}
j = self._payload_post("/chat/user_info/", data)
if j.get("profiles") is None:
raise FBchatException("No users/pages returned: {}".format(j))
entries = {}
for _id in j["profiles"]:
k = j["profiles"][_id]
if k["type"] in ["user", "friend"]:
entries[_id] = {
"id": _id,
"type": ThreadType.USER,
"url": k.get("uri"),
"first_name": k.get("firstName"),
"is_viewer_friend": k.get("is_friend"),
"gender": k.get("gender"),
"profile_picture": {"uri": k.get("thumbSrc")},
"name": k.get("name"),
}
elif k["type"] == "page":
entries[_id] = {
"id": _id,
"type": ThreadType.PAGE,
"url": k.get("uri"),
"profile_picture": {"uri": k.get("thumbSrc")},
"name": k.get("name"),
}
else:
raise FBchatException(
"{} had an unknown thread type: {}".format(_id, k)
)
log.debug(entries)
return entries
[docs] def fetchUserInfo(self, *user_ids):
"""
Get users' info from IDs, unordered
.. warning::
Sends two requests, to fetch all available info!
:param user_ids: One or more user ID(s) to query
:return: :class:`User` objects, labeled by their ID
:rtype: dict
:raises: FBchatException if request failed
"""
threads = self.fetchThreadInfo(*user_ids)
users = {}
for id_, thread in threads.items():
if thread.type == ThreadType.USER:
users[id_] = thread
else:
raise FBchatUserError("Thread {} was not a user".format(thread))
return users
[docs] def fetchPageInfo(self, *page_ids):
"""
Get pages' info from IDs, unordered
.. warning::
Sends two requests, to fetch all available info!
:param page_ids: One or more page ID(s) to query
:return: :class:`Page` objects, labeled by their ID
:rtype: dict
:raises: FBchatException if request failed
"""
threads = self.fetchThreadInfo(*page_ids)
pages = {}
for id_, thread in threads.items():
if thread.type == ThreadType.PAGE:
pages[id_] = thread
else:
raise FBchatUserError("Thread {} was not a page".format(thread))
return pages
[docs] def fetchGroupInfo(self, *group_ids):
"""
Get groups' info from IDs, unordered
:param group_ids: One or more group ID(s) to query
:return: :class:`Group` objects, labeled by their ID
:rtype: dict
:raises: FBchatException if request failed
"""
threads = self.fetchThreadInfo(*group_ids)
groups = {}
for id_, thread in threads.items():
if thread.type == ThreadType.GROUP:
groups[id_] = thread
else:
raise FBchatUserError("Thread {} was not a group".format(thread))
return groups
[docs] def fetchThreadInfo(self, *thread_ids):
"""
Get threads' info from IDs, unordered
.. warning::
Sends two requests if users or pages are present, to fetch all available info!
:param thread_ids: One or more thread ID(s) to query
:return: :class:`Thread` objects, labeled by their ID
:rtype: dict
:raises: FBchatException if request failed
"""
queries = []
for thread_id in thread_ids:
params = {
"id": thread_id,
"message_limit": 0,
"load_messages": False,
"load_read_receipts": False,
"before": None,
}
queries.append(_graphql.from_doc_id("2147762685294928", params))
j = self.graphql_requests(*queries)
for i, entry in enumerate(j):
if entry.get("message_thread") is None:
# If you don't have an existing thread with this person, attempt to retrieve user data anyways
j[i]["message_thread"] = {
"thread_key": {"other_user_id": thread_ids[i]},
"thread_type": "ONE_TO_ONE",
}
pages_and_user_ids = [
k["message_thread"]["thread_key"]["other_user_id"]
for k in j
if k["message_thread"].get("thread_type") == "ONE_TO_ONE"
]
pages_and_users = {}
if len(pages_and_user_ids) != 0:
pages_and_users = self._fetchInfo(*pages_and_user_ids)
rtn = {}
for i, entry in enumerate(j):
entry = entry["message_thread"]
if entry.get("thread_type") == "GROUP":
_id = entry["thread_key"]["thread_fbid"]
rtn[_id] = Group._from_graphql(entry)
elif entry.get("thread_type") == "ONE_TO_ONE":
_id = entry["thread_key"]["other_user_id"]
if pages_and_users.get(_id) is None:
raise FBchatException("Could not fetch thread {}".format(_id))
entry.update(pages_and_users[_id])
if entry["type"] == ThreadType.USER:
rtn[_id] = User._from_graphql(entry)
else:
rtn[_id] = Page._from_graphql(entry)
else:
raise FBchatException(
"{} had an unknown thread type: {}".format(thread_ids[i], entry)
)
return rtn
[docs] def fetchThreadMessages(self, thread_id=None, limit=20, before=None):
"""
Get the last messages in a thread
:param thread_id: User/Group ID to get messages from. See :ref:`intro_threads`
:param limit: Max. number of messages to retrieve
:param before: A timestamp, indicating from which point to retrieve messages
:type limit: int
:type before: int
:return: :class:`Message` objects
:rtype: list
:raises: FBchatException if request failed
"""
thread_id, thread_type = self._getThread(thread_id, None)
params = {
"id": thread_id,
"message_limit": limit,
"load_messages": True,
"load_read_receipts": True,
"before": before,
}
j = self.graphql_request(_graphql.from_doc_id("1860982147341344", params))
if j.get("message_thread") is None:
raise FBchatException("Could not fetch thread {}: {}".format(thread_id, j))
messages = [
Message._from_graphql(message)
for message in j["message_thread"]["messages"]["nodes"]
]
messages.reverse()
read_receipts = j["message_thread"]["read_receipts"]["nodes"]
for message in messages:
for receipt in read_receipts:
if int(receipt["watermark"]) >= int(message.timestamp):
message.read_by.append(receipt["actor"]["id"])
return messages
[docs] def fetchThreadList(
self, offset=None, limit=20, thread_location=ThreadLocation.INBOX, before=None
):
"""Get thread list of your facebook account
:param offset: Deprecated. Do not use!
:param limit: Max. number of threads to retrieve. Capped at 20
:param thread_location: ThreadLocation: INBOX, PENDING, ARCHIVED or OTHER
:param before: A timestamp (in milliseconds), indicating from which point to retrieve threads
:type limit: int
:type before: int
:return: :class:`Thread` objects
:rtype: list
:raises: FBchatException if request failed
"""
if offset is not None:
log.warning(
"Using `offset` in `fetchThreadList` is no longer supported, "
"since Facebook migrated to the use of GraphQL in this request. "
"Use `before` instead."
)
if limit > 20 or limit < 1:
raise FBchatUserError("`limit` should be between 1 and 20")
if thread_location in ThreadLocation:
loc_str = thread_location.value
else:
raise FBchatUserError('"thread_location" must be a value of ThreadLocation')
params = {
"limit": limit,
"tags": [loc_str],
"before": before,
"includeDeliveryReceipts": True,
"includeSeqID": False,
}
j = self.graphql_request(_graphql.from_doc_id("1349387578499440", params))
rtn = []
for node in j["viewer"]["message_threads"]["nodes"]:
_type = node.get("thread_type")
if _type == "GROUP":
rtn.append(Group._from_graphql(node))
elif _type == "ONE_TO_ONE":
rtn.append(User._from_thread_fetch(node))
else:
raise FBchatException(
"Unknown thread type: {}, with data: {}".format(_type, node)
)
return rtn
[docs] def fetchUnread(self):
"""
Get the unread thread list
:return: List of unread thread ids
:rtype: list
:raises: FBchatException if request failed
"""
form = {
"folders[0]": "inbox",
"client": "mercury",
"last_action_timestamp": now() - 60 * 1000
# 'last_action_timestamp': 0
}
j = self._payload_post("/ajax/mercury/unread_threads.php", form)
result = j["unread_thread_fbids"][0]
return result["thread_fbids"] + result["other_user_fbids"]
[docs] def fetchUnseen(self):
"""
Get the unseen (new) thread list
:return: List of unseen thread ids
:rtype: list
:raises: FBchatException if request failed
"""
j = self._payload_post("/mercury/unseen_thread_ids/", None)
result = j["unseen_thread_fbids"][0]
return result["thread_fbids"] + result["other_user_fbids"]
[docs] def fetchImageUrl(self, image_id):
"""Fetches the url to the original image from an image attachment ID
:param image_id: The image you want to fethc
:type image_id: str
:return: An url where you can download the original image
:rtype: str
:raises: FBchatException if request failed
"""
image_id = str(image_id)
data = {"photo_id": str(image_id)}
j = self._post("/mercury/attachments/photo/", data)
url = get_jsmods_require(j, 3)
if url is None:
raise FBchatException("Could not fetch image url from: {}".format(j))
return url
[docs] def fetchMessageInfo(self, mid, thread_id=None):
"""
Fetches :class:`Message` object from the message id
:param mid: Message ID to fetch from
:param thread_id: User/Group ID to get message info from. See :ref:`intro_threads`
:return: :class:`Message` object
:rtype: Message
:raises: FBchatException if request failed
"""
thread_id, thread_type = self._getThread(thread_id, None)
message_info = self._forcedFetch(thread_id, mid).get("message")
return Message._from_graphql(message_info)
[docs] def fetchPollOptions(self, poll_id):
"""
Fetches list of :class:`PollOption` objects from the poll id
:param poll_id: Poll ID to fetch from
:rtype: list
:raises: FBchatException if request failed
"""
data = {"question_id": poll_id}
j = self._payload_post("/ajax/mercury/get_poll_options", data)
return [PollOption._from_graphql(m) for m in j]
[docs] def fetchPlanInfo(self, plan_id):
"""
Fetches a :class:`Plan` object from the plan id
:param plan_id: Plan ID to fetch from
:return: :class:`Plan` object
:rtype: Plan
:raises: FBchatException if request failed
"""
data = {"event_reminder_id": plan_id}
j = self._payload_post("/ajax/eventreminder", data)
return Plan._from_fetch(j)
def _getPrivateData(self):
j = self.graphql_request(_graphql.from_doc_id("1868889766468115", {}))
return j["viewer"]
[docs] def getPhoneNumbers(self):
"""
Fetches a list of user phone numbers.
:return: List of phone numbers
:rtype: list
"""
data = self._getPrivateData()
return [
j["phone_number"]["universal_number"] for j in data["user"]["all_phones"]
]
[docs] def getEmails(self):
"""
Fetches a list of user emails.
:return: List of emails
:rtype: list
"""
data = self._getPrivateData()
return [j["display_email"] for j in data["all_emails"]]
[docs] def getUserActiveStatus(self, user_id):
"""
Gets friend active status as an :class:`ActiveStatus` object.
Returns ``None`` if status isn't known.
.. warning::
Only works when listening.
:param user_id: ID of the user
:return: Given user active status
:rtype: ActiveStatus
"""
return self._buddylist.get(str(user_id))
"""
END FETCH METHODS
"""
"""
SEND METHODS
"""
def _oldMessage(self, message):
return message if isinstance(message, Message) else Message(text=message)
def _getSendData(self, message=None, thread_id=None, thread_type=ThreadType.USER):
"""Returns the data needed to send a request to `SendURL`"""
messageAndOTID = generateOfflineThreadingID()
timestamp = now()
data = {
"client": "mercury",
"author": "fbid:{}".format(self._uid),
"timestamp": timestamp,
"source": "source:chat:web",
"offline_threading_id": messageAndOTID,
"message_id": messageAndOTID,
"threading_id": generateMessageID(self._client_id),
"ephemeral_ttl_mode:": "0",
}
# Set recipient
if thread_type in [ThreadType.USER, ThreadType.PAGE]:
data["other_user_fbid"] = thread_id
elif thread_type == ThreadType.GROUP:
data["thread_fbid"] = thread_id
if message is None:
message = Message()
if message.text or message.sticker or message.emoji_size:
data["action_type"] = "ma-type:user-generated-message"
if message.text:
data["body"] = message.text
for i, mention in enumerate(message.mentions):
data["profile_xmd[{}][id]".format(i)] = mention.thread_id
data["profile_xmd[{}][offset]".format(i)] = mention.offset
data["profile_xmd[{}][length]".format(i)] = mention.length
data["profile_xmd[{}][type]".format(i)] = "p"
if message.emoji_size:
if message.text:
data["tags[0]"] = "hot_emoji_size:" + message.emoji_size.name.lower()
else:
data["sticker_id"] = message.emoji_size.value
if message.sticker:
data["sticker_id"] = message.sticker.uid
if message.quick_replies:
xmd = {"quick_replies": []}
for quick_reply in message.quick_replies:
q = dict()
q["content_type"] = quick_reply._type
q["payload"] = quick_reply.payload
q["external_payload"] = quick_reply.external_payload
q["data"] = quick_reply.data
if quick_reply.is_response:
q["ignore_for_webhook"] = False
if isinstance(quick_reply, QuickReplyText):
q["title"] = quick_reply.title
if not isinstance(quick_reply, QuickReplyLocation):
q["image_url"] = quick_reply.image_url
xmd["quick_replies"].append(q)
if len(message.quick_replies) == 1 and message.quick_replies[0].is_response:
xmd["quick_replies"] = xmd["quick_replies"][0]
data["platform_xmd"] = json.dumps(xmd)
if message.reply_to_id:
data["replied_to_message_id"] = message.reply_to_id
return data
def _doSendRequest(self, data, get_thread_id=False):
"""Sends the data to `SendURL`, and returns the message ID or None on failure"""
j = self._post("/messaging/send/", data)
# update JS token if received in response
fb_dtsg = get_jsmods_require(j, 2)
if fb_dtsg is not None:
self._state.fb_dtsg = fb_dtsg
try:
message_ids = [
(action["message_id"], action["thread_fbid"])
for action in j["payload"]["actions"]
if "message_id" in action
]
if len(message_ids) != 1:
log.warning("Got multiple message ids' back: {}".format(message_ids))
if get_thread_id:
return message_ids[0]
else:
return message_ids[0][0]
except (KeyError, IndexError, TypeError) as e:
raise FBchatException(
"Error when sending message: "
"No message IDs could be found: {}".format(j)
)
[docs] def send(self, message, thread_id=None, thread_type=ThreadType.USER):
"""
Sends a message to a thread
:param message: Message to send
:param thread_id: User/Group ID to send to. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_threads`
:type message: Message
:type thread_type: ThreadType
:return: :ref:`Message ID <intro_message_ids>` of the sent message
:raises: FBchatException if request failed
"""
thread_id, thread_type = self._getThread(thread_id, thread_type)
data = self._getSendData(
message=message, thread_id=thread_id, thread_type=thread_type
)
return self._doSendRequest(data)
[docs] def sendMessage(self, message, thread_id=None, thread_type=ThreadType.USER):
"""
Deprecated. Use :func:`fbchat.Client.send` instead
"""
return self.send(
Message(text=message), thread_id=thread_id, thread_type=thread_type
)
[docs] def sendEmoji(
self,
emoji=None,
size=EmojiSize.SMALL,
thread_id=None,
thread_type=ThreadType.USER,
):
"""
Deprecated. Use :func:`fbchat.Client.send` instead
"""
return self.send(
Message(text=emoji, emoji_size=size),
thread_id=thread_id,
thread_type=thread_type,
)
[docs] def wave(self, wave_first=True, thread_id=None, thread_type=None):
"""
Says hello with a wave to a thread!
:param wave_first: Whether to wave first or wave back
:param thread_id: User/Group ID to send to. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_threads`
:type thread_type: ThreadType
:return: :ref:`Message ID <intro_message_ids>` of the sent message
:raises: FBchatException if request failed
"""
thread_id, thread_type = self._getThread(thread_id, thread_type)
data = self._getSendData(thread_id=thread_id, thread_type=thread_type)
data["action_type"] = "ma-type:user-generated-message"
data["lightweight_action_attachment[lwa_state]"] = (
"INITIATED" if wave_first else "RECIPROCATED"
)
data["lightweight_action_attachment[lwa_type]"] = "WAVE"
if thread_type == ThreadType.USER:
data["specific_to_list[0]"] = "fbid:{}".format(thread_id)
return self._doSendRequest(data)
[docs] def quickReply(self, quick_reply, payload=None, thread_id=None, thread_type=None):
"""
Replies to a chosen quick reply
:param quick_reply: Quick reply to reply to
:param payload: Optional answer to the quick reply
:param thread_id: User/Group ID to send to. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_threads`
:type quick_reply: QuickReply
:type thread_type: ThreadType
:return: :ref:`Message ID <intro_message_ids>` of the sent message
:raises: FBchatException if request failed
"""
quick_reply.is_response = True
if isinstance(quick_reply, QuickReplyText):
return self.send(
Message(text=quick_reply.title, quick_replies=[quick_reply])
)
elif isinstance(quick_reply, QuickReplyLocation):
if not isinstance(payload, LocationAttachment):
raise ValueError(
"Payload must be an instance of `fbchat.LocationAttachment`"
)
return self.sendLocation(
payload, thread_id=thread_id, thread_type=thread_type
)
elif isinstance(quick_reply, QuickReplyEmail):
if not payload:
payload = self.getEmails()[0]
quick_reply.external_payload = quick_reply.payload
quick_reply.payload = payload
return self.send(Message(text=payload, quick_replies=[quick_reply]))
elif isinstance(quick_reply, QuickReplyPhoneNumber):
if not payload:
payload = self.getPhoneNumbers()[0]
quick_reply.external_payload = quick_reply.payload
quick_reply.payload = payload
return self.send(Message(text=payload, quick_replies=[quick_reply]))
[docs] def unsend(self, mid):
"""
Unsends a message (removes for everyone)
:param mid: :ref:`Message ID <intro_message_ids>` of the message to unsend
"""
data = {"message_id": mid}
j = self._payload_post("/messaging/unsend_message/?dpr=1", data)
def _sendLocation(
self, location, current=True, message=None, thread_id=None, thread_type=None
):
thread_id, thread_type = self._getThread(thread_id, thread_type)
data = self._getSendData(
message=message, thread_id=thread_id, thread_type=thread_type
)
data["action_type"] = "ma-type:user-generated-message"
data["location_attachment[coordinates][latitude]"] = location.latitude
data["location_attachment[coordinates][longitude]"] = location.longitude
data["location_attachment[is_current_location]"] = current
return self._doSendRequest(data)
[docs] def sendLocation(self, location, message=None, thread_id=None, thread_type=None):
"""
Sends a given location to a thread as the user's current location
:param location: Location to send
:param message: Additional message
:param thread_id: User/Group ID to send to. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_threads`
:type location: LocationAttachment
:type message: Message
:type thread_type: ThreadType
:return: :ref:`Message ID <intro_message_ids>` of the sent message
:raises: FBchatException if request failed
"""
self._sendLocation(
location=location,
current=True,
message=message,
thread_id=thread_id,
thread_type=thread_type,
)
[docs] def sendPinnedLocation(
self, location, message=None, thread_id=None, thread_type=None
):
"""
Sends a given location to a thread as a pinned location
:param location: Location to send
:param message: Additional message
:param thread_id: User/Group ID to send to. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_threads`
:type location: LocationAttachment
:type message: Message
:type thread_type: ThreadType
:return: :ref:`Message ID <intro_message_ids>` of the sent message
:raises: FBchatException if request failed
"""
self._sendLocation(
location=location,
current=False,
message=message,
thread_id=thread_id,
thread_type=thread_type,
)
def _upload(self, files, voice_clip=False):
"""
Uploads files to Facebook
`files` should be a list of files that requests can upload, see:
http://docs.python-requests.org/en/master/api/#requests.request
Returns a list of tuples with a file's ID and mimetype
"""
file_dict = {"upload_{}".format(i): f for i, f in enumerate(files)}
data = {"voice_clip": voice_clip}
j = self._payload_post(
"https://upload.facebook.com/ajax/mercury/upload.php", data, files=file_dict
)
if len(j["metadata"]) != len(files):
raise FBchatException(
"Some files could not be uploaded: {}, {}".format(j, files)
)
return [
(data[mimetype_to_key(data["filetype"])], data["filetype"])
for data in j["metadata"]
]
def _sendFiles(
self, files, message=None, thread_id=None, thread_type=ThreadType.USER
):
"""
Sends files from file IDs to a thread
`files` should be a list of tuples, with a file's ID and mimetype
"""
thread_id, thread_type = self._getThread(thread_id, thread_type)
data = self._getSendData(
message=self._oldMessage(message),
thread_id=thread_id,
thread_type=thread_type,
)
data["action_type"] = "ma-type:user-generated-message"
data["has_attachment"] = True
for i, (file_id, mimetype) in enumerate(files):
data["{}s[{}]".format(mimetype_to_key(mimetype), i)] = file_id
return self._doSendRequest(data)
[docs] def sendRemoteFiles(
self, file_urls, message=None, thread_id=None, thread_type=ThreadType.USER
):
"""
Sends files from URLs to a thread
:param file_urls: URLs of files to upload and send
:param message: Additional message
:param thread_id: User/Group ID to send to. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_threads`
:type thread_type: ThreadType
:return: :ref:`Message ID <intro_message_ids>` of the sent files
:raises: FBchatException if request failed
"""
file_urls = require_list(file_urls)
files = self._upload(get_files_from_urls(file_urls))
return self._sendFiles(
files=files, message=message, thread_id=thread_id, thread_type=thread_type
)
[docs] def sendLocalFiles(
self, file_paths, message=None, thread_id=None, thread_type=ThreadType.USER
):
"""
Sends local files to a thread
:param file_paths: Paths of files to upload and send
:param message: Additional message
:param thread_id: User/Group ID to send to. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_threads`
:type thread_type: ThreadType
:return: :ref:`Message ID <intro_message_ids>` of the sent files
:raises: FBchatException if request failed
"""
file_paths = require_list(file_paths)
with get_files_from_paths(file_paths) as x:
files = self._upload(x)
return self._sendFiles(
files=files, message=message, thread_id=thread_id, thread_type=thread_type
)
[docs] def sendRemoteVoiceClips(
self, clip_urls, message=None, thread_id=None, thread_type=ThreadType.USER
):
"""
Sends voice clips from URLs to a thread
:param clip_urls: URLs of clips to upload and send
:param message: Additional message
:param thread_id: User/Group ID to send to. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_threads`
:type thread_type: ThreadType
:return: :ref:`Message ID <intro_message_ids>` of the sent files
:raises: FBchatException if request failed
"""
clip_urls = require_list(clip_urls)
files = self._upload(get_files_from_urls(clip_urls), voice_clip=True)
return self._sendFiles(
files=files, message=message, thread_id=thread_id, thread_type=thread_type
)
[docs] def sendLocalVoiceClips(
self, clip_paths, message=None, thread_id=None, thread_type=ThreadType.USER
):
"""
Sends local voice clips to a thread
:param clip_paths: Paths of clips to upload and send
:param message: Additional message
:param thread_id: User/Group ID to send to. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_threads`
:type thread_type: ThreadType
:return: :ref:`Message ID <intro_message_ids>` of the sent files
:raises: FBchatException if request failed
"""
clip_paths = require_list(clip_paths)
with get_files_from_paths(clip_paths) as x:
files = self._upload(x, voice_clip=True)
return self._sendFiles(
files=files, message=message, thread_id=thread_id, thread_type=thread_type
)
[docs] def sendImage(
self,
image_id,
message=None,
thread_id=None,
thread_type=ThreadType.USER,
is_gif=False,
):
"""Deprecated."""
if is_gif:
mimetype = "image/gif"
else:
mimetype = "image/png"
return self._sendFiles(
files=[(image_id, mimetype)],
message=message,
thread_id=thread_id,
thread_type=thread_type,
)
[docs] def sendRemoteImage(
self, image_url, message=None, thread_id=None, thread_type=ThreadType.USER
):
"""
Deprecated. Use :func:`fbchat.Client.sendRemoteFiles` instead
"""
return self.sendRemoteFiles(
file_urls=[image_url],
message=message,
thread_id=thread_id,
thread_type=thread_type,
)
[docs] def sendLocalImage(
self, image_path, message=None, thread_id=None, thread_type=ThreadType.USER
):
"""
Deprecated. Use :func:`fbchat.Client.sendLocalFiles` instead
"""
return self.sendLocalFiles(
file_paths=[image_path],
message=message,
thread_id=thread_id,
thread_type=thread_type,
)
[docs] def forwardAttachment(self, attachment_id, thread_id=None):
"""
Forwards an attachment
:param attachment_id: Attachment ID to forward
:param thread_id: User/Group ID to send to. See :ref:`intro_threads`
:raises: FBchatException if request failed
"""
thread_id, thread_type = self._getThread(thread_id, None)
data = {
"attachment_id": attachment_id,
"recipient_map[{}]".format(generateOfflineThreadingID()): thread_id,
}
j = self._payload_post("/mercury/attachments/forward/", data)
if not j.get("success"):
raise FBchatFacebookError(
"Failed forwarding attachment: {}".format(j["error"]),
fb_error_message=j["error"],
)
[docs] def createGroup(self, message, user_ids):
"""
Creates a group with the given ids
:param message: The initial message
:param user_ids: A list of users to create the group with.
:return: ID of the new group
:raises: FBchatException if request failed
"""
data = self._getSendData(message=self._oldMessage(message))
if len(user_ids) < 2:
raise FBchatUserError("Error when creating group: Not enough participants")
for i, user_id in enumerate(user_ids + [self._uid]):
data["specific_to_list[{}]".format(i)] = "fbid:{}".format(user_id)
message_id, thread_id = self._doSendRequest(data, get_thread_id=True)
if not thread_id:
raise FBchatException(
"Error when creating group: No thread_id could be found"
)
return thread_id
[docs] def addUsersToGroup(self, user_ids, thread_id=None):
"""
Adds users to a group.
:param user_ids: One or more user IDs to add
:param thread_id: Group ID to add people to. See :ref:`intro_threads`
:type user_ids: list
:raises: FBchatException if request failed
"""
thread_id, thread_type = self._getThread(thread_id, None)
data = self._getSendData(thread_id=thread_id, thread_type=ThreadType.GROUP)
data["action_type"] = "ma-type:log-message"
data["log_message_type"] = "log:subscribe"
user_ids = require_list(user_ids)
for i, user_id in enumerate(user_ids):
if user_id == self._uid:
raise FBchatUserError(
"Error when adding users: Cannot add self to group thread"
)
else:
data[
"log_message_data[added_participants][{}]".format(i)
] = "fbid:{}".format(user_id)
return self._doSendRequest(data)
[docs] def removeUserFromGroup(self, user_id, thread_id=None):
"""
Removes users from a group.
:param user_id: User ID to remove
:param thread_id: Group ID to remove people from. See :ref:`intro_threads`
:raises: FBchatException if request failed
"""
thread_id, thread_type = self._getThread(thread_id, None)
data = {"uid": user_id, "tid": thread_id}
j = self._payload_post("/chat/remove_participants/", data)
def _adminStatus(self, admin_ids, admin, thread_id=None):
thread_id, thread_type = self._getThread(thread_id, None)
data = {"add": admin, "thread_fbid": thread_id}
admin_ids = require_list(admin_ids)
for i, admin_id in enumerate(admin_ids):
data["admin_ids[{}]".format(i)] = str(admin_id)
j = self._payload_post("/messaging/save_admins/?dpr=1", data)
[docs] def addGroupAdmins(self, admin_ids, thread_id=None):
"""
Sets specifed users as group admins.
:param admin_ids: One or more user IDs to set admin
:param thread_id: Group ID to remove people from. See :ref:`intro_threads`
:raises: FBchatException if request failed
"""
self._adminStatus(admin_ids, True, thread_id)
[docs] def removeGroupAdmins(self, admin_ids, thread_id=None):
"""
Removes admin status from specifed users.
:param admin_ids: One or more user IDs to remove admin
:param thread_id: Group ID to remove people from. See :ref:`intro_threads`
:raises: FBchatException if request failed
"""
self._adminStatus(admin_ids, False, thread_id)
[docs] def changeGroupApprovalMode(self, require_admin_approval, thread_id=None):
"""
Changes group's approval mode
:param require_admin_approval: True or False
:param thread_id: Group ID to remove people from. See :ref:`intro_threads`
:raises: FBchatException if request failed
"""
thread_id, thread_type = self._getThread(thread_id, None)
data = {"set_mode": int(require_admin_approval), "thread_fbid": thread_id}
j = self._payload_post("/messaging/set_approval_mode/?dpr=1", data)
def _usersApproval(self, user_ids, approve, thread_id=None):
thread_id, thread_type = self._getThread(thread_id, None)
user_ids = list(require_list(user_ids))
data = {
"client_mutation_id": "0",
"actor_id": self._uid,
"thread_fbid": thread_id,
"user_ids": user_ids,
"response": "ACCEPT" if approve else "DENY",
"surface": "ADMIN_MODEL_APPROVAL_CENTER",
}
j = self.graphql_request(
_graphql.from_doc_id("1574519202665847", {"data": data})
)
[docs] def acceptUsersToGroup(self, user_ids, thread_id=None):
"""
Accepts users to the group from the group's approval
:param user_ids: One or more user IDs to accept
:param thread_id: Group ID to accept users to. See :ref:`intro_threads`
:raises: FBchatException if request failed
"""
self._usersApproval(user_ids, True, thread_id)
[docs] def denyUsersFromGroup(self, user_ids, thread_id=None):
"""
Denies users from the group's approval
:param user_ids: One or more user IDs to deny
:param thread_id: Group ID to deny users from. See :ref:`intro_threads`
:raises: FBchatException if request failed
"""
self._usersApproval(user_ids, False, thread_id)
def _changeGroupImage(self, image_id, thread_id=None):
"""
Changes a thread image from an image id
:param image_id: ID of uploaded image
:param thread_id: User/Group ID to change image. See :ref:`intro_threads`
:raises: FBchatException if request failed
"""
thread_id, thread_type = self._getThread(thread_id, None)
data = {"thread_image_id": image_id, "thread_id": thread_id}
j = self._payload_post("/messaging/set_thread_image/?dpr=1", data)
return image_id
[docs] def changeGroupImageRemote(self, image_url, thread_id=None):
"""
Changes a thread image from a URL
:param image_url: URL of an image to upload and change
:param thread_id: User/Group ID to change image. See :ref:`intro_threads`
:raises: FBchatException if request failed
"""
(image_id, mimetype), = self._upload(get_files_from_urls([image_url]))
return self._changeGroupImage(image_id, thread_id)
[docs] def changeGroupImageLocal(self, image_path, thread_id=None):
"""
Changes a thread image from a local path
:param image_path: Path of an image to upload and change
:param thread_id: User/Group ID to change image. See :ref:`intro_threads`
:raises: FBchatException if request failed
"""
with get_files_from_paths([image_path]) as files:
(image_id, mimetype), = self._upload(files)
return self._changeGroupImage(image_id, thread_id)
[docs] def changeThreadTitle(self, title, thread_id=None, thread_type=ThreadType.USER):
"""
Changes title of a thread.
If this is executed on a user thread, this will change the nickname of that user, effectively changing the title
:param title: New group thread title
:param thread_id: Group ID to change title of. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_threads`
:type thread_type: ThreadType
:raises: FBchatException if request failed
"""
thread_id, thread_type = self._getThread(thread_id, thread_type)
if thread_type == ThreadType.USER:
# The thread is a user, so we change the user's nickname
return self.changeNickname(
title, thread_id, thread_id=thread_id, thread_type=thread_type
)
data = {"thread_name": title, "thread_id": thread_id}
j = self._payload_post("/messaging/set_thread_name/?dpr=1", data)
[docs] def changeNickname(
self, nickname, user_id, thread_id=None, thread_type=ThreadType.USER
):
"""
Changes the nickname of a user in a thread
:param nickname: New nickname
:param user_id: User that will have their nickname changed
:param thread_id: User/Group ID to change color of. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_threads`
:type thread_type: ThreadType
:raises: FBchatException if request failed
"""
thread_id, thread_type = self._getThread(thread_id, thread_type)
data = {
"nickname": nickname,
"participant_id": user_id,
"thread_or_other_fbid": thread_id,
}
j = self._payload_post(
"/messaging/save_thread_nickname/?source=thread_settings&dpr=1", data
)
[docs] def changeThreadColor(self, color, thread_id=None):
"""
Changes thread color
:param color: New thread color
:param thread_id: User/Group ID to change color of. See :ref:`intro_threads`
:type color: ThreadColor
:raises: FBchatException if request failed
"""
thread_id, thread_type = self._getThread(thread_id, None)
data = {
"color_choice": color.value if color != ThreadColor.MESSENGER_BLUE else "",
"thread_or_other_fbid": thread_id,
}
j = self._payload_post(
"/messaging/save_thread_color/?source=thread_settings&dpr=1", data
)
[docs] def changeThreadEmoji(self, emoji, thread_id=None):
"""
Changes thread color
Trivia: While changing the emoji, the Facebook web client actually sends multiple different requests, though only this one is required to make the change
:param color: New thread emoji
:param thread_id: User/Group ID to change emoji of. See :ref:`intro_threads`
:raises: FBchatException if request failed
"""
thread_id, thread_type = self._getThread(thread_id, None)
data = {"emoji_choice": emoji, "thread_or_other_fbid": thread_id}
j = self._payload_post(
"/messaging/save_thread_emoji/?source=thread_settings&dpr=1", data
)
[docs] def reactToMessage(self, message_id, reaction):
"""
Reacts to a message, or removes reaction
:param message_id: :ref:`Message ID <intro_message_ids>` to react to
:param reaction: Reaction emoji to use, if None removes reaction
:type reaction: MessageReaction or None
:raises: FBchatException if request failed
"""
data = {
"action": "ADD_REACTION" if reaction else "REMOVE_REACTION",
"client_mutation_id": "1",
"actor_id": self._uid,
"message_id": str(message_id),
"reaction": reaction.value if reaction else None,
}
data = {"doc_id": 1491398900900362, "variables": json.dumps({"data": data})}
j = self._payload_post("/webgraphql/mutation", data)
handle_graphql_errors(j)
[docs] def createPlan(self, plan, thread_id=None):
"""
Sets a plan
:param plan: Plan to set
:param thread_id: User/Group ID to send plan to. See :ref:`intro_threads`
:type plan: Plan
:raises: FBchatException if request failed
"""
thread_id, thread_type = self._getThread(thread_id, None)
data = {
"event_type": "EVENT",
"event_time": plan.time,
"title": plan.title,
"thread_id": thread_id,
"location_id": plan.location_id or "",
"location_name": plan.location or "",
"acontext": ACONTEXT,
}
j = self._payload_post("/ajax/eventreminder/create", data)
if "error" in j:
raise FBchatFacebookError(
"Failed creating plan: {}".format(j["error"]),
fb_error_message=j["error"],
)
[docs] def editPlan(self, plan, new_plan):
"""
Edits a plan
:param plan: Plan to edit
:param new_plan: New plan
:type plan: Plan
:raises: FBchatException if request failed
"""
data = {
"event_reminder_id": plan.uid,
"delete": "false",
"date": new_plan.time,
"location_name": new_plan.location or "",
"location_id": new_plan.location_id or "",
"title": new_plan.title,
"acontext": ACONTEXT,
}
j = self._payload_post("/ajax/eventreminder/submit", data)
[docs] def deletePlan(self, plan):
"""
Deletes a plan
:param plan: Plan to delete
:raises: FBchatException if request failed
"""
data = {"event_reminder_id": plan.uid, "delete": "true", "acontext": ACONTEXT}
j = self._payload_post("/ajax/eventreminder/submit", data)
[docs] def changePlanParticipation(self, plan, take_part=True):
"""
Changes participation in a plan
:param plan: Plan to take part in or not
:param take_part: Whether to take part in the plan
:raises: FBchatException if request failed
"""
data = {
"event_reminder_id": plan.uid,
"guest_state": "GOING" if take_part else "DECLINED",
"acontext": ACONTEXT,
}
j = self._payload_post("/ajax/eventreminder/rsvp", data)
[docs] def eventReminder(self, thread_id, time, title, location="", location_id=""):
"""
Deprecated. Use :func:`fbchat.Client.createPlan` instead
"""
plan = Plan(time=time, title=title, location=location, location_id=location_id)
self.createPlan(plan=plan, thread_id=thread_id)
[docs] def createPoll(self, poll, thread_id=None):
"""
Creates poll in a group thread
:param poll: Poll to create
:param thread_id: User/Group ID to create poll in. See :ref:`intro_threads`
:type poll: Poll
:raises: FBchatException if request failed
"""
thread_id, thread_type = self._getThread(thread_id, None)
# We're using ordered dicts, because the Facebook endpoint that parses the POST
# parameters is badly implemented, and deals with ordering the options wrongly.
# If you can find a way to fix this for the endpoint, or if you find another
# endpoint, please do suggest it ;)
data = OrderedDict([("question_text", poll.title), ("target_id", thread_id)])
for i, option in enumerate(poll.options):
data["option_text_array[{}]".format(i)] = option.text
data["option_is_selected_array[{}]".format(i)] = str(int(option.vote))
j = self._payload_post("/messaging/group_polling/create_poll/?dpr=1", data)
if j.get("status") != "success":
raise FBchatFacebookError(
"Failed creating poll: {}".format(j.get("errorTitle")),
fb_error_message=j.get("errorMessage"),
)
[docs] def updatePollVote(self, poll_id, option_ids=[], new_options=[]):
"""
Updates a poll vote
:param poll_id: ID of the poll to update vote
:param option_ids: List of the option IDs to vote
:param new_options: List of the new option names
:param thread_id: User/Group ID to change status in. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_threads`
:type thread_type: ThreadType
:raises: FBchatException if request failed
"""
data = {"question_id": poll_id}
for i, option_id in enumerate(option_ids):
data["selected_options[{}]".format(i)] = option_id
for i, option_text in enumerate(new_options):
data["new_options[{}]".format(i)] = option_text
j = self._payload_post("/messaging/group_polling/update_vote/?dpr=1", data)
if j.get("status") != "success":
raise FBchatFacebookError(
"Failed updating poll vote: {}".format(j.get("errorTitle")),
fb_error_message=j.get("errorMessage"),
)
[docs] def setTypingStatus(self, status, thread_id=None, thread_type=None):
"""
Sets users typing status in a thread
:param status: Specify the typing status
:param thread_id: User/Group ID to change status in. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_threads`
:type status: TypingStatus
:type thread_type: ThreadType
:raises: FBchatException if request failed
"""
thread_id, thread_type = self._getThread(thread_id, thread_type)
data = {
"typ": status.value,
"thread": thread_id,
"to": thread_id if thread_type == ThreadType.USER else "",
"source": "mercury-chat",
}
j = self._payload_post("/ajax/messaging/typ.php", data)
"""
END SEND METHODS
"""
[docs] def markAsDelivered(self, thread_id, message_id):
"""
Mark a message as delivered
:param thread_id: User/Group ID to which the message belongs. See :ref:`intro_threads`
:param message_id: Message ID to set as delivered. See :ref:`intro_threads`
:return: True
:raises: FBchatException if request failed
"""
data = {
"message_ids[0]": message_id,
"thread_ids[%s][0]" % thread_id: message_id,
}
j = self._payload_post("/ajax/mercury/delivery_receipts.php", data)
return True
def _readStatus(self, read, thread_ids):
thread_ids = require_list(thread_ids)
data = {"watermarkTimestamp": now(), "shouldSendReadReceipt": "true"}
for thread_id in thread_ids:
data["ids[{}]".format(thread_id)] = "true" if read else "false"
j = self._payload_post("/ajax/mercury/change_read_status.php", data)
[docs] def markAsRead(self, thread_ids=None):
"""
Mark threads as read
All messages inside the threads will be marked as read
:param thread_ids: User/Group IDs to set as read. See :ref:`intro_threads`
:raises: FBchatException if request failed
"""
self._readStatus(True, thread_ids)
[docs] def markAsUnread(self, thread_ids=None):
"""
Mark threads as unread
All messages inside the threads will be marked as unread
:param thread_ids: User/Group IDs to set as unread. See :ref:`intro_threads`
:raises: FBchatException if request failed
"""
self._readStatus(False, thread_ids)
[docs] def markAsSeen(self):
"""
.. todo::
Documenting this
"""
j = self._payload_post("/ajax/mercury/mark_seen.php", {"seen_timestamp": now()})
[docs] def friendConnect(self, friend_id):
"""
.. todo::
Documenting this
"""
data = {"to_friend": friend_id, "action": "confirm"}
j = self._payload_post("/ajax/add_friend/action.php?dpr=1", data)
[docs] def removeFriend(self, friend_id=None):
"""
Removes a specifed friend from your friend list
:param friend_id: The ID of the friend that you want to remove
:return: True
:raises: FBchatException if request failed
"""
data = {"uid": friend_id}
j = self._payload_post("/ajax/profile/removefriendconfirm.php", data)
return True
[docs] def blockUser(self, user_id):
"""
Blocks messages from a specifed user
:param user_id: The ID of the user that you want to block
:return: True
:raises: FBchatException if request failed
"""
data = {"fbid": user_id}
j = self._payload_post("/messaging/block_messages/?dpr=1", data)
return True
[docs] def unblockUser(self, user_id):
"""
Unblocks messages from a blocked user
:param user_id: The ID of the user that you want to unblock
:return: Whether the request was successful
:raises: FBchatException if request failed
"""
data = {"fbid": user_id}
j = self._payload_post("/messaging/unblock_messages/?dpr=1", data)
return True
[docs] def moveThreads(self, location, thread_ids):
"""
Moves threads to specifed location
:param location: ThreadLocation: INBOX, PENDING, ARCHIVED or OTHER
:param thread_ids: Thread IDs to move. See :ref:`intro_threads`
:return: True
:raises: FBchatException if request failed
"""
thread_ids = require_list(thread_ids)
if location == ThreadLocation.PENDING:
location = ThreadLocation.OTHER
if location == ThreadLocation.ARCHIVED:
data_archive = dict()
data_unpin = dict()
for thread_id in thread_ids:
data_archive["ids[{}]".format(thread_id)] = "true"
data_unpin["ids[{}]".format(thread_id)] = "false"
j_archive = self._payload_post(
"/ajax/mercury/change_archived_status.php?dpr=1", data_archive
)
j_unpin = self._payload_post(
"/ajax/mercury/change_pinned_status.php?dpr=1", data_unpin
)
else:
data = dict()
for i, thread_id in enumerate(thread_ids):
data["{}[{}]".format(location.name.lower(), i)] = thread_id
j = self._payload_post("/ajax/mercury/move_thread.php", data)
return True
[docs] def deleteThreads(self, thread_ids):
"""
Deletes threads
:param thread_ids: Thread IDs to delete. See :ref:`intro_threads`
:return: True
:raises: FBchatException if request failed
"""
thread_ids = require_list(thread_ids)
data_unpin = dict()
data_delete = dict()
for i, thread_id in enumerate(thread_ids):
data_unpin["ids[{}]".format(thread_id)] = "false"
data_delete["ids[{}]".format(i)] = thread_id
j_unpin = self._payload_post(
"/ajax/mercury/change_pinned_status.php?dpr=1", data_unpin
)
j_delete = self._payload_post(
"/ajax/mercury/delete_thread.php?dpr=1", data_delete
)
return True
[docs] def markAsSpam(self, thread_id=None):
"""
Mark a thread as spam and delete it
:param thread_id: User/Group ID to mark as spam. See :ref:`intro_threads`
:return: True
:raises: FBchatException if request failed
"""
thread_id, thread_type = self._getThread(thread_id, None)
j = self._payload_post("/ajax/mercury/mark_spam.php?dpr=1", {"id": thread_id})
return True
[docs] def deleteMessages(self, message_ids):
"""
Deletes specifed messages
:param message_ids: Message IDs to delete
:return: True
:raises: FBchatException if request failed
"""
message_ids = require_list(message_ids)
data = dict()
for i, message_id in enumerate(message_ids):
data["message_ids[{}]".format(i)] = message_id
j = self._payload_post("/ajax/mercury/delete_messages.php?dpr=1", data)
return True
[docs] def muteThread(self, mute_time=-1, thread_id=None):
"""
Mutes thread
:param mute_time: Mute time in seconds, leave blank to mute forever
:param thread_id: User/Group ID to mute. See :ref:`intro_threads`
"""
thread_id, thread_type = self._getThread(thread_id, None)
data = {"mute_settings": str(mute_time), "thread_fbid": thread_id}
j = self._payload_post("/ajax/mercury/change_mute_thread.php?dpr=1", data)
[docs] def unmuteThread(self, thread_id=None):
"""
Unmutes thread
:param thread_id: User/Group ID to unmute. See :ref:`intro_threads`
"""
return self.muteThread(0, thread_id)
[docs] def muteThreadReactions(self, mute=True, thread_id=None):
"""
Mutes thread reactions
:param mute: Boolean. True to mute, False to unmute
:param thread_id: User/Group ID to mute. See :ref:`intro_threads`
"""
thread_id, thread_type = self._getThread(thread_id, None)
data = {"reactions_mute_mode": int(mute), "thread_fbid": thread_id}
j = self._payload_post(
"/ajax/mercury/change_reactions_mute_thread/?dpr=1", data
)
[docs] def unmuteThreadReactions(self, thread_id=None):
"""
Unmutes thread reactions
:param thread_id: User/Group ID to unmute. See :ref:`intro_threads`
"""
return self.muteThreadReactions(False, thread_id)
[docs] def muteThreadMentions(self, mute=True, thread_id=None):
"""
Mutes thread mentions
:param mute: Boolean. True to mute, False to unmute
:param thread_id: User/Group ID to mute. See :ref:`intro_threads`
"""
thread_id, thread_type = self._getThread(thread_id, None)
data = {"mentions_mute_mode": int(mute), "thread_fbid": thread_id}
j = self._payload_post("/ajax/mercury/change_mentions_mute_thread/?dpr=1", data)
[docs] def unmuteThreadMentions(self, thread_id=None):
"""
Unmutes thread mentions
:param thread_id: User/Group ID to unmute. See :ref:`intro_threads`
"""
return self.muteThreadMentions(False, thread_id)
"""
LISTEN METHODS
"""
def _ping(self):
data = {
"seq": self._seq,
"channel": "p_" + self._uid,
"clientid": self._client_id,
"partition": -2,
"cap": 0,
"uid": self._uid,
"sticky_token": self._sticky,
"sticky_pool": self._pool,
"viewer_uid": self._uid,
"state": "active",
}
j = self._get(
"https://{}-edge-chat.facebook.com/active_ping".format(self._pull_channel),
data,
)
def _pullMessage(self):
"""Call pull api with seq value to get message data."""
data = {
"seq": self._seq,
"msgs_recv": 0,
"sticky_token": self._sticky,
"sticky_pool": self._pool,
"clientid": self._client_id,
"state": "active" if self._markAlive else "offline",
}
return self._get(
"https://{}-edge-chat.facebook.com/pull".format(self._pull_channel), data
)
def _parseDelta(self, m):
def getThreadIdAndThreadType(msg_metadata):
"""Returns a tuple consisting of thread ID and thread type"""
id_thread = None
type_thread = None
if "threadFbId" in msg_metadata["threadKey"]:
id_thread = str(msg_metadata["threadKey"]["threadFbId"])
type_thread = ThreadType.GROUP
elif "otherUserFbId" in msg_metadata["threadKey"]:
id_thread = str(msg_metadata["threadKey"]["otherUserFbId"])
type_thread = ThreadType.USER
return id_thread, type_thread
delta = m["delta"]
delta_type = delta.get("type")
delta_class = delta.get("class")
metadata = delta.get("messageMetadata")
if metadata:
mid = metadata["messageId"]
author_id = str(metadata["actorFbId"])
ts = int(metadata.get("timestamp"))
# Added participants
if "addedParticipants" in delta:
added_ids = [str(x["userFbId"]) for x in delta["addedParticipants"]]
thread_id = str(metadata["threadKey"]["threadFbId"])
self.onPeopleAdded(
mid=mid,
added_ids=added_ids,
author_id=author_id,
thread_id=thread_id,
ts=ts,
msg=m,
)
# Left/removed participants
elif "leftParticipantFbId" in delta:
removed_id = str(delta["leftParticipantFbId"])
thread_id = str(metadata["threadKey"]["threadFbId"])
self.onPersonRemoved(
mid=mid,
removed_id=removed_id,
author_id=author_id,
thread_id=thread_id,
ts=ts,
msg=m,
)
# Color change
elif delta_type == "change_thread_theme":
new_color = ThreadColor._from_graphql(delta["untypedData"]["theme_color"])
thread_id, thread_type = getThreadIdAndThreadType(metadata)
self.onColorChange(
mid=mid,
author_id=author_id,
new_color=new_color,
thread_id=thread_id,
thread_type=thread_type,
ts=ts,
metadata=metadata,
msg=m,
)
# Emoji change
elif delta_type == "change_thread_icon":
new_emoji = delta["untypedData"]["thread_icon"]
thread_id, thread_type = getThreadIdAndThreadType(metadata)
self.onEmojiChange(
mid=mid,
author_id=author_id,
new_emoji=new_emoji,
thread_id=thread_id,
thread_type=thread_type,
ts=ts,
metadata=metadata,
msg=m,
)
# Thread title change
elif delta_class == "ThreadName":
new_title = delta["name"]
thread_id, thread_type = getThreadIdAndThreadType(metadata)
self.onTitleChange(
mid=mid,
author_id=author_id,
new_title=new_title,
thread_id=thread_id,
thread_type=thread_type,
ts=ts,
metadata=metadata,
msg=m,
)
# Forced fetch
elif delta_class == "ForcedFetch":
mid = delta.get("messageId")
if mid is None:
self.onUnknownMesssageType(msg=m)
else:
thread_id = str(delta["threadKey"]["threadFbId"])
fetch_info = self._forcedFetch(thread_id, mid)
fetch_data = fetch_info["message"]
author_id = fetch_data["message_sender"]["id"]
ts = fetch_data["timestamp_precise"]
if fetch_data.get("__typename") == "ThreadImageMessage":
# Thread image change
image_metadata = fetch_data.get("image_with_metadata")
image_id = (
int(image_metadata["legacy_attachment_id"])
if image_metadata
else None
)
self.onImageChange(
mid=mid,
author_id=author_id,
new_image=image_id,
thread_id=thread_id,
thread_type=ThreadType.GROUP,
ts=ts,
msg=m,
)
# Nickname change
elif delta_type == "change_thread_nickname":
changed_for = str(delta["untypedData"]["participant_id"])
new_nickname = delta["untypedData"]["nickname"]
thread_id, thread_type = getThreadIdAndThreadType(metadata)
self.onNicknameChange(
mid=mid,
author_id=author_id,
changed_for=changed_for,
new_nickname=new_nickname,
thread_id=thread_id,
thread_type=thread_type,
ts=ts,
metadata=metadata,
msg=m,
)
# Admin added or removed in a group thread
elif delta_type == "change_thread_admins":
thread_id, thread_type = getThreadIdAndThreadType(metadata)
target_id = delta["untypedData"]["TARGET_ID"]
admin_event = delta["untypedData"]["ADMIN_EVENT"]
if admin_event == "add_admin":
self.onAdminAdded(
mid=mid,
added_id=target_id,
author_id=author_id,
thread_id=thread_id,
thread_type=thread_type,
ts=ts,
msg=m,
)
elif admin_event == "remove_admin":
self.onAdminRemoved(
mid=mid,
removed_id=target_id,
author_id=author_id,
thread_id=thread_id,
thread_type=thread_type,
ts=ts,
msg=m,
)
# Group approval mode change
elif delta_type == "change_thread_approval_mode":
thread_id, thread_type = getThreadIdAndThreadType(metadata)
approval_mode = bool(int(delta["untypedData"]["APPROVAL_MODE"]))
self.onApprovalModeChange(
mid=mid,
approval_mode=approval_mode,
author_id=author_id,
thread_id=thread_id,
thread_type=thread_type,
ts=ts,
msg=m,
)
# Message delivered
elif delta_class == "DeliveryReceipt":
message_ids = delta["messageIds"]
delivered_for = str(
delta.get("actorFbId") or delta["threadKey"]["otherUserFbId"]
)
ts = int(delta["deliveredWatermarkTimestampMs"])
thread_id, thread_type = getThreadIdAndThreadType(delta)
self.onMessageDelivered(
msg_ids=message_ids,
delivered_for=delivered_for,
thread_id=thread_id,
thread_type=thread_type,
ts=ts,
metadata=metadata,
msg=m,
)
# Message seen
elif delta_class == "ReadReceipt":
seen_by = str(delta.get("actorFbId") or delta["threadKey"]["otherUserFbId"])
seen_ts = int(delta["actionTimestampMs"])
delivered_ts = int(delta["watermarkTimestampMs"])
thread_id, thread_type = getThreadIdAndThreadType(delta)
self.onMessageSeen(
seen_by=seen_by,
thread_id=thread_id,
thread_type=thread_type,
seen_ts=seen_ts,
ts=delivered_ts,
metadata=metadata,
msg=m,
)
# Messages marked as seen
elif delta_class == "MarkRead":
seen_ts = int(
delta.get("actionTimestampMs") or delta.get("actionTimestamp")
)
delivered_ts = int(
delta.get("watermarkTimestampMs") or delta.get("watermarkTimestamp")
)
threads = []
if "folders" not in delta:
threads = [
getThreadIdAndThreadType({"threadKey": thr})
for thr in delta.get("threadKeys")
]
# thread_id, thread_type = getThreadIdAndThreadType(delta)
self.onMarkedSeen(
threads=threads, seen_ts=seen_ts, ts=delivered_ts, metadata=delta, msg=m
)
# Game played
elif delta_type == "instant_game_update":
game_id = delta["untypedData"]["game_id"]
game_name = delta["untypedData"]["game_name"]
score = delta["untypedData"].get("score")
if score is not None:
score = int(score)
leaderboard = delta["untypedData"].get("leaderboard")
if leaderboard is not None:
leaderboard = json.loads(leaderboard)["scores"]
thread_id, thread_type = getThreadIdAndThreadType(metadata)
self.onGamePlayed(
mid=mid,
author_id=author_id,
game_id=game_id,
game_name=game_name,
score=score,
leaderboard=leaderboard,
thread_id=thread_id,
thread_type=thread_type,
ts=ts,
metadata=metadata,
msg=m,
)
# Group call started/ended
elif delta_type == "rtc_call_log":
thread_id, thread_type = getThreadIdAndThreadType(metadata)
call_status = delta["untypedData"]["event"]
call_duration = int(delta["untypedData"]["call_duration"])
is_video_call = bool(int(delta["untypedData"]["is_video_call"]))
if call_status == "call_started":
self.onCallStarted(
mid=mid,
caller_id=author_id,
is_video_call=is_video_call,
thread_id=thread_id,
thread_type=thread_type,
ts=ts,
metadata=metadata,
msg=m,
)
elif call_status == "call_ended":
self.onCallEnded(
mid=mid,
caller_id=author_id,
is_video_call=is_video_call,
call_duration=call_duration,
thread_id=thread_id,
thread_type=thread_type,
ts=ts,
metadata=metadata,
msg=m,
)
# User joined to group call
elif delta_type == "participant_joined_group_call":
thread_id, thread_type = getThreadIdAndThreadType(metadata)
is_video_call = bool(int(delta["untypedData"]["group_call_type"]))
self.onUserJoinedCall(
mid=mid,
joined_id=author_id,
is_video_call=is_video_call,
thread_id=thread_id,
thread_type=thread_type,
ts=ts,
metadata=metadata,
msg=m,
)
# Group poll event
elif delta_type == "group_poll":
thread_id, thread_type = getThreadIdAndThreadType(metadata)
event_type = delta["untypedData"]["event_type"]
poll_json = json.loads(delta["untypedData"]["question_json"])
poll = Poll._from_graphql(poll_json)
if event_type == "question_creation":
# User created group poll
self.onPollCreated(
mid=mid,
poll=poll,
author_id=author_id,
thread_id=thread_id,
thread_type=thread_type,
ts=ts,
metadata=metadata,
msg=m,
)
elif event_type == "update_vote":
# User voted on group poll
added_options = json.loads(delta["untypedData"]["added_option_ids"])
removed_options = json.loads(delta["untypedData"]["removed_option_ids"])
self.onPollVoted(
mid=mid,
poll=poll,
added_options=added_options,
removed_options=removed_options,
author_id=author_id,
thread_id=thread_id,
thread_type=thread_type,
ts=ts,
metadata=metadata,
msg=m,
)
# Plan created
elif delta_type == "lightweight_event_create":
thread_id, thread_type = getThreadIdAndThreadType(metadata)
self.onPlanCreated(
mid=mid,
plan=Plan._from_pull(delta["untypedData"]),
author_id=author_id,
thread_id=thread_id,
thread_type=thread_type,
ts=ts,
metadata=metadata,
msg=m,
)
# Plan ended
elif delta_type == "lightweight_event_notify":
thread_id, thread_type = getThreadIdAndThreadType(metadata)
self.onPlanEnded(
mid=mid,
plan=Plan._from_pull(delta["untypedData"]),
thread_id=thread_id,
thread_type=thread_type,
ts=ts,
metadata=metadata,
msg=m,
)
# Plan edited
elif delta_type == "lightweight_event_update":
thread_id, thread_type = getThreadIdAndThreadType(metadata)
self.onPlanEdited(
mid=mid,
plan=Plan._from_pull(delta["untypedData"]),
author_id=author_id,
thread_id=thread_id,
thread_type=thread_type,
ts=ts,
metadata=metadata,
msg=m,
)
# Plan deleted
elif delta_type == "lightweight_event_delete":
thread_id, thread_type = getThreadIdAndThreadType(metadata)
self.onPlanDeleted(
mid=mid,
plan=Plan._from_pull(delta["untypedData"]),
author_id=author_id,
thread_id=thread_id,
thread_type=thread_type,
ts=ts,
metadata=metadata,
msg=m,
)
# Plan participation change
elif delta_type == "lightweight_event_rsvp":
thread_id, thread_type = getThreadIdAndThreadType(metadata)
take_part = delta["untypedData"]["guest_status"] == "GOING"
self.onPlanParticipation(
mid=mid,
plan=Plan._from_pull(delta["untypedData"]),
take_part=take_part,
author_id=author_id,
thread_id=thread_id,
thread_type=thread_type,
ts=ts,
metadata=metadata,
msg=m,
)
# Client payload (that weird numbers)
elif delta_class == "ClientPayload":
payload = json.loads("".join(chr(z) for z in delta["payload"]))
ts = m.get("ofd_ts")
for d in payload.get("deltas", []):
# Message reaction
if d.get("deltaMessageReaction"):
i = d["deltaMessageReaction"]
thread_id, thread_type = getThreadIdAndThreadType(i)
mid = i["messageId"]
author_id = str(i["userId"])
reaction = (
MessageReaction(i["reaction"]) if i.get("reaction") else None
)
add_reaction = not bool(i["action"])
if add_reaction:
self.onReactionAdded(
mid=mid,
reaction=reaction,
author_id=author_id,
thread_id=thread_id,
thread_type=thread_type,
ts=ts,
msg=m,
)
else:
self.onReactionRemoved(
mid=mid,
author_id=author_id,
thread_id=thread_id,
thread_type=thread_type,
ts=ts,
msg=m,
)
# Viewer status change
elif d.get("deltaChangeViewerStatus"):
i = d["deltaChangeViewerStatus"]
thread_id, thread_type = getThreadIdAndThreadType(i)
author_id = str(i["actorFbid"])
reason = i["reason"]
can_reply = i["canViewerReply"]
if reason == 2:
if can_reply:
self.onUnblock(
author_id=author_id,
thread_id=thread_id,
thread_type=thread_type,
ts=ts,
msg=m,
)
else:
self.onBlock(
author_id=author_id,
thread_id=thread_id,
thread_type=thread_type,
ts=ts,
msg=m,
)
# Live location info
elif d.get("liveLocationData"):
i = d["liveLocationData"]
thread_id, thread_type = getThreadIdAndThreadType(i)
for l in i["messageLiveLocations"]:
mid = l["messageId"]
author_id = str(l["senderId"])
location = LiveLocationAttachment._from_pull(l)
self.onLiveLocation(
mid=mid,
location=location,
author_id=author_id,
thread_id=thread_id,
thread_type=thread_type,
ts=ts,
msg=m,
)
# Message deletion
elif d.get("deltaRecallMessageData"):
i = d["deltaRecallMessageData"]
thread_id, thread_type = getThreadIdAndThreadType(i)
mid = i["messageID"]
ts = i["deletionTimestamp"]
author_id = str(i["senderID"])
self.onMessageUnsent(
mid=mid,
author_id=author_id,
thread_id=thread_id,
thread_type=thread_type,
ts=ts,
msg=m,
)
elif d.get("deltaMessageReply"):
i = d["deltaMessageReply"]
metadata = i["message"]["messageMetadata"]
thread_id, thread_type = getThreadIdAndThreadType(metadata)
message = Message._from_reply(i["message"])
message.replied_to = Message._from_reply(i["repliedToMessage"])
message.reply_to_id = message.replied_to.uid
self.onMessage(
mid=message.uid,
author_id=message.author,
message=message.text,
message_object=message,
thread_id=thread_id,
thread_type=thread_type,
ts=message.timestamp,
metadata=metadata,
msg=m,
)
# New message
elif delta.get("class") == "NewMessage":
thread_id, thread_type = getThreadIdAndThreadType(metadata)
self.onMessage(
mid=mid,
author_id=author_id,
message=delta.get("body", ""),
message_object=Message._from_pull(
delta,
mid=mid,
tags=metadata.get("tags"),
author=author_id,
timestamp=ts,
),
thread_id=thread_id,
thread_type=thread_type,
ts=ts,
metadata=metadata,
msg=m,
)
# Unknown message type
else:
self.onUnknownMesssageType(msg=m)
def _parseMessage(self, content):
"""Get message and author name from content. May contain multiple messages in the content."""
self._seq = content.get("seq", "0")
if "lb_info" in content:
self._sticky = content["lb_info"]["sticky"]
self._pool = content["lb_info"]["pool"]
if "batches" in content:
for batch in content["batches"]:
self._parseMessage(batch)
if "ms" not in content:
return
for m in content["ms"]:
mtype = m.get("type")
try:
# Things that directly change chat
if mtype == "delta":
self._parseDelta(m)
# Inbox
elif mtype == "inbox":
self.onInbox(
unseen=m["unseen"],
unread=m["unread"],
recent_unread=m["recent_unread"],
msg=m,
)
# Typing
elif mtype == "typ" or mtype == "ttyp":
author_id = str(m.get("from"))
thread_id = m.get("thread_fbid")
if thread_id:
thread_type = ThreadType.GROUP
thread_id = str(thread_id)
else:
thread_type = ThreadType.USER
if author_id == self._uid:
thread_id = m.get("to")
else:
thread_id = author_id
typing_status = TypingStatus(m.get("st"))
self.onTyping(
author_id=author_id,
status=typing_status,
thread_id=thread_id,
thread_type=thread_type,
msg=m,
)
# Delivered
# Seen
# elif mtype == "m_read_receipt":
#
# self.onSeen(m.get('realtime_viewer_fbid'), m.get('reader'), m.get('time'))
elif mtype in ["jewel_requests_add"]:
from_id = m["from"]
self.onFriendRequest(from_id=from_id, msg=m)
# Happens on every login
elif mtype == "qprimer":
self.onQprimer(ts=m.get("made"), msg=m)
# Is sent before any other message
elif mtype == "deltaflow":
pass
# Chat timestamp
elif mtype == "chatproxy-presence":
statuses = dict()
for id_, data in m.get("buddyList", {}).items():
statuses[id_] = ActiveStatus._from_chatproxy_presence(id_, data)
self._buddylist[id_] = statuses[id_]
self.onChatTimestamp(buddylist=statuses, msg=m)
# Buddylist overlay
elif mtype == "buddylist_overlay":
statuses = dict()
for id_, data in m.get("overlay", {}).items():
old_in_game = None
if id_ in self._buddylist:
old_in_game = self._buddylist[id_].in_game
statuses[id_] = ActiveStatus._from_buddylist_overlay(
data, old_in_game
)
self._buddylist[id_] = statuses[id_]
self.onBuddylistOverlay(statuses=statuses, msg=m)
# Unknown message type
else:
self.onUnknownMesssageType(msg=m)
except Exception as e:
self.onMessageError(exception=e, msg=m)
[docs] def startListening(self):
"""
Start listening from an external event loop
:raises: FBchatException if request failed
"""
self.listening = True
[docs] def doOneListen(self, markAlive=None):
"""
Does one cycle of the listening loop.
This method is useful if you want to control fbchat from an external event loop
.. warning::
``markAlive`` parameter is deprecated, use :func:`Client.setActiveStatus`
or ``markAlive`` parameter in :func:`Client.listen` instead.
:return: Whether the loop should keep running
:rtype: bool
"""
if markAlive is not None:
self._markAlive = markAlive
try:
if self._markAlive:
self._ping()
content = self._pullMessage()
if content:
self._parseMessage(content)
except KeyboardInterrupt:
return False
except requests.Timeout:
pass
except requests.ConnectionError:
# If the client has lost their internet connection, keep trying every 30 seconds
time.sleep(30)
except FBchatFacebookError as e:
# Fix 502 and 503 pull errors
if e.request_status_code in [502, 503]:
# Bump pull channel, while contraining withing 0-4
self._pull_channel = (self._pull_channel + 1) % 5
self.startListening()
else:
raise e
except Exception as e:
return self.onListenError(exception=e)
return True
[docs] def stopListening(self):
"""Cleans up the variables from startListening"""
self.listening = False
self._sticky, self._pool = (None, None)
[docs] def listen(self, markAlive=None):
"""
Initializes and runs the listening loop continually
:param markAlive: Whether this should ping the Facebook server each time the loop runs
:type markAlive: bool
"""
if markAlive is not None:
self.setActiveStatus(markAlive)
self.startListening()
self.onListening()
while self.listening and self.doOneListen():
pass
self.stopListening()
[docs] def setActiveStatus(self, markAlive):
"""
Changes client active status while listening
:param markAlive: Whether to show if client is active
:type markAlive: bool
"""
self._markAlive = markAlive
"""
END LISTEN METHODS
"""
"""
EVENTS
"""
[docs] def onLoggingIn(self, email=None):
"""
Called when the client is logging in
:param email: The email of the client
"""
log.info("Logging in {}...".format(email))
[docs] def on2FACode(self):
"""Called when a 2FA code is needed to progress"""
return input("Please enter your 2FA code --> ")
[docs] def onLoggedIn(self, email=None):
"""
Called when the client is successfully logged in
:param email: The email of the client
"""
log.info("Login of {} successful.".format(email))
[docs] def onListening(self):
"""Called when the client is listening"""
log.info("Listening...")
[docs] def onListenError(self, exception=None):
"""
Called when an error was encountered while listening
:param exception: The exception that was encountered
:return: Whether the loop should keep running
"""
log.exception("Got exception while listening")
return True
[docs] def onMessage(
self,
mid=None,
author_id=None,
message=None,
message_object=None,
thread_id=None,
thread_type=ThreadType.USER,
ts=None,
metadata=None,
msg=None,
):
"""
Called when the client is listening, and somebody sends a message
:param mid: The message ID
:param author_id: The ID of the author
:param message: (deprecated. Use ``message_object.text`` instead)
:param message_object: The message (As a `Message` object)
:param thread_id: Thread ID that the message was sent to. See :ref:`intro_threads`
:param thread_type: Type of thread that the message was sent to. See :ref:`intro_threads`
:param ts: The timestamp of the message
:param metadata: Extra metadata about the message
:param msg: A full set of the data recieved
:type message_object: Message
:type thread_type: ThreadType
"""
log.info("{} from {} in {}".format(message_object, thread_id, thread_type.name))
[docs] def onColorChange(
self,
mid=None,
author_id=None,
new_color=None,
thread_id=None,
thread_type=ThreadType.USER,
ts=None,
metadata=None,
msg=None,
):
"""
Called when the client is listening, and somebody changes a thread's color
:param mid: The action ID
:param author_id: The ID of the person who changed the color
:param new_color: The new color
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads`
:param ts: A timestamp of the action
:param metadata: Extra metadata about the action
:param msg: A full set of the data recieved
:type new_color: ThreadColor
:type thread_type: ThreadType
"""
log.info(
"Color change from {} in {} ({}): {}".format(
author_id, thread_id, thread_type.name, new_color
)
)
[docs] def onEmojiChange(
self,
mid=None,
author_id=None,
new_emoji=None,
thread_id=None,
thread_type=ThreadType.USER,
ts=None,
metadata=None,
msg=None,
):
"""
Called when the client is listening, and somebody changes a thread's emoji
:param mid: The action ID
:param author_id: The ID of the person who changed the emoji
:param new_emoji: The new emoji
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads`
:param ts: A timestamp of the action
:param metadata: Extra metadata about the action
:param msg: A full set of the data recieved
:type thread_type: ThreadType
"""
log.info(
"Emoji change from {} in {} ({}): {}".format(
author_id, thread_id, thread_type.name, new_emoji
)
)
[docs] def onTitleChange(
self,
mid=None,
author_id=None,
new_title=None,
thread_id=None,
thread_type=ThreadType.USER,
ts=None,
metadata=None,
msg=None,
):
"""
Called when the client is listening, and somebody changes the title of a thread
:param mid: The action ID
:param author_id: The ID of the person who changed the title
:param new_title: The new title
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads`
:param ts: A timestamp of the action
:param metadata: Extra metadata about the action
:param msg: A full set of the data recieved
:type thread_type: ThreadType
"""
log.info(
"Title change from {} in {} ({}): {}".format(
author_id, thread_id, thread_type.name, new_title
)
)
[docs] def onImageChange(
self,
mid=None,
author_id=None,
new_image=None,
thread_id=None,
thread_type=ThreadType.GROUP,
ts=None,
msg=None,
):
"""
Called when the client is listening, and somebody changes the image of a thread
:param mid: The action ID
:param author_id: The ID of the person who changed the image
:param new_image: The ID of the new image
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads`
:param ts: A timestamp of the action
:param msg: A full set of the data recieved
:type thread_type: ThreadType
"""
log.info("{} changed thread image in {}".format(author_id, thread_id))
[docs] def onNicknameChange(
self,
mid=None,
author_id=None,
changed_for=None,
new_nickname=None,
thread_id=None,
thread_type=ThreadType.USER,
ts=None,
metadata=None,
msg=None,
):
"""
Called when the client is listening, and somebody changes the nickname of a person
:param mid: The action ID
:param author_id: The ID of the person who changed the nickname
:param changed_for: The ID of the person whom got their nickname changed
:param new_nickname: The new nickname
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads`
:param ts: A timestamp of the action
:param metadata: Extra metadata about the action
:param msg: A full set of the data recieved
:type thread_type: ThreadType
"""
log.info(
"Nickname change from {} in {} ({}) for {}: {}".format(
author_id, thread_id, thread_type.name, changed_for, new_nickname
)
)
[docs] def onAdminAdded(
self,
mid=None,
added_id=None,
author_id=None,
thread_id=None,
thread_type=ThreadType.GROUP,
ts=None,
msg=None,
):
"""
Called when the client is listening, and somebody adds an admin to a group thread
:param mid: The action ID
:param added_id: The ID of the admin who got added
:param author_id: The ID of the person who added the admins
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param ts: A timestamp of the action
:param msg: A full set of the data recieved
"""
log.info("{} added admin: {} in {}".format(author_id, added_id, thread_id))
[docs] def onAdminRemoved(
self,
mid=None,
removed_id=None,
author_id=None,
thread_id=None,
thread_type=ThreadType.GROUP,
ts=None,
msg=None,
):
"""
Called when the client is listening, and somebody removes an admin from a group thread
:param mid: The action ID
:param removed_id: The ID of the admin who got removed
:param author_id: The ID of the person who removed the admins
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param ts: A timestamp of the action
:param msg: A full set of the data recieved
"""
log.info("{} removed admin: {} in {}".format(author_id, removed_id, thread_id))
[docs] def onApprovalModeChange(
self,
mid=None,
approval_mode=None,
author_id=None,
thread_id=None,
thread_type=ThreadType.GROUP,
ts=None,
msg=None,
):
"""
Called when the client is listening, and somebody changes approval mode in a group thread
:param mid: The action ID
:param approval_mode: True if approval mode is activated
:param author_id: The ID of the person who changed approval mode
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param ts: A timestamp of the action
:param msg: A full set of the data recieved
"""
if approval_mode:
log.info("{} activated approval mode in {}".format(author_id, thread_id))
else:
log.info("{} disabled approval mode in {}".format(author_id, thread_id))
[docs] def onMessageSeen(
self,
seen_by=None,
thread_id=None,
thread_type=ThreadType.USER,
seen_ts=None,
ts=None,
metadata=None,
msg=None,
):
"""
Called when the client is listening, and somebody marks a message as seen
:param seen_by: The ID of the person who marked the message as seen
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads`
:param seen_ts: A timestamp of when the person saw the message
:param ts: A timestamp of the action
:param metadata: Extra metadata about the action
:param msg: A full set of the data recieved
:type thread_type: ThreadType
"""
log.info(
"Messages seen by {} in {} ({}) at {}s".format(
seen_by, thread_id, thread_type.name, seen_ts / 1000
)
)
[docs] def onMessageDelivered(
self,
msg_ids=None,
delivered_for=None,
thread_id=None,
thread_type=ThreadType.USER,
ts=None,
metadata=None,
msg=None,
):
"""
Called when the client is listening, and somebody marks messages as delivered
:param msg_ids: The messages that are marked as delivered
:param delivered_for: The person that marked the messages as delivered
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads`
:param ts: A timestamp of the action
:param metadata: Extra metadata about the action
:param msg: A full set of the data recieved
:type thread_type: ThreadType
"""
log.info(
"Messages {} delivered to {} in {} ({}) at {}s".format(
msg_ids, delivered_for, thread_id, thread_type.name, ts / 1000
)
)
[docs] def onMarkedSeen(
self, threads=None, seen_ts=None, ts=None, metadata=None, msg=None
):
"""
Called when the client is listening, and the client has successfully marked threads as seen
:param threads: The threads that were marked
:param author_id: The ID of the person who changed the emoji
:param seen_ts: A timestamp of when the threads were seen
:param ts: A timestamp of the action
:param metadata: Extra metadata about the action
:param msg: A full set of the data recieved
:type thread_type: ThreadType
"""
log.info(
"Marked messages as seen in threads {} at {}s".format(
[(x[0], x[1].name) for x in threads], seen_ts / 1000
)
)
[docs] def onMessageUnsent(
self,
mid=None,
author_id=None,
thread_id=None,
thread_type=None,
ts=None,
msg=None,
):
"""
Called when the client is listening, and someone unsends (deletes for everyone) a message
:param mid: ID of the unsent message
:param author_id: The ID of the person who unsent the message
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads`
:param ts: A timestamp of the action
:param msg: A full set of the data recieved
:type thread_type: ThreadType
"""
log.info(
"{} unsent the message {} in {} ({}) at {}s".format(
author_id, repr(mid), thread_id, thread_type.name, ts / 1000
)
)
[docs] def onPeopleAdded(
self,
mid=None,
added_ids=None,
author_id=None,
thread_id=None,
ts=None,
msg=None,
):
"""
Called when the client is listening, and somebody adds people to a group thread
:param mid: The action ID
:param added_ids: The IDs of the people who got added
:param author_id: The ID of the person who added the people
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param ts: A timestamp of the action
:param msg: A full set of the data recieved
"""
log.info(
"{} added: {} in {}".format(author_id, ", ".join(added_ids), thread_id)
)
[docs] def onPersonRemoved(
self,
mid=None,
removed_id=None,
author_id=None,
thread_id=None,
ts=None,
msg=None,
):
"""
Called when the client is listening, and somebody removes a person from a group thread
:param mid: The action ID
:param removed_id: The ID of the person who got removed
:param author_id: The ID of the person who removed the person
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param ts: A timestamp of the action
:param msg: A full set of the data recieved
"""
log.info("{} removed: {} in {}".format(author_id, removed_id, thread_id))
[docs] def onFriendRequest(self, from_id=None, msg=None):
"""
Called when the client is listening, and somebody sends a friend request
:param from_id: The ID of the person that sent the request
:param msg: A full set of the data recieved
"""
log.info("Friend request from {}".format(from_id))
[docs] def onInbox(self, unseen=None, unread=None, recent_unread=None, msg=None):
"""
.. todo::
Documenting this
:param unseen: --
:param unread: --
:param recent_unread: --
:param msg: A full set of the data recieved
"""
log.info("Inbox event: {}, {}, {}".format(unseen, unread, recent_unread))
[docs] def onTyping(
self, author_id=None, status=None, thread_id=None, thread_type=None, msg=None
):
"""
Called when the client is listening, and somebody starts or stops typing into a chat
:param author_id: The ID of the person who sent the action
:param status: The typing status
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads`
:param msg: A full set of the data recieved
:type typing_status: TypingStatus
:type thread_type: ThreadType
"""
pass
[docs] def onGamePlayed(
self,
mid=None,
author_id=None,
game_id=None,
game_name=None,
score=None,
leaderboard=None,
thread_id=None,
thread_type=None,
ts=None,
metadata=None,
msg=None,
):
"""
Called when the client is listening, and somebody plays a game
:param mid: The action ID
:param author_id: The ID of the person who played the game
:param game_id: The ID of the game
:param game_name: Name of the game
:param score: Score obtained in the game
:param leaderboard: Actual leaderboard of the game in the thread
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads`
:param ts: A timestamp of the action
:param metadata: Extra metadata about the action
:param msg: A full set of the data recieved
:type thread_type: ThreadType
"""
log.info(
'{} played "{}" in {} ({})'.format(
author_id, game_name, thread_id, thread_type.name
)
)
[docs] def onReactionAdded(
self,
mid=None,
reaction=None,
author_id=None,
thread_id=None,
thread_type=None,
ts=None,
msg=None,
):
"""
Called when the client is listening, and somebody reacts to a message
:param mid: Message ID, that user reacted to
:param reaction: Reaction
:param add_reaction: Whether user added or removed reaction
:param author_id: The ID of the person who reacted to the message
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads`
:param ts: A timestamp of the action
:param msg: A full set of the data recieved
:type reaction: MessageReaction
:type thread_type: ThreadType
"""
log.info(
"{} reacted to message {} with {} in {} ({})".format(
author_id, mid, reaction.name, thread_id, thread_type.name
)
)
[docs] def onReactionRemoved(
self,
mid=None,
author_id=None,
thread_id=None,
thread_type=None,
ts=None,
msg=None,
):
"""
Called when the client is listening, and somebody removes reaction from a message
:param mid: Message ID, that user reacted to
:param author_id: The ID of the person who removed reaction
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads`
:param ts: A timestamp of the action
:param msg: A full set of the data recieved
:type thread_type: ThreadType
"""
log.info(
"{} removed reaction from {} message in {} ({})".format(
author_id, mid, thread_id, thread_type
)
)
[docs] def onBlock(
self, author_id=None, thread_id=None, thread_type=None, ts=None, msg=None
):
"""
Called when the client is listening, and somebody blocks client
:param author_id: The ID of the person who blocked
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads`
:param ts: A timestamp of the action
:param msg: A full set of the data recieved
:type thread_type: ThreadType
"""
log.info(
"{} blocked {} ({}) thread".format(author_id, thread_id, thread_type.name)
)
[docs] def onUnblock(
self, author_id=None, thread_id=None, thread_type=None, ts=None, msg=None
):
"""
Called when the client is listening, and somebody blocks client
:param author_id: The ID of the person who unblocked
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads`
:param ts: A timestamp of the action
:param msg: A full set of the data recieved
:type thread_type: ThreadType
"""
log.info(
"{} unblocked {} ({}) thread".format(author_id, thread_id, thread_type.name)
)
[docs] def onLiveLocation(
self,
mid=None,
location=None,
author_id=None,
thread_id=None,
thread_type=None,
ts=None,
msg=None,
):
"""
Called when the client is listening and somebody sends live location info
:param mid: The action ID
:param location: Sent location info
:param author_id: The ID of the person who sent location info
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads`
:param ts: A timestamp of the action
:param msg: A full set of the data recieved
:type location: LiveLocationAttachment
:type thread_type: ThreadType
"""
log.info(
"{} sent live location info in {} ({}) with latitude {} and longitude {}".format(
author_id, thread_id, thread_type, location.latitude, location.longitude
)
)
[docs] def onCallStarted(
self,
mid=None,
caller_id=None,
is_video_call=None,
thread_id=None,
thread_type=None,
ts=None,
metadata=None,
msg=None,
):
"""
.. todo::
Make this work with private calls
Called when the client is listening, and somebody starts a call in a group
:param mid: The action ID
:param caller_id: The ID of the person who started the call
:param is_video_call: True if it's video call
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads`
:param ts: A timestamp of the action
:param metadata: Extra metadata about the action
:param msg: A full set of the data recieved
:type thread_type: ThreadType
"""
log.info(
"{} started call in {} ({})".format(caller_id, thread_id, thread_type.name)
)
[docs] def onCallEnded(
self,
mid=None,
caller_id=None,
is_video_call=None,
call_duration=None,
thread_id=None,
thread_type=None,
ts=None,
metadata=None,
msg=None,
):
"""
.. todo::
Make this work with private calls
Called when the client is listening, and somebody ends a call in a group
:param mid: The action ID
:param caller_id: The ID of the person who ended the call
:param is_video_call: True if it was video call
:param call_duration: Call duration in seconds
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads`
:param ts: A timestamp of the action
:param metadata: Extra metadata about the action
:param msg: A full set of the data recieved
:type thread_type: ThreadType
"""
log.info(
"{} ended call in {} ({})".format(caller_id, thread_id, thread_type.name)
)
[docs] def onUserJoinedCall(
self,
mid=None,
joined_id=None,
is_video_call=None,
thread_id=None,
thread_type=None,
ts=None,
metadata=None,
msg=None,
):
"""
Called when the client is listening, and somebody joins a group call
:param mid: The action ID
:param joined_id: The ID of the person who joined the call
:param is_video_call: True if it's video call
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads`
:param ts: A timestamp of the action
:param metadata: Extra metadata about the action
:param msg: A full set of the data recieved
:type thread_type: ThreadType
"""
log.info(
"{} joined call in {} ({})".format(joined_id, thread_id, thread_type.name)
)
[docs] def onPollCreated(
self,
mid=None,
poll=None,
author_id=None,
thread_id=None,
thread_type=None,
ts=None,
metadata=None,
msg=None,
):
"""
Called when the client is listening, and somebody creates a group poll
:param mid: The action ID
:param poll: Created poll
:param author_id: The ID of the person who created the poll
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads`
:param ts: A timestamp of the action
:param metadata: Extra metadata about the action
:param msg: A full set of the data recieved
:type poll: Poll
:type thread_type: ThreadType
"""
log.info(
"{} created poll {} in {} ({})".format(
author_id, poll, thread_id, thread_type.name
)
)
[docs] def onPollVoted(
self,
mid=None,
poll=None,
added_options=None,
removed_options=None,
author_id=None,
thread_id=None,
thread_type=None,
ts=None,
metadata=None,
msg=None,
):
"""
Called when the client is listening, and somebody votes in a group poll
:param mid: The action ID
:param poll: Poll, that user voted in
:param author_id: The ID of the person who voted in the poll
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads`
:param ts: A timestamp of the action
:param metadata: Extra metadata about the action
:param msg: A full set of the data recieved
:type poll: Poll
:type thread_type: ThreadType
"""
log.info(
"{} voted in poll {} in {} ({})".format(
author_id, poll, thread_id, thread_type.name
)
)
[docs] def onPlanCreated(
self,
mid=None,
plan=None,
author_id=None,
thread_id=None,
thread_type=None,
ts=None,
metadata=None,
msg=None,
):
"""
Called when the client is listening, and somebody creates a plan
:param mid: The action ID
:param plan: Created plan
:param author_id: The ID of the person who created the plan
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads`
:param ts: A timestamp of the action
:param metadata: Extra metadata about the action
:param msg: A full set of the data recieved
:type plan: Plan
:type thread_type: ThreadType
"""
log.info(
"{} created plan {} in {} ({})".format(
author_id, plan, thread_id, thread_type.name
)
)
[docs] def onPlanEnded(
self,
mid=None,
plan=None,
thread_id=None,
thread_type=None,
ts=None,
metadata=None,
msg=None,
):
"""
Called when the client is listening, and a plan ends
:param mid: The action ID
:param plan: Ended plan
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads`
:param ts: A timestamp of the action
:param metadata: Extra metadata about the action
:param msg: A full set of the data recieved
:type plan: Plan
:type thread_type: ThreadType
"""
log.info(
"Plan {} has ended in {} ({})".format(plan, thread_id, thread_type.name)
)
[docs] def onPlanEdited(
self,
mid=None,
plan=None,
author_id=None,
thread_id=None,
thread_type=None,
ts=None,
metadata=None,
msg=None,
):
"""
Called when the client is listening, and somebody edits a plan
:param mid: The action ID
:param plan: Edited plan
:param author_id: The ID of the person who edited the plan
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads`
:param ts: A timestamp of the action
:param metadata: Extra metadata about the action
:param msg: A full set of the data recieved
:type plan: Plan
:type thread_type: ThreadType
"""
log.info(
"{} edited plan {} in {} ({})".format(
author_id, plan, thread_id, thread_type.name
)
)
[docs] def onPlanDeleted(
self,
mid=None,
plan=None,
author_id=None,
thread_id=None,
thread_type=None,
ts=None,
metadata=None,
msg=None,
):
"""
Called when the client is listening, and somebody deletes a plan
:param mid: The action ID
:param plan: Deleted plan
:param author_id: The ID of the person who deleted the plan
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads`
:param ts: A timestamp of the action
:param metadata: Extra metadata about the action
:param msg: A full set of the data recieved
:type plan: Plan
:type thread_type: ThreadType
"""
log.info(
"{} deleted plan {} in {} ({})".format(
author_id, plan, thread_id, thread_type.name
)
)
[docs] def onPlanParticipation(
self,
mid=None,
plan=None,
take_part=None,
author_id=None,
thread_id=None,
thread_type=None,
ts=None,
metadata=None,
msg=None,
):
"""
Called when the client is listening, and somebody takes part in a plan or not
:param mid: The action ID
:param plan: Plan
:param take_part: Whether the person takes part in the plan or not
:param author_id: The ID of the person who will participate in the plan or not
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads`
:param ts: A timestamp of the action
:param metadata: Extra metadata about the action
:param msg: A full set of the data recieved
:type plan: Plan
:type take_part: bool
:type thread_type: ThreadType
"""
if take_part:
log.info(
"{} will take part in {} in {} ({})".format(
author_id, plan, thread_id, thread_type.name
)
)
else:
log.info(
"{} won't take part in {} in {} ({})".format(
author_id, plan, thread_id, thread_type.name
)
)
[docs] def onQprimer(self, ts=None, msg=None):
"""
Called when the client just started listening
:param ts: A timestamp of the action
:param msg: A full set of the data recieved
"""
pass
[docs] def onChatTimestamp(self, buddylist=None, msg=None):
"""
Called when the client receives chat online presence update
:param buddylist: A list of dicts with friend id and last seen timestamp
:param msg: A full set of the data recieved
"""
log.debug("Chat Timestamps received: {}".format(buddylist))
[docs] def onBuddylistOverlay(self, statuses=None, msg=None):
"""
Called when the client is listening and client receives information about friend active status
:param statuses: Dictionary with user IDs as keys and :class:`ActiveStatus` as values
:param msg: A full set of the data recieved
:type statuses: dict
"""
log.debug("Buddylist overlay received: {}".format(statuses))
[docs] def onUnknownMesssageType(self, msg=None):
"""
Called when the client is listening, and some unknown data was recieved
:param msg: A full set of the data recieved
"""
log.debug("Unknown message received: {}".format(msg))
[docs] def onMessageError(self, exception=None, msg=None):
"""
Called when an error was encountered while parsing recieved data
:param exception: The exception that was encountered
:param msg: A full set of the data recieved
"""
log.exception("Exception in parsing of {}".format(msg))
"""
END EVENTS
"""