diff --git a/docs/conf.py b/docs/conf.py index eac4081..e9ca33e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,6 +19,7 @@ import os import sys + sys.path.insert(0, os.path.abspath('..')) import fbchat @@ -39,7 +40,7 @@ extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', - 'sphinx.ext.viewcode' + 'sphinx.ext.viewcode', ] # Add any paths that contain templates here, relative to this directory. @@ -121,15 +122,12 @@ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -138,20 +136,14 @@ latex_elements = { # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, project + '.tex', title, - author, 'manual'), -] +latex_documents = [(master_doc, project + '.tex', title, author, 'manual')] # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, project, title, - [author], 1) -] +man_pages = [(master_doc, project, title, [author], 1)] # -- Options for Texinfo output ------------------------------------------- @@ -160,9 +152,7 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, project, title, - author, project, description, - 'Miscellaneous'), + (master_doc, project, title, author, project, description, 'Miscellaneous') ] @@ -170,7 +160,6 @@ texinfo_documents = [ intersphinx_mapping = {'https://docs.python.org/3/': None} - add_function_parentheses = False html_theme_options = { @@ -178,12 +167,10 @@ html_theme_options = { 'github_user': 'carpedm20', 'github_repo': project, 'github_banner': True, - 'show_related': False + 'show_related': False, } -html_sidebars = { - '**': ['sidebar.html', 'searchbox.html'] -} +html_sidebars = {'**': ['sidebar.html', 'searchbox.html']} html_show_sphinx = False html_show_sourcelink = False diff --git a/examples/echobot.py b/examples/echobot.py index 1d93407..d03159e 100644 --- a/examples/echobot.py +++ b/examples/echobot.py @@ -14,5 +14,6 @@ class EchoBot(Client): if author_id != self.uid: self.send(message_object, thread_id=thread_id, thread_type=thread_type) + client = EchoBot("", "") client.listen() diff --git a/examples/interract.py b/examples/interract.py index 0b4ddb9..bb5771f 100644 --- a/examples/interract.py +++ b/examples/interract.py @@ -12,22 +12,48 @@ thread_type = ThreadType.GROUP client.send(Message(text=''), thread_id=thread_id, thread_type=thread_type) # Will send the default `like` emoji -client.send(Message(emoji_size=EmojiSize.LARGE), thread_id=thread_id, thread_type=thread_type) +client.send( + Message(emoji_size=EmojiSize.LARGE), thread_id=thread_id, thread_type=thread_type +) # Will send the emoji `👍` -client.send(Message(text='👍', emoji_size=EmojiSize.LARGE), thread_id=thread_id, thread_type=thread_type) +client.send( + Message(text='👍', emoji_size=EmojiSize.LARGE), + thread_id=thread_id, + thread_type=thread_type, +) # Will send the sticker with ID `767334476626295` -client.send(Message(sticker=Sticker('767334476626295')), thread_id=thread_id, thread_type=thread_type) +client.send( + Message(sticker=Sticker('767334476626295')), + thread_id=thread_id, + thread_type=thread_type, +) # Will send a message with a mention -client.send(Message(text='This is a @mention', mentions=[Mention(thread_id, offset=10, length=8)]), thread_id=thread_id, thread_type=thread_type) +client.send( + Message( + text='This is a @mention', mentions=[Mention(thread_id, offset=10, length=8)] + ), + thread_id=thread_id, + thread_type=thread_type, +) # Will send the image located at `` -client.sendLocalImage('', message=Message(text='This is a local image'), thread_id=thread_id, thread_type=thread_type) +client.sendLocalImage( + '', + message=Message(text='This is a local image'), + thread_id=thread_id, + thread_type=thread_type, +) # Will download the image at the url ``, and then send it -client.sendRemoteImage('', message=Message(text='This is a remote image'), thread_id=thread_id, thread_type=thread_type) +client.sendRemoteImage( + '', + message=Message(text='This is a remote image'), + thread_id=thread_id, + thread_type=thread_type, +) # Only do these actions if the thread is a group @@ -39,17 +65,23 @@ if thread_type == ThreadType.GROUP: client.addUsersToGroup('', thread_id=thread_id) # Will add the users with IDs `<1st user id>`, `<2nd user id>` and `<3th user id>` to the thread - client.addUsersToGroup(['<1st user id>', '<2nd user id>', '<3rd user id>'], thread_id=thread_id) + client.addUsersToGroup( + ['<1st user id>', '<2nd user id>', '<3rd user id>'], thread_id=thread_id + ) # Will change the nickname of the user `` to `` -client.changeNickname('', '', thread_id=thread_id, thread_type=thread_type) +client.changeNickname( + '', '', thread_id=thread_id, thread_type=thread_type +) # Will change the title of the thread to `` client.changeThreadTitle('<title>', thread_id=thread_id, thread_type=thread_type) # Will set the typing status of the thread to `TYPING` -client.setTypingStatus(TypingStatus.TYPING, thread_id=thread_id, thread_type=thread_type) +client.setTypingStatus( + TypingStatus.TYPING, thread_id=thread_id, thread_type=thread_type +) # Will change the thread color to `MESSENGER_BLUE` client.changeThreadColor(ThreadColor.MESSENGER_BLUE, thread_id=thread_id) diff --git a/examples/keepbot.py b/examples/keepbot.py index 1189f7b..e881940 100644 --- a/examples/keepbot.py +++ b/examples/keepbot.py @@ -14,18 +14,23 @@ old_nicknames = { '12345678901': "User nr. 1's nickname", '12345678902': "User nr. 2's nickname", '12345678903': "User nr. 3's nickname", - '12345678904': "User nr. 4's nickname" + '12345678904': "User nr. 4's nickname", } + class KeepBot(Client): def onColorChange(self, author_id, new_color, thread_id, thread_type, **kwargs): if old_thread_id == thread_id and old_color != new_color: - log.info("{} changed the thread color. It will be changed back".format(author_id)) + log.info( + "{} changed the thread color. It will be changed back".format(author_id) + ) self.changeThreadColor(old_color, thread_id=thread_id) def onEmojiChange(self, author_id, new_emoji, thread_id, thread_type, **kwargs): if old_thread_id == thread_id and new_emoji != old_emoji: - log.info("{} changed the thread emoji. It will be changed back".format(author_id)) + log.info( + "{} changed the thread emoji. It will be changed back".format(author_id) + ) self.changeThreadEmoji(old_emoji, thread_id=thread_id) def onPeopleAdded(self, added_ids, author_id, thread_id, **kwargs): @@ -36,19 +41,43 @@ class KeepBot(Client): def onPersonRemoved(self, removed_id, author_id, thread_id, **kwargs): # No point in trying to add ourself - if old_thread_id == thread_id and removed_id != self.uid and author_id != self.uid: + if ( + old_thread_id == thread_id + and removed_id != self.uid + and author_id != self.uid + ): log.info("{} got removed. They will be re-added".format(removed_id)) self.addUsersToGroup(removed_id, thread_id=thread_id) def onTitleChange(self, author_id, new_title, thread_id, thread_type, **kwargs): if old_thread_id == thread_id and old_title != new_title: - log.info("{} changed the thread title. It will be changed back".format(author_id)) - self.changeThreadTitle(old_title, thread_id=thread_id, thread_type=thread_type) + log.info( + "{} changed the thread title. It will be changed back".format(author_id) + ) + self.changeThreadTitle( + old_title, thread_id=thread_id, thread_type=thread_type + ) + + def onNicknameChange( + self, author_id, changed_for, new_nickname, thread_id, thread_type, **kwargs + ): + if ( + old_thread_id == thread_id + and changed_for in old_nicknames + and old_nicknames[changed_for] != new_nickname + ): + log.info( + "{} changed {}'s' nickname. It will be changed back".format( + author_id, changed_for + ) + ) + self.changeNickname( + old_nicknames[changed_for], + changed_for, + thread_id=thread_id, + thread_type=thread_type, + ) - def onNicknameChange(self, author_id, changed_for, new_nickname, thread_id, thread_type, **kwargs): - if old_thread_id == thread_id and changed_for in old_nicknames and old_nicknames[changed_for] != new_nickname: - log.info("{} changed {}'s' nickname. It will be changed back".format(author_id, changed_for)) - self.changeNickname(old_nicknames[changed_for], changed_for, thread_id=thread_id, thread_type=thread_type) client = KeepBot("<email>", "<password>") client.listen() diff --git a/examples/removebot.py b/examples/removebot.py index b6e7d51..b14532b 100644 --- a/examples/removebot.py +++ b/examples/removebot.py @@ -3,6 +3,7 @@ from fbchat import log, Client from fbchat.models import * + class RemoveBot(Client): def onMessage(self, author_id, message_object, thread_id, thread_type, **kwargs): # We can only kick people from group chats, so no need to try if it's a user chat @@ -11,7 +12,14 @@ class RemoveBot(Client): self.removeUserFromGroup(author_id, thread_id=thread_id) else: # Sends the data to the inherited onMessage, so that we can still see when a message is recieved - super(RemoveBot, self).onMessage(author_id=author_id, message_object=message_object, thread_id=thread_id, thread_type=thread_type, **kwargs) + super(RemoveBot, self).onMessage( + author_id=author_id, + message_object=message_object, + thread_id=thread_id, + thread_type=thread_type, + **kwargs + ) + client = RemoveBot("<email>", "<password>") client.listen() diff --git a/fbchat/__init__.py b/fbchat/__init__.py index be12b28..4e5a05a 100644 --- a/fbchat/__init__.py +++ b/fbchat/__init__.py @@ -19,6 +19,4 @@ __license__ = 'BSD 3-Clause' __author__ = 'Taehoon Kim; Moreels Pieter-Jan; Mads Marquart' __email__ = 'carpedm20@gmail.com' -__all__ = [ - 'Client', -] +__all__ = ['Client'] diff --git a/fbchat/client.py b/fbchat/client.py index 337f152..af85344 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -13,6 +13,7 @@ from .models import * from .graphql import * import time import json + try: from urllib.parse import urlparse, parse_qs except ImportError: @@ -37,7 +38,15 @@ class Client(object): Note: Modifying this results in undefined behaviour """ - def __init__(self, email, password, user_agent=None, max_tries=5, session_cookies=None, logging_level=logging.INFO): + def __init__( + self, + email, + password, + user_agent=None, + max_tries=5, + session_cookies=None, + logging_level=logging.INFO, + ): """Initializes and logs in the client :param email: Facebook `email`, `id` or `phone number` @@ -69,17 +78,21 @@ class Client(object): user_agent = choice(USER_AGENTS) self._header = { - 'Content-Type' : 'application/x-www-form-urlencoded', - 'Referer' : self.req_url.BASE, - 'Origin' : self.req_url.BASE, - 'User-Agent' : user_agent, - 'Connection' : 'keep-alive', + 'Content-Type': 'application/x-www-form-urlencoded', + 'Referer': self.req_url.BASE, + 'Origin': self.req_url.BASE, + 'User-Agent': user_agent, + 'Connection': 'keep-alive', } 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) or not self.isLoggedIn(): + if ( + not session_cookies + or not self.setSession(session_cookies) + or not self.isLoggedIn() + ): self.login(email, password, max_tries) else: self.email = email @@ -113,59 +126,141 @@ class Client(object): return True return False - def _get(self, url, query=None, timeout=30, fix_request=False, as_json=False, error_retries=3): + def _get( + self, + url, + query=None, + timeout=30, + fix_request=False, + as_json=False, + error_retries=3, + ): payload = self._generatePayload(query) - r = self._session.get(url, headers=self._header, params=payload, timeout=timeout, verify=self.ssl_verify) + r = self._session.get( + url, + headers=self._header, + params=payload, + timeout=timeout, + verify=self.ssl_verify, + ) if not fix_request: return r try: return check_request(r, as_json=as_json) except FBchatFacebookError as e: if error_retries > 0 and self._fix_fb_errors(e.fb_error_code): - return self._get(url, query=query, timeout=timeout, fix_request=fix_request, as_json=as_json, error_retries=error_retries-1) + return self._get( + url, + query=query, + timeout=timeout, + fix_request=fix_request, + as_json=as_json, + error_retries=error_retries - 1, + ) raise e - def _post(self, url, query=None, timeout=30, fix_request=False, as_json=False, error_retries=3): + def _post( + self, + url, + query=None, + timeout=30, + fix_request=False, + as_json=False, + error_retries=3, + ): payload = self._generatePayload(query) - r = self._session.post(url, headers=self._header, data=payload, timeout=timeout, verify=self.ssl_verify) + r = self._session.post( + url, + headers=self._header, + data=payload, + timeout=timeout, + verify=self.ssl_verify, + ) if not fix_request: return r try: return check_request(r, as_json=as_json) except FBchatFacebookError as e: if error_retries > 0 and self._fix_fb_errors(e.fb_error_code): - return self._post(url, query=query, timeout=timeout, fix_request=fix_request, as_json=as_json, error_retries=error_retries-1) + return self._post( + url, + query=query, + timeout=timeout, + fix_request=fix_request, + as_json=as_json, + error_retries=error_retries - 1, + ) raise e def _graphql(self, payload, error_retries=3): - content = self._post(self.req_url.GRAPHQL, payload, fix_request=True, as_json=False) + 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) + return self._graphql(payload, error_retries=error_retries - 1) raise e def _cleanGet(self, url, query=None, timeout=30, allow_redirects=True): - return self._session.get(url, headers=self._header, params=query, timeout=timeout, verify=self.ssl_verify, - allow_redirects=allow_redirects) + return self._session.get( + url, + headers=self._header, + params=query, + timeout=timeout, + verify=self.ssl_verify, + allow_redirects=allow_redirects, + ) def _cleanPost(self, url, query=None, timeout=30): self.req_counter += 1 - return self._session.post(url, headers=self._header, data=query, timeout=timeout, verify=self.ssl_verify) + return self._session.post( + url, + headers=self._header, + data=query, + timeout=timeout, + verify=self.ssl_verify, + ) - def _postFile(self, url, files=None, query=None, timeout=30, fix_request=False, as_json=False, error_retries=3): - payload=self._generatePayload(query) + def _postFile( + self, + url, + files=None, + query=None, + timeout=30, + fix_request=False, + as_json=False, + error_retries=3, + ): + payload = self._generatePayload(query) # Removes 'Content-Type' from the header - headers = dict((i, self._header[i]) for i in self._header if i != 'Content-Type') - r = self._session.post(url, headers=headers, data=payload, timeout=timeout, files=files, verify=self.ssl_verify) + headers = dict( + (i, self._header[i]) for i in self._header if i != 'Content-Type' + ) + r = self._session.post( + url, + headers=headers, + data=payload, + timeout=timeout, + files=files, + verify=self.ssl_verify, + ) if not fix_request: return r try: return check_request(r, as_json=as_json) except FBchatFacebookError as e: if error_retries > 0 and self._fix_fb_errors(e.fb_error_code): - return self._postFile(url, files=files, query=query, timeout=timeout, fix_request=fix_request, as_json=as_json, error_retries=error_retries-1) + return self._postFile( + url, + files=files, + query=query, + timeout=timeout, + fix_request=fix_request, + as_json=as_json, + error_retries=error_retries - 1, + ) raise e def graphql_requests(self, *queries): @@ -178,11 +273,15 @@ class Client(object): :rtype: tuple """ - return tuple(self._graphql({ - 'method': 'GET', - 'response_format': 'json', - 'queries': graphql_queries_to_json(*queries) - })) + return tuple( + self._graphql( + { + 'method': 'GET', + 'response_format': 'json', + 'queries': graphql_queries_to_json(*queries), + } + ) + ) def graphql_request(self, query): """ @@ -209,7 +308,7 @@ class Client(object): def _postLogin(self): self.payloadDefault = OrderedDict() - self.client_id = hex(int(random()*2147483648))[2:] + self.client_id = hex(int(random() * 2147483648))[2:] self.start_time = now() self.uid = self._session.cookies.get_dict().get('c_user') if self.uid is None: @@ -227,8 +326,7 @@ class Client(object): else: self.fb_dtsg = re.search(r'name="fb_dtsg" value="(.*?)"', r.text).group(1) - - fb_h_element = soup.find("input", {'name':'h'}) + fb_h_element = soup.find("input", {'name': 'h'}) if fb_h_element: self.fb_h = fb_h_element['value'] @@ -236,7 +334,9 @@ class Client(object): self.ttstamp += str(ord(i)) self.ttstamp += '2' # Set default payload - self.payloadDefault['__rev'] = int(r.text.split('"client_revision":',1)[1].split(",",1)[0]) + self.payloadDefault['__rev'] = int( + r.text.split('"client_revision":', 1)[1].split(",", 1)[0] + ) self.payloadDefault['__user'] = self.uid self.payloadDefault['__a'] = '1' self.payloadDefault['ttstamp'] = self.ttstamp @@ -247,7 +347,11 @@ class Client(object): raise FBchatUserError("Email and password not found.") soup = bs(self._get(self.req_url.MOBILE).text, "html.parser") - data = dict((elem['name'], elem['value']) for elem in soup.findAll("input") if elem.has_attr('value') and elem.has_attr('name')) + data = dict( + (elem['name'], elem['value']) + for elem in soup.findAll("input") + if elem.has_attr('value') and elem.has_attr('name') + ) data['email'] = self.email data['pass'] = self.password data['login'] = 'Log In' @@ -255,8 +359,7 @@ class Client(object): r = self._cleanPost(self.req_url.LOGIN, data) # Usually, 'Checkpoint' will refer to 2FA - if ('checkpoint' in r.url - and ('id="approvals_code"' in r.text.lower())): + if 'checkpoint' in r.url and ('id="approvals_code"' in r.text.lower()): r = self._2FA(r) # Sometimes Facebook tries to show the user a "Save Device" dialog @@ -276,8 +379,8 @@ class Client(object): s = self.on2FACode() data['approvals_code'] = s - data['fb_dtsg'] = soup.find("input", {'name':'fb_dtsg'})['value'] - data['nh'] = soup.find("input", {'name':'nh'})['value'] + data['fb_dtsg'] = soup.find("input", {'name': 'fb_dtsg'})['value'] + data['nh'] = soup.find("input", {'name': 'nh'})['value'] data['submit[Submit Code]'] = 'Submit Code' data['codes_submitted'] = 0 log.info('Submitting 2FA code.') @@ -287,37 +390,45 @@ class Client(object): if 'home' in r.url: return r - del(data['approvals_code']) - del(data['submit[Submit Code]']) - del(data['codes_submitted']) + del (data['approvals_code']) + del (data['submit[Submit Code]']) + del (data['codes_submitted']) data['name_action_selected'] = 'save_device' data['submit[Continue]'] = 'Continue' - log.info('Saving browser.') # At this stage, we have dtsg, nh, name_action_selected, submit[Continue] + log.info( + 'Saving browser.' + ) # At this stage, we have dtsg, nh, name_action_selected, submit[Continue] r = self._cleanPost(self.req_url.CHECKPOINT, data) if 'home' in r.url: return r - del(data['name_action_selected']) - log.info('Starting Facebook checkup flow.') # At this stage, we have dtsg, nh, submit[Continue] + del (data['name_action_selected']) + log.info( + 'Starting Facebook checkup flow.' + ) # At this stage, we have dtsg, nh, submit[Continue] r = self._cleanPost(self.req_url.CHECKPOINT, data) if 'home' in r.url: return r - del(data['submit[Continue]']) + del (data['submit[Continue]']) data['submit[This was me]'] = 'This Was Me' - log.info('Verifying login attempt.') # At this stage, we have dtsg, nh, submit[This was me] + log.info( + 'Verifying login attempt.' + ) # At this stage, we have dtsg, nh, submit[This was me] r = self._cleanPost(self.req_url.CHECKPOINT, data) if 'home' in r.url: return r - del(data['submit[This was me]']) + del (data['submit[This was me]']) data['submit[Continue]'] = 'Continue' data['name_action_selected'] = 'save_device' - log.info('Saving device again.') # At this stage, we have dtsg, nh, submit[Continue], name_action_selected + log.info( + 'Saving device again.' + ) # At this stage, we have dtsg, nh, submit[Continue], name_action_selected r = self._cleanPost(self.req_url.CHECKPOINT, data) return r @@ -355,7 +466,9 @@ class Client(object): try: # Load cookies into current session - self._session.cookies = requests.cookies.merge_cookies(self._session.cookies, session_cookies) + self._session.cookies = requests.cookies.merge_cookies( + self._session.cookies, session_cookies + ) self._postLogin() except Exception as e: log.exception('Failed loading session') @@ -384,17 +497,25 @@ class Client(object): self.email = email self.password = password - for i in range(1, max_tries+1): + for i in range(1, max_tries + 1): login_successful, login_url = self._login() if not login_successful: - log.warning('Attempt #{} failed{}'.format(i, {True:', retrying'}.get(i < max_tries, ''))) + log.warning( + 'Attempt #{} failed{}'.format( + i, {True: ', retrying'}.get(i < max_tries, '') + ) + ) time.sleep(1) continue else: self.onLoggedIn(email=email) break else: - raise FBchatUserError('Login failed. Check email/password. (Failed on url: {})'.format(login_url)) + raise FBchatUserError( + 'Login failed. Check email/password. (Failed on url: {})'.format( + login_url + ) + ) def logout(self): """ @@ -409,10 +530,7 @@ class Client(object): 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) - data = { - 'ref': "mb", - 'h': self.fb_h - } + data = {'ref': "mb", 'h': self.fb_h} r = self._get(self.req_url.LOGOUT, data) @@ -468,12 +586,14 @@ class Client(object): """ def _forcedFetch(self, thread_id, mid): - j = self.graphql_request(GraphQL(doc_id='1768656253222505', params={ - 'thread_and_message_id': { - 'thread_id': thread_id, - 'message_id': mid - } - })) + j = self.graphql_request( + GraphQL( + 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): @@ -498,9 +618,9 @@ class Client(object): break # fetchThreadList returns at max 20 threads before last_thread_timestamp (included) - candidates = self.fetchThreadList(before=last_thread_timestamp, - thread_location=thread_location - ) + candidates = self.fetchThreadList( + before=last_thread_timestamp, thread_location=thread_location + ) if len(candidates) > 1: threads += candidates[1:] @@ -510,16 +630,18 @@ class Client(object): 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): + 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): + 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: @@ -544,7 +666,10 @@ class Client(object): 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: + 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) else: pass @@ -561,10 +686,10 @@ class Client(object): :raises: FBchatException if request failed """ - data = { - 'viewer': self.uid, - } - j = self._post(self.req_url.ALL_USERS, query=data, fix_request=True, as_json=True) + data = {'viewer': self.uid} + j = self._post( + self.req_url.ALL_USERS, query=data, fix_request=True, as_json=True + ) if j.get('payload') is None: raise FBchatException('Missing payload while fetching users: {}'.format(j)) @@ -576,7 +701,17 @@ class Client(object): if k['id'] in ['0', 0]: # Skip invalid users pass - users.append(User(k['id'], first_name=k.get('firstName'), url=k.get('uri'), photo=k.get('thumbSrc'), name=k.get('name'), is_friend=k.get('is_friend'), gender=GENDERS.get(k.get('gender')))) + users.append( + User( + k['id'], + first_name=k.get('firstName'), + url=k.get('uri'), + photo=k.get('thumbSrc'), + name=k.get('name'), + is_friend=k.get('is_friend'), + gender=GENDERS.get(k.get('gender')), + ) + ) return users @@ -591,7 +726,9 @@ class Client(object): :raises: FBchatException if request failed """ - j = self.graphql_request(GraphQL(query=GraphQL.SEARCH_USER, params={'search': name, 'limit': limit})) + j = self.graphql_request( + GraphQL(query=GraphQL.SEARCH_USER, params={'search': name, 'limit': limit}) + ) return [graphql_to_user(node) for node in j[name]['users']['nodes']] @@ -605,7 +742,9 @@ class Client(object): :raises: FBchatException if request failed """ - j = self.graphql_request(GraphQL(query=GraphQL.SEARCH_PAGE, params={'search': name, 'limit': limit})) + j = self.graphql_request( + GraphQL(query=GraphQL.SEARCH_PAGE, params={'search': name, 'limit': limit}) + ) return [graphql_to_page(node) for node in j[name]['pages']['nodes']] @@ -620,7 +759,9 @@ class Client(object): :raises: FBchatException if request failed """ - j = self.graphql_request(GraphQL(query=GraphQL.SEARCH_GROUP, params={'search': name, 'limit': limit})) + j = self.graphql_request( + GraphQL(query=GraphQL.SEARCH_GROUP, params={'search': name, 'limit': limit}) + ) return [graphql_to_group(node) for node in j['viewer']['groups']['nodes']] @@ -635,7 +776,11 @@ class Client(object): :raises: FBchatException if request failed """ - j = self.graphql_request(GraphQL(query=GraphQL.SEARCH_THREAD, params={'search': name, 'limit': limit})) + j = self.graphql_request( + GraphQL( + query=GraphQL.SEARCH_THREAD, params={'search': name, 'limit': limit} + ) + ) rtn = [] for node in j[name]['threads']['nodes']: @@ -650,7 +795,11 @@ class Client(object): # We don't handle Facebook "Groups" pass else: - log.warning('Unknown __typename: {} in {}'.format(repr(node['__typename']), node)) + log.warning( + 'Unknown __typename: {} in {}'.format( + repr(node['__typename']), node + ) + ) return rtn @@ -677,7 +826,9 @@ class Client(object): "identifier": "thread_fbid", "thread_fbid": thread_id, } - j = self._post(self.req_url.SEARCH_MESSAGES, data, fix_request=True, as_json=True) + j = self._post( + self.req_url.SEARCH_MESSAGES, data, fix_request=True, as_json=True + ) result = j["payload"]["search_snippets"][query] snippets = result[thread_id]["snippets"] if result.get(thread_id) else [] @@ -701,7 +852,9 @@ class Client(object): :rtype: generator :raises: FBchatException if request failed """ - message_ids = self.searchForMessageIDs(query, offset=offset, limit=limit, thread_id=thread_id) + message_ids = self.searchForMessageIDs( + query, offset=offset, limit=limit, thread_id=thread_id + ) for mid in message_ids: yield self.fetchMessageInfo(mid, thread_id) @@ -719,23 +872,30 @@ class Client(object): :rtype: generator :raises: FBchatException if request failed """ - data = { - "query": query, - "snippetLimit": thread_limit - } - j = self._post(self.req_url.SEARCH_MESSAGES, data, fix_request=True, as_json=True) + data = {"query": query, "snippetLimit": thread_limit} + j = self._post( + self.req_url.SEARCH_MESSAGES, data, fix_request=True, as_json=True + ) result = j["payload"]["search_snippets"][query] if fetch_messages: - return {thread_id: self.searchForMessages(query, limit=message_limit, thread_id=thread_id) for thread_id in result} + return { + thread_id: self.searchForMessages( + query, limit=message_limit, thread_id=thread_id + ) + for thread_id in result + } else: - return {thread_id: self.searchForMessageIDs(query, limit=message_limit, thread_id=thread_id) for thread_id in result} + return { + thread_id: self.searchForMessageIDs( + 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) - } + data = {"ids[{}]".format(i): _id for i, _id in enumerate(ids)} j = self._post(self.req_url.INFO, data, fix_request=True, as_json=True) if j.get('payload') is None or j['payload'].get('profiles') is None: @@ -753,7 +913,7 @@ class Client(object): 'is_viewer_friend': k.get('is_friend'), 'gender': k.get('gender'), 'profile_picture': {'uri': k.get('thumbSrc')}, - 'name': k.get('name') + 'name': k.get('name'), } elif k['type'] == 'page': entries[_id] = { @@ -761,10 +921,12 @@ class Client(object): 'type': ThreadType.PAGE, 'url': k.get('uri'), 'profile_picture': {'uri': k.get('thumbSrc')}, - 'name': k.get('name') + 'name': k.get('name'), } else: - raise FBchatException('{} had an unknown thread type: {}'.format(_id, k)) + raise FBchatException( + '{} had an unknown thread type: {}'.format(_id, k) + ) log.debug(entries) return entries @@ -850,13 +1012,18 @@ class Client(object): queries = [] for thread_id in thread_ids: - queries.append(GraphQL(doc_id='2147762685294928', params={ - 'id': thread_id, - 'message_limit': 0, - 'load_messages': False, - 'load_read_receipts': False, - 'before': None - })) + queries.append( + GraphQL( + doc_id='2147762685294928', + params={ + 'id': thread_id, + 'message_limit': 0, + 'load_messages': False, + 'load_read_receipts': False, + 'before': None, + }, + ) + ) j = self.graphql_requests(*queries) @@ -864,13 +1031,15 @@ class Client(object): 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' + '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_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) @@ -891,7 +1060,9 @@ class Client(object): else: rtn[_id] = graphql_to_page(entry) else: - raise FBchatException('{} had an unknown thread type: {}'.format(thread_ids[i], entry)) + raise FBchatException( + '{} had an unknown thread type: {}'.format(thread_ids[i], entry) + ) return rtn @@ -911,18 +1082,30 @@ class Client(object): thread_id, thread_type = self._getThread(thread_id, None) - j = self.graphql_request(GraphQL(doc_id='1386147188135407', params={ - 'id': thread_id, - 'message_limit': limit, - 'load_messages': True, - 'load_read_receipts': True, - 'before': before - })) + j = self.graphql_request( + GraphQL( + doc_id='1386147188135407', + params={ + 'id': thread_id, + 'message_limit': limit, + 'load_messages': True, + 'load_read_receipts': True, + 'before': before, + }, + ) + ) if j.get('message_thread') is None: raise FBchatException('Could not fetch thread {}: {}'.format(thread_id, j)) - messages = list(reversed([graphql_to_message(message) for message in j['message_thread']['messages']['nodes']])) + messages = list( + reversed( + [ + graphql_to_message(message) + for message in j['message_thread']['messages']['nodes'] + ] + ) + ) read_receipts = j['message_thread']['read_receipts']['nodes'] for message in messages: @@ -932,7 +1115,9 @@ class Client(object): return messages - def fetchThreadList(self, offset=None, limit=20, thread_location=ThreadLocation.INBOX, before=None): + 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! @@ -947,7 +1132,9 @@ class Client(object): """ 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') + 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') @@ -957,15 +1144,22 @@ class Client(object): else: raise FBchatUserError('"thread_location" must be a value of ThreadLocation') - j = self.graphql_request(GraphQL(doc_id='1349387578499440', params={ - 'limit': limit, - 'tags': [loc_str], - 'before': before, - 'includeDeliveryReceipts': True, - 'includeSeqID': False - })) + j = self.graphql_request( + GraphQL( + doc_id='1349387578499440', + params={ + 'limit': limit, + 'tags': [loc_str], + 'before': before, + 'includeDeliveryReceipts': True, + 'includeSeqID': False, + }, + ) + ) - return [graphql_to_thread(node) for node in j['viewer']['message_threads']['nodes']] + return [ + graphql_to_thread(node) for node in j['viewer']['message_threads']['nodes'] + ] def fetchUnread(self): """ @@ -978,11 +1172,13 @@ class Client(object): form = { 'folders[0]': 'inbox', 'client': 'mercury', - 'last_action_timestamp': now() - 60*1000 + 'last_action_timestamp': now() - 60 * 1000 # 'last_action_timestamp': 0 } - j = self._post(self.req_url.UNREAD_THREADS, form, fix_request=True, as_json=True) + j = self._post( + self.req_url.UNREAD_THREADS, form, fix_request=True, as_json=True + ) return j['payload']['unread_thread_fbids'][0]['other_user_fbids'] @@ -994,7 +1190,9 @@ class Client(object): :rtype: list :raises: FBchatException if request failed """ - j = self._post(self.req_url.UNSEEN_THREADS, None, fix_request=True, as_json=True) + j = self._post( + self.req_url.UNSEEN_THREADS, None, fix_request=True, as_json=True + ) return j['payload']['unseen_thread_fbids'][0]['other_user_fbids'] @@ -1008,7 +1206,9 @@ class Client(object): :raises: FBchatException if request failed """ image_id = str(image_id) - j = check_request(self._get(ReqUrl.ATTACHMENT_PHOTO, query={'photo_id': str(image_id)})) + j = check_request( + self._get(ReqUrl.ATTACHMENT_PHOTO, query={'photo_id': str(image_id)}) + ) url = get_jsmods_require(j, 3) if url is None: @@ -1038,11 +1238,11 @@ class Client(object): :rtype: list :raises: FBchatException if request failed """ - data = { - "question_id": poll_id - } + data = {"question_id": poll_id} - j = self._post(self.req_url.GET_POLL_OPTIONS, data, fix_request=True, as_json=True) + j = self._post( + self.req_url.GET_POLL_OPTIONS, data, fix_request=True, as_json=True + ) return [graphql_to_poll_option(m) for m in j["payload"]] @@ -1055,9 +1255,7 @@ class Client(object): :rtype: models.Plan :raises: FBchatException if request failed """ - data = { - "event_reminder_id": plan_id - } + data = {"event_reminder_id": plan_id} j = self._post(self.req_url.PLAN_INFO, data, fix_request=True, as_json=True) plan = graphql_to_plan(j["payload"]) return plan @@ -1074,7 +1272,9 @@ class Client(object): :rtype: list """ data = self._getPrivateData() - return [j['phone_number']['universal_number'] for j in data['user']['all_phones']] + return [ + j['phone_number']['universal_number'] for j in data['user']['all_phones'] + ] def getEmails(self): """ @@ -1117,13 +1317,13 @@ class Client(object): timestamp = now() data = { 'client': self.client, - 'author' : 'fbid:' + str(self.uid), - 'timestamp' : timestamp, - 'source' : 'source:chat:web', + 'author': 'fbid:' + str(self.uid), + 'timestamp': timestamp, + 'source': 'source:chat:web', 'offline_threading_id': messageAndOTID, - 'message_id' : messageAndOTID, + 'message_id': messageAndOTID, 'threading_id': generateMessageID(self.client_id), - 'ephemeral_ttl_mode:': '0' + 'ephemeral_ttl_mode:': '0', } # Set recipient @@ -1187,7 +1387,11 @@ class Client(object): self.payloadDefault['fb_dtsg'] = fb_dtsg try: - message_ids = [(action['message_id'], action['thread_fbid']) for action in j['payload']['actions'] if 'message_id' in action] + 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: @@ -1195,7 +1399,11 @@ class Client(object): 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)) + raise FBchatException( + 'Error when sending message: No message IDs could be found: {}'.format( + j + ) + ) def send(self, message, thread_id=None, thread_type=ThreadType.USER): """ @@ -1210,7 +1418,9 @@ class Client(object): :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) + data = self._getSendData( + message=message, thread_id=thread_id, thread_type=thread_type + ) return self._doSendRequest(data) @@ -1218,13 +1428,25 @@ class Client(object): """ Deprecated. Use :func:`fbchat.Client.send` instead """ - return self.send(Message(text=message), thread_id=thread_id, thread_type=thread_type) + return self.send( + Message(text=message), thread_id=thread_id, thread_type=thread_type + ) - def sendEmoji(self, emoji=None, size=EmojiSize.SMALL, thread_id=None, thread_type=ThreadType.USER): + 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) + return self.send( + Message(text=emoji, emoji_size=size), + thread_id=thread_id, + thread_type=thread_type, + ) def wave(self, wave_first=True, thread_id=None, thread_type=None): """ @@ -1240,7 +1462,9 @@ class Client(object): 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_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) @@ -1261,17 +1485,26 @@ class Client(object): """ quick_reply.is_response = True if isinstance(quick_reply, QuickReplyText): - return self.send(Message(text=quick_reply.title, quick_replies=[quick_reply])) + 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.models.LocationAttachment`") - return self.sendLocation(payload, thread_id=thread_id, thread_type=thread_type) + if not isinstance(payload, LocationAttachment): + raise ValueError( + "Payload must be an instance of `fbchat.models.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] + 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] + 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])) @@ -1282,9 +1515,7 @@ class Client(object): :param mid: :ref:`Message ID <intro_message_ids>` of the message to unsend """ - data = { - 'message_id': mid, - } + data = {'message_id': mid} r = self._post(self.req_url.UNSEND, data) r.raise_for_status() @@ -1309,7 +1540,12 @@ class Client(object): :return: :ref:`Message ID <intro_message_ids>` of the sent message :raises: FBchatException if request failed """ - self._sendLocation(location=location, current=True, thread_id=thread_id, thread_type=thread_type) + self._sendLocation( + location=location, + current=True, + thread_id=thread_id, + thread_type=thread_type, + ) def sendPinnedLocation(self, location, thread_id=None, thread_type=None): """ @@ -1323,7 +1559,12 @@ class Client(object): :return: :ref:`Message ID <intro_message_ids>` of the sent message :raises: FBchatException if request failed """ - self._sendLocation(location=location, current=False, thread_id=thread_id, thread_type=thread_type) + self._sendLocation( + location=location, + current=False, + thread_id=thread_id, + thread_type=thread_type, + ) def _upload(self, files, voice_clip=False): """ @@ -1336,25 +1577,40 @@ class Client(object): """ file_dict = {'upload_{}'.format(i): f for i, f in enumerate(files)} - data = { - "voice_clip": voice_clip, - } + data = {"voice_clip": voice_clip} - j = self._postFile(self.req_url.UPLOAD, files=file_dict, query=data, fix_request=True, as_json=True) + j = self._postFile( + self.req_url.UPLOAD, + files=file_dict, + query=data, + fix_request=True, + as_json=True, + ) if len(j['payload']['metadata']) != len(files): - raise FBchatException("Some files could not be uploaded: {}, {}".format(j, 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['payload']['metadata']] + return [ + (data[mimetype_to_key(data['filetype'])], data['filetype']) + for data in j['payload']['metadata'] + ] - def _sendFiles(self, files, message=None, thread_id=None, thread_type=ThreadType.USER): + 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 = 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 @@ -1364,7 +1620,9 @@ class Client(object): return self._doSendRequest(data) - def sendRemoteFiles(self, file_urls, message=None, thread_id=None, thread_type=ThreadType.USER): + def sendRemoteFiles( + self, file_urls, message=None, thread_id=None, thread_type=ThreadType.USER + ): """ Sends files from URLs to a thread @@ -1378,9 +1636,13 @@ class Client(object): """ 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) + return self._sendFiles( + files=files, message=message, thread_id=thread_id, thread_type=thread_type + ) - def sendLocalFiles(self, file_paths, message=None, thread_id=None, thread_type=ThreadType.USER): + def sendLocalFiles( + self, file_paths, message=None, thread_id=None, thread_type=ThreadType.USER + ): """ Sends local files to a thread @@ -1395,9 +1657,13 @@ class Client(object): 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) + return self._sendFiles( + files=files, message=message, thread_id=thread_id, thread_type=thread_type + ) - def sendRemoteVoiceClips(self, clip_urls, message=None, thread_id=None, thread_type=ThreadType.USER): + def sendRemoteVoiceClips( + self, clip_urls, message=None, thread_id=None, thread_type=ThreadType.USER + ): """ Sends voice clips from URLs to a thread @@ -1411,9 +1677,13 @@ class Client(object): """ 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) + return self._sendFiles( + files=files, message=message, thread_id=thread_id, thread_type=thread_type + ) - def sendLocalVoiceClips(self, clip_paths, message=None, thread_id=None, thread_type=ThreadType.USER): + def sendLocalVoiceClips( + self, clip_paths, message=None, thread_id=None, thread_type=ThreadType.USER + ): """ Sends local voice clips to a thread @@ -1428,28 +1698,61 @@ class Client(object): 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) + return self._sendFiles( + files=files, message=message, thread_id=thread_id, thread_type=thread_type + ) - def sendImage(self, image_id, message=None, thread_id=None, thread_type=ThreadType.USER, is_gif=False): + def sendImage( + self, + image_id, + message=None, + thread_id=None, + thread_type=ThreadType.USER, + is_gif=False, + ): """ Deprecated. Use :func:`fbchat.Client._sendFiles` instead """ if is_gif: - return self._sendFiles(files=[(image_id, "image/png")], message=message, thread_id=thread_id, thread_type=thread_type) + return self._sendFiles( + files=[(image_id, "image/png")], + message=message, + thread_id=thread_id, + thread_type=thread_type, + ) else: - return self._sendFiles(files=[(image_id, "image/gif")], message=message, thread_id=thread_id, thread_type=thread_type) + return self._sendFiles( + files=[(image_id, "image/gif")], + message=message, + thread_id=thread_id, + thread_type=thread_type, + ) - def sendRemoteImage(self, image_url, message=None, thread_id=None, thread_type=ThreadType.USER): + 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) + return self.sendRemoteFiles( + file_urls=[image_url], + message=message, + thread_id=thread_id, + thread_type=thread_type, + ) - def sendLocalImage(self, image_path, message=None, thread_id=None, thread_type=ThreadType.USER): + 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) + return self.sendLocalFiles( + file_paths=[image_path], + message=message, + thread_id=thread_id, + thread_type=thread_type, + ) def createGroup(self, message, user_ids): """ @@ -1470,7 +1773,9 @@ class Client(object): 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") + raise FBchatException( + "Error when creating group: No thread_id could be found" + ) return thread_id def addUsersToGroup(self, user_ids, thread_id=None): @@ -1492,9 +1797,13 @@ class Client(object): 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') + raise FBchatUserError( + 'Error when adding users: Cannot add self to group thread' + ) else: - data['log_message_data[added_participants][' + str(i) + ']'] = "fbid:" + str(user_id) + data[ + 'log_message_data[added_participants][' + str(i) + ']' + ] = "fbid:" + str(user_id) return self._doSendRequest(data) @@ -1509,20 +1818,14 @@ class Client(object): 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) 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 - } + data = {"add": admin, "thread_fbid": thread_id} admin_ids = require_list(admin_ids) @@ -1561,10 +1864,7 @@ class Client(object): """ 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) @@ -1573,16 +1873,21 @@ class Client(object): user_ids = list(require_list(user_ids)) - j = self.graphql_request(GraphQL(doc_id='1574519202665847', params={ - '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( + doc_id='1574519202665847', + params={ + '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', + } + }, + ) + ) def acceptUsersToGroup(self, user_ids, thread_id=None): """ @@ -1615,10 +1920,7 @@ class Client(object): 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} j = self._post(self.req_url.THREAD_IMAGE, data, fix_request=True, as_json=True) return image_id @@ -1665,16 +1967,17 @@ class Client(object): 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) + return self.changeNickname( + title, thread_id, thread_id=thread_id, thread_type=thread_type + ) - 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) - def changeNickname(self, nickname, user_id, thread_id=None, thread_type=ThreadType.USER): + def changeNickname( + self, nickname, user_id, thread_id=None, thread_type=ThreadType.USER + ): """ Changes the nickname of a user in a thread @@ -1690,10 +1993,12 @@ class Client(object): data = { 'nickname': nickname, 'participant_id': user_id, - 'thread_or_other_fbid': thread_id + 'thread_or_other_fbid': thread_id, } - j = self._post(self.req_url.THREAD_NICKNAME, data, fix_request=True, as_json=True) + j = self._post( + self.req_url.THREAD_NICKNAME, data, fix_request=True, as_json=True + ) def changeThreadColor(self, color, thread_id=None): """ @@ -1708,7 +2013,7 @@ class Client(object): data = { '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) @@ -1725,10 +2030,7 @@ class Client(object): """ 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) @@ -1743,15 +2045,17 @@ class Client(object): """ data = { "doc_id": 1491398900900362, - "variables": json.dumps({ - "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 + "variables": json.dumps( + { + "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, + } } - }) + ), } self._post(self.req_url.MESSAGE_REACTION, data, fix_request=True, as_json=True) @@ -1768,20 +2072,21 @@ class Client(object): full_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 '', + "event_time": plan.time, + "title": plan.title, + "thread_id": thread_id, + "location_id": plan.location_id or '', + "location_name": plan.location or '', "acontext": { - "action_history": [{ - "surface": "messenger_chat_tab", - "mechanism": "messenger_composer" - }] - } + "action_history": [ + {"surface": "messenger_chat_tab", "mechanism": "messenger_composer"} + ] + }, } - j = self._post(self.req_url.PLAN_CREATE, full_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): """ @@ -1800,14 +2105,15 @@ class Client(object): "location_id": new_plan.location_id or '', "title": new_plan.title, "acontext": { - "action_history": [{ - "surface": "messenger_chat_tab", - "mechanism": "reminder_banner" - }] - } + "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) + j = self._post( + self.req_url.PLAN_CHANGE, full_data, fix_request=True, as_json=True + ) def deletePlan(self, plan): """ @@ -1820,14 +2126,15 @@ class Client(object): "event_reminder_id": plan.uid, "delete": "true", "acontext": { - "action_history": [{ - "surface": "messenger_chat_tab", - "mechanism": "reminder_banner" - }] - } + "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) + j = self._post( + self.req_url.PLAN_CHANGE, full_data, fix_request=True, as_json=True + ) def changePlanParticipation(self, plan, take_part=True): """ @@ -1841,20 +2148,26 @@ class Client(object): "event_reminder_id": plan.uid, "guest_state": "GOING" if take_part else "DECLINED", "acontext": { - "action_history": [{ - "surface": "messenger_chat_tab", - "mechanism": "reminder_banner" - }] - } + "action_history": [ + {"surface": "messenger_chat_tab", "mechanism": "reminder_banner"} + ] + }, } - j = self._post(self.req_url.PLAN_PARTICIPATION, full_data, fix_request=True, as_json=True) + j = self._post( + self.req_url.PLAN_PARTICIPATION, full_data, fix_request=True, as_json=True + ) def eventReminder(self, thread_id, time, title, location='', location_id=''): """ Deprecated. Use :func:`fbchat.Client.createPlan` instead """ - self.createPlan(plan=Plan(time=time, title=title, location=location, location_id=location_id), thread_id=thread_id) + self.createPlan( + plan=Plan( + time=time, title=title, location=location, location_id=location_id + ), + thread_id=thread_id, + ) def createPoll(self, poll, thread_id=None): """ @@ -1874,10 +2187,7 @@ class Client(object): # # 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), - ]) + 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 @@ -1897,9 +2207,7 @@ class Client(object): :type thread_type: models.ThreadType :raises: FBchatException if request failed """ - data = { - "question_id": poll_id - } + data = {"question_id": poll_id} for i, option_id in enumerate(option_ids): data["selected_options[{}]".format(i)] = option_id @@ -1926,7 +2234,7 @@ class Client(object): "typ": status.value, "thread": thread_id, "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) @@ -1946,7 +2254,7 @@ class Client(object): """ data = { "message_ids[0]": message_id, - "thread_ids[%s][0]" % thread_id: message_id + "thread_ids[%s][0]" % thread_id: message_id, } r = self._post(self.req_url.DELIVERED, data) @@ -1955,10 +2263,7 @@ class Client(object): def _readStatus(self, read, thread_ids): thread_ids = require_list(thread_ids) - data = { - "watermarkTimestamp": now(), - "shouldSendReadReceipt": 'true', - } + data = {"watermarkTimestamp": now(), "shouldSendReadReceipt": 'true'} for thread_id in thread_ids: data["ids[{}]".format(thread_id)] = 'true' if read else 'false' @@ -2001,10 +2306,7 @@ class Client(object): .. todo:: Documenting this """ - data = { - "to_friend": friend_id, - "action": "confirm" - } + data = {"to_friend": friend_id, "action": "confirm"} r = self._post(self.req_url.CONNECT, data) return r.ok @@ -2016,11 +2318,7 @@ class Client(object): :param friend_id: The ID of the friend that you want to remove :return: Returns error if the removing was unsuccessful, returns True when successful. """ - payload = { - "friend_id": friend_id, - "unref": "none", - "confirm": "Confirm", - } + payload = {"friend_id": friend_id, "unref": "none", "confirm": "Confirm"} r = self._post(self.req_url.REMOVE_FRIEND, payload) query = parse_qs(urlparse(r.url).query) if "err" not in query: @@ -2038,9 +2336,7 @@ class Client(object): :return: Whether the request was successful :raises: FBchatException if request failed """ - data = { - 'fbid': user_id - } + data = {'fbid': user_id} r = self._post(self.req_url.BLOCK_USER, data) return r.ok @@ -2052,9 +2348,7 @@ class Client(object): :return: Whether the request was successful :raises: FBchatException if request failed """ - data = { - 'fbid': user_id - } + data = {'fbid': user_id} r = self._post(self.req_url.UNBLOCK_USER, data) return r.ok @@ -2142,10 +2436,7 @@ class Client(object): :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 - } + data = {"mute_settings": str(mute_time), "thread_fbid": thread_id} r = self._post(self.req_url.MUTE_THREAD, data) r.raise_for_status() @@ -2165,10 +2456,7 @@ class Client(object): :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 - } + data = {"reactions_mute_mode": int(mute), "thread_fbid": thread_id} r = self._post(self.req_url.MUTE_REACTIONS, data) r.raise_for_status() @@ -2188,10 +2476,7 @@ class Client(object): :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 - } + data = {"mentions_mute_mode": int(mute), "thread_fbid": thread_id} r = self._post(self.req_url.MUTE_MENTIONS, data) r.raise_for_status() @@ -2248,7 +2533,8 @@ class Client(object): for batch in content['batches']: self._parseMessage(batch) - if 'ms' not in content: return + if 'ms' not in content: + return for m in content["ms"]: mtype = m.get("type") @@ -2280,38 +2566,78 @@ class Client(object): # Added participants if 'addedParticipants' in delta: - added_ids = [str(x['userFbId']) for x in delta['addedParticipants']] + 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) + 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) + 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 = graphql_color_to_enum(delta["untypedData"]["theme_color"]) + new_color = graphql_color_to_enum( + 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) + 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) + 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) + 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": @@ -2327,18 +2653,37 @@ class Client(object): 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) + 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) + 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": @@ -2346,49 +2691,104 @@ class Client(object): 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) + 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) + 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) + 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"]) + 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) + 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_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) + 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")) + 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")] + 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) + self.onMarkedSeen( + threads=threads, + seen_ts=seen_ts, + ts=delivered_ts, + metadata=delta, + msg=m, + ) # Game played elif delta_type == "instant_game_update": @@ -2401,9 +2801,19 @@ class Client(object): 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) + 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": @@ -2412,18 +2822,45 @@ class Client(object): 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) + 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) + 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) + 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": @@ -2433,51 +2870,112 @@ class Client(object): poll = graphql_to_poll(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) + 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) + 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) plan = graphql_to_plan(delta["untypedData"]) - self.onPlanCreated(mid=mid, plan=plan, author_id=author_id, thread_id=thread_id, thread_type=thread_type, - ts=ts, metadata=metadata, msg=m) + self.onPlanCreated( + mid=mid, + plan=plan, + 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) plan = graphql_to_plan(delta["untypedData"]) - self.onPlanEnded(mid=mid, plan=plan, thread_id=thread_id, thread_type=thread_type, - ts=ts, metadata=metadata, msg=m) + self.onPlanEnded( + mid=mid, + plan=plan, + 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) plan = graphql_to_plan(delta["untypedData"]) - self.onPlanEdited(mid=mid, plan=plan, author_id=author_id, thread_id=thread_id, - thread_type=thread_type, ts=ts, metadata=metadata, msg=m) + self.onPlanEdited( + mid=mid, + plan=plan, + 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) plan = graphql_to_plan(delta["untypedData"]) - self.onPlanDeleted(mid=mid, plan=plan, author_id=author_id, thread_id=thread_id, - thread_type=thread_type, ts=ts, metadata=metadata, msg=m) + self.onPlanDeleted( + mid=mid, + plan=plan, + author_id=author_id, + thread_id=thread_id, + thread_type=thread_type, + ts=ts, + metadata=metadata, + msg=m, + ) - # Plan participation change + # Plan participation change elif delta_type == "lightweight_event_rsvp": thread_id, thread_type = getThreadIdAndThreadType(metadata) plan = graphql_to_plan(delta["untypedData"]) take_part = delta["untypedData"]["guest_status"] == "GOING" - self.onPlanParticipation(mid=mid, plan=plan, take_part=take_part, author_id=author_id, - thread_id=thread_id, thread_type=thread_type, ts=ts, metadata=metadata, msg=m) + self.onPlanParticipation( + mid=mid, + plan=plan, + 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": @@ -2491,14 +2989,31 @@ class Client(object): thread_id, thread_type = getThreadIdAndThreadType(i) mid = i["messageId"] author_id = str(i["userId"]) - reaction = MessageReaction(i["reaction"]) if i.get("reaction") else None + 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) + 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) + 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'): @@ -2509,9 +3024,21 @@ class Client(object): 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) + 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) + 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'): @@ -2521,8 +3048,15 @@ class Client(object): mid = l["messageId"] author_id = str(l["senderId"]) location = graphql_to_live_location(l) - self.onLiveLocation(mid=mid, location=location, author_id=author_id, thread_id=thread_id, - thread_type=thread_type, ts=ts, msg=m) + 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'): @@ -2531,17 +3065,32 @@ class Client(object): 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) + self.onMessageUnsent( + mid=mid, + author_id=author_id, + thread_id=thread_id, + thread_type=thread_type, + ts=ts, + msg=m, + ) # New message elif delta.get("class") == "NewMessage": mentions = [] if delta.get('data') and delta['data'].get('prng'): try: - mentions = [Mention(str(mention.get('i')), offset=mention.get('o'), length=mention.get('l')) for mention in parse_json(delta['data']['prng'])] + mentions = [ + Mention( + str(mention.get('i')), + offset=mention.get('o'), + length=mention.get('l'), + ) + for mention in parse_json(delta['data']['prng']) + ] except Exception: - log.exception('An exception occured while reading attachments') + log.exception( + 'An exception occured while reading attachments' + ) sticker = None attachments = [] @@ -2552,26 +3101,42 @@ class Client(object): mercury = a['mercury'] if mercury.get('blob_attachment'): image_metadata = a.get('imageMetadata', {}) - attach_type = mercury['blob_attachment']['__typename'] - attachment = graphql_to_attachment(mercury['blob_attachment']) + attach_type = mercury['blob_attachment'][ + '__typename' + ] + attachment = graphql_to_attachment( + mercury['blob_attachment'] + ) - if attach_type in ['MessageFile', 'MessageVideo', 'MessageAudio']: + if attach_type in [ + 'MessageFile', + 'MessageVideo', + 'MessageAudio', + ]: # TODO: Add more data here for audio files attachment.size = int(a['fileSize']) attachments.append(attachment) elif mercury.get('sticker_attachment'): - sticker = graphql_to_sticker(mercury['sticker_attachment']) + sticker = graphql_to_sticker( + mercury['sticker_attachment'] + ) elif mercury.get('extensible_attachment'): - attachment = graphql_to_extensible_attachment(mercury['extensible_attachment']) + attachment = graphql_to_extensible_attachment( + mercury['extensible_attachment'] + ) if isinstance(attachment, UnsentMessage): unsent = True elif attachment: attachments.append(attachment) except Exception: - log.exception('An exception occured while reading attachments: {}'.format(delta['attachments'])) + log.exception( + 'An exception occured while reading attachments: {}'.format( + delta['attachments'] + ) + ) if metadata and metadata.get('tags'): emoji_size = get_emojisize_from_tags(metadata.get('tags')) @@ -2586,11 +3151,20 @@ class Client(object): message.uid = mid message.author = author_id message.timestamp = ts - #message.reactions = {} + # message.reactions = {} message.unsent = unsent thread_id, thread_type = getThreadIdAndThreadType(metadata) - self.onMessage(mid=mid, author_id=author_id, message=delta.get('body', ''), message_object=message, - thread_id=thread_id, thread_type=thread_type, ts=ts, metadata=metadata, msg=m) + self.onMessage( + mid=mid, + author_id=author_id, + message=delta.get('body', ''), + message_object=message, + thread_id=thread_id, + thread_type=thread_type, + ts=ts, + metadata=metadata, + msg=m, + ) # Unknown message type else: @@ -2598,7 +3172,12 @@ class Client(object): # Inbox elif mtype == "inbox": - self.onInbox(unseen=m["unseen"], unread=m["unread"], recent_unread=m["recent_unread"], msg=m) + self.onInbox( + unseen=m["unseen"], + unread=m["unread"], + recent_unread=m["recent_unread"], + msg=m, + ) # Typing elif mtype == "typ" or mtype == "ttyp": @@ -2614,7 +3193,13 @@ class Client(object): 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) + self.onTyping( + author_id=author_id, + status=typing_status, + thread_id=thread_id, + thread_type=thread_type, + msg=m, + ) # Delivered @@ -2652,7 +3237,9 @@ class Client(object): self._buddylist[_id].active = active self._buddylist[_id].in_game = in_game else: - self._buddylist[_id] = ActiveStatus(active=active, last_active=last_active, in_game=in_game) + self._buddylist[_id] = ActiveStatus( + active=active, last_active=last_active, in_game=in_game + ) self.onChatTimestamp(buddylist=buddylist, msg=m) @@ -2664,9 +3251,15 @@ class Client(object): last_active = payload.get('la') active = payload.get('a') in [2, 3] - in_game = self._buddylist[_id].in_game if self._buddylist.get(_id) else False + in_game = ( + self._buddylist[_id].in_game + if self._buddylist.get(_id) + else False + ) - status = ActiveStatus(active=active, last_active=last_active, in_game=in_game) + status = ActiveStatus( + active=active, last_active=last_active, in_game=in_game + ) if self._buddylist.get(_id): self._buddylist[_id].last_active = last_active @@ -2807,8 +3400,18 @@ class Client(object): log.exception('Got exception while listening') return True - - 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): + 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 @@ -2826,7 +3429,17 @@ class Client(object): """ log.info("{} from {} in {}".format(message_object, thread_id, thread_type.name)) - def onColorChange(self, mid=None, author_id=None, new_color=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg=None): + 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 @@ -2841,9 +3454,23 @@ class Client(object): :type new_color: models.ThreadColor :type thread_type: models.ThreadType """ - log.info("Color change from {} in {} ({}): {}".format(author_id, thread_id, thread_type.name, new_color)) + log.info( + "Color change from {} in {} ({}): {}".format( + author_id, thread_id, thread_type.name, new_color + ) + ) - def onEmojiChange(self, mid=None, author_id=None, new_emoji=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg=None): + 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 @@ -2857,9 +3484,23 @@ class Client(object): :param msg: A full set of the data recieved :type thread_type: models.ThreadType """ - log.info("Emoji change from {} in {} ({}): {}".format(author_id, thread_id, thread_type.name, new_emoji)) + log.info( + "Emoji change from {} in {} ({}): {}".format( + author_id, thread_id, thread_type.name, new_emoji + ) + ) - def onTitleChange(self, mid=None, author_id=None, new_title=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg=None): + 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 @@ -2873,10 +3514,22 @@ class Client(object): :param msg: A full set of the data recieved :type thread_type: models.ThreadType """ - log.info("Title change from {} in {} ({}): {}".format(author_id, thread_id, thread_type.name, new_title)) + log.info( + "Title change from {} in {} ({}): {}".format( + author_id, thread_id, thread_type.name, new_title + ) + ) - - def onImageChange(self, mid=None, author_id=None, new_image=None, thread_id=None, thread_type=ThreadType.GROUP, ts=None, msg=None): + 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 @@ -2891,8 +3544,18 @@ class Client(object): """ log.info("{} changed thread image in {}".format(author_id, thread_id)) - - 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): + 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 @@ -2907,10 +3570,22 @@ class Client(object): :param msg: A full set of the data recieved :type thread_type: models.ThreadType """ - log.info("Nickname change from {} in {} ({}) for {}: {}".format(author_id, thread_id, thread_type.name, changed_for, new_nickname)) + log.info( + "Nickname change from {} in {} ({}) for {}: {}".format( + author_id, thread_id, thread_type.name, changed_for, new_nickname + ) + ) - - def onAdminAdded(self, mid=None, added_id=None, author_id=None, thread_id=None, thread_type=ThreadType.GROUP, ts=None, msg=None): + 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 @@ -2923,8 +3598,16 @@ class Client(object): """ log.info("{} added admin: {} in {}".format(author_id, added_id, thread_id)) - - def onAdminRemoved(self, mid=None, removed_id=None, author_id=None, thread_id=None, thread_type=ThreadType.GROUP, ts=None, msg=None): + 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 @@ -2937,8 +3620,16 @@ class Client(object): """ log.info("{} removed admin: {} in {}".format(author_id, removed_id, thread_id)) - - def onApprovalModeChange(self, mid=None, approval_mode=None, author_id=None, thread_id=None, thread_type=ThreadType.GROUP, ts=None, msg=None): + 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 @@ -2954,7 +3645,16 @@ class Client(object): else: log.info("{} disabled approval mode in {}".format(author_id, thread_id)) - def onMessageSeen(self, seen_by=None, thread_id=None, thread_type=ThreadType.USER, seen_ts=None, ts=None, metadata=None, msg=None): + 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 @@ -2967,9 +3667,22 @@ class Client(object): :param msg: A full set of the data recieved :type thread_type: models.ThreadType """ - log.info("Messages seen by {} in {} ({}) at {}s".format(seen_by, thread_id, thread_type.name, seen_ts/1000)) + log.info( + "Messages seen by {} in {} ({}) at {}s".format( + seen_by, thread_id, thread_type.name, seen_ts / 1000 + ) + ) - def onMessageDelivered(self, msg_ids=None, delivered_for=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg=None): + 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 @@ -2982,9 +3695,15 @@ class Client(object): :param msg: A full set of the data recieved :type thread_type: models.ThreadType """ - log.info("Messages {} delivered to {} in {} ({}) at {}s".format(msg_ids, delivered_for, thread_id, thread_type.name, ts/1000)) + log.info( + "Messages {} delivered to {} in {} ({}) at {}s".format( + msg_ids, delivered_for, thread_id, thread_type.name, ts / 1000 + ) + ) - def onMarkedSeen(self, threads=None, seen_ts=None, ts=None, metadata=None, msg=None): + 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 @@ -2996,9 +3715,21 @@ class Client(object): :param msg: A full set of the data recieved :type thread_type: models.ThreadType """ - log.info("Marked messages as seen in threads {} at {}s".format([(x[0], x[1].name) for x in threads], seen_ts/1000)) + log.info( + "Marked messages as seen in threads {} at {}s".format( + [(x[0], x[1].name) for x in threads], seen_ts / 1000 + ) + ) - def onMessageUnsent(self, mid=None, author_id=None, thread_id=None, thread_type=None, ts=None, msg=None): + 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 @@ -3010,9 +3741,21 @@ class Client(object): :param msg: A full set of the data recieved :type thread_type: models.ThreadType """ - log.info("{} unsent the message {} in {} ({}) at {}s".format(author_id, repr(mid), thread_id, thread_type.name, ts/1000)) + log.info( + "{} unsent the message {} in {} ({}) at {}s".format( + author_id, repr(mid), thread_id, thread_type.name, ts / 1000 + ) + ) - def onPeopleAdded(self, mid=None, added_ids=None, author_id=None, thread_id=None, ts=None, msg=None): + 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 @@ -3023,9 +3766,19 @@ class Client(object): :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)) + log.info( + "{} added: {} in {}".format(author_id, ', '.join(added_ids), thread_id) + ) - def onPersonRemoved(self, mid=None, removed_id=None, author_id=None, thread_id=None, ts=None, msg=None): + 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 @@ -3059,7 +3812,9 @@ class Client(object): """ log.info('Inbox event: {}, {}, {}'.format(unseen, unread, recent_unread)) - def onTyping(self, author_id=None, status=None, thread_id=None, thread_type=None, msg=None): + 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 @@ -3073,7 +3828,20 @@ class Client(object): """ pass - 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): + 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 @@ -3090,9 +3858,22 @@ class Client(object): :param msg: A full set of the data recieved :type thread_type: models.ThreadType """ - log.info("{} played \"{}\" in {} ({})".format(author_id, game_name, thread_id, thread_type.name)) + log.info( + "{} played \"{}\" in {} ({})".format( + author_id, game_name, thread_id, thread_type.name + ) + ) - def onReactionAdded(self, mid=None, reaction=None, author_id=None, thread_id=None, thread_type=None, ts=None, msg=None): + 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 @@ -3107,9 +3888,21 @@ class Client(object): :type reaction: models.MessageReaction :type thread_type: models.ThreadType """ - log.info("{} reacted to message {} with {} in {} ({})".format(author_id, mid, reaction.name, thread_id, thread_type.name)) + log.info( + "{} reacted to message {} with {} in {} ({})".format( + author_id, mid, reaction.name, thread_id, thread_type.name + ) + ) - def onReactionRemoved(self, mid=None, author_id=None, thread_id=None, thread_type=None, ts=None, msg=None): + 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 @@ -3121,9 +3914,15 @@ class Client(object): :param msg: A full set of the data recieved :type thread_type: models.ThreadType """ - log.info("{} removed reaction from {} message in {} ({})".format(author_id, mid, thread_id, thread_type)) + log.info( + "{} removed reaction from {} message in {} ({})".format( + author_id, mid, thread_id, thread_type + ) + ) - def onBlock(self, author_id=None, thread_id=None, thread_type=None, ts=None, msg=None): + 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 @@ -3134,9 +3933,13 @@ class Client(object): :param msg: A full set of the data recieved :type thread_type: models.ThreadType """ - log.info("{} blocked {} ({}) thread".format(author_id, thread_id, thread_type.name)) + log.info( + "{} blocked {} ({}) thread".format(author_id, thread_id, thread_type.name) + ) - def onUnblock(self, author_id=None, thread_id=None, thread_type=None, ts=None, msg=None): + 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 @@ -3147,9 +3950,20 @@ class Client(object): :param msg: A full set of the data recieved :type thread_type: models.ThreadType """ - log.info("{} unblocked {} ({}) thread".format(author_id, thread_id, thread_type.name)) + log.info( + "{} unblocked {} ({}) thread".format(author_id, thread_id, thread_type.name) + ) - def onLiveLocation(self, mid=None, location=None, author_id=None, thread_id=None, thread_type=None, ts=None, msg=None): + 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 @@ -3163,9 +3977,23 @@ class Client(object): :type location: models.LiveLocationAttachment :type thread_type: models.ThreadType """ - log.info("{} sent live location info in {} ({}) with latitude {} and longitude {}".format(author_id, thread_id, thread_type, location.latitude, location.longitude)) + log.info( + "{} sent live location info in {} ({}) with latitude {} and longitude {}".format( + author_id, thread_id, thread_type, location.latitude, location.longitude + ) + ) - def onCallStarted(self, mid=None, caller_id=None, is_video_call=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None): + 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 @@ -3182,9 +4010,22 @@ class Client(object): :param msg: A full set of the data recieved :type thread_type: models.ThreadType """ - log.info("{} started call in {} ({})".format(caller_id, thread_id, thread_type.name)) + log.info( + "{} started call in {} ({})".format(caller_id, thread_id, thread_type.name) + ) - 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): + 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 @@ -3202,9 +4043,21 @@ class Client(object): :param msg: A full set of the data recieved :type thread_type: models.ThreadType """ - log.info("{} ended call in {} ({})".format(caller_id, thread_id, thread_type.name)) + log.info( + "{} ended call in {} ({})".format(caller_id, thread_id, thread_type.name) + ) - def onUserJoinedCall(self, mid=None, joined_id=None, is_video_call=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None): + 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 @@ -3218,9 +4071,21 @@ class Client(object): :param msg: A full set of the data recieved :type thread_type: models.ThreadType """ - log.info("{} joined call in {} ({})".format(joined_id, thread_id, thread_type.name)) + log.info( + "{} joined call in {} ({})".format(joined_id, thread_id, thread_type.name) + ) - def onPollCreated(self, mid=None, poll=None, author_id=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None): + 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 @@ -3235,9 +4100,25 @@ class Client(object): :type poll: models.Poll :type thread_type: models.ThreadType """ - log.info("{} created poll {} in {} ({})".format(author_id, poll, thread_id, thread_type.name)) + log.info( + "{} created poll {} in {} ({})".format( + author_id, poll, thread_id, thread_type.name + ) + ) - 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): + 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 @@ -3252,9 +4133,23 @@ class Client(object): :type poll: models.Poll :type thread_type: models.ThreadType """ - log.info("{} voted in poll {} in {} ({})".format(author_id, poll, thread_id, thread_type.name)) + log.info( + "{} voted in poll {} in {} ({})".format( + author_id, poll, thread_id, thread_type.name + ) + ) - def onPlanCreated(self, mid=None, plan=None, author_id=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None): + 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 @@ -3269,9 +4164,22 @@ class Client(object): :type plan: models.Plan :type thread_type: models.ThreadType """ - log.info("{} created plan {} in {} ({})".format(author_id, plan, thread_id, thread_type.name)) + log.info( + "{} created plan {} in {} ({})".format( + author_id, plan, thread_id, thread_type.name + ) + ) - def onPlanEnded(self, mid=None, plan=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None): + 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 @@ -3285,9 +4193,21 @@ class Client(object): :type plan: models.Plan :type thread_type: models.ThreadType """ - log.info("Plan {} has ended in {} ({})".format(plan, thread_id, thread_type.name)) + log.info( + "Plan {} has ended in {} ({})".format(plan, thread_id, thread_type.name) + ) - def onPlanEdited(self, mid=None, plan=None, author_id=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None): + 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 @@ -3302,9 +4222,23 @@ class Client(object): :type plan: models.Plan :type thread_type: models.ThreadType """ - log.info("{} edited plan {} in {} ({})".format(author_id, plan, thread_id, thread_type.name)) + log.info( + "{} edited plan {} in {} ({})".format( + author_id, plan, thread_id, thread_type.name + ) + ) - def onPlanDeleted(self, mid=None, plan=None, author_id=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None): + 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 @@ -3319,9 +4253,24 @@ class Client(object): :type plan: models.Plan :type thread_type: models.ThreadType """ - log.info("{} deleted plan {} in {} ({})".format(author_id, plan, thread_id, thread_type.name)) + log.info( + "{} deleted plan {} in {} ({})".format( + author_id, plan, thread_id, thread_type.name + ) + ) - 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): + 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 @@ -3339,9 +4288,17 @@ class Client(object): :type thread_type: models.ThreadType """ if take_part: - log.info("{} will take part in {} in {} ({})".format(author_id, plan, thread_id, thread_type.name)) + 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)) + log.info( + "{} won't take part in {} in {} ({})".format( + author_id, plan, thread_id, thread_type.name + ) + ) def onQprimer(self, ts=None, msg=None): """ diff --git a/fbchat/graphql.py b/fbchat/graphql.py index f3c09b5..b736342 100644 --- a/fbchat/graphql.py +++ b/fbchat/graphql.py @@ -10,6 +10,7 @@ from .utils import * FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL WHITESPACE = re.compile(r'[ \t\n\r]*', FLAGS) + class ConcatJSONDecoder(json.JSONDecoder): def decode(self, s, _w=WHITESPACE.match): s_len = len(s) @@ -21,8 +22,11 @@ class ConcatJSONDecoder(json.JSONDecoder): end = _w(s, end).end() objs.append(obj) return objs + + # End shameless copy + def graphql_color_to_enum(color): if color is None: return None @@ -32,6 +36,7 @@ def graphql_color_to_enum(color): color_value = '#{}'.format(color.lower()) return enum_extend_if_invalid(ThreadColor, color_value) + def get_customization_info(thread): if thread is None or thread.get('customization_info') is None: return {} @@ -39,9 +44,13 @@ def get_customization_info(thread): rtn = { 'emoji': info.get('emoji'), - 'color': graphql_color_to_enum(info.get('outgoing_bubble_color')) + 'color': graphql_color_to_enum(info.get('outgoing_bubble_color')), } - if thread.get('thread_type') == 'GROUP' or thread.get('is_group_thread') or thread.get('thread_key', {}).get('thread_fbid'): + if ( + thread.get('thread_type') == 'GROUP' + or thread.get('is_group_thread') + or thread.get('thread_key', {}).get('thread_fbid') + ): rtn['nicknames'] = {} for k in info.get('participant_customizations', []): rtn['nicknames'][k['participant_id']] = k.get('nickname') @@ -64,9 +73,7 @@ def get_customization_info(thread): def graphql_to_sticker(s): if not s: return None - sticker = Sticker( - uid=s['id'] - ) + sticker = Sticker(uid=s['id']) if s.get('pack'): sticker.pack = s['pack'].get('id') if s.get('sprite_image'): @@ -83,19 +90,21 @@ def graphql_to_sticker(s): sticker.label = s['label'] return sticker + def graphql_to_attachment(a): _type = a['__typename'] if _type in ['MessageImage', 'MessageAnimatedImage']: return ImageAttachment( - original_extension=a.get('original_extension') or (a['filename'].split('-')[0] if a.get('filename') else None), + original_extension=a.get('original_extension') + or (a['filename'].split('-')[0] if a.get('filename') else None), width=a.get('original_dimensions', {}).get('width'), height=a.get('original_dimensions', {}).get('height'), - is_animated=_type=='MessageAnimatedImage', + is_animated=_type == 'MessageAnimatedImage', thumbnail_url=a.get('thumbnail', {}).get('uri'), preview=a.get('preview') or a.get('preview_image'), large_preview=a.get('large_preview'), animated_preview=a.get('animated_image'), - uid=a.get('legacy_attachment_id') + uid=a.get('legacy_attachment_id'), ) elif _type == 'MessageVideo': return VideoAttachment( @@ -106,26 +115,25 @@ def graphql_to_attachment(a): small_image=a.get('chat_image'), medium_image=a.get('inbox_image'), large_image=a.get('large_image'), - uid=a.get('legacy_attachment_id') + uid=a.get('legacy_attachment_id'), ) elif _type == 'MessageAudio': return AudioAttachment( filename=a.get('filename'), url=a.get('playable_url'), duration=a.get('playable_duration_in_ms'), - audio_type=a.get('audio_type') + audio_type=a.get('audio_type'), ) elif _type == 'MessageFile': return FileAttachment( url=a.get('url'), name=a.get('filename'), is_malicious=a.get('is_malicious'), - uid=a.get('message_file_fbid') + uid=a.get('message_file_fbid'), ) else: - return Attachment( - uid=a.get('legacy_attachment_id') - ) + return Attachment(uid=a.get('legacy_attachment_id')) + def graphql_to_extensible_attachment(a): story = a.get('story_attachment') @@ -134,7 +142,9 @@ def graphql_to_extensible_attachment(a): if target: _type = target['__typename'] if _type == 'MessageLocation': - latitude, longitude = get_url_parameter(get_url_parameter(story['url'], 'u'), 'where1').split(", ") + latitude, longitude = get_url_parameter( + get_url_parameter(story['url'], 'u'), 'where1' + ).split(", ") rtn = LocationAttachment( uid=int(story['deduplication_key']), latitude=float(latitude), @@ -149,10 +159,16 @@ def graphql_to_extensible_attachment(a): elif _type == 'MessageLiveLocation': rtn = LiveLocationAttachment( uid=int(story['target']['live_location_id']), - latitude=story['target']['coordinate']['latitude'] if story['target'].get('coordinate') else None, - longitude=story['target']['coordinate']['longitude'] if story['target'].get('coordinate') else None, + latitude=story['target']['coordinate']['latitude'] + if story['target'].get('coordinate') + else None, + longitude=story['target']['coordinate']['longitude'] + if story['target'].get('coordinate') + else None, name=story['title_with_entities']['text'], - expiration_time=story['target']['expiration_time'] if story['target'].get('expiration_time') else None, + expiration_time=story['target']['expiration_time'] + if story['target'].get('expiration_time') + else None, is_expired=story['target']['is_expired'], ) if story['media']: @@ -164,22 +180,41 @@ def graphql_to_extensible_attachment(a): elif _type in ['ExternalUrl', 'Story']: return ShareAttachment( uid=a.get('legacy_attachment_id'), - author=story['target']['actors'][0]['id'] if story['target'].get('actors') else None, + author=story['target']['actors'][0]['id'] + if story['target'].get('actors') + else None, url=story['url'], - original_url=get_url_parameter(story['url'], 'u') if "/l.php?u=" in story['url'] else story['url'], + original_url=get_url_parameter(story['url'], 'u') + if "/l.php?u=" in story['url'] + else story['url'], title=story['title_with_entities'].get('text'), - description=story['description'].get('text') if story.get('description') else None, + description=story['description'].get('text') + if story.get('description') + else None, source=story['source']['text'], - image_url=story['media']['image']['uri'] if story.get('media') else None, - original_image_url=(get_url_parameter(story['media']['image']['uri'], 'url') if "/safe_image.php" in story['media']['image']['uri'] else story['media']['image']['uri']) if story.get('media') else None, - image_width=story['media']['image']['width'] if story.get('media') else None, - image_height=story['media']['image']['height'] if story.get('media') else None, - attachments=[graphql_to_subattachment(attachment) for attachment in story.get('subattachments')], + image_url=story['media']['image']['uri'] + if story.get('media') + else None, + original_image_url=( + get_url_parameter(story['media']['image']['uri'], 'url') + if "/safe_image.php" in story['media']['image']['uri'] + else story['media']['image']['uri'] + ) + if story.get('media') + else None, + image_width=story['media']['image']['width'] + if story.get('media') + else None, + image_height=story['media']['image']['height'] + if story.get('media') + else None, + attachments=[ + graphql_to_subattachment(attachment) + for attachment in story.get('subattachments') + ], ) else: - return UnsentMessage( - uid=a.get('legacy_attachment_id'), - ) + return UnsentMessage(uid=a.get('legacy_attachment_id')) def graphql_to_subattachment(a): @@ -192,25 +227,32 @@ def graphql_to_subattachment(a): uid=a['target'].get('video_id'), ) + def graphql_to_live_location(a): return LiveLocationAttachment( uid=a['id'], - latitude=a['coordinate']['latitude'] / (10 ** 8) if not a.get('stopReason') else None, - longitude=a['coordinate']['longitude'] / (10 ** 8) if not a.get('stopReason') else None, + latitude=a['coordinate']['latitude'] / (10 ** 8) + if not a.get('stopReason') + else None, + longitude=a['coordinate']['longitude'] / (10 ** 8) + if not a.get('stopReason') + else None, name=a.get('locationTitle'), expiration_time=a['expirationTime'], is_expired=bool(a.get('stopReason')), ) + def graphql_to_poll(a): rtn = Poll( title=a.get('title') if a.get('title') else a.get('text'), - options=[graphql_to_poll_option(m) for m in a.get('options')] + options=[graphql_to_poll_option(m) for m in a.get('options')], ) rtn.uid = int(a["id"]) rtn.options_count = a.get("total_count") return rtn + def graphql_to_poll_option(a): if a.get('viewer_has_voted') is None: vote = None @@ -218,21 +260,27 @@ def graphql_to_poll_option(a): vote = a['viewer_has_voted'] else: vote = a['viewer_has_voted'] == 'true' - rtn = PollOption( - text=a.get('text'), - vote=vote - ) + rtn = PollOption(text=a.get('text'), vote=vote) rtn.uid = int(a["id"]) - rtn.voters = [m.get('node').get('id') for m in a.get('voters').get('edges')] if isinstance(a.get('voters'), dict) else a.get('voters') - rtn.votes_count = a.get('voters').get('count') if isinstance(a.get('voters'), dict) else a.get('total_count') + rtn.voters = ( + [m.get('node').get('id') for m in a.get('voters').get('edges')] + if isinstance(a.get('voters'), dict) + else a.get('voters') + ) + rtn.votes_count = ( + a.get('voters').get('count') + if isinstance(a.get('voters'), dict) + else a.get('total_count') + ) return rtn + def graphql_to_plan(a): if a.get('event_members'): rtn = Plan( time=a.get('event_time'), title=a.get('title'), - location=a.get('location_name') + location=a.get('location_name'), ) if a.get('location_id') != 0: rtn.location_id = str(a.get('location_id')) @@ -248,7 +296,7 @@ def graphql_to_plan(a): time=a.get('event_time'), title=a.get('event_title'), location=a.get('event_location_name'), - location_id=a.get('event_location_id') + location_id=a.get('event_location_id'), ) rtn.uid = a.get('event_id') rtn.author_id = a.get('event_creator_id') @@ -257,25 +305,40 @@ def graphql_to_plan(a): rtn = Plan( time=a.get('time'), title=a.get('event_title'), - location=a.get('location_name') + location=a.get('location_name'), ) rtn.uid = a.get('id') rtn.author_id = a.get('lightweight_event_creator').get('id') guests = a.get('event_reminder_members').get('edges') - rtn.going = [m.get('node').get('id') for m in guests if m.get('guest_list_state') == "GOING"] - rtn.declined = [m.get('node').get('id') for m in guests if m.get('guest_list_state') == "DECLINED"] - rtn.invited = [m.get('node').get('id') for m in guests if m.get('guest_list_state') == "INVITED"] + rtn.going = [ + m.get('node').get('id') for m in guests if m.get('guest_list_state') == "GOING" + ] + rtn.declined = [ + m.get('node').get('id') + for m in guests + if m.get('guest_list_state') == "DECLINED" + ] + rtn.invited = [ + m.get('node').get('id') + for m in guests + if m.get('guest_list_state') == "INVITED" + ] return rtn + def graphql_to_quick_reply(q, is_response=False): data = dict() _type = q.get('content_type').lower() - if q.get('payload'): data["payload"] = q["payload"] - if q.get('data'): data["data"] = q["data"] - if q.get('image_url') and _type is not QuickReplyLocation._type: data["image_url"] = q["image_url"] + if q.get('payload'): + data["payload"] = q["payload"] + if q.get('data'): + data["data"] = q["data"] + if q.get('image_url') and _type is not QuickReplyLocation._type: + data["image_url"] = q["image_url"] data["is_response"] = is_response if _type == QuickReplyText._type: - if q.get('title') is not None: data["title"] = q["title"] + if q.get('title') is not None: + data["title"] = q["title"] rtn = QuickReplyText(**data) elif _type == QuickReplyLocation._type: rtn = QuickReplyLocation(**data) @@ -285,6 +348,7 @@ def graphql_to_quick_reply(q, is_response=False): rtn = QuickReplyEmail(**data) return rtn + def graphql_to_message(message): if message.get('message_sender') is None: message['message_sender'] = {} @@ -292,9 +356,16 @@ def graphql_to_message(message): message['message'] = {} rtn = Message( text=message.get('message').get('text'), - mentions=[Mention(m.get('entity', {}).get('id'), offset=m.get('offset'), length=m.get('length')) for m in message.get('message').get('ranges', [])], + mentions=[ + Mention( + m.get('entity', {}).get('id'), + offset=m.get('offset'), + length=m.get('length'), + ) + for m in message.get('message').get('ranges', []) + ], emoji_size=get_emojisize_from_tags(message.get('tags_list')), - sticker=graphql_to_sticker(message.get('sticker')) + sticker=graphql_to_sticker(message.get('sticker')), ) rtn.uid = str(message.get('message_id')) rtn.author = str(message.get('message_sender').get('id')) @@ -307,13 +378,18 @@ def graphql_to_message(message): for r in message.get('message_reactions') } if message.get('blob_attachments') is not None: - rtn.attachments = [graphql_to_attachment(attachment) for attachment in message['blob_attachments']] + rtn.attachments = [ + graphql_to_attachment(attachment) + for attachment in message['blob_attachments'] + ] if message.get('platform_xmd_encoded'): quick_replies = json.loads(message['platform_xmd_encoded']).get('quick_replies') if isinstance(quick_replies, list): rtn.quick_replies = [graphql_to_quick_reply(q) for q in quick_replies] elif isinstance(quick_replies, dict): - rtn.quick_replies = [graphql_to_quick_reply(quick_replies, is_response=True)] + rtn.quick_replies = [ + graphql_to_quick_reply(quick_replies, is_response=True) + ] if message.get('extensible_attachment') is not None: attachment = graphql_to_extensible_attachment(message['extensible_attachment']) if isinstance(attachment, UnsentMessage): @@ -322,13 +398,18 @@ def graphql_to_message(message): rtn.attachments.append(attachment) return rtn + def graphql_to_user(user): if user.get('profile_picture') is None: user['profile_picture'] = {} c_info = get_customization_info(user) plan = None if user.get('event_reminders'): - plan = graphql_to_plan(user['event_reminders']['nodes'][0]) if user['event_reminders'].get('nodes') else None + plan = ( + graphql_to_plan(user['event_reminders']['nodes'][0]) + if user['event_reminders'].get('nodes') + else None + ) return User( user['id'], url=user.get('url'), @@ -347,6 +428,7 @@ def graphql_to_user(user): plan=plan, ) + def graphql_to_thread(thread): if thread['thread_type'] == 'GROUP': return graphql_to_group(thread) @@ -354,11 +436,17 @@ def graphql_to_thread(thread): if thread.get('big_image_src') is None: thread['big_image_src'] = {} c_info = get_customization_info(thread) - participants = [node['messaging_actor'] for node in thread['all_participants']['nodes']] - user = next(p for p in participants if p['id'] == thread['thread_key']['other_user_id']) + participants = [ + node['messaging_actor'] for node in thread['all_participants']['nodes'] + ] + user = next( + p for p in participants if p['id'] == thread['thread_key']['other_user_id'] + ) last_message_timestamp = None if 'last_message' in thread: - last_message_timestamp = thread['last_message']['nodes'][0]['timestamp_precise'] + last_message_timestamp = thread['last_message']['nodes'][0][ + 'timestamp_precise' + ] first_name = user.get('short_name') if first_name is None: @@ -368,7 +456,11 @@ def graphql_to_thread(thread): plan = None if thread.get('event_reminders'): - plan = graphql_to_plan(thread['event_reminders']['nodes'][0]) if thread['event_reminders'].get('nodes') else None + plan = ( + graphql_to_plan(thread['event_reminders']['nodes'][0]) + if thread['event_reminders'].get('nodes') + else None + ) return User( user['id'], @@ -389,7 +481,12 @@ def graphql_to_thread(thread): plan=plan, ) else: - raise FBchatException('Unknown thread type: {}, with data: {}'.format(thread.get('thread_type'), thread)) + raise FBchatException( + 'Unknown thread type: {}, with data: {}'.format( + thread.get('thread_type'), thread + ) + ) + def graphql_to_group(group): if group.get('image') is None: @@ -400,17 +497,32 @@ def graphql_to_group(group): last_message_timestamp = group['last_message']['nodes'][0]['timestamp_precise'] plan = None if group.get('event_reminders'): - plan = graphql_to_plan(group['event_reminders']['nodes'][0]) if group['event_reminders'].get('nodes') else None + plan = ( + graphql_to_plan(group['event_reminders']['nodes'][0]) + if group['event_reminders'].get('nodes') + else None + ) return Group( group['thread_key']['thread_fbid'], - participants=set([node['messaging_actor']['id'] for node in group['all_participants']['nodes']]), + participants=set( + [ + node['messaging_actor']['id'] + for node in group['all_participants']['nodes'] + ] + ), nicknames=c_info.get('nicknames'), color=c_info.get('color'), emoji=c_info.get('emoji'), - admins = set([node.get('id') for node in group.get('thread_admins')]), - approval_mode = bool(group.get('approval_mode')) if group.get('approval_mode') is not None else None, - approval_requests = set(node["requester"]['id'] for node in group['group_approval_queue']['nodes']) if group.get('group_approval_queue') else None, - join_link = group['joinable_mode'].get('link'), + admins=set([node.get('id') for node in group.get('thread_admins')]), + approval_mode=bool(group.get('approval_mode')) + if group.get('approval_mode') is not None + else None, + approval_requests=set( + node["requester"]['id'] for node in group['group_approval_queue']['nodes'] + ) + if group.get('group_approval_queue') + else None, + join_link=group['joinable_mode'].get('link'), photo=group['image'].get('uri'), name=group.get('name'), message_count=group.get('messages_count'), @@ -418,6 +530,7 @@ def graphql_to_group(group): plan=plan, ) + def graphql_to_page(page): if page.get('profile_picture') is None: page['profile_picture'] = {} @@ -425,7 +538,11 @@ def graphql_to_page(page): page['city'] = {} plan = None if page.get('event_reminders'): - plan = graphql_to_plan(page['event_reminders']['nodes'][0]) if page['event_reminders'].get('nodes') else None + plan = ( + graphql_to_plan(page['event_reminders']['nodes'][0]) + if page['event_reminders'].get('nodes') + else None + ) return Page( page['id'], url=page.get('url'), @@ -437,6 +554,7 @@ def graphql_to_page(page): plan=plan, ) + def graphql_queries_to_json(*queries): """ Queries should be a list of GraphQL objects @@ -446,14 +564,15 @@ def graphql_queries_to_json(*queries): rtn['q{}'.format(i)] = query.value return json.dumps(rtn) + def graphql_response_to_json(content): - content = strip_to_json(content) # Usually only needed in some error cases + content = strip_to_json(content) # Usually only needed in some error cases try: j = json.loads(content, cls=ConcatJSONDecoder) except Exception: raise FBchatException('Error while parsing JSON: {}'.format(repr(content))) - rtn = [None]*(len(j)) + rtn = [None] * (len(j)) for x in j: if 'error_results' in x: del rtn[-1] @@ -470,25 +589,18 @@ def graphql_response_to_json(content): return rtn + class GraphQL(object): def __init__(self, query=None, doc_id=None, params=None): if params is None: params = {} if query is not None: - self.value = { - 'priority': 0, - 'q': query, - 'query_params': params - } + self.value = {'priority': 0, 'q': query, 'query_params': params} elif doc_id is not None: - self.value = { - 'doc_id': doc_id, - 'query_params': params - } + self.value = {'doc_id': doc_id, 'query_params': params} else: raise FBchatUserError('A query or doc_id must be specified') - FRAGMENT_USER = """ QueryFragment User: User { id, @@ -582,7 +694,8 @@ class GraphQL(object): } """ - SEARCH_USER = """ + SEARCH_USER = ( + """ Query SearchUser(<search> = '', <limit> = 10) { entities_named(<search>) { search_results.of_type(user).first(<limit>) as users { @@ -592,9 +705,12 @@ class GraphQL(object): } } } - """ + FRAGMENT_USER + """ + + FRAGMENT_USER + ) - SEARCH_GROUP = """ + SEARCH_GROUP = ( + """ Query SearchGroup(<search> = '', <limit> = 10, <pic_size> = 32) { viewer() { message_threads.with_thread_name(<search>).last(<limit>) as groups { @@ -604,9 +720,12 @@ class GraphQL(object): } } } - """ + FRAGMENT_GROUP + """ + + FRAGMENT_GROUP + ) - SEARCH_PAGE = """ + SEARCH_PAGE = ( + """ Query SearchPage(<search> = '', <limit> = 10) { entities_named(<search>) { search_results.of_type(page).first(<limit>) as pages { @@ -616,9 +735,12 @@ class GraphQL(object): } } } - """ + FRAGMENT_PAGE + """ + + FRAGMENT_PAGE + ) - SEARCH_THREAD = """ + SEARCH_THREAD = ( + """ Query SearchThread(<search> = '', <limit> = 10) { entities_named(<search>) { search_results.first(<limit>) as threads { @@ -631,4 +753,8 @@ class GraphQL(object): } } } - """ + FRAGMENT_USER + FRAGMENT_GROUP + FRAGMENT_PAGE + """ + + FRAGMENT_USER + + FRAGMENT_GROUP + + FRAGMENT_PAGE + ) diff --git a/fbchat/models.py b/fbchat/models.py index e9e8167..8e74a40 100644 --- a/fbchat/models.py +++ b/fbchat/models.py @@ -8,6 +8,7 @@ from string import Formatter class FBchatException(Exception): """Custom exception thrown by fbchat. All exceptions in the fbchat module inherits this""" + class FBchatFacebookError(FBchatException): #: The error code that Facebook returned fb_error_code = None @@ -15,16 +16,25 @@ class FBchatFacebookError(FBchatException): fb_error_message = None #: The status code that was sent in the http response (eg. 404) (Usually only set if not successful, aka. not 200) request_status_code = None - def __init__(self, message, fb_error_code=None, fb_error_message=None, request_status_code=None): + + def __init__( + self, + message, + fb_error_code=None, + fb_error_message=None, + request_status_code=None, + ): super(FBchatFacebookError, self).__init__(message) """Thrown by fbchat when Facebook returns an error""" self.fb_error_code = str(fb_error_code) self.fb_error_message = fb_error_message self.request_status_code = request_status_code + class FBchatUserError(FBchatException): """Thrown by fbchat when wrong values are entered""" + class Thread(object): #: The unique identifier of the thread. Can be used a `thread_id`. See :ref:`intro_threads` for more info uid = None @@ -40,7 +50,17 @@ class Thread(object): message_count = None #: Set :class:`Plan` plan = None - def __init__(self, _type, uid, photo=None, name=None, last_message_timestamp=None, message_count=None, plan=None): + + def __init__( + self, + _type, + uid, + photo=None, + name=None, + last_message_timestamp=None, + message_count=None, + plan=None, + ): """Represents a Facebook thread""" self.uid = str(uid) self.type = _type @@ -79,7 +99,21 @@ class User(Thread): #: The default emoji emoji = None - def __init__(self, uid, url=None, first_name=None, last_name=None, is_friend=None, gender=None, affinity=None, nickname=None, own_nickname=None, color=None, emoji=None, **kwargs): + def __init__( + self, + uid, + url=None, + first_name=None, + last_name=None, + is_friend=None, + gender=None, + affinity=None, + nickname=None, + own_nickname=None, + color=None, + emoji=None, + **kwargs + ): """Represents a Facebook user. Inherits `Thread`""" super(User, self).__init__(ThreadType.USER, uid, **kwargs) self.url = url @@ -112,7 +146,20 @@ class Group(Thread): # Link for joining group join_link = None - def __init__(self, uid, participants=None, nicknames=None, color=None, emoji=None, admins=None, approval_mode=None, approval_requests=None, join_link=None, privacy_mode=None, **kwargs): + def __init__( + self, + uid, + participants=None, + nicknames=None, + color=None, + emoji=None, + admins=None, + approval_mode=None, + approval_requests=None, + join_link=None, + privacy_mode=None, + **kwargs + ): """Represents a Facebook group. Inherits `Thread`""" super(Group, self).__init__(ThreadType.GROUP, uid, **kwargs) if participants is None: @@ -156,7 +203,16 @@ class Page(Thread): #: The page's category category = None - def __init__(self, uid, url=None, city=None, likes=None, sub_title=None, category=None, **kwargs): + def __init__( + self, + uid, + url=None, + city=None, + likes=None, + sub_title=None, + category=None, + **kwargs + ): """Represents a Facebook page. Inherits `Thread`""" super(Page, self).__init__(ThreadType.PAGE, uid, **kwargs) self.url = url @@ -196,7 +252,15 @@ class Message(object): #: Whether the message is unsent (deleted for everyone) unsent = None - def __init__(self, text=None, mentions=None, emoji_size=None, sticker=None, attachments=None, quick_replies=None): + def __init__( + self, + text=None, + mentions=None, + emoji_size=None, + sticker=None, + attachments=None, + quick_replies=None, + ): """Represents a Facebook message""" self.text = text if mentions is None: @@ -218,7 +282,9 @@ class Message(object): return self.__unicode__() def __unicode__(self): - return '<Message ({}): {}, mentions={} emoji_size={} attachments={}>'.format(self.uid, repr(self.text), self.mentions, self.emoji_size, self.attachments) + return '<Message ({}): {}, mentions={} emoji_size={} attachments={}>'.format( + self.uid, repr(self.text), self.mentions, self.emoji_size, self.attachments + ) @classmethod def formatMentions(cls, text, *args, **kwargs): @@ -246,26 +312,34 @@ class Message(object): offset += len(literal_text) result += literal_text - if field_name is None: continue + if field_name is None: + continue if field_name == '': field_name = str(i) i += 1 elif automatic and field_name.isdigit(): - raise ValueError("cannot switch from automatic field numbering to manual field specification") + raise ValueError( + "cannot switch from automatic field numbering to manual field specification" + ) thread_id, name = f.get_field(field_name, args, kwargs)[0] - if format_spec: name = f.format_field(name, format_spec) - if conversion: name = f.convert_field(name, conversion) + if format_spec: + name = f.format_field(name, format_spec) + if conversion: + name = f.convert_field(name, conversion) result += name - mentions.append(Mention(thread_id=thread_id, offset=offset, length=len(name))) + mentions.append( + Mention(thread_id=thread_id, offset=offset, length=len(name)) + ) offset += len(name) message = cls(text=result, mentions=mentions) return message + class Attachment(object): #: The attachment ID uid = None @@ -274,12 +348,13 @@ class Attachment(object): """Represents a Facebook attachment""" self.uid = uid -class UnsentMessage(Attachment): +class UnsentMessage(Attachment): def __init__(self, *args, **kwargs): """Represents an unsent message attachment""" super(UnsentMessage, self).__init__(*args, **kwargs) + class Sticker(Attachment): #: The sticker-pack's ID pack = None @@ -311,6 +386,7 @@ class Sticker(Attachment): """Represents a Facebook sticker that has been sent to a Facebook thread as an attachment""" super(Sticker, self).__init__(*args, **kwargs) + class ShareAttachment(Attachment): #: ID of the author of the shared post author = None @@ -335,7 +411,21 @@ class ShareAttachment(Attachment): #: List of additional attachments attachments = None - def __init__(self, author=None, url=None, original_url=None, title=None, description=None, source=None, image_url=None, original_image_url=None, image_width=None, image_height=None, attachments=None, **kwargs): + def __init__( + self, + author=None, + url=None, + original_url=None, + title=None, + description=None, + source=None, + image_url=None, + original_image_url=None, + image_width=None, + image_height=None, + attachments=None, + **kwargs + ): """Represents a shared item (eg. URL) that has been sent as a Facebook attachment""" super(ShareAttachment, self).__init__(**kwargs) self.author = author @@ -352,6 +442,7 @@ class ShareAttachment(Attachment): attachments = [] self.attachments = attachments + class LocationAttachment(Attachment): #: Latidute of the location latitude = None @@ -372,6 +463,7 @@ class LocationAttachment(Attachment): self.latitude = latitude self.longitude = longitude + class LiveLocationAttachment(LocationAttachment): #: Name of the location name = None @@ -386,6 +478,7 @@ class LiveLocationAttachment(LocationAttachment): self.expiration_time = expiration_time self.is_expired = is_expired + class FileAttachment(Attachment): #: Url where you can download the file url = None @@ -404,6 +497,7 @@ class FileAttachment(Attachment): self.name = name self.is_malicious = is_malicious + class AudioAttachment(Attachment): #: Name of the file filename = None @@ -414,7 +508,9 @@ class AudioAttachment(Attachment): #: Audio type audio_type = None - def __init__(self, filename=None, url=None, duration=None, audio_type=None, **kwargs): + def __init__( + self, filename=None, url=None, duration=None, audio_type=None, **kwargs + ): """Represents an audio file that has been sent as a Facebook attachment""" super(AudioAttachment, self).__init__(**kwargs) self.filename = filename @@ -422,6 +518,7 @@ class AudioAttachment(Attachment): self.duration = duration self.audio_type = audio_type + class ImageAttachment(Attachment): #: The extension of the original image (eg. 'png') original_extension = None @@ -457,7 +554,18 @@ class ImageAttachment(Attachment): #: Height of the animated preview image animated_preview_height = None - def __init__(self, original_extension=None, width=None, height=None, is_animated=None, thumbnail_url=None, preview=None, large_preview=None, animated_preview=None, **kwargs): + def __init__( + self, + original_extension=None, + width=None, + height=None, + is_animated=None, + thumbnail_url=None, + preview=None, + large_preview=None, + animated_preview=None, + **kwargs + ): """ Represents an image that has been sent as a Facebook attachment To retrieve the full image url, use: :func:`fbchat.Client.fetchImageUrl`, @@ -492,6 +600,7 @@ class ImageAttachment(Attachment): self.animated_preview_width = animated_preview.get('width') self.animated_preview_height = animated_preview.get('height') + class VideoAttachment(Attachment): #: Size of the original video in bytes size = None @@ -525,7 +634,18 @@ class VideoAttachment(Attachment): #: Height of the large preview image large_image_height = None - def __init__(self, size=None, width=None, height=None, duration=None, preview_url=None, small_image=None, medium_image=None, large_image=None, **kwargs): + def __init__( + self, + size=None, + width=None, + height=None, + duration=None, + preview_url=None, + small_image=None, + medium_image=None, + large_image=None, + **kwargs + ): """Represents a video that has been sent as a Facebook attachment""" super(VideoAttachment, self).__init__(**kwargs) self.size = size @@ -571,7 +691,10 @@ class Mention(object): return self.__unicode__() def __unicode__(self): - return '<Mention {}: offset={} length={}>'.format(self.thread_id, self.offset, self.length) + return '<Mention {}: offset={} length={}>'.format( + self.thread_id, self.offset, self.length + ) + class QuickReply(object): #: Payload of the quick reply @@ -595,6 +718,7 @@ class QuickReply(object): def __unicode__(self): return '<{}: payload={!r}>'.format(self.__class__.__name__, self.payload) + class QuickReplyText(QuickReply): #: Title of the quick reply title = None @@ -609,6 +733,7 @@ class QuickReplyText(QuickReply): self.title = title self.image_url = image_url + class QuickReplyLocation(QuickReply): #: Type of the quick reply _type = "location" @@ -618,6 +743,7 @@ class QuickReplyLocation(QuickReply): super(QuickReplyLocation, self).__init__(**kwargs) self.is_response = False + class QuickReplyPhoneNumber(QuickReply): #: URL of the quick reply image (optional) image_url = None @@ -629,6 +755,7 @@ class QuickReplyPhoneNumber(QuickReply): super(QuickReplyPhoneNumber, self).__init__(**kwargs) self.image_url = image_url + class QuickReplyEmail(QuickReply): #: URL of the quick reply image (optional) image_url = None @@ -640,6 +767,7 @@ class QuickReplyEmail(QuickReply): super(QuickReplyEmail, self).__init__(**kwargs) self.image_url = image_url + class Poll(object): #: ID of the poll uid = None @@ -659,7 +787,10 @@ class Poll(object): return self.__unicode__() def __unicode__(self): - return '<Poll ({}): {} options={}>'.format(self.uid, repr(self.title), self.options) + return '<Poll ({}): {} options={}>'.format( + self.uid, repr(self.title), self.options + ) + class PollOption(object): #: ID of the poll option @@ -682,7 +813,10 @@ class PollOption(object): return self.__unicode__() def __unicode__(self): - return '<PollOption ({}): {} voters={}>'.format(self.uid, repr(self.text), self.voters) + return '<PollOption ({}): {} voters={}>'.format( + self.uid, repr(self.text), self.voters + ) + class Plan(object): #: ID of the plan @@ -719,7 +853,14 @@ class Plan(object): return self.__unicode__() def __unicode__(self): - return '<Plan ({}): {} time={}, location={}, location_id={}>'.format(self.uid, repr(self.title), self.time, repr(self.location), repr(self.location_id)) + return '<Plan ({}): {} time={}, location={}, location_id={}>'.format( + self.uid, + repr(self.title), + self.time, + repr(self.location), + repr(self.location_id), + ) + class ActiveStatus(object): #: Whether the user is active now @@ -738,41 +879,55 @@ class ActiveStatus(object): return self.__unicode__() def __unicode__(self): - return '<ActiveStatus: active={} last_active={} in_game={}>'.format(self.active, self.last_active, self.in_game) + return '<ActiveStatus: active={} last_active={} in_game={}>'.format( + self.active, self.last_active, self.in_game + ) + class Enum(aenum.Enum): """Used internally by fbchat to support enumerations""" + def __repr__(self): # For documentation: return '{}.{}'.format(type(self).__name__, self.name) + class ThreadType(Enum): """Used to specify what type of Facebook thread is being used. See :ref:`intro_threads` for more info""" + USER = 1 GROUP = 2 ROOM = 2 PAGE = 3 + class ThreadLocation(Enum): """Used to specify where a thread is located (inbox, pending, archived, other).""" + INBOX = 'INBOX' PENDING = 'PENDING' ARCHIVED = 'ARCHIVED' OTHER = 'OTHER' + class TypingStatus(Enum): """Used to specify whether the user is typing or has stopped typing""" + STOPPED = 0 TYPING = 1 + class EmojiSize(Enum): """Used to specify the size of a sent emoji""" + LARGE = '369239383222810' MEDIUM = '369239343222814' SMALL = '369239263222822' + class ThreadColor(Enum): """Used to specify a thread colors""" + MESSENGER_BLUE = '#0084ff' VIKING = '#44bec7' GOLDEN_POPPY = '#ffc300' @@ -789,8 +944,10 @@ class ThreadColor(Enum): BRILLIANT_ROSE = '#ff5ca1' BILOBA_FLOWER = '#a695c7' + class MessageReaction(Enum): """Used to specify a message reaction""" + LOVE = '😍' SMILE = '😆' WOW = '😮' diff --git a/fbchat/utils.py b/fbchat/utils.py index 2e1a39a..3433c56 100644 --- a/fbchat/utils.py +++ b/fbchat/utils.py @@ -16,10 +16,12 @@ from .models import * try: from urllib.parse import urlencode, parse_qs, urlparse + basestring = (str, bytes) except ImportError: from urllib import urlencode from urlparse import parse_qs, urlparse + basestring = basestring # Python 2's `input` executes the input, whereas `raw_input` just returns the input @@ -42,7 +44,7 @@ USER_AGENTS = [ "Mozilla/5.0 (Windows NT 6.3; WOW64; ; NCT50_AAP285C84A1328) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1", "Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6" + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6", ] LIKES = { @@ -51,7 +53,7 @@ LIKES = { 'small': EmojiSize.SMALL, 'l': EmojiSize.LARGE, 'm': EmojiSize.MEDIUM, - 's': EmojiSize.SMALL + 's': EmojiSize.SMALL, } @@ -69,24 +71,25 @@ GENDERS = { 9: 'male_plural', 10: 'neuter_plural', 11: 'unknown_plural', - # For graphql requests 'UNKNOWN': 'unknown', 'FEMALE': 'female_singular', 'MALE': 'male_singular', - #'': 'female_singular_guess', - #'': 'male_singular_guess', - #'': 'mixed', + # '': 'female_singular_guess', + # '': 'male_singular_guess', + # '': 'mixed', 'NEUTER': 'neuter_singular', - #'': 'unknown_singular', - #'': 'female_plural', - #'': 'male_plural', - #'': 'neuter_plural', - #'': 'unknown_plural', + # '': 'unknown_singular', + # '': 'female_plural', + # '': 'male_plural', + # '': 'neuter_plural', + # '': 'unknown_plural', } + class ReqUrl(object): """A class containing all urls used by `fbchat`""" + SEARCH = "https://www.facebook.com/ajax/typeahead/search.php" LOGIN = "https://m.facebook.com/login.php?login_attempt=1" SEND = "https://www.facebook.com/messaging/send/" @@ -94,8 +97,12 @@ class ReqUrl(object): UNSEEN_THREADS = "https://www.facebook.com/mercury/unseen_thread_ids/" THREADS = "https://www.facebook.com/ajax/mercury/threadlist_info.php" MOVE_THREAD = "https://www.facebook.com/ajax/mercury/move_thread.php" - ARCHIVED_STATUS = "https://www.facebook.com/ajax/mercury/change_archived_status.php?dpr=1" - PINNED_STATUS = "https://www.facebook.com/ajax/mercury/change_pinned_status.php?dpr=1" + ARCHIVED_STATUS = ( + "https://www.facebook.com/ajax/mercury/change_archived_status.php?dpr=1" + ) + PINNED_STATUS = ( + "https://www.facebook.com/ajax/mercury/change_pinned_status.php?dpr=1" + ) MESSAGES = "https://www.facebook.com/ajax/mercury/thread_info.php" READ_STATUS = "https://www.facebook.com/ajax/mercury/change_read_status.php" DELIVERED = "https://www.facebook.com/ajax/mercury/delivery_receipts.php" @@ -135,8 +142,12 @@ class ReqUrl(object): DELETE_THREAD = "https://www.facebook.com/ajax/mercury/delete_thread.php?dpr=1" DELETE_MESSAGES = "https://www.facebook.com/ajax/mercury/delete_messages.php?dpr=1" MUTE_THREAD = "https://www.facebook.com/ajax/mercury/change_mute_thread.php?dpr=1" - MUTE_REACTIONS = "https://www.facebook.com/ajax/mercury/change_reactions_mute_thread/?dpr=1" - MUTE_MENTIONS = "https://www.facebook.com/ajax/mercury/change_mentions_mute_thread/?dpr=1" + MUTE_REACTIONS = ( + "https://www.facebook.com/ajax/mercury/change_reactions_mute_thread/?dpr=1" + ) + MUTE_MENTIONS = ( + "https://www.facebook.com/ajax/mercury/change_mentions_mute_thread/?dpr=1" + ) CREATE_POLL = "https://www.facebook.com/messaging/group_polling/create_poll/?dpr=1" UPDATE_VOTE = "https://www.facebook.com/messaging/group_polling/update_vote/?dpr=1" GET_POLL_OPTIONS = "https://www.facebook.com/ajax/mercury/get_poll_options" @@ -148,41 +159,51 @@ class ReqUrl(object): def change_pull_channel(self, channel=None): if channel is None: - self.pull_channel = (self.pull_channel + 1) % 5 # Pull channel will be 0-4 + self.pull_channel = (self.pull_channel + 1) % 5 # Pull channel will be 0-4 else: self.pull_channel = channel self.STICKY = "https://{}-edge-chat.facebook.com/pull".format(self.pull_channel) - self.PING = "https://{}-edge-chat.facebook.com/active_ping".format(self.pull_channel) + self.PING = "https://{}-edge-chat.facebook.com/active_ping".format( + self.pull_channel + ) facebookEncoding = 'UTF-8' + def now(): - return int(time()*1000) + return int(time() * 1000) + def strip_to_json(text): try: - return text[text.index('{'):] + return text[text.index('{') :] except ValueError: raise FBchatException('No JSON object found: {!r}'.format(text)) + def get_decoded_r(r): return get_decoded(r._content) + def get_decoded(content): return content.decode(facebookEncoding) + def parse_json(content): return json.loads(content) + def get_json(r): return json.loads(strip_to_json(get_decoded_r(r))) + def digitToChar(digit): if digit < 10: return str(digit) return chr(ord('a') + digit - 10) + def str_base(number, base): if number < 0: return '-' + str_base(-number, base) @@ -191,14 +212,17 @@ def str_base(number, base): return str_base(d, base) + digitToChar(m) return digitToChar(m) + def generateMessageID(client_id=None): k = now() l = int(random() * 4294967295) return "<{}:{}-{}@mail.projektitan.com>".format(k, l, client_id) + def getSignatureID(): return hex(int(random() * 2147483648)) + def generateOfflineThreadingID(): ret = now() value = int(random() * 4294967295) @@ -206,20 +230,39 @@ def generateOfflineThreadingID(): msgs = format(ret, 'b') + string return str(int(msgs, 2)) + def check_json(j): if j.get('error') is None: return if 'errorDescription' in j: # 'errorDescription' is in the users own language! - raise FBchatFacebookError('Error #{} when sending request: {}'.format(j['error'], j['errorDescription']), fb_error_code=j['error'], fb_error_message=j['errorDescription']) + raise FBchatFacebookError( + 'Error #{} when sending request: {}'.format( + j['error'], j['errorDescription'] + ), + fb_error_code=j['error'], + fb_error_message=j['errorDescription'], + ) elif 'debug_info' in j['error'] and 'code' in j['error']: - raise FBchatFacebookError('Error #{} when sending request: {}'.format(j['error']['code'], repr(j['error']['debug_info'])), fb_error_code=j['error']['code'], fb_error_message=j['error']['debug_info']) + raise FBchatFacebookError( + 'Error #{} when sending request: {}'.format( + j['error']['code'], repr(j['error']['debug_info']) + ), + fb_error_code=j['error']['code'], + fb_error_message=j['error']['debug_info'], + ) else: - raise FBchatFacebookError('Error {} when sending request'.format(j['error']), fb_error_code=j['error']) + raise FBchatFacebookError( + 'Error {} when sending request'.format(j['error']), fb_error_code=j['error'] + ) + def check_request(r, as_json=True): if not r.ok: - raise FBchatFacebookError('Error when sending request: Got {} response'.format(r.status_code), request_status_code=r.status_code) + raise FBchatFacebookError( + 'Error when sending request: Got {} response'.format(r.status_code), + request_status_code=r.status_code, + ) content = get_decoded_r(r) @@ -238,14 +281,20 @@ def check_request(r, as_json=True): else: return content + def get_jsmods_require(j, index): if j.get('jsmods') and j['jsmods'].get('require'): try: return j['jsmods']['require'][0][index][0] except (KeyError, IndexError) as e: - log.warning('Error when getting jsmods_require: {}. Facebook might have changed protocol'.format(j)) + log.warning( + 'Error when getting jsmods_require: {}. Facebook might have changed protocol'.format( + j + ) + ) return None + def get_emojisize_from_tags(tags): if tags is None: return None @@ -254,15 +303,19 @@ def get_emojisize_from_tags(tags): try: return LIKES[tmp[0].split(':')[1]] except (KeyError, IndexError): - log.exception('Could not determine emoji size from {} - {}'.format(tags, tmp)) + log.exception( + 'Could not determine emoji size from {} - {}'.format(tags, tmp) + ) return None + def require_list(list_): if isinstance(list_, list): return set(list_) else: return set([list_]) + def mimetype_to_key(mimetype): if not mimetype: return "file_id" @@ -280,11 +333,13 @@ def get_files_from_urls(file_urls): r = requests.get(file_url) # We could possibly use r.headers.get('Content-Disposition'), see # https://stackoverflow.com/a/37060758 - files.append(( - basename(file_url), - r.content, - r.headers.get('Content-Type') or guess_type(file_url)[0], - )) + files.append( + ( + basename(file_url), + r.content, + r.headers.get('Content-Type') or guess_type(file_url)[0], + ) + ) return files @@ -292,26 +347,31 @@ def get_files_from_urls(file_urls): def get_files_from_paths(filenames): files = [] for filename in filenames: - files.append(( - basename(filename), - open(filename, 'rb'), - guess_type(filename)[0], - )) + files.append( + (basename(filename), open(filename, 'rb'), guess_type(filename)[0]) + ) yield files for fn, fp, ft in files: fp.close() + def enum_extend_if_invalid(enumeration, value): try: return enumeration(value) except ValueError: - log.warning("Failed parsing {.__name__}({!r}). Extending enum.".format(enumeration, value)) + log.warning( + "Failed parsing {.__name__}({!r}). Extending enum.".format( + enumeration, value + ) + ) aenum.extend_enum(enumeration, "UNKNOWN_{}".format(value).upper(), value) return enumeration(value) + def get_url_parameters(url, *args): params = parse_qs(urlparse(url).query) return [params[arg][0] for arg in args if params.get(arg)] + def get_url_parameter(url, param): return get_url_parameters(url, param)[0] diff --git a/pyproject.toml b/pyproject.toml index 929a534..d1cc257 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,15 @@ +[tool.black] +line-length = 88 +exclude = ''' +/( + \.git + | \.pytest_cache + | build + | dist + | venv +)/ +''' + [build-system] requires = ["flit"] build-backend = "flit.buildapi" diff --git a/tests/conftest.py b/tests/conftest.py index 014b5a9..a1e88cd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,17 +17,21 @@ def user(client2): @pytest.fixture(scope="session") def group(pytestconfig): - return {"id": load_variable("group_id", pytestconfig.cache), "type": ThreadType.GROUP} + return { + "id": load_variable("group_id", pytestconfig.cache), + "type": ThreadType.GROUP, + } -@pytest.fixture(scope="session", params=[ - "user", "group", pytest.param("none", marks=[pytest.mark.xfail()]) -]) +@pytest.fixture( + scope="session", + params=["user", "group", pytest.param("none", marks=[pytest.mark.xfail()])], +) def thread(request, user, group): return { "user": user, "group": group, - "none": {"id": "0", "type": ThreadType.GROUP} + "none": {"id": "0", "type": ThreadType.GROUP}, }[request.param] diff --git a/tests/test_fetch.py b/tests/test_fetch.py index 4150664..d509fad 100644 --- a/tests/test_fetch.py +++ b/tests/test_fetch.py @@ -48,7 +48,9 @@ def test_fetch_message_mentions(client, thread, message_with_mentions): mid = client.send(message_with_mentions) message, = client.fetchThreadMessages(limit=1) - assert subset(vars(message), uid=mid, author=client.uid, text=message_with_mentions.text) + assert subset( + vars(message), uid=mid, author=client.uid, text=message_with_mentions.text + ) # The mentions are not ordered by offset for m in message.mentions: assert vars(m) in [vars(x) for x in message_with_mentions.mentions] @@ -58,7 +60,9 @@ def test_fetch_message_info_mentions(client, thread, message_with_mentions): mid = client.send(message_with_mentions) message = client.fetchMessageInfo(mid, thread_id=thread["id"]) - assert subset(vars(message), uid=mid, author=client.uid, text=message_with_mentions.text) + assert subset( + vars(message), uid=mid, author=client.uid, text=message_with_mentions.text + ) # The mentions are not ordered by offset for m in message.mentions: assert vars(m) in [vars(x) for x in message_with_mentions.mentions] diff --git a/tests/test_plans.py b/tests/test_plans.py index 9365d4a..9e5e6b6 100644 --- a/tests/test_plans.py +++ b/tests/test_plans.py @@ -9,17 +9,17 @@ from utils import random_hex, subset from time import time -@pytest.fixture(scope="module", params=[ - Plan(int(time()) + 100, random_hex()), - pytest.param( - Plan(int(time()), random_hex()), - marks=[pytest.mark.xfail(raises=FBchatFacebookError)] - ), - pytest.param( - Plan(0, None), - marks=[pytest.mark.xfail()], - ), -]) +@pytest.fixture( + scope="module", + params=[ + Plan(int(time()) + 100, random_hex()), + pytest.param( + Plan(int(time()), random_hex()), + marks=[pytest.mark.xfail(raises=FBchatFacebookError)], + ), + pytest.param(Plan(0, None), marks=[pytest.mark.xfail()]), + ], +) def plan_data(request, client, user, thread, catch_event, compare): with catch_event("onPlanCreated") as x: client.createPlan(request.param, thread["id"]) @@ -50,15 +50,14 @@ def test_fetch_plan_info(client, catch_event, plan_data): event, plan = plan_data fetched_plan = client.fetchPlanInfo(plan.uid) assert subset( - vars(fetched_plan), - time=plan.time, - title=plan.title, - author_id=int(client.uid), + vars(fetched_plan), time=plan.time, title=plan.title, author_id=int(client.uid) ) @pytest.mark.parametrize("take_part", [False, True]) -def test_change_plan_participation(client, thread, catch_event, compare, plan_data, take_part): +def test_change_plan_participation( + client, thread, catch_event, compare, plan_data, take_part +): event, plan = plan_data with catch_event("onPlanParticipation") as x: client.changePlanParticipation(plan, take_part=take_part) @@ -94,18 +93,22 @@ def test_on_plan_ended(client, thread, catch_event, compare): with catch_event("onPlanEnded") as x: client.createPlan(Plan(int(time()) + 120, "Wait for ending")) x.wait(180) - assert subset(x.res, thread_id=client.uid if thread["type"] == ThreadType.USER else thread["id"], thread_type=thread["type"]) + assert subset( + x.res, + thread_id=client.uid if thread["type"] == ThreadType.USER else thread["id"], + thread_type=thread["type"], + ) -#createPlan(self, plan, thread_id=None) -#editPlan(self, plan, new_plan) -#deletePlan(self, plan) -#changePlanParticipation(self, plan, take_part=True) +# createPlan(self, plan, thread_id=None) +# editPlan(self, plan, new_plan) +# deletePlan(self, plan) +# changePlanParticipation(self, plan, take_part=True) -#onPlanCreated(self, mid=None, plan=None, author_id=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None) -#onPlanEnded(self, mid=None, plan=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None) -#onPlanEdited(self, mid=None, plan=None, author_id=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None) -#onPlanDeleted(self, mid=None, plan=None, author_id=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None) -#onPlanParticipation(self, mid=None, plan=None, take_part=None, author_id=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None) +# onPlanCreated(self, mid=None, plan=None, author_id=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None) +# onPlanEnded(self, mid=None, plan=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None) +# onPlanEdited(self, mid=None, plan=None, author_id=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None) +# onPlanDeleted(self, mid=None, plan=None, author_id=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None) +# onPlanParticipation(self, mid=None, plan=None, take_part=None, author_id=None, thread_id=None, thread_type=None, ts=None, metadata=None, msg=None) -#fetchPlanInfo(self, plan_id) +# fetchPlanInfo(self, plan_id) diff --git a/tests/test_polls.py b/tests/test_polls.py index 96de743..32c2457 100644 --- a/tests/test_polls.py +++ b/tests/test_polls.py @@ -8,28 +8,40 @@ from fbchat.models import Poll, PollOption, ThreadType from utils import random_hex, subset -@pytest.fixture(scope="module", params=[ - Poll(title=random_hex(), options=[]), - Poll(title=random_hex(), options=[ - PollOption(random_hex(), vote=True), - PollOption(random_hex(), vote=True), - ]), - Poll(title=random_hex(), options=[ - PollOption(random_hex(), vote=False), - PollOption(random_hex(), vote=False), - ]), - Poll(title=random_hex(), options=[ - PollOption(random_hex(), vote=True), - PollOption(random_hex(), vote=True), - PollOption(random_hex(), vote=False), - PollOption(random_hex(), vote=False), - PollOption(random_hex()), - PollOption(random_hex()), - ]), - pytest.param( - Poll(title=None, options=[]), marks=[pytest.mark.xfail(raises=ValueError)] - ), -]) +@pytest.fixture( + scope="module", + params=[ + Poll(title=random_hex(), options=[]), + Poll( + title=random_hex(), + options=[ + PollOption(random_hex(), vote=True), + PollOption(random_hex(), vote=True), + ], + ), + Poll( + title=random_hex(), + options=[ + PollOption(random_hex(), vote=False), + PollOption(random_hex(), vote=False), + ], + ), + Poll( + title=random_hex(), + options=[ + PollOption(random_hex(), vote=True), + PollOption(random_hex(), vote=True), + PollOption(random_hex(), vote=False), + PollOption(random_hex(), vote=False), + PollOption(random_hex()), + PollOption(random_hex()), + ], + ), + pytest.param( + Poll(title=None, options=[]), marks=[pytest.mark.xfail(raises=ValueError)] + ), + ], +) def poll_data(request, client1, group, catch_event): with catch_event("onPollCreated") as x: client1.createPoll(request.param, thread_id=group["id"]) @@ -45,11 +57,17 @@ def test_create_poll(client1, group, catch_event, poll_data): thread_id=group["id"], thread_type=ThreadType.GROUP, ) - assert subset(vars(event["poll"]), title=poll.title, options_count=len(poll.options)) - for recv_option in event["poll"].options: # The recieved options may not be the full list + assert subset( + vars(event["poll"]), title=poll.title, options_count=len(poll.options) + ) + for recv_option in event[ + "poll" + ].options: # The recieved options may not be the full list old_option, = list(filter(lambda o: o.text == recv_option.text, poll.options)) voters = [client1.uid] if old_option.vote else [] - assert subset(vars(recv_option), voters=voters, votes_count=len(voters), vote=False) + assert subset( + vars(recv_option), voters=voters, votes_count=len(voters), vote=False + ) def test_fetch_poll_options(client1, group, catch_event, poll_data): @@ -62,11 +80,15 @@ def test_fetch_poll_options(client1, group, catch_event, poll_data): @pytest.mark.trylast def test_update_poll_vote(client1, group, catch_event, poll_data): event, poll, options = poll_data - new_vote_ids = [o.uid for o in options[0:len(options):2] if not o.vote] - re_vote_ids = [o.uid for o in options[0:len(options):2] if o.vote] + new_vote_ids = [o.uid for o in options[0 : len(options) : 2] if not o.vote] + re_vote_ids = [o.uid for o in options[0 : len(options) : 2] if o.vote] new_options = [random_hex(), random_hex()] with catch_event("onPollVoted") as x: - client1.updatePollVote(event["poll"].uid, option_ids=new_vote_ids + re_vote_ids, new_options=new_options) + client1.updatePollVote( + event["poll"].uid, + option_ids=new_vote_ids + re_vote_ids, + new_options=new_options, + ) assert subset( x.res, @@ -74,8 +96,12 @@ def test_update_poll_vote(client1, group, catch_event, poll_data): thread_id=group["id"], thread_type=ThreadType.GROUP, ) - assert subset(vars(x.res["poll"]), title=poll.title, options_count=len(options + new_options)) + assert subset( + vars(x.res["poll"]), title=poll.title, options_count=len(options + new_options) + ) for o in new_vote_ids: assert o in x.res["added_options"] assert len(x.res["added_options"]) == len(new_vote_ids) + len(new_options) - assert set(x.res["removed_options"]) == set(o.uid for o in options if o.vote and o.uid not in re_vote_ids) + assert set(x.res["removed_options"]) == set( + o.uid for o in options if o.vote and o.uid not in re_vote_ids + ) diff --git a/tests/test_send.py b/tests/test_send.py index 3b53059..3990600 100644 --- a/tests/test_send.py +++ b/tests/test_send.py @@ -38,7 +38,12 @@ def test_send_mentions(client, catch_event, compare, message_with_mentions): mid = client.send(message_with_mentions) assert compare(x, mid=mid, message=message_with_mentions.text) - assert subset(vars(x.res["message_object"]), uid=mid, author=client.uid, text=message_with_mentions.text) + assert subset( + vars(x.res["message_object"]), + uid=mid, + author=client.uid, + text=message_with_mentions.text, + ) # The mentions are not ordered by offset for m in x.res["message_object"].mentions: assert vars(m) in [vars(x) for x in message_with_mentions.mentions] @@ -76,7 +81,15 @@ def test_send_images(client, catch_event, compare, method_name, url): def test_send_local_files(client, catch_event, compare): - files = ["image.png", "image.jpg", "image.gif", "file.json", "file.txt", "audio.mp3", "video.mp4"] + files = [ + "image.png", + "image.jpg", + "image.gif", + "file.json", + "file.txt", + "audio.mp3", + "video.mp4", + ] text = "Files sent locally" with catch_event("onMessage") as x: mid = client.sendLocalFiles( @@ -95,7 +108,10 @@ def test_send_remote_files(client, catch_event, compare): text = "Files sent from remote" with catch_event("onMessage") as x: mid = client.sendRemoteFiles( - ["https://github.com/carpedm20/fbchat/raw/master/tests/{}".format(f) for f in files], + [ + "https://github.com/carpedm20/fbchat/raw/master/tests/{}".format(f) + for f in files + ], message=Message(text), ) diff --git a/tests/test_thread_interraction.py b/tests/test_thread_interraction.py index be5b0ff..8a34530 100644 --- a/tests/test_thread_interraction.py +++ b/tests/test_thread_interraction.py @@ -67,14 +67,19 @@ def test_change_nickname(client, client_all, catch_event, compare): assert compare(x, changed_for=client_all.uid, new_nickname=nickname) -@pytest.mark.parametrize("emoji", [ - "😀", - "😂", - "😕", - "😍", - pytest.param("🙃", marks=[pytest.mark.xfail(raises=FBchatFacebookError)]), - pytest.param("not an emoji", marks=[pytest.mark.xfail(raises=FBchatFacebookError)]), -]) +@pytest.mark.parametrize( + "emoji", + [ + "😀", + "😂", + "😕", + "😍", + pytest.param("🙃", marks=[pytest.mark.xfail(raises=FBchatFacebookError)]), + pytest.param( + "not an emoji", marks=[pytest.mark.xfail(raises=FBchatFacebookError)] + ), + ], +) def test_change_emoji(client, catch_event, compare, emoji): with catch_event("onEmojiChange") as x: client.changeThreadEmoji(emoji) @@ -85,7 +90,9 @@ def test_change_image_local(client1, group, catch_event): url = path.join(path.dirname(__file__), "resources", "image.png") with catch_event("onImageChange") as x: image_id = client1.changeGroupImageLocal(url, group["id"]) - assert subset(x.res, new_image=image_id, author_id=client1.uid, thread_id=group["id"]) + assert subset( + x.res, new_image=image_id, author_id=client1.uid, thread_id=group["id"] + ) # To be changed when merged into master @@ -93,7 +100,9 @@ def test_change_image_remote(client1, group, catch_event): url = "https://github.com/carpedm20/fbchat/raw/master/tests/image.png" with catch_event("onImageChange") as x: image_id = client1.changeGroupImageRemote(url, group["id"]) - assert subset(x.res, new_image=image_id, author_id=client1.uid, thread_id=group["id"]) + assert subset( + x.res, new_image=image_id, author_id=client1.uid, thread_id=group["id"] + ) @pytest.mark.parametrize( @@ -138,6 +147,7 @@ def test_change_approval_mode(client1, group, catch_event, require_admin_approva thread_id=group["id"], ) + @pytest.mark.parametrize("mute_time", [0, 10, 100, 1000, -1]) def test_mute_thread(client, mute_time): assert client.muteThread(mute_time)