Compare commits
29 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
f8d3b571ba | ||
|
64b1e52d4c | ||
|
b650f7ee9a | ||
|
3443a233f4 | ||
|
160386be62 | ||
|
64bdde8f33 | ||
|
89a277c354 | ||
|
8238387c7d | ||
|
6c829581af | ||
|
d180650c1b | ||
|
772bf5518f | ||
|
153dc0bdad | ||
|
b4b8914448 | ||
|
2ea2c89b4a | ||
|
479ca59a6a | ||
|
343f987a78 | ||
|
bad9c7a4b9 | ||
|
576e0949e0 | ||
|
d807648d2b | ||
|
0ae213c240 | ||
|
08117e7a54 | ||
|
51c3226070 | ||
|
5396d19d7d | ||
|
11501e6899 | ||
|
4eb49b9119 | ||
|
4c2da22750 | ||
|
753b9cbae2 | ||
|
2c73cabe22 | ||
|
d6ca091b7b |
@@ -13,7 +13,7 @@ If you are looking for information on a specific function, class, or method, thi
|
|||||||
Client
|
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)
|
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)
|
.. 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
|
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
|
.. literalinclude:: ../examples/interract.py
|
||||||
|
|
||||||
|
@@ -8,7 +8,7 @@ FAQ
|
|||||||
Version X broke my installation
|
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
|
most of the things may be broken at any point in time
|
||||||
|
|
||||||
Downgrade to an earlier version of fbchat, run this command
|
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.
|
`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
|
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
|
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 import Client
|
||||||
from fbchat.models import *
|
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``.
|
: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.
|
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`,
|
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`
|
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.
|
`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::
|
Use :func:`Client.getSession` to retrieve the cookies::
|
||||||
|
|
||||||
session_cookies = client.getSession()
|
session_cookies = client.getSession()
|
||||||
|
@@ -8,8 +8,8 @@ client = Client('<email>', '<password>')
|
|||||||
# Fetches a list of all users you're currently chatting with, as `User` objects
|
# Fetches a list of all users you're currently chatting with, as `User` objects
|
||||||
users = client.fetchAllUsers()
|
users = client.fetchAllUsers()
|
||||||
|
|
||||||
print("users' IDs: {}".format(user.uid for user in users))
|
print("users' IDs: {}".format([user.uid for user in users]))
|
||||||
print("users' names: {}".format(user.name for user in users))
|
print("users' names: {}".format([user.name for user in users]))
|
||||||
|
|
||||||
|
|
||||||
# If we have a user id, we can use `fetchUserInfo` to fetch a `User` object
|
# If we have a user id, we can use `fetchUserInfo` to fetch a `User` object
|
||||||
@@ -18,7 +18,7 @@ user = client.fetchUserInfo('<user id>')['<user id>']
|
|||||||
users = client.fetchUserInfo('<1st user id>', '<2nd user id>', '<3rd user id>')
|
users = client.fetchUserInfo('<1st user id>', '<2nd user id>', '<3rd user id>')
|
||||||
|
|
||||||
print("user's name: {}".format(user.name))
|
print("user's name: {}".format(user.name))
|
||||||
print("users' names: {}".format(users[k].name for k in users))
|
print("users' names: {}".format([users[k].name for k in users]))
|
||||||
|
|
||||||
|
|
||||||
# `searchForUsers` searches for the user and gives us a list of the results,
|
# `searchForUsers` searches for the user and gives us a list of the results,
|
||||||
|
@@ -15,7 +15,7 @@ from __future__ import unicode_literals
|
|||||||
from .client import *
|
from .client import *
|
||||||
|
|
||||||
__title__ = 'fbchat'
|
__title__ = 'fbchat'
|
||||||
__version__ = '1.4.0'
|
__version__ = '1.4.2'
|
||||||
__description__ = 'Facebook Chat (Messenger) for Python'
|
__description__ = 'Facebook Chat (Messenger) for Python'
|
||||||
|
|
||||||
__copyright__ = 'Copyright 2015 - 2018 by Taehoon Kim'
|
__copyright__ = 'Copyright 2015 - 2018 by Taehoon Kim'
|
||||||
|
@@ -168,10 +168,12 @@ class Client(object):
|
|||||||
|
|
||||||
def graphql_requests(self, *queries):
|
def graphql_requests(self, *queries):
|
||||||
"""
|
"""
|
||||||
.. todo::
|
:param queries: Zero or more GraphQL objects
|
||||||
Documenting this
|
:type queries: GraphQL
|
||||||
|
|
||||||
:raises: FBchatException if request failed
|
:raises: FBchatException if request failed
|
||||||
|
:return: A tuple containing json graphql queries
|
||||||
|
:rtype: tuple
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return tuple(self._graphql({
|
return tuple(self._graphql({
|
||||||
@@ -238,22 +240,6 @@ class Client(object):
|
|||||||
self.payloadDefault['ttstamp'] = self.ttstamp
|
self.payloadDefault['ttstamp'] = self.ttstamp
|
||||||
self.payloadDefault['fb_dtsg'] = self.fb_dtsg
|
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):
|
def _login(self):
|
||||||
if not (self.email and self.password):
|
if not (self.email and self.password):
|
||||||
raise FBchatUserError("Email and password not found.")
|
raise FBchatUserError("Email and password not found.")
|
||||||
@@ -457,7 +443,8 @@ class Client(object):
|
|||||||
return given_thread_id, given_thread_type
|
return given_thread_id, given_thread_type
|
||||||
|
|
||||||
def setDefaultThread(self, thread_id, 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_id: User/Group ID to default to. See :ref:`intro_threads`
|
||||||
:param thread_type: See :ref:`intro_threads`
|
:param thread_type: See :ref:`intro_threads`
|
||||||
@@ -544,7 +531,6 @@ class Client(object):
|
|||||||
|
|
||||||
return [graphql_to_page(node) for node in j[name]['pages']['nodes']]
|
return [graphql_to_page(node) for node in j[name]['pages']['nodes']]
|
||||||
|
|
||||||
# TODO intergrate Rooms
|
|
||||||
def searchForGroups(self, name, limit=1):
|
def searchForGroups(self, name, limit=1):
|
||||||
"""
|
"""
|
||||||
Find and get group thread by its name
|
Find and get group thread by its name
|
||||||
@@ -585,7 +571,6 @@ class Client(object):
|
|||||||
elif node['__typename'] == 'Group':
|
elif node['__typename'] == 'Group':
|
||||||
# We don't handle Facebook "Groups"
|
# We don't handle Facebook "Groups"
|
||||||
pass
|
pass
|
||||||
# TODO Add Rooms
|
|
||||||
else:
|
else:
|
||||||
log.warning('Unknown __typename: {} in {}'.format(repr(node['__typename']), node))
|
log.warning('Unknown __typename: {} in {}'.format(repr(node['__typename']), node))
|
||||||
|
|
||||||
@@ -818,9 +803,6 @@ class Client(object):
|
|||||||
if entry.get('thread_type') == 'GROUP':
|
if entry.get('thread_type') == 'GROUP':
|
||||||
_id = entry['thread_key']['thread_fbid']
|
_id = entry['thread_key']['thread_fbid']
|
||||||
rtn[_id] = graphql_to_group(entry)
|
rtn[_id] = graphql_to_group(entry)
|
||||||
elif entry.get('thread_type') == 'ROOM':
|
|
||||||
_id = entry['thread_key']['thread_fbid']
|
|
||||||
rtn[_id] = graphql_to_room(entry)
|
|
||||||
elif entry.get('thread_type') == 'ONE_TO_ONE':
|
elif entry.get('thread_type') == 'ONE_TO_ONE':
|
||||||
_id = entry['thread_key']['other_user_id']
|
_id = entry['thread_key']['other_user_id']
|
||||||
if pages_and_users.get(_id) is None:
|
if pages_and_users.get(_id) is None:
|
||||||
@@ -855,14 +837,22 @@ class Client(object):
|
|||||||
'id': thread_id,
|
'id': thread_id,
|
||||||
'message_limit': limit,
|
'message_limit': limit,
|
||||||
'load_messages': True,
|
'load_messages': True,
|
||||||
'load_read_receipts': False,
|
'load_read_receipts': True,
|
||||||
'before': before
|
'before': before
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if j.get('message_thread') is None:
|
if j.get('message_thread') is None:
|
||||||
raise FBchatException('Could not fetch thread {}: {}'.format(thread_id, j))
|
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):
|
def fetchThreadList(self, offset=None, limit=20, thread_location=ThreadLocation.INBOX, before=None):
|
||||||
"""Get thread list of your facebook account
|
"""Get thread list of your facebook account
|
||||||
@@ -1720,7 +1710,7 @@ class Client(object):
|
|||||||
}
|
}
|
||||||
|
|
||||||
for thread_id in thread_ids:
|
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)
|
r = self._post(self.req_url.READ_STATUS, data)
|
||||||
return r.ok
|
return r.ok
|
||||||
@@ -1966,47 +1956,32 @@ class Client(object):
|
|||||||
LISTEN METHODS
|
LISTEN METHODS
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _ping(self, sticky, pool):
|
def _ping(self):
|
||||||
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.uid,
|
'uid': self.uid,
|
||||||
'sticky_token': sticky,
|
'sticky_token': self.sticky,
|
||||||
'sticky_pool': pool,
|
'sticky_pool': self.pool,
|
||||||
'viewer_uid': self.uid,
|
'viewer_uid': self.uid,
|
||||||
'state': 'active'
|
'state': 'active',
|
||||||
}
|
}
|
||||||
self._get(self.req_url.PING, data, fix_request=True, as_json=False)
|
self._get(self.req_url.PING, data, fix_request=True, as_json=False)
|
||||||
|
|
||||||
def _fetchSticky(self):
|
def _pullMessage(self, markAlive=True):
|
||||||
"""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):
|
|
||||||
"""Call pull api with seq value to get message data."""
|
"""Call pull api with seq value to get message data."""
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"msgs_recv": 0,
|
"msgs_recv": 0,
|
||||||
"sticky_token": sticky,
|
"sticky_token": self.sticky,
|
||||||
"sticky_pool": pool,
|
"sticky_pool": self.pool,
|
||||||
"clientid": self.client_id,
|
"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')
|
self.seq = j.get('seq', '0')
|
||||||
return j
|
return j
|
||||||
@@ -2014,6 +1989,14 @@ class Client(object):
|
|||||||
def _parseMessage(self, content):
|
def _parseMessage(self, content):
|
||||||
"""Get message and author name from content. May contain multiple messages in the 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
|
if 'ms' not in content: return
|
||||||
|
|
||||||
for m in content["ms"]:
|
for m in content["ms"]:
|
||||||
@@ -2359,7 +2342,6 @@ class Client(object):
|
|||||||
:raises: FBchatException if request failed
|
:raises: FBchatException if request failed
|
||||||
"""
|
"""
|
||||||
self.listening = True
|
self.listening = True
|
||||||
self.sticky, self.pool = self._fetchSticky()
|
|
||||||
|
|
||||||
def doOneListen(self, markAlive=True):
|
def doOneListen(self, markAlive=True):
|
||||||
"""
|
"""
|
||||||
@@ -2373,8 +2355,8 @@ class Client(object):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if markAlive:
|
if markAlive:
|
||||||
self._ping(self.sticky, self.pool)
|
self._ping()
|
||||||
content = self._pullMessage(self.sticky, self.pool)
|
content = self._pullMessage(markAlive)
|
||||||
if content:
|
if content:
|
||||||
self._parseMessage(content)
|
self._parseMessage(content)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
@@ -42,7 +42,7 @@ def get_customization_info(thread):
|
|||||||
'emoji': info.get('emoji'),
|
'emoji': info.get('emoji'),
|
||||||
'color': graphql_color_to_enum(info.get('outgoing_bubble_color'))
|
'color': graphql_color_to_enum(info.get('outgoing_bubble_color'))
|
||||||
}
|
}
|
||||||
if thread.get('thread_type') in ('GROUP', 'ROOM') or thread.get('is_group_thread') or thread.get('thread_key', {}).get('thread_fbid'):
|
if thread.get('thread_type') == 'GROUP' or thread.get('is_group_thread') or thread.get('thread_key', {}).get('thread_fbid'):
|
||||||
rtn['nicknames'] = {}
|
rtn['nicknames'] = {}
|
||||||
for k in info.get('participant_customizations', []):
|
for k in info.get('participant_customizations', []):
|
||||||
rtn['nicknames'][k['participant_id']] = k.get('nickname')
|
rtn['nicknames'][k['participant_id']] = k.get('nickname')
|
||||||
@@ -220,7 +220,9 @@ def graphql_to_user(user):
|
|||||||
if user.get('profile_picture') is None:
|
if user.get('profile_picture') is None:
|
||||||
user['profile_picture'] = {}
|
user['profile_picture'] = {}
|
||||||
c_info = get_customization_info(user)
|
c_info = get_customization_info(user)
|
||||||
plan = graphql_to_plan(user['event_reminders']['nodes'][0]) if user.get('event_reminders', dict()).get('nodes') else None
|
plan = None
|
||||||
|
if user.get('event_reminders'):
|
||||||
|
plan = graphql_to_plan(user['event_reminders']['nodes'][0]) if user['event_reminders'].get('nodes') else None
|
||||||
return User(
|
return User(
|
||||||
user['id'],
|
user['id'],
|
||||||
url=user.get('url'),
|
url=user.get('url'),
|
||||||
@@ -258,7 +260,9 @@ def graphql_to_thread(thread):
|
|||||||
else:
|
else:
|
||||||
last_name = user.get('name').split(first_name, 1).pop().strip()
|
last_name = user.get('name').split(first_name, 1).pop().strip()
|
||||||
|
|
||||||
plan = graphql_to_plan(thread['event_reminders']['nodes'][0]) if thread.get('event_reminders', dict()).get('nodes') else None
|
plan = None
|
||||||
|
if thread.get('event_reminders'):
|
||||||
|
plan = graphql_to_plan(thread['event_reminders']['nodes'][0]) if thread['event_reminders'].get('nodes') else None
|
||||||
|
|
||||||
return User(
|
return User(
|
||||||
user['id'],
|
user['id'],
|
||||||
@@ -288,13 +292,19 @@ def graphql_to_group(group):
|
|||||||
last_message_timestamp = None
|
last_message_timestamp = None
|
||||||
if 'last_message' in group:
|
if 'last_message' in group:
|
||||||
last_message_timestamp = group['last_message']['nodes'][0]['timestamp_precise']
|
last_message_timestamp = group['last_message']['nodes'][0]['timestamp_precise']
|
||||||
plan = graphql_to_plan(group['event_reminders']['nodes'][0]) if group.get('event_reminders', dict()).get('nodes') else None
|
plan = None
|
||||||
|
if group.get('event_reminders'):
|
||||||
|
plan = graphql_to_plan(group['event_reminders']['nodes'][0]) if group['event_reminders'].get('nodes') else None
|
||||||
return Group(
|
return Group(
|
||||||
group['thread_key']['thread_fbid'],
|
group['thread_key']['thread_fbid'],
|
||||||
participants=set([node['messaging_actor']['id'] for node in group['all_participants']['nodes']]),
|
participants=set([node['messaging_actor']['id'] for node in group['all_participants']['nodes']]),
|
||||||
nicknames=c_info.get('nicknames'),
|
nicknames=c_info.get('nicknames'),
|
||||||
color=c_info.get('color'),
|
color=c_info.get('color'),
|
||||||
emoji=c_info.get('emoji'),
|
emoji=c_info.get('emoji'),
|
||||||
|
admins = set([node.get('id') for node in group.get('thread_admins')]),
|
||||||
|
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'),
|
photo=group['image'].get('uri'),
|
||||||
name=group.get('name'),
|
name=group.get('name'),
|
||||||
message_count=group.get('messages_count'),
|
message_count=group.get('messages_count'),
|
||||||
@@ -302,34 +312,14 @@ def graphql_to_group(group):
|
|||||||
plan=plan,
|
plan=plan,
|
||||||
)
|
)
|
||||||
|
|
||||||
def graphql_to_room(room):
|
|
||||||
if room.get('image') is None:
|
|
||||||
room['image'] = {}
|
|
||||||
c_info = get_customization_info(room)
|
|
||||||
plan = graphql_to_plan(room['event_reminders']['nodes'][0]) if room.get('event_reminders', dict()).get('nodes') else None
|
|
||||||
return Room(
|
|
||||||
room['thread_key']['thread_fbid'],
|
|
||||||
participants=set([node['messaging_actor']['id'] for node in room['all_participants']['nodes']]),
|
|
||||||
nicknames=c_info.get('nicknames'),
|
|
||||||
color=c_info.get('color'),
|
|
||||||
emoji=c_info.get('emoji'),
|
|
||||||
photo=room['image'].get('uri'),
|
|
||||||
name=room.get('name'),
|
|
||||||
message_count=room.get('messages_count'),
|
|
||||||
admins = set([node.get('id') for node in room.get('thread_admins')]),
|
|
||||||
approval_mode = bool(room.get('approval_mode')),
|
|
||||||
approval_requests = set(node.get('id') for node in room['thread_queue_metadata'].get('approval_requests', {}).get('nodes')),
|
|
||||||
join_link = room['joinable_mode'].get('link'),
|
|
||||||
privacy_mode = bool(room.get('privacy_mode')),
|
|
||||||
plan=plan,
|
|
||||||
)
|
|
||||||
|
|
||||||
def graphql_to_page(page):
|
def graphql_to_page(page):
|
||||||
if page.get('profile_picture') is None:
|
if page.get('profile_picture') is None:
|
||||||
page['profile_picture'] = {}
|
page['profile_picture'] = {}
|
||||||
if page.get('city') is None:
|
if page.get('city') is None:
|
||||||
page['city'] = {}
|
page['city'] = {}
|
||||||
plan = graphql_to_plan(page['event_reminders']['nodes'][0]) if page.get('event_reminders', dict()).get('nodes') else None
|
plan = None
|
||||||
|
if page.get('event_reminders'):
|
||||||
|
plan = graphql_to_plan(page['event_reminders']['nodes'][0]) if page['event_reminders'].get('nodes') else None
|
||||||
return Page(
|
return Page(
|
||||||
page['id'],
|
page['id'],
|
||||||
url=page.get('url'),
|
url=page.get('url'),
|
||||||
@@ -433,6 +423,40 @@ class GraphQL(object):
|
|||||||
},
|
},
|
||||||
outgoing_bubble_color,
|
outgoing_bubble_color,
|
||||||
emoji
|
emoji
|
||||||
|
},
|
||||||
|
thread_admins {
|
||||||
|
id
|
||||||
|
},
|
||||||
|
group_approval_queue {
|
||||||
|
nodes {
|
||||||
|
requester {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
approval_mode,
|
||||||
|
joinable_mode {
|
||||||
|
mode,
|
||||||
|
link
|
||||||
|
},
|
||||||
|
event_reminders {
|
||||||
|
nodes {
|
||||||
|
id,
|
||||||
|
lightweight_event_creator {
|
||||||
|
id
|
||||||
|
},
|
||||||
|
time,
|
||||||
|
location_name,
|
||||||
|
event_title,
|
||||||
|
event_reminder_members {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
},
|
||||||
|
guest_list_state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
@@ -102,8 +102,16 @@ class Group(Thread):
|
|||||||
color = None
|
color = None
|
||||||
#: The groups's default emoji
|
#: The groups's default emoji
|
||||||
emoji = None
|
emoji = None
|
||||||
|
# Set containing user IDs of thread admins
|
||||||
|
admins = None
|
||||||
|
# True if users need approval to join
|
||||||
|
approval_mode = None
|
||||||
|
# Set containing user IDs requesting to join
|
||||||
|
approval_requests = None
|
||||||
|
# Link for joining group
|
||||||
|
join_link = None
|
||||||
|
|
||||||
def __init__(self, uid, participants=None, nicknames=None, color=None, emoji=None, **kwargs):
|
def __init__(self, uid, participants=None, nicknames=None, color=None, emoji=None, admins=None, approval_mode=None, approval_requests=None, join_link=None, privacy_mode=None, **kwargs):
|
||||||
"""Represents a Facebook group. Inherits `Thread`"""
|
"""Represents a Facebook group. Inherits `Thread`"""
|
||||||
super(Group, self).__init__(ThreadType.GROUP, uid, **kwargs)
|
super(Group, self).__init__(ThreadType.GROUP, uid, **kwargs)
|
||||||
if participants is None:
|
if participants is None:
|
||||||
@@ -114,24 +122,6 @@ class Group(Thread):
|
|||||||
self.nicknames = nicknames
|
self.nicknames = nicknames
|
||||||
self.color = color
|
self.color = color
|
||||||
self.emoji = emoji
|
self.emoji = emoji
|
||||||
|
|
||||||
|
|
||||||
class Room(Group):
|
|
||||||
# Set containing user IDs of thread admins
|
|
||||||
admins = None
|
|
||||||
# True if users need approval to join
|
|
||||||
approval_mode = None
|
|
||||||
# Set containing user IDs requesting to join
|
|
||||||
approval_requests = None
|
|
||||||
# Link for joining room
|
|
||||||
join_link = None
|
|
||||||
# True is room is not discoverable
|
|
||||||
privacy_mode = None
|
|
||||||
|
|
||||||
def __init__(self, uid, admins=None, approval_mode=None, approval_requests=None, join_link=None, privacy_mode=None, **kwargs):
|
|
||||||
"""Represents a Facebook room. Inherits `Group`"""
|
|
||||||
super(Room, self).__init__(uid, **kwargs)
|
|
||||||
self.type = ThreadType.ROOM
|
|
||||||
if admins is None:
|
if admins is None:
|
||||||
admins = set()
|
admins = set()
|
||||||
self.admins = admins
|
self.admins = admins
|
||||||
@@ -140,6 +130,16 @@ class Room(Group):
|
|||||||
approval_requests = set()
|
approval_requests = set()
|
||||||
self.approval_requests = approval_requests
|
self.approval_requests = approval_requests
|
||||||
self.join_link = join_link
|
self.join_link = join_link
|
||||||
|
|
||||||
|
|
||||||
|
class Room(Group):
|
||||||
|
# True is room is not discoverable
|
||||||
|
privacy_mode = None
|
||||||
|
|
||||||
|
def __init__(self, uid, privacy_mode=None, **kwargs):
|
||||||
|
"""Deprecated. Use :class:`Group` instead"""
|
||||||
|
super(Room, self).__init__(uid, **kwargs)
|
||||||
|
self.type = ThreadType.ROOM
|
||||||
self.privacy_mode = privacy_mode
|
self.privacy_mode = privacy_mode
|
||||||
|
|
||||||
|
|
||||||
@@ -180,6 +180,8 @@ class Message(object):
|
|||||||
timestamp = None
|
timestamp = None
|
||||||
#: Whether the message is read
|
#: Whether the message is read
|
||||||
is_read = None
|
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
|
#: A dict with user's IDs as keys, and their :class:`MessageReaction` as values
|
||||||
reactions = None
|
reactions = None
|
||||||
#: The actual message
|
#: The actual message
|
||||||
@@ -201,6 +203,7 @@ class Message(object):
|
|||||||
attachments = []
|
attachments = []
|
||||||
self.attachments = attachments
|
self.attachments = attachments
|
||||||
self.reactions = {}
|
self.reactions = {}
|
||||||
|
self.read_by = []
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self.__unicode__()
|
return self.__unicode__()
|
||||||
@@ -530,8 +533,8 @@ class ThreadType(Enum):
|
|||||||
"""Used to specify what type of Facebook thread is being used. See :ref:`intro_threads` for more info"""
|
"""Used to specify what type of Facebook thread is being used. See :ref:`intro_threads` for more info"""
|
||||||
USER = 1
|
USER = 1
|
||||||
GROUP = 2
|
GROUP = 2
|
||||||
|
ROOM = 2
|
||||||
PAGE = 3
|
PAGE = 3
|
||||||
ROOM = 4
|
|
||||||
|
|
||||||
class ThreadLocation(Enum):
|
class ThreadLocation(Enum):
|
||||||
"""Used to specify where a thread is located (inbox, pending, archived, other)."""
|
"""Used to specify where a thread is located (inbox, pending, archived, other)."""
|
||||||
|
@@ -20,7 +20,9 @@ def group(pytestconfig):
|
|||||||
return {"id": load_variable("group_id", pytestconfig.cache), "type": ThreadType.GROUP}
|
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):
|
def thread(request, user, group):
|
||||||
return {
|
return {
|
||||||
"user": user,
|
"user": user,
|
||||||
|
@@ -11,8 +11,14 @@ from time import time
|
|||||||
|
|
||||||
@pytest.fixture(scope="module", params=[
|
@pytest.fixture(scope="module", params=[
|
||||||
Plan(int(time()) + 100, random_hex()),
|
Plan(int(time()) + 100, random_hex()),
|
||||||
pytest.mark.xfail(Plan(int(time()), random_hex()), raises=FBchatFacebookError),
|
pytest.param(
|
||||||
pytest.mark.xfail(Plan(0, None)),
|
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):
|
def plan_data(request, client, user, thread, catch_event, compare):
|
||||||
with catch_event("onPlanCreated") as x:
|
with catch_event("onPlanCreated") as x:
|
||||||
|
@@ -26,7 +26,9 @@ from utils import random_hex, subset
|
|||||||
PollOption(random_hex()),
|
PollOption(random_hex()),
|
||||||
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):
|
def poll_data(request, client1, group, catch_event):
|
||||||
with catch_event("onPollCreated") as x:
|
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.param("🙃", marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
|
||||||
pytest.mark.xfail("not an emoji", raises=FBchatFacebookError)
|
pytest.param("not an emoji", marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
|
||||||
])
|
])
|
||||||
def test_change_emoji(client, catch_event, compare, emoji):
|
def test_change_emoji(client, catch_event, compare, emoji):
|
||||||
with catch_event("onEmojiChange") as x:
|
with catch_event("onEmojiChange") as x:
|
||||||
@@ -101,7 +101,7 @@ def test_change_image_remote(client1, group, catch_event):
|
|||||||
[
|
[
|
||||||
x
|
x
|
||||||
if x in [ThreadColor.MESSENGER_BLUE, ThreadColor.PUMPKIN]
|
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
|
for x in ThreadColor
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@@ -23,15 +23,15 @@ EMOJI_LIST = [
|
|||||||
("😆", EmojiSize.LARGE),
|
("😆", EmojiSize.LARGE),
|
||||||
# These fail in `catch_event` because the emoji is made into a sticker
|
# These fail in `catch_event` because the emoji is made into a sticker
|
||||||
# This should be fixed
|
# This should be fixed
|
||||||
pytest.mark.xfail((None, EmojiSize.SMALL)),
|
pytest.param(None, EmojiSize.SMALL, marks=[pytest.mark.xfail()]),
|
||||||
pytest.mark.xfail((None, EmojiSize.MEDIUM)),
|
pytest.param(None, EmojiSize.MEDIUM, marks=[pytest.mark.xfail()]),
|
||||||
pytest.mark.xfail((None, EmojiSize.LARGE)),
|
pytest.param(None, EmojiSize.LARGE, marks=[pytest.mark.xfail()]),
|
||||||
]
|
]
|
||||||
|
|
||||||
STICKER_LIST = [
|
STICKER_LIST = [
|
||||||
Sticker("767334476626295"),
|
Sticker("767334476626295"),
|
||||||
pytest.mark.xfail(Sticker("0"), raises=FBchatFacebookError),
|
pytest.param(Sticker("0"), marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
|
||||||
pytest.mark.xfail(Sticker(None), raises=FBchatFacebookError),
|
pytest.param(Sticker(None), marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
|
||||||
]
|
]
|
||||||
|
|
||||||
TEXT_LIST = [
|
TEXT_LIST = [
|
||||||
@@ -40,8 +40,8 @@ TEXT_LIST = [
|
|||||||
"\\\n\t%?&'\"",
|
"\\\n\t%?&'\"",
|
||||||
"ˁҭʚ¹Ʋջوװ՞ޱɣࠚԹБɑȑңКએ֭ʗыԈٌʼőԈ×௴nચϚࠖణٔє܅Ԇޑط",
|
"ˁҭʚ¹Ʋջوװ՞ޱɣࠚԹБɑȑңКએ֭ʗыԈٌʼőԈ×௴nચϚࠖణٔє܅Ԇޑط",
|
||||||
"a" * 20000, # Maximum amount of characters you can send
|
"a" * 20000, # Maximum amount of characters you can send
|
||||||
pytest.mark.xfail("a" * 20001, raises=FBchatFacebookError),
|
pytest.param("a" * 20001, marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
|
||||||
pytest.mark.xfail(None, raises=FBchatFacebookError),
|
pytest.param(None, marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user