This repository has been archived on 2025-07-31. You can view files and clone it, but cannot push or open issues or pull requests.
Files
fbchat/fbchat/client.py
2017-02-03 15:46:50 +05:00

844 lines
27 KiB
Python

# -*- coding: UTF-8 -*-
"""
fbchat
~~~~~~
Facebook Chat (Messenger) for Python
:copyright: (c) 2015 by Taehoon Kim.
:copyright: (c) 2015-2016 by PidgeyL.
:license: BSD, see LICENSE for more details.
"""
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
from mimetypes import guess_type
from .utils import *
from .models import *
from .stickers import *
import time
import sys
# 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"
facebookEncoding = 'UTF-8'
# Log settings
log = logging.getLogger("client")
class Client(object):
"""A client for the Facebook Chat (Messenger).
See http://github.com/carpedm20/fbchat for complete
documentation for the API.
"""
def __init__(self, email, password, debug=True, user_agent=None , max_retries=5):
"""A client for the Facebook Chat (Messenger).
:param email: Facebook `email` or `id` or `phone number`
:param password: Facebook account password
import fbchat
chat = fbchat.Client(email, password)
"""
if not (email and password):
raise Exception("id and password or config is needed")
self.email = email
self.password = password
self.debug = debug
self._session = requests.session()
self.req_counter = 1
self.seq = "0"
self.payloadDefault={}
self.client = 'mercury'
self.listening = False
self.GENDERS = {
0: 'unknown',
1: 'female_singular',
2: 'male_singular',
3: 'female_singular_guess',
4: 'male_singular_guess',
5: 'mixed',
6: 'neuter_singular',
7: 'unknown_singular',
8: 'female_plural',
9: 'male_plural',
10: 'neuter_plural',
11: 'unknown_plural',
}
if not user_agent:
user_agent = choice(USER_AGENTS)
self._header = {
'Content-Type' : 'application/x-www-form-urlencoded',
'Referer' : BaseURL,
'Origin' : BaseURL,
'User-Agent' : user_agent,
'Connection' : 'keep-alive',
}
# 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():
log.warning("Attempt #{} failed{}".format(i,{True:', retrying'}.get(i<5,'')))
time.sleep(1)
continue
else:
log.info("Login successful")
break
else:
raise Exception("login failed. Check id/password")
self.threads = []
def _console(self, 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:
self.ttstamp += str(ord(i))
self.ttstamp += '2'
def _generatePayload(self, query):
'''
Adds the following defaults to the payload:
__rev, __user, __a, ttstamp, fb_dtsg, __req
'''
payload = self.payloadDefault.copy()
if query:
payload.update(query)
payload['__req'] = str_base(self.req_counter, 36)
payload['seq'] = self.seq
self.req_counter += 1
return payload
def _get(self, url, query=None, timeout=30):
payload=self._generatePayload(query)
return self._session.get(url, headers=self._header, params=payload, timeout=timeout)
def _post(self, url, query=None, timeout=30):
payload=self._generatePayload(query)
return self._session.post(url, headers=self._header, data=payload, timeout=timeout)
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")
soup = bs(self._get(MobileURL).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)
if 'home' in r.url:
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.ttstamp = ''
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])
self.payloadDefault['__user'] = self.uid
self.payloadDefault['__a'] = '1'
self.payloadDefault['ttstamp'] = self.ttstamp
self.payloadDefault['fb_dtsg'] = self.fb_dtsg
self.form = {
'channel' : self.user_channel,
'partition' : '-2',
'clientid' : self.client_id,
'viewer_uid' : self.uid,
'uid' : self.uid,
'state' : 'active',
'format' : 'json',
'idle' : 0,
'cap' : '8'
}
self.prev = now()
self.tmp_prev = now()
self.last_sync = now()
return True
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
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):
""" Gets all users from chat with info included """
data = {
'viewer': self.uid,
}
r = self._post(AllUsersURL, query=data)
if not r.ok or len(r.text) == 0:
return None
j = get_json(r.text)
if not j['payload']:
return None
payload = j['payload']
users = []
for k in payload.keys():
user = self._adapt_user_in_chat_to_user_model(payload[k])
users.append(User(user))
return users
def getUsers(self, name):
"""Find and get user by his/her name
:param name: name of a person
"""
payload = {
'value' : name.lower(),
'viewer' : self.uid,
'rsp' : "search",
'context' : "search",
'path' : "/home.php",
'request_id' : str(uuid1()),
}
r = self._get(SearchURL, payload)
self.j = j = get_json(r.text)
users = []
for entry in j['payload']['entries']:
if entry['type'] == 'user':
users.append(User(entry))
return users # have bug TypeError: __repr__ returned non-string (type bytes)
def send(self, recipient_id, message=None, message_type='user', like=None, image_id=None):
"""Send a message with given thread id
: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
:param like: size of the like sticker you want to send
:param image_id: id for the image to send, gotten from the UploadURL
"""
if message_type.lower() == 'group':
thread_id = recipient_id
user_id = None
else:
thread_id = None
user_id = recipient_id
messageAndOTID=generateOfflineThreadingID()
timestamp = now()
date = datetime.now()
data = {
'client': self.client,
'action_type' : 'ma-type:user-generated-message',
'author' : 'fbid:' + str(self.uid),
'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_filtered_content_bh': False,
'is_filtered_content_account': False,
'is_filtered_content_quasar': False,
'is_filtered_content_invalid_app': False,
'is_spoof_warning' : False,
'source' : 'source:chat:web',
'source_tags[0]' : 'source:chat',
'body' : message,
'html_body' : False,
'ui_push_phase' : 'V3',
'status' : '0',
'offline_threading_id':messageAndOTID,
'message_id' : messageAndOTID,
'threading_id':generateMessageID(self.client_id),
'ephemeral_ttl_mode:': '0',
'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),
}
if message_type.lower() == 'group':
data["thread_fbid"] = recipient_id
else:
data["other_user_fbid"] = recipient_id
if image_id:
data['image_ids[0]'] = image_id
if like:
try:
sticker = LIKES[like.lower()]
except KeyError:
# 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)
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
:param image: URL for an image to download and send
"""
mimetype = guess_type(image)[0]
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
:param image: path to a local image to send
"""
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(facebookEncoding)
# 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, 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 = {}
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:
return None
j = get_json(r.text)
if not j['payload']:
return None
messages = []
for message in j['payload']['actions']:
messages.append(Message(**message))
return list(reversed(messages))
def getThreadList(self, start, end=None):
"""Get thread list of your facebook account.
:param start: the start index of a thread
:param end: (optional) the last index of a thread
"""
if not end: end = start + 20
if end <= start: end = start + end
timestamp = now()
date = datetime.now()
data = {
'client' : self.client,
'inbox[offset]' : start,
'inbox[limit]' : end,
}
r = self._post(ThreadsURL, data)
if not r.ok or len(r.text) == 0:
return None
j = get_json(r.text)
# Get names for people
participants = {}
try:
for participant in j['payload']['participants']:
participants[participant["fbid"]] = participant["name"]
except Exception as e:
log.warning(str(j))
# Prevent duplicates in self.threads
threadIDs = [getattr(x, "thread_id") for x in self.threads]
for thread in j['payload']['threads']:
if thread["thread_id"] not in threadIDs:
try:
thread["other_user_name"] = participants[int(thread["other_user_fbid"])]
except:
thread["other_user_name"] = ""
t = Thread(**thread)
self.threads.append(t)
return self.threads
def getUnread(self):
form = {
'client': 'mercury_sync',
'folders[0]': 'inbox',
'last_action_timestamp': now() - 60*1000
#'last_action_timestamp': 0
}
r = self._post(ThreadSyncURL, form)
if not r.ok or len(r.text) == 0:
return None
j = get_json(r.text)
result = {
"message_counts": j['payload']['message_counts'],
"unseen_threads": j['payload']['unseen_thread_ids']
}
return result
def markAsDelivered(self, userID, threadID):
data = {"message_ids[0]": threadID}
data["thread_ids[%s][0]"%userID] = threadID
r = self._post(DeliveredURL, data)
return r.ok
def markAsRead(self, userID):
data = {
"watermarkTimestamp": now(),
"shouldSendReadReceipt": True
}
data["ids[%s]"%userID] = True
r = self._post(ReadStatusURL, data)
return r.ok
def markAsSeen(self):
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 = {
'channel': self.user_channel,
'clientid': self.client_id,
'partition': -2,
'cap': 0,
'uid': self.uid,
'sticky': sticky,
'viewer_uid': self.uid
}
r = self._get(PingURL, data)
return r.ok
def _getSticky(self):
'''
Call pull api to get sticky and pool parameter,
newer api needs these parameter to work.
'''
data = {
"msgs_recv": 0,
"channel": self.user_channel,
"clientid": self.client_id
}
r = self._get(StickyURL, data)
j = get_json(r.text)
if 'lb_info' not in j:
raise Exception('Get sticky pool error')
sticky = j['lb_info']['sticky']
pool = j['lb_info']['pool']
return sticky, pool
def _pullMessage(self, sticky, pool):
'''
Call pull api with seq value to get message data.
'''
data = {
"msgs_recv": 0,
"sticky_token": sticky,
"sticky_pool": pool,
"clientid": self.client_id,
}
r = self._get(StickyURL, data)
r.encoding = facebookEncoding
j = get_json(r.text)
self.seq = j.get('seq', '0')
return j
def _parseMessage(self, content):
'''
Get message and author name from content.
May contains multiple messages in the content.
'''
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']:
if m['event'] in ['deliver']:
mid = m['message']['mid']
message = m['message']['body']
fbid = m['message']['sender_fbid']
name = m['message']['sender_name']
self.on_message(mid, fbid, name, message, m)
elif m['type'] in ['typ']:
self.on_typing(m.get("from"))
elif m['type'] in ['m_read_receipt']:
self.on_read(m.get('realtime_viewer_fbid'), m.get('reader'), m.get('time'))
elif m['type'] in ['inbox']:
viewer = m.get('realtime_viewer_fbid')
unseen = m.get('unseen')
unread = m.get('unread')
other_unseen = m.get('other_unseen')
other_unread = m.get('other_unread')
timestamp = m.get('seen_timestamp')
self.on_inbox(viewer, unseen, unread, other_unseen, other_unread, timestamp)
elif m['type'] in ['qprimer']:
self.on_qprimer(m.get('made'))
elif m['type'] in ['delta']:
if 'messageMetadata' in m['delta']:
mid = m['delta']['messageMetadata']['messageId']
message = m['delta'].get('body','')
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:
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)
def listen(self, markAlive=True):
self.listening = True
sticky, pool = self._getSticky()
log.info("Listening...")
while self.listening:
try:
if markAlive: self.ping(sticky)
try:
content = self._pullMessage(sticky, pool)
if content: self._parseMessage(content)
except requests.exceptions.RequestException as e:
continue
except KeyboardInterrupt:
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
"""
data = {"ids[{}]".format(i):user_id for i,user_id in enumerate(user_ids)}
r = self._post(UserInfoURL, data)
info = get_json(r.text)
full_data= [details for profile,details in info['payload']['profiles'].items()]
if len(full_data)==1:
full_data=full_data[0]
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
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
'''
print("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_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