Added GraphQL requests
This commit is contained in:
@@ -2,8 +2,8 @@
|
|||||||
.. module:: fbchat
|
.. module:: fbchat
|
||||||
.. _faq:
|
.. _faq:
|
||||||
|
|
||||||
Frequently asked questions
|
FAQ
|
||||||
==========================
|
===
|
||||||
|
|
||||||
Version X broke my installation
|
Version X broke my installation
|
||||||
-------------------------------
|
-------------------------------
|
||||||
|
@@ -55,7 +55,7 @@ This is required for many of `fbchat`'s functions, since Facebook differetiates
|
|||||||
Searching for group chats and finding their ID is not yet possible with `fbchat`,
|
Searching for group chats and finding their ID is not yet possible with `fbchat`,
|
||||||
but searching for users is possible via. :func:`Client.searchForUsers`. See :ref:`intro_fetching`
|
but searching for users is possible via. :func:`Client.searchForUsers`. See :ref:`intro_fetching`
|
||||||
|
|
||||||
You can get your own user ID by using :any:`Client.id`
|
You can get your own user ID by using :any:`Client.uid`
|
||||||
|
|
||||||
Getting the ID of a group chat is fairly trivial though, since you only need to navigate to `<https://www.facebook.com/messages/>`_,
|
Getting the ID of a group chat is fairly trivial though, since you only need to navigate to `<https://www.facebook.com/messages/>`_,
|
||||||
click on the group you want to find the ID of, and then read the id from the address bar.
|
click on the group you want to find the ID of, and then read the id from the address bar.
|
||||||
@@ -108,7 +108,7 @@ like adding users to and removing users from a group chat, logically only works
|
|||||||
The simplest way of using `fbchat` is to send a message.
|
The simplest way of using `fbchat` is to send a message.
|
||||||
The following snippet will, as you've probably already figured out, send the message `test message` to your account::
|
The following snippet will, as you've probably already figured out, send the message `test message` to your account::
|
||||||
|
|
||||||
message_id = client.sendMessage('test message', thread_id=client.id, thread_type=ThreadType.USER)
|
message_id = client.sendMessage('test message', thread_id=client.uid, thread_type=ThreadType.USER)
|
||||||
|
|
||||||
You can see a full example showing all the possible thread interactions with `fbchat` by going to :ref:`examples`
|
You can see a full example showing all the possible thread interactions with `fbchat` by going to :ref:`examples`
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ The following snippet will search for users by their name, take the first (and m
|
|||||||
|
|
||||||
users = client.searchForUsers('<name of user>')
|
users = client.searchForUsers('<name of user>')
|
||||||
user = users[0]
|
user = users[0]
|
||||||
print("User's ID: {}".format(user.id))
|
print("User's ID: {}".format(user.uid))
|
||||||
print("User's name: {}".format(user.name))
|
print("User's name: {}".format(user.name))
|
||||||
print("User's profile picture url: {}".format(user.photo))
|
print("User's profile picture url: {}".format(user.photo))
|
||||||
print("User's main url: {}".format(user.url))
|
print("User's main url: {}".format(user.url))
|
||||||
|
@@ -5,7 +5,7 @@ pushd %~dp0
|
|||||||
REM Command file for Sphinx documentation
|
REM Command file for Sphinx documentation
|
||||||
|
|
||||||
if "%SPHINXBUILD%" == "" (
|
if "%SPHINXBUILD%" == "" (
|
||||||
set SPHINXBUILD=python3.6 -msphinx
|
set SPHINXBUILD=python -msphinx
|
||||||
)
|
)
|
||||||
set SOURCEDIR=.
|
set SOURCEDIR=.
|
||||||
set BUILDDIR=_build
|
set BUILDDIR=_build
|
||||||
|
@@ -11,13 +11,11 @@ This page will be periodically updated to show missing features and documentatio
|
|||||||
Missing Functionality
|
Missing Functionality
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
- Implement Client.searchForThread
|
|
||||||
- This will use the graphql request API
|
|
||||||
- Implement Client.searchForMessage
|
- Implement Client.searchForMessage
|
||||||
- This will use the graphql request API
|
- This will use the graphql request API
|
||||||
- Implement chatting with pages
|
- Implement chatting with pages properly
|
||||||
- This might require a new :class:`models.ThreadType`, something like ``ThreadType.PAGE``
|
- Write better FAQ
|
||||||
- Rework `Message` model, to make the whole process more streamlined
|
- Explain usage of graphql
|
||||||
|
|
||||||
|
|
||||||
Documentation
|
Documentation
|
||||||
|
@@ -5,8 +5,8 @@ from fbchat.models import *
|
|||||||
|
|
||||||
client = Client('<email>', '<password>')
|
client = Client('<email>', '<password>')
|
||||||
|
|
||||||
print('Own id: {}'.format(client.id))
|
print('Own id: {}'.format(client.uid))
|
||||||
|
|
||||||
client.sendMessage('Hi me!', thread_id=self.id, thread_type=ThreadType.USER)
|
client.sendMessage('Hi me!', thread_id=client.uid, thread_type=ThreadType.USER)
|
||||||
|
|
||||||
client.logout()
|
client.logout()
|
||||||
|
@@ -34,19 +34,19 @@ print("Is user client's friend: {}".format(user.is_friend))
|
|||||||
# Fetches a list of the 20 top threads you're currently chatting with
|
# Fetches a list of the 20 top threads you're currently chatting with
|
||||||
threads = client.fetchThreadList()
|
threads = client.fetchThreadList()
|
||||||
# Fetches the next 10 threads
|
# Fetches the next 10 threads
|
||||||
threads += client.fetchThreadList(offset=20, amount=10)
|
threads += client.fetchThreadList(offset=20, limit=10)
|
||||||
|
|
||||||
print("Thread's INFO: {}".format(threads))
|
print("Threads: {}".format(threads))
|
||||||
|
|
||||||
|
|
||||||
# Gets the last 10 messages sent to the thread
|
# Gets the last 10 messages sent to the thread
|
||||||
messages = client.fetchThreadMessages(offset=0, amount=10, thread_id='<thread id>', thread_type=ThreadType)
|
messages = client.fetchThreadMessages(thread_id='<thread id>', limit=10)
|
||||||
# Since the message come in reversed order, reverse them
|
# Since the message come in reversed order, reverse them
|
||||||
messages.reverse()
|
messages.reverse()
|
||||||
|
|
||||||
# Prints the content of all the messages
|
# Prints the content of all the messages
|
||||||
for message in messages:
|
for message in messages:
|
||||||
print(message.body)
|
print(message.text)
|
||||||
|
|
||||||
|
|
||||||
# Here should be an example of `getUnread`
|
# Here should be an example of `getUnread`
|
||||||
|
312
fbchat/client.py
312
fbchat/client.py
@@ -10,6 +10,7 @@ from bs4 import BeautifulSoup as bs
|
|||||||
from mimetypes import guess_type
|
from mimetypes import guess_type
|
||||||
from .utils import *
|
from .utils import *
|
||||||
from .models import *
|
from .models import *
|
||||||
|
from .graphql import *
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ class Client(object):
|
|||||||
|
|
||||||
listening = False
|
listening = False
|
||||||
"""Whether the client is listening. Used when creating an external event loop to determine when to stop listening"""
|
"""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.
|
The ID of the client.
|
||||||
Can be used as `thread_id`. See :ref:`intro_threads` for more info.
|
Can be used as `thread_id`. See :ref:`intro_threads` for more info.
|
||||||
@@ -53,7 +54,6 @@ class Client(object):
|
|||||||
self.client = 'mercury'
|
self.client = 'mercury'
|
||||||
self.default_thread_id = None
|
self.default_thread_id = None
|
||||||
self.default_thread_type = None
|
self.default_thread_type = None
|
||||||
self.threads = []
|
|
||||||
|
|
||||||
if not user_agent:
|
if not user_agent:
|
||||||
user_agent = choice(USER_AGENTS)
|
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')
|
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)
|
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
|
END INTERNAL REQUEST METHODS
|
||||||
"""
|
"""
|
||||||
@@ -117,18 +138,23 @@ class Client(object):
|
|||||||
LOGIN METHODS
|
LOGIN METHODS
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def _resetValues(self):
|
||||||
|
self.payloadDefault={}
|
||||||
|
self._session = requests.session()
|
||||||
|
self.req_counter = 1
|
||||||
|
self.seq = "0"
|
||||||
|
self.uid = None
|
||||||
|
|
||||||
def _postLogin(self):
|
def _postLogin(self):
|
||||||
self.payloadDefault = {}
|
self.payloadDefault = {}
|
||||||
self.client_id = hex(int(random()*2147483648))[2:]
|
self.client_id = hex(int(random()*2147483648))[2:]
|
||||||
self.start_time = now()
|
self.start_time = now()
|
||||||
self.id = str(self._session.cookies['c_user'])
|
self.uid = str(self._session.cookies['c_user'])
|
||||||
self.user_channel = "p_" + self.id
|
self.user_channel = "p_" + self.uid
|
||||||
self.ttstamp = ''
|
self.ttstamp = ''
|
||||||
|
|
||||||
r = self._get(ReqUrl.BASE)
|
r = self._get(ReqUrl.BASE)
|
||||||
soup = bs(r.text, "lxml")
|
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_dtsg = soup.find("input", {'name':'fb_dtsg'})['value']
|
||||||
self.fb_h = soup.find("input", {'name':'h'})['value']
|
self.fb_h = soup.find("input", {'name':'h'})['value']
|
||||||
for i in self.fb_dtsg:
|
for i in self.fb_dtsg:
|
||||||
@@ -136,7 +162,7 @@ class Client(object):
|
|||||||
self.ttstamp += '2'
|
self.ttstamp += '2'
|
||||||
# Set default payload
|
# Set default payload
|
||||||
self.payloadDefault['__rev'] = int(r.text.split('"client_revision":',1)[1].split(",",1)[0])
|
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['__a'] = '1'
|
||||||
self.payloadDefault['ttstamp'] = self.ttstamp
|
self.payloadDefault['ttstamp'] = self.ttstamp
|
||||||
self.payloadDefault['fb_dtsg'] = self.fb_dtsg
|
self.payloadDefault['fb_dtsg'] = self.fb_dtsg
|
||||||
@@ -145,8 +171,8 @@ class Client(object):
|
|||||||
'channel' : self.user_channel,
|
'channel' : self.user_channel,
|
||||||
'partition' : '-2',
|
'partition' : '-2',
|
||||||
'clientid' : self.client_id,
|
'clientid' : self.client_id,
|
||||||
'viewer_uid' : self.id,
|
'viewer_uid' : self.uid,
|
||||||
'uid' : self.id,
|
'uid' : self.uid,
|
||||||
'state' : 'active',
|
'state' : 'active',
|
||||||
'format' : 'json',
|
'format' : 'json',
|
||||||
'idle' : 0,
|
'idle' : 0,
|
||||||
@@ -267,9 +293,13 @@ class Client(object):
|
|||||||
if not session_cookies or 'c_user' not in session_cookies:
|
if not session_cookies or 'c_user' not in session_cookies:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
# Load cookies into current session
|
# Load cookies into current session
|
||||||
self._session.cookies = requests.cookies.merge_cookies(self._session.cookies, session_cookies)
|
self._session.cookies = requests.cookies.merge_cookies(self._session.cookies, session_cookies)
|
||||||
self._postLogin()
|
self._postLogin()
|
||||||
|
except Exception:
|
||||||
|
self._resetValues()
|
||||||
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def login(self, email, password, max_tries=5):
|
def login(self, email, password, max_tries=5):
|
||||||
@@ -320,12 +350,7 @@ class Client(object):
|
|||||||
|
|
||||||
r = self._get(ReqUrl.LOGOUT, data)
|
r = self._get(ReqUrl.LOGOUT, data)
|
||||||
|
|
||||||
# reset value
|
self._resetValues()
|
||||||
self.payloadDefault={}
|
|
||||||
self._session = requests.session()
|
|
||||||
self.req_counter = 1
|
|
||||||
self.seq = "0"
|
|
||||||
self.id = None
|
|
||||||
|
|
||||||
return r.ok
|
return r.ok
|
||||||
|
|
||||||
@@ -385,7 +410,7 @@ class Client(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'viewer': self.id,
|
'viewer': self.uid,
|
||||||
}
|
}
|
||||||
j = checkRequest(self._post(ReqUrl.ALL_USERS, query=data))
|
j = checkRequest(self._post(ReqUrl.ALL_USERS, query=data))
|
||||||
if not j['payload']:
|
if not j['payload']:
|
||||||
@@ -399,51 +424,82 @@ class Client(object):
|
|||||||
|
|
||||||
return users
|
return users
|
||||||
|
|
||||||
def _searchFor(self, name):
|
def searchForUsers(self, name, limit=1):
|
||||||
payload = {
|
"""
|
||||||
'value' : name.lower(),
|
Find and get user by his/her name
|
||||||
'viewer' : self.id,
|
|
||||||
'rsp' : 'search',
|
|
||||||
'context' : 'search',
|
|
||||||
'path' : '/home.php',
|
|
||||||
'request_id' : str(uuid1()),
|
|
||||||
}
|
|
||||||
|
|
||||||
j = checkRequest(self._get(ReqUrl.SEARCH, payload))
|
:param name: Name of the user
|
||||||
|
:param limit: The max. amount of users to fetch
|
||||||
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
|
|
||||||
:return: :class:`models.User` objects, ordered by relevance
|
:return: :class:`models.User` objects, ordered by relevance
|
||||||
:rtype: list
|
:rtype: list
|
||||||
:raises: Exception if request failed
|
:raises: Exception if request failed
|
||||||
"""
|
"""
|
||||||
|
|
||||||
entries = self._searchFor(name)
|
j = self.graphql_request(GraphQL(query=GraphQL.SEARCH_USER, params={'search': name, 'limit': limit}))
|
||||||
return [k for k in entries if k.type == ThreadType.USER]
|
|
||||||
|
|
||||||
def searchForPages(self, name):
|
return [graphql_to_user(node) for node in j[name]['users']['nodes']]
|
||||||
"""Find and get page by its name
|
|
||||||
|
|
||||||
: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
|
:return: :class:`models.Page` objects, ordered by relevance
|
||||||
:rtype: list
|
:rtype: list
|
||||||
:raises: Exception if request failed
|
:raises: Exception if request failed
|
||||||
"""
|
"""
|
||||||
|
|
||||||
entries = self._searchFor(name)
|
j = self.graphql_request(GraphQL(query=GraphQL.SEARCH_PAGE, params={'search': name, 'limit': limit}))
|
||||||
return [k for k in entries if k.type == ThreadType.PAGE]
|
|
||||||
|
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):
|
def _fetchInfo(self, *ids):
|
||||||
data = {
|
data = {
|
||||||
@@ -459,16 +515,19 @@ class Client(object):
|
|||||||
k = j['payload']['profiles'][_id]
|
k = j['payload']['profiles'][_id]
|
||||||
if k['type'] in ['user', 'friend']:
|
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'])
|
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':
|
elif k['type'] == 'page':
|
||||||
entries[_id] = Page(_id, url=k['uri'], city=None, likees=None, sub_text=None, photo=k['thumbSrc'], name=k['name'])
|
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
|
return entries
|
||||||
|
|
||||||
def fetchUserInfo(self, *user_ids):
|
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
|
: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
|
:rtype: dict
|
||||||
:raises: Exception if request failed
|
:raises: Exception if request failed
|
||||||
"""
|
"""
|
||||||
@@ -482,10 +541,11 @@ class Client(object):
|
|||||||
return users
|
return users
|
||||||
|
|
||||||
def fetchPageInfo(self, *page_ids):
|
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
|
:param page_ids: One or more page ID(s) to query
|
||||||
:return: :class:`models.Page` objects
|
:return: :class:`models.Page` objects, labeled by their ID
|
||||||
:rtype: dict
|
:rtype: dict
|
||||||
:raises: Exception if request failed
|
:raises: Exception if request failed
|
||||||
"""
|
"""
|
||||||
@@ -498,95 +558,79 @@ class Client(object):
|
|||||||
|
|
||||||
return users
|
return users
|
||||||
|
|
||||||
def fetchThreadMessages(self, offset=0, amount=20, thread_id=None, thread_type=ThreadType.USER):
|
def fetchThreadMessages(self, thread_id=None, limit=20, before=None):
|
||||||
"""Get the last messages in a thread
|
"""
|
||||||
|
Get the last messages in a thread
|
||||||
|
|
||||||
.. todo::
|
:param thread_id: User/Group ID to default to. See :ref:`intro_threads`
|
||||||
Fix this. Facebook broke it somehow. Also, clean up return values
|
:param limit: Max. number of messages to retrieve
|
||||||
|
:param before: A timestamp, indicating from which point to retrieve messages
|
||||||
:param offset: Where to start retrieving messages from
|
:type limit: int
|
||||||
:param amount: Number of messages to retrieve
|
:type before: int
|
||||||
:param thread_id: User/Group ID to retrieve from. See :ref:`intro_threads`
|
:return: :class:`models.Message` objects
|
||||||
:param thread_type: See :ref:`intro_threads`
|
|
||||||
:type offset: int
|
|
||||||
:type amount: int
|
|
||||||
:type thread_type: models.ThreadType
|
|
||||||
:return: Dictionaries, containing message data
|
|
||||||
:rtype: list
|
:rtype: list
|
||||||
:raises: Exception if request failed
|
: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:
|
if j['message_thread'] is None:
|
||||||
raise Exception('`amount` must be a positive integer, got {}'.format(amount))
|
raise Exception('Could not fetch thread {}'.format(thread_id))
|
||||||
|
|
||||||
if thread_type == ThreadType.USER:
|
return list(reversed([graphql_to_message(message) for message in j['message_thread']['messages']['nodes']]))
|
||||||
key = 'user_ids'
|
|
||||||
elif thread_type == ThreadType.GROUP:
|
|
||||||
key = 'thread_fbids'
|
|
||||||
|
|
||||||
data = {
|
def fetchThreadList(self, offset=0, limit=20):
|
||||||
'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):
|
|
||||||
"""Get thread list of your facebook account
|
"""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 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 offset: int
|
||||||
:type amount: int
|
:type limit: int
|
||||||
:return: Dictionaries, containing thread data
|
:return: :class:`models.Thread` objects
|
||||||
:rtype: list
|
:rtype: list
|
||||||
:raises: Exception if request failed
|
:raises: Exception if request failed
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if amount > 20 or amount < 1:
|
if limit > 20 or limit < 1:
|
||||||
raise Exception('`amount` should be between 1 and 20')
|
raise Exception('`limit` should be between 1 and 20')
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'client' : self.client,
|
'client' : self.client,
|
||||||
'inbox[offset]' : start,
|
'inbox[offset]' : offset,
|
||||||
'inbox[limit]' : amount,
|
'inbox[limit]' : limit,
|
||||||
}
|
}
|
||||||
|
|
||||||
j = checkRequest(self._post(ReqUrl.THREADS, data))
|
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 = {}
|
participants = {}
|
||||||
try:
|
for p in j['payload']['participants']:
|
||||||
for participant in j['payload']['participants']:
|
if p['type'] == 'page':
|
||||||
participants[participant["fbid"]] = participant["name"]
|
participants[p['fbid']] = Page(p['fbid'], url=p['href'], photo=p['image_src'], name=p['name'])
|
||||||
except Exception:
|
elif p['type'] == 'user':
|
||||||
log.exception('Exception while getting names for people in getThreadList. {}'.format(j))
|
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
|
entries = []
|
||||||
threadIDs = [getattr(x, "thread_id") for x in self.threads]
|
for k in j['payload']['threads']:
|
||||||
for thread in j['payload']['threads']:
|
if k['thread_type'] == 1:
|
||||||
if thread["thread_id"] not in threadIDs:
|
if k['other_user_fbid'] not in participants:
|
||||||
try:
|
raise Exception('A thread was not in participants: {}'.format(j['payload']))
|
||||||
thread["other_user_name"] = participants[int(thread["other_user_fbid"])]
|
entries.append(participants[k['other_user_fbid']])
|
||||||
except:
|
elif k['thread_type'] == 2:
|
||||||
thread["other_user_name"] = ""
|
entries.append(Group(k['thread_fbid'], participants=[p.strip('fbid:') for p in k['participants']], photo=k['image_src'], name=k['name']))
|
||||||
t = Thread(**thread)
|
else:
|
||||||
self.threads.append(t)
|
raise Exception('A thread had an unknown thread type: {}'.format(k))
|
||||||
|
|
||||||
return self.threads
|
return entries
|
||||||
|
|
||||||
def fetchUnread(self):
|
def fetchUnread(self):
|
||||||
"""
|
"""
|
||||||
@@ -624,7 +668,7 @@ class Client(object):
|
|||||||
date = datetime.now()
|
date = datetime.now()
|
||||||
data = {
|
data = {
|
||||||
'client': self.client,
|
'client': self.client,
|
||||||
'author' : 'fbid:' + str(self.id),
|
'author' : 'fbid:' + str(self.uid),
|
||||||
'timestamp' : timestamp,
|
'timestamp' : timestamp,
|
||||||
'timestamp_absolute' : 'Today',
|
'timestamp_absolute' : 'Today',
|
||||||
'timestamp_relative' : str(date.hour) + ":" + str(date.minute).zfill(2),
|
'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))
|
log.warning("Got multiple message ids' back: {}".format(message_ids))
|
||||||
message_id = message_ids[0]
|
message_id = message_ids[0]
|
||||||
except (KeyError, IndexError) as e:
|
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
|
return message_id
|
||||||
|
|
||||||
def sendMessage(self, message, thread_id=None, thread_type=ThreadType.USER):
|
def sendMessage(self, message, thread_id=None, thread_type=ThreadType.USER):
|
||||||
@@ -694,7 +735,7 @@ class Client(object):
|
|||||||
data['body'] = message or ''
|
data['body'] = message or ''
|
||||||
data['has_attachment'] = False
|
data['has_attachment'] = False
|
||||||
data['specific_to_list[0]'] = 'fbid:' + thread_id
|
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)
|
return self._doSendRequest(data)
|
||||||
|
|
||||||
@@ -716,7 +757,7 @@ class Client(object):
|
|||||||
data['action_type'] = 'ma-type:user-generated-message'
|
data['action_type'] = 'ma-type:user-generated-message'
|
||||||
data['has_attachment'] = False
|
data['has_attachment'] = False
|
||||||
data['specific_to_list[0]'] = 'fbid:' + thread_id
|
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:
|
if emoji:
|
||||||
data['body'] = emoji
|
data['body'] = emoji
|
||||||
@@ -758,7 +799,7 @@ class Client(object):
|
|||||||
data['body'] = message or ''
|
data['body'] = message or ''
|
||||||
data['has_attachment'] = True
|
data['has_attachment'] = True
|
||||||
data['specific_to_list[0]'] = 'fbid:' + str(thread_id)
|
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
|
data['image_ids[0]'] = image_id
|
||||||
|
|
||||||
@@ -822,7 +863,7 @@ class Client(object):
|
|||||||
user_ids = set(user_ids)
|
user_ids = set(user_ids)
|
||||||
|
|
||||||
for i, user_id in enumerate(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')
|
raise Exception('Error when adding users: Cannot add self to group thread')
|
||||||
else:
|
else:
|
||||||
data['log_message_data[added_participants][' + str(i) + ']'] = "fbid:" + str(user_id)
|
data['log_message_data[added_participants][' + str(i) + ']'] = "fbid:" + str(user_id)
|
||||||
@@ -944,7 +985,7 @@ class Client(object):
|
|||||||
"data": {
|
"data": {
|
||||||
"action": "ADD_REACTION",
|
"action": "ADD_REACTION",
|
||||||
"client_mutation_id": "1",
|
"client_mutation_id": "1",
|
||||||
"actor_id": self.id,
|
"actor_id": self.uid,
|
||||||
"message_id": str(message_id),
|
"message_id": str(message_id),
|
||||||
"reaction": reaction.value
|
"reaction": reaction.value
|
||||||
}
|
}
|
||||||
@@ -1039,17 +1080,19 @@ class Client(object):
|
|||||||
LISTEN METHODS
|
LISTEN METHODS
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _ping(self, sticky):
|
def _ping(self, sticky, pool):
|
||||||
data = {
|
data = {
|
||||||
'channel': self.user_channel,
|
'channel': self.user_channel,
|
||||||
'clientid': self.client_id,
|
'clientid': self.client_id,
|
||||||
'partition': -2,
|
'partition': -2,
|
||||||
'cap': 0,
|
'cap': 0,
|
||||||
'uid': self.id,
|
'uid': self.uid,
|
||||||
'sticky': sticky,
|
'sticky_token': sticky,
|
||||||
'viewer_uid': self.id
|
'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):
|
def _fetchSticky(self):
|
||||||
"""Call pull api to get sticky and pool parameter, newer api needs these parameters to work"""
|
"""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
|
if 'ms' not in content: return
|
||||||
|
|
||||||
log.debug("Received {}".format(content["ms"]))
|
|
||||||
for m in content["ms"]:
|
for m in content["ms"]:
|
||||||
mtype = m.get("type")
|
mtype = m.get("type")
|
||||||
try:
|
try:
|
||||||
@@ -1265,7 +1307,7 @@ class Client(object):
|
|||||||
:rtype: bool
|
:rtype: bool
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
#if markAlive: self._ping(self.sticky)
|
if markAlive: self._ping(self.sticky, self.pool)
|
||||||
try:
|
try:
|
||||||
content = self._pullMessage(self.sticky, self.pool)
|
content = self._pullMessage(self.sticky, self.pool)
|
||||||
if content: self._parseMessage(content)
|
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
|
from __future__ import unicode_literals
|
||||||
import enum
|
import enum
|
||||||
|
|
||||||
|
|
||||||
class Thread(object):
|
class Thread(object):
|
||||||
#: The unique identifier of the user. Can be used a `thread_id`. See :ref:`intro_threads` for more info
|
#: The unique identifier of the thread. Can be used a `thread_id`. See :ref:`intro_threads` for more info
|
||||||
id = None
|
uid = str
|
||||||
#: Specifies the type of thread. Uses ThreadType
|
#: Specifies the type of thread. Can be used a `thread_type`. See :ref:`intro_threads` for more info
|
||||||
type = None
|
type = None
|
||||||
#: The thread's picture
|
#: The thread's picture
|
||||||
photo = None
|
photo = str
|
||||||
#: The name of the thread
|
#: 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"""
|
"""Represents a Facebook thread"""
|
||||||
self.id = str(_id)
|
self.uid = str(uid)
|
||||||
self.type = _type
|
self.type = _type
|
||||||
self.photo = photo
|
self.photo = photo
|
||||||
self.name = name
|
self.name = name
|
||||||
@@ -24,60 +25,112 @@ class Thread(object):
|
|||||||
return self.__unicode__()
|
return self.__unicode__()
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return '<{} {} ({})>'.format(self.type.name, self.name, self.id)
|
return '<{} {} ({})>'.format(self.type.name, self.name, self.uid)
|
||||||
|
|
||||||
|
|
||||||
class User(Thread):
|
class User(Thread):
|
||||||
#: The profile url
|
#: The profile url
|
||||||
url = None
|
url = str
|
||||||
#: The users first name
|
#: The users first name
|
||||||
first_name = None
|
first_name = str
|
||||||
#: The users last name
|
#: The users last name
|
||||||
last_name = None
|
last_name = str
|
||||||
#: Whether the user and the client are friends
|
#: Whether the user and the client are friends
|
||||||
is_friend = None
|
is_friend = bool
|
||||||
#: The user's gender
|
#: 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`"""
|
"""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.url = url
|
||||||
self.first_name = first_name
|
self.first_name = first_name
|
||||||
self.last_name = last_name
|
self.last_name = last_name
|
||||||
self.is_friend = is_friend
|
self.is_friend = is_friend
|
||||||
self.gender = gender
|
self.gender = gender
|
||||||
|
self.affinity = affinity
|
||||||
|
|
||||||
|
|
||||||
class Group(Thread):
|
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`"""
|
"""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):
|
class Page(Thread):
|
||||||
#: The page's custom url
|
#: The page's custom url
|
||||||
url = None
|
url = str
|
||||||
#: The name of the page's location city
|
#: The name of the page's location city
|
||||||
city = None
|
city = str
|
||||||
#: Amount of likes the page has
|
#: Amount of likes the page has
|
||||||
likees = None
|
likes = int
|
||||||
#: Some extra information about the page
|
#: 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`"""
|
"""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.url = url
|
||||||
self.city = city
|
self.city = city
|
||||||
self.likees = likees
|
self.likes = likes
|
||||||
self.sub_text = sub_text
|
self.sub_title = sub_title
|
||||||
|
self.category = category
|
||||||
|
|
||||||
|
|
||||||
class Message(object):
|
class Message(object):
|
||||||
"""Represents a message. Currently just acts as a dict"""
|
#: The message ID
|
||||||
def __init__(self, **entries):
|
uid = str
|
||||||
self.__dict__.update(entries)
|
#: 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):
|
class Enum(enum.Enum):
|
||||||
"""Used internally by fbchat to support enumerations"""
|
"""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"
|
"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 = {
|
GENDERS = {
|
||||||
|
# For standard requests
|
||||||
0: 'unknown',
|
0: 'unknown',
|
||||||
1: 'female_singular',
|
1: 'female_singular',
|
||||||
2: 'male_singular',
|
2: 'male_singular',
|
||||||
@@ -45,6 +52,21 @@ GENDERS = {
|
|||||||
9: 'male_plural',
|
9: 'male_plural',
|
||||||
10: 'neuter_plural',
|
10: 'neuter_plural',
|
||||||
11: 'unknown_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):
|
class ReqUrl(object):
|
||||||
@@ -61,7 +83,7 @@ class ReqUrl(object):
|
|||||||
BASE = "https://www.facebook.com"
|
BASE = "https://www.facebook.com"
|
||||||
MOBILE = "https://m.facebook.com/"
|
MOBILE = "https://m.facebook.com/"
|
||||||
STICKY = "https://0-edge-chat.facebook.com/pull"
|
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"
|
UPLOAD = "https://upload.facebook.com/ajax/mercury/upload.php"
|
||||||
INFO = "https://www.facebook.com/chat/user_info/"
|
INFO = "https://www.facebook.com/chat/user_info/"
|
||||||
CONNECT = "https://www.facebook.com/ajax/add_friend/action.php?dpr=1"
|
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"
|
THREAD_EMOJI = "https://www.facebook.com/messaging/save_thread_emoji/?source=thread_settings&dpr=1"
|
||||||
MESSAGE_REACTION = "https://www.facebook.com/webgraphql/mutation"
|
MESSAGE_REACTION = "https://www.facebook.com/webgraphql/mutation"
|
||||||
TYPING = "https://www.facebook.com/ajax/messaging/typ.php"
|
TYPING = "https://www.facebook.com/ajax/messaging/typ.php"
|
||||||
|
GRAPHQL = "https://www.facebook.com/api/graphqlbatch/"
|
||||||
|
|
||||||
|
|
||||||
facebookEncoding = 'UTF-8'
|
facebookEncoding = 'UTF-8'
|
||||||
|
|
||||||
@@ -112,7 +136,7 @@ def str_base(number, base):
|
|||||||
def generateMessageID(client_id=None):
|
def generateMessageID(client_id=None):
|
||||||
k = now()
|
k = now()
|
||||||
l = int(random() * 4294967295)
|
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():
|
def getSignatureID():
|
||||||
return hex(int(random() * 2147483648))
|
return hex(int(random() * 2147483648))
|
||||||
@@ -124,7 +148,17 @@ def generateOfflineThreadingID():
|
|||||||
msgs = format(ret, 'b') + string
|
msgs = format(ret, 'b') + string
|
||||||
return str(int(msgs, 2))
|
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:
|
if not r.ok:
|
||||||
raise Exception('Error when sending request: Got {} response'.format(r.status_code))
|
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:
|
if content is None or len(content) == 0:
|
||||||
raise Exception('Error when sending request: Got empty response')
|
raise Exception('Error when sending request: Got empty response')
|
||||||
|
|
||||||
if check_json:
|
if do_json_check:
|
||||||
|
try:
|
||||||
j = json.loads(strip_to_json(content))
|
j = json.loads(strip_to_json(content))
|
||||||
if 'error' in j:
|
except Exception as e:
|
||||||
# 'errorDescription' is in the users own language!
|
raise Exception('Error while parsing JSON: {}'.format(repr(content)))
|
||||||
raise Exception('Error #{} when sending request: {}'.format(j['error'], j['errorDescription']))
|
check_json(j)
|
||||||
return j
|
return j
|
||||||
else:
|
else:
|
||||||
return r
|
return content
|
||||||
|
21
tests.py
21
tests.py
@@ -86,7 +86,7 @@ class TestFbchat(unittest.TestCase):
|
|||||||
u = users[0]
|
u = users[0]
|
||||||
|
|
||||||
# Test if values are set correctly
|
# Test if values are set correctly
|
||||||
self.assertEqual(u.id, '4')
|
self.assertEqual(u.uid, '4')
|
||||||
self.assertEqual(u.type, ThreadType.USER)
|
self.assertEqual(u.type, ThreadType.USER)
|
||||||
self.assertEqual(u.photo[:4], 'http')
|
self.assertEqual(u.photo[:4], 'http')
|
||||||
self.assertEqual(u.url[:4], 'http')
|
self.assertEqual(u.url[:4], 'http')
|
||||||
@@ -117,18 +117,21 @@ class TestFbchat(unittest.TestCase):
|
|||||||
self.assertTrue(client.sendLocalImage(image_local_url, 'test_send_group_images_local★', user_id, ThreadType.USER))
|
self.assertTrue(client.sendLocalImage(image_local_url, 'test_send_group_images_local★', user_id, ThreadType.USER))
|
||||||
self.assertTrue(client.sendLocalImage(image_local_url, 'test_send_group_images_local★', group_id, ThreadType.GROUP))
|
self.assertTrue(client.sendLocalImage(image_local_url, 'test_send_group_images_local★', group_id, ThreadType.GROUP))
|
||||||
|
|
||||||
|
def test_fetchThreadList(self):
|
||||||
|
client.fetchThreadList(offset=0, limit=20)
|
||||||
|
|
||||||
def test_fetchThreadMessages(self):
|
def test_fetchThreadMessages(self):
|
||||||
client.sendMessage('test_user_getThreadInfo★', thread_id=user_id, thread_type=ThreadType.USER)
|
client.sendMessage('test_user_getThreadInfo★', thread_id=user_id, thread_type=ThreadType.USER)
|
||||||
|
|
||||||
info = client.fetchThreadMessages(offset=0, amount=2, thread_id=user_id, thread_type=ThreadType.USER)
|
messages = client.fetchThreadMessages(thread_id=user_id, limit=1)
|
||||||
self.assertEqual(info[0].author, 'fbid:' + client.uid)
|
self.assertEqual(messages[0].author, client.uid)
|
||||||
self.assertEqual(info[0].body, 'test_user_getThreadInfo★')
|
self.assertEqual(messages[0].text, 'test_user_getThreadInfo★')
|
||||||
|
|
||||||
client.sendMessage('test_group_getThreadInfo★', thread_id=group_id, thread_type=ThreadType.GROUP)
|
client.sendMessage('test_group_getThreadInfo★', thread_id=group_id, thread_type=ThreadType.GROUP)
|
||||||
|
|
||||||
info = client.fetchThreadMessages(offset=0, amount=2, thread_id=group_id, thread_type=ThreadType.GROUP)
|
messages = client.fetchThreadMessages(thread_id=group_id, limit=1)
|
||||||
self.assertEqual(info[0].author, 'fbid:' + client.uid)
|
self.assertEqual(messages[0].author, client.uid)
|
||||||
self.assertEqual(info[0].body, 'test_group_getThreadInfo★')
|
self.assertEqual(messages[0].text, 'test_group_getThreadInfo★')
|
||||||
|
|
||||||
def test_listen(self):
|
def test_listen(self):
|
||||||
client.startListening()
|
client.startListening()
|
||||||
@@ -150,9 +153,9 @@ class TestFbchat(unittest.TestCase):
|
|||||||
client.changeThreadTitle('test_changeThreadTitle★', thread_id=user_id, thread_type=ThreadType.USER)
|
client.changeThreadTitle('test_changeThreadTitle★', thread_id=user_id, thread_type=ThreadType.USER)
|
||||||
|
|
||||||
def test_changeNickname(self):
|
def test_changeNickname(self):
|
||||||
client.changeNickname('test_changeNicknameSelf★', client.id, thread_id=user_id, thread_type=ThreadType.USER)
|
client.changeNickname('test_changeNicknameSelf★', client.uid, thread_id=user_id, thread_type=ThreadType.USER)
|
||||||
client.changeNickname('test_changeNicknameOther★', user_id, thread_id=user_id, thread_type=ThreadType.USER)
|
client.changeNickname('test_changeNicknameOther★', user_id, thread_id=user_id, thread_type=ThreadType.USER)
|
||||||
client.changeNickname('test_changeNicknameSelf★', client.id, thread_id=group_id, thread_type=ThreadType.GROUP)
|
client.changeNickname('test_changeNicknameSelf★', client.uid, thread_id=group_id, thread_type=ThreadType.GROUP)
|
||||||
client.changeNickname('test_changeNicknameOther★', user_id, thread_id=group_id, thread_type=ThreadType.GROUP)
|
client.changeNickname('test_changeNicknameOther★', user_id, thread_id=group_id, thread_type=ThreadType.GROUP)
|
||||||
|
|
||||||
def test_changeThreadEmoji(self):
|
def test_changeThreadEmoji(self):
|
||||||
|
Reference in New Issue
Block a user