diff --git a/docs/index.rst b/docs/index.rst index a91cfb7..e7c9366 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,5 +1,7 @@ Mastodon.py =========== +.. py:module:: mastodon +.. py:class: Mastodon .. code-block:: python @@ -37,14 +39,114 @@ as a single python module. By default, it talks to the `Mastodon flagship instance`_, but it can be set to talk to any node running Mastodon. +A note about IDs +---------------- +Mastodons API uses IDs in several places: User IDs, Toot IDs, ... + +While debugging, it might be tempting to copy-paste in IDs from the +web interface into your code. This will not work, as the IDs on the web +interface and in the URLs are not the same as the IDs used internally +in the API, so don't do that. + +Return values +------------- Unless otherwise specified, all data is returned as python dictionaries, matching the JSON format used by the API. -For complete documentation on what every function returns, -check the `Mastodon API docs`_, or just play around a bit - the -format of the data is generally very easy to understand. -.. py:module:: mastodon -.. py:class: Mastodon +User dicts +~~~~~~~~~~ +.. code-block:: python + + mastodon.account(<numerical id>) + # Returns the following dictionary: + { + 'display_name': The user's display name + 'acct': The user's account name as username@domain (@domain omitted for local users) + 'following_count': How many people they follow + 'url': Their URL; usually 'https://mastodon.social/users/<acct>' + 'statuses_count': How many statuses they have + 'followers_count': How many followers they have + 'avatar': URL for their avatar + 'note': Their bio + 'header': URL for their header image + 'id': Same as <numerical id> + 'username': The username (what you @ them with) + } + +Toot dicts +~~~~~~~~~~ +.. code-block:: python + + mastodon.toot("Hello from Python") + # Returns the following dictionary: + { + 'sensitive': Denotes whether the toot is marked sensitive + 'created_at': Creation time + 'mentions': A list of account dicts mentioned in the toot + 'uri': Descriptor for the toot + EG 'tag:mastodon.social,2016-11-25:objectId=<id>:objectType=Status' + 'tags': A list of hashtag dicts used in the toot + 'in_reply_to_id': Numerical id of the toot this toot is in response to + 'id': Numerical id of this toot + 'reblogs_count': Number of reblogs + 'favourites_count': Number of favourites + 'reblog': Denotes whether the toot is a reblog + 'url': URL of the toot + 'content': Content of the toot, as HTML: '<p>Hello from Python</p>' + 'favourited': Denotes whether the logged in user has favourited this toot + 'account': Account dict for the logged in account + } + +Relationship dicts +~~~~~~~~~~~~~~~~~~ +.. code-block:: python + + mastodon.account_follow(<numerical id>) + # Returns the following dictionary: + { + 'followed_by': Boolean denoting whether they follow you back + 'following': Boolean denoting whether you follow them + 'id': Numerical id (same one as <numerical id>) + 'blocking': Boolean denoting whether you are blocking them + } + +Notification dicts +~~~~~~~~~~~~~~~~~~ +.. code-block:: python + + mastodon.notifications()[0] + # Returns the following dictionary: + { + 'id': id of the notification. + 'type': "mention", "reblog", "favourite" or "follow". + 'status': In case of "mention", the mentioning status. + In case of reblog / favourite, the reblogged / favourited status. + 'account': User dict of the user from whom the notification originates. + } + +Context dicts +~~~~~~~~~~~~~ +.. code-block:: python + + mastodon.status_context(<numerical id>) + # Returns the following dictionary: + { + 'descendants': A list of toot dicts + 'ancestors': A list of toot dicts + } + +Media dicts +~~~~~~~~~~~ +.. code-block:: python + + mastodon.media_post("image.jpg", "image/jpeg") + # Returns the following dictionary: + { + 'text_url': The display text for the media (what shows up in toots) + 'preview_url': The URL for the media preview + 'type': Media type, EG 'image' + 'url': The URL for the media + } App registration and user authentication ---------------------------------------- @@ -91,7 +193,6 @@ This function allows you to get information about a users notifications. .. automethod:: Mastodon.notifications - Reading data: Accounts ---------------------- These functions allow you to get information about accounts and @@ -103,7 +204,6 @@ their relationships. .. automethod:: Mastodon.account_following .. automethod:: Mastodon.account_followers .. automethod:: Mastodon.account_relationships -.. automethod:: Mastodon.account_suggestions .. automethod:: Mastodon.account_search Writing data: Statuses @@ -113,11 +213,11 @@ interact with already posted statuses. .. automethod:: Mastodon.status_post .. automethod:: Mastodon.toot -.. automethod:: Mastodon.status_delete .. automethod:: Mastodon.status_reblog .. automethod:: Mastodon.status_unreblog .. automethod:: Mastodon.status_favourite .. automethod:: Mastodon.status_unfavourite +.. automethod:: Mastodon.status_delete Writing data: Accounts ---------------------- @@ -137,6 +237,7 @@ to attach media to statuses. .. automethod:: Mastodon.media_post + .. _Mastodon: https://github.com/Gargron/mastodon .. _Mastodon flagship instance: http://mastodon.social/ .. _Mastodon api docs: https://github.com/Gargron/mastodon/wiki/API 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: @staticmethod def create_app(client_name, scopes = ['read', 'write', 'follow'], redirect_uris = None, to_file = None, api_base_url = __DEFAULT_BASE_URL): """ - Creates a new app with given client_name and scopes (read, write, follow) + Create a new app with given client_name and scopes (read, write, follow) 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. @@ -64,7 +64,7 @@ class Mastodon: ### 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): """ - Creates a new API wrapper instance based on the given client_secret and client_id. If you + Create a new API wrapper instance based on the given client_secret and client_id. If you give a client_id and it is not a file, you must also give a secret. You can also directly specify an access_token, directly or as a file. @@ -108,15 +108,15 @@ class Mastodon: def log_in(self, username, password, scopes = ['read', 'write', 'follow'], to_file = None): """ - Logs in and sets access_token to what was returned. Note that your + Log in and sets access_token to what was returned. Note that your username is the e-mail you use to log in into mastodon. Can persist access token to file, to be used in the constructor. - Will throw an exception if username / password are wrong, scopes are not - valid or granted scopes differ from requested. + Will throw a MastodonIllegalArgumentError if username / password + are wrong, scopes are not valid or granted scopes differ from requested. - Returns the access_token, as well. + Returns the access_token. """ params = self.__generate_params(locals()) params['client_id'] = self.client_id @@ -125,7 +125,7 @@ class Mastodon: params['scope'] = " ".join(scopes) try: - response = self.__api_request('POST', '/oauth/token', params) + response = self.__api_request('POST', '/oauth/token', params, do_ratelimiting = False) self.access_token = response['access_token'] except: raise MastodonIllegalArgumentError('Invalid user name, password or scopes.') @@ -147,35 +147,45 @@ class Mastodon: ## def timeline(self, timeline = "home", max_id = None, since_id = None, limit = None): """ - Returns statuses, most recent ones first. Timeline can be home, mentions, public + Fetch statuses, most recent ones first. Timeline can be home, mentions, public or tag/hashtag. See the following functions documentation for what those do. The default timeline is the "home" timeline. + + Returns a list of toot dicts. """ params = self.__generate_params(locals(), ['timeline']) return self.__api_request('GET', '/api/v1/timelines/' + timeline, params) def timeline_home(self, max_id = None, since_id = None, limit = None): """ - Returns the authenticated users home timeline (i.e. followed users and self). + Fetch the authenticated users home timeline (i.e. followed users and self). + + Returns a list of toot dicts. """ return self.timeline('home', max_id = max_id, since_id = since_id, limit = limit) def timeline_mentions(self, max_id = None, since_id = None, limit = None): """ - Returns the authenticated users mentions. + Fetches the authenticated users mentions. + + Returns a list of toot dicts. """ return self.timeline('mentions', max_id = max_id, since_id = since_id, limit = limit) def timeline_public(self, max_id = None, since_id = None, limit = None): """ - Returns the public / visible-network timeline. + Fetches the public / visible-network timeline. + + Returns a list of toot dicts. """ return self.timeline('public', max_id = max_id, since_id = since_id, limit = limit) def timeline_hashtag(self, hashtag, max_id = None, since_id = None, limit = None): """ - Returns all toots with a given hashtag. + Fetch a timeline of toots with a given hashtag. + + Returns a list of toot dicts. """ return self.timeline('tag/' + str(hashtag), max_id = max_id, since_id = since_id, limit = limit) @@ -184,25 +194,33 @@ class Mastodon: ### def status(self, id): """ - Returns a status. + Fetch information about a single toot. + + Returns a toot dict. """ return self.__api_request('GET', '/api/v1/statuses/' + str(id)) def status_context(self, id): """ - Returns ancestors and descendants of the status. + Fetch information about ancestors and descendants of a toot. + + Returns a context dict. """ return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/context') def status_reblogged_by(self, id): """ - Returns a list of users that have reblogged a status. + Fetch a list of users that have reblogged a status. + + Returns a list of user dicts. """ return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/reblogged_by') def status_favourited_by(self, id): """ - Returns a list of users that have favourited a status. + Fetch a list of users that have favourited a status. + + Returns a list of user dicts. """ return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/favourited_by') @@ -211,8 +229,10 @@ class Mastodon: ### def notifications(self): """ - Returns notifications (mentions, favourites, reblogs, follows) for the authenticated + Fetch notifications (mentions, favourites, reblogs, follows) for the authenticated user. + + Returns a list of notification dicts. """ return self.__api_request('GET', '/api/v1/notifications') @@ -221,53 +241,61 @@ class Mastodon: ### def account(self, id): """ - Returns account. + Fetch account information by user id. + + Returns a user dict. """ return self.__api_request('GET', '/api/v1/accounts/' + str(id)) def account_verify_credentials(self): """ - Returns authenticated user's account. + Fetch authenticated user's account information. + + Returns a user dict. """ return self.__api_request('GET', '/api/v1/accounts/verify_credentials') def account_statuses(self, id, max_id = None, since_id = None, limit = None): """ - Returns statuses by user. Same options as timeline are permitted. + Fetch statuses by user id. Same options as timeline are permitted. + + Returns a list of toot dicts. """ params = self.__generate_params(locals(), ['id']) return self.__api_request('GET', '/api/v1/accounts/' + str(id) + '/statuses', params) def account_following(self, id): """ - Returns users the given user is following. + Fetch users the given user is following. + + Returns a list of user dicts. """ return self.__api_request('GET', '/api/v1/accounts/' + str(id) + '/following') def account_followers(self, id): """ - Returns users the given user is followed by. + Fetch users the given user is followed by. + + Returns a list of user dicts. """ return self.__api_request('GET', '/api/v1/accounts/' + str(id) + '/followers') def account_relationships(self, id): """ - Returns relationships (following, followed_by, blocking) of the logged in user to + Fetch relationships (following, followed_by, blocking) of the logged in user to a given account. id can be a list. + + Returns a list of relationship dicts. """ params = self.__generate_params(locals()) return self.__api_request('GET', '/api/v1/accounts/relationships', params) - - def account_suggestions(self): - """ - Returns accounts that the system suggests the authenticated user to follow. - """ - return self.__api_request('GET', '/api/v1/accounts/suggestions') def account_search(self, q, limit = None): """ - Returns matching accounts. Will lookup an account remotely if the search term is + Fetch matching accounts. Will lookup an account remotely if the search term is in the username@domain format and not yet in the database. + + Returns a list of user dicts. """ params = self.__generate_params(locals()) return self.__api_request('GET', '/api/v1/accounts/search', params) @@ -277,10 +305,10 @@ class Mastodon: ### def status_post(self, status, in_reply_to_id = None, media_ids = None): """ - Posts a status. Can optionally be in reply to another status and contain + Post a status. Can optionally be in reply to another status and contain up to four pieces of media (Uploaded via media_post()). - Returns the new status. + Returns a toot dict with the new status. """ params = self.__generate_params(locals()) return self.__api_request('POST', '/api/v1/statuses', params) @@ -288,41 +316,46 @@ class Mastodon: def toot(self, status): """ Synonym for status_post that only takes the status text as input. + + Returns a toot dict with the new status. """ return self.status_post(status) def status_delete(self, id): """ - Deletes a status + Delete a status + + Returns an empty dict for good measure. """ return self.__api_request('DELETE', '/api/v1/statuses/' + str(id)) def status_reblog(self, id): - """Reblogs a status. + """Reblog a status. - Returns a new status that wraps around the reblogged one.""" + Returns a toot with with a new status that wraps around the reblogged one. + """ return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/reblog") def status_unreblog(self, id): """ - Un-reblogs a status. + Un-reblog a status. - Returns the status that used to be reblogged. + Returns a toot dict with the status that used to be reblogged. """ return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/unreblog") def status_favourite(self, id): """ - Favourites a status. + Favourite a status. - Returns the favourited status. + Returns a toot dict with the favourited status. """ return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/favourite") def status_unfavourite(self, id): - """Favourites a status. + """Favourite a status. - Returns the un-favourited status. + Returns a toot dict with the un-favourited status. """ return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/unfavourite") @@ -331,33 +364,33 @@ class Mastodon: ### def account_follow(self, id): """ - Follows a user. + Follow a user. - Returns the updated relationship to the user. + Returns a relationship dict containing the updated relationship to the user. """ return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/follow") def account_unfollow(self, id): """ - Unfollows a user. + Unfollow a user. - Returns the updated relationship to the user. + Returns a relationship dict containing the updated relationship to the user. """ return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/unfollow") def account_block(self, id): """ - Blocks a user. + Block a user. - Returns the updated relationship to the user. + Returns a relationship dict containing the updated relationship to the user. """ return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/block") def account_unblock(self, id): """ - Unblocks a user. + Unblock a user. - Returns the updated relationship to the user. + Returns a relationship dict containing the updated relationship to the user. """ return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/unblock") @@ -366,20 +399,18 @@ class Mastodon: ### def media_post(self, media_file, mime_type = None): """ - Posts an image. media_file can either be image data or + Post an image. media_file can either be image data or 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. - Returns the uploaded media metadata object. Importantly, this contains - the ID that can then be used in status_post() to attach the media to - a toot. - Throws a MastodonIllegalArgumentError if the mime type of the passed data or file can not be determined properly. + + Returns a media dict. This contains the id that can be used in + status_post to attach the media file to a toot. """ - - if os.path.isfile(media_file): + if os.path.isfile(media_file) and mime_type == None: mime_type = mimetypes.guess_type(media_file)[0] media_file = open(media_file, 'rb') @@ -395,7 +426,7 @@ class Mastodon: ### # Internal helpers, dragons probably ### - def __api_request(self, method, endpoint, params = {}, files = {}): + def __api_request(self, method, endpoint, params = {}, files = {}, do_ratelimiting = True): """ Internal API request helper. @@ -410,7 +441,7 @@ class Mastodon: # "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. - if self.ratelimit_method == "pace": + if do_ratelimiting and self.ratelimit_method == "pace": if self.ratelimit_remaining == 0: to_next = self.ratelimit_reset - time.time() if to_next > 0: @@ -472,20 +503,21 @@ class Mastodon: raise MastodonAPIError("Could not parse response as JSON, respose code was " + str(response_object.status_code)) # Handle rate limiting - self.ratelimit_remaining = int(response_object.headers['X-RateLimit-Remaining']) - self.ratelimit_limit = int(response_object.headers['X-RateLimit-Limit']) - self.ratelimit_reset = (datetime.strptime(response_object.headers['X-RateLimit-Reset'], "%Y-%m-%dT%H:%M:%S.%fZ") - datetime(1970, 1, 1)).total_seconds() - self.ratelimit_lastcall = time.time() - - if "error" in response and response["error"] == "Throttled": - if self.ratelimit_method == "throw": - raise MastodonRatelimitError("Hit rate limit.") - - if self.ratelimit_method == "wait" or self.ratelimit_method == "pace": - to_next = self.ratelimit_reset - time.time() - if to_next > 0: - time.sleep(to_next) - request_complete = False + 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']) + self.ratelimit_reset = (datetime.strptime(response_object.headers['X-RateLimit-Reset'], "%Y-%m-%dT%H:%M:%S.%fZ") - datetime(1970, 1, 1)).total_seconds() + self.ratelimit_lastcall = time.time() + + if "error" in response and response["error"] == "Throttled": + if self.ratelimit_method == "throw": + raise MastodonRatelimitError("Hit rate limit.") + + if self.ratelimit_method == "wait" or self.ratelimit_method == "pace": + to_next = self.ratelimit_reset - time.time() + if to_next > 0: + time.sleep(to_next) + request_complete = False return response