diff options
-rw-r--r-- | docs/index.rst | 23 | ||||
-rw-r--r-- | mastodon/Mastodon.py | 4 | ||||
-rw-r--r-- | mastodon/streaming.py | 60 |
3 files changed, 66 insertions, 21 deletions
diff --git a/docs/index.rst b/docs/index.rst index 4d7073c..fc8c003 100644 --- a/docs/index.rst +++ b/docs/index.rst | |||
@@ -546,6 +546,26 @@ Push subscription dicts | |||
546 | # if webpushes have been requested for those events. | 546 | # if webpushes have been requested for those events. |
547 | } | 547 | } |
548 | 548 | ||
549 | Push notification dicts | ||
550 | ~~~~~~~~~~~~~~~~~~~~~~~ | ||
551 | .. _push notification dict: | ||
552 | |||
553 | .. code-block:: python | ||
554 | |||
555 | mastodon.push_subscription_decrypt_push(...) | ||
556 | # Returns the following dictionary | ||
557 | { | ||
558 | 'access_token': # Access token that can be used to access the API as the | ||
559 | # notified user | ||
560 | 'body': # Text body of the notification | ||
561 | 'icon': # URL to an icon for the notification | ||
562 | 'notification_id': # ID that can be passed to notification() to get the full | ||
563 | # notification object, | ||
564 | 'notification_type': # 'mention', 'reblog', 'follow' or 'favourite' | ||
565 | 'preferred_locale': # The users preferred locale | ||
566 | 'title': # Title for the notification | ||
567 | } | ||
568 | |||
549 | App registration and user authentication | 569 | App registration and user authentication |
550 | ---------------------------------------- | 570 | ---------------------------------------- |
551 | Before you can use the mastodon API, you have to register your | 571 | Before you can use the mastodon API, you have to register your |
@@ -795,7 +815,8 @@ will return a handle corresponding to the open connection. If, in addition, `asy | |||
795 | the thread will attempt to reconnect to the streaming API if any errors are encountered, waiting | 815 | the thread will attempt to reconnect to the streaming API if any errors are encountered, waiting |
796 | `async_reconnect_wait_sec` seconds between reconnection attempts. Note that no effort is made | 816 | `async_reconnect_wait_sec` seconds between reconnection attempts. Note that no effort is made |
797 | to "catch up" - events created while the connection is broken will not be received. If you need to make | 817 | to "catch up" - events created while the connection is broken will not be received. If you need to make |
798 | sure to get absolutely all notifications / deletes / toots, you will have to do that manually. | 818 | sure to get absolutely all notifications / deletes / toots, you will have to do that manually, e.g. |
819 | using the `on_abort` handler to fill in events since the last received one and then reconnecting. | ||
799 | 820 | ||
800 | The connection may be closed at any time by calling the handles close() method. The | 821 | The connection may be closed at any time by calling the handles close() method. The |
801 | current status of the handler thread can be checked with the handles is_alive() function, | 822 | current status of the handler thread can be checked with the handles is_alive() function, |
diff --git a/mastodon/Mastodon.py b/mastodon/Mastodon.py index c2da613..b5c82f8 100644 --- a/mastodon/Mastodon.py +++ b/mastodon/Mastodon.py | |||
@@ -132,6 +132,7 @@ class Mastodon: | |||
132 | __DICT_VERSION_ACTIVITY = "2.1.2" | 132 | __DICT_VERSION_ACTIVITY = "2.1.2" |
133 | __DICT_VERSION_REPORT = "1.1.0" | 133 | __DICT_VERSION_REPORT = "1.1.0" |
134 | __DICT_VERSION_PUSH = "2.4.0" | 134 | __DICT_VERSION_PUSH = "2.4.0" |
135 | __DICT_VERSION_PUSH_NOTIF = "2.4.0" | ||
135 | 136 | ||
136 | ### | 137 | ### |
137 | # Registering apps | 138 | # Registering apps |
@@ -1609,13 +1610,14 @@ class Mastodon: | |||
1609 | 1610 | ||
1610 | return priv_dict, pub_dict | 1611 | return priv_dict, pub_dict |
1611 | 1612 | ||
1613 | @api_version("2.4.0", "2.4.0", __DICT_VERSION_PUSH_NOTIF) | ||
1612 | def push_subscription_decrypt_push(self, data, decrypt_params, encryption_header, crypto_key_header): | 1614 | def push_subscription_decrypt_push(self, data, decrypt_params, encryption_header, crypto_key_header): |
1613 | """ | 1615 | """ |
1614 | Decrypts `data` received in a webpush request. Requires the private key dict | 1616 | Decrypts `data` received in a webpush request. Requires the private key dict |
1615 | from `push_subscription_generate_keys()`_ (`decrypt_params`) as well as the | 1617 | from `push_subscription_generate_keys()`_ (`decrypt_params`) as well as the |
1616 | Encryption and server Crypto-Key headers from the received webpush | 1618 | Encryption and server Crypto-Key headers from the received webpush |
1617 | 1619 | ||
1618 | Returns the decoded webpush. | 1620 | Returns the decoded webpush as a `push notification dict`_. |
1619 | """ | 1621 | """ |
1620 | salt = self.__decode_webpush_b64(encryption_header.split("salt=")[1].strip()) | 1622 | salt = self.__decode_webpush_b64(encryption_header.split("salt=")[1].strip()) |
1621 | dhparams = self.__decode_webpush_b64(crypto_key_header.split("dh=")[1].split(";")[0].strip()) | 1623 | dhparams = self.__decode_webpush_b64(crypto_key_header.split("dh=")[1].split(";")[0].strip()) |
diff --git a/mastodon/streaming.py b/mastodon/streaming.py index 8c3ec19..65ec30a 100644 --- a/mastodon/streaming.py +++ b/mastodon/streaming.py | |||
@@ -25,8 +25,15 @@ class StreamListener(object): | |||
25 | describing the notification.""" | 25 | describing the notification.""" |
26 | pass | 26 | pass |
27 | 27 | ||
28 | def on_abort(self): | 28 | def on_abort(self, err): |
29 | """There was a connection error or read timeout.""" | 29 | """There was a connection error, read timeout or other error fatal to |
30 | the streaming connection. The exception object about to be raised | ||
31 | is passed to this function for reference. | ||
32 | |||
33 | Note that the exception will be raised properly once you return from this | ||
34 | function, so if you are using this handler to reconnect, either never | ||
35 | return or start a thread and then catch and ignore the exception. | ||
36 | """ | ||
30 | pass | 37 | pass |
31 | 38 | ||
32 | def on_delete(self, status_id): | 39 | def on_delete(self, status_id): |
@@ -55,8 +62,10 @@ class StreamListener(object): | |||
55 | try: | 62 | try: |
56 | line = line_buffer.decode('utf-8') | 63 | line = line_buffer.decode('utf-8') |
57 | except UnicodeDecodeError as err: | 64 | except UnicodeDecodeError as err: |
65 | exception = MastodonMalformedEventError("Malformed UTF-8") | ||
66 | self.on_abort(exception) | ||
58 | six.raise_from( | 67 | six.raise_from( |
59 | MastodonMalformedEventError("Malformed UTF-8"), | 68 | exception, |
60 | err | 69 | err |
61 | ) | 70 | ) |
62 | if line == '': | 71 | if line == '': |
@@ -68,15 +77,17 @@ class StreamListener(object): | |||
68 | else: | 77 | else: |
69 | line_buffer.extend(chunk) | 78 | line_buffer.extend(chunk) |
70 | except ChunkedEncodingError as err: | 79 | except ChunkedEncodingError as err: |
71 | self.on_abort() | 80 | exception = MastodonNetworkError("Server ceased communication.") |
81 | self.on_abort(exception) | ||
72 | six.raise_from( | 82 | six.raise_from( |
73 | MastodonNetworkError("Server ceased communication."), | 83 | exception, |
74 | err | 84 | err |
75 | ) | 85 | ) |
76 | except MastodonReadTimeout as err: | 86 | except MastodonReadTimeout as err: |
77 | self.on_abort() | 87 | exception = MastodonReadTimeout("Timed out while reading from server."), |
88 | self.on_abort(exception) | ||
78 | six.raise_from( | 89 | six.raise_from( |
79 | MastodonReadTimeout("Timed out while reading from server."), | 90 | exception, |
80 | err | 91 | err |
81 | ) | 92 | ) |
82 | 93 | ||
@@ -84,7 +95,12 @@ class StreamListener(object): | |||
84 | if line.startswith(':'): | 95 | if line.startswith(':'): |
85 | self.handle_heartbeat() | 96 | self.handle_heartbeat() |
86 | else: | 97 | else: |
87 | key, value = line.split(': ', 1) | 98 | try: |
99 | key, value = line.split(': ', 1) | ||
100 | except: | ||
101 | exception = MastodonMalformedEventError("Malformed event.") | ||
102 | self.on_abort(exception) | ||
103 | raise exception | ||
88 | # According to the MDN spec, repeating the 'data' key | 104 | # According to the MDN spec, repeating the 'data' key |
89 | # represents a newline(!) | 105 | # represents a newline(!) |
90 | if key in event: | 106 | if key in event: |
@@ -99,24 +115,30 @@ class StreamListener(object): | |||
99 | data = event['data'] | 115 | data = event['data'] |
100 | payload = json.loads(data, object_hook = Mastodon._Mastodon__json_hooks) | 116 | payload = json.loads(data, object_hook = Mastodon._Mastodon__json_hooks) |
101 | except KeyError as err: | 117 | except KeyError as err: |
102 | six.raise_from( | 118 | exception = MastodonMalformedEventError('Missing field', err.args[0], event) |
103 | MastodonMalformedEventError('Missing field', err.args[0], event), | 119 | self.on_abort(exception) |
104 | err | 120 | six.raise_from( |
105 | ) | 121 | exception, |
122 | err | ||
123 | ) | ||
106 | except ValueError as err: | 124 | except ValueError as err: |
107 | # py2: plain ValueError | 125 | # py2: plain ValueError |
108 | # py3: json.JSONDecodeError, a subclass of ValueError | 126 | # py3: json.JSONDecodeError, a subclass of ValueError |
109 | six.raise_from( | 127 | exception = MastodonMalformedEventError('Bad JSON', data) |
110 | MastodonMalformedEventError('Bad JSON', data), | 128 | self.on_abort(exception) |
111 | err | 129 | six.raise_from( |
112 | ) | 130 | exception, |
131 | err | ||
132 | ) | ||
113 | 133 | ||
114 | handler_name = 'on_' + name | 134 | handler_name = 'on_' + name |
115 | try: | 135 | try: |
116 | handler = getattr(self, handler_name) | 136 | handler = getattr(self, handler_name) |
117 | except AttributeError as err: | 137 | except AttributeError as err: |
138 | exception = MastodonMalformedEventError('Bad event type', name) | ||
139 | self.on_abort(exception) | ||
118 | six.raise_from( | 140 | six.raise_from( |
119 | MastodonMalformedEventError('Bad event type', name), | 141 | exception, |
120 | err | 142 | err |
121 | ) | 143 | ) |
122 | else: | 144 | else: |