diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f317fa7..7ceadf1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -37,6 +37,7 @@ v2.0.0 (IN PROGRESS) * Add media attachment editing * Add email domain blocking support (`admin_email_domain_blocks`, `admin_email_domain_block`, `admin_email_domain_block_create`, `admin_email_domain_block_delete`) * Add `instance_extended_description` +* Add `instance_translation_languages`, translation support (`status_translate`) v1.8.1 ------ diff --git a/docs/05_statuses.rst b/docs/05_statuses.rst index 3fd1da4..3aa53ab 100644 --- a/docs/05_statuses.rst +++ b/docs/05_statuses.rst @@ -84,4 +84,9 @@ Reading Writing ~~~~~~~ -.. automethod:: Mastodon.poll_vote \ No newline at end of file +.. automethod:: Mastodon.poll_vote + +Translation +----------- +These functions allow you to get machine translations for statuses, if the instance supports it. +.. automethod:: Mastodon.status_translate \ No newline at end of file diff --git a/docs/08_instances.rst b/docs/08_instances.rst index 21b0671..79749f1 100644 --- a/docs/08_instances.rst +++ b/docs/08_instances.rst @@ -18,7 +18,7 @@ current instance as well as data from the instance-wide profile directory. .. automethod:: Mastodon.instance_nodeinfo .. automethod:: Mastodon.instance_rules .. automethod:: Mastodon.instance_extended_description - + Profile directory ~~~~~~~~~~~~~~~~~ .. automethod:: Mastodon.directory @@ -59,3 +59,7 @@ These functions allow you to search for users, tags and, when enabled, full text .. automethod:: Mastodon.search .. automethod:: Mastodon.search_v2 + +Translation support +------------------- +.. automethod:: Mastodon.instance_translation_languages \ No newline at end of file diff --git a/mastodon/instance.py b/mastodon/instance.py index 1b7f3ff..06718d2 100644 --- a/mastodon/instance.py +++ b/mastodon/instance.py @@ -6,7 +6,7 @@ from mastodon.compat import urlparse from mastodon.internals import Mastodon as Internals from mastodon.return_types import Instance, InstanceV2, NonPaginatableList, Activity, Nodeinfo, AttribAccessDict, Rule, Announcement, CustomEmoji, Account, IdType, ExtendedDescription -from typing import Union, Optional +from typing import Union, Optional, Dict, List class Mastodon(Internals): ### @@ -206,3 +206,16 @@ class Mastodon(Internals): Retrieve the instance's extended description. """ return self.__api_request('GET', '/api/v1/instance/extended_description', parse=False).decode("utf-8") + + def instance_translation_languages(self) -> Dict[str, List[str]]: + """ + Retrieve the instance's translation languages. + + Returns a dict with language pairs, where the key is the language code and the value is a list of language codes that the instance can translate that language to. + """ + ret_value = self.__api_request('GET', '/api/v1/instance/translation_languages') + result_real = AttribAccessDict() + for key, value in ret_value.items(): + result_real[key] = NonPaginatableList(value) + return result_real + \ No newline at end of file diff --git a/mastodon/statuses.py b/mastodon/statuses.py index fb54262..1187ae7 100644 --- a/mastodon/statuses.py +++ b/mastodon/statuses.py @@ -9,7 +9,7 @@ from mastodon.utility import api_version from mastodon.internals import Mastodon as Internals from mastodon.return_types import Status, IdType, ScheduledStatus, PreviewCard, Context, NonPaginatableList, Account,\ - MediaAttachment, Poll, StatusSource, StatusEdit, PaginatableList, PathOrFile + MediaAttachment, Poll, StatusSource, StatusEdit, PaginatableList, PathOrFile, Translation from typing import Union, Optional, List, Dict, Any, Tuple @@ -570,3 +570,18 @@ class Mastodon(Internals): """ id = self.__unpack_id(id) self.__api_request('DELETE', f'/api/v1/scheduled_statuses/{id}') + + ## + # Translation + ## + @api_version("4.0.0", "4.0.0") + def status_translate(self, id: Union[Status, IdType], lang: Optional[str] = None) -> Translation: + """ + Translate the status content into some language. + + Raises a MastodonAPIError if the server can't perform the requested translation, for any + reason (doesn't support translation, unsupported language pair, etc.). + """ + id = self.__unpack_id(id) + params = self.__generate_params(locals(), ['id']) + return self.__api_request('POST', f'/api/v1/statuses/{id}/translate', params) \ No newline at end of file diff --git a/tests/cassettes/test_status_translate.yaml b/tests/cassettes/test_status_translate.yaml new file mode 100644 index 0000000..cbc8950 --- /dev/null +++ b/tests/cassettes/test_status_translate.yaml @@ -0,0 +1,193 @@ +interactions: +- request: + body: status=Toot%21 + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer __MASTODON_PY_TEST_ACCESS_TOKEN + Connection: + - keep-alive + Content-Length: + - '14' + Content-Type: + - application/x-www-form-urlencoded + User-Agent: + - tests/v311 + method: POST + uri: http://localhost:3000/api/v1/statuses + response: + body: + string: '{"id":"114008596311558624","created_at":"2025-02-15T15:25:22.431Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":false,"spoiler_text":"","visibility":"private","language":"ja","uri":"http://localhost:3000/users/mastodonpy_test/statuses/114008596311558624","url":"http://localhost:3000/@mastodonpy_test/114008596311558624","replies_count":0,"reblogs_count":0,"favourites_count":0,"edited_at":null,"favourited":false,"reblogged":false,"muted":false,"bookmarked":false,"pinned":false,"content":"\u003cp\u003eToot!\u003c/p\u003e","filtered":[],"reblog":null,"application":{"name":"Mastodon.py + test suite","website":null},"account":{"id":"113998801242326861","username":"mastodonpy_test","acct":"mastodonpy_test","display_name":"John + Lennon","locked":true,"bot":false,"discoverable":null,"indexable":false,"group":false,"created_at":"2025-02-13T00:00:00.000Z","note":"\u003cp\u003eI + walk funny\u003c/p\u003e","url":"http://localhost:3000/@mastodonpy_test","uri":"http://localhost:3000/users/mastodonpy_test","avatar":"http://localhost:3000/avatars/original/missing.png","avatar_static":"http://localhost:3000/avatars/original/missing.png","header":"http://localhost:3000/headers/original/missing.png","header_static":"http://localhost:3000/headers/original/missing.png","followers_count":0,"following_count":1,"statuses_count":9,"last_status_at":"2025-02-15","hide_collections":null,"noindex":false,"emojis":[],"roles":[],"fields":[{"name":"bread","value":"toasty.","verified_at":null},{"name":"lasagna","value":"no!!!","verified_at":null}]},"media_attachments":[],"mentions":[],"tags":[],"emojis":[],"card":null,"poll":null}' + headers: + Cache-Control: + - private, no-store + Content-Length: + - '1638' + Content-Security-Policy: + - default-src 'none'; frame-ancestors 'none'; form-action 'none' + Content-Type: + - application/json; charset=utf-8 + ETag: + - W/"b7efe40a95ee9fc07d42c7656cbdf8ec" + Referrer-Policy: + - strict-origin-when-cross-origin + Server-Timing: + - cache_read.active_support;dur=0.08, sql.active_record;dur=19.05, cache_generate.active_support;dur=5.82, + cache_write.active_support;dur=0.18, instantiation.active_record;dur=1.14, + start_processing.action_controller;dur=0.00, transaction.active_record;dur=10.37, + render.active_model_serializers;dur=23.35, process_action.action_controller;dur=79.00 + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-RateLimit-Limit: + - '300' + X-RateLimit-Remaining: + - '299' + X-RateLimit-Reset: + - '2025-02-15T18:00:00.477175Z' + X-Request-Id: + - f44e326a-a840-4685-92b7-008f5552cff8 + X-Runtime: + - '0.103655' + X-XSS-Protection: + - '0' + vary: + - Authorization, Origin + status: + code: 200 + message: OK +- request: + body: lang=de + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer __MASTODON_PY_TEST_ACCESS_TOKEN + Connection: + - keep-alive + Content-Length: + - '7' + Content-Type: + - application/x-www-form-urlencoded + User-Agent: + - tests/v311 + method: POST + uri: http://localhost:3000/api/v1/statuses/114008596311558624/translate + response: + body: + string: '{"error":"This action is not allowed"}' + headers: + Cache-Control: + - private, no-store + Content-Length: + - '38' + Content-Security-Policy: + - default-src 'none'; frame-ancestors 'none'; form-action 'none' + Content-Type: + - application/json; charset=utf-8 + Referrer-Policy: + - strict-origin-when-cross-origin + Server-Timing: + - cache_read.active_support;dur=0.02, sql.active_record;dur=1.74, cache_generate.active_support;dur=1.03, + cache_write.active_support;dur=0.08, instantiation.active_record;dur=0.58, + start_processing.action_controller;dur=0.01, render.active_model_serializers;dur=0.05, + process_action.action_controller;dur=45.87 + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-RateLimit-Limit: + - '300' + X-RateLimit-Remaining: + - '299' + X-RateLimit-Reset: + - '2025-02-15T15:30:00.565674Z' + X-Request-Id: + - 3e421f8f-e7eb-4e1c-9d60-6d5d726ab2c1 + X-Runtime: + - '0.081421' + X-XSS-Protection: + - '0' + vary: + - Authorization, Origin + status: + code: 403 + message: Forbidden +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer __MASTODON_PY_TEST_ACCESS_TOKEN + Connection: + - keep-alive + Content-Length: + - '0' + User-Agent: + - tests/v311 + method: DELETE + uri: http://localhost:3000/api/v1/statuses/114008596311558624 + response: + body: + string: '{"id":"114008596311558624","created_at":"2025-02-15T15:25:22.431Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":false,"spoiler_text":"","visibility":"private","language":"ja","uri":"http://localhost:3000/users/mastodonpy_test/statuses/114008596311558624","url":"http://localhost:3000/@mastodonpy_test/114008596311558624","replies_count":0,"reblogs_count":0,"favourites_count":0,"edited_at":null,"favourited":false,"reblogged":false,"muted":false,"bookmarked":false,"pinned":false,"text":"Toot!","filtered":[],"reblog":null,"application":{"name":"Mastodon.py + test suite","website":null},"account":{"id":"113998801242326861","username":"mastodonpy_test","acct":"mastodonpy_test","display_name":"John + Lennon","locked":true,"bot":false,"discoverable":null,"indexable":false,"group":false,"created_at":"2025-02-13T00:00:00.000Z","note":"\u003cp\u003eI + walk funny\u003c/p\u003e","url":"http://localhost:3000/@mastodonpy_test","uri":"http://localhost:3000/users/mastodonpy_test","avatar":"http://localhost:3000/avatars/original/missing.png","avatar_static":"http://localhost:3000/avatars/original/missing.png","header":"http://localhost:3000/headers/original/missing.png","header_static":"http://localhost:3000/headers/original/missing.png","followers_count":0,"following_count":1,"statuses_count":8,"last_status_at":"2025-02-15","hide_collections":null,"noindex":false,"emojis":[],"roles":[],"fields":[{"name":"bread","value":"toasty.","verified_at":null},{"name":"lasagna","value":"no!!!","verified_at":null}]},"media_attachments":[],"mentions":[],"tags":[],"emojis":[],"card":null,"poll":null}' + headers: + Cache-Control: + - private, no-store + Content-Length: + - '1608' + Content-Security-Policy: + - default-src 'none'; frame-ancestors 'none'; form-action 'none' + Content-Type: + - application/json; charset=utf-8 + ETag: + - W/"34838106aadb8da6b97b3fb5ba7dc85e" + Referrer-Policy: + - strict-origin-when-cross-origin + Server-Timing: + - cache_read.active_support;dur=0.05, sql.active_record;dur=7.63, cache_generate.active_support;dur=3.53, + cache_write.active_support;dur=0.16, instantiation.active_record;dur=0.61, + start_processing.action_controller;dur=0.00, transaction.active_record;dur=4.03, + render.active_model_serializers;dur=27.61, process_action.action_controller;dur=57.26 + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-RateLimit-Limit: + - '30' + X-RateLimit-Remaining: + - '29' + X-RateLimit-Reset: + - '2025-02-15T15:30:00.683901Z' + X-Request-Id: + - a79bf3e1-4091-4332-9c47-45e8766ba9e6 + X-Runtime: + - '0.072595' + X-XSS-Protection: + - '0' + vary: + - Authorization, Origin + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_translation_languages.yaml b/tests/cassettes/test_translation_languages.yaml new file mode 100644 index 0000000..dae2ed7 --- /dev/null +++ b/tests/cassettes/test_translation_languages.yaml @@ -0,0 +1,63 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer __MASTODON_PY_TEST_ACCESS_TOKEN + Connection: + - keep-alive + User-Agent: + - tests/v311 + method: GET + uri: http://localhost:3000/api/v1/instance/translation_languages + response: + body: + string: '{}' + headers: + Cache-Control: + - max-age=300, public, stale-while-revalidate=30, stale-if-error=86400 + Content-Length: + - '2' + Content-Security-Policy: + - default-src 'none'; frame-ancestors 'none'; form-action 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Sat, 15 Feb 2025 15:20:56 GMT + ETag: + - W/"44136fa355b3678a1146ad16f7e8649e" + Referrer-Policy: + - strict-origin-when-cross-origin + Server-Timing: + - cache_read.active_support;dur=0.04, sql.active_record;dur=3.48, cache_generate.active_support;dur=6.47, + cache_write.active_support;dur=0.13, instantiation.active_record;dur=0.38, + start_processing.action_controller;dur=0.01, render.active_model_serializers;dur=0.02, + process_action.action_controller;dur=37.74 + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-RateLimit-Limit: + - '300' + X-RateLimit-Remaining: + - '299' + X-RateLimit-Reset: + - '2025-02-15T15:25:00.208109Z' + X-Request-Id: + - 915a0f13-8d73-43e0-bcfe-2f43294bdb6d + X-Runtime: + - '0.074836' + X-XSS-Protection: + - '0' + vary: + - Accept, Origin + status: + code: 200 + message: OK +version: 1 diff --git a/tests/test_instance.py b/tests/test_instance.py index 95f35f4..0902add 100644 --- a/tests/test_instance.py +++ b/tests/test_instance.py @@ -100,3 +100,7 @@ def test_version_parsing(api): assert parse_version_string(api._Mastodon__normalize_version_string("3.5.0 (compatible; Pleroma 1.2.3)")) == (3, 5, 0) assert parse_version_string(api._Mastodon__normalize_version_string("3.2.1rc3 (compatible; Akkoma 3.2.4+shinychariot)")) == (3, 2, 1) assert parse_version_string(api._Mastodon__normalize_version_string("3.5.3+0.17.3+git-6f4cb2f")) == (3, 5, 3) + +@pytest.mark.vcr() +def test_translation_languages(api): + assert api.instance_translation_languages() is not None \ No newline at end of file diff --git a/tests/test_status.py b/tests/test_status.py index a96580c..2fde314 100644 --- a/tests/test_status.py +++ b/tests/test_status.py @@ -289,3 +289,10 @@ def test_status_update_with_media_edit(api2): assert updated_status['media_attachments'][0]['preview_url'] != status['media_attachments'][0]['preview_url'] finally: api2.status_delete(status['id']) + +@pytest.mark.vcr() +def test_status_translate(api, status): + # our test server does not support translation, so this will raise a MastodonAPIError + with pytest.raises(MastodonAPIError): + translation = api.status_translate(status['id'], 'de') + \ No newline at end of file