aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnsem <[email protected]>2017-04-07 21:59:39 +0000
committerAnsem <[email protected]>2017-04-07 21:59:39 +0000
commitebfe65a2957b3fe28ddf0bb769a488325a44cde3 (patch)
tree0c24e1a24760fe629da528ab674b23e9b8505171 /mastodon
parent853ae97dccc91988874ae229e9977c8f41a02e44 (diff)
downloadmastodon.py-ebfe65a2957b3fe28ddf0bb769a488325a44cde3.tar.gz
Add support for OAuth2
Diffstat (limited to 'mastodon')
-rw-r--r--mastodon/Mastodon.py146
1 files changed, 110 insertions, 36 deletions
diff --git a/mastodon/Mastodon.py b/mastodon/Mastodon.py
index 9967bdb..d2d759a 100644
--- a/mastodon/Mastodon.py
+++ b/mastodon/Mastodon.py
@@ -1,17 +1,20 @@
1# coding: utf-8 1# coding: utf-8
2 2
3import requests 3
4import os 4import os
5from urllib.parse import urlencode
5import os.path 6import os.path
6import mimetypes 7import mimetypes
7import time 8import time
8import random 9import random
9import string 10import string
10import pytz
11import datetime 11import datetime
12import dateutil
13import dateutil.parser 12import dateutil.parser
14 13
14import pytz
15import dateutil
16import requests
17
15class Mastodon: 18class Mastodon:
16 """ 19 """
17 Super basic but thorough and easy to use mastodon.social 20 Super basic but thorough and easy to use mastodon.social
@@ -19,9 +22,10 @@ class Mastodon:
19 22
20 If anything is unclear, check the official API docs at 23 If anything is unclear, check the official API docs at
21 https://github.com/Gargron/mastodon/wiki/API 24 https://github.com/Gargron/mastodon/wiki/API
22 25
23 Presently, only username-password login is supported, somebody please 26 Supported:
24 patch in Real Proper OAuth if desired. 27 Username-Password Login
28 OAuth2
25 """ 29 """
26 __DEFAULT_BASE_URL = 'https://mastodon.social' 30 __DEFAULT_BASE_URL = 'https://mastodon.social'
27 __DEFAULT_TIMEOUT = 300 31 __DEFAULT_TIMEOUT = 300
@@ -31,7 +35,7 @@ class Mastodon:
31 # Registering apps 35 # Registering apps
32 ### 36 ###
33 @staticmethod 37 @staticmethod
34 def create_app(client_name, scopes = ['read', 'write', 'follow'], redirect_uris = None, to_file = None, api_base_url = __DEFAULT_BASE_URL, request_timeout = __DEFAULT_TIMEOUT): 38 def create_app(client_name, scopes = ['read', 'write', 'follow'], redirect_uris = None, website = None, to_file = None, api_base_url = __DEFAULT_BASE_URL, request_timeout = __DEFAULT_TIMEOUT):
35 """ 39 """
36 Create a new app with given client_name and scopes (read, write, follow) 40 Create a new app with given client_name and scopes (read, write, follow)
37 41
@@ -50,12 +54,15 @@ class Mastodon:
50 } 54 }
51 55
52 try: 56 try:
53 if redirect_uris != None: 57 if redirect_uris is not None:
54 request_data['redirect_uris'] = redirect_uris; 58 request_data['redirect_uris'] = redirect_uris;
55 else: 59 else:
56 request_data['redirect_uris'] = 'urn:ietf:wg:oauth:2.0:oob'; 60 request_data['redirect_uris'] = 'urn:ietf:wg:oauth:2.0:oob';
57 61 if website is not None:
58 response = requests.post(api_base_url + '/api/v1/apps', data = request_data, timeout = request_timeout).json() 62 request_data['website'] = website
63
64 response = requests.post(api_base_url + '/api/v1/apps', data = request_data, timeout = request_timeout)
65 response = response.json()
59 except Exception as e: 66 except Exception as e:
60 import traceback 67 import traceback
61 traceback.print_exc() 68 traceback.print_exc()
@@ -99,6 +106,8 @@ class Mastodon:
99 self.access_token = access_token 106 self.access_token = access_token
100 self.debug_requests = debug_requests 107 self.debug_requests = debug_requests
101 self.ratelimit_method = ratelimit_method 108 self.ratelimit_method = ratelimit_method
109 self._token_expired = datetime.datetime.now()
110 self._refresh_token = None
102 111
103 self.ratelimit_limit = 150 112 self.ratelimit_limit = 150
104 self.ratelimit_reset = time.time() 113 self.ratelimit_reset = time.time()
@@ -122,32 +131,97 @@ class Mastodon:
122 if self.access_token != None and os.path.isfile(self.access_token): 131 if self.access_token != None and os.path.isfile(self.access_token):
123 with open(self.access_token, 'r') as token_file: 132 with open(self.access_token, 'r') as token_file:
124 self.access_token = token_file.readline().rstrip() 133 self.access_token = token_file.readline().rstrip()
125 134
126 def log_in(self, username, password, scopes = ['read', 'write', 'follow'], to_file = None): 135
127 """ 136 @property
128 Log in and sets access_token to what was returned. Note that your 137 def token_expired(self) -> bool:
129 username is the e-mail you use to log in into mastodon. 138 if self._token_expired < datetime.datetime.now():
130 139 return True
131 Can persist access token to file, to be used in the constructor. 140 else:
132 141 return False
133 Will throw a MastodonIllegalArgumentError if username / password 142
134 are wrong, scopes are not valid or granted scopes differ from requested. 143 @token_expired.setter
135 144 def token_expired(self, value: int):
136 Returns the access_token. 145 self._token_expired = datetime.datetime.now() + datetime.timedelta(seconds=value)
137 """ 146 return
138 params = self.__generate_params(locals()) 147
148 @property
149 def refresh_token(self) -> str:
150 return self._refresh_token
151
152 @refresh_token.setter
153 def refresh_token(self, value):
154 self._refresh_token = value
155 return
156
157 def auth_request_url(self, client_id: str = None, redirect_uris: str = "urn:ietf:wg:oauth:2.0:oob") -> str:
158 """Returns the url that a client needs to request the grant from the server.
159 https://mastodon.social/oauth/authorize?client_id=XXX&response_type=code&redirect_uris=YYY
160 """
161 if client_id is None:
162 client_id = self.client_id
163 else:
164 if os.path.isfile(client_id):
165 with open(client_id, 'r') as secret_file:
166 client_id = secret_file.readline().rstrip()
167
168 params = {}
169 params['client_id'] = client_id
170 params['response_type'] = "code"
171 params['redirect_uri'] = redirect_uris
172 formatted_params = urlencode(params)
173 return "".join([self.api_base_url, "/oauth/authorize?", formatted_params])
174
175 def log_in(self, username: str = None, password: str = None,\
176 code: str = None, redirect_uri: str = "urn:ietf:wg:oauth:2.0:oob", refresh_token: str = None,\
177 scopes: list = ['read', 'write', 'follow'], to_file: str = None) -> str:
178 """
179 Docs: https://github.com/doorkeeper-gem/doorkeeper/wiki/Interacting-as-an-OAuth-client-with-Doorkeeper
180
181 Notes:
182 Your username is the e-mail you use to log in into mastodon.
183
184 Can persist access token to file, to be used in the constructor.
185
186 Supports refresh_token but Mastodon.social doesn't implement it at the moment.
187
188 Handles password, authorization_code, and refresh_token authentication.
189
190 Will throw a MastodonIllegalArgumentError if username / password
191 are wrong, scopes are not valid or granted scopes differ from requested.
192
193 Returns:
194 {
195 'scope': 'read',
196 'created_at': 1491599341,
197 'access_token': 'd8daf46d...',
198 'token_type': 'bearer'
199 }
200 """
201 if username is not None and password is not None:
202 params = self.__generate_params(locals(), ['scopes', 'to_file', 'code', 'refresh_token'])
203 params['grant_type'] = 'password'
204 elif code is not None:
205 params = self.__generate_params(locals(), ['scopes', 'to_file', 'username', 'password', 'refresh_token'])
206 params['grant_type'] = 'authorization_code'
207 elif refresh_token is not None:
208 params = self.__generate_params(locals(), ['scopes', 'to_file', 'username', 'password', 'code'])
209 params['grant_type'] = 'refresh_token'
210 else:
211 raise MastodonIllegalArgumentError('Invalid user name, password, redirect_uris or scopes')
212
139 params['client_id'] = self.client_id 213 params['client_id'] = self.client_id
140 params['client_secret'] = self.client_secret 214 params['client_secret'] = self.client_secret
141 params['grant_type'] = 'password' 215
142 params['scope'] = " ".join(scopes)
143
144 try: 216 try:
145 response = self.__api_request('POST', '/oauth/token', params, do_ratelimiting = False) 217 response = self.__api_request('POST', '/oauth/token', params, do_ratelimiting = False)
146 self.access_token = response['access_token'] 218 self.access_token = response['access_token']
219 self.refresh_token = response.get('refresh_token')
220 self.token_expired = int(response.get('expires_in', 0))
147 except Exception as e: 221 except Exception as e:
148 import traceback 222 import traceback
149 traceback.print_exc() 223 traceback.print_exc()
150 raise MastodonIllegalArgumentError('Invalid user name, password or scopes: %s' % e) 224 raise MastodonIllegalArgumentError('Invalid user name, password, redirect_uris or scopes: %s' % e)
151 225
152 requested_scopes = " ".join(sorted(scopes)) 226 requested_scopes = " ".join(sorted(scopes))
153 received_scopes = " ".join(sorted(response["scope"].split(" "))) 227 received_scopes = " ".join(sorted(response["scope"].split(" ")))
@@ -157,7 +231,7 @@ class Mastodon:
157 231
158 if to_file != None: 232 if to_file != None:
159 with open(to_file, 'w') as token_file: 233 with open(to_file, 'w') as token_file:
160 token_file.write(response['access_token'] + '\n') 234 token_file.write(response + '\n')
161 235
162 return response['access_token'] 236 return response['access_token']
163 237
@@ -700,13 +774,13 @@ class Mastodon:
700 if self.ratelimit_method == "throw": 774 if self.ratelimit_method == "throw":
701 raise MastodonRatelimitError("Hit rate limit.") 775 raise MastodonRatelimitError("Hit rate limit.")
702 776
703 if self.ratelimit_method == "wait" or self.ratelimit_method == "pace": 777 if self.ratelimit_method == "wait" or self.ratelimit_method == "pace":
704 to_next = self.ratelimit_reset - time.time() 778 to_next = self.ratelimit_reset - time.time()
705 if to_next > 0: 779 if to_next > 0:
706 # As a precaution, never sleep longer than 5 minutes 780 # As a precaution, never sleep longer than 5 minutes
707 to_next = min(to_next, 5 * 60) 781 to_next = min(to_next, 5 * 60)
708 time.sleep(to_next) 782 time.sleep(to_next)
709 request_complete = False 783 request_complete = False
710 784
711 return response 785 return response
712 786
Powered by cgit v1.2.3 (git 2.41.0)