diff options
Diffstat (limited to 'mastodon')
-rw-r--r-- | mastodon/Mastodon.py | 244 |
1 files changed, 174 insertions, 70 deletions
diff --git a/mastodon/Mastodon.py b/mastodon/Mastodon.py index 3d04955..befb030 100644 --- a/mastodon/Mastodon.py +++ b/mastodon/Mastodon.py | |||
@@ -167,7 +167,6 @@ class Mastodon: | |||
167 | If anything is unclear, check the official API docs at | 167 | If anything is unclear, check the official API docs at |
168 | https://github.com/mastodon/documentation/blob/master/content/en/client/intro.md | 168 | https://github.com/mastodon/documentation/blob/master/content/en/client/intro.md |
169 | """ | 169 | """ |
170 | __DEFAULT_BASE_URL = 'https://mastodon.social' | ||
171 | __DEFAULT_TIMEOUT = 300 | 170 | __DEFAULT_TIMEOUT = 300 |
172 | __DEFAULT_STREAM_TIMEOUT = 300 | 171 | __DEFAULT_STREAM_TIMEOUT = 300 |
173 | __DEFAULT_STREAM_RECONNECT_WAIT_SEC = 5 | 172 | __DEFAULT_STREAM_RECONNECT_WAIT_SEC = 5 |
@@ -260,17 +259,17 @@ class Mastodon: | |||
260 | ### | 259 | ### |
261 | @staticmethod | 260 | @staticmethod |
262 | def create_app(client_name, scopes=__DEFAULT_SCOPES, redirect_uris=None, website=None, to_file=None, | 261 | def create_app(client_name, scopes=__DEFAULT_SCOPES, redirect_uris=None, website=None, to_file=None, |
263 | api_base_url=__DEFAULT_BASE_URL, request_timeout=__DEFAULT_TIMEOUT, session=None): | 262 | api_base_url=None, request_timeout=__DEFAULT_TIMEOUT, session=None): |
264 | """ | 263 | """ |
265 | Create a new app with given `client_name` and `scopes` (The basic scopes are "read", "write", "follow" and "push" | 264 | Create a new app with given `client_name` and `scopes` (The basic scopes are "read", "write", "follow" and "push" |
266 | - more granular scopes are available, please refer to Mastodon documentation for which). | 265 | - more granular scopes are available, please refer to Mastodon documentation for which) on the instance given |
266 | by `api_base_url`. | ||
267 | 267 | ||
268 | Specify `redirect_uris` if you want users to be redirected to a certain page after authenticating in an OAuth flow. | 268 | Specify `redirect_uris` if you want users to be redirected to a certain page after authenticating in an OAuth flow. |
269 | You can specify multiple URLs by passing a list. Note that if you wish to use OAuth authentication with redirects, | 269 | You can specify multiple URLs by passing a list. Note that if you wish to use OAuth authentication with redirects, |
270 | the redirect URI must be one of the URLs specified here. | 270 | the redirect URI must be one of the URLs specified here. |
271 | 271 | ||
272 | Specify `to_file` to persist your app's info to a file so you can use it in the constructor. | 272 | Specify `to_file` to persist your app's info to a file so you can use it in the constructor. |
273 | Specify `api_base_url` if you want to register an app on an instance different from the flagship one. | ||
274 | Specify `website` to give a website for your app. | 273 | Specify `website` to give a website for your app. |
275 | 274 | ||
276 | Specify `session` with a requests.Session for it to be used instead of the default. This can be | 275 | Specify `session` with a requests.Session for it to be used instead of the default. This can be |
@@ -282,6 +281,8 @@ class Mastodon: | |||
282 | 281 | ||
283 | Returns `client_id` and `client_secret`, both as strings. | 282 | Returns `client_id` and `client_secret`, both as strings. |
284 | """ | 283 | """ |
284 | if api_base_url is None: | ||
285 | raise MastodonIllegalArgumentError("API base URL is required.") | ||
285 | api_base_url = Mastodon.__protocolize(api_base_url) | 286 | api_base_url = Mastodon.__protocolize(api_base_url) |
286 | 287 | ||
287 | request_data = { | 288 | request_data = { |
@@ -299,12 +300,10 @@ class Mastodon: | |||
299 | if website is not None: | 300 | if website is not None: |
300 | request_data['website'] = website | 301 | request_data['website'] = website |
301 | if session: | 302 | if session: |
302 | ret = session.post(api_base_url + '/api/v1/apps', | 303 | ret = session.post(api_base_url + '/api/v1/apps', data=request_data, timeout=request_timeout) |
303 | data=request_data, timeout=request_timeout) | ||
304 | response = ret.json() | 304 | response = ret.json() |
305 | else: | 305 | else: |
306 | response = requests.post( | 306 | response = requests.post(api_base_url + '/api/v1/apps', data=request_data, timeout=request_timeout) |
307 | api_base_url + '/api/v1/apps', data=request_data, timeout=request_timeout) | ||
308 | response = response.json() | 307 | response = response.json() |
309 | except Exception as e: | 308 | except Exception as e: |
310 | raise MastodonNetworkError("Could not complete request: %s" % e) | 309 | raise MastodonNetworkError("Could not complete request: %s" % e) |
@@ -321,17 +320,15 @@ class Mastodon: | |||
321 | ### | 320 | ### |
322 | # Authentication, including constructor | 321 | # Authentication, including constructor |
323 | ### | 322 | ### |
324 | def __init__(self, client_id=None, client_secret=None, access_token=None, | 323 | def __init__(self, client_id=None, client_secret=None, access_token=None, api_base_url=None, debug_requests=False, |
325 | api_base_url=None, debug_requests=False, | 324 | ratelimit_method="wait", ratelimit_pacefactor=1.1, request_timeout=__DEFAULT_TIMEOUT, mastodon_version=None, |
326 | ratelimit_method="wait", ratelimit_pacefactor=1.1, | 325 | version_check_mode="created", session=None, feature_set="mainline", user_agent="mastodonpy", lang=None): |
327 | request_timeout=__DEFAULT_TIMEOUT, mastodon_version=None, | 326 | """ |
328 | version_check_mode="created", session=None, feature_set="mainline", user_agent="mastodonpy"): | 327 | Create a new API wrapper instance based on the given `client_secret` and `client_id` on the |
329 | """ | 328 | instance given by `api_base_url`. If you give a `client_id` and it is not a file, you must |
330 | Create a new API wrapper instance based on the given `client_secret` and `client_id`. If you | 329 | also give a secret. If you specify an `access_token` then you don't need to specify a `client_id`. |
331 | give a `client_id` and it is not a file, you must also give a secret. If you specify an | 330 | It is allowed to specify neither - in this case, you will be restricted to only using endpoints |
332 | `access_token` then you don't need to specify a `client_id`. It is allowed to specify | 331 | that do not require authentication. If a file is given as `client_id`, client ID, secret and |
333 | neither - in this case, you will be restricted to only using endpoints that do not | ||
334 | require authentication. If a file is given as `client_id`, client ID, secret and | ||
335 | base url are read from that file. | 332 | base url are read from that file. |
336 | 333 | ||
337 | You can also specify an `access_token`, directly or as a file (as written by `log_in()`_). If | 334 | You can also specify an `access_token`, directly or as a file (as written by `log_in()`_). If |
@@ -347,10 +344,6 @@ class Mastodon: | |||
347 | even in "wait" and "pace" mode, requests can still fail due to network or other problems! Also | 344 | even in "wait" and "pace" mode, requests can still fail due to network or other problems! Also |
348 | note that "pace" and "wait" are NOT thread safe. | 345 | note that "pace" and "wait" are NOT thread safe. |
349 | 346 | ||
350 | Specify `api_base_url` if you wish to talk to an instance other than the flagship one. When | ||
351 | reading from client id or access token files as written by Mastodon.py 1.5.0 or larger, | ||
352 | this can be omitted. | ||
353 | |||
354 | By default, a timeout of 300 seconds is used for all requests. If you wish to change this, | 347 | By default, a timeout of 300 seconds is used for all requests. If you wish to change this, |
355 | pass the desired timeout (in seconds) as `request_timeout`. | 348 | pass the desired timeout (in seconds) as `request_timeout`. |
356 | 349 | ||
@@ -376,12 +369,15 @@ class Mastodon: | |||
376 | the app name will be used as `User-Agent` header as default. It is possible to modify old secret files and append | 369 | the app name will be used as `User-Agent` header as default. It is possible to modify old secret files and append |
377 | a client app name to use it as a `User-Agent` name. | 370 | a client app name to use it as a `User-Agent` name. |
378 | 371 | ||
372 | `lang` can be used to change the locale Mastodon will use to generate responses. Valid parameters are all ISO 639-1 (two letter) | ||
373 | or for a language that has none, 639-3 (three letter) language codes. This affects some error messages (those related to validation) and | ||
374 | trends. You can change the language using `set_language()`_. | ||
375 | |||
379 | If no other `User-Agent` is specified, "mastodonpy" will be used. | 376 | If no other `User-Agent` is specified, "mastodonpy" will be used. |
380 | """ | 377 | """ |
381 | self.api_base_url = None | 378 | self.api_base_url = api_base_url |
382 | if api_base_url is not None: | 379 | if self.api_base_url is not None: |
383 | self.api_base_url = Mastodon.__protocolize(api_base_url) | 380 | self.api_base_url = self.__protocolize(self.api_base_url) |
384 | |||
385 | self.client_id = client_id | 381 | self.client_id = client_id |
386 | self.client_secret = client_secret | 382 | self.client_secret = client_secret |
387 | self.access_token = access_token | 383 | self.access_token = access_token |
@@ -389,7 +385,7 @@ class Mastodon: | |||
389 | self.ratelimit_method = ratelimit_method | 385 | self.ratelimit_method = ratelimit_method |
390 | self._token_expired = datetime.datetime.now() | 386 | self._token_expired = datetime.datetime.now() |
391 | self._refresh_token = None | 387 | self._refresh_token = None |
392 | 388 | ||
393 | self.__logged_in_id = None | 389 | self.__logged_in_id = None |
394 | 390 | ||
395 | self.ratelimit_limit = 300 | 391 | self.ratelimit_limit = 300 |
@@ -412,6 +408,9 @@ class Mastodon: | |||
412 | # General defined user-agent | 408 | # General defined user-agent |
413 | self.user_agent = user_agent | 409 | self.user_agent = user_agent |
414 | 410 | ||
411 | # Save language | ||
412 | self.lang = lang | ||
413 | |||
415 | # Token loading | 414 | # Token loading |
416 | if self.client_id is not None: | 415 | if self.client_id is not None: |
417 | if os.path.isfile(self.client_id): | 416 | if os.path.isfile(self.client_id): |
@@ -422,9 +421,9 @@ class Mastodon: | |||
422 | try_base_url = secret_file.readline().rstrip() | 421 | try_base_url = secret_file.readline().rstrip() |
423 | if try_base_url is not None and len(try_base_url) != 0: | 422 | if try_base_url is not None and len(try_base_url) != 0: |
424 | try_base_url = Mastodon.__protocolize(try_base_url) | 423 | try_base_url = Mastodon.__protocolize(try_base_url) |
424 | print(self.api_base_url, try_base_url) | ||
425 | if not (self.api_base_url is None or try_base_url == self.api_base_url): | 425 | if not (self.api_base_url is None or try_base_url == self.api_base_url): |
426 | raise MastodonIllegalArgumentError( | 426 | raise MastodonIllegalArgumentError('Mismatch in base URLs between files and/or specified') |
427 | 'Mismatch in base URLs between files and/or specified') | ||
428 | self.api_base_url = try_base_url | 427 | self.api_base_url = try_base_url |
429 | 428 | ||
430 | # With new registrations we support the 4th line to store a client_name and use it as user-agent | 429 | # With new registrations we support the 4th line to store a client_name and use it as user-agent |
@@ -433,8 +432,7 @@ class Mastodon: | |||
433 | self.user_agent = client_name.rstrip() | 432 | self.user_agent = client_name.rstrip() |
434 | else: | 433 | else: |
435 | if self.client_secret is None: | 434 | if self.client_secret is None: |
436 | raise MastodonIllegalArgumentError( | 435 | raise MastodonIllegalArgumentError('Specified client id directly, but did not supply secret') |
437 | 'Specified client id directly, but did not supply secret') | ||
438 | 436 | ||
439 | if self.access_token is not None and os.path.isfile(self.access_token): | 437 | if self.access_token is not None and os.path.isfile(self.access_token): |
440 | with open(self.access_token, 'r') as token_file: | 438 | with open(self.access_token, 'r') as token_file: |
@@ -444,10 +442,14 @@ class Mastodon: | |||
444 | if try_base_url is not None and len(try_base_url) != 0: | 442 | if try_base_url is not None and len(try_base_url) != 0: |
445 | try_base_url = Mastodon.__protocolize(try_base_url) | 443 | try_base_url = Mastodon.__protocolize(try_base_url) |
446 | if not (self.api_base_url is None or try_base_url == self.api_base_url): | 444 | if not (self.api_base_url is None or try_base_url == self.api_base_url): |
447 | raise MastodonIllegalArgumentError( | 445 | raise MastodonIllegalArgumentError('Mismatch in base URLs between files and/or specified') |
448 | 'Mismatch in base URLs between files and/or specified') | ||
449 | self.api_base_url = try_base_url | 446 | self.api_base_url = try_base_url |
450 | 447 | ||
448 | # Verify we have a base URL, protocolize | ||
449 | if self.api_base_url is None: | ||
450 | raise MastodonIllegalArgumentError("API base URL is required.") | ||
451 | self.api_base_url = Mastodon.__protocolize(self.api_base_url) | ||
452 | |||
451 | if not version_check_mode in ["created", "changed", "none"]: | 453 | if not version_check_mode in ["created", "changed", "none"]: |
452 | raise MastodonIllegalArgumentError("Invalid version check method.") | 454 | raise MastodonIllegalArgumentError("Invalid version check method.") |
453 | self.version_check_mode = version_check_mode | 455 | self.version_check_mode = version_check_mode |
@@ -470,6 +472,13 @@ class Mastodon: | |||
470 | if ratelimit_method not in ["throw", "wait", "pace"]: | 472 | if ratelimit_method not in ["throw", "wait", "pace"]: |
471 | raise MastodonIllegalArgumentError("Invalid ratelimit method.") | 473 | raise MastodonIllegalArgumentError("Invalid ratelimit method.") |
472 | 474 | ||
475 | def set_language(self, lang): | ||
476 | """ | ||
477 | Set the locale Mastodon will use to generate responses. Valid parameters are all ISO 639-1 (two letter) or, for languages that do | ||
478 | not have one, 639-3 (three letter) language codes. This affects some error messages (those related to validation) and trends. | ||
479 | """ | ||
480 | self.lang = lang | ||
481 | |||
473 | def __normalize_version_string(self, version_string): | 482 | def __normalize_version_string(self, version_string): |
474 | # Split off everything after the first space, to take care of Pleromalikes so that the parser doesn't get confused in case those have a + somewhere in their version | 483 | # Split off everything after the first space, to take care of Pleromalikes so that the parser doesn't get confused in case those have a + somewhere in their version |
475 | version_string = version_string.split(" ")[0] | 484 | version_string = version_string.split(" ")[0] |
@@ -541,24 +550,27 @@ class Mastodon: | |||
541 | """ | 550 | """ |
542 | return Mastodon.__SUPPORTED_MASTODON_VERSION | 551 | return Mastodon.__SUPPORTED_MASTODON_VERSION |
543 | 552 | ||
544 | def auth_request_url(self, client_id=None, redirect_uris="urn:ietf:wg:oauth:2.0:oob", scopes=__DEFAULT_SCOPES, force_login=False, state=None): | 553 | def auth_request_url(self, client_id=None, redirect_uris="urn:ietf:wg:oauth:2.0:oob", scopes=__DEFAULT_SCOPES, force_login=False, state=None, lang=None): |
545 | """ | 554 | """ |
546 | Returns the URL that a client needs to request an OAuth grant from the server. | 555 | Returns the URL that a client needs to request an OAuth grant from the server. |
547 | 556 | ||
548 | To log in with OAuth, send your user to this URL. The user will then log in and | 557 | To log in with OAuth, send your user to this URL. The user will then log in and |
549 | get a code which you can pass to log_in. | 558 | get a code which you can pass to `log_in()`_. |
550 | 559 | ||
551 | scopes are as in `log_in()`_, redirect_uris is where the user should be redirected to | 560 | `scopes` are as in `log_in()`_, redirect_uris is where the user should be redirected to |
552 | after authentication. Note that redirect_uris must be one of the URLs given during | 561 | after authentication. Note that `redirect_uris` must be one of the URLs given during |
553 | app registration. When using urn:ietf:wg:oauth:2.0:oob, the code is simply displayed, | 562 | app registration. When using urn:ietf:wg:oauth:2.0:oob, the code is simply displayed, |
554 | otherwise it is added to the given URL as the "code" request parameter. | 563 | otherwise it is added to the given URL as the "code" request parameter. |
555 | 564 | ||
556 | Pass force_login if you want the user to always log in even when already logged | 565 | Pass force_login if you want the user to always log in even when already logged |
557 | into web Mastodon (i.e. when registering multiple different accounts in an app). | 566 | into web Mastodon (i.e. when registering multiple different accounts in an app). |
558 | 567 | ||
559 | State is the oauth `state`parameter to pass to the server. It is strongly suggested | 568 | `state` is the oauth `state` parameter to pass to the server. It is strongly suggested |
560 | to use a random, nonguessable value (i.e. nothing meaningful and no incrementing ID) | 569 | to use a random, nonguessable value (i.e. nothing meaningful and no incrementing ID) |
561 | to preserve security guarantees. It can be left out for non-web login flows. | 570 | to preserve security guarantees. It can be left out for non-web login flows. |
571 | |||
572 | Pass an ISO 639-1 (two letter) or, for languages that do not have one, 639-3 (three letter) | ||
573 | language code as `lang` to control the display language for the oauth form. | ||
562 | """ | 574 | """ |
563 | if client_id is None: | 575 | if client_id is None: |
564 | client_id = self.client_id | 576 | client_id = self.client_id |
@@ -574,6 +586,7 @@ class Mastodon: | |||
574 | params['scope'] = " ".join(scopes) | 586 | params['scope'] = " ".join(scopes) |
575 | params['force_login'] = force_login | 587 | params['force_login'] = force_login |
576 | params['state'] = state | 588 | params['state'] = state |
589 | params['lang'] = lang | ||
577 | formatted_params = urlencode(params) | 590 | formatted_params = urlencode(params) |
578 | return "".join([self.api_base_url, "/oauth/authorize?", formatted_params]) | 591 | return "".join([self.api_base_url, "/oauth/authorize?", formatted_params]) |
579 | 592 | ||
@@ -675,8 +688,9 @@ class Mastodon: | |||
675 | Creates a new user account with the given username, password and email. "agreement" | 688 | Creates a new user account with the given username, password and email. "agreement" |
676 | must be set to true (after showing the user the instance's user agreement and having | 689 | must be set to true (after showing the user the instance's user agreement and having |
677 | them agree to it), "locale" specifies the language for the confirmation email as an | 690 | them agree to it), "locale" specifies the language for the confirmation email as an |
678 | ISO 639-1 (two-letter) language code. `reason` can be used to specify why a user | 691 | ISO 639-1 (two letter) or, if a language does not have one, 639-3 (three letter) language |
679 | would like to join if approved-registrations mode is on. | 692 | code. `reason` can be used to specify why a user would like to join if approved-registrations |
693 | mode is on. | ||
680 | 694 | ||
681 | Does not require an access token, but does require a client grant. | 695 | Does not require an access token, but does require a client grant. |
682 | 696 | ||
@@ -999,7 +1013,7 @@ class Mastodon: | |||
999 | Does not require authentication for publicly visible statuses. | 1013 | Does not require authentication for publicly visible statuses. |
1000 | 1014 | ||
1001 | This function is deprecated as of 3.0.0 and the endpoint does not | 1015 | This function is deprecated as of 3.0.0 and the endpoint does not |
1002 | exist anymore - you should just use the "card" field of the status dicts | 1016 | exist anymore - you should just use the "card" field of the toot dicts |
1003 | instead. Mastodon.py will try to mimic the old behaviour, but this | 1017 | instead. Mastodon.py will try to mimic the old behaviour, but this |
1004 | is somewhat inefficient and not guaranteed to be the case forever. | 1018 | is somewhat inefficient and not guaranteed to be the case forever. |
1005 | 1019 | ||
@@ -1540,9 +1554,16 @@ class Mastodon: | |||
1540 | ### | 1554 | ### |
1541 | # Reading data: Trends | 1555 | # Reading data: Trends |
1542 | ### | 1556 | ### |
1543 | @api_version("2.4.3", "3.0.0", __DICT_VERSION_HASHTAG) | 1557 | @api_version("2.4.3", "3.5.0", __DICT_VERSION_HASHTAG) |
1544 | def trends(self, limit=None): | 1558 | def trends(self, limit=None): |
1545 | """ | 1559 | """ |
1560 | Alias for `trending_tags()`_ | ||
1561 | """ | ||
1562 | return self.trending_tags(limit=limit) | ||
1563 | |||
1564 | @api_version("3.5.0", "3.5.0", __DICT_VERSION_HASHTAG) | ||
1565 | def trending_tags(self, limit=None, lang=None): | ||
1566 | """ | ||
1546 | Fetch trending-hashtag information, if the instance provides such information. | 1567 | Fetch trending-hashtag information, if the instance provides such information. |
1547 | 1568 | ||
1548 | Specify `limit` to limit how many results are returned (the maximum number | 1569 | Specify `limit` to limit how many results are returned (the maximum number |
@@ -1551,13 +1572,49 @@ class Mastodon: | |||
1551 | Does not require authentication unless locked down by the administrator. | 1572 | Does not require authentication unless locked down by the administrator. |
1552 | 1573 | ||
1553 | Important versioning note: This endpoint does not exist for Mastodon versions | 1574 | Important versioning note: This endpoint does not exist for Mastodon versions |
1554 | between 2.8.0 (inclusive) and 3.0.0 (exclusive). | 1575 | between 2.8.0 (inclusive) and 3.0.0 (exclusive). |
1576 | |||
1577 | Pass `lang` to override the global locale parameter, which may affect trend ordering. | ||
1555 | 1578 | ||
1556 | Returns a list of `hashtag dicts`_, sorted by the instance's trending algorithm, | 1579 | Returns a list of `hashtag dicts`_, sorted by the instance's trending algorithm, |
1557 | descending. | 1580 | descending. |
1558 | """ | 1581 | """ |
1559 | params = self.__generate_params(locals()) | 1582 | params = self.__generate_params(locals()) |
1560 | return self.__api_request('GET', '/api/v1/trends', params) | 1583 | if self.verify_minimum_version("3.5.0", cached=True): |
1584 | # Starting 3.5.0, old version is deprecated | ||
1585 | return self.__api_request('GET', '/api/v1/trends/tags', params) | ||
1586 | else: | ||
1587 | return self.__api_request('GET', '/api/v1/trends', params) | ||
1588 | |||
1589 | @api_version("3.5.0", "3.5.0", __DICT_VERSION_STATUS) | ||
1590 | def trending_statuses(self): | ||
1591 | """ | ||
1592 | Fetch trending-status information, if the instance provides such information. | ||
1593 | |||
1594 | Specify `limit` to limit how many results are returned (the maximum number | ||
1595 | of results is 10, the endpoint is not paginated). | ||
1596 | |||
1597 | Pass `lang` to override the global locale parameter, which may affect trend ordering. | ||
1598 | |||
1599 | Returns a list of `toot dicts`_, sorted by the instances's trending algorithm, | ||
1600 | descending. | ||
1601 | """ | ||
1602 | params = self.__generate_params(locals()) | ||
1603 | return self.__api_request('GET', '/api/v1/trends/statuses', params) | ||
1604 | |||
1605 | @api_version("3.5.0", "3.5.0", __DICT_VERSION_CARD) | ||
1606 | def trending_links(self): | ||
1607 | """ | ||
1608 | Fetch trending-link information, if the instance provides such information. | ||
1609 | |||
1610 | Specify `limit` to limit how many results are returned (the maximum number | ||
1611 | of results is 10, the endpoint is not paginated). | ||
1612 | |||
1613 | Returns a list of `card dicts`_, sorted by the instances's trending algorithm, | ||
1614 | descending. | ||
1615 | """ | ||
1616 | params = self.__generate_params(locals()) | ||
1617 | return self.__api_request('GET', '/api/v1/trends/links', params) | ||
1561 | 1618 | ||
1562 | ### | 1619 | ### |
1563 | # Reading data: Lists | 1620 | # Reading data: Lists |
@@ -1656,6 +1713,8 @@ class Mastodon: | |||
1656 | Warning: This method has now finally been removed, and will not | 1713 | Warning: This method has now finally been removed, and will not |
1657 | work on Mastodon versions 2.5.0 and above. | 1714 | work on Mastodon versions 2.5.0 and above. |
1658 | """ | 1715 | """ |
1716 | if self.verify_minimum_version("2.5.0", cached=True): | ||
1717 | raise MastodonVersionError("API removed in Mastodon 2.5.0") | ||
1659 | return self.__api_request('GET', '/api/v1/reports') | 1718 | return self.__api_request('GET', '/api/v1/reports') |
1660 | 1719 | ||
1661 | ### | 1720 | ### |
@@ -1943,7 +2002,8 @@ class Mastodon: | |||
1943 | displayed. | 2002 | displayed. |
1944 | 2003 | ||
1945 | Specify `language` to override automatic language detection. The parameter | 2004 | Specify `language` to override automatic language detection. The parameter |
1946 | accepts all valid ISO 639-2 language codes. | 2005 | accepts all valid ISO 639-1 (2-letter) or for languages where that do not |
2006 | have one, 639-3 (three letter) language codes. | ||
1947 | 2007 | ||
1948 | You can set `idempotency_key` to a value to uniquely identify an attempt | 2008 | You can set `idempotency_key` to a value to uniquely identify an attempt |
1949 | at posting a status. Even if you call this function more than once, | 2009 | at posting a status. Even if you call this function more than once, |
@@ -2496,7 +2556,7 @@ class Mastodon: | |||
2496 | """ | 2556 | """ |
2497 | Set a note (visible to the logged in user only) for the given account. | 2557 | Set a note (visible to the logged in user only) for the given account. |
2498 | 2558 | ||
2499 | Returns a `status dict`_ with the `note` updated. | 2559 | Returns a `toot dict`_ with the `note` updated. |
2500 | """ | 2560 | """ |
2501 | id = self.__unpack_id(id) | 2561 | id = self.__unpack_id(id) |
2502 | params = self.__generate_params(locals(), ["id"]) | 2562 | params = self.__generate_params(locals(), ["id"]) |
@@ -2662,18 +2722,25 @@ class Mastodon: | |||
2662 | ### | 2722 | ### |
2663 | # Writing data: Reports | 2723 | # Writing data: Reports |
2664 | ### | 2724 | ### |
2665 | @api_version("1.1.0", "2.5.0", __DICT_VERSION_REPORT) | 2725 | @api_version("1.1.0", "3.5.0", __DICT_VERSION_REPORT) |
2666 | def report(self, account_id, status_ids=None, comment=None, forward=False): | 2726 | def report(self, account_id, status_ids=None, comment=None, forward=False, category=None, rule_ids=None): |
2667 | """ | 2727 | """ |
2668 | Report statuses to the instances administrators. | 2728 | Report statuses to the instances administrators. |
2669 | 2729 | ||
2670 | Accepts a list of toot IDs associated with the report, and a comment. | 2730 | Accepts a list of toot IDs associated with the report, and a comment. |
2671 | 2731 | ||
2672 | Set forward to True to forward a report of a remote user to that users | 2732 | Starting with Mastodon 3.5.0, you can also pass a `category` (one out of |
2733 | "spam", "violation" or "other") and `rule_ids` (a list of rule IDs corresponding | ||
2734 | to the rules returned by the `instance()`_ API). | ||
2735 | |||
2736 | Set `forward` to True to forward a report of a remote user to that users | ||
2673 | instance as well as sending it to the instance local administrators. | 2737 | instance as well as sending it to the instance local administrators. |
2674 | 2738 | ||
2675 | Returns a `report dict`_. | 2739 | Returns a `report dict`_. |
2676 | """ | 2740 | """ |
2741 | if category is not None and not category in ["spam", "violation", "other"]: | ||
2742 | raise MastodonIllegalArgumentError("Invalid report category (must be spam, violation or other)") | ||
2743 | |||
2677 | account_id = self.__unpack_id(account_id) | 2744 | account_id = self.__unpack_id(account_id) |
2678 | 2745 | ||
2679 | if status_ids is not None: | 2746 | if status_ids is not None: |
@@ -3263,6 +3330,39 @@ class Mastodon: | |||
3263 | id = self.__unpack_id(id) | 3330 | id = self.__unpack_id(id) |
3264 | return self.__api_request('POST', '/api/v1/admin/reports/{0}/resolve'.format(id)) | 3331 | return self.__api_request('POST', '/api/v1/admin/reports/{0}/resolve'.format(id)) |
3265 | 3332 | ||
3333 | @api_version("3.5.0", "3.5.0", __DICT_VERSION_HASHTAG) | ||
3334 | def admin_trending_tags(self, limit=None): | ||
3335 | """ | ||
3336 | Admin version of `trending_tags()`_. Includes unapproved tags. | ||
3337 | |||
3338 | Returns a list of `hashtag dicts`_, sorted by the instance's trending algorithm, | ||
3339 | descending. | ||
3340 | """ | ||
3341 | params = self.__generate_params(locals()) | ||
3342 | return self.__api_request('GET', '/api/v1/admin/trends/tags', params) | ||
3343 | |||
3344 | @api_version("3.5.0", "3.5.0", __DICT_VERSION_STATUS) | ||
3345 | def admin_trending_statuses(self): | ||
3346 | """ | ||
3347 | Admin version of `trending_statuses()`_. Includes unapproved tags. | ||
3348 | |||
3349 | Returns a list of `toot dicts`_, sorted by the instance's trending algorithm, | ||
3350 | descending. | ||
3351 | """ | ||
3352 | params = self.__generate_params(locals()) | ||
3353 | return self.__api_request('GET', '/api/v1/admin/trends/statuses', params) | ||
3354 | |||
3355 | @api_version("3.5.0", "3.5.0", __DICT_VERSION_CARD) | ||
3356 | def admin_trending_links(self): | ||
3357 | """ | ||
3358 | Admin version of `trending_links()`_. Includes unapproved tags. | ||
3359 | |||
3360 | Returns a list of `card dicts`_, sorted by the instance's trending algorithm, | ||
3361 | descending. | ||
3362 | """ | ||
3363 | params = self.__generate_params(locals()) | ||
3364 | return self.__api_request('GET', '/api/v1/admin/trends/links', params) | ||
3365 | |||
3266 | @api_version("4.0.0","4.0.0","4.0.0") | 3366 | @api_version("4.0.0","4.0.0","4.0.0") |
3267 | def admin_domain_blocks(self, id:str=None, limit:int=None): | 3367 | def admin_domain_blocks(self, id:str=None, limit:int=None): |
3268 | """ | 3368 | """ |
@@ -3275,7 +3375,7 @@ class Mastodon: | |||
3275 | id = self.__unpack_id(id) | 3375 | id = self.__unpack_id(id) |
3276 | if id is not None: | 3376 | if id is not None: |
3277 | return self.__api_request('GET', '/api/v1/admin/domain_blocks/{0}'.format(id)) | 3377 | return self.__api_request('GET', '/api/v1/admin/domain_blocks/{0}'.format(id)) |
3278 | else | 3378 | else: |
3279 | params = params = self.__generate_params(locals(),['limit']) | 3379 | params = params = self.__generate_params(locals(),['limit']) |
3280 | return self.__api_request('GET', '/api/v1/admin/domain_blocks/') | 3380 | return self.__api_request('GET', '/api/v1/admin/domain_blocks/') |
3281 | 3381 | ||
@@ -3303,8 +3403,7 @@ class Mastodon: | |||
3303 | params = self.__generate_params(locals()) | 3403 | params = self.__generate_params(locals()) |
3304 | 3404 | ||
3305 | 3405 | ||
3306 | self.__api_request( | 3406 | self.__api_request('POST', '/api/v1/admin/domain_blocks/', params) |
3307 | 'POST', '/api/v1/admin/domain_blocks/, params) | ||
3308 | 3407 | ||
3309 | @api_version("4.0.0","4.0.0","4.0.0") | 3408 | @api_version("4.0.0","4.0.0","4.0.0") |
3310 | def admin_update_domain_block(self, id:str, severity:str=None, reject_media:bool=None, reject_reports:bool=None, private_comment:str=None, public_comment:str=None, obfuscate:bool=None): | 3409 | def admin_update_domain_block(self, id:str, severity:str=None, reject_media:bool=None, reject_reports:bool=None, private_comment:str=None, public_comment:str=None, obfuscate:bool=None): |
@@ -3329,8 +3428,7 @@ class Mastodon: | |||
3329 | 3428 | ||
3330 | params = self.__generate_params(locals()) | 3429 | params = self.__generate_params(locals()) |
3331 | 3430 | ||
3332 | self.__api_request( | 3431 | self.__api_request('PUT', '/api/v1/admin/domain_blocks/', params) |
3333 | 'PUT', '/api/v1/admin/domain_blocks/, params) | ||
3334 | 3432 | ||
3335 | @api_version("4.0.0","4.0.0","4.0.0") | 3433 | @api_version("4.0.0","4.0.0","4.0.0") |
3336 | def admin_delete_domain_blocks(self, id:str=None): | 3434 | def admin_delete_domain_blocks(self, id:str=None): |
@@ -3344,7 +3442,7 @@ class Mastodon: | |||
3344 | id = self.__unpack_id(id) | 3442 | id = self.__unpack_id(id) |
3345 | if id is not None: | 3443 | if id is not None: |
3346 | return self.__api_request('DELETE', '/api/v1/admin/domain_blocks/{0}'.format(id)) | 3444 | return self.__api_request('DELETE', '/api/v1/admin/domain_blocks/{0}'.format(id)) |
3347 | else | 3445 | else: |
3348 | raise AttributeError("You must provide an id of an existing domain block to remove it.") | 3446 | raise AttributeError("You must provide an id of an existing domain block to remove it.") |
3349 | 3447 | ||
3350 | ### | 3448 | ### |
@@ -3644,6 +3742,7 @@ class Mastodon: | |||
3644 | """ | 3742 | """ |
3645 | known_date_fields = ["created_at", "week", "day", "expires_at", "scheduled_at", | 3743 | known_date_fields = ["created_at", "week", "day", "expires_at", "scheduled_at", |
3646 | "updated_at", "last_status_at", "starts_at", "ends_at", "published_at", "edited_at"] | 3744 | "updated_at", "last_status_at", "starts_at", "ends_at", "published_at", "edited_at"] |
3745 | mark_delete = [] | ||
3647 | for k, v in json_object.items(): | 3746 | for k, v in json_object.items(): |
3648 | if k in known_date_fields: | 3747 | if k in known_date_fields: |
3649 | if v is not None: | 3748 | if v is not None: |
@@ -3653,11 +3752,11 @@ class Mastodon: | |||
3653 | else: | 3752 | else: |
3654 | json_object[k] = dateutil.parser.parse(v) | 3753 | json_object[k] = dateutil.parser.parse(v) |
3655 | except: | 3754 | except: |
3656 | if isinstance(v, str) and len(x.strip()) == 0: | 3755 | # When we can't parse a date, we just leave the field out |
3657 | # Pleroma bug workaround: Empty string becomes start of epoch | 3756 | mark_delete.append(k) |
3658 | json_object[k] = datetime.datetime.fromtimestamp(0) | 3757 | # Two step process because otherwise python gets very upset |
3659 | else: | 3758 | for k in mark_delete: |
3660 | raise MastodonAPIError('Encountered invalid date.') | 3759 | del json_object[k] |
3661 | return json_object | 3760 | return json_object |
3662 | 3761 | ||
3663 | @staticmethod | 3762 | @staticmethod |
@@ -3710,12 +3809,20 @@ class Mastodon: | |||
3710 | isotime = isotime[:-2] + ":" + isotime[-2:] | 3809 | isotime = isotime[:-2] + ":" + isotime[-2:] |
3711 | return isotime | 3810 | return isotime |
3712 | 3811 | ||
3713 | def __api_request(self, method, endpoint, params={}, files={}, headers={}, access_token_override=None, base_url_override=None, do_ratelimiting=True, use_json=False, parse=True, return_response_object=False, skip_error_check=False): | 3812 | def __api_request(self, method, endpoint, params={}, files={}, headers={}, access_token_override=None, base_url_override=None, |
3813 | do_ratelimiting=True, use_json=False, parse=True, return_response_object=False, skip_error_check=False, lang_override=None): | ||
3714 | """ | 3814 | """ |
3715 | Internal API request helper. | 3815 | Internal API request helper. |
3716 | """ | 3816 | """ |
3717 | response = None | 3817 | response = None |
3718 | remaining_wait = 0 | 3818 | remaining_wait = 0 |
3819 | |||
3820 | # Add language to params if not None | ||
3821 | lang = self.lang | ||
3822 | if lang_override is not None: | ||
3823 | lang = lang_override | ||
3824 | if lang is not None: | ||
3825 | params["lang"] = lang | ||
3719 | 3826 | ||
3720 | # "pace" mode ratelimiting: Assume constant rate of requests, sleep a little less long than it | 3827 | # "pace" mode ratelimiting: Assume constant rate of requests, sleep a little less long than it |
3721 | # would take to not hit the rate limit at that request rate. | 3828 | # would take to not hit the rate limit at that request rate. |
@@ -3774,8 +3881,7 @@ class Mastodon: | |||
3774 | else: | 3881 | else: |
3775 | kwargs['data'] = params | 3882 | kwargs['data'] = params |
3776 | 3883 | ||
3777 | response_object = self.session.request( | 3884 | response_object = self.session.request(method, base_url + endpoint, **kwargs) |
3778 | method, base_url + endpoint, **kwargs) | ||
3779 | except Exception as e: | 3885 | except Exception as e: |
3780 | raise MastodonNetworkError( | 3886 | raise MastodonNetworkError( |
3781 | "Could not complete request: %s" % e) | 3887 | "Could not complete request: %s" % e) |
@@ -3818,15 +3924,14 @@ class Mastodon: | |||
3818 | 3924 | ||
3819 | # Handle response | 3925 | # Handle response |
3820 | if self.debug_requests: | 3926 | if self.debug_requests: |
3821 | print('Mastodon: Response received with code ' + | 3927 | print('Mastodon: Response received with code ' + str(response_object.status_code) + '.') |
3822 | str(response_object.status_code) + '.') | ||
3823 | print('response headers: ' + str(response_object.headers)) | 3928 | print('response headers: ' + str(response_object.headers)) |
3824 | print('Response text content: ' + str(response_object.text)) | 3929 | print('Response text content: ' + str(response_object.text)) |
3825 | 3930 | ||
3826 | if not response_object.ok: | 3931 | if not response_object.ok: |
3827 | try: | 3932 | try: |
3828 | response = response_object.json( | 3933 | response = response_object.json(object_hook=self.__json_hooks) |
3829 | object_hook=self.__json_hooks) | 3934 | print(response) |
3830 | if isinstance(response, dict) and 'error' in response: | 3935 | if isinstance(response, dict) and 'error' in response: |
3831 | error_msg = response['error'] | 3936 | error_msg = response['error'] |
3832 | elif isinstance(response, str): | 3937 | elif isinstance(response, str): |
@@ -3880,8 +3985,7 @@ class Mastodon: | |||
3880 | 3985 | ||
3881 | if parse: | 3986 | if parse: |
3882 | try: | 3987 | try: |
3883 | response = response_object.json( | 3988 | response = response_object.json(object_hook=self.__json_hooks) |
3884 | object_hook=self.__json_hooks) | ||
3885 | except: | 3989 | except: |
3886 | raise MastodonAPIError( | 3990 | raise MastodonAPIError( |
3887 | "Could not parse response as JSON, response code was %s, " | 3991 | "Could not parse response as JSON, response code was %s, " |