Merge branch 'master' into master
This commit is contained in:
215
fbchat/client.py
215
fbchat/client.py
@@ -13,7 +13,9 @@
|
||||
|
||||
import requests
|
||||
import json
|
||||
import logging
|
||||
from uuid import uuid1
|
||||
import warnings
|
||||
from random import random, choice
|
||||
from datetime import datetime
|
||||
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"
|
||||
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"
|
||||
|
||||
# Log settings
|
||||
log = logging.getLogger("client")
|
||||
|
||||
class Client(object):
|
||||
"""A client for the Facebook Chat (Messenger).
|
||||
@@ -83,15 +91,28 @@ class Client(object):
|
||||
'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):
|
||||
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)
|
||||
continue
|
||||
else:
|
||||
self._console("login successful")
|
||||
log.info("Login successful")
|
||||
break
|
||||
else:
|
||||
raise Exception("login failed. Check id/password")
|
||||
@@ -100,7 +121,23 @@ class Client(object):
|
||||
self.threads = []
|
||||
|
||||
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):
|
||||
for i in self.fb_dtsg:
|
||||
@@ -131,12 +168,12 @@ class Client(object):
|
||||
def _cleanPost(self, url, query=None, timeout=30):
|
||||
self.req_counter += 1
|
||||
return self._session.post(url, headers=self._header, data=query, timeout=timeout)
|
||||
|
||||
|
||||
def _postFile(self, url, files=None, timeout=30):
|
||||
payload=self._generatePayload(None)
|
||||
return self._session.post(url, data=payload, timeout=timeout, files=files)
|
||||
|
||||
|
||||
|
||||
|
||||
def login(self):
|
||||
if not (self.email and self.password):
|
||||
raise Exception("id and password or config is needed")
|
||||
@@ -159,6 +196,7 @@ class Client(object):
|
||||
r = self._get(BaseURL)
|
||||
soup = bs(r.text, "lxml")
|
||||
self.fb_dtsg = soup.find("input", {'name':'fb_dtsg'})['value']
|
||||
self.fb_h = soup.find("input", {'name':'h'})['value']
|
||||
self._setttstamp()
|
||||
# Set default payload
|
||||
self.payloadDefault['__rev'] = int(r.text.split('"revision":',1)[1].split(",",1)[0])
|
||||
@@ -187,6 +225,19 @@ class Client(object):
|
||||
else:
|
||||
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):
|
||||
pass
|
||||
|
||||
@@ -264,8 +315,10 @@ class Client(object):
|
||||
'manual_retry_cnt' : '0',
|
||||
'signatureID' : getSignatureID(),
|
||||
'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[1]' : 'fbid:' + str(self.uid),
|
||||
'specific_to_list[1]' : 'fbid:' + str(self.uid),
|
||||
|
||||
}
|
||||
if message_type.lower() == 'group':
|
||||
@@ -286,14 +339,13 @@ class Client(object):
|
||||
|
||||
r = self._post(SendURL, data)
|
||||
|
||||
if self.debug:
|
||||
print(r)
|
||||
print(data)
|
||||
log.debug("Sending {}".format(r))
|
||||
log.debug("With data {}".format(data))
|
||||
return r.ok
|
||||
|
||||
def sendRemoteImage(self, recipient_id, message=None, message_type='user', image=''):
|
||||
"""Send an image from a URL
|
||||
|
||||
|
||||
: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_type: determines if the recipient_id is for user or thread
|
||||
@@ -303,10 +355,10 @@ class Client(object):
|
||||
remote_image = requests.get(image).content
|
||||
image_id = self.uploadImage({'file': (image, remote_image, mimetype)})
|
||||
return self.send(recipient_id, message, message_type, None, image_id)
|
||||
|
||||
|
||||
def sendLocalImage(self, recipient_id, message=None, message_type='user', image=''):
|
||||
"""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 message: a text that you want to send
|
||||
: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]
|
||||
image_id = self.uploadImage({'file': (image, open(image), mimetype)})
|
||||
return self.send(recipient_id, message, message_type, None, image_id)
|
||||
|
||||
|
||||
def uploadImage(self, image):
|
||||
"""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
|
||||
"""
|
||||
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
|
||||
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
|
||||
|
||||
:param userID: ID of the user you want the messages from
|
||||
:param start: the start 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 end <= start: end = start + end
|
||||
|
||||
data = {}
|
||||
data['messages[user_ids][%s][offset]'%userID] = start
|
||||
data['messages[user_ids][%s][limit]'%userID] = end
|
||||
data['messages[user_ids][%s][timestamp]'%userID] = now()
|
||||
if thread_type == 'user':
|
||||
key = 'user_ids'
|
||||
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)
|
||||
if not r.ok or len(r.text) == 0:
|
||||
@@ -385,7 +445,7 @@ class Client(object):
|
||||
for participant in j['payload']['participants']:
|
||||
participants[participant["fbid"]] = participant["name"]
|
||||
except Exception as e:
|
||||
print(j)
|
||||
log.warning(str(j))
|
||||
|
||||
# Prevent duplicates 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})
|
||||
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):
|
||||
data = {
|
||||
@@ -461,7 +534,11 @@ class Client(object):
|
||||
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)
|
||||
j = get_json(r.text)
|
||||
@@ -500,6 +577,8 @@ class Client(object):
|
||||
'''
|
||||
|
||||
if 'ms' not in content: return
|
||||
|
||||
log.debug("Received {}".format(content["ms"]))
|
||||
for m in content['ms']:
|
||||
try:
|
||||
if m['type'] in ['m_messaging', 'messaging']:
|
||||
@@ -530,9 +609,11 @@ class Client(object):
|
||||
fbid = m['delta']['messageMetadata']['actorFbId']
|
||||
name = None
|
||||
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:
|
||||
if self.debug:
|
||||
print(m)
|
||||
log.debug("Unknwon type {}".format(m))
|
||||
except Exception as e:
|
||||
# ex_type, ex, tb = sys.exc_info()
|
||||
self.on_message_error(sys.exc_info(), m)
|
||||
@@ -542,9 +623,7 @@ class Client(object):
|
||||
self.listening = True
|
||||
sticky, pool = self._getSticky()
|
||||
|
||||
if self.debug:
|
||||
print("Listening...")
|
||||
|
||||
log.info("Listening...")
|
||||
while self.listening:
|
||||
try:
|
||||
if markAlive: self.ping(sticky)
|
||||
@@ -557,13 +636,13 @@ class Client(object):
|
||||
break
|
||||
except requests.exceptions.Timeout:
|
||||
pass
|
||||
|
||||
|
||||
def getUserInfo(self,*user_ids):
|
||||
"""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)}
|
||||
r = self._post(UserInfoURL, data)
|
||||
info = get_json(r.text)
|
||||
@@ -573,6 +652,70 @@ class Client(object):
|
||||
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.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):
|
||||
@@ -610,8 +760,7 @@ class Client(object):
|
||||
'''
|
||||
subclass Client and override this method to add custom behavior on event
|
||||
'''
|
||||
print("Exception: ")
|
||||
print(exception)
|
||||
log.warning("Exception:\n{}".format(exception))
|
||||
|
||||
|
||||
def on_qprimer(self, timestamp):
|
||||
|
Reference in New Issue
Block a user