Merge branch 'master' into extensible_attachments
This commit is contained in:
@@ -13,7 +13,7 @@ If you are looking for information on a specific function, class, or method, thi
|
||||
Client
|
||||
------
|
||||
|
||||
This is the main class of `fbchat`, which contains all the methods you use to interract with Facebook.
|
||||
This is the main class of `fbchat`, which contains all the methods you use to interact with Facebook.
|
||||
You can extend this class, and overwrite the events, to provide custom event handling (mainly used while listening)
|
||||
|
||||
.. autoclass:: Client(email, password, user_agent=None, max_tries=5, session_cookies=None, logging_level=logging.INFO)
|
||||
|
@@ -18,7 +18,7 @@ This will show basic usage of `fbchat`
|
||||
Interacting with Threads
|
||||
------------------------
|
||||
|
||||
This will interract with the thread in every way `fbchat` supports
|
||||
This will interact with the thread in every way `fbchat` supports
|
||||
|
||||
.. literalinclude:: ../examples/interract.py
|
||||
|
||||
|
@@ -8,7 +8,7 @@ FAQ
|
||||
Version X broke my installation
|
||||
-------------------------------
|
||||
|
||||
We try to provide backwards compatability where possible, but since we're not part of Facebook,
|
||||
We try to provide backwards compatibility where possible, but since we're not part of Facebook,
|
||||
most of the things may be broken at any point in time
|
||||
|
||||
Downgrade to an earlier version of fbchat, run this command
|
||||
|
@@ -6,7 +6,7 @@ Introduction
|
||||
============
|
||||
|
||||
`fbchat` uses your email and password to communicate with the Facebook server.
|
||||
That means that you should always store your password in a seperate file, in case e.g. someone looks over your shoulder while you're writing code.
|
||||
That means that you should always store your password in a separate file, in case e.g. someone looks over your shoulder while you're writing code.
|
||||
You should also make sure that the file's access control is appropriately restrictive
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ Logging In
|
||||
----------
|
||||
|
||||
Simply create an instance of :class:`Client`. If you have two factor authentication enabled, type the code in the terminal prompt
|
||||
(If you want to supply the code in another fasion, overwrite :func:`Client.on2FACode`)::
|
||||
(If you want to supply the code in another fashion, overwrite :func:`Client.on2FACode`)::
|
||||
|
||||
from fbchat import Client
|
||||
from fbchat.models import *
|
||||
@@ -50,7 +50,7 @@ A thread can refer to two things: A Messenger group chat or a single Facebook us
|
||||
|
||||
:class:`models.ThreadType` is an enumerator with two values: ``USER`` and ``GROUP``.
|
||||
These will specify whether the thread is a single user chat or a group chat.
|
||||
This is required for many of `fbchat`'s functions, since Facebook differetiates between these two internally
|
||||
This is required for many of `fbchat`'s functions, since Facebook differentiates between these two internally
|
||||
|
||||
Searching for group chats and finding their ID can be done via. :func:`Client.searchForGroups`,
|
||||
and searching for users is possible via. :func:`Client.searchForUsers`. See :ref:`intro_fetching`
|
||||
@@ -141,7 +141,7 @@ Sessions
|
||||
--------
|
||||
|
||||
`fbchat` provides functions to retrieve and set the session cookies.
|
||||
This will enable you to store the session cookies in a seperate file, so that you don't have to login each time you start your script.
|
||||
This will enable you to store the session cookies in a separate file, so that you don't have to login each time you start your script.
|
||||
Use :func:`Client.getSession` to retrieve the cookies::
|
||||
|
||||
session_cookies = client.getSession()
|
||||
|
@@ -15,7 +15,7 @@ To use the tests, copy ``tests/data.json`` to ``tests/my_data.json`` or type the
|
||||
Please remember to test all supported python versions.
|
||||
If you've made any changes to the 2FA functionality, test it with a 2FA enabled account.
|
||||
|
||||
If you only want to execute specific tests, pass the function names in the commandline (not including the `test_` prefix). Example::
|
||||
If you only want to execute specific tests, pass the function names in the command line (not including the `test_` prefix). Example::
|
||||
|
||||
$ python tests.py sendMessage sessions sendEmoji
|
||||
|
||||
|
@@ -15,7 +15,7 @@ from __future__ import unicode_literals
|
||||
from .client import *
|
||||
|
||||
__title__ = 'fbchat'
|
||||
__version__ = '1.4.0'
|
||||
__version__ = '1.4.2'
|
||||
__description__ = 'Facebook Chat (Messenger) for Python'
|
||||
|
||||
__copyright__ = 'Copyright 2015 - 2018 by Taehoon Kim'
|
||||
|
@@ -168,10 +168,12 @@ class Client(object):
|
||||
|
||||
def graphql_requests(self, *queries):
|
||||
"""
|
||||
.. todo::
|
||||
Documenting this
|
||||
:param queries: Zero or more GraphQL objects
|
||||
:type queries: GraphQL
|
||||
|
||||
:raises: FBchatException if request failed
|
||||
:return: A tuple containing json graphql queries
|
||||
:rtype: tuple
|
||||
"""
|
||||
|
||||
return tuple(self._graphql({
|
||||
@@ -238,22 +240,6 @@ class Client(object):
|
||||
self.payloadDefault['ttstamp'] = self.ttstamp
|
||||
self.payloadDefault['fb_dtsg'] = self.fb_dtsg
|
||||
|
||||
self.form = {
|
||||
'channel' : self.user_channel,
|
||||
'partition' : '-2',
|
||||
'clientid' : self.client_id,
|
||||
'viewer_uid' : self.uid,
|
||||
'uid' : self.uid,
|
||||
'state' : 'active',
|
||||
'format' : 'json',
|
||||
'idle' : 0,
|
||||
'cap' : '8'
|
||||
}
|
||||
|
||||
self.prev = now()
|
||||
self.tmp_prev = now()
|
||||
self.last_sync = now()
|
||||
|
||||
def _login(self):
|
||||
if not (self.email and self.password):
|
||||
raise FBchatUserError("Email and password not found.")
|
||||
@@ -457,7 +443,8 @@ class Client(object):
|
||||
return given_thread_id, given_thread_type
|
||||
|
||||
def setDefaultThread(self, thread_id, thread_type):
|
||||
"""Sets default thread to send messages to
|
||||
"""
|
||||
Sets default thread to send messages to
|
||||
|
||||
:param thread_id: User/Group ID to default to. See :ref:`intro_threads`
|
||||
:param thread_type: See :ref:`intro_threads`
|
||||
@@ -515,7 +502,7 @@ class Client(object):
|
||||
|
||||
return users
|
||||
|
||||
def searchForUsers(self, name, limit=1):
|
||||
def searchForUsers(self, name, limit=10):
|
||||
"""
|
||||
Find and get user by his/her name
|
||||
|
||||
@@ -530,7 +517,7 @@ class Client(object):
|
||||
|
||||
return [graphql_to_user(node) for node in j[name]['users']['nodes']]
|
||||
|
||||
def searchForPages(self, name, limit=1):
|
||||
def searchForPages(self, name, limit=10):
|
||||
"""
|
||||
Find and get page by its name
|
||||
|
||||
@@ -544,7 +531,7 @@ class Client(object):
|
||||
|
||||
return [graphql_to_page(node) for node in j[name]['pages']['nodes']]
|
||||
|
||||
def searchForGroups(self, name, limit=1):
|
||||
def searchForGroups(self, name, limit=10):
|
||||
"""
|
||||
Find and get group thread by its name
|
||||
|
||||
@@ -559,7 +546,7 @@ class Client(object):
|
||||
|
||||
return [graphql_to_group(node) for node in j['viewer']['groups']['nodes']]
|
||||
|
||||
def searchForThreads(self, name, limit=1):
|
||||
def searchForThreads(self, name, limit=10):
|
||||
"""
|
||||
Find and get a thread by its name
|
||||
|
||||
@@ -850,14 +837,22 @@ class Client(object):
|
||||
'id': thread_id,
|
||||
'message_limit': limit,
|
||||
'load_messages': True,
|
||||
'load_read_receipts': False,
|
||||
'load_read_receipts': True,
|
||||
'before': before
|
||||
}))
|
||||
|
||||
if j.get('message_thread') is None:
|
||||
raise FBchatException('Could not fetch thread {}: {}'.format(thread_id, j))
|
||||
|
||||
return list(reversed([graphql_to_message(message) for message in j['message_thread']['messages']['nodes']]))
|
||||
messages = list(reversed([graphql_to_message(message) for message in j['message_thread']['messages']['nodes']]))
|
||||
read_receipts = j['message_thread']['read_receipts']['nodes']
|
||||
|
||||
for message in messages:
|
||||
for receipt in read_receipts:
|
||||
if int(receipt['watermark']) >= int(message.timestamp):
|
||||
message.read_by.append(receipt['actor']['id'])
|
||||
|
||||
return messages
|
||||
|
||||
def fetchThreadList(self, offset=None, limit=20, thread_location=ThreadLocation.INBOX, before=None):
|
||||
"""Get thread list of your facebook account
|
||||
@@ -1801,7 +1796,7 @@ class Client(object):
|
||||
}
|
||||
|
||||
for thread_id in thread_ids:
|
||||
data["ids[{}]".format(thread_id)] = read
|
||||
data["ids[{}]".format(thread_id)] = 'true' if read else 'false'
|
||||
|
||||
r = self._post(self.req_url.READ_STATUS, data)
|
||||
return r.ok
|
||||
@@ -2047,48 +2042,32 @@ class Client(object):
|
||||
LISTEN METHODS
|
||||
"""
|
||||
|
||||
def _ping(self, sticky, pool):
|
||||
def _ping(self):
|
||||
data = {
|
||||
'channel': self.user_channel,
|
||||
'clientid': self.client_id,
|
||||
'partition': -2,
|
||||
'cap': 0,
|
||||
'uid': self.uid,
|
||||
'sticky_token': sticky,
|
||||
'sticky_pool': pool,
|
||||
'sticky_token': self.sticky,
|
||||
'sticky_pool': self.pool,
|
||||
'viewer_uid': self.uid,
|
||||
'state': 'active',
|
||||
}
|
||||
self._get(self.req_url.PING, data, fix_request=True, as_json=False)
|
||||
|
||||
def _fetchSticky(self):
|
||||
"""Call pull api to get sticky and pool parameter, newer api needs these parameters to work"""
|
||||
|
||||
data = {
|
||||
"msgs_recv": 0,
|
||||
"channel": self.user_channel,
|
||||
"clientid": self.client_id
|
||||
}
|
||||
|
||||
j = self._get(self.req_url.STICKY, data, fix_request=True, as_json=True)
|
||||
|
||||
if j.get('lb_info') is None:
|
||||
raise FBchatException('Missing lb_info: {}'.format(j))
|
||||
|
||||
return j['lb_info']['sticky'], j['lb_info']['pool']
|
||||
|
||||
def _pullMessage(self, sticky, pool, markAlive=True):
|
||||
def _pullMessage(self, markAlive=True):
|
||||
"""Call pull api with seq value to get message data."""
|
||||
|
||||
data = {
|
||||
"msgs_recv": 0,
|
||||
"sticky_token": sticky,
|
||||
"sticky_pool": pool,
|
||||
"sticky_token": self.sticky,
|
||||
"sticky_pool": self.pool,
|
||||
"clientid": self.client_id,
|
||||
'state': 'active' if markAlive else 'offline',
|
||||
}
|
||||
|
||||
j = self._get(ReqUrl.STICKY, data, fix_request=True, as_json=True)
|
||||
j = self._get(self.req_url.STICKY, data, fix_request=True, as_json=True)
|
||||
|
||||
self.seq = j.get('seq', '0')
|
||||
return j
|
||||
@@ -2096,6 +2075,14 @@ class Client(object):
|
||||
def _parseMessage(self, content):
|
||||
"""Get message and author name from content. May contain multiple messages in the content."""
|
||||
|
||||
if 'lb_info' in content:
|
||||
self.sticky = content['lb_info']['sticky']
|
||||
self.pool = content['lb_info']['pool']
|
||||
|
||||
if 'batches' in content:
|
||||
for batch in content['batches']:
|
||||
self._parseMessage(batch)
|
||||
|
||||
if 'ms' not in content: return
|
||||
|
||||
for m in content["ms"]:
|
||||
@@ -2505,7 +2492,6 @@ class Client(object):
|
||||
:raises: FBchatException if request failed
|
||||
"""
|
||||
self.listening = True
|
||||
self.sticky, self.pool = self._fetchSticky()
|
||||
|
||||
def doOneListen(self, markAlive=True):
|
||||
"""
|
||||
@@ -2519,8 +2505,8 @@ class Client(object):
|
||||
"""
|
||||
try:
|
||||
if markAlive:
|
||||
self._ping(self.sticky, self.pool)
|
||||
content = self._pullMessage(self.sticky, self.pool, markAlive)
|
||||
self._ping()
|
||||
content = self._pullMessage(markAlive)
|
||||
if content:
|
||||
self._parseMessage(content)
|
||||
except KeyboardInterrupt:
|
||||
|
@@ -381,8 +381,8 @@ def graphql_to_group(group):
|
||||
color=c_info.get('color'),
|
||||
emoji=c_info.get('emoji'),
|
||||
admins = set([node.get('id') for node in group.get('thread_admins')]),
|
||||
approval_mode = bool(group.get('approval_mode')),
|
||||
approval_requests = set(node["requester"]['id'] for node in group['group_approval_queue']['nodes']),
|
||||
approval_mode = bool(group.get('approval_mode')) if group.get('approval_mode') is not None else None,
|
||||
approval_requests = set(node["requester"]['id'] for node in group['group_approval_queue']['nodes']) if group.get('group_approval_queue') else None,
|
||||
join_link = group['joinable_mode'].get('link'),
|
||||
photo=group['image'].get('uri'),
|
||||
name=group.get('name'),
|
||||
@@ -556,7 +556,7 @@ class GraphQL(object):
|
||||
"""
|
||||
|
||||
SEARCH_USER = """
|
||||
Query SearchUser(<search> = '', <limit> = 1) {
|
||||
Query SearchUser(<search> = '', <limit> = 10) {
|
||||
entities_named(<search>) {
|
||||
search_results.of_type(user).first(<limit>) as users {
|
||||
nodes {
|
||||
@@ -568,7 +568,7 @@ class GraphQL(object):
|
||||
""" + FRAGMENT_USER
|
||||
|
||||
SEARCH_GROUP = """
|
||||
Query SearchGroup(<search> = '', <limit> = 1, <pic_size> = 32) {
|
||||
Query SearchGroup(<search> = '', <limit> = 10, <pic_size> = 32) {
|
||||
viewer() {
|
||||
message_threads.with_thread_name(<search>).last(<limit>) as groups {
|
||||
nodes {
|
||||
@@ -580,7 +580,7 @@ class GraphQL(object):
|
||||
""" + FRAGMENT_GROUP
|
||||
|
||||
SEARCH_PAGE = """
|
||||
Query SearchPage(<search> = '', <limit> = 1) {
|
||||
Query SearchPage(<search> = '', <limit> = 10) {
|
||||
entities_named(<search>) {
|
||||
search_results.of_type(page).first(<limit>) as pages {
|
||||
nodes {
|
||||
@@ -592,7 +592,7 @@ class GraphQL(object):
|
||||
""" + FRAGMENT_PAGE
|
||||
|
||||
SEARCH_THREAD = """
|
||||
Query SearchThread(<search> = '', <limit> = 1) {
|
||||
Query SearchThread(<search> = '', <limit> = 10) {
|
||||
entities_named(<search>) {
|
||||
search_results.first(<limit>) as threads {
|
||||
nodes {
|
||||
|
@@ -180,6 +180,8 @@ class Message(object):
|
||||
timestamp = None
|
||||
#: Whether the message is read
|
||||
is_read = None
|
||||
#: A list of pepole IDs who read the message, works only with :func:`fbchat.Client.fetchThreadMessages`
|
||||
read_by = None
|
||||
#: A dict with user's IDs as keys, and their :class:`MessageReaction` as values
|
||||
reactions = None
|
||||
#: The actual message
|
||||
@@ -203,6 +205,7 @@ class Message(object):
|
||||
attachments = []
|
||||
self.attachments = attachments
|
||||
self.reactions = {}
|
||||
self.read_by = []
|
||||
self.deleted = False
|
||||
|
||||
def __repr__(self):
|
||||
|
@@ -20,7 +20,9 @@ def group(pytestconfig):
|
||||
return {"id": load_variable("group_id", pytestconfig.cache), "type": ThreadType.GROUP}
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", params=["user", "group", pytest.mark.xfail("none")])
|
||||
@pytest.fixture(scope="session", params=[
|
||||
"user", "group", pytest.param("none", marks=[pytest.mark.xfail()])
|
||||
])
|
||||
def thread(request, user, group):
|
||||
return {
|
||||
"user": user,
|
||||
|
@@ -11,8 +11,14 @@ from time import time
|
||||
|
||||
@pytest.fixture(scope="module", params=[
|
||||
Plan(int(time()) + 100, random_hex()),
|
||||
pytest.mark.xfail(Plan(int(time()), random_hex()), raises=FBchatFacebookError),
|
||||
pytest.mark.xfail(Plan(0, None)),
|
||||
pytest.param(
|
||||
Plan(int(time()), random_hex()),
|
||||
marks=[pytest.mark.xfail(raises=FBchatFacebookError)]
|
||||
),
|
||||
pytest.param(
|
||||
Plan(0, None),
|
||||
marks=[pytest.mark.xfail()],
|
||||
),
|
||||
])
|
||||
def plan_data(request, client, user, thread, catch_event, compare):
|
||||
with catch_event("onPlanCreated") as x:
|
||||
|
@@ -26,7 +26,9 @@ from utils import random_hex, subset
|
||||
PollOption(random_hex()),
|
||||
PollOption(random_hex()),
|
||||
]),
|
||||
pytest.mark.xfail(Poll(title=None, options=[]), raises=ValueError),
|
||||
pytest.param(
|
||||
Poll(title=None, options=[]), marks=[pytest.mark.xfail(raises=ValueError)]
|
||||
),
|
||||
])
|
||||
def poll_data(request, client1, group, catch_event):
|
||||
with catch_event("onPollCreated") as x:
|
||||
|
@@ -72,8 +72,8 @@ def test_change_nickname(client, client_all, catch_event, compare):
|
||||
"😂",
|
||||
"😕",
|
||||
"😍",
|
||||
pytest.mark.xfail("🙃", raises=FBchatFacebookError),
|
||||
pytest.mark.xfail("not an emoji", raises=FBchatFacebookError)
|
||||
pytest.param("🙃", marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
|
||||
pytest.param("not an emoji", marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
|
||||
])
|
||||
def test_change_emoji(client, catch_event, compare, emoji):
|
||||
with catch_event("onEmojiChange") as x:
|
||||
@@ -101,7 +101,7 @@ def test_change_image_remote(client1, group, catch_event):
|
||||
[
|
||||
x
|
||||
if x in [ThreadColor.MESSENGER_BLUE, ThreadColor.PUMPKIN]
|
||||
else pytest.mark.expensive(x)
|
||||
else pytest.param(x, marks=[pytest.mark.expensive()])
|
||||
for x in ThreadColor
|
||||
],
|
||||
)
|
||||
|
@@ -23,15 +23,15 @@ EMOJI_LIST = [
|
||||
("😆", EmojiSize.LARGE),
|
||||
# These fail in `catch_event` because the emoji is made into a sticker
|
||||
# This should be fixed
|
||||
pytest.mark.xfail((None, EmojiSize.SMALL)),
|
||||
pytest.mark.xfail((None, EmojiSize.MEDIUM)),
|
||||
pytest.mark.xfail((None, EmojiSize.LARGE)),
|
||||
pytest.param(None, EmojiSize.SMALL, marks=[pytest.mark.xfail()]),
|
||||
pytest.param(None, EmojiSize.MEDIUM, marks=[pytest.mark.xfail()]),
|
||||
pytest.param(None, EmojiSize.LARGE, marks=[pytest.mark.xfail()]),
|
||||
]
|
||||
|
||||
STICKER_LIST = [
|
||||
Sticker("767334476626295"),
|
||||
pytest.mark.xfail(Sticker("0"), raises=FBchatFacebookError),
|
||||
pytest.mark.xfail(Sticker(None), raises=FBchatFacebookError),
|
||||
pytest.param(Sticker("0"), marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
|
||||
pytest.param(Sticker(None), marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
|
||||
]
|
||||
|
||||
TEXT_LIST = [
|
||||
@@ -40,8 +40,8 @@ TEXT_LIST = [
|
||||
"\\\n\t%?&'\"",
|
||||
"ˁҭʚ¹Ʋջوװ՞ޱɣࠚԹБɑȑңКએ֭ʗыԈٌʼőԈ×௴nચϚࠖణٔє܅Ԇޑط",
|
||||
"a" * 20000, # Maximum amount of characters you can send
|
||||
pytest.mark.xfail("a" * 20001, raises=FBchatFacebookError),
|
||||
pytest.mark.xfail(None, raises=FBchatFacebookError),
|
||||
pytest.param("a" * 20001, marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
|
||||
pytest.param(None, marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
|
||||
]
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user