Skip to content

Commit

Permalink
add lang parameter
Browse files Browse the repository at this point in the history
  • Loading branch information
halcy authored and halcy committed Nov 24, 2022
1 parent 89678e0 commit 4be050d
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 29 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ v1.8.0
------
* BREAKING CHANGE: Switch the base URL to None, throw an error when no base url is passed. Having mastosoc as default was sensible when there were only three mastodon servers. It is not sensible now and trips people up constantly.
* Fix an issue with the fix for the Pleroma date bug (thanks adbenitez)
* Add trending APIs (`trending_tags`, `trending_statuses`, `trending_links`, `admin_trending_tags`, `admin_trending_statuses`, `admin_trending_links`)
* Add `lang` parameter and document what it does properly.
* Add `category` and `rule_ids` to `reports``

v1.7.0
------
Expand Down
2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Refer to mastodon changelog and API docs for details when implementing, add or m
* [ ] Add GET /api/v1/accounts/familiar_followers to REST API
* [ ] Add POST /api/v1/accounts/:id/remove_from_followers to REST API
* [x] Add category and rule_ids params to POST /api/v1/reports IN REST API
* [ ] Add global lang param to REST API
* [x] Add global lang param to REST API
* [x] Add types param to GET /api/v1/notifications in REST API
* [x] Add notifications for moderators about new sign-ups
* [ ] v2 admin account api
Expand Down
83 changes: 57 additions & 26 deletions mastodon/Mastodon.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,11 +320,9 @@ def create_app(client_name, scopes=__DEFAULT_SCOPES, redirect_uris=None, website
###
# Authentication, including constructor
###
def __init__(self, client_id=None, client_secret=None, access_token=None,
api_base_url=None, debug_requests=False,
ratelimit_method="wait", ratelimit_pacefactor=1.1,
request_timeout=__DEFAULT_TIMEOUT, mastodon_version=None,
version_check_mode="created", session=None, feature_set="mainline", user_agent="mastodonpy"):
def __init__(self, client_id=None, client_secret=None, access_token=None, api_base_url=None, debug_requests=False,
ratelimit_method="wait", ratelimit_pacefactor=1.1, request_timeout=__DEFAULT_TIMEOUT, mastodon_version=None,
version_check_mode="created", session=None, feature_set="mainline", user_agent="mastodonpy", lang=None):
"""
Create a new API wrapper instance based on the given `client_secret` and `client_id` on the
instance given by `api_base_url`. If you give a `client_id` and it is not a file, you must
Expand Down Expand Up @@ -371,6 +369,10 @@ def __init__(self, client_id=None, client_secret=None, access_token=None,
the app name will be used as `User-Agent` header as default. It is possible to modify old secret files and append
a client app name to use it as a `User-Agent` name.
`lang` can be used to change the locale Mastodon will use to generate responses. Valid parameters are all ISO 639-1 (two letter)
or for a language that has none, 639-3 (three letter) language codes. This affects some error messages (those related to validation) and
trends. You can change the language using `set_language()`_.
If no other `User-Agent` is specified, "mastodonpy" will be used.
"""
self.api_base_url = api_base_url
Expand All @@ -383,7 +385,7 @@ def __init__(self, client_id=None, client_secret=None, access_token=None,
self.ratelimit_method = ratelimit_method
self._token_expired = datetime.datetime.now()
self._refresh_token = None

self.__logged_in_id = None

self.ratelimit_limit = 300
Expand All @@ -406,6 +408,9 @@ def __init__(self, client_id=None, client_secret=None, access_token=None,
# General defined user-agent
self.user_agent = user_agent

# Save language
self.lang = lang

# Token loading
if self.client_id is not None:
if os.path.isfile(self.client_id):
Expand Down Expand Up @@ -467,6 +472,13 @@ def __init__(self, client_id=None, client_secret=None, access_token=None,
if ratelimit_method not in ["throw", "wait", "pace"]:
raise MastodonIllegalArgumentError("Invalid ratelimit method.")

def set_language(self, lang):
"""
Set the locale Mastodon will use to generate responses. Valid parameters are all ISO 639-1 (two letter) or, for languages that do
not have one, 639-3 (three letter) language codes. This affects some error messages (those related to validation) and trends.
"""
self.lang = lang

def __normalize_version_string(self, version_string):
# 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
version_string = version_string.split(" ")[0]
Expand Down Expand Up @@ -538,24 +550,27 @@ def get_supported_version():
"""
return Mastodon.__SUPPORTED_MASTODON_VERSION

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):
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):
"""
Returns the URL that a client needs to request an OAuth grant from the server.
To log in with OAuth, send your user to this URL. The user will then log in and
get a code which you can pass to log_in.
get a code which you can pass to `log_in()`_.
scopes are as in `log_in()`_, redirect_uris is where the user should be redirected to
after authentication. Note that redirect_uris must be one of the URLs given during
`scopes` are as in `log_in()`_, redirect_uris is where the user should be redirected to
after authentication. Note that `redirect_uris` must be one of the URLs given during
app registration. When using urn:ietf:wg:oauth:2.0:oob, the code is simply displayed,
otherwise it is added to the given URL as the "code" request parameter.
Pass force_login if you want the user to always log in even when already logged
into web Mastodon (i.e. when registering multiple different accounts in an app).
State is the oauth `state`parameter to pass to the server. It is strongly suggested
`state` is the oauth `state` parameter to pass to the server. It is strongly suggested
to use a random, nonguessable value (i.e. nothing meaningful and no incrementing ID)
to preserve security guarantees. It can be left out for non-web login flows.
Pass an ISO 639-1 (two letter) or, for languages that do not have one, 639-3 (three letter)
language code as `lang` to control the display language for the oauth form.
"""
if client_id is None:
client_id = self.client_id
Expand All @@ -571,6 +586,7 @@ def auth_request_url(self, client_id=None, redirect_uris="urn:ietf:wg:oauth:2.0:
params['scope'] = " ".join(scopes)
params['force_login'] = force_login
params['state'] = state
params['lang'] = lang
formatted_params = urlencode(params)
return "".join([self.api_base_url, "/oauth/authorize?", formatted_params])

Expand Down Expand Up @@ -672,8 +688,9 @@ def create_account(self, username, password, email, agreement=False, reason=None
Creates a new user account with the given username, password and email. "agreement"
must be set to true (after showing the user the instance's user agreement and having
them agree to it), "locale" specifies the language for the confirmation email as an
ISO 639-1 (two-letter) language code. `reason` can be used to specify why a user
would like to join if approved-registrations mode is on.
ISO 639-1 (two letter) or, if a language does not have one, 639-3 (three letter) language
code. `reason` can be used to specify why a user would like to join if approved-registrations
mode is on.
Does not require an access token, but does require a client grant.
Expand Down Expand Up @@ -1542,10 +1559,10 @@ def trends(self, limit=None):
"""
Alias for `trending_tags()`_
"""
return self.trending_tags(limit = limit)
return self.trending_tags(limit=limit)

@api_version("3.5.0", "3.5.0", __DICT_VERSION_HASHTAG)
def trending_tags(self, limit=None):
def trending_tags(self, limit=None, lang=None):
"""
Fetch trending-hashtag information, if the instance provides such information.
Expand All @@ -1557,6 +1574,8 @@ def trending_tags(self, limit=None):
Important versioning note: This endpoint does not exist for Mastodon versions
between 2.8.0 (inclusive) and 3.0.0 (exclusive).
Pass `lang` to override the global locale parameter, which may affect trend ordering.
Returns a list of `hashtag dicts`_, sorted by the instance's trending algorithm,
descending.
"""
Expand All @@ -1575,6 +1594,8 @@ def trending_statuses(self):
Specify `limit` to limit how many results are returned (the maximum number
of results is 10, the endpoint is not paginated).
Pass `lang` to override the global locale parameter, which may affect trend ordering.
Returns a list of `toot dicts`_, sorted by the instances's trending algorithm,
descending.
"""
Expand Down Expand Up @@ -1981,7 +2002,8 @@ def status_post(self, status, in_reply_to_id=None, media_ids=None,
displayed.
Specify `language` to override automatic language detection. The parameter
accepts all valid ISO 639-2 language codes.
accepts all valid ISO 639-1 (2-letter) or for languages where that do not
have one, 639-3 (three letter) language codes.
You can set `idempotency_key` to a value to uniquely identify an attempt
at posting a status. Even if you call this function more than once,
Expand Down Expand Up @@ -3638,6 +3660,7 @@ def __json_date_parse(json_object):
"""
known_date_fields = ["created_at", "week", "day", "expires_at", "scheduled_at",
"updated_at", "last_status_at", "starts_at", "ends_at", "published_at", "edited_at"]
mark_delete = []
for k, v in json_object.items():
if k in known_date_fields:
if v is not None:
Expand All @@ -3648,7 +3671,10 @@ def __json_date_parse(json_object):
json_object[k] = dateutil.parser.parse(v)
except:
# When we can't parse a date, we just leave the field out
del json_object[k]
mark_delete.append(k)
# Two step process because otherwise python gets very upset
for k in mark_delete:
del json_object[k]
return json_object

@staticmethod
Expand Down Expand Up @@ -3701,12 +3727,20 @@ def __consistent_isoformat_utc(datetime_val):
isotime = isotime[:-2] + ":" + isotime[-2:]
return isotime

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):
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, lang_override=None):
"""
Internal API request helper.
"""
response = None
remaining_wait = 0

# Add language to params if not None
lang = self.lang
if lang_override is not None:
lang = lang_override
if lang is not None:
params["lang"] = lang

# "pace" mode ratelimiting: Assume constant rate of requests, sleep a little less long than it
# would take to not hit the rate limit at that request rate.
Expand Down Expand Up @@ -3765,8 +3799,7 @@ def __api_request(self, method, endpoint, params={}, files={}, headers={}, acces
else:
kwargs['data'] = params

response_object = self.session.request(
method, base_url + endpoint, **kwargs)
response_object = self.session.request(method, base_url + endpoint, **kwargs)
except Exception as e:
raise MastodonNetworkError(
"Could not complete request: %s" % e)
Expand Down Expand Up @@ -3809,15 +3842,14 @@ def __api_request(self, method, endpoint, params={}, files={}, headers={}, acces

# Handle response
if self.debug_requests:
print('Mastodon: Response received with code ' +
str(response_object.status_code) + '.')
print('Mastodon: Response received with code ' + str(response_object.status_code) + '.')
print('response headers: ' + str(response_object.headers))
print('Response text content: ' + str(response_object.text))

if not response_object.ok:
try:
response = response_object.json(
object_hook=self.__json_hooks)
response = response_object.json(object_hook=self.__json_hooks)
print(response)
if isinstance(response, dict) and 'error' in response:
error_msg = response['error']
elif isinstance(response, str):
Expand Down Expand Up @@ -3871,8 +3903,7 @@ def __api_request(self, method, endpoint, params={}, files={}, headers={}, acces

if parse:
try:
response = response_object.json(
object_hook=self.__json_hooks)
response = response_object.json(object_hook=self.__json_hooks)
except:
raise MastodonAPIError(
"Could not parse response as JSON, response code was %s, "
Expand Down
136 changes: 136 additions & 0 deletions tests/cassettes/test_lang_for_errors.yaml

Large diffs are not rendered by default.

25 changes: 23 additions & 2 deletions tests/test_errors.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import pytest
import vcr
from mastodon.Mastodon import MastodonAPIError
import json

try:
from mock import MagicMock
Expand All @@ -8,8 +10,7 @@

def test_nonstandard_errors(api):
response = MagicMock()
response.json = MagicMock(return_value=
"I am a non-standard instance and this error is a plain string.")
response.json = MagicMock(return_value="I am a non-standard instance and this error is a plain string.")
response.ok = False
response.status_code = 501
session = MagicMock()
Expand All @@ -19,3 +20,23 @@ def test_nonstandard_errors(api):
with pytest.raises(MastodonAPIError):
api.instance()

@pytest.mark.vcr()
def test_lang_for_errors(api):
try:
api.status_post("look at me i am funny shark gawr gura: " + "a" * 50000)
except Exception as e:
e1 = str(e)
api.set_language("de")
try:
api.status_post("look at me i am funny shark gawr gura: " + "a" * 50000)
except Exception as e:
e2 = str(e)
assert e1 != e2

def test_broken_date(api):
dict1 = json.loads('{"uri":"icosahedron.website", "created_at": "", "edited_at": "2012-09-27"}', object_hook=api._Mastodon__json_hooks)
dict2 = json.loads('{"uri":"icosahedron.website", "created_at": "2012-09-27", "subfield": {"edited_at": "null"}}', object_hook=api._Mastodon__json_hooks)
assert "edited_at" in dict1
assert not "created_at" in dict1
assert "created_at" in dict2
assert not "edited_at" in dict2.subfield

0 comments on commit 4be050d

Please sign in to comment.