2016-11-23 23:30:51 +01:00
# coding: utf-8
import requests
import os
import os . path
2016-11-24 12:34:31 +01:00
import mimetypes
import time
import random
import string
2016-11-25 23:14:00 +01:00
import pytz
import datetime
import dateutil
import dateutil . parser
2016-11-23 23:30:51 +01:00
class Mastodon :
2017-01-23 21:16:43 +01:00
"""
Super basic but thorough and easy to use mastodon . social
2016-11-24 00:36:00 +01:00
api wrapper in python .
2017-01-23 21:16:43 +01:00
2016-11-24 00:36:00 +01:00
If anything is unclear , check the official API docs at
https : / / github . com / Gargron / mastodon / wiki / API
2017-01-23 21:16:43 +01:00
2016-11-24 00:36:00 +01:00
Presently , only username - password login is supported , somebody please
patch in Real Proper OAuth if desired .
2016-11-23 23:30:51 +01:00
"""
__DEFAULT_BASE_URL = ' https://mastodon.social '
2016-12-13 17:17:33 +01:00
__DEFAULT_TIMEOUT = 300
2017-01-23 21:16:43 +01:00
2016-11-23 23:30:51 +01:00
###
# Registering apps
###
2017-01-23 21:16:43 +01:00
@staticmethod
2017-03-03 17:10:04 +01:00
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 ) :
2016-11-24 00:36:00 +01:00
"""
2016-11-25 20:33:00 +01:00
Create a new app with given client_name and scopes ( read , write , follow )
2017-01-23 21:16:43 +01:00
2016-11-24 00:36:00 +01:00
Specify redirect_uris if you want users to be redirected to a certain page after authenticating .
Specify to_file to persist your apps info to a file so you can use them in the constructor .
Specify api_base_url if you want to register an app on an instance different from the flagship one .
2017-01-23 21:16:43 +01:00
2016-11-25 18:17:39 +01:00
Presently , app registration is open by default , but this is not guaranteed to be the case for all
future mastodon instances or even the flagship instance in the future .
2017-01-23 21:16:43 +01:00
2016-11-24 00:36:00 +01:00
Returns client_id and client_secret .
2016-11-23 23:30:51 +01:00
"""
request_data = {
' client_name ' : client_name ,
' scopes ' : " " . join ( scopes )
}
2017-01-23 21:16:43 +01:00
2016-12-13 17:17:33 +01:00
try :
if redirect_uris != None :
request_data [ ' redirect_uris ' ] = redirect_uris ;
else :
request_data [ ' redirect_uris ' ] = ' urn:ietf:wg:oauth:2.0:oob ' ;
2017-01-23 21:16:43 +01:00
2017-03-03 17:23:11 +01:00
response = requests . post ( api_base_url + ' /api/v1/apps ' , data = request_data , timeout = request_timeout ) . json ( )
2017-04-01 14:13:46 +02:00
except Exception as e :
import traceback
traceback . print_exc ( )
raise MastodonNetworkError ( " Could not complete request: %s " % e )
2017-01-23 21:16:43 +01:00
2016-11-23 23:30:51 +01:00
if to_file != None :
with open ( to_file , ' w ' ) as secret_file :
secret_file . write ( response [ ' client_id ' ] + ' \n ' )
secret_file . write ( response [ ' client_secret ' ] + ' \n ' )
2017-01-23 21:16:43 +01:00
2016-11-23 23:30:51 +01:00
return ( response [ ' client_id ' ] , response [ ' client_secret ' ] )
2017-01-23 21:16:43 +01:00
2016-11-23 23:30:51 +01:00
###
# Authentication, including constructor
###
2016-12-13 17:17:33 +01:00
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 = 1.1 , request_timeout = __DEFAULT_TIMEOUT ) :
2016-11-24 00:36:00 +01:00
"""
2016-11-25 20:33:00 +01:00
Create a new API wrapper instance based on the given client_secret and client_id . If you
2016-11-24 00:36:00 +01:00
give a client_id and it is not a file , you must also give a secret .
2017-01-23 21:16:43 +01:00
2016-11-25 23:14:00 +01:00
You can also specify an access_token , directly or as a file ( as written by log_in ) .
2017-01-23 21:16:43 +01:00
2016-11-25 18:17:39 +01:00
Mastodon . py can try to respect rate limits in several ways , controlled by ratelimit_method .
" throw " makes functions throw a MastodonRatelimitError when the rate
limit is hit . " wait " mode will , once the limit is hit , wait and retry the request as soon
as the rate limit resets , until it succeeds . " pace " works like throw , but tries to wait in
2017-01-23 21:16:43 +01:00
between calls so that the limit is generally not hit ( How hard it tries to not hit the rate
2016-11-25 18:17:39 +01:00
limit can be controlled by ratelimit_pacefactor ) . The default setting is " wait " . Note that
even in " wait " and " pace " mode , requests can still fail due to network or other problems ! Also
note that " pace " and " wait " are NOT thread safe .
2017-01-23 21:16:43 +01:00
2016-11-24 00:36:00 +01:00
Specify api_base_url if you wish to talk to an instance other than the flagship one .
2016-12-13 17:17:33 +01:00
If a file is given as client_id , read client ID and secret from that file .
2017-01-23 21:16:43 +01:00
2016-12-13 17:17:33 +01:00
By defautl , a timeout of 300 seconds is used for all requests . If you wish to change this ,
pass the desired timeout ( in seconds ) as request_timeout .
2016-11-23 23:30:51 +01:00
"""
self . api_base_url = api_base_url
2017-01-23 21:16:43 +01:00
self . client_id = client_id
2016-11-23 23:30:51 +01:00
self . client_secret = client_secret
self . access_token = access_token
2016-11-24 20:07:00 +01:00
self . debug_requests = debug_requests
2016-11-25 18:17:39 +01:00
self . ratelimit_method = ratelimit_method
2017-01-23 21:16:43 +01:00
2016-11-25 18:17:39 +01:00
self . ratelimit_limit = 150
self . ratelimit_reset = time . time ( )
self . ratelimit_remaining = 150
self . ratelimit_lastcall = time . time ( )
2016-11-25 23:14:00 +01:00
self . ratelimit_pacefactor = ratelimit_pacefactor
2017-01-23 21:16:43 +01:00
2016-12-13 17:17:33 +01:00
self . request_timeout = request_timeout
2017-01-23 21:16:43 +01:00
2016-11-25 23:28:30 +01:00
if not ratelimit_method in [ " throw " , " wait " , " pace " ] :
raise MastodonIllegalArgumentError ( " Invalid ratelimit method. " )
2017-01-23 21:16:43 +01:00
2016-11-23 23:30:51 +01:00
if os . path . isfile ( self . client_id ) :
with open ( self . client_id , ' r ' ) as secret_file :
self . client_id = secret_file . readline ( ) . rstrip ( )
self . client_secret = secret_file . readline ( ) . rstrip ( )
else :
if self . client_secret == None :
2016-11-25 18:17:39 +01:00
raise MastodonIllegalArgumentError ( ' Specified client id directly, but did not supply secret ' )
2017-01-23 21:16:43 +01:00
2016-11-23 23:30:51 +01:00
if self . access_token != None and os . path . isfile ( self . access_token ) :
with open ( self . access_token , ' r ' ) as token_file :
self . access_token = token_file . readline ( ) . rstrip ( )
2017-01-23 21:16:43 +01:00
2016-11-23 23:30:51 +01:00
def log_in ( self , username , password , scopes = [ ' read ' , ' write ' , ' follow ' ] , to_file = None ) :
2016-11-24 00:36:00 +01:00
"""
2016-11-25 20:57:53 +01:00
Log in and sets access_token to what was returned . Note that your
2016-11-25 18:17:39 +01:00
username is the e - mail you use to log in into mastodon .
2017-01-23 21:16:43 +01:00
2016-11-25 18:17:39 +01:00
Can persist access token to file , to be used in the constructor .
2017-01-23 21:16:43 +01:00
Will throw a MastodonIllegalArgumentError if username / password
2016-11-25 20:57:53 +01:00
are wrong , scopes are not valid or granted scopes differ from requested .
2017-01-23 21:16:43 +01:00
2016-11-25 20:57:53 +01:00
Returns the access_token .
2016-11-23 23:30:51 +01:00
"""
params = self . __generate_params ( locals ( ) )
params [ ' client_id ' ] = self . client_id
params [ ' client_secret ' ] = self . client_secret
params [ ' grant_type ' ] = ' password '
params [ ' scope ' ] = " " . join ( scopes )
2017-01-23 21:16:43 +01:00
2016-11-24 12:03:42 +01:00
try :
2017-01-23 21:16:43 +01:00
response = self . __api_request ( ' POST ' , ' /oauth/token ' , params , do_ratelimiting = False )
2016-11-24 12:03:42 +01:00
self . access_token = response [ ' access_token ' ]
2017-04-01 14:13:46 +02:00
except Exception as e :
import traceback
traceback . print_exc ( )
raise MastodonIllegalArgumentError ( ' Invalid user name, password or scopes: %s ' % e )
2017-01-23 21:16:43 +01:00
2016-11-24 12:07:41 +01:00
requested_scopes = " " . join ( sorted ( scopes ) )
received_scopes = " " . join ( sorted ( response [ " scope " ] . split ( " " ) ) )
2017-01-23 21:16:43 +01:00
2016-11-24 12:03:42 +01:00
if requested_scopes != received_scopes :
2016-11-25 18:17:39 +01:00
raise MastodonAPIError ( ' Granted scopes " ' + received_scopes + ' " differ from requested scopes " ' + requested_scopes + ' " . ' )
2017-01-23 21:16:43 +01:00
2016-11-23 23:30:51 +01:00
if to_file != None :
with open ( to_file , ' w ' ) as token_file :
token_file . write ( response [ ' access_token ' ] + ' \n ' )
2017-01-23 21:16:43 +01:00
2016-11-23 23:30:51 +01:00
return response [ ' access_token ' ]
2017-01-23 21:16:43 +01:00
2016-11-23 23:30:51 +01:00
###
# Reading data: Timelines
##
2016-11-25 15:39:53 +01:00
def timeline ( self , timeline = " home " , max_id = None , since_id = None , limit = None ) :
2016-11-24 00:36:00 +01:00
"""
2016-11-25 20:33:00 +01:00
Fetch statuses , most recent ones first . Timeline can be home , mentions , public
2016-11-25 15:39:53 +01:00
or tag / hashtag . See the following functions documentation for what those do .
2017-01-23 21:16:43 +01:00
2016-11-25 15:39:53 +01:00
The default timeline is the " home " timeline .
2016-11-25 20:33:00 +01:00
Returns a list of toot dicts .
2016-11-24 00:55:09 +01:00
"""
2016-11-23 23:30:51 +01:00
params = self . __generate_params ( locals ( ) , [ ' timeline ' ] )
return self . __api_request ( ' GET ' , ' /api/v1/timelines/ ' + timeline , params )
2017-01-23 21:16:43 +01:00
2016-11-25 15:39:53 +01:00
def timeline_home ( self , max_id = None , since_id = None , limit = None ) :
"""
2016-11-25 20:33:00 +01:00
Fetch the authenticated users home timeline ( i . e . followed users and self ) .
Returns a list of toot dicts .
2016-11-25 15:39:53 +01:00
"""
return self . timeline ( ' home ' , max_id = max_id , since_id = since_id , limit = limit )
2017-01-23 21:16:43 +01:00
2016-11-25 15:39:53 +01:00
def timeline_mentions ( self , max_id = None , since_id = None , limit = None ) :
"""
2016-11-25 20:33:00 +01:00
Fetches the authenticated users mentions .
Returns a list of toot dicts .
2016-11-25 15:39:53 +01:00
"""
return self . timeline ( ' mentions ' , max_id = max_id , since_id = since_id , limit = limit )
2017-01-23 21:16:43 +01:00
2016-11-25 15:39:53 +01:00
def timeline_public ( self , max_id = None , since_id = None , limit = None ) :
"""
2016-11-25 20:33:00 +01:00
Fetches the public / visible - network timeline .
Returns a list of toot dicts .
2016-11-25 15:39:53 +01:00
"""
return self . timeline ( ' public ' , max_id = max_id , since_id = since_id , limit = limit )
2017-01-23 21:16:43 +01:00
2016-11-25 15:39:53 +01:00
def timeline_hashtag ( self , hashtag , max_id = None , since_id = None , limit = None ) :
"""
2016-11-25 20:33:00 +01:00
Fetch a timeline of toots with a given hashtag .
Returns a list of toot dicts .
2016-11-25 15:39:53 +01:00
"""
return self . timeline ( ' tag/ ' + str ( hashtag ) , max_id = max_id , since_id = since_id , limit = limit )
2017-01-23 21:16:43 +01:00
2016-11-23 23:30:51 +01:00
###
# Reading data: Statuses
###
def status ( self , id ) :
2016-11-24 00:36:00 +01:00
"""
2016-11-25 20:33:00 +01:00
Fetch information about a single toot .
Returns a toot dict .
2016-11-24 00:55:09 +01:00
"""
2016-11-23 23:30:51 +01:00
return self . __api_request ( ' GET ' , ' /api/v1/statuses/ ' + str ( id ) )
def status_context ( self , id ) :
2016-11-24 00:36:00 +01:00
"""
2016-11-25 20:33:00 +01:00
Fetch information about ancestors and descendants of a toot .
Returns a context dict .
2016-11-24 00:55:09 +01:00
"""
2016-11-23 23:30:51 +01:00
return self . __api_request ( ' GET ' , ' /api/v1/statuses/ ' + str ( id ) + ' /context ' )
2017-01-23 21:16:43 +01:00
2016-11-23 23:30:51 +01:00
def status_reblogged_by ( self , id ) :
2016-11-24 00:36:00 +01:00
"""
2016-11-25 20:33:00 +01:00
Fetch a list of users that have reblogged a status .
Returns a list of user dicts .
2016-11-24 00:55:09 +01:00
"""
2016-11-23 23:30:51 +01:00
return self . __api_request ( ' GET ' , ' /api/v1/statuses/ ' + str ( id ) + ' /reblogged_by ' )
2017-01-23 21:16:43 +01:00
2016-11-23 23:30:51 +01:00
def status_favourited_by ( self , id ) :
2016-11-24 00:36:00 +01:00
"""
2016-11-25 20:33:00 +01:00
Fetch a list of users that have favourited a status .
Returns a list of user dicts .
2016-11-24 00:55:09 +01:00
"""
2016-11-23 23:30:51 +01:00
return self . __api_request ( ' GET ' , ' /api/v1/statuses/ ' + str ( id ) + ' /favourited_by ' )
2017-01-23 21:16:43 +01:00
2016-11-25 15:39:53 +01:00
###
# Reading data: Notifications
###
def notifications ( self ) :
"""
2016-11-25 20:33:00 +01:00
Fetch notifications ( mentions , favourites , reblogs , follows ) for the authenticated
2016-11-25 15:39:53 +01:00
user .
2016-11-25 20:33:00 +01:00
2016-11-25 20:46:20 +01:00
Returns a list of notification dicts .
2016-11-25 15:39:53 +01:00
"""
return self . __api_request ( ' GET ' , ' /api/v1/notifications ' )
2017-01-23 21:16:43 +01:00
2016-11-23 23:30:51 +01:00
###
# Reading data: Accounts
###
def account ( self , id ) :
2016-11-24 00:36:00 +01:00
"""
2016-11-25 20:33:00 +01:00
Fetch account information by user id .
Returns a user dict .
2016-11-24 00:55:09 +01:00
"""
2016-11-23 23:30:51 +01:00
return self . __api_request ( ' GET ' , ' /api/v1/accounts/ ' + str ( id ) )
def account_verify_credentials ( self ) :
2016-11-24 00:36:00 +01:00
"""
2016-11-25 20:33:00 +01:00
Fetch authenticated user ' s account information.
Returns a user dict .
2016-11-24 00:55:09 +01:00
"""
2016-11-23 23:30:51 +01:00
return self . __api_request ( ' GET ' , ' /api/v1/accounts/verify_credentials ' )
2017-01-23 21:16:43 +01:00
2016-11-23 23:30:51 +01:00
def account_statuses ( self , id , max_id = None , since_id = None , limit = None ) :
2016-11-24 00:36:00 +01:00
"""
2016-11-25 20:33:00 +01:00
Fetch statuses by user id . Same options as timeline are permitted .
Returns a list of toot dicts .
2016-11-24 00:55:09 +01:00
"""
2016-11-23 23:30:51 +01:00
params = self . __generate_params ( locals ( ) , [ ' id ' ] )
2016-11-24 20:03:08 +01:00
return self . __api_request ( ' GET ' , ' /api/v1/accounts/ ' + str ( id ) + ' /statuses ' , params )
2016-11-23 23:30:51 +01:00
def account_following ( self , id ) :
2016-11-24 00:36:00 +01:00
"""
2016-11-25 20:33:00 +01:00
Fetch users the given user is following .
Returns a list of user dicts .
2016-11-24 00:55:09 +01:00
"""
2016-11-23 23:30:51 +01:00
return self . __api_request ( ' GET ' , ' /api/v1/accounts/ ' + str ( id ) + ' /following ' )
def account_followers ( self , id ) :
2016-11-24 00:36:00 +01:00
"""
2016-11-25 20:33:00 +01:00
Fetch users the given user is followed by .
Returns a list of user dicts .
2016-11-24 00:55:09 +01:00
"""
2016-11-23 23:30:51 +01:00
return self . __api_request ( ' GET ' , ' /api/v1/accounts/ ' + str ( id ) + ' /followers ' )
def account_relationships ( self , id ) :
2016-11-24 00:36:00 +01:00
"""
2017-01-23 21:16:43 +01:00
Fetch relationships ( following , followed_by , blocking ) of the logged in user to
2016-11-24 00:36:00 +01:00
a given account . id can be a list .
2016-11-25 20:33:00 +01:00
Returns a list of relationship dicts .
2016-11-24 00:55:09 +01:00
"""
2016-11-23 23:30:51 +01:00
params = self . __generate_params ( locals ( ) )
return self . __api_request ( ' GET ' , ' /api/v1/accounts/relationships ' , params )
def account_search ( self , q , limit = None ) :
2016-11-24 00:36:00 +01:00
"""
2017-01-23 21:16:43 +01:00
Fetch matching accounts . Will lookup an account remotely if the search term is
2016-11-24 00:36:00 +01:00
in the username @domain format and not yet in the database .
2016-11-25 20:33:00 +01:00
Returns a list of user dicts .
2016-11-24 00:55:09 +01:00
"""
2016-11-23 23:30:51 +01:00
params = self . __generate_params ( locals ( ) )
return self . __api_request ( ' GET ' , ' /api/v1/accounts/search ' , params )
2017-01-23 21:16:43 +01:00
2016-11-23 23:30:51 +01:00
###
# Writing data: Statuses
###
2017-01-23 21:16:43 +01:00
def status_post ( self , status , in_reply_to_id = None , media_ids = None , sensitive = False , visibility = ' ' ) :
2016-11-24 00:36:00 +01:00
"""
2016-11-25 20:33:00 +01:00
Post a status . Can optionally be in reply to another status and contain
2016-11-26 00:03:19 +01:00
up to four pieces of media ( Uploaded via media_post ( ) ) . media_ids can
also be the media dicts returned by media_post - they are unpacked
automatically .
2017-01-23 21:16:43 +01:00
The ' sensitive ' boolean decides whether or not media attached to the post
should be marked as sensitive , which hides it by default on the Mastodon
web front - end .
The visibility parameter is a string value and matches the visibility
option on the / api / v1 / status POST API endpoint . It accepts any of :
' private ' - post will be visible only to followers
' unlisted ' - post will be public but not appear on the public timeline
' public ' - post will be public
If not passed in , visibility defaults to match the current account ' s
privacy setting ( private if the account is locked , public otherwise ) .
2016-11-25 20:33:00 +01:00
Returns a toot dict with the new status .
2016-11-24 00:55:09 +01:00
"""
2016-11-26 00:03:19 +01:00
params_initial = locals ( )
2017-01-23 21:16:43 +01:00
# Validate visibility parameter
valid_visibilities = [ ' private ' , ' public ' , ' unlisted ' , ' ' ]
if params_initial [ ' visibility ' ] . lower ( ) not in valid_visibilities :
raise ValueError ( ' Invalid visibility value! Acceptable values are %s ' % valid_visibilities )
2016-11-26 00:03:19 +01:00
if media_ids != None :
try :
media_ids_proper = [ ]
for media_id in media_ids :
if isinstance ( media_id , dict ) :
media_ids_proper . append ( media_id [ " id " ] )
else :
media_ids_proper . append ( media_id )
2017-04-01 14:13:46 +02:00
except Exception as e :
import traceback
traceback . print_exc ( )
raise MastodonIllegalArgumentError ( " Invalid media dict: %s " % e )
2017-01-23 21:16:43 +01:00
2016-11-26 00:03:19 +01:00
params_initial [ " media_ids " ] = media_ids_proper
2017-01-23 10:57:13 +01:00
params = self . __generate_params ( params_initial )
2016-11-23 23:30:51 +01:00
return self . __api_request ( ' POST ' , ' /api/v1/statuses ' , params )
2017-01-23 21:16:43 +01:00
def toot ( self , status ) :
2016-11-24 00:36:00 +01:00
"""
2016-11-24 00:55:09 +01:00
Synonym for status_post that only takes the status text as input .
2016-11-25 20:33:00 +01:00
Returns a toot dict with the new status .
2016-11-24 00:55:09 +01:00
"""
2017-01-23 21:16:43 +01:00
return self . status_post ( status )
2016-11-23 23:30:51 +01:00
def status_delete ( self , id ) :
2016-11-24 00:36:00 +01:00
"""
2016-11-25 20:33:00 +01:00
Delete a status
Returns an empty dict for good measure .
2016-11-24 00:55:09 +01:00
"""
2016-11-23 23:30:51 +01:00
return self . __api_request ( ' DELETE ' , ' /api/v1/statuses/ ' + str ( id ) )
def status_reblog ( self , id ) :
2016-11-25 20:33:00 +01:00
""" Reblog a status.
2017-01-23 21:16:43 +01:00
2016-11-25 20:33:00 +01:00
Returns a toot with with a new status that wraps around the reblogged one .
"""
2016-11-23 23:30:51 +01:00
return self . __api_request ( ' POST ' , ' /api/v1/statuses/ ' + str ( id ) + " /reblog " )
def status_unreblog ( self , id ) :
2016-11-24 00:36:00 +01:00
"""
2016-11-25 20:33:00 +01:00
Un - reblog a status .
2017-01-23 21:16:43 +01:00
2016-11-25 20:33:00 +01:00
Returns a toot dict with the status that used to be reblogged .
2016-11-24 00:55:09 +01:00
"""
2016-11-23 23:30:51 +01:00
return self . __api_request ( ' POST ' , ' /api/v1/statuses/ ' + str ( id ) + " /unreblog " )
def status_favourite ( self , id ) :
2016-11-24 00:36:00 +01:00
"""
2016-11-25 20:33:00 +01:00
Favourite a status .
2017-01-23 21:16:43 +01:00
2016-11-25 20:33:00 +01:00
Returns a toot dict with the favourited status .
2016-11-24 00:55:09 +01:00
"""
2016-11-23 23:30:51 +01:00
return self . __api_request ( ' POST ' , ' /api/v1/statuses/ ' + str ( id ) + " /favourite " )
2017-01-23 21:16:43 +01:00
2016-11-23 23:30:51 +01:00
def status_unfavourite ( self , id ) :
2016-11-25 20:33:00 +01:00
""" Favourite a status.
2017-01-23 21:16:43 +01:00
2016-11-25 20:33:00 +01:00
Returns a toot dict with the un - favourited status .
2016-11-24 00:55:09 +01:00
"""
2016-11-23 23:30:51 +01:00
return self . __api_request ( ' POST ' , ' /api/v1/statuses/ ' + str ( id ) + " /unfavourite " )
2017-01-23 21:16:43 +01:00
2016-11-23 23:30:51 +01:00
###
2016-11-24 03:03:52 +01:00
# Writing data: Accounts
2016-11-23 23:30:51 +01:00
###
def account_follow ( self , id ) :
2016-11-24 00:36:00 +01:00
"""
2016-11-25 20:33:00 +01:00
Follow a user .
2017-01-23 21:16:43 +01:00
2016-11-25 20:33:00 +01:00
Returns a relationship dict containing the updated relationship to the user .
2016-11-24 00:55:09 +01:00
"""
2016-11-23 23:30:51 +01:00
return self . __api_request ( ' POST ' , ' /api/v1/accounts/ ' + str ( id ) + " /follow " )
2017-01-23 21:16:43 +01:00
2016-11-23 23:30:51 +01:00
def account_unfollow ( self , id ) :
2016-11-24 00:36:00 +01:00
"""
2016-11-25 20:33:00 +01:00
Unfollow a user .
2017-01-23 21:16:43 +01:00
2016-11-25 20:33:00 +01:00
Returns a relationship dict containing the updated relationship to the user .
2016-11-24 00:55:09 +01:00
"""
2016-11-23 23:30:51 +01:00
return self . __api_request ( ' POST ' , ' /api/v1/accounts/ ' + str ( id ) + " /unfollow " )
2017-01-23 21:16:43 +01:00
2016-11-23 23:30:51 +01:00
def account_block ( self , id ) :
2016-11-24 00:36:00 +01:00
"""
2016-11-25 20:33:00 +01:00
Block a user .
2017-01-23 21:16:43 +01:00
2016-11-25 20:33:00 +01:00
Returns a relationship dict containing the updated relationship to the user .
2016-11-24 00:55:09 +01:00
"""
2016-11-23 23:30:51 +01:00
return self . __api_request ( ' POST ' , ' /api/v1/accounts/ ' + str ( id ) + " /block " )
2017-01-23 21:16:43 +01:00
2016-11-23 23:30:51 +01:00
def account_unblock ( self , id ) :
2016-11-24 00:36:00 +01:00
"""
2016-11-25 20:33:00 +01:00
Unblock a user .
2017-01-23 21:16:43 +01:00
2016-11-25 20:33:00 +01:00
Returns a relationship dict containing the updated relationship to the user .
2016-11-24 00:55:09 +01:00
"""
2016-11-23 23:30:51 +01:00
return self . __api_request ( ' POST ' , ' /api/v1/accounts/ ' + str ( id ) + " /unblock " )
###
# Writing data: Media
###
2016-11-24 12:34:31 +01:00
def media_post ( self , media_file , mime_type = None ) :
2016-11-24 00:36:00 +01:00
"""
2016-11-25 20:33:00 +01:00
Post an image . media_file can either be image data or
2016-11-24 12:34:31 +01:00
a file name . If image data is passed directly , the mime
type has to be specified manually , otherwise , it is
determined from the file name .
2017-01-23 21:16:43 +01:00
Throws a MastodonIllegalArgumentError if the mime type of the
2016-11-25 18:17:39 +01:00
passed data or file can not be determined properly .
2016-11-25 20:33:00 +01:00
Returns a media dict . This contains the id that can be used in
status_post to attach the media file to a toot .
2016-11-24 00:55:09 +01:00
"""
2016-11-25 20:33:00 +01:00
if os . path . isfile ( media_file ) and mime_type == None :
2016-11-24 12:34:31 +01:00
mime_type = mimetypes . guess_type ( media_file ) [ 0 ]
2016-11-23 23:30:51 +01:00
media_file = open ( media_file , ' rb ' )
2017-01-23 21:16:43 +01:00
2016-11-24 12:34:31 +01:00
if mime_type == None :
2016-11-25 18:17:39 +01:00
raise MastodonIllegalArgumentError ( ' Could not determine mime type or data passed directly without mime type. ' )
2017-01-23 21:16:43 +01:00
2016-11-24 12:34:31 +01:00
random_suffix = ' ' . join ( random . choice ( string . ascii_uppercase + string . digits ) for _ in range ( 10 ) )
file_name = " mastodonpyupload_ " + str ( time . time ( ) ) + " _ " + str ( random_suffix ) + mimetypes . guess_extension ( mime_type )
2017-01-23 21:16:43 +01:00
2016-11-24 12:34:31 +01:00
media_file_description = ( file_name , media_file , mime_type )
return self . __api_request ( ' POST ' , ' /api/v1/media ' , files = { ' file ' : media_file_description } )
2017-01-23 21:16:43 +01:00
2016-11-23 23:30:51 +01:00
###
# Internal helpers, dragons probably
###
2016-11-25 23:14:00 +01:00
def __datetime_to_epoch ( self , date_time ) :
2016-11-24 00:36:00 +01:00
"""
2016-11-25 23:14:00 +01:00
Converts a python datetime to unix epoch , accounting for
time zones and such .
2017-01-23 21:16:43 +01:00
2016-11-25 23:14:00 +01:00
Assumes UTC if timezone is not given .
"""
date_time_utc = None
if date_time . tzinfo == None :
date_time_utc = date_time . replace ( tzinfo = pytz . utc )
else :
date_time_utc = date_time . astimezone ( pytz . utc )
2017-01-23 21:16:43 +01:00
2016-11-25 23:14:00 +01:00
epoch_utc = datetime . datetime . utcfromtimestamp ( 0 ) . replace ( tzinfo = pytz . utc )
2017-01-23 21:16:43 +01:00
2016-11-25 23:14:00 +01:00
return ( date_time_utc - epoch_utc ) . total_seconds ( )
2017-01-23 21:16:43 +01:00
2016-11-25 23:14:00 +01:00
def __api_request ( self , method , endpoint , params = { } , files = { } , do_ratelimiting = True ) :
"""
Internal API request helper .
2016-11-24 00:55:09 +01:00
"""
2016-11-23 23:30:51 +01:00
response = None
headers = None
2017-01-23 21:16:43 +01:00
2016-11-25 18:17:39 +01:00
# "pace" mode ratelimiting: Assume constant rate of requests, sleep a little less long than it
# would take to not hit the rate limit at that request rate.
2016-11-25 20:57:53 +01:00
if do_ratelimiting and self . ratelimit_method == " pace " :
2016-11-25 18:17:39 +01:00
if self . ratelimit_remaining == 0 :
to_next = self . ratelimit_reset - time . time ( )
if to_next > 0 :
2016-11-25 23:14:00 +01:00
# As a precaution, never sleep longer than 5 minutes
to_next = min ( to_next , 5 * 60 )
2016-11-25 18:17:39 +01:00
time . sleep ( to_next )
else :
time_waited = time . time ( ) - self . ratelimit_lastcall
time_wait = float ( self . ratelimit_reset - time . time ( ) ) / float ( self . ratelimit_remaining )
remaining_wait = time_wait - time_waited
2017-01-23 21:16:43 +01:00
2016-11-25 18:17:39 +01:00
if remaining_wait > 0 :
2016-11-25 23:14:00 +01:00
to_next = remaining_wait / self . ratelimit_pacefactor
to_next = min ( to_next , 5 * 60 )
time . sleep ( to_next )
2017-01-23 21:16:43 +01:00
2016-11-25 18:17:39 +01:00
# Generate request headers
2016-11-23 23:30:51 +01:00
if self . access_token != None :
headers = { ' Authorization ' : ' Bearer ' + self . access_token }
2017-01-23 21:16:43 +01:00
2016-11-24 20:07:00 +01:00
if self . debug_requests == True :
2016-11-24 20:03:08 +01:00
print ( ' Mastodon: Request to endpoint " ' + endpoint + ' " using method " ' + method + ' " . ' )
print ( ' Parameters: ' + str ( params ) )
print ( ' Headers: ' + str ( headers ) )
print ( ' Files: ' + str ( files ) )
2016-11-25 18:17:39 +01:00
# Make request
request_complete = False
while not request_complete :
request_complete = True
2017-01-23 21:16:43 +01:00
2016-11-25 18:17:39 +01:00
response_object = None
try :
if method == ' GET ' :
2016-12-13 17:17:33 +01:00
response_object = requests . get ( self . api_base_url + endpoint , data = params , headers = headers , files = files , timeout = self . request_timeout )
2017-01-23 21:16:43 +01:00
2016-11-25 18:17:39 +01:00
if method == ' POST ' :
2016-12-13 17:17:33 +01:00
response_object = requests . post ( self . api_base_url + endpoint , data = params , headers = headers , files = files , timeout = self . request_timeout )
2017-01-23 21:16:43 +01:00
2016-11-25 18:17:39 +01:00
if method == ' DELETE ' :
2016-12-13 17:17:33 +01:00
response_object = requests . delete ( self . api_base_url + endpoint , data = params , headers = headers , files = files , timeout = self . request_timeout )
2017-04-01 14:13:46 +02:00
except Exception as e :
import traceback
traceback . print_exc ( )
raise MastodonNetworkError ( " Could not complete request: %s " % e )
2017-01-23 21:16:43 +01:00
2016-11-25 18:17:39 +01:00
if response_object == None :
raise MastodonIllegalArgumentError ( " Illegal request. " )
2017-01-23 21:16:43 +01:00
2016-11-25 18:17:39 +01:00
# Handle response
if self . debug_requests == True :
print ( ' Mastodon: Response received with code ' + str ( response_object . status_code ) + ' . ' )
2017-04-01 15:04:45 +02:00
print ( ' response headers: ' + str ( response_object . headers ) )
2016-11-25 18:17:39 +01:00
print ( ' Response text content: ' + str ( response_object . text ) )
2017-01-23 21:16:43 +01:00
2016-11-25 18:17:39 +01:00
if response_object . status_code == 404 :
raise MastodonAPIError ( ' Endpoint not found. ' )
2017-01-23 21:16:43 +01:00
2016-11-25 18:17:39 +01:00
if response_object . status_code == 500 :
raise MastodonAPIError ( ' General API problem. ' )
2017-01-23 21:16:43 +01:00
2016-11-25 18:17:39 +01:00
try :
response = response_object . json ( )
except :
2017-04-01 14:13:46 +02:00
import traceback
traceback . print_exc ( )
2017-04-03 12:02:17 +02:00
raise MastodonAPIError ( " Could not parse response as JSON, response code was %s , bad json content was ' %s ' " % ( response_object . status_code , response_object . content ) )
2017-01-23 21:16:43 +01:00
2016-11-25 18:17:39 +01:00
# Handle rate limiting
2016-11-25 23:28:30 +01:00
if ' X-RateLimit-Remaining ' in response_object . headers and do_ratelimiting :
self . ratelimit_remaining = int ( response_object . headers [ ' X-RateLimit-Remaining ' ] )
self . ratelimit_limit = int ( response_object . headers [ ' X-RateLimit-Limit ' ] )
2016-11-25 23:14:00 +01:00
2016-11-25 23:28:30 +01:00
try :
2016-11-25 23:14:00 +01:00
ratelimit_reset_datetime = dateutil . parser . parse ( response_object . headers [ ' X-RateLimit-Reset ' ] )
self . ratelimit_reset = self . __datetime_to_epoch ( ratelimit_reset_datetime )
# Adjust server time to local clock
server_time_datetime = dateutil . parser . parse ( response_object . headers [ ' Date ' ] )
server_time = self . __datetime_to_epoch ( server_time_datetime )
server_time_diff = time . time ( ) - server_time
self . ratelimit_reset + = server_time_diff
self . ratelimit_lastcall = time . time ( )
2017-04-01 14:13:46 +02:00
except Exception as e :
import traceback
traceback . print_exc ( )
raise MastodonRatelimitError ( " Rate limit time calculations failed: %s " % e )
2017-01-23 21:16:43 +01:00
2016-11-25 23:28:30 +01:00
if " error " in response and response [ " error " ] == " Throttled " :
if self . ratelimit_method == " throw " :
raise MastodonRatelimitError ( " Hit rate limit. " )
2016-11-25 23:14:00 +01:00
if self . ratelimit_method == " wait " or self . ratelimit_method == " pace " :
to_next = self . ratelimit_reset - time . time ( )
if to_next > 0 :
# As a precaution, never sleep longer than 5 minutes
2017-01-23 21:16:43 +01:00
to_next = min ( to_next , 5 * 60 )
2016-11-25 23:14:00 +01:00
time . sleep ( to_next )
2016-11-25 23:28:30 +01:00
request_complete = False
2017-01-23 21:16:43 +01:00
2016-11-24 12:03:42 +01:00
return response
2017-01-23 21:16:43 +01:00
2016-11-23 23:30:51 +01:00
def __generate_params ( self , params , exclude = [ ] ) :
2016-11-24 00:36:00 +01:00
"""
2016-11-24 00:55:09 +01:00
Internal named - parameters - to - dict helper .
2017-01-23 21:16:43 +01:00
2016-11-26 00:03:19 +01:00
Note for developers : If called with locals ( ) as params ,
as is the usual practice in this code , the __generate_params call
2017-01-23 21:16:43 +01:00
( or at least the locals ( ) call ) should generally be the first thing
2016-11-26 00:03:19 +01:00
in your function .
2016-11-24 00:55:09 +01:00
"""
2016-11-23 23:30:51 +01:00
params = dict ( params )
2017-01-23 21:16:43 +01:00
2016-11-23 23:30:51 +01:00
del params [ ' self ' ]
param_keys = list ( params . keys ( ) )
for key in param_keys :
if params [ key ] == None or key in exclude :
del params [ key ]
2017-01-23 21:16:43 +01:00
2016-11-23 23:30:51 +01:00
param_keys = list ( params . keys ( ) )
for key in param_keys :
if isinstance ( params [ key ] , list ) :
params [ key + " [] " ] = params [ key ]
del params [ key ]
2017-01-23 21:16:43 +01:00
2016-11-23 23:30:51 +01:00
return params
2016-11-25 18:17:39 +01:00
##
# Exceptions
##
class MastodonIllegalArgumentError ( ValueError ) :
pass
2016-11-25 23:14:00 +01:00
class MastodonFileNotFoundError ( IOError ) :
pass
2016-11-25 18:17:39 +01:00
class MastodonNetworkError ( IOError ) :
pass
class MastodonAPIError ( Exception ) :
pass
class MastodonRatelimitError ( Exception ) :
pass