diff --git a/docs/index.rst b/docs/index.rst index 7561b6f..a7d2945 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -800,6 +800,12 @@ Reading data: Emoji .. automethod:: Mastodon.custom_emojis +Reading data: Endorsements +-------------------------- + +.. automethod:: Mastodon.endorsements + + Writing data: Statuses ---------------------- These functions allow you to post statuses to Mastodon and to @@ -838,6 +844,8 @@ These functions allow you to interact with other accounts: To (un)follow and .. automethod:: Mastodon.account_unblock .. automethod:: Mastodon.account_mute .. automethod:: Mastodon.account_unmute +.. automethod:: Mastodon.account_pin +.. automethod:: Mastodon.account_unpin .. automethod:: Mastodon.account_update_credentials Writing data: Keyword filters diff --git a/mastodon/Mastodon.py b/mastodon/Mastodon.py index d272a33..ca7dbd6 100644 --- a/mastodon/Mastodon.py +++ b/mastodon/Mastodon.py @@ -923,6 +923,20 @@ class Mastodon: """ return self.__api_request('GET', '/api/v1/suggestions') + ### + # Reading data: Endorsements + ### + @api_version("2.5.0", "2.5.0", __DICT_VERSION_ACCOUNT) + def endorsements(self): + """ + Fetch list of users endorsemed by the logged-in user. + + Returns a list of `user dicts`_. + + """ + return self.__api_request('GET', '/api/v1/endorsements') + + ### # Reading data: Searching ### @@ -1463,8 +1477,9 @@ class Mastodon: Returns a `relationship dict`_ containing the updated relationship to the user. """ id = self.__unpack_id(id) + params = self.__generate_params(locals(), ['id']) url = '/api/v1/accounts/{0}/mute'.format(str(id)) - return self.__api_request('POST', url) + return self.__api_request('POST', url, params) @api_version("1.1.0", "1.4.0", __DICT_VERSION_RELATIONSHIP) def account_unmute(self, id): @@ -1545,6 +1560,28 @@ class Mastodon: return self.__api_request('PATCH', '/api/v1/accounts/update_credentials', params, files=files) + @api_version("2.5.0", "2.5.0", __DICT_VERSION_RELATIONSHIP) + def account_pin(self, id): + """ + Pin / endorse a user. + + Returns a `relationship dict`_ containing the updated relationship to the user. + """ + id = self.__unpack_id(id) + url = '/api/v1/accounts/{0}/pin'.format(str(id)) + return self.__api_request('POST', url) + + @api_version("2.5.0", "2.5.0", __DICT_VERSION_RELATIONSHIP) + def account_unpin(self, id): + """ + Unpin / un-endorse a user. + + Returns a `relationship dict`_ containing the updated relationship to the user. + """ + id = self.__unpack_id(id) + url = '/api/v1/accounts/{0}/unpin'.format(str(id)) + return self.__api_request('POST', url) + ### # Writing data: Keyword filters ### diff --git a/tests/cassettes/test_account_pin_unpin.yaml b/tests/cassettes/test_account_pin_unpin.yaml new file mode 100644 index 0000000..eaf3563 --- /dev/null +++ b/tests/cassettes/test_account_pin_unpin.yaml @@ -0,0 +1,233 @@ +interactions: +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Authorization: [Bearer __MASTODON_PY_TEST_ACCESS_TOKEN_2] + Connection: [keep-alive] + User-Agent: [python-requests/2.18.4] + method: GET + uri: http://localhost:3000/api/v1/accounts/verify_credentials + response: + body: {string: '{"id":"1","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2019-04-27T18:52:42.626Z","note":"\u003cp\u003e\u003c/p\u003e","url":"http://localhost/@admin","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":0,"statuses_count":0,"source":{"privacy":"public","sensitive":false,"language":null,"note":"","fields":[]},"emojis":[],"fields":[]}'} + headers: + Cache-Control: ['max-age=0, private, must-revalidate'] + Content-Type: [application/json; charset=utf-8] + ETag: [W/"0f8b38bbae62fe172a62496fe49e206e"] + 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: [692fe5c4-e557-4dd5-99f0-44b812f2e9f3] + X-Runtime: ['0.019931'] + X-XSS-Protection: [1; mode=block] + content-length: ['609'] + status: {code: 200, message: OK} +- request: + body: reblogs=1&id=1 + 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: [python-requests/2.18.4] + method: POST + uri: http://localhost:3000/api/v1/accounts/1/follow + response: + body: {string: '{"id":"1","following":true,"showing_reblogs":true,"followed_by":false,"blocking":false,"blocked_by":false,"muting":false,"muting_notifications":false,"requested":false,"domain_blocking":false,"endorsed":false}'} + headers: + Cache-Control: ['max-age=0, private, must-revalidate'] + Content-Type: [application/json; charset=utf-8] + ETag: [W/"1e1fa14c270ff6960152323eda93cc79"] + 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: [ad3bfb7e-d816-4b51-8c90-e431d2330775] + X-Runtime: ['0.062775'] + X-XSS-Protection: [1; mode=block] + content-length: ['209'] + 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: POST + uri: http://localhost:3000/api/v1/accounts/1/unpin + response: + body: {string: '{"id":"1","following":true,"showing_reblogs":true,"followed_by":false,"blocking":false,"blocked_by":false,"muting":false,"muting_notifications":false,"requested":false,"domain_blocking":false,"endorsed":false}'} + headers: + Cache-Control: ['max-age=0, private, must-revalidate'] + Content-Type: [application/json; charset=utf-8] + ETag: [W/"1e1fa14c270ff6960152323eda93cc79"] + 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: [312e2867-4349-4145-8bb0-231e59e3d02c] + X-Runtime: ['0.065496'] + X-XSS-Protection: [1; mode=block] + content-length: ['209'] + 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: POST + uri: http://localhost:3000/api/v1/accounts/1/pin + response: + body: {string: '{"id":"1","following":true,"showing_reblogs":true,"followed_by":false,"blocking":false,"blocked_by":false,"muting":false,"muting_notifications":false,"requested":false,"domain_blocking":false,"endorsed":true}'} + headers: + Cache-Control: ['max-age=0, private, must-revalidate'] + Content-Type: [application/json; charset=utf-8] + ETag: [W/"c6dfb53299bc83b46a9edb08d3930925"] + 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: [c9dae17d-d180-447b-bb49-4f6d4ce07f40] + X-Runtime: ['0.050245'] + X-XSS-Protection: [1; mode=block] + content-length: ['208'] + 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/endorsements + response: + body: {string: '[{"id":"1","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2019-04-27T18:52:42.626Z","note":"\u003cp\u003e\u003c/p\u003e","url":"http://localhost/@admin","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":1,"following_count":0,"statuses_count":0,"emojis":[],"fields":[]}]'} + headers: + Cache-Control: ['max-age=0, private, must-revalidate'] + Content-Type: [application/json; charset=utf-8] + ETag: [W/"ae491d6254719d99832526fc14bfb68b"] + Link: ['; rel="prev"'] + 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: [855af719-1d1a-4fe0-93e7-b2ff9293a363] + X-Runtime: ['0.018452'] + X-XSS-Protection: [1; mode=block] + content-length: ['525'] + 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: POST + uri: http://localhost:3000/api/v1/accounts/1/unpin + response: + body: {string: '{"id":"1","following":true,"showing_reblogs":true,"followed_by":false,"blocking":false,"blocked_by":false,"muting":false,"muting_notifications":false,"requested":false,"domain_blocking":false,"endorsed":false}'} + headers: + Cache-Control: ['max-age=0, private, must-revalidate'] + Content-Type: [application/json; charset=utf-8] + ETag: [W/"1e1fa14c270ff6960152323eda93cc79"] + 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: [e750e8c4-eae0-4ed6-9563-00ac35d85daa] + X-Runtime: ['0.025393'] + X-XSS-Protection: [1; mode=block] + content-length: ['209'] + 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/endorsements + response: + body: {string: '[]'} + headers: + Cache-Control: ['max-age=0, private, must-revalidate'] + Content-Type: [application/json; charset=utf-8] + ETag: [W/"e2dc74be154e07a1d543fffa1604d2ab"] + 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: [15996817-bf63-4f5e-a34d-c6cf7702822c] + X-Runtime: ['0.019443'] + 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] + Content-Length: ['0'] + User-Agent: [python-requests/2.18.4] + method: POST + uri: http://localhost:3000/api/v1/accounts/1/unfollow + response: + body: {string: '{"id":"1","following":false,"showing_reblogs":false,"followed_by":false,"blocking":false,"blocked_by":false,"muting":false,"muting_notifications":false,"requested":false,"domain_blocking":false,"endorsed":false}'} + headers: + Cache-Control: ['max-age=0, private, must-revalidate'] + Content-Type: [application/json; charset=utf-8] + ETag: [W/"714cd19713205feb75ce771b61d4564b"] + 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: [7710c7c9-40d9-4cd9-b483-2863d00916ff] + X-Runtime: ['0.041778'] + X-XSS-Protection: [1; mode=block] + content-length: ['211'] + status: {code: 200, message: OK} +version: 1 diff --git a/tests/cassettes/test_mutes.yaml b/tests/cassettes/test_mutes.yaml index 73b8df6..aca7b97 100644 --- a/tests/cassettes/test_mutes.yaml +++ b/tests/cassettes/test_mutes.yaml @@ -14,7 +14,7 @@ interactions: headers: Cache-Control: ['max-age=0, private, must-revalidate'] Content-Type: [application/json; charset=utf-8] - ETag: [W/"90b6becb2b7152e68f928ca54703d5b5"] + ETag: [W/"0c6c8cc3cb75164dfb4ec124c252008f"] Referrer-Policy: [strict-origin-when-cross-origin] Transfer-Encoding: [chunked] Vary: ['Accept-Encoding, Origin'] @@ -22,8 +22,8 @@ interactions: X-Download-Options: [noopen] X-Frame-Options: [SAMEORIGIN] X-Permitted-Cross-Domain-Policies: [none] - X-Request-Id: [1ecc4a5b-3fa6-4aab-9f51-62a63df2668c] - X-Runtime: ['0.021739'] + X-Request-Id: [1bef2ecf-ea12-422d-802a-456ab7096e0b] + X-Runtime: ['0.060542'] X-XSS-Protection: [1; mode=block] content-length: ['2'] status: {code: 200, message: OK} diff --git a/tests/conftest.py b/tests/conftest.py index 3291bbe..66fffdc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,6 @@ import pytest -def _api(access_token='__MASTODON_PY_TEST_ACCESS_TOKEN', version="2.4.3", version_check_mode="created"): +def _api(access_token='__MASTODON_PY_TEST_ACCESS_TOKEN', version="2.8.0", version_check_mode="created"): import mastodon return mastodon.Mastodon( api_base_url='http://localhost:3000', diff --git a/tests/test_account.py b/tests/test_account.py index 77a8f48..cd7633d 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -175,3 +175,34 @@ def test_follow_suggestions(api2, status): suggestions2 = api2.suggestions() assert(len(suggestions2) < len(suggestions)) +@pytest.mark.vcr() +def test_account_pin_unpin(api, api2): + user = api2.account_verify_credentials() + + # Make sure we are in the correct state + try: + api.account_follow(user) + except: + pass + + try: + api.account_unpin(user) + except: + pass + + relationship = api.account_pin(user) + endorsed = api.endorsements() + + try: + assert relationship + assert relationship['endorsed'] + assert user["id"] in map(lambda x: x["id"], endorsed) + finally: + relationship = api.account_unpin(user) + endorsed2 = api.endorsements() + api.account_unfollow(user) + assert relationship + assert not relationship['endorsed'] + assert not user["id"] in map(lambda x: x["id"], endorsed2) + +