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:
@@ -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,6 +315,8 @@ 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),
@@ -286,9 +339,8 @@ 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=''):
@@ -322,24 +374,32 @@ class Client(object):
: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)
@@ -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):