aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--mastodon/Mastodon.py230
1 files changed, 124 insertions, 106 deletions
diff --git a/mastodon/Mastodon.py b/mastodon/Mastodon.py
index 7cbdcc4..a50c5ab 100644
--- a/mastodon/Mastodon.py
+++ b/mastodon/Mastodon.py
@@ -13,59 +13,59 @@ import dateutil
13import dateutil.parser 13import dateutil.parser
14 14
15class Mastodon: 15class Mastodon:
16 """ 16 """
17 Super basic but thorough and easy to use mastodon.social 17 Super basic but thorough and easy to use mastodon.social
18 api wrapper in python. 18 api wrapper in python.
19 19
20 If anything is unclear, check the official API docs at 20 If anything is unclear, check the official API docs at
21 https://github.com/Gargron/mastodon/wiki/API 21 https://github.com/Gargron/mastodon/wiki/API
22 22
23 Presently, only username-password login is supported, somebody please 23 Presently, only username-password login is supported, somebody please
24 patch in Real Proper OAuth if desired. 24 patch in Real Proper OAuth if desired.
25 """ 25 """
26 __DEFAULT_BASE_URL = 'https://mastodon.social' 26 __DEFAULT_BASE_URL = 'https://mastodon.social'
27 __DEFAULT_TIMEOUT = 300 27 __DEFAULT_TIMEOUT = 300
28 28
29 29
30 ### 30 ###
31 # Registering apps 31 # Registering apps
32 ### 32 ###
33 @staticmethod 33 @staticmethod
34 def create_app(client_name, scopes = ['read', 'write', 'follow'], redirect_uris = None, to_file = None, api_base_url = __DEFAULT_BASE_URL): 34 def create_app(client_name, scopes = ['read', 'write', 'follow'], redirect_uris = None, to_file = None, api_base_url = __DEFAULT_BASE_URL):
35 """ 35 """
36 Create a new app with given client_name and scopes (read, write, follow) 36 Create a new app with given client_name and scopes (read, write, follow)
37 37
38 Specify redirect_uris if you want users to be redirected to a certain page after authenticating. 38 Specify redirect_uris if you want users to be redirected to a certain page after authenticating.
39 Specify to_file to persist your apps info to a file so you can use them in the constructor. 39 Specify to_file to persist your apps info to a file so you can use them in the constructor.
40 Specify api_base_url if you want to register an app on an instance different from the flagship one. 40 Specify api_base_url if you want to register an app on an instance different from the flagship one.
41 41
42 Presently, app registration is open by default, but this is not guaranteed to be the case for all 42 Presently, app registration is open by default, but this is not guaranteed to be the case for all
43 future mastodon instances or even the flagship instance in the future. 43 future mastodon instances or even the flagship instance in the future.
44 44
45 Returns client_id and client_secret. 45 Returns client_id and client_secret.
46 """ 46 """
47 request_data = { 47 request_data = {
48 'client_name': client_name, 48 'client_name': client_name,
49 'scopes': " ".join(scopes) 49 'scopes': " ".join(scopes)
50 } 50 }
51 51
52 try: 52 try:
53 if redirect_uris != None: 53 if redirect_uris != None:
54 request_data['redirect_uris'] = redirect_uris; 54 request_data['redirect_uris'] = redirect_uris;
55 else: 55 else:
56 request_data['redirect_uris'] = 'urn:ietf:wg:oauth:2.0:oob'; 56 request_data['redirect_uris'] = 'urn:ietf:wg:oauth:2.0:oob';
57 57
58 response = requests.post(api_base_url + '/api/v1/apps', data = request_data, timeout = self.request_timeout).json() 58 response = requests.post(api_base_url + '/api/v1/apps', data = request_data, timeout = self.request_timeout).json()
59 except: 59 except:
60 raise MastodonNetworkError("Could not complete request.") 60 raise MastodonNetworkError("Could not complete request.")
61 61
62 if to_file != None: 62 if to_file != None:
63 with open(to_file, 'w') as secret_file: 63 with open(to_file, 'w') as secret_file:
64 secret_file.write(response['client_id'] + '\n') 64 secret_file.write(response['client_id'] + '\n')
65 secret_file.write(response['client_secret'] + '\n') 65 secret_file.write(response['client_secret'] + '\n')
66 66
67 return (response['client_id'], response['client_secret']) 67 return (response['client_id'], response['client_secret'])
68 68
69 ### 69 ###
70 # Authentication, including constructor 70 # Authentication, including constructor
71 ### 71 ###
@@ -73,42 +73,42 @@ class Mastodon:
73 """ 73 """
74 Create a new API wrapper instance based on the given client_secret and client_id. If you 74 Create a new API wrapper instance based on the given client_secret and client_id. If you
75 give a client_id and it is not a file, you must also give a secret. 75 give a client_id and it is not a file, you must also give a secret.
76 76
77 You can also specify an access_token, directly or as a file (as written by log_in). 77 You can also specify an access_token, directly or as a file (as written by log_in).
78 78
79 Mastodon.py can try to respect rate limits in several ways, controlled by ratelimit_method. 79 Mastodon.py can try to respect rate limits in several ways, controlled by ratelimit_method.
80 "throw" makes functions throw a MastodonRatelimitError when the rate 80 "throw" makes functions throw a MastodonRatelimitError when the rate
81 limit is hit. "wait" mode will, once the limit is hit, wait and retry the request as soon 81 limit is hit. "wait" mode will, once the limit is hit, wait and retry the request as soon
82 as the rate limit resets, until it succeeds. "pace" works like throw, but tries to wait in 82 as the rate limit resets, until it succeeds. "pace" works like throw, but tries to wait in
83 between calls so that the limit is generally not hit (How hard it tries to not hit the rate 83 between calls so that the limit is generally not hit (How hard it tries to not hit the rate
84 limit can be controlled by ratelimit_pacefactor). The default setting is "wait". Note that 84 limit can be controlled by ratelimit_pacefactor). The default setting is "wait". Note that
85 even in "wait" and "pace" mode, requests can still fail due to network or other problems! Also 85 even in "wait" and "pace" mode, requests can still fail due to network or other problems! Also
86 note that "pace" and "wait" are NOT thread safe. 86 note that "pace" and "wait" are NOT thread safe.
87 87
88 Specify api_base_url if you wish to talk to an instance other than the flagship one. 88 Specify api_base_url if you wish to talk to an instance other than the flagship one.
89 If a file is given as client_id, read client ID and secret from that file. 89 If a file is given as client_id, read client ID and secret from that file.
90 90
91 By defautl, a timeout of 300 seconds is used for all requests. If you wish to change this, 91 By defautl, a timeout of 300 seconds is used for all requests. If you wish to change this,
92 pass the desired timeout (in seconds) as request_timeout. 92 pass the desired timeout (in seconds) as request_timeout.
93 """ 93 """
94 self.api_base_url = api_base_url 94 self.api_base_url = api_base_url
95 self.client_id = client_id 95 self.client_id = client_id
96 self.client_secret = client_secret 96 self.client_secret = client_secret
97 self.access_token = access_token 97 self.access_token = access_token
98 self.debug_requests = debug_requests 98 self.debug_requests = debug_requests
99 self.ratelimit_method = ratelimit_method 99 self.ratelimit_method = ratelimit_method
100 100
101 self.ratelimit_limit = 150 101 self.ratelimit_limit = 150
102 self.ratelimit_reset = time.time() 102 self.ratelimit_reset = time.time()
103 self.ratelimit_remaining = 150 103 self.ratelimit_remaining = 150
104 self.ratelimit_lastcall = time.time() 104 self.ratelimit_lastcall = time.time()
105 self.ratelimit_pacefactor = ratelimit_pacefactor 105 self.ratelimit_pacefactor = ratelimit_pacefactor
106 106
107 self.request_timeout = request_timeout 107 self.request_timeout = request_timeout
108 108
109 if not ratelimit_method in ["throw", "wait", "pace"]: 109 if not ratelimit_method in ["throw", "wait", "pace"]:
110 raise MastodonIllegalArgumentError("Invalid ratelimit method.") 110 raise MastodonIllegalArgumentError("Invalid ratelimit method.")
111 111
112 if os.path.isfile(self.client_id): 112 if os.path.isfile(self.client_id):
113 with open(self.client_id, 'r') as secret_file: 113 with open(self.client_id, 'r') as secret_file:
114 self.client_id = secret_file.readline().rstrip() 114 self.client_id = secret_file.readline().rstrip()
@@ -116,21 +116,21 @@ class Mastodon:
116 else: 116 else:
117 if self.client_secret == None: 117 if self.client_secret == None:
118 raise MastodonIllegalArgumentError('Specified client id directly, but did not supply secret') 118 raise MastodonIllegalArgumentError('Specified client id directly, but did not supply secret')
119 119
120 if self.access_token != None and os.path.isfile(self.access_token): 120 if self.access_token != None and os.path.isfile(self.access_token):
121 with open(self.access_token, 'r') as token_file: 121 with open(self.access_token, 'r') as token_file:
122 self.access_token = token_file.readline().rstrip() 122 self.access_token = token_file.readline().rstrip()
123 123
124 def log_in(self, username, password, scopes = ['read', 'write', 'follow'], to_file = None): 124 def log_in(self, username, password, scopes = ['read', 'write', 'follow'], to_file = None):
125 """ 125 """
126 Log in and sets access_token to what was returned. Note that your 126 Log in and sets access_token to what was returned. Note that your
127 username is the e-mail you use to log in into mastodon. 127 username is the e-mail you use to log in into mastodon.
128 128
129 Can persist access token to file, to be used in the constructor. 129 Can persist access token to file, to be used in the constructor.
130 130
131 Will throw a MastodonIllegalArgumentError if username / password 131 Will throw a MastodonIllegalArgumentError if username / password
132 are wrong, scopes are not valid or granted scopes differ from requested. 132 are wrong, scopes are not valid or granted scopes differ from requested.
133 133
134 Returns the access_token. 134 Returns the access_token.
135 """ 135 """
136 params = self.__generate_params(locals()) 136 params = self.__generate_params(locals())
@@ -138,25 +138,25 @@ class Mastodon:
138 params['client_secret'] = self.client_secret 138 params['client_secret'] = self.client_secret
139 params['grant_type'] = 'password' 139 params['grant_type'] = 'password'
140 params['scope'] = " ".join(scopes) 140 params['scope'] = " ".join(scopes)
141 141
142 try: 142 try:
143 response = self.__api_request('POST', '/oauth/token', params, do_ratelimiting = False) 143 response = self.__api_request('POST', '/oauth/token', params, do_ratelimiting = False)
144 self.access_token = response['access_token'] 144 self.access_token = response['access_token']
145 except: 145 except:
146 raise MastodonIllegalArgumentError('Invalid user name, password or scopes.') 146 raise MastodonIllegalArgumentError('Invalid user name, password or scopes.')
147 147
148 requested_scopes = " ".join(sorted(scopes)) 148 requested_scopes = " ".join(sorted(scopes))
149 received_scopes = " ".join(sorted(response["scope"].split(" "))) 149 received_scopes = " ".join(sorted(response["scope"].split(" ")))
150 150
151 if requested_scopes != received_scopes: 151 if requested_scopes != received_scopes:
152 raise MastodonAPIError('Granted scopes "' + received_scopes + '" differ from requested scopes "' + requested_scopes + '".') 152 raise MastodonAPIError('Granted scopes "' + received_scopes + '" differ from requested scopes "' + requested_scopes + '".')
153 153
154 if to_file != None: 154 if to_file != None:
155 with open(to_file, 'w') as token_file: 155 with open(to_file, 'w') as token_file:
156 token_file.write(response['access_token'] + '\n') 156 token_file.write(response['access_token'] + '\n')
157 157
158 return response['access_token'] 158 return response['access_token']
159 159
160 ### 160 ###
161 # Reading data: Timelines 161 # Reading data: Timelines
162 ## 162 ##
@@ -164,14 +164,14 @@ class Mastodon:
164 """ 164 """
165 Fetch statuses, most recent ones first. Timeline can be home, mentions, public 165 Fetch statuses, most recent ones first. Timeline can be home, mentions, public
166 or tag/hashtag. See the following functions documentation for what those do. 166 or tag/hashtag. See the following functions documentation for what those do.
167 167
168 The default timeline is the "home" timeline. 168 The default timeline is the "home" timeline.
169 169
170 Returns a list of toot dicts. 170 Returns a list of toot dicts.
171 """ 171 """
172 params = self.__generate_params(locals(), ['timeline']) 172 params = self.__generate_params(locals(), ['timeline'])
173 return self.__api_request('GET', '/api/v1/timelines/' + timeline, params) 173 return self.__api_request('GET', '/api/v1/timelines/' + timeline, params)
174 174
175 def timeline_home(self, max_id = None, since_id = None, limit = None): 175 def timeline_home(self, max_id = None, since_id = None, limit = None):
176 """ 176 """
177 Fetch the authenticated users home timeline (i.e. followed users and self). 177 Fetch the authenticated users home timeline (i.e. followed users and self).
@@ -179,7 +179,7 @@ class Mastodon:
179 Returns a list of toot dicts. 179 Returns a list of toot dicts.
180 """ 180 """
181 return self.timeline('home', max_id = max_id, since_id = since_id, limit = limit) 181 return self.timeline('home', max_id = max_id, since_id = since_id, limit = limit)
182 182
183 def timeline_mentions(self, max_id = None, since_id = None, limit = None): 183 def timeline_mentions(self, max_id = None, since_id = None, limit = None):
184 """ 184 """
185 Fetches the authenticated users mentions. 185 Fetches the authenticated users mentions.
@@ -187,7 +187,7 @@ class Mastodon:
187 Returns a list of toot dicts. 187 Returns a list of toot dicts.
188 """ 188 """
189 return self.timeline('mentions', max_id = max_id, since_id = since_id, limit = limit) 189 return self.timeline('mentions', max_id = max_id, since_id = since_id, limit = limit)
190 190
191 def timeline_public(self, max_id = None, since_id = None, limit = None): 191 def timeline_public(self, max_id = None, since_id = None, limit = None):
192 """ 192 """
193 Fetches the public / visible-network timeline. 193 Fetches the public / visible-network timeline.
@@ -195,7 +195,7 @@ class Mastodon:
195 Returns a list of toot dicts. 195 Returns a list of toot dicts.
196 """ 196 """
197 return self.timeline('public', max_id = max_id, since_id = since_id, limit = limit) 197 return self.timeline('public', max_id = max_id, since_id = since_id, limit = limit)
198 198
199 def timeline_hashtag(self, hashtag, max_id = None, since_id = None, limit = None): 199 def timeline_hashtag(self, hashtag, max_id = None, since_id = None, limit = None):
200 """ 200 """
201 Fetch a timeline of toots with a given hashtag. 201 Fetch a timeline of toots with a given hashtag.
@@ -203,7 +203,7 @@ class Mastodon:
203 Returns a list of toot dicts. 203 Returns a list of toot dicts.
204 """ 204 """
205 return self.timeline('tag/' + str(hashtag), max_id = max_id, since_id = since_id, limit = limit) 205 return self.timeline('tag/' + str(hashtag), max_id = max_id, since_id = since_id, limit = limit)
206 206
207 ### 207 ###
208 # Reading data: Statuses 208 # Reading data: Statuses
209 ### 209 ###
@@ -222,7 +222,7 @@ class Mastodon:
222 Returns a context dict. 222 Returns a context dict.
223 """ 223 """
224 return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/context') 224 return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/context')
225 225
226 def status_reblogged_by(self, id): 226 def status_reblogged_by(self, id):
227 """ 227 """
228 Fetch a list of users that have reblogged a status. 228 Fetch a list of users that have reblogged a status.
@@ -230,7 +230,7 @@ class Mastodon:
230 Returns a list of user dicts. 230 Returns a list of user dicts.
231 """ 231 """
232 return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/reblogged_by') 232 return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/reblogged_by')
233 233
234 def status_favourited_by(self, id): 234 def status_favourited_by(self, id):
235 """ 235 """
236 Fetch a list of users that have favourited a status. 236 Fetch a list of users that have favourited a status.
@@ -238,7 +238,7 @@ class Mastodon:
238 Returns a list of user dicts. 238 Returns a list of user dicts.
239 """ 239 """
240 return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/favourited_by') 240 return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/favourited_by')
241 241
242 ### 242 ###
243 # Reading data: Notifications 243 # Reading data: Notifications
244 ### 244 ###
@@ -250,7 +250,7 @@ class Mastodon:
250 Returns a list of notification dicts. 250 Returns a list of notification dicts.
251 """ 251 """
252 return self.__api_request('GET', '/api/v1/notifications') 252 return self.__api_request('GET', '/api/v1/notifications')
253 253
254 ### 254 ###
255 # Reading data: Accounts 255 # Reading data: Accounts
256 ### 256 ###
@@ -269,7 +269,7 @@ class Mastodon:
269 Returns a user dict. 269 Returns a user dict.
270 """ 270 """
271 return self.__api_request('GET', '/api/v1/accounts/verify_credentials') 271 return self.__api_request('GET', '/api/v1/accounts/verify_credentials')
272 272
273 def account_statuses(self, id, max_id = None, since_id = None, limit = None): 273 def account_statuses(self, id, max_id = None, since_id = None, limit = None):
274 """ 274 """
275 Fetch statuses by user id. Same options as timeline are permitted. 275 Fetch statuses by user id. Same options as timeline are permitted.
@@ -297,7 +297,7 @@ class Mastodon:
297 297
298 def account_relationships(self, id): 298 def account_relationships(self, id):
299 """ 299 """
300 Fetch relationships (following, followed_by, blocking) of the logged in user to 300 Fetch relationships (following, followed_by, blocking) of the logged in user to
301 a given account. id can be a list. 301 a given account. id can be a list.
302 302
303 Returns a list of relationship dicts. 303 Returns a list of relationship dicts.
@@ -307,28 +307,46 @@ class Mastodon:
307 307
308 def account_search(self, q, limit = None): 308 def account_search(self, q, limit = None):
309 """ 309 """
310 Fetch matching accounts. Will lookup an account remotely if the search term is 310 Fetch matching accounts. Will lookup an account remotely if the search term is
311 in the username@domain format and not yet in the database. 311 in the username@domain format and not yet in the database.
312 312
313 Returns a list of user dicts. 313 Returns a list of user dicts.
314 """ 314 """
315 params = self.__generate_params(locals()) 315 params = self.__generate_params(locals())
316 return self.__api_request('GET', '/api/v1/accounts/search', params) 316 return self.__api_request('GET', '/api/v1/accounts/search', params)
317 317
318 ### 318 ###
319 # Writing data: Statuses 319 # Writing data: Statuses
320 ### 320 ###
321 def status_post(self, status, in_reply_to_id = None, media_ids = None): 321 def status_post(self, status, in_reply_to_id = None, media_ids = None, sensitive = False, visibility = ''):
322 """ 322 """
323 Post a status. Can optionally be in reply to another status and contain 323 Post a status. Can optionally be in reply to another status and contain
324 up to four pieces of media (Uploaded via media_post()). media_ids can 324 up to four pieces of media (Uploaded via media_post()). media_ids can
325 also be the media dicts returned by media_post - they are unpacked 325 also be the media dicts returned by media_post - they are unpacked
326 automatically. 326 automatically.
327 327
328 The 'sensitive' boolean decides whether or not media attached to the post
329 should be marked as sensitive, which hides it by default on the Mastodon
330 web front-end.
331
332 The visibility parameter is a string value and matches the visibility
333 option on the /api/v1/status POST API endpoint. It accepts any of:
334 'private' - post will be visible only to followers
335 'unlisted' - post will be public but not appear on the public timeline
336 'public' - post will be public
337
338 If not passed in, visibility defaults to match the current account's
339 privacy setting (private if the account is locked, public otherwise).
340
328 Returns a toot dict with the new status. 341 Returns a toot dict with the new status.
329 """ 342 """
330 params_initial = locals() 343 params_initial = locals()
331 344
345 # Validate visibility parameter
346 valid_visibilities = ['private', 'public', 'unlisted', '']
347 if params_initial['visibility'].lower() not in valid_visibilities:
348 raise ValueError('Invalid visibility value! Acceptable values are %s' % valid_visibilities)
349
332 if media_ids != None: 350 if media_ids != None:
333 try: 351 try:
334 media_ids_proper = [] 352 media_ids_proper = []
@@ -339,12 +357,12 @@ class Mastodon:
339 media_ids_proper.append(media_id) 357 media_ids_proper.append(media_id)
340 except: 358 except:
341 raise MastodonIllegalArgumentError("Invalid media dict.") 359 raise MastodonIllegalArgumentError("Invalid media dict.")
342 360
343 params_initial["media_ids"] = media_ids_proper 361 params_initial["media_ids"] = media_ids_proper
344 362
345 params = self.__generate_params(params_initial) 363 params = self.__generate_params(params_initial)
346 return self.__api_request('POST', '/api/v1/statuses', params) 364 return self.__api_request('POST', '/api/v1/statuses', params)
347 365
348 def toot(self, status): 366 def toot(self, status):
349 """ 367 """
350 Synonym for status_post that only takes the status text as input. 368 Synonym for status_post that only takes the status text as input.
@@ -352,7 +370,7 @@ class Mastodon:
352 Returns a toot dict with the new status. 370 Returns a toot dict with the new status.
353 """ 371 """
354 return self.status_post(status) 372 return self.status_post(status)
355 373
356 def status_delete(self, id): 374 def status_delete(self, id):
357 """ 375 """
358 Delete a status 376 Delete a status
@@ -363,7 +381,7 @@ class Mastodon:
363 381
364 def status_reblog(self, id): 382 def status_reblog(self, id):
365 """Reblog a status. 383 """Reblog a status.
366 384
367 Returns a toot with with a new status that wraps around the reblogged one. 385 Returns a toot with with a new status that wraps around the reblogged one.
368 """ 386 """
369 return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/reblog") 387 return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/reblog")
@@ -371,7 +389,7 @@ class Mastodon:
371 def status_unreblog(self, id): 389 def status_unreblog(self, id):
372 """ 390 """
373 Un-reblog a status. 391 Un-reblog a status.
374 392
375 Returns a toot dict with the status that used to be reblogged. 393 Returns a toot dict with the status that used to be reblogged.
376 """ 394 """
377 return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/unreblog") 395 return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/unreblog")
@@ -379,49 +397,49 @@ class Mastodon:
379 def status_favourite(self, id): 397 def status_favourite(self, id):
380 """ 398 """
381 Favourite a status. 399 Favourite a status.
382 400
383 Returns a toot dict with the favourited status. 401 Returns a toot dict with the favourited status.
384 """ 402 """
385 return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/favourite") 403 return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/favourite")
386 404
387 def status_unfavourite(self, id): 405 def status_unfavourite(self, id):
388 """Favourite a status. 406 """Favourite a status.
389 407
390 Returns a toot dict with the un-favourited status. 408 Returns a toot dict with the un-favourited status.
391 """ 409 """
392 return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/unfavourite") 410 return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/unfavourite")
393 411
394 ### 412 ###
395 # Writing data: Accounts 413 # Writing data: Accounts
396 ### 414 ###
397 def account_follow(self, id): 415 def account_follow(self, id):
398 """ 416 """
399 Follow a user. 417 Follow a user.
400 418
401 Returns a relationship dict containing the updated relationship to the user. 419 Returns a relationship dict containing the updated relationship to the user.
402 """ 420 """
403 return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/follow") 421 return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/follow")
404 422
405 def account_unfollow(self, id): 423 def account_unfollow(self, id):
406 """ 424 """
407 Unfollow a user. 425 Unfollow a user.
408 426
409 Returns a relationship dict containing the updated relationship to the user. 427 Returns a relationship dict containing the updated relationship to the user.
410 """ 428 """
411 return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/unfollow") 429 return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/unfollow")
412 430
413 def account_block(self, id): 431 def account_block(self, id):
414 """ 432 """
415 Block a user. 433 Block a user.
416 434
417 Returns a relationship dict containing the updated relationship to the user. 435 Returns a relationship dict containing the updated relationship to the user.
418 """ 436 """
419 return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/block") 437 return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/block")
420 438
421 def account_unblock(self, id): 439 def account_unblock(self, id):
422 """ 440 """
423 Unblock a user. 441 Unblock a user.
424 442
425 Returns a relationship dict containing the updated relationship to the user. 443 Returns a relationship dict containing the updated relationship to the user.
426 """ 444 """
427 return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/unblock") 445 return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/unblock")
@@ -435,8 +453,8 @@ class Mastodon:
435 a file name. If image data is passed directly, the mime 453 a file name. If image data is passed directly, the mime
436 type has to be specified manually, otherwise, it is 454 type has to be specified manually, otherwise, it is
437 determined from the file name. 455 determined from the file name.
438 456
439 Throws a MastodonIllegalArgumentError if the mime type of the 457 Throws a MastodonIllegalArgumentError if the mime type of the
440 passed data or file can not be determined properly. 458 passed data or file can not be determined properly.
441 459
442 Returns a media dict. This contains the id that can be used in 460 Returns a media dict. This contains the id that can be used in
@@ -445,16 +463,16 @@ class Mastodon:
445 if os.path.isfile(media_file) and mime_type == None: 463 if os.path.isfile(media_file) and mime_type == None:
446 mime_type = mimetypes.guess_type(media_file)[0] 464 mime_type = mimetypes.guess_type(media_file)[0]
447 media_file = open(media_file, 'rb') 465 media_file = open(media_file, 'rb')
448 466
449 if mime_type == None: 467 if mime_type == None:
450 raise MastodonIllegalArgumentError('Could not determine mime type or data passed directly without mime type.') 468 raise MastodonIllegalArgumentError('Could not determine mime type or data passed directly without mime type.')
451 469
452 random_suffix = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(10)) 470 random_suffix = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(10))
453 file_name = "mastodonpyupload_" + str(time.time()) + "_" + str(random_suffix) + mimetypes.guess_extension(mime_type) 471 file_name = "mastodonpyupload_" + str(time.time()) + "_" + str(random_suffix) + mimetypes.guess_extension(mime_type)
454 472
455 media_file_description = (file_name, media_file, mime_type) 473 media_file_description = (file_name, media_file, mime_type)
456 return self.__api_request('POST', '/api/v1/media', files = {'file': media_file_description}) 474 return self.__api_request('POST', '/api/v1/media', files = {'file': media_file_description})
457 475
458 ### 476 ###
459 # Internal helpers, dragons probably 477 # Internal helpers, dragons probably
460 ### 478 ###
@@ -462,7 +480,7 @@ class Mastodon:
462 """ 480 """
463 Converts a python datetime to unix epoch, accounting for 481 Converts a python datetime to unix epoch, accounting for
464 time zones and such. 482 time zones and such.
465 483
466 Assumes UTC if timezone is not given. 484 Assumes UTC if timezone is not given.
467 """ 485 """
468 date_time_utc = None 486 date_time_utc = None
@@ -470,18 +488,18 @@ class Mastodon:
470 date_time_utc = date_time.replace(tzinfo = pytz.utc) 488 date_time_utc = date_time.replace(tzinfo = pytz.utc)
471 else: 489 else:
472 date_time_utc = date_time.astimezone(pytz.utc) 490 date_time_utc = date_time.astimezone(pytz.utc)
473 491
474 epoch_utc = datetime.datetime.utcfromtimestamp(0).replace(tzinfo = pytz.utc) 492 epoch_utc = datetime.datetime.utcfromtimestamp(0).replace(tzinfo = pytz.utc)
475 493
476 return (date_time_utc - epoch_utc).total_seconds() 494 return (date_time_utc - epoch_utc).total_seconds()
477 495
478 def __api_request(self, method, endpoint, params = {}, files = {}, do_ratelimiting = True): 496 def __api_request(self, method, endpoint, params = {}, files = {}, do_ratelimiting = True):
479 """ 497 """
480 Internal API request helper. 498 Internal API request helper.
481 """ 499 """
482 response = None 500 response = None
483 headers = None 501 headers = None
484 502
485 # "pace" mode ratelimiting: Assume constant rate of requests, sleep a little less long than it 503 # "pace" mode ratelimiting: Assume constant rate of requests, sleep a little less long than it
486 # would take to not hit the rate limit at that request rate. 504 # would take to not hit the rate limit at that request rate.
487 if do_ratelimiting and self.ratelimit_method == "pace": 505 if do_ratelimiting and self.ratelimit_method == "pace":
@@ -495,16 +513,16 @@ class Mastodon:
495 time_waited = time.time() - self.ratelimit_lastcall 513 time_waited = time.time() - self.ratelimit_lastcall
496 time_wait = float(self.ratelimit_reset - time.time()) / float(self.ratelimit_remaining) 514 time_wait = float(self.ratelimit_reset - time.time()) / float(self.ratelimit_remaining)
497 remaining_wait = time_wait - time_waited 515 remaining_wait = time_wait - time_waited
498 516
499 if remaining_wait > 0: 517 if remaining_wait > 0:
500 to_next = remaining_wait / self.ratelimit_pacefactor 518 to_next = remaining_wait / self.ratelimit_pacefactor
501 to_next = min(to_next, 5 * 60) 519 to_next = min(to_next, 5 * 60)
502 time.sleep(to_next) 520 time.sleep(to_next)
503 521
504 # Generate request headers 522 # Generate request headers
505 if self.access_token != None: 523 if self.access_token != None:
506 headers = {'Authorization': 'Bearer ' + self.access_token} 524 headers = {'Authorization': 'Bearer ' + self.access_token}
507 525
508 if self.debug_requests == True: 526 if self.debug_requests == True:
509 print('Mastodon: Request to endpoint "' + endpoint + '" using method "' + method + '".') 527 print('Mastodon: Request to endpoint "' + endpoint + '" using method "' + method + '".')
510 print('Parameters: ' + str(params)) 528 print('Parameters: ' + str(params))
@@ -515,40 +533,40 @@ class Mastodon:
515 request_complete = False 533 request_complete = False
516 while not request_complete: 534 while not request_complete:
517 request_complete = True 535 request_complete = True
518 536
519 response_object = None 537 response_object = None
520 try: 538 try:
521 if method == 'GET': 539 if method == 'GET':
522 response_object = requests.get(self.api_base_url + endpoint, data = params, headers = headers, files = files, timeout = self.request_timeout) 540 response_object = requests.get(self.api_base_url + endpoint, data = params, headers = headers, files = files, timeout = self.request_timeout)
523 541
524 if method == 'POST': 542 if method == 'POST':
525 response_object = requests.post(self.api_base_url + endpoint, data = params, headers = headers, files = files, timeout = self.request_timeout) 543 response_object = requests.post(self.api_base_url + endpoint, data = params, headers = headers, files = files, timeout = self.request_timeout)
526 544
527 if method == 'DELETE': 545 if method == 'DELETE':
528 response_object = requests.delete(self.api_base_url + endpoint, data = params, headers = headers, files = files, timeout = self.request_timeout) 546 response_object = requests.delete(self.api_base_url + endpoint, data = params, headers = headers, files = files, timeout = self.request_timeout)
529 except: 547 except:
530 raise MastodonNetworkError("Could not complete request.") 548 raise MastodonNetworkError("Could not complete request.")
531 549
532 if response_object == None: 550 if response_object == None:
533 raise MastodonIllegalArgumentError("Illegal request.") 551 raise MastodonIllegalArgumentError("Illegal request.")
534 552
535 # Handle response 553 # Handle response
536 if self.debug_requests == True: 554 if self.debug_requests == True:
537 print('Mastodon: Response received with code ' + str(response_object.status_code) + '.') 555 print('Mastodon: Response received with code ' + str(response_object.status_code) + '.')
538 print('Respose headers: ' + str(response_object.headers)) 556 print('Respose headers: ' + str(response_object.headers))
539 print('Response text content: ' + str(response_object.text)) 557 print('Response text content: ' + str(response_object.text))
540 558
541 if response_object.status_code == 404: 559 if response_object.status_code == 404:
542 raise MastodonAPIError('Endpoint not found.') 560 raise MastodonAPIError('Endpoint not found.')
543 561
544 if response_object.status_code == 500: 562 if response_object.status_code == 500:
545 raise MastodonAPIError('General API problem.') 563 raise MastodonAPIError('General API problem.')
546 564
547 try: 565 try:
548 response = response_object.json() 566 response = response_object.json()
549 except: 567 except:
550 raise MastodonAPIError("Could not parse response as JSON, respose code was " + str(response_object.status_code)) 568 raise MastodonAPIError("Could not parse response as JSON, respose code was " + str(response_object.status_code))
551 569
552 # Handle rate limiting 570 # Handle rate limiting
553 if 'X-RateLimit-Remaining' in response_object.headers and do_ratelimiting: 571 if 'X-RateLimit-Remaining' in response_object.headers and do_ratelimiting:
554 self.ratelimit_remaining = int(response_object.headers['X-RateLimit-Remaining']) 572 self.ratelimit_remaining = int(response_object.headers['X-RateLimit-Remaining'])
@@ -566,7 +584,7 @@ class Mastodon:
566 self.ratelimit_lastcall = time.time() 584 self.ratelimit_lastcall = time.time()
567 except: 585 except:
568 raise MastodonRatelimitError("Rate limit time calculations failed.") 586 raise MastodonRatelimitError("Rate limit time calculations failed.")
569 587
570 if "error" in response and response["error"] == "Throttled": 588 if "error" in response and response["error"] == "Throttled":
571 if self.ratelimit_method == "throw": 589 if self.ratelimit_method == "throw":
572 raise MastodonRatelimitError("Hit rate limit.") 590 raise MastodonRatelimitError("Hit rate limit.")
@@ -575,35 +593,35 @@ class Mastodon:
575 to_next = self.ratelimit_reset - time.time() 593 to_next = self.ratelimit_reset - time.time()
576 if to_next > 0: 594 if to_next > 0:
577 # As a precaution, never sleep longer than 5 minutes 595 # As a precaution, never sleep longer than 5 minutes
578 to_next = min(to_next, 5 * 60) 596 to_next = min(to_next, 5 * 60)
579 time.sleep(to_next) 597 time.sleep(to_next)
580 request_complete = False 598 request_complete = False
581 599
582 return response 600 return response
583 601
584 def __generate_params(self, params, exclude = []): 602 def __generate_params(self, params, exclude = []):
585 """ 603 """
586 Internal named-parameters-to-dict helper. 604 Internal named-parameters-to-dict helper.
587 605
588 Note for developers: If called with locals() as params, 606 Note for developers: If called with locals() as params,
589 as is the usual practice in this code, the __generate_params call 607 as is the usual practice in this code, the __generate_params call
590 (or at least the locals() call) should generally be the first thing 608 (or at least the locals() call) should generally be the first thing
591 in your function. 609 in your function.
592 """ 610 """
593 params = dict(params) 611 params = dict(params)
594 612
595 del params['self'] 613 del params['self']
596 param_keys = list(params.keys()) 614 param_keys = list(params.keys())
597 for key in param_keys: 615 for key in param_keys:
598 if params[key] == None or key in exclude: 616 if params[key] == None or key in exclude:
599 del params[key] 617 del params[key]
600 618
601 param_keys = list(params.keys()) 619 param_keys = list(params.keys())
602 for key in param_keys: 620 for key in param_keys:
603 if isinstance(params[key], list): 621 if isinstance(params[key], list):
604 params[key + "[]"] = params[key] 622 params[key + "[]"] = params[key]
605 del params[key] 623 del params[key]
606 624
607 return params 625 return params
608 626
609## 627##
Powered by cgit v1.2.3 (git 2.41.0)