aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'mastodon')
-rw-r--r--mastodon/Mastodon.py160
-rw-r--r--mastodon/streaming.py50
2 files changed, 162 insertions, 48 deletions
diff --git a/mastodon/Mastodon.py b/mastodon/Mastodon.py
index bdf7f1b..296921e 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))
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
126 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
@@ -276,6 +293,7 @@ class Mastodon:
276 secret_file.write(response['client_id'] + "\n") 293 secret_file.write(response['client_id'] + "\n")
277 secret_file.write(response['client_secret'] + "\n") 294 secret_file.write(response['client_secret'] + "\n")
278 secret_file.write(api_base_url + "\n") 295 secret_file.write(api_base_url + "\n")
296 secret_file.write(client_name + "\n")
279 297
280 return (response['client_id'], response['client_secret']) 298 return (response['client_id'], response['client_secret'])
281 299
@@ -286,7 +304,7 @@ class Mastodon:
286 api_base_url=None, debug_requests=False, 304 api_base_url=None, debug_requests=False,
287 ratelimit_method="wait", ratelimit_pacefactor=1.1, 305 ratelimit_method="wait", ratelimit_pacefactor=1.1,
288 request_timeout=__DEFAULT_TIMEOUT, mastodon_version=None, 306 request_timeout=__DEFAULT_TIMEOUT, mastodon_version=None,
289 version_check_mode = "created", session=None, feature_set="mainline"): 307 version_check_mode = "created", session=None, feature_set="mainline", user_agent=None):
290 """ 308 """
291 Create a new API wrapper instance based on the given `client_secret` and `client_id`. If you 309 Create a new API wrapper instance based on the given `client_secret` and `client_id`. If you
292 give a `client_id` and it is not a file, you must also give a secret. If you specify an 310 give a `client_id` and it is not a file, you must also give a secret. If you specify an
@@ -331,6 +349,11 @@ class Mastodon:
331 `feature_set` can be used to enable behaviour specific to non-mainline Mastodon API implementations. 349 `feature_set` can be used to enable behaviour specific to non-mainline Mastodon API implementations.
332 Details are documented in the functions that provide such functionality. Currently supported feature 350 Details are documented in the functions that provide such functionality. Currently supported feature
333 sets are `mainline`, `fedibird` and `pleroma`. 351 sets are `mainline`, `fedibird` and `pleroma`.
352
353 For some mastodon-instances a `User-Agent` header is needed. This can be set by parameter `user_agent`. From now
354 `create_app()` stores the application name into the client secret file. If `client_id` points to this file,
355 the app name will be used as `User-Agent` header as default. It's possible to modify old secret files and append
356 a client app name to use it as a `User-Agent` name.
334 """ 357 """
335 self.api_base_url = None 358 self.api_base_url = None
336 if not api_base_url is None: 359 if not api_base_url is None:
@@ -362,6 +385,9 @@ class Mastodon:
362 self.feature_set = feature_set 385 self.feature_set = feature_set
363 if not self.feature_set in ["mainline", "fedibird", "pleroma"]: 386 if not self.feature_set in ["mainline", "fedibird", "pleroma"]:
364 raise MastodonIllegalArgumentError('Requested invalid feature set') 387 raise MastodonIllegalArgumentError('Requested invalid feature set')
388
389 # General defined user-agent
390 self.user_agent = user_agent
365 391
366 # Token loading 392 # Token loading
367 if self.client_id is not None: 393 if self.client_id is not None:
@@ -376,6 +402,11 @@ class Mastodon:
376 if not (self.api_base_url is None or try_base_url == self.api_base_url): 402 if not (self.api_base_url is None or try_base_url == self.api_base_url):
377 raise MastodonIllegalArgumentError('Mismatch in base URLs between files and/or specified') 403 raise MastodonIllegalArgumentError('Mismatch in base URLs between files and/or specified')
378 self.api_base_url = try_base_url 404 self.api_base_url = try_base_url
405
406 # With new registrations we support the 4th line to store a client_name and use it as user-agent
407 client_name = secret_file.readline()
408 if client_name and self.user_agent is None:
409 self.user_agent = client_name.rstrip()
379 else: 410 else:
380 if self.client_secret is None: 411 if self.client_secret is None:
381 raise MastodonIllegalArgumentError('Specified client id directly, but did not supply secret') 412 raise MastodonIllegalArgumentError('Specified client id directly, but did not supply secret')
@@ -390,20 +421,20 @@ class Mastodon:
390 if not (self.api_base_url is None or try_base_url == self.api_base_url): 421 if not (self.api_base_url is None or try_base_url == self.api_base_url):
391 raise MastodonIllegalArgumentError('Mismatch in base URLs between files and/or specified') 422 raise MastodonIllegalArgumentError('Mismatch in base URLs between files and/or specified')
392 self.api_base_url = try_base_url 423 self.api_base_url = try_base_url
424
425 if not version_check_mode in ["created", "changed", "none"]:
426 raise MastodonIllegalArgumentError("Invalid version check method.")
427 self.version_check_mode = version_check_mode
393 428
394 # Versioning 429 # Versioning
395 if mastodon_version == None: 430 if mastodon_version == None and self.version_check_mode != 'none':
396 self.retrieve_mastodon_version() 431 self.retrieve_mastodon_version()
397 else: 432 elif self.version_check_mode != 'none':
398 try: 433 try:
399 self.mastodon_major, self.mastodon_minor, self.mastodon_patch = parse_version_string(mastodon_version) 434 self.mastodon_major, self.mastodon_minor, self.mastodon_patch = parse_version_string(mastodon_version)
400 except: 435 except:
401 raise MastodonVersionError("Bad version specified") 436 raise MastodonVersionError("Bad version specified")
402 437
403 if not version_check_mode in ["created", "changed", "none"]:
404 raise MastodonIllegalArgumentError("Invalid version check method.")
405 self.version_check_mode = version_check_mode
406
407 # Ratelimiting parameter check 438 # Ratelimiting parameter check
408 if ratelimit_method not in ["throw", "wait", "pace"]: 439 if ratelimit_method not in ["throw", "wait", "pace"]:
409 raise MastodonIllegalArgumentError("Invalid ratelimit method.") 440 raise MastodonIllegalArgumentError("Invalid ratelimit method.")
@@ -965,10 +996,13 @@ class Mastodon:
965 # Reading data: Notifications 996 # Reading data: Notifications
966 ### 997 ###
967 @api_version("1.0.0", "2.9.0", __DICT_VERSION_NOTIFICATION) 998 @api_version("1.0.0", "2.9.0", __DICT_VERSION_NOTIFICATION)
968 def notifications(self, id=None, account_id=None, max_id=None, min_id=None, since_id=None, limit=None, mentions_only=None): 999 def notifications(self, id=None, account_id=None, max_id=None, min_id=None, since_id=None, limit=None, exclude_types=None):
969 """ 1000 """
970 Fetch notifications (mentions, favourites, reblogs, follows) for the logged-in 1001 Fetch notifications (mentions, favourites, reblogs, follows) for the logged-in
971 user. Pass `account_id` to get only notifications originating from the given account. 1002 user. Pass `account_id` to get only notifications originating from the given account.
1003
1004 Parameter `exclude_types` is an array of the following `follow`, `favourite`, `reblog`,
1005 `mention`, `poll`, `follow_request`
972 1006
973 Can be passed an `id` to fetch a single notification. 1007 Can be passed an `id` to fetch a single notification.
974 1008
@@ -1028,8 +1062,8 @@ class Mastodon:
1028 """ 1062 """
1029 return self.account_verify_credentials() 1063 return self.account_verify_credentials()
1030 1064
1031 @api_version("1.0.0", "2.7.0", __DICT_VERSION_STATUS) 1065 @api_version("1.0.0", "2.8.0", __DICT_VERSION_STATUS)
1032 def account_statuses(self, id, only_media=False, pinned=False, exclude_replies=False, max_id=None, min_id=None, since_id=None, limit=None): 1066 def account_statuses(self, id, only_media=False, pinned=False, exclude_replies=False, exclude_reblogs=False, tagged=None, max_id=None, min_id=None, since_id=None, limit=None):
1033 """ 1067 """
1034 Fetch statuses by user `id`. Same options as `timeline()`_ are permitted. 1068 Fetch statuses by user `id`. Same options as `timeline()`_ are permitted.
1035 Returned toots are from the perspective of the logged-in user, i.e. 1069 Returned toots are from the perspective of the logged-in user, i.e.
@@ -1040,6 +1074,8 @@ class Mastodon:
1040 If `pinned` is set, return only statuses that have been pinned. Note that 1074 If `pinned` is set, return only statuses that have been pinned. Note that
1041 as of Mastodon 2.1.0, this only works properly for instance-local users. 1075 as of Mastodon 2.1.0, this only works properly for instance-local users.
1042 If `exclude_replies` is set, filter out all statuses that are replies. 1076 If `exclude_replies` is set, filter out all statuses that are replies.
1077 If `exclude_reblogs` is set, filter out all statuses that are reblogs.
1078 If `tagged` is set, return only statuses that are tagged with `tagged`. Only a single tag without a '#' is valid.
1043 1079
1044 Does not require authentication for Mastodon versions after 2.7.0 (returns 1080 Does not require authentication for Mastodon versions after 2.7.0 (returns
1045 publicly visible statuses in that case), for publicly visible accounts. 1081 publicly visible statuses in that case), for publicly visible accounts.
@@ -1063,7 +1099,9 @@ class Mastodon:
1063 del params["only_media"] 1099 del params["only_media"]
1064 if exclude_replies == False: 1100 if exclude_replies == False:
1065 del params["exclude_replies"] 1101 del params["exclude_replies"]
1066 1102 if exclude_reblogs == False:
1103 del params["exclude_reblogs"]
1104
1067 url = '/api/v1/accounts/{0}/statuses'.format(str(id)) 1105 url = '/api/v1/accounts/{0}/statuses'.format(str(id))
1068 return self.__api_request('GET', url, params) 1106 return self.__api_request('GET', url, params)
1069 1107
@@ -1633,13 +1671,23 @@ class Mastodon:
1633 # Reading data: Bookmarks 1671 # Reading data: Bookmarks
1634 ### 1672 ###
1635 @api_version("3.1.0", "3.1.0", __DICT_VERSION_STATUS) 1673 @api_version("3.1.0", "3.1.0", __DICT_VERSION_STATUS)
1636 def bookmarks(self): 1674 def bookmarks(self, max_id=None, min_id=None, since_id=None, limit=None):
1637 """ 1675 """
1638 Get a list of statuses bookmarked by the logged-in user. 1676 Get a list of statuses bookmarked by the logged-in user.
1639 1677
1640 Returns a list of `toot dicts`_. 1678 Returns a list of `toot dicts`_.
1641 """ 1679 """
1642 return self.__api_request('GET', '/api/v1/bookmarks') 1680 if max_id != None:
1681 max_id = self.__unpack_id(max_id)
1682
1683 if min_id != None:
1684 min_id = self.__unpack_id(min_id)
1685
1686 if since_id != None:
1687 since_id = self.__unpack_id(since_id)
1688
1689 params = self.__generate_params(locals())
1690 return self.__api_request('GET', '/api/v1/bookmarks', params)
1643 1691
1644 ### 1692 ###
1645 # Writing data: Statuses 1693 # Writing data: Statuses
@@ -2424,7 +2472,7 @@ class Mastodon:
2424 if not status_ids is None: 2472 if not status_ids is None:
2425 if not isinstance(status_ids, list): 2473 if not isinstance(status_ids, list):
2426 status_ids = [status_ids] 2474 status_ids = [status_ids]
2427 status_ids = list(map(lambda x: self.__unpack_id(x), status_ids)) 2475 status_ids = list(map(lambda x: self.__unpack_id(x), status_ids))
2428 2476
2429 params_initial = locals() 2477 params_initial = locals()
2430 if forward == False: 2478 if forward == False:
@@ -3041,8 +3089,8 @@ class Mastodon:
3041 Returns the next page or None if no further data is available. 3089 Returns the next page or None if no further data is available.
3042 """ 3090 """
3043 if isinstance(previous_page, list) and len(previous_page) != 0: 3091 if isinstance(previous_page, list) and len(previous_page) != 0:
3044 if hasattr(previous_page[-1], '_pagination_next'): 3092 if hasattr(previous_page, '_pagination_next'):
3045 params = copy.deepcopy(previous_page[-1]._pagination_next) 3093 params = copy.deepcopy(previous_page._pagination_next)
3046 else: 3094 else:
3047 return None 3095 return None
3048 else: 3096 else:
@@ -3065,8 +3113,8 @@ class Mastodon:
3065 Returns the previous page or None if no further data is available. 3113 Returns the previous page or None if no further data is available.
3066 """ 3114 """
3067 if isinstance(next_page, list) and len(next_page) != 0: 3115 if isinstance(next_page, list) and len(next_page) != 0:
3068 if hasattr(next_page[0], '_pagination_prev'): 3116 if hasattr(next_page, '_pagination_prev'):
3069 params = copy.deepcopy(next_page[0]._pagination_prev) 3117 params = copy.deepcopy(next_page._pagination_prev)
3070 else: 3118 else:
3071 return None 3119 return None
3072 else: 3120 else:
@@ -3231,7 +3279,7 @@ class Mastodon:
3231 if (key in json_object and isinstance(json_object[key], six.text_type)): 3279 if (key in json_object and isinstance(json_object[key], six.text_type)):
3232 if json_object[key].lower() == 'true': 3280 if json_object[key].lower() == 'true':
3233 json_object[key] = True 3281 json_object[key] = True
3234 if json_object[key].lower() == 'False': 3282 if json_object[key].lower() == 'false':
3235 json_object[key] = False 3283 json_object[key] = False
3236 return json_object 3284 return json_object
3237 3285
@@ -3305,6 +3353,10 @@ class Mastodon:
3305 if not access_token_override is None: 3353 if not access_token_override is None:
3306 headers['Authorization'] = 'Bearer ' + access_token_override 3354 headers['Authorization'] = 'Bearer ' + access_token_override
3307 3355
3356 # Add user-agent
3357 if self.user_agent:
3358 headers['User-Agent'] = self.user_agent
3359
3308 # Determine base URL 3360 # Determine base URL
3309 base_url = self.api_base_url 3361 base_url = self.api_base_url
3310 if not base_url_override is None: 3362 if not base_url_override is None:
@@ -3356,8 +3408,11 @@ class Mastodon:
3356 self.ratelimit_limit = int(response_object.headers['X-RateLimit-Limit']) 3408 self.ratelimit_limit = int(response_object.headers['X-RateLimit-Limit'])
3357 3409
3358 try: 3410 try:
3359 ratelimit_reset_datetime = dateutil.parser.parse(response_object.headers['X-RateLimit-Reset']) 3411 if str(int(response_object.headers['X-RateLimit-Reset'])) == response_object.headers['X-RateLimit-Reset']:
3360 self.ratelimit_reset = self.__datetime_to_epoch(ratelimit_reset_datetime) 3412 self.ratelimit_reset = int(response_object.headers['X-RateLimit-Reset'])
3413 else:
3414 ratelimit_reset_datetime = dateutil.parser.parse(response_object.headers['X-RateLimit-Reset'])
3415 self.ratelimit_reset = self.__datetime_to_epoch(ratelimit_reset_datetime)
3361 3416
3362 # Adjust server time to local clock 3417 # Adjust server time to local clock
3363 if 'Date' in response_object.headers: 3418 if 'Date' in response_object.headers:
@@ -3444,6 +3499,7 @@ class Mastodon:
3444 if isinstance(response, list) and \ 3499 if isinstance(response, list) and \
3445 'Link' in response_object.headers and \ 3500 'Link' in response_object.headers and \
3446 response_object.headers['Link'] != "": 3501 response_object.headers['Link'] != "":
3502 response = AttribAccessList(response)
3447 tmp_urls = requests.utils.parse_header_links( 3503 tmp_urls = requests.utils.parse_header_links(
3448 response_object.headers['Link'].rstrip('>').replace('>,<', ',<')) 3504 response_object.headers['Link'].rstrip('>').replace('>,<', ',<'))
3449 for url in tmp_urls: 3505 for url in tmp_urls:
@@ -3468,7 +3524,12 @@ class Mastodon:
3468 del next_params['since_id'] 3524 del next_params['since_id']
3469 if "min_id" in next_params: 3525 if "min_id" in next_params:
3470 del next_params['min_id'] 3526 del next_params['min_id']
3471 response[-1]._pagination_next = next_params 3527 response._pagination_next = next_params
3528
3529 # Maybe other API users rely on the pagination info in the last item
3530 # Will be removed in future
3531 if isinstance(response[-1], AttribAccessDict):
3532 response[-1]._pagination_next = next_params
3472 3533
3473 if url['rel'] == 'prev': 3534 if url['rel'] == 'prev':
3474 # Be paranoid and extract since_id or min_id specifically 3535 # Be paranoid and extract since_id or min_id specifically
@@ -3487,8 +3548,13 @@ class Mastodon:
3487 prev_params['since_id'] = since_id 3548 prev_params['since_id'] = since_id
3488 if "max_id" in prev_params: 3549 if "max_id" in prev_params:
3489 del prev_params['max_id'] 3550 del prev_params['max_id']
3490 response[0]._pagination_prev = prev_params 3551 response._pagination_prev = prev_params
3491 3552
3553 # Maybe other API users rely on the pagination info in the first item
3554 # Will be removed in future
3555 if isinstance(response[0], AttribAccessDict):
3556 response[0]._pagination_prev = prev_params
3557
3492 # New and fantastico (post-2.6.0): min_id pagination 3558 # New and fantastico (post-2.6.0): min_id pagination
3493 matchgroups = re.search(r"[?&]min_id=([^&]+)", prev_url) 3559 matchgroups = re.search(r"[?&]min_id=([^&]+)", prev_url)
3494 if matchgroups: 3560 if matchgroups:
@@ -3502,7 +3568,12 @@ class Mastodon:
3502 prev_params['min_id'] = min_id 3568 prev_params['min_id'] = min_id
3503 if "max_id" in prev_params: 3569 if "max_id" in prev_params:
3504 del prev_params['max_id'] 3570 del prev_params['max_id']
3505 response[0]._pagination_prev = prev_params 3571 response._pagination_prev = prev_params
3572
3573 # Maybe other API users rely on the pagination info in the first item
3574 # Will be removed in future
3575 if isinstance(response[0], AttribAccessDict):
3576 response[0]._pagination_prev = prev_params
3506 3577
3507 return response 3578 return response
3508 3579
@@ -3547,6 +3618,8 @@ class Mastodon:
3547 # Connect function (called and then potentially passed to async handler) 3618 # Connect function (called and then potentially passed to async handler)
3548 def connect_func(): 3619 def connect_func():
3549 headers = {"Authorization": "Bearer " + self.access_token} if self.access_token else {} 3620 headers = {"Authorization": "Bearer " + self.access_token} if self.access_token else {}
3621 if self.user_agent:
3622 headers['User-Agent'] = self.user_agent
3550 connection = self.session.get(url + endpoint, headers = headers, data = params, stream = True, 3623 connection = self.session.get(url + endpoint, headers = headers, data = params, stream = True,
3551 timeout=(self.request_timeout, timeout)) 3624 timeout=(self.request_timeout, timeout))
3552 3625
@@ -3568,7 +3641,8 @@ class Mastodon:
3568 3641
3569 def close(self): 3642 def close(self):
3570 self.closed = True 3643 self.closed = True
3571 self.connection.close() 3644 if not self.connection is None:
3645 self.connection.close()
3572 3646
3573 def is_alive(self): 3647 def is_alive(self):
3574 return self._thread.is_alive() 3648 return self._thread.is_alive()
@@ -3579,6 +3653,14 @@ class Mastodon:
3579 else: 3653 else:
3580 return True 3654 return True
3581 3655
3656 def _sleep_attentive(self):
3657 if self._thread != threading.current_thread():
3658 raise RuntimeError ("Illegal call from outside the stream_handle thread")
3659 time_remaining = self.reconnect_async_wait_sec
3660 while time_remaining>0 and not self.closed:
3661 time.sleep(0.5)
3662 time_remaining -= 0.5
3663
3582 def _threadproc(self): 3664 def _threadproc(self):
3583 self._thread = threading.current_thread() 3665 self._thread = threading.current_thread()
3584 3666
@@ -3600,16 +3682,26 @@ class Mastodon:
3600 self.reconnecting = True 3682 self.reconnecting = True
3601 connect_success = False 3683 connect_success = False
3602 while not connect_success: 3684 while not connect_success:
3603 connect_success = True 3685 if self.closed:
3686 # Someone from outside stopped the streaming
3687 self.running = False
3688 break
3604 try: 3689 try:
3605 self.connection = self.connect_func() 3690 the_connection = self.connect_func()
3606 if self.connection.status_code != 200: 3691 if the_connection.status_code != 200:
3607 time.sleep(self.reconnect_async_wait_sec) 3692 exception = MastodonNetworkError(f"Could not connect to server. "
3608 connect_success = False 3693 f"HTTP status: {the_connection.status_code}")
3609 exception = MastodonNetworkError("Could not connect to server.")
3610 listener.on_abort(exception) 3694 listener.on_abort(exception)
3695 self._sleep_attentive()
3696 if self.closed:
3697 # Here we have maybe a rare race condition. Exactly on connect, someone
3698 # stopped the streaming before. We close the previous established connection:
3699 the_connection.close()
3700 else:
3701 self.connection = the_connection
3702 connect_success = True
3611 except: 3703 except:
3612 time.sleep(self.reconnect_async_wait_sec) 3704 self._sleep_attentive()
3613 connect_success = False 3705 connect_success = False
3614 self.reconnecting = False 3706 self.reconnecting = False
3615 else: 3707 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)