From fe5b9d1a46a89b45b200e3bfe3844f36c22d805a Mon Sep 17 00:00:00 2001 From: Daniel Schwarz Date: Tue, 13 Dec 2022 12:45:07 -0500 Subject: [PATCH 1/4] React properly to 422: Validation Failed. Status has already been taken errors --- toot/api.py | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/toot/api.py b/toot/api.py index 1ce1ec0..2e44b12 100644 --- a/toot/api.py +++ b/toot/api.py @@ -6,7 +6,7 @@ import uuid from urllib.parse import urlparse, urlencode, quote from toot import http, CLIENT_NAME, CLIENT_WEBSITE -from toot.exceptions import AuthenticationError +from toot.exceptions import AuthenticationError, ApiError from toot.utils import str_bool SCOPES = 'read write follow' @@ -23,6 +23,24 @@ def _status_action(app, user, status_id, action): return http.post(app, user, url).json() +def _status_toggle_action(app, user, status_id, action): + url = '/api/v1/statuses/{}/{}'.format(status_id, action) + + try: + response = http.post(app, user, url).json() + except ApiError as e: + # For "toggle" operations, Mastodon returns unhelpful + # 422: "Validation failed: Status has already been taken" + # responses when you try to bookmark a status already + # bookmarked, or favourite a status already favourited + # so we just swallow those errors here + if str(e) == "Validation failed: Status has already been taken": + response = None + else: + # not the error we expected; re-raise the exception + raise e + finally: + return response def create_app(domain, scheme='https'): url = '{}://{}/api/v1/apps'.format(scheme, domain) @@ -180,38 +198,40 @@ def delete_status(app, user, status_id): def favourite(app, user, status_id): - return _status_action(app, user, status_id, 'favourite') + return _status_toggle_action(app, user, status_id, 'favourite') def unfavourite(app, user, status_id): - return _status_action(app, user, status_id, 'unfavourite') + return _status_toggle_action(app, user, status_id, 'unfavourite') def reblog(app, user, status_id): - return _status_action(app, user, status_id, 'reblog') + return _status_toggle_action(app, user, status_id, 'reblog') def unreblog(app, user, status_id): - return _status_action(app, user, status_id, 'unreblog') + return _status_toggle_action(app, user, status_id, 'unreblog') def pin(app, user, status_id): - return _status_action(app, user, status_id, 'pin') + return _status_toggle_action(app, user, status_id, 'pin') def unpin(app, user, status_id): - return _status_action(app, user, status_id, 'unpin') + return _status_toggle_action(app, user, status_id, 'unpin') def bookmark(app, user, status_id): - return _status_action(app, user, status_id, 'bookmark') + return _status_toggle_action(app, user, status_id, 'bookmark') def unbookmark(app, user, status_id): - return _status_action(app, user, status_id, 'unbookmark') + return _status_toggle_action(app, user, status_id, 'unbookmark') def translate(app, user, status_id): + # don't use status_toggle_action for translate as this is + # not toggling anything server-side; it's a read only operation. return _status_action(app, user, status_id, 'translate') From ecb9c75f2e3d88a064571a69f2d2b5e01f8a9e5c Mon Sep 17 00:00:00 2001 From: Daniel Schwarz Date: Tue, 13 Dec 2022 12:45:07 -0500 Subject: [PATCH 2/4] React properly to 422: Validation Failed. Status has already been taken errors --- toot/api.py | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/toot/api.py b/toot/api.py index 478949e..f3c3a21 100644 --- a/toot/api.py +++ b/toot/api.py @@ -4,7 +4,7 @@ import uuid from urllib.parse import urlparse, urlencode, quote from toot import http, CLIENT_NAME, CLIENT_WEBSITE -from toot.exceptions import AuthenticationError +from toot.exceptions import AuthenticationError, ApiError from toot.utils import str_bool SCOPES = 'read write follow' @@ -27,6 +27,23 @@ def _tag_action(app, user, tag_name, action): return http.post(app, user, url).json() +def _status_toggle_action(app, user, status_id, action, data=None): + url = '/api/v1/statuses/{}/{}'.format(status_id, action) + + try: + response = http.post(app, user, url, data=data).json() + except ApiError as e: + # For "toggle" operations, Mastodon returns unhelpful + # 422: "Validation failed:" + # responses when you try to bookmark a status already + # bookmarked, or favourite a status already favourited + # so we just swallow those errors here + if str(e).startswith("Validation failed:"): + return None # FIXME: return mock OK Response object? + else: + # not the error we expected; re-raise the exception + raise e + return response def create_app(domain, scheme='https'): url = '{}://{}/api/v1/apps'.format(scheme, domain) @@ -184,38 +201,40 @@ def delete_status(app, user, status_id): def favourite(app, user, status_id): - return _status_action(app, user, status_id, 'favourite') + return _status_toggle_action(app, user, status_id, 'favourite') def unfavourite(app, user, status_id): - return _status_action(app, user, status_id, 'unfavourite') + return _status_toggle_action(app, user, status_id, 'unfavourite') def reblog(app, user, status_id, visibility="public"): - return _status_action(app, user, status_id, 'reblog', data={"visibility": visibility}) + return _status_toggle_action(app, user, status_id, 'reblog', data={"visibility": visibility}) def unreblog(app, user, status_id): - return _status_action(app, user, status_id, 'unreblog') + return _status_toggle_action(app, user, status_id, 'unreblog') def pin(app, user, status_id): - return _status_action(app, user, status_id, 'pin') + return _status_toggle_action(app, user, status_id, 'pin') def unpin(app, user, status_id): - return _status_action(app, user, status_id, 'unpin') + return _status_toggle_action(app, user, status_id, 'unpin') def bookmark(app, user, status_id): - return _status_action(app, user, status_id, 'bookmark') + return _status_toggle_action(app, user, status_id, 'bookmark') def unbookmark(app, user, status_id): - return _status_action(app, user, status_id, 'unbookmark') + return _status_toggle_action(app, user, status_id, 'unbookmark') def translate(app, user, status_id): + # don't use status_toggle_action for translate as this is + # not toggling anything server-side; it's a read only operation. return _status_action(app, user, status_id, 'translate') From d21b2920cb479662a12fdbafedc7c49aab636f01 Mon Sep 17 00:00:00 2001 From: Daniel Schwarz Date: Tue, 5 Mar 2024 19:58:54 -0500 Subject: [PATCH 3/4] Fix for compatibility with more recent versions of toot --- toot/api.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/toot/api.py b/toot/api.py index c48a76c..df008d5 100644 --- a/toot/api.py +++ b/toot/api.py @@ -27,11 +27,6 @@ def _tag_action(app, user, tag_name, action): def _status_toggle_action(app, user, status_id, action, data=None): url = '/api/v1/statuses/{}/{}'.format(status_id, action) - return http.post(app, user, url, data=data).json() - - -def _status_toggle_action(app, user, status_id, action): - url = '/api/v1/statuses/{}/{}'.format(status_id, action) try: response = http.post(app, user, url).json() @@ -49,7 +44,7 @@ def _status_toggle_action(app, user, status_id, action): finally: return response - + def create_app(domain, scheme='https'): url = f"{scheme}://{domain}/api/v1/apps" From 31bbb20324bddc985d61384a4182709c83ff7454 Mon Sep 17 00:00:00 2001 From: Daniel Schwarz Date: Tue, 5 Mar 2024 20:07:28 -0500 Subject: [PATCH 4/4] Make this fix compatible with latest master --- toot/api.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/toot/api.py b/toot/api.py index 470eabb..2c95249 100644 --- a/toot/api.py +++ b/toot/api.py @@ -8,7 +8,7 @@ from typing import BinaryIO, List, Optional from urllib.parse import urlparse, urlencode, quote from toot import App, User, http, CLIENT_NAME, CLIENT_WEBSITE -from toot.exceptions import AuthenticationError, ApiError, ConsoleError +from toot.exceptions import ApiError, ConsoleError from toot.utils import drop_empty_values, str_bool, str_bool_nullable @@ -76,10 +76,6 @@ def _status_toggle_action(app, user, status_id, action, data=None): def create_app(domain, scheme='https'): url = f"{scheme}://{domain}/api/v1/apps" -#def create_app(base_url): -# url = f"{base_url}/api/v1/apps" - - json = { 'client_name': CLIENT_NAME, 'redirect_uris': 'urn:ietf:wg:oauth:2.0:oob',