Merge branch 'master' into refactor-model-parsing

This commit is contained in:
Mads Marquart
2019-04-17 22:01:04 +02:00
5 changed files with 224 additions and 268 deletions

View File

@@ -40,7 +40,7 @@ jobs:
file_glob: true file_glob: true
file: dist/* file: dist/*
skip_cleanup: true skip_cleanup: true
draft: true draft: false
on: on:
tags: true tags: true

View File

@@ -14,7 +14,7 @@ from ._graphql import graphql_queries_to_json, graphql_response_to_json, GraphQL
from ._client import Client from ._client import Client
__title__ = "fbchat" __title__ = "fbchat"
__version__ = "1.6.3" __version__ = "1.6.4"
__description__ = "Facebook Chat (Messenger) for Python" __description__ = "Facebook Chat (Messenger) for Python"
__copyright__ = "Copyright 2015 - 2019 by Taehoon Kim" __copyright__ = "Copyright 2015 - 2019 by Taehoon Kim"

View File

@@ -20,6 +20,13 @@ except ImportError:
from urlparse import urlparse, parse_qs from urlparse import urlparse, parse_qs
ACONTEXT = {
"action_history": [
{"surface": "messenger_chat_tab", "mechanism": "messenger_composer"}
]
}
class Client(object): class Client(object):
"""A client for the Facebook Chat (Messenger). """A client for the Facebook Chat (Messenger).
@@ -60,7 +67,6 @@ class Client(object):
:type logging_level: int :type logging_level: int
:raises: FBchatException on failed login :raises: FBchatException on failed login
""" """
self.sticky, self.pool = (None, None) self.sticky, self.pool = (None, None)
self._session = requests.session() self._session = requests.session()
self.req_counter = 1 self.req_counter = 1
@@ -166,6 +172,7 @@ class Client(object):
timeout=30, timeout=30,
fix_request=False, fix_request=False,
as_json=False, as_json=False,
as_graphql=False,
error_retries=3, error_retries=3,
): ):
payload = self._generatePayload(query) payload = self._generatePayload(query)
@@ -179,6 +186,10 @@ class Client(object):
if not fix_request: if not fix_request:
return r return r
try: try:
if as_graphql:
content = check_request(r, as_json=False)
return graphql_response_to_json(content)
else:
return check_request(r, as_json=as_json) return check_request(r, as_json=as_json)
except FBchatFacebookError as e: except FBchatFacebookError as e:
if error_retries > 0 and self._fix_fb_errors(e.fb_error_code): if error_retries > 0 and self._fix_fb_errors(e.fb_error_code):
@@ -188,21 +199,11 @@ class Client(object):
timeout=timeout, timeout=timeout,
fix_request=fix_request, fix_request=fix_request,
as_json=as_json, as_json=as_json,
as_graphql=as_graphql,
error_retries=error_retries - 1, error_retries=error_retries - 1,
) )
raise e raise e
def _graphql(self, payload, error_retries=3):
content = self._post(
self.req_url.GRAPHQL, payload, fix_request=True, as_json=False
)
try:
return graphql_response_to_json(content)
except FBchatFacebookError as e:
if error_retries > 0 and self._fix_fb_errors(e.fb_error_code):
return self._graphql(payload, error_retries=error_retries - 1)
raise e
def _cleanGet(self, url, query=None, timeout=30, allow_redirects=True): def _cleanGet(self, url, query=None, timeout=30, allow_redirects=True):
return self._session.get( return self._session.get(
url, url,
@@ -272,15 +273,13 @@ class Client(object):
:return: A tuple containing json graphql queries :return: A tuple containing json graphql queries
:rtype: tuple :rtype: tuple
""" """
data = {
return tuple(
self._graphql(
{
"method": "GET", "method": "GET",
"response_format": "json", "response_format": "json",
"queries": graphql_queries_to_json(*queries), "queries": graphql_queries_to_json(*queries),
} }
) return tuple(
self._post(self.req_url.GRAPHQL, data, fix_request=True, as_graphql=True)
) )
def graphql_request(self, query): def graphql_request(self, query):
@@ -314,7 +313,7 @@ class Client(object):
if self.uid is None: if self.uid is None:
raise FBchatException("Could not find c_user cookie") raise FBchatException("Could not find c_user cookie")
self.uid = str(self.uid) self.uid = str(self.uid)
self.user_channel = "p_" + self.uid self.user_channel = "p_{}".format(self.uid)
self.ttstamp = "" self.ttstamp = ""
r = self._get(self.req_url.BASE) r = self._get(self.req_url.BASE)
@@ -390,9 +389,9 @@ class Client(object):
if "home" in r.url: if "home" in r.url:
return r return r
del (data["approvals_code"]) del data["approvals_code"]
del (data["submit[Submit Code]"]) del data["submit[Submit Code]"]
del (data["codes_submitted"]) del data["codes_submitted"]
data["name_action_selected"] = "save_device" data["name_action_selected"] = "save_device"
data["submit[Continue]"] = "Continue" data["submit[Continue]"] = "Continue"
@@ -404,7 +403,7 @@ class Client(object):
if "home" in r.url: if "home" in r.url:
return r return r
del (data["name_action_selected"]) del data["name_action_selected"]
log.info( log.info(
"Starting Facebook checkup flow." "Starting Facebook checkup flow."
) # At this stage, we have dtsg, nh, submit[Continue] ) # At this stage, we have dtsg, nh, submit[Continue]
@@ -413,7 +412,7 @@ class Client(object):
if "home" in r.url: if "home" in r.url:
return r return r
del (data["submit[Continue]"]) del data["submit[Continue]"]
data["submit[This was me]"] = "This Was Me" data["submit[This was me]"] = "This Was Me"
log.info( log.info(
"Verifying login attempt." "Verifying login attempt."
@@ -423,7 +422,7 @@ class Client(object):
if "home" in r.url: if "home" in r.url:
return r return r
del (data["submit[This was me]"]) del data["submit[This was me]"]
data["submit[Continue]"] = "Continue" data["submit[Continue]"] = "Continue"
data["name_action_selected"] = "save_device" data["name_action_selected"] = "save_device"
log.info( log.info(
@@ -459,7 +458,6 @@ class Client(object):
:return: False if `session_cookies` does not contain proper cookies :return: False if `session_cookies` does not contain proper cookies
:rtype: bool :rtype: bool
""" """
# Quick check to see if session_cookies is formatted properly # Quick check to see if session_cookies is formatted properly
if not session_cookies or "c_user" not in session_cookies: if not session_cookies or "c_user" not in session_cookies:
return False return False
@@ -512,9 +510,8 @@ class Client(object):
break break
else: else:
raise FBchatUserError( raise FBchatUserError(
"Login failed. Check email/password. (Failed on url: {})".format( "Login failed. Check email/password. "
login_url "(Failed on url: {})".format(login_url)
)
) )
def logout(self): def logout(self):
@@ -525,7 +522,6 @@ class Client(object):
:return: True if the action was successful :return: True if the action was successful
:rtype: bool :rtype: bool
""" """
if not hasattr(self, "fb_h"): if not hasattr(self, "fb_h"):
h_r = self._post(self.req_url.MODERN_SETTINGS_MENU, {"pmid": "4"}) h_r = self._post(self.req_url.MODERN_SETTINGS_MENU, {"pmid": "4"})
self.fb_h = re.search(r'name=\\"h\\" value=\\"(.*?)\\"', h_r.text).group(1) self.fb_h = re.search(r'name=\\"h\\" value=\\"(.*?)\\"', h_r.text).group(1)
@@ -586,15 +582,8 @@ class Client(object):
""" """
def _forcedFetch(self, thread_id, mid): def _forcedFetch(self, thread_id, mid):
j = self.graphql_request( params = {"thread_and_message_id": {"thread_id": thread_id, "message_id": mid}}
GraphQL( return self.graphql_request(GraphQL(doc_id="1768656253222505", params=params))
doc_id="1768656253222505",
params={
"thread_and_message_id": {"thread_id": thread_id, "message_id": mid}
},
)
)
return j
def fetchThreads(self, thread_location, before=None, after=None, limit=None): def fetchThreads(self, thread_location, before=None, after=None, limit=None):
""" """
@@ -671,8 +660,6 @@ class Client(object):
and user_id not in users_to_fetch and user_id not in users_to_fetch
): ):
users_to_fetch.append(user_id) users_to_fetch.append(user_id)
else:
pass
for user_id, user in self.fetchUserInfo(*users_to_fetch).items(): for user_id, user in self.fetchUserInfo(*users_to_fetch).items():
users.append(user) users.append(user)
return users return users
@@ -685,7 +672,6 @@ class Client(object):
:rtype: list :rtype: list
:raises: FBchatException if request failed :raises: FBchatException if request failed
""" """
data = {"viewer": self.uid} data = {"viewer": self.uid}
j = self._post( j = self._post(
self.req_url.ALL_USERS, query=data, fix_request=True, as_json=True self.req_url.ALL_USERS, query=data, fix_request=True, as_json=True
@@ -700,7 +686,6 @@ class Client(object):
# Skip invalid users # Skip invalid users
continue continue
users.append(User._from_all_fetch(data)) users.append(User._from_all_fetch(data))
return users return users
def searchForUsers(self, name, limit=10): def searchForUsers(self, name, limit=10):
@@ -713,10 +698,8 @@ class Client(object):
:rtype: list :rtype: list
:raises: FBchatException if request failed :raises: FBchatException if request failed
""" """
params = {"search": name, "limit": limit}
j = self.graphql_request( j = self.graphql_request(GraphQL(query=GraphQL.SEARCH_USER, params=params))
GraphQL(query=GraphQL.SEARCH_USER, params={"search": name, "limit": limit})
)
return [User._from_graphql(node) for node in j[name]["users"]["nodes"]] return [User._from_graphql(node) for node in j[name]["users"]["nodes"]]
@@ -729,10 +712,8 @@ class Client(object):
:rtype: list :rtype: list
:raises: FBchatException if request failed :raises: FBchatException if request failed
""" """
params = {"search": name, "limit": limit}
j = self.graphql_request( j = self.graphql_request(GraphQL(query=GraphQL.SEARCH_PAGE, params=params))
GraphQL(query=GraphQL.SEARCH_PAGE, params={"search": name, "limit": limit})
)
return [Page._from_graphql(node) for node in j[name]["pages"]["nodes"]] return [Page._from_graphql(node) for node in j[name]["pages"]["nodes"]]
@@ -746,10 +727,8 @@ class Client(object):
:rtype: list :rtype: list
:raises: FBchatException if request failed :raises: FBchatException if request failed
""" """
params = {"search": name, "limit": limit}
j = self.graphql_request( j = self.graphql_request(GraphQL(query=GraphQL.SEARCH_GROUP, params=params))
GraphQL(query=GraphQL.SEARCH_GROUP, params={"search": name, "limit": limit})
)
return [Group._from_graphql(node) for node in j["viewer"]["groups"]["nodes"]] return [Group._from_graphql(node) for node in j["viewer"]["groups"]["nodes"]]
@@ -763,12 +742,8 @@ class Client(object):
:rtype: list :rtype: list
:raises: FBchatException if request failed :raises: FBchatException if request failed
""" """
params = {"search": name, "limit": limit}
j = self.graphql_request( j = self.graphql_request(GraphQL(query=GraphQL.SEARCH_THREAD, params=params))
GraphQL(
query=GraphQL.SEARCH_THREAD, params={"search": name, "limit": limit}
)
)
rtn = [] rtn = []
for node in j[name]["threads"]["nodes"]: for node in j[name]["threads"]["nodes"]:
@@ -784,9 +759,7 @@ class Client(object):
pass pass
else: else:
log.warning( log.warning(
"Unknown __typename: {} in {}".format( "Unknown type {} in {}".format(repr(node["__typename"]), node)
repr(node["__typename"]), node
)
) )
return rtn return rtn
@@ -864,21 +837,15 @@ class Client(object):
j = self._post( j = self._post(
self.req_url.SEARCH_MESSAGES, data, fix_request=True, as_json=True self.req_url.SEARCH_MESSAGES, data, fix_request=True, as_json=True
) )
result = j["payload"]["search_snippets"][query] result = j["payload"]["search_snippets"][query]
if fetch_messages: if fetch_messages:
return { search_method = self.searchForMessages
thread_id: self.searchForMessages(
query, limit=message_limit, thread_id=thread_id
)
for thread_id in result
}
else: else:
search_method = self.searchForMessageIDs
return { return {
thread_id: self.searchForMessageIDs( thread_id: search_method(query, limit=message_limit, thread_id=thread_id)
query, limit=message_limit, thread_id=thread_id
)
for thread_id in result for thread_id in result
} }
@@ -931,14 +898,13 @@ class Client(object):
:rtype: dict :rtype: dict
:raises: FBchatException if request failed :raises: FBchatException if request failed
""" """
threads = self.fetchThreadInfo(*user_ids) threads = self.fetchThreadInfo(*user_ids)
users = {} users = {}
for k in threads: for id_, thread in threads.items():
if threads[k].type == ThreadType.USER: if thread.type == ThreadType.USER:
users[k] = threads[k] users[id_] = thread
else: else:
raise FBchatUserError("Thread {} was not a user".format(threads[k])) raise FBchatUserError("Thread {} was not a user".format(thread))
return users return users
@@ -954,14 +920,13 @@ class Client(object):
:rtype: dict :rtype: dict
:raises: FBchatException if request failed :raises: FBchatException if request failed
""" """
threads = self.fetchThreadInfo(*page_ids) threads = self.fetchThreadInfo(*page_ids)
pages = {} pages = {}
for k in threads: for id_, thread in threads.items():
if threads[k].type == ThreadType.PAGE: if thread.type == ThreadType.PAGE:
pages[k] = threads[k] pages[id_] = thread
else: else:
raise FBchatUserError("Thread {} was not a page".format(threads[k])) raise FBchatUserError("Thread {} was not a page".format(thread))
return pages return pages
@@ -974,14 +939,13 @@ class Client(object):
:rtype: dict :rtype: dict
:raises: FBchatException if request failed :raises: FBchatException if request failed
""" """
threads = self.fetchThreadInfo(*group_ids) threads = self.fetchThreadInfo(*group_ids)
groups = {} groups = {}
for k in threads: for id_, thread in threads.items():
if threads[k].type == ThreadType.GROUP: if thread.type == ThreadType.GROUP:
groups[k] = threads[k] groups[id_] = thread
else: else:
raise FBchatUserError("Thread {} was not a group".format(threads[k])) raise FBchatUserError("Thread {} was not a group".format(thread))
return groups return groups
@@ -997,21 +961,16 @@ class Client(object):
:rtype: dict :rtype: dict
:raises: FBchatException if request failed :raises: FBchatException if request failed
""" """
queries = [] queries = []
for thread_id in thread_ids: for thread_id in thread_ids:
queries.append(
GraphQL(
doc_id="2147762685294928",
params = { params = {
"id": thread_id, "id": thread_id,
"message_limit": 0, "message_limit": 0,
"load_messages": False, "load_messages": False,
"load_read_receipts": False, "load_read_receipts": False,
"before": None, "before": None,
}, }
) queries.append(GraphQL(doc_id="2147762685294928", params=params))
)
j = self.graphql_requests(*queries) j = self.graphql_requests(*queries)
@@ -1067,33 +1026,26 @@ class Client(object):
:rtype: list :rtype: list
:raises: FBchatException if request failed :raises: FBchatException if request failed
""" """
thread_id, thread_type = self._getThread(thread_id, None) thread_id, thread_type = self._getThread(thread_id, None)
j = self.graphql_request(
GraphQL(
doc_id="1386147188135407",
params = { params = {
"id": thread_id, "id": thread_id,
"message_limit": limit, "message_limit": limit,
"load_messages": True, "load_messages": True,
"load_read_receipts": True, "load_read_receipts": True,
"before": before, "before": before,
}, }
) j = self.graphql_request(GraphQL(doc_id="1860982147341344", params=params))
)
if j.get("message_thread") is None: if j.get("message_thread") is None:
raise FBchatException("Could not fetch thread {}: {}".format(thread_id, j)) raise FBchatException("Could not fetch thread {}: {}".format(thread_id, j))
messages = list( messages = [
reversed(
[
Message._from_graphql(message) Message._from_graphql(message)
for message in j["message_thread"]["messages"]["nodes"] for message in j["message_thread"]["messages"]["nodes"]
] ]
) messages.reverse()
)
read_receipts = j["message_thread"]["read_receipts"]["nodes"] read_receipts = j["message_thread"]["read_receipts"]["nodes"]
for message in messages: for message in messages:
@@ -1118,10 +1070,11 @@ class Client(object):
:rtype: list :rtype: list
:raises: FBchatException if request failed :raises: FBchatException if request failed
""" """
if offset is not None: if offset is not None:
log.warning( log.warning(
"Using `offset` in `fetchThreadList` is no longer supported, since Facebook migrated to the use of GraphQL in this request. Use `before` instead" "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: if limit > 20 or limit < 1:
@@ -1132,18 +1085,14 @@ class Client(object):
else: else:
raise FBchatUserError('"thread_location" must be a value of ThreadLocation') raise FBchatUserError('"thread_location" must be a value of ThreadLocation')
j = self.graphql_request(
GraphQL(
doc_id="1349387578499440",
params = { params = {
"limit": limit, "limit": limit,
"tags": [loc_str], "tags": [loc_str],
"before": before, "before": before,
"includeDeliveryReceipts": True, "includeDeliveryReceipts": True,
"includeSeqID": False, "includeSeqID": False,
}, }
) j = self.graphql_request(GraphQL(doc_id="1349387578499440", params=params))
)
rtn = [] rtn = []
for node in j["viewer"]["message_threads"]["nodes"]: for node in j["viewer"]["message_threads"]["nodes"]:
@@ -1172,13 +1121,11 @@ class Client(object):
"last_action_timestamp": now() - 60 * 1000 "last_action_timestamp": now() - 60 * 1000
# 'last_action_timestamp': 0 # 'last_action_timestamp': 0
} }
j = self._post( j = self._post(
self.req_url.UNREAD_THREADS, form, fix_request=True, as_json=True self.req_url.UNREAD_THREADS, form, fix_request=True, as_json=True
) )
payload = j["payload"]["unread_thread_fbids"][0] payload = j["payload"]["unread_thread_fbids"][0]
return payload["thread_fbids"] + payload["other_user_fbids"] return payload["thread_fbids"] + payload["other_user_fbids"]
def fetchUnseen(self): def fetchUnseen(self):
@@ -1194,7 +1141,6 @@ class Client(object):
) )
payload = j["payload"]["unseen_thread_fbids"][0] payload = j["payload"]["unseen_thread_fbids"][0]
return payload["thread_fbids"] + payload["other_user_fbids"] return payload["thread_fbids"] + payload["other_user_fbids"]
def fetchImageUrl(self, image_id): def fetchImageUrl(self, image_id):
@@ -1207,8 +1153,9 @@ class Client(object):
:raises: FBchatException if request failed :raises: FBchatException if request failed
""" """
image_id = str(image_id) image_id = str(image_id)
j = check_request( data = {"photo_id": str(image_id)}
self._get(ReqUrl.ATTACHMENT_PHOTO, query={"photo_id": str(image_id)}) j = self._get(
ReqUrl.ATTACHMENT_PHOTO, query=data, fix_request=True, as_json=True
) )
url = get_jsmods_require(j, 3) url = get_jsmods_require(j, 3)
@@ -1239,11 +1186,9 @@ class Client(object):
:raises: FBchatException if request failed :raises: FBchatException if request failed
""" """
data = {"question_id": poll_id} data = {"question_id": poll_id}
j = self._post( j = self._post(
self.req_url.GET_POLL_OPTIONS, data, fix_request=True, as_json=True self.req_url.GET_POLL_OPTIONS, data, fix_request=True, as_json=True
) )
return [PollOption._from_graphql(m) for m in j["payload"]] return [PollOption._from_graphql(m) for m in j["payload"]]
def fetchPlanInfo(self, plan_id): def fetchPlanInfo(self, plan_id):
@@ -1316,7 +1261,7 @@ class Client(object):
timestamp = now() timestamp = now()
data = { data = {
"client": self.client, "client": self.client,
"author": "fbid:" + str(self.uid), "author": "fbid:{}".format(self.uid),
"timestamp": timestamp, "timestamp": timestamp,
"source": "source:chat:web", "source": "source:chat:web",
"offline_threading_id": messageAndOTID, "offline_threading_id": messageAndOTID,
@@ -1374,6 +1319,9 @@ class Client(object):
xmd["quick_replies"] = xmd["quick_replies"][0] xmd["quick_replies"] = xmd["quick_replies"][0]
data["platform_xmd"] = json.dumps(xmd) data["platform_xmd"] = json.dumps(xmd)
if message.reply_to_id:
data["replied_to_message_id"] = message.reply_to_id
return data return data
def _doSendRequest(self, data, get_thread_id=False): def _doSendRequest(self, data, get_thread_id=False):
@@ -1399,9 +1347,8 @@ class Client(object):
return message_ids[0][0] return message_ids[0][0]
except (KeyError, IndexError, TypeError) as e: except (KeyError, IndexError, TypeError) as e:
raise FBchatException( raise FBchatException(
"Error when sending message: No message IDs could be found: {}".format( "Error when sending message: "
j "No message IDs could be found: {}".format(j)
)
) )
def send(self, message, thread_id=None, thread_type=ThreadType.USER): def send(self, message, thread_id=None, thread_type=ThreadType.USER):
@@ -1420,7 +1367,6 @@ class Client(object):
data = self._getSendData( data = self._getSendData(
message=message, thread_id=thread_id, thread_type=thread_type message=message, thread_id=thread_id, thread_type=thread_type
) )
return self._doSendRequest(data) return self._doSendRequest(data)
def sendMessage(self, message, thread_id=None, thread_type=ThreadType.USER): def sendMessage(self, message, thread_id=None, thread_type=ThreadType.USER):
@@ -1713,15 +1659,11 @@ class Client(object):
Deprecated. Use :func:`fbchat.Client._sendFiles` instead Deprecated. Use :func:`fbchat.Client._sendFiles` instead
""" """
if is_gif: if is_gif:
return self._sendFiles( mimetype = "image/gif"
files=[(image_id, "image/png")],
message=message,
thread_id=thread_id,
thread_type=thread_type,
)
else: else:
mimetype = "image/png"
return self._sendFiles( return self._sendFiles(
files=[(image_id, "image/gif")], files=[(image_id, mimetype)],
message=message, message=message,
thread_id=thread_id, thread_id=thread_id,
thread_type=thread_type, thread_type=thread_type,
@@ -1801,8 +1743,8 @@ class Client(object):
) )
else: else:
data[ data[
"log_message_data[added_participants][" + str(i) + "]" "log_message_data[added_participants][{}]".format(i)
] = "fbid:" + str(user_id) ] = "fbid:{}".format(user_id)
return self._doSendRequest(data) return self._doSendRequest(data)
@@ -1814,11 +1756,9 @@ class Client(object):
:param thread_id: Group ID to remove people from. See :ref:`intro_threads` :param thread_id: Group ID to remove people from. See :ref:`intro_threads`
:raises: FBchatException if request failed :raises: FBchatException if request failed
""" """
thread_id, thread_type = self._getThread(thread_id, None) thread_id, thread_type = self._getThread(thread_id, None)
data = {"uid": user_id, "tid": thread_id} data = {"uid": user_id, "tid": thread_id}
j = self._post(self.req_url.REMOVE_USER, data, fix_request=True, as_json=True) j = self._post(self.req_url.REMOVE_USER, data, fix_request=True, as_json=True)
def _adminStatus(self, admin_ids, admin, thread_id=None): def _adminStatus(self, admin_ids, admin, thread_id=None):
@@ -1829,7 +1769,7 @@ class Client(object):
admin_ids = require_list(admin_ids) admin_ids = require_list(admin_ids)
for i, admin_id in enumerate(admin_ids): for i, admin_id in enumerate(admin_ids):
data["admin_ids[" + str(i) + "]"] = str(admin_id) data["admin_ids[{}]".format(i)] = str(admin_id)
j = self._post(self.req_url.SAVE_ADMINS, data, fix_request=True, as_json=True) j = self._post(self.req_url.SAVE_ADMINS, data, fix_request=True, as_json=True)
@@ -1864,7 +1804,6 @@ class Client(object):
thread_id, thread_type = self._getThread(thread_id, None) thread_id, thread_type = self._getThread(thread_id, None)
data = {"set_mode": int(require_admin_approval), "thread_fbid": thread_id} data = {"set_mode": int(require_admin_approval), "thread_fbid": thread_id}
j = self._post(self.req_url.APPROVAL_MODE, data, fix_request=True, as_json=True) j = self._post(self.req_url.APPROVAL_MODE, data, fix_request=True, as_json=True)
def _usersApproval(self, user_ids, approve, thread_id=None): def _usersApproval(self, user_ids, approve, thread_id=None):
@@ -1872,11 +1811,7 @@ class Client(object):
user_ids = list(require_list(user_ids)) user_ids = list(require_list(user_ids))
j = self.graphql_request( data = {
GraphQL(
doc_id="1574519202665847",
params={
"data": {
"client_mutation_id": "0", "client_mutation_id": "0",
"actor_id": self.uid, "actor_id": self.uid,
"thread_fbid": thread_id, "thread_fbid": thread_id,
@@ -1884,8 +1819,8 @@ class Client(object):
"response": "ACCEPT" if approve else "DENY", "response": "ACCEPT" if approve else "DENY",
"surface": "ADMIN_MODEL_APPROVAL_CENTER", "surface": "ADMIN_MODEL_APPROVAL_CENTER",
} }
}, j = self.graphql_request(
) GraphQL(doc_id="1574519202665847", params={"data": data})
) )
def acceptUsersToGroup(self, user_ids, thread_id=None): def acceptUsersToGroup(self, user_ids, thread_id=None):
@@ -1916,7 +1851,6 @@ class Client(object):
:param thread_id: User/Group ID to change image. See :ref:`intro_threads` :param thread_id: User/Group ID to change image. See :ref:`intro_threads`
:raises: FBchatException if request failed :raises: FBchatException if request failed
""" """
thread_id, thread_type = self._getThread(thread_id, None) thread_id, thread_type = self._getThread(thread_id, None)
data = {"thread_image_id": image_id, "thread_id": thread_id} data = {"thread_image_id": image_id, "thread_id": thread_id}
@@ -1932,7 +1866,6 @@ class Client(object):
:param thread_id: User/Group ID to change image. See :ref:`intro_threads` :param thread_id: User/Group ID to change image. See :ref:`intro_threads`
:raises: FBchatException if request failed :raises: FBchatException if request failed
""" """
(image_id, mimetype), = self._upload(get_files_from_urls([image_url])) (image_id, mimetype), = self._upload(get_files_from_urls([image_url]))
return self._changeGroupImage(image_id, thread_id) return self._changeGroupImage(image_id, thread_id)
@@ -1944,7 +1877,6 @@ class Client(object):
:param thread_id: User/Group ID to change image. See :ref:`intro_threads` :param thread_id: User/Group ID to change image. See :ref:`intro_threads`
:raises: FBchatException if request failed :raises: FBchatException if request failed
""" """
with get_files_from_paths([image_path]) as files: with get_files_from_paths([image_path]) as files:
(image_id, mimetype), = self._upload(files) (image_id, mimetype), = self._upload(files)
@@ -1961,7 +1893,6 @@ class Client(object):
:type thread_type: models.ThreadType :type thread_type: models.ThreadType
:raises: FBchatException if request failed :raises: FBchatException if request failed
""" """
thread_id, thread_type = self._getThread(thread_id, thread_type) thread_id, thread_type = self._getThread(thread_id, thread_type)
if thread_type == ThreadType.USER: if thread_type == ThreadType.USER:
@@ -1971,7 +1902,6 @@ class Client(object):
) )
data = {"thread_name": title, "thread_id": thread_id} data = {"thread_name": title, "thread_id": thread_id}
j = self._post(self.req_url.THREAD_NAME, data, fix_request=True, as_json=True) j = self._post(self.req_url.THREAD_NAME, data, fix_request=True, as_json=True)
def changeNickname( def changeNickname(
@@ -1994,7 +1924,6 @@ class Client(object):
"participant_id": user_id, "participant_id": user_id,
"thread_or_other_fbid": thread_id, "thread_or_other_fbid": thread_id,
} }
j = self._post( j = self._post(
self.req_url.THREAD_NICKNAME, data, fix_request=True, as_json=True self.req_url.THREAD_NICKNAME, data, fix_request=True, as_json=True
) )
@@ -2014,7 +1943,6 @@ class Client(object):
"color_choice": color.value if color != ThreadColor.MESSENGER_BLUE else "", "color_choice": color.value if color != ThreadColor.MESSENGER_BLUE else "",
"thread_or_other_fbid": thread_id, "thread_or_other_fbid": thread_id,
} }
j = self._post(self.req_url.THREAD_COLOR, data, fix_request=True, as_json=True) j = self._post(self.req_url.THREAD_COLOR, data, fix_request=True, as_json=True)
def changeThreadEmoji(self, emoji, thread_id=None): def changeThreadEmoji(self, emoji, thread_id=None):
@@ -2030,7 +1958,6 @@ class Client(object):
thread_id, thread_type = self._getThread(thread_id, None) thread_id, thread_type = self._getThread(thread_id, None)
data = {"emoji_choice": emoji, "thread_or_other_fbid": thread_id} data = {"emoji_choice": emoji, "thread_or_other_fbid": thread_id}
j = self._post(self.req_url.THREAD_EMOJI, data, fix_request=True, as_json=True) j = self._post(self.req_url.THREAD_EMOJI, data, fix_request=True, as_json=True)
def reactToMessage(self, message_id, reaction): def reactToMessage(self, message_id, reaction):
@@ -2043,19 +1970,13 @@ class Client(object):
:raises: FBchatException if request failed :raises: FBchatException if request failed
""" """
data = { data = {
"doc_id": 1491398900900362,
"variables": json.dumps(
{
"data": {
"action": "ADD_REACTION" if reaction else "REMOVE_REACTION", "action": "ADD_REACTION" if reaction else "REMOVE_REACTION",
"client_mutation_id": "1", "client_mutation_id": "1",
"actor_id": self.uid, "actor_id": self.uid,
"message_id": str(message_id), "message_id": str(message_id),
"reaction": reaction.value if reaction else None, "reaction": reaction.value if reaction else None,
} }
} data = {"doc_id": 1491398900900362, "variables": json.dumps({"data": data})}
),
}
self._post(self.req_url.MESSAGE_REACTION, data, fix_request=True, as_json=True) self._post(self.req_url.MESSAGE_REACTION, data, fix_request=True, as_json=True)
def createPlan(self, plan, thread_id=None): def createPlan(self, plan, thread_id=None):
@@ -2069,23 +1990,16 @@ class Client(object):
""" """
thread_id, thread_type = self._getThread(thread_id, None) thread_id, thread_type = self._getThread(thread_id, None)
full_data = { data = {
"event_type": "EVENT", "event_type": "EVENT",
"event_time": plan.time, "event_time": plan.time,
"title": plan.title, "title": plan.title,
"thread_id": thread_id, "thread_id": thread_id,
"location_id": plan.location_id or "", "location_id": plan.location_id or "",
"location_name": plan.location or "", "location_name": plan.location or "",
"acontext": { "acontext": ACONTEXT,
"action_history": [
{"surface": "messenger_chat_tab", "mechanism": "messenger_composer"}
]
},
} }
j = self._post(self.req_url.PLAN_CREATE, data, fix_request=True, as_json=True)
j = self._post(
self.req_url.PLAN_CREATE, full_data, fix_request=True, as_json=True
)
def editPlan(self, plan, new_plan): def editPlan(self, plan, new_plan):
""" """
@@ -2096,23 +2010,16 @@ class Client(object):
:type plan: models.Plan :type plan: models.Plan
:raises: FBchatException if request failed :raises: FBchatException if request failed
""" """
full_data = { data = {
"event_reminder_id": plan.uid, "event_reminder_id": plan.uid,
"delete": "false", "delete": "false",
"date": new_plan.time, "date": new_plan.time,
"location_name": new_plan.location or "", "location_name": new_plan.location or "",
"location_id": new_plan.location_id or "", "location_id": new_plan.location_id or "",
"title": new_plan.title, "title": new_plan.title,
"acontext": { "acontext": ACONTEXT,
"action_history": [
{"surface": "messenger_chat_tab", "mechanism": "reminder_banner"}
]
},
} }
j = self._post(self.req_url.PLAN_CHANGE, data, fix_request=True, as_json=True)
j = self._post(
self.req_url.PLAN_CHANGE, full_data, fix_request=True, as_json=True
)
def deletePlan(self, plan): def deletePlan(self, plan):
""" """
@@ -2121,19 +2028,8 @@ class Client(object):
:param plan: Plan to delete :param plan: Plan to delete
:raises: FBchatException if request failed :raises: FBchatException if request failed
""" """
full_data = { data = {"event_reminder_id": plan.uid, "delete": "true", "acontext": ACONTEXT}
"event_reminder_id": plan.uid, j = self._post(self.req_url.PLAN_CHANGE, data, fix_request=True, as_json=True)
"delete": "true",
"acontext": {
"action_history": [
{"surface": "messenger_chat_tab", "mechanism": "reminder_banner"}
]
},
}
j = self._post(
self.req_url.PLAN_CHANGE, full_data, fix_request=True, as_json=True
)
def changePlanParticipation(self, plan, take_part=True): def changePlanParticipation(self, plan, take_part=True):
""" """
@@ -2143,30 +2039,21 @@ class Client(object):
:param take_part: Whether to take part in the plan :param take_part: Whether to take part in the plan
:raises: FBchatException if request failed :raises: FBchatException if request failed
""" """
full_data = { data = {
"event_reminder_id": plan.uid, "event_reminder_id": plan.uid,
"guest_state": "GOING" if take_part else "DECLINED", "guest_state": "GOING" if take_part else "DECLINED",
"acontext": { "acontext": ACONTEXT,
"action_history": [
{"surface": "messenger_chat_tab", "mechanism": "reminder_banner"}
]
},
} }
j = self._post( j = self._post(
self.req_url.PLAN_PARTICIPATION, full_data, fix_request=True, as_json=True self.req_url.PLAN_PARTICIPATION, data, fix_request=True, as_json=True
) )
def eventReminder(self, thread_id, time, title, location="", location_id=""): def eventReminder(self, thread_id, time, title, location="", location_id=""):
""" """
Deprecated. Use :func:`fbchat.Client.createPlan` instead Deprecated. Use :func:`fbchat.Client.createPlan` instead
""" """
self.createPlan( plan = Plan(time=time, title=title, location=location, location_id=location_id)
plan=Plan( self.createPlan(plan=plan, thread_id=thread_id)
time=time, title=title, location=location, location_id=location_id
),
thread_id=thread_id,
)
def createPoll(self, poll, thread_id=None): def createPoll(self, poll, thread_id=None):
""" """
@@ -2235,7 +2122,6 @@ class Client(object):
"to": thread_id if thread_type == ThreadType.USER else "", "to": thread_id if thread_type == ThreadType.USER else "",
"source": "mercury-chat", "source": "mercury-chat",
} }
j = self._post(self.req_url.TYPING, data, fix_request=True, as_json=True) j = self._post(self.req_url.TYPING, data, fix_request=True, as_json=True)
""" """
@@ -2436,8 +2322,7 @@ class Client(object):
""" """
thread_id, thread_type = self._getThread(thread_id, None) thread_id, thread_type = self._getThread(thread_id, None)
data = {"mute_settings": str(mute_time), "thread_fbid": thread_id} data = {"mute_settings": str(mute_time), "thread_fbid": thread_id}
r = self._post(self.req_url.MUTE_THREAD, data) content = self._post(self.req_url.MUTE_THREAD, data, fix_request=True)
r.raise_for_status()
def unmuteThread(self, thread_id=None): def unmuteThread(self, thread_id=None):
""" """
@@ -2456,8 +2341,7 @@ class Client(object):
""" """
thread_id, thread_type = self._getThread(thread_id, None) thread_id, thread_type = self._getThread(thread_id, None)
data = {"reactions_mute_mode": int(mute), "thread_fbid": thread_id} data = {"reactions_mute_mode": int(mute), "thread_fbid": thread_id}
r = self._post(self.req_url.MUTE_REACTIONS, data) r = self._post(self.req_url.MUTE_REACTIONS, data, fix_request=True)
r.raise_for_status()
def unmuteThreadReactions(self, thread_id=None): def unmuteThreadReactions(self, thread_id=None):
""" """
@@ -2476,8 +2360,7 @@ class Client(object):
""" """
thread_id, thread_type = self._getThread(thread_id, None) thread_id, thread_type = self._getThread(thread_id, None)
data = {"mentions_mute_mode": int(mute), "thread_fbid": thread_id} data = {"mentions_mute_mode": int(mute), "thread_fbid": thread_id}
r = self._post(self.req_url.MUTE_MENTIONS, data) r = self._post(self.req_url.MUTE_MENTIONS, data, fix_request=True)
r.raise_for_status()
def unmuteThreadMentions(self, thread_id=None): def unmuteThreadMentions(self, thread_id=None):
""" """
@@ -2507,7 +2390,6 @@ class Client(object):
def _pullMessage(self): def _pullMessage(self):
"""Call pull api with seq value to get message data.""" """Call pull api with seq value to get message data."""
data = { data = {
"msgs_recv": 0, "msgs_recv": 0,
"sticky_token": self.sticky, "sticky_token": self.sticky,
@@ -2515,11 +2397,7 @@ class Client(object):
"clientid": self.client_id, "clientid": self.client_id,
"state": "active" if self._markAlive else "offline", "state": "active" if self._markAlive else "offline",
} }
return self._get(self.req_url.STICKY, data, fix_request=True, as_json=True)
j = self._get(self.req_url.STICKY, data, fix_request=True, as_json=True)
self.seq = j.get("seq", "0")
return j
def _parseDelta(self, m): def _parseDelta(self, m):
def getThreadIdAndThreadType(msg_metadata): def getThreadIdAndThreadType(msg_metadata):
@@ -3027,6 +2905,24 @@ class Client(object):
msg=m, 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"])
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 # New message
elif delta.get("class") == "NewMessage": elif delta.get("class") == "NewMessage":
thread_id, thread_type = getThreadIdAndThreadType(metadata) thread_id, thread_type = getThreadIdAndThreadType(metadata)
@@ -3050,6 +2946,7 @@ class Client(object):
def _parseMessage(self, content): def _parseMessage(self, content):
"""Get message and author name from content. May contain multiple messages in the 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: if "lb_info" in content:
self.sticky = content["lb_info"]["sticky"] self.sticky = content["lb_info"]["sticky"]

View File

@@ -86,6 +86,10 @@ class Message(object):
quick_replies = attr.ib(factory=list, converter=lambda x: [] if x is None else x) quick_replies = attr.ib(factory=list, converter=lambda x: [] if x is None else x)
#: Whether the message is unsent (deleted for everyone) #: Whether the message is unsent (deleted for everyone)
unsent = attr.ib(False, init=False) unsent = attr.ib(False, init=False)
#: Message ID you want to reply to
reply_to_id = attr.ib(None)
#: Replied message
replied_to = attr.ib(None, init=False)
@classmethod @classmethod
def formatMentions(cls, text, *args, **kwargs): def formatMentions(cls, text, *args, **kwargs):
@@ -192,6 +196,56 @@ class Message(object):
rtn.unsent = True rtn.unsent = True
elif attachment: elif attachment:
rtn.attachments.append(attachment) rtn.attachments.append(attachment)
if data.get("replied_to_message") is not None:
rtn.replied_to = cls._from_graphql(data["replied_to_message"]["message"])
return rtn
@classmethod
def _from_reply(cls, data):
rtn = cls(
text=data.get("body"),
mentions=[
Mention(m.get("i"), offset=m.get("o"), length=m.get("l"))
for m in json.loads(data.get("data", {}).get("prng", "[]"))
],
emoji_size=EmojiSize._from_tags(data["messageMetadata"].get("tags")),
)
metadata = data.get("messageMetadata", {})
rtn.uid = metadata.get("messageId")
rtn.author = str(metadata.get("actorFbId"))
rtn.timestamp = metadata.get("timestamp")
rtn.unsent = False
if data.get("data", {}).get("platform_xmd"):
quick_replies = json.loads(data["data"]["platform_xmd"]).get(
"quick_replies"
)
if isinstance(quick_replies, list):
rtn.quick_replies = [
_quick_reply.graphql_to_quick_reply(q) for q in quick_replies
]
elif isinstance(quick_replies, dict):
rtn.quick_replies = [
_quick_reply.graphql_to_quick_reply(quick_replies, is_response=True)
]
if data.get("attachments") is not None:
for attachment in data["attachments"]:
attachment = json.loads(attachment["mercuryJSON"])
if attachment.get("blob_attachment"):
rtn.attachments.append(
_file.graphql_to_attachment(attachment["blob_attachment"])
)
if attachment.get("extensible_attachment"):
extensible_attachment = graphql_to_extensible_attachment(
attachment["extensible_attachment"]
)
if isinstance(extensible_attachment, _attachment.UnsentMessage):
rtn.unsent = True
else:
rtn.attachments.append(extensible_attachment)
if attachment.get("sticker_attachment"):
rtn.sticker = _sticker.Sticker._from_graphql(
attachment["sticker_attachment"]
)
return rtn return rtn
@classmethod @classmethod

View File

@@ -41,6 +41,11 @@ class ThreadColor(Enum):
CAMEO = "#d4a88c" CAMEO = "#d4a88c"
BRILLIANT_ROSE = "#ff5ca1" BRILLIANT_ROSE = "#ff5ca1"
BILOBA_FLOWER = "#a695c7" BILOBA_FLOWER = "#a695c7"
TICKLE_ME_PINK = "#ff7ca8"
MALACHITE = "#1adb5b"
RUBY = "#f01d6a"
DARK_TANGERINE = "#ff9c19"
BRIGHT_TURQUOISE = "#0edcde"
@classmethod @classmethod
def _from_graphql(cls, color): def _from_graphql(cls, color):