aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'mastodon')
-rw-r--r--mastodon/Mastodon.py150
1 files changed, 113 insertions, 37 deletions
diff --git a/mastodon/Mastodon.py b/mastodon/Mastodon.py
index 493fb40..ee11496 100644
--- a/mastodon/Mastodon.py
+++ b/mastodon/Mastodon.py
@@ -1,19 +1,18 @@
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 12import dateutil
13import dateutil.parser 13import dateutil.parser
14
15from contextlib import closing 14from contextlib import closing
16 15import requests
17 16
18class Mastodon: 17class Mastodon:
19 """ 18 """
@@ -22,9 +21,10 @@ class Mastodon:
22 21
23 If anything is unclear, check the official API docs at 22 If anything is unclear, check the official API docs at
24 https://github.com/Gargron/mastodon/wiki/API 23 https://github.com/Gargron/mastodon/wiki/API
25 24
26 Presently, only username-password login is supported, somebody please 25 Supported:
27 patch in Real Proper OAuth if desired. 26 Username-Password Login
27 OAuth2
28 """ 28 """
29 __DEFAULT_BASE_URL = 'https://mastodon.social' 29 __DEFAULT_BASE_URL = 'https://mastodon.social'
30 __DEFAULT_TIMEOUT = 300 30 __DEFAULT_TIMEOUT = 300
@@ -34,7 +34,7 @@ class Mastodon:
34 # Registering apps 34 # Registering apps
35 ### 35 ###
36 @staticmethod 36 @staticmethod
37 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): 37 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):
38 """ 38 """
39 Create a new app with given client_name and scopes (read, write, follow) 39 Create a new app with given client_name and scopes (read, write, follow)
40 40
@@ -53,12 +53,15 @@ class Mastodon:
53 } 53 }
54 54
55 try: 55 try:
56 if redirect_uris != None: 56 if redirect_uris is not None:
57 request_data['redirect_uris'] = redirect_uris; 57 request_data['redirect_uris'] = redirect_uris;
58 else: 58 else:
59 request_data['redirect_uris'] = 'urn:ietf:wg:oauth:2.0:oob'; 59 request_data['redirect_uris'] = 'urn:ietf:wg:oauth:2.0:oob';
60 60 if website is not None:
61 response = requests.post(api_base_url + '/api/v1/apps', data = request_data, timeout = request_timeout).json() 61 request_data['website'] = website
62
63 response = requests.post(api_base_url + '/api/v1/apps', data = request_data, timeout = request_timeout)
64 response = response.json()
62 except Exception as e: 65 except Exception as e:
63 import traceback 66 import traceback
64 traceback.print_exc() 67 traceback.print_exc()
@@ -102,6 +105,8 @@ class Mastodon:
102 self.access_token = access_token 105 self.access_token = access_token
103 self.debug_requests = debug_requests 106 self.debug_requests = debug_requests
104 self.ratelimit_method = ratelimit_method 107 self.ratelimit_method = ratelimit_method
108 self._token_expired = datetime.datetime.now()
109 self._refresh_token = None
105 110
106 self.ratelimit_limit = 150 111 self.ratelimit_limit = 150
107 self.ratelimit_reset = time.time() 112 self.ratelimit_reset = time.time()
@@ -125,32 +130,92 @@ class Mastodon:
125 if self.access_token != None and os.path.isfile(self.access_token): 130 if self.access_token != None and os.path.isfile(self.access_token):
126 with open(self.access_token, 'r') as token_file: 131 with open(self.access_token, 'r') as token_file:
127 self.access_token = token_file.readline().rstrip() 132 self.access_token = token_file.readline().rstrip()
128 133
129 def log_in(self, username, password, scopes = ['read', 'write', 'follow'], to_file = None): 134
130 """ 135 @property
131 Log in and sets access_token to what was returned. Note that your 136 def token_expired(self) -> bool:
132 username is the e-mail you use to log in into mastodon. 137 if self._token_expired < datetime.datetime.now():
133 138 return True
134 Can persist access token to file, to be used in the constructor. 139 else:
135 140 return False
136 Will throw a MastodonIllegalArgumentError if username / password 141
137 are wrong, scopes are not valid or granted scopes differ from requested. 142 @token_expired.setter
138 143 def token_expired(self, value: int):
139 Returns the access_token. 144 self._token_expired = datetime.datetime.now() + datetime.timedelta(seconds=value)
140 """ 145 return
141 params = self.__generate_params(locals()) 146
147 @property
148 def refresh_token(self) -> str:
149 return self._refresh_token
150
151 @refresh_token.setter
152 def refresh_token(self, value):
153 self._refresh_token = value
154 return
155
156 def auth_request_url(self, client_id: str = None, redirect_uris: str = "urn:ietf:wg:oauth:2.0:oob") -> str:
157 """Returns the url that a client needs to request the grant from the server.
158 https://mastodon.social/oauth/authorize?client_id=XXX&response_type=code&redirect_uris=YYY
159 """
160 if client_id is None:
161 client_id = self.client_id
162 else:
163 if os.path.isfile(client_id):
164 with open(client_id, 'r') as secret_file:
165 client_id = secret_file.readline().rstrip()
166
167 params = {}
168 params['client_id'] = client_id
169 params['response_type'] = "code"
170 params['redirect_uri'] = redirect_uris
171 formatted_params = urlencode(params)
172 return "".join([self.api_base_url, "/oauth/authorize?", formatted_params])
173
174 def log_in(self, username: str = None, password: str = None,\
175 code: str = None, redirect_uri: str = "urn:ietf:wg:oauth:2.0:oob", refresh_token: str = None,\
176 scopes: list = ['read', 'write', 'follow'], to_file: str = None) -> str:
177 """
178 Docs: https://github.com/doorkeeper-gem/doorkeeper/wiki/Interacting-as-an-OAuth-client-with-Doorkeeper
179
180 Notes:
181 Your username is the e-mail you use to log in into mastodon.
182
183 Can persist access token to file, to be used in the constructor.
184
185 Supports refresh_token but Mastodon.social doesn't implement it at the moment.
186
187 Handles password, authorization_code, and refresh_token authentication.
188
189 Will throw a MastodonIllegalArgumentError if username / password
190 are wrong, scopes are not valid or granted scopes differ from requested.
191
192 Returns:
193 str @access_token
194 """
195 if username is not None and password is not None:
196 params = self.__generate_params(locals(), ['scopes', 'to_file', 'code', 'refresh_token'])
197 params['grant_type'] = 'password'
198 elif code is not None:
199 params = self.__generate_params(locals(), ['scopes', 'to_file', 'username', 'password', 'refresh_token'])
200 params['grant_type'] = 'authorization_code'
201 elif refresh_token is not None:
202 params = self.__generate_params(locals(), ['scopes', 'to_file', 'username', 'password', 'code'])
203 params['grant_type'] = 'refresh_token'
204 else:
205 raise MastodonIllegalArgumentError('Invalid user name, password, redirect_uris or scopes')
206
142 params['client_id'] = self.client_id 207 params['client_id'] = self.client_id
143 params['client_secret'] = self.client_secret 208 params['client_secret'] = self.client_secret
144 params['grant_type'] = 'password' 209
145 params['scope'] = " ".join(scopes)
146
147 try: 210 try:
148 response = self.__api_request('POST', '/oauth/token', params, do_ratelimiting = False) 211 response = self.__api_request('POST', '/oauth/token', params, do_ratelimiting = False)
149 self.access_token = response['access_token'] 212 self.access_token = response['access_token']
213 self.refresh_token = response.get('refresh_token')
214 self.token_expired = int(response.get('expires_in', 0))
150 except Exception as e: 215 except Exception as e:
151 import traceback 216 import traceback
152 traceback.print_exc() 217 traceback.print_exc()
153 raise MastodonIllegalArgumentError('Invalid user name, password or scopes: %s' % e) 218 raise MastodonIllegalArgumentError('Invalid user name, password, redirect_uris or scopes: %s' % e)
154 219
155 requested_scopes = " ".join(sorted(scopes)) 220 requested_scopes = " ".join(sorted(scopes))
156 received_scopes = " ".join(sorted(response["scope"].split(" "))) 221 received_scopes = " ".join(sorted(response["scope"].split(" ")))
@@ -335,6 +400,17 @@ class Mastodon:
335 """ 400 """
336 params = self.__generate_params(locals()) 401 params = self.__generate_params(locals())
337 return self.__api_request('GET', '/api/v1/accounts/search', params) 402 return self.__api_request('GET', '/api/v1/accounts/search', params)
403
404
405 def content_search(self, q, resolve = False):
406 """
407 Fetch matching hashtags, accounts and statuses. Will search federated
408 instances if resolve is True.
409
410 Returns a dict of lists.
411 """
412 params = self.__generate_params(locals())
413 return self.__api_request('GET', '/api/v1/search', params)
338 414
339 ### 415 ###
340 # Reading data: Mutes and Blocks 416 # Reading data: Mutes and Blocks
@@ -568,7 +644,7 @@ class Mastodon:
568 Returns a media dict. This contains the id that can be used in 644 Returns a media dict. This contains the id that can be used in
569 status_post to attach the media file to a toot. 645 status_post to attach the media file to a toot.
570 """ 646 """
571 if os.path.isfile(media_file) and mime_type == None: 647 if mime_type == None and os.path.isfile(media_file):
572 mime_type = mimetypes.guess_type(media_file)[0] 648 mime_type = mimetypes.guess_type(media_file)[0]
573 media_file = open(media_file, 'rb') 649 media_file = open(media_file, 'rb')
574 650
@@ -734,13 +810,13 @@ class Mastodon:
734 if self.ratelimit_method == "throw": 810 if self.ratelimit_method == "throw":
735 raise MastodonRatelimitError("Hit rate limit.") 811 raise MastodonRatelimitError("Hit rate limit.")
736 812
737 if self.ratelimit_method == "wait" or self.ratelimit_method == "pace": 813 if self.ratelimit_method == "wait" or self.ratelimit_method == "pace":
738 to_next = self.ratelimit_reset - time.time() 814 to_next = self.ratelimit_reset - time.time()
739 if to_next > 0: 815 if to_next > 0:
740 # As a precaution, never sleep longer than 5 minutes 816 # As a precaution, never sleep longer than 5 minutes
741 to_next = min(to_next, 5 * 60) 817 to_next = min(to_next, 5 * 60)
742 time.sleep(to_next) 818 time.sleep(to_next)
743 request_complete = False 819 request_complete = False
744 820
745 return response 821 return response
746 822
Powered by cgit v1.2.3 (git 2.41.0)