diff --git a/docs/index.rst b/docs/index.rst index c0aebb7..b9fcde1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -756,6 +756,21 @@ Preference dicts # content warnings by default } +Featured tag dicts +~~~~~~~~~~~~~~~~~~ +.. _featured tag dict: + +.. code-block:: python + + mastodon.featured_tags()[0] + # Returns the following dictionary: + { + 'id': # The featured tags id + 'name': # The featured tags name (without leading #) + 'statuses_count': # Number of publicly visible statuses posted with this hashtag that this instance knows about + 'last_status_at': # The last time a public status containing this hashtag was added to this instances database + # (can be None if there are none) + } Admin account dicts ~~~~~~~~~~~~~~~~~~~ @@ -907,6 +922,13 @@ their relationships. .. automethod:: Mastodon.account_relationships .. automethod:: Mastodon.account_search +Reading data: Featured tags +~~~~~~~~~~~~~~~~~~~~~~~~~~~ +These functions allow retrieving info about a users featured and suggested tags. + +.. automethod:: Mastodon.featured_tags +.. automethod:: Mastodon.featured_tag_suggestions + Reading data: Keyword filters ----------------------------- These functions allow you to get information about keyword filters. @@ -1058,6 +1080,13 @@ These functions allow you to interact with other accounts: To (un)follow and .. automethod:: Mastodon.account_unpin .. automethod:: Mastodon.account_update_credentials +Writing data: Featured tags +~~~~~~~~~~~~~~~~~~~~~~~~~~~ +These functions allow setting which tags are featured on a users profile. + +.. automethod:: Mastodon.featured_tag_create +.. automethod:: Mastodon.featured_tag_delete + Writing data: Keyword filters ----------------------------- These functions allow you to manipulate keyword filters. diff --git a/mastodon/Mastodon.py b/mastodon/Mastodon.py index 899aae4..9c6f1ce 100644 --- a/mastodon/Mastodon.py +++ b/mastodon/Mastodon.py @@ -212,6 +212,7 @@ class Mastodon: __DICT_VERSION_SCHEDULED_STATUS = bigger_version("2.7.0", __DICT_VERSION_STATUS) __DICT_VERSION_PREFERENCES = "2.8.0" __DICT_VERSION_ADMIN_ACCOUNT = "2.9.1" + __DICT_VERSION_FEATURED_TAG = "3.0.0" ### # Registering apps @@ -1144,6 +1145,28 @@ class Mastodon: url = '/api/v1/accounts/{0}/lists'.format(str(id)) return self.__api_request('GET', url, params) + ### + # Reading data: Featured hashtags + ### + @api_version("3.0.0", "3.0.0", __DICT_VERSION_FEATURED_TAG) + def featured_tags(self): + """ + Return the hashtags the logged-in user has set to be featured on + their profile as a list of `featured tag dicts`_. + + Returns a list of `featured tag dicts`_. + """ + return self.__api_request('GET', '/api/v1/featured_tags') + + @api_version("3.0.0", "3.0.0", __DICT_VERSION_HASHTAG) + def featured_tag_suggestions(self): + """ + Returns the logged-in users 10 most commonly hashtags. + + Returns a list of `hashtag dicts`_. + """ + return self.__api_request('GET', '/api/v1/featured_tags/suggestions') + ### # Reading data: Keyword filters ### @@ -2137,6 +2160,28 @@ class Mastodon: url = '/api/v1/accounts/{0}/unpin'.format(str(id)) return self.__api_request('POST', url) + ### + # Writing data: Featured hashtags + ### + @api_version("3.0.0", "3.0.0", __DICT_VERSION_FEATURED_TAG) + def featured_tag_create(self, name): + """ + Creates a new featured hashtag displayed on the logged-in users profile. + + Returns a `featured tag dict`_ with the newly featured tag. + """ + params = self.__generate_params(locals()) + return self.__api_request('POST', '/api/v1/featured_tags', params) + + @api_version("3.0.0", "3.0.0", __DICT_VERSION_FEATURED_TAG) + def featured_tag_delete(self, id): + """ + Deletes one of the logged-in users featured hashtags. + """ + id = self.__unpack_id(id) + url = '/api/v1/featured_tags/{0}'.format(str(id)) + self.__api_request('DELETE', url) + ### # Writing data: Keyword filters ### @@ -2982,7 +3027,7 @@ class Mastodon: """ Parse dates in certain known json fields, if possible. """ - known_date_fields = ["created_at", "week", "day", "expires_at", "scheduled_at", "updated_at"] + known_date_fields = ["created_at", "week", "day", "expires_at", "scheduled_at", "updated_at", "last_status_at"] for k, v in json_object.items(): if k in known_date_fields: if v != None: diff --git a/tests/cassettes/test_featured_tags.yaml b/tests/cassettes/test_featured_tags.yaml new file mode 100644 index 0000000..8b1aa78 --- /dev/null +++ b/tests/cassettes/test_featured_tags.yaml @@ -0,0 +1,113 @@ +interactions: +- request: + body: name=mastopytesttag + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Authorization: [Bearer __MASTODON_PY_TEST_ACCESS_TOKEN] + Connection: [keep-alive] + Content-Length: ['19'] + Content-Type: [application/x-www-form-urlencoded] + User-Agent: [python-requests/2.18.4] + method: POST + uri: http://localhost:3000/api/v1/featured_tags + response: + body: {string: '{"id":"1","name":"mastopytesttag","statuses_count":0,"last_status_at":null}'} + headers: + Cache-Control: ['no-cache, no-store'] + Content-Type: [application/json; charset=utf-8] + Referrer-Policy: [strict-origin-when-cross-origin] + Transfer-Encoding: [chunked] + Vary: ['Accept-Encoding, Origin'] + X-Content-Type-Options: [nosniff] + X-Download-Options: [noopen] + X-Frame-Options: [SAMEORIGIN] + X-Permitted-Cross-Domain-Policies: [none] + X-Request-Id: [319b60a5-1408-4515-9b41-91a9eb90b81c] + X-Runtime: ['0.122775'] + X-XSS-Protection: [1; mode=block] + content-length: ['75'] + status: {code: 200, message: OK} +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Authorization: [Bearer __MASTODON_PY_TEST_ACCESS_TOKEN] + Connection: [keep-alive] + User-Agent: [python-requests/2.18.4] + method: GET + uri: http://localhost:3000/api/v1/featured_tags + response: + body: {string: '[{"id":"1","name":"mastopytesttag","statuses_count":0,"last_status_at":null}]'} + headers: + Cache-Control: ['no-cache, no-store'] + Content-Type: [application/json; charset=utf-8] + Referrer-Policy: [strict-origin-when-cross-origin] + Transfer-Encoding: [chunked] + Vary: ['Accept-Encoding, Origin'] + X-Content-Type-Options: [nosniff] + X-Download-Options: [noopen] + X-Frame-Options: [SAMEORIGIN] + X-Permitted-Cross-Domain-Policies: [none] + X-Request-Id: [6c35b860-5264-46a1-9219-388f2d65f038] + X-Runtime: ['0.030432'] + X-XSS-Protection: [1; mode=block] + content-length: ['77'] + status: {code: 200, message: OK} +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Authorization: [Bearer __MASTODON_PY_TEST_ACCESS_TOKEN] + Connection: [keep-alive] + Content-Length: ['0'] + User-Agent: [python-requests/2.18.4] + method: DELETE + uri: http://localhost:3000/api/v1/featured_tags/1 + response: + body: {string: '{}'} + headers: + Cache-Control: ['no-cache, no-store'] + Content-Type: [application/json; charset=utf-8] + Referrer-Policy: [strict-origin-when-cross-origin] + Transfer-Encoding: [chunked] + Vary: ['Accept-Encoding, Origin'] + X-Content-Type-Options: [nosniff] + X-Download-Options: [noopen] + X-Frame-Options: [SAMEORIGIN] + X-Permitted-Cross-Domain-Policies: [none] + X-Request-Id: [7c062d77-90bd-4400-9476-42495d98d77b] + X-Runtime: ['0.031225'] + X-XSS-Protection: [1; mode=block] + content-length: ['2'] + status: {code: 200, message: OK} +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Authorization: [Bearer __MASTODON_PY_TEST_ACCESS_TOKEN] + Connection: [keep-alive] + User-Agent: [python-requests/2.18.4] + method: GET + uri: http://localhost:3000/api/v1/featured_tags + response: + body: {string: '[]'} + headers: + Cache-Control: ['no-cache, no-store'] + Content-Type: [application/json; charset=utf-8] + Referrer-Policy: [strict-origin-when-cross-origin] + Transfer-Encoding: [chunked] + Vary: ['Accept-Encoding, Origin'] + X-Content-Type-Options: [nosniff] + X-Download-Options: [noopen] + X-Frame-Options: [SAMEORIGIN] + X-Permitted-Cross-Domain-Policies: [none] + X-Request-Id: [8e5eaf25-5e6a-4998-bb44-188b123b726c] + X-Runtime: ['0.020343'] + X-XSS-Protection: [1; mode=block] + content-length: ['2'] + status: {code: 200, message: OK} +version: 1 diff --git a/tests/cassettes/test_suggested_tags.yaml b/tests/cassettes/test_suggested_tags.yaml new file mode 100644 index 0000000..2a73e03 --- /dev/null +++ b/tests/cassettes/test_suggested_tags.yaml @@ -0,0 +1,90 @@ +interactions: +- request: + body: status=cool+free+%23ringtones + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Authorization: [Bearer __MASTODON_PY_TEST_ACCESS_TOKEN] + Connection: [keep-alive] + Content-Length: ['29'] + Content-Type: [application/x-www-form-urlencoded] + User-Agent: [python-requests/2.18.4] + method: POST + uri: http://localhost:3000/api/v1/statuses + response: + body: {string: '{"id":"102951123020457171","created_at":"2019-10-12T19:44:29.956Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":false,"spoiler_text":"","visibility":"public","language":"ja","uri":"http://localhost/users/mastodonpy_test/statuses/102951123020457171","url":"http://localhost/@mastodonpy_test/102951123020457171","replies_count":0,"reblogs_count":0,"favourites_count":0,"favourited":false,"reblogged":false,"muted":false,"pinned":false,"content":"\u003cp\u003ecool + free \u003ca href=\"http://localhost/tags/ringtones\" class=\"mention hashtag\" + rel=\"tag\"\u003e#\u003cspan\u003eringtones\u003c/span\u003e\u003c/a\u003e\u003c/p\u003e","reblog":null,"application":{"name":"Mastodon.py + test suite","website":null},"account":{"id":"1234567890123456","username":"mastodonpy_test","acct":"mastodonpy_test","display_name":"","locked":false,"bot":false,"created_at":"2019-06-22T23:11:52.441Z","note":"\u003cp\u003e\u003c/p\u003e","url":"http://localhost/@mastodonpy_test","avatar":"http://localhost/avatars/original/missing.png","avatar_static":"http://localhost/avatars/original/missing.png","header":"http://localhost/headers/original/missing.png","header_static":"http://localhost/headers/original/missing.png","followers_count":0,"following_count":1,"statuses_count":3,"last_status_at":"2019-10-12T19:44:29.976Z","emojis":[],"fields":[]},"media_attachments":[],"mentions":[],"tags":[{"name":"ringtones","url":"http://localhost/tags/ringtones"}],"emojis":[],"card":null,"poll":null}'} + headers: + Cache-Control: ['no-cache, no-store'] + Content-Type: [application/json; charset=utf-8] + Referrer-Policy: [strict-origin-when-cross-origin] + Transfer-Encoding: [chunked] + Vary: ['Accept-Encoding, Origin'] + X-Content-Type-Options: [nosniff] + X-Download-Options: [noopen] + X-Frame-Options: [SAMEORIGIN] + X-Permitted-Cross-Domain-Policies: [none] + X-Request-Id: [7c348bde-1d33-451a-8ae0-87cb09d8a1a2] + X-Runtime: ['0.222922'] + X-XSS-Protection: [1; mode=block] + content-length: ['1494'] + status: {code: 200, message: OK} +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Authorization: [Bearer __MASTODON_PY_TEST_ACCESS_TOKEN] + Connection: [keep-alive] + User-Agent: [python-requests/2.18.4] + method: GET + uri: http://localhost:3000/api/v1/featured_tags/suggestions + response: + body: {string: '[{"name":"ringtones","url":"http://localhost/tags/ringtones","history":[{"day":"1570838400","uses":"1","accounts":"1"},{"day":"1570752000","uses":"0","accounts":"0"},{"day":"1570665600","uses":"0","accounts":"0"},{"day":"1570579200","uses":"0","accounts":"0"},{"day":"1570492800","uses":"0","accounts":"0"},{"day":"1570406400","uses":"0","accounts":"0"},{"day":"1570320000","uses":"0","accounts":"0"}]}]'} + headers: + Cache-Control: ['no-cache, no-store'] + Content-Type: [application/json; charset=utf-8] + Referrer-Policy: [strict-origin-when-cross-origin] + Transfer-Encoding: [chunked] + Vary: ['Accept-Encoding, Origin'] + X-Content-Type-Options: [nosniff] + X-Download-Options: [noopen] + X-Frame-Options: [SAMEORIGIN] + X-Permitted-Cross-Domain-Policies: [none] + X-Request-Id: [fc6ec1a8-9c2e-4c08-925d-f80522d5bf65] + X-Runtime: ['0.061457'] + X-XSS-Protection: [1; mode=block] + content-length: ['403'] + status: {code: 200, message: OK} +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Authorization: [Bearer __MASTODON_PY_TEST_ACCESS_TOKEN] + Connection: [keep-alive] + Content-Length: ['0'] + User-Agent: [python-requests/2.18.4] + method: DELETE + uri: http://localhost:3000/api/v1/statuses/102951123020457171 + response: + body: {string: '{"id":"102951123020457171","created_at":"2019-10-12T19:44:29.956Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":false,"spoiler_text":"","visibility":"public","language":"ja","uri":"http://localhost/users/mastodonpy_test/statuses/102951123020457171","url":"http://localhost/@mastodonpy_test/102951123020457171","replies_count":0,"reblogs_count":0,"favourites_count":0,"favourited":false,"reblogged":false,"muted":false,"pinned":false,"text":"cool + free #ringtones","reblog":null,"application":{"name":"Mastodon.py test suite","website":null},"account":{"id":"1234567890123456","username":"mastodonpy_test","acct":"mastodonpy_test","display_name":"","locked":false,"bot":false,"created_at":"2019-06-22T23:11:52.441Z","note":"\u003cp\u003e\u003c/p\u003e","url":"http://localhost/@mastodonpy_test","avatar":"http://localhost/avatars/original/missing.png","avatar_static":"http://localhost/avatars/original/missing.png","header":"http://localhost/headers/original/missing.png","header_static":"http://localhost/headers/original/missing.png","followers_count":0,"following_count":1,"statuses_count":3,"last_status_at":"2019-10-12T19:44:29.976Z","emojis":[],"fields":[]},"media_attachments":[],"mentions":[],"tags":[{"name":"ringtones","url":"http://localhost/tags/ringtones"}],"emojis":[],"card":null,"poll":null}'} + headers: + Cache-Control: ['no-cache, no-store'] + Content-Type: [application/json; charset=utf-8] + Referrer-Policy: [strict-origin-when-cross-origin] + Transfer-Encoding: [chunked] + Vary: ['Accept-Encoding, Origin'] + X-Content-Type-Options: [nosniff] + X-Download-Options: [noopen] + X-Frame-Options: [SAMEORIGIN] + X-Permitted-Cross-Domain-Policies: [none] + X-Request-Id: [68301b80-982f-4397-8c6b-d9e7d8cd308b] + X-Runtime: ['0.118422'] + X-XSS-Protection: [1; mode=block] + content-length: ['1325'] + status: {code: 200, message: OK} +version: 1 diff --git a/tests/test_account.py b/tests/test_account.py index 55f766f..2e823d1 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -1,6 +1,7 @@ import pytest from mastodon.Mastodon import MastodonAPIError, MastodonIllegalArgumentError import re +import time @pytest.mark.vcr() def test_account(api): @@ -215,3 +216,36 @@ def test_account_pin_unpin(api, api2): def test_preferences(api): prefs = api.preferences() assert prefs + +@pytest.mark.vcr() +def test_suggested_tags(api): + try: + status = api.status_post("cool free #ringtones") + time.sleep(2) + + suggests = api.featured_tag_suggestions() + assert suggests + assert len(suggests) > 0 + finally: + api.status_delete(status) + +@pytest.mark.vcr() +def test_featured_tags(api): + featured_tag = api.featured_tag_create("mastopytesttag") + assert featured_tag + + tag_list = api.featured_tags() + assert featured_tag.name in list(map(lambda x: x.name, tag_list)) + + api.featured_tag_delete(featured_tag) + tag_list = api.featured_tags() + assert not featured_tag.name in list(map(lambda x: x.name, tag_list)) + + + + + + + + +