2017-09-05 22:59:32 +02:00
|
|
|
"""
|
2017-04-09 11:21:56 +02:00
|
|
|
Handlers for the Streaming API:
|
|
|
|
https://github.com/tootsuite/mastodon/blob/master/docs/Using-the-API/Streaming-API.md
|
2017-09-05 22:59:32 +02:00
|
|
|
"""
|
2017-04-09 11:21:56 +02:00
|
|
|
|
|
|
|
import json
|
|
|
|
import six
|
2017-11-24 13:59:13 +01:00
|
|
|
from mastodon import Mastodon
|
2018-04-19 17:10:42 +02:00
|
|
|
from mastodon.Mastodon import MastodonMalformedEventError, MastodonNetworkError, MastodonReadTimeout
|
|
|
|
from requests.exceptions import ChunkedEncodingError, ReadTimeout
|
2017-04-09 11:21:56 +02:00
|
|
|
|
|
|
|
class StreamListener(object):
|
2017-09-05 22:59:32 +02:00
|
|
|
"""Callbacks for the streaming API. Create a subclass, override the on_xxx
|
2017-04-09 11:21:56 +02:00
|
|
|
methods for the kinds of events you're interested in, then pass an instance
|
|
|
|
of your subclass to Mastodon.user_stream(), Mastodon.public_stream(), or
|
2017-09-05 22:59:32 +02:00
|
|
|
Mastodon.hashtag_stream()."""
|
2017-04-09 11:21:56 +02:00
|
|
|
|
|
|
|
def on_update(self, status):
|
2017-09-05 22:59:32 +02:00
|
|
|
"""A new status has appeared! 'status' is the parsed JSON dictionary
|
2017-11-24 13:59:13 +01:00
|
|
|
describing the status."""
|
2017-04-09 11:21:56 +02:00
|
|
|
pass
|
|
|
|
|
|
|
|
def on_notification(self, notification):
|
2017-09-05 22:59:32 +02:00
|
|
|
"""A new notification. 'notification' is the parsed JSON dictionary
|
|
|
|
describing the notification."""
|
2017-04-09 11:21:56 +02:00
|
|
|
pass
|
|
|
|
|
2018-06-04 17:58:11 +02:00
|
|
|
def on_abort(self):
|
|
|
|
"""Some error happened that requires that the connection should
|
|
|
|
be aborted (or re-established)"""
|
|
|
|
pass
|
|
|
|
|
2017-04-09 11:21:56 +02:00
|
|
|
def on_delete(self, status_id):
|
2017-09-05 22:59:32 +02:00
|
|
|
"""A status has been deleted. status_id is the status' integer ID."""
|
2017-04-09 11:21:56 +02:00
|
|
|
pass
|
|
|
|
|
|
|
|
def handle_heartbeat(self):
|
2017-09-05 22:59:32 +02:00
|
|
|
"""The server has sent us a keep-alive message. This callback may be
|
2017-04-09 11:21:56 +02:00
|
|
|
useful to carry out periodic housekeeping tasks, or just to confirm
|
2017-09-05 22:59:32 +02:00
|
|
|
that the connection is still open."""
|
2017-11-24 13:59:13 +01:00
|
|
|
pass
|
|
|
|
|
2017-12-19 13:49:00 +01:00
|
|
|
def handle_stream(self, response):
|
2017-09-05 22:59:32 +02:00
|
|
|
"""
|
2017-04-09 11:21:56 +02:00
|
|
|
Handles a stream of events from the Mastodon server. When each event
|
|
|
|
is received, the corresponding .on_[name]() method is called.
|
|
|
|
|
2017-12-19 13:49:00 +01:00
|
|
|
response; a requests response object with the open stream for reading.
|
2017-09-05 22:59:32 +02:00
|
|
|
"""
|
2018-01-29 12:24:53 +01:00
|
|
|
event = {}
|
2017-12-19 13:49:00 +01:00
|
|
|
line_buffer = bytearray()
|
2018-02-20 14:04:17 +01:00
|
|
|
try:
|
|
|
|
for chunk in response.iter_content(chunk_size = 1):
|
|
|
|
if chunk:
|
|
|
|
if chunk == b'\n':
|
|
|
|
try:
|
|
|
|
line = line_buffer.decode('utf-8')
|
|
|
|
except UnicodeDecodeError as err:
|
|
|
|
six.raise_from(
|
|
|
|
MastodonMalformedEventError("Malformed UTF-8"),
|
|
|
|
err
|
|
|
|
)
|
|
|
|
if line == '':
|
|
|
|
self._dispatch(event)
|
|
|
|
event = {}
|
|
|
|
else:
|
|
|
|
event = self._parse_line(line, event)
|
|
|
|
line_buffer = bytearray()
|
2018-01-29 12:24:53 +01:00
|
|
|
else:
|
2018-02-20 14:04:17 +01:00
|
|
|
line_buffer.extend(chunk)
|
|
|
|
except ChunkedEncodingError as err:
|
2018-06-04 17:58:11 +02:00
|
|
|
self.on_abort()
|
2018-02-20 14:04:17 +01:00
|
|
|
six.raise_from(
|
|
|
|
MastodonNetworkError("Server ceased communication."),
|
|
|
|
err
|
|
|
|
)
|
2018-04-19 17:10:42 +02:00
|
|
|
except MastodonReadTimeout as err:
|
2018-06-04 17:58:11 +02:00
|
|
|
self.on_abort()
|
2018-04-19 17:10:42 +02:00
|
|
|
six.raise_from(
|
|
|
|
MastodonReadTimeout("Timed out while reading from server."),
|
|
|
|
err
|
|
|
|
)
|
|
|
|
|
2018-01-29 12:24:53 +01:00
|
|
|
def _parse_line(self, line, event):
|
2017-12-19 13:49:00 +01:00
|
|
|
if line.startswith(':'):
|
|
|
|
self.handle_heartbeat()
|
|
|
|
else:
|
|
|
|
key, value = line.split(': ', 1)
|
|
|
|
# According to the MDN spec, repeating the 'data' key
|
|
|
|
# represents a newline(!)
|
2018-01-29 12:24:53 +01:00
|
|
|
if key in event:
|
|
|
|
event[key] += '\n' + value
|
2017-04-09 11:21:56 +02:00
|
|
|
else:
|
2018-01-29 12:24:53 +01:00
|
|
|
event[key] = value
|
|
|
|
return event
|
|
|
|
|
2017-11-24 13:59:13 +01:00
|
|
|
def _dispatch(self, event):
|
2017-04-09 11:21:56 +02:00
|
|
|
try:
|
|
|
|
name = event['event']
|
|
|
|
data = event['data']
|
2017-11-24 13:59:13 +01:00
|
|
|
payload = json.loads(data, object_hook = Mastodon._Mastodon__json_hooks)
|
2017-04-09 11:21:56 +02:00
|
|
|
except KeyError as err:
|
2017-11-24 13:59:13 +01:00
|
|
|
six.raise_from(
|
2017-11-24 14:20:27 +01:00
|
|
|
MastodonMalformedEventError('Missing field', err.args[0], event),
|
2017-11-24 13:59:13 +01:00
|
|
|
err
|
|
|
|
)
|
2017-04-09 11:21:56 +02:00
|
|
|
except ValueError as err:
|
2017-11-24 13:59:13 +01:00
|
|
|
# py2: plain ValueError
|
|
|
|
# py3: json.JSONDecodeError, a subclass of ValueError
|
|
|
|
six.raise_from(
|
2017-11-24 14:20:27 +01:00
|
|
|
MastodonMalformedEventError('Bad JSON', data),
|
2017-11-24 13:59:13 +01:00
|
|
|
err
|
|
|
|
)
|
|
|
|
|
2017-04-09 11:21:56 +02:00
|
|
|
handler_name = 'on_' + name
|
|
|
|
try:
|
|
|
|
handler = getattr(self, handler_name)
|
2017-11-24 15:08:34 +01:00
|
|
|
except AttributeError as err:
|
|
|
|
six.raise_from(
|
|
|
|
MastodonMalformedEventError('Bad event type', name),
|
|
|
|
err
|
|
|
|
)
|
2017-04-09 11:21:56 +02:00
|
|
|
else:
|
|
|
|
handler(payload)
|
2017-11-24 15:08:34 +01:00
|
|
|
|
|
|
|
class CallbackStreamListener(StreamListener):
|
|
|
|
"""
|
|
|
|
Simple callback stream handler class.
|
|
|
|
Can optionally additionally send local update events to a separate handler.
|
|
|
|
"""
|
|
|
|
def __init__(self, update_handler = None, local_update_handler = None, delete_handler = None, notification_handler = None):
|
|
|
|
super(CallbackStreamListener, self).__init__()
|
|
|
|
self.update_handler = update_handler
|
|
|
|
self.local_update_handler = local_update_handler
|
|
|
|
self.delete_handler = delete_handler
|
|
|
|
self.notification_handler = notification_handler
|
|
|
|
|
|
|
|
def on_update(self, status):
|
|
|
|
if self.update_handler != None:
|
|
|
|
self.update_handler(status)
|
|
|
|
|
|
|
|
try:
|
|
|
|
if self.local_update_handler != None and not "@" in status["account"]["acct"]:
|
|
|
|
self.local_update_handler(status)
|
|
|
|
except Exception as err:
|
|
|
|
six.raise_from(
|
|
|
|
MastodonMalformedEventError('received bad update', status),
|
|
|
|
err
|
|
|
|
)
|
|
|
|
|
|
|
|
def on_delete(self, deleted_id):
|
|
|
|
if self.delete_handler != None:
|
|
|
|
self.delete_handler(deleted_id)
|
|
|
|
|
|
|
|
def on_notification(self, notification):
|
|
|
|
if self.notification_handler != None:
|
|
|
|
self.notification_handler(notification)
|