diff options
-rw-r--r-- | CHANGELOG.rst | 50 | ||||
-rw-r--r-- | mastodon/Mastodon.py | 31 | ||||
-rw-r--r-- | tests/cassettes/test_revoke.yaml | 253 | ||||
-rw-r--r-- | tests/test_auth.py | 27 |
4 files changed, 334 insertions, 27 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1ce779a..c027b91 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst | |||
@@ -5,38 +5,44 @@ version number. Breaking changes will be indicated by a change in the minor | |||
5 | v1.5.3 (in progress) | 5 | v1.5.3 (in progress) |
6 | -------------------- | 6 | -------------------- |
7 | * 3.1.3 support | 7 | * 3.1.3 support |
8 | * Add v2 media_post api | 8 | * Added v2 media_post api |
9 | * 3.1.4 support | 9 | * 3.1.4 support |
10 | * Add "remote", "local" and "only_media" parameter for timelines more broadly | 10 | * Added "remote", "local" and "only_media" parameter for timelines more broadly |
11 | * Document updates to instance information api return value | 11 | * Documented updates to instance information api return value |
12 | * 3.2.0 support | 12 | * 3.2.0 support |
13 | * Add account notes API | 13 | * Added account notes API |
14 | * Add thumbnail support to media_post / media_update | 14 | * Added thumbnail support to media_post / media_update |
15 | * Document new keys in media API | 15 | * Documented new keys in media API |
16 | * 3.3.0 support | 16 | * 3.3.0 support |
17 | * Add "notify" parameter for following. | 17 | * Added "notify" parameter for following. |
18 | * Add support for timed mutes | 18 | * Added support for timed mutes |
19 | * Add support for getting an accounts features tags via account_featured_tags | 19 | * Added support for getting an accounts features tags via account_featured_tags |
20 | * Miscelaneous additions | ||
21 | * Added support for paginating by date via converting dates to snowflake IDs (on Mastodon only - thanks to edent for the suggestion) | ||
22 | * Added a method to revoke oauth tokens (thanks fluffy-critter) | ||
20 | * Fixes | 23 | * Fixes |
21 | * Various small and big fixes, improving reliablity and test coverage | 24 | * Various small and big fixes, improving reliablity and test coverage |
22 | 25 | * Changed URLs from "tootsuite" to "mastodon" in several places (thanks andypiper) | |
26 | * Fixed some fields not converting to datetimes (thanks SouthFox-D) | ||
27 | * Improved oauth web flow support | ||
28 | |||
23 | v1.5.2 | 29 | v1.5.2 |
24 | ------ | 30 | ------ |
25 | * BREAKING CHANGE (but to a representation that was intended to be internal): Greatly improve how pagination info is stored (arittner) | 31 | * BREAKING CHANGE (but to a representation that was intended to be internal): Greatly improve how pagination info is stored (arittner) |
26 | * Add "unknown event" handler for streaming (arittner) | 32 | * Added "unknown event" handler for streaming (arittner) |
27 | * Add support for exclude_types in notifications api (MicroCheapFx) | 33 | * Added support for exclude_types in notifications api (MicroCheapFx) |
28 | * Add pagination to bookmarks (arittner) | 34 | * Added pagination to bookmarks (arittner) |
29 | * Make connecting for streaming more resilient (arittner) | 35 | * Made connecting for streaming more resilient (arittner) |
30 | * Allow specifying a user agent header (arittner) | 36 | * Allowed specifying a user agent header (arittner) |
31 | * Add support for tagged and exclude_reblogs on account_statuses api (arittner) | 37 | * Addeded support for tagged and exclude_reblogs on account_statuses api (arittner) |
32 | * Add support for reports without attached statuses (arittner) | 38 | * Added support for reports without attached statuses (arittner) |
33 | * General fixes | 39 | * General fixes |
34 | * Fix a typo in __json_fruefalse_parse (zen-tools) | 40 | * Fixed a typo in __json_fruefalse_parse (zen-tools) |
35 | * Some non-mastodon related fixes | 41 | * Some non-mastodon related fixes |
36 | * Fix a typo in error message for content_type (rinpatch | 42 | * Fixed a typo in error message for content_type (rinpatch |
37 | * Add support for specifying file name when uploading (animeavi) | 43 | * Added support for specifying file name when uploading (animeavi) |
38 | * Fix several crashes related to gotosocials version string (fwaggle) | 44 | * Fixed several crashes related to gotosocials version string (fwaggle) |
39 | * Fix an issue related to hometowns version string | 45 | * Fixed an issue related to hometowns version string |
40 | 46 | ||
41 | v1.5.1 | 47 | v1.5.1 |
42 | ------ | 48 | ------ |
diff --git a/mastodon/Mastodon.py b/mastodon/Mastodon.py index 95a33b8..98578fb 100644 --- a/mastodon/Mastodon.py +++ b/mastodon/Mastodon.py | |||
@@ -513,6 +513,8 @@ class Mastodon: | |||
513 | 513 | ||
514 | def auth_request_url(self, client_id=None, redirect_uris="urn:ietf:wg:oauth:2.0:oob", | 514 | def auth_request_url(self, client_id=None, redirect_uris="urn:ietf:wg:oauth:2.0:oob", |
515 | scopes=__DEFAULT_SCOPES, force_login=False): | 515 | scopes=__DEFAULT_SCOPES, force_login=False): |
516 | |||
517 | 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): | ||
516 | """ | 518 | """ |
517 | Returns the URL that a client needs to request an OAuth grant from the server. | 519 | Returns the URL that a client needs to request an OAuth grant from the server. |
518 | 520 | ||
@@ -526,6 +528,10 @@ class Mastodon: | |||
526 | 528 | ||
527 | Pass force_login if you want the user to always log in even when already logged | 529 | Pass force_login if you want the user to always log in even when already logged |
528 | into web Mastodon (i.e. when registering multiple different accounts in an app). | 530 | into web Mastodon (i.e. when registering multiple different accounts in an app). |
531 | |||
532 | State is the oauth `state`parameter to pass to the server. It is strongly suggested | ||
533 | to use a random, nonguessable value (i.e. nothing meaningful and no incrementing ID) | ||
534 | to preserve security guarantees. It can be left out for non-web login flows. | ||
529 | """ | 535 | """ |
530 | if client_id is None: | 536 | if client_id is None: |
531 | client_id = self.client_id | 537 | client_id = self.client_id |
@@ -540,12 +546,11 @@ class Mastodon: | |||
540 | params['redirect_uri'] = redirect_uris | 546 | params['redirect_uri'] = redirect_uris |
541 | params['scope'] = " ".join(scopes) | 547 | params['scope'] = " ".join(scopes) |
542 | params['force_login'] = force_login | 548 | params['force_login'] = force_login |
549 | params['state'] = state | ||
543 | formatted_params = urlencode(params) | 550 | formatted_params = urlencode(params) |
544 | return "".join([self.api_base_url, "/oauth/authorize?", formatted_params]) | 551 | return "".join([self.api_base_url, "/oauth/authorize?", formatted_params]) |
545 | 552 | ||
546 | def log_in(self, username=None, password=None, | 553 | def log_in(self, username=None, password=None, code=None, redirect_uri="urn:ietf:wg:oauth:2.0:oob", refresh_token=None, scopes=__DEFAULT_SCOPES, to_file=None): |
547 | code=None, redirect_uri="urn:ietf:wg:oauth:2.0:oob", refresh_token=None, | ||
548 | scopes=__DEFAULT_SCOPES, to_file=None): | ||
549 | """ | 554 | """ |
550 | Get the access token for a user. | 555 | Get the access token for a user. |
551 | 556 | ||
@@ -620,6 +625,26 @@ class Mastodon: | |||
620 | 625 | ||
621 | return response['access_token'] | 626 | return response['access_token'] |
622 | 627 | ||
628 | |||
629 | def revoke_access_token(self): | ||
630 | """ | ||
631 | Revoke the oauth token the user is currently authenticated with, effectively removing | ||
632 | the apps access and requiring the user to log in again. | ||
633 | """ | ||
634 | if self.access_token is None: | ||
635 | raise MastodonIllegalArgumentError("Not logged in, do not have a token to revoke.") | ||
636 | if self.client_id is None or self.client_secret is None: | ||
637 | raise MastodonIllegalArgumentError("Client authentication (id + secret) is required to revoke tokens.") | ||
638 | params = collections.OrderedDict([]) | ||
639 | params['client_id'] = self.client_id | ||
640 | params['client_secret'] = self.client_secret | ||
641 | params['token'] = self.access_token | ||
642 | self.__api_request('POST', '/oauth/revoke', params) | ||
643 | |||
644 | # We are now logged out, clear token and logged in id | ||
645 | self.access_token = None | ||
646 | self.__logged_in_id = None | ||
647 | |||
623 | @api_version("2.7.0", "2.7.0", "2.7.0") | 648 | @api_version("2.7.0", "2.7.0", "2.7.0") |
624 | def create_account(self, username, password, email, agreement=False, reason=None, locale="en", scopes=__DEFAULT_SCOPES, to_file=None): | 649 | def create_account(self, username, password, email, agreement=False, reason=None, locale="en", scopes=__DEFAULT_SCOPES, to_file=None): |
625 | """ | 650 | """ |
diff --git a/tests/cassettes/test_revoke.yaml b/tests/cassettes/test_revoke.yaml new file mode 100644 index 0000000..1bbc487 --- /dev/null +++ b/tests/cassettes/test_revoke.yaml | |||
@@ -0,0 +1,253 @@ | |||
1 | interactions: | ||
2 | - request: | ||
3 | body: username=mastodonpy_test_2%40localhost%3A3000&password=5fc638e0e53eafd9c4145b6bb852667d&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&grant_type=password&client_id=__MASTODON_PY_TEST_CLIENT_ID&client_secret=__MASTODON_PY_TEST_CLIENT_SECRET&scope=read+write+follow+push | ||
4 | headers: | ||
5 | Accept: | ||
6 | - '*/*' | ||
7 | Accept-Encoding: | ||
8 | - gzip, deflate | ||
9 | Connection: | ||
10 | - keep-alive | ||
11 | Content-Length: | ||
12 | - '271' | ||
13 | Content-Type: | ||
14 | - application/x-www-form-urlencoded | ||
15 | User-Agent: | ||
16 | - tests/v311 | ||
17 | method: POST | ||
18 | uri: http://localhost:3000/oauth/token | ||
19 | response: | ||
20 | body: | ||
21 | string: '{"access_token":"s3ZSxpaa2Uhe9EcHankvkfaQZQGiWpdEWIhX7GuhDlk","token_type":"Bearer","scope":"read | ||
22 | write follow push","created_at":1668370881}' | ||
23 | headers: | ||
24 | Cache-Control: | ||
25 | - no-store | ||
26 | Content-Security-Policy: | ||
27 | - 'base-uri ''none''; default-src ''none''; frame-ancestors ''none''; font-src | ||
28 | ''self'' http://localhost:3000; img-src ''self'' https: data: blob: http://localhost:3000; | ||
29 | style-src ''self'' http://localhost:3000 ''nonce-pCi2AQ9aKYXwS29cp2OHAg==''; | ||
30 | media-src ''self'' https: data: http://localhost:3000; frame-src ''self'' | ||
31 | https:; manifest-src ''self'' http://localhost:3000; connect-src ''self'' | ||
32 | data: blob: http://localhost:3000 http://localhost:3000 ws://localhost:4000 | ||
33 | ws://localhost:3035 http://localhost:3035; script-src ''self'' ''unsafe-inline'' | ||
34 | ''unsafe-eval'' http://localhost:3000; child-src ''self'' blob: http://localhost:3000; | ||
35 | worker-src ''self'' blob: http://localhost:3000' | ||
36 | Content-Type: | ||
37 | - application/json; charset=utf-8 | ||
38 | ETag: | ||
39 | - W/"b4c5259bc2edbe94aab5df0f3b8ba79a" | ||
40 | Pragma: | ||
41 | - no-cache | ||
42 | Referrer-Policy: | ||
43 | - strict-origin-when-cross-origin | ||
44 | Transfer-Encoding: | ||
45 | - chunked | ||
46 | Vary: | ||
47 | - Accept, Origin | ||
48 | X-Content-Type-Options: | ||
49 | - nosniff | ||
50 | X-Download-Options: | ||
51 | - noopen | ||
52 | X-Frame-Options: | ||
53 | - SAMEORIGIN | ||
54 | X-Permitted-Cross-Domain-Policies: | ||
55 | - none | ||
56 | X-Request-Id: | ||
57 | - 086350f4-00fc-4d82-a5ce-2b557df59682 | ||
58 | X-Runtime: | ||
59 | - '0.040539' | ||
60 | X-XSS-Protection: | ||
61 | - 1; mode=block | ||
62 | status: | ||
63 | code: 200 | ||
64 | message: OK | ||
65 | - request: | ||
66 | body: client_id=__MASTODON_PY_TEST_CLIENT_ID&client_secret=__MASTODON_PY_TEST_CLIENT_SECRET&token=s3ZSxpaa2Uhe9EcHankvkfaQZQGiWpdEWIhX7GuhDlk | ||
67 | headers: | ||
68 | Accept: | ||
69 | - '*/*' | ||
70 | Accept-Encoding: | ||
71 | - gzip, deflate | ||
72 | Authorization: | ||
73 | - Bearer s3ZSxpaa2Uhe9EcHankvkfaQZQGiWpdEWIhX7GuhDlk | ||
74 | Connection: | ||
75 | - keep-alive | ||
76 | Content-Length: | ||
77 | - '135' | ||
78 | Content-Type: | ||
79 | - application/x-www-form-urlencoded | ||
80 | User-Agent: | ||
81 | - tests/v311 | ||
82 | method: POST | ||
83 | uri: http://localhost:3000/oauth/revoke | ||
84 | response: | ||
85 | body: | ||
86 | string: '{}' | ||
87 | headers: | ||
88 | Cache-Control: | ||
89 | - max-age=0, private, must-revalidate | ||
90 | Content-Security-Policy: | ||
91 | - 'base-uri ''none''; default-src ''none''; frame-ancestors ''none''; font-src | ||
92 | ''self'' http://localhost:3000; img-src ''self'' https: data: blob: http://localhost:3000; | ||
93 | style-src ''self'' http://localhost:3000 ''nonce-PqM4ChK427oGZ5jIKprkYQ==''; | ||
94 | media-src ''self'' https: data: http://localhost:3000; frame-src ''self'' | ||
95 | https:; manifest-src ''self'' http://localhost:3000; connect-src ''self'' | ||
96 | data: blob: http://localhost:3000 http://localhost:3000 ws://localhost:4000 | ||
97 | ws://localhost:3035 http://localhost:3035; script-src ''self'' ''unsafe-inline'' | ||
98 | ''unsafe-eval'' http://localhost:3000; child-src ''self'' blob: http://localhost:3000; | ||
99 | worker-src ''self'' blob: http://localhost:3000' | ||
100 | Content-Type: | ||
101 | - application/json; charset=utf-8 | ||
102 | ETag: | ||
103 | - W/"44136fa355b3678a1146ad16f7e8649e" | ||
104 | Referrer-Policy: | ||
105 | - strict-origin-when-cross-origin | ||
106 | Transfer-Encoding: | ||
107 | - chunked | ||
108 | Vary: | ||
109 | - Accept | ||
110 | X-Content-Type-Options: | ||
111 | - nosniff | ||
112 | X-Download-Options: | ||
113 | - noopen | ||
114 | X-Frame-Options: | ||
115 | - SAMEORIGIN | ||
116 | X-Permitted-Cross-Domain-Policies: | ||
117 | - none | ||
118 | X-Request-Id: | ||
119 | - 36ec7e63-b15b-487a-aa4a-c396db945794 | ||
120 | X-Runtime: | ||
121 | - '0.010431' | ||
122 | X-XSS-Protection: | ||
123 | - 1; mode=block | ||
124 | status: | ||
125 | code: 200 | ||
126 | message: OK | ||
127 | - request: | ||
128 | body: status=illegal+access+detected | ||
129 | headers: | ||
130 | Accept: | ||
131 | - '*/*' | ||
132 | Accept-Encoding: | ||
133 | - gzip, deflate | ||
134 | Connection: | ||
135 | - keep-alive | ||
136 | Content-Length: | ||
137 | - '30' | ||
138 | Content-Type: | ||
139 | - application/x-www-form-urlencoded | ||
140 | User-Agent: | ||
141 | - tests/v311 | ||
142 | method: POST | ||
143 | uri: http://localhost:3000/api/v1/statuses | ||
144 | response: | ||
145 | body: | ||
146 | string: '{"error":"The access token is invalid"}' | ||
147 | headers: | ||
148 | Cache-Control: | ||
149 | - no-store | ||
150 | Content-Security-Policy: | ||
151 | - 'base-uri ''none''; default-src ''none''; frame-ancestors ''none''; font-src | ||
152 | ''self'' http://localhost:3000; img-src ''self'' https: data: blob: http://localhost:3000; | ||
153 | style-src ''self'' http://localhost:3000 ''nonce-UClOh6+Y0zf3a4O/ysqT/w==''; | ||
154 | media-src ''self'' https: data: http://localhost:3000; frame-src ''self'' | ||
155 | https:; manifest-src ''self'' http://localhost:3000; connect-src ''self'' | ||
156 | data: blob: http://localhost:3000 http://localhost:3000 ws://localhost:4000 | ||
157 | ws://localhost:3035 http://localhost:3035; script-src ''self'' ''unsafe-inline'' | ||
158 | ''unsafe-eval'' http://localhost:3000; child-src ''self'' blob: http://localhost:3000; | ||
159 | worker-src ''self'' blob: http://localhost:3000' | ||
160 | Content-Type: | ||
161 | - application/json; charset=utf-8 | ||
162 | Pragma: | ||
163 | - no-cache | ||
164 | Referrer-Policy: | ||
165 | - strict-origin-when-cross-origin | ||
166 | Transfer-Encoding: | ||
167 | - chunked | ||
168 | Vary: | ||
169 | - Accept, Origin | ||
170 | WWW-Authenticate: | ||
171 | - Bearer realm="Doorkeeper", error="invalid_token", error_description="The access | ||
172 | token is invalid" | ||
173 | X-Content-Type-Options: | ||
174 | - nosniff | ||
175 | X-Download-Options: | ||
176 | - noopen | ||
177 | X-Frame-Options: | ||
178 | - SAMEORIGIN | ||
179 | X-Permitted-Cross-Domain-Policies: | ||
180 | - none | ||
181 | X-Request-Id: | ||
182 | - ab1f9d04-149b-431a-b2b0-79921d45f3bc | ||
183 | X-Runtime: | ||
184 | - '0.005292' | ||
185 | X-XSS-Protection: | ||
186 | - 1; mode=block | ||
187 | status: | ||
188 | code: 401 | ||
189 | message: Unauthorized | ||
190 | - request: | ||
191 | body: status=illegal+access+detected | ||
192 | headers: | ||
193 | Accept: | ||
194 | - '*/*' | ||
195 | Accept-Encoding: | ||
196 | - gzip, deflate | ||
197 | Connection: | ||
198 | - keep-alive | ||
199 | Content-Length: | ||
200 | - '30' | ||
201 | Content-Type: | ||
202 | - application/x-www-form-urlencoded | ||
203 | User-Agent: | ||
204 | - tests/v311 | ||
205 | method: POST | ||
206 | uri: http://localhost:3000/api/v1/statuses | ||
207 | response: | ||
208 | body: | ||
209 | string: '{"error":"The access token is invalid"}' | ||
210 | headers: | ||
211 | Cache-Control: | ||
212 | - no-store | ||
213 | Content-Security-Policy: | ||
214 | - 'base-uri ''none''; default-src ''none''; frame-ancestors ''none''; font-src | ||
215 | ''self'' http://localhost:3000; img-src ''self'' https: data: blob: http://localhost:3000; | ||
216 | style-src ''self'' http://localhost:3000 ''nonce-ZTmQUUs9q7lX74Aa3bTgzA==''; | ||
217 | media-src ''self'' https: data: http://localhost:3000; frame-src ''self'' | ||
218 | https:; manifest-src ''self'' http://localhost:3000; connect-src ''self'' | ||
219 | data: blob: http://localhost:3000 http://localhost:3000 ws://localhost:4000 | ||
220 | ws://localhost:3035 http://localhost:3035; script-src ''self'' ''unsafe-inline'' | ||
221 | ''unsafe-eval'' http://localhost:3000; child-src ''self'' blob: http://localhost:3000; | ||
222 | worker-src ''self'' blob: http://localhost:3000' | ||
223 | Content-Type: | ||
224 | - application/json; charset=utf-8 | ||
225 | Pragma: | ||
226 | - no-cache | ||
227 | Referrer-Policy: | ||
228 | - strict-origin-when-cross-origin | ||
229 | Transfer-Encoding: | ||
230 | - chunked | ||
231 | Vary: | ||
232 | - Accept, Origin | ||
233 | WWW-Authenticate: | ||
234 | - Bearer realm="Doorkeeper", error="invalid_token", error_description="The access | ||
235 | token is invalid" | ||
236 | X-Content-Type-Options: | ||
237 | - nosniff | ||
238 | X-Download-Options: | ||
239 | - noopen | ||
240 | X-Frame-Options: | ||
241 | - SAMEORIGIN | ||
242 | X-Permitted-Cross-Domain-Policies: | ||
243 | - none | ||
244 | X-Request-Id: | ||
245 | - 9a7ad262-0d94-4df6-92a5-a670d6515979 | ||
246 | X-Runtime: | ||
247 | - '0.004613' | ||
248 | X-XSS-Protection: | ||
249 | - 1; mode=block | ||
250 | status: | ||
251 | code: 401 | ||
252 | message: Unauthorized | ||
253 | version: 1 | ||
diff --git a/tests/test_auth.py b/tests/test_auth.py index c3acb66..a14b4e7 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py | |||
@@ -22,15 +22,38 @@ def test_log_in_none(api_anonymous): | |||
22 | with pytest.raises(MastodonIllegalArgumentError): | 22 | with pytest.raises(MastodonIllegalArgumentError): |
23 | api_anonymous.log_in() | 23 | api_anonymous.log_in() |
24 | 24 | ||
25 | |||
26 | @pytest.mark.vcr() | 25 | @pytest.mark.vcr() |
27 | def test_log_in_password(api_anonymous): | 26 | def test_log_in_password(api_anonymous): |
28 | token = api_anonymous.log_in( | 27 | token = api_anonymous.log_in( |
29 | username='mastodonpy_test_2@localhost:3000', | 28 | username='mastodonpy_test_2@localhost:3000', |
30 | password='5fc638e0e53eafd9c4145b6bb852667d') | 29 | password='5fc638e0e53eafd9c4145b6bb852667d' |
30 | ) | ||
31 | assert token | 31 | assert token |
32 | 32 | ||
33 | @pytest.mark.vcr() | 33 | @pytest.mark.vcr() |
34 | def test_revoke(api_anonymous): | ||
35 | token = api_anonymous.log_in( | ||
36 | username='mastodonpy_test_2@localhost:3000', | ||
37 | password='5fc638e0e53eafd9c4145b6bb852667d' | ||
38 | ) | ||
39 | api_anonymous.revoke_access_token() | ||
40 | |||
41 | try: | ||
42 | api_anonymous.toot("illegal access detected") | ||
43 | assert False | ||
44 | except Exception as e: | ||
45 | print(e) | ||
46 | pass | ||
47 | |||
48 | api_revoked_token = Mastodon(access_token = token) | ||
49 | try: | ||
50 | api_anonymous.toot("illegal access detected") | ||
51 | assert False | ||
52 | except Exception as e: | ||
53 | print(e) | ||
54 | pass | ||
55 | |||
56 | @pytest.mark.vcr() | ||
34 | def test_log_in_password_incorrect(api_anonymous): | 57 | def test_log_in_password_incorrect(api_anonymous): |
35 | with pytest.raises(MastodonIllegalArgumentError): | 58 | with pytest.raises(MastodonIllegalArgumentError): |
36 | api_anonymous.log_in( | 59 | api_anonymous.log_in( |