Revert "add event hooks, change method names"
This commit is contained in:
416
fbchat/client.py
416
fbchat/client.py
@@ -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
|
||||||
from .event_hook import EventHook
|
import sys
|
||||||
|
|
||||||
|
|
||||||
# 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,51 +89,6 @@ 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)
|
||||||
|
|
||||||
@@ -159,7 +114,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.isLoggedIn():
|
if not session_cookies or not self.setSession(session_cookies) or not self.is_logged_in():
|
||||||
self.login(email, password, max_retries)
|
self.login(email, password, max_retries)
|
||||||
|
|
||||||
def _console(self, msg):
|
def _console(self, msg):
|
||||||
@@ -180,6 +135,11 @@ 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
|
||||||
@@ -211,7 +171,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 _postLogin(self):
|
def _post_login(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()
|
||||||
@@ -225,11 +185,7 @@ 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
|
||||||
@@ -274,7 +230,7 @@ class Client(object):
|
|||||||
r = self._cleanGet(SaveDeviceURL)
|
r = self._cleanGet(SaveDeviceURL)
|
||||||
|
|
||||||
if 'home' in r.url:
|
if 'home' in r.url:
|
||||||
self._postLogin()
|
self._post_login()
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
@@ -330,10 +286,13 @@ class Client(object):
|
|||||||
r = self._cleanPost(CheckpointURL, data)
|
r = self._cleanPost(CheckpointURL, data)
|
||||||
return r
|
return r
|
||||||
|
|
||||||
def isLoggedIn(self):
|
def is_logged_in(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)
|
||||||
return 'home' in r.url
|
if 'home' in r.url:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
def getSession(self):
|
def getSession(self):
|
||||||
"""Returns the session cookies"""
|
"""Returns the session cookies"""
|
||||||
@@ -341,6 +300,7 @@ 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
|
||||||
"""
|
"""
|
||||||
@@ -348,15 +308,16 @@ 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._postLogin()
|
self._post_login()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def login(self, email, password, max_retries=5):
|
def login(self, email, password, max_retries=5):
|
||||||
self.onLoggingIn(email)
|
# Logging in
|
||||||
|
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.")
|
||||||
|
|
||||||
@@ -365,11 +326,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 < 5, '')))
|
log.warning("Attempt #{} failed{}".format(i,{True:', retrying'}.get(i < max_retries, '')))
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
self.onLoggedIn(email)
|
log.info("Login of {} successful.".format(email))
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
raise Exception("Login failed. Check email/password.")
|
raise Exception("Login failed. Check email/password.")
|
||||||
@@ -399,6 +360,39 @@ 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 """
|
||||||
|
|
||||||
@@ -416,7 +410,7 @@ class Client(object):
|
|||||||
|
|
||||||
for k in payload.keys():
|
for k in payload.keys():
|
||||||
try:
|
try:
|
||||||
user = User.adaptFromChat(payload[k])
|
user = self._adapt_user_in_chat_to_user_model(payload[k])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -520,10 +514,13 @@ class Client(object):
|
|||||||
if image_id:
|
if image_id:
|
||||||
data['image_ids[0]'] = image_id
|
data['image_ids[0]'] = image_id
|
||||||
|
|
||||||
if like and not type(like) is Sticker:
|
if like:
|
||||||
data["sticker_id"] = Sticker.SMALL.value
|
try:
|
||||||
else:
|
sticker = LIKES[like.lower()]
|
||||||
data["sticker_id"] = like.value
|
except KeyError:
|
||||||
|
# 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)
|
||||||
|
|
||||||
@@ -552,6 +549,7 @@ 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
|
||||||
|
|
||||||
@@ -577,6 +575,7 @@ 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
|
||||||
|
|
||||||
@@ -589,6 +588,7 @@ 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,6 +625,7 @@ 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.
|
||||||
|
|
||||||
@@ -667,6 +668,7 @@ class Client(object):
|
|||||||
|
|
||||||
return self.threads
|
return self.threads
|
||||||
|
|
||||||
|
|
||||||
def getUnread(self):
|
def getUnread(self):
|
||||||
form = {
|
form = {
|
||||||
'client': 'mercury_sync',
|
'client': 'mercury_sync',
|
||||||
@@ -695,6 +697,7 @@ 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(),
|
||||||
@@ -705,11 +708,13 @@ 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"
|
||||||
@@ -719,6 +724,7 @@ 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,
|
||||||
@@ -732,8 +738,11 @@ 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, newer api needs these parameter to work."""
|
"""Call pull api to get sticky and pool parameter,
|
||||||
|
newer api needs these parameter to work.
|
||||||
|
"""
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"msgs_recv": 0,
|
"msgs_recv": 0,
|
||||||
@@ -751,6 +760,7 @@ 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."""
|
||||||
|
|
||||||
@@ -768,6 +778,7 @@ 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.
|
||||||
@@ -776,141 +787,71 @@ 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:
|
||||||
# Things that directly change chat
|
if m['type'] in ['m_messaging', 'messaging']:
|
||||||
if mtype == "delta":
|
if m['event'] in ['deliver']:
|
||||||
|
mid = m['message']['mid']
|
||||||
def getThreadIdAndThreadType(msg_metadata):
|
message = m['message']['body']
|
||||||
"""Returns a tuple consisting of thread id and thread type"""
|
fbid = m['message']['sender_fbid']
|
||||||
id_thread = None
|
name = m['message']['sender_name']
|
||||||
type_thread = None
|
self.on_message(mid, fbid, name, message, m)
|
||||||
if 'threadFbId' in msg_metadata['threadKey']:
|
elif m['type'] in ['typ']:
|
||||||
id_thread = str(msg_metadata['threadKey']['threadFbId'])
|
self.on_typing(m.get("from"))
|
||||||
type_thread = ThreadType.GROUP
|
elif m['type'] in ['m_read_receipt']:
|
||||||
elif 'otherUserFbId' in msg_metadata['threadKey']:
|
self.on_read(m.get('realtime_viewer_fbid'), m.get('reader'), m.get('time'))
|
||||||
id_thread = str(msg_metadata['threadKey']['otherUserFbId'])
|
elif m['type'] in ['inbox']:
|
||||||
type_thread = ThreadType.USER
|
viewer = m.get('realtime_viewer_fbid')
|
||||||
return id_thread, type_thread
|
unseen = m.get('unseen')
|
||||||
|
unread = m.get('unread')
|
||||||
delta = m["delta"]
|
other_unseen = m.get('other_unseen')
|
||||||
delta_type = delta.get("type")
|
other_unread = m.get('other_unread')
|
||||||
metadata = delta.get("messageMetadata")
|
timestamp = m.get('seen_timestamp')
|
||||||
|
self.on_inbox(viewer, unseen, unread, other_unseen, other_unread, timestamp)
|
||||||
if metadata is not None:
|
elif m['type'] in ['qprimer']:
|
||||||
mid = metadata["messageId"]
|
self.on_qprimer(m.get('made'))
|
||||||
author_id = str(metadata['actorFbId'])
|
elif m['type'] in ['delta']:
|
||||||
ts = int(metadata["timestamp"])
|
if 'leftParticipantFbId' in m['delta']:
|
||||||
|
user_id = m['delta']['leftParticipantFbId']
|
||||||
# Added participants
|
actor_id = m['delta']['messageMetadata']['actorFbId']
|
||||||
if 'addedParticipants' in delta:
|
thread_id = m['delta']['messageMetadata']['threadKey']['threadFbId']
|
||||||
added_ids = [str(x['userFbId']) for x in delta['addedParticipants']]
|
self.on_person_removed(user_id, actor_id, thread_id)
|
||||||
thread_id = str(metadata['threadKey']['threadFbId'])
|
elif 'addedParticipants' in m['delta']:
|
||||||
self.onPeopleAdded(mid=mid, added_ids=added_ids, author_id=author_id, thread_id=thread_id, ts=ts)
|
user_ids = [x['userFbId'] for x in m['delta']['addedParticipants']]
|
||||||
continue
|
actor_id = m['delta']['messageMetadata']['actorFbId']
|
||||||
|
thread_id = m['delta']['messageMetadata']['threadKey']['threadFbId']
|
||||||
# Left/removed participants
|
self.on_people_added(user_ids, actor_id, thread_id)
|
||||||
elif 'leftParticipantFbId' in delta:
|
elif 'messageMetadata' in m['delta']:
|
||||||
removed_id = str(delta['leftParticipantFbId'])
|
recipient_id = 0
|
||||||
thread_id = str(metadata['threadKey']['threadFbId'])
|
thread_type = None
|
||||||
self.onPersonRemoved(mid=mid, removed_id=removed_id, author_id=author_id, thread_id=thread_id, ts=ts)
|
if 'threadKey' in m['delta']['messageMetadata']:
|
||||||
continue
|
if 'threadFbId' in m['delta']['messageMetadata']['threadKey']:
|
||||||
|
recipient_id = m['delta']['messageMetadata']['threadKey']['threadFbId']
|
||||||
# Color change
|
thread_type = 'group'
|
||||||
elif delta_type == "change_thread_theme":
|
elif 'otherUserFbId' in m['delta']['messageMetadata']['threadKey']:
|
||||||
new_color = delta["untypedData"]["theme_color"]
|
recipient_id = m['delta']['messageMetadata']['threadKey']['otherUserFbId']
|
||||||
thread_id, thread_type = getThreadIdAndThreadType(metadata)
|
thread_type = 'user'
|
||||||
self.onColorChange(mid=mid, author_id=author_id, new_color=new_color, thread_id=thread_id,
|
mid = m['delta']['messageMetadata']['messageId']
|
||||||
thread_type=thread_type, ts=ts, metadata=metadata)
|
message = m['delta'].get('body','')
|
||||||
continue
|
fbid = m['delta']['messageMetadata']['actorFbId']
|
||||||
|
self.on_message_new(mid, fbid, message, m, recipient_id, thread_type)
|
||||||
# Emoji change
|
elif m['type'] in ['jewel_requests_add']:
|
||||||
elif delta_type == "change_thread_icon":
|
from_id = m['from']
|
||||||
new_emoji = delta["untypedData"]["thread_icon"]
|
self.on_friend_request(from_id)
|
||||||
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.onUnknownMesssageType(msg=m)
|
self.on_unknown_type(m)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.debug(str(e))
|
# ex_type, ex, tb = sys.exc_info()
|
||||||
|
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."""
|
||||||
@@ -926,19 +867,22 @@ 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.startListening()
|
self.start_listening()
|
||||||
self.onListening()
|
|
||||||
|
|
||||||
|
log.info("Listening...")
|
||||||
while self.listening:
|
while self.listening:
|
||||||
self.doOneListen(markAlive)
|
self.do_one_listen(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.
|
||||||
@@ -956,6 +900,7 @@ 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)
|
||||||
@@ -964,7 +909,8 @@ 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
|
||||||
@@ -980,12 +926,13 @@ class Client(object):
|
|||||||
|
|
||||||
return r.ok
|
return r.ok
|
||||||
|
|
||||||
def addUserToChat(self, threadID, userID):
|
def add_users_to_chat(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):
|
||||||
@@ -1029,3 +976,64 @@ 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))
|
||||||
|
|
||||||
|
@@ -1,57 +0,0 @@
|
|||||||
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()
|
|
@@ -1,8 +1,6 @@
|
|||||||
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__()
|
||||||
@@ -11,7 +9,6 @@ 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':
|
||||||
@@ -25,62 +22,10 @@ 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'
|
|
||||||
|
8
fbchat/stickers.py
Normal file
8
fbchat/stickers.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
LIKES={
|
||||||
|
'l': '369239383222810',
|
||||||
|
'm': '369239343222814',
|
||||||
|
's': '369239263222822'
|
||||||
|
}
|
||||||
|
LIKES['large'] = LIKES['l']
|
||||||
|
LIKES['medium'] =LIKES['m']
|
||||||
|
LIKES['small'] = LIKES['s']
|
@@ -2,7 +2,6 @@ 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",
|
||||||
@@ -40,7 +39,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)
|
||||||
@@ -51,14 +50,15 @@ 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))
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user