aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'mastodon')
-rw-r--r--mastodon/Mastodon.py102
-rw-r--r--mastodon/streaming.py50
2 files changed, 118 insertions, 34 deletions
diff --git a/mastodon/Mastodon.py b/mastodon/Mastodon.py
index ab9ac1f..3851fbf 100644
--- a/mastodon/Mastodon.py
+++ b/mastodon/Mastodon.py
@@ -120,10 +120,27 @@ class AttribAccessDict(dict):
120 raise AttributeError("Attribute-style access is read only") 120 raise AttributeError("Attribute-style access is read only")
121 super(AttribAccessDict, self).__setattr__(attr, val) 121 super(AttribAccessDict, self).__setattr__(attr, val)
122 122
123
123### 124###
124# The actual Mastodon class 125# List helper class.
126# Defined at top level so it can be pickled.
125### 127###
128class AttribAccessList(list):
129 def __getattr__(self, attr):
130 if attr in self:
131 return self[attr]
132 else:
133 raise AttributeError("Attribute not found: " + str(attr))
126 134
135 def __setattr__(self, attr, val):
136 if attr in self:
137 raise AttributeError("Attribute-style access is read only")
138 super(AttribAccessList, self).__setattr__(attr, val)
139
140
141###
142# The actual Mastodon class
143###
127class Mastodon: 144class Mastodon:
128 """ 145 """
129 Thorough and easy to use Mastodon 146 Thorough and easy to use Mastodon
@@ -1636,13 +1653,23 @@ class Mastodon:
1636 # Reading data: Bookmarks 1653 # Reading data: Bookmarks
1637 ### 1654 ###
1638 @api_version("3.1.0", "3.1.0", __DICT_VERSION_STATUS) 1655 @api_version("3.1.0", "3.1.0", __DICT_VERSION_STATUS)
1639 def bookmarks(self): 1656 def bookmarks(self, max_id=None, min_id=None, since_id=None, limit=None):
1640 """ 1657 """
1641 Get a list of statuses bookmarked by the logged-in user. 1658 Get a list of statuses bookmarked by the logged-in user.
1642 1659
1643 Returns a list of `toot dicts`_. 1660 Returns a list of `toot dicts`_.
1644 """ 1661 """
1645 return self.__api_request('GET', '/api/v1/bookmarks') 1662 if max_id != None:
1663 max_id = self.__unpack_id(max_id)
1664
1665 if min_id != None:
1666 min_id = self.__unpack_id(min_id)
1667
1668 if since_id != None:
1669 since_id = self.__unpack_id(since_id)
1670
1671 params = self.__generate_params(locals())
1672 return self.__api_request('GET', '/api/v1/bookmarks', params)
1646 1673
1647 ### 1674 ###
1648 # Writing data: Statuses 1675 # Writing data: Statuses
@@ -3043,8 +3070,8 @@ class Mastodon:
3043 Returns the next page or None if no further data is available. 3070 Returns the next page or None if no further data is available.
3044 """ 3071 """
3045 if isinstance(previous_page, list) and len(previous_page) != 0: 3072 if isinstance(previous_page, list) and len(previous_page) != 0:
3046 if hasattr(previous_page[-1], '_pagination_next'): 3073 if hasattr(previous_page, '_pagination_next'):
3047 params = copy.deepcopy(previous_page[-1]._pagination_next) 3074 params = copy.deepcopy(previous_page._pagination_next)
3048 else: 3075 else:
3049 return None 3076 return None
3050 else: 3077 else:
@@ -3067,8 +3094,8 @@ class Mastodon:
3067 Returns the previous page or None if no further data is available. 3094 Returns the previous page or None if no further data is available.
3068 """ 3095 """
3069 if isinstance(next_page, list) and len(next_page) != 0: 3096 if isinstance(next_page, list) and len(next_page) != 0:
3070 if hasattr(next_page[0], '_pagination_prev'): 3097 if hasattr(next_page, '_pagination_prev'):
3071 params = copy.deepcopy(next_page[0]._pagination_prev) 3098 params = copy.deepcopy(next_page._pagination_prev)
3072 else: 3099 else:
3073 return None 3100 return None
3074 else: 3101 else:
@@ -3233,7 +3260,7 @@ class Mastodon:
3233 if (key in json_object and isinstance(json_object[key], six.text_type)): 3260 if (key in json_object and isinstance(json_object[key], six.text_type)):
3234 if json_object[key].lower() == 'true': 3261 if json_object[key].lower() == 'true':
3235 json_object[key] = True 3262 json_object[key] = True
3236 if json_object[key].lower() == 'False': 3263 if json_object[key].lower() == 'false':
3237 json_object[key] = False 3264 json_object[key] = False
3238 return json_object 3265 return json_object
3239 3266
@@ -3446,6 +3473,7 @@ class Mastodon:
3446 if isinstance(response, list) and \ 3473 if isinstance(response, list) and \
3447 'Link' in response_object.headers and \ 3474 'Link' in response_object.headers and \
3448 response_object.headers['Link'] != "": 3475 response_object.headers['Link'] != "":
3476 response = AttribAccessList(response)
3449 tmp_urls = requests.utils.parse_header_links( 3477 tmp_urls = requests.utils.parse_header_links(
3450 response_object.headers['Link'].rstrip('>').replace('>,<', ',<')) 3478 response_object.headers['Link'].rstrip('>').replace('>,<', ',<'))
3451 for url in tmp_urls: 3479 for url in tmp_urls:
@@ -3470,7 +3498,12 @@ class Mastodon:
3470 del next_params['since_id'] 3498 del next_params['since_id']
3471 if "min_id" in next_params: 3499 if "min_id" in next_params:
3472 del next_params['min_id'] 3500 del next_params['min_id']
3473 response[-1]._pagination_next = next_params 3501 response._pagination_next = next_params
3502
3503 # Maybe other API users rely on the pagination info in the last item
3504 # Will be removed in future
3505 if isinstance(response[-1], AttribAccessDict):
3506 response[-1]._pagination_next = next_params
3474 3507
3475 if url['rel'] == 'prev': 3508 if url['rel'] == 'prev':
3476 # Be paranoid and extract since_id or min_id specifically 3509 # Be paranoid and extract since_id or min_id specifically
@@ -3489,8 +3522,13 @@ class Mastodon:
3489 prev_params['since_id'] = since_id 3522 prev_params['since_id'] = since_id
3490 if "max_id" in prev_params: 3523 if "max_id" in prev_params:
3491 del prev_params['max_id'] 3524 del prev_params['max_id']
3492 response[0]._pagination_prev = prev_params 3525 response._pagination_prev = prev_params
3493 3526
3527 # Maybe other API users rely on the pagination info in the first item
3528 # Will be removed in future
3529 if isinstance(response[0], AttribAccessDict):
3530 response[0]._pagination_prev = prev_params
3531
3494 # New and fantastico (post-2.6.0): min_id pagination 3532 # New and fantastico (post-2.6.0): min_id pagination
3495 matchgroups = re.search(r"[?&]min_id=([^&]+)", prev_url) 3533 matchgroups = re.search(r"[?&]min_id=([^&]+)", prev_url)
3496 if matchgroups: 3534 if matchgroups:
@@ -3504,7 +3542,12 @@ class Mastodon:
3504 prev_params['min_id'] = min_id 3542 prev_params['min_id'] = min_id
3505 if "max_id" in prev_params: 3543 if "max_id" in prev_params:
3506 del prev_params['max_id'] 3544 del prev_params['max_id']
3507 response[0]._pagination_prev = prev_params 3545 response._pagination_prev = prev_params
3546
3547 # Maybe other API users rely on the pagination info in the first item
3548 # Will be removed in future
3549 if isinstance(response[0], AttribAccessDict):
3550 response[0]._pagination_prev = prev_params
3508 3551
3509 return response 3552 return response
3510 3553
@@ -3570,7 +3613,8 @@ class Mastodon:
3570 3613
3571 def close(self): 3614 def close(self):
3572 self.closed = True 3615 self.closed = True
3573 self.connection.close() 3616 if not self.connection is None:
3617 self.connection.close()
3574 3618
3575 def is_alive(self): 3619 def is_alive(self):
3576 return self._thread.is_alive() 3620 return self._thread.is_alive()
@@ -3581,6 +3625,14 @@ class Mastodon:
3581 else: 3625 else:
3582 return True 3626 return True
3583 3627
3628 def _sleep_attentive(self):
3629 if self._thread != threading.current_thread():
3630 raise RuntimeError ("Illegal call from outside the stream_handle thread")
3631 time_remaining = self.reconnect_async_wait_sec
3632 while time_remaining>0 and not self.closed:
3633 time.sleep(0.5)
3634 time_remaining -= 0.5
3635
3584 def _threadproc(self): 3636 def _threadproc(self):
3585 self._thread = threading.current_thread() 3637 self._thread = threading.current_thread()
3586 3638
@@ -3602,16 +3654,26 @@ class Mastodon:
3602 self.reconnecting = True 3654 self.reconnecting = True
3603 connect_success = False 3655 connect_success = False
3604 while not connect_success: 3656 while not connect_success:
3605 connect_success = True 3657 if self.closed:
3658 # Someone from outside stopped the streaming
3659 self.running = False
3660 break
3606 try: 3661 try:
3607 self.connection = self.connect_func() 3662 the_connection = self.connect_func()
3608 if self.connection.status_code != 200: 3663 if the_connection.status_code != 200:
3609 time.sleep(self.reconnect_async_wait_sec) 3664 exception = MastodonNetworkError(f"Could not connect to server. "
3610 connect_success = False 3665 f"HTTP status: {the_connection.status_code}")
3611 exception = MastodonNetworkError("Could not connect to server.")
3612 listener.on_abort(exception) 3666 listener.on_abort(exception)
3667 self._sleep_attentive()
3668 if self.closed:
3669 # Here we have maybe a rare race condition. Exactly on connect, someone
3670 # stopped the streaming before. We close the previous established connection:
3671 the_connection.close()
3672 else:
3673 self.connection = the_connection
3674 connect_success = True
3613 except: 3675 except:
3614 time.sleep(self.reconnect_async_wait_sec) 3676 self._sleep_attentive()
3615 connect_success = False 3677 connect_success = False
3616 self.reconnecting = False 3678 self.reconnecting = False
3617 else: 3679 else:
diff --git a/mastodon/streaming.py b/mastodon/streaming.py
index 214ed1c..ceb61ea 100644
--- a/mastodon/streaming.py
+++ b/mastodon/streaming.py
@@ -45,6 +45,16 @@ class StreamListener(object):
45 contains the resulting conversation dict.""" 45 contains the resulting conversation dict."""
46 pass 46 pass
47 47
48 def on_unknown_event(self, name, unknown_event = None):
49 """An unknown mastodon API event has been received. The name contains the event-name and unknown_event
50 contains the content of the unknown event.
51
52 This function must be implemented, if unknown events should be handled without an error.
53 """
54 exception = MastodonMalformedEventError('Bad event type', name)
55 self.on_abort(exception)
56 raise exception
57
48 def handle_heartbeat(self): 58 def handle_heartbeat(self):
49 """The server has sent us a keep-alive message. This callback may be 59 """The server has sent us a keep-alive message. This callback may be
50 useful to carry out periodic housekeeping tasks, or just to confirm 60 useful to carry out periodic housekeeping tasks, or just to confirm
@@ -56,6 +66,11 @@ class StreamListener(object):
56 Handles a stream of events from the Mastodon server. When each event 66 Handles a stream of events from the Mastodon server. When each event
57 is received, the corresponding .on_[name]() method is called. 67 is received, the corresponding .on_[name]() method is called.
58 68
69 When the Mastodon API changes, the on_unknown_event(name, content)
70 function is called.
71 The default behavior is to throw an error. Define a callback handler
72 to intercept unknown events if needed (and avoid errors)
73
59 response; a requests response object with the open stream for reading. 74 response; a requests response object with the open stream for reading.
60 """ 75 """
61 event = {} 76 event = {}
@@ -137,33 +152,32 @@ class StreamListener(object):
137 exception, 152 exception,
138 err 153 err
139 ) 154 )
140 155 # New mastodon API also supports event names with dots:
141 handler_name = 'on_' + name 156 handler_name = 'on_' + name.replace('.', '_')
142 try: 157 # A generic way to handle unknown events to make legacy code more stable for future changes
143 handler = getattr(self, handler_name) 158 handler = getattr(self, handler_name, self.on_unknown_event)
144 except AttributeError as err: 159 if handler != self.on_unknown_event:
145 exception = MastodonMalformedEventError('Bad event type', name)
146 self.on_abort(exception)
147 six.raise_from(
148 exception,
149 err
150 )
151 else:
152 handler(payload) 160 handler(payload)
161 else:
162 handler(name, payload)
163
153 164
154class CallbackStreamListener(StreamListener): 165class CallbackStreamListener(StreamListener):
155 """ 166 """
156 Simple callback stream handler class. 167 Simple callback stream handler class.
157 Can optionally additionally send local update events to a separate handler. 168 Can optionally additionally send local update events to a separate handler.
169 Define an unknown_event_handler for new Mastodon API events. If not, the
170 listener will raise an error on new, not handled, events from the API.
158 """ 171 """
159 def __init__(self, update_handler = None, local_update_handler = None, delete_handler = None, notification_handler = None, conversation_handler = None): 172 def __init__(self, update_handler = None, local_update_handler = None, delete_handler = None, notification_handler = None, conversation_handler = None, unknown_event_handler = None):
160 super(CallbackStreamListener, self).__init__() 173 super(CallbackStreamListener, self).__init__()
161 self.update_handler = update_handler 174 self.update_handler = update_handler
162 self.local_update_handler = local_update_handler 175 self.local_update_handler = local_update_handler
163 self.delete_handler = delete_handler 176 self.delete_handler = delete_handler
164 self.notification_handler = notification_handler 177 self.notification_handler = notification_handler
165 self.conversation_handler = conversation_handler 178 self.conversation_handler = conversation_handler
166 179 self.unknown_event_handler = unknown_event_handler
180
167 def on_update(self, status): 181 def on_update(self, status):
168 if self.update_handler != None: 182 if self.update_handler != None:
169 self.update_handler(status) 183 self.update_handler(status)
@@ -188,3 +202,11 @@ class CallbackStreamListener(StreamListener):
188 def on_conversation(self, conversation): 202 def on_conversation(self, conversation):
189 if self.conversation_handler != None: 203 if self.conversation_handler != None:
190 self.conversation_handler(conversation) 204 self.conversation_handler(conversation)
205
206 def on_unknown_event(self, name, unknown_event = None):
207 if self.unknown_event_handler != None:
208 self.unknown_event_handler(name, unknown_event)
209 else:
210 exception = MastodonMalformedEventError('Bad event type', name)
211 self.on_abort(exception)
212 raise exception
Powered by cgit v1.2.3 (git 2.41.0)