Merge branch 'master' into master

This commit is contained in:
Taehoon Kim
2017-01-30 21:48:55 +09:00
committed by GitHub

View File

@@ -13,7 +13,9 @@
import requests import requests
import json import json
import logging
from uuid import uuid1 from uuid import uuid1
import warnings
from random import random, choice from random import random, choice
from datetime import datetime from datetime import datetime
from bs4 import BeautifulSoup as bs from bs4 import BeautifulSoup as bs
@@ -39,6 +41,12 @@ StickyURL ="https://0-edge-chat.facebook.com/pull"
PingURL ="https://0-channel-proxy-06-ash2.facebook.com/active_ping" PingURL ="https://0-channel-proxy-06-ash2.facebook.com/active_ping"
UploadURL ="https://upload.facebook.com/ajax/mercury/upload.php" UploadURL ="https://upload.facebook.com/ajax/mercury/upload.php"
UserInfoURL ="https://www.facebook.com/chat/user_info/" 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"
# Log settings
log = logging.getLogger("client")
class Client(object): class Client(object):
"""A client for the Facebook Chat (Messenger). """A client for the Facebook Chat (Messenger).
@@ -83,15 +91,28 @@ class Client(object):
'Connection' : 'keep-alive', 'Connection' : 'keep-alive',
} }
self._console("Logging in...") # Configure the logger differently based on the 'debug' parameter
if debug:
logging_level = logging.DEBUG
else:
logging_level = logging.WARNING
# Creates the console handler
handler = logging.StreamHandler()
handler.setLevel(logging_level)
log.addHandler(handler)
log.setLevel(logging.DEBUG)
# Logging in
log.info("Logging in...")
for i in range(1,max_retries+1): for i in range(1,max_retries+1):
if not self.login(): if not self.login():
self._console("Attempt #{} failed{}".format(i,{True:', retrying'}.get(i<5,''))) log.warning("Attempt #{} failed{}".format(i,{True:', retrying'}.get(i<5,'')))
time.sleep(1) time.sleep(1)
continue continue
else: else:
self._console("login successful") log.info("Login successful")
break break
else: else:
raise Exception("login failed. Check id/password") raise Exception("login failed. Check id/password")
@@ -100,7 +121,23 @@ class Client(object):
self.threads = [] self.threads = []
def _console(self, msg): def _console(self, msg):
if self.debug: print(msg) """Assumes an INFO level and log it.
This method shouldn't be used anymore.
Use the log itself:
>>> import logging
>>> from fbchat.client import Client, log
>>> log.setLevel(logging.DEBUG)
You can do the same thing by addint the 'debug' argument:
>>> from fbchat import Client
>>> client = Client("...", "...", debug=True)
"""
warnings.warn(
"Client._console shouldn't be used. Use 'log.<level>'",
DeprecationWarning)
if self.debug: log.info(msg)
def _setttstamp(self): def _setttstamp(self):
for i in self.fb_dtsg: for i in self.fb_dtsg:
@@ -131,12 +168,12 @@ class Client(object):
def _cleanPost(self, url, query=None, timeout=30): def _cleanPost(self, url, query=None, timeout=30):
self.req_counter += 1 self.req_counter += 1
return self._session.post(url, headers=self._header, data=query, timeout=timeout) return self._session.post(url, headers=self._header, data=query, timeout=timeout)
def _postFile(self, url, files=None, timeout=30): def _postFile(self, url, files=None, timeout=30):
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 login(self): def login(self):
if not (self.email and self.password): if not (self.email and self.password):
raise Exception("id and password or config is needed") raise Exception("id and password or config is needed")
@@ -159,6 +196,7 @@ class Client(object):
r = self._get(BaseURL) r = self._get(BaseURL)
soup = bs(r.text, "lxml") soup = bs(r.text, "lxml")
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._setttstamp() self._setttstamp()
# 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])
@@ -187,6 +225,19 @@ class Client(object):
else: else:
return False return False
def logout(self, timeout=30):
data = {}
data['ref'] = "mb"
data['h'] = self.fb_h
payload=self._generatePayload(data)
r = self._session.get(LogoutURL, headers=self._header, params=payload, timeout=timeout)
# reset value
self.payloadDefault={}
self._session = requests.session()
self.req_counter = 1
self.seq = "0"
return r
def listen(self): def listen(self):
pass pass
@@ -264,8 +315,10 @@ class Client(object):
'manual_retry_cnt' : '0', 'manual_retry_cnt' : '0',
'signatureID' : getSignatureID(), 'signatureID' : getSignatureID(),
'has_attachment' : image_id != None, 'has_attachment' : image_id != None,
'other_user_fbid' : user_id,
'thread_fbid': thread_id,
'specific_to_list[0]' : 'fbid:' + str(recipient_id), 'specific_to_list[0]' : 'fbid:' + str(recipient_id),
'specific_to_list[1]' : 'fbid:' + str(self.uid), 'specific_to_list[1]' : 'fbid:' + str(self.uid),
} }
if message_type.lower() == 'group': if message_type.lower() == 'group':
@@ -286,14 +339,13 @@ class Client(object):
r = self._post(SendURL, data) r = self._post(SendURL, data)
if self.debug: log.debug("Sending {}".format(r))
print(r) log.debug("With data {}".format(data))
print(data)
return r.ok return r.ok
def sendRemoteImage(self, recipient_id, message=None, message_type='user', image=''): def sendRemoteImage(self, recipient_id, message=None, message_type='user', image=''):
"""Send an image from a URL """Send an image from a URL
:param recipient_id: the user id or thread id that you want to send a message to :param recipient_id: the user id or thread id that you want to send a message to
:param message: a text that you want to send :param message: a text that you want to send
:param message_type: determines if the recipient_id is for user or thread :param message_type: determines if the recipient_id is for user or thread
@@ -303,10 +355,10 @@ class Client(object):
remote_image = requests.get(image).content remote_image = requests.get(image).content
image_id = self.uploadImage({'file': (image, remote_image, mimetype)}) image_id = self.uploadImage({'file': (image, remote_image, mimetype)})
return self.send(recipient_id, message, message_type, None, image_id) return self.send(recipient_id, message, message_type, None, image_id)
def sendLocalImage(self, recipient_id, message=None, message_type='user', image=''): def sendLocalImage(self, recipient_id, message=None, message_type='user', image=''):
"""Send an image from a file path """Send an image from a file path
:param recipient_id: the user id or thread id that you want to send a message to :param recipient_id: the user id or thread id that you want to send a message to
:param message: a text that you want to send :param message: a text that you want to send
:param message_type: determines if the recipient_id is for user or thread :param message_type: determines if the recipient_id is for user or thread
@@ -315,31 +367,39 @@ class Client(object):
mimetype = guess_type(image)[0] mimetype = guess_type(image)[0]
image_id = self.uploadImage({'file': (image, open(image), mimetype)}) image_id = self.uploadImage({'file': (image, open(image), mimetype)})
return self.send(recipient_id, message, message_type, None, image_id) return self.send(recipient_id, message, message_type, 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
:param image: a tuple of (file name, data, mime type) to upload to facebook :param image: a tuple of (file name, data, mime type) to upload to facebook
""" """
r = self._postFile(UploadURL, image) r = self._postFile(UploadURL, image)
if isinstance(r._content, str) is False:
r._content = r._content.decode("utf-8")
# 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, start, end=None): def getThreadInfo(self, userID, start, end=None, thread_type='user'):
"""Get the info of one Thread """Get the info of one Thread
:param userID: ID of the user you want the messages from :param userID: ID of the user you want the messages from
:param start: the start index of a thread :param start: the start index of a thread
:param end: (optional) the last index of a thread :param end: (optional) the last index of a thread
:param thread_type: (optional) change from 'user' for group threads
""" """
if not end: end = start + 20 if not end: end = start + 20
if end <= start: end = start + end if end <= start: end = start + end
data = {} data = {}
data['messages[user_ids][%s][offset]'%userID] = start if thread_type == 'user':
data['messages[user_ids][%s][limit]'%userID] = end key = 'user_ids'
data['messages[user_ids][%s][timestamp]'%userID] = now() else:
key = 'thread_fbids'
data['messages[{}][{}][offset]'.format(key, userID)] = start
data['messages[{}][{}][limit]'.format(key, userID)] = end
data['messages[{}][{}][timestamp]'.format(key, userID)] = now()
r = self._post(MessagesURL, query=data) r = self._post(MessagesURL, query=data)
if not r.ok or len(r.text) == 0: if not r.ok or len(r.text) == 0:
@@ -385,7 +445,7 @@ class Client(object):
for participant in j['payload']['participants']: for participant in j['payload']['participants']:
participants[participant["fbid"]] = participant["name"] participants[participant["fbid"]] = participant["name"]
except Exception as e: except Exception as e:
print(j) log.warning(str(j))
# Prevent duplicates in self.threads # Prevent duplicates in self.threads
threadIDs = [getattr(x, "thread_id") for x in self.threads] threadIDs = [getattr(x, "thread_id") for x in self.threads]
@@ -440,6 +500,19 @@ class Client(object):
r = self._post(MarkSeenURL, {"seen_timestamp": 0}) r = self._post(MarkSeenURL, {"seen_timestamp": 0})
return r.ok return r.ok
def friend_connect(self, friend_id):
data = {
"to_friend": friend_id,
"action": "confirm"
}
r = self._post(ConnectURL, data)
if self.debug:
print(r)
print(data)
return r.ok
def ping(self, sticky): def ping(self, sticky):
data = { data = {
@@ -461,7 +534,11 @@ class Client(object):
newer api needs these parameter to work. newer api needs these parameter to work.
''' '''
data = {"msgs_recv": 0} data = {
"msgs_recv": 0,
"channel": self.user_channel,
"clientid": self.client_id
}
r = self._get(StickyURL, data) r = self._get(StickyURL, data)
j = get_json(r.text) j = get_json(r.text)
@@ -500,6 +577,8 @@ class Client(object):
''' '''
if 'ms' not in content: return if 'ms' not in content: return
log.debug("Received {}".format(content["ms"]))
for m in content['ms']: for m in content['ms']:
try: try:
if m['type'] in ['m_messaging', 'messaging']: if m['type'] in ['m_messaging', 'messaging']:
@@ -530,9 +609,11 @@ class Client(object):
fbid = m['delta']['messageMetadata']['actorFbId'] fbid = m['delta']['messageMetadata']['actorFbId']
name = None name = None
self.on_message(mid, fbid, name, message, m) self.on_message(mid, fbid, name, message, m)
elif m['type'] in ['jewel_requests_add']:
from_id = m['from']
self.on_friend_request(from_id)
else: else:
if self.debug: log.debug("Unknwon type {}".format(m))
print(m)
except Exception as e: except Exception as e:
# ex_type, ex, tb = sys.exc_info() # ex_type, ex, tb = sys.exc_info()
self.on_message_error(sys.exc_info(), m) self.on_message_error(sys.exc_info(), m)
@@ -542,9 +623,7 @@ class Client(object):
self.listening = True self.listening = True
sticky, pool = self._getSticky() sticky, pool = self._getSticky()
if self.debug: log.info("Listening...")
print("Listening...")
while self.listening: while self.listening:
try: try:
if markAlive: self.ping(sticky) if markAlive: self.ping(sticky)
@@ -557,13 +636,13 @@ class Client(object):
break break
except requests.exceptions.Timeout: except requests.exceptions.Timeout:
pass pass
def getUserInfo(self,*user_ids): def getUserInfo(self,*user_ids):
"""Get user info from id. Unordered. """Get user info from id. Unordered.
:param user_ids: one or more user id(s) to query :param user_ids: one or more user id(s) to query
""" """
data = {"ids[{}]".format(i):user_id for i,user_id in enumerate(user_ids)} data = {"ids[{}]".format(i):user_id for i,user_id in enumerate(user_ids)}
r = self._post(UserInfoURL, data) r = self._post(UserInfoURL, data)
info = get_json(r.text) info = get_json(r.text)
@@ -573,6 +652,70 @@ class Client(object):
return full_data return full_data
def remove_user_from_chat(self, threadID, userID):
"""Remove user (userID) from group chat (threadID)
:param threadID: group chat id
:param userID: user id to remove from chat
"""
data = {
"uid" : userID,
"tid" : threadID
}
r = self._post(RemoveUserURL, data)
self._console(r)
self._console(data)
return r.ok
def changeThreadTitle(self, threadID, newTitle):
"""Change title of a group conversation
:param threadID: group chat id
:param newTitle: new group chat title
"""
messageAndOTID = generateOfflineThreadingID()
timestamp = now()
date = datetime.now()
data = {
'client' : self.client,
'action_type' : 'ma-type:log-message',
'author' : 'fbid:' + str(self.uid),
'thread_id' : '',
'author_email' : '',
'coordinates' : '',
'timestamp' : timestamp,
'timestamp_absolute' : 'Today',
'timestamp_relative' : str(date.hour) + ":" + str(date.minute).zfill(2),
'timestamp_time_passed' : '0',
'is_unread' : False,
'is_cleared' : False,
'is_forward' : False,
'is_filtered_content' : False,
'is_spoof_warning' : False,
'source' : 'source:chat:web',
'source_tags[0]' : 'source:chat',
'status' : '0',
'offline_threading_id' : messageAndOTID,
'message_id' : messageAndOTID,
'threading_id': generateMessageID(self.client_id),
'manual_retry_cnt' : '0',
'thread_fbid' : threadID,
'log_message_data[name]' : newTitle,
'log_message_type' : 'log:thread-name'
}
r = self._post(SendURL, data)
self._console(r)
self._console(data)
return r.ok
@@ -582,7 +725,14 @@ class Client(object):
''' '''
self.markAsDelivered(author_id, mid) self.markAsDelivered(author_id, mid)
self.markAsRead(author_id) self.markAsRead(author_id)
print("%s said: %s"%(author_name, message)) 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
'''
print("friend request from %s"%from_id)
def on_typing(self, author_id): def on_typing(self, author_id):
@@ -610,8 +760,7 @@ class Client(object):
''' '''
subclass Client and override this method to add custom behavior on event subclass Client and override this method to add custom behavior on event
''' '''
print("Exception: ") log.warning("Exception:\n{}".format(exception))
print(exception)
def on_qprimer(self, timestamp): def on_qprimer(self, timestamp):