Merge pull request #145 from Dainius14/dev
Add more events and send methods, some fixes
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -25,5 +25,5 @@ develop-eggs
|
||||
docs/_build/
|
||||
|
||||
# Data for tests
|
||||
test_data.json
|
||||
my_test_data.json
|
||||
tests.data
|
306
fbchat/client.py
306
fbchat/client.py
@@ -11,6 +11,7 @@
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from urllib import parse
|
||||
import requests
|
||||
import logging
|
||||
from uuid import uuid1
|
||||
@@ -30,29 +31,7 @@ try:
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
# URLs
|
||||
LoginURL ="https://m.facebook.com/login.php?login_attempt=1"
|
||||
SearchURL ="https://www.facebook.com/ajax/typeahead/search.php"
|
||||
SendURL ="https://www.facebook.com/messaging/send/"
|
||||
ThreadsURL ="https://www.facebook.com/ajax/mercury/threadlist_info.php"
|
||||
ThreadSyncURL="https://www.facebook.com/ajax/mercury/thread_sync.php"
|
||||
MessagesURL ="https://www.facebook.com/ajax/mercury/thread_info.php"
|
||||
ReadStatusURL="https://www.facebook.com/ajax/mercury/change_read_status.php"
|
||||
DeliveredURL ="https://www.facebook.com/ajax/mercury/delivery_receipts.php"
|
||||
MarkSeenURL ="https://www.facebook.com/ajax/mercury/mark_seen.php"
|
||||
BaseURL ="https://www.facebook.com"
|
||||
MobileURL ="https://m.facebook.com/"
|
||||
StickyURL ="https://0-edge-chat.facebook.com/pull"
|
||||
PingURL ="https://0-channel-proxy-06-ash2.facebook.com/active_ping"
|
||||
UploadURL ="https://upload.facebook.com/ajax/mercury/upload.php"
|
||||
UserInfoURL ="https://www.facebook.com/chat/user_info/"
|
||||
ConnectURL ="https://www.facebook.com/ajax/add_friend/action.php?dpr=1"
|
||||
RemoveUserURL="https://www.facebook.com/chat/remove_participants/"
|
||||
LogoutURL ="https://www.facebook.com/logout.php"
|
||||
AllUsersURL ="https://www.facebook.com/chat/user_info_all"
|
||||
SaveDeviceURL="https://m.facebook.com/login/save-device/cancel/"
|
||||
CheckpointURL="https://m.facebook.com/login/checkpoint/"
|
||||
facebookEncoding = 'UTF-8'
|
||||
|
||||
|
||||
# Log settings
|
||||
log = logging.getLogger("client")
|
||||
@@ -102,8 +81,8 @@ class Client(object):
|
||||
|
||||
self._header = {
|
||||
'Content-Type' : 'application/x-www-form-urlencoded',
|
||||
'Referer' : BaseURL,
|
||||
'Origin' : BaseURL,
|
||||
'Referer' : ReqUrl.BASE,
|
||||
'Origin' : ReqUrl.BASE,
|
||||
'User-Agent' : user_agent,
|
||||
'Connection' : 'keep-alive',
|
||||
}
|
||||
@@ -133,8 +112,11 @@ class Client(object):
|
||||
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.onMessageSeen = EventHook(seen_by=str, thread_id=str, thread_type=ThreadType, seen_ts=int, delivered_ts=int, metadata=dict)
|
||||
self.onMessageDelivered = EventHook(msg_ids=list, delivered_for=str, thread_id=str, thread_type=ThreadType, ts=int, metadata=dict)
|
||||
self.onMarkedSeen = EventHook(threads=list, seen_ts=int, delivered_ts=int, metadata=dict)
|
||||
|
||||
self.onInbox = EventHook(unseen=int, unread=int, recent_unread=int)
|
||||
self.onPeopleAdded = EventHook(added_ids=list, author_id=str, thread_id=str)
|
||||
@@ -166,6 +148,14 @@ class Client(object):
|
||||
self.onPersonRemoved += lambda removed_id, author_id, thread_id:\
|
||||
log.info("%s removed: %s" % (author_id, removed_id))
|
||||
|
||||
|
||||
self.onMessageSeen += lambda seen_by, thread_id, thread_type, seen_ts, delivered_ts, metadata:\
|
||||
log.info("Messages seen by %s in %s (%s) at %ss", seen_by, thread_id, thread_type.name, seen_ts/1000)
|
||||
self.onMessageDelivered += lambda msg_ids, delivered_for, thread_id, thread_type, ts, metadata:\
|
||||
log.info("Messages %s delivered to %s in %s (%s) at %ss", msg_ids, delivered_for, thread_id, thread_type.name, ts/1000)
|
||||
self.onMarkedSeen += lambda threads, seen_ts, delivered_ts, metadata:\
|
||||
log.info("Marked messages as seen in threads %s at %ss", [(x[0], x[1].name) for x in threads], seen_ts/1000)
|
||||
|
||||
self.onUnknownMesssageType += lambda msg: log.info("Unknown message type received: %s" % msg)
|
||||
self.onMessageError += lambda exception, msg: log.exception(exception)
|
||||
|
||||
@@ -263,11 +253,11 @@ class Client(object):
|
||||
self.payloadDefault = {}
|
||||
self.client_id = hex(int(random()*2147483648))[2:]
|
||||
self.start_time = now()
|
||||
self.uid = int(self._session.cookies['c_user'])
|
||||
self.user_channel = "p_" + str(self.uid)
|
||||
self.uid = str(self._session.cookies['c_user'])
|
||||
self.user_channel = "p_" + self.uid
|
||||
self.ttstamp = ''
|
||||
|
||||
r = self._get(BaseURL)
|
||||
r = self._get(ReqUrl.BASE)
|
||||
soup = bs(r.text, "lxml")
|
||||
log.debug(r.text)
|
||||
log.debug(r.url)
|
||||
@@ -303,13 +293,13 @@ class Client(object):
|
||||
if not (self.email and self.password):
|
||||
raise Exception("Email and password not found.")
|
||||
|
||||
soup = bs(self._get(MobileURL).text, "lxml")
|
||||
soup = bs(self._get(ReqUrl.MOBILE).text, "lxml")
|
||||
data = dict((elem['name'], elem['value']) for elem in soup.findAll("input") if elem.has_attr('value') and elem.has_attr('name'))
|
||||
data['email'] = self.email
|
||||
data['pass'] = self.password
|
||||
data['login'] = 'Log In'
|
||||
|
||||
r = self._cleanPost(LoginURL, data)
|
||||
r = self._cleanPost(ReqUrl.LOGIN, data)
|
||||
|
||||
# Usually, 'Checkpoint' will refer to 2FA
|
||||
if 'checkpoint' in r.url and 'Enter Security Code to Continue' in r.text:
|
||||
@@ -317,7 +307,7 @@ class Client(object):
|
||||
|
||||
# Sometimes Facebook tries to show the user a "Save Device" dialog
|
||||
if 'save-device' in r.url:
|
||||
r = self._cleanGet(SaveDeviceURL)
|
||||
r = self._cleanGet(ReqUrl.SAVE_DEVICE)
|
||||
|
||||
if 'home' in r.url:
|
||||
self._postLogin()
|
||||
@@ -337,7 +327,7 @@ class Client(object):
|
||||
data['codes_submitted'] = 0
|
||||
log.info('Submitting 2FA code.')
|
||||
|
||||
r = self._cleanPost(CheckpointURL, data)
|
||||
r = self._cleanPost(ReqUrl.CHECKPOINT, data)
|
||||
|
||||
if 'home' in r.url:
|
||||
return r
|
||||
@@ -349,14 +339,14 @@ class Client(object):
|
||||
data['name_action_selected'] = 'save_device'
|
||||
data['submit[Continue]'] = 'Continue'
|
||||
log.info('Saving browser.') # At this stage, we have dtsg, nh, name_action_selected, submit[Continue]
|
||||
r = self._cleanPost(CheckpointURL, data)
|
||||
r = self._cleanPost(ReqUrl.CHECKPOINT, data)
|
||||
|
||||
if 'home' in r.url:
|
||||
return r
|
||||
|
||||
del(data['name_action_selected'])
|
||||
log.info('Starting Facebook checkup flow.') # At this stage, we have dtsg, nh, submit[Continue]
|
||||
r = self._cleanPost(CheckpointURL, data)
|
||||
r = self._cleanPost(ReqUrl.CHECKPOINT, data)
|
||||
|
||||
if 'home' in r.url:
|
||||
return r
|
||||
@@ -364,7 +354,7 @@ class Client(object):
|
||||
del(data['submit[Continue]'])
|
||||
data['submit[This was me]'] = 'This Was Me'
|
||||
log.info('Verifying login attempt.') # At this stage, we have dtsg, nh, submit[This was me]
|
||||
r = self._cleanPost(CheckpointURL, data)
|
||||
r = self._cleanPost(ReqUrl.CHECKPOINT, data)
|
||||
|
||||
if 'home' in r.url:
|
||||
return r
|
||||
@@ -373,12 +363,12 @@ class Client(object):
|
||||
data['submit[Continue]'] = 'Continue'
|
||||
data['name_action_selected'] = 'save_device'
|
||||
log.info('Saving device again.') # At this stage, we have dtsg, nh, submit[Continue], name_action_selected
|
||||
r = self._cleanPost(CheckpointURL, data)
|
||||
r = self._cleanPost(ReqUrl.CHECKPOINT, data)
|
||||
return r
|
||||
|
||||
def isLoggedIn(self):
|
||||
# Send a request to the login url, to see if we're directed to the home page.
|
||||
r = self._cleanGet(LoginURL)
|
||||
r = self._cleanGet(ReqUrl.LOGIN)
|
||||
return 'home' in r.url
|
||||
|
||||
def getSession(self):
|
||||
@@ -428,7 +418,7 @@ class Client(object):
|
||||
}
|
||||
|
||||
payload=self._generatePayload(data)
|
||||
r = self._session.get(LogoutURL, headers=self._header, params=payload, timeout=timeout)
|
||||
r = self._session.get(ReqUrl.LOGOUT, headers=self._header, params=payload, timeout=timeout)
|
||||
# reset value
|
||||
self.payloadDefault={}
|
||||
self._session = requests.session()
|
||||
@@ -456,13 +446,29 @@ class Client(object):
|
||||
self.default_thread_id = None
|
||||
self.default_thread_type = None
|
||||
|
||||
def _getThreadId(self, given_thread_id, given_thread_type):
|
||||
# type: (str, ThreadType) -> (str, ThreadType)
|
||||
"""
|
||||
Checks if thread ID is given, checks if default is set and returns correct values
|
||||
|
||||
:raises ValueError: if thread ID is not given and there is no default
|
||||
:return: tuple of thread ID and thread type
|
||||
"""
|
||||
if given_thread_id is None:
|
||||
if self.default_thread_id is not None:
|
||||
return self.default_thread_id, self.default_thread_type
|
||||
else:
|
||||
raise ValueError('Thread ID is not set.')
|
||||
else:
|
||||
return given_thread_id, given_thread_type
|
||||
|
||||
def getAllUsers(self):
|
||||
""" Gets all users from chat with info included """
|
||||
|
||||
data = {
|
||||
'viewer': self.uid,
|
||||
}
|
||||
r = self._post(AllUsersURL, query=data)
|
||||
r = self._post(ReqUrl.ALL_USERS, query=data)
|
||||
if not r.ok or len(r.text) == 0:
|
||||
return None
|
||||
j = get_json(r.text)
|
||||
@@ -496,7 +502,7 @@ class Client(object):
|
||||
'request_id' : str(uuid1()),
|
||||
}
|
||||
|
||||
r = self._get(SearchURL, payload)
|
||||
r = self._get(ReqUrl.SEARCH, payload)
|
||||
self.j = j = get_json(r.text)
|
||||
|
||||
users = []
|
||||
@@ -505,20 +511,13 @@ class Client(object):
|
||||
users.append(User(entry))
|
||||
return users # have bug TypeError: __repr__ returned non-string (type bytes)
|
||||
|
||||
|
||||
"""
|
||||
SEND METHODS
|
||||
"""
|
||||
|
||||
def _getSendData(self, thread_id=None, thread_type=ThreadType.USER):
|
||||
"""Returns the data needed to send a request to `SendURL`"""
|
||||
|
||||
if thread_id is None:
|
||||
if self.default_thread_id is not None:
|
||||
thread_id = self.default_thread_id
|
||||
thread_type = self.default_thread_type
|
||||
else:
|
||||
raise ValueError('Thread ID is not set.')
|
||||
|
||||
messageAndOTID = generateOfflineThreadingID()
|
||||
timestamp = now()
|
||||
date = datetime.now()
|
||||
@@ -543,7 +542,7 @@ class Client(object):
|
||||
'html_body' : False,
|
||||
'ui_push_phase' : 'V3',
|
||||
'status' : '0',
|
||||
'offline_threading_id':messageAndOTID,
|
||||
'offline_threading_id': messageAndOTID,
|
||||
'message_id' : messageAndOTID,
|
||||
'threading_id': generateMessageID(self.client_id),
|
||||
'ephemeral_ttl_mode:': '0',
|
||||
@@ -561,7 +560,7 @@ class Client(object):
|
||||
|
||||
def _doSendRequest(self, data):
|
||||
"""Sends the data to `SendURL`, and returns """
|
||||
r = self._post(SendURL, data)
|
||||
r = self._post(ReqUrl.SEND, data)
|
||||
|
||||
if not r.ok:
|
||||
log.warning('Error when sending message: Got {} response'.format(r.status_code))
|
||||
@@ -612,44 +611,47 @@ class Client(object):
|
||||
:param thread_type: specify whether thread_id is user or group chat
|
||||
:return: a list of message ids of the sent message(s)
|
||||
"""
|
||||
data = self._getSendData(thread_id=thread_id, thread_type=ThreadType.GROUP)
|
||||
thread_id, thread_type = self._getThreadId(thread_id, thread_type)
|
||||
data = self._getSendData(thread_id, thread_type)
|
||||
|
||||
data['action_type'] = 'ma-type:user-generated-message'
|
||||
data['body'] = message or ''
|
||||
data['has_attachment'] = False
|
||||
data['specific_to_list[0]'] = 'fbid:' + str(thread_id)
|
||||
data['specific_to_list[1]'] = 'fbid:' + str(self.uid)
|
||||
data['specific_to_list[0]'] = 'fbid:' + thread_id
|
||||
data['specific_to_list[1]'] = 'fbid:' + self.uid
|
||||
|
||||
return self._doSendRequest(data)
|
||||
|
||||
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.
|
||||
Sends an emoji. If emoji and size are not specified a small like is sent.
|
||||
|
||||
:param emoji: the chosen emoji to send
|
||||
:param emoji: the chosen emoji to send. If not specified, default thread emoji is sent
|
||||
:param size: size of emoji to send
|
||||
:param thread_id: user/group chat ID
|
||||
:param thread_type: specify whether thread_id is user or group chat
|
||||
:return: a list of message ids of the sent message(s)
|
||||
"""
|
||||
data = self._getSendData(thread_id=thread_id, thread_type=ThreadType.GROUP)
|
||||
|
||||
thread_id, thread_type = self._getThreadId(thread_id, thread_type)
|
||||
data = self._getSendData(thread_id, thread_type)
|
||||
data['action_type'] = 'ma-type:user-generated-message'
|
||||
data['has_attachment'] = False
|
||||
data['specific_to_list[0]'] = 'fbid:' + thread_id
|
||||
data['specific_to_list[1]'] = 'fbid:' + self.uid
|
||||
|
||||
if emoji:
|
||||
data['action_type'] = 'ma-type:user-generated-message'
|
||||
data['body'] = emoji or ''
|
||||
data['has_attachment'] = False
|
||||
data['specific_to_list[0]'] = 'fbid:' + str(thread_id)
|
||||
data['specific_to_list[1]'] = 'fbid:' + str(self.uid)
|
||||
data['tags[0]'] = 'hot_emoji_size:' + size['name']
|
||||
data['body'] = emoji
|
||||
data['tags[0]'] = 'hot_emoji_size:' + size.name.lower()
|
||||
else:
|
||||
data["sticker_id"] = size['value']
|
||||
data["sticker_id"] = size.value
|
||||
|
||||
return self._doSendRequest(data)
|
||||
|
||||
def sendImage(self, image_id, message=None, thread_id=None, thread_type=ThreadType.USER):
|
||||
"""Sends an already uploaded image with the id image_id to the thread"""
|
||||
data = self._getSendData(thread_id=thread_id, thread_type=ThreadType.GROUP)
|
||||
thread_id, thread_type = self._getThreadId(thread_id, thread_type)
|
||||
data = self._getSendData(thread_id, thread_type)
|
||||
|
||||
data['action_type'] = 'ma-type:user-generated-message'
|
||||
data['body'] = message or ''
|
||||
@@ -708,6 +710,8 @@ class Client(object):
|
||||
if image is not None:
|
||||
deprecation('sendRemoteImage(image)', deprecated_in='0.10.2', details='Use sendLocalImage(image_path) instead')
|
||||
image_path = image
|
||||
|
||||
thread_id, thread_type = self._getThreadId(thread_id, None)
|
||||
mimetype = guess_type(image_path)[0]
|
||||
image_id = self._uploadImage({'file': (image_path, open(image_path, 'rb'), mimetype)})
|
||||
return self.sendImage(image_id=image_id, message=message, thread_id=thread_id, thread_type=thread_type)
|
||||
@@ -721,8 +725,8 @@ class Client(object):
|
||||
:param thread_id: group chat ID
|
||||
:return: a list of message ids of the sent message(s)
|
||||
"""
|
||||
|
||||
data = self._getSendData(thread_id=thread_id, thread_type=ThreadType.GROUP)
|
||||
thread_id, thread_type = self._getThreadId(thread_id, None)
|
||||
data = self._getSendData(thread_id, ThreadType.GROUP)
|
||||
|
||||
data['action_type'] = 'ma-type:log-message'
|
||||
data['log_message_type'] = 'log:subscribe'
|
||||
@@ -736,22 +740,20 @@ class Client(object):
|
||||
# type: (str, str) -> bool
|
||||
"""
|
||||
Adds users to given (or default, if not) thread.
|
||||
|
||||
:param user_id: user ID to remove
|
||||
:param thread_id: group chat ID
|
||||
:return: true if user was removed
|
||||
"""
|
||||
|
||||
if thread_id is None and self.def_thread_type == ThreadType.GROUP:
|
||||
thread_id = self.def_thread_id
|
||||
elif thread_id is None:
|
||||
raise ValueError('Default Thread ID is not set.')
|
||||
thread_id = self._getThreadId(thread_id, None)
|
||||
|
||||
data = {
|
||||
"uid": user_id,
|
||||
"tid": thread_id
|
||||
}
|
||||
|
||||
r = self._post(RemoveUserURL, data)
|
||||
r = self._post(ReqUrl.REMOVE_USER, data)
|
||||
|
||||
return r.ok
|
||||
|
||||
@@ -772,12 +774,13 @@ class Client(object):
|
||||
def changeGroupTitle(self, title, thread_id=None):
|
||||
"""
|
||||
Change title of a group conversation.
|
||||
|
||||
:param title: new group chat title
|
||||
:param thread_id: group chat ID
|
||||
:return: a list of message ids of the sent message(s)
|
||||
"""
|
||||
|
||||
data = self._getSendData(thread_id=thread_id, thread_type=ThreadType.GROUP)
|
||||
thread_id, thread_type = self._getThreadId(thread_id, None)
|
||||
data = self._getSendData(thread_id, ThreadType.GROUP)
|
||||
|
||||
data['action_type'] = 'ma-type:log-message'
|
||||
data['log_message_data[name]'] = title
|
||||
@@ -785,6 +788,73 @@ class Client(object):
|
||||
|
||||
return self._doSendRequest(data)
|
||||
|
||||
def changeThreadColor(self, new_color, thread_id=None):
|
||||
# type: (ChatColor, str, ThreadType) -> bool
|
||||
"""
|
||||
Changes thread color to specified color. For more info about color names - see wiki.
|
||||
|
||||
:param new_color: new color name
|
||||
:param thread_id: user/group chat ID
|
||||
:return: True if color was changed
|
||||
"""
|
||||
thread_id = self._getThreadId(thread_id, None)
|
||||
|
||||
data = {
|
||||
"color_choice": new_color.value,
|
||||
"thread_or_other_fbid": thread_id
|
||||
}
|
||||
|
||||
r = self._post(ReqUrl.CHAT_COLOR, data)
|
||||
|
||||
return r.ok
|
||||
|
||||
def reactToMessage(self, message_id, reaction):
|
||||
# type: (str, MessageReaction) -> bool
|
||||
"""
|
||||
Reacts to a message.
|
||||
|
||||
:param message_id: message ID to react to
|
||||
:param reaction: reaction emoji to send
|
||||
:return: true if reacted
|
||||
"""
|
||||
full_data = {
|
||||
"doc_id": 1491398900900362,
|
||||
"dpr": 1,
|
||||
"variables": {
|
||||
"data": {
|
||||
"action": "ADD_REACTION",
|
||||
"client_mutation_id": "1",
|
||||
"actor_id": self.uid,
|
||||
"message_id": message_id,
|
||||
"reaction": reaction.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r = self._post(ReqUrl.MESSAGE_REACTION + "/?" + parse.urlencode(full_data))
|
||||
return r.ok
|
||||
|
||||
def setTypingStatus(self, status, thread_id=None, thread_type=None):
|
||||
# type: (TypingStatus, str, ThreadType) -> bool
|
||||
"""
|
||||
Sets users typing status.
|
||||
|
||||
:param status: typing or not typing
|
||||
:param thread_id: user/group chat ID
|
||||
:return: True if status changed
|
||||
"""
|
||||
thread_id, thread_type = self._getThreadId(thread_id, None)
|
||||
|
||||
data = {
|
||||
"typ": status.value,
|
||||
"thread": thread_id,
|
||||
"to": thread_id if thread_type == ThreadType.USER else "",
|
||||
"source": "mercury-chat"
|
||||
}
|
||||
|
||||
r = self._post(ReqUrl.TYPING, data)
|
||||
return r.ok
|
||||
|
||||
"""
|
||||
END SEND METHODS
|
||||
"""
|
||||
@@ -795,7 +865,7 @@ class Client(object):
|
||||
:param image: a tuple of (file name, data, mime type) to upload to facebook
|
||||
"""
|
||||
|
||||
r = self._postFile(UploadURL, image)
|
||||
r = self._postFile(ReqUrl.UPLOAD, image)
|
||||
response_content = {}
|
||||
if isinstance(r.content, str) is False:
|
||||
response_content = r.content.decode(facebookEncoding)
|
||||
@@ -812,11 +882,7 @@ class Client(object):
|
||||
:return: a list of messages
|
||||
"""
|
||||
|
||||
if thread_id is None and self.is_def_thread_set:
|
||||
thread_id = self.def_thread_id
|
||||
thread_type = self.def_thread_type
|
||||
elif thread_id is None and not self.is_def_thread_set:
|
||||
raise ValueError('Default Thread ID is not set.')
|
||||
thread_id, thread_type = self._getThreadId(thread_id, thread_type)
|
||||
|
||||
assert last_n > 0, 'length must be positive integer, got %d' % last_n
|
||||
|
||||
@@ -829,13 +895,13 @@ class Client(object):
|
||||
'messages[{}][{}][limit]'.format(key, thread_id): last_n - 1,
|
||||
'messages[{}][{}][timestamp]'.format(key, thread_id): now()}
|
||||
|
||||
r = self._post(MessagesURL, query=data)
|
||||
r = self._post(ReqUrl.MESSAGES, query=data)
|
||||
if not r.ok or len(r.text) == 0:
|
||||
return None
|
||||
return []
|
||||
|
||||
j = get_json(r.text)
|
||||
if not j['payload']:
|
||||
return None
|
||||
return []
|
||||
|
||||
messages = []
|
||||
for message in j['payload'].get('actions'):
|
||||
@@ -859,9 +925,9 @@ class Client(object):
|
||||
'inbox[limit]' : length,
|
||||
}
|
||||
|
||||
r = self._post(ThreadsURL, data)
|
||||
r = self._post(ReqUrl.THREADS, data)
|
||||
if not r.ok or len(r.text) == 0:
|
||||
return None
|
||||
return []
|
||||
|
||||
j = get_json(r.text)
|
||||
|
||||
@@ -894,7 +960,7 @@ class Client(object):
|
||||
# 'last_action_timestamp': 0
|
||||
}
|
||||
|
||||
r = self._post(ThreadSyncURL, form)
|
||||
r = self._post(ReqUrl.THREAD_SYNC, form)
|
||||
if not r.ok or len(r.text) == 0:
|
||||
return None
|
||||
|
||||
@@ -911,7 +977,7 @@ class Client(object):
|
||||
"thread_ids[%s][0]" % userID: threadID
|
||||
}
|
||||
|
||||
r = self._post(DeliveredURL, data)
|
||||
r = self._post(ReqUrl.DELIVERED, data)
|
||||
return r.ok
|
||||
|
||||
def markAsRead(self, userID):
|
||||
@@ -921,11 +987,11 @@ class Client(object):
|
||||
"ids[%s]" % userID: True
|
||||
}
|
||||
|
||||
r = self._post(ReadStatusURL, data)
|
||||
r = self._post(ReqUrl.READ_STATUS, data)
|
||||
return r.ok
|
||||
|
||||
def markAsSeen(self):
|
||||
r = self._post(MarkSeenURL, {"seen_timestamp": 0})
|
||||
r = self._post(ReqUrl.MARK_SEEN, {"seen_timestamp": 0})
|
||||
return r.ok
|
||||
|
||||
@deprecated(deprecated_in='0.10.2', details='Use friendConnect() instead')
|
||||
@@ -939,7 +1005,7 @@ class Client(object):
|
||||
"action": "confirm"
|
||||
}
|
||||
|
||||
r = self._post(ConnectURL, data)
|
||||
r = self._post(ReqUrl.CONNECT, data)
|
||||
return r.ok
|
||||
|
||||
def ping(self, sticky):
|
||||
@@ -952,7 +1018,7 @@ class Client(object):
|
||||
'sticky': sticky,
|
||||
'viewer_uid': self.uid
|
||||
}
|
||||
r = self._get(PingURL, data)
|
||||
r = self._get(ReqUrl.PING, data)
|
||||
return r.ok
|
||||
|
||||
def _getSticky(self):
|
||||
@@ -966,7 +1032,7 @@ class Client(object):
|
||||
"clientid": self.client_id
|
||||
}
|
||||
|
||||
r = self._get(StickyURL, data)
|
||||
r = self._get(ReqUrl.STICKY, data)
|
||||
j = get_json(r.text)
|
||||
|
||||
if 'lb_info' not in j:
|
||||
@@ -986,7 +1052,7 @@ class Client(object):
|
||||
"clientid": self.client_id,
|
||||
}
|
||||
|
||||
r = self._get(StickyURL, data)
|
||||
r = self._get(ReqUrl.STICKY, data)
|
||||
r.encoding = facebookEncoding
|
||||
j = get_json(r.text)
|
||||
|
||||
@@ -1024,7 +1090,7 @@ class Client(object):
|
||||
if metadata is not None:
|
||||
mid = metadata["messageId"]
|
||||
author_id = str(metadata['actorFbId'])
|
||||
ts = int(metadata["timestamp"])
|
||||
ts = int(metadata.get("timestamp"))
|
||||
|
||||
# Added participants
|
||||
if 'addedParticipants' in delta:
|
||||
@@ -1076,18 +1142,38 @@ class Client(object):
|
||||
thread_id=thread_id, thread_type=thread_type, ts=ts, metadata=metadata)
|
||||
continue
|
||||
|
||||
# Message delivered
|
||||
elif delta.get("class") == "DeliveryReceipt":
|
||||
message_ids = delta["messageIds"]
|
||||
delivered_for = str(delta["actorFbId"])
|
||||
ts = int(delta["deliveredWatermarkTimestampMs"])
|
||||
thread_id, thread_type = getThreadIdAndThreadType(delta)
|
||||
self.onMessageDelivered(msg_ids=message_ids, delivered_for=delivered_for,
|
||||
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()
|
||||
# Message seen
|
||||
elif delta.get("class") == "ReadReceipt":
|
||||
seen_by = str(delta.get("actorFbId") or delta["threadKey"]["otherUserFbId"])
|
||||
seen_ts = int(delta["actionTimestampMs"])
|
||||
delivered_ts = int(delta["watermarkTimestampMs"])
|
||||
thread_id, thread_type = getThreadIdAndThreadType(delta)
|
||||
self.onMessageSeen(seen_by=seen_by, thread_id=thread_id, thread_type=thread_type,
|
||||
seen_ts=seen_ts, delivered_ts=delivered_ts, metadata=metadata)
|
||||
continue
|
||||
|
||||
# Messages marked as seen
|
||||
elif delta.get("class") == "MarkRead":
|
||||
seen_ts = int(delta.get("actionTimestampMs") or delta.get("actionTimestamp"))
|
||||
delivered_ts = int(delta.get("watermarkTimestampMs") or delta.get("watermarkTimestamp"))
|
||||
|
||||
threads = []
|
||||
if "folders" not in delta:
|
||||
threads = [getThreadIdAndThreadType({"threadKey": thr}) for thr in delta.get("threadKeys")]
|
||||
|
||||
# thread_id, thread_type = getThreadIdAndThreadType(delta)
|
||||
self.onMarkedSeen(threads=threads, seen_ts=seen_ts, delivered_ts=delivered_ts, metadata=delta)
|
||||
continue
|
||||
|
||||
# New message
|
||||
elif delta.get("class") == "NewMessage":
|
||||
@@ -1107,6 +1193,8 @@ class Client(object):
|
||||
# typing_status = TypingStatus(m.get("st"))
|
||||
# self.onTyping(author_id=author_id, typing_status=typing_status)
|
||||
|
||||
# Delivered
|
||||
|
||||
# Seen
|
||||
# elif mtype == "m_read_receipt":
|
||||
#
|
||||
@@ -1129,7 +1217,7 @@ class Client(object):
|
||||
self.onUnknownMesssageType(msg=m)
|
||||
|
||||
except Exception as e:
|
||||
self.onMessageError(exception=e, msg=msg)
|
||||
self.onMessageError(exception=e, msg=m)
|
||||
|
||||
|
||||
@deprecated(deprecated_in='0.10.2', details='Use startListening() instead')
|
||||
@@ -1202,7 +1290,7 @@ class Client(object):
|
||||
|
||||
|
||||
data = {"ids[{}]".format(i):uid for i,uid in enumerate(user_ids)}
|
||||
r = self._post(UserInfoURL, data)
|
||||
r = self._post(ReqUrl.USER_INFO, data)
|
||||
info = get_json(r.text)
|
||||
full_data= [details for profile,details in info['payload']['profiles'].items()]
|
||||
if len(full_data)==1:
|
||||
|
@@ -69,22 +69,13 @@ class ThreadType(Enum):
|
||||
GROUP = 2
|
||||
|
||||
class TypingStatus(Enum):
|
||||
DELETED = 0
|
||||
STOPPED = 0
|
||||
TYPING = 1
|
||||
|
||||
class EmojiSize(Enum):
|
||||
LARGE = {
|
||||
'value': '369239383222810',
|
||||
'name': 'large'
|
||||
}
|
||||
MEDIUM = {
|
||||
'value': '369239343222814',
|
||||
'name': 'medium'
|
||||
}
|
||||
SMALL = {
|
||||
'value': '369239263222822',
|
||||
'name': 'small'
|
||||
}
|
||||
LARGE = '369239383222810'
|
||||
MEDIUM = '369239343222814'
|
||||
SMALL = '369239263222822'
|
||||
|
||||
LIKES = {
|
||||
'l': EmojiSize.LARGE,
|
||||
@@ -94,3 +85,29 @@ LIKES = {
|
||||
LIKES['large'] = LIKES['l']
|
||||
LIKES['medium'] =LIKES['m']
|
||||
LIKES['small'] = LIKES['s']
|
||||
|
||||
class ChatColor(Enum):
|
||||
MESSENGER_BLUE = ''
|
||||
VIKING = '#44bec7'
|
||||
GOLDEN_POPPY = '#ffc300'
|
||||
RADICAL_RED = '#fa3c4c'
|
||||
SHOCKING = '#d696bb'
|
||||
PICTON_BLUE = '#6699cc'
|
||||
FREE_SPEECH_GREEN = '#13cf13'
|
||||
PUMPKIN = '#ff7e29'
|
||||
LIGHT_CORAL = '#e68585'
|
||||
MEDIUM_SLATE_BLUE = '#7646ff'
|
||||
DEEP_SKY_BLUE = '#20cef5'
|
||||
FERN = '#67b868'
|
||||
CAMEO = '#d4a88c'
|
||||
BRILLIANT_ROSE = '#ff5ca1'
|
||||
BILOBA_FLOWER = '#a695c7'
|
||||
|
||||
class MessageReaction(Enum):
|
||||
LOVE = '😍'
|
||||
SMILE = '😆'
|
||||
WOW = '😮'
|
||||
SAD = '😢'
|
||||
ANGRY = '😠'
|
||||
YES = '👍'
|
||||
NO = '👎'
|
||||
|
@@ -3,6 +3,8 @@ import json
|
||||
from time import time
|
||||
from random import random
|
||||
import warnings
|
||||
from .models import *
|
||||
|
||||
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_3) AppleWebKit/601.1.10 (KHTML, like Gecko) Version/8.0.5 Safari/601.1.10",
|
||||
@@ -26,6 +28,33 @@ GENDERS = {
|
||||
11: 'unknown_plural',
|
||||
}
|
||||
|
||||
class ReqUrl:
|
||||
SEARCH = "https://www.facebook.com/ajax/typeahead/search.php"
|
||||
LOGIN = "https://m.facebook.com/login.php?login_attempt=1"
|
||||
SEND = "https://www.facebook.com/messaging/send/"
|
||||
THREAD_SYNC = "https://www.facebook.com/ajax/mercury/thread_sync.php"
|
||||
THREADS = "https://www.facebook.com/ajax/mercury/threadlist_info.php"
|
||||
MESSAGES = "https://www.facebook.com/ajax/mercury/thread_info.php"
|
||||
READ_STATUS = "https://www.facebook.com/ajax/mercury/change_read_status.php"
|
||||
DELIVERED = "https://www.facebook.com/ajax/mercury/delivery_receipts.php"
|
||||
MARK_SEEN = "https://www.facebook.com/ajax/mercury/mark_seen.php"
|
||||
BASE = "https://www.facebook.com"
|
||||
MOBILE = "https://m.facebook.com/"
|
||||
STICKY = "https://0-edge-chat.facebook.com/pull"
|
||||
PING = "https://0-channel-proxy-06-ash2.facebook.com/active_ping"
|
||||
UPLOAD = "https://upload.facebook.com/ajax/mercury/upload.php"
|
||||
USER_INFO = "https://www.facebook.com/chat/user_info/"
|
||||
CONNECT = "https://www.facebook.com/ajax/add_friend/action.php?dpr=1"
|
||||
REMOVE_USER = "https://www.facebook.com/chat/remove_participants/"
|
||||
LOGOUT = "https://www.facebook.com/logout.php"
|
||||
ALL_USERS = "https://www.facebook.com/chat/user_info_all"
|
||||
SAVE_DEVICE = "https://m.facebook.com/login/save-device/cancel/"
|
||||
CHECKPOINT = "https://m.facebook.com/login/checkpoint/"
|
||||
CHAT_COLOR = "https://www.facebook.com/messaging/save_thread_color/?source=thread_settings&dpr=1"
|
||||
MESSAGE_REACTION = "https://www.facebook.com/webgraphql/mutation"
|
||||
TYPING = "https://www.facebook.com/ajax/messaging/typ.php"
|
||||
|
||||
facebookEncoding = 'UTF-8'
|
||||
|
||||
def now():
|
||||
return int(time()*1000)
|
||||
|
67
tests.py
67
tests.py
@@ -18,14 +18,7 @@ logging.basicConfig(level=logging.INFO)
|
||||
Tests for fbchat
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
To use these tests, make a json file called test_data.json, put this example in it, and fill in the gaps:
|
||||
{
|
||||
"email": "example@email.com",
|
||||
"password": "example_password",
|
||||
"group_thread_id": 0,
|
||||
"user_thread_id": 0
|
||||
}
|
||||
or type this information manually in the terminal prompts.
|
||||
To use these tests copy test_data.json to my_test_data.json or type this information manually in the terminal prompts.
|
||||
|
||||
- email: Your (or a test user's) email / phone number
|
||||
- password: Your (or a test user's) password
|
||||
@@ -68,9 +61,13 @@ class TestFbchat(unittest.TestCase):
|
||||
self.assertTrue(client.isLoggedIn())
|
||||
|
||||
def test_setDefaultThreadId(self):
|
||||
client.setDefaultThreadId(client.uid, ThreadType.USER)
|
||||
client.setDefaultThread(client.uid, ThreadType.USER)
|
||||
self.assertTrue(client.sendMessage("test_default_recipient"))
|
||||
|
||||
def test_resetDefaultThreadId(self):
|
||||
client.resetDefaultThread()
|
||||
self.assertRaises(ValueError, client.sendMessage("should_not_send"))
|
||||
|
||||
def test_getAllUsers(self):
|
||||
users = client.getAllUsers()
|
||||
self.assertGreater(len(users), 0)
|
||||
@@ -83,19 +80,20 @@ class TestFbchat(unittest.TestCase):
|
||||
|
||||
# Test if values are set correctly
|
||||
self.assertIsInstance(u.uid, int)
|
||||
self.assertEquals(u.type, 'user')
|
||||
self.assertEquals(u.photo[:4], 'http')
|
||||
self.assertEquals(u.url[:4], 'http')
|
||||
self.assertEquals(u.name, 'Mark Zuckerberg')
|
||||
self.assertEqual(u.type, 'user')
|
||||
self.assertEqual(u.photo[:4], 'http')
|
||||
self.assertEqual(u.url[:4], 'http')
|
||||
self.assertEqual(u.name, 'Mark Zuckerberg')
|
||||
self.assertGreater(u.score, 0)
|
||||
|
||||
def test_sendEmoji(self):
|
||||
self.assertTrue(client.sendEmoji(EmojiSize.SMALL, user_uid, ThreadType.USER))
|
||||
self.assertTrue(client.sendEmoji(EmojiSize.MEDIUM, user_uid, ThreadType.USER))
|
||||
self.assertTrue(client.sendEmoji(EmojiSize.LARGE, user_uid, ThreadType.USER))
|
||||
self.assertTrue(client.sendEmoji(EmojiSize.SMALL, group_uid, ThreadType.GROUP))
|
||||
self.assertTrue(client.sendEmoji(EmojiSize.MEDIUM, group_uid, ThreadType.GROUP))
|
||||
self.assertTrue(client.sendEmoji(EmojiSize.LARGE, group_uid, ThreadType.GROUP))
|
||||
self.assertTrue(client.sendEmoji(size=EmojiSize.SMALL, thread_id=user_uid, thread_type=ThreadType.USER))
|
||||
self.assertTrue(client.sendEmoji(size=EmojiSize.MEDIUM, thread_id=user_uid, thread_type=ThreadType.USER))
|
||||
self.assertTrue(client.sendEmoji("😆", EmojiSize.LARGE, user_uid, ThreadType.USER))
|
||||
|
||||
self.assertTrue(client.sendEmoji(size=EmojiSize.SMALL, thread_id=group_uid, thread_type=ThreadType.GROUP))
|
||||
self.assertTrue(client.sendEmoji(size=EmojiSize.MEDIUM, thread_id=group_uid, thread_type=ThreadType.GROUP))
|
||||
self.assertTrue(client.sendEmoji("😆", EmojiSize.LARGE, group_uid, ThreadType.GROUP))
|
||||
|
||||
def test_sendMessage(self):
|
||||
self.assertTrue(client.sendMessage('test_send_user', user_uid, ThreadType.USER))
|
||||
@@ -109,13 +107,13 @@ class TestFbchat(unittest.TestCase):
|
||||
# Idk why but doesnt work, payload is null
|
||||
self.assertTrue(client.sendLocalImage(image_local_url, 'test_send_group_images_local', user_uid, ThreadType.USER))
|
||||
self.assertTrue(client.sendLocalImage(image_local_url, 'test_send_group_images_local', group_uid, ThreadType.GROUP))
|
||||
|
||||
|
||||
def test_getThreadInfo(self):
|
||||
client.sendMessage('test_user_getThreadInfo', user_uid, ThreadType.USER)
|
||||
time.sleep(3)
|
||||
info = client.getThreadInfo(20, user_uid, ThreadType.USER)
|
||||
self.assertEquals(info[0].author, 'fbid:' + client.uid)
|
||||
self.assertEquals(info[0].body, 'test_user_getThreadInfo')
|
||||
self.assertEqual(info[0].author, 'fbid:' + client.uid)
|
||||
self.assertEqual(info[0].body, 'test_user_getThreadInfo')
|
||||
|
||||
client.sendMessage('test_group_getThreadInfo', group_uid, ThreadType.GROUP)
|
||||
time.sleep(3)
|
||||
@@ -141,6 +139,29 @@ class TestFbchat(unittest.TestCase):
|
||||
def test_changeThreadTitle(self):
|
||||
self.assertTrue(client.changeThreadTitle('test_changeThreadTitle', group_uid))
|
||||
|
||||
def test_changeThreadColor(self):
|
||||
self.assertTrue(client.changeThreadColor(ChatColor.BRILLIANT_ROSE, group_uid))
|
||||
client.sendMessage(ChatColor.BRILLIANT_ROSE.name, group_uid, ThreadType.GROUP)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
self.assertTrue(client.changeThreadColor(ChatColor.MESSENGER_BLUE, group_uid))
|
||||
client.sendMessage(ChatColor.MESSENGER_BLUE.name, group_uid, ThreadType.GROUP)
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
self.assertTrue(client.changeThreadColor(ChatColor.BRILLIANT_ROSE, user_uid))
|
||||
client.sendMessage(ChatColor.BRILLIANT_ROSE.name, user_uid, ThreadType.USER)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
self.assertTrue(client.changeThreadColor(ChatColor.MESSENGER_BLUE, user_uid))
|
||||
client.sendMessage(ChatColor.MESSENGER_BLUE.name, user_uid, ThreadType.USER)
|
||||
|
||||
def test_reactToMessage(self):
|
||||
mid = client.sendMessage("react_to_message", user_uid, ThreadType.USER)[0]
|
||||
self.assertTrue(client.reactToMessage(mid, fbchat.MessageReaction.LOVE))
|
||||
|
||||
|
||||
def start_test(param_client, param_group_uid, param_user_uid, tests=[]):
|
||||
global client
|
||||
@@ -169,7 +190,7 @@ if __name__ == 'tests':
|
||||
pass
|
||||
|
||||
try:
|
||||
with open(path.join(path.dirname(__file__), 'test_data.json'), 'r') as f:
|
||||
with open(path.join(path.dirname(__file__), 'my_test_data.json'), 'r') as f:
|
||||
json = json.load(f)
|
||||
email = json['email']
|
||||
password = json['password']
|
||||
|
Reference in New Issue
Block a user