Merge pull request #134 from Dainius14/master

NOT BACKWARDS COMPATIBLE: Added event hooks, changed method names
This commit is contained in:
Mads T Marquart
2017-05-08 20:27:26 +02:00
committed by GitHub
5 changed files with 322 additions and 226 deletions

View File

@@ -21,9 +21,9 @@ 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 .stickers import *
import time import time
import sys from .event_hook import EventHook
# Python 3 does not have raw_input, whereas Python 2 has and it's more secure # Python 3 does not have raw_input, whereas Python 2 has and it's more secure
try: try:
@@ -89,6 +89,51 @@ class Client(object):
self.listening = False self.listening = False
self.threads = [] self.threads = []
# 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)
if not user_agent: if not user_agent:
user_agent = choice(USER_AGENTS) user_agent = choice(USER_AGENTS)
@@ -114,7 +159,7 @@ class Client(object):
log.addHandler(handler) log.addHandler(handler)
# 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 _console(self, msg): def _console(self, msg):
@@ -135,11 +180,6 @@ class Client(object):
DeprecationWarning) DeprecationWarning)
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
@@ -171,7 +211,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()
@@ -185,7 +225,11 @@ 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
@@ -230,7 +274,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
@@ -286,13 +330,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"""
@@ -300,7 +341,6 @@ class Client(object):
def setSession(self, session_cookies): def setSession(self, session_cookies):
"""Loads session cookies """Loads session cookies
:param session_cookies: dictionary containing session cookies :param session_cookies: dictionary containing session cookies
Return false if session_cookies does not contain proper cookies Return false if session_cookies does not contain proper cookies
""" """
@@ -308,16 +348,15 @@ class Client(object):
# Quick check to see if session_cookies is formatted properly # Quick check to see if session_cookies is formatted properly
if not session_cookies or 'c_user' not in session_cookies: if not session_cookies or 'c_user' not in session_cookies:
return False return False
# 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)
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.")
@@ -326,11 +365,11 @@ class Client(object):
for i in range(1, max_retries+1): for i in range(1, max_retries+1):
if not self._login(): if not self._login():
log.warning("Attempt #{} failed{}".format(i,{True:', retrying'}.get(i < max_retries, ''))) log.warning("Attempt #{} failed{}".format(i, {True: ', retrying'}.get(i < 5, '')))
time.sleep(1) time.sleep(1)
continue continue
else: else:
log.info("Login of {} successful.".format(email)) self.onLoggedIn(email)
break break
else: else:
raise Exception("Login failed. Check email/password.") raise Exception("Login failed. Check email/password.")
@@ -360,39 +399,6 @@ class Client(object):
self.def_is_user = is_user self.def_is_user = is_user
self.is_def_recipient_set = True self.is_def_recipient_set = True
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 getAllUsers(self): def getAllUsers(self):
""" Gets all users from chat with info included """ """ Gets all users from chat with info included """
@@ -410,7 +416,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
@@ -514,13 +520,10 @@ class Client(object):
if image_id: if image_id:
data['image_ids[0]'] = image_id data['image_ids[0]'] = image_id
if like: if like and not type(like) is Sticker:
try: data["sticker_id"] = Sticker.SMALL.value
sticker = LIKES[like.lower()] else:
except KeyError: data["sticker_id"] = like.value
# if user doesn't enter l or m or s, then use the large one
sticker = LIKES['l']
data["sticker_id"] = sticker
r = self._post(SendURL, data) r = self._post(SendURL, data)
@@ -549,7 +552,6 @@ class Client(object):
log.debug("With data {}".format(data)) log.debug("With data {}".format(data))
return message_ids return message_ids
def sendRemoteImage(self, recipient_id=None, message=None, is_user=True, image=''): def sendRemoteImage(self, recipient_id=None, message=None, is_user=True, image=''):
"""Send an image from a URL """Send an image from a URL
@@ -575,7 +577,6 @@ class Client(object):
image_id = self.uploadImage({'file': (image, open(image, 'rb'), mimetype)}) image_id = self.uploadImage({'file': (image, open(image, 'rb'), mimetype)})
return self.send(recipient_id, message, is_user, None, image_id) return self.send(recipient_id, message, is_user, None, image_id)
def uploadImage(self, image): def uploadImage(self, image):
"""Upload an image and get the image_id for sending in a message """Upload an image and get the image_id for sending in a message
@@ -588,7 +589,6 @@ class Client(object):
# Strip the start and parse out the returned image_id # Strip the start and parse out the returned image_id
return json.loads(r._content[9:])['payload']['metadata'][0]['image_id'] return json.loads(r._content[9:])['payload']['metadata'][0]['image_id']
def getThreadInfo(self, userID, last_n=20, start=None, is_user=True): def getThreadInfo(self, userID, last_n=20, start=None, is_user=True):
"""Get the info of one Thread """Get the info of one Thread
@@ -625,7 +625,6 @@ class Client(object):
messages.append(Message(**message)) messages.append(Message(**message))
return list(reversed(messages)) return list(reversed(messages))
def getThreadList(self, start, length=20): def getThreadList(self, start, length=20):
"""Get thread list of your facebook account. """Get thread list of your facebook account.
@@ -668,7 +667,6 @@ class Client(object):
return self.threads return self.threads
def getUnread(self): def getUnread(self):
form = { form = {
'client': 'mercury_sync', 'client': 'mercury_sync',
@@ -697,7 +695,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(),
@@ -708,13 +705,11 @@ 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
def friendConnect(self, friend_id):
def friend_connect(self, friend_id):
data = { data = {
"to_friend": friend_id, "to_friend": friend_id,
"action": "confirm" "action": "confirm"
@@ -724,7 +719,6 @@ class Client(object):
return r.ok return r.ok
def ping(self, sticky): def ping(self, sticky):
data = { data = {
'channel': self.user_channel, 'channel': self.user_channel,
@@ -738,11 +732,8 @@ 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.
"""
data = { data = {
"msgs_recv": 0, "msgs_recv": 0,
@@ -760,7 +751,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."""
@@ -778,7 +768,6 @@ 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 contains multiple messages in the content. May contains multiple messages in the content.
@@ -787,71 +776,141 @@ class Client(object):
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, ts=ts)
actor_id = m['delta']['messageMetadata']['actorFbId'] continue
thread_id = m['delta']['messageMetadata']['threadKey']['threadFbId']
self.on_people_added(user_ids, actor_id, thread_id) # Left/removed participants
elif 'messageMetadata' in m['delta']: elif 'leftParticipantFbId' in delta:
recipient_id = 0 removed_id = str(delta['leftParticipantFbId'])
thread_type = None thread_id = str(metadata['threadKey']['threadFbId'])
if 'threadKey' in m['delta']['messageMetadata']: self.onPersonRemoved(mid=mid, removed_id=removed_id, author_id=author_id, thread_id=thread_id, ts=ts)
if 'threadFbId' in m['delta']['messageMetadata']['threadKey']: continue
recipient_id = m['delta']['messageMetadata']['threadKey']['threadFbId']
thread_type = 'group' # Color change
elif 'otherUserFbId' in m['delta']['messageMetadata']['threadKey']: elif delta_type == "change_thread_theme":
recipient_id = m['delta']['messageMetadata']['threadKey']['otherUserFbId'] new_color = delta["untypedData"]["theme_color"]
thread_type = 'user' thread_id, thread_type = getThreadIdAndThreadType(metadata)
mid = m['delta']['messageMetadata']['messageId'] self.onColorChange(mid=mid, author_id=author_id, new_color=new_color, thread_id=thread_id,
message = m['delta'].get('body','') thread_type=thread_type, ts=ts, metadata=metadata)
fbid = m['delta']['messageMetadata']['actorFbId'] continue
self.on_message_new(mid, fbid, message, m, recipient_id, thread_type)
elif m['type'] in ['jewel_requests_add']: # Emoji change
from_id = m['from'] elif delta_type == "change_thread_icon":
self.on_friend_request(from_id) 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)
def startListening(self):
def start_listening(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()
def doOneListen(self, markAlive=True):
def do_one_listen(self, markAlive=True):
"""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."""
@@ -867,22 +926,19 @@ class Client(object):
except requests.exceptions.Timeout: except requests.exceptions.Timeout:
pass pass
def stopListening(self):
def stop_listening(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):
"""Get user info from id. Unordered. """Get user info from id. Unordered.
@@ -900,7 +956,6 @@ class Client(object):
user_ids = [fbidStrip(uid) for uid in user_ids] user_ids = [fbidStrip(uid) for uid in user_ids]
data = {"ids[{}]".format(i):uid for i,uid in enumerate(user_ids)} data = {"ids[{}]".format(i):uid for i,uid in enumerate(user_ids)}
r = self._post(UserInfoURL, data) r = self._post(UserInfoURL, data)
info = get_json(r.text) info = get_json(r.text)
@@ -909,8 +964,7 @@ class Client(object):
full_data=full_data[0] full_data=full_data[0]
return full_data return full_data
def removeUserFromChat(self, threadID, userID):
def remove_user_from_chat(self, threadID, userID):
"""Remove user (userID) from group chat (threadID) """Remove user (userID) from group chat (threadID)
:param threadID: group chat id :param threadID: group chat id
@@ -926,13 +980,12 @@ class Client(object):
return r.ok return r.ok
def add_users_to_chat(self, threadID, userID): def addUserToChat(self, threadID, userID):
"""Add user (userID) to group chat (threadID) """Add user (userID) to group chat (threadID)
:param threadID: group chat id :param threadID: group chat id
:param userID: user id to add to chat :param userID: user id to add to chat
""" """
return self.send(threadID, is_user=False, add_user_ids=[userID]) return self.send(threadID, is_user=False, add_user_ids=[userID])
def changeThreadTitle(self, threadID, newTitle): def changeThreadTitle(self, threadID, newTitle):
@@ -976,64 +1029,3 @@ class Client(object):
r = self._post(SendURL, data) r = self._post(SendURL, data)
return r.ok return r.ok
def on_message_new(self, mid, author_id, message, metadata, recipient_id, thread_type):
"""subclass Client and override this method to add custom behavior on event
This version of on_message recieves recipient_id and thread_type.
For backwards compatability, this data is sent directly to the old on_message.
"""
self.on_message(mid, author_id, None, 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"""
self.markAsDelivered(author_id, mid)
self.markAsRead(author_id)
log.info("%s said: %s" % (author_name, message))
def on_friend_request(self, from_id):
"""subclass Client and override this method to add custom behavior on event"""
log.info("Friend request from %s." % from_id)
def on_typing(self, author_id):
"""subclass Client and override this method to add custom behavior on event"""
pass
def on_read(self, author, reader, time):
"""subclass Client and override this method to add custom behavior on event"""
pass
def on_people_added(self, user_ids, actor_id, thread_id):
"""subclass Client and override this method to add custom behavior on event"""
log.info("User(s) {} was added to {} by {}".format(repr(user_ids), thread_id, actor_id))
def on_person_removed(self, user_id, actor_id, thread_id):
"""subclass Client and override this method to add custom behavior on event"""
log.info("User {} was removed from {} by {}".format(user_id, thread_id, actor_id))
def on_inbox(self, viewer, unseen, unread, other_unseen, other_unread, timestamp):
"""subclass Client and override this method to add custom behavior on event"""
pass
def on_message_error(self, exception, message):
"""subclass Client and override this method to add custom behavior on event"""
log.warning("Exception:\n{}".format(exception))
def on_qprimer(self, timestamp):
pass
def on_unknown_type(self, m):
"""subclass Client and override this method to add custom behavior on event"""
log.debug("Unknown type {}".format(m))

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

@@ -1,6 +1,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from enum import Enum
import sys import sys
class Base(): class Base():
def __repr__(self): def __repr__(self):
uni = self.__unicode__() uni = self.__unicode__()
@@ -9,6 +11,7 @@ class Base():
def __unicode__(self): def __unicode__(self):
return u'<%s %s (%s)>' % (self.type.upper(), self.name, self.url) return u'<%s %s (%s)>' % (self.type.upper(), self.name, self.url)
class User(Base): class User(Base):
def __init__(self, data): def __init__(self, data):
if data['type'] != 'user': if data['type'] != 'user':
@@ -22,10 +25,62 @@ class User(Base):
self.data = data self.data = data
@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(): 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)
class ThreadType(Enum):
USER = 1
GROUP = 2
class TypingStatus(Enum):
Deleted = 0
Typing = 1
class Sticker(Enum):
LARGE = '369239383222810'
MEDIUM = '369239343222814'
SMALL = '369239263222822'

View File

@@ -1,8 +0,0 @@
LIKES={
'l': '369239383222810',
'm': '369239343222814',
's': '369239263222822'
}
LIKES['large'] = LIKES['l']
LIKES['medium'] =LIKES['m']
LIKES['small'] = LIKES['s']

View File

@@ -2,6 +2,7 @@ import re
import json import json
from time import time from time import time
from random import random from random import random
USER_AGENTS = [ USER_AGENTS = [
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/601.1.10 (KHTML, like Gecko) Version/8.0.5 Safari/601.1.10", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/601.1.10 (KHTML, like Gecko) Version/8.0.5 Safari/601.1.10",
@@ -39,7 +40,7 @@ def digit_to_char(digit):
return str(digit) return str(digit)
return chr(ord('a') + digit - 10) return chr(ord('a') + digit - 10)
def str_base(number,base): def str_base(number, base):
if number < 0: if number < 0:
return '-' + str_base(-number, base) return '-' + str_base(-number, base)
(d, m) = divmod(number, base) (d, m) = divmod(number, base)
@@ -50,15 +51,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))