Added GraphQL requests

This commit is contained in:
Mads Marquart
2017-06-22 22:38:15 +02:00
parent c81d7d2bfb
commit 11e59e023c
12 changed files with 564 additions and 202 deletions

View File

@@ -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
View 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

View File

@@ -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"""

View File

@@ -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