aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLorenz Diener <[email protected]>2016-11-25 20:57:53 +0100
committerLorenz Diener <[email protected]>2016-11-25 20:57:53 +0100
commitab5889404144834b2671d2d25b207fe761418d5c (patch)
tree0bd05a5cd7d233b102cbf89176b88bf2af92ad5e /mastodon/Mastodon.py
parente4e3a8eb93721bf12e4a5b2bf45dc3c2a473dc6f (diff)
parentb958ce54ba32968ef159bda91c8f480c74374f68 (diff)
downloadmastodon.py-ab5889404144834b2671d2d25b207fe761418d5c.tar.gz
Merge remote-tracking branch 'refs/remotes/origin/master' into ratelimits
# Conflicts: # mastodon/Mastodon.py
Diffstat (limited to 'mastodon/Mastodon.py')
-rw-r--r--mastodon/Mastodon.py176
1 files changed, 104 insertions, 72 deletions
diff --git a/mastodon/Mastodon.py b/mastodon/Mastodon.py
index 8af739d..bc1c52b 100644
--- a/mastodon/Mastodon.py
+++ b/mastodon/Mastodon.py
@@ -29,7 +29,7 @@ class Mastodon:
29 @staticmethod 29 @staticmethod
30 def create_app(client_name, scopes = ['read', 'write', 'follow'], redirect_uris = None, to_file = None, api_base_url = __DEFAULT_BASE_URL): 30 def create_app(client_name, scopes = ['read', 'write', 'follow'], redirect_uris = None, to_file = None, api_base_url = __DEFAULT_BASE_URL):
31 """ 31 """
32 Creates a new app with given client_name and scopes (read, write, follow) 32 Create a new app with given client_name and scopes (read, write, follow)
33 33
34 Specify redirect_uris if you want users to be redirected to a certain page after authenticating. 34 Specify redirect_uris if you want users to be redirected to a certain page after authenticating.
35 Specify to_file to persist your apps info to a file so you can use them in the constructor. 35 Specify to_file to persist your apps info to a file so you can use them in the constructor.
@@ -64,7 +64,7 @@ class Mastodon:
64 ### 64 ###
65 def __init__(self, client_id, client_secret = None, access_token = None, api_base_url = __DEFAULT_BASE_URL, debug_requests = False, ratelimit_method = "wait", ratelimit_pacefactor = 0.9): 65 def __init__(self, client_id, client_secret = None, access_token = None, api_base_url = __DEFAULT_BASE_URL, debug_requests = False, ratelimit_method = "wait", ratelimit_pacefactor = 0.9):
66 """ 66 """
67 Creates a new API wrapper instance based on the given client_secret and client_id. If you 67 Create a new API wrapper instance based on the given client_secret and client_id. If you
68 give a client_id and it is not a file, you must also give a secret. 68 give a client_id and it is not a file, you must also give a secret.
69 69
70 You can also directly specify an access_token, directly or as a file. 70 You can also directly specify an access_token, directly or as a file.
@@ -108,15 +108,15 @@ class Mastodon:
108 108
109 def log_in(self, username, password, scopes = ['read', 'write', 'follow'], to_file = None): 109 def log_in(self, username, password, scopes = ['read', 'write', 'follow'], to_file = None):
110 """ 110 """
111 Logs in and sets access_token to what was returned. Note that your 111 Log in and sets access_token to what was returned. Note that your
112 username is the e-mail you use to log in into mastodon. 112 username is the e-mail you use to log in into mastodon.
113 113
114 Can persist access token to file, to be used in the constructor. 114 Can persist access token to file, to be used in the constructor.
115 115
116 Will throw an exception if username / password are wrong, scopes are not 116 Will throw a MastodonIllegalArgumentError if username / password
117 valid or granted scopes differ from requested. 117 are wrong, scopes are not valid or granted scopes differ from requested.
118 118
119 Returns the access_token, as well. 119 Returns the access_token.
120 """ 120 """
121 params = self.__generate_params(locals()) 121 params = self.__generate_params(locals())
122 params['client_id'] = self.client_id 122 params['client_id'] = self.client_id
@@ -125,7 +125,7 @@ class Mastodon:
125 params['scope'] = " ".join(scopes) 125 params['scope'] = " ".join(scopes)
126 126
127 try: 127 try:
128 response = self.__api_request('POST', '/oauth/token', params) 128 response = self.__api_request('POST', '/oauth/token', params, do_ratelimiting = False)
129 self.access_token = response['access_token'] 129 self.access_token = response['access_token']
130 except: 130 except:
131 raise MastodonIllegalArgumentError('Invalid user name, password or scopes.') 131 raise MastodonIllegalArgumentError('Invalid user name, password or scopes.')
@@ -147,35 +147,45 @@ class Mastodon:
147 ## 147 ##
148 def timeline(self, timeline = "home", max_id = None, since_id = None, limit = None): 148 def timeline(self, timeline = "home", max_id = None, since_id = None, limit = None):
149 """ 149 """
150 Returns statuses, most recent ones first. Timeline can be home, mentions, public 150 Fetch statuses, most recent ones first. Timeline can be home, mentions, public
151 or tag/hashtag. See the following functions documentation for what those do. 151 or tag/hashtag. See the following functions documentation for what those do.
152 152
153 The default timeline is the "home" timeline. 153 The default timeline is the "home" timeline.
154
155 Returns a list of toot dicts.
154 """ 156 """
155 params = self.__generate_params(locals(), ['timeline']) 157 params = self.__generate_params(locals(), ['timeline'])
156 return self.__api_request('GET', '/api/v1/timelines/' + timeline, params) 158 return self.__api_request('GET', '/api/v1/timelines/' + timeline, params)
157 159
158 def timeline_home(self, max_id = None, since_id = None, limit = None): 160 def timeline_home(self, max_id = None, since_id = None, limit = None):
159 """ 161 """
160 Returns the authenticated users home timeline (i.e. followed users and self). 162 Fetch the authenticated users home timeline (i.e. followed users and self).
163
164 Returns a list of toot dicts.
161 """ 165 """
162 return self.timeline('home', max_id = max_id, since_id = since_id, limit = limit) 166 return self.timeline('home', max_id = max_id, since_id = since_id, limit = limit)
163 167
164 def timeline_mentions(self, max_id = None, since_id = None, limit = None): 168 def timeline_mentions(self, max_id = None, since_id = None, limit = None):
165 """ 169 """
166 Returns the authenticated users mentions. 170 Fetches the authenticated users mentions.
171
172 Returns a list of toot dicts.
167 """ 173 """
168 return self.timeline('mentions', max_id = max_id, since_id = since_id, limit = limit) 174 return self.timeline('mentions', max_id = max_id, since_id = since_id, limit = limit)
169 175
170 def timeline_public(self, max_id = None, since_id = None, limit = None): 176 def timeline_public(self, max_id = None, since_id = None, limit = None):
171 """ 177 """
172 Returns the public / visible-network timeline. 178 Fetches the public / visible-network timeline.
179
180 Returns a list of toot dicts.
173 """ 181 """
174 return self.timeline('public', max_id = max_id, since_id = since_id, limit = limit) 182 return self.timeline('public', max_id = max_id, since_id = since_id, limit = limit)
175 183
176 def timeline_hashtag(self, hashtag, max_id = None, since_id = None, limit = None): 184 def timeline_hashtag(self, hashtag, max_id = None, since_id = None, limit = None):
177 """ 185 """
178 Returns all toots with a given hashtag. 186 Fetch a timeline of toots with a given hashtag.
187
188 Returns a list of toot dicts.
179 """ 189 """
180 return self.timeline('tag/' + str(hashtag), max_id = max_id, since_id = since_id, limit = limit) 190 return self.timeline('tag/' + str(hashtag), max_id = max_id, since_id = since_id, limit = limit)
181 191
@@ -184,25 +194,33 @@ class Mastodon:
184 ### 194 ###
185 def status(self, id): 195 def status(self, id):
186 """ 196 """
187 Returns a status. 197 Fetch information about a single toot.
198
199 Returns a toot dict.
188 """ 200 """
189 return self.__api_request('GET', '/api/v1/statuses/' + str(id)) 201 return self.__api_request('GET', '/api/v1/statuses/' + str(id))
190 202
191 def status_context(self, id): 203 def status_context(self, id):
192 """ 204 """
193 Returns ancestors and descendants of the status. 205 Fetch information about ancestors and descendants of a toot.
206
207 Returns a context dict.
194 """ 208 """
195 return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/context') 209 return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/context')
196 210
197 def status_reblogged_by(self, id): 211 def status_reblogged_by(self, id):
198 """ 212 """
199 Returns a list of users that have reblogged a status. 213 Fetch a list of users that have reblogged a status.
214
215 Returns a list of user dicts.
200 """ 216 """
201 return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/reblogged_by') 217 return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/reblogged_by')
202 218
203 def status_favourited_by(self, id): 219 def status_favourited_by(self, id):
204 """ 220 """
205 Returns a list of users that have favourited a status. 221 Fetch a list of users that have favourited a status.
222
223 Returns a list of user dicts.
206 """ 224 """
207 return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/favourited_by') 225 return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/favourited_by')
208 226
@@ -211,8 +229,10 @@ class Mastodon:
211 ### 229 ###
212 def notifications(self): 230 def notifications(self):
213 """ 231 """
214 Returns notifications (mentions, favourites, reblogs, follows) for the authenticated 232 Fetch notifications (mentions, favourites, reblogs, follows) for the authenticated
215 user. 233 user.
234
235 Returns a list of notification dicts.
216 """ 236 """
217 return self.__api_request('GET', '/api/v1/notifications') 237 return self.__api_request('GET', '/api/v1/notifications')
218 238
@@ -221,53 +241,61 @@ class Mastodon:
221 ### 241 ###
222 def account(self, id): 242 def account(self, id):
223 """ 243 """
224 Returns account. 244 Fetch account information by user id.
245
246 Returns a user dict.
225 """ 247 """
226 return self.__api_request('GET', '/api/v1/accounts/' + str(id)) 248 return self.__api_request('GET', '/api/v1/accounts/' + str(id))
227 249
228 def account_verify_credentials(self): 250 def account_verify_credentials(self):
229 """ 251 """
230 Returns authenticated user's account. 252 Fetch authenticated user's account information.
253
254 Returns a user dict.
231 """ 255 """
232 return self.__api_request('GET', '/api/v1/accounts/verify_credentials') 256 return self.__api_request('GET', '/api/v1/accounts/verify_credentials')
233 257
234 def account_statuses(self, id, max_id = None, since_id = None, limit = None): 258 def account_statuses(self, id, max_id = None, since_id = None, limit = None):
235 """ 259 """
236 Returns statuses by user. Same options as timeline are permitted. 260 Fetch statuses by user id. Same options as timeline are permitted.
261
262 Returns a list of toot dicts.
237 """ 263 """
238 params = self.__generate_params(locals(), ['id']) 264 params = self.__generate_params(locals(), ['id'])
239 return self.__api_request('GET', '/api/v1/accounts/' + str(id) + '/statuses', params) 265 return self.__api_request('GET', '/api/v1/accounts/' + str(id) + '/statuses', params)
240 266
241 def account_following(self, id): 267 def account_following(self, id):
242 """ 268 """
243 Returns users the given user is following. 269 Fetch users the given user is following.
270
271 Returns a list of user dicts.
244 """ 272 """
245 return self.__api_request('GET', '/api/v1/accounts/' + str(id) + '/following') 273 return self.__api_request('GET', '/api/v1/accounts/' + str(id) + '/following')
246 274
247 def account_followers(self, id): 275 def account_followers(self, id):
248 """ 276 """
249 Returns users the given user is followed by. 277 Fetch users the given user is followed by.
278
279 Returns a list of user dicts.
250 """ 280 """
251 return self.__api_request('GET', '/api/v1/accounts/' + str(id) + '/followers') 281 return self.__api_request('GET', '/api/v1/accounts/' + str(id) + '/followers')
252 282
253 def account_relationships(self, id): 283 def account_relationships(self, id):
254 """ 284 """
255 Returns relationships (following, followed_by, blocking) of the logged in user to 285 Fetch relationships (following, followed_by, blocking) of the logged in user to
256 a given account. id can be a list. 286 a given account. id can be a list.
287
288 Returns a list of relationship dicts.
257 """ 289 """
258 params = self.__generate_params(locals()) 290 params = self.__generate_params(locals())
259 return self.__api_request('GET', '/api/v1/accounts/relationships', params) 291 return self.__api_request('GET', '/api/v1/accounts/relationships', params)
260
261 def account_suggestions(self):
262 """
263 Returns accounts that the system suggests the authenticated user to follow.
264 """
265 return self.__api_request('GET', '/api/v1/accounts/suggestions')
266 292
267 def account_search(self, q, limit = None): 293 def account_search(self, q, limit = None):
268 """ 294 """
269 Returns matching accounts. Will lookup an account remotely if the search term is 295 Fetch matching accounts. Will lookup an account remotely if the search term is
270 in the username@domain format and not yet in the database. 296 in the username@domain format and not yet in the database.
297
298 Returns a list of user dicts.
271 """ 299 """
272 params = self.__generate_params(locals()) 300 params = self.__generate_params(locals())
273 return self.__api_request('GET', '/api/v1/accounts/search', params) 301 return self.__api_request('GET', '/api/v1/accounts/search', params)
@@ -277,10 +305,10 @@ class Mastodon:
277 ### 305 ###
278 def status_post(self, status, in_reply_to_id = None, media_ids = None): 306 def status_post(self, status, in_reply_to_id = None, media_ids = None):
279 """ 307 """
280 Posts a status. Can optionally be in reply to another status and contain 308 Post a status. Can optionally be in reply to another status and contain
281 up to four pieces of media (Uploaded via media_post()). 309 up to four pieces of media (Uploaded via media_post()).
282 310
283 Returns the new status. 311 Returns a toot dict with the new status.
284 """ 312 """
285 params = self.__generate_params(locals()) 313 params = self.__generate_params(locals())
286 return self.__api_request('POST', '/api/v1/statuses', params) 314 return self.__api_request('POST', '/api/v1/statuses', params)
@@ -288,41 +316,46 @@ class Mastodon:
288 def toot(self, status): 316 def toot(self, status):
289 """ 317 """
290 Synonym for status_post that only takes the status text as input. 318 Synonym for status_post that only takes the status text as input.
319
320 Returns a toot dict with the new status.
291 """ 321 """
292 return self.status_post(status) 322 return self.status_post(status)
293 323
294 def status_delete(self, id): 324 def status_delete(self, id):
295 """ 325 """
296 Deletes a status 326 Delete a status
327
328 Returns an empty dict for good measure.
297 """ 329 """
298 return self.__api_request('DELETE', '/api/v1/statuses/' + str(id)) 330 return self.__api_request('DELETE', '/api/v1/statuses/' + str(id))
299 331
300 def status_reblog(self, id): 332 def status_reblog(self, id):
301 """Reblogs a status. 333 """Reblog a status.
302 334
303 Returns a new status that wraps around the reblogged one.""" 335 Returns a toot with with a new status that wraps around the reblogged one.
336 """
304 return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/reblog") 337 return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/reblog")
305 338
306 def status_unreblog(self, id): 339 def status_unreblog(self, id):
307 """ 340 """
308 Un-reblogs a status. 341 Un-reblog a status.
309 342
310 Returns the status that used to be reblogged. 343 Returns a toot dict with the status that used to be reblogged.
311 """ 344 """
312 return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/unreblog") 345 return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/unreblog")
313 346
314 def status_favourite(self, id): 347 def status_favourite(self, id):
315 """ 348 """
316 Favourites a status. 349 Favourite a status.
317 350
318 Returns the favourited status. 351 Returns a toot dict with the favourited status.
319 """ 352 """
320 return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/favourite") 353 return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/favourite")
321 354
322 def status_unfavourite(self, id): 355 def status_unfavourite(self, id):
323 """Favourites a status. 356 """Favourite a status.
324 357
325 Returns the un-favourited status. 358 Returns a toot dict with the un-favourited status.
326 """ 359 """
327 return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/unfavourite") 360 return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/unfavourite")
328 361
@@ -331,33 +364,33 @@ class Mastodon:
331 ### 364 ###
332 def account_follow(self, id): 365 def account_follow(self, id):
333 """ 366 """
334 Follows a user. 367 Follow a user.
335 368
336 Returns the updated relationship to the user. 369 Returns a relationship dict containing the updated relationship to the user.
337 """ 370 """
338 return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/follow") 371 return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/follow")
339 372
340 def account_unfollow(self, id): 373 def account_unfollow(self, id):
341 """ 374 """
342 Unfollows a user. 375 Unfollow a user.
343 376
344 Returns the updated relationship to the user. 377 Returns a relationship dict containing the updated relationship to the user.
345 """ 378 """
346 return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/unfollow") 379 return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/unfollow")
347 380
348 def account_block(self, id): 381 def account_block(self, id):
349 """ 382 """
350 Blocks a user. 383 Block a user.
351 384
352 Returns the updated relationship to the user. 385 Returns a relationship dict containing the updated relationship to the user.
353 """ 386 """
354 return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/block") 387 return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/block")
355 388
356 def account_unblock(self, id): 389 def account_unblock(self, id):
357 """ 390 """
358 Unblocks a user. 391 Unblock a user.
359 392
360 Returns the updated relationship to the user. 393 Returns a relationship dict containing the updated relationship to the user.
361 """ 394 """
362 return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/unblock") 395 return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/unblock")
363 396
@@ -366,20 +399,18 @@ class Mastodon:
366 ### 399 ###
367 def media_post(self, media_file, mime_type = None): 400 def media_post(self, media_file, mime_type = None):
368 """ 401 """
369 Posts an image. media_file can either be image data or 402 Post an image. media_file can either be image data or
370 a file name. If image data is passed directly, the mime 403 a file name. If image data is passed directly, the mime
371 type has to be specified manually, otherwise, it is 404 type has to be specified manually, otherwise, it is
372 determined from the file name. 405 determined from the file name.
373 406
374 Returns the uploaded media metadata object. Importantly, this contains
375 the ID that can then be used in status_post() to attach the media to
376 a toot.
377
378 Throws a MastodonIllegalArgumentError if the mime type of the 407 Throws a MastodonIllegalArgumentError if the mime type of the
379 passed data or file can not be determined properly. 408 passed data or file can not be determined properly.
409
410 Returns a media dict. This contains the id that can be used in
411 status_post to attach the media file to a toot.
380 """ 412 """
381 413 if os.path.isfile(media_file) and mime_type == None:
382 if os.path.isfile(media_file):
383 mime_type = mimetypes.guess_type(media_file)[0] 414 mime_type = mimetypes.guess_type(media_file)[0]
384 media_file = open(media_file, 'rb') 415 media_file = open(media_file, 'rb')
385 416
@@ -395,7 +426,7 @@ class Mastodon:
395 ### 426 ###
396 # Internal helpers, dragons probably 427 # Internal helpers, dragons probably
397 ### 428 ###
398 def __api_request(self, method, endpoint, params = {}, files = {}): 429 def __api_request(self, method, endpoint, params = {}, files = {}, do_ratelimiting = True):
399 """ 430 """
400 Internal API request helper. 431 Internal API request helper.
401 432
@@ -410,7 +441,7 @@ class Mastodon:
410 441
411 # "pace" mode ratelimiting: Assume constant rate of requests, sleep a little less long than it 442 # "pace" mode ratelimiting: Assume constant rate of requests, sleep a little less long than it
412 # would take to not hit the rate limit at that request rate. 443 # would take to not hit the rate limit at that request rate.
413 if self.ratelimit_method == "pace": 444 if do_ratelimiting and self.ratelimit_method == "pace":
414 if self.ratelimit_remaining == 0: 445 if self.ratelimit_remaining == 0:
415 to_next = self.ratelimit_reset - time.time() 446 to_next = self.ratelimit_reset - time.time()
416 if to_next > 0: 447 if to_next > 0:
@@ -472,20 +503,21 @@ class Mastodon:
472 raise MastodonAPIError("Could not parse response as JSON, respose code was " + str(response_object.status_code)) 503 raise MastodonAPIError("Could not parse response as JSON, respose code was " + str(response_object.status_code))
473 504
474 # Handle rate limiting 505 # Handle rate limiting
475 self.ratelimit_remaining = int(response_object.headers['X-RateLimit-Remaining']) 506 if 'X-RateLimit-Remaining' in response_object.headers and do_ratelimiting:
476 self.ratelimit_limit = int(response_object.headers['X-RateLimit-Limit']) 507 self.ratelimit_remaining = int(response_object.headers['X-RateLimit-Remaining'])
477 self.ratelimit_reset = (datetime.strptime(response_object.headers['X-RateLimit-Reset'], "%Y-%m-%dT%H:%M:%S.%fZ") - datetime(1970, 1, 1)).total_seconds() 508 self.ratelimit_limit = int(response_object.headers['X-RateLimit-Limit'])
478 self.ratelimit_lastcall = time.time() 509 self.ratelimit_reset = (datetime.strptime(response_object.headers['X-RateLimit-Reset'], "%Y-%m-%dT%H:%M:%S.%fZ") - datetime(1970, 1, 1)).total_seconds()
479 510 self.ratelimit_lastcall = time.time()
480 if "error" in response and response["error"] == "Throttled": 511
481 if self.ratelimit_method == "throw": 512 if "error" in response and response["error"] == "Throttled":
482 raise MastodonRatelimitError("Hit rate limit.") 513 if self.ratelimit_method == "throw":
483 514 raise MastodonRatelimitError("Hit rate limit.")
484 if self.ratelimit_method == "wait" or self.ratelimit_method == "pace": 515
485 to_next = self.ratelimit_reset - time.time() 516 if self.ratelimit_method == "wait" or self.ratelimit_method == "pace":
486 if to_next > 0: 517 to_next = self.ratelimit_reset - time.time()
487 time.sleep(to_next) 518 if to_next > 0:
488 request_complete = False 519 time.sleep(to_next)
520 request_complete = False
489 521
490 return response 522 return response
491 523
Powered by cgit v1.2.3 (git 2.41.0)