Added GraphQL requests
This commit is contained in:
318
fbchat/client.py
318
fbchat/client.py
@@ -10,6 +10,7 @@ from bs4 import BeautifulSoup as bs
|
||||
from mimetypes import guess_type
|
||||
from .utils import *
|
||||
from .models import *
|
||||
from .graphql import *
|
||||
import time
|
||||
|
||||
|
||||
@@ -22,7 +23,7 @@ class Client(object):
|
||||
|
||||
listening = False
|
||||
"""Whether the client is listening. Used when creating an external event loop to determine when to stop listening"""
|
||||
id = None
|
||||
uid = None
|
||||
"""
|
||||
The ID of the client.
|
||||
Can be used as `thread_id`. See :ref:`intro_threads` for more info.
|
||||
@@ -53,7 +54,6 @@ class Client(object):
|
||||
self.client = 'mercury'
|
||||
self.default_thread_id = None
|
||||
self.default_thread_type = None
|
||||
self.threads = []
|
||||
|
||||
if not user_agent:
|
||||
user_agent = choice(USER_AGENTS)
|
||||
@@ -109,6 +109,27 @@ class Client(object):
|
||||
headers = dict((i, self._header[i]) for i in self._header if i != 'Content-Type')
|
||||
return self._session.post(url, headers=headers, data=payload, timeout=timeout, files=files)
|
||||
|
||||
def graphql_request(self, *queries):
|
||||
"""
|
||||
.. todo::
|
||||
Documenting this
|
||||
|
||||
:raises: Exception if request failed
|
||||
"""
|
||||
payload = {
|
||||
'method': 'GET',
|
||||
'response_format': 'json',
|
||||
'queries': graphql_queries_to_json(*queries)
|
||||
}
|
||||
|
||||
j = graphql_response_to_json(checkRequest(self._post(ReqUrl.GRAPHQL, payload), do_json_check=False))
|
||||
|
||||
if len(j) == 1:
|
||||
return j[0]
|
||||
else:
|
||||
return tuple(j)
|
||||
|
||||
|
||||
"""
|
||||
END INTERNAL REQUEST METHODS
|
||||
"""
|
||||
@@ -117,18 +138,23 @@ class Client(object):
|
||||
LOGIN METHODS
|
||||
"""
|
||||
|
||||
def _resetValues(self):
|
||||
self.payloadDefault={}
|
||||
self._session = requests.session()
|
||||
self.req_counter = 1
|
||||
self.seq = "0"
|
||||
self.uid = None
|
||||
|
||||
def _postLogin(self):
|
||||
self.payloadDefault = {}
|
||||
self.client_id = hex(int(random()*2147483648))[2:]
|
||||
self.start_time = now()
|
||||
self.id = str(self._session.cookies['c_user'])
|
||||
self.user_channel = "p_" + self.id
|
||||
self.uid = str(self._session.cookies['c_user'])
|
||||
self.user_channel = "p_" + self.uid
|
||||
self.ttstamp = ''
|
||||
|
||||
r = self._get(ReqUrl.BASE)
|
||||
soup = bs(r.text, "lxml")
|
||||
log.debug(r.text)
|
||||
log.debug(r.url)
|
||||
self.fb_dtsg = soup.find("input", {'name':'fb_dtsg'})['value']
|
||||
self.fb_h = soup.find("input", {'name':'h'})['value']
|
||||
for i in self.fb_dtsg:
|
||||
@@ -136,7 +162,7 @@ class Client(object):
|
||||
self.ttstamp += '2'
|
||||
# Set default payload
|
||||
self.payloadDefault['__rev'] = int(r.text.split('"client_revision":',1)[1].split(",",1)[0])
|
||||
self.payloadDefault['__user'] = self.id
|
||||
self.payloadDefault['__user'] = self.uid
|
||||
self.payloadDefault['__a'] = '1'
|
||||
self.payloadDefault['ttstamp'] = self.ttstamp
|
||||
self.payloadDefault['fb_dtsg'] = self.fb_dtsg
|
||||
@@ -145,8 +171,8 @@ class Client(object):
|
||||
'channel' : self.user_channel,
|
||||
'partition' : '-2',
|
||||
'clientid' : self.client_id,
|
||||
'viewer_uid' : self.id,
|
||||
'uid' : self.id,
|
||||
'viewer_uid' : self.uid,
|
||||
'uid' : self.uid,
|
||||
'state' : 'active',
|
||||
'format' : 'json',
|
||||
'idle' : 0,
|
||||
@@ -267,9 +293,13 @@ class Client(object):
|
||||
if not session_cookies or 'c_user' not in session_cookies:
|
||||
return False
|
||||
|
||||
# Load cookies into current session
|
||||
self._session.cookies = requests.cookies.merge_cookies(self._session.cookies, session_cookies)
|
||||
self._postLogin()
|
||||
try:
|
||||
# Load cookies into current session
|
||||
self._session.cookies = requests.cookies.merge_cookies(self._session.cookies, session_cookies)
|
||||
self._postLogin()
|
||||
except Exception:
|
||||
self._resetValues()
|
||||
return False
|
||||
return True
|
||||
|
||||
def login(self, email, password, max_tries=5):
|
||||
@@ -320,12 +350,7 @@ class Client(object):
|
||||
|
||||
r = self._get(ReqUrl.LOGOUT, data)
|
||||
|
||||
# reset value
|
||||
self.payloadDefault={}
|
||||
self._session = requests.session()
|
||||
self.req_counter = 1
|
||||
self.seq = "0"
|
||||
self.id = None
|
||||
self._resetValues()
|
||||
|
||||
return r.ok
|
||||
|
||||
@@ -385,7 +410,7 @@ class Client(object):
|
||||
"""
|
||||
|
||||
data = {
|
||||
'viewer': self.id,
|
||||
'viewer': self.uid,
|
||||
}
|
||||
j = checkRequest(self._post(ReqUrl.ALL_USERS, query=data))
|
||||
if not j['payload']:
|
||||
@@ -399,51 +424,82 @@ class Client(object):
|
||||
|
||||
return users
|
||||
|
||||
def _searchFor(self, name):
|
||||
payload = {
|
||||
'value' : name.lower(),
|
||||
'viewer' : self.id,
|
||||
'rsp' : 'search',
|
||||
'context' : 'search',
|
||||
'path' : '/home.php',
|
||||
'request_id' : str(uuid1()),
|
||||
}
|
||||
def searchForUsers(self, name, limit=1):
|
||||
"""
|
||||
Find and get user by his/her name
|
||||
|
||||
j = checkRequest(self._get(ReqUrl.SEARCH, payload))
|
||||
|
||||
entries = []
|
||||
for k in j['payload']['entries']:
|
||||
if k['type'] in ['user', 'friend']:
|
||||
entries.append(User(k['uid'], url=k['path'], first_name=k['firstname'], last_name=k['lastname'], is_friend=k['is_connected'], photo=k['photo'], name=k['text']))
|
||||
if k['type'] == 'page':
|
||||
if 'city_text' not in k:
|
||||
k['city_text'] = None
|
||||
entries.append(Page(k['uid'], url=k['path'], city=k['city_text'], likees=k['feedback_count'], sub_text=k['subtext'], photo=k['photo'], name=k['text']))
|
||||
return entries
|
||||
|
||||
def searchForUsers(self, name):
|
||||
"""Find and get user by his/her name
|
||||
|
||||
:param name: name of a user
|
||||
:param name: Name of the user
|
||||
:param limit: The max. amount of users to fetch
|
||||
:return: :class:`models.User` objects, ordered by relevance
|
||||
:rtype: list
|
||||
:raises: Exception if request failed
|
||||
"""
|
||||
|
||||
entries = self._searchFor(name)
|
||||
return [k for k in entries if k.type == ThreadType.USER]
|
||||
j = self.graphql_request(GraphQL(query=GraphQL.SEARCH_USER, params={'search': name, 'limit': limit}))
|
||||
|
||||
def searchForPages(self, name):
|
||||
"""Find and get page by its name
|
||||
return [graphql_to_user(node) for node in j[name]['users']['nodes']]
|
||||
|
||||
:param name: name of a page
|
||||
def searchForPages(self, name, limit=1):
|
||||
"""
|
||||
Find and get page by its name
|
||||
|
||||
:param name: Name of the page
|
||||
:return: :class:`models.Page` objects, ordered by relevance
|
||||
:rtype: list
|
||||
:raises: Exception if request failed
|
||||
"""
|
||||
|
||||
entries = self._searchFor(name)
|
||||
return [k for k in entries if k.type == ThreadType.PAGE]
|
||||
j = self.graphql_request(GraphQL(query=GraphQL.SEARCH_PAGE, params={'search': name, 'limit': limit}))
|
||||
|
||||
return [graphql_to_page(node) for node in j[name]['pages']['nodes']]
|
||||
|
||||
#entries = self._searchFor(name)
|
||||
#return [k for k in entries if k.type == ThreadType.PAGE]
|
||||
|
||||
def searchForGroups(self, name, limit=1):
|
||||
"""
|
||||
Find and get group thread by its name
|
||||
|
||||
:param name: Name of the group thread
|
||||
:param limit: The max. amount of groups to fetch
|
||||
:return: :class:`models.Group` objects, ordered by relevance
|
||||
:rtype: list
|
||||
:raises: Exception if request failed
|
||||
"""
|
||||
|
||||
j = self.graphql_request(GraphQL(query=GraphQL.SEARCH_GROUP, params={'search': name, 'limit': limit}))
|
||||
|
||||
return [graphql_to_group(node) for node in j['viewer']['groups']['nodes']]
|
||||
|
||||
def searchForThreads(self, name, limit=1):
|
||||
"""
|
||||
Find and get a thread by its name
|
||||
|
||||
:param name: Name of the thread
|
||||
:param limit: The max. amount of groups to fetch
|
||||
:return: :class:`models.User`, :class:`models.Group` and :class:`models.Page` objects, ordered by relevance
|
||||
:rtype: list
|
||||
:raises: Exception if request failed
|
||||
"""
|
||||
|
||||
j = self.graphql_request(GraphQL(query=GraphQL.SEARCH_THREAD, params={'search': name, 'limit': limit}))
|
||||
|
||||
rtn = []
|
||||
for node in j[name]['threads']['nodes']:
|
||||
if node['__typename'] == 'User':
|
||||
rtn.append(graphql_to_user(node))
|
||||
elif node['__typename'] == 'MessageThread':
|
||||
# MessageThread => Group thread
|
||||
rtn.append(graphql_to_group(node))
|
||||
elif node['__typename'] == 'Page':
|
||||
rtn.append(graphql_to_page(node))
|
||||
elif node['__typename'] == 'Group':
|
||||
# We don't handle Facebook "Groups"
|
||||
pass
|
||||
else:
|
||||
log.warning('Unknown __typename: {} in {}'.format(repr(node['__typename']), node))
|
||||
|
||||
return rtn
|
||||
|
||||
def _fetchInfo(self, *ids):
|
||||
data = {
|
||||
@@ -459,16 +515,19 @@ class Client(object):
|
||||
k = j['payload']['profiles'][_id]
|
||||
if k['type'] in ['user', 'friend']:
|
||||
entries[_id] = User(_id, url=k['uri'], first_name=k['firstName'], is_friend=k['is_friend'], gender=GENDERS[k['gender']], photo=k['thumbSrc'], name=k['name'])
|
||||
if k['type'] == 'page':
|
||||
entries[_id] = Page(_id, url=k['uri'], city=None, likees=None, sub_text=None, photo=k['thumbSrc'], name=k['name'])
|
||||
elif k['type'] == 'page':
|
||||
entries[_id] = Page(_id, url=k['uri'], photo=k['thumbSrc'], name=k['name'])
|
||||
else:
|
||||
raise Exception('{} had an unknown thread type: {}'.format(_id, k))
|
||||
|
||||
return entries
|
||||
|
||||
def fetchUserInfo(self, *user_ids):
|
||||
"""Get users' info from ids, unordered
|
||||
"""
|
||||
Get users' info from IDs, unordered
|
||||
|
||||
:param user_ids: One or more user ID(s) to query
|
||||
:return: :class:`models.User` objects
|
||||
:return: :class:`models.User` objects, labeled by their ID
|
||||
:rtype: dict
|
||||
:raises: Exception if request failed
|
||||
"""
|
||||
@@ -482,10 +541,11 @@ class Client(object):
|
||||
return users
|
||||
|
||||
def fetchPageInfo(self, *page_ids):
|
||||
"""Get page's info from ids, unordered
|
||||
"""
|
||||
Get pages' info from IDs, unordered
|
||||
|
||||
:param user_ids: One or more page ID(s) to query
|
||||
:return: :class:`models.Page` objects
|
||||
:param page_ids: One or more page ID(s) to query
|
||||
:return: :class:`models.Page` objects, labeled by their ID
|
||||
:rtype: dict
|
||||
:raises: Exception if request failed
|
||||
"""
|
||||
@@ -498,95 +558,79 @@ class Client(object):
|
||||
|
||||
return users
|
||||
|
||||
def fetchThreadMessages(self, offset=0, amount=20, thread_id=None, thread_type=ThreadType.USER):
|
||||
"""Get the last messages in a thread
|
||||
def fetchThreadMessages(self, thread_id=None, limit=20, before=None):
|
||||
"""
|
||||
Get the last messages in a thread
|
||||
|
||||
.. todo::
|
||||
Fix this. Facebook broke it somehow. Also, clean up return values
|
||||
|
||||
:param offset: Where to start retrieving messages from
|
||||
:param amount: Number of messages to retrieve
|
||||
:param thread_id: User/Group ID to retrieve from. See :ref:`intro_threads`
|
||||
:param thread_type: See :ref:`intro_threads`
|
||||
:type offset: int
|
||||
:type amount: int
|
||||
:type thread_type: models.ThreadType
|
||||
:return: Dictionaries, containing message data
|
||||
:param thread_id: User/Group ID to default to. See :ref:`intro_threads`
|
||||
:param limit: Max. number of messages to retrieve
|
||||
:param before: A timestamp, indicating from which point to retrieve messages
|
||||
:type limit: int
|
||||
:type before: int
|
||||
:return: :class:`models.Message` objects
|
||||
:rtype: list
|
||||
:raises: Exception if request failed
|
||||
"""
|
||||
|
||||
thread_id, thread_type = self._getThread(thread_id, thread_type)
|
||||
j = self.graphql_request(GraphQL(doc_id='1386147188135407', params={
|
||||
'id': thread_id,
|
||||
'message_limit': limit,
|
||||
'load_messages': True,
|
||||
'load_read_receipts': False,
|
||||
'before': before
|
||||
}))
|
||||
|
||||
if amount < 1:
|
||||
raise Exception('`amount` must be a positive integer, got {}'.format(amount))
|
||||
if j['message_thread'] is None:
|
||||
raise Exception('Could not fetch thread {}'.format(thread_id))
|
||||
|
||||
if thread_type == ThreadType.USER:
|
||||
key = 'user_ids'
|
||||
elif thread_type == ThreadType.GROUP:
|
||||
key = 'thread_fbids'
|
||||
return list(reversed([graphql_to_message(message) for message in j['message_thread']['messages']['nodes']]))
|
||||
|
||||
data = {
|
||||
'messages[{}][{}][offset]'.format(key, thread_id): offset,
|
||||
'messages[{}][{}][limit]'.format(key, thread_id): 19,
|
||||
'messages[{}][{}][timestamp]'.format(key, thread_id): now()
|
||||
}
|
||||
|
||||
j = checkRequest(self._post(ReqUrl.MESSAGES, query=data))
|
||||
if not j['payload']:
|
||||
raise Exception('Missing payload: {}, with data: {}'.format(j, data))
|
||||
|
||||
messages = []
|
||||
for message in j['payload'].get('actions'):
|
||||
messages.append(Message(**message))
|
||||
return list(reversed(messages))
|
||||
|
||||
def fetchThreadList(self, offset=0, amount=20):
|
||||
def fetchThreadList(self, offset=0, limit=20):
|
||||
"""Get thread list of your facebook account
|
||||
|
||||
.. todo::
|
||||
Clean up return values
|
||||
|
||||
:param offset: The offset, from where in the list to recieve threads from
|
||||
:param amount: The amount of threads to recieve. Maximum of 20
|
||||
:param limit: Max. number of threads to retrieve. Capped at 20
|
||||
:type offset: int
|
||||
:type amount: int
|
||||
:return: Dictionaries, containing thread data
|
||||
:type limit: int
|
||||
:return: :class:`models.Thread` objects
|
||||
:rtype: list
|
||||
:raises: Exception if request failed
|
||||
"""
|
||||
|
||||
if amount > 20 or amount < 1:
|
||||
raise Exception('`amount` should be between 1 and 20')
|
||||
if limit > 20 or limit < 1:
|
||||
raise Exception('`limit` should be between 1 and 20')
|
||||
|
||||
data = {
|
||||
'client' : self.client,
|
||||
'inbox[offset]' : start,
|
||||
'inbox[limit]' : amount,
|
||||
'inbox[offset]' : offset,
|
||||
'inbox[limit]' : limit,
|
||||
}
|
||||
|
||||
j = checkRequest(self._post(ReqUrl.THREADS, data))
|
||||
if j.get('payload') is None:
|
||||
raise Exception('Missing payload: {}, with data: {}'.format(j, data))
|
||||
|
||||
# Get names for people
|
||||
participants = {}
|
||||
try:
|
||||
for participant in j['payload']['participants']:
|
||||
participants[participant["fbid"]] = participant["name"]
|
||||
except Exception:
|
||||
log.exception('Exception while getting names for people in getThreadList. {}'.format(j))
|
||||
for p in j['payload']['participants']:
|
||||
if p['type'] == 'page':
|
||||
participants[p['fbid']] = Page(p['fbid'], url=p['href'], photo=p['image_src'], name=p['name'])
|
||||
elif p['type'] == 'user':
|
||||
participants[p['fbid']] = User(p['fbid'], url=p['href'], first_name=p['short_name'], is_friend=p['is_friend'], gender=GENDERS[p['gender']], photo=p['image_src'], name=p['name'])
|
||||
else:
|
||||
raise Exception('A participant had an unknown type {}: {}'.format(p['type'], p))
|
||||
|
||||
# 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)
|
||||
entries = []
|
||||
for k in j['payload']['threads']:
|
||||
if k['thread_type'] == 1:
|
||||
if k['other_user_fbid'] not in participants:
|
||||
raise Exception('A thread was not in participants: {}'.format(j['payload']))
|
||||
entries.append(participants[k['other_user_fbid']])
|
||||
elif k['thread_type'] == 2:
|
||||
entries.append(Group(k['thread_fbid'], participants=[p.strip('fbid:') for p in k['participants']], photo=k['image_src'], name=k['name']))
|
||||
else:
|
||||
raise Exception('A thread had an unknown thread type: {}'.format(k))
|
||||
|
||||
return self.threads
|
||||
return entries
|
||||
|
||||
def fetchUnread(self):
|
||||
"""
|
||||
@@ -624,7 +668,7 @@ class Client(object):
|
||||
date = datetime.now()
|
||||
data = {
|
||||
'client': self.client,
|
||||
'author' : 'fbid:' + str(self.id),
|
||||
'author' : 'fbid:' + str(self.uid),
|
||||
'timestamp' : timestamp,
|
||||
'timestamp_absolute' : 'Today',
|
||||
'timestamp_relative' : str(date.hour) + ":" + str(date.minute).zfill(2),
|
||||
@@ -669,11 +713,8 @@ class Client(object):
|
||||
log.warning("Got multiple message ids' back: {}".format(message_ids))
|
||||
message_id = message_ids[0]
|
||||
except (KeyError, IndexError) as e:
|
||||
raise Exception('Error when sending message: No message IDs could be found')
|
||||
raise Exception('Error when sending message: No message IDs could be found: {}'.format(j))
|
||||
|
||||
log.info('Message sent.')
|
||||
log.debug('Sending with data {}'.format(data))
|
||||
log.debug('Recieved message ID {}'.format(message_id))
|
||||
return message_id
|
||||
|
||||
def sendMessage(self, message, thread_id=None, thread_type=ThreadType.USER):
|
||||
@@ -694,7 +735,7 @@ class Client(object):
|
||||
data['body'] = message or ''
|
||||
data['has_attachment'] = False
|
||||
data['specific_to_list[0]'] = 'fbid:' + thread_id
|
||||
data['specific_to_list[1]'] = 'fbid:' + self.id
|
||||
data['specific_to_list[1]'] = 'fbid:' + self.uid
|
||||
|
||||
return self._doSendRequest(data)
|
||||
|
||||
@@ -716,7 +757,7 @@ class Client(object):
|
||||
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.id
|
||||
data['specific_to_list[1]'] = 'fbid:' + self.uid
|
||||
|
||||
if emoji:
|
||||
data['body'] = emoji
|
||||
@@ -758,7 +799,7 @@ class Client(object):
|
||||
data['body'] = message or ''
|
||||
data['has_attachment'] = True
|
||||
data['specific_to_list[0]'] = 'fbid:' + str(thread_id)
|
||||
data['specific_to_list[1]'] = 'fbid:' + str(self.id)
|
||||
data['specific_to_list[1]'] = 'fbid:' + str(self.uid)
|
||||
|
||||
data['image_ids[0]'] = image_id
|
||||
|
||||
@@ -822,7 +863,7 @@ class Client(object):
|
||||
user_ids = set(user_ids)
|
||||
|
||||
for i, user_id in enumerate(user_ids):
|
||||
if user_id == self.id:
|
||||
if user_id == self.uid:
|
||||
raise Exception('Error when adding users: Cannot add self to group thread')
|
||||
else:
|
||||
data['log_message_data[added_participants][' + str(i) + ']'] = "fbid:" + str(user_id)
|
||||
@@ -944,7 +985,7 @@ class Client(object):
|
||||
"data": {
|
||||
"action": "ADD_REACTION",
|
||||
"client_mutation_id": "1",
|
||||
"actor_id": self.id,
|
||||
"actor_id": self.uid,
|
||||
"message_id": str(message_id),
|
||||
"reaction": reaction.value
|
||||
}
|
||||
@@ -1039,17 +1080,19 @@ class Client(object):
|
||||
LISTEN METHODS
|
||||
"""
|
||||
|
||||
def _ping(self, sticky):
|
||||
def _ping(self, sticky, pool):
|
||||
data = {
|
||||
'channel': self.user_channel,
|
||||
'clientid': self.client_id,
|
||||
'partition': -2,
|
||||
'cap': 0,
|
||||
'uid': self.id,
|
||||
'sticky': sticky,
|
||||
'viewer_uid': self.id
|
||||
'uid': self.uid,
|
||||
'sticky_token': sticky,
|
||||
'sticky_pool': pool,
|
||||
'viewer_uid': self.uid,
|
||||
'state': 'active'
|
||||
}
|
||||
checkRequest(self._get(ReqUrl.PING, data), check_json=False)
|
||||
checkRequest(self._get(ReqUrl.PING, data), do_json_check=False)
|
||||
|
||||
def _fetchSticky(self):
|
||||
"""Call pull api to get sticky and pool parameter, newer api needs these parameters to work"""
|
||||
@@ -1087,7 +1130,6 @@ class Client(object):
|
||||
|
||||
if 'ms' not in content: return
|
||||
|
||||
log.debug("Received {}".format(content["ms"]))
|
||||
for m in content["ms"]:
|
||||
mtype = m.get("type")
|
||||
try:
|
||||
@@ -1265,7 +1307,7 @@ class Client(object):
|
||||
:rtype: bool
|
||||
"""
|
||||
try:
|
||||
#if markAlive: self._ping(self.sticky)
|
||||
if markAlive: self._ping(self.sticky, self.pool)
|
||||
try:
|
||||
content = self._pullMessage(self.sticky, self.pool)
|
||||
if content: self._parseMessage(content)
|
||||
|
231
fbchat/graphql.py
Normal file
231
fbchat/graphql.py
Normal file
@@ -0,0 +1,231 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import json
|
||||
import re
|
||||
from .models import *
|
||||
from .utils import *
|
||||
|
||||
# Shameless copy from https://stackoverflow.com/a/8730674
|
||||
FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL
|
||||
WHITESPACE = re.compile(r'[ \t\n\r]*', FLAGS)
|
||||
|
||||
class ConcatJSONDecoder(json.JSONDecoder):
|
||||
def decode(self, s, _w=WHITESPACE.match):
|
||||
s_len = len(s)
|
||||
|
||||
objs = []
|
||||
end = 0
|
||||
while end != s_len:
|
||||
obj, end = self.raw_decode(s, idx=_w(s, end).end())
|
||||
end = _w(s, end).end()
|
||||
objs.append(obj)
|
||||
return objs
|
||||
# End shameless copy
|
||||
|
||||
def graphql_to_message(message):
|
||||
if message.get('message_sender') is None:
|
||||
message['message_sender'] = {}
|
||||
if message.get('message') is None:
|
||||
message['message'] = {}
|
||||
is_read = None
|
||||
if message.get('unread') is not None:
|
||||
is_read = not message['unread']
|
||||
return Message(
|
||||
message.get('message_id'),
|
||||
author=message.get('message_sender').get('id'),
|
||||
timestamp=message.get('timestamp_precise'),
|
||||
is_read=is_read,
|
||||
reactions=message.get('message_reactions'),
|
||||
text=message.get('message').get('text'),
|
||||
mentions=[Mention(m.get('entity', {}).get('id'), offset=m.get('offset'), length=m.get('length')) for m in message.get('message').get('ranges', [])],
|
||||
sticker=message.get('sticker'),
|
||||
attachments=message.get('blob_attachments')
|
||||
)
|
||||
|
||||
def graphql_to_user(user):
|
||||
if user.get('profile_picture') is None:
|
||||
user['profile_picture'] = {}
|
||||
return User(
|
||||
user['id'],
|
||||
url=user.get('url'),
|
||||
first_name=user.get('first_name'),
|
||||
last_name=user.get('last_name'),
|
||||
is_friend=user.get('is_viewer_friend'),
|
||||
gender=GENDERS[user.get('gender')],
|
||||
affinity=user.get('affinity'),
|
||||
photo=user['profile_picture'].get('uri'),
|
||||
name=user.get('name')
|
||||
)
|
||||
|
||||
def graphql_to_group(group):
|
||||
if group.get('image') is None:
|
||||
group['image'] = {}
|
||||
return Group(
|
||||
group['thread_key']['thread_fbid'],
|
||||
participants=[node['messaging_actor']['id'] for node in group['all_participants']['nodes']],
|
||||
photo=group['image'].get('uri'),
|
||||
name=group.get('name')
|
||||
)
|
||||
|
||||
def graphql_to_page(page):
|
||||
if page.get('profile_picture') is None:
|
||||
page['profile_picture'] = {}
|
||||
if page.get('city') is None:
|
||||
page['city'] = {}
|
||||
return Page(
|
||||
page['id'],
|
||||
url=page.get('url'),
|
||||
city=page.get('city').get('name'),
|
||||
category=page.get('category_type'),
|
||||
photo=page['profile_picture'].get('uri'),
|
||||
name=page.get('name')
|
||||
)
|
||||
|
||||
def graphql_queries_to_json(*queries):
|
||||
"""
|
||||
Queries should be a list of GraphQL objects
|
||||
"""
|
||||
rtn = {}
|
||||
for i, query in enumerate(queries):
|
||||
rtn['q{}'.format(i)] = query.value
|
||||
return json.dumps(rtn)
|
||||
|
||||
def graphql_response_to_json(content):
|
||||
j = json.loads(content, cls=ConcatJSONDecoder)
|
||||
|
||||
rtn = [None]*(len(j))
|
||||
for x in j:
|
||||
if 'error_results' in x:
|
||||
del rtn[-1]
|
||||
continue
|
||||
check_json(x)
|
||||
[(key, value)] = x.items()
|
||||
check_json(value)
|
||||
if 'response' in value:
|
||||
rtn[int(key[1:])] = value['response']
|
||||
else:
|
||||
rtn[int(key[1:])] = value['data']
|
||||
|
||||
log.debug(rtn)
|
||||
|
||||
return rtn
|
||||
|
||||
class GraphQL(object):
|
||||
def __init__(self, query=None, doc_id=None, params={}):
|
||||
if query is not None:
|
||||
self.value = {
|
||||
'priority': 0,
|
||||
'q': query,
|
||||
'query_params': params
|
||||
}
|
||||
elif doc_id is not None:
|
||||
self.value = {
|
||||
'doc_id': doc_id,
|
||||
'query_params': params
|
||||
}
|
||||
else:
|
||||
raise Exception('A query or doc_id must be specified')
|
||||
|
||||
|
||||
FRAGMENT_USER = """
|
||||
QueryFragment User: User {
|
||||
id,
|
||||
name,
|
||||
first_name,
|
||||
last_name,
|
||||
profile_picture.width(<pic_size>).height(<pic_size>) {
|
||||
uri
|
||||
},
|
||||
is_viewer_friend,
|
||||
url,
|
||||
gender,
|
||||
viewer_affinity
|
||||
}
|
||||
"""
|
||||
|
||||
FRAGMENT_GROUP = """
|
||||
QueryFragment Group: MessageThread {
|
||||
name,
|
||||
thread_key {
|
||||
thread_fbid
|
||||
},
|
||||
image {
|
||||
uri
|
||||
},
|
||||
is_group_thread,
|
||||
all_participants {
|
||||
nodes {
|
||||
messaging_actor {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
FRAGMENT_PAGE = """
|
||||
QueryFragment Page: Page {
|
||||
id,
|
||||
name,
|
||||
profile_picture.width(32).height(32) {
|
||||
uri
|
||||
},
|
||||
url,
|
||||
category_type,
|
||||
city {
|
||||
name
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
SEARCH_USER = """
|
||||
Query SearchUser(<search> = '', <limit> = 1) {
|
||||
entities_named(<search>) {
|
||||
search_results.of_type(user).first(<limit>) as users {
|
||||
nodes {
|
||||
@User
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""" + FRAGMENT_USER
|
||||
|
||||
SEARCH_GROUP = """
|
||||
Query SearchGroup(<search> = '', <limit> = 1, <pic_size> = 32) {
|
||||
viewer() {
|
||||
message_threads.with_thread_name(<search>).last(<limit>) as groups {
|
||||
nodes {
|
||||
@Group
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""" + FRAGMENT_GROUP
|
||||
|
||||
SEARCH_PAGE = """
|
||||
Query SearchPage(<search> = '', <limit> = 1) {
|
||||
entities_named(<search>) {
|
||||
search_results.of_type(page).first(<limit>) as pages {
|
||||
nodes {
|
||||
@Page
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""" + FRAGMENT_PAGE
|
||||
|
||||
SEARCH_THREAD = """
|
||||
Query SearchThread(<search> = '', <limit> = 1) {
|
||||
entities_named(<search>) {
|
||||
search_results.first(<limit>) as threads {
|
||||
nodes {
|
||||
__typename,
|
||||
@User,
|
||||
@Group,
|
||||
@Page
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""" + FRAGMENT_USER + FRAGMENT_GROUP + FRAGMENT_PAGE
|
109
fbchat/models.py
109
fbchat/models.py
@@ -3,19 +3,20 @@
|
||||
from __future__ import unicode_literals
|
||||
import enum
|
||||
|
||||
|
||||
class Thread(object):
|
||||
#: The unique identifier of the user. Can be used a `thread_id`. See :ref:`intro_threads` for more info
|
||||
id = None
|
||||
#: Specifies the type of thread. Uses ThreadType
|
||||
#: The unique identifier of the thread. Can be used a `thread_id`. See :ref:`intro_threads` for more info
|
||||
uid = str
|
||||
#: Specifies the type of thread. Can be used a `thread_type`. See :ref:`intro_threads` for more info
|
||||
type = None
|
||||
#: The thread's picture
|
||||
photo = None
|
||||
photo = str
|
||||
#: The name of the thread
|
||||
name = None
|
||||
name = str
|
||||
|
||||
def __init__(self, _type, _id, photo=None, name=None):
|
||||
def __init__(self, _type, uid, photo=None, name=None):
|
||||
"""Represents a Facebook thread"""
|
||||
self.id = str(_id)
|
||||
self.uid = str(uid)
|
||||
self.type = _type
|
||||
self.photo = photo
|
||||
self.name = name
|
||||
@@ -24,60 +25,112 @@ class Thread(object):
|
||||
return self.__unicode__()
|
||||
|
||||
def __unicode__(self):
|
||||
return '<{} {} ({})>'.format(self.type.name, self.name, self.id)
|
||||
return '<{} {} ({})>'.format(self.type.name, self.name, self.uid)
|
||||
|
||||
|
||||
class User(Thread):
|
||||
#: The profile url
|
||||
url = None
|
||||
url = str
|
||||
#: The users first name
|
||||
first_name = None
|
||||
first_name = str
|
||||
#: The users last name
|
||||
last_name = None
|
||||
last_name = str
|
||||
#: Whether the user and the client are friends
|
||||
is_friend = None
|
||||
is_friend = bool
|
||||
#: The user's gender
|
||||
gender = None
|
||||
gender = str
|
||||
#: From 0 to 1. How close the client is to the user
|
||||
affinity = float
|
||||
|
||||
def __init__(self, _id, url=None, first_name=None, last_name=None, is_friend=None, gender=None, **kwargs):
|
||||
def __init__(self, uid, url=None, first_name=None, last_name=None, is_friend=None, gender=None, affinity=None, **kwargs):
|
||||
"""Represents a Facebook user. Inherits `Thread`"""
|
||||
super(User, self).__init__(ThreadType.USER, _id, **kwargs)
|
||||
super(User, self).__init__(ThreadType.USER, uid, **kwargs)
|
||||
self.url = url
|
||||
self.first_name = first_name
|
||||
self.last_name = last_name
|
||||
self.is_friend = is_friend
|
||||
self.gender = gender
|
||||
self.affinity = affinity
|
||||
|
||||
|
||||
class Group(Thread):
|
||||
def __init__(self, _id, **kwargs):
|
||||
#: List of the group thread's participant user IDs
|
||||
participants = list
|
||||
|
||||
def __init__(self, uid, participants=[], **kwargs):
|
||||
"""Represents a Facebook group. Inherits `Thread`"""
|
||||
super(Group, self).__init__(ThreadType.GROUP, _id, **kwargs)
|
||||
super(Group, self).__init__(ThreadType.GROUP, uid, **kwargs)
|
||||
self.participants = participants
|
||||
|
||||
|
||||
class Page(Thread):
|
||||
#: The page's custom url
|
||||
url = None
|
||||
url = str
|
||||
#: The name of the page's location city
|
||||
city = None
|
||||
city = str
|
||||
#: Amount of likes the page has
|
||||
likees = None
|
||||
likes = int
|
||||
#: Some extra information about the page
|
||||
sub_text = None
|
||||
sub_title = str
|
||||
#: The page's category
|
||||
category = str
|
||||
|
||||
def __init__(self, _id, url=None, city=None, likees=None, sub_text=None, **kwargs):
|
||||
def __init__(self, uid, url=None, city=None, likes=None, sub_title=None, category=None, **kwargs):
|
||||
"""Represents a Facebook page. Inherits `Thread`"""
|
||||
super(Page, self).__init__(ThreadType.PAGE, _id, **kwargs)
|
||||
super(Page, self).__init__(ThreadType.PAGE, uid, **kwargs)
|
||||
self.url = url
|
||||
self.city = city
|
||||
self.likees = likees
|
||||
self.sub_text = sub_text
|
||||
self.likes = likes
|
||||
self.sub_title = sub_title
|
||||
self.category = category
|
||||
|
||||
|
||||
class Message(object):
|
||||
"""Represents a message. Currently just acts as a dict"""
|
||||
def __init__(self, **entries):
|
||||
self.__dict__.update(entries)
|
||||
#: The message ID
|
||||
uid = str
|
||||
#: ID of the sender
|
||||
author = int
|
||||
#: Timestamp of when the message was sent
|
||||
timestamp = str
|
||||
#: Whether the message is read
|
||||
is_read = bool
|
||||
#: A list of message reactions
|
||||
reactions = list
|
||||
#: The actual message
|
||||
text = str
|
||||
#: A list of :class:`Mention` objects
|
||||
mentions = list
|
||||
#: An ID of a sent sticker
|
||||
sticker = str
|
||||
#: A list of attachments
|
||||
attachments = list
|
||||
|
||||
def __init__(self, uid, author=None, timestamp=None, is_read=None, reactions=[], text=None, mentions=[], sticker=None, attachments=[]):
|
||||
"""Represents a Facebook message"""
|
||||
self.uid = uid
|
||||
self.author = author
|
||||
self.timestamp = timestamp
|
||||
self.is_read = is_read
|
||||
self.reactions = reactions
|
||||
self.text = text
|
||||
self.mentions = mentions
|
||||
self.sticker = sticker
|
||||
self.attachments = attachments
|
||||
|
||||
|
||||
class Mention(object):
|
||||
#: The user ID the mention is pointing at
|
||||
user_id = str
|
||||
#: The character where the mention starts
|
||||
offset = int
|
||||
#: The length of the mention
|
||||
length = int
|
||||
|
||||
def __init__(self, user_id, offset=0, length=10):
|
||||
"""Represents a @mention"""
|
||||
self.user_id = user_id
|
||||
self.offset = offset
|
||||
self.length = length
|
||||
|
||||
class Enum(enum.Enum):
|
||||
"""Used internally by fbchat to support enumerations"""
|
||||
|
@@ -32,7 +32,14 @@ USER_AGENTS = [
|
||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6"
|
||||
]
|
||||
|
||||
TYPES = {
|
||||
'Page': ThreadType.PAGE,
|
||||
'User': ThreadType.USER,
|
||||
'Group': ThreadType.GROUP
|
||||
}
|
||||
|
||||
GENDERS = {
|
||||
# For standard requests
|
||||
0: 'unknown',
|
||||
1: 'female_singular',
|
||||
2: 'male_singular',
|
||||
@@ -45,6 +52,21 @@ GENDERS = {
|
||||
9: 'male_plural',
|
||||
10: 'neuter_plural',
|
||||
11: 'unknown_plural',
|
||||
|
||||
# For graphql requests
|
||||
#'': 'unknown',
|
||||
'FEMALE': 'female_singular',
|
||||
'MALE': 'male_singular',
|
||||
#'': 'female_singular_guess',
|
||||
#'': 'male_singular_guess',
|
||||
#'': 'mixed',
|
||||
#'': 'neuter_singular',
|
||||
#'': 'unknown_singular',
|
||||
#'': 'female_plural',
|
||||
#'': 'male_plural',
|
||||
#'': 'neuter_plural',
|
||||
#'': 'unknown_plural',
|
||||
None: None
|
||||
}
|
||||
|
||||
class ReqUrl(object):
|
||||
@@ -61,7 +83,7 @@ class ReqUrl(object):
|
||||
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"
|
||||
PING = "https://0-edge-chat.facebook.com/active_ping"
|
||||
UPLOAD = "https://upload.facebook.com/ajax/mercury/upload.php"
|
||||
INFO = "https://www.facebook.com/chat/user_info/"
|
||||
CONNECT = "https://www.facebook.com/ajax/add_friend/action.php?dpr=1"
|
||||
@@ -75,6 +97,8 @@ class ReqUrl(object):
|
||||
THREAD_EMOJI = "https://www.facebook.com/messaging/save_thread_emoji/?source=thread_settings&dpr=1"
|
||||
MESSAGE_REACTION = "https://www.facebook.com/webgraphql/mutation"
|
||||
TYPING = "https://www.facebook.com/ajax/messaging/typ.php"
|
||||
GRAPHQL = "https://www.facebook.com/api/graphqlbatch/"
|
||||
|
||||
|
||||
facebookEncoding = 'UTF-8'
|
||||
|
||||
@@ -112,7 +136,7 @@ def str_base(number, base):
|
||||
def generateMessageID(client_id=None):
|
||||
k = now()
|
||||
l = int(random() * 4294967295)
|
||||
return "<%s:%s-%s@mail.projektitan.com>" % (k, l, client_id)
|
||||
return "<{}:{}-{}@mail.projektitan.com>".format(k, l, client_id)
|
||||
|
||||
def getSignatureID():
|
||||
return hex(int(random() * 2147483648))
|
||||
@@ -124,7 +148,17 @@ def generateOfflineThreadingID():
|
||||
msgs = format(ret, 'b') + string
|
||||
return str(int(msgs, 2))
|
||||
|
||||
def checkRequest(r, check_json=True):
|
||||
def check_json(j):
|
||||
if 'error' in j and j['error'] is not None:
|
||||
if 'errorDescription' in j:
|
||||
# 'errorDescription' is in the users own language!
|
||||
raise Exception('Error #{} when sending request: {}'.format(j['error'], j['errorDescription']))
|
||||
elif 'debug_info' in j['error']:
|
||||
raise Exception('Error #{} when sending request: {}'.format(j['error']['code'], repr(j['error']['debug_info'])))
|
||||
else:
|
||||
raise Exception('Error {} when sending request'.format(j['error']))
|
||||
|
||||
def checkRequest(r, do_json_check=True):
|
||||
if not r.ok:
|
||||
raise Exception('Error when sending request: Got {} response'.format(r.status_code))
|
||||
|
||||
@@ -133,11 +167,12 @@ def checkRequest(r, check_json=True):
|
||||
if content is None or len(content) == 0:
|
||||
raise Exception('Error when sending request: Got empty response')
|
||||
|
||||
if check_json:
|
||||
j = json.loads(strip_to_json(content))
|
||||
if 'error' in j:
|
||||
# 'errorDescription' is in the users own language!
|
||||
raise Exception('Error #{} when sending request: {}'.format(j['error'], j['errorDescription']))
|
||||
if do_json_check:
|
||||
try:
|
||||
j = json.loads(strip_to_json(content))
|
||||
except Exception as e:
|
||||
raise Exception('Error while parsing JSON: {}'.format(repr(content)))
|
||||
check_json(j)
|
||||
return j
|
||||
else:
|
||||
return r
|
||||
return content
|
||||
|
Reference in New Issue
Block a user