add event hooks, change method names
This commit is contained in:
361
fbchat/client.py
361
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
|
||||||
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:
|
||||||
@@ -75,7 +75,6 @@ class Client(object):
|
|||||||
import fbchat
|
import fbchat
|
||||||
chat = fbchat.Client(email, password)
|
chat = fbchat.Client(email, password)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if do_login and not (email and password):
|
if do_login and not (email and password):
|
||||||
raise Exception("Email and password not found.")
|
raise Exception("Email and password not found.")
|
||||||
|
|
||||||
@@ -103,6 +102,37 @@ class Client(object):
|
|||||||
11: 'unknown_plural',
|
11: 'unknown_plural',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Setup event hooks
|
||||||
|
self.onLoggingIn = EventHook()
|
||||||
|
self.onLoggedIn = EventHook()
|
||||||
|
|
||||||
|
self.onMessage = EventHook(mid=str, author_id=str, message=str, recipient_id=int, msg_type=MsgType, ts=str, metadata=dict)
|
||||||
|
self.onColorChange = EventHook(mid=str, author_id=str, new_color=str, changed_for=str, msg_type=MsgType, ts=str)
|
||||||
|
self.onTitleChange = EventHook(mid=str, author_id=str, new_title=str, changed_for=str, thread_id=str, ts=str)
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Setup event handlers
|
||||||
|
self.onLoggingIn += lambda: log.info("Logging in...")
|
||||||
|
self.onLoggedIn += lambda: log.info("Login successful.")
|
||||||
|
self.onListening += lambda: log.info("Listening...")
|
||||||
|
|
||||||
|
self.onMessage += lambda mid, author_id, message, recipient_id, msg_type, ts, metadata:\
|
||||||
|
log.info("Message from %s: %s" % (author_id, message))
|
||||||
|
self.onColorChange += lambda mid, author_id, new_color, changed_for, msg_type, ts:\
|
||||||
|
log.info("%s changed color to %s" % (author_id, new_color))
|
||||||
|
self.onTitleChange += lambda mid, author_id, new_title, changed_for, thread_id, ts:\
|
||||||
|
log.info("%s changed title of %s to %s" % (author_id, 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))
|
||||||
|
|
||||||
if not user_agent:
|
if not user_agent:
|
||||||
user_agent = choice(USER_AGENTS)
|
user_agent = choice(USER_AGENTS)
|
||||||
|
|
||||||
@@ -151,11 +181,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
|
||||||
@@ -187,7 +212,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()
|
||||||
@@ -201,7 +226,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
|
||||||
@@ -246,7 +275,7 @@ class Client(object):
|
|||||||
r = self._cleanGet(SaveDeviceURL)
|
r = self._cleanGet(SaveDeviceURL)
|
||||||
|
|
||||||
if 'home' in r.url:
|
if 'home' in r.url:
|
||||||
self._post_login()
|
self._postLogin()
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
@@ -328,14 +357,13 @@ class Client(object):
|
|||||||
return False
|
return False
|
||||||
# Load cookies into current session
|
# Load cookies into current session
|
||||||
self._session.cookies = requests.cookies.merge_cookies(self._session.cookies, j)
|
self._session.cookies = requests.cookies.merge_cookies(self._session.cookies, j)
|
||||||
self._post_login()
|
self._postLogin()
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception('Invalid json in {}, or bad merging of cookies'.format(sessionfile))
|
raise Exception('Invalid json in {}, or bad merging of cookies'.format(sessionfile))
|
||||||
|
|
||||||
def login(self, email, password, max_retries=5):
|
def login(self, email, password, max_retries=5):
|
||||||
# Logging in
|
self.onLoggingIn()
|
||||||
log.info("Logging in...")
|
|
||||||
|
|
||||||
self.email = email
|
self.email = email
|
||||||
self.password = password
|
self.password = password
|
||||||
@@ -346,7 +374,7 @@ class Client(object):
|
|||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
log.info("Login successful.")
|
self.onLoggedIn()
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
raise Exception("Login failed. Check email/password.")
|
raise Exception("Login failed. Check email/password.")
|
||||||
@@ -376,39 +404,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 """
|
||||||
|
|
||||||
@@ -426,7 +421,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
|
||||||
|
|
||||||
@@ -434,7 +429,6 @@ class Client(object):
|
|||||||
|
|
||||||
return users
|
return users
|
||||||
|
|
||||||
|
|
||||||
def getUsers(self, name):
|
def getUsers(self, name):
|
||||||
"""Find and get user by his/her name
|
"""Find and get user by his/her name
|
||||||
|
|
||||||
@@ -529,13 +523,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)
|
||||||
|
|
||||||
@@ -556,7 +547,6 @@ class Client(object):
|
|||||||
log.debug("With data {}".format(data))
|
log.debug("With data {}".format(data))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -582,7 +572,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
|
||||||
|
|
||||||
@@ -595,7 +584,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
|
||||||
|
|
||||||
@@ -632,7 +620,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.
|
||||||
|
|
||||||
@@ -675,7 +662,6 @@ class Client(object):
|
|||||||
|
|
||||||
return self.threads
|
return self.threads
|
||||||
|
|
||||||
|
|
||||||
def getUnread(self):
|
def getUnread(self):
|
||||||
form = {
|
form = {
|
||||||
'client': 'mercury_sync',
|
'client': 'mercury_sync',
|
||||||
@@ -704,7 +690,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(),
|
||||||
@@ -715,13 +700,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"
|
||||||
@@ -731,7 +714,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,
|
||||||
@@ -745,11 +727,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,
|
||||||
@@ -767,7 +746,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."""
|
||||||
|
|
||||||
@@ -785,7 +763,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.
|
||||||
@@ -794,71 +771,120 @@ 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 getRecipientIdAndMsgType(msg_metadata):
|
||||||
fbid = m['message']['sender_fbid']
|
"""Returns a tuple consisting of recipient id and message type"""
|
||||||
name = m['message']['sender_name']
|
recipient = None
|
||||||
self.on_message(mid, fbid, name, message, m)
|
message_type = None
|
||||||
elif m['type'] in ['typ']:
|
if 'threadFbId' in msg_metadata['threadKey']:
|
||||||
self.on_typing(m.get("from"))
|
recipient = msg_metadata['threadKey']['threadFbId']
|
||||||
elif m['type'] in ['m_read_receipt']:
|
message_type = MsgType.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']:
|
recipient = msg_metadata['threadKey']['otherUserFbId']
|
||||||
viewer = m.get('realtime_viewer_fbid')
|
message_type = MsgType.USER
|
||||||
unseen = m.get('unseen')
|
return recipient, message_type
|
||||||
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']:
|
# Added participants
|
||||||
self.on_qprimer(m.get('made'))
|
if 'addedParticipants' in delta:
|
||||||
elif m['type'] in ['delta']:
|
added_ids = [x['userFbId'] for x in delta['addedParticipants']]
|
||||||
if 'leftParticipantFbId' in m['delta']:
|
author_id = metadata['actorFbId']
|
||||||
user_id = m['delta']['leftParticipantFbId']
|
thread_id = metadata['threadKey']['threadFbId']
|
||||||
actor_id = m['delta']['messageMetadata']['actorFbId']
|
ts = metadata["timestamp"]
|
||||||
thread_id = m['delta']['messageMetadata']['threadKey']['threadFbId']
|
self.onPeopleAdded(added_ids=added_ids, author_id=author_id, thread_id=thread_id, ts=ts)
|
||||||
self.on_person_removed(user_id, actor_id, thread_id)
|
|
||||||
elif 'addedParticipants' in m['delta']:
|
# Left/removed participants
|
||||||
user_ids = [x['userFbId'] for x in m['delta']['addedParticipants']]
|
elif 'leftParticipantFbId' in delta:
|
||||||
actor_id = m['delta']['messageMetadata']['actorFbId']
|
removed_id = delta['leftParticipantFbId']
|
||||||
thread_id = m['delta']['messageMetadata']['threadKey']['threadFbId']
|
author_id = metadata['actorFbId']
|
||||||
self.on_people_added(user_ids, actor_id, thread_id)
|
thread_id = metadata['threadKey']['threadFbId']
|
||||||
elif 'messageMetadata' in m['delta']:
|
ts = metadata["timestamp"]
|
||||||
recipient_id = 0
|
self.onPersonRemoved(removed_id=removed_id, author_id=author_id, thread_id=thread_id, ts=ts)
|
||||||
thread_type = None
|
|
||||||
if 'threadKey' in m['delta']['messageMetadata']:
|
# Seen
|
||||||
if 'threadFbId' in m['delta']['messageMetadata']['threadKey']:
|
elif delta.get("class") == "ReadReceipt":
|
||||||
recipient_id = m['delta']['messageMetadata']['threadKey']['threadFbId']
|
seen_by = delta["actorFbId"] or delta["threadKey"]["otherUserFbId"]
|
||||||
thread_type = 'group'
|
thread_id = delta["threadKey"].get("threadFbId")
|
||||||
elif 'otherUserFbId' in m['delta']['messageMetadata']['threadKey']:
|
timestamp = delta["actionTimestampMs"]
|
||||||
recipient_id = m['delta']['messageMetadata']['threadKey']['otherUserFbId']
|
self.onSeen(seen_by=seen_by, thread_id=thread_id, timestamp=timestamp)
|
||||||
thread_type = 'user'
|
|
||||||
mid = m['delta']['messageMetadata']['messageId']
|
# Color change
|
||||||
message = m['delta'].get('body','')
|
elif delta_type == "change_thread_theme":
|
||||||
fbid = m['delta']['messageMetadata']['actorFbId']
|
mid = metadata["messageId"]
|
||||||
self.on_message_new(mid, fbid, message, m, recipient_id, thread_type)
|
author_id = metadata["actorFbId"]
|
||||||
elif m['type'] in ['jewel_requests_add']:
|
new_color = delta["untypedData"]["theme_color"]
|
||||||
from_id = m['from']
|
changed_for, msg_type = getRecipientIdAndMsgType(metadata)
|
||||||
self.on_friend_request(from_id)
|
ts = metadata["timestamp"]
|
||||||
|
self.onColorChange(mid=mid, author_id=author_id, new_color=new_color, changed_for=changed_for,
|
||||||
|
msg_type=msg_type, ts=ts)
|
||||||
|
|
||||||
|
# Title change
|
||||||
|
elif delta_type == "change_thread_nickname":
|
||||||
|
mid = metadata["messageId"]
|
||||||
|
author_id = metadata["actorFbId"]
|
||||||
|
changed_for = delta["untypedData"]["participant_id"]
|
||||||
|
new_title = delta["untypedData"]["nickanme"]
|
||||||
|
thread_id = metadata["threadKey"].get("threadFbId")
|
||||||
|
ts = metadata["timestamp"]
|
||||||
|
self.onTitleChange(mid=mid, author_id=author_id, changed_for=changed_for, new_title=new_title,
|
||||||
|
thread_id=thread_id, ts=ts)
|
||||||
|
|
||||||
|
# New message
|
||||||
|
elif "messageMetadata" in delta:
|
||||||
|
mid = metadata['messageId']
|
||||||
|
message = delta.get('body', '')
|
||||||
|
author_id = metadata['actorFbId']
|
||||||
|
recipient_id, msg_type = getRecipientIdAndMsgType(metadata)
|
||||||
|
ts = metadata["timestamp"]
|
||||||
|
self.onMessage(mid=mid, author_id=author_id, message=message,
|
||||||
|
recipient_id=recipient_id, msg_type=msg_type, ts=ts, metadata=m)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
else:
|
else:
|
||||||
log.debug("Unknown type {}".format(m))
|
log.debug("Unknown type %s" % 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."""
|
||||||
@@ -874,22 +900,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.
|
||||||
@@ -907,7 +930,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)
|
||||||
@@ -916,8 +938,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
|
||||||
@@ -933,13 +954,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):
|
||||||
@@ -983,58 +1003,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
|
|
||||||
|
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()
|
@@ -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 MsgType(Enum):
|
||||||
|
USER = 1
|
||||||
|
GROUP = 2
|
||||||
|
|
||||||
|
|
||||||
|
class TypingStatus(Enum):
|
||||||
|
Deleted = 0
|
||||||
|
Typing = 1
|
||||||
|
|
||||||
|
|
||||||
|
class Sticker(Enum):
|
||||||
|
LARGE = '369239383222810'
|
||||||
|
MEDIUM = '369239343222814'
|
||||||
|
SMALL = '369239263222822'
|
||||||
|
@@ -1,8 +0,0 @@
|
|||||||
LIKES={
|
|
||||||
'l': '369239383222810',
|
|
||||||
'm': '369239343222814',
|
|
||||||
's': '369239263222822'
|
|
||||||
}
|
|
||||||
LIKES['large'] = LIKES['l']
|
|
||||||
LIKES['medium'] =LIKES['m']
|
|
||||||
LIKES['small'] = LIKES['s']
|
|
@@ -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",
|
||||||
@@ -22,7 +23,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)
|
||||||
@@ -33,15 +34,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))
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user