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]
.idea/
# Test scripts
*.sh

View File

@@ -20,6 +20,7 @@ from bs4 import BeautifulSoup as bs
from mimetypes import guess_type
from .utils import *
from .models import *
from .event_hook import *
import time
import sys
@@ -68,7 +69,8 @@ class Client(object):
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).
:param email: Facebook `email` or `id` or `phone number`
@@ -92,6 +94,8 @@ class Client(object):
self.default_thread_type = None
self.threads = []
self._setupEventHooks()
if not user_agent:
user_agent = choice(USER_AGENTS)
@@ -114,9 +118,55 @@ class Client(object):
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.is_logged_in():
if not session_cookies or not self.setSession(session_cookies) or not self.isLoggedIn():
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')
def _console(self, msg):
"""Assumes an INFO level and log it.
@@ -133,11 +183,6 @@ class Client(object):
"""
log.debug(msg)
def _setttstamp(self):
for i in self.fb_dtsg:
self.ttstamp += str(ord(i))
self.ttstamp += '2'
def _generatePayload(self, query):
"""Adds the following defaults to the payload:
__rev, __user, __a, ttstamp, fb_dtsg, __req
@@ -169,7 +214,7 @@ class Client(object):
payload=self._generatePayload(None)
return self._session.post(url, data=payload, timeout=timeout, files=files)
def _post_login(self):
def _postLogin(self):
self.payloadDefault = {}
self.client_id = hex(int(random()*2147483648))[2:]
self.start_time = now()
@@ -183,7 +228,9 @@ class Client(object):
log.debug(r.url)
self.fb_dtsg = soup.find("input", {'name':'fb_dtsg'})['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
self.payloadDefault['__rev'] = int(r.text.split('"revision":',1)[1].split(",",1)[0])
self.payloadDefault['__user'] = self.uid
@@ -228,7 +275,7 @@ class Client(object):
r = self._cleanGet(SaveDeviceURL)
if 'home' in r.url:
self._post_login()
self._postLogin()
return True
else:
return False
@@ -284,13 +331,10 @@ class Client(object):
r = self._cleanPost(CheckpointURL, data)
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.
r = self._cleanGet(LoginURL)
if 'home' in r.url:
return True
else:
return False
return 'home' in r.url
def getSession(self):
"""Returns the session cookies"""
@@ -309,12 +353,11 @@ class Client(object):
# Load cookies into current session
self._session.cookies = requests.cookies.merge_cookies(self._session.cookies, session_cookies)
self._post_login()
self._postLogin()
return True
def login(self, email, password, max_retries=5):
# Logging in
log.info("Logging in {}...".format(email))
self.onLoggingIn(email=email)
if not (email and password):
raise Exception("Email and password not set.")
@@ -328,7 +371,7 @@ class Client(object):
time.sleep(1)
continue
else:
log.info("Login of {} successful.".format(email))
self.onLoggedIn(email=email)
break
else:
raise Exception("Login failed. Check email/password.")
@@ -350,49 +393,23 @@ class Client(object):
@deprecated(deprecated_in='0.10.2', details='Use setDefaultThread instead')
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.
:param thread_id: user/group ID to default to
:param thread_type: type of thread_id
"""
self.default_thread_id = thread_id
self.default_thread_type = thread_type
def _adapt_user_in_chat_to_user_model(self, 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,
}
def resetDefaultThread(self):
# type: () -> None
"""Resets default thread."""
self.default_thread_id = None
self.default_thread_type = None
def getAllUsers(self):
""" Gets all users from chat with info included """
@@ -411,7 +428,7 @@ class Client(object):
for k in payload.keys():
try:
user = self._adapt_user_in_chat_to_user_model(payload[k])
user = User.adaptFromChat(payload[k])
except KeyError:
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):
# type: (str, str, ThreadType, EmojiSize, str, list, str) -> list
"""Send a message with given thread id
: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:
log.warning('Error when sending message: Got {} response'.format(r.status_code))
return False
return None
response_content = {}
if isinstance(r.content, str) is False:
@@ -547,15 +565,15 @@ class Client(object):
if 'error' in j:
# 'errorDescription' is in the users own language!
log.warning('Error #{} when sending message: {}'.format(j['error'], j['errorDescription']))
return False
return None
message_ids = []
try:
message_ids += [action['message_id'] for action in j['payload']['actions'] if 'message_id' in action]
message_ids[0] # Try accessing element
message_ids[0] # Try accessing element
except (KeyError, IndexError) as e:
log.warning('Error when sending message: No message ids could be found')
return False
return None
log.info('Message sent.')
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)
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.
:param message: message to send
:param thread_id: user/group chat ID
: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)
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.
:param emoji: WIP
:param size: size of emoji to send
:param thread_id: user/group chat ID
:param emoji: WIP
:param thread_type: specify whether thread_id is user or group chat
: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)
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.
:param image_url: URL of an image to upload and send
:param message: additional message
: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)
"""
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
if is_user is not None:
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)
# 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.
:param image_path: path of an image to upload and send
:param message: additional message
: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)
def addUsersToChat(self, user_ids, thread_id=None):
# type: (list, str) -> list
"""
Adds users to given (or default, if not) thread.
:param user_ids: list of user ids to add
:param thread_id: group chat ID
: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):
# type: (str, str) -> bool
"""
Adds users to given (or default, if not) thread.
: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']
def getThreadInfo(self, last_n=20, thread_id=None, thread_type=ThreadType.USER):
# type: (int, str, ThreadType) -> list
"""Get the info of one Thread
:param last_n: number of retrieved messages from start (default 20)
@@ -745,6 +776,7 @@ class Client(object):
def getThreadList(self, start, length=20):
# type: (int, int) -> list
"""Get thread list of your facebook account.
:param start: the start index of a thread
@@ -786,7 +818,6 @@ class Client(object):
return self.threads
def getUnread(self):
form = {
'client': 'mercury_sync',
@@ -815,7 +846,6 @@ class Client(object):
r = self._post(DeliveredURL, data)
return r.ok
def markAsRead(self, userID):
data = {
"watermarkTimestamp": now(),
@@ -826,23 +856,24 @@ class Client(object):
r = self._post(ReadStatusURL, data)
return r.ok
def markAsSeen(self):
r = self._post(MarkSeenURL, {"seen_timestamp": 0})
return r.ok
@deprecated(deprecated_in='0.10.2', details='Use friendConnect() instead')
def friend_connect(self, friend_id):
return self.friendConnect(friend_id)
def friendConnect(self, friend_id):
# type: (str) -> bool
data = {
"to_friend": friend_id,
"action": "confirm"
}
r = self._post(ConnectURL, data)
return r.ok
def ping(self, sticky):
data = {
'channel': self.user_channel,
@@ -856,7 +887,6 @@ class Client(object):
r = self._get(PingURL, data)
return r.ok
def _getSticky(self):
"""Call pull api to get sticky and pool parameter,
newer api needs these parameter to work.
@@ -878,7 +908,6 @@ class Client(object):
pool = j['lb_info']['pool']
return sticky, pool
def _pullMessage(self, sticky, pool):
"""Call pull api with seq value to get message data."""
@@ -896,80 +925,161 @@ class Client(object):
self.seq = j.get('seq', '0')
return j
def _parseMessage(self, content):
"""Get message and author name from content.
May contains multiple messages in the content.
"""
"""Get message and author name from content. May contain multiple messages in the content."""
if 'ms' not in content: return
log.debug("Received {}".format(content["ms"]))
for m in content['ms']:
for m in content["ms"]:
mtype = m.get("type")
try:
if m['type'] in ['m_messaging', 'messaging']:
if m['event'] in ['deliver']:
mid = m['message']['mid']
message = m['message']['body']
fbid = m['message']['sender_fbid']
name = m['message']['sender_name']
self.on_message(mid, fbid, name, message, m)
elif m['type'] in ['typ']:
self.on_typing(m.get("from"))
elif m['type'] in ['m_read_receipt']:
self.on_read(m.get('realtime_viewer_fbid'), m.get('reader'), m.get('time'))
elif m['type'] in ['inbox']:
viewer = m.get('realtime_viewer_fbid')
unseen = m.get('unseen')
unread = m.get('unread')
other_unseen = m.get('other_unseen')
other_unread = m.get('other_unread')
timestamp = m.get('seen_timestamp')
self.on_inbox(viewer, unseen, unread, other_unseen, other_unread, timestamp)
elif m['type'] in ['qprimer']:
self.on_qprimer(m.get('made'))
elif m['type'] in ['delta']:
if 'leftParticipantFbId' in m['delta']:
user_id = m['delta']['leftParticipantFbId']
actor_id = m['delta']['messageMetadata']['actorFbId']
thread_id = m['delta']['messageMetadata']['threadKey']['threadFbId']
self.on_person_removed(user_id, actor_id, thread_id)
elif 'addedParticipants' in m['delta']:
user_ids = [x['userFbId'] for x in m['delta']['addedParticipants']]
actor_id = m['delta']['messageMetadata']['actorFbId']
thread_id = m['delta']['messageMetadata']['threadKey']['threadFbId']
self.on_people_added(user_ids, actor_id, thread_id)
elif 'messageMetadata' in m['delta']:
recipient_id = 0
thread_type = None
if 'threadKey' in m['delta']['messageMetadata']:
if 'threadFbId' in m['delta']['messageMetadata']['threadKey']:
recipient_id = m['delta']['messageMetadata']['threadKey']['threadFbId']
thread_type = 'group'
elif 'otherUserFbId' in m['delta']['messageMetadata']['threadKey']:
recipient_id = m['delta']['messageMetadata']['threadKey']['otherUserFbId']
thread_type = 'user'
mid = m['delta']['messageMetadata']['messageId']
message = m['delta'].get('body','')
fbid = m['delta']['messageMetadata']['actorFbId']
self.on_message_new(mid, fbid, message, m, recipient_id, thread_type)
elif m['type'] in ['jewel_requests_add']:
from_id = m['from']
self.on_friend_request(from_id)
# Things that directly change chat
if mtype == "delta":
def getThreadIdAndThreadType(msg_metadata):
"""Returns a tuple consisting of thread id and thread type"""
id_thread = None
type_thread = None
if 'threadFbId' in msg_metadata['threadKey']:
id_thread = str(msg_metadata['threadKey']['threadFbId'])
type_thread = ThreadType.GROUP
elif 'otherUserFbId' in msg_metadata['threadKey']:
id_thread = str(msg_metadata['threadKey']['otherUserFbId'])
type_thread = ThreadType.USER
return id_thread, type_thread
delta = m["delta"]
delta_type = delta.get("type")
metadata = delta.get("messageMetadata")
if metadata is not None:
mid = metadata["messageId"]
author_id = str(metadata['actorFbId'])
ts = int(metadata["timestamp"])
# Added participants
if 'addedParticipants' in delta:
added_ids = [str(x['userFbId']) for x in delta['addedParticipants']]
thread_id = str(metadata['threadKey']['threadFbId'])
self.onPeopleAdded(mid=mid, added_ids=added_ids, author_id=author_id, thread_id=thread_id,
ts=ts)
continue
# 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)
continue
# Color change
elif delta_type == "change_thread_theme":
new_color = 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)
continue
# 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:
self.on_unknown_type(m)
self.onUnknownMesssageType(msg=m)
except Exception as e:
# ex_type, ex, tb = sys.exc_info()
self.on_message_error(sys.exc_info(), m)
log.debug(str(e))
@deprecated(deprecated_in='0.10.2', details='Use startListening() instead')
def start_listening(self):
return self.startListening()
def startListening(self):
"""Start listening from an external event loop."""
self.listening = True
self.sticky, self.pool = self._getSticky()
@deprecated(deprecated_in='0.10.2', details='Use doOneListen() instead')
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.
This method is only useful if you want to control fbchat from an
external event loop."""
@@ -986,20 +1096,24 @@ class Client(object):
pass
@deprecated(deprecated_in='0.10.2', details='Use stopListening() instead')
def stop_listening(self):
return self.stopListening()
def stopListening(self):
"""Cleans up the variables from start_listening."""
self.listening = False
self.sticky, self.pool = (None, None)
def listen(self, markAlive=True):
self.start_listening()
self.startListening()
self.onListening()
log.info("Listening...")
while self.listening:
self.do_one_listen(markAlive)
self.doOneListen(markAlive)
self.stop_listening()
self.stopListening()
def getUserInfo(self, *user_ids):
@@ -1037,7 +1151,6 @@ class Client(object):
"""
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):
"""subclass Client and override this method to add custom behavior on event"""
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
from enum import Enum
class Base():
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):
class User:
def __init__(self, data):
if data['type'] != 'user':
raise Exception("[!] %s <%s> is not a user" % (data['text'], data['path']))
@@ -22,11 +14,53 @@ class User(Base):
self.score = data['score']
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):
self.__dict__.update(entries)
class Message():
class Message:
def __init__(self, **entries):
self.__dict__.update(entries)
@@ -40,22 +74,15 @@ class TypingStatus(Enum):
# WIP
class StickerSize(Enum):
class EmojiSize(Enum):
LARGE = '369239383222810'
MEDIUM = '369239343222814'
SMALL = '369239263222822'
#class Size(Enum):
# LARGE = 'large'
# MEDIUM = 'medium'
# SMALL = 'small'
Size = StickerSize
LIKES = {
'l': Size.LARGE,
'm': Size.MEDIUM,
's': Size.SMALL
'l': EmojiSize.LARGE,
'm': EmojiSize.MEDIUM,
's': EmojiSize.SMALL
}
LIKES['large'] = LIKES['l']
LIKES['medium'] =LIKES['m']

View File

@@ -26,6 +26,7 @@ GENDERS = {
11: 'unknown_plural',
}
def now():
return int(time()*1000)
@@ -40,7 +41,7 @@ def digit_to_char(digit):
return str(digit)
return chr(ord('a') + digit - 10)
def str_base(number,base):
def str_base(number, base):
if number < 0:
return '-' + str_base(-number, base)
(d, m) = divmod(number, base)
@@ -51,17 +52,17 @@ def str_base(number,base):
def generateMessageID(client_id=None):
k = now()
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():
return hex(int(random() * 2147483648))
def generateOfflineThreadingID() :
def generateOfflineThreadingID():
ret = now()
value = int(random() * 4294967295);
value = int(random() * 4294967295)
string = ("0000000000000000000000" + bin(value))[-22:]
msgs = bin(ret) + string
return str(int(msgs,2))
return str(int(msgs, 2))
def isUserToThreadType(is_user):
return ThreadType.USER if is_user else ThreadType.GROUP