diff --git a/docs/index.rst b/docs/index.rst index 473777aa..e7b1ed9f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1275,6 +1275,19 @@ The streaming functions take instances of `StreamListener` as the `listener` par A `CallbackStreamListener` class that allows you to specify function callbacks directly is included for convenience. +For new well-known events implement the streaming function in `StreamListener` or `CallbackStreamListener`. +The function name is `on_` + the event name. If the event-name contains dots, use an underscore instead. + +E.g. for `'status.update'` the listener function should be named as `on_status_update`. + +It may be that future Mastodon versions will come with completely new (unknown) event names. In this +case a (deprecated) Mastodon.py would throw an error. If you want to avoid this in general, you can +override the listener function `on_unknown_event`. This has an additional parameter `name` which informs +about the name of the event. `unknown_event` contains the content of the event. + +Alternatively, a callback function can be passed in the `unknown_event_handler` parameter in the +`CallbackStreamListener` constructor. + When in not-async mode or async mode without async_reconnect, the stream functions may raise various exceptions: `MastodonMalformedEventError` if a received event cannot be parsed and `MastodonNetworkError` if any connection problems occur. @@ -1294,6 +1307,7 @@ StreamListener .. automethod:: StreamListener.on_notification .. automethod:: StreamListener.on_delete .. automethod:: StreamListener.on_conversation +.. automethod:: StreamListener.on_unknown_event .. automethod:: StreamListener.on_abort .. automethod:: StreamListener.handle_heartbeat diff --git a/mastodon/streaming.py b/mastodon/streaming.py index 214ed1c3..ceb61eaa 100644 --- a/mastodon/streaming.py +++ b/mastodon/streaming.py @@ -45,6 +45,16 @@ def on_conversation(self, conversation): contains the resulting conversation dict.""" pass + def on_unknown_event(self, name, unknown_event = None): + """An unknown mastodon API event has been received. The name contains the event-name and unknown_event + contains the content of the unknown event. + + This function must be implemented, if unknown events should be handled without an error. + """ + exception = MastodonMalformedEventError('Bad event type', name) + self.on_abort(exception) + raise exception + def handle_heartbeat(self): """The server has sent us a keep-alive message. This callback may be useful to carry out periodic housekeeping tasks, or just to confirm @@ -56,6 +66,11 @@ def handle_stream(self, response): Handles a stream of events from the Mastodon server. When each event is received, the corresponding .on_[name]() method is called. + When the Mastodon API changes, the on_unknown_event(name, content) + function is called. + The default behavior is to throw an error. Define a callback handler + to intercept unknown events if needed (and avoid errors) + response; a requests response object with the open stream for reading. """ event = {} @@ -137,33 +152,32 @@ def _dispatch(self, event): exception, err ) - - handler_name = 'on_' + name - try: - handler = getattr(self, handler_name) - except AttributeError as err: - exception = MastodonMalformedEventError('Bad event type', name) - self.on_abort(exception) - six.raise_from( - exception, - err - ) - else: + # New mastodon API also supports event names with dots: + handler_name = 'on_' + name.replace('.', '_') + # A generic way to handle unknown events to make legacy code more stable for future changes + handler = getattr(self, handler_name, self.on_unknown_event) + if handler != self.on_unknown_event: handler(payload) + else: + handler(name, payload) + class CallbackStreamListener(StreamListener): """ Simple callback stream handler class. Can optionally additionally send local update events to a separate handler. + Define an unknown_event_handler for new Mastodon API events. If not, the + listener will raise an error on new, not handled, events from the API. """ - def __init__(self, update_handler = None, local_update_handler = None, delete_handler = None, notification_handler = None, conversation_handler = None): + def __init__(self, update_handler = None, local_update_handler = None, delete_handler = None, notification_handler = None, conversation_handler = None, unknown_event_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 self.conversation_handler = conversation_handler - + self.unknown_event_handler = unknown_event_handler + def on_update(self, status): if self.update_handler != None: self.update_handler(status) @@ -188,3 +202,11 @@ def on_notification(self, notification): def on_conversation(self, conversation): if self.conversation_handler != None: self.conversation_handler(conversation) + + def on_unknown_event(self, name, unknown_event = None): + if self.unknown_event_handler != None: + self.unknown_event_handler(name, unknown_event) + else: + exception = MastodonMalformedEventError('Bad event type', name) + self.on_abort(exception) + raise exception diff --git a/tests/test_streaming.py b/tests/test_streaming.py index cddb79af..8912b9cc 100644 --- a/tests/test_streaming.py +++ b/tests/test_streaming.py @@ -61,6 +61,8 @@ def __init__(self): self.notifications = [] self.deletes = [] self.heartbeats = 0 + self.bla_called = False + self.do_something_called = False def on_update(self, status): self.updates.append(status) @@ -72,6 +74,11 @@ def on_delete(self, status_id): self.deletes.append(status_id) def on_blahblah(self, data): + self.bla_called = True + pass + + def on_do_something(self, data): + self.do_something_called = True pass def handle_heartbeat(self): @@ -158,6 +165,37 @@ def test_unknown_event(): 'data: {}', '', ]) + assert listener.bla_called == True + assert listener.updates == [] + assert listener.notifications == [] + assert listener.deletes == [] + assert listener.heartbeats == 0 + +def test_unknown_handled_event(): + """Be tolerant of new unknown event types, if on_unknown_event is available""" + listener = Listener() + listener.on_unknown_event = lambda name, payload: None + + listener.handle_stream_([ + 'event: complete.new.event', + 'data: {"k": "v"}', + '', + ]) + + assert listener.updates == [] + assert listener.notifications == [] + assert listener.deletes == [] + assert listener.heartbeats == 0 + +def test_dotted_unknown_event(): + """Be tolerant of new event types with dots in the event-name""" + listener = Listener() + listener.handle_stream_([ + 'event: do.something', + 'data: {}', + '', + ]) + assert listener.do_something_called == True assert listener.updates == [] assert listener.notifications == [] assert listener.deletes == [] @@ -169,7 +207,7 @@ def test_invalid_event(): with pytest.raises(MastodonMalformedEventError): listener.handle_stream_([ 'event: whatup', - 'data: {}', + 'data: {"k": "v"}', '', ])