Compare commits

..

73 Commits

Author SHA1 Message Date
Mads Marquart
884af48270 Version up, thanks to @gave92
Properly fixed `markAsRead`, @gave92  reminded me that I forgot to change the `True` to `'true'` when removing `encode_params`
2018-03-21 10:05:07 +01:00
Mads Marquart
95f018fad3 Fixed example Echobot 2018-03-19 21:40:51 +01:00
Mads Marquart
b44758a195 Version up, thanks to @gave92
Fix `markAsRead` and `fetchUnread`; fixes #261
Added the `ssl_verify` instance variable, which allows disabling SSL varification for proxies
2018-03-19 21:28:48 +01:00
Mads Marquart
f1c20d490e Removed encode_params from PR, as discussed in #269 2018-03-19 21:15:23 +01:00
Mads Marquart
04372d498e Merge pull request #269 from gave92/FetchUnread
Fix `markAsRead` and `fetchUnread`; fixes #261
Added the `ssl_verify` instance variable, which allows disabling SSL varification for proxies
2018-03-19 21:08:45 +01:00
Marco Gavelli
63ea899605 fix for python3 2018-03-19 20:47:41 +01:00
Marco Gavelli
4fdd145d1e verify in _postFile 2018-03-19 16:52:22 +01:00
Marco Gavelli
57ee68b0e0 added documentation to markAsRead 2018-03-19 16:38:19 +01:00
Marco Gavelli
99c6884681 added documentation to fetchUnread 2018-03-19 16:29:26 +01:00
Marco Gavelli
1c1438e9bc fix for markAsRead, fetchUnread 2018-03-18 11:18:46 +01:00
Marco Gavelli
22f1b3e489 fix FetchUnread 2018-03-17 19:32:45 +01:00
Mads Marquart
fb1ad5800c Minor fix for searchFor. See comments on #266 2018-03-05 22:07:16 +01:00
Taehoon Kim
4dd15b05ef version up thanks to @2FWAH's PR #266 #267 2018-03-03 22:49:25 +09:00
Taehoon Kim
d7cdb644c4 Merge pull request #265 from 2FWAH/fix-fetchThreadList-archived
Fix ThreadLocation to work with new GraphQL and archived threads
2018-03-03 22:22:21 +09:00
Taehoon Kim
bfcf4950b3 Merge pull request #266 from 2FWAH/fill-last_message_timestamp-in-fetchThreadList
Add last_message_timestamp support
2018-03-03 22:21:49 +09:00
Taehoon Kim
6612c97f05 Merge pull request #267 from danijeljw/patch-1
duplicate lines removed from setup
2018-03-03 22:20:28 +09:00
Danijel-James Wynyard
b92cf62726 duplicate lines removed 2018-03-03 12:08:05 +11:00
2FWAH
a53ba33a81 Set offset to 'None' by default 2018-02-23 09:23:34 +01:00
2FWAH
c04d38cf63 Handle last_message_timestamp
Set last_message_timestamp for one to one and group conversations.
2018-02-22 19:53:56 +01:00
2FWAH
a051adcbc0 Fix ThreadLocation to work with new GraphQL 2018-02-22 17:49:26 +01:00
Mads Marquart
900a9cdf72 Version up, thanks to @gave92
`fetchThreadList` is updated with a GraphQL implementation. See #241
2018-02-18 22:40:13 +01:00
Mads Marquart
611b329934 Merge pull request #259 from gave92/fetchThreadListGraphQL
Added GraphQL alternative to fetchThreadList; fixes #241
2018-02-18 22:36:23 +01:00
Mads Marquart
2642788bc1 Merged fetchThreadListGraphQL into fetchThreadList 2018-02-18 22:32:12 +01:00
Marco Gavelli
8268445f0b Changed return type for ONE_TO_ONE to User 2018-02-18 22:49:47 +01:00
Marco Gavelli
c12dcd9263 Added GraphQL alternative to fetchThreadList; fixes #241 2018-02-17 14:29:31 +01:00
Mads Marquart
3142524809 Version up, thanks to @DeltaF1
`onFriendRequest` functionality is restored
2018-02-07 11:30:19 +01:00
Mads Marquart
4c9d3bd9d7 Merge pull request #255 from DeltaF1/master
Restored onFriendRequest functionality
2018-02-07 11:29:04 +01:00
DeltaF1
ba103066b8 Restored onFriendRequest functionality 2018-02-06 00:30:35 -05:00
Mads Marquart
0b0d6179a2 Version up, thanks to @sdnian
`fetchThreadMessages` and `listen` can now parse AudioAttachments
2018-01-30 17:20:47 +01:00
Mads Marquart
e8806d4ef8 Merge pull request #254 from sdnian/bransh1
modify AudioAttachment function
2018-01-30 17:15:55 +01:00
Steve Nian
c96e5f174c update 2018-01-30 20:22:18 +08:00
Steve Nian
315242e069 update 2018-01-30 20:17:09 +08:00
Steve Nian
a94fa5fbe3 AudioAttachment 2018-01-30 17:33:29 +08:00
Mads Marquart
90203afdd0 Fixes documentation error 2018-01-23 20:20:13 +01:00
Mads Marquart
2c0d098852 Fixes #240, small backwards-compatablitity issue when sending images 2018-01-08 21:55:11 +01:00
Mads Marquart
e4290cd465 Version up, thanks to @lobstr 2018-01-02 13:40:50 +01:00
Mads Marquart
46b85dec5c Merge remote-tracking branch 'lobstr/master' 2018-01-02 13:40:25 +01:00
Mads Marquart
bbc34bd009 Added onTyping method 2018-01-02 13:33:13 +01:00
cirrux
c495317e65 Fix setTypingStatus to send correctly 2018-01-01 23:11:35 -05:00
cirrux
a946050228 Re-enable typing notification 2017-12-31 12:27:55 -05:00
cirrux
83789dcefa Fix attachment parsing for newer structure 2017-12-26 19:12:10 -05:00
Mads Marquart
4f1f9bf1ce Fixed errors on unknown genders 2017-12-15 23:46:47 +01:00
Mads Marquart
32c72c2f35 Version up, thanks to @Dante383 2017-12-10 20:08:13 +01:00
Dante
42ae0035af typo in function name
checkRequest --> check_request
2017-12-10 14:16:17 +01:00
Mads Marquart
96e28fdbe6 Fixed error when recieving share attachments 2017-11-16 15:14:46 +01:00
Taehoon Kim
0f889f50cf Update README.rst 2017-11-14 17:25:01 +09:00
Mads Marquart
478eaebdec Removed copyright icon from README.rst, fixing #219 2017-10-21 18:47:12 +02:00
Mads Marquart
7ecf229db5 See commit description
- Deprecated `sendMessage` and `sendEmoji` in favor of `send`
- (Almost) Fully integrated attachment support
- Updated tests
- General cleanup
2017-10-21 17:59:44 +02:00
Mads Marquart
dda75c6099 Merge remote-tracking branch 'svaikstude/feature/@mention' 2017-10-21 12:10:58 +02:00
Mads Marquart
28d5ac9f90 Merge branch 'attachment-support' 2017-10-21 12:09:52 +02:00
Mads Marquart
52acfb4636 Removed json from main client.py 2017-10-21 11:58:57 +02:00
Mads Marquart
2a64bad385 Merge branch 'feature/eventReminder' 2017-10-21 11:49:37 +02:00
Mads Marquart
1a73699f1a Merge branch 'mutable-default' 2017-10-21 11:48:06 +02:00
Mads Marquart
1b5a7a0063 Merge branch 'rooms' 2017-10-21 11:46:28 +02:00
ekohilas
4b3eb440cf fixed missing get 2017-10-20 03:17:26 +11:00
ekohilas
d1f457866b fixed dict typo 2017-10-20 03:08:27 +11:00
ekohilas
6f29aa82cb fixed class mistype 2017-10-15 15:15:56 +11:00
ekohilas
b1a2ff7d84 updated for python2.7 2017-10-15 03:56:09 +11:00
ekohilas
883b16e251 fixed simple merge 2017-10-08 03:32:20 +11:00
ekohilas
116b39cf6a fixed superclass init error 2017-10-08 03:25:25 +11:00
ekohilas
eae1db9c7d removed list and rstrip 2017-10-08 03:25:25 +11:00
ekohilas
730bab5d40 added rooms under thread_info 2017-10-08 03:25:10 +11:00
ekohilas
d52dac233e made appropriate changes to default args of rooms 2017-10-08 03:22:15 +11:00
ekohilas
1f37277a8d started adding rooms 2017-10-08 03:22:15 +11:00
ekohilas
15014d7055 merging from upstream
Merge remote-tracking branch 'upstream/master'
2017-10-07 15:06:31 +11:00
ekohilas
7a35ca05b1 fixed all mutable default argument gotchas 2017-10-07 14:15:45 +11:00
svaikstude
be6b6909d9 Update client.py 2017-10-06 17:05:09 +02:00
Manvydas Kriauciunas
42c1d26b2e new feature to support @mention in sendMessage 2017-10-06 16:31:25 +02:00
Manvydas Kriauciunas
d38f8ad2ec new feature eventReminder added 2017-10-06 16:23:47 +02:00
Mads Marquart
34452f9220 Changed API description 2017-09-26 16:49:10 +02:00
Mads Marquart
24831b2462 Merge branch 'master' into attachment-support 2017-09-26 16:45:47 +02:00
Mads Marquart
1beb821b2c Added function to fetch url from image id
Fixes #84
2017-09-21 23:58:50 +02:00
Mads Marquart
a58791048a Added attachment and mention support in onMessage
Deprecated `message` in `onMessage`
2017-09-21 22:32:11 +02:00
14 changed files with 905 additions and 361 deletions

3
.gitignore vendored
View File

@@ -24,7 +24,8 @@ develop-eggs
# Sphinx documentation # Sphinx documentation
docs/_build/ docs/_build/
# Data for tests # Scripts and data for tests
my_tests.py
my_test_data.json my_test_data.json
my_data.json my_data.json
tests.data tests.data

View File

@@ -27,4 +27,8 @@ Installation:
$ pip install fbchat $ pip install fbchat
© Copyright 2015 - 2017 by Taehoon Kim / `@carpedm20 <http://carpedm20.github.io/about/>`__ Maintainer
----------
- Mads Marquart / `@madsmtm <https://github.com/madsmtm>`__
- Taehoon Kim / `@carpedm20 <http://carpedm20.github.io/about/>`__

View File

@@ -70,8 +70,8 @@ The same method can be applied to some user accounts, though if they've set a cu
Here's an snippet showing the usage of thread IDs and thread types, where ``<user id>`` and ``<group id>`` Here's an snippet showing the usage of thread IDs and thread types, where ``<user id>`` and ``<group id>``
corresponds to the ID of a single user, and the ID of a group respectively:: corresponds to the ID of a single user, and the ID of a group respectively::
client.sendMessage('<message>', thread_id='<user id>', thread_type=ThreadType.USER) client.send(Message(text='<message>'), thread_id='<user id>', thread_type=ThreadType.USER)
client.sendMessage('<message>', thread_id='<group id>', thread_type=ThreadType.GROUP) client.send(Message(text='<message>'), thread_id='<group id>', thread_type=ThreadType.GROUP)
Some functions (e.g. :func:`Client.changeThreadColor`) don't require a thread type, so in these cases you just provide the thread ID:: Some functions (e.g. :func:`Client.changeThreadColor`) don't require a thread type, so in these cases you just provide the thread ID::
@@ -91,7 +91,7 @@ Some of `fbchat`'s functions require these ID's, like :func:`Client.reactToMessa
and some of then provide this ID, like :func:`Client.sendMessage`. and some of then provide this ID, like :func:`Client.sendMessage`.
This snippet shows how to send a message, and then use the returned ID to react to that message with a 😍 emoji:: This snippet shows how to send a message, and then use the returned ID to react to that message with a 😍 emoji::
message_id = client.sendMessage('message', thread_id=thread_id, thread_type=thread_type) message_id = client.send(Message(text='message'), thread_id=thread_id, thread_type=thread_type)
client.reactToMessage(message_id, MessageReaction.LOVE) client.reactToMessage(message_id, MessageReaction.LOVE)
@@ -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.uid, thread_type=ThreadType.USER) message_id = client.send(Message(text='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`
@@ -175,8 +175,8 @@ meaning it will simply print information to the console when an event happens
The event actions can be changed by subclassing the :class:`Client`, and then overwriting the event methods:: The event actions can be changed by subclassing the :class:`Client`, and then overwriting the event methods::
class CustomClient(Client): class CustomClient(Client):
def onMessage(self, mid, author_id, message, thread_id, thread_type, ts, metadata, msg, **kwargs): def onMessage(self, mid, author_id, message_object, thread_id, thread_type, ts, metadata, msg, **kwargs):
# Do something with the message here # Do something with message_object here
pass pass
client = CustomClient('<email>', '<password>') client = CustomClient('<email>', '<password>')
@@ -184,13 +184,13 @@ The event actions can be changed by subclassing the :class:`Client`, and then ov
**Notice:** The following snippet is as equally valid as the previous one:: **Notice:** The following snippet is as equally valid as the previous one::
class CustomClient(Client): class CustomClient(Client):
def onMessage(self, message, author_id, thread_id, thread_type, **kwargs): def onMessage(self, message_object, author_id, thread_id, thread_type, **kwargs):
# Do something with the message here # Do something with message_object here
pass pass
client = CustomClient('<email>', '<password>') client = CustomClient('<email>', '<password>')
The change was in the parameters that our `onMessage` method took: ``message`` and ``author_id`` got swapped, The change was in the parameters that our `onMessage` method took: ``message_object`` and ``author_id`` got swapped,
and ``mid``, ``ts``, ``metadata`` and ``msg`` got removed, but the function still works, since we included ``**kwargs`` and ``mid``, ``ts``, ``metadata`` and ``msg`` got removed, but the function still works, since we included ``**kwargs``
.. note:: .. note::

View File

@@ -7,6 +7,6 @@ client = Client('<email>', '<password>')
print('Own id: {}'.format(client.uid)) print('Own id: {}'.format(client.uid))
client.sendMessage('Hi me!', thread_id=client.uid, thread_type=ThreadType.USER) client.send(Message(text='Hi me!'), thread_id=client.uid, thread_type=ThreadType.USER)
client.logout() client.logout()

View File

@@ -4,15 +4,15 @@ from fbchat import log, Client
# Subclass fbchat.Client and override required methods # Subclass fbchat.Client and override required methods
class EchoBot(Client): class EchoBot(Client):
def onMessage(self, author_id, message, thread_id, thread_type, **kwargs): def onMessage(self, author_id, message_object, thread_id, thread_type, **kwargs):
self.markAsDelivered(author_id, thread_id) self.markAsDelivered(thread_id, message_object.uid)
self.markAsRead(author_id) self.markAsRead(thread_id)
log.info("Message from {} in {} ({}): {}".format(author_id, thread_id, thread_type.name, message)) log.info("{} from {} in {}".format(message_object, thread_id, thread_type.name))
# If you're not the author, echo # If you're not the author, echo
if author_id != self.uid: if author_id != self.uid:
self.sendMessage(message, thread_id=thread_id, thread_type=thread_type) self.send(message_object, thread_id=thread_id, thread_type=thread_type)
client = EchoBot("<email>", "<password>") client = EchoBot("<email>", "<password>")
client.listen() client.listen()

View File

@@ -9,19 +9,25 @@ thread_id = '1234567890'
thread_type = ThreadType.GROUP thread_type = ThreadType.GROUP
# Will send a message to the thread # Will send a message to the thread
client.sendMessage('<message>', thread_id=thread_id, thread_type=thread_type) client.send(Message(text='<message>'), thread_id=thread_id, thread_type=thread_type)
# Will send the default `like` emoji # Will send the default `like` emoji
client.sendEmoji(emoji=None, size=EmojiSize.LARGE, thread_id=thread_id, thread_type=thread_type) client.send(Message(emoji_size=EmojiSize.LARGE), thread_id=thread_id, thread_type=thread_type)
# Will send the emoji `👍` # Will send the emoji `👍`
client.sendEmoji(emoji='👍', size=EmojiSize.LARGE, thread_id=thread_id, thread_type=thread_type) client.send(Message(text='👍', emoji_size=EmojiSize.LARGE), thread_id=thread_id, thread_type=thread_type)
# Will send the sticker with ID `767334476626295`
client.send(Message(sticker=Sticker('767334476626295')), thread_id=thread_id, thread_type=thread_type)
# Will send a message with a mention
client.send(Message(text='This is a @mention', mentions=[Mention(thread_id, offset=10, length=8)]), thread_id=thread_id, thread_type=thread_type)
# Will send the image located at `<image path>` # Will send the image located at `<image path>`
client.sendLocalImage('<image path>', message='This is a local image', thread_id=thread_id, thread_type=thread_type) client.sendLocalImage('<image path>', message=Message(text='This is a local image'), thread_id=thread_id, thread_type=thread_type)
# Will download the image at the url `<image url>`, and then send it # Will download the image at the url `<image url>`, and then send it
client.sendRemoteImage('<image url>', message='This is a remote image', thread_id=thread_id, thread_type=thread_type) client.sendRemoteImage('<image url>', message=Message(text='This is a remote image'), thread_id=thread_id, thread_type=thread_type)
# Only do these actions if the thread is a group # Only do these actions if the thread is a group

View File

@@ -4,14 +4,14 @@ from fbchat import log, Client
from fbchat.models import * from fbchat.models import *
class RemoveBot(Client): class RemoveBot(Client):
def onMessage(self, author_id, message, thread_id, thread_type, **kwargs): def onMessage(self, author_id, message_object, thread_id, thread_type, **kwargs):
# We can only kick people from group chats, so no need to try if it's a user chat # We can only kick people from group chats, so no need to try if it's a user chat
if message == 'Remove me!' and thread_type == ThreadType.GROUP: if message_object.text == 'Remove me!' and thread_type == ThreadType.GROUP:
log.info('{} will be removed from {}'.format(author_id, thread_id)) log.info('{} will be removed from {}'.format(author_id, thread_id))
self.removeUserFromGroup(author_id, thread_id=thread_id) self.removeUserFromGroup(author_id, thread_id=thread_id)
else: else:
# Sends the data to the inherited onMessage, so that we can still see when a message is recieved # Sends the data to the inherited onMessage, so that we can still see when a message is recieved
super(type(self), self).onMessage(author_id=author_id, message=message, thread_id=thread_id, thread_type=thread_type, **kwargs) super(RemoveBot, self).onMessage(author_id=author_id, message_object=message_object, thread_id=thread_id, thread_type=thread_type, **kwargs)
client = RemoveBot("<email>", "<password>") client = RemoveBot("<email>", "<password>")
client.listen() client.listen()

View File

@@ -17,7 +17,7 @@ from .client import *
__copyright__ = 'Copyright 2015 - {} by Taehoon Kim'.format(datetime.now().year) __copyright__ = 'Copyright 2015 - {} by Taehoon Kim'.format(datetime.now().year)
__version__ = '1.0.25' __version__ = '1.3.6'
__license__ = 'BSD' __license__ = 'BSD'
__author__ = 'Taehoon Kim; Moreels Pieter-Jan; Mads Marquart' __author__ = 'Taehoon Kim; Moreels Pieter-Jan; Mads Marquart'
__email__ = 'carpedm20@gmail.com' __email__ = 'carpedm20@gmail.com'

View File

@@ -5,7 +5,6 @@ import requests
import urllib import urllib
from uuid import uuid1 from uuid import uuid1
from random import choice from random import choice
from datetime import datetime
from bs4 import BeautifulSoup as bs from bs4 import BeautifulSoup as bs
from mimetypes import guess_type from mimetypes import guess_type
from .utils import * from .utils import *
@@ -21,6 +20,8 @@ class Client(object):
See https://fbchat.readthedocs.io for complete documentation of the API. See https://fbchat.readthedocs.io for complete documentation of the API.
""" """
ssl_verify = True
"""Verify ssl certificate, set to False to allow debugging with a proxy"""
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"""
uid = None uid = None
@@ -106,7 +107,7 @@ class Client(object):
def _get(self, url, query=None, timeout=30, fix_request=False, as_json=False, error_retries=3): def _get(self, url, query=None, timeout=30, fix_request=False, as_json=False, error_retries=3):
payload = self._generatePayload(query) payload = self._generatePayload(query)
r = self._session.get(url, headers=self._header, params=payload, timeout=timeout) r = self._session.get(url, headers=self._header, params=payload, timeout=timeout, verify=self.ssl_verify)
if not fix_request: if not fix_request:
return r return r
try: try:
@@ -118,7 +119,7 @@ class Client(object):
def _post(self, url, query=None, timeout=30, fix_request=False, as_json=False, error_retries=3): def _post(self, url, query=None, timeout=30, fix_request=False, as_json=False, error_retries=3):
payload = self._generatePayload(query) payload = self._generatePayload(query)
r = self._session.post(url, headers=self._header, data=payload, timeout=timeout) r = self._session.post(url, headers=self._header, data=payload, timeout=timeout, verify=self.ssl_verify)
if not fix_request: if not fix_request:
return r return r
try: try:
@@ -138,17 +139,17 @@ class Client(object):
raise e raise e
def _cleanGet(self, url, query=None, timeout=30): def _cleanGet(self, url, query=None, timeout=30):
return self._session.get(url, headers=self._header, params=query, timeout=timeout) return self._session.get(url, headers=self._header, params=query, timeout=timeout, verify=self.ssl_verify)
def _cleanPost(self, url, query=None, timeout=30): def _cleanPost(self, url, query=None, timeout=30):
self.req_counter += 1 self.req_counter += 1
return self._session.post(url, headers=self._header, data=query, timeout=timeout) return self._session.post(url, headers=self._header, data=query, timeout=timeout, verify=self.ssl_verify)
def _postFile(self, url, files=None, query=None, timeout=30, fix_request=False, as_json=False, error_retries=3): def _postFile(self, url, files=None, query=None, timeout=30, fix_request=False, as_json=False, error_retries=3):
payload=self._generatePayload(query) payload=self._generatePayload(query)
# Removes 'Content-Type' from the header # Removes 'Content-Type' from the header
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')
r = self._session.post(url, headers=headers, data=payload, timeout=timeout, files=files) r = self._session.post(url, headers=headers, data=payload, timeout=timeout, files=files, verify=self.ssl_verify)
if not fix_request: if not fix_request:
return r return r
try: try:
@@ -480,7 +481,7 @@ class Client(object):
if k['id'] in ['0', 0]: if k['id'] in ['0', 0]:
# Skip invalid users # Skip invalid users
pass pass
users.append(User(k['id'], first_name=k.get('firstName'), url=k.get('uri'), photo=k.get('thumbSrc'), name=k.get('name'), is_friend=k.get('is_friend'), gender=GENDERS[k.get('gender')])) users.append(User(k['id'], first_name=k.get('firstName'), url=k.get('uri'), photo=k.get('thumbSrc'), name=k.get('name'), is_friend=k.get('is_friend'), gender=GENDERS.get(k.get('gender'))))
return users return users
@@ -513,6 +514,7 @@ 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
@@ -553,6 +555,7 @@ 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))
@@ -707,6 +710,9 @@ 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:
@@ -725,7 +731,7 @@ class Client(object):
""" """
Get the last messages in a thread Get the last messages in a thread
:param thread_id: User/Group ID to default to. See :ref:`intro_threads` :param thread_id: User/Group ID to get messages from. See :ref:`intro_threads`
:param limit: Max. number of messages to retrieve :param limit: Max. number of messages to retrieve
:param before: A timestamp, indicating from which point to retrieve messages :param before: A timestamp, indicating from which point to retrieve messages
:type limit: int :type limit: int
@@ -735,6 +741,8 @@ class Client(object):
:raises: FBchatException if request failed :raises: FBchatException if request failed
""" """
thread_id, thread_type = self._getThread(thread_id, None)
j = self.graphql_request(GraphQL(doc_id='1386147188135407', params={ j = self.graphql_request(GraphQL(doc_id='1386147188135407', params={
'id': thread_id, 'id': thread_id,
'message_limit': limit, 'message_limit': limit,
@@ -748,19 +756,23 @@ class Client(object):
return list(reversed([graphql_to_message(message) for message in j['message_thread']['messages']['nodes']])) return list(reversed([graphql_to_message(message) for message in j['message_thread']['messages']['nodes']]))
def fetchThreadList(self, offset=0, limit=20, thread_location=ThreadLocation.INBOX): 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
:param offset: The offset, from where in the list to recieve threads from :param offset: Deprecated. Do not use!
:param limit: Max. number of threads to retrieve. Capped at 20 :param limit: Max. number of threads to retrieve. Capped at 20
:param thread_location: models.ThreadLocation: INBOX, PENDING, ARCHIVED or OTHER :param thread_location: models.ThreadLocation: INBOX, PENDING, ARCHIVED or OTHER
:type offset: int :param before: A timestamp (in milliseconds), indicating from which point to retrieve threads
:type limit: int :type limit: int
:type before: int
:return: :class:`models.Thread` objects :return: :class:`models.Thread` objects
:rtype: list :rtype: list
:raises: FBchatException if request failed :raises: FBchatException if request failed
""" """
if offset is not None:
log.warning('Using `offset` in `fetchThreadList` is no longer supported, since Facebook migrated to the use of GraphQL in this request. Use `before` instead')
if limit > 20 or limit < 1: if limit > 20 or limit < 1:
raise FBchatUserError('`limit` should be between 1 and 20') raise FBchatUserError('`limit` should be between 1 and 20')
@@ -769,61 +781,63 @@ class Client(object):
else: else:
raise FBchatUserError('"thread_location" must be a value of ThreadLocation') raise FBchatUserError('"thread_location" must be a value of ThreadLocation')
data = { j = self.graphql_request(GraphQL(doc_id='1349387578499440', params={
'client' : self.client, 'limit': limit,
loc_str + '[offset]' : offset, 'tags': [loc_str],
loc_str + '[limit]' : limit, 'before': before,
} 'includeDeliveryReceipts': True,
'includeSeqID': False
}))
j = self._post(self.req_url.THREADS, data, fix_request=True, as_json=True) return [graphql_to_thread(node) for node in j['viewer']['message_threads']['nodes']]
if j.get('payload') is None:
raise FBchatException('Missing payload: {}, with data: {}'.format(j, data))
participants = {}
if 'participants' in j['payload']:
for p in j['payload']['participants']:
if p['type'] == 'page':
participants[p['fbid']] = Page(p['fbid'], url=p['href'], photo=p['image_src'], name=p['name'])
elif p['type'] == 'user':
participants[p['fbid']] = User(p['fbid'], url=p['href'], first_name=p['short_name'], is_friend=p['is_friend'], gender=GENDERS[p['gender']], photo=p['image_src'], name=p['name'])
else:
raise FBchatException('A participant had an unknown type {}: {}'.format(p['type'], p))
entries = []
if 'threads' in j['payload']:
for k in j['payload']['threads']:
if k['thread_type'] == 1:
if k['other_user_fbid'] not in participants:
raise FBchatException('The thread {} was not in participants: {}'.format(k, j['payload']))
participants[k['other_user_fbid']].message_count = k['message_count']
entries.append(participants[k['other_user_fbid']])
elif k['thread_type'] == 2:
entries.append(Group(k['thread_fbid'], participants=set([p.strip('fbid:') for p in k['participants']]), photo=k['image_src'], name=k['name'], message_count=k['message_count']))
else:
raise FBchatException('A thread had an unknown thread type: {}'.format(k))
return entries
def fetchUnread(self): def fetchUnread(self):
""" """
.. todo:: Get the unread thread list
Documenting this
:return: List of unread thread ids
:rtype: list
:raises: FBchatException if request failed :raises: FBchatException if request failed
""" """
form = { form = {
'client': 'mercury_sync',
'folders[0]': 'inbox', 'folders[0]': 'inbox',
'client': 'mercury',
'last_action_timestamp': now() - 60*1000 'last_action_timestamp': now() - 60*1000
# 'last_action_timestamp': 0 # 'last_action_timestamp': 0
} }
j = self._post(self.req_url.THREAD_SYNC, form, fix_request=True, as_json=True) j = self._post(self.req_url.UNREAD_THREADS, form, fix_request=True, as_json=True)
return { return j['payload']['unread_thread_fbids'][0]['other_user_fbids']
"message_counts": j['payload']['message_counts'],
"unseen_threads": j['payload']['unseen_thread_ids'] def fetchUnseen(self):
} """
Get the unseen (new) thread list
:return: List of unseen thread ids
:rtype: list
:raises: FBchatException if request failed
"""
j = self._post(self.req_url.UNSEEN_THREADS, None, fix_request=True, as_json=True)
return j['payload']['unseen_thread_fbids'][0]['other_user_fbids']
def fetchImageUrl(self, image_id):
"""Fetches the url to the original image from an image attachment ID
:param image_id: The image you want to fethc
:type image_id: str
:return: An url where you can download the original image
:rtype: str
:raises: FBChatException if request failed
"""
image_id = str(image_id)
j = check_request(self._get(ReqUrl.ATTACHMENT_PHOTO, query={'photo_id': str(image_id)}))
url = get_jsmods_require(j, 3)
if url is None:
raise FBChatException('Could not fetch image url from: {}'.format(j))
return url
""" """
END FETCH METHODS END FETCH METHODS
@@ -833,45 +847,53 @@ class Client(object):
SEND METHODS SEND METHODS
""" """
def _getSendData(self, thread_id=None, thread_type=ThreadType.USER): def _oldMessage(self, message):
return message if isinstance(message, Message) else Message(text=message)
def _getSendData(self, message=None, thread_id=None, thread_type=ThreadType.USER):
"""Returns the data needed to send a request to `SendURL`""" """Returns the data needed to send a request to `SendURL`"""
messageAndOTID = generateOfflineThreadingID() messageAndOTID = generateOfflineThreadingID()
timestamp = now() timestamp = now()
date = datetime.now()
data = { data = {
'client': self.client, 'client': self.client,
'author' : 'fbid:' + str(self.uid), 'author' : 'fbid:' + str(self.uid),
'timestamp' : timestamp, 'timestamp' : timestamp,
'timestamp_absolute' : 'Today',
'timestamp_relative' : str(date.hour) + ":" + str(date.minute).zfill(2),
'timestamp_time_passed' : '0',
'is_unread' : False,
'is_cleared' : False,
'is_forward' : False,
'is_filtered_content' : False,
'is_filtered_content_bh': False,
'is_filtered_content_account': False,
'is_filtered_content_quasar': False,
'is_filtered_content_invalid_app': False,
'is_spoof_warning' : False,
'source' : 'source:chat:web', 'source' : 'source:chat:web',
'source_tags[0]' : 'source:chat',
'html_body' : False,
'ui_push_phase' : 'V3',
'status' : '0',
'offline_threading_id': messageAndOTID, 'offline_threading_id': messageAndOTID,
'message_id' : messageAndOTID, 'message_id' : messageAndOTID,
'threading_id': generateMessageID(self.client_id), 'threading_id': generateMessageID(self.client_id),
'ephemeral_ttl_mode:': '0', 'ephemeral_ttl_mode:': '0'
'manual_retry_cnt' : '0',
'signatureID' : getSignatureID()
} }
# Set recipient # Set recipient
if thread_type in [ThreadType.USER, ThreadType.PAGE]: if thread_type in [ThreadType.USER, ThreadType.PAGE]:
data["other_user_fbid"] = thread_id data['other_user_fbid'] = thread_id
elif thread_type == ThreadType.GROUP: elif thread_type == ThreadType.GROUP:
data["thread_fbid"] = thread_id data['thread_fbid'] = thread_id
if message is None:
message = Message()
if message.text or message.sticker or message.emoji_size:
data['action_type'] = 'ma-type:user-generated-message'
if message.text:
data['body'] = message.text
for i, mention in enumerate(message.mentions):
data['profile_xmd[{}][id]'.format(i)] = mention.thread_id
data['profile_xmd[{}][offset]'.format(i)] = mention.offset
data['profile_xmd[{}][length]'.format(i)] = mention.length
data['profile_xmd[{}][type]'.format(i)] = 'p'
if message.emoji_size:
if message.text:
data['tags[0]'] = 'hot_emoji_size:' + message.emoji_size.name.lower()
else:
data['sticker_id'] = message.emoji_size.value
if message.sticker:
data['sticker_id'] = message.sticker.uid
return data return data
@@ -888,63 +910,40 @@ class Client(object):
raise FBchatException('Error when sending message: No message IDs could be found: {}'.format(j)) raise FBchatException('Error when sending message: No message IDs could be found: {}'.format(j))
# update JS token if received in response # update JS token if received in response
if j.get('jsmods') is not None and j['jsmods'].get('require') is not None: fb_dtsg = get_jsmods_require(j, 2)
try: if fb_dtsg is not None:
self.payloadDefault['fb_dtsg'] = j['jsmods']['require'][0][3][0] self.payloadDefault['fb_dtsg'] = fb_dtsg
except (KeyError, IndexError) as e:
log.warning('Error when updating fb_dtsg. Facebook might have changed protocol!')
return message_id return message_id
def sendMessage(self, message, thread_id=None, thread_type=ThreadType.USER): def send(self, message, thread_id=None, thread_type=ThreadType.USER):
""" """
Sends a message to a thread Sends a message to a thread
:param message: Message to send :param message: Message to send
:param thread_id: User/Group ID to send to. See :ref:`intro_threads` :param thread_id: User/Group ID to send to. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_threads` :param thread_type: See :ref:`intro_threads`
:type message: models.Message
:type thread_type: models.ThreadType :type thread_type: models.ThreadType
:return: :ref:`Message ID <intro_message_ids>` of the sent message :return: :ref:`Message ID <intro_message_ids>` of the sent message
:raises: FBchatException if request failed :raises: FBchatException if request failed
""" """
thread_id, thread_type = self._getThread(thread_id, thread_type) thread_id, thread_type = self._getThread(thread_id, thread_type)
data = self._getSendData(thread_id, thread_type) data = self._getSendData(message=message, thread_id=thread_id, thread_type=thread_type)
data['action_type'] = 'ma-type:user-generated-message'
data['body'] = message or ''
data['has_attachment'] = False
data['specific_to_list[0]'] = 'fbid:' + thread_id
data['specific_to_list[1]'] = 'fbid:' + self.uid
return self._doSendRequest(data) return self._doSendRequest(data)
def sendMessage(self, message, thread_id=None, thread_type=ThreadType.USER):
"""
Deprecated. Use :func:`fbchat.Client.send` instead
"""
return self.send(Message(text=message), thread_id=thread_id, thread_type=thread_type)
def sendEmoji(self, emoji=None, size=EmojiSize.SMALL, thread_id=None, thread_type=ThreadType.USER): def sendEmoji(self, emoji=None, size=EmojiSize.SMALL, thread_id=None, thread_type=ThreadType.USER):
""" """
Sends an emoji to a thread Deprecated. Use :func:`fbchat.Client.send` instead
:param emoji: The chosen emoji to send. If not specified, the default `like` emoji is sent
:param size: If not specified, a small emoji is sent
:param thread_id: User/Group ID to send to. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_threads`
:type size: models.EmojiSize
:type thread_type: models.ThreadType
:return: :ref:`Message ID <intro_message_ids>` of the sent emoji
:raises: FBchatException if request failed
""" """
thread_id, thread_type = self._getThread(thread_id, thread_type) return self.send(Message(text=emoji, emoji_size=size), thread_id=thread_id, thread_type=thread_type)
data = self._getSendData(thread_id, thread_type)
data['action_type'] = 'ma-type:user-generated-message'
data['has_attachment'] = False
data['specific_to_list[0]'] = 'fbid:' + thread_id
data['specific_to_list[1]'] = 'fbid:' + self.uid
if emoji:
data['body'] = emoji
data['tags[0]'] = 'hot_emoji_size:' + size.name.lower()
else:
data["sticker_id"] = size.value
return self._doSendRequest(data)
def _uploadImage(self, image_path, data, mimetype): def _uploadImage(self, image_path, data, mimetype):
"""Upload an image and get the image_id for sending in a message""" """Upload an image and get the image_id for sending in a message"""
@@ -964,25 +963,13 @@ class Client(object):
def sendImage(self, image_id, message=None, thread_id=None, thread_type=ThreadType.USER, is_gif=False): def sendImage(self, image_id, message=None, thread_id=None, thread_type=ThreadType.USER, is_gif=False):
""" """
Sends an already uploaded image to a thread. (Used by :func:`Client.sendRemoteImage` and :func:`Client.sendLocalImage`) Deprecated. Use :func:`fbchat.Client.send` instead
:param image_id: ID of an image that's already uploaded to Facebook
:param message: Additional message
:param thread_id: User/Group ID to send to. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_threads`
:param is_gif: if sending GIF, True, else False
:type thread_type: models.ThreadType
:return: :ref:`Message ID <intro_message_ids>` of the sent image
:raises: FBchatException if request failed
""" """
thread_id, thread_type = self._getThread(thread_id, thread_type) thread_id, thread_type = self._getThread(thread_id, thread_type)
data = self._getSendData(thread_id, thread_type) data = self._getSendData(message=self._oldMessage(message), thread_id=thread_id, thread_type=thread_type)
data['action_type'] = 'ma-type:user-generated-message' data['action_type'] = 'ma-type:user-generated-message'
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[1]'] = 'fbid:' + str(self.uid)
if not is_gif: if not is_gif:
data['image_ids[0]'] = image_id data['image_ids[0]'] = image_id
@@ -1039,7 +1026,7 @@ class Client(object):
:raises: FBchatException if request failed :raises: FBchatException if request failed
""" """
thread_id, thread_type = self._getThread(thread_id, None) thread_id, thread_type = self._getThread(thread_id, None)
data = self._getSendData(thread_id, ThreadType.GROUP) data = self._getSendData(thread_id=thread_id, thread_type=ThreadType.GROUP)
data['action_type'] = 'ma-type:log-message' data['action_type'] = 'ma-type:log-message'
data['log_message_type'] = 'log:subscribe' data['log_message_type'] = 'log:subscribe'
@@ -1094,7 +1081,7 @@ class Client(object):
# The thread is a user, so we change the user's nickname # The thread is a user, so we change the user's nickname
return self.changeNickname(title, thread_id, thread_id=thread_id, thread_type=thread_type) return self.changeNickname(title, thread_id, thread_id=thread_id, thread_type=thread_type)
else: else:
data = self._getSendData(thread_id, thread_type) data = self._getSendData(thread_id=thread_id, thread_type=thread_type)
data['action_type'] = 'ma-type:log-message' data['action_type'] = 'ma-type:log-message'
data['log_message_data[name]'] = title data['log_message_data[name]'] = title
@@ -1192,6 +1179,43 @@ class Client(object):
j = self._post('{}/?{}'.format(self.req_url.MESSAGE_REACTION, url_part), fix_request=True, as_json=True) j = self._post('{}/?{}'.format(self.req_url.MESSAGE_REACTION, url_part), fix_request=True, as_json=True)
def eventReminder(self, thread_id, time, title, location='', location_id=''):
"""
Sets an event reminder
..warning::
Does not work in Python2.7
..todo::
Make this work in Python2.7
:param thread_id: User/Group ID to send event to. See :ref:`intro_threads`
:param time: Event time (unix time stamp)
:param title: Event title
:param location: Event location name
:param location_id: Event location ID
:raises: FBchatException if request failed
"""
full_data = {
"event_type": "EVENT",
"dpr": 1,
"event_time" : time,
"title" : title,
"thread_id" : thread_id,
"location_id" : location_id,
"location_name" : location,
"acontext": {
"action_history": [{
"surface": "messenger_chat_tab",
"mechanism": "messenger_composer"
}]
}
}
url_part = urllib.parse.urlencode(full_data)
j = self._post('{}/?{}'.format(self.req_url.EVENT_REMINDER, url_part), fix_request=True, as_json=True)
def setTypingStatus(self, status, thread_id=None, thread_type=None): def setTypingStatus(self, status, thread_id=None, thread_type=None):
""" """
Sets users typing status in a thread Sets users typing status in a thread
@@ -1203,7 +1227,7 @@ class Client(object):
:type thread_type: models.ThreadType :type thread_type: models.ThreadType
:raises: FBchatException if request failed :raises: FBchatException if request failed
""" """
thread_id, thread_type = self._getThread(thread_id, None) thread_id, thread_type = self._getThread(thread_id, thread_type)
data = { data = {
"typ": status.value, "typ": status.value,
@@ -1218,28 +1242,36 @@ class Client(object):
END SEND METHODS END SEND METHODS
""" """
def markAsDelivered(self, userID, threadID): def markAsDelivered(self, thread_id, message_id):
""" """
.. todo:: Mark a message as delivered
Documenting this
:param thread_id: User/Group ID to which the message belongs. See :ref:`intro_threads`
:param message_id: Message ID to set as delivered. See :ref:`intro_threads`
:return: Whether the request was successful
:raises: FBchatException if request failed
""" """
data = { data = {
"message_ids[0]": threadID, "message_ids[0]": message_id,
"thread_ids[%s][0]" % userID: threadID "thread_ids[%s][0]" % thread_id: message_id
} }
r = self._post(self.req_url.DELIVERED, data) r = self._post(self.req_url.DELIVERED, data)
return r.ok return r.ok
def markAsRead(self, userID): def markAsRead(self, thread_id):
""" """
.. todo:: Mark a thread as read
Documenting this All messages inside the thread will be marked as read
:param thread_id: User/Group ID to set as read. See :ref:`intro_threads`
:return: Whether the request was successful
:raises: FBchatException if request failed
""" """
data = { data = {
"ids[%s]" % thread_id: 'true',
"watermarkTimestamp": now(), "watermarkTimestamp": now(),
"shouldSendReadReceipt": True, "shouldSendReadReceipt": 'true',
"ids[%s]" % userID: True
} }
r = self._post(self.req_url.READ_STATUS, data) r = self._post(self.req_url.READ_STATUS, data)
@@ -1343,7 +1375,7 @@ class Client(object):
delta_type = delta.get("type") delta_type = delta.get("type")
metadata = delta.get("messageMetadata") metadata = delta.get("messageMetadata")
if metadata is not None: if metadata:
mid = metadata["messageId"] mid = metadata["messageId"]
author_id = str(metadata['actorFbId']) author_id = str(metadata['actorFbId'])
ts = int(metadata.get("timestamp")) ts = int(metadata.get("timestamp"))
@@ -1424,9 +1456,52 @@ class Client(object):
# New message # New message
elif delta.get("class") == "NewMessage": elif delta.get("class") == "NewMessage":
message = delta.get('body', '') mentions = []
if delta.get('data') and delta['data'].get('prng'):
try:
mentions = [Mention(str(mention.get('i')), offset=mention.get('o'), length=mention.get('l')) for mention in parse_json(delta['data']['prng'])]
except Exception:
log.exception('An exception occured while reading attachments')
sticker = None
attachments = []
if delta.get('attachments'):
try:
for a in delta['attachments']:
mercury = a['mercury']
if mercury.get('blob_attachment'):
image_metadata = a.get('imageMetadata', {})
attach_type = mercury['blob_attachment']['__typename']
attachment = graphql_to_attachment(mercury.get('blob_attachment', {}))
if attach_type == ['MessageFile', 'MessageVideo', 'MessageAudio']:
# TODO: Add more data here for audio files
attachment.size = int(a['fileSize'])
attachments.append(attachment)
elif mercury.get('sticker_attachment'):
sticker = graphql_to_sticker(a['mercury']['sticker_attachment'])
elif mercury.get('extensible_attachment'):
# TODO: Add more data here for shared stuff (URLs, events and so on)
pass
except Exception:
log.exception('An exception occured while reading attachments: {}'.format(delta['attachments']))
if metadata and metadata.get('tags'):
emoji_size = get_emojisize_from_tags(metadata.get('tags'))
message = Message(
text=delta.get('body'),
mentions=mentions,
emoji_size=emoji_size,
sticker=sticker,
attachments=attachments
)
message.uid = mid
message.author = author_id
message.timestamp = ts
#message.reactions = {}
thread_id, thread_type = getThreadIdAndThreadType(metadata) thread_id, thread_type = getThreadIdAndThreadType(metadata)
self.onMessage(mid=mid, author_id=author_id, message=message, self.onMessage(mid=mid, author_id=author_id, message=delta.get('body', ''), message_object=message,
thread_id=thread_id, thread_type=thread_type, ts=ts, metadata=metadata, msg=m) thread_id=thread_id, thread_type=thread_type, ts=ts, metadata=metadata, msg=m)
# Unknown message type # Unknown message type
@@ -1438,10 +1513,15 @@ class Client(object):
self.onInbox(unseen=m["unseen"], unread=m["unread"], recent_unread=m["recent_unread"], msg=m) self.onInbox(unseen=m["unseen"], unread=m["unread"], recent_unread=m["recent_unread"], msg=m)
# Typing # Typing
# elif mtype == "typ": elif mtype == "typ":
# author_id = str(m.get("from")) author_id = str(m.get("from"))
# typing_status = TypingStatus(m.get("st")) thread_id = str(m.get("to"))
# self.onTyping(author_id=author_id, typing_status=typing_status) if thread_id == self.uid:
thread_type = ThreadType.USER
else:
thread_type = ThreadType.GROUP
typing_status = TypingStatus(m.get("st"))
self.onTyping(author_id=author_id, status=typing_status, thread_id=thread_id, thread_type=thread_type, msg=m)
# Delivered # Delivered
@@ -1450,9 +1530,9 @@ class Client(object):
# #
# self.onSeen(m.get('realtime_viewer_fbid'), m.get('reader'), m.get('time')) # self.onSeen(m.get('realtime_viewer_fbid'), m.get('reader'), m.get('time'))
# elif mtype in ['jewel_requests_add']: elif mtype in ['jewel_requests_add']:
# from_id = m['from'] from_id = m['from']
# self.on_friend_request(from_id) self.onFriendRequest(from_id=from_id, msg=m)
# Happens on every login # Happens on every login
elif mtype == "qprimer": elif mtype == "qprimer":
@@ -1584,23 +1664,25 @@ class Client(object):
return True return True
def onMessage(self, mid=None, author_id=None, message=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg={}): def onMessage(self, mid=None, author_id=None, message=None, message_object=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg=None):
""" """
Called when the client is listening, and somebody sends a message Called when the client is listening, and somebody sends a message
:param mid: The message ID :param mid: The message ID
:param author_id: The ID of the author :param author_id: The ID of the author
:param message: The message :param message: (deprecated. Use `message_object.text` instead)
:param message_object: The message (As a `Message` object)
:param thread_id: Thread ID that the message was sent to. See :ref:`intro_threads` :param thread_id: Thread ID that the message was sent to. See :ref:`intro_threads`
:param thread_type: Type of thread that the message was sent to. See :ref:`intro_threads` :param thread_type: Type of thread that the message was sent to. See :ref:`intro_threads`
:param ts: The timestamp of the message :param ts: The timestamp of the message
:param metadata: Extra metadata about the message :param metadata: Extra metadata about the message
:param msg: A full set of the data recieved :param msg: A full set of the data recieved
:type message_object: models.Message
:type thread_type: models.ThreadType :type thread_type: models.ThreadType
""" """
log.info("Message from {} in {} ({}): {}".format(author_id, thread_id, thread_type.name, message)) log.info("{} from {} in {}".format(message_object, thread_id, thread_type.name))
def onColorChange(self, mid=None, author_id=None, new_color=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg={}): def onColorChange(self, mid=None, author_id=None, new_color=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg=None):
""" """
Called when the client is listening, and somebody changes a thread's color Called when the client is listening, and somebody changes a thread's color
@@ -1617,7 +1699,7 @@ class Client(object):
""" """
log.info("Color change from {} in {} ({}): {}".format(author_id, thread_id, thread_type.name, new_color)) log.info("Color change from {} in {} ({}): {}".format(author_id, thread_id, thread_type.name, new_color))
def onEmojiChange(self, mid=None, author_id=None, new_emoji=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg={}): def onEmojiChange(self, mid=None, author_id=None, new_emoji=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg=None):
""" """
Called when the client is listening, and somebody changes a thread's emoji Called when the client is listening, and somebody changes a thread's emoji
@@ -1633,7 +1715,7 @@ class Client(object):
""" """
log.info("Emoji change from {} in {} ({}): {}".format(author_id, thread_id, thread_type.name, new_emoji)) log.info("Emoji change from {} in {} ({}): {}".format(author_id, thread_id, thread_type.name, new_emoji))
def onTitleChange(self, mid=None, author_id=None, new_title=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg={}): def onTitleChange(self, mid=None, author_id=None, new_title=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg=None):
""" """
Called when the client is listening, and somebody changes the title of a thread Called when the client is listening, and somebody changes the title of a thread
@@ -1649,7 +1731,7 @@ class Client(object):
""" """
log.info("Title change from {} in {} ({}): {}".format(author_id, thread_id, thread_type.name, new_title)) log.info("Title change from {} in {} ({}): {}".format(author_id, thread_id, thread_type.name, new_title))
def onNicknameChange(self, mid=None, author_id=None, changed_for=None, new_nickname=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg={}): def onNicknameChange(self, mid=None, author_id=None, changed_for=None, new_nickname=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg=None):
""" """
Called when the client is listening, and somebody changes the nickname of a person Called when the client is listening, and somebody changes the nickname of a person
@@ -1667,7 +1749,7 @@ class Client(object):
log.info("Nickname change from {} in {} ({}) for {}: {}".format(author_id, thread_id, thread_type.name, changed_for, new_nickname)) log.info("Nickname change from {} in {} ({}) for {}: {}".format(author_id, thread_id, thread_type.name, changed_for, new_nickname))
def onMessageSeen(self, seen_by=None, thread_id=None, thread_type=ThreadType.USER, seen_ts=None, ts=None, metadata=None, msg={}): def onMessageSeen(self, seen_by=None, thread_id=None, thread_type=ThreadType.USER, seen_ts=None, ts=None, metadata=None, msg=None):
""" """
Called when the client is listening, and somebody marks a message as seen Called when the client is listening, and somebody marks a message as seen
@@ -1682,7 +1764,7 @@ class Client(object):
""" """
log.info("Messages seen by {} in {} ({}) at {}s".format(seen_by, thread_id, thread_type.name, seen_ts/1000)) log.info("Messages seen by {} in {} ({}) at {}s".format(seen_by, thread_id, thread_type.name, seen_ts/1000))
def onMessageDelivered(self, msg_ids=None, delivered_for=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg={}): def onMessageDelivered(self, msg_ids=None, delivered_for=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg=None):
""" """
Called when the client is listening, and somebody marks messages as delivered Called when the client is listening, and somebody marks messages as delivered
@@ -1697,7 +1779,7 @@ class Client(object):
""" """
log.info("Messages {} delivered to {} in {} ({}) at {}s".format(msg_ids, delivered_for, thread_id, thread_type.name, ts/1000)) log.info("Messages {} delivered to {} in {} ({}) at {}s".format(msg_ids, delivered_for, thread_id, thread_type.name, ts/1000))
def onMarkedSeen(self, threads=None, seen_ts=None, ts=None, metadata=None, msg={}): def onMarkedSeen(self, threads=None, seen_ts=None, ts=None, metadata=None, msg=None):
""" """
Called when the client is listening, and the client has successfully marked threads as seen Called when the client is listening, and the client has successfully marked threads as seen
@@ -1712,7 +1794,7 @@ class Client(object):
log.info("Marked messages as seen in threads {} at {}s".format([(x[0], x[1].name) for x in threads], seen_ts/1000)) log.info("Marked messages as seen in threads {} at {}s".format([(x[0], x[1].name) for x in threads], seen_ts/1000))
def onPeopleAdded(self, mid=None, added_ids=None, author_id=None, thread_id=None, ts=None, msg={}): def onPeopleAdded(self, mid=None, added_ids=None, author_id=None, thread_id=None, ts=None, msg=None):
""" """
Called when the client is listening, and somebody adds people to a group thread Called when the client is listening, and somebody adds people to a group thread
@@ -1725,7 +1807,7 @@ class Client(object):
""" """
log.info("{} added: {}".format(author_id, ', '.join(added_ids))) log.info("{} added: {}".format(author_id, ', '.join(added_ids)))
def onPersonRemoved(self, mid=None, removed_id=None, author_id=None, thread_id=None, ts=None, msg={}): def onPersonRemoved(self, mid=None, removed_id=None, author_id=None, thread_id=None, ts=None, msg=None):
""" """
Called when the client is listening, and somebody removes a person from a group thread Called when the client is listening, and somebody removes a person from a group thread
@@ -1738,7 +1820,7 @@ class Client(object):
""" """
log.info("{} removed: {}".format(author_id, removed_id)) log.info("{} removed: {}".format(author_id, removed_id))
def onFriendRequest(self, from_id=None, msg={}): def onFriendRequest(self, from_id=None, msg=None):
""" """
Called when the client is listening, and somebody sends a friend request Called when the client is listening, and somebody sends a friend request
@@ -1747,7 +1829,7 @@ class Client(object):
""" """
log.info("Friend request from {}".format(from_id)) log.info("Friend request from {}".format(from_id))
def onInbox(self, unseen=None, unread=None, recent_unread=None, msg={}): def onInbox(self, unseen=None, unread=None, recent_unread=None, msg=None):
""" """
.. todo:: .. todo::
Documenting this Documenting this
@@ -1759,7 +1841,21 @@ class Client(object):
""" """
log.info('Inbox event: {}, {}, {}'.format(unseen, unread, recent_unread)) log.info('Inbox event: {}, {}, {}'.format(unseen, unread, recent_unread))
def onQprimer(self, ts=None, msg={}): def onTyping(self, author_id=None, status=None, thread_id=None, thread_type=None, msg=None):
"""
Called when the client is listening, and somebody starts or stops typing into a chat
:param author_id: The ID of the person who sent the action
:param status: The typing status
:param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads`
:param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads`
:param msg: A full set of the data recieved
:type typing_status: models.TypingStatus
:type thread_type: models.ThreadType
"""
pass
def onQprimer(self, ts=None, msg=None):
""" """
Called when the client just started listening Called when the client just started listening
@@ -1768,7 +1864,7 @@ class Client(object):
""" """
pass pass
def onChatTimestamp(self, buddylist={}, msg={}): def onChatTimestamp(self, buddylist=None, msg=None):
""" """
Called when the client receives chat online presence update Called when the client receives chat online presence update
@@ -1777,7 +1873,7 @@ class Client(object):
""" """
log.debug('Chat Timestamps received: {}'.format(buddylist)) log.debug('Chat Timestamps received: {}'.format(buddylist))
def onUnknownMesssageType(self, msg={}): def onUnknownMesssageType(self, msg=None):
""" """
Called when the client is listening, and some unknown data was recieved Called when the client is listening, and some unknown data was recieved
@@ -1785,7 +1881,7 @@ class Client(object):
""" """
log.debug('Unknown message received: {}'.format(msg)) log.debug('Unknown message received: {}'.format(msg))
def onMessageError(self, exception=None, msg={}): def onMessageError(self, exception=None, msg=None):
""" """
Called when an error was encountered while parsing recieved data Called when an error was encountered while parsing recieved data

View File

@@ -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') == 'GROUP' or thread.get('is_group_thread') or thread.get('thread_key', {}).get('thread_fbid'): if thread.get('thread_type') in ('GROUP', 'ROOM') 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')
@@ -61,26 +61,95 @@ def get_customization_info(thread):
rtn['own_nickname'] = pc[1].get('nickname') rtn['own_nickname'] = pc[1].get('nickname')
return rtn return rtn
def graphql_to_sticker(s):
if not s:
return None
sticker = Sticker(
uid=s['id']
)
if s.get('pack'):
sticker.pack = s['pack'].get('id')
if s.get('sprite_image'):
sticker.is_animated = True
sticker.medium_sprite_image = s['sprite_image'].get('uri')
sticker.large_sprite_image = s['sprite_image_2x'].get('uri')
sticker.frames_per_row = s.get('frames_per_row')
sticker.frames_per_col = s.get('frames_per_column')
sticker.frame_rate = s.get('frame_rate')
sticker.url = s.get('url')
sticker.width = s.get('width')
sticker.height = s.get('height')
if s.get('label'):
sticker.label = s['label']
return sticker
def graphql_to_attachment(a):
_type = a['__typename']
if _type in ['MessageImage', 'MessageAnimatedImage']:
return ImageAttachment(
original_extension=a.get('original_extension') or (a['filename'].split('-')[0] if a.get('filename') else None),
width=a.get('original_dimensions', {}).get('width'),
height=a.get('original_dimensions', {}).get('height'),
is_animated=_type=='MessageAnimatedImage',
thumbnail_url=a.get('thumbnail', {}).get('uri'),
preview=a.get('preview') or a.get('preview_image'),
large_preview=a.get('large_preview'),
animated_preview=a.get('animated_image'),
uid=a.get('legacy_attachment_id')
)
elif _type == 'MessageVideo':
return VideoAttachment(
width=a.get('original_dimensions', {}).get('width'),
height=a.get('original_dimensions', {}).get('height'),
duration=a.get('playable_duration_in_ms'),
preview_url=a.get('playable_url'),
small_image=a.get('chat_image'),
medium_image=a.get('inbox_image'),
large_image=a.get('large_image'),
uid=a.get('legacy_attachment_id')
)
elif _type == 'MessageAudio':
return AudioAttachment(
filename=a.get('filename'),
url=a.get('playable_url'),
duration=a.get('playable_duration_in_ms'),
audio_type=a.get('audio_type')
)
elif _type == 'MessageFile':
return FileAttachment(
url=a.get('url'),
name=a.get('filename'),
is_malicious=a.get('is_malicious'),
uid=a.get('message_file_fbid')
)
else:
return Attachment(
uid=a.get('legacy_attachment_id')
)
def graphql_to_message(message): def graphql_to_message(message):
if message.get('message_sender') is None: if message.get('message_sender') is None:
message['message_sender'] = {} message['message_sender'] = {}
if message.get('message') is None: if message.get('message') is None:
message['message'] = {} message['message'] = {}
is_read = None rtn = Message(
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'), 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', [])], 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'), emoji_size=get_emojisize_from_tags(message.get('tags_list')),
attachments=message.get('blob_attachments'), sticker=graphql_to_sticker(message.get('sticker'))
extensible_attachment=message.get('extensible_attachment')
) )
rtn.uid = str(message.get('message_id'))
rtn.author = str(message.get('message_sender').get('id'))
rtn.timestamp = message.get('timestamp_precise')
if message.get('unread') is not None:
rtn.is_read = not message['unread']
rtn.reactions = {str(r['user']['id']):MessageReaction(r['reaction']) for r in message.get('message_reactions')}
if message.get('blob_attachments') is not None:
rtn.attachments = [graphql_to_attachment(attachment) for attachment in message['blob_attachments']]
# TODO: This is still missing parsing:
# message.get('extensible_attachment')
return rtn
def graphql_to_user(user): def graphql_to_user(user):
if user.get('profile_picture') is None: if user.get('profile_picture') is None:
@@ -92,7 +161,7 @@ def graphql_to_user(user):
first_name=user.get('first_name'), first_name=user.get('first_name'),
last_name=user.get('last_name'), last_name=user.get('last_name'),
is_friend=user.get('is_viewer_friend'), is_friend=user.get('is_viewer_friend'),
gender=GENDERS[user.get('gender')], gender=GENDERS.get(user.get('gender')),
affinity=user.get('affinity'), affinity=user.get('affinity'),
nickname=c_info.get('nickname'), nickname=c_info.get('nickname'),
color=c_info.get('color'), color=c_info.get('color'),
@@ -103,10 +172,46 @@ def graphql_to_user(user):
message_count=user.get('messages_count') message_count=user.get('messages_count')
) )
def graphql_to_thread(thread):
if thread['thread_type'] == 'GROUP':
return graphql_to_group(thread)
elif thread['thread_type'] == 'ONE_TO_ONE':
if thread.get('big_image_src') is None:
thread['big_image_src'] = {}
c_info = get_customization_info(thread)
participants = [node['messaging_actor'] for node in thread['all_participants']['nodes']]
user = next(p for p in participants if p['id'] == thread['thread_key']['other_user_id'])
last_message_timestamp = None
if 'last_message' in thread:
last_message_timestamp = thread['last_message']['nodes'][0]['timestamp_precise']
return User(
user['id'],
url=user.get('url'),
name=user.get('name'),
first_name=user.get('short_name'),
last_name=user.get('name').split(user.get('short_name'),1)[1].strip(),
is_friend=user.get('is_viewer_friend'),
gender=GENDERS.get(user.get('gender')),
affinity=user.get('affinity'),
nickname=c_info.get('nickname'),
color=c_info.get('color'),
emoji=c_info.get('emoji'),
own_nickname=c_info.get('own_nickname'),
photo=user['big_image_src'].get('uri'),
message_count=thread.get('messages_count'),
last_message_timestamp=last_message_timestamp
)
else:
raise FBchatException('Unknown thread type: {}, with data: {}'.format(thread.get('thread_type'), thread))
def graphql_to_group(group): def graphql_to_group(group):
if group.get('image') is None: if group.get('image') is None:
group['image'] = {} group['image'] = {}
c_info = get_customization_info(group) c_info = get_customization_info(group)
last_message_timestamp = None
if 'last_message' in group:
last_message_timestamp = group['last_message']['nodes'][0]['timestamp_precise']
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']]),
@@ -115,7 +220,28 @@ def graphql_to_group(group):
emoji=c_info.get('emoji'), emoji=c_info.get('emoji'),
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'),
last_message_timestamp=last_message_timestamp
)
def graphql_to_room(room):
if room.get('image') is None:
room['image'] = {}
c_info = get_customization_info(room)
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')),
) )
def graphql_to_page(page): def graphql_to_page(page):
@@ -167,7 +293,9 @@ def graphql_response_to_json(content):
return rtn return rtn
class GraphQL(object): class GraphQL(object):
def __init__(self, query=None, doc_id=None, params={}): def __init__(self, query=None, doc_id=None, params=None):
if params is None:
params = {}
if query is not None: if query is not None:
self.value = { self.value = {
'priority': 0, 'priority': 0,

View File

@@ -9,11 +9,11 @@ class FBchatException(Exception):
class FBchatFacebookError(FBchatException): class FBchatFacebookError(FBchatException):
#: The error code that Facebook returned #: The error code that Facebook returned
fb_error_code = str fb_error_code = None
#: The error message that Facebook returned (In the user's own language) #: The error message that Facebook returned (In the user's own language)
fb_error_message = str fb_error_message = None
#: The status code that was sent in the http response (eg. 404) (Usually only set if not successful, aka. not 200) #: The status code that was sent in the http response (eg. 404) (Usually only set if not successful, aka. not 200)
request_status_code = int request_status_code = None
def __init__(self, message, fb_error_code=None, fb_error_message=None, request_status_code=None): def __init__(self, message, fb_error_code=None, fb_error_message=None, request_status_code=None):
super(FBchatFacebookError, self).__init__(message) super(FBchatFacebookError, self).__init__(message)
"""Thrown by fbchat when Facebook returns an error""" """Thrown by fbchat when Facebook returns an error"""
@@ -26,17 +26,17 @@ class FBchatUserError(FBchatException):
class Thread(object): class Thread(object):
#: The unique identifier of the thread. 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
uid = str uid = None
#: Specifies the type of thread. Can be used a `thread_type`. See :ref:`intro_threads` for more info #: 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 #: A url to the thread's picture
photo = str photo = None
#: The name of the thread #: The name of the thread
name = str name = None
#: Timestamp of last message #: Timestamp of last message
last_message_timestamp = str last_message_timestamp = None
#: Number of messages in the thread #: Number of messages in the thread
message_count = int message_count = None
def __init__(self, _type, uid, photo=None, name=None, last_message_timestamp=None, message_count=None): def __init__(self, _type, uid, photo=None, name=None, last_message_timestamp=None, message_count=None):
"""Represents a Facebook thread""" """Represents a Facebook thread"""
self.uid = str(uid) self.uid = str(uid)
@@ -55,25 +55,25 @@ class Thread(object):
class User(Thread): class User(Thread):
#: The profile url #: The profile url
url = str url = None
#: The users first name #: The users first name
first_name = str first_name = None
#: The users last name #: The users last name
last_name = str last_name = None
#: Whether the user and the client are friends #: Whether the user and the client are friends
is_friend = bool is_friend = None
#: The user's gender #: The user's gender
gender = str gender = None
#: From 0 to 1. How close the client is to the user #: From 0 to 1. How close the client is to the user
affinity = float affinity = None
#: The user's nickname #: The user's nickname
nickname = str nickname = None
#: The clients nickname, as seen by the user #: The clients nickname, as seen by the user
own_nickname = str own_nickname = None
#: A :class:`ThreadColor`. The message color #: A :class:`ThreadColor`. The message color
color = None color = None
#: The default emoji #: The default emoji
emoji = str emoji = None
def __init__(self, uid, url=None, first_name=None, last_name=None, is_friend=None, gender=None, affinity=None, nickname=None, own_nickname=None, color=None, emoji=None, **kwargs): def __init__(self, uid, url=None, first_name=None, last_name=None, is_friend=None, gender=None, affinity=None, nickname=None, own_nickname=None, color=None, emoji=None, **kwargs):
"""Represents a Facebook user. Inherits `Thread`""" """Represents a Facebook user. Inherits `Thread`"""
@@ -92,34 +92,65 @@ class User(Thread):
class Group(Thread): class Group(Thread):
#: Unique list (set) of the group thread's participant user IDs #: Unique list (set) of the group thread's participant user IDs
participants = set participants = None
#: Dict, containing user nicknames mapped to their IDs #: A dict, containing user nicknames mapped to their IDs
nicknames = dict nicknames = None
#: A :class:`ThreadColor`. The groups's message color #: A :class:`ThreadColor`. The groups's message color
color = None color = None
#: The groups's default emoji #: The groups's default emoji
emoji = str emoji = None
def __init__(self, uid, participants=set(), nicknames=[], color=None, emoji=None, **kwargs): def __init__(self, uid, participants=None, nicknames=None, color=None, emoji=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:
participants = set()
self.participants = participants self.participants = participants
if nicknames is None:
nicknames = []
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:
admins = set()
self.admins = admins
self.approval_mode = approval_mode
if approval_requests is None:
approval_requests = set()
self.approval_requests = approval_requests
self.join_link = join_link
self.privacy_mode = privacy_mode
class Page(Thread): class Page(Thread):
#: The page's custom url #: The page's custom url
url = str url = None
#: The name of the page's location city #: The name of the page's location city
city = str city = None
#: Amount of likes the page has #: Amount of likes the page has
likes = int likes = None
#: Some extra information about the page #: Some extra information about the page
sub_title = str sub_title = None
#: The page's category #: The page's category
category = str category = None
def __init__(self, uid, url=None, city=None, likes=None, sub_title=None, category=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`"""
@@ -132,55 +163,279 @@ class Page(Thread):
class Message(object): class Message(object):
#: The message ID
uid = str
#: ID of the sender
author = int
#: Timestamp of when the message was sent
timestamp = str
#: Whether the message is read
is_read = bool
#: A list of message reactions
reactions = list
#: The actual message #: The actual message
text = str text = None
#: A list of :class:`Mention` objects #: A list of :class:`Mention` objects
mentions = list mentions = None
#: An ID of a sent sticker #: A :class:`EmojiSize`. Size of a sent emoji
sticker = str emoji_size = None
#: The message ID
uid = None
#: ID of the sender
author = None
#: Timestamp of when the message was sent
timestamp = None
#: Whether the message is read
is_read = None
#: A dict with user's IDs as keys, and their :class:`MessageReaction` as values
reactions = None
#: The actual message
text = None
#: A :class:`Sticker`
sticker = None
#: A list of attachments #: A list of attachments
attachments = list attachments = None
#: An extensible attachment, e.g. share object
extensible_attachment = dict
def __init__(self, uid, author=None, timestamp=None, is_read=None, reactions=[], text=None, mentions=[], sticker=None, attachments=[], extensible_attachment={}): def __init__(self, text=None, mentions=None, emoji_size=None, sticker=None, attachments=None):
"""Represents a Facebook message""" """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.text = text
if mentions is None:
mentions = []
self.mentions = mentions self.mentions = mentions
self.emoji_size = emoji_size
self.sticker = sticker self.sticker = sticker
if attachments is None:
attachments = []
self.attachments = attachments self.attachments = attachments
self.extensible_attachment = extensible_attachment self.reactions = {}
def __repr__(self):
return self.__unicode__()
def __unicode__(self):
return '<Message ({}): {}, mentions={} emoji_size={} attachments={}>'.format(self.uid, repr(self.text), self.mentions, self.emoji_size, self.attachments)
class Attachment(object):
#: The attachment ID
uid = None
def __init__(self, uid=None):
"""Represents a Facebook attachment"""
self.uid = uid
class Sticker(Attachment):
#: The sticker-pack's ID
pack = None
#: Whether the sticker is animated
is_animated = False
# If the sticker is animated, the following should be present
#: URL to a medium spritemap
medium_sprite_image = None
#: URL to a large spritemap
large_sprite_image = None
#: The amount of frames present in the spritemap pr. row
frames_per_row = None
#: The amount of frames present in the spritemap pr. coloumn
frames_per_col = None
#: The frame rate the spritemap is intended to be played in
frame_rate = None
#: URL to the sticker's image
url = None
#: Width of the sticker
width = None
#: Height of the sticker
height = None
#: The sticker's label/name
label = None
def __init__(self, *args, **kwargs):
"""Represents a Facebook sticker that has been sent to a Facebook thread as an attachment"""
super(Sticker, self).__init__(*args, **kwargs)
class ShareAttachment(Attachment):
def __init__(self, **kwargs):
"""Represents a shared item (eg. URL) that has been sent as a Facebook attachment - *Currently Incomplete!*"""
super(ShareAttachment, self).__init__(**kwargs)
class FileAttachment(Attachment):
#: Url where you can download the file
url = None
#: Size of the file in bytes
size = None
#: Name of the file
name = None
#: Whether Facebook determines that this file may be harmful
is_malicious = None
def __init__(self, url=None, size=None, name=None, is_malicious=None, **kwargs):
"""Represents a file that has been sent as a Facebook attachment"""
super(FileAttachment, self).__init__(**kwargs)
self.url = url
self.size = size
self.name = name
self.is_malicious = is_malicious
class AudioAttachment(Attachment):
#: Name of the file
filename = None
#: Url of the audio file
url = None
#: Duration of the audioclip in milliseconds
duration = None
#: Audio type
audio_type = None
def __init__(self, filename=None, url=None, duration=None, audio_type=None, **kwargs):
"""Represents an audio file that has been sent as a Facebook attachment"""
super(AudioAttachment, self).__init__(**kwargs)
self.filename = filename
self.url = url
self.duration = duration
self.audio_type = audio_type
class ImageAttachment(Attachment):
#: The extension of the original image (eg. 'png')
original_extension = None
#: Width of original image
width = None
#: Height of original image
height = None
#: Whether the image is animated
is_animated = None
#: URL to a thumbnail of the image
thumbnail_url = None
#: URL to a medium preview of the image
preview_url = None
#: Width of the medium preview image
preview_width = None
#: Height of the medium preview image
preview_height = None
#: URL to a large preview of the image
large_preview_url = None
#: Width of the large preview image
large_preview_width = None
#: Height of the large preview image
large_preview_height = None
#: URL to an animated preview of the image (eg. for gifs)
animated_preview_url = None
#: Width of the animated preview image
animated_preview_width = None
#: Height of the animated preview image
animated_preview_height = None
def __init__(self, original_extension=None, width=None, height=None, is_animated=None, thumbnail_url=None, preview=None, large_preview=None, animated_preview=None, **kwargs):
"""
Represents an image that has been sent as a Facebook attachment
To retrieve the full image url, use: :func:`fbchat.Client.fetchImageUrl`,
and pass it the uid of the image attachment
"""
super(ImageAttachment, self).__init__(**kwargs)
self.original_extension = original_extension
if width is not None:
width = int(width)
self.width = width
if height is not None:
height = int(height)
self.height = height
self.is_animated = is_animated
self.thumbnail_url = thumbnail_url
if preview is None:
preview = {}
self.preview_url = preview.get('uri')
self.preview_width = preview.get('width')
self.preview_height = preview.get('height')
if large_preview is None:
large_preview = {}
self.large_preview_url = large_preview.get('uri')
self.large_preview_width = large_preview.get('width')
self.large_preview_height = large_preview.get('height')
if animated_preview is None:
animated_preview = {}
self.animated_preview_url = animated_preview.get('uri')
self.animated_preview_width = animated_preview.get('width')
self.animated_preview_height = animated_preview.get('height')
class VideoAttachment(Attachment):
#: Size of the original video in bytes
size = None
#: Width of original video
width = None
#: Height of original video
height = None
#: Length of video in milliseconds
duration = None
#: URL to very compressed preview video
preview_url = None
#: URL to a small preview image of the video
small_image_url = None
#: Width of the small preview image
small_image_width = None
#: Height of the small preview image
small_image_height = None
#: URL to a medium preview image of the video
medium_image_url = None
#: Width of the medium preview image
medium_image_width = None
#: Height of the medium preview image
medium_image_height = None
#: URL to a large preview image of the video
large_image_url = None
#: Width of the large preview image
large_image_width = None
#: Height of the large preview image
large_image_height = None
def __init__(self, size=None, width=None, height=None, duration=None, preview_url=None, small_image=None, medium_image=None, large_image=None, **kwargs):
"""Represents a video that has been sent as a Facebook attachment"""
super(VideoAttachment, self).__init__(**kwargs)
self.size = size
self.width = width
self.height = height
self.duration = duration
self.preview_url = preview_url
if small_image is None:
small_image = {}
self.small_image_url = small_image.get('uri')
self.small_image_width = small_image.get('width')
self.small_image_height = small_image.get('height')
if medium_image is None:
medium_image = {}
self.medium_image_url = medium_image.get('uri')
self.medium_image_width = medium_image.get('width')
self.medium_image_height = medium_image.get('height')
if large_image is None:
large_image = {}
self.large_image_url = large_image.get('uri')
self.large_image_width = large_image.get('width')
self.large_image_height = large_image.get('height')
class Mention(object): class Mention(object):
#: The user ID the mention is pointing at #: The thread ID the mention is pointing at
user_id = str thread_id = None
#: The character where the mention starts #: The character where the mention starts
offset = int offset = None
#: The length of the mention #: The length of the mention
length = int length = None
def __init__(self, user_id, offset=0, length=10): def __init__(self, thread_id, offset=0, length=10):
"""Represents a @mention""" """Represents a @mention"""
self.user_id = user_id self.thread_id = thread_id
self.offset = offset self.offset = offset
self.length = length self.length = length
def __repr__(self):
return self.__unicode__()
def __unicode__(self):
return '<Mention {}: offset={} length={}>'.format(self.thread_id, self.offset, self.length)
class Enum(enum.Enum): class Enum(enum.Enum):
"""Used internally by fbchat to support enumerations""" """Used internally by fbchat to support enumerations"""
def __repr__(self): def __repr__(self):
@@ -192,13 +447,14 @@ class ThreadType(Enum):
USER = 1 USER = 1
GROUP = 2 GROUP = 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)."""
INBOX = 'inbox' INBOX = 'INBOX'
PENDING = 'pending' PENDING = 'PENDING'
ARCHIVED = 'action:archived' ARCHIVED = 'ARCHIVED'
OTHER = 'other' OTHER = 'OTHER'
class TypingStatus(Enum): class TypingStatus(Enum):
"""Used to specify whether the user is typing or has stopped typing""" """Used to specify whether the user is typing or has stopped typing"""
@@ -238,22 +494,3 @@ class MessageReaction(Enum):
ANGRY = '😠' ANGRY = '😠'
YES = '👍' YES = '👍'
NO = '👎' NO = '👎'
LIKES = {
'large': EmojiSize.LARGE,
'medium': EmojiSize.MEDIUM,
'small': EmojiSize.SMALL,
'l': EmojiSize.LARGE,
'm': EmojiSize.MEDIUM,
's': EmojiSize.SMALL
}
MessageReactionFix = {
'😍': ('0001f60d', '%F0%9F%98%8D'),
'😆': ('0001f606', '%F0%9F%98%86'),
'😮': ('0001f62e', '%F0%9F%98%AE'),
'😢': ('0001f622', '%F0%9F%98%A2'),
'😠': ('0001f620', '%F0%9F%98%A0'),
'👍': ('0001f44d', '%F0%9F%91%8D'),
'👎': ('0001f44e', '%F0%9F%91%8E')
}

View File

@@ -9,6 +9,13 @@ import warnings
import logging import logging
from .models import * from .models import *
try:
from urllib.parse import urlencode
basestring = (str, bytes)
except ImportError:
from urllib import urlencode
basestring = basestring
# Python 2's `input` executes the input, whereas `raw_input` just returns the input # Python 2's `input` executes the input, whereas `raw_input` just returns the input
try: try:
input = raw_input input = raw_input
@@ -32,6 +39,26 @@ 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"
] ]
LIKES = {
'large': EmojiSize.LARGE,
'medium': EmojiSize.MEDIUM,
'small': EmojiSize.SMALL,
'l': EmojiSize.LARGE,
'm': EmojiSize.MEDIUM,
's': EmojiSize.SMALL
}
MessageReactionFix = {
'😍': ('0001f60d', '%F0%9F%98%8D'),
'😆': ('0001f606', '%F0%9F%98%86'),
'😮': ('0001f62e', '%F0%9F%98%AE'),
'😢': ('0001f622', '%F0%9F%98%A2'),
'😠': ('0001f620', '%F0%9F%98%A0'),
'👍': ('0001f44d', '%F0%9F%91%8D'),
'👎': ('0001f44e', '%F0%9F%91%8E')
}
GENDERS = { GENDERS = {
# For standard requests # For standard requests
0: 'unknown', 0: 'unknown',
@@ -54,13 +81,12 @@ GENDERS = {
#'': 'female_singular_guess', #'': 'female_singular_guess',
#'': 'male_singular_guess', #'': 'male_singular_guess',
#'': 'mixed', #'': 'mixed',
#'': 'neuter_singular', 'NEUTER': 'neuter_singular',
#'': 'unknown_singular', #'': 'unknown_singular',
#'': 'female_plural', #'': 'female_plural',
#'': 'male_plural', #'': 'male_plural',
#'': 'neuter_plural', #'': 'neuter_plural',
#'': 'unknown_plural', #'': 'unknown_plural',
None: None
} }
class ReqUrl(object): class ReqUrl(object):
@@ -68,7 +94,8 @@ class ReqUrl(object):
SEARCH = "https://www.facebook.com/ajax/typeahead/search.php" SEARCH = "https://www.facebook.com/ajax/typeahead/search.php"
LOGIN = "https://m.facebook.com/login.php?login_attempt=1" LOGIN = "https://m.facebook.com/login.php?login_attempt=1"
SEND = "https://www.facebook.com/messaging/send/" SEND = "https://www.facebook.com/messaging/send/"
THREAD_SYNC = "https://www.facebook.com/ajax/mercury/thread_sync.php" UNREAD_THREADS = "https://www.facebook.com/ajax/mercury/unread_threads.php"
UNSEEN_THREADS = "https://www.facebook.com/mercury/unseen_thread_ids/"
THREADS = "https://www.facebook.com/ajax/mercury/threadlist_info.php" THREADS = "https://www.facebook.com/ajax/mercury/threadlist_info.php"
MESSAGES = "https://www.facebook.com/ajax/mercury/thread_info.php" MESSAGES = "https://www.facebook.com/ajax/mercury/thread_info.php"
READ_STATUS = "https://www.facebook.com/ajax/mercury/change_read_status.php" READ_STATUS = "https://www.facebook.com/ajax/mercury/change_read_status.php"
@@ -92,6 +119,8 @@ class ReqUrl(object):
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/" GRAPHQL = "https://www.facebook.com/api/graphqlbatch/"
ATTACHMENT_PHOTO = "https://www.facebook.com/mercury/attachments/photo/"
EVENT_REMINDER = "https://www.facebook.com/ajax/eventreminder/create"
pull_channel = 0 pull_channel = 0
@@ -121,6 +150,9 @@ def get_decoded_r(r):
def get_decoded(content): def get_decoded(content):
return content.decode(facebookEncoding) return content.decode(facebookEncoding)
def parse_json(content):
return json.loads(content)
def get_json(r): def get_json(r):
return json.loads(strip_to_json(get_decoded_r(r))) return json.loads(strip_to_json(get_decoded_r(r)))
@@ -182,3 +214,22 @@ def check_request(r, as_json=True):
return j return j
else: else:
return content return content
def get_jsmods_require(j, index):
if j.get('jsmods') and j['jsmods'].get('require'):
try:
return j['jsmods']['require'][0][index][0]
except (KeyError, IndexError) as e:
log.warning('Error when getting jsmods_require: {}. Facebook might have changed protocol'.format(j))
return None
def get_emojisize_from_tags(tags):
if tags is None:
return None
tmp = [tag for tag in tags if tag.startswith('hot_emoji_size:')]
if len(tmp) > 0:
try:
return LIKES[tmp[0].split(':')[1]]
except (KeyError, IndexError):
log.exception('Could not determine emoji size from {} - {}'.format(tags, tmp))
return None

View File

@@ -54,10 +54,8 @@ setup(
classifiers=[ classifiers=[
'Development Status :: 2 - Pre-Alpha', 'Development Status :: 2 - Pre-Alpha',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'Intended Audience :: Developers',
'Intended Audience :: Information Technology', 'Intended Audience :: Information Technology',
'License :: OSI Approved :: BSD License', 'License :: OSI Approved :: BSD License',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent', 'Operating System :: OS Independent',
'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 2.7',

153
tests.py
View File

@@ -22,6 +22,8 @@ Full documentation on https://fbchat.readthedocs.io/
""" """
test_sticker_id = '767334476626295'
class CustomClient(Client): class CustomClient(Client):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.got_qprimer = False self.got_qprimer = False
@@ -64,16 +66,14 @@ class TestFbchat(unittest.TestCase):
def test_defaultThread(self): def test_defaultThread(self):
# setDefaultThread # setDefaultThread
client.setDefaultThread(group_id, ThreadType.GROUP) for thread in threads:
self.assertTrue(client.sendMessage('test_default_recipient★')) client.setDefaultThread(thread_id=thread['id'], thread_type=thread['type'])
self.assertTrue(client.send(Message(text='test_default_recipient★')))
client.setDefaultThread(user_id, ThreadType.USER)
self.assertTrue(client.sendMessage('test_default_recipient★'))
# resetDefaultThread # resetDefaultThread
client.resetDefaultThread() client.resetDefaultThread()
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
client.sendMessage('should_not_send') client.send(Message(text='should_not_send'))
def test_fetchAllUsers(self): def test_fetchAllUsers(self):
users = client.fetchAllUsers() users = client.fetchAllUsers()
@@ -96,46 +96,44 @@ class TestFbchat(unittest.TestCase):
groups = client.searchForGroups('') groups = client.searchForGroups('')
self.assertGreater(len(groups), 0) self.assertGreater(len(groups), 0)
def test_sendEmoji(self): def test_send(self):
self.assertIsNotNone(client.sendEmoji(size=EmojiSize.SMALL, thread_id=user_id, thread_type=ThreadType.USER)) for thread in threads:
self.assertIsNotNone(client.sendEmoji(size=EmojiSize.MEDIUM, thread_id=user_id, thread_type=ThreadType.USER)) client.setDefaultThread(thread_id=thread['id'], thread_type=thread['type'])
self.assertIsNotNone(client.sendEmoji('😆', EmojiSize.LARGE, user_id, ThreadType.USER))
self.assertIsNotNone(client.sendEmoji(size=EmojiSize.SMALL, thread_id=group_id, thread_type=ThreadType.GROUP)) self.assertIsNotNone(client.send(Message(emoji_size=EmojiSize.SMALL)))
self.assertIsNotNone(client.sendEmoji(size=EmojiSize.MEDIUM, thread_id=group_id, thread_type=ThreadType.GROUP)) self.assertIsNotNone(client.send(Message(emoji_size=EmojiSize.MEDIUM)))
self.assertIsNotNone(client.sendEmoji('😆', EmojiSize.LARGE, group_id, ThreadType.GROUP)) self.assertIsNotNone(client.send(Message(text='😆', emoji_size=EmojiSize.LARGE)))
def test_sendMessage(self): self.assertIsNotNone(client.send(Message(text='test_send★')))
self.assertIsNotNone(client.sendMessage('test_send_user★', user_id, ThreadType.USER)) with self.assertRaises(FBchatFacebookError):
self.assertIsNotNone(client.sendMessage('test_send_group', group_id, ThreadType.GROUP)) self.assertIsNotNone(client.send(Message(text='test_send_should_fail'), thread_id=thread['id'], thread_type=(ThreadType.GROUP if thread['type'] == ThreadType.USER else ThreadType.USER)))
with self.assertRaises(Exception):
client.sendMessage('test_send_user_should_fail★', user_id, ThreadType.GROUP) self.assertIsNotNone(client.send(Message(text='Hi there @user', mentions=[Mention(user_id, offset=9, length=5)])))
with self.assertRaises(Exception): self.assertIsNotNone(client.send(Message(text='Hi there @group', mentions=[Mention(group_id, offset=9, length=6)])))
client.sendMessage('test_send_group_should_fail★', group_id, ThreadType.USER)
self.assertIsNotNone(client.send(Message(sticker=Sticker(test_sticker_id))))
def test_sendImages(self): def test_sendImages(self):
image_url = 'https://cdn4.iconfinder.com/data/icons/ionicons/512/icon-image-128.png' image_url = 'https://cdn4.iconfinder.com/data/icons/ionicons/512/icon-image-128.png'
image_local_url = path.join(path.dirname(__file__), 'tests/image.png') image_local_url = path.join(path.dirname(__file__), 'tests/image.png')
self.assertTrue(client.sendRemoteImage(image_url, 'test_send_user_images_remote★', user_id, ThreadType.USER)) for thread in threads:
self.assertTrue(client.sendRemoteImage(image_url, 'test_send_group_images_remote★', group_id, ThreadType.GROUP)) client.setDefaultThread(thread_id=thread['id'], thread_type=thread['type'])
self.assertTrue(client.sendLocalImage(image_local_url, 'test_send_group_images_local★', user_id, ThreadType.USER)) mentions = [Mention(thread['id'], offset=26, length=4)]
self.assertTrue(client.sendLocalImage(image_local_url, 'test_send_group_images_local★', group_id, ThreadType.GROUP)) self.assertTrue(client.sendRemoteImage(image_url, Message(text='test_send_image_remote_to_@you★', mentions=mentions)))
self.assertTrue(client.sendLocalImage(image_local_url, Message(text='test_send_image_local_to__@you★', mentions=mentions)))
def test_fetchThreadList(self): def test_fetchThreadList(self):
client.fetchThreadList(offset=0, limit=20) threads = client.fetchThreadList(limit=2)
self.assertEqual(len(threads), 2)
def test_fetchThreadMessages(self): def test_fetchThreadMessages(self):
client.sendMessage('test_user_getThreadInfo★', thread_id=user_id, thread_type=ThreadType.USER) for thread in threads:
client.setDefaultThread(thread_id=thread['id'], thread_type=thread['type'])
client.send(Message(text='test_getThreadInfo★'))
messages = client.fetchThreadMessages(thread_id=user_id, limit=1) messages = client.fetchThreadMessages(limit=1)
self.assertEqual(messages[0].author, client.uid) self.assertEqual(messages[0].author, client.uid)
self.assertEqual(messages[0].text, 'test_user_getThreadInfo★') self.assertEqual(messages[0].text, 'test_getThreadInfo★')
client.sendMessage('test_group_getThreadInfo★', thread_id=group_id, thread_type=ThreadType.GROUP)
messages = client.fetchThreadMessages(thread_id=group_id, limit=1)
self.assertEqual(messages[0].author, client.uid)
self.assertEqual(messages[0].text, 'test_group_getThreadInfo★')
def test_listen(self): def test_listen(self):
client.startListening() client.startListening()
@@ -156,48 +154,48 @@ class TestFbchat(unittest.TestCase):
client.addUsersToGroup(user_id, thread_id=group_id) client.addUsersToGroup(user_id, thread_id=group_id)
def test_changeThreadTitle(self): def test_changeThreadTitle(self):
client.changeThreadTitle('test_changeThreadTitle★', thread_id=group_id, thread_type=ThreadType.GROUP) for thread in threads:
client.changeThreadTitle('test_changeThreadTitle★', thread_id=user_id, thread_type=ThreadType.USER) client.changeThreadTitle('test_changeThreadTitle★', thread_id=thread['id'], thread_type=thread['type'])
def test_changeNickname(self): def test_changeNickname(self):
client.changeNickname('test_changeNicknameSelf★', client.uid, thread_id=user_id, thread_type=ThreadType.USER) for thread in threads:
client.changeNickname('test_changeNicknameOther★', user_id, thread_id=user_id, thread_type=ThreadType.USER) client.setDefaultThread(thread_id=thread['id'], thread_type=thread['type'])
client.changeNickname('test_changeNicknameSelf★', client.uid, thread_id=group_id, thread_type=ThreadType.GROUP) client.changeNickname('test_changeNicknameSelf★', client.uid)
client.changeNickname('test_changeNicknameOther★', user_id, thread_id=group_id, thread_type=ThreadType.GROUP) client.changeNickname('test_changeNicknameOther★', user_id)
def test_changeThreadEmoji(self): def test_changeThreadEmoji(self):
client.changeThreadEmoji('😀', group_id) for thread in threads:
client.changeThreadEmoji('😀', user_id) client.changeThreadEmoji('😀', thread_id=thread['id'])
client.changeThreadEmoji('😆', group_id) client.changeThreadEmoji('😀', thread_id=thread['id'])
client.changeThreadEmoji('😆', user_id)
def test_changeThreadColor(self): def test_changeThreadColor(self):
client.changeThreadColor(ThreadColor.BRILLIANT_ROSE, group_id) for thread in threads:
client.changeThreadColor(ThreadColor.MESSENGER_BLUE, group_id) client.setDefaultThread(thread_id=thread['id'], thread_type=thread['type'])
client.changeThreadColor(ThreadColor.BRILLIANT_ROSE, user_id) client.changeThreadColor(ThreadColor.BRILLIANT_ROSE)
client.changeThreadColor(ThreadColor.MESSENGER_BLUE, user_id) client.changeThreadColor(ThreadColor.MESSENGER_BLUE)
def test_reactToMessage(self): def test_reactToMessage(self):
mid = client.sendMessage('test_reactToMessage★', user_id, ThreadType.USER) for thread in threads:
client.reactToMessage(mid, MessageReaction.LOVE) mid = client.send(Message(text='test_reactToMessage★'), thread_id=thread['id'], thread_type=thread['type'])
mid = client.sendMessage('test_reactToMessage★', group_id, ThreadType.GROUP) client.reactToMessage(mid, MessageReaction.LOVE)
client.reactToMessage(mid, MessageReaction.LOVE)
def test_setTypingStatus(self): def test_setTypingStatus(self):
client.setTypingStatus(TypingStatus.TYPING, thread_id=user_id, thread_type=ThreadType.USER) for thread in threads:
client.setTypingStatus(TypingStatus.STOPPED, thread_id=user_id, thread_type=ThreadType.USER) client.setDefaultThread(thread_id=thread['id'], thread_type=thread['type'])
client.setTypingStatus(TypingStatus.TYPING, thread_id=group_id, thread_type=ThreadType.GROUP) client.setTypingStatus(TypingStatus.TYPING)
client.setTypingStatus(TypingStatus.STOPPED, thread_id=group_id, thread_type=ThreadType.GROUP) client.setTypingStatus(TypingStatus.STOPPED)
def start_test(param_client, param_group_id, param_user_id, tests=[]): def start_test(param_client, param_group_id, param_user_id, param_threads, tests=[]):
global client global client
global group_id global group_id
global user_id global user_id
global threads
client = param_client client = param_client
group_id = param_group_id group_id = param_group_id
user_id = param_user_id user_id = param_user_id
threads = param_threads
tests = ['test_' + test if 'test_' != test[:5] else test for test in tests] tests = ['test_' + test if 'test_' != test[:5] else test for test in tests]
@@ -220,19 +218,44 @@ if __name__ == '__main__':
try: try:
with open(path.join(path.dirname(__file__), 'tests/my_data.json'), 'r') as f: with open(path.join(path.dirname(__file__), 'tests/my_data.json'), 'r') as f:
json = json.load(f) j = json.load(f)
email = json['email'] email = j['email']
password = json['password'] password = j['password']
user_id = json['user_thread_id'] user_id = j['user_thread_id']
group_id = json['group_thread_id'] group_id = j['group_thread_id']
session = j.get('session')
except (IOError, IndexError) as e: except (IOError, IndexError) as e:
email = input('Email: ') email = input('Email: ')
password = getpass() password = getpass()
group_id = input('Please enter a group thread id (To test group functionality): ') group_id = input('Please enter a group thread id (To test group functionality): ')
user_id = input('Please enter a user thread id (To test kicking/adding functionality): ') user_id = input('Please enter a user thread id (To test kicking/adding functionality): ')
threads = [
{
'id': user_id,
'type': ThreadType.USER
},
{
'id': group_id,
'type': ThreadType.GROUP
}
]
print('Logging in...') print('Logging in...')
client = CustomClient(email, password, logging_level=logging_level) client = CustomClient(email, password, logging_level=logging_level, session_cookies=session)
# Warning! Taking user input directly like this could be dangerous! Use only for testing purposes! # Warning! Taking user input directly like this could be dangerous! Use only for testing purposes!
start_test(client, group_id, user_id, argv[1:]) start_test(client, group_id, user_id, threads, argv[1:])
with open(path.join(path.dirname(__file__), 'tests/my_data.json'), 'w') as f:
session = None
try:
session = client.getSession()
except Exception:
print('Unable to fetch client session!')
json.dump({
'email': email,
'password': password,
'user_thread_id': user_id,
'group_thread_id': group_id,
'session': session
}, f)