Merge remote-tracking branch 'refs/remotes/origin/master' into ratelimits

# Conflicts:
#	mastodon/Mastodon.py
This commit is contained in:
Lorenz Diener 2016-11-25 20:57:53 +01:00
commit ab58894041
S'han modificat 2 arxius amb 213 adicions i 80 eliminacions

Veure arxiu

@ -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

Veure arxiu

@ -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