reintroduce things skipped on conflict

This commit is contained in:
Dainius
2017-05-10 18:16:41 +03:00
parent 0d75c09036
commit 357083efce
5 changed files with 362 additions and 162 deletions

2
.gitignore vendored
View File

@@ -1,5 +1,7 @@
*py[co] *py[co]
.idea/
# Test scripts # Test scripts
*.sh *.sh

View File

@@ -20,6 +20,7 @@ from bs4 import BeautifulSoup as bs
from mimetypes import guess_type from mimetypes import guess_type
from .utils import * from .utils import *
from .models import * from .models import *
from .event_hook import *
import time import time
import sys import sys
@@ -68,7 +69,8 @@ class Client(object):
documentation for the API. documentation for the API.
""" """
def __init__(self, email, password, debug=False, info_log=False, user_agent=None, max_retries=5, session_cookies=None, logging_level=logging.INFO): def __init__(self, email, password, debug=False, info_log=False, user_agent=None, max_retries=5,
session_cookies=None, logging_level=logging.INFO):
"""A client for the Facebook Chat (Messenger). """A client for the Facebook Chat (Messenger).
:param email: Facebook `email` or `id` or `phone number` :param email: Facebook `email` or `id` or `phone number`
@@ -92,6 +94,8 @@ class Client(object):
self.default_thread_type = None self.default_thread_type = None
self.threads = [] self.threads = []
self._setupEventHooks()
if not user_agent: if not user_agent:
user_agent = choice(USER_AGENTS) user_agent = choice(USER_AGENTS)
@@ -114,9 +118,55 @@ class Client(object):
handler.setLevel(logging_level) handler.setLevel(logging_level)
# If session cookies aren't set, not properly loaded or gives us an invalid session, then do the login # 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.is_logged_in(): if not session_cookies or not self.setSession(session_cookies) or not self.isLoggedIn():
self.login(email, password, max_retries) self.login(email, password, max_retries)
def _setupEventHooks(self):
# Setup event hooks
self.onLoggingIn = EventHook(email=str)
self.onLoggedIn = EventHook(email=str)
self.onListening = EventHook()
self.onMessage = EventHook(mid=str, author_id=str, message=str, thread_id=int, thread_type=ThreadType, ts=str, metadata=dict)
self.onColorChange = EventHook(mid=str, author_id=str, new_color=str, thread_id=str, thread_type=ThreadType, ts=str, metadata=dict)
self.onEmojiChange = EventHook(mid=str, author_id=str, new_emoji=str, thread_id=str, thread_type=ThreadType, ts=str, metadata=dict)
self.onTitleChange = EventHook(mid=str, author_id=str, new_title=str, thread_id=str, thread_type=ThreadType, ts=str, metadata=dict)
self.onNicknameChange = EventHook(mid=str, author_id=str, changed_for=str, new_title=str, thread_id=str, thread_type=ThreadType, ts=str, metadata=dict)
# self.onTyping = EventHook(author_id=int, typing_status=TypingStatus)
# self.onSeen = EventHook(seen_by=str, thread_id=str, timestamp=str)
self.onInbox = EventHook(unseen=int, unread=int, recent_unread=int)
self.onPeopleAdded = EventHook(added_ids=list, author_id=str, thread_id=str)
self.onPersonRemoved = EventHook(removed_id=str, author_id=str, thread_id=str)
self.onFriendRequest = EventHook(from_id=str)
self.onUnknownMesssageType = EventHook(msg=dict)
# Setup event handlers
self.onLoggingIn += lambda email: log.info("Logging in %s..." % email)
self.onLoggedIn += lambda email: log.info("Login of %s successful." % email)
self.onListening += lambda: log.info("Listening...")
self.onMessage += lambda mid, author_id, message, thread_id, thread_type, ts, metadata:\
log.info("Message from %s in %s (%s): %s" % (author_id, thread_id, thread_type.name, message))
self.onColorChange += lambda mid, author_id, new_color, thread_id, thread_type, ts, metadata:\
log.info("Color change from %s in %s (%s): %s" % (author_id, thread_id, thread_type.name, new_color))
self.onEmojiChange += lambda mid, author_id, new_emoji, thread_id, thread_type, ts, metadata:\
log.info("Emoji change from %s in %s (%s): %s" % (author_id, thread_id, thread_type.name, new_emoji))
self.onTitleChange += lambda mid, author_id, new_title, thread_id, thread_type, ts, metadata:\
log.info("Title change from %s in %s (%s): %s" % (author_id, thread_id, thread_type.name, new_title))
self.onNicknameChange += lambda mid, author_id, new_title, changed_for, thread_id, thread_type, ts, metadata:\
log.info("Nickname change from %s in %s (%s) for %s: %s" % (author_id, thread_id, thread_type.name, changed_for, new_title))
self.onPeopleAdded += lambda added_ids, author_id, thread_id:\
log.info("%s added: %s" % (author_id, [x for x in added_ids]))
self.onPersonRemoved += lambda removed_id, author_id, thread_id:\
log.info("%s removed: %s" % (author_id, removed_id))
self.onUnknownMesssageType += lambda msg:\
log.info("Unknown message type received: %s" % msg)
@deprecated(deprecated_in='0.6.0', details='Use log.<level> instead') @deprecated(deprecated_in='0.6.0', details='Use log.<level> instead')
def _console(self, msg): def _console(self, msg):
"""Assumes an INFO level and log it. """Assumes an INFO level and log it.
@@ -133,11 +183,6 @@ class Client(object):
""" """
log.debug(msg) log.debug(msg)
def _setttstamp(self):
for i in self.fb_dtsg:
self.ttstamp += str(ord(i))
self.ttstamp += '2'
def _generatePayload(self, query): def _generatePayload(self, query):
"""Adds the following defaults to the payload: """Adds the following defaults to the payload:
__rev, __user, __a, ttstamp, fb_dtsg, __req __rev, __user, __a, ttstamp, fb_dtsg, __req
@@ -169,7 +214,7 @@ class Client(object):
payload=self._generatePayload(None) payload=self._generatePayload(None)
return self._session.post(url, data=payload, timeout=timeout, files=files) return self._session.post(url, data=payload, timeout=timeout, files=files)
def _post_login(self): def _postLogin(self):
self.payloadDefault = {} self.payloadDefault = {}
self.client_id = hex(int(random()*2147483648))[2:] self.client_id = hex(int(random()*2147483648))[2:]
self.start_time = now() self.start_time = now()
@@ -183,7 +228,9 @@ class Client(object):
log.debug(r.url) log.debug(r.url)
self.fb_dtsg = soup.find("input", {'name':'fb_dtsg'})['value'] self.fb_dtsg = soup.find("input", {'name':'fb_dtsg'})['value']
self.fb_h = soup.find("input", {'name':'h'})['value'] self.fb_h = soup.find("input", {'name':'h'})['value']
self._setttstamp() for i in self.fb_dtsg:
self.ttstamp += str(ord(i))
self.ttstamp += '2'
# Set default payload # Set default payload
self.payloadDefault['__rev'] = int(r.text.split('"revision":',1)[1].split(",",1)[0]) self.payloadDefault['__rev'] = int(r.text.split('"revision":',1)[1].split(",",1)[0])
self.payloadDefault['__user'] = self.uid self.payloadDefault['__user'] = self.uid
@@ -228,7 +275,7 @@ class Client(object):
r = self._cleanGet(SaveDeviceURL) r = self._cleanGet(SaveDeviceURL)
if 'home' in r.url: if 'home' in r.url:
self._post_login() self._postLogin()
return True return True
else: else:
return False return False
@@ -284,13 +331,10 @@ class Client(object):
r = self._cleanPost(CheckpointURL, data) r = self._cleanPost(CheckpointURL, data)
return r return r
def is_logged_in(self): def isLoggedIn(self):
# Send a request to the login url, to see if we're directed to the home page. # Send a request to the login url, to see if we're directed to the home page.
r = self._cleanGet(LoginURL) r = self._cleanGet(LoginURL)
if 'home' in r.url: return 'home' in r.url
return True
else:
return False
def getSession(self): def getSession(self):
"""Returns the session cookies""" """Returns the session cookies"""
@@ -309,12 +353,11 @@ class Client(object):
# Load cookies into current session # 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._post_login() self._postLogin()
return True return True
def login(self, email, password, max_retries=5): def login(self, email, password, max_retries=5):
# Logging in self.onLoggingIn(email=email)
log.info("Logging in {}...".format(email))
if not (email and password): if not (email and password):
raise Exception("Email and password not set.") raise Exception("Email and password not set.")
@@ -328,7 +371,7 @@ class Client(object):
time.sleep(1) time.sleep(1)
continue continue
else: else:
log.info("Login of {} successful.".format(email)) self.onLoggedIn(email=email)
break break
else: else:
raise Exception("Login failed. Check email/password.") raise Exception("Login failed. Check email/password.")
@@ -350,9 +393,10 @@ class Client(object):
@deprecated(deprecated_in='0.10.2', details='Use setDefaultThread instead') @deprecated(deprecated_in='0.10.2', details='Use setDefaultThread instead')
def setDefaultRecipient(self, recipient_id, is_user=True): def setDefaultRecipient(self, recipient_id, is_user=True):
self.setDefaultThread(recipient_id, thread_type=isUserToThreadType(is_user)) self.setDefaultThread(str(recipient_id), thread_type=isUserToThreadType(is_user))
def setDefaultThread(self, thread_id=None, thread_type=ThreadType.USER): def setDefaultThread(self, thread_id, thread_type):
# type: (str, ThreadType) -> None
"""Sets default thread to send messages and images to. """Sets default thread to send messages and images to.
:param thread_id: user/group ID to default to :param thread_id: user/group ID to default to
@@ -361,38 +405,11 @@ class Client(object):
self.default_thread_id = thread_id self.default_thread_id = thread_id
self.default_thread_type = thread_type self.default_thread_type = thread_type
def _adapt_user_in_chat_to_user_model(self, user_in_chat): def resetDefaultThread(self):
""" Adapts user info from chat to User model acceptable initial dict # type: () -> None
"""Resets default thread."""
:param user_in_chat: user info from chat self.default_thread_id = None
self.default_thread_type = None
'dir': None,
'mThumbSrcSmall': None,
'is_friend': False,
'is_nonfriend_messenger_contact': True,
'alternateName': '',
'i18nGender': 16777216,
'vanity': '',
'type': 'friend',
'searchTokens': ['Voznesenskij', 'Sergej'],
'thumbSrc': 'https://fb-s-b-a.akamaihd.net/h-ak-xfa1/v/t1.0-1/c9.0.32.32/p32x32/10354686_10150004552801856_220367501106153455_n.jpg?oh=71a87d76d4e4d17615a20c43fb8dbb47&oe=59118CE4&__gda__=1493753268_ae75cef40e9785398e744259ccffd7ff',
'mThumbSrcLarge': None,
'firstName': 'Sergej',
'name': 'Sergej Voznesenskij',
'uri': 'https://www.facebook.com/profile.php?id=100014812758264',
'id': '100014812758264',
'gender': 2
"""
return {
'type': 'user',
'uid': user_in_chat['id'],
'photo': user_in_chat['thumbSrc'],
'path': user_in_chat['uri'],
'text': user_in_chat['name'],
'score': '',
'data': user_in_chat,
}
def getAllUsers(self): def getAllUsers(self):
""" Gets all users from chat with info included """ """ Gets all users from chat with info included """
@@ -411,7 +428,7 @@ class Client(object):
for k in payload.keys(): for k in payload.keys():
try: try:
user = self._adapt_user_in_chat_to_user_model(payload[k]) user = User.adaptFromChat(payload[k])
except KeyError: except KeyError:
continue continue
@@ -448,6 +465,7 @@ class Client(object):
""" """
def _send(self, thread_id=None, message=None, thread_type=ThreadType.USER, emoji_size=None, image_id=None, add_user_ids=None, new_title=None): def _send(self, thread_id=None, message=None, thread_type=ThreadType.USER, emoji_size=None, image_id=None, add_user_ids=None, new_title=None):
# type: (str, str, ThreadType, EmojiSize, str, list, str) -> list
"""Send a message with given thread id """Send a message with given thread id
:param thread_id: the user id or thread id that you want to send a message to :param thread_id: the user id or thread id that you want to send a message to
@@ -538,7 +556,7 @@ class Client(object):
if not r.ok: if not r.ok:
log.warning('Error when sending message: Got {} response'.format(r.status_code)) log.warning('Error when sending message: Got {} response'.format(r.status_code))
return False return None
response_content = {} response_content = {}
if isinstance(r.content, str) is False: if isinstance(r.content, str) is False:
@@ -547,7 +565,7 @@ class Client(object):
if 'error' in j: if 'error' in j:
# 'errorDescription' is in the users own language! # 'errorDescription' is in the users own language!
log.warning('Error #{} when sending message: {}'.format(j['error'], j['errorDescription'])) log.warning('Error #{} when sending message: {}'.format(j['error'], j['errorDescription']))
return False return None
message_ids = [] message_ids = []
try: try:
@@ -555,7 +573,7 @@ class Client(object):
message_ids[0] # Try accessing element message_ids[0] # Try accessing element
except (KeyError, IndexError) as e: except (KeyError, IndexError) as e:
log.warning('Error when sending message: No message ids could be found') log.warning('Error when sending message: No message ids could be found')
return False return None
log.info('Message sent.') log.info('Message sent.')
log.debug("Sending {}".format(r)) log.debug("Sending {}".format(r))
@@ -567,8 +585,10 @@ class Client(object):
return self._send(thread_id=recipient_id, message=message, thread_type=isUserToThreadType(is_user), emoji_size=LIKES[like], image_id=image_id, add_user_ids=add_user_ids) return self._send(thread_id=recipient_id, message=message, thread_type=isUserToThreadType(is_user), emoji_size=LIKES[like], image_id=image_id, add_user_ids=add_user_ids)
def sendMessage(self, message, thread_id=None, thread_type=ThreadType.USER): def sendMessage(self, message, thread_id=None, thread_type=ThreadType.USER):
# type: (str, str, ThreadType) -> list
""" """
Sends a message to given (or default, if not) thread with an additional image. Sends a message to given (or default, if not) thread with an additional image.
:param message: message to send :param message: message to send
:param thread_id: user/group chat ID :param thread_id: user/group chat ID
:param thread_type: specify whether thread_id is user or group chat :param thread_type: specify whether thread_id is user or group chat
@@ -576,20 +596,25 @@ class Client(object):
""" """
return self._send(thread_id=thread_id, message=message, thread_type=thread_type) return self._send(thread_id=thread_id, message=message, thread_type=thread_type)
def sendEmoji(self, thread_id, size=Size.MEDIUM, emoji=None, thread_type=ThreadType.USER): def sendEmoji(self, emoji=None, size=EmojiSize.SMALL, thread_id=None, thread_type=ThreadType.USER):
# type: (str, EmojiSize, str, ThreadType) -> list
""" """
Sends an emoji to given (or default, if not) thread. Sends an emoji to given (or default, if not) thread.
:param emoji: WIP
:param size: size of emoji to send :param size: size of emoji to send
:param thread_id: user/group chat ID :param thread_id: user/group chat ID
:param emoji: WIP
:param thread_type: specify whether thread_id is user or group chat :param thread_type: specify whether thread_id is user or group chat
:return: a list of message ids of the sent message(s) :return: a list of message ids of the sent message(s)
""" """
return self._send(thread_id=thread_id, thread_type=thread_type, emoji_size=size) return self._send(thread_id=thread_id, thread_type=thread_type, emoji_size=size)
def sendRemoteImage(self, image_url, message=None, thread_id=None, thread_type=ThreadType.USER, recipient_id=None, is_user=None, image=None): def sendRemoteImage(self, image_url, message=None, thread_id=None, thread_type=ThreadType.USER,
recipient_id=None, is_user=None, image=None):
# type: (str, str, str, ThreadType) -> list
""" """
Sends an image from given URL to given (or default, if not) thread. Sends an image from given URL to given (or default, if not) thread.
:param image_url: URL of an image to upload and send :param image_url: URL of an image to upload and send
:param message: additional message :param message: additional message
:param thread_id: user/group chat ID :param thread_id: user/group chat ID
@@ -597,7 +622,6 @@ class Client(object):
:return: a list of message ids of the sent message(s) :return: a list of message ids of the sent message(s)
""" """
if recipient_id is not None: if recipient_id is not None:
deprecation('sendRemoteImage(recipient_id)', deprecated_in='0.10.2', details='Use sendRemoteImage(thread_id) instead')
thread_id = recipient_id thread_id = recipient_id
if is_user is not None: if is_user is not None:
deprecation('sendRemoteImage(is_user)', deprecated_in='0.10.2', details='Use sendRemoteImage(thread_type) instead') deprecation('sendRemoteImage(is_user)', deprecated_in='0.10.2', details='Use sendRemoteImage(thread_type) instead')
@@ -611,9 +635,12 @@ class Client(object):
return self._send(thread_id=thread_id, message=message, thread_type=thread_type, image_id=image_id) return self._send(thread_id=thread_id, message=message, thread_type=thread_type, image_id=image_id)
# Doesn't upload properly # Doesn't upload properly
def sendLocalImage(self, image_path, message=None, thread_id=None, thread_type=ThreadType.USER, recipient_id=None, is_user=None, image=None): def sendLocalImage(self, image_path, message=None, thread_id=None, thread_type=ThreadType.USER,
recipient_id=None, is_user=None, image=None):
# type: (str, str, str, ThreadType) -> list
""" """
Sends an image from given URL to given (or default, if not) thread. Sends an image from given URL to given (or default, if not) thread.
:param image_path: path of an image to upload and send :param image_path: path of an image to upload and send
:param message: additional message :param message: additional message
:param thread_id: user/group chat ID :param thread_id: user/group chat ID
@@ -634,15 +661,18 @@ class Client(object):
return self._send(thread_id=thread_id, message=message, thread_type=thread_type, image_id=image_id) return self._send(thread_id=thread_id, message=message, thread_type=thread_type, image_id=image_id)
def addUsersToChat(self, user_ids, thread_id=None): def addUsersToChat(self, user_ids, thread_id=None):
# type: (list, str) -> list
""" """
Adds users to given (or default, if not) thread. Adds users to given (or default, if not) thread.
:param user_ids: list of user ids to add :param user_ids: list of user ids to add
:param thread_id: group chat ID :param thread_id: group chat ID
:return: a list of message ids of the sent message(s) :return: a list of message ids of the sent message(s)
""" """
return self._send(thread_id=thread_id, thread_type=ThreadType.GROUP, add_user_ids=users) return self._send(thread_id=thread_id, thread_type=ThreadType.GROUP, add_user_ids=user_ids)
def removeUserFromChat(self, user_id, thread_id=None): def removeUserFromChat(self, user_id, thread_id=None):
# type: (str, str) -> bool
""" """
Adds users to given (or default, if not) thread. Adds users to given (or default, if not) thread.
:param user_id: user ID to remove :param user_id: user ID to remove
@@ -705,6 +735,7 @@ class Client(object):
return json.loads(response_content[9:])['payload']['metadata'][0]['image_id'] return json.loads(response_content[9:])['payload']['metadata'][0]['image_id']
def getThreadInfo(self, last_n=20, thread_id=None, thread_type=ThreadType.USER): def getThreadInfo(self, last_n=20, thread_id=None, thread_type=ThreadType.USER):
# type: (int, str, ThreadType) -> list
"""Get the info of one Thread """Get the info of one Thread
:param last_n: number of retrieved messages from start (default 20) :param last_n: number of retrieved messages from start (default 20)
@@ -745,6 +776,7 @@ class Client(object):
def getThreadList(self, start, length=20): def getThreadList(self, start, length=20):
# type: (int, int) -> list
"""Get thread list of your facebook account. """Get thread list of your facebook account.
:param start: the start index of a thread :param start: the start index of a thread
@@ -786,7 +818,6 @@ class Client(object):
return self.threads return self.threads
def getUnread(self): def getUnread(self):
form = { form = {
'client': 'mercury_sync', 'client': 'mercury_sync',
@@ -815,7 +846,6 @@ class Client(object):
r = self._post(DeliveredURL, data) r = self._post(DeliveredURL, data)
return r.ok return r.ok
def markAsRead(self, userID): def markAsRead(self, userID):
data = { data = {
"watermarkTimestamp": now(), "watermarkTimestamp": now(),
@@ -826,23 +856,24 @@ class Client(object):
r = self._post(ReadStatusURL, data) r = self._post(ReadStatusURL, data)
return r.ok return r.ok
def markAsSeen(self): def markAsSeen(self):
r = self._post(MarkSeenURL, {"seen_timestamp": 0}) r = self._post(MarkSeenURL, {"seen_timestamp": 0})
return r.ok return r.ok
@deprecated(deprecated_in='0.10.2', details='Use friendConnect() instead')
def friend_connect(self, friend_id): def friend_connect(self, friend_id):
return self.friendConnect(friend_id)
def friendConnect(self, friend_id):
# type: (str) -> bool
data = { data = {
"to_friend": friend_id, "to_friend": friend_id,
"action": "confirm" "action": "confirm"
} }
r = self._post(ConnectURL, data) r = self._post(ConnectURL, data)
return r.ok return r.ok
def ping(self, sticky): def ping(self, sticky):
data = { data = {
'channel': self.user_channel, 'channel': self.user_channel,
@@ -856,7 +887,6 @@ class Client(object):
r = self._get(PingURL, data) r = self._get(PingURL, data)
return r.ok return r.ok
def _getSticky(self): def _getSticky(self):
"""Call pull api to get sticky and pool parameter, """Call pull api to get sticky and pool parameter,
newer api needs these parameter to work. newer api needs these parameter to work.
@@ -878,7 +908,6 @@ class Client(object):
pool = j['lb_info']['pool'] pool = j['lb_info']['pool']
return sticky, pool return sticky, pool
def _pullMessage(self, sticky, pool): def _pullMessage(self, sticky, pool):
"""Call pull api with seq value to get message data.""" """Call pull api with seq value to get message data."""
@@ -896,80 +925,161 @@ class Client(object):
self.seq = j.get('seq', '0') self.seq = j.get('seq', '0')
return j return j
def _parseMessage(self, content): def _parseMessage(self, content):
"""Get message and author name from content. """Get message and author name from content. May contain multiple messages in the content."""
May contains multiple messages in the content.
"""
if 'ms' not in content: return if 'ms' not in content: return
log.debug("Received {}".format(content["ms"])) log.debug("Received {}".format(content["ms"]))
for m in content['ms']: for m in content["ms"]:
mtype = m.get("type")
try: try:
if m['type'] in ['m_messaging', 'messaging']: # Things that directly change chat
if m['event'] in ['deliver']: if mtype == "delta":
mid = m['message']['mid']
message = m['message']['body'] def getThreadIdAndThreadType(msg_metadata):
fbid = m['message']['sender_fbid'] """Returns a tuple consisting of thread id and thread type"""
name = m['message']['sender_name'] id_thread = None
self.on_message(mid, fbid, name, message, m) type_thread = None
elif m['type'] in ['typ']: if 'threadFbId' in msg_metadata['threadKey']:
self.on_typing(m.get("from")) id_thread = str(msg_metadata['threadKey']['threadFbId'])
elif m['type'] in ['m_read_receipt']: type_thread = ThreadType.GROUP
self.on_read(m.get('realtime_viewer_fbid'), m.get('reader'), m.get('time')) elif 'otherUserFbId' in msg_metadata['threadKey']:
elif m['type'] in ['inbox']: id_thread = str(msg_metadata['threadKey']['otherUserFbId'])
viewer = m.get('realtime_viewer_fbid') type_thread = ThreadType.USER
unseen = m.get('unseen') return id_thread, type_thread
unread = m.get('unread')
other_unseen = m.get('other_unseen') delta = m["delta"]
other_unread = m.get('other_unread') delta_type = delta.get("type")
timestamp = m.get('seen_timestamp') metadata = delta.get("messageMetadata")
self.on_inbox(viewer, unseen, unread, other_unseen, other_unread, timestamp)
elif m['type'] in ['qprimer']: if metadata is not None:
self.on_qprimer(m.get('made')) mid = metadata["messageId"]
elif m['type'] in ['delta']: author_id = str(metadata['actorFbId'])
if 'leftParticipantFbId' in m['delta']: ts = int(metadata["timestamp"])
user_id = m['delta']['leftParticipantFbId']
actor_id = m['delta']['messageMetadata']['actorFbId'] # Added participants
thread_id = m['delta']['messageMetadata']['threadKey']['threadFbId'] if 'addedParticipants' in delta:
self.on_person_removed(user_id, actor_id, thread_id) added_ids = [str(x['userFbId']) for x in delta['addedParticipants']]
elif 'addedParticipants' in m['delta']: thread_id = str(metadata['threadKey']['threadFbId'])
user_ids = [x['userFbId'] for x in m['delta']['addedParticipants']] self.onPeopleAdded(mid=mid, added_ids=added_ids, author_id=author_id, thread_id=thread_id,
actor_id = m['delta']['messageMetadata']['actorFbId'] ts=ts)
thread_id = m['delta']['messageMetadata']['threadKey']['threadFbId'] continue
self.on_people_added(user_ids, actor_id, thread_id)
elif 'messageMetadata' in m['delta']: # Left/removed participants
recipient_id = 0 elif 'leftParticipantFbId' in delta:
thread_type = None removed_id = str(delta['leftParticipantFbId'])
if 'threadKey' in m['delta']['messageMetadata']: thread_id = str(metadata['threadKey']['threadFbId'])
if 'threadFbId' in m['delta']['messageMetadata']['threadKey']: self.onPersonRemoved(mid=mid, removed_id=removed_id, author_id=author_id, thread_id=thread_id,
recipient_id = m['delta']['messageMetadata']['threadKey']['threadFbId'] ts=ts)
thread_type = 'group' continue
elif 'otherUserFbId' in m['delta']['messageMetadata']['threadKey']:
recipient_id = m['delta']['messageMetadata']['threadKey']['otherUserFbId'] # Color change
thread_type = 'user' elif delta_type == "change_thread_theme":
mid = m['delta']['messageMetadata']['messageId'] new_color = delta["untypedData"]["theme_color"]
message = m['delta'].get('body','') thread_id, thread_type = getThreadIdAndThreadType(metadata)
fbid = m['delta']['messageMetadata']['actorFbId'] self.onColorChange(mid=mid, author_id=author_id, new_color=new_color, thread_id=thread_id,
self.on_message_new(mid, fbid, message, m, recipient_id, thread_type) thread_type=thread_type, ts=ts, metadata=metadata)
elif m['type'] in ['jewel_requests_add']: continue
from_id = m['from']
self.on_friend_request(from_id) # 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)
continue
# Thread title change
elif delta.get("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)
continue
# Nickname change
elif delta_type == "change_thread_nickname":
changed_for = str(delta["untypedData"]["participant_id"])
new_title = delta["untypedData"]["nickname"]
thread_id, thread_type = getThreadIdAndThreadType(metadata)
self.onNicknameChange(mid=mid, author_id=author_id, changed_for=changed_for,
new_title=new_title,
thread_id=thread_id, thread_type=thread_type, ts=ts, metadata=metadata)
continue
# TODO properly implement these as they differ on different scenarios
# Seen
# elif delta.get("class") == "ReadReceipt":
# seen_by = delta["actorFbId"] or delta["threadKey"]["otherUserFbId"]
# thread_id = delta["threadKey"].get("threadFbId")
# self.onSeen(seen_by=seen_by, thread_id=thread_id, ts=ts)
#
# # Message delivered
# elif delta.get("class") == 'DeliveryReceipt':
# time_delivered = delta['deliveredWatermarkTimestampMs']
# self.onDelivered()
# New message
elif delta.get("class") == "NewMessage":
message = delta.get('body', '')
thread_id, thread_type = getThreadIdAndThreadType(metadata)
self.onMessage(mid=mid, author_id=author_id, message=message,
thread_id=thread_id, thread_type=thread_type, ts=ts, metadata=m)
continue
# Inbox
if mtype == "inbox":
self.onInbox(unseen=m["unseen"], unread=m["unread"], recent_unread=m["recent_unread"])
# Typing
# elif mtype == "typ":
# author_id = str(m.get("from"))
# typing_status = TypingStatus(m.get("st"))
# self.onTyping(author_id=author_id, typing_status=typing_status)
# Seen
# elif mtype == "m_read_receipt":
#
# self.onSeen(m.get('realtime_viewer_fbid'), m.get('reader'), m.get('time'))
# elif mtype in ['jewel_requests_add']:
# from_id = m['from']
# self.on_friend_request(from_id)
# Happens on every login
elif mtype == "qprimer":
pass
# Is sent before any other message
elif mtype == "deltaflow":
pass
# Unknown message type
else: else:
self.on_unknown_type(m) self.onUnknownMesssageType(msg=m)
except Exception as e: except Exception as e:
# ex_type, ex, tb = sys.exc_info() log.debug(str(e))
self.on_message_error(sys.exc_info(), m)
@deprecated(deprecated_in='0.10.2', details='Use startListening() instead')
def start_listening(self): def start_listening(self):
return self.startListening()
def startListening(self):
"""Start listening from an external event loop.""" """Start listening from an external event loop."""
self.listening = True self.listening = True
self.sticky, self.pool = self._getSticky() self.sticky, self.pool = self._getSticky()
@deprecated(deprecated_in='0.10.2', details='Use doOneListen() instead')
def do_one_listen(self, markAlive=True): def do_one_listen(self, markAlive=True):
return self.doOneListen(markAlive)
def doOneListen(self, markAlive=True):
# type: (bool) -> None
"""Does one cycle of the listening loop. """Does one cycle of the listening loop.
This method is only useful if you want to control fbchat from an This method is only useful if you want to control fbchat from an
external event loop.""" external event loop."""
@@ -986,20 +1096,24 @@ class Client(object):
pass pass
@deprecated(deprecated_in='0.10.2', details='Use stopListening() instead')
def stop_listening(self): def stop_listening(self):
return self.stopListening()
def stopListening(self):
"""Cleans up the variables from start_listening.""" """Cleans up the variables from start_listening."""
self.listening = False self.listening = False
self.sticky, self.pool = (None, None) self.sticky, self.pool = (None, None)
def listen(self, markAlive=True): def listen(self, markAlive=True):
self.start_listening() self.startListening()
self.onListening()
log.info("Listening...")
while self.listening: while self.listening:
self.do_one_listen(markAlive) self.doOneListen(markAlive)
self.stop_listening() self.stopListening()
def getUserInfo(self, *user_ids): def getUserInfo(self, *user_ids):
@@ -1037,7 +1151,6 @@ class Client(object):
""" """
self.on_message(mid, author_id, None, message, metadata) self.on_message(mid, author_id, None, message, metadata)
@deprecated(deprecated_in='0.7.0', details='Use on_message_new() instead')
def on_message(self, mid, author_id, author_name, message, metadata): def on_message(self, mid, author_id, author_name, message, metadata):
"""subclass Client and override this method to add custom behavior on event""" """subclass Client and override this method to add custom behavior on event"""
self.markAsDelivered(author_id, mid) self.markAsDelivered(author_id, mid)

57
fbchat/event_hook.py Normal file
View File

@@ -0,0 +1,57 @@
import inspect
class EventHook(object):
"""
A simple implementation of the Observer-Pattern.
The user can specify an event signature upon inizializazion,
defined by kwargs in the form of argumentname=class (e.g. id=int).
The arguments' types are not checked in this implementation though.
Callables with a fitting signature can be added with += or removed with -=.
All listeners can be notified by calling the EventHook class with fitting
arguments.
Thanks http://stackoverflow.com/a/35957226/5556222
"""
def __init__(self, **signature):
self._signature = signature
self._argnames = set(signature.keys())
self._handlers = []
def _kwargs_str(self):
return ", ".join(k+"="+v.__name__ for k, v in self._signature.items())
def __iadd__(self, handler):
params = inspect.signature(handler).parameters
valid = True
argnames = set(n for n in params.keys())
if argnames != self._argnames:
valid = False
for p in params.values():
if p.kind == p.VAR_KEYWORD:
valid = True
break
if p.kind not in (p.POSITIONAL_OR_KEYWORD, p.KEYWORD_ONLY):
valid = False
break
if not valid:
raise ValueError("Listener must have these arguments: (%s)"
% self._kwargs_str())
self._handlers.append(handler)
return self
def __isub__(self, handler):
self._handlers.remove(handler)
return self
def __call__(self, *args, **kwargs):
if args or set(kwargs.keys()) != self._argnames:
raise ValueError("This EventHook must be called with these " +
"keyword arguments: (%s)" % self._kwargs_str() +
", but was called with: (%s)" %self._signature)
for handler in self._handlers[:]:
handler(**kwargs)
def __repr__(self):
return "EventHook(%s)" % self._kwargs_str()

View File

@@ -2,15 +2,7 @@ from __future__ import unicode_literals
import sys import sys
from enum import Enum from enum import Enum
class Base(): class User:
def __repr__(self):
uni = self.__unicode__()
return uni.encode('utf-8') if sys.version_info < (3, 0) else uni
def __unicode__(self):
return u'<%s %s (%s)>' % (self.type.upper(), self.name, self.url)
class User(Base):
def __init__(self, data): def __init__(self, data):
if data['type'] != 'user': if data['type'] != 'user':
raise Exception("[!] %s <%s> is not a user" % (data['text'], data['path'])) raise Exception("[!] %s <%s> is not a user" % (data['text'], data['path']))
@@ -22,11 +14,53 @@ class User(Base):
self.score = data['score'] self.score = data['score']
self.data = data self.data = data
class Thread(): def __repr__(self):
uni = self.__unicode__()
return uni.encode('utf-8') if sys.version_info < (3, 0) else uni
def __unicode__(self):
return u'<%s %s (%s)>' % (self.type.upper(), self.name, self.url)
@staticmethod
def adaptFromChat(user_in_chat):
""" Adapts user info from chat to User model acceptable initial dict
:param user_in_chat: user info from chat
'dir': None,
'mThumbSrcSmall': None,
'is_friend': False,
'is_nonfriend_messenger_contact': True,
'alternateName': '',
'i18nGender': 16777216,
'vanity': '',
'type': 'friend',
'searchTokens': ['Voznesenskij', 'Sergej'],
'thumbSrc': 'https://fb-s-b-a.akamaihd.net/h-ak-xfa1/v/t1.0-1/c9.0.32.32/p32x32/10354686_10150004552801856_220367501106153455_n.jpg?oh=71a87d76d4e4d17615a20c43fb8dbb47&oe=59118CE4&__gda__=1493753268_ae75cef40e9785398e744259ccffd7ff',
'mThumbSrcLarge': None,
'firstName': 'Sergej',
'name': 'Sergej Voznesenskij',
'uri': 'https://www.facebook.com/profile.php?id=100014812758264',
'id': '100014812758264',
'gender': 2
"""
return {
'type': 'user',
'uid': user_in_chat['id'],
'photo': user_in_chat['thumbSrc'],
'path': user_in_chat['uri'],
'text': user_in_chat['name'],
'score': '',
'data': user_in_chat,
}
class Thread:
def __init__(self, **entries): def __init__(self, **entries):
self.__dict__.update(entries) self.__dict__.update(entries)
class Message(): class Message:
def __init__(self, **entries): def __init__(self, **entries):
self.__dict__.update(entries) self.__dict__.update(entries)
@@ -40,22 +74,15 @@ class TypingStatus(Enum):
# WIP # WIP
class StickerSize(Enum): class EmojiSize(Enum):
LARGE = '369239383222810' LARGE = '369239383222810'
MEDIUM = '369239343222814' MEDIUM = '369239343222814'
SMALL = '369239263222822' SMALL = '369239263222822'
#class Size(Enum):
# LARGE = 'large'
# MEDIUM = 'medium'
# SMALL = 'small'
Size = StickerSize
LIKES = { LIKES = {
'l': Size.LARGE, 'l': EmojiSize.LARGE,
'm': Size.MEDIUM, 'm': EmojiSize.MEDIUM,
's': Size.SMALL 's': EmojiSize.SMALL
} }
LIKES['large'] = LIKES['l'] LIKES['large'] = LIKES['l']
LIKES['medium'] =LIKES['m'] LIKES['medium'] =LIKES['m']

View File

@@ -26,6 +26,7 @@ GENDERS = {
11: 'unknown_plural', 11: 'unknown_plural',
} }
def now(): def now():
return int(time()*1000) return int(time()*1000)
@@ -51,14 +52,14 @@ def str_base(number,base):
def generateMessageID(client_id=None): def generateMessageID(client_id=None):
k = now() k = now()
l = int(random() * 4294967295) l = int(random() * 4294967295)
return ("<%s:%s-%s@mail.projektitan.com>" % (k, l, client_id)); return "<%s:%s-%s@mail.projektitan.com>" % (k, l, client_id)
def getSignatureID(): def getSignatureID():
return hex(int(random() * 2147483648)) return hex(int(random() * 2147483648))
def generateOfflineThreadingID(): def generateOfflineThreadingID():
ret = now() ret = now()
value = int(random() * 4294967295); value = int(random() * 4294967295)
string = ("0000000000000000000000" + bin(value))[-22:] string = ("0000000000000000000000" + bin(value))[-22:]
msgs = bin(ret) + string msgs = bin(ret) + string
return str(int(msgs, 2)) return str(int(msgs, 2))