diff options
author | Lorenz Diener <[email protected]> | 2019-04-28 17:56:20 +0200 |
---|---|---|
committer | Lorenz Diener <[email protected]> | 2019-04-28 17:56:20 +0200 |
commit | b6692f0b16820da8388a5445bcfbf464a4648c91 (patch) | |
tree | 1facbe6e5755f8c3894048cda3bbb0ce9466fa20 | |
parent | a29d278bf9cacf5f888561564f112312707e32fd (diff) | |
download | mastodon.py-b6692f0b16820da8388a5445bcfbf464a4648c91.tar.gz |
Add account creation
-rw-r--r-- | mastodon/Mastodon.py | 72 | ||||
-rw-r--r-- | tests/cassettes/test_app_account_create.yaml | 116 | ||||
-rw-r--r-- | tests/test_create_app.py | 21 |
3 files changed, 206 insertions, 3 deletions
diff --git a/mastodon/Mastodon.py b/mastodon/Mastodon.py index b19c3de..444c12d 100644 --- a/mastodon/Mastodon.py +++ b/mastodon/Mastodon.py | |||
@@ -476,7 +476,71 @@ class Mastodon: | |||
476 | self.__logged_in_id = None | 476 | self.__logged_in_id = None |
477 | 477 | ||
478 | return response['access_token'] | 478 | return response['access_token'] |
479 | 479 | ||
480 | @api_version("2.7.0", "2.7.0", "2.7.0") | ||
481 | def create_account(self, username, password, email, agreement=False, locale="en", scopes=__DEFAULT_SCOPES, to_file=None): | ||
482 | """ | ||
483 | Creates a new user account with the given username, password and email. "agreement" | ||
484 | must be set to true (after showing the user the instances user agreement and having | ||
485 | them agree to it), "locale" specifies the language for the confirmation e-mail as an | ||
486 | ISO 639-1 (two-letter) language code. | ||
487 | |||
488 | Does not require an access token, but does require a client grant. | ||
489 | |||
490 | By default, this method is rate-limited by IP to 5 requests per 30 minutes. | ||
491 | |||
492 | Returns an access token (just like log_in), which it can also persist to to_file, | ||
493 | and sets it internally so that the user is now logged in. Note that this token | ||
494 | can only be used after the user has confirmed their e-mail. | ||
495 | """ | ||
496 | params = self.__generate_params(locals(), ['to_file', 'scopes']) | ||
497 | params['client_id'] = self.client_id | ||
498 | params['client_secret'] = self.client_secret | ||
499 | |||
500 | if agreement == False: | ||
501 | del params_initial['agreement'] | ||
502 | |||
503 | # Step 1: Get a user-free token via oauth | ||
504 | try: | ||
505 | oauth_params = {} | ||
506 | oauth_params['scope'] = " ".join(scopes) | ||
507 | oauth_params['client_id'] = self.client_id | ||
508 | oauth_params['client_secret'] = self.client_secret | ||
509 | oauth_params['grant_type'] = 'client_credentials' | ||
510 | |||
511 | response = self.__api_request('POST', '/oauth/token', oauth_params, do_ratelimiting=False) | ||
512 | temp_access_token = response['access_token'] | ||
513 | except Exception as e: | ||
514 | raise MastodonIllegalArgumentError('Invalid request during oauth phase: %s' % e) | ||
515 | |||
516 | # Step 2: Use that to create a user | ||
517 | try: | ||
518 | response = self.__api_request('POST', '/api/v1/accounts', params, do_ratelimiting=False, | ||
519 | access_token_override = temp_access_token) | ||
520 | self.access_token = response['access_token'] | ||
521 | self.__set_refresh_token(response.get('refresh_token')) | ||
522 | self.__set_token_expired(int(response.get('expires_in', 0))) | ||
523 | except Exception as e: | ||
524 | raise MastodonIllegalArgumentError('Invalid request: %s' % e) | ||
525 | |||
526 | # Step 3: Check scopes, persist, et cetera | ||
527 | received_scopes = response["scope"].split(" ") | ||
528 | for scope_set in self.__SCOPE_SETS.keys(): | ||
529 | if scope_set in received_scopes: | ||
530 | received_scopes += self.__SCOPE_SETS[scope_set] | ||
531 | |||
532 | if not set(scopes) <= set(received_scopes): | ||
533 | raise MastodonAPIError( | ||
534 | 'Granted scopes "' + " ".join(received_scopes) + '" do not contain all of the requested scopes "' + " ".join(scopes) + '".') | ||
535 | |||
536 | if to_file is not None: | ||
537 | with open(to_file, 'w') as token_file: | ||
538 | token_file.write(response['access_token'] + '\n') | ||
539 | |||
540 | self.__logged_in_id = None | ||
541 | |||
542 | return response['access_token'] | ||
543 | |||
480 | ### | 544 | ### |
481 | # Reading data: Instances | 545 | # Reading data: Instances |
482 | ### | 546 | ### |
@@ -2287,7 +2351,7 @@ class Mastodon: | |||
2287 | json_object = Mastodon.__json_allow_dict_attrs(json_object) | 2351 | json_object = Mastodon.__json_allow_dict_attrs(json_object) |
2288 | return json_object | 2352 | return json_object |
2289 | 2353 | ||
2290 | def __api_request(self, method, endpoint, params={}, files={}, headers={}, do_ratelimiting=True): | 2354 | def __api_request(self, method, endpoint, params={}, files={}, headers={}, access_token_override=None, do_ratelimiting=True): |
2291 | """ | 2355 | """ |
2292 | Internal API request helper. | 2356 | Internal API request helper. |
2293 | """ | 2357 | """ |
@@ -2314,8 +2378,10 @@ class Mastodon: | |||
2314 | 2378 | ||
2315 | # Generate request headers | 2379 | # Generate request headers |
2316 | headers = copy.deepcopy(headers) | 2380 | headers = copy.deepcopy(headers) |
2317 | if self.access_token is not None: | 2381 | if not self.access_token is None: |
2318 | headers['Authorization'] = 'Bearer ' + self.access_token | 2382 | headers['Authorization'] = 'Bearer ' + self.access_token |
2383 | if not access_token_override is None: | ||
2384 | headers['Authorization'] = 'Bearer ' + access_token_override | ||
2319 | 2385 | ||
2320 | if self.debug_requests: | 2386 | if self.debug_requests: |
2321 | print('Mastodon: Request to endpoint "' + endpoint + '" using method "' + method + '".') | 2387 | print('Mastodon: Request to endpoint "' + endpoint + '" using method "' + method + '".') |
diff --git a/tests/cassettes/test_app_account_create.yaml b/tests/cassettes/test_app_account_create.yaml new file mode 100644 index 0000000..83f7212 --- /dev/null +++ b/tests/cassettes/test_app_account_create.yaml | |||
@@ -0,0 +1,116 @@ | |||
1 | interactions: | ||
2 | - request: | ||
3 | body: client_name=mastodon.py+generated+test+app&scopes=read+write+follow+push&redirect_uris=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob | ||
4 | headers: | ||
5 | Accept: ['*/*'] | ||
6 | Accept-Encoding: ['gzip, deflate'] | ||
7 | Connection: [keep-alive] | ||
8 | Content-Length: ['122'] | ||
9 | Content-Type: [application/x-www-form-urlencoded] | ||
10 | User-Agent: [python-requests/2.18.4] | ||
11 | method: POST | ||
12 | uri: http://localhost:3000/api/v1/apps | ||
13 | response: | ||
14 | body: {string: '{"id":"17","name":"mastodon.py generated test app","website":null,"redirect_uri":"urn:ietf:wg:oauth:2.0:oob","client_id":"6b8b07698f3f57d73b0fd779fac2fcc64d6e852d3334425a6b50b53bc32db986","client_secret":"df903d79cc8a27d8d4f9aa8213cf65a9681fea679a56643fcb3e5a3f66c4f9c7","vapid_key":"BCryMB_mKFcSpmXE3kJ1Ri3ZFVdBLjRsX54VYhE21BMyftx8k67qWxFs2OCuQCtj0k1ILESkQhGuOKJcQnodx4g="}'} | ||
15 | headers: | ||
16 | Cache-Control: ['max-age=0, private, must-revalidate'] | ||
17 | Content-Type: [application/json; charset=utf-8] | ||
18 | ETag: [W/"3a06e1b620ce8b2c3ce4045d1e2c179b"] | ||
19 | Referrer-Policy: [strict-origin-when-cross-origin] | ||
20 | Transfer-Encoding: [chunked] | ||
21 | Vary: ['Accept-Encoding, Origin'] | ||
22 | X-Content-Type-Options: [nosniff] | ||
23 | X-Download-Options: [noopen] | ||
24 | X-Frame-Options: [SAMEORIGIN] | ||
25 | X-Permitted-Cross-Domain-Policies: [none] | ||
26 | X-Request-Id: [258ec6bb-4a82-41c3-b55b-560fdfb5ad13] | ||
27 | X-Runtime: ['0.025005'] | ||
28 | X-XSS-Protection: [1; mode=block] | ||
29 | content-length: ['374'] | ||
30 | status: {code: 200, message: OK} | ||
31 | - request: | ||
32 | body: null | ||
33 | headers: | ||
34 | Accept: ['*/*'] | ||
35 | Accept-Encoding: ['gzip, deflate'] | ||
36 | Connection: [keep-alive] | ||
37 | User-Agent: [python-requests/2.18.4] | ||
38 | method: GET | ||
39 | uri: http://localhost:3000/api/v1/instance/ | ||
40 | response: | ||
41 | body: {string: '{"uri":"localhost","title":"Mastodon","description":"","email":"","version":"2.8.0","urls":{"streaming_api":"ws://localhost:4000"},"stats":{"user_count":2,"status_count":15,"domain_count":0},"thumbnail":"http://localhost/packs/media/images/preview-9a17d32fc48369e8ccd910a75260e67d.jpg","languages":["en"],"registrations":true,"contact_account":null}'} | ||
42 | headers: | ||
43 | Cache-Control: ['max-age=300, public'] | ||
44 | Content-Type: [application/json; charset=utf-8] | ||
45 | Date: ['Sun, 28 Apr 2019 15:56:05 GMT'] | ||
46 | ETag: [W/"4ba18203af6a9d9402c05e8ffc21ac45"] | ||
47 | Referrer-Policy: [strict-origin-when-cross-origin] | ||
48 | Transfer-Encoding: [chunked] | ||
49 | Vary: ['Accept-Encoding, Origin'] | ||
50 | X-Content-Type-Options: [nosniff] | ||
51 | X-Download-Options: [noopen] | ||
52 | X-Frame-Options: [SAMEORIGIN] | ||
53 | X-Permitted-Cross-Domain-Policies: [none] | ||
54 | X-Request-Id: [bfb330d1-53c3-4fcc-b79e-30e256a2f845] | ||
55 | X-Runtime: ['0.025590'] | ||
56 | X-XSS-Protection: [1; mode=block] | ||
57 | content-length: ['349'] | ||
58 | status: {code: 200, message: OK} | ||
59 | - request: | ||
60 | body: scope=read+write+follow+push&client_id=6b8b07698f3f57d73b0fd779fac2fcc64d6e852d3334425a6b50b53bc32db986&client_secret=df903d79cc8a27d8d4f9aa8213cf65a9681fea679a56643fcb3e5a3f66c4f9c7&grant_type=client_credentials | ||
61 | headers: | ||
62 | Accept: ['*/*'] | ||
63 | Accept-Encoding: ['gzip, deflate'] | ||
64 | Connection: [keep-alive] | ||
65 | Content-Length: ['212'] | ||
66 | Content-Type: [application/x-www-form-urlencoded] | ||
67 | User-Agent: [python-requests/2.18.4] | ||
68 | method: POST | ||
69 | uri: http://localhost:3000/oauth/token | ||
70 | response: | ||
71 | body: {string: '{"access_token":"756276912d1d5d74cfbf2e275517a234cee584891ca1f87a3fb2bdbd1fed1ca1","token_type":"Bearer","scope":"read | ||
72 | write follow push","created_at":1556466966}'} | ||
73 | headers: | ||
74 | Cache-Control: ['private, no-store'] | ||
75 | Content-Type: [application/json; charset=utf-8] | ||
76 | ETag: [W/"851cc791f71c2ce9bf821e023e26b361"] | ||
77 | Pragma: [no-cache] | ||
78 | Transfer-Encoding: [chunked] | ||
79 | Vary: ['Accept-Encoding, Origin'] | ||
80 | X-Request-Id: [0205c26b-9c77-4c32-bb59-a2d69eafde33] | ||
81 | X-Runtime: ['0.039194'] | ||
82 | content-length: ['162'] | ||
83 | status: {code: 200, message: OK} | ||
84 | - request: | ||
85 | body: locale=en&agreement=1&email=email%40localhost11707&password=swordfish&username=coolguy11707&client_id=6b8b07698f3f57d73b0fd779fac2fcc64d6e852d3334425a6b50b53bc32db986&client_secret=df903d79cc8a27d8d4f9aa8213cf65a9681fea679a56643fcb3e5a3f66c4f9c7 | ||
86 | headers: | ||
87 | Accept: ['*/*'] | ||
88 | Accept-Encoding: ['gzip, deflate'] | ||
89 | Authorization: [Bearer 756276912d1d5d74cfbf2e275517a234cee584891ca1f87a3fb2bdbd1fed1ca1] | ||
90 | Connection: [keep-alive] | ||
91 | Content-Length: ['245'] | ||
92 | Content-Type: [application/x-www-form-urlencoded] | ||
93 | User-Agent: [python-requests/2.18.4] | ||
94 | method: POST | ||
95 | uri: http://localhost:3000/api/v1/accounts | ||
96 | response: | ||
97 | body: {string: '{"access_token":"b20f513163b154065d17f5aff37b779f51d13c152fe6e7d1be366d64d2e74e39","token_type":"Bearer","scope":"read | ||
98 | write follow push","created_at":1556466966}'} | ||
99 | headers: | ||
100 | Cache-Control: ['private, no-store'] | ||
101 | Content-Type: [application/json; charset=utf-8] | ||
102 | ETag: [W/"7ba50c22bb330ba5a0936c19bea78093"] | ||
103 | Pragma: [no-cache] | ||
104 | Referrer-Policy: [strict-origin-when-cross-origin] | ||
105 | Transfer-Encoding: [chunked] | ||
106 | Vary: ['Accept-Encoding, Origin'] | ||
107 | X-Content-Type-Options: [nosniff] | ||
108 | X-Download-Options: [noopen] | ||
109 | X-Frame-Options: [SAMEORIGIN] | ||
110 | X-Permitted-Cross-Domain-Policies: [none] | ||
111 | X-Request-Id: [51ac5a4c-e850-46be-89c6-681e46f891dd] | ||
112 | X-Runtime: ['0.208778'] | ||
113 | X-XSS-Protection: [1; mode=block] | ||
114 | content-length: ['162'] | ||
115 | status: {code: 200, message: OK} | ||
116 | version: 1 | ||
diff --git a/tests/test_create_app.py b/tests/test_create_app.py index 67318e9..f1153bc 100644 --- a/tests/test_create_app.py +++ b/tests/test_create_app.py | |||
@@ -1,6 +1,8 @@ | |||
1 | from mastodon import Mastodon | 1 | from mastodon import Mastodon |
2 | import pytest | 2 | import pytest |
3 | import requests | 3 | import requests |
4 | import time | ||
5 | |||
4 | try: | 6 | try: |
5 | from mock import Mock | 7 | from mock import Mock |
6 | except ImportError: | 8 | except ImportError: |
@@ -46,3 +48,22 @@ def test_app_verify_credentials(api): | |||
46 | app = api.app_verify_credentials() | 48 | app = api.app_verify_credentials() |
47 | assert app | 49 | assert app |
48 | assert app.name == 'Mastodon.py test suite' | 50 | assert app.name == 'Mastodon.py test suite' |
51 | |||
52 | @pytest.mark.vcr() | ||
53 | def test_app_account_create(): | ||
54 | # This leaves behind stuff on the test server, which is unfortunate, but eh. | ||
55 | suffix = str(time.time()).replace(".", "")[-5:] | ||
56 | |||
57 | test_app = test_app = Mastodon.create_app( | ||
58 | "mastodon.py generated test app", | ||
59 | api_base_url="http://localhost:3000/" | ||
60 | ) | ||
61 | |||
62 | test_app_api = Mastodon( | ||
63 | test_app[0], | ||
64 | test_app[1], | ||
65 | api_base_url="http://localhost:3000/" | ||
66 | ) | ||
67 | test_token = test_app_api.create_account("coolguy" + suffix, "swordfish", "email@localhost" + suffix, agreement=True) | ||
68 | assert test_token | ||
69 | |||