reintroduce things skipped on conflict
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,7 @@
|
||||
*py[co]
|
||||
|
||||
.idea/
|
||||
|
||||
# Test scripts
|
||||
*.sh
|
||||
|
||||
|
383
fbchat/client.py
383
fbchat/client.py
@@ -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
57
fbchat/event_hook.py
Normal 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()
|
@@ -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']
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user