diff options
-rw-r--r-- | README.rst | 24 | ||||
-rw-r--r-- | docs/conf.py | 32 | ||||
-rw-r--r-- | docs/index.rst | 301 | ||||
-rw-r--r-- | mastodon/Mastodon.py | 1489 | ||||
-rw-r--r-- | mastodon/streaming.py | 50 |
5 files changed, 1009 insertions, 887 deletions
@@ -6,7 +6,7 @@ Feature complete for public API as of Mastodon version 3.0.1 (pypi) / 3.3.0 (cur | |||
6 | .. code-block:: python | 6 | .. code-block:: python |
7 | 7 | ||
8 | # Register your app! This only needs to be done once. Uncomment the code and substitute in your information. | 8 | # Register your app! This only needs to be done once. Uncomment the code and substitute in your information. |
9 | 9 | ||
10 | from mastodon import Mastodon | 10 | from mastodon import Mastodon |
11 | 11 | ||
12 | ''' | 12 | ''' |
@@ -20,7 +20,7 @@ Feature complete for public API as of Mastodon version 3.0.1 (pypi) / 3.3.0 (cur | |||
20 | # Then login. This can be done every time, or use persisted. | 20 | # Then login. This can be done every time, or use persisted. |
21 | 21 | ||
22 | from mastodon import Mastodon | 22 | from mastodon import Mastodon |
23 | 23 | ||
24 | mastodon = Mastodon(client_id = 'pytooter_clientcred.secret') | 24 | mastodon = Mastodon(client_id = 'pytooter_clientcred.secret') |
25 | mastodon.log_in( | 25 | mastodon.log_in( |
26 | '[email protected]', | 26 | '[email protected]', |
@@ -31,32 +31,32 @@ Feature complete for public API as of Mastodon version 3.0.1 (pypi) / 3.3.0 (cur | |||
31 | # To post, create an actual API instance. | 31 | # To post, create an actual API instance. |
32 | 32 | ||
33 | from mastodon import Mastodon | 33 | from mastodon import Mastodon |
34 | 34 | ||
35 | mastodon = Mastodon(access_token = 'pytooter_usercred.secret') | 35 | mastodon = Mastodon(access_token = 'pytooter_usercred.secret') |
36 | mastodon.toot('Tooting from python using #mastodonpy !') | 36 | mastodon.toot('Tooting from Python using #mastodonpy !') |
37 | 37 | ||
38 | You can install Mastodon.py via pypi: | 38 | You can install Mastodon.py via pypi: |
39 | 39 | ||
40 | .. code-block:: Bash | 40 | .. code-block:: Bash |
41 | 41 | ||
42 | # Python 3 | 42 | # Python 3 |
43 | pip3 install Mastodon.py | 43 | pip3 install Mastodon.py |
44 | 44 | ||
45 | Note that python 2.7 is now no longer officially supported. It will still | 45 | Note that Python 2.7 is now no longer officially supported. It will still |
46 | work for a while, and we will fix issues as they come up, but we will not | 46 | work for a while, and we will fix issues as they come up, but we will not |
47 | be testing specifically for python 2.7 any longer. | 47 | be testing specifically for Python 2.7 any longer. |
48 | 48 | ||
49 | Full documentation and basic usage examples can be found | 49 | Full documentation and basic usage examples can be found |
50 | at http://mastodonpy.readthedocs.io/en/stable/ . | 50 | at https://mastodonpy.readthedocs.io/en/stable/ |
51 | 51 | ||
52 | Acknowledgements | 52 | Acknowledgements |
53 | ---------------- | 53 | ---------------- |
54 | Mastodon.py contains work by a large amount of contributors, many of which have | 54 | Mastodon.py contains work by a large amount of contributors, many of which have |
55 | put significant work into making it a better library. You can find some information | 55 | put significant work into making it a better library. You can find some information |
56 | about who helped with which particular feature or fix in the changelog. | 56 | about who helped with which particular feature or fix in the changelog. |
57 | 57 | ||
58 | .. image:: https://circleci.com/gh/halcy/Mastodon.py.svg?style=svg | 58 | .. image:: https://circleci.com/gh/halcy/Mastodon.py.svg?style=svg |
59 | :target: https://app.circleci.com/pipelines/github/halcy/Mastodon.py | 59 | :target: https://app.circleci.com/pipelines/github/halcy/Mastodon.py |
60 | .. image:: https://codecov.io/gh/halcy/Mastodon.py/branch/master/graph/badge.svg | 60 | .. image:: https://codecov.io/gh/halcy/Mastodon.py/branch/master/graph/badge.svg |
61 | :target: https://codecov.io/gh/halcy/Mastodon.py | 61 | :target: https://codecov.io/gh/halcy/Mastodon.py |
62 | 62 | ||
diff --git a/docs/conf.py b/docs/conf.py index 14a3902..ac8858f 100644 --- a/docs/conf.py +++ b/docs/conf.py | |||
@@ -29,7 +29,7 @@ | |||
29 | import os | 29 | import os |
30 | import sys | 30 | import sys |
31 | sys.path.insert(0, os.path.abspath('../')) | 31 | sys.path.insert(0, os.path.abspath('../')) |
32 | autodoc_member_order = 'by_source' | 32 | autodoc_member_order = 'bysource' |
33 | # print(sys.path) | 33 | # print(sys.path) |
34 | 34 | ||
35 | # Add any Sphinx extension module names here, as strings. They can be | 35 | # Add any Sphinx extension module names here, as strings. They can be |
@@ -58,7 +58,7 @@ master_doc = 'index' | |||
58 | 58 | ||
59 | # General information about the project. | 59 | # General information about the project. |
60 | project = u'Mastodon.py' | 60 | project = u'Mastodon.py' |
61 | copyright = u'2016-2020, Lorenz Diener' | 61 | copyright = u'2016-2022, Lorenz Diener' |
62 | author = u'Lorenz Diener' | 62 | author = u'Lorenz Diener' |
63 | 63 | ||
64 | # The version info for the project you're documenting, acts as replacement for | 64 | # The version info for the project you're documenting, acts as replacement for |
@@ -68,14 +68,14 @@ author = u'Lorenz Diener' | |||
68 | # The short X.Y version. | 68 | # The short X.Y version. |
69 | version = u'1.5' | 69 | version = u'1.5' |
70 | # The full version, including alpha/beta/rc tags. | 70 | # The full version, including alpha/beta/rc tags. |
71 | release = u'1.5.1' | 71 | release = u'1.5.2' |
72 | 72 | ||
73 | # The language for content autogenerated by Sphinx. Refer to documentation | 73 | # The language for content autogenerated by Sphinx. Refer to documentation |
74 | # for a list of supported languages. | 74 | # for a list of supported languages. |
75 | # | 75 | # |
76 | # This is also used if you do content translation via gettext catalogs. | 76 | # This is also used if you do content translation via gettext catalogs. |
77 | # Usually you set "language" from the command line for these cases. | 77 | # Usually you set "language" from the command line for these cases. |
78 | language = None | 78 | # language = None |
79 | 79 | ||
80 | # There are two options for replacing |today|: either, you set today to some | 80 | # There are two options for replacing |today|: either, you set today to some |
81 | # non-false value, then it is used: | 81 | # non-false value, then it is used: |
@@ -247,21 +247,21 @@ htmlhelp_basename = 'Mastodonpydoc' | |||
247 | # -- Options for LaTeX output --------------------------------------------- | 247 | # -- Options for LaTeX output --------------------------------------------- |
248 | 248 | ||
249 | latex_elements = { | 249 | latex_elements = { |
250 | # The paper size ('letterpaper' or 'a4paper'). | 250 | # The paper size ('letterpaper' or 'a4paper'). |
251 | # | 251 | # |
252 | # 'papersize': 'letterpaper', | 252 | # 'papersize': 'letterpaper', |
253 | 253 | ||
254 | # The font size ('10pt', '11pt' or '12pt'). | 254 | # The font size ('10pt', '11pt' or '12pt'). |
255 | # | 255 | # |
256 | # 'pointsize': '10pt', | 256 | # 'pointsize': '10pt', |
257 | 257 | ||
258 | # Additional stuff for the LaTeX preamble. | 258 | # Additional stuff for the LaTeX preamble. |
259 | # | 259 | # |
260 | # 'preamble': '', | 260 | # 'preamble': '', |
261 | 261 | ||
262 | # Latex figure (float) alignment | 262 | # Latex figure (float) alignment |
263 | # | 263 | # |
264 | # 'figure_align': 'htbp', | 264 | # 'figure_align': 'htbp', |
265 | } | 265 | } |
266 | 266 | ||
267 | # Grouping the document tree into LaTeX files. List of tuples | 267 | # Grouping the document tree into LaTeX files. List of tuples |
diff --git a/docs/index.rst b/docs/index.rst index 1e90bc2..025896d 100644 --- a/docs/index.rst +++ b/docs/index.rst | |||
@@ -2,7 +2,7 @@ Mastodon.py | |||
2 | =========== | 2 | =========== |
3 | .. py:module:: mastodon | 3 | .. py:module:: mastodon |
4 | .. py:class: Mastodon | 4 | .. py:class: Mastodon |
5 | 5 | ||
6 | Register your app! This only needs to be done once. Uncomment the code and substitute in your information: | 6 | Register your app! This only needs to be done once. Uncomment the code and substitute in your information: |
7 | 7 | ||
8 | .. code-block:: python | 8 | .. code-block:: python |
@@ -22,7 +22,7 @@ Then login. This can be done every time, or you can use the persisted informatio | |||
22 | .. code-block:: python | 22 | .. code-block:: python |
23 | 23 | ||
24 | from mastodon import Mastodon | 24 | from mastodon import Mastodon |
25 | 25 | ||
26 | mastodon = Mastodon( | 26 | mastodon = Mastodon( |
27 | client_id = 'pytooter_clientcred.secret', | 27 | client_id = 'pytooter_clientcred.secret', |
28 | api_base_url = 'https://mastodon.social' | 28 | api_base_url = 'https://mastodon.social' |
@@ -38,38 +38,38 @@ To post, create an actual API instance: | |||
38 | .. code-block:: python | 38 | .. code-block:: python |
39 | 39 | ||
40 | from mastodon import Mastodon | 40 | from mastodon import Mastodon |
41 | 41 | ||
42 | mastodon = Mastodon( | 42 | mastodon = Mastodon( |
43 | access_token = 'pytooter_usercred.secret', | 43 | access_token = 'pytooter_usercred.secret', |
44 | api_base_url = 'https://mastodon.social' | 44 | api_base_url = 'https://mastodon.social' |
45 | ) | 45 | ) |
46 | mastodon.toot('Tooting from python using #mastodonpy !') | 46 | mastodon.toot('Tooting from Python using #mastodonpy !') |
47 | 47 | ||
48 | `Mastodon`_ is an ActivityPub and OStatus based twitter-like federated social | 48 | `Mastodon`_ is an ActivityPub-based Twitter-like federated social |
49 | network node. It has an API that allows you to interact with its | 49 | network node. It has an API that allows you to interact with its |
50 | every aspect. This is a simple python wrapper for that api, provided | 50 | every aspect. This is a simple Python wrapper for that API, provided |
51 | as a single python module. By default, it talks to the | 51 | as a single Python module. By default, it talks to the |
52 | `Mastodon flagship instance`_, but it can be set to talk to any | 52 | `Mastodon flagship instance`_, but it can be set to talk to any |
53 | node running Mastodon by setting `api_base_url` when creating the | 53 | node running Mastodon by setting `api_base_url` when creating the |
54 | api object (or creating an app). | 54 | API object (or creating an app). |
55 | 55 | ||
56 | Mastodon.py aims to implement the complete public Mastodon API. As | 56 | Mastodon.py aims to implement the complete public Mastodon API. As |
57 | of this time, it is feature complete for Mastodon version 3.0.1. Pleromas | 57 | of this time, it is feature complete for Mastodon version 3.0.1. Pleroma's |
58 | Mastodon API layer, while not an official target, should also be basically | 58 | Mastodon API layer, while not an official target, should also be basically |
59 | compatible, and Mastodon.py does make some allowances for behaviour that isn't | 59 | compatible, and Mastodon.py does make some allowances for behaviour that isn't |
60 | strictly like Mastodons. | 60 | strictly like that of Mastodon. |
61 | 61 | ||
62 | A note about rate limits | 62 | A note about rate limits |
63 | ------------------------ | 63 | ------------------------ |
64 | Mastodons API rate limits per user account. By default, the limit is 300 requests | 64 | Mastodon's API rate limits per user account. By default, the limit is 300 requests |
65 | per 5 minute time slot. This can differ from instance to instance and is subject to change. | 65 | per 5 minute time slot. This can differ from instance to instance and is subject to change. |
66 | Mastodon.py has three modes for dealing with rate limiting that you can pass to | 66 | Mastodon.py has three modes for dealing with rate limiting that you can pass to |
67 | the constructor, "throw", "wait" and "pace", "wait" being the default. | 67 | the constructor, "throw", "wait" and "pace", "wait" being the default. |
68 | 68 | ||
69 | In "throw" mode, Mastodon.py makes no attempt to stick to rate limits. When | 69 | In "throw" mode, Mastodon.py makes no attempt to stick to rate limits. When |
70 | a request hits the rate limit, it simply throws a `MastodonRateLimitError`. This is | 70 | a request hits the rate limit, it simply throws a `MastodonRateLimitError`. This is |
71 | for applications that need to handle all rate limiting themselves (i.e. interactive apps), | 71 | for applications that need to handle all rate limiting themselves (i.e. interactive apps), |
72 | or applications wanting to use Mastodon.py in a multi-threaded context ("wait" and "pace" | 72 | or applications wanting to use Mastodon.py in a multi-threaded context ("wait" and "pace" |
73 | modes are not thread safe). | 73 | modes are not thread safe). |
74 | 74 | ||
75 | .. note:: | 75 | .. note:: |
@@ -95,10 +95,10 @@ modes are not thread safe). | |||
95 | In "wait" mode, once a request hits the rate limit, Mastodon.py will wait until | 95 | In "wait" mode, once a request hits the rate limit, Mastodon.py will wait until |
96 | the rate limit resets and then try again, until the request succeeds or an error | 96 | the rate limit resets and then try again, until the request succeeds or an error |
97 | is encountered. This mode is for applications that would rather just not worry about rate limits | 97 | is encountered. This mode is for applications that would rather just not worry about rate limits |
98 | much, don't poll the api all that often, and are okay with a call sometimes just taking | 98 | much, don't poll the API all that often, and are okay with a call sometimes just taking |
99 | a while. | 99 | a while. |
100 | 100 | ||
101 | In "pace" mode, Mastodon.py will delay each new request after the first one such that, | 101 | In "pace" mode, Mastodon.py will delay each new request after the first one such that, |
102 | if requests were to continue at the same rate, only a certain fraction (set in the | 102 | if requests were to continue at the same rate, only a certain fraction (set in the |
103 | constructor as `ratelimit_pacefactor`) of the rate limit will be used up. The fraction can | 103 | constructor as `ratelimit_pacefactor`) of the rate limit will be used up. The fraction can |
104 | be (and by default, is) greater than one. If the rate limit is hit, "pace" behaves like | 104 | be (and by default, is) greater than one. If the rate limit is hit, "pace" behaves like |
@@ -107,7 +107,7 @@ a loop without ever sleeping at all yourself. It is for applications that would | |||
107 | just pretend there is no such thing as a rate limit and are fine with sometimes not | 107 | just pretend there is no such thing as a rate limit and are fine with sometimes not |
108 | being very interactive. | 108 | being very interactive. |
109 | 109 | ||
110 | In addition to the per-user limit, there is a per-IP limit of 7500 requests per 5 | 110 | In addition to the per-user limit, there is a per-IP limit of 7500 requests per 5 |
111 | minute time slot, and tighter limits on logins. Mastodon.py does not make any effort | 111 | minute time slot, and tighter limits on logins. Mastodon.py does not make any effort |
112 | to respect these. | 112 | to respect these. |
113 | 113 | ||
@@ -117,36 +117,36 @@ in, do consider using Mastodon.py without authenticating to get the full per-IP | |||
117 | 117 | ||
118 | A note about pagination | 118 | A note about pagination |
119 | ----------------------- | 119 | ----------------------- |
120 | Many of Mastodons API endpoints are paginated. What this means is that if you request | 120 | Many of Mastodon's API endpoints are paginated. What this means is that if you request |
121 | data from them, you might not get all the data at once - instead, you might only get the | 121 | data from them, you might not get all the data at once - instead, you might only get the |
122 | first few results. | 122 | first few results. |
123 | 123 | ||
124 | All endpoints that are paginated have four parameters: `since_id`, `max_id`, `min_id` and | 124 | All endpoints that are paginated have four parameters: `since_id`, `max_id`, `min_id` and |
125 | `limit`. `since_id` allows you to specify the smallest id you want in the returned data, but | 125 | `limit`. `since_id` allows you to specify the smallest id you want in the returned data, but |
126 | you will still always get the newest data, so if there are too many statuses between | 126 | you will still always get the newest data, so if there are too many statuses between |
127 | the newest one and `since_id`, some will not be returned. `min_id`, on the other hand, gives | 127 | the newest one and `since_id`, some will not be returned. `min_id`, on the other hand, gives |
128 | you statuses with that minimum id and newer, starting at the given id. `max_id`, similarly, | 128 | you statuses with that minimum id and newer, starting at the given id. `max_id`, similarly, |
129 | allows you to specify the largest id you want. By specifying either min_id or `max_id` | 129 | allows you to specify the largest id you want. By specifying either min_id or `max_id` |
130 | (generally, only one, not both, though specifying both is supported starting with Mastodon | 130 | (generally, only one, not both, though specifying both is supported starting with Mastodon |
131 | version 3.3.0) of them you can go through pages forwards and backwards. | 131 | version 3.3.0) of them you can go through pages forwards and backwards. |
132 | 132 | ||
133 | On Mastodon mainline, you can, pass datetime objects as IDs when fetching posts, | 133 | On Mastodon mainline, you can, pass datetime objects as IDs when fetching posts, |
134 | since the IDs used are Snowflake IDs and dates can be approximately converted to those. | 134 | since the IDs used are Snowflake IDs and dates can be approximately converted to those. |
135 | This is guaranteed to work on mainline Mastodon servers and very likely to work on all | 135 | This is guaranteed to work on mainline Mastodon servers and very likely to work on all |
136 | forks, but will **not** work on other servers implementing the API, like Pleroma, Misskey | 136 | forks, but will **not** work on other servers implementing the API, like Pleroma, Misskey |
137 | or Gotosocial. You should not use this if you want your application to be universally | 137 | or Gotosocial. You should not use this if you want your application to be universally |
138 | compatible. | 138 | compatible. |
139 | 139 | ||
140 | `limit` allows you to specify how many results you would like returned. Note that an | 140 | `limit` allows you to specify how many results you would like returned. Note that an |
141 | instance may choose to return less results than you requested - by default, Mastodon | 141 | instance may choose to return less results than you requested - by default, Mastodon |
142 | will return no more than 40 statues and no more than 80 accounts no matter how high | 142 | will return no more than 40 statuses and no more than 80 accounts no matter how high |
143 | you set the limit. | 143 | you set the limit. |
144 | 144 | ||
145 | The responses returned by paginated endpoints contain a "link" header that specifies | 145 | The responses returned by paginated endpoints contain a "link" header that specifies |
146 | which parameters to use to get the next and previous pages. Mastodon.py parses these | 146 | which parameters to use to get the next and previous pages. Mastodon.py parses these |
147 | and stores them (if present) in the first (for the previous page) and last (for the | 147 | and stores them (if present) in the first (for the previous page) and last (for the |
148 | next page) item of the returned list as _pagination_prev and _pagination_next. They | 148 | next page) item of the returned list as _pagination_prev and _pagination_next. They |
149 | are accessible only via attribute-style access. Note that this means that if you | 149 | are accessible only via attribute-style access. Note that this means that if you |
150 | want to persist pagination info with your data, you'll have to take care of that | 150 | want to persist pagination info with your data, you'll have to take care of that |
151 | manually (or persist objects, not just dicts). | 151 | manually (or persist objects, not just dicts). |
152 | 152 | ||
@@ -155,9 +155,9 @@ a paginated request as well as for fetching all pages starting from a first page | |||
155 | 155 | ||
156 | Two notes about IDs | 156 | Two notes about IDs |
157 | ------------------- | 157 | ------------------- |
158 | Mastodons API uses IDs in several places: User IDs, Toot IDs, ... | 158 | Mastodon's API uses IDs in several places: User IDs, Toot IDs, ... |
159 | 159 | ||
160 | While debugging, it might be tempting to copy-paste in IDs from the | 160 | While debugging, it might be tempting to copy-paste IDs from the |
161 | web interface into your code. This will not work, as the IDs on the web | 161 | web interface into your code. This will not work, as the IDs on the web |
162 | interface and in the URLs are not the same as the IDs used internally | 162 | interface and in the URLs are not the same as the IDs used internally |
163 | in the API, so don't do that. | 163 | in the API, so don't do that. |
@@ -165,30 +165,30 @@ in the API, so don't do that. | |||
165 | ID unpacking | 165 | ID unpacking |
166 | ~~~~~~~~~~~~ | 166 | ~~~~~~~~~~~~ |
167 | Wherever Mastodon.py expects an ID as a parameter, you can also pass a | 167 | Wherever Mastodon.py expects an ID as a parameter, you can also pass a |
168 | dict that contains an id - this means that, for example, instead of writing | 168 | dict that contains an id - this means that, for example, instead of writing |
169 | 169 | ||
170 | .. code-block:: python | 170 | .. code-block:: python |
171 | 171 | ||
172 | mastodon.status_post("@somebody wow!", in_reply_to_id = toot["id"]) | 172 | mastodon.status_post("@somebody wow!", in_reply_to_id = toot["id"]) |
173 | 173 | ||
174 | you can also just write | 174 | you can also just write |
175 | 175 | ||
176 | .. code-block:: python | 176 | .. code-block:: python |
177 | 177 | ||
178 | mastodon.status_post("@somebody wow!", in_reply_to_id = toot) | 178 | mastodon.status_post("@somebody wow!", in_reply_to_id = toot) |
179 | 179 | ||
180 | and everything will work as intended. | 180 | and everything will work as intended. |
181 | 181 | ||
182 | Error handling | 182 | Error handling |
183 | -------------- | 183 | -------------- |
184 | When Mastodon.py encounters an error, it will raise an exception, generally with | 184 | When Mastodon.py encounters an error, it will raise an exception, generally with |
185 | some text included to tell you what went wrong. | 185 | some text included to tell you what went wrong. |
186 | 186 | ||
187 | The base class that all mastodon exceptions inherit from is `MastodonError`. | 187 | The base class that all Mastodon exceptions inherit from is `MastodonError`. |
188 | If you are only interested in the fact an error was raised somewhere in | 188 | If you are only interested in the fact an error was raised somewhere in |
189 | Mastodon.py, and not the details, this is the exception you can catch. | 189 | Mastodon.py, and not the details, this is the exception you can catch. |
190 | 190 | ||
191 | `MastodonIllegalArgumentError` is generally a programming problem - you asked the | 191 | `MastodonIllegalArgumentError` is generally a programming problem - you asked the |
192 | API to do something obviously invalid (i.e. specify a privacy option that does | 192 | API to do something obviously invalid (i.e. specify a privacy option that does |
193 | not exist). | 193 | not exist). |
194 | 194 | ||
@@ -199,16 +199,16 @@ of `MastodonNetworkError`, `MastodonReadTimeout`, which is thrown when a streami | |||
199 | API stream times out during reading. | 199 | API stream times out during reading. |
200 | 200 | ||
201 | `MastodonAPIError` is an error returned from the Mastodon instance - the server | 201 | `MastodonAPIError` is an error returned from the Mastodon instance - the server |
202 | has decided it can't fullfill your request (i.e. you requested info on a user that | 202 | has decided it can't fulfil your request (i.e. you requested info on a user that |
203 | does not exist). It is further split into `MastodonNotFoundError` (API returned 404) | 203 | does not exist). It is further split into `MastodonNotFoundError` (API returned 404) |
204 | and `MastodonUnauthorizedError` (API returned 401). Different error codes might exist, | 204 | and `MastodonUnauthorizedError` (API returned 401). Different error codes might exist, |
205 | but are not currently handled separately. | 205 | but are not currently handled separately. |
206 | 206 | ||
207 | `MastodonMalformedEventError` is raised when a streaming API listener receives an | 207 | `MastodonMalformedEventError` is raised when a streaming API listener receives an |
208 | invalid event. There have been reports that this can sometimes happen after prolonged | 208 | invalid event. There have been reports that this can sometimes happen after prolonged |
209 | operation due to an upstream problem in the requests/urllib libraries. | 209 | operation due to an upstream problem in the requests/urllib libraries. |
210 | 210 | ||
211 | `MastodonRatelimitError` is raised when you hit an API rate limit. You should try | 211 | `MastodonRatelimitError` is raised when you hit an API rate limit. You should try |
212 | again after a while (see the rate limiting section above). | 212 | again after a while (see the rate limiting section above). |
213 | 213 | ||
214 | `MastodonServerError` is raised when the server throws an internal error, likely due | 214 | `MastodonServerError` is raised when the server throws an internal error, likely due |
@@ -224,9 +224,9 @@ While you take the extra step of removing the code, please take a moment to cons | |||
224 | 224 | ||
225 | Return values | 225 | Return values |
226 | ------------- | 226 | ------------- |
227 | Unless otherwise specified, all data is returned as python dictionaries, matching | 227 | Unless otherwise specified, all data is returned as Python dictionaries, matching |
228 | the JSON format used by the API. Dates returned by the API are in ISO 8601 format | 228 | the JSON format used by the API. Dates returned by the API are in ISO 8601 format |
229 | and are parsed into python datetime objects. | 229 | and are parsed into Python datetime objects. |
230 | 230 | ||
231 | To make access easier, the dictionaries returned are wrapped by a class that adds | 231 | To make access easier, the dictionaries returned are wrapped by a class that adds |
232 | read-only attributes for all dict values - this means that, for example, instead of | 232 | read-only attributes for all dict values - this means that, for example, instead of |
@@ -235,13 +235,13 @@ writing | |||
235 | .. code-block:: python | 235 | .. code-block:: python |
236 | 236 | ||
237 | description = mastodon.account_verify_credentials()["source"]["note"] | 237 | description = mastodon.account_verify_credentials()["source"]["note"] |
238 | 238 | ||
239 | you can also just write | 239 | you can also just write |
240 | 240 | ||
241 | .. code-block:: python | 241 | .. code-block:: python |
242 | 242 | ||
243 | description = mastodon.account_verify_credentials().source.note | 243 | description = mastodon.account_verify_credentials().source.note |
244 | 244 | ||
245 | and everything will work as intended. The class used for this is exposed as | 245 | and everything will work as intended. The class used for this is exposed as |
246 | `AttribAccessDict`. | 246 | `AttribAccessDict`. |
247 | 247 | ||
@@ -268,12 +268,12 @@ User / account dicts | |||
268 | 'followers_count': # How many followers they have | 268 | 'followers_count': # How many followers they have |
269 | 'statuses_count': # How many statuses they have | 269 | 'statuses_count': # How many statuses they have |
270 | 'note': # Their bio | 270 | 'note': # Their bio |
271 | 'url': # Their URL; usually 'https://mastodon.social/users/<acct>' | 271 | 'url': # Their URL; for example 'https://mastodon.social/users/<acct>' |
272 | 'avatar': # URL for their avatar, can be animated | 272 | 'avatar': # URL for their avatar, can be animated |
273 | 'header': # URL for their header image, can be animated | 273 | 'header': # URL for their header image, can be animated |
274 | 'avatar_static': # URL for their avatar, never animated | 274 | 'avatar_static': # URL for their avatar, never animated |
275 | 'header_static': # URL for their header image, never animated | 275 | 'header_static': # URL for their header image, never animated |
276 | 'source': # Additional information - only present for user dict returned | 276 | 'source': # Additional information - only present for user dict returned |
277 | # from account_verify_credentials() | 277 | # from account_verify_credentials() |
278 | 'moved_to_account': # If set, a user dict of the account this user has | 278 | 'moved_to_account': # If set, a user dict of the account this user has |
279 | # set up as their moved-to address. | 279 | # set up as their moved-to address. |
@@ -288,11 +288,11 @@ User / account dicts | |||
288 | mastodon.account_verify_credentials()["source"] | 288 | mastodon.account_verify_credentials()["source"] |
289 | # Returns the following dictionary: | 289 | # Returns the following dictionary: |
290 | { | 290 | { |
291 | 'privacy': # The users default visibility setting ("private", "unlisted" or "public") | 291 | 'privacy': # The user's default visibility setting ("private", "unlisted" or "public") |
292 | 'sensitive': # Denotes whether user media should be marked sensitive by default | 292 | 'sensitive': # Denotes whether user media should be marked sensitive by default |
293 | 'note': # Plain text version of the users bio | 293 | 'note': # Plain text version of the user's bio |
294 | } | 294 | } |
295 | 295 | ||
296 | Toot dicts | 296 | Toot dicts |
297 | ~~~~~~~~~~ | 297 | ~~~~~~~~~~ |
298 | .. _toot dict: | 298 | .. _toot dict: |
@@ -327,9 +327,9 @@ Toot dicts | |||
327 | 'application': # Application dict for the client used to post the toot (Does not federate | 327 | 'application': # Application dict for the client used to post the toot (Does not federate |
328 | # and is therefore always None for remote toots, can also be None for | 328 | # and is therefore always None for remote toots, can also be None for |
329 | # local toots for some legacy applications). | 329 | # local toots for some legacy applications). |
330 | 'language': # The language of the toot, if specified by the server, | 330 | 'language': # The language of the toot, if specified by the server, |
331 | # as ISO 639-1 (two-letter) language code. | 331 | # as ISO 639-1 (two-letter) language code. |
332 | 'muted': # Boolean denoting whether the user has muted this status by | 332 | 'muted': # Boolean denoting whether the user has muted this status by |
333 | # way of conversation muting | 333 | # way of conversation muting |
334 | 'pinned': # Boolean denoting whether or not the status is currently pinned for the | 334 | 'pinned': # Boolean denoting whether or not the status is currently pinned for the |
335 | # associated account. | 335 | # associated account. |
@@ -346,12 +346,12 @@ Mention dicts | |||
346 | .. code-block:: python | 346 | .. code-block:: python |
347 | 347 | ||
348 | { | 348 | { |
349 | 'url': # Mentioned users profile URL (potentially remote) | 349 | 'url': # Mentioned user's profile URL (potentially remote) |
350 | 'username': # Mentioned users user name (not including domain) | 350 | 'username': # Mentioned user's user name (not including domain) |
351 | 'acct': # Mentioned users account name (including domain) | 351 | 'acct': # Mentioned user's account name (including domain) |
352 | 'id': # Mentioned users (local) account ID | 352 | 'id': # Mentioned user's (local) account ID |
353 | } | 353 | } |
354 | 354 | ||
355 | Scheduled toot dicts | 355 | Scheduled toot dicts |
356 | ~~~~~~~~~~~~~~~~~~~~ | 356 | ~~~~~~~~~~~~~~~~~~~~ |
357 | .. _scheduled toot dict: | 357 | .. _scheduled toot dict: |
@@ -400,25 +400,25 @@ Poll dicts | |||
400 | 'emojis': # List of emoji dicts for all emoji used in answer strings, | 400 | 'emojis': # List of emoji dicts for all emoji used in answer strings, |
401 | 'own_votes': # The logged-in users votes, as a list of indices to the options. | 401 | 'own_votes': # The logged-in users votes, as a list of indices to the options. |
402 | } | 402 | } |
403 | 403 | ||
404 | 404 | ||
405 | Conversation dicts | 405 | Conversation dicts |
406 | ~~~~~~~~~~~~~~~~~~ | 406 | ~~~~~~~~~~~~~~~~~~ |
407 | .. _conversation dict: | 407 | .. _conversation dict: |
408 | 408 | ||
409 | .. code-block:: python | 409 | .. code-block:: python |
410 | 410 | ||
411 | mastodon.conversations()[0] | 411 | mastodon.conversations()[0] |
412 | # Returns the following dictionary: | 412 | # Returns the following dictionary: |
413 | { | 413 | { |
414 | 'id': # The ID of this conversation object | 414 | 'id': # The ID of this conversation object |
415 | 'unread': # Boolean indicating whether this conversation has yet to be | 415 | 'unread': # Boolean indicating whether this conversation has yet to be |
416 | # read by the user | 416 | # read by the user |
417 | 'accounts': # List of accounts (other than the logged-in account) that | 417 | 'accounts': # List of accounts (other than the logged-in account) that |
418 | # are part of this conversation | 418 | # are part of this conversation |
419 | 'last_status': # The newest status in this conversation | 419 | 'last_status': # The newest status in this conversation |
420 | } | 420 | } |
421 | 421 | ||
422 | Hashtag dicts | 422 | Hashtag dicts |
423 | ~~~~~~~~~~~~~ | 423 | ~~~~~~~~~~~~~ |
424 | .. _hashtag dict: | 424 | .. _hashtag dict: |
@@ -442,7 +442,7 @@ Hashtag usage history dicts | |||
442 | 'uses': # Number of statuses using this hashtag on that day | 442 | 'uses': # Number of statuses using this hashtag on that day |
443 | 'accounts': # Number of accounts using this hashtag in at least one status on that day | 443 | 'accounts': # Number of accounts using this hashtag in at least one status on that day |
444 | } | 444 | } |
445 | 445 | ||
446 | Emoji dicts | 446 | Emoji dicts |
447 | ~~~~~~~~~~~ | 447 | ~~~~~~~~~~~ |
448 | .. _emoji dict: | 448 | .. _emoji dict: |
@@ -451,16 +451,16 @@ Emoji dicts | |||
451 | 451 | ||
452 | { | 452 | { |
453 | 'shortcode': # Emoji shortcode, without surrounding colons | 453 | 'shortcode': # Emoji shortcode, without surrounding colons |
454 | 'url': # URL for the emoji image, can be animated | 454 | 'url': # URL for the emoji image, can be animated |
455 | 'static_url': # URL for the emoji image, never animated | 455 | 'static_url': # URL for the emoji image, never animated |
456 | 'visible_in_picker': # True if the emoji is enabled, False if not. | 456 | 'visible_in_picker': # True if the emoji is enabled, False if not. |
457 | 'category': # The category to display the emoji under (not present if none is set) | 457 | 'category': # The category to display the emoji under (not present if none is set) |
458 | } | 458 | } |
459 | 459 | ||
460 | Application dicts | 460 | Application dicts |
461 | ~~~~~~~~~~~~~~~~~ | 461 | ~~~~~~~~~~~~~~~~~ |
462 | .. _application dict: | 462 | .. _application dict: |
463 | 463 | ||
464 | .. code-block:: python | 464 | .. code-block:: python |
465 | 465 | ||
466 | { | 466 | { |
@@ -468,8 +468,8 @@ Application dicts | |||
468 | 'website': # The applications website | 468 | 'website': # The applications website |
469 | 'vapid_key': # A vapid key that can be used in web applications | 469 | 'vapid_key': # A vapid key that can be used in web applications |
470 | } | 470 | } |
471 | 471 | ||
472 | 472 | ||
473 | Relationship dicts | 473 | Relationship dicts |
474 | ~~~~~~~~~~~~~~~~~~ | 474 | ~~~~~~~~~~~~~~~~~~ |
475 | .. _relationship dict: | 475 | .. _relationship dict: |
@@ -485,11 +485,11 @@ Relationship dicts | |||
485 | 'blocking': # Boolean denoting whether the logged-in user has blocked the specified user | 485 | 'blocking': # Boolean denoting whether the logged-in user has blocked the specified user |
486 | 'blocked_by': # Boolean denoting whether the logged-in user has been blocked by the specified user, if information is available | 486 | 'blocked_by': # Boolean denoting whether the logged-in user has been blocked by the specified user, if information is available |
487 | 'muting': # Boolean denoting whether the logged-in user has muted the specified user | 487 | 'muting': # Boolean denoting whether the logged-in user has muted the specified user |
488 | 'muting_notifications': # Boolean denoting wheter the logged-in user has muted notifications | 488 | 'muting_notifications': # Boolean denoting wheter the logged-in user has muted notifications |
489 | # related to the specified user | 489 | # related to the specified user |
490 | 'requested': # Boolean denoting whether the logged-in user has sent the specified | 490 | 'requested': # Boolean denoting whether the logged-in user has sent the specified |
491 | # user a follow request | 491 | # user a follow request |
492 | 'domain_blocking': # Boolean denoting whether the logged-in user has blocked the | 492 | 'domain_blocking': # Boolean denoting whether the logged-in user has blocked the |
493 | # specified users domain | 493 | # specified users domain |
494 | 'showing_reblogs': # Boolean denoting whether the specified users reblogs show up on the | 494 | 'showing_reblogs': # Boolean denoting whether the specified users reblogs show up on the |
495 | # logged-in users Timeline | 495 | # logged-in users Timeline |
@@ -502,7 +502,7 @@ Relationship dicts | |||
502 | Filter dicts | 502 | Filter dicts |
503 | ~~~~~~~~~~~~ | 503 | ~~~~~~~~~~~~ |
504 | .. _filter dict: | 504 | .. _filter dict: |
505 | 505 | ||
506 | .. code-block:: python | 506 | .. code-block:: python |
507 | 507 | ||
508 | mastodon.filter(<numerical id>) | 508 | mastodon.filter(<numerical id>) |
@@ -516,7 +516,7 @@ Filter dicts | |||
516 | # or if it should be ran client-side. | 516 | # or if it should be ran client-side. |
517 | 'whole_word': # Boolean denoting whether this filter can match partial words | 517 | 'whole_word': # Boolean denoting whether this filter can match partial words |
518 | } | 518 | } |
519 | 519 | ||
520 | Notification dicts | 520 | Notification dicts |
521 | ~~~~~~~~~~~~~~~~~~ | 521 | ~~~~~~~~~~~~~~~~~~ |
522 | .. _notification dict: | 522 | .. _notification dict: |
@@ -552,14 +552,14 @@ List dicts | |||
552 | .. _list dict: | 552 | .. _list dict: |
553 | 553 | ||
554 | .. code-block:: python | 554 | .. code-block:: python |
555 | 555 | ||
556 | mastodon.list(<numerical id>) | 556 | mastodon.list(<numerical id>) |
557 | # Returns the following dictionary: | 557 | # Returns the following dictionary: |
558 | { | 558 | { |
559 | 'id': # id of the list | 559 | 'id': # id of the list |
560 | 'title': # title of the list | 560 | 'title': # title of the list |
561 | } | 561 | } |
562 | 562 | ||
563 | Media dicts | 563 | Media dicts |
564 | ~~~~~~~~~~~ | 564 | ~~~~~~~~~~~ |
565 | .. _media dict: | 565 | .. _media dict: |
@@ -575,7 +575,7 @@ Media dicts | |||
575 | 'remote_url': # The remote URL for the media (if the image is from a remote instance) | 575 | 'remote_url': # The remote URL for the media (if the image is from a remote instance) |
576 | 'preview_url': # The URL for the media preview | 576 | 'preview_url': # The URL for the media preview |
577 | 'text_url': # The display text for the media (what shows up in toots) | 577 | 'text_url': # The display text for the media (what shows up in toots) |
578 | 'meta': # Dictionary of two metadata dicts (see below), | 578 | 'meta': # Dictionary of two metadata dicts (see below), |
579 | # 'original' and 'small' (preview). Either may be empty. | 579 | # 'original' and 'small' (preview). Either may be empty. |
580 | # May additionally contain an "fps" field giving a videos frames per second (possibly | 580 | # May additionally contain an "fps" field giving a videos frames per second (possibly |
581 | # rounded), and a "length" field giving a videos length in a human-readable format. | 581 | # rounded), and a "length" field giving a videos length in a human-readable format. |
@@ -584,7 +584,7 @@ Media dicts | |||
584 | 'blurhash': # The blurhash for the image, used for preview / placeholder generation | 584 | 'blurhash': # The blurhash for the image, used for preview / placeholder generation |
585 | 'description': # If set, the user-provided description for this media. | 585 | 'description': # If set, the user-provided description for this media. |
586 | } | 586 | } |
587 | 587 | ||
588 | # Metadata dicts (image) - all fields are optional: | 588 | # Metadata dicts (image) - all fields are optional: |
589 | { | 589 | { |
590 | 'width': # Width of the image in pixels | 590 | 'width': # Width of the image in pixels |
@@ -592,7 +592,7 @@ Media dicts | |||
592 | 'aspect': # Aspect ratio of the image as a floating point number | 592 | 'aspect': # Aspect ratio of the image as a floating point number |
593 | 'size': # Textual representation of the image size in pixels, e.g. '800x600' | 593 | 'size': # Textual representation of the image size in pixels, e.g. '800x600' |
594 | } | 594 | } |
595 | 595 | ||
596 | # Metadata dicts (video, gifv) - all fields are optional: | 596 | # Metadata dicts (video, gifv) - all fields are optional: |
597 | { | 597 | { |
598 | 'width': # Width of the video in pixels | 598 | 'width': # Width of the video in pixels |
@@ -607,14 +607,14 @@ Media dicts | |||
607 | { | 607 | { |
608 | 'duration': # Duration of the audio file in seconds | 608 | 'duration': # Duration of the audio file in seconds |
609 | 'bitrate': # Average bit-rate of the audio file in bytes per second | 609 | 'bitrate': # Average bit-rate of the audio file in bytes per second |
610 | } | 610 | } |
611 | 611 | ||
612 | # Focus Metadata dict: | 612 | # Focus Metadata dict: |
613 | { | 613 | { |
614 | 'x': Focus point x coordinate (between -1 and 1) | 614 | 'x': Focus point x coordinate (between -1 and 1) |
615 | 'y': Focus point x coordinate (between -1 and 1) | 615 | 'y': Focus point x coordinate (between -1 and 1) |
616 | } | 616 | } |
617 | 617 | ||
618 | # Media colors dict: | 618 | # Media colors dict: |
619 | { | 619 | { |
620 | 'foreground': # Estimated foreground colour for the attachment thumbnail | 620 | 'foreground': # Estimated foreground colour for the attachment thumbnail |
@@ -635,7 +635,7 @@ Card dicts | |||
635 | 'description': # The description of the card. | 635 | 'description': # The description of the card. |
636 | 'type': # Embed type: 'link', 'photo', 'video', or 'rich' | 636 | 'type': # Embed type: 'link', 'photo', 'video', or 'rich' |
637 | 'image': # (optional) The image associated with the card. | 637 | 'image': # (optional) The image associated with the card. |
638 | 638 | ||
639 | # OEmbed data (all optional): | 639 | # OEmbed data (all optional): |
640 | 'author_name': # Name of the embedded contents author | 640 | 'author_name': # Name of the embedded contents author |
641 | 'author_url': # URL pointing to the embedded contents author | 641 | 'author_url': # URL pointing to the embedded contents author |
@@ -660,8 +660,8 @@ Search result dicts | |||
660 | 'accounts': # List of user dicts resulting from the query | 660 | 'accounts': # List of user dicts resulting from the query |
661 | 'hashtags': # List of hashtag dicts resulting from the query | 661 | 'hashtags': # List of hashtag dicts resulting from the query |
662 | 'statuses': # List of toot dicts resulting from the query | 662 | 'statuses': # List of toot dicts resulting from the query |
663 | } | 663 | } |
664 | 664 | ||
665 | Instance dicts | 665 | Instance dicts |
666 | ~~~~~~~~~~~~~~ | 666 | ~~~~~~~~~~~~~~ |
667 | .. _instance dict: | 667 | .. _instance dict: |
@@ -673,17 +673,17 @@ Instance dicts | |||
673 | { | 673 | { |
674 | 'description': # A brief instance description set by the admin | 674 | 'description': # A brief instance description set by the admin |
675 | 'short_description': # An even briefer instance description | 675 | 'short_description': # An even briefer instance description |
676 | 'email': # The admin contact e-mail | 676 | 'email': # The admin contact email |
677 | 'title': # The instances title | 677 | 'title': # The instance's title |
678 | 'uri': # The instances URL | 678 | 'uri': # The instance's URL |
679 | 'version': # The instances mastodon version | 679 | 'version': # The instance's Mastodon version |
680 | 'urls': # Additional URLs dict, presently only 'streaming_api' with the | 680 | 'urls': # Additional URLs dict, presently only 'streaming_api' with the |
681 | # stream websocket address. | 681 | # stream websocket address. |
682 | 'stats: # A dictionary containing three stats, user_count (number of local users), | 682 | 'stats: # A dictionary containing three stats, user_count (number of local users), |
683 | # status_count (number of local statuses) and domain_count (number of known | 683 | # status_count (number of local statuses) and domain_count (number of known |
684 | # instance domains other than this one). | 684 | # instance domains other than this one). |
685 | 'contact_account': # User dict of the primary contact for the instance | 685 | 'contact_account': # User dict of the primary contact for the instance |
686 | 'languages': # Array of ISO 639-1 (two-letter) language codes the instance | 686 | 'languages': # Array of ISO 639-1 (two-letter) language codes the instance |
687 | # has chosen to advertise. | 687 | # has chosen to advertise. |
688 | 'registrations': # Boolean indication whether registrations on this instance are open | 688 | 'registrations': # Boolean indication whether registrations on this instance are open |
689 | # (True) or not (False) | 689 | # (True) or not (False) |
@@ -703,8 +703,8 @@ Activity dicts | |||
703 | 'logins': # Number of users that logged in that week | 703 | 'logins': # Number of users that logged in that week |
704 | 'registrations': # Number of new users that week | 704 | 'registrations': # Number of new users that week |
705 | 'statuses': # Number of statuses posted that week | 705 | 'statuses': # Number of statuses posted that week |
706 | } | 706 | } |
707 | 707 | ||
708 | Report dicts | 708 | Report dicts |
709 | ~~~~~~~~~~~~ | 709 | ~~~~~~~~~~~~ |
710 | .. _report dict: | 710 | .. _report dict: |
@@ -717,19 +717,19 @@ Report dicts | |||
717 | 'id': # Numerical id of the report | 717 | 'id': # Numerical id of the report |
718 | 'action_taken': # True if a moderator or admin has processed the | 718 | 'action_taken': # True if a moderator or admin has processed the |
719 | # report, False otherwise. | 719 | # report, False otherwise. |
720 | 720 | ||
721 | # The following fields are only present in the report dicts returned by moderation API: | 721 | # The following fields are only present in the report dicts returned by moderation API: |
722 | 'comment': # Text comment submitted with the report | 722 | 'comment': # Text comment submitted with the report |
723 | 'created_at': # Time at which this report was created, as a datetime object | 723 | 'created_at': # Time at which this report was created, as a datetime object |
724 | 'updated_at': # Last time this report has been updated, as a datetime object | 724 | 'updated_at': # Last time this report has been updated, as a datetime object |
725 | 'account': # User dict of the user that filed this report | 725 | 'account': # User dict of the user that filed this report |
726 | 'target_account': # Account that has been reported with this report | 726 | 'target_account': # Account that has been reported with this report |
727 | 'assigned_account': # If the report as been assigned to an account, | 727 | 'assigned_account': # If the report as been assigned to an account, |
728 | # User dict of that account (None if not) | 728 | # User dict of that account (None if not) |
729 | 'action_taken_by_account': # User dict of the account that processed this report | 729 | 'action_taken_by_account': # User dict of the account that processed this report |
730 | 'statuses': # List of statuses attached to the report, as toot dicts | 730 | 'statuses': # List of statuses attached to the report, as toot dicts |
731 | } | 731 | } |
732 | 732 | ||
733 | Push subscription dicts | 733 | Push subscription dicts |
734 | ~~~~~~~~~~~~~~~~~~~~~~~ | 734 | ~~~~~~~~~~~~~~~~~~~~~~~ |
735 | .. _push subscription dict: | 735 | .. _push subscription dict: |
@@ -746,7 +746,7 @@ Push subscription dicts | |||
746 | # 'favourite', 'reblog' and 'mention', with value True | 746 | # 'favourite', 'reblog' and 'mention', with value True |
747 | # if webpushes have been requested for those events. | 747 | # if webpushes have been requested for those events. |
748 | } | 748 | } |
749 | 749 | ||
750 | Push notification dicts | 750 | Push notification dicts |
751 | ~~~~~~~~~~~~~~~~~~~~~~~ | 751 | ~~~~~~~~~~~~~~~~~~~~~~~ |
752 | .. _push notification dict: | 752 | .. _push notification dict: |
@@ -756,37 +756,37 @@ Push notification dicts | |||
756 | mastodon.push_subscription_decrypt_push(...) | 756 | mastodon.push_subscription_decrypt_push(...) |
757 | # Returns the following dictionary | 757 | # Returns the following dictionary |
758 | { | 758 | { |
759 | 'access_token': # Access token that can be used to access the API as the | 759 | 'access_token': # Access token that can be used to access the API as the |
760 | # notified user | 760 | # notified user |
761 | 'body': # Text body of the notification | 761 | 'body': # Text body of the notification |
762 | 'icon': # URL to an icon for the notification | 762 | 'icon': # URL to an icon for the notification |
763 | 'notification_id': # ID that can be passed to notification() to get the full | 763 | 'notification_id': # ID that can be passed to notification() to get the full |
764 | # notification object, | 764 | # notification object, |
765 | 'notification_type': # 'mention', 'reblog', 'follow' or 'favourite' | 765 | 'notification_type': # 'mention', 'reblog', 'follow' or 'favourite' |
766 | 'preferred_locale': # The users preferred locale | 766 | 'preferred_locale': # The user's preferred locale |
767 | 'title': # Title for the notification | 767 | 'title': # Title for the notification |
768 | } | 768 | } |
769 | 769 | ||
770 | Preference dicts | 770 | Preference dicts |
771 | ~~~~~~~~~~~~~~~~ | 771 | ~~~~~~~~~~~~~~~~ |
772 | .. _preference dict: | 772 | .. _preference dict: |
773 | 773 | ||
774 | .. code-block:: python | 774 | .. code-block:: python |
775 | 775 | ||
776 | mastodon.preferences() | 776 | mastodon.preferences() |
777 | # Returns the following dictionary | 777 | # Returns the following dictionary |
778 | { | 778 | { |
779 | 'posting:default:visibility': # The default visibility setting for the users posts, | 779 | 'posting:default:visibility': # The default visibility setting for the user's posts, |
780 | # as a string | 780 | # as a string |
781 | 'posting:default:sensitive': # Boolean indicating whether the users uploads should | 781 | 'posting:default:sensitive': # Boolean indicating whether the user's uploads should |
782 | # be marked sensitive by default | 782 | # be marked sensitive by default |
783 | 'posting:default:language': # The users default post language, if set (None if not) | 783 | 'posting:default:language': # The user's default post language, if set (None if not) |
784 | 'reading:expand:media': # How the user wishes to be shown sensitive media. Can be | 784 | 'reading:expand:media': # How the user wishes to be shown sensitive media. Can be |
785 | # 'default' (hide if sensitive), 'hide_all' or 'show_all' | 785 | # 'default' (hide if sensitive), 'hide_all' or 'show_all' |
786 | 'reading:expand:spoilers': # Boolean indicating whether the user wishes to expand | 786 | 'reading:expand:spoilers': # Boolean indicating whether the user wishes to expand |
787 | # content warnings by default | 787 | # content warnings by default |
788 | } | 788 | } |
789 | 789 | ||
790 | Featured tag dicts | 790 | Featured tag dicts |
791 | ~~~~~~~~~~~~~~~~~~ | 791 | ~~~~~~~~~~~~~~~~~~ |
792 | .. _featured tag dict: | 792 | .. _featured tag dict: |
@@ -799,10 +799,10 @@ Featured tag dicts | |||
799 | 'id': # The featured tags id | 799 | 'id': # The featured tags id |
800 | 'name': # The featured tags name (without leading #) | 800 | 'name': # The featured tags name (without leading #) |
801 | 'statuses_count': # Number of publicly visible statuses posted with this hashtag that this instance knows about | 801 | 'statuses_count': # Number of publicly visible statuses posted with this hashtag that this instance knows about |
802 | 'last_status_at': # The last time a public status containing this hashtag was added to this instances database | 802 | 'last_status_at': # The last time a public status containing this hashtag was added to this instance's database |
803 | # (can be None if there are none) | 803 | # (can be None if there are none) |
804 | } | 804 | } |
805 | 805 | ||
806 | Read marker dicts | 806 | Read marker dicts |
807 | ~~~~~~~~~~~~~~~~~ | 807 | ~~~~~~~~~~~~~~~~~ |
808 | .. _read marker dict: | 808 | .. _read marker dict: |
@@ -815,7 +815,7 @@ Read marker dicts | |||
815 | 'last_read_id': # ID of the last read object in the timeline | 815 | 'last_read_id': # ID of the last read object in the timeline |
816 | 'version': # A counter that is incremented whenever the marker is set to a new status | 816 | 'version': # A counter that is incremented whenever the marker is set to a new status |
817 | 'updated_at': # The time the marker was last set, as a datetime object | 817 | 'updated_at': # The time the marker was last set, as a datetime object |
818 | } | 818 | } |
819 | 819 | ||
820 | Announcement dicts | 820 | Announcement dicts |
821 | ~~~~~~~~~~~~~~~~~~ | 821 | ~~~~~~~~~~~~~~~~~~ |
@@ -824,7 +824,7 @@ Announcement dicts | |||
824 | .. code-block:: python | 824 | .. code-block:: python |
825 | 825 | ||
826 | mastodon.annoucements()[0] | 826 | mastodon.annoucements()[0] |
827 | # Returns the following dictionary: | 827 | # Returns the following dictionary: |
828 | { | 828 | { |
829 | 'id': # The annoucements id | 829 | 'id': # The annoucements id |
830 | 'content': # The contents of the annoucement, as an html string | 830 | 'content': # The contents of the annoucement, as an html string |
@@ -852,7 +852,7 @@ Admin account dicts | |||
852 | .. _admin account dict: | 852 | .. _admin account dict: |
853 | 853 | ||
854 | .. code-block:: python | 854 | .. code-block:: python |
855 | 855 | ||
856 | mastodon.admin_account(id) | 856 | mastodon.admin_account(id) |
857 | # Returns the following dictionary | 857 | # Returns the following dictionary |
858 | { | 858 | { |
@@ -860,10 +860,10 @@ Admin account dicts | |||
860 | 'username': # The users username, no leading @ | 860 | 'username': # The users username, no leading @ |
861 | 'domain': # The users domain | 861 | 'domain': # The users domain |
862 | 'created_at': # The time of account creation | 862 | 'created_at': # The time of account creation |
863 | 'email': # For local users, the users e-mail | 863 | 'email': # For local users, the user's email |
864 | 'ip': # For local users, the users last known IP address | 864 | 'ip': # For local users, the user's last known IP address |
865 | 'role': # 'admin', 'moderator' or None | 865 | 'role': # 'admin', 'moderator' or None |
866 | 'confirmed': # For local users, False if the user has not confirmed their e-mail, True otherwise | 866 | 'confirmed': # For local users, False if the user has not confirmed their email, True otherwise |
867 | 'suspended': # Boolean indicating whether the user has been suspended | 867 | 'suspended': # Boolean indicating whether the user has been suspended |
868 | 'silenced': # Boolean indicating whether the user has been suspended | 868 | 'silenced': # Boolean indicating whether the user has been suspended |
869 | 'disabled': # For local users, boolean indicating whether the user has had their login disabled | 869 | 'disabled': # For local users, boolean indicating whether the user has had their login disabled |
@@ -871,29 +871,29 @@ Admin account dicts | |||
871 | 'locale': # For local users, the locale the user has set, | 871 | 'locale': # For local users, the locale the user has set, |
872 | 'invite_request': # If the user requested an invite, the invite request comment of that user. (TODO permanent?) | 872 | 'invite_request': # If the user requested an invite, the invite request comment of that user. (TODO permanent?) |
873 | 'invited_by_account_id': # Present if the user was invited by another user and set to the inviting users id. | 873 | 'invited_by_account_id': # Present if the user was invited by another user and set to the inviting users id. |
874 | 'account': # The users account, as a standard user dict | 874 | 'account': # The user's account, as a standard user dict |
875 | } | 875 | } |
876 | 876 | ||
877 | App registration and user authentication | 877 | App registration and user authentication |
878 | ---------------------------------------- | 878 | ---------------------------------------- |
879 | Before you can use the mastodon API, you have to register your | 879 | Before you can use the Mastodon API, you have to register your |
880 | application (which gets you a client key and client secret) | 880 | application (which gets you a client key and client secret) |
881 | and then log in (which gets you an access token). These functions | 881 | and then log in (which gets you an access token). These functions |
882 | allow you to do those things. Additionally, it is also possible | 882 | allow you to do those things. Additionally, it is also possible |
883 | to programmatically register a new user. | 883 | to programmatically register a new user. |
884 | 884 | ||
885 | For convenience, once you have a client id, secret and access token, | 885 | For convenience, once you have a client id, secret and access token, |
886 | you can simply pass them to the constructor of the class, too! | 886 | you can simply pass them to the constructor of the class, too! |
887 | 887 | ||
888 | Note that while it is perfectly reasonable to log back in whenever | 888 | Note that while it is perfectly reasonable to log back in whenever |
889 | your app starts, registering a new application on every | 889 | your app starts, registering a new application on every |
890 | startup is not, so don't do that - instead, register an application | 890 | startup is not, so don't do that - instead, register an application |
891 | once, and then persist your client id and secret. A convenient method | 891 | once, and then persist your client id and secret. A convenient method |
892 | for this is provided by the functions dealing with registering the app, | 892 | for this is provided by the functions dealing with registering the app, |
893 | logging in and the Mastodon classes constructor. | 893 | logging in and the Mastodon classes constructor. |
894 | 894 | ||
895 | To talk to an instance different from the flagship instance, specify | 895 | To talk to an instance different from the flagship instance, specify |
896 | the api_base_url (usually, just the URL of the instance, i.e. | 896 | the api_base_url (usually, just the URL of the instance, i.e. |
897 | https://mastodon.social/ for the flagship instance). If no protocol | 897 | https://mastodon.social/ for the flagship instance). If no protocol |
898 | is specified, Mastodon.py defaults to https. | 898 | is specified, Mastodon.py defaults to https. |
899 | 899 | ||
@@ -909,17 +909,17 @@ Versioning | |||
909 | ---------- | 909 | ---------- |
910 | Mastodon.py will check if a certain endpoint is available before doing API | 910 | Mastodon.py will check if a certain endpoint is available before doing API |
911 | calls. By default, it checks against the version of Mastodon retrieved on | 911 | calls. By default, it checks against the version of Mastodon retrieved on |
912 | init(), or the version you specified. Mastodon.py can be set (in the | 912 | init(), or the version you specified. Mastodon.py can be set (in the |
913 | constructor) to either check if an endpoint is available at all (this is the | 913 | constructor) to either check if an endpoint is available at all (this is the |
914 | default) or to check if the endpoint is available and behaves as in the newest | 914 | default) or to check if the endpoint is available and behaves as in the newest |
915 | Mastodon version (with regards to parameters as well as return values). | 915 | Mastodon version (with regards to parameters as well as return values). |
916 | Version checking can also be disabled altogether. If a version check fails, | 916 | Version checking can also be disabled altogether. If a version check fails, |
917 | Mastodon.py throws a `MastodonVersionError`. | 917 | Mastodon.py throws a `MastodonVersionError`. |
918 | 918 | ||
919 | With the following functions, you can make Mastodon.py re-check the server | 919 | With the following functions, you can make Mastodon.py re-check the server |
920 | version or explicitly determine if a specific minimum Version is available. | 920 | version or explicitly determine if a specific minimum Version is available. |
921 | Long-running applications that aim to support multiple Mastodon versions | 921 | Long-running applications that aim to support multiple Mastodon versions |
922 | should do this from time to time in case a server they are running against | 922 | should do this from time to time in case a server they are running against |
923 | updated. | 923 | updated. |
924 | 924 | ||
925 | .. automethod:: Mastodon.retrieve_mastodon_version | 925 | .. automethod:: Mastodon.retrieve_mastodon_version |
@@ -979,7 +979,7 @@ This function allows you to get and refresh information about polls. | |||
979 | 979 | ||
980 | Reading data: Notifications | 980 | Reading data: Notifications |
981 | --------------------------- | 981 | --------------------------- |
982 | This function allows you to get information about a users notifications. | 982 | This function allows you to get information about a user's notifications. |
983 | 983 | ||
984 | .. automethod:: Mastodon.notifications | 984 | .. automethod:: Mastodon.notifications |
985 | 985 | ||
@@ -1020,7 +1020,7 @@ Reading data: Follow suggestions | |||
1020 | Reading data: Profile directory | 1020 | Reading data: Profile directory |
1021 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | 1021 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
1022 | 1022 | ||
1023 | .. authomethod:: Mastodon.directory | 1023 | .. automethod:: Mastodon.directory |
1024 | 1024 | ||
1025 | Reading data: Lists | 1025 | Reading data: Lists |
1026 | ------------------- | 1026 | ------------------- |
@@ -1033,7 +1033,7 @@ These functions allow you to view information about lists. | |||
1033 | Reading data: Follows | 1033 | Reading data: Follows |
1034 | --------------------- | 1034 | --------------------- |
1035 | 1035 | ||
1036 | .. automethod:: Mastodon.followshttps://docs.joinmastodon.org/api/rest | 1036 | .. automethod:: Mastodon.follows |
1037 | 1037 | ||
1038 | Reading data: Favourites | 1038 | Reading data: Favourites |
1039 | ------------------------ | 1039 | ------------------------ |
@@ -1078,7 +1078,7 @@ of reports filed by the logged in user. It has since been removed. | |||
1078 | 1078 | ||
1079 | 1079 | ||
1080 | Writing data: Last-read markers | 1080 | Writing data: Last-read markers |
1081 | -------------------------- | 1081 | -------------------------------- |
1082 | This function allows you to set get last read position for timelines. | 1082 | This function allows you to set get last read position for timelines. |
1083 | 1083 | ||
1084 | .. automethod:: Mastodon.markers_get | 1084 | .. automethod:: Mastodon.markers_get |
@@ -1139,8 +1139,8 @@ interact with already posted statuses. | |||
1139 | 1139 | ||
1140 | Writing data: Scheduled statuses | 1140 | Writing data: Scheduled statuses |
1141 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | 1141 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
1142 | Mastodon allows you to schedule statuses (using `status_post()`_. | 1142 | Mastodon allows you to schedule statuses (using `status_post()`_). |
1143 | The functions in this section allow you to update or delete thusly | 1143 | The functions in this section allow you to update or delete |
1144 | scheduled statuses. | 1144 | scheduled statuses. |
1145 | 1145 | ||
1146 | .. automethod:: Mastodon.scheduled_status_update | 1146 | .. automethod:: Mastodon.scheduled_status_update |
@@ -1171,7 +1171,6 @@ These functions allow you to interact with other accounts: To (un)follow and | |||
1171 | (un)block. | 1171 | (un)block. |
1172 | 1172 | ||
1173 | .. automethod:: Mastodon.account_follow | 1173 | .. automethod:: Mastodon.account_follow |
1174 | .. automethod:: Mastodon.follows | ||
1175 | .. automethod:: Mastodon.account_unfollow | 1174 | .. automethod:: Mastodon.account_unfollow |
1176 | .. automethod:: Mastodon.account_block | 1175 | .. automethod:: Mastodon.account_block |
1177 | .. automethod:: Mastodon.account_unblock | 1176 | .. automethod:: Mastodon.account_unblock |
@@ -1185,7 +1184,7 @@ These functions allow you to interact with other accounts: To (un)follow and | |||
1185 | 1184 | ||
1186 | Writing data: Featured tags | 1185 | Writing data: Featured tags |
1187 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ | 1186 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
1188 | These functions allow setting which tags are featured on a users profile. | 1187 | These functions allow setting which tags are featured on a user's profile. |
1189 | 1188 | ||
1190 | .. automethod:: Mastodon.featured_tag_create | 1189 | .. automethod:: Mastodon.featured_tag_create |
1191 | .. automethod:: Mastodon.featured_tag_delete | 1190 | .. automethod:: Mastodon.featured_tag_delete |
@@ -1240,7 +1239,7 @@ Writing data: Reports | |||
1240 | .. automethod:: Mastodon.report | 1239 | .. automethod:: Mastodon.report |
1241 | 1240 | ||
1242 | Writing data: Last-read markers | 1241 | Writing data: Last-read markers |
1243 | -------------------------- | 1242 | ------------------------------- |
1244 | This function allows you to set the last read position for timelines to | 1243 | This function allows you to set the last read position for timelines to |
1245 | allow for persisting where the user was reading a timeline between sessions | 1244 | allow for persisting where the user was reading a timeline between sessions |
1246 | and clients / devices. | 1245 | and clients / devices. |
@@ -1296,12 +1295,12 @@ using the `on_abort` handler to fill in events since the last received one and t | |||
1296 | Both `run_async` and `reconnect_async` default to false, and you'll have to set each to true | 1295 | Both `run_async` and `reconnect_async` default to false, and you'll have to set each to true |
1297 | separately to get the behaviour described above. | 1296 | separately to get the behaviour described above. |
1298 | 1297 | ||
1299 | The connection may be closed at any time by calling the handles close() method. The | 1298 | The connection may be closed at any time by calling the handles close() method. The |
1300 | current status of the handler thread can be checked with the handles is_alive() function, | 1299 | current status of the handler thread can be checked with the handles is_alive() function, |
1301 | and the streaming status can be checked by calling is_receiving(). | 1300 | and the streaming status can be checked by calling is_receiving(). |
1302 | 1301 | ||
1303 | The streaming functions take instances of `StreamListener` as the `listener` parameter. | 1302 | The streaming functions take instances of `StreamListener` as the `listener` parameter. |
1304 | A `CallbackStreamListener` class that allows you to specify function callbacks | 1303 | A `CallbackStreamListener` class that allows you to specify function callbacks |
1305 | directly is included for convenience. | 1304 | directly is included for convenience. |
1306 | 1305 | ||
1307 | For new well-known events implement the streaming function in `StreamListener` or `CallbackStreamListener`. | 1306 | For new well-known events implement the streaming function in `StreamListener` or `CallbackStreamListener`. |
@@ -1335,7 +1334,7 @@ StreamListener | |||
1335 | .. automethod:: StreamListener.on_notification | 1334 | .. automethod:: StreamListener.on_notification |
1336 | .. automethod:: StreamListener.on_delete | 1335 | .. automethod:: StreamListener.on_delete |
1337 | .. automethod:: StreamListener.on_conversation | 1336 | .. automethod:: StreamListener.on_conversation |
1338 | .. automethod:: StreamListener.on_status_update | 1337 | .. automethod:: StreamListener.on_status_update |
1339 | .. automethod:: StreamListener.on_unknown_event | 1338 | .. automethod:: StreamListener.on_unknown_event |
1340 | .. automethod:: StreamListener.on_abort | 1339 | .. automethod:: StreamListener.on_abort |
1341 | .. automethod:: StreamListener.handle_heartbeat | 1340 | .. automethod:: StreamListener.handle_heartbeat |
@@ -1348,14 +1347,14 @@ CallbackStreamListener | |||
1348 | Push subscriptions | 1347 | Push subscriptions |
1349 | ------------------ | 1348 | ------------------ |
1350 | These functions allow you to manage webpush subscriptions and to decrypt received | 1349 | These functions allow you to manage webpush subscriptions and to decrypt received |
1351 | pushes. Note that the intended setup is not mastodon pushing directly to a users client - | 1350 | pushes. Note that the intended setup is not Mastodon pushing directly to a user's client - |
1352 | the push endpoint should usually be a relay server that then takes care of delivering the | 1351 | the push endpoint should usually be a relay server that then takes care of delivering the |
1353 | (encrypted) push to the end user via some mechanism, where it can then be decrypted and | 1352 | (encrypted) push to the end user via some mechanism, where it can then be decrypted and |
1354 | displayed. | 1353 | displayed. |
1355 | 1354 | ||
1356 | Mastodon allows an application to have one webpush subscription per user at a time. | 1355 | Mastodon allows an application to have one webpush subscription per user at a time. |
1357 | 1356 | ||
1358 | All crypto utilities require Mastodon.pys optional "webpush" feature dependencies | 1357 | All crypto utilities require Mastodon.py's optional "webpush" feature dependencies |
1359 | (specifically, the "cryptography" and "http_ece" packages). | 1358 | (specifically, the "cryptography" and "http_ece" packages). |
1360 | 1359 | ||
1361 | .. automethod:: Mastodon.push_subscription | 1360 | .. automethod:: Mastodon.push_subscription |
@@ -1372,8 +1371,8 @@ Moderation API | |||
1372 | -------------- | 1371 | -------------- |
1373 | These functions allow you to perform moderation actions on users and generally | 1372 | These functions allow you to perform moderation actions on users and generally |
1374 | process reports using the API. To do this, you need access to the "admin:read" and/or | 1373 | process reports using the API. To do this, you need access to the "admin:read" and/or |
1375 | "admin:write" scopes or their more granular variants (both for the application and the | 1374 | "admin:write" scopes or their more granular variants (both for the application and the |
1376 | access token), as well as at least moderator access. Mastodon.py will not request these | 1375 | access token), as well as at least moderator access. Mastodon.py will not request these |
1377 | by default, as that would be very dangerous. | 1376 | by default, as that would be very dangerous. |
1378 | 1377 | ||
1379 | BIG WARNING: TREAT ANY ACCESS TOKENS THAT HAVE ADMIN CREDENTIALS AS EXTREMELY, MASSIVELY | 1378 | BIG WARNING: TREAT ANY ACCESS TOKENS THAT HAVE ADMIN CREDENTIALS AS EXTREMELY, MASSIVELY |
@@ -1403,10 +1402,10 @@ have admin: scopes attached with a lot of care, but be extra careful with those | |||
1403 | 1402 | ||
1404 | Acknowledgements | 1403 | Acknowledgements |
1405 | ---------------- | 1404 | ---------------- |
1406 | Mastodon.py contains work by a large amount of contributors, many of which have | 1405 | Mastodon.py contains work by a large number of contributors, many of which have |
1407 | put significant work into making it a better library. You can find some information | 1406 | put significant work into making it a better library. You can find some information |
1408 | about who helped with which particular feature or fix in the changelog. | 1407 | about who helped with which particular feature or fix in the changelog. |
1409 | 1408 | ||
1410 | .. _Mastodon: https://github.com/mastodon/mastodon | 1409 | .. _Mastodon: https://github.com/mastodon/mastodon |
1411 | .. _Mastodon flagship instance: http://mastodon.social/ | 1410 | .. _Mastodon flagship instance: https://mastodon.social/ |
1412 | .. _Official Mastodon api docs: https://docs.joinmastodon.org/api/rest | 1411 | .. _Official Mastodon API docs: https://docs.joinmastodon.org/client/intro/ |
diff --git a/mastodon/Mastodon.py b/mastodon/Mastodon.py index 48d850b..98578fb 100644 --- a/mastodon/Mastodon.py +++ b/mastodon/Mastodon.py | |||
@@ -1,5 +1,7 @@ | |||
1 | # coding: utf-8 | 1 | # coding: utf-8 |
2 | 2 | ||
3 | import json | ||
4 | import base64 | ||
3 | import os | 5 | import os |
4 | import os.path | 6 | import os.path |
5 | import mimetypes | 7 | import mimetypes |
@@ -31,15 +33,13 @@ try: | |||
31 | from cryptography.hazmat.primitives import serialization | 33 | from cryptography.hazmat.primitives import serialization |
32 | except: | 34 | except: |
33 | IMPL_HAS_CRYPTO = False | 35 | IMPL_HAS_CRYPTO = False |
34 | 36 | ||
35 | IMPL_HAS_ECE = True | 37 | IMPL_HAS_ECE = True |
36 | try: | 38 | try: |
37 | import http_ece | 39 | import http_ece |
38 | except: | 40 | except: |
39 | IMPL_HAS_ECE = False | 41 | IMPL_HAS_ECE = False |
40 | 42 | ||
41 | import base64 | ||
42 | import json | ||
43 | 43 | ||
44 | IMPL_HAS_BLURHASH = True | 44 | IMPL_HAS_BLURHASH = True |
45 | try: | 45 | try: |
@@ -60,9 +60,11 @@ except ImportError: | |||
60 | ### | 60 | ### |
61 | # Version check functions, including decorator and parser | 61 | # Version check functions, including decorator and parser |
62 | ### | 62 | ### |
63 | |||
64 | |||
63 | def parse_version_string(version_string): | 65 | def parse_version_string(version_string): |
64 | """Parses a semver version string, stripping off "rc" stuff if present.""" | 66 | """Parses a semver version string, stripping off "rc" stuff if present.""" |
65 | string_parts = version_string.split(".") | 67 | string_parts = version_string.split(".") |
66 | version_parts = [ | 68 | version_parts = [ |
67 | int(re.match("([0-9]*)", string_parts[0]).group(0)), | 69 | int(re.match("([0-9]*)", string_parts[0]).group(0)), |
68 | int(re.match("([0-9]*)", string_parts[1]).group(0)), | 70 | int(re.match("([0-9]*)", string_parts[1]).group(0)), |
@@ -70,6 +72,7 @@ def parse_version_string(version_string): | |||
70 | ] | 72 | ] |
71 | return version_parts | 73 | return version_parts |
72 | 74 | ||
75 | |||
73 | def bigger_version(version_string_a, version_string_b): | 76 | def bigger_version(version_string_a, version_string_b): |
74 | """Returns the bigger version of two version strings.""" | 77 | """Returns the bigger version of two version strings.""" |
75 | major_a, minor_a, patch_a = parse_version_string(version_string_a) | 78 | major_a, minor_a, patch_a = parse_version_string(version_string_a) |
@@ -83,25 +86,31 @@ def bigger_version(version_string_a, version_string_b): | |||
83 | return version_string_a | 86 | return version_string_a |
84 | return version_string_b | 87 | return version_string_b |
85 | 88 | ||
89 | |||
86 | def api_version(created_ver, last_changed_ver, return_value_ver): | 90 | def api_version(created_ver, last_changed_ver, return_value_ver): |
87 | """Version check decorator. Currently only checks Bigger Than.""" | 91 | """Version check decorator. Currently only checks Bigger Than.""" |
88 | def api_min_version_decorator(function): | 92 | def api_min_version_decorator(function): |
89 | def wrapper(function, self, *args, **kwargs): | 93 | def wrapper(function, self, *args, **kwargs): |
90 | if not self.version_check_mode == "none": | 94 | if not self.version_check_mode == "none": |
91 | if self.version_check_mode == "created": | 95 | if self.version_check_mode == "created": |
92 | version = created_ver | 96 | version = created_ver |
93 | else: | 97 | else: |
94 | version = bigger_version(last_changed_ver, return_value_ver) | 98 | version = bigger_version( |
99 | last_changed_ver, return_value_ver) | ||
95 | major, minor, patch = parse_version_string(version) | 100 | major, minor, patch = parse_version_string(version) |
96 | if major > self.mastodon_major: | 101 | if major > self.mastodon_major: |
97 | raise MastodonVersionError("Version check failed (Need version " + version + ")") | 102 | raise MastodonVersionError( |
103 | "Version check failed (Need version " + version + ")") | ||
98 | elif major == self.mastodon_major and minor > self.mastodon_minor: | 104 | elif major == self.mastodon_major and minor > self.mastodon_minor: |
99 | print(self.mastodon_minor) | 105 | print(self.mastodon_minor) |
100 | raise MastodonVersionError("Version check failed (Need version " + version + ")") | 106 | raise MastodonVersionError( |
107 | "Version check failed (Need version " + version + ")") | ||
101 | elif major == self.mastodon_major and minor == self.mastodon_minor and patch > self.mastodon_patch: | 108 | elif major == self.mastodon_major and minor == self.mastodon_minor and patch > self.mastodon_patch: |
102 | raise MastodonVersionError("Version check failed (Need version " + version + ", patch is " + str(self.mastodon_patch) + ")") | 109 | raise MastodonVersionError( |
110 | "Version check failed (Need version " + version + ", patch is " + str(self.mastodon_patch) + ")") | ||
103 | return function(self, *args, **kwargs) | 111 | return function(self, *args, **kwargs) |
104 | function.__doc__ = function.__doc__ + "\n\n *Added: Mastodon v" + created_ver + ", last changed: Mastodon v" + last_changed_ver + "*" | 112 | function.__doc__ = function.__doc__ + "\n\n *Added: Mastodon v" + \ |
113 | created_ver + ", last changed: Mastodon v" + last_changed_ver + "*" | ||
105 | return decorate(function, wrapper) | 114 | return decorate(function, wrapper) |
106 | return api_min_version_decorator | 115 | return api_min_version_decorator |
107 | 116 | ||
@@ -109,13 +118,15 @@ def api_version(created_ver, last_changed_ver, return_value_ver): | |||
109 | # Dict helper class. | 118 | # Dict helper class. |
110 | # Defined at top level so it can be pickled. | 119 | # Defined at top level so it can be pickled. |
111 | ### | 120 | ### |
121 | |||
122 | |||
112 | class AttribAccessDict(dict): | 123 | class AttribAccessDict(dict): |
113 | def __getattr__(self, attr): | 124 | def __getattr__(self, attr): |
114 | if attr in self: | 125 | if attr in self: |
115 | return self[attr] | 126 | return self[attr] |
116 | else: | 127 | else: |
117 | raise AttributeError("Attribute not found: " + str(attr)) | 128 | raise AttributeError("Attribute not found: " + str(attr)) |
118 | 129 | ||
119 | def __setattr__(self, attr, val): | 130 | def __setattr__(self, attr, val): |
120 | if attr in self: | 131 | if attr in self: |
121 | raise AttributeError("Attribute-style access is read only") | 132 | raise AttributeError("Attribute-style access is read only") |
@@ -145,10 +156,10 @@ class AttribAccessList(list): | |||
145 | class Mastodon: | 156 | class Mastodon: |
146 | """ | 157 | """ |
147 | Thorough and easy to use Mastodon | 158 | Thorough and easy to use Mastodon |
148 | api wrapper in python. | 159 | API wrapper in Python. |
149 | 160 | ||
150 | If anything is unclear, check the official API docs at | 161 | If anything is unclear, check the official API docs at |
151 | https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md | 162 | https://github.com/mastodon/documentation/blob/master/content/en/client/intro.md |
152 | """ | 163 | """ |
153 | __DEFAULT_BASE_URL = 'https://mastodon.social' | 164 | __DEFAULT_BASE_URL = 'https://mastodon.social' |
154 | __DEFAULT_TIMEOUT = 300 | 165 | __DEFAULT_TIMEOUT = 300 |
@@ -157,29 +168,29 @@ class Mastodon: | |||
157 | __DEFAULT_SCOPES = ['read', 'write', 'follow', 'push'] | 168 | __DEFAULT_SCOPES = ['read', 'write', 'follow', 'push'] |
158 | __SCOPE_SETS = { | 169 | __SCOPE_SETS = { |
159 | 'read': [ | 170 | 'read': [ |
160 | 'read:accounts', | 171 | 'read:accounts', |
161 | 'read:blocks', | 172 | 'read:blocks', |
162 | 'read:favourites', | 173 | 'read:favourites', |
163 | 'read:filters', | 174 | 'read:filters', |
164 | 'read:follows', | 175 | 'read:follows', |
165 | 'read:lists', | 176 | 'read:lists', |
166 | 'read:mutes', | 177 | 'read:mutes', |
167 | 'read:notifications', | 178 | 'read:notifications', |
168 | 'read:search', | 179 | 'read:search', |
169 | 'read:statuses', | 180 | 'read:statuses', |
170 | 'read:bookmarks' | 181 | 'read:bookmarks' |
171 | ], | 182 | ], |
172 | 'write': [ | 183 | 'write': [ |
173 | 'write:accounts', | 184 | 'write:accounts', |
174 | 'write:blocks', | 185 | 'write:blocks', |
175 | 'write:favourites', | 186 | 'write:favourites', |
176 | 'write:filters', | 187 | 'write:filters', |
177 | 'write:follows', | 188 | 'write:follows', |
178 | 'write:lists', | 189 | 'write:lists', |
179 | 'write:media', | 190 | 'write:media', |
180 | 'write:mutes', | 191 | 'write:mutes', |
181 | 'write:notifications', | 192 | 'write:notifications', |
182 | 'write:reports', | 193 | 'write:reports', |
183 | 'write:statuses', | 194 | 'write:statuses', |
184 | 'write:bookmarks' | 195 | 'write:bookmarks' |
185 | ], | 196 | ], |
@@ -187,54 +198,62 @@ class Mastodon: | |||
187 | 'read:blocks', | 198 | 'read:blocks', |
188 | 'read:follows', | 199 | 'read:follows', |
189 | 'read:mutes', | 200 | 'read:mutes', |
190 | 'write:blocks', | 201 | 'write:blocks', |
191 | 'write:follows', | 202 | 'write:follows', |
192 | 'write:mutes', | 203 | 'write:mutes', |
193 | ], | 204 | ], |
194 | 'admin:read': [ | 205 | 'admin:read': [ |
195 | 'admin:read:accounts', | 206 | 'admin:read:accounts', |
196 | 'admin:read:reports', | 207 | 'admin:read:reports', |
197 | ], | 208 | ], |
198 | 'admin:write': [ | 209 | 'admin:write': [ |
199 | 'admin:write:accounts', | 210 | 'admin:write:accounts', |
200 | 'admin:write:reports', | 211 | 'admin:write:reports', |
201 | ], | 212 | ], |
202 | } | 213 | } |
203 | __VALID_SCOPES = ['read', 'write', 'follow', 'push', 'admin:read', 'admin:write'] + \ | 214 | __VALID_SCOPES = ['read', 'write', 'follow', 'push', 'admin:read', 'admin:write'] + \ |
204 | __SCOPE_SETS['read'] + __SCOPE_SETS['write'] + __SCOPE_SETS['admin:read'] + __SCOPE_SETS['admin:write'] | 215 | __SCOPE_SETS['read'] + __SCOPE_SETS['write'] + \ |
205 | 216 | __SCOPE_SETS['admin:read'] + __SCOPE_SETS['admin:write'] | |
217 | |||
206 | __SUPPORTED_MASTODON_VERSION = "3.1.1" | 218 | __SUPPORTED_MASTODON_VERSION = "3.1.1" |
207 | 219 | ||
208 | # Dict versions | 220 | # Dict versions |
209 | __DICT_VERSION_APPLICATION = "2.7.2" | 221 | __DICT_VERSION_APPLICATION = "2.7.2" |
210 | __DICT_VERSION_MENTION = "1.0.0" | 222 | __DICT_VERSION_MENTION = "1.0.0" |
211 | __DICT_VERSION_MEDIA = "3.2.0" | 223 | __DICT_VERSION_MEDIA = "3.2.0" |
212 | __DICT_VERSION_ACCOUNT = "3.3.0" | 224 | __DICT_VERSION_ACCOUNT = "3.3.0" |
213 | __DICT_VERSION_POLL = "2.8.0" | 225 | __DICT_VERSION_POLL = "2.8.0" |
214 | __DICT_VERSION_STATUS = bigger_version(bigger_version(bigger_version(bigger_version(bigger_version("3.1.0", __DICT_VERSION_MEDIA), __DICT_VERSION_ACCOUNT), __DICT_VERSION_APPLICATION), __DICT_VERSION_MENTION), __DICT_VERSION_POLL) | 226 | __DICT_VERSION_STATUS = bigger_version(bigger_version(bigger_version(bigger_version(bigger_version( |
227 | "3.1.0", __DICT_VERSION_MEDIA), __DICT_VERSION_ACCOUNT), __DICT_VERSION_APPLICATION), __DICT_VERSION_MENTION), __DICT_VERSION_POLL) | ||
215 | __DICT_VERSION_INSTANCE = bigger_version("3.1.4", __DICT_VERSION_ACCOUNT) | 228 | __DICT_VERSION_INSTANCE = bigger_version("3.1.4", __DICT_VERSION_ACCOUNT) |
216 | __DICT_VERSION_HASHTAG = "2.3.4" | 229 | __DICT_VERSION_HASHTAG = "2.3.4" |
217 | __DICT_VERSION_EMOJI = "3.0.0" | 230 | __DICT_VERSION_EMOJI = "3.0.0" |
218 | __DICT_VERSION_RELATIONSHIP = "3.3.0" | 231 | __DICT_VERSION_RELATIONSHIP = "3.3.0" |
219 | __DICT_VERSION_NOTIFICATION = bigger_version(bigger_version("1.0.0", __DICT_VERSION_ACCOUNT), __DICT_VERSION_STATUS) | 232 | __DICT_VERSION_NOTIFICATION = bigger_version(bigger_version( |
233 | "1.0.0", __DICT_VERSION_ACCOUNT), __DICT_VERSION_STATUS) | ||
220 | __DICT_VERSION_CONTEXT = bigger_version("1.0.0", __DICT_VERSION_STATUS) | 234 | __DICT_VERSION_CONTEXT = bigger_version("1.0.0", __DICT_VERSION_STATUS) |
221 | __DICT_VERSION_LIST = "2.1.0" | 235 | __DICT_VERSION_LIST = "2.1.0" |
222 | __DICT_VERSION_CARD = "3.2.0" | 236 | __DICT_VERSION_CARD = "3.2.0" |
223 | __DICT_VERSION_SEARCHRESULT = bigger_version(bigger_version(bigger_version("1.0.0", __DICT_VERSION_ACCOUNT), __DICT_VERSION_STATUS), __DICT_VERSION_HASHTAG) | 237 | __DICT_VERSION_SEARCHRESULT = bigger_version(bigger_version(bigger_version( |
224 | __DICT_VERSION_ACTIVITY = "2.1.2" | 238 | "1.0.0", __DICT_VERSION_ACCOUNT), __DICT_VERSION_STATUS), __DICT_VERSION_HASHTAG) |
239 | __DICT_VERSION_ACTIVITY = "2.1.2" | ||
225 | __DICT_VERSION_REPORT = "2.9.1" | 240 | __DICT_VERSION_REPORT = "2.9.1" |
226 | __DICT_VERSION_PUSH = "2.4.0" | 241 | __DICT_VERSION_PUSH = "2.4.0" |
227 | __DICT_VERSION_PUSH_NOTIF = "2.4.0" | 242 | __DICT_VERSION_PUSH_NOTIF = "2.4.0" |
228 | __DICT_VERSION_FILTER = "2.4.3" | 243 | __DICT_VERSION_FILTER = "2.4.3" |
229 | __DICT_VERSION_CONVERSATION = bigger_version(bigger_version("2.6.0", __DICT_VERSION_ACCOUNT), __DICT_VERSION_STATUS) | 244 | __DICT_VERSION_CONVERSATION = bigger_version(bigger_version( |
230 | __DICT_VERSION_SCHEDULED_STATUS = bigger_version("2.7.0", __DICT_VERSION_STATUS) | 245 | "2.6.0", __DICT_VERSION_ACCOUNT), __DICT_VERSION_STATUS) |
246 | __DICT_VERSION_SCHEDULED_STATUS = bigger_version( | ||
247 | "2.7.0", __DICT_VERSION_STATUS) | ||
231 | __DICT_VERSION_PREFERENCES = "2.8.0" | 248 | __DICT_VERSION_PREFERENCES = "2.8.0" |
232 | __DICT_VERSION_ADMIN_ACCOUNT = bigger_version("2.9.1", __DICT_VERSION_ACCOUNT) | 249 | __DICT_VERSION_ADMIN_ACCOUNT = bigger_version( |
250 | "2.9.1", __DICT_VERSION_ACCOUNT) | ||
233 | __DICT_VERSION_FEATURED_TAG = "3.0.0" | 251 | __DICT_VERSION_FEATURED_TAG = "3.0.0" |
234 | __DICT_VERSION_MARKER = "3.0.0" | 252 | __DICT_VERSION_MARKER = "3.0.0" |
235 | __DICT_VERSION_REACTION = "3.1.0" | 253 | __DICT_VERSION_REACTION = "3.1.0" |
236 | __DICT_VERSION_ANNOUNCEMENT = bigger_version("3.1.0", __DICT_VERSION_REACTION) | 254 | __DICT_VERSION_ANNOUNCEMENT = bigger_version( |
237 | 255 | "3.1.0", __DICT_VERSION_REACTION) | |
256 | |||
238 | ### | 257 | ### |
239 | # Registering apps | 258 | # Registering apps |
240 | ### | 259 | ### |
@@ -242,23 +261,23 @@ class Mastodon: | |||
242 | def create_app(client_name, scopes=__DEFAULT_SCOPES, redirect_uris=None, website=None, to_file=None, | 261 | def create_app(client_name, scopes=__DEFAULT_SCOPES, redirect_uris=None, website=None, to_file=None, |
243 | api_base_url=__DEFAULT_BASE_URL, request_timeout=__DEFAULT_TIMEOUT, session=None): | 262 | api_base_url=__DEFAULT_BASE_URL, request_timeout=__DEFAULT_TIMEOUT, session=None): |
244 | """ | 263 | """ |
245 | Create a new app with given `client_name` and `scopes` (The basic scropse are "read", "write", "follow" and "push" | 264 | Create a new app with given `client_name` and `scopes` (The basic scopes are "read", "write", "follow" and "push" |
246 | - more granular scopes are available, please refere to Mastodon documentation for which). | 265 | - more granular scopes are available, please refere to Mastodon documentation for which). |
247 | 266 | ||
248 | Specify `redirect_uris` if you want users to be redirected to a certain page after authenticating in an oauth flow. | 267 | Specify `redirect_uris` if you want users to be redirected to a certain page after authenticating in an OAuth flow. |
249 | You can specify multiple URLs by passing a list. Note that if you wish to use OAuth authentication with redirects, | 268 | You can specify multiple URLs by passing a list. Note that if you wish to use OAuth authentication with redirects, |
250 | the redirect URI must be one of the URLs specified here. | 269 | the redirect URI must be one of the URLs specified here. |
251 | 270 | ||
252 | Specify `to_file` to persist your apps info to a file so you can use them in the constructor. | 271 | Specify `to_file` to persist your app's info to a file so you can use it in the constructor. |
253 | Specify `api_base_url` if you want to register an app on an instance different from the flagship one. | 272 | Specify `api_base_url` if you want to register an app on an instance different from the flagship one. |
254 | Specify `website` to give a website for your app. | 273 | Specify `website` to give a website for your app. |
255 | 274 | ||
256 | Specify `session` with a requests.Session for it to be used instead of the deafult. This can be | 275 | Specify `session` with a requests.Session for it to be used instead of the default. This can be |
257 | used to, amongst other things, adjust proxy or ssl certificate settings. | 276 | used to, amongst other things, adjust proxy or SSL certificate settings. |
258 | 277 | ||
259 | Presently, app registration is open by default, but this is not guaranteed to be the case for all | 278 | Presently, app registration is open by default, but this is not guaranteed to be the case for all |
260 | future mastodon instances or even the flagship instance in the future. | 279 | Mastodon instances in the future. |
261 | 280 | ||
262 | 281 | ||
263 | Returns `client_id` and `client_secret`, both as strings. | 282 | Returns `client_id` and `client_secret`, both as strings. |
264 | """ | 283 | """ |
@@ -279,10 +298,12 @@ class Mastodon: | |||
279 | if website is not None: | 298 | if website is not None: |
280 | request_data['website'] = website | 299 | request_data['website'] = website |
281 | if session: | 300 | if session: |
282 | ret = session.post(api_base_url + '/api/v1/apps', data=request_data, timeout=request_timeout) | 301 | ret = session.post(api_base_url + '/api/v1/apps', |
302 | data=request_data, timeout=request_timeout) | ||
283 | response = ret.json() | 303 | response = ret.json() |
284 | else: | 304 | else: |
285 | response = requests.post(api_base_url + '/api/v1/apps', data=request_data, timeout=request_timeout) | 305 | response = requests.post( |
306 | api_base_url + '/api/v1/apps', data=request_data, timeout=request_timeout) | ||
286 | response = response.json() | 307 | response = response.json() |
287 | except Exception as e: | 308 | except Exception as e: |
288 | raise MastodonNetworkError("Could not complete request: %s" % e) | 309 | raise MastodonNetworkError("Could not complete request: %s" % e) |
@@ -293,7 +314,7 @@ class Mastodon: | |||
293 | secret_file.write(response['client_secret'] + "\n") | 314 | secret_file.write(response['client_secret'] + "\n") |
294 | secret_file.write(api_base_url + "\n") | 315 | secret_file.write(api_base_url + "\n") |
295 | secret_file.write(client_name + "\n") | 316 | secret_file.write(client_name + "\n") |
296 | 317 | ||
297 | return (response['client_id'], response['client_secret']) | 318 | return (response['client_id'], response['client_secret']) |
298 | 319 | ||
299 | ### | 320 | ### |
@@ -303,13 +324,13 @@ class Mastodon: | |||
303 | api_base_url=None, debug_requests=False, | 324 | api_base_url=None, debug_requests=False, |
304 | ratelimit_method="wait", ratelimit_pacefactor=1.1, | 325 | ratelimit_method="wait", ratelimit_pacefactor=1.1, |
305 | request_timeout=__DEFAULT_TIMEOUT, mastodon_version=None, | 326 | request_timeout=__DEFAULT_TIMEOUT, mastodon_version=None, |
306 | version_check_mode = "created", session=None, feature_set="mainline", user_agent="mastodonpy"): | 327 | version_check_mode="created", session=None, feature_set="mainline", user_agent="mastodonpy"): |
307 | """ | 328 | """ |
308 | Create a new API wrapper instance based on the given `client_secret` and `client_id`. If you | 329 | Create a new API wrapper instance based on the given `client_secret` and `client_id`. If you |
309 | give a `client_id` and it is not a file, you must also give a secret. If you specify an | 330 | give a `client_id` and it is not a file, you must also give a secret. If you specify an |
310 | `access_token` then you don't need to specify a `client_id`. It is allowed to specify | 331 | `access_token` then you don't need to specify a `client_id`. It is allowed to specify |
311 | neither - in this case, you will be restricted to only using endpoints that do not | 332 | neither - in this case, you will be restricted to only using endpoints that do not |
312 | require authentication. If a file is given as `client_id`, client ID, secret and | 333 | require authentication. If a file is given as `client_id`, client ID, secret and |
313 | base url are read from that file. | 334 | base url are read from that file. |
314 | 335 | ||
315 | You can also specify an `access_token`, directly or as a file (as written by `log_in()`_). If | 336 | You can also specify an `access_token`, directly or as a file (as written by `log_in()`_). If |
@@ -320,7 +341,7 @@ class Mastodon: | |||
320 | "throw" makes functions throw a `MastodonRatelimitError` when the rate | 341 | "throw" makes functions throw a `MastodonRatelimitError` when the rate |
321 | limit is hit. "wait" mode will, once the limit is hit, wait and retry the request as soon | 342 | limit is hit. "wait" mode will, once the limit is hit, wait and retry the request as soon |
322 | as the rate limit resets, until it succeeds. "pace" works like throw, but tries to wait in | 343 | as the rate limit resets, until it succeeds. "pace" works like throw, but tries to wait in |
323 | between calls so that the limit is generally not hit (How hard it tries to not hit the rate | 344 | between calls so that the limit is generally not hit (how hard it tries to avoid hitting the rate |
324 | limit can be controlled by ratelimit_pacefactor). The default setting is "wait". Note that | 345 | limit can be controlled by ratelimit_pacefactor). The default setting is "wait". Note that |
325 | even in "wait" and "pace" mode, requests can still fail due to network or other problems! Also | 346 | even in "wait" and "pace" mode, requests can still fail due to network or other problems! Also |
326 | note that "pace" and "wait" are NOT thread safe. | 347 | note that "pace" and "wait" are NOT thread safe. |
@@ -333,33 +354,33 @@ class Mastodon: | |||
333 | pass the desired timeout (in seconds) as `request_timeout`. | 354 | pass the desired timeout (in seconds) as `request_timeout`. |
334 | 355 | ||
335 | For fine-tuned control over the requests object use `session` with a requests.Session. | 356 | For fine-tuned control over the requests object use `session` with a requests.Session. |
336 | 357 | ||
337 | The `mastodon_version` parameter can be used to specify the version of Mastodon that Mastodon.py will | 358 | The `mastodon_version` parameter can be used to specify the version of Mastodon that Mastodon.py will |
338 | expect to be installed on the server. The function will throw an error if an unparseable | 359 | expect to be installed on the server. The function will throw an error if an unparseable |
339 | Version is specified. If no version is specified, Mastodon.py will set `mastodon_version` to the | 360 | Version is specified. If no version is specified, Mastodon.py will set `mastodon_version` to the |
340 | detected version. | 361 | detected version. |
341 | 362 | ||
342 | The version check mode can be set to "created" (the default behaviour), "changed" or "none". If set to | 363 | The version check mode can be set to "created" (the default behaviour), "changed" or "none". If set to |
343 | "created", Mastodon.py will throw an error if the version of Mastodon it is connected to is too old | 364 | "created", Mastodon.py will throw an error if the version of Mastodon it is connected to is too old |
344 | to have an endpoint. If it is set to "changed", it will throw an error if the endpoints behaviour has | 365 | to have an endpoint. If it is set to "changed", it will throw an error if the endpoint's behaviour has |
345 | changed after the version of Mastodon that is connected has been released. If it is set to "none", | 366 | changed after the version of Mastodon that is connected has been released. If it is set to "none", |
346 | version checking is disabled. | 367 | version checking is disabled. |
347 | 368 | ||
348 | `feature_set` can be used to enable behaviour specific to non-mainline Mastodon API implementations. | 369 | `feature_set` can be used to enable behaviour specific to non-mainline Mastodon API implementations. |
349 | Details are documented in the functions that provide such functionality. Currently supported feature | 370 | Details are documented in the functions that provide such functionality. Currently supported feature |
350 | sets are `mainline`, `fedibird` and `pleroma`. | 371 | sets are `mainline`, `fedibird` and `pleroma`. |
351 | 372 | ||
352 | For some mastodon-instances a `User-Agent` header is needed. This can be set by parameter `user_agent`. Starting from | 373 | For some Mastodon instances a `User-Agent` header is needed. This can be set by parameter `user_agent`. Starting from |
353 | Mastodon.py 1.5.2 `create_app()` stores the application name into the client secret file. If `client_id` points to this file, | 374 | Mastodon.py 1.5.2 `create_app()` stores the application name into the client secret file. If `client_id` points to this file, |
354 | the app name will be used as `User-Agent` header as default. It's possible to modify old secret files and append | 375 | the app name will be used as `User-Agent` header as default. It is possible to modify old secret files and append |
355 | a client app name to use it as a `User-Agent` name. | 376 | a client app name to use it as a `User-Agent` name. |
356 | 377 | ||
357 | If no other user agent is specified, "mastodonpy" will be used. | 378 | If no other `User-Agent` is specified, "mastodonpy" will be used. |
358 | """ | 379 | """ |
359 | self.api_base_url = None | 380 | self.api_base_url = None |
360 | if not api_base_url is None: | 381 | if not api_base_url is None: |
361 | self.api_base_url = Mastodon.__protocolize(api_base_url) | 382 | self.api_base_url = Mastodon.__protocolize(api_base_url) |
362 | 383 | ||
363 | self.client_id = client_id | 384 | self.client_id = client_id |
364 | self.client_secret = client_secret | 385 | self.client_secret = client_secret |
365 | self.access_token = access_token | 386 | self.access_token = access_token |
@@ -367,9 +388,9 @@ class Mastodon: | |||
367 | self.ratelimit_method = ratelimit_method | 388 | self.ratelimit_method = ratelimit_method |
368 | self._token_expired = datetime.datetime.now() | 389 | self._token_expired = datetime.datetime.now() |
369 | self._refresh_token = None | 390 | self._refresh_token = None |
370 | 391 | ||
371 | self.__logged_in_id = None | 392 | self.__logged_in_id = None |
372 | 393 | ||
373 | self.ratelimit_limit = 300 | 394 | self.ratelimit_limit = 300 |
374 | self.ratelimit_reset = time.time() | 395 | self.ratelimit_reset = time.time() |
375 | self.ratelimit_remaining = 300 | 396 | self.ratelimit_remaining = 300 |
@@ -389,19 +410,20 @@ class Mastodon: | |||
389 | 410 | ||
390 | # General defined user-agent | 411 | # General defined user-agent |
391 | self.user_agent = user_agent | 412 | self.user_agent = user_agent |
392 | 413 | ||
393 | # Token loading | 414 | # Token loading |
394 | if self.client_id is not None: | 415 | if self.client_id is not None: |
395 | if os.path.isfile(self.client_id): | 416 | if os.path.isfile(self.client_id): |
396 | with open(self.client_id, 'r') as secret_file: | 417 | with open(self.client_id, 'r') as secret_file: |
397 | self.client_id = secret_file.readline().rstrip() | 418 | self.client_id = secret_file.readline().rstrip() |
398 | self.client_secret = secret_file.readline().rstrip() | 419 | self.client_secret = secret_file.readline().rstrip() |
399 | 420 | ||
400 | try_base_url = secret_file.readline().rstrip() | 421 | try_base_url = secret_file.readline().rstrip() |
401 | if (not try_base_url is None) and len(try_base_url) != 0: | 422 | if (not try_base_url is None) and len(try_base_url) != 0: |
402 | try_base_url = Mastodon.__protocolize(try_base_url) | 423 | try_base_url = Mastodon.__protocolize(try_base_url) |
403 | if not (self.api_base_url is None or try_base_url == self.api_base_url): | 424 | if not (self.api_base_url is None or try_base_url == self.api_base_url): |
404 | raise MastodonIllegalArgumentError('Mismatch in base URLs between files and/or specified') | 425 | raise MastodonIllegalArgumentError( |
426 | 'Mismatch in base URLs between files and/or specified') | ||
405 | self.api_base_url = try_base_url | 427 | self.api_base_url = try_base_url |
406 | 428 | ||
407 | # With new registrations we support the 4th line to store a client_name and use it as user-agent | 429 | # With new registrations we support the 4th line to store a client_name and use it as user-agent |
@@ -410,17 +432,19 @@ class Mastodon: | |||
410 | self.user_agent = client_name.rstrip() | 432 | self.user_agent = client_name.rstrip() |
411 | else: | 433 | else: |
412 | if self.client_secret is None: | 434 | if self.client_secret is None: |
413 | raise MastodonIllegalArgumentError('Specified client id directly, but did not supply secret') | 435 | raise MastodonIllegalArgumentError( |
436 | 'Specified client id directly, but did not supply secret') | ||
414 | 437 | ||
415 | if self.access_token is not None and os.path.isfile(self.access_token): | 438 | if self.access_token is not None and os.path.isfile(self.access_token): |
416 | with open(self.access_token, 'r') as token_file: | 439 | with open(self.access_token, 'r') as token_file: |
417 | self.access_token = token_file.readline().rstrip() | 440 | self.access_token = token_file.readline().rstrip() |
418 | 441 | ||
419 | try_base_url = token_file.readline().rstrip() | 442 | try_base_url = token_file.readline().rstrip() |
420 | if (not try_base_url is None) and len(try_base_url) != 0: | 443 | if (not try_base_url is None) and len(try_base_url) != 0: |
421 | try_base_url = Mastodon.__protocolize(try_base_url) | 444 | try_base_url = Mastodon.__protocolize(try_base_url) |
422 | if not (self.api_base_url is None or try_base_url == self.api_base_url): | 445 | if not (self.api_base_url is None or try_base_url == self.api_base_url): |
423 | raise MastodonIllegalArgumentError('Mismatch in base URLs between files and/or specified') | 446 | raise MastodonIllegalArgumentError( |
447 | 'Mismatch in base URLs between files and/or specified') | ||
424 | self.api_base_url = try_base_url | 448 | self.api_base_url = try_base_url |
425 | 449 | ||
426 | if not version_check_mode in ["created", "changed", "none"]: | 450 | if not version_check_mode in ["created", "changed", "none"]: |
@@ -430,25 +454,25 @@ class Mastodon: | |||
430 | self.mastodon_major = 1 | 454 | self.mastodon_major = 1 |
431 | self.mastodon_minor = 0 | 455 | self.mastodon_minor = 0 |
432 | self.mastodon_patch = 0 | 456 | self.mastodon_patch = 0 |
433 | 457 | ||
434 | # Versioning | 458 | # Versioning |
435 | if mastodon_version == None and self.version_check_mode != 'none': | 459 | if mastodon_version == None and self.version_check_mode != 'none': |
436 | self.retrieve_mastodon_version() | 460 | self.retrieve_mastodon_version() |
437 | elif self.version_check_mode != 'none': | 461 | elif self.version_check_mode != 'none': |
438 | try: | 462 | try: |
439 | self.mastodon_major, self.mastodon_minor, self.mastodon_patch = parse_version_string(mastodon_version) | 463 | self.mastodon_major, self.mastodon_minor, self.mastodon_patch = parse_version_string( |
464 | mastodon_version) | ||
440 | except: | 465 | except: |
441 | raise MastodonVersionError("Bad version specified") | 466 | raise MastodonVersionError("Bad version specified") |
442 | 467 | ||
443 | # Ratelimiting parameter check | 468 | # Ratelimiting parameter check |
444 | if ratelimit_method not in ["throw", "wait", "pace"]: | 469 | if ratelimit_method not in ["throw", "wait", "pace"]: |
445 | raise MastodonIllegalArgumentError("Invalid ratelimit method.") | 470 | raise MastodonIllegalArgumentError("Invalid ratelimit method.") |
446 | 471 | ||
447 | |||
448 | def retrieve_mastodon_version(self): | 472 | def retrieve_mastodon_version(self): |
449 | """ | 473 | """ |
450 | Determine installed mastodon version and set major, minor and patch (not including RC info) accordingly. | 474 | Determine installed Mastodon version and set major, minor and patch (not including RC info) accordingly. |
451 | 475 | ||
452 | Returns the version string, possibly including rc info. | 476 | Returns the version string, possibly including rc info. |
453 | """ | 477 | """ |
454 | try: | 478 | try: |
@@ -456,16 +480,17 @@ class Mastodon: | |||
456 | except: | 480 | except: |
457 | # instance() was added in 1.1.0, so our best guess is 1.0.0. | 481 | # instance() was added in 1.1.0, so our best guess is 1.0.0. |
458 | version_str = "1.0.0" | 482 | version_str = "1.0.0" |
459 | 483 | ||
460 | self.mastodon_major, self.mastodon_minor, self.mastodon_patch = parse_version_string(version_str) | 484 | self.mastodon_major, self.mastodon_minor, self.mastodon_patch = parse_version_string( |
485 | version_str) | ||
461 | return version_str | 486 | return version_str |
462 | 487 | ||
463 | def verify_minimum_version(self, version_str, cached=False): | 488 | def verify_minimum_version(self, version_str, cached=False): |
464 | """ | 489 | """ |
465 | Update version info from server and verify that at least the specified version is present. | 490 | Update version info from server and verify that at least the specified version is present. |
466 | 491 | ||
467 | If you specify "cached", the version info update part is skipped. | 492 | If you specify "cached", the version info update part is skipped. |
468 | 493 | ||
469 | Returns True if version requirement is satisfied, False if not. | 494 | Returns True if version requirement is satisfied, False if not. |
470 | """ | 495 | """ |
471 | if not cached: | 496 | if not cached: |
@@ -485,21 +510,24 @@ class Mastodon: | |||
485 | Retrieve the maximum version of Mastodon supported by this version of Mastodon.py | 510 | Retrieve the maximum version of Mastodon supported by this version of Mastodon.py |
486 | """ | 511 | """ |
487 | return Mastodon.__SUPPORTED_MASTODON_VERSION | 512 | return Mastodon.__SUPPORTED_MASTODON_VERSION |
488 | 513 | ||
514 | def auth_request_url(self, client_id=None, redirect_uris="urn:ietf:wg:oauth:2.0:oob", | ||
515 | scopes=__DEFAULT_SCOPES, force_login=False): | ||
516 | |||
489 | def auth_request_url(self, client_id=None, redirect_uris="urn:ietf:wg:oauth:2.0:oob", scopes=__DEFAULT_SCOPES, force_login=False, state=None): | 517 | def auth_request_url(self, client_id=None, redirect_uris="urn:ietf:wg:oauth:2.0:oob", scopes=__DEFAULT_SCOPES, force_login=False, state=None): |
490 | """ | 518 | """ |
491 | Returns the url that a client needs to request an oauth grant from the server. | 519 | Returns the URL that a client needs to request an OAuth grant from the server. |
492 | 520 | ||
493 | To log in with oauth, send your user to this URL. The user will then log in and | 521 | To log in with OAuth, send your user to this URL. The user will then log in and |
494 | get a code which you can pass to log_in. | 522 | get a code which you can pass to log_in. |
495 | 523 | ||
496 | scopes are as in `log_in()`_, redirect_uris is where the user should be redirected to | 524 | scopes are as in `log_in()`_, redirect_uris is where the user should be redirected to |
497 | after authentication. Note that redirect_uris must be one of the URLs given during | 525 | after authentication. Note that redirect_uris must be one of the URLs given during |
498 | app registration. When using urn:ietf:wg:oauth:2.0:oob, the code is simply displayed, | 526 | app registration. When using urn:ietf:wg:oauth:2.0:oob, the code is simply displayed, |
499 | otherwise it is added to the given URL as the "code" request parameter. | 527 | otherwise it is added to the given URL as the "code" request parameter. |
500 | 528 | ||
501 | Pass force_login if you want the user to always log in even when already logged | 529 | Pass force_login if you want the user to always log in even when already logged |
502 | into web mastodon (i.e. when registering multiple different accounts in an app). | 530 | into web Mastodon (i.e. when registering multiple different accounts in an app). |
503 | 531 | ||
504 | State is the oauth `state`parameter to pass to the server. It is strongly suggested | 532 | State is the oauth `state`parameter to pass to the server. It is strongly suggested |
505 | to use a random, nonguessable value (i.e. nothing meaningful and no incrementing ID) | 533 | to use a random, nonguessable value (i.e. nothing meaningful and no incrementing ID) |
@@ -525,18 +553,18 @@ class Mastodon: | |||
525 | def log_in(self, username=None, password=None, code=None, redirect_uri="urn:ietf:wg:oauth:2.0:oob", refresh_token=None, scopes=__DEFAULT_SCOPES, to_file=None): | 553 | def log_in(self, username=None, password=None, code=None, redirect_uri="urn:ietf:wg:oauth:2.0:oob", refresh_token=None, scopes=__DEFAULT_SCOPES, to_file=None): |
526 | """ | 554 | """ |
527 | Get the access token for a user. | 555 | Get the access token for a user. |
528 | 556 | ||
529 | The username is the e-mail used to log in into mastodon. | 557 | The username is the email address used to log in into Mastodon. |
530 | 558 | ||
531 | Can persist access token to file `to_file`, to be used in the constructor. | 559 | Can persist access token to file `to_file`, to be used in the constructor. |
532 | 560 | ||
533 | Handles password and OAuth-based authorization. | 561 | Handles password and OAuth-based authorization. |
534 | 562 | ||
535 | Will throw a `MastodonIllegalArgumentError` if the OAuth or the | 563 | Will throw a `MastodonIllegalArgumentError` if the OAuth or the |
536 | username / password credentials given are incorrect, and | 564 | username / password credentials given are incorrect, and |
537 | `MastodonAPIError` if all of the requested scopes were not granted. | 565 | `MastodonAPIError` if all of the requested scopes were not granted. |
538 | 566 | ||
539 | For OAuth2, obtain a code via having your user go to the url returned by | 567 | For OAuth 2, obtain a code via having your user go to the URL returned by |
540 | `auth_request_url()`_ and pass it as the code parameter. In this case, | 568 | `auth_request_url()`_ and pass it as the code parameter. In this case, |
541 | make sure to also pass the same redirect_uri parameter as you used when | 569 | make sure to also pass the same redirect_uri parameter as you used when |
542 | generating the auth request URL. | 570 | generating the auth request URL. |
@@ -544,31 +572,38 @@ class Mastodon: | |||
544 | Returns the access token as a string. | 572 | Returns the access token as a string. |
545 | """ | 573 | """ |
546 | if username is not None and password is not None: | 574 | if username is not None and password is not None: |
547 | params = self.__generate_params(locals(), ['scopes', 'to_file', 'code', 'refresh_token']) | 575 | params = self.__generate_params( |
576 | locals(), ['scopes', 'to_file', 'code', 'refresh_token']) | ||
548 | params['grant_type'] = 'password' | 577 | params['grant_type'] = 'password' |
549 | elif code is not None: | 578 | elif code is not None: |
550 | params = self.__generate_params(locals(), ['scopes', 'to_file', 'username', 'password', 'refresh_token']) | 579 | params = self.__generate_params( |
580 | locals(), ['scopes', 'to_file', 'username', 'password', 'refresh_token']) | ||
551 | params['grant_type'] = 'authorization_code' | 581 | params['grant_type'] = 'authorization_code' |
552 | elif refresh_token is not None: | 582 | elif refresh_token is not None: |
553 | params = self.__generate_params(locals(), ['scopes', 'to_file', 'username', 'password', 'code']) | 583 | params = self.__generate_params( |
584 | locals(), ['scopes', 'to_file', 'username', 'password', 'code']) | ||
554 | params['grant_type'] = 'refresh_token' | 585 | params['grant_type'] = 'refresh_token' |
555 | else: | 586 | else: |
556 | raise MastodonIllegalArgumentError('Invalid arguments given. username and password or code are required.') | 587 | raise MastodonIllegalArgumentError( |
588 | 'Invalid arguments given. username and password or code are required.') | ||
557 | 589 | ||
558 | params['client_id'] = self.client_id | 590 | params['client_id'] = self.client_id |
559 | params['client_secret'] = self.client_secret | 591 | params['client_secret'] = self.client_secret |
560 | params['scope'] = " ".join(scopes) | 592 | params['scope'] = " ".join(scopes) |
561 | 593 | ||
562 | try: | 594 | try: |
563 | response = self.__api_request('POST', '/oauth/token', params, do_ratelimiting=False) | 595 | response = self.__api_request( |
596 | 'POST', '/oauth/token', params, do_ratelimiting=False) | ||
564 | self.access_token = response['access_token'] | 597 | self.access_token = response['access_token'] |
565 | self.__set_refresh_token(response.get('refresh_token')) | 598 | self.__set_refresh_token(response.get('refresh_token')) |
566 | self.__set_token_expired(int(response.get('expires_in', 0))) | 599 | self.__set_token_expired(int(response.get('expires_in', 0))) |
567 | except Exception as e: | 600 | except Exception as e: |
568 | if username is not None or password is not None: | 601 | if username is not None or password is not None: |
569 | raise MastodonIllegalArgumentError('Invalid user name, password, or redirect_uris: %s' % e) | 602 | raise MastodonIllegalArgumentError( |
603 | 'Invalid user name, password, or redirect_uris: %s' % e) | ||
570 | elif code is not None: | 604 | elif code is not None: |
571 | raise MastodonIllegalArgumentError('Invalid access token or redirect_uris: %s' % e) | 605 | raise MastodonIllegalArgumentError( |
606 | 'Invalid access token or redirect_uris: %s' % e) | ||
572 | else: | 607 | else: |
573 | raise MastodonIllegalArgumentError('Invalid request: %s' % e) | 608 | raise MastodonIllegalArgumentError('Invalid request: %s' % e) |
574 | 609 | ||
@@ -576,7 +611,7 @@ class Mastodon: | |||
576 | for scope_set in self.__SCOPE_SETS.keys(): | 611 | for scope_set in self.__SCOPE_SETS.keys(): |
577 | if scope_set in received_scopes: | 612 | if scope_set in received_scopes: |
578 | received_scopes += self.__SCOPE_SETS[scope_set] | 613 | received_scopes += self.__SCOPE_SETS[scope_set] |
579 | 614 | ||
580 | if not set(scopes) <= set(received_scopes): | 615 | if not set(scopes) <= set(received_scopes): |
581 | raise MastodonAPIError( | 616 | raise MastodonAPIError( |
582 | 'Granted scopes "' + " ".join(received_scopes) + '" do not contain all of the requested scopes "' + " ".join(scopes) + '".') | 617 | 'Granted scopes "' + " ".join(received_scopes) + '" do not contain all of the requested scopes "' + " ".join(scopes) + '".') |
@@ -585,11 +620,12 @@ class Mastodon: | |||
585 | with open(to_file, 'w') as token_file: | 620 | with open(to_file, 'w') as token_file: |
586 | token_file.write(response['access_token'] + "\n") | 621 | token_file.write(response['access_token'] + "\n") |
587 | token_file.write(self.api_base_url + "\n") | 622 | token_file.write(self.api_base_url + "\n") |
588 | 623 | ||
589 | self.__logged_in_id = None | 624 | self.__logged_in_id = None |
590 | 625 | ||
591 | return response['access_token'] | 626 | return response['access_token'] |
592 | 627 | ||
628 | |||
593 | def revoke_access_token(self): | 629 | def revoke_access_token(self): |
594 | """ | 630 | """ |
595 | Revoke the oauth token the user is currently authenticated with, effectively removing | 631 | Revoke the oauth token the user is currently authenticated with, effectively removing |
@@ -604,7 +640,7 @@ class Mastodon: | |||
604 | params['client_secret'] = self.client_secret | 640 | params['client_secret'] = self.client_secret |
605 | params['token'] = self.access_token | 641 | params['token'] = self.access_token |
606 | self.__api_request('POST', '/oauth/revoke', params) | 642 | self.__api_request('POST', '/oauth/revoke', params) |
607 | 643 | ||
608 | # We are now logged out, clear token and logged in id | 644 | # We are now logged out, clear token and logged in id |
609 | self.access_token = None | 645 | self.access_token = None |
610 | self.__logged_in_id = None | 646 | self.__logged_in_id = None |
@@ -613,26 +649,26 @@ class Mastodon: | |||
613 | def create_account(self, username, password, email, agreement=False, reason=None, locale="en", scopes=__DEFAULT_SCOPES, to_file=None): | 649 | def create_account(self, username, password, email, agreement=False, reason=None, locale="en", scopes=__DEFAULT_SCOPES, to_file=None): |
614 | """ | 650 | """ |
615 | Creates a new user account with the given username, password and email. "agreement" | 651 | Creates a new user account with the given username, password and email. "agreement" |
616 | must be set to true (after showing the user the instances user agreement and having | 652 | must be set to true (after showing the user the instance's user agreement and having |
617 | them agree to it), "locale" specifies the language for the confirmation e-mail as an | 653 | them agree to it), "locale" specifies the language for the confirmation email as an |
618 | ISO 639-1 (two-letter) language code. `reason` can be used to specify why a user | 654 | ISO 639-1 (two-letter) language code. `reason` can be used to specify why a user |
619 | would like to join if approved-registrations mode is on. | 655 | would like to join if approved-registrations mode is on. |
620 | 656 | ||
621 | Does not require an access token, but does require a client grant. | 657 | Does not require an access token, but does require a client grant. |
622 | 658 | ||
623 | By default, this method is rate-limited by IP to 5 requests per 30 minutes. | 659 | By default, this method is rate-limited by IP to 5 requests per 30 minutes. |
624 | 660 | ||
625 | Returns an access token (just like log_in), which it can also persist to to_file, | 661 | Returns an access token (just like log_in), which it can also persist to to_file, |
626 | and sets it internally so that the user is now logged in. Note that this token | 662 | and sets it internally so that the user is now logged in. Note that this token |
627 | can only be used after the user has confirmed their e-mail. | 663 | can only be used after the user has confirmed their email. |
628 | """ | 664 | """ |
629 | params = self.__generate_params(locals(), ['to_file', 'scopes']) | 665 | params = self.__generate_params(locals(), ['to_file', 'scopes']) |
630 | params['client_id'] = self.client_id | 666 | params['client_id'] = self.client_id |
631 | params['client_secret'] = self.client_secret | 667 | params['client_secret'] = self.client_secret |
632 | 668 | ||
633 | if agreement == False: | 669 | if agreement == False: |
634 | del params['agreement'] | 670 | del params['agreement'] |
635 | 671 | ||
636 | # Step 1: Get a user-free token via oauth | 672 | # Step 1: Get a user-free token via oauth |
637 | try: | 673 | try: |
638 | oauth_params = {} | 674 | oauth_params = {} |
@@ -640,41 +676,43 @@ class Mastodon: | |||
640 | oauth_params['client_id'] = self.client_id | 676 | oauth_params['client_id'] = self.client_id |
641 | oauth_params['client_secret'] = self.client_secret | 677 | oauth_params['client_secret'] = self.client_secret |
642 | oauth_params['grant_type'] = 'client_credentials' | 678 | oauth_params['grant_type'] = 'client_credentials' |
643 | 679 | ||
644 | response = self.__api_request('POST', '/oauth/token', oauth_params, do_ratelimiting=False) | 680 | response = self.__api_request( |
681 | 'POST', '/oauth/token', oauth_params, do_ratelimiting=False) | ||
645 | temp_access_token = response['access_token'] | 682 | temp_access_token = response['access_token'] |
646 | except Exception as e: | 683 | except Exception as e: |
647 | raise MastodonIllegalArgumentError('Invalid request during oauth phase: %s' % e) | 684 | raise MastodonIllegalArgumentError( |
648 | 685 | 'Invalid request during oauth phase: %s' % e) | |
686 | |||
649 | # Step 2: Use that to create a user | 687 | # Step 2: Use that to create a user |
650 | try: | 688 | try: |
651 | response = self.__api_request('POST', '/api/v1/accounts', params, do_ratelimiting=False, | 689 | response = self.__api_request('POST', '/api/v1/accounts', params, do_ratelimiting=False, |
652 | access_token_override = temp_access_token) | 690 | access_token_override=temp_access_token) |
653 | self.access_token = response['access_token'] | 691 | self.access_token = response['access_token'] |
654 | self.__set_refresh_token(response.get('refresh_token')) | 692 | self.__set_refresh_token(response.get('refresh_token')) |
655 | self.__set_token_expired(int(response.get('expires_in', 0))) | 693 | self.__set_token_expired(int(response.get('expires_in', 0))) |
656 | except Exception as e: | 694 | except Exception as e: |
657 | raise MastodonIllegalArgumentError('Invalid request: %s' % e) | 695 | raise MastodonIllegalArgumentError('Invalid request: %s' % e) |
658 | 696 | ||
659 | # Step 3: Check scopes, persist, et cetera | 697 | # Step 3: Check scopes, persist, et cetera |
660 | received_scopes = response["scope"].split(" ") | 698 | received_scopes = response["scope"].split(" ") |
661 | for scope_set in self.__SCOPE_SETS.keys(): | 699 | for scope_set in self.__SCOPE_SETS.keys(): |
662 | if scope_set in received_scopes: | 700 | if scope_set in received_scopes: |
663 | received_scopes += self.__SCOPE_SETS[scope_set] | 701 | received_scopes += self.__SCOPE_SETS[scope_set] |
664 | 702 | ||
665 | if not set(scopes) <= set(received_scopes): | 703 | if not set(scopes) <= set(received_scopes): |
666 | raise MastodonAPIError( | 704 | raise MastodonAPIError( |
667 | 'Granted scopes "' + " ".join(received_scopes) + '" do not contain all of the requested scopes "' + " ".join(scopes) + '".') | 705 | 'Granted scopes "' + " ".join(received_scopes) + '" do not contain all of the requested scopes "' + " ".join(scopes) + '".') |
668 | 706 | ||
669 | if to_file is not None: | 707 | if to_file is not None: |
670 | with open(to_file, 'w') as token_file: | 708 | with open(to_file, 'w') as token_file: |
671 | token_file.write(response['access_token'] + "\n") | 709 | token_file.write(response['access_token'] + "\n") |
672 | token_file.write(self.api_base_url + "\n") | 710 | token_file.write(self.api_base_url + "\n") |
673 | 711 | ||
674 | self.__logged_in_id = None | 712 | self.__logged_in_id = None |
675 | 713 | ||
676 | return response['access_token'] | 714 | return response['access_token'] |
677 | 715 | ||
678 | ### | 716 | ### |
679 | # Reading data: Instances | 717 | # Reading data: Instances |
680 | ### | 718 | ### |
@@ -701,9 +739,9 @@ class Mastodon: | |||
701 | """ | 739 | """ |
702 | Retrieve activity stats about the instance. May be disabled by the instance administrator - throws | 740 | Retrieve activity stats about the instance. May be disabled by the instance administrator - throws |
703 | a MastodonNotFoundError in that case. | 741 | a MastodonNotFoundError in that case. |
704 | 742 | ||
705 | Activity is returned for 12 weeks going back from the current week. | 743 | Activity is returned for 12 weeks going back from the current week. |
706 | 744 | ||
707 | Returns a list of `activity dicts`_. | 745 | Returns a list of `activity dicts`_. |
708 | """ | 746 | """ |
709 | return self.__api_request('GET', '/api/v1/instance/activity') | 747 | return self.__api_request('GET', '/api/v1/instance/activity') |
@@ -713,7 +751,7 @@ class Mastodon: | |||
713 | """ | 751 | """ |
714 | Retrieve the instances that this instance knows about. May be disabled by the instance administrator - throws | 752 | Retrieve the instances that this instance knows about. May be disabled by the instance administrator - throws |
715 | a MastodonNotFoundError in that case. | 753 | a MastodonNotFoundError in that case. |
716 | 754 | ||
717 | Returns a list of URL strings. | 755 | Returns a list of URL strings. |
718 | """ | 756 | """ |
719 | return self.__api_request('GET', '/api/v1/instance/peers') | 757 | return self.__api_request('GET', '/api/v1/instance/peers') |
@@ -723,37 +761,39 @@ class Mastodon: | |||
723 | """ | 761 | """ |
724 | Basic health check. Returns True if healthy, False if not. | 762 | Basic health check. Returns True if healthy, False if not. |
725 | """ | 763 | """ |
726 | status = self.__api_request('GET', '/health', parse=False).decode("utf-8") | 764 | status = self.__api_request( |
765 | 'GET', '/health', parse=False).decode("utf-8") | ||
727 | return status in ["OK", "success"] | 766 | return status in ["OK", "success"] |
728 | 767 | ||
729 | @api_version("3.0.0", "3.0.0", "3.0.0") | 768 | @api_version("3.0.0", "3.0.0", "3.0.0") |
730 | def instance_nodeinfo(self, schema = "http://nodeinfo.diaspora.software/ns/schema/2.0"): | 769 | def instance_nodeinfo(self, schema="http://nodeinfo.diaspora.software/ns/schema/2.0"): |
731 | """ | 770 | """ |
732 | Retrieves the instances nodeinfo information. | 771 | Retrieves the instance's nodeinfo information. |
733 | 772 | ||
734 | For information on what the nodeinfo can contain, see the nodeinfo | 773 | For information on what the nodeinfo can contain, see the nodeinfo |
735 | specification: https://github.com/jhass/nodeinfo . By default, | 774 | specification: https://github.com/jhass/nodeinfo . By default, |
736 | Mastodon.py will try to retrieve the version 2.0 schema nodeinfo. | 775 | Mastodon.py will try to retrieve the version 2.0 schema nodeinfo. |
737 | 776 | ||
738 | To override the schema, specify the desired schema with the `schema` | 777 | To override the schema, specify the desired schema with the `schema` |
739 | parameter. | 778 | parameter. |
740 | """ | 779 | """ |
741 | links = self.__api_request('GET', '/.well-known/nodeinfo')["links"] | 780 | links = self.__api_request('GET', '/.well-known/nodeinfo')["links"] |
742 | 781 | ||
743 | schema_url = None | 782 | schema_url = None |
744 | for available_schema in links: | 783 | for available_schema in links: |
745 | if available_schema.rel == schema: | 784 | if available_schema.rel == schema: |
746 | schema_url = available_schema.href | 785 | schema_url = available_schema.href |
747 | 786 | ||
748 | if schema_url is None: | 787 | if schema_url is None: |
749 | raise MastodonIllegalArgumentError("Requested nodeinfo schema is not available.") | 788 | raise MastodonIllegalArgumentError( |
750 | 789 | "Requested nodeinfo schema is not available.") | |
790 | |||
751 | try: | 791 | try: |
752 | return self.__api_request('GET', schema_url, base_url_override="") | 792 | return self.__api_request('GET', schema_url, base_url_override="") |
753 | except MastodonNotFoundError: | 793 | except MastodonNotFoundError: |
754 | parse = urlparse(schema_url) | 794 | parse = urlparse(schema_url) |
755 | return self.__api_request('GET', parse.path + parse.params + parse.query + parse.fragment) | 795 | return self.__api_request('GET', parse.path + parse.params + parse.query + parse.fragment) |
756 | 796 | ||
757 | ### | 797 | ### |
758 | # Reading data: Timelines | 798 | # Reading data: Timelines |
759 | ## | 799 | ## |
@@ -762,30 +802,30 @@ class Mastodon: | |||
762 | """ | 802 | """ |
763 | Fetch statuses, most recent ones first. `timeline` can be 'home', 'local', 'public', | 803 | Fetch statuses, most recent ones first. `timeline` can be 'home', 'local', 'public', |
764 | 'tag/hashtag' or 'list/id'. See the following functions documentation for what those do. | 804 | 'tag/hashtag' or 'list/id'. See the following functions documentation for what those do. |
765 | 805 | ||
766 | The default timeline is the "home" timeline. | 806 | The default timeline is the "home" timeline. |
767 | 807 | ||
768 | Specify `only_media` to only get posts with attached media. Specify `local` to only get local statuses, | 808 | Specify `only_media` to only get posts with attached media. Specify `local` to only get local statuses, |
769 | and `remote` to only get remote statuses. Some options are mutually incompatible as dictated by logic. | 809 | and `remote` to only get remote statuses. Some options are mutually incompatible as dictated by logic. |
770 | 810 | ||
771 | May or may not require authentication depending on server settings and what is specifically requested. | 811 | May or may not require authentication depending on server settings and what is specifically requested. |
772 | 812 | ||
773 | Returns a list of `toot dicts`_. | 813 | Returns a list of `toot dicts`_. |
774 | """ | 814 | """ |
775 | if max_id != None: | 815 | if max_id != None: |
776 | max_id = self.__unpack_id(max_id, dateconv=True) | 816 | max_id = self.__unpack_id(max_id, dateconv=True) |
777 | 817 | ||
778 | if min_id != None: | 818 | if min_id != None: |
779 | min_id = self.__unpack_id(min_id, dateconv=True) | 819 | min_id = self.__unpack_id(min_id, dateconv=True) |
780 | 820 | ||
781 | if since_id != None: | 821 | if since_id != None: |
782 | since_id = self.__unpack_id(since_id, dateconv=True) | 822 | since_id = self.__unpack_id(since_id, dateconv=True) |
783 | 823 | ||
784 | params_initial = locals() | 824 | params_initial = locals() |
785 | 825 | ||
786 | if local == False: | 826 | if local == False: |
787 | del params_initial['local'] | 827 | del params_initial['local'] |
788 | 828 | ||
789 | if remote == False: | 829 | if remote == False: |
790 | del params_initial['remote'] | 830 | del params_initial['remote'] |
791 | 831 | ||
@@ -799,11 +839,11 @@ class Mastodon: | |||
799 | params = self.__generate_params(params_initial, ['timeline']) | 839 | params = self.__generate_params(params_initial, ['timeline']) |
800 | url = '/api/v1/timelines/{0}'.format(timeline) | 840 | url = '/api/v1/timelines/{0}'.format(timeline) |
801 | return self.__api_request('GET', url, params) | 841 | return self.__api_request('GET', url, params) |
802 | 842 | ||
803 | @api_version("1.0.0", "3.1.4", __DICT_VERSION_STATUS) | 843 | @api_version("1.0.0", "3.1.4", __DICT_VERSION_STATUS) |
804 | def timeline_home(self, max_id=None, min_id=None, since_id=None, limit=None, only_media=False, local=False, remote=False): | 844 | def timeline_home(self, max_id=None, min_id=None, since_id=None, limit=None, only_media=False, local=False, remote=False): |
805 | """ | 845 | """ |
806 | Convenience method: Fetches the logged-in users home timeline (i.e. followed users and self). Params as in `timeline()`. | 846 | Convenience method: Fetches the logged-in user's home timeline (i.e. followed users and self). Params as in `timeline()`. |
807 | 847 | ||
808 | Returns a list of `toot dicts`_. | 848 | Returns a list of `toot dicts`_. |
809 | """ | 849 | """ |
@@ -826,17 +866,18 @@ class Mastodon: | |||
826 | Returns a list of `toot dicts`_. | 866 | Returns a list of `toot dicts`_. |
827 | """ | 867 | """ |
828 | return self.timeline('public', max_id=max_id, min_id=min_id, since_id=since_id, limit=limit, only_media=only_media, local=local, remote=remote) | 868 | return self.timeline('public', max_id=max_id, min_id=min_id, since_id=since_id, limit=limit, only_media=only_media, local=local, remote=remote) |
829 | 869 | ||
830 | @api_version("1.0.0", "3.1.4", __DICT_VERSION_STATUS) | 870 | @api_version("1.0.0", "3.1.4", __DICT_VERSION_STATUS) |
831 | def timeline_hashtag(self, hashtag, local=False, max_id=None, min_id=None, since_id=None, limit=None, only_media=False, remote=False): | 871 | def timeline_hashtag(self, hashtag, local=False, max_id=None, min_id=None, since_id=None, limit=None, only_media=False, remote=False): |
832 | """ | 872 | """ |
833 | Convenience method: Fetch a timeline of toots with a given hashtag. The hashtag parameter | 873 | Convenience method: Fetch a timeline of toots with a given hashtag. The hashtag parameter |
834 | should not contain the leading #. Params as in `timeline()`. | 874 | should not contain the leading #. Params as in `timeline()`. |
835 | 875 | ||
836 | Returns a list of `toot dicts`_. | 876 | Returns a list of `toot dicts`_. |
837 | """ | 877 | """ |
838 | if hashtag.startswith("#"): | 878 | if hashtag.startswith("#"): |
839 | raise MastodonIllegalArgumentError("Hashtag parameter should omit leading #") | 879 | raise MastodonIllegalArgumentError( |
880 | "Hashtag parameter should omit leading #") | ||
840 | return self.timeline('tag/{0}'.format(hashtag), max_id=max_id, min_id=min_id, since_id=since_id, limit=limit, only_media=only_media, local=local, remote=remote) | 881 | return self.timeline('tag/{0}'.format(hashtag), max_id=max_id, min_id=min_id, since_id=since_id, limit=limit, only_media=only_media, local=local, remote=remote) |
841 | 882 | ||
842 | @api_version("2.1.0", "3.1.4", __DICT_VERSION_STATUS) | 883 | @api_version("2.1.0", "3.1.4", __DICT_VERSION_STATUS) |
@@ -852,22 +893,22 @@ class Mastodon: | |||
852 | @api_version("2.6.0", "2.6.0", __DICT_VERSION_CONVERSATION) | 893 | @api_version("2.6.0", "2.6.0", __DICT_VERSION_CONVERSATION) |
853 | def conversations(self, max_id=None, min_id=None, since_id=None, limit=None): | 894 | def conversations(self, max_id=None, min_id=None, since_id=None, limit=None): |
854 | """ | 895 | """ |
855 | Fetches a users conversations. | 896 | Fetches a user's conversations. |
856 | 897 | ||
857 | Returns a list of `conversation dicts`_. | 898 | Returns a list of `conversation dicts`_. |
858 | """ | 899 | """ |
859 | if max_id != None: | 900 | if max_id != None: |
860 | max_id = self.__unpack_id(max_id, dateconv=True) | 901 | max_id = self.__unpack_id(max_id, dateconv=True) |
861 | 902 | ||
862 | if min_id != None: | 903 | if min_id != None: |
863 | min_id = self.__unpack_id(min_id, dateconv=True) | 904 | min_id = self.__unpack_id(min_id, dateconv=True) |
864 | 905 | ||
865 | if since_id != None: | 906 | if since_id != None: |
866 | since_id = self.__unpack_id(since_id, dateconv=True) | 907 | since_id = self.__unpack_id(since_id, dateconv=True) |
867 | 908 | ||
868 | params = self.__generate_params(locals()) | 909 | params = self.__generate_params(locals()) |
869 | return self.__api_request('GET', "/api/v1/conversations/", params) | 910 | return self.__api_request('GET', "/api/v1/conversations/", params) |
870 | 911 | ||
871 | ### | 912 | ### |
872 | # Reading data: Statuses | 913 | # Reading data: Statuses |
873 | ### | 914 | ### |
@@ -894,7 +935,7 @@ class Mastodon: | |||
894 | 935 | ||
895 | This function is deprecated as of 3.0.0 and the endpoint does not | 936 | This function is deprecated as of 3.0.0 and the endpoint does not |
896 | exist anymore - you should just use the "card" field of the status dicts | 937 | exist anymore - you should just use the "card" field of the status dicts |
897 | instead. Mastodon.py will try to mimick the old behaviour, but this | 938 | instead. Mastodon.py will try to mimic the old behaviour, but this |
898 | is somewhat inefficient and not guaranteed to be the case forever. | 939 | is somewhat inefficient and not guaranteed to be the case forever. |
899 | 940 | ||
900 | Returns a `card dict`_. | 941 | Returns a `card dict`_. |
@@ -956,7 +997,7 @@ class Mastodon: | |||
956 | Returns a list of `scheduled toot dicts`_. | 997 | Returns a list of `scheduled toot dicts`_. |
957 | """ | 998 | """ |
958 | return self.__api_request('GET', '/api/v1/scheduled_statuses') | 999 | return self.__api_request('GET', '/api/v1/scheduled_statuses') |
959 | 1000 | ||
960 | @api_version("2.7.0", "2.7.0", __DICT_VERSION_SCHEDULED_STATUS) | 1001 | @api_version("2.7.0", "2.7.0", __DICT_VERSION_SCHEDULED_STATUS) |
961 | def scheduled_status(self, id): | 1002 | def scheduled_status(self, id): |
962 | """ | 1003 | """ |
@@ -967,7 +1008,7 @@ class Mastodon: | |||
967 | id = self.__unpack_id(id) | 1008 | id = self.__unpack_id(id) |
968 | url = '/api/v1/scheduled_statuses/{0}'.format(str(id)) | 1009 | url = '/api/v1/scheduled_statuses/{0}'.format(str(id)) |
969 | return self.__api_request('GET', url) | 1010 | return self.__api_request('GET', url) |
970 | 1011 | ||
971 | ### | 1012 | ### |
972 | # Reading data: Polls | 1013 | # Reading data: Polls |
973 | ### | 1014 | ### |
@@ -981,7 +1022,7 @@ class Mastodon: | |||
981 | id = self.__unpack_id(id) | 1022 | id = self.__unpack_id(id) |
982 | url = '/api/v1/polls/{0}'.format(str(id)) | 1023 | url = '/api/v1/polls/{0}'.format(str(id)) |
983 | return self.__api_request('GET', url) | 1024 | return self.__api_request('GET', url) |
984 | 1025 | ||
985 | ### | 1026 | ### |
986 | # Reading data: Notifications | 1027 | # Reading data: Notifications |
987 | ### | 1028 | ### |
@@ -994,7 +1035,7 @@ class Mastodon: | |||
994 | Parameter `exclude_types` is an array of the following `follow`, `favourite`, `reblog`, | 1035 | Parameter `exclude_types` is an array of the following `follow`, `favourite`, `reblog`, |
995 | `mention`, `poll`, `follow_request`. Specifying `mentions_only` is a deprecated way to | 1036 | `mention`, `poll`, `follow_request`. Specifying `mentions_only` is a deprecated way to |
996 | set `exclude_types` to all but mentions. | 1037 | set `exclude_types` to all but mentions. |
997 | 1038 | ||
998 | Can be passed an `id` to fetch a single notification. | 1039 | Can be passed an `id` to fetch a single notification. |
999 | 1040 | ||
1000 | Returns a list of `notification dicts`_. | 1041 | Returns a list of `notification dicts`_. |
@@ -1002,23 +1043,25 @@ class Mastodon: | |||
1002 | if not mentions_only is None: | 1043 | if not mentions_only is None: |
1003 | if not exclude_types is None: | 1044 | if not exclude_types is None: |
1004 | if mentions_only: | 1045 | if mentions_only: |
1005 | exclude_types = ["follow", "favourite", "reblog", "poll", "follow_request"] | 1046 | exclude_types = ["follow", "favourite", |
1047 | "reblog", "poll", "follow_request"] | ||
1006 | else: | 1048 | else: |
1007 | raise MastodonIllegalArgumentError('Cannot specify exclude_types when mentions_only is present') | 1049 | raise MastodonIllegalArgumentError( |
1050 | 'Cannot specify exclude_types when mentions_only is present') | ||
1008 | del mentions_only | 1051 | del mentions_only |
1009 | 1052 | ||
1010 | if max_id != None: | 1053 | if max_id != None: |
1011 | max_id = self.__unpack_id(max_id, dateconv=True) | 1054 | max_id = self.__unpack_id(max_id, dateconv=True) |
1012 | 1055 | ||
1013 | if min_id != None: | 1056 | if min_id != None: |
1014 | min_id = self.__unpack_id(min_id, dateconv=True) | 1057 | min_id = self.__unpack_id(min_id, dateconv=True) |
1015 | 1058 | ||
1016 | if since_id != None: | 1059 | if since_id != None: |
1017 | since_id = self.__unpack_id(since_id, dateconv=True) | 1060 | since_id = self.__unpack_id(since_id, dateconv=True) |
1018 | 1061 | ||
1019 | if account_id != None: | 1062 | if account_id != None: |
1020 | account_id = self.__unpack_id(account_id) | 1063 | account_id = self.__unpack_id(account_id) |
1021 | 1064 | ||
1022 | if id is None: | 1065 | if id is None: |
1023 | params = self.__generate_params(locals(), ['id']) | 1066 | params = self.__generate_params(locals(), ['id']) |
1024 | return self.__api_request('GET', '/api/v1/notifications', params) | 1067 | return self.__api_request('GET', '/api/v1/notifications', params) |
@@ -1034,15 +1077,15 @@ class Mastodon: | |||
1034 | def account(self, id): | 1077 | def account(self, id): |
1035 | """ | 1078 | """ |
1036 | Fetch account information by user `id`. | 1079 | Fetch account information by user `id`. |
1037 | 1080 | ||
1038 | Does not require authentication for publicly visible accounts. | 1081 | Does not require authentication for publicly visible accounts. |
1039 | 1082 | ||
1040 | Returns a `user dict`_. | 1083 | Returns a `user dict`_. |
1041 | """ | 1084 | """ |
1042 | id = self.__unpack_id(id) | 1085 | id = self.__unpack_id(id) |
1043 | url = '/api/v1/accounts/{0}'.format(str(id)) | 1086 | url = '/api/v1/accounts/{0}'.format(str(id)) |
1044 | return self.__api_request('GET', url) | 1087 | return self.__api_request('GET', url) |
1045 | 1088 | ||
1046 | @api_version("1.0.0", "2.1.0", __DICT_VERSION_ACCOUNT) | 1089 | @api_version("1.0.0", "2.1.0", __DICT_VERSION_ACCOUNT) |
1047 | def account_verify_credentials(self): | 1090 | def account_verify_credentials(self): |
1048 | """ | 1091 | """ |
@@ -1055,12 +1098,12 @@ class Mastodon: | |||
1055 | @api_version("1.0.0", "2.1.0", __DICT_VERSION_ACCOUNT) | 1098 | @api_version("1.0.0", "2.1.0", __DICT_VERSION_ACCOUNT) |
1056 | def me(self): | 1099 | def me(self): |
1057 | """ | 1100 | """ |
1058 | Get this users account. Symonym for `account_verify_credentials()`, does exactly | 1101 | Get this user's account. Synonym for `account_verify_credentials()`, does exactly |
1059 | the same thing, just exists becase `account_verify_credentials()` has a confusing | 1102 | the same thing, just exists becase `account_verify_credentials()` has a confusing |
1060 | name. | 1103 | name. |
1061 | """ | 1104 | """ |
1062 | return self.account_verify_credentials() | 1105 | return self.account_verify_credentials() |
1063 | 1106 | ||
1064 | @api_version("1.0.0", "2.8.0", __DICT_VERSION_STATUS) | 1107 | @api_version("1.0.0", "2.8.0", __DICT_VERSION_STATUS) |
1065 | def account_statuses(self, id, only_media=False, pinned=False, exclude_replies=False, exclude_reblogs=False, tagged=None, max_id=None, min_id=None, since_id=None, limit=None): | 1108 | def account_statuses(self, id, only_media=False, pinned=False, exclude_replies=False, exclude_reblogs=False, tagged=None, max_id=None, min_id=None, since_id=None, limit=None): |
1066 | """ | 1109 | """ |
@@ -1070,7 +1113,7 @@ class Mastodon: | |||
1070 | included. | 1113 | included. |
1071 | 1114 | ||
1072 | If `only_media` is set, return only statuses with media attachments. | 1115 | If `only_media` is set, return only statuses with media attachments. |
1073 | If `pinned` is set, return only statuses that have been pinned. Note that | 1116 | If `pinned` is set, return only statuses that have been pinned. Note that |
1074 | as of Mastodon 2.1.0, this only works properly for instance-local users. | 1117 | as of Mastodon 2.1.0, this only works properly for instance-local users. |
1075 | If `exclude_replies` is set, filter out all statuses that are replies. | 1118 | If `exclude_replies` is set, filter out all statuses that are replies. |
1076 | If `exclude_reblogs` is set, filter out all statuses that are reblogs. | 1119 | If `exclude_reblogs` is set, filter out all statuses that are reblogs. |
@@ -1084,13 +1127,13 @@ class Mastodon: | |||
1084 | id = self.__unpack_id(id) | 1127 | id = self.__unpack_id(id) |
1085 | if max_id != None: | 1128 | if max_id != None: |
1086 | max_id = self.__unpack_id(max_id, dateconv=True) | 1129 | max_id = self.__unpack_id(max_id, dateconv=True) |
1087 | 1130 | ||
1088 | if min_id != None: | 1131 | if min_id != None: |
1089 | min_id = self.__unpack_id(min_id, dateconv=True) | 1132 | min_id = self.__unpack_id(min_id, dateconv=True) |
1090 | 1133 | ||
1091 | if since_id != None: | 1134 | if since_id != None: |
1092 | since_id = self.__unpack_id(since_id, dateconv=True) | 1135 | since_id = self.__unpack_id(since_id, dateconv=True) |
1093 | 1136 | ||
1094 | params = self.__generate_params(locals(), ['id']) | 1137 | params = self.__generate_params(locals(), ['id']) |
1095 | if pinned == False: | 1138 | if pinned == False: |
1096 | del params["pinned"] | 1139 | del params["pinned"] |
@@ -1114,13 +1157,13 @@ class Mastodon: | |||
1114 | id = self.__unpack_id(id) | 1157 | id = self.__unpack_id(id) |
1115 | if max_id != None: | 1158 | if max_id != None: |
1116 | max_id = self.__unpack_id(max_id, dateconv=True) | 1159 | max_id = self.__unpack_id(max_id, dateconv=True) |
1117 | 1160 | ||
1118 | if min_id != None: | 1161 | if min_id != None: |
1119 | min_id = self.__unpack_id(min_id, dateconv=True) | 1162 | min_id = self.__unpack_id(min_id, dateconv=True) |
1120 | 1163 | ||
1121 | if since_id != None: | 1164 | if since_id != None: |
1122 | since_id = self.__unpack_id(since_id, dateconv=True) | 1165 | since_id = self.__unpack_id(since_id, dateconv=True) |
1123 | 1166 | ||
1124 | params = self.__generate_params(locals(), ['id']) | 1167 | params = self.__generate_params(locals(), ['id']) |
1125 | url = '/api/v1/accounts/{0}/following'.format(str(id)) | 1168 | url = '/api/v1/accounts/{0}/following'.format(str(id)) |
1126 | return self.__api_request('GET', url, params) | 1169 | return self.__api_request('GET', url, params) |
@@ -1135,21 +1178,21 @@ class Mastodon: | |||
1135 | id = self.__unpack_id(id) | 1178 | id = self.__unpack_id(id) |
1136 | if max_id != None: | 1179 | if max_id != None: |
1137 | max_id = self.__unpack_id(max_id, dateconv=True) | 1180 | max_id = self.__unpack_id(max_id, dateconv=True) |
1138 | 1181 | ||
1139 | if min_id != None: | 1182 | if min_id != None: |
1140 | min_id = self.__unpack_id(min_id, dateconv=True) | 1183 | min_id = self.__unpack_id(min_id, dateconv=True) |
1141 | 1184 | ||
1142 | if since_id != None: | 1185 | if since_id != None: |
1143 | since_id = self.__unpack_id(since_id, dateconv=True) | 1186 | since_id = self.__unpack_id(since_id, dateconv=True) |
1144 | 1187 | ||
1145 | params = self.__generate_params(locals(), ['id']) | 1188 | params = self.__generate_params(locals(), ['id']) |
1146 | url = '/api/v1/accounts/{0}/followers'.format(str(id)) | 1189 | url = '/api/v1/accounts/{0}/followers'.format(str(id)) |
1147 | return self.__api_request('GET', url, params) | 1190 | return self.__api_request('GET', url, params) |
1148 | 1191 | ||
1149 | @api_version("1.0.0", "1.4.0", __DICT_VERSION_RELATIONSHIP) | 1192 | @api_version("1.0.0", "1.4.0", __DICT_VERSION_RELATIONSHIP) |
1150 | def account_relationships(self, id): | 1193 | def account_relationships(self, id): |
1151 | """ | 1194 | """ |
1152 | Fetch relationship (following, followed_by, blocking, follow requested) of | 1195 | Fetch relationship (following, followed_by, blocking, follow requested) of |
1153 | the logged in user to a given account. `id` can be a list. | 1196 | the logged in user to a given account. `id` can be a list. |
1154 | 1197 | ||
1155 | Returns a list of `relationship dicts`_. | 1198 | Returns a list of `relationship dicts`_. |
@@ -1169,92 +1212,92 @@ class Mastodon: | |||
1169 | Returns a list of `user dicts`_. | 1212 | Returns a list of `user dicts`_. |
1170 | """ | 1213 | """ |
1171 | params = self.__generate_params(locals()) | 1214 | params = self.__generate_params(locals()) |
1172 | 1215 | ||
1173 | if params["following"] == False: | 1216 | if params["following"] == False: |
1174 | del params["following"] | 1217 | del params["following"] |
1175 | 1218 | ||
1176 | return self.__api_request('GET', '/api/v1/accounts/search', params) | 1219 | return self.__api_request('GET', '/api/v1/accounts/search', params) |
1177 | 1220 | ||
1178 | @api_version("2.1.0", "2.1.0", __DICT_VERSION_LIST) | 1221 | @api_version("2.1.0", "2.1.0", __DICT_VERSION_LIST) |
1179 | def account_lists(self, id): | 1222 | def account_lists(self, id): |
1180 | """ | 1223 | """ |
1181 | Get all of the logged-in users lists which the specified user is | 1224 | Get all of the logged-in user's lists which the specified user is |
1182 | a member of. | 1225 | a member of. |
1183 | 1226 | ||
1184 | Returns a list of `list dicts`_. | 1227 | Returns a list of `list dicts`_. |
1185 | """ | 1228 | """ |
1186 | id = self.__unpack_id(id) | 1229 | id = self.__unpack_id(id) |
1187 | params = self.__generate_params(locals(), ['id']) | 1230 | params = self.__generate_params(locals(), ['id']) |
1188 | url = '/api/v1/accounts/{0}/lists'.format(str(id)) | 1231 | url = '/api/v1/accounts/{0}/lists'.format(str(id)) |
1189 | return self.__api_request('GET', url, params) | 1232 | return self.__api_request('GET', url, params) |
1190 | 1233 | ||
1191 | ### | 1234 | ### |
1192 | # Reading data: Featured hashtags | 1235 | # Reading data: Featured hashtags |
1193 | ### | 1236 | ### |
1194 | @api_version("3.0.0", "3.0.0", __DICT_VERSION_FEATURED_TAG) | 1237 | @api_version("3.0.0", "3.0.0", __DICT_VERSION_FEATURED_TAG) |
1195 | def featured_tags(self): | 1238 | def featured_tags(self): |
1196 | """ | 1239 | """ |
1197 | Return the hashtags the logged-in user has set to be featured on | 1240 | Return the hashtags the logged-in user has set to be featured on |
1198 | their profile as a list of `featured tag dicts`_. | 1241 | their profile as a list of `featured tag dicts`_. |
1199 | 1242 | ||
1200 | Returns a list of `featured tag dicts`_. | 1243 | Returns a list of `featured tag dicts`_. |
1201 | """ | 1244 | """ |
1202 | return self.__api_request('GET', '/api/v1/featured_tags') | 1245 | return self.__api_request('GET', '/api/v1/featured_tags') |
1203 | 1246 | ||
1204 | @api_version("3.0.0", "3.0.0", __DICT_VERSION_HASHTAG) | 1247 | @api_version("3.0.0", "3.0.0", __DICT_VERSION_HASHTAG) |
1205 | def featured_tag_suggestions(self): | 1248 | def featured_tag_suggestions(self): |
1206 | """ | 1249 | """ |
1207 | Returns the logged-in users 10 most commonly hashtags. | 1250 | Returns the logged-in user's 10 most commonly-used hashtags. |
1208 | 1251 | ||
1209 | Returns a list of `hashtag dicts`_. | 1252 | Returns a list of `hashtag dicts`_. |
1210 | """ | 1253 | """ |
1211 | return self.__api_request('GET', '/api/v1/featured_tags/suggestions') | 1254 | return self.__api_request('GET', '/api/v1/featured_tags/suggestions') |
1212 | 1255 | ||
1213 | ### | 1256 | ### |
1214 | # Reading data: Keyword filters | 1257 | # Reading data: Keyword filters |
1215 | ### | 1258 | ### |
1216 | @api_version("2.4.3", "2.4.3", __DICT_VERSION_FILTER) | 1259 | @api_version("2.4.3", "2.4.3", __DICT_VERSION_FILTER) |
1217 | def filters(self): | 1260 | def filters(self): |
1218 | """ | 1261 | """ |
1219 | Fetch all of the logged-in users filters. | 1262 | Fetch all of the logged-in user's filters. |
1220 | 1263 | ||
1221 | Returns a list of `filter dicts`_. Not paginated. | 1264 | Returns a list of `filter dicts`_. Not paginated. |
1222 | """ | 1265 | """ |
1223 | return self.__api_request('GET', '/api/v1/filters') | 1266 | return self.__api_request('GET', '/api/v1/filters') |
1224 | 1267 | ||
1225 | @api_version("2.4.3", "2.4.3", __DICT_VERSION_FILTER) | 1268 | @api_version("2.4.3", "2.4.3", __DICT_VERSION_FILTER) |
1226 | def filter(self, id): | 1269 | def filter(self, id): |
1227 | """ | 1270 | """ |
1228 | Fetches information about the filter with the specified `id`. | 1271 | Fetches information about the filter with the specified `id`. |
1229 | 1272 | ||
1230 | Returns a `filter dict`_. | 1273 | Returns a `filter dict`_. |
1231 | """ | 1274 | """ |
1232 | id = self.__unpack_id(id) | 1275 | id = self.__unpack_id(id) |
1233 | url = '/api/v1/filters/{0}'.format(str(id)) | 1276 | url = '/api/v1/filters/{0}'.format(str(id)) |
1234 | return self.__api_request('GET', url) | 1277 | return self.__api_request('GET', url) |
1235 | 1278 | ||
1236 | @api_version("2.4.3", "2.4.3", __DICT_VERSION_FILTER) | 1279 | @api_version("2.4.3", "2.4.3", __DICT_VERSION_FILTER) |
1237 | def filters_apply(self, objects, filters, context): | 1280 | def filters_apply(self, objects, filters, context): |
1238 | """ | 1281 | """ |
1239 | Helper function: Applies a list of filters to a list of either statuses | 1282 | Helper function: Applies a list of filters to a list of either statuses |
1240 | or notifications and returns only those matched by none. This function will | 1283 | or notifications and returns only those matched by none. This function will |
1241 | apply all filters that match the context provided in `context`, i.e. | 1284 | apply all filters that match the context provided in `context`, i.e. |
1242 | if you want to apply only notification-relevant filters, specify | 1285 | if you want to apply only notification-relevant filters, specify |
1243 | 'notifications'. Valid contexts are 'home', 'notifications', 'public' and 'thread'. | 1286 | 'notifications'. Valid contexts are 'home', 'notifications', 'public' and 'thread'. |
1244 | """ | 1287 | """ |
1245 | 1288 | ||
1246 | # Build filter regex | 1289 | # Build filter regex |
1247 | filter_strings = [] | 1290 | filter_strings = [] |
1248 | for keyword_filter in filters: | 1291 | for keyword_filter in filters: |
1249 | if not context in keyword_filter["context"]: | 1292 | if not context in keyword_filter["context"]: |
1250 | continue | 1293 | continue |
1251 | 1294 | ||
1252 | filter_string = re.escape(keyword_filter["phrase"]) | 1295 | filter_string = re.escape(keyword_filter["phrase"]) |
1253 | if keyword_filter["whole_word"] == True: | 1296 | if keyword_filter["whole_word"] == True: |
1254 | filter_string = "\\b" + filter_string + "\\b" | 1297 | filter_string = "\\b" + filter_string + "\\b" |
1255 | filter_strings.append(filter_string) | 1298 | filter_strings.append(filter_string) |
1256 | filter_re = re.compile("|".join(filter_strings), flags = re.IGNORECASE) | 1299 | filter_re = re.compile("|".join(filter_strings), flags=re.IGNORECASE) |
1257 | 1300 | ||
1258 | # Apply | 1301 | # Apply |
1259 | filter_results = [] | 1302 | filter_results = [] |
1260 | for filter_object in objects: | 1303 | for filter_object in objects: |
@@ -1267,7 +1310,7 @@ class Mastodon: | |||
1267 | if not filter_re.search(filter_text): | 1310 | if not filter_re.search(filter_text): |
1268 | filter_results.append(filter_object) | 1311 | filter_results.append(filter_object) |
1269 | return filter_results | 1312 | return filter_results |
1270 | 1313 | ||
1271 | ### | 1314 | ### |
1272 | # Reading data: Follow suggestions | 1315 | # Reading data: Follow suggestions |
1273 | ### | 1316 | ### |
@@ -1277,10 +1320,10 @@ class Mastodon: | |||
1277 | Fetch follow suggestions for the logged-in user. | 1320 | Fetch follow suggestions for the logged-in user. |
1278 | 1321 | ||
1279 | Returns a list of `user dicts`_. | 1322 | Returns a list of `user dicts`_. |
1280 | 1323 | ||
1281 | """ | 1324 | """ |
1282 | return self.__api_request('GET', '/api/v1/suggestions') | 1325 | return self.__api_request('GET', '/api/v1/suggestions') |
1283 | 1326 | ||
1284 | ### | 1327 | ### |
1285 | # Reading data: Follow suggestions | 1328 | # Reading data: Follow suggestions |
1286 | ### | 1329 | ### |
@@ -1290,27 +1333,27 @@ class Mastodon: | |||
1290 | Fetch the contents of the profile directory, if enabled on the server. | 1333 | Fetch the contents of the profile directory, if enabled on the server. |
1291 | 1334 | ||
1292 | Returns a list of `user dicts`_. | 1335 | Returns a list of `user dicts`_. |
1293 | 1336 | ||
1294 | """ | 1337 | """ |
1295 | return self.__api_request('GET', '/api/v1/directory') | 1338 | return self.__api_request('GET', '/api/v1/directory') |
1296 | 1339 | ||
1297 | ### | 1340 | ### |
1298 | # Reading data: Endorsements | 1341 | # Reading data: Endorsements |
1299 | ### | 1342 | ### |
1300 | @api_version("2.5.0", "2.5.0", __DICT_VERSION_ACCOUNT) | 1343 | @api_version("2.5.0", "2.5.0", __DICT_VERSION_ACCOUNT) |
1301 | def endorsements(self): | 1344 | def endorsements(self): |
1302 | """ | 1345 | """ |
1303 | Fetch list of users endorsemed by the logged-in user. | 1346 | Fetch list of users endorsed by the logged-in user. |
1304 | 1347 | ||
1305 | Returns a list of `user dicts`_. | 1348 | Returns a list of `user dicts`_. |
1306 | 1349 | ||
1307 | """ | 1350 | """ |
1308 | return self.__api_request('GET', '/api/v1/endorsements') | 1351 | return self.__api_request('GET', '/api/v1/endorsements') |
1309 | 1352 | ||
1310 | |||
1311 | ### | 1353 | ### |
1312 | # Reading data: Searching | 1354 | # Reading data: Searching |
1313 | ### | 1355 | ### |
1356 | |||
1314 | def __ensure_search_params_acceptable(self, account_id, offset, min_id, max_id): | 1357 | def __ensure_search_params_acceptable(self, account_id, offset, min_id, max_id): |
1315 | """ | 1358 | """ |
1316 | Internal Helper: Throw a MastodonVersionError if version is < 2.8.0 but parameters | 1359 | Internal Helper: Throw a MastodonVersionError if version is < 2.8.0 but parameters |
@@ -1318,8 +1361,9 @@ class Mastodon: | |||
1318 | """ | 1361 | """ |
1319 | if not account_id is None or not offset is None or not min_id is None or not max_id is None: | 1362 | if not account_id is None or not offset is None or not min_id is None or not max_id is None: |
1320 | if self.verify_minimum_version("2.8.0", cached=True) == False: | 1363 | if self.verify_minimum_version("2.8.0", cached=True) == False: |
1321 | raise MastodonVersionError("Advanced search parameters require Mastodon 2.8.0+") | 1364 | raise MastodonVersionError( |
1322 | 1365 | "Advanced search parameters require Mastodon 2.8.0+") | |
1366 | |||
1323 | @api_version("1.1.0", "2.8.0", __DICT_VERSION_SEARCHRESULT) | 1367 | @api_version("1.1.0", "2.8.0", __DICT_VERSION_SEARCHRESULT) |
1324 | def search(self, q, resolve=True, result_type=None, account_id=None, offset=None, min_id=None, max_id=None, exclude_unreviewed=True): | 1368 | def search(self, q, resolve=True, result_type=None, account_id=None, offset=None, min_id=None, max_id=None, exclude_unreviewed=True): |
1325 | """ | 1369 | """ |
@@ -1327,32 +1371,33 @@ class Mastodon: | |||
1327 | lookups if resolve is True. Full-text search is only enabled if | 1371 | lookups if resolve is True. Full-text search is only enabled if |
1328 | the instance supports it, and is restricted to statuses the logged-in | 1372 | the instance supports it, and is restricted to statuses the logged-in |
1329 | user wrote or was mentioned in. | 1373 | user wrote or was mentioned in. |
1330 | 1374 | ||
1331 | `result_type` can be one of "accounts", "hashtags" or "statuses", to only | 1375 | `result_type` can be one of "accounts", "hashtags" or "statuses", to only |
1332 | search for that type of object. | 1376 | search for that type of object. |
1333 | 1377 | ||
1334 | Specify `account_id` to only get results from the account with that id. | 1378 | Specify `account_id` to only get results from the account with that id. |
1335 | 1379 | ||
1336 | `offset`, `min_id` and `max_id` can be used to paginate. | 1380 | `offset`, `min_id` and `max_id` can be used to paginate. |
1337 | 1381 | ||
1338 | `exclude_unreviewed` can be used to restrict search results for hashtags to only | 1382 | `exclude_unreviewed` can be used to restrict search results for hashtags to only |
1339 | those that have been reviewed by moderators. It is on by default. | 1383 | those that have been reviewed by moderators. It is on by default. |
1340 | 1384 | ||
1341 | Will use search_v1 (no tag dicts in return values) on Mastodon versions before | 1385 | Will use search_v1 (no tag dicts in return values) on Mastodon versions before |
1342 | 2.4.1), search_v2 otherwise. Parameters other than resolve are only available | 1386 | 2.4.1), search_v2 otherwise. Parameters other than resolve are only available |
1343 | on Mastodon 2.8.0 or above - this function will throw a MastodonVersionError | 1387 | on Mastodon 2.8.0 or above - this function will throw a MastodonVersionError |
1344 | if you try to use them on versions before that. Note that the cached version | 1388 | if you try to use them on versions before that. Note that the cached version |
1345 | number will be used for this to avoid uneccesary requests. | 1389 | number will be used for this to avoid uneccesary requests. |
1346 | 1390 | ||
1347 | Returns a `search result dict`_, with tags as `hashtag dicts`_. | 1391 | Returns a `search result dict`_, with tags as `hashtag dicts`_. |
1348 | """ | 1392 | """ |
1349 | if self.verify_minimum_version("2.4.1", cached=True) == True: | 1393 | if self.verify_minimum_version("2.4.1", cached=True) == True: |
1350 | return self.search_v2(q, resolve=resolve, result_type=result_type, account_id=account_id, | 1394 | return self.search_v2(q, resolve=resolve, result_type=result_type, account_id=account_id, |
1351 | offset=offset, min_id=min_id, max_id=max_id) | 1395 | offset=offset, min_id=min_id, max_id=max_id) |
1352 | else: | 1396 | else: |
1353 | self.__ensure_search_params_acceptable(account_id, offset, min_id, max_id) | 1397 | self.__ensure_search_params_acceptable( |
1398 | account_id, offset, min_id, max_id) | ||
1354 | return self.search_v1(q, resolve=resolve) | 1399 | return self.search_v1(q, resolve=resolve) |
1355 | 1400 | ||
1356 | @api_version("1.1.0", "2.1.0", "2.1.0") | 1401 | @api_version("1.1.0", "2.1.0", "2.1.0") |
1357 | def search_v1(self, q, resolve=False): | 1402 | def search_v1(self, q, resolve=False): |
1358 | """ | 1403 | """ |
@@ -1371,43 +1416,44 @@ class Mastodon: | |||
1371 | """ | 1416 | """ |
1372 | Identical to `search_v1()`, except in that it returns tags as | 1417 | Identical to `search_v1()`, except in that it returns tags as |
1373 | `hashtag dicts`_, has more parameters, and resolves by default. | 1418 | `hashtag dicts`_, has more parameters, and resolves by default. |
1374 | 1419 | ||
1375 | For more details documentation, please see `search()` | 1420 | For more details documentation, please see `search()` |
1376 | 1421 | ||
1377 | Returns a `search result dict`_. | 1422 | Returns a `search result dict`_. |
1378 | """ | 1423 | """ |
1379 | self.__ensure_search_params_acceptable(account_id, offset, min_id, max_id) | 1424 | self.__ensure_search_params_acceptable( |
1425 | account_id, offset, min_id, max_id) | ||
1380 | params = self.__generate_params(locals()) | 1426 | params = self.__generate_params(locals()) |
1381 | 1427 | ||
1382 | if resolve == False: | 1428 | if resolve == False: |
1383 | del params["resolve"] | 1429 | del params["resolve"] |
1384 | 1430 | ||
1385 | if exclude_unreviewed == False or not self.verify_minimum_version("3.0.0", cached=True): | 1431 | if exclude_unreviewed == False or not self.verify_minimum_version("3.0.0", cached=True): |
1386 | del params["exclude_unreviewed"] | 1432 | del params["exclude_unreviewed"] |
1387 | 1433 | ||
1388 | if "result_type" in params: | 1434 | if "result_type" in params: |
1389 | params["type"] = params["result_type"] | 1435 | params["type"] = params["result_type"] |
1390 | del params["result_type"] | 1436 | del params["result_type"] |
1391 | 1437 | ||
1392 | return self.__api_request('GET', '/api/v2/search', params) | 1438 | return self.__api_request('GET', '/api/v2/search', params) |
1393 | 1439 | ||
1394 | ### | 1440 | ### |
1395 | # Reading data: Trends | 1441 | # Reading data: Trends |
1396 | ### | 1442 | ### |
1397 | @api_version("2.4.3", "3.0.0", __DICT_VERSION_HASHTAG) | 1443 | @api_version("2.4.3", "3.0.0", __DICT_VERSION_HASHTAG) |
1398 | def trends(self, limit = None): | 1444 | def trends(self, limit=None): |
1399 | """ | 1445 | """ |
1400 | Fetch trending-hashtag information, if the instance provides such information. | 1446 | Fetch trending-hashtag information, if the instance provides such information. |
1401 | 1447 | ||
1402 | Specify `limit` to limit how many results are returned (the maximum number | 1448 | Specify `limit` to limit how many results are returned (the maximum number |
1403 | of results is 10, the endpoint is not paginated). | 1449 | of results is 10, the endpoint is not paginated). |
1404 | 1450 | ||
1405 | Does not require authentication unless locked down by the administrator. | 1451 | Does not require authentication unless locked down by the administrator. |
1406 | 1452 | ||
1407 | Important versioning note: This endpoint does not exist for Mastodon versions | 1453 | Important versioning note: This endpoint does not exist for Mastodon versions |
1408 | between 2.8.0 (inclusive) and 3.0.0 (exclusive). | 1454 | between 2.8.0 (inclusive) and 3.0.0 (exclusive). |
1409 | 1455 | ||
1410 | Returns a list of `hashtag dicts`_, sorted by the instances trending algorithm, | 1456 | Returns a list of `hashtag dicts`_, sorted by the instance's trending algorithm, |
1411 | descending. | 1457 | descending. |
1412 | """ | 1458 | """ |
1413 | params = self.__generate_params(locals()) | 1459 | params = self.__generate_params(locals()) |
@@ -1420,7 +1466,7 @@ class Mastodon: | |||
1420 | def lists(self): | 1466 | def lists(self): |
1421 | """ | 1467 | """ |
1422 | Fetch a list of all the Lists by the logged-in user. | 1468 | Fetch a list of all the Lists by the logged-in user. |
1423 | 1469 | ||
1424 | Returns a list of `list dicts`_. | 1470 | Returns a list of `list dicts`_. |
1425 | """ | 1471 | """ |
1426 | return self.__api_request('GET', '/api/v1/lists') | 1472 | return self.__api_request('GET', '/api/v1/lists') |
@@ -1429,37 +1475,37 @@ class Mastodon: | |||
1429 | def list(self, id): | 1475 | def list(self, id): |
1430 | """ | 1476 | """ |
1431 | Fetch info about a specific list. | 1477 | Fetch info about a specific list. |
1432 | 1478 | ||
1433 | Returns a `list dict`_. | 1479 | Returns a `list dict`_. |
1434 | """ | 1480 | """ |
1435 | id = self.__unpack_id(id) | 1481 | id = self.__unpack_id(id) |
1436 | return self.__api_request('GET', '/api/v1/lists/{0}'.format(id)) | 1482 | return self.__api_request('GET', '/api/v1/lists/{0}'.format(id)) |
1437 | 1483 | ||
1438 | @api_version("2.1.0", "2.6.0", __DICT_VERSION_ACCOUNT) | 1484 | @api_version("2.1.0", "2.6.0", __DICT_VERSION_ACCOUNT) |
1439 | def list_accounts(self, id, max_id=None, min_id=None, since_id=None, limit=None): | 1485 | def list_accounts(self, id, max_id=None, min_id=None, since_id=None, limit=None): |
1440 | """ | 1486 | """ |
1441 | Get the accounts that are on the given list. | 1487 | Get the accounts that are on the given list. |
1442 | 1488 | ||
1443 | Returns a list of `user dicts`_. | 1489 | Returns a list of `user dicts`_. |
1444 | """ | 1490 | """ |
1445 | id = self.__unpack_id(id) | 1491 | id = self.__unpack_id(id) |
1446 | 1492 | ||
1447 | if max_id != None: | 1493 | if max_id != None: |
1448 | max_id = self.__unpack_id(max_id, dateconv=True) | 1494 | max_id = self.__unpack_id(max_id, dateconv=True) |
1449 | 1495 | ||
1450 | if min_id != None: | 1496 | if min_id != None: |
1451 | min_id = self.__unpack_id(min_id, dateconv=True) | 1497 | min_id = self.__unpack_id(min_id, dateconv=True) |
1452 | 1498 | ||
1453 | if since_id != None: | 1499 | if since_id != None: |
1454 | since_id = self.__unpack_id(since_id, dateconv=True) | 1500 | since_id = self.__unpack_id(since_id, dateconv=True) |
1455 | 1501 | ||
1456 | params = self.__generate_params(locals(), ['id']) | 1502 | params = self.__generate_params(locals(), ['id']) |
1457 | return self.__api_request('GET', '/api/v1/lists/{0}/accounts'.format(id)) | 1503 | return self.__api_request('GET', '/api/v1/lists/{0}/accounts'.format(id)) |
1458 | 1504 | ||
1459 | ### | 1505 | ### |
1460 | # Reading data: Mutes and Blocks | 1506 | # Reading data: Mutes and Blocks |
1461 | ### | 1507 | ### |
1462 | @api_version("1.1.0", "2.6.0", __DICT_VERSION_ACCOUNT) | 1508 | @api_version("1.1.0", "2.6.0", __DICT_VERSION_ACCOUNT) |
1463 | def mutes(self, max_id=None, min_id=None, since_id=None, limit=None): | 1509 | def mutes(self, max_id=None, min_id=None, since_id=None, limit=None): |
1464 | """ | 1510 | """ |
1465 | Fetch a list of users muted by the logged-in user. | 1511 | Fetch a list of users muted by the logged-in user. |
@@ -1468,13 +1514,13 @@ class Mastodon: | |||
1468 | """ | 1514 | """ |
1469 | if max_id != None: | 1515 | if max_id != None: |
1470 | max_id = self.__unpack_id(max_id, dateconv=True) | 1516 | max_id = self.__unpack_id(max_id, dateconv=True) |
1471 | 1517 | ||
1472 | if min_id != None: | 1518 | if min_id != None: |
1473 | min_id = self.__unpack_id(min_id, dateconv=True) | 1519 | min_id = self.__unpack_id(min_id, dateconv=True) |
1474 | 1520 | ||
1475 | if since_id != None: | 1521 | if since_id != None: |
1476 | since_id = self.__unpack_id(since_id, dateconv=True) | 1522 | since_id = self.__unpack_id(since_id, dateconv=True) |
1477 | 1523 | ||
1478 | params = self.__generate_params(locals()) | 1524 | params = self.__generate_params(locals()) |
1479 | return self.__api_request('GET', '/api/v1/mutes', params) | 1525 | return self.__api_request('GET', '/api/v1/mutes', params) |
1480 | 1526 | ||
@@ -1487,13 +1533,13 @@ class Mastodon: | |||
1487 | """ | 1533 | """ |
1488 | if max_id != None: | 1534 | if max_id != None: |
1489 | max_id = self.__unpack_id(max_id, dateconv=True) | 1535 | max_id = self.__unpack_id(max_id, dateconv=True) |
1490 | 1536 | ||
1491 | if min_id != None: | 1537 | if min_id != None: |
1492 | min_id = self.__unpack_id(min_id, dateconv=True) | 1538 | min_id = self.__unpack_id(min_id, dateconv=True) |
1493 | 1539 | ||
1494 | if since_id != None: | 1540 | if since_id != None: |
1495 | since_id = self.__unpack_id(since_id, dateconv=True) | 1541 | since_id = self.__unpack_id(since_id, dateconv=True) |
1496 | 1542 | ||
1497 | params = self.__generate_params(locals()) | 1543 | params = self.__generate_params(locals()) |
1498 | return self.__api_request('GET', '/api/v1/blocks', params) | 1544 | return self.__api_request('GET', '/api/v1/blocks', params) |
1499 | 1545 | ||
@@ -1506,9 +1552,9 @@ class Mastodon: | |||
1506 | Fetch a list of reports made by the logged-in user. | 1552 | Fetch a list of reports made by the logged-in user. |
1507 | 1553 | ||
1508 | Returns a list of `report dicts`_. | 1554 | Returns a list of `report dicts`_. |
1509 | 1555 | ||
1510 | Warning: This method has now finally been removed, and will not | 1556 | Warning: This method has now finally been removed, and will not |
1511 | work on mastodon versions 2.5.0 and above. | 1557 | work on Mastodon versions 2.5.0 and above. |
1512 | """ | 1558 | """ |
1513 | return self.__api_request('GET', '/api/v1/reports') | 1559 | return self.__api_request('GET', '/api/v1/reports') |
1514 | 1560 | ||
@@ -1524,13 +1570,13 @@ class Mastodon: | |||
1524 | """ | 1570 | """ |
1525 | if max_id != None: | 1571 | if max_id != None: |
1526 | max_id = self.__unpack_id(max_id, dateconv=True) | 1572 | max_id = self.__unpack_id(max_id, dateconv=True) |
1527 | 1573 | ||
1528 | if min_id != None: | 1574 | if min_id != None: |
1529 | min_id = self.__unpack_id(min_id, dateconv=True) | 1575 | min_id = self.__unpack_id(min_id, dateconv=True) |
1530 | 1576 | ||
1531 | if since_id != None: | 1577 | if since_id != None: |
1532 | since_id = self.__unpack_id(since_id, dateconv=True) | 1578 | since_id = self.__unpack_id(since_id, dateconv=True) |
1533 | 1579 | ||
1534 | params = self.__generate_params(locals()) | 1580 | params = self.__generate_params(locals()) |
1535 | return self.__api_request('GET', '/api/v1/favourites', params) | 1581 | return self.__api_request('GET', '/api/v1/favourites', params) |
1536 | 1582 | ||
@@ -1546,13 +1592,13 @@ class Mastodon: | |||
1546 | """ | 1592 | """ |
1547 | if max_id != None: | 1593 | if max_id != None: |
1548 | max_id = self.__unpack_id(max_id, dateconv=True) | 1594 | max_id = self.__unpack_id(max_id, dateconv=True) |
1549 | 1595 | ||
1550 | if min_id != None: | 1596 | if min_id != None: |
1551 | min_id = self.__unpack_id(min_id, dateconv=True) | 1597 | min_id = self.__unpack_id(min_id, dateconv=True) |
1552 | 1598 | ||
1553 | if since_id != None: | 1599 | if since_id != None: |
1554 | since_id = self.__unpack_id(since_id, dateconv=True) | 1600 | since_id = self.__unpack_id(since_id, dateconv=True) |
1555 | 1601 | ||
1556 | params = self.__generate_params(locals()) | 1602 | params = self.__generate_params(locals()) |
1557 | return self.__api_request('GET', '/api/v1/follow_requests', params) | 1603 | return self.__api_request('GET', '/api/v1/follow_requests', params) |
1558 | 1604 | ||
@@ -1568,13 +1614,13 @@ class Mastodon: | |||
1568 | """ | 1614 | """ |
1569 | if max_id != None: | 1615 | if max_id != None: |
1570 | max_id = self.__unpack_id(max_id, dateconv=True) | 1616 | max_id = self.__unpack_id(max_id, dateconv=True) |
1571 | 1617 | ||
1572 | if min_id != None: | 1618 | if min_id != None: |
1573 | min_id = self.__unpack_id(min_id, dateconv=True) | 1619 | min_id = self.__unpack_id(min_id, dateconv=True) |
1574 | 1620 | ||
1575 | if since_id != None: | 1621 | if since_id != None: |
1576 | since_id = self.__unpack_id(since_id, dateconv=True) | 1622 | since_id = self.__unpack_id(since_id, dateconv=True) |
1577 | 1623 | ||
1578 | params = self.__generate_params(locals()) | 1624 | params = self.__generate_params(locals()) |
1579 | return self.__api_request('GET', '/api/v1/domain_blocks', params) | 1625 | return self.__api_request('GET', '/api/v1/domain_blocks', params) |
1580 | 1626 | ||
@@ -1589,7 +1635,6 @@ class Mastodon: | |||
1589 | Does not require authentication unless locked down by the administrator. | 1635 | Does not require authentication unless locked down by the administrator. |
1590 | 1636 | ||
1591 | Returns a list of `emoji dicts`_. | 1637 | Returns a list of `emoji dicts`_. |
1592 | |||
1593 | """ | 1638 | """ |
1594 | return self.__api_request('GET', '/api/v1/custom_emojis') | 1639 | return self.__api_request('GET', '/api/v1/custom_emojis') |
1595 | 1640 | ||
@@ -1602,7 +1647,6 @@ class Mastodon: | |||
1602 | Fetch information about the current application. | 1647 | Fetch information about the current application. |
1603 | 1648 | ||
1604 | Returns an `application dict`_. | 1649 | Returns an `application dict`_. |
1605 | |||
1606 | """ | 1650 | """ |
1607 | return self.__api_request('GET', '/api/v1/apps/verify_credentials') | 1651 | return self.__api_request('GET', '/api/v1/apps/verify_credentials') |
1608 | 1652 | ||
@@ -1615,7 +1659,6 @@ class Mastodon: | |||
1615 | Fetch the current push subscription the logged-in user has for this app. | 1659 | Fetch the current push subscription the logged-in user has for this app. |
1616 | 1660 | ||
1617 | Returns a `push subscription dict`_. | 1661 | Returns a `push subscription dict`_. |
1618 | |||
1619 | """ | 1662 | """ |
1620 | return self.__api_request('GET', '/api/v1/push/subscription') | 1663 | return self.__api_request('GET', '/api/v1/push/subscription') |
1621 | 1664 | ||
@@ -1625,28 +1668,27 @@ class Mastodon: | |||
1625 | @api_version("2.8.0", "2.8.0", __DICT_VERSION_PREFERENCES) | 1668 | @api_version("2.8.0", "2.8.0", __DICT_VERSION_PREFERENCES) |
1626 | def preferences(self): | 1669 | def preferences(self): |
1627 | """ | 1670 | """ |
1628 | Fetch the users preferences, which can be used to set some default options. | 1671 | Fetch the user's preferences, which can be used to set some default options. |
1629 | As of 2.8.0, apps can only fetch, not update preferences. | 1672 | As of 2.8.0, apps can only fetch, not update preferences. |
1630 | 1673 | ||
1631 | Returns a `preference dict`_. | 1674 | Returns a `preference dict`_. |
1632 | |||
1633 | """ | 1675 | """ |
1634 | return self.__api_request('GET', '/api/v1/preferences') | 1676 | return self.__api_request('GET', '/api/v1/preferences') |
1635 | 1677 | ||
1636 | ## | 1678 | ## |
1637 | # Reading data: Announcements | 1679 | # Reading data: Announcements |
1638 | ## | 1680 | ## |
1639 | 1681 | ||
1640 | #/api/v1/announcements | 1682 | # /api/v1/announcements |
1641 | @api_version("3.1.0", "3.1.0", __DICT_VERSION_ANNOUNCEMENT) | 1683 | @api_version("3.1.0", "3.1.0", __DICT_VERSION_ANNOUNCEMENT) |
1642 | def announcements(self): | 1684 | def announcements(self): |
1643 | """ | 1685 | """ |
1644 | Fetch currently active annoucements. | 1686 | Fetch currently active announcements. |
1645 | 1687 | ||
1646 | Returns a list of `annoucement dicts`_. | 1688 | Returns a list of `announcement dicts`_. |
1647 | """ | 1689 | """ |
1648 | return self.__api_request('GET', '/api/v1/announcements') | 1690 | return self.__api_request('GET', '/api/v1/announcements') |
1649 | 1691 | ||
1650 | ## | 1692 | ## |
1651 | # Reading data: Read markers | 1693 | # Reading data: Read markers |
1652 | ## | 1694 | ## |
@@ -1655,15 +1697,15 @@ class Mastodon: | |||
1655 | """ | 1697 | """ |
1656 | Get the last-read-location markers for the specified timelines. Valid timelines | 1698 | Get the last-read-location markers for the specified timelines. Valid timelines |
1657 | are the same as in `timeline()`_ | 1699 | are the same as in `timeline()`_ |
1658 | 1700 | ||
1659 | Note that despite the singular name, `timeline` can be a list. | 1701 | Note that despite the singular name, `timeline` can be a list. |
1660 | 1702 | ||
1661 | Returns a dict of `read marker dicts`_, keyed by timeline name. | 1703 | Returns a dict of `read marker dicts`_, keyed by timeline name. |
1662 | """ | 1704 | """ |
1663 | if not isinstance(timeline, (list, tuple)): | 1705 | if not isinstance(timeline, (list, tuple)): |
1664 | timeline = [timeline] | 1706 | timeline = [timeline] |
1665 | params = self.__generate_params(locals()) | 1707 | params = self.__generate_params(locals()) |
1666 | 1708 | ||
1667 | return self.__api_request('GET', '/api/v1/markers', params) | 1709 | return self.__api_request('GET', '/api/v1/markers', params) |
1668 | 1710 | ||
1669 | ### | 1711 | ### |
@@ -1673,7 +1715,7 @@ class Mastodon: | |||
1673 | def bookmarks(self, max_id=None, min_id=None, since_id=None, limit=None): | 1715 | def bookmarks(self, max_id=None, min_id=None, since_id=None, limit=None): |
1674 | """ | 1716 | """ |
1675 | Get a list of statuses bookmarked by the logged-in user. | 1717 | Get a list of statuses bookmarked by the logged-in user. |
1676 | 1718 | ||
1677 | Returns a list of `toot dicts`_. | 1719 | Returns a list of `toot dicts`_. |
1678 | """ | 1720 | """ |
1679 | if max_id != None: | 1721 | if max_id != None: |
@@ -1687,7 +1729,7 @@ class Mastodon: | |||
1687 | 1729 | ||
1688 | params = self.__generate_params(locals()) | 1730 | params = self.__generate_params(locals()) |
1689 | return self.__api_request('GET', '/api/v1/bookmarks', params) | 1731 | return self.__api_request('GET', '/api/v1/bookmarks', params) |
1690 | 1732 | ||
1691 | ### | 1733 | ### |
1692 | # Writing data: Statuses | 1734 | # Writing data: Statuses |
1693 | ### | 1735 | ### |
@@ -1699,10 +1741,10 @@ class Mastodon: | |||
1699 | """ | 1741 | """ |
1700 | Post a status. Can optionally be in reply to another status and contain | 1742 | Post a status. Can optionally be in reply to another status and contain |
1701 | media. | 1743 | media. |
1702 | 1744 | ||
1703 | `media_ids` should be a list. (If it's not, the function will turn it | 1745 | `media_ids` should be a list. (If it's not, the function will turn it |
1704 | into one.) It can contain up to four pieces of media (uploaded via | 1746 | into one.) It can contain up to four pieces of media (uploaded via |
1705 | `media_post()`_). `media_ids` can also be the `media dicts`_ returned | 1747 | `media_post()`_). `media_ids` can also be the `media dicts`_ returned |
1706 | by `media_post()`_ - they are unpacked automatically. | 1748 | by `media_post()`_ - they are unpacked automatically. |
1707 | 1749 | ||
1708 | The `sensitive` boolean decides whether or not media attached to the post | 1750 | The `sensitive` boolean decides whether or not media attached to the post |
@@ -1738,44 +1780,48 @@ class Mastodon: | |||
1738 | 1780 | ||
1739 | Pass `poll` to attach a poll to the status. An appropriate object can be | 1781 | Pass `poll` to attach a poll to the status. An appropriate object can be |
1740 | constructed using `make_poll()`_ . Note that as of Mastodon version | 1782 | constructed using `make_poll()`_ . Note that as of Mastodon version |
1741 | 2.8.2, you can only have either media or a poll attached, not both at | 1783 | 2.8.2, you can only have either media or a poll attached, not both at |
1742 | the same time. | 1784 | the same time. |
1743 | 1785 | ||
1744 | **Specific to `pleroma` feature set:**: Specify `content_type` to set | 1786 | **Specific to "pleroma" feature set:**: Specify `content_type` to set |
1745 | the content type of your post on Pleroma. It accepts 'text/plain' (default), | 1787 | the content type of your post on Pleroma. It accepts 'text/plain' (default), |
1746 | 'text/markdown', 'text/html' and 'text/bbcode. This parameter is not | 1788 | 'text/markdown', 'text/html' and 'text/bbcode'. This parameter is not |
1747 | supported on Mastodon servers, but will be safely ignored if set. | 1789 | supported on Mastodon servers, but will be safely ignored if set. |
1748 | 1790 | ||
1749 | **Specific to `fedibird` feature set:**: The `quote_id` parameter is | 1791 | **Specific to "fedibird" feature set:**: The `quote_id` parameter is |
1750 | a non-standard extension that specifies the id of a quoted status. | 1792 | a non-standard extension that specifies the id of a quoted status. |
1751 | 1793 | ||
1752 | Returns a `toot dict`_ with the new status. | 1794 | Returns a `toot dict`_ with the new status. |
1753 | """ | 1795 | """ |
1754 | if quote_id != None: | 1796 | if quote_id != None: |
1755 | if self.feature_set != "fedibird": | 1797 | if self.feature_set != "fedibird": |
1756 | raise MastodonIllegalArgumentError('quote_id is only available with feature set fedibird') | 1798 | raise MastodonIllegalArgumentError( |
1799 | 'quote_id is only available with feature set fedibird') | ||
1757 | quote_id = self.__unpack_id(quote_id) | 1800 | quote_id = self.__unpack_id(quote_id) |
1758 | 1801 | ||
1759 | if content_type != None: | 1802 | if content_type != None: |
1760 | if self.feature_set != "pleroma": | 1803 | if self.feature_set != "pleroma": |
1761 | raise MastodonIllegalArgumentError('content_type is only available with feature set pleroma') | 1804 | raise MastodonIllegalArgumentError( |
1805 | 'content_type is only available with feature set pleroma') | ||
1762 | # It would be better to read this from nodeinfo and cache, but this is easier | 1806 | # It would be better to read this from nodeinfo and cache, but this is easier |
1763 | if not content_type in ["text/plain", "text/html", "text/markdown", "text/bbcode"]: | 1807 | if not content_type in ["text/plain", "text/html", "text/markdown", "text/bbcode"]: |
1764 | raise MastodonIllegalArgumentError('Invalid content type specified') | 1808 | raise MastodonIllegalArgumentError( |
1765 | 1809 | 'Invalid content type specified') | |
1810 | |||
1766 | if in_reply_to_id != None: | 1811 | if in_reply_to_id != None: |
1767 | in_reply_to_id = self.__unpack_id(in_reply_to_id) | 1812 | in_reply_to_id = self.__unpack_id(in_reply_to_id) |
1768 | 1813 | ||
1769 | if scheduled_at != None: | 1814 | if scheduled_at != None: |
1770 | scheduled_at = self.__consistent_isoformat_utc(scheduled_at) | 1815 | scheduled_at = self.__consistent_isoformat_utc(scheduled_at) |
1771 | 1816 | ||
1772 | params_initial = locals() | 1817 | params_initial = locals() |
1773 | 1818 | ||
1774 | # Validate poll/media exclusivity | 1819 | # Validate poll/media exclusivity |
1775 | if not poll is None: | 1820 | if not poll is None: |
1776 | if (not media_ids is None) and len(media_ids) != 0: | 1821 | if (not media_ids is None) and len(media_ids) != 0: |
1777 | raise ValueError('Status can have media or poll attached - not both.') | 1822 | raise ValueError( |
1778 | 1823 | 'Status can have media or poll attached - not both.') | |
1824 | |||
1779 | # Validate visibility parameter | 1825 | # Validate visibility parameter |
1780 | valid_visibilities = ['private', 'public', 'unlisted', 'direct'] | 1826 | valid_visibilities = ['private', 'public', 'unlisted', 'direct'] |
1781 | if params_initial['visibility'] == None: | 1827 | if params_initial['visibility'] == None: |
@@ -1784,7 +1830,7 @@ class Mastodon: | |||
1784 | params_initial['visibility'] = params_initial['visibility'].lower() | 1830 | params_initial['visibility'] = params_initial['visibility'].lower() |
1785 | if params_initial['visibility'] not in valid_visibilities: | 1831 | if params_initial['visibility'] not in valid_visibilities: |
1786 | raise ValueError('Invalid visibility value! Acceptable ' | 1832 | raise ValueError('Invalid visibility value! Acceptable ' |
1787 | 'values are %s' % valid_visibilities) | 1833 | 'values are %s' % valid_visibilities) |
1788 | 1834 | ||
1789 | if params_initial['language'] == None: | 1835 | if params_initial['language'] == None: |
1790 | del params_initial['language'] | 1836 | del params_initial['language'] |
@@ -1795,7 +1841,7 @@ class Mastodon: | |||
1795 | headers = {} | 1841 | headers = {} |
1796 | if idempotency_key != None: | 1842 | if idempotency_key != None: |
1797 | headers['Idempotency-Key'] = idempotency_key | 1843 | headers['Idempotency-Key'] = idempotency_key |
1798 | 1844 | ||
1799 | if media_ids is not None: | 1845 | if media_ids is not None: |
1800 | try: | 1846 | try: |
1801 | media_ids_proper = [] | 1847 | media_ids_proper = [] |
@@ -1817,7 +1863,7 @@ class Mastodon: | |||
1817 | use_json = True | 1863 | use_json = True |
1818 | 1864 | ||
1819 | params = self.__generate_params(params_initial, ['idempotency_key']) | 1865 | params = self.__generate_params(params_initial, ['idempotency_key']) |
1820 | return self.__api_request('POST', '/api/v1/statuses', params, headers = headers, use_json = use_json) | 1866 | return self.__api_request('POST', '/api/v1/statuses', params, headers=headers, use_json=use_json) |
1821 | 1867 | ||
1822 | @api_version("1.0.0", "2.8.0", __DICT_VERSION_STATUS) | 1868 | @api_version("1.0.0", "2.8.0", __DICT_VERSION_STATUS) |
1823 | def toot(self, status): | 1869 | def toot(self, status): |
@@ -1832,14 +1878,14 @@ class Mastodon: | |||
1832 | 1878 | ||
1833 | @api_version("1.0.0", "2.8.0", __DICT_VERSION_STATUS) | 1879 | @api_version("1.0.0", "2.8.0", __DICT_VERSION_STATUS) |
1834 | def status_reply(self, to_status, status, in_reply_to_id=None, media_ids=None, | 1880 | def status_reply(self, to_status, status, in_reply_to_id=None, media_ids=None, |
1835 | sensitive=False, visibility=None, spoiler_text=None, | 1881 | sensitive=False, visibility=None, spoiler_text=None, |
1836 | language=None, idempotency_key=None, content_type=None, | 1882 | language=None, idempotency_key=None, content_type=None, |
1837 | scheduled_at=None, poll=None, untag=False): | 1883 | scheduled_at=None, poll=None, untag=False): |
1838 | """ | 1884 | """ |
1839 | Helper function - acts like status_post, but prepends the name of all | 1885 | Helper function - acts like status_post, but prepends the name of all |
1840 | the users that are being replied to to the status text and retains | 1886 | the users that are being replied to to the status text and retains |
1841 | CW and visibility if not explicitly overridden. | 1887 | CW and visibility if not explicitly overridden. |
1842 | 1888 | ||
1843 | Set `untag` to True if you want the reply to only go to the user you | 1889 | Set `untag` to True if you want the reply to only go to the user you |
1844 | are replying to, removing every other mentioned user from the | 1890 | are replying to, removing every other mentioned user from the |
1845 | conversation. | 1891 | conversation. |
@@ -1848,52 +1894,53 @@ class Mastodon: | |||
1848 | del keyword_args["self"] | 1894 | del keyword_args["self"] |
1849 | del keyword_args["to_status"] | 1895 | del keyword_args["to_status"] |
1850 | del keyword_args["untag"] | 1896 | del keyword_args["untag"] |
1851 | 1897 | ||
1852 | user_id = self.__get_logged_in_id() | 1898 | user_id = self.__get_logged_in_id() |
1853 | 1899 | ||
1854 | # Determine users to mention | 1900 | # Determine users to mention |
1855 | mentioned_accounts = collections.OrderedDict() | 1901 | mentioned_accounts = collections.OrderedDict() |
1856 | mentioned_accounts[to_status.account.id] = to_status.account.acct | 1902 | mentioned_accounts[to_status.account.id] = to_status.account.acct |
1857 | 1903 | ||
1858 | if not untag: | 1904 | if not untag: |
1859 | for account in to_status.mentions: | 1905 | for account in to_status.mentions: |
1860 | if account.id != user_id and not account.id in mentioned_accounts.keys(): | 1906 | if account.id != user_id and not account.id in mentioned_accounts.keys(): |
1861 | mentioned_accounts[account.id] = account.acct | 1907 | mentioned_accounts[account.id] = account.acct |
1862 | 1908 | ||
1863 | # Join into one piece of text. The space is added inside because of self-replies. | 1909 | # Join into one piece of text. The space is added inside because of self-replies. |
1864 | status = "".join(map(lambda x: "@" + x + " ", mentioned_accounts.values())) + status | 1910 | status = "".join(map(lambda x: "@" + x + " ", |
1865 | 1911 | mentioned_accounts.values())) + status | |
1912 | |||
1866 | # Retain visibility / cw | 1913 | # Retain visibility / cw |
1867 | if visibility == None and 'visibility' in to_status: | 1914 | if visibility == None and 'visibility' in to_status: |
1868 | visibility = to_status.visibility | 1915 | visibility = to_status.visibility |
1869 | if spoiler_text == None and 'spoiler_text' in to_status: | 1916 | if spoiler_text == None and 'spoiler_text' in to_status: |
1870 | spoiler_text = to_status.spoiler_text | 1917 | spoiler_text = to_status.spoiler_text |
1871 | 1918 | ||
1872 | keyword_args["status"] = status | 1919 | keyword_args["status"] = status |
1873 | keyword_args["visibility"] = visibility | 1920 | keyword_args["visibility"] = visibility |
1874 | keyword_args["spoiler_text"] = spoiler_text | 1921 | keyword_args["spoiler_text"] = spoiler_text |
1875 | keyword_args["in_reply_to_id"] = to_status.id | 1922 | keyword_args["in_reply_to_id"] = to_status.id |
1876 | return self.status_post(**keyword_args) | 1923 | return self.status_post(**keyword_args) |
1877 | 1924 | ||
1878 | @api_version("2.8.0", "2.8.0", __DICT_VERSION_POLL) | 1925 | @api_version("2.8.0", "2.8.0", __DICT_VERSION_POLL) |
1879 | def make_poll(self, options, expires_in, multiple=False, hide_totals=False): | 1926 | def make_poll(self, options, expires_in, multiple=False, hide_totals=False): |
1880 | """ | 1927 | """ |
1881 | Generate a poll object that can be passed as the `poll` option when posting a status. | 1928 | Generate a poll object that can be passed as the `poll` option when posting a status. |
1882 | 1929 | ||
1883 | options is an array of strings with the poll options (Maximum, by default: 4), | 1930 | options is an array of strings with the poll options (Maximum, by default: 4), |
1884 | expires_in is the time in seconds for which the poll should be open. | 1931 | expires_in is the time in seconds for which the poll should be open. |
1885 | Set multiple to True to allow people to choose more than one answer. Set | 1932 | Set multiple to True to allow people to choose more than one answer. Set |
1886 | hide_totals to True to hide the results of the poll until it has expired. | 1933 | hide_totals to True to hide the results of the poll until it has expired. |
1887 | """ | 1934 | """ |
1888 | poll_params = locals() | 1935 | poll_params = locals() |
1889 | del poll_params["self"] | 1936 | del poll_params["self"] |
1890 | return poll_params | 1937 | return poll_params |
1891 | 1938 | ||
1892 | @api_version("1.0.0", "1.0.0", "1.0.0") | 1939 | @api_version("1.0.0", "1.0.0", "1.0.0") |
1893 | def status_delete(self, id): | 1940 | def status_delete(self, id): |
1894 | """ | 1941 | """ |
1895 | Delete a status | 1942 | Delete a status |
1896 | 1943 | ||
1897 | Returns the now-deleted status, with an added "source" attribute that contains | 1944 | Returns the now-deleted status, with an added "source" attribute that contains |
1898 | the text that was used to compose this status (this can be used to power | 1945 | the text that was used to compose this status (this can be used to power |
1899 | "delete and redraft" functionality) | 1946 | "delete and redraft" functionality) |
@@ -1906,7 +1953,7 @@ class Mastodon: | |||
1906 | def status_reblog(self, id, visibility=None): | 1953 | def status_reblog(self, id, visibility=None): |
1907 | """ | 1954 | """ |
1908 | Reblog / boost a status. | 1955 | Reblog / boost a status. |
1909 | 1956 | ||
1910 | The visibility parameter functions the same as in `status_post()`_ and | 1957 | The visibility parameter functions the same as in `status_post()`_ and |
1911 | allows you to reduce the visibility of a reblogged status. | 1958 | allows you to reduce the visibility of a reblogged status. |
1912 | 1959 | ||
@@ -1918,8 +1965,8 @@ class Mastodon: | |||
1918 | params['visibility'] = params['visibility'].lower() | 1965 | params['visibility'] = params['visibility'].lower() |
1919 | if params['visibility'] not in valid_visibilities: | 1966 | if params['visibility'] not in valid_visibilities: |
1920 | raise ValueError('Invalid visibility value! Acceptable ' | 1967 | raise ValueError('Invalid visibility value! Acceptable ' |
1921 | 'values are %s' % valid_visibilities) | 1968 | 'values are %s' % valid_visibilities) |
1922 | 1969 | ||
1923 | id = self.__unpack_id(id) | 1970 | id = self.__unpack_id(id) |
1924 | url = '/api/v1/statuses/{0}/reblog'.format(str(id)) | 1971 | url = '/api/v1/statuses/{0}/reblog'.format(str(id)) |
1925 | return self.__api_request('POST', url, params) | 1972 | return self.__api_request('POST', url, params) |
@@ -1956,7 +2003,7 @@ class Mastodon: | |||
1956 | id = self.__unpack_id(id) | 2003 | id = self.__unpack_id(id) |
1957 | url = '/api/v1/statuses/{0}/unfavourite'.format(str(id)) | 2004 | url = '/api/v1/statuses/{0}/unfavourite'.format(str(id)) |
1958 | return self.__api_request('POST', url) | 2005 | return self.__api_request('POST', url) |
1959 | 2006 | ||
1960 | @api_version("1.4.0", "2.0.0", __DICT_VERSION_STATUS) | 2007 | @api_version("1.4.0", "2.0.0", __DICT_VERSION_STATUS) |
1961 | def status_mute(self, id): | 2008 | def status_mute(self, id): |
1962 | """ | 2009 | """ |
@@ -2000,8 +2047,7 @@ class Mastodon: | |||
2000 | id = self.__unpack_id(id) | 2047 | id = self.__unpack_id(id) |
2001 | url = '/api/v1/statuses/{0}/unpin'.format(str(id)) | 2048 | url = '/api/v1/statuses/{0}/unpin'.format(str(id)) |
2002 | return self.__api_request('POST', url) | 2049 | return self.__api_request('POST', url) |
2003 | 2050 | ||
2004 | |||
2005 | @api_version("3.1.0", "3.1.0", __DICT_VERSION_STATUS) | 2051 | @api_version("3.1.0", "3.1.0", __DICT_VERSION_STATUS) |
2006 | def status_bookmark(self, id): | 2052 | def status_bookmark(self, id): |
2007 | """ | 2053 | """ |
@@ -2031,9 +2077,9 @@ class Mastodon: | |||
2031 | def scheduled_status_update(self, id, scheduled_at): | 2077 | def scheduled_status_update(self, id, scheduled_at): |
2032 | """ | 2078 | """ |
2033 | Update the scheduled time of a scheduled status. | 2079 | Update the scheduled time of a scheduled status. |
2034 | 2080 | ||
2035 | New time must be at least 5 minutes into the future. | 2081 | New time must be at least 5 minutes into the future. |
2036 | 2082 | ||
2037 | Returns a `scheduled toot dict`_ | 2083 | Returns a `scheduled toot dict`_ |
2038 | """ | 2084 | """ |
2039 | scheduled_at = self.__consistent_isoformat_utc(scheduled_at) | 2085 | scheduled_at = self.__consistent_isoformat_utc(scheduled_at) |
@@ -2041,7 +2087,7 @@ class Mastodon: | |||
2041 | params = self.__generate_params(locals(), ['id']) | 2087 | params = self.__generate_params(locals(), ['id']) |
2042 | url = '/api/v1/scheduled_statuses/{0}'.format(str(id)) | 2088 | url = '/api/v1/scheduled_statuses/{0}'.format(str(id)) |
2043 | return self.__api_request('PUT', url, params) | 2089 | return self.__api_request('PUT', url, params) |
2044 | 2090 | ||
2045 | @api_version("2.7.0", "2.7.0", "2.7.0") | 2091 | @api_version("2.7.0", "2.7.0", "2.7.0") |
2046 | def scheduled_status_delete(self, id): | 2092 | def scheduled_status_delete(self, id): |
2047 | """ | 2093 | """ |
@@ -2050,7 +2096,7 @@ class Mastodon: | |||
2050 | id = self.__unpack_id(id) | 2096 | id = self.__unpack_id(id) |
2051 | url = '/api/v1/scheduled_statuses/{0}'.format(str(id)) | 2097 | url = '/api/v1/scheduled_statuses/{0}'.format(str(id)) |
2052 | self.__api_request('DELETE', url) | 2098 | self.__api_request('DELETE', url) |
2053 | 2099 | ||
2054 | ### | 2100 | ### |
2055 | # Writing data: Polls | 2101 | # Writing data: Polls |
2056 | ### | 2102 | ### |
@@ -2058,45 +2104,44 @@ class Mastodon: | |||
2058 | def poll_vote(self, id, choices): | 2104 | def poll_vote(self, id, choices): |
2059 | """ | 2105 | """ |
2060 | Vote in the given poll. | 2106 | Vote in the given poll. |
2061 | 2107 | ||
2062 | `choices` is the index of the choice you wish to register a vote for | 2108 | `choices` is the index of the choice you wish to register a vote for |
2063 | (i.e. its index in the corresponding polls `options` field. In case | 2109 | (i.e. its index in the corresponding polls `options` field. In case |
2064 | of a poll that allows selection of more than one option, a list of | 2110 | of a poll that allows selection of more than one option, a list of |
2065 | indices can be passed. | 2111 | indices can be passed. |
2066 | 2112 | ||
2067 | You can only submit choices for any given poll once in case of | 2113 | You can only submit choices for any given poll once in case of |
2068 | single-option polls, or only once per option in case of multi-option | 2114 | single-option polls, or only once per option in case of multi-option |
2069 | polls. | 2115 | polls. |
2070 | 2116 | ||
2071 | Returns the updated `poll dict`_ | 2117 | Returns the updated `poll dict`_ |
2072 | """ | 2118 | """ |
2073 | id = self.__unpack_id(id) | 2119 | id = self.__unpack_id(id) |
2074 | if not isinstance(choices, list): | 2120 | if not isinstance(choices, list): |
2075 | choices = [choices] | 2121 | choices = [choices] |
2076 | params = self.__generate_params(locals(), ['id']) | 2122 | params = self.__generate_params(locals(), ['id']) |
2077 | 2123 | ||
2078 | url = '/api/v1/polls/{0}/votes'.format(id) | 2124 | url = '/api/v1/polls/{0}/votes'.format(id) |
2079 | self.__api_request('POST', url, params) | 2125 | self.__api_request('POST', url, params) |
2080 | 2126 | ||
2081 | |||
2082 | ### | 2127 | ### |
2083 | # Writing data: Notifications | 2128 | # Writing data: Notifications |
2084 | ### | 2129 | ### |
2130 | |||
2085 | @api_version("1.0.0", "1.0.0", "1.0.0") | 2131 | @api_version("1.0.0", "1.0.0", "1.0.0") |
2086 | def notifications_clear(self): | 2132 | def notifications_clear(self): |
2087 | """ | 2133 | """ |
2088 | Clear out a users notifications | 2134 | Clear out a user's notifications |
2089 | """ | 2135 | """ |
2090 | self.__api_request('POST', '/api/v1/notifications/clear') | 2136 | self.__api_request('POST', '/api/v1/notifications/clear') |
2091 | 2137 | ||
2092 | |||
2093 | @api_version("1.3.0", "2.9.2", "2.9.2") | 2138 | @api_version("1.3.0", "2.9.2", "2.9.2") |
2094 | def notifications_dismiss(self, id): | 2139 | def notifications_dismiss(self, id): |
2095 | """ | 2140 | """ |
2096 | Deletes a single notification | 2141 | Deletes a single notification |
2097 | """ | 2142 | """ |
2098 | id = self.__unpack_id(id) | 2143 | id = self.__unpack_id(id) |
2099 | 2144 | ||
2100 | if self.verify_minimum_version("2.9.2"): | 2145 | if self.verify_minimum_version("2.9.2"): |
2101 | url = '/api/v1/notifications/{0}/dismiss'.format(str(id)) | 2146 | url = '/api/v1/notifications/{0}/dismiss'.format(str(id)) |
2102 | self.__api_request('POST', url) | 2147 | self.__api_request('POST', url) |
@@ -2111,7 +2156,7 @@ class Mastodon: | |||
2111 | def conversations_read(self, id): | 2156 | def conversations_read(self, id): |
2112 | """ | 2157 | """ |
2113 | Marks a single conversation as read. | 2158 | Marks a single conversation as read. |
2114 | 2159 | ||
2115 | Returns the updated `conversation dict`_. | 2160 | Returns the updated `conversation dict`_. |
2116 | """ | 2161 | """ |
2117 | id = self.__unpack_id(id) | 2162 | id = self.__unpack_id(id) |
@@ -2133,10 +2178,10 @@ class Mastodon: | |||
2133 | """ | 2178 | """ |
2134 | id = self.__unpack_id(id) | 2179 | id = self.__unpack_id(id) |
2135 | params = self.__generate_params(locals()) | 2180 | params = self.__generate_params(locals()) |
2136 | 2181 | ||
2137 | if params["reblogs"] == None: | 2182 | if params["reblogs"] == None: |
2138 | del params["reblogs"] | 2183 | del params["reblogs"] |
2139 | 2184 | ||
2140 | url = '/api/v1/accounts/{0}/follow'.format(str(id)) | 2185 | url = '/api/v1/accounts/{0}/follow'.format(str(id)) |
2141 | return self.__api_request('POST', url, params) | 2186 | return self.__api_request('POST', url, params) |
2142 | 2187 | ||
@@ -2213,8 +2258,8 @@ class Mastodon: | |||
2213 | @api_version("1.1.1", "3.1.0", __DICT_VERSION_ACCOUNT) | 2258 | @api_version("1.1.1", "3.1.0", __DICT_VERSION_ACCOUNT) |
2214 | def account_update_credentials(self, display_name=None, note=None, | 2259 | def account_update_credentials(self, display_name=None, note=None, |
2215 | avatar=None, avatar_mime_type=None, | 2260 | avatar=None, avatar_mime_type=None, |
2216 | header=None, header_mime_type=None, | 2261 | header=None, header_mime_type=None, |
2217 | locked=None, bot=None, | 2262 | locked=None, bot=None, |
2218 | discoverable=None, fields=None): | 2263 | discoverable=None, fields=None): |
2219 | """ | 2264 | """ |
2220 | Update the profile for the currently logged-in user. | 2265 | Update the profile for the currently logged-in user. |
@@ -2223,51 +2268,53 @@ class Mastodon: | |||
2223 | 2268 | ||
2224 | `avatar` and 'header' are images. As with media uploads, it is possible to either | 2269 | `avatar` and 'header' are images. As with media uploads, it is possible to either |
2225 | pass image data and a mime type, or a filename of an image file, for either. | 2270 | pass image data and a mime type, or a filename of an image file, for either. |
2226 | 2271 | ||
2227 | `locked` specifies whether the user needs to manually approve follow requests. | 2272 | `locked` specifies whether the user needs to manually approve follow requests. |
2228 | 2273 | ||
2229 | `bot` specifies whether the user should be set to a bot. | 2274 | `bot` specifies whether the user should be set to a bot. |
2230 | 2275 | ||
2231 | `discoverable` specifies whether the user should appear in the user directory. | 2276 | `discoverable` specifies whether the user should appear in the user directory. |
2232 | 2277 | ||
2233 | `fields` can be a list of up to four name-value pairs (specified as tuples) to | 2278 | `fields` can be a list of up to four name-value pairs (specified as tuples) to |
2234 | appear as semi-structured information in the users profile. | 2279 | appear as semi-structured information in the user's profile. |
2235 | 2280 | ||
2236 | Returns the updated `user dict` of the logged-in user. | 2281 | Returns the updated `user dict` of the logged-in user. |
2237 | """ | 2282 | """ |
2238 | params_initial = collections.OrderedDict(locals()) | 2283 | params_initial = collections.OrderedDict(locals()) |
2239 | 2284 | ||
2240 | # Convert fields | 2285 | # Convert fields |
2241 | if fields != None: | 2286 | if fields != None: |
2242 | if len(fields) > 4: | 2287 | if len(fields) > 4: |
2243 | raise MastodonIllegalArgumentError('A maximum of four fields are allowed.') | 2288 | raise MastodonIllegalArgumentError( |
2244 | 2289 | 'A maximum of four fields are allowed.') | |
2290 | |||
2245 | fields_attributes = [] | 2291 | fields_attributes = [] |
2246 | for idx, (field_name, field_value) in enumerate(fields): | 2292 | for idx, (field_name, field_value) in enumerate(fields): |
2247 | params_initial['fields_attributes[' + str(idx) + '][name]'] = field_name | 2293 | params_initial['fields_attributes[' + |
2248 | params_initial['fields_attributes[' + str(idx) + '][value]'] = field_value | 2294 | str(idx) + '][name]'] = field_name |
2249 | 2295 | params_initial['fields_attributes[' + | |
2296 | str(idx) + '][value]'] = field_value | ||
2297 | |||
2250 | # Clean up params | 2298 | # Clean up params |
2251 | for param in ["avatar", "avatar_mime_type", "header", "header_mime_type", "fields"]: | 2299 | for param in ["avatar", "avatar_mime_type", "header", "header_mime_type", "fields"]: |
2252 | if param in params_initial: | 2300 | if param in params_initial: |
2253 | del params_initial[param] | 2301 | del params_initial[param] |
2254 | 2302 | ||
2255 | # Create file info | 2303 | # Create file info |
2256 | files = {} | 2304 | files = {} |
2257 | if not avatar is None: | 2305 | if not avatar is None: |
2258 | files["avatar"] = self.__load_media_file(avatar, avatar_mime_type) | 2306 | files["avatar"] = self.__load_media_file(avatar, avatar_mime_type) |
2259 | if not header is None: | 2307 | if not header is None: |
2260 | files["header"] = self.__load_media_file(header, header_mime_type) | 2308 | files["header"] = self.__load_media_file(header, header_mime_type) |
2261 | 2309 | ||
2262 | params = self.__generate_params(params_initial) | 2310 | params = self.__generate_params(params_initial) |
2263 | return self.__api_request('PATCH', '/api/v1/accounts/update_credentials', params, files=files) | 2311 | return self.__api_request('PATCH', '/api/v1/accounts/update_credentials', params, files=files) |
2264 | 2312 | ||
2265 | |||
2266 | @api_version("2.5.0", "2.5.0", __DICT_VERSION_RELATIONSHIP) | 2313 | @api_version("2.5.0", "2.5.0", __DICT_VERSION_RELATIONSHIP) |
2267 | def account_pin(self, id): | 2314 | def account_pin(self, id): |
2268 | """ | 2315 | """ |
2269 | Pin / endorse a user. | 2316 | Pin / endorse a user. |
2270 | 2317 | ||
2271 | Returns a `relationship dict`_ containing the updated relationship to the user. | 2318 | Returns a `relationship dict`_ containing the updated relationship to the user. |
2272 | """ | 2319 | """ |
2273 | id = self.__unpack_id(id) | 2320 | id = self.__unpack_id(id) |
@@ -2295,34 +2342,34 @@ class Mastodon: | |||
2295 | id = self.__unpack_id(id) | 2342 | id = self.__unpack_id(id) |
2296 | params = self.__generate_params(locals(), ["id"]) | 2343 | params = self.__generate_params(locals(), ["id"]) |
2297 | return self.__api_request('POST', '/api/v1/accounts/{0}/note'.format(str(id)), params) | 2344 | return self.__api_request('POST', '/api/v1/accounts/{0}/note'.format(str(id)), params) |
2298 | 2345 | ||
2299 | @api_version("3.3.0", "3.3.0", __DICT_VERSION_HASHTAG) | 2346 | @api_version("3.3.0", "3.3.0", __DICT_VERSION_HASHTAG) |
2300 | def account_featured_tags(self, id): | 2347 | def account_featured_tags(self, id): |
2301 | """ | 2348 | """ |
2302 | Get an accounts featured hashtags. | 2349 | Get an account's featured hashtags. |
2303 | 2350 | ||
2304 | Returns a list of `hashtag dicts`_ (NOT `featured tag dicts`_). | 2351 | Returns a list of `hashtag dicts`_ (NOT `featured tag dicts`_). |
2305 | """ | 2352 | """ |
2306 | id = self.__unpack_id(id) | 2353 | id = self.__unpack_id(id) |
2307 | return self.__api_request('GET', '/api/v1/accounts/{0}/featured_tags'.format(str(id))) | 2354 | return self.__api_request('GET', '/api/v1/accounts/{0}/featured_tags'.format(str(id))) |
2308 | 2355 | ||
2309 | ### | 2356 | ### |
2310 | # Writing data: Featured hashtags | 2357 | # Writing data: Featured hashtags |
2311 | ### | 2358 | ### |
2312 | @api_version("3.0.0", "3.0.0", __DICT_VERSION_FEATURED_TAG) | 2359 | @api_version("3.0.0", "3.0.0", __DICT_VERSION_FEATURED_TAG) |
2313 | def featured_tag_create(self, name): | 2360 | def featured_tag_create(self, name): |
2314 | """ | 2361 | """ |
2315 | Creates a new featured hashtag displayed on the logged-in users profile. | 2362 | Creates a new featured hashtag displayed on the logged-in user's profile. |
2316 | 2363 | ||
2317 | Returns a `featured tag dict`_ with the newly featured tag. | 2364 | Returns a `featured tag dict`_ with the newly featured tag. |
2318 | """ | 2365 | """ |
2319 | params = self.__generate_params(locals()) | 2366 | params = self.__generate_params(locals()) |
2320 | return self.__api_request('POST', '/api/v1/featured_tags', params) | 2367 | return self.__api_request('POST', '/api/v1/featured_tags', params) |
2321 | 2368 | ||
2322 | @api_version("3.0.0", "3.0.0", __DICT_VERSION_FEATURED_TAG) | 2369 | @api_version("3.0.0", "3.0.0", __DICT_VERSION_FEATURED_TAG) |
2323 | def featured_tag_delete(self, id): | 2370 | def featured_tag_delete(self, id): |
2324 | """ | 2371 | """ |
2325 | Deletes one of the logged-in users featured hashtags. | 2372 | Deletes one of the logged-in user's featured hashtags. |
2326 | """ | 2373 | """ |
2327 | id = self.__unpack_id(id) | 2374 | id = self.__unpack_id(id) |
2328 | url = '/api/v1/featured_tags/{0}'.format(str(id)) | 2375 | url = '/api/v1/featured_tags/{0}'.format(str(id)) |
@@ -2332,44 +2379,44 @@ class Mastodon: | |||
2332 | # Writing data: Keyword filters | 2379 | # Writing data: Keyword filters |
2333 | ### | 2380 | ### |
2334 | @api_version("2.4.3", "2.4.3", __DICT_VERSION_FILTER) | 2381 | @api_version("2.4.3", "2.4.3", __DICT_VERSION_FILTER) |
2335 | def filter_create(self, phrase, context, irreversible = False, whole_word = True, expires_in = None): | 2382 | def filter_create(self, phrase, context, irreversible=False, whole_word=True, expires_in=None): |
2336 | """ | 2383 | """ |
2337 | Creates a new keyword filter. `phrase` is the phrase that should be | 2384 | Creates a new keyword filter. `phrase` is the phrase that should be |
2338 | filtered out, `context` specifies from where to filter the keywords. | 2385 | filtered out, `context` specifies from where to filter the keywords. |
2339 | Valid contexts are 'home', 'notifications', 'public' and 'thread'. | 2386 | Valid contexts are 'home', 'notifications', 'public' and 'thread'. |
2340 | 2387 | ||
2341 | Set `irreversible` to True if you want the filter to just delete statuses | 2388 | Set `irreversible` to True if you want the filter to just delete statuses |
2342 | server side. This works only for the 'home' and 'notifications' contexts. | 2389 | server side. This works only for the 'home' and 'notifications' contexts. |
2343 | 2390 | ||
2344 | Set `whole_word` to False if you want to allow filter matches to | 2391 | Set `whole_word` to False if you want to allow filter matches to |
2345 | start or end within a word, not only at word boundaries. | 2392 | start or end within a word, not only at word boundaries. |
2346 | 2393 | ||
2347 | Set `expires_in` to specify for how many seconds the filter should be | 2394 | Set `expires_in` to specify for how many seconds the filter should be |
2348 | kept around. | 2395 | kept around. |
2349 | 2396 | ||
2350 | Returns the `filter dict`_ of the newly created filter. | 2397 | Returns the `filter dict`_ of the newly created filter. |
2351 | """ | 2398 | """ |
2352 | params = self.__generate_params(locals()) | 2399 | params = self.__generate_params(locals()) |
2353 | 2400 | ||
2354 | for context_val in context: | 2401 | for context_val in context: |
2355 | if not context_val in ['home', 'notifications', 'public', 'thread']: | 2402 | if not context_val in ['home', 'notifications', 'public', 'thread']: |
2356 | raise MastodonIllegalArgumentError('Invalid filter context.') | 2403 | raise MastodonIllegalArgumentError('Invalid filter context.') |
2357 | 2404 | ||
2358 | return self.__api_request('POST', '/api/v1/filters', params) | 2405 | return self.__api_request('POST', '/api/v1/filters', params) |
2359 | 2406 | ||
2360 | @api_version("2.4.3", "2.4.3", __DICT_VERSION_FILTER) | 2407 | @api_version("2.4.3", "2.4.3", __DICT_VERSION_FILTER) |
2361 | def filter_update(self, id, phrase = None, context = None, irreversible = None, whole_word = None, expires_in = None): | 2408 | def filter_update(self, id, phrase=None, context=None, irreversible=None, whole_word=None, expires_in=None): |
2362 | """ | 2409 | """ |
2363 | Updates the filter with the given `id`. Parameters are the same | 2410 | Updates the filter with the given `id`. Parameters are the same |
2364 | as in `filter_create()`. | 2411 | as in `filter_create()`. |
2365 | 2412 | ||
2366 | Returns the `filter dict`_ of the updated filter. | 2413 | Returns the `filter dict`_ of the updated filter. |
2367 | """ | 2414 | """ |
2368 | id = self.__unpack_id(id) | 2415 | id = self.__unpack_id(id) |
2369 | params = self.__generate_params(locals(), ['id']) | 2416 | params = self.__generate_params(locals(), ['id']) |
2370 | url = '/api/v1/filters/{0}'.format(str(id)) | 2417 | url = '/api/v1/filters/{0}'.format(str(id)) |
2371 | return self.__api_request('PUT', url, params) | 2418 | return self.__api_request('PUT', url, params) |
2372 | 2419 | ||
2373 | @api_version("2.4.3", "2.4.3", "2.4.3") | 2420 | @api_version("2.4.3", "2.4.3", "2.4.3") |
2374 | def filter_delete(self, id): | 2421 | def filter_delete(self, id): |
2375 | """ | 2422 | """ |
@@ -2378,7 +2425,7 @@ class Mastodon: | |||
2378 | id = self.__unpack_id(id) | 2425 | id = self.__unpack_id(id) |
2379 | url = '/api/v1/filters/{0}'.format(str(id)) | 2426 | url = '/api/v1/filters/{0}'.format(str(id)) |
2380 | self.__api_request('DELETE', url) | 2427 | self.__api_request('DELETE', url) |
2381 | 2428 | ||
2382 | ### | 2429 | ### |
2383 | # Writing data: Follow suggestions | 2430 | # Writing data: Follow suggestions |
2384 | ### | 2431 | ### |
@@ -2398,23 +2445,23 @@ class Mastodon: | |||
2398 | def list_create(self, title): | 2445 | def list_create(self, title): |
2399 | """ | 2446 | """ |
2400 | Create a new list with the given `title`. | 2447 | Create a new list with the given `title`. |
2401 | 2448 | ||
2402 | Returns the `list dict`_ of the created list. | 2449 | Returns the `list dict`_ of the created list. |
2403 | """ | 2450 | """ |
2404 | params = self.__generate_params(locals()) | 2451 | params = self.__generate_params(locals()) |
2405 | return self.__api_request('POST', '/api/v1/lists', params) | 2452 | return self.__api_request('POST', '/api/v1/lists', params) |
2406 | 2453 | ||
2407 | @api_version("2.1.0", "2.1.0", __DICT_VERSION_LIST) | 2454 | @api_version("2.1.0", "2.1.0", __DICT_VERSION_LIST) |
2408 | def list_update(self, id, title): | 2455 | def list_update(self, id, title): |
2409 | """ | 2456 | """ |
2410 | Update info about a list, where "info" is really the lists `title`. | 2457 | Update info about a list, where "info" is really the lists `title`. |
2411 | 2458 | ||
2412 | Returns the `list dict`_ of the modified list. | 2459 | Returns the `list dict`_ of the modified list. |
2413 | """ | 2460 | """ |
2414 | id = self.__unpack_id(id) | 2461 | id = self.__unpack_id(id) |
2415 | params = self.__generate_params(locals(), ['id']) | 2462 | params = self.__generate_params(locals(), ['id']) |
2416 | return self.__api_request('PUT', '/api/v1/lists/{0}'.format(id), params) | 2463 | return self.__api_request('PUT', '/api/v1/lists/{0}'.format(id), params) |
2417 | 2464 | ||
2418 | @api_version("2.1.0", "2.1.0", "2.1.0") | 2465 | @api_version("2.1.0", "2.1.0", "2.1.0") |
2419 | def list_delete(self, id): | 2466 | def list_delete(self, id): |
2420 | """ | 2467 | """ |
@@ -2422,61 +2469,63 @@ class Mastodon: | |||
2422 | """ | 2469 | """ |
2423 | id = self.__unpack_id(id) | 2470 | id = self.__unpack_id(id) |
2424 | self.__api_request('DELETE', '/api/v1/lists/{0}'.format(id)) | 2471 | self.__api_request('DELETE', '/api/v1/lists/{0}'.format(id)) |
2425 | 2472 | ||
2426 | @api_version("2.1.0", "2.1.0", "2.1.0") | 2473 | @api_version("2.1.0", "2.1.0", "2.1.0") |
2427 | def list_accounts_add(self, id, account_ids): | 2474 | def list_accounts_add(self, id, account_ids): |
2428 | """ | 2475 | """ |
2429 | Add the account(s) given in `account_ids` to the list. | 2476 | Add the account(s) given in `account_ids` to the list. |
2430 | """ | 2477 | """ |
2431 | id = self.__unpack_id(id) | 2478 | id = self.__unpack_id(id) |
2432 | 2479 | ||
2433 | if not isinstance(account_ids, list): | 2480 | if not isinstance(account_ids, list): |
2434 | account_ids = [account_ids] | 2481 | account_ids = [account_ids] |
2435 | account_ids = list(map(lambda x: self.__unpack_id(x), account_ids)) | 2482 | account_ids = list(map(lambda x: self.__unpack_id(x), account_ids)) |
2436 | 2483 | ||
2437 | params = self.__generate_params(locals(), ['id']) | 2484 | params = self.__generate_params(locals(), ['id']) |
2438 | self.__api_request('POST', '/api/v1/lists/{0}/accounts'.format(id), params) | 2485 | self.__api_request( |
2439 | 2486 | 'POST', '/api/v1/lists/{0}/accounts'.format(id), params) | |
2487 | |||
2440 | @api_version("2.1.0", "2.1.0", "2.1.0") | 2488 | @api_version("2.1.0", "2.1.0", "2.1.0") |
2441 | def list_accounts_delete(self, id, account_ids): | 2489 | def list_accounts_delete(self, id, account_ids): |
2442 | """ | 2490 | """ |
2443 | Remove the account(s) given in `account_ids` from the list. | 2491 | Remove the account(s) given in `account_ids` from the list. |
2444 | """ | 2492 | """ |
2445 | id = self.__unpack_id(id) | 2493 | id = self.__unpack_id(id) |
2446 | 2494 | ||
2447 | if not isinstance(account_ids, list): | 2495 | if not isinstance(account_ids, list): |
2448 | account_ids = [account_ids] | 2496 | account_ids = [account_ids] |
2449 | account_ids = list(map(lambda x: self.__unpack_id(x), account_ids)) | 2497 | account_ids = list(map(lambda x: self.__unpack_id(x), account_ids)) |
2450 | 2498 | ||
2451 | params = self.__generate_params(locals(), ['id']) | 2499 | params = self.__generate_params(locals(), ['id']) |
2452 | self.__api_request('DELETE', '/api/v1/lists/{0}/accounts'.format(id), params) | 2500 | self.__api_request( |
2453 | 2501 | 'DELETE', '/api/v1/lists/{0}/accounts'.format(id), params) | |
2502 | |||
2454 | ### | 2503 | ### |
2455 | # Writing data: Reports | 2504 | # Writing data: Reports |
2456 | ### | 2505 | ### |
2457 | @api_version("1.1.0", "2.5.0", __DICT_VERSION_REPORT) | 2506 | @api_version("1.1.0", "2.5.0", __DICT_VERSION_REPORT) |
2458 | def report(self, account_id, status_ids = None, comment = None, forward = False): | 2507 | def report(self, account_id, status_ids=None, comment=None, forward=False): |
2459 | """ | 2508 | """ |
2460 | Report statuses to the instances administrators. | 2509 | Report statuses to the instances administrators. |
2461 | 2510 | ||
2462 | Accepts a list of toot IDs associated with the report, and a comment. | 2511 | Accepts a list of toot IDs associated with the report, and a comment. |
2463 | 2512 | ||
2464 | Set forward to True to forward a report of a remote user to that users | 2513 | Set forward to True to forward a report of a remote user to that users |
2465 | instance as well as sending it to the instance local administrators. | 2514 | instance as well as sending it to the instance local administrators. |
2466 | 2515 | ||
2467 | Returns a `report dict`_. | 2516 | Returns a `report dict`_. |
2468 | """ | 2517 | """ |
2469 | account_id = self.__unpack_id(account_id) | 2518 | account_id = self.__unpack_id(account_id) |
2470 | 2519 | ||
2471 | if not status_ids is None: | 2520 | if not status_ids is None: |
2472 | if not isinstance(status_ids, list): | 2521 | if not isinstance(status_ids, list): |
2473 | status_ids = [status_ids] | 2522 | status_ids = [status_ids] |
2474 | status_ids = list(map(lambda x: self.__unpack_id(x), status_ids)) | 2523 | status_ids = list(map(lambda x: self.__unpack_id(x), status_ids)) |
2475 | 2524 | ||
2476 | params_initial = locals() | 2525 | params_initial = locals() |
2477 | if forward == False: | 2526 | if forward == False: |
2478 | del params_initial['forward'] | 2527 | del params_initial['forward'] |
2479 | 2528 | ||
2480 | params = self.__generate_params(params_initial) | 2529 | params = self.__generate_params(params_initial) |
2481 | return self.__api_request('POST', '/api/v1/reports/', params) | 2530 | return self.__api_request('POST', '/api/v1/reports/', params) |
2482 | 2531 | ||
@@ -2487,7 +2536,7 @@ class Mastodon: | |||
2487 | def follow_request_authorize(self, id): | 2536 | def follow_request_authorize(self, id): |
2488 | """ | 2537 | """ |
2489 | Accept an incoming follow request. | 2538 | Accept an incoming follow request. |
2490 | 2539 | ||
2491 | Returns the updated `relationship dict`_ for the requesting account. | 2540 | Returns the updated `relationship dict`_ for the requesting account. |
2492 | """ | 2541 | """ |
2493 | id = self.__unpack_id(id) | 2542 | id = self.__unpack_id(id) |
@@ -2498,7 +2547,7 @@ class Mastodon: | |||
2498 | def follow_request_reject(self, id): | 2547 | def follow_request_reject(self, id): |
2499 | """ | 2548 | """ |
2500 | Reject an incoming follow request. | 2549 | Reject an incoming follow request. |
2501 | 2550 | ||
2502 | Returns the updated `relationship dict`_ for the requesting account. | 2551 | Returns the updated `relationship dict`_ for the requesting account. |
2503 | """ | 2552 | """ |
2504 | id = self.__unpack_id(id) | 2553 | id = self.__unpack_id(id) |
@@ -2512,9 +2561,9 @@ class Mastodon: | |||
2512 | def media_post(self, media_file, mime_type=None, description=None, focus=None, file_name=None, thumbnail=None, thumbnail_mime_type=None, synchronous=False): | 2561 | def media_post(self, media_file, mime_type=None, description=None, focus=None, file_name=None, thumbnail=None, thumbnail_mime_type=None, synchronous=False): |
2513 | """ | 2562 | """ |
2514 | Post an image, video or audio file. `media_file` can either be data or | 2563 | Post an image, video or audio file. `media_file` can either be data or |
2515 | a file name. If data is passed directly, the mime type has to be specified | 2564 | a file name. If data is passed directly, the mime type has to be specified |
2516 | manually, otherwise, it is determined from the file name. `focus` should be a tuple | 2565 | manually, otherwise, it is determined from the file name. `focus` should be a tuple |
2517 | of floats between -1 and 1, giving the x and y coordinates of the images | 2566 | of floats between -1 and 1, giving the x and y coordinates of the images |
2518 | focus point for cropping (with the origin being the images center). | 2567 | focus point for cropping (with the origin being the images center). |
2519 | 2568 | ||
2520 | Throws a `MastodonIllegalArgumentError` if the mime type of the | 2569 | Throws a `MastodonIllegalArgumentError` if the mime type of the |
@@ -2537,22 +2586,27 @@ class Mastodon: | |||
2537 | "synchronous" to emulate the old behaviour. Not recommended, inefficient | 2586 | "synchronous" to emulate the old behaviour. Not recommended, inefficient |
2538 | and deprecated, you know the deal. | 2587 | and deprecated, you know the deal. |
2539 | """ | 2588 | """ |
2540 | files = {'file': self.__load_media_file(media_file, mime_type, file_name)} | 2589 | files = {'file': self.__load_media_file( |
2590 | media_file, mime_type, file_name)} | ||
2541 | 2591 | ||
2542 | if focus != None: | 2592 | if focus != None: |
2543 | focus = str(focus[0]) + "," + str(focus[1]) | 2593 | focus = str(focus[0]) + "," + str(focus[1]) |
2544 | 2594 | ||
2545 | if not thumbnail is None: | 2595 | if not thumbnail is None: |
2546 | if not self.verify_minimum_version("3.2.0"): | 2596 | if not self.verify_minimum_version("3.2.0"): |
2547 | raise MastodonVersionError('Thumbnail requires version > 3.2.0') | 2597 | raise MastodonVersionError( |
2548 | files["thumbnail"] = self.__load_media_file(thumbnail, thumbnail_mime_type) | 2598 | 'Thumbnail requires version > 3.2.0') |
2599 | files["thumbnail"] = self.__load_media_file( | ||
2600 | thumbnail, thumbnail_mime_type) | ||
2549 | 2601 | ||
2550 | # Disambiguate URL by version | 2602 | # Disambiguate URL by version |
2551 | if self.verify_minimum_version("3.1.4"): | 2603 | if self.verify_minimum_version("3.1.4"): |
2552 | ret_dict = self.__api_request('POST', '/api/v2/media', files = files, params={'description': description, 'focus': focus}) | 2604 | ret_dict = self.__api_request( |
2605 | 'POST', '/api/v2/media', files=files, params={'description': description, 'focus': focus}) | ||
2553 | else: | 2606 | else: |
2554 | ret_dict = self.__api_request('POST', '/api/v1/media', files = files, params={'description': description, 'focus': focus}) | 2607 | ret_dict = self.__api_request( |
2555 | 2608 | 'POST', '/api/v1/media', files=files, params={'description': description, 'focus': focus}) | |
2609 | |||
2556 | # Wait for processing? | 2610 | # Wait for processing? |
2557 | if synchronous: | 2611 | if synchronous: |
2558 | if self.verify_minimum_version("3.1.4"): | 2612 | if self.verify_minimum_version("3.1.4"): |
@@ -2561,36 +2615,40 @@ class Mastodon: | |||
2561 | ret_dict = self.media(ret_dict) | 2615 | ret_dict = self.media(ret_dict) |
2562 | time.sleep(1.0) | 2616 | time.sleep(1.0) |
2563 | except: | 2617 | except: |
2564 | raise MastodonAPIError("Attachment could not be processed") | 2618 | raise MastodonAPIError( |
2619 | "Attachment could not be processed") | ||
2565 | else: | 2620 | else: |
2566 | # Old version always waits | 2621 | # Old version always waits |
2567 | return ret_dict | 2622 | return ret_dict |
2568 | 2623 | ||
2569 | return ret_dict | 2624 | return ret_dict |
2570 | 2625 | ||
2571 | @api_version("2.3.0", "3.2.0", __DICT_VERSION_MEDIA) | 2626 | @api_version("2.3.0", "3.2.0", __DICT_VERSION_MEDIA) |
2572 | def media_update(self, id, description=None, focus=None, thumbnail=None, thumbnail_mime_type=None): | 2627 | def media_update(self, id, description=None, focus=None, thumbnail=None, thumbnail_mime_type=None): |
2573 | """ | 2628 | """ |
2574 | Update the metadata of the media file with the given `id`. `description` and | 2629 | Update the metadata of the media file with the given `id`. `description` and |
2575 | `focus` and `thumbnail` are as in `media_post()`_ . | 2630 | `focus` and `thumbnail` are as in `media_post()`_ . |
2576 | 2631 | ||
2577 | Returns the updated `media dict`_. | 2632 | Returns the updated `media dict`_. |
2578 | """ | 2633 | """ |
2579 | id = self.__unpack_id(id) | 2634 | id = self.__unpack_id(id) |
2580 | 2635 | ||
2581 | if focus != None: | 2636 | if focus != None: |
2582 | focus = str(focus[0]) + "," + str(focus[1]) | 2637 | focus = str(focus[0]) + "," + str(focus[1]) |
2583 | 2638 | ||
2584 | params = self.__generate_params(locals(), ['id', 'thumbnail', 'thumbnail_mime_type']) | 2639 | params = self.__generate_params( |
2640 | locals(), ['id', 'thumbnail', 'thumbnail_mime_type']) | ||
2585 | 2641 | ||
2586 | if not thumbnail is None: | 2642 | if not thumbnail is None: |
2587 | if not self.verify_minimum_version("3.2.0"): | 2643 | if not self.verify_minimum_version("3.2.0"): |
2588 | raise MastodonVersionError('Thumbnail requires version > 3.2.0') | 2644 | raise MastodonVersionError( |
2589 | files = {"thumbnail": self.__load_media_file(thumbnail, thumbnail_mime_type)} | 2645 | 'Thumbnail requires version > 3.2.0') |
2590 | return self.__api_request('PUT', '/api/v1/media/{0}'.format(str(id)), params, files = files) | 2646 | files = {"thumbnail": self.__load_media_file( |
2647 | thumbnail, thumbnail_mime_type)} | ||
2648 | return self.__api_request('PUT', '/api/v1/media/{0}'.format(str(id)), params, files=files) | ||
2591 | else: | 2649 | else: |
2592 | return self.__api_request('PUT', '/api/v1/media/{0}'.format(str(id)), params) | 2650 | return self.__api_request('PUT', '/api/v1/media/{0}'.format(str(id)), params) |
2593 | 2651 | ||
2594 | @api_version("3.1.4", "3.1.4", __DICT_VERSION_MEDIA) | 2652 | @api_version("3.1.4", "3.1.4", __DICT_VERSION_MEDIA) |
2595 | def media(self, id): | 2653 | def media(self, id): |
2596 | """ | 2654 | """ |
@@ -2626,124 +2684,125 @@ class Mastodon: | |||
2626 | def markers_set(self, timelines, last_read_ids): | 2684 | def markers_set(self, timelines, last_read_ids): |
2627 | """ | 2685 | """ |
2628 | Set the "last read" marker(s) for the given timeline(s) to the given id(s) | 2686 | Set the "last read" marker(s) for the given timeline(s) to the given id(s) |
2629 | 2687 | ||
2630 | Note that if you give an invalid timeline name, this will silently do nothing. | 2688 | Note that if you give an invalid timeline name, this will silently do nothing. |
2631 | 2689 | ||
2632 | Returns a dict with the updated `read marker dicts`_, keyed by timeline name. | 2690 | Returns a dict with the updated `read marker dicts`_, keyed by timeline name. |
2633 | """ | 2691 | """ |
2634 | if not isinstance(timelines, (list, tuple)): | 2692 | if not isinstance(timelines, (list, tuple)): |
2635 | timelines = [timelines] | 2693 | timelines = [timelines] |
2636 | 2694 | ||
2637 | if not isinstance(last_read_ids, (list, tuple)): | 2695 | if not isinstance(last_read_ids, (list, tuple)): |
2638 | last_read_ids = [last_read_ids] | 2696 | last_read_ids = [last_read_ids] |
2639 | 2697 | ||
2640 | if len(last_read_ids) != len(timelines): | 2698 | if len(last_read_ids) != len(timelines): |
2641 | raise MastodonIllegalArgumentError("Number of specified timelines and ids must be the same") | 2699 | raise MastodonIllegalArgumentError( |
2642 | 2700 | "Number of specified timelines and ids must be the same") | |
2701 | |||
2643 | params = collections.OrderedDict() | 2702 | params = collections.OrderedDict() |
2644 | for timeline, last_read_id in zip(timelines, last_read_ids): | 2703 | for timeline, last_read_id in zip(timelines, last_read_ids): |
2645 | params[timeline] = collections.OrderedDict() | 2704 | params[timeline] = collections.OrderedDict() |
2646 | params[timeline]["last_read_id"] = self.__unpack_id(last_read_id) | 2705 | params[timeline]["last_read_id"] = self.__unpack_id(last_read_id) |
2647 | 2706 | ||
2648 | return self.__api_request('POST', '/api/v1/markers', params, use_json=True) | 2707 | return self.__api_request('POST', '/api/v1/markers', params, use_json=True) |
2649 | 2708 | ||
2650 | ### | 2709 | ### |
2651 | # Writing data: Push subscriptions | 2710 | # Writing data: Push subscriptions |
2652 | ### | 2711 | ### |
2653 | @api_version("2.4.0", "2.4.0", __DICT_VERSION_PUSH) | 2712 | @api_version("2.4.0", "2.4.0", __DICT_VERSION_PUSH) |
2654 | def push_subscription_set(self, endpoint, encrypt_params, follow_events=None, | 2713 | def push_subscription_set(self, endpoint, encrypt_params, follow_events=None, |
2655 | favourite_events=None, reblog_events=None, | 2714 | favourite_events=None, reblog_events=None, |
2656 | mention_events=None, poll_events=None, | 2715 | mention_events=None, poll_events=None, |
2657 | follow_request_events=None): | 2716 | follow_request_events=None): |
2658 | """ | 2717 | """ |
2659 | Sets up or modifies the push subscription the logged-in user has for this app. | 2718 | Sets up or modifies the push subscription the logged-in user has for this app. |
2660 | 2719 | ||
2661 | `endpoint` is the endpoint URL mastodon should call for pushes. Note that mastodon | 2720 | `endpoint` is the endpoint URL mastodon should call for pushes. Note that mastodon |
2662 | requires https for this URL. `encrypt_params` is a dict with key parameters that allow | 2721 | requires https for this URL. `encrypt_params` is a dict with key parameters that allow |
2663 | the server to encrypt data for you: A public key `pubkey` and a shared secret `auth`. | 2722 | the server to encrypt data for you: A public key `pubkey` and a shared secret `auth`. |
2664 | You can generate this as well as the corresponding private key using the | 2723 | You can generate this as well as the corresponding private key using the |
2665 | `push_subscription_generate_keys()`_ function. | 2724 | `push_subscription_generate_keys()`_ function. |
2666 | 2725 | ||
2667 | The rest of the parameters controls what kind of events you wish to subscribe to. | 2726 | The rest of the parameters controls what kind of events you wish to subscribe to. |
2668 | 2727 | ||
2669 | Returns a `push subscription dict`_. | 2728 | Returns a `push subscription dict`_. |
2670 | """ | 2729 | """ |
2671 | endpoint = Mastodon.__protocolize(endpoint) | 2730 | endpoint = Mastodon.__protocolize(endpoint) |
2672 | 2731 | ||
2673 | push_pubkey_b64 = base64.b64encode(encrypt_params['pubkey']) | 2732 | push_pubkey_b64 = base64.b64encode(encrypt_params['pubkey']) |
2674 | push_auth_b64 = base64.b64encode(encrypt_params['auth']) | 2733 | push_auth_b64 = base64.b64encode(encrypt_params['auth']) |
2675 | 2734 | ||
2676 | params = { | 2735 | params = { |
2677 | 'subscription[endpoint]': endpoint, | 2736 | 'subscription[endpoint]': endpoint, |
2678 | 'subscription[keys][p256dh]': push_pubkey_b64, | 2737 | 'subscription[keys][p256dh]': push_pubkey_b64, |
2679 | 'subscription[keys][auth]': push_auth_b64 | 2738 | 'subscription[keys][auth]': push_auth_b64 |
2680 | } | 2739 | } |
2681 | 2740 | ||
2682 | if follow_events != None: | 2741 | if follow_events != None: |
2683 | params['data[alerts][follow]'] = follow_events | 2742 | params['data[alerts][follow]'] = follow_events |
2684 | 2743 | ||
2685 | if favourite_events != None: | 2744 | if favourite_events != None: |
2686 | params['data[alerts][favourite]'] = favourite_events | 2745 | params['data[alerts][favourite]'] = favourite_events |
2687 | 2746 | ||
2688 | if reblog_events != None: | 2747 | if reblog_events != None: |
2689 | params['data[alerts][reblog]'] = reblog_events | 2748 | params['data[alerts][reblog]'] = reblog_events |
2690 | 2749 | ||
2691 | if mention_events != None: | 2750 | if mention_events != None: |
2692 | params['data[alerts][mention]'] = mention_events | 2751 | params['data[alerts][mention]'] = mention_events |
2693 | 2752 | ||
2694 | if poll_events != None: | 2753 | if poll_events != None: |
2695 | params['data[alerts][poll]'] = poll_events | 2754 | params['data[alerts][poll]'] = poll_events |
2696 | 2755 | ||
2697 | if follow_request_events != None: | 2756 | if follow_request_events != None: |
2698 | params['data[alerts][follow_request]'] = follow_request_events | 2757 | params['data[alerts][follow_request]'] = follow_request_events |
2699 | 2758 | ||
2700 | # Canonicalize booleans | 2759 | # Canonicalize booleans |
2701 | params = self.__generate_params(params) | 2760 | params = self.__generate_params(params) |
2702 | 2761 | ||
2703 | return self.__api_request('POST', '/api/v1/push/subscription', params) | 2762 | return self.__api_request('POST', '/api/v1/push/subscription', params) |
2704 | 2763 | ||
2705 | @api_version("2.4.0", "2.4.0", __DICT_VERSION_PUSH) | 2764 | @api_version("2.4.0", "2.4.0", __DICT_VERSION_PUSH) |
2706 | def push_subscription_update(self, follow_events=None, | 2765 | def push_subscription_update(self, follow_events=None, |
2707 | favourite_events=None, reblog_events=None, | 2766 | favourite_events=None, reblog_events=None, |
2708 | mention_events=None, poll_events=None, | 2767 | mention_events=None, poll_events=None, |
2709 | follow_request_events=None): | 2768 | follow_request_events=None): |
2710 | """ | 2769 | """ |
2711 | Modifies what kind of events the app wishes to subscribe to. | 2770 | Modifies what kind of events the app wishes to subscribe to. |
2712 | 2771 | ||
2713 | Returns the updated `push subscription dict`_. | 2772 | Returns the updated `push subscription dict`_. |
2714 | """ | 2773 | """ |
2715 | params = {} | 2774 | params = {} |
2716 | 2775 | ||
2717 | if follow_events != None: | 2776 | if follow_events != None: |
2718 | params['data[alerts][follow]'] = follow_events | 2777 | params['data[alerts][follow]'] = follow_events |
2719 | 2778 | ||
2720 | if favourite_events != None: | 2779 | if favourite_events != None: |
2721 | params['data[alerts][favourite]'] = favourite_events | 2780 | params['data[alerts][favourite]'] = favourite_events |
2722 | 2781 | ||
2723 | if reblog_events != None: | 2782 | if reblog_events != None: |
2724 | params['data[alerts][reblog]'] = reblog_events | 2783 | params['data[alerts][reblog]'] = reblog_events |
2725 | 2784 | ||
2726 | if mention_events != None: | 2785 | if mention_events != None: |
2727 | params['data[alerts][mention]'] = mention_events | 2786 | params['data[alerts][mention]'] = mention_events |
2728 | 2787 | ||
2729 | if poll_events != None: | 2788 | if poll_events != None: |
2730 | params['data[alerts][poll]'] = poll_events | 2789 | params['data[alerts][poll]'] = poll_events |
2731 | 2790 | ||
2732 | if follow_request_events != None: | 2791 | if follow_request_events != None: |
2733 | params['data[alerts][follow_request]'] = follow_request_events | 2792 | params['data[alerts][follow_request]'] = follow_request_events |
2734 | 2793 | ||
2735 | # Canonicalize booleans | 2794 | # Canonicalize booleans |
2736 | params = self.__generate_params(params) | 2795 | params = self.__generate_params(params) |
2737 | 2796 | ||
2738 | return self.__api_request('PUT', '/api/v1/push/subscription', params) | 2797 | return self.__api_request('PUT', '/api/v1/push/subscription', params) |
2739 | 2798 | ||
2740 | @api_version("2.4.0", "2.4.0", "2.4.0") | 2799 | @api_version("2.4.0", "2.4.0", "2.4.0") |
2741 | def push_subscription_delete(self): | 2800 | def push_subscription_delete(self): |
2742 | """ | 2801 | """ |
2743 | Remove the current push subscription the logged-in user has for this app. | 2802 | Remove the current push subscription the logged-in user has for this app. |
2744 | """ | 2803 | """ |
2745 | self.__api_request('DELETE', '/api/v1/push/subscription') | 2804 | self.__api_request('DELETE', '/api/v1/push/subscription') |
2746 | 2805 | ||
2747 | ### | 2806 | ### |
2748 | # Writing data: Annoucements | 2807 | # Writing data: Annoucements |
2749 | ### | 2808 | ### |
@@ -2753,37 +2812,39 @@ class Mastodon: | |||
2753 | Set the given annoucement to read. | 2812 | Set the given annoucement to read. |
2754 | """ | 2813 | """ |
2755 | id = self.__unpack_id(id) | 2814 | id = self.__unpack_id(id) |
2756 | 2815 | ||
2757 | url = '/api/v1/announcements/{0}/dismiss'.format(str(id)) | 2816 | url = '/api/v1/announcements/{0}/dismiss'.format(str(id)) |
2758 | self.__api_request('POST', url) | 2817 | self.__api_request('POST', url) |
2759 | 2818 | ||
2760 | @api_version("3.1.0", "3.1.0", "3.1.0") | 2819 | @api_version("3.1.0", "3.1.0", "3.1.0") |
2761 | def announcement_reaction_create(self, id, reaction): | 2820 | def announcement_reaction_create(self, id, reaction): |
2762 | """ | 2821 | """ |
2763 | Add a reaction to an announcement. `reaction` can either be a unicode emoji | 2822 | Add a reaction to an announcement. `reaction` can either be a unicode emoji |
2764 | or the name of one of the instances custom emoji. | 2823 | or the name of one of the instances custom emoji. |
2765 | 2824 | ||
2766 | Will throw an API error if the reaction name is not one of the allowed things | 2825 | Will throw an API error if the reaction name is not one of the allowed things |
2767 | or when trying to add a reaction that the user has already added (adding a | 2826 | or when trying to add a reaction that the user has already added (adding a |
2768 | reaction that a different user added is legal and increments the count). | 2827 | reaction that a different user added is legal and increments the count). |
2769 | """ | 2828 | """ |
2770 | id = self.__unpack_id(id) | 2829 | id = self.__unpack_id(id) |
2771 | 2830 | ||
2772 | url = '/api/v1/announcements/{0}/reactions/{1}'.format(str(id), reaction) | 2831 | url = '/api/v1/announcements/{0}/reactions/{1}'.format( |
2832 | str(id), reaction) | ||
2773 | self.__api_request('PUT', url) | 2833 | self.__api_request('PUT', url) |
2774 | 2834 | ||
2775 | @api_version("3.1.0", "3.1.0", "3.1.0") | 2835 | @api_version("3.1.0", "3.1.0", "3.1.0") |
2776 | def announcement_reaction_delete(self, id, reaction): | 2836 | def announcement_reaction_delete(self, id, reaction): |
2777 | """ | 2837 | """ |
2778 | Remove a reaction to an announcement. | 2838 | Remove a reaction to an announcement. |
2779 | 2839 | ||
2780 | Will throw an API error if the reaction does not exist. | 2840 | Will throw an API error if the reaction does not exist. |
2781 | """ | 2841 | """ |
2782 | id = self.__unpack_id(id) | 2842 | id = self.__unpack_id(id) |
2783 | 2843 | ||
2784 | url = '/api/v1/announcements/{0}/reactions/{1}'.format(str(id), reaction) | 2844 | url = '/api/v1/announcements/{0}/reactions/{1}'.format( |
2845 | str(id), reaction) | ||
2785 | self.__api_request('DELETE', url) | 2846 | self.__api_request('DELETE', url) |
2786 | 2847 | ||
2787 | ### | 2848 | ### |
2788 | # Moderation API | 2849 | # Moderation API |
2789 | ### | 2850 | ### |
@@ -2791,7 +2852,7 @@ class Mastodon: | |||
2791 | def admin_accounts(self, remote=False, by_domain=None, status='active', username=None, display_name=None, email=None, ip=None, staff_only=False, max_id=None, min_id=None, since_id=None, limit=None): | 2852 | def admin_accounts(self, remote=False, by_domain=None, status='active', username=None, display_name=None, email=None, ip=None, staff_only=False, max_id=None, min_id=None, since_id=None, limit=None): |
2792 | """ | 2853 | """ |
2793 | Fetches a list of accounts that match given criteria. By default, local accounts are returned. | 2854 | Fetches a list of accounts that match given criteria. By default, local accounts are returned. |
2794 | 2855 | ||
2795 | * Set `remote` to True to get remote accounts, otherwise local accounts are returned (default: local accounts) | 2856 | * Set `remote` to True to get remote accounts, otherwise local accounts are returned (default: local accounts) |
2796 | * Set `by_domain` to a domain to get only accounts from that domain. | 2857 | * Set `by_domain` to a domain to get only accounts from that domain. |
2797 | * Set `status` to one of "active", "pending", "disabled", "silenced" or "suspended" to get only accounts with that moderation status (default: active) | 2858 | * Set `status` to one of "active", "pending", "disabled", "silenced" or "suspended" to get only accounts with that moderation status (default: active) |
@@ -2800,64 +2861,66 @@ class Mastodon: | |||
2800 | * Set `email` to an email to get only accounts with that email (this only works on local accounts). | 2861 | * Set `email` to an email to get only accounts with that email (this only works on local accounts). |
2801 | * Set `ip` to an ip (as a string, standard v4/v6 notation) to get only accounts whose last active ip is that ip (this only works on local accounts). | 2862 | * Set `ip` to an ip (as a string, standard v4/v6 notation) to get only accounts whose last active ip is that ip (this only works on local accounts). |
2802 | * Set `staff_only` to True to only get staff accounts (this only works on local accounts). | 2863 | * Set `staff_only` to True to only get staff accounts (this only works on local accounts). |
2803 | 2864 | ||
2804 | Note that setting the boolean parameters to False does not mean "give me users to which this does not apply" but | 2865 | Note that setting the boolean parameters to False does not mean "give me users to which this does not apply" but |
2805 | instead means "I do not care if users have this attribute". | 2866 | instead means "I do not care if users have this attribute". |
2806 | 2867 | ||
2807 | Returns a list of `admin account dicts`_. | 2868 | Returns a list of `admin account dicts`_. |
2808 | """ | 2869 | """ |
2809 | if max_id != None: | 2870 | if max_id != None: |
2810 | max_id = self.__unpack_id(max_id, dateconv=True) | 2871 | max_id = self.__unpack_id(max_id, dateconv=True) |
2811 | 2872 | ||
2812 | if min_id != None: | 2873 | if min_id != None: |
2813 | min_id = self.__unpack_id(min_id, dateconv=True) | 2874 | min_id = self.__unpack_id(min_id, dateconv=True) |
2814 | 2875 | ||
2815 | if since_id != None: | 2876 | if since_id != None: |
2816 | since_id = self.__unpack_id(since_id, dateconv=True) | 2877 | since_id = self.__unpack_id(since_id, dateconv=True) |
2817 | 2878 | ||
2818 | params = self.__generate_params(locals(), ['remote', 'status', 'staff_only']) | 2879 | params = self.__generate_params( |
2819 | 2880 | locals(), ['remote', 'status', 'staff_only']) | |
2881 | |||
2820 | if remote == True: | 2882 | if remote == True: |
2821 | params["remote"] = True | 2883 | params["remote"] = True |
2822 | 2884 | ||
2823 | mod_statuses = ["active", "pending", "disabled", "silenced", "suspended"] | 2885 | mod_statuses = ["active", "pending", |
2886 | "disabled", "silenced", "suspended"] | ||
2824 | if not status in mod_statuses: | 2887 | if not status in mod_statuses: |
2825 | raise ValueError("Invalid moderation status requested.") | 2888 | raise ValueError("Invalid moderation status requested.") |
2826 | 2889 | ||
2827 | if staff_only == True: | 2890 | if staff_only == True: |
2828 | params["staff"] = True | 2891 | params["staff"] = True |
2829 | 2892 | ||
2830 | for mod_status in mod_statuses: | 2893 | for mod_status in mod_statuses: |
2831 | if status == mod_status: | 2894 | if status == mod_status: |
2832 | params[status] = True | 2895 | params[status] = True |
2833 | 2896 | ||
2834 | return self.__api_request('GET', '/api/v1/admin/accounts', params) | 2897 | return self.__api_request('GET', '/api/v1/admin/accounts', params) |
2835 | 2898 | ||
2836 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_ADMIN_ACCOUNT) | 2899 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_ADMIN_ACCOUNT) |
2837 | def admin_account(self, id): | 2900 | def admin_account(self, id): |
2838 | """ | 2901 | """ |
2839 | Fetches a single `admin account dict`_ for the user with the given id. | 2902 | Fetches a single `admin account dict`_ for the user with the given id. |
2840 | 2903 | ||
2841 | Returns that dict. | 2904 | Returns that dict. |
2842 | """ | 2905 | """ |
2843 | id = self.__unpack_id(id) | 2906 | id = self.__unpack_id(id) |
2844 | return self.__api_request('GET', '/api/v1/admin/accounts/{0}'.format(id)) | 2907 | return self.__api_request('GET', '/api/v1/admin/accounts/{0}'.format(id)) |
2845 | 2908 | ||
2846 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_ADMIN_ACCOUNT) | 2909 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_ADMIN_ACCOUNT) |
2847 | def admin_account_enable(self, id): | 2910 | def admin_account_enable(self, id): |
2848 | """ | 2911 | """ |
2849 | Reenables login for a local account for which login has been disabled. | 2912 | Reenables login for a local account for which login has been disabled. |
2850 | 2913 | ||
2851 | Returns the updated `admin account dict`_. | 2914 | Returns the updated `admin account dict`_. |
2852 | """ | 2915 | """ |
2853 | id = self.__unpack_id(id) | 2916 | id = self.__unpack_id(id) |
2854 | return self.__api_request('POST', '/api/v1/admin/accounts/{0}/enable'.format(id)) | 2917 | return self.__api_request('POST', '/api/v1/admin/accounts/{0}/enable'.format(id)) |
2855 | 2918 | ||
2856 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_ADMIN_ACCOUNT) | 2919 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_ADMIN_ACCOUNT) |
2857 | def admin_account_approve(self, id): | 2920 | def admin_account_approve(self, id): |
2858 | """ | 2921 | """ |
2859 | Approves a pending account. | 2922 | Approves a pending account. |
2860 | 2923 | ||
2861 | Returns the updated `admin account dict`_. | 2924 | Returns the updated `admin account dict`_. |
2862 | """ | 2925 | """ |
2863 | id = self.__unpack_id(id) | 2926 | id = self.__unpack_id(id) |
@@ -2867,37 +2930,37 @@ class Mastodon: | |||
2867 | def admin_account_reject(self, id): | 2930 | def admin_account_reject(self, id): |
2868 | """ | 2931 | """ |
2869 | Rejects and deletes a pending account. | 2932 | Rejects and deletes a pending account. |
2870 | 2933 | ||
2871 | Returns the updated `admin account dict`_ for the account that is now gone. | 2934 | Returns the updated `admin account dict`_ for the account that is now gone. |
2872 | """ | 2935 | """ |
2873 | id = self.__unpack_id(id) | 2936 | id = self.__unpack_id(id) |
2874 | return self.__api_request('POST', '/api/v1/admin/accounts/{0}/reject'.format(id)) | 2937 | return self.__api_request('POST', '/api/v1/admin/accounts/{0}/reject'.format(id)) |
2875 | 2938 | ||
2876 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_ADMIN_ACCOUNT) | 2939 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_ADMIN_ACCOUNT) |
2877 | def admin_account_unsilence(self, id): | 2940 | def admin_account_unsilence(self, id): |
2878 | """ | 2941 | """ |
2879 | Unsilences an account. | 2942 | Unsilences an account. |
2880 | 2943 | ||
2881 | Returns the updated `admin account dict`_. | 2944 | Returns the updated `admin account dict`_. |
2882 | """ | 2945 | """ |
2883 | id = self.__unpack_id(id) | 2946 | id = self.__unpack_id(id) |
2884 | return self.__api_request('POST', '/api/v1/admin/accounts/{0}/unsilence'.format(id)) | 2947 | return self.__api_request('POST', '/api/v1/admin/accounts/{0}/unsilence'.format(id)) |
2885 | 2948 | ||
2886 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_ADMIN_ACCOUNT) | 2949 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_ADMIN_ACCOUNT) |
2887 | def admin_account_unsuspend(self, id): | 2950 | def admin_account_unsuspend(self, id): |
2888 | """ | 2951 | """ |
2889 | Unsuspends an account. | 2952 | Unsuspends an account. |
2890 | 2953 | ||
2891 | Returns the updated `admin account dict`_. | 2954 | Returns the updated `admin account dict`_. |
2892 | """ | 2955 | """ |
2893 | id = self.__unpack_id(id) | 2956 | id = self.__unpack_id(id) |
2894 | return self.__api_request('POST', '/api/v1/admin/accounts/{0}/unsuspend'.format(id)) | 2957 | return self.__api_request('POST', '/api/v1/admin/accounts/{0}/unsuspend'.format(id)) |
2895 | 2958 | ||
2896 | @api_version("3.3.0", "3.3.0", __DICT_VERSION_ADMIN_ACCOUNT) | 2959 | @api_version("3.3.0", "3.3.0", __DICT_VERSION_ADMIN_ACCOUNT) |
2897 | def admin_account_delete(self, id): | 2960 | def admin_account_delete(self, id): |
2898 | """ | 2961 | """ |
2899 | Delete a local user account. | 2962 | Delete a local user account. |
2900 | 2963 | ||
2901 | The deleted accounts `admin account dict`_. | 2964 | The deleted accounts `admin account dict`_. |
2902 | """ | 2965 | """ |
2903 | id = self.__unpack_id(id) | 2966 | id = self.__unpack_id(id) |
@@ -2907,7 +2970,7 @@ class Mastodon: | |||
2907 | def admin_account_unsensitive(self, id): | 2970 | def admin_account_unsensitive(self, id): |
2908 | """ | 2971 | """ |
2909 | Unmark an account as force-sensitive. | 2972 | Unmark an account as force-sensitive. |
2910 | 2973 | ||
2911 | Returns the updated `admin account dict`_. | 2974 | Returns the updated `admin account dict`_. |
2912 | """ | 2975 | """ |
2913 | id = self.__unpack_id(id) | 2976 | id = self.__unpack_id(id) |
@@ -2917,209 +2980,221 @@ class Mastodon: | |||
2917 | def admin_account_moderate(self, id, action=None, report_id=None, warning_preset_id=None, text=None, send_email_notification=True): | 2980 | def admin_account_moderate(self, id, action=None, report_id=None, warning_preset_id=None, text=None, send_email_notification=True): |
2918 | """ | 2981 | """ |
2919 | Perform a moderation action on an account. | 2982 | Perform a moderation action on an account. |
2920 | 2983 | ||
2921 | Valid actions are: | 2984 | Valid actions are: |
2922 | * "disable" - for a local user, disable login. | 2985 | * "disable" - for a local user, disable login. |
2923 | * "silence" - hide the users posts from all public timelines. | 2986 | * "silence" - hide the users posts from all public timelines. |
2924 | * "suspend" - irreversibly delete all the users posts, past and future. | 2987 | * "suspend" - irreversibly delete all the user's posts, past and future. |
2925 | * "sensitive" - forcce an accounts media visibility to always be sensitive. | 2988 | * "sensitive" - forcce an accounts media visibility to always be sensitive. |
2989 | |||
2926 | If no action is specified, the user is only issued a warning. | 2990 | If no action is specified, the user is only issued a warning. |
2927 | 2991 | ||
2928 | Specify the id of a report as `report_id` to close the report with this moderation action as the resolution. | 2992 | Specify the id of a report as `report_id` to close the report with this moderation action as the resolution. |
2929 | Specify `warning_preset_id` to use a warning preset as the notification text to the user, or `text` to specify text directly. | 2993 | Specify `warning_preset_id` to use a warning preset as the notification text to the user, or `text` to specify text directly. |
2930 | If both are specified, they are concatenated (preset first). Note that there is currently no API to retrieve or create | 2994 | If both are specified, they are concatenated (preset first). Note that there is currently no API to retrieve or create |
2931 | warning presets. | 2995 | warning presets. |
2932 | 2996 | ||
2933 | Set `send_email_notification` to False to not send the user an e-mail notification informing them of the moderation action. | 2997 | Set `send_email_notification` to False to not send the user an email notification informing them of the moderation action. |
2934 | """ | 2998 | """ |
2935 | if action is None: | 2999 | if action is None: |
2936 | action = "none" | 3000 | action = "none" |
2937 | 3001 | ||
2938 | if send_email_notification == False: | 3002 | if send_email_notification == False: |
2939 | send_email_notification = None | 3003 | send_email_notification = None |
2940 | 3004 | ||
2941 | id = self.__unpack_id(id) | 3005 | id = self.__unpack_id(id) |
2942 | if not report_id is None: | 3006 | if not report_id is None: |
2943 | report_id = self.__unpack_id(report_id) | 3007 | report_id = self.__unpack_id(report_id) |
2944 | 3008 | ||
2945 | params = self.__generate_params(locals(), ['id', 'action']) | 3009 | params = self.__generate_params(locals(), ['id', 'action']) |
2946 | 3010 | ||
2947 | params["type"] = action | 3011 | params["type"] = action |
2948 | 3012 | ||
2949 | self.__api_request('POST', '/api/v1/admin/accounts/{0}/action'.format(id), params) | 3013 | self.__api_request( |
2950 | 3014 | 'POST', '/api/v1/admin/accounts/{0}/action'.format(id), params) | |
3015 | |||
2951 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_REPORT) | 3016 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_REPORT) |
2952 | def admin_reports(self, resolved=False, account_id=None, target_account_id=None, max_id=None, min_id=None, since_id=None, limit=None): | 3017 | def admin_reports(self, resolved=False, account_id=None, target_account_id=None, max_id=None, min_id=None, since_id=None, limit=None): |
2953 | """ | 3018 | """ |
2954 | Fetches the list of reports. | 3019 | Fetches the list of reports. |
2955 | 3020 | ||
2956 | Set `resolved` to True to search for resolved reports. `account_id` and `target_account_id` | 3021 | Set `resolved` to True to search for resolved reports. `account_id` and `target_account_id` |
2957 | can be used to get reports filed by or about a specific user. | 3022 | can be used to get reports filed by or about a specific user. |
2958 | 3023 | ||
2959 | Returns a list of `report dicts`_. | 3024 | Returns a list of `report dicts`_. |
2960 | """ | 3025 | """ |
2961 | if max_id != None: | 3026 | if max_id != None: |
2962 | max_id = self.__unpack_id(max_id, dateconv=True) | 3027 | max_id = self.__unpack_id(max_id, dateconv=True) |
2963 | 3028 | ||
2964 | if min_id != None: | 3029 | if min_id != None: |
2965 | min_id = self.__unpack_id(min_id, dateconv=True) | 3030 | min_id = self.__unpack_id(min_id, dateconv=True) |
2966 | 3031 | ||
2967 | if since_id != None: | 3032 | if since_id != None: |
2968 | since_id = self.__unpack_id(since_id, dateconv=True) | 3033 | since_id = self.__unpack_id(since_id, dateconv=True) |
2969 | 3034 | ||
2970 | if not account_id is None: | 3035 | if not account_id is None: |
2971 | account_id = self.__unpack_id(account_id) | 3036 | account_id = self.__unpack_id(account_id) |
2972 | 3037 | ||
2973 | if not target_account_id is None: | 3038 | if not target_account_id is None: |
2974 | target_account_id = self.__unpack_id(target_account_id) | 3039 | target_account_id = self.__unpack_id(target_account_id) |
2975 | 3040 | ||
2976 | if resolved == False: | 3041 | if resolved == False: |
2977 | resolved = None | 3042 | resolved = None |
2978 | 3043 | ||
2979 | params = self.__generate_params(locals()) | 3044 | params = self.__generate_params(locals()) |
2980 | return self.__api_request('GET', '/api/v1/admin/reports', params) | 3045 | return self.__api_request('GET', '/api/v1/admin/reports', params) |
2981 | 3046 | ||
2982 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_REPORT) | 3047 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_REPORT) |
2983 | def admin_report(self, id): | 3048 | def admin_report(self, id): |
2984 | """ | 3049 | """ |
2985 | Fetches the report with the given id. | 3050 | Fetches the report with the given id. |
2986 | 3051 | ||
2987 | Returns a `report dict`_. | 3052 | Returns a `report dict`_. |
2988 | """ | 3053 | """ |
2989 | id = self.__unpack_id(id) | 3054 | id = self.__unpack_id(id) |
2990 | return self.__api_request('GET', '/api/v1/admin/reports/{0}'.format(id)) | 3055 | return self.__api_request('GET', '/api/v1/admin/reports/{0}'.format(id)) |
2991 | 3056 | ||
2992 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_REPORT) | 3057 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_REPORT) |
2993 | def admin_report_assign(self, id): | 3058 | def admin_report_assign(self, id): |
2994 | """ | 3059 | """ |
2995 | Assigns the given report to the logged-in user. | 3060 | Assigns the given report to the logged-in user. |
2996 | 3061 | ||
2997 | Returns the updated `report dict`_. | 3062 | Returns the updated `report dict`_. |
2998 | """ | 3063 | """ |
2999 | id = self.__unpack_id(id) | 3064 | id = self.__unpack_id(id) |
3000 | return self.__api_request('POST', '/api/v1/admin/reports/{0}/assign_to_self'.format(id)) | 3065 | return self.__api_request('POST', '/api/v1/admin/reports/{0}/assign_to_self'.format(id)) |
3001 | 3066 | ||
3002 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_REPORT) | 3067 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_REPORT) |
3003 | def admin_report_unassign(self, id): | 3068 | def admin_report_unassign(self, id): |
3004 | """ | 3069 | """ |
3005 | Unassigns the given report from the logged-in user. | 3070 | Unassigns the given report from the logged-in user. |
3006 | 3071 | ||
3007 | Returns the updated `report dict`_. | 3072 | Returns the updated `report dict`_. |
3008 | """ | 3073 | """ |
3009 | id = self.__unpack_id(id) | 3074 | id = self.__unpack_id(id) |
3010 | return self.__api_request('POST', '/api/v1/admin/reports/{0}/unassign'.format(id)) | 3075 | return self.__api_request('POST', '/api/v1/admin/reports/{0}/unassign'.format(id)) |
3011 | 3076 | ||
3012 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_REPORT) | 3077 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_REPORT) |
3013 | def admin_report_reopen(self, id): | 3078 | def admin_report_reopen(self, id): |
3014 | """ | 3079 | """ |
3015 | Reopens a closed report. | 3080 | Reopens a closed report. |
3016 | 3081 | ||
3017 | Returns the updated `report dict`_. | 3082 | Returns the updated `report dict`_. |
3018 | """ | 3083 | """ |
3019 | id = self.__unpack_id(id) | 3084 | id = self.__unpack_id(id) |
3020 | return self.__api_request('POST', '/api/v1/admin/reports/{0}/reopen'.format(id)) | 3085 | return self.__api_request('POST', '/api/v1/admin/reports/{0}/reopen'.format(id)) |
3021 | 3086 | ||
3022 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_REPORT) | 3087 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_REPORT) |
3023 | def admin_report_resolve(self, id): | 3088 | def admin_report_resolve(self, id): |
3024 | """ | 3089 | """ |
3025 | Marks a report as resolved (without taking any action). | 3090 | Marks a report as resolved (without taking any action). |
3026 | 3091 | ||
3027 | Returns the updated `report dict`_. | 3092 | Returns the updated `report dict`_. |
3028 | """ | 3093 | """ |
3029 | id = self.__unpack_id(id) | 3094 | id = self.__unpack_id(id) |
3030 | return self.__api_request('POST', '/api/v1/admin/reports/{0}/resolve'.format(id)) | 3095 | return self.__api_request('POST', '/api/v1/admin/reports/{0}/resolve'.format(id)) |
3031 | 3096 | ||
3032 | ### | 3097 | ### |
3033 | # Push subscription crypto utilities | 3098 | # Push subscription crypto utilities |
3034 | ### | 3099 | ### |
3035 | def push_subscription_generate_keys(self): | 3100 | def push_subscription_generate_keys(self): |
3036 | """ | 3101 | """ |
3037 | Generates a private key, public key and shared secret for use in webpush subscriptions. | 3102 | Generates a private key, public key and shared secret for use in webpush subscriptions. |
3038 | 3103 | ||
3039 | Returns two dicts: One with the private key and shared secret and another with the | 3104 | Returns two dicts: One with the private key and shared secret and another with the |
3040 | public key and shared secret. | 3105 | public key and shared secret. |
3041 | """ | 3106 | """ |
3042 | if not IMPL_HAS_CRYPTO: | 3107 | if not IMPL_HAS_CRYPTO: |
3043 | raise NotImplementedError('To use the crypto tools, please install the webpush feature dependencies.') | 3108 | raise NotImplementedError( |
3044 | 3109 | 'To use the crypto tools, please install the webpush feature dependencies.') | |
3045 | push_key_pair = ec.generate_private_key(ec.SECP256R1(), default_backend()) | 3110 | |
3111 | push_key_pair = ec.generate_private_key( | ||
3112 | ec.SECP256R1(), default_backend()) | ||
3046 | push_key_priv = push_key_pair.private_numbers().private_value | 3113 | push_key_priv = push_key_pair.private_numbers().private_value |
3047 | 3114 | ||
3048 | crypto_ver = cryptography.__version__ | 3115 | crypto_ver = cryptography.__version__ |
3049 | if len(crypto_ver) < 5: | 3116 | if len(crypto_ver) < 5: |
3050 | crypto_ver += ".0" | 3117 | crypto_ver += ".0" |
3051 | if bigger_version(crypto_ver, "2.5.0") == crypto_ver: | 3118 | if bigger_version(crypto_ver, "2.5.0") == crypto_ver: |
3052 | push_key_pub = push_key_pair.public_key().public_bytes(serialization.Encoding.X962, serialization.PublicFormat.UncompressedPoint) | 3119 | push_key_pub = push_key_pair.public_key().public_bytes( |
3120 | serialization.Encoding.X962, serialization.PublicFormat.UncompressedPoint) | ||
3053 | else: | 3121 | else: |
3054 | push_key_pub = push_key_pair.public_key().public_numbers().encode_point() | 3122 | push_key_pub = push_key_pair.public_key().public_numbers().encode_point() |
3055 | push_shared_secret = os.urandom(16) | 3123 | push_shared_secret = os.urandom(16) |
3056 | 3124 | ||
3057 | priv_dict = { | 3125 | priv_dict = { |
3058 | 'privkey': push_key_priv, | 3126 | 'privkey': push_key_priv, |
3059 | 'auth': push_shared_secret | 3127 | 'auth': push_shared_secret |
3060 | } | 3128 | } |
3061 | 3129 | ||
3062 | pub_dict = { | 3130 | pub_dict = { |
3063 | 'pubkey': push_key_pub, | 3131 | 'pubkey': push_key_pub, |
3064 | 'auth': push_shared_secret | 3132 | 'auth': push_shared_secret |
3065 | } | 3133 | } |
3066 | 3134 | ||
3067 | return priv_dict, pub_dict | 3135 | return priv_dict, pub_dict |
3068 | 3136 | ||
3069 | @api_version("2.4.0", "2.4.0", __DICT_VERSION_PUSH_NOTIF) | 3137 | @api_version("2.4.0", "2.4.0", __DICT_VERSION_PUSH_NOTIF) |
3070 | def push_subscription_decrypt_push(self, data, decrypt_params, encryption_header, crypto_key_header): | 3138 | def push_subscription_decrypt_push(self, data, decrypt_params, encryption_header, crypto_key_header): |
3071 | """ | 3139 | """ |
3072 | Decrypts `data` received in a webpush request. Requires the private key dict | 3140 | Decrypts `data` received in a webpush request. Requires the private key dict |
3073 | from `push_subscription_generate_keys()`_ (`decrypt_params`) as well as the | 3141 | from `push_subscription_generate_keys()`_ (`decrypt_params`) as well as the |
3074 | Encryption and server Crypto-Key headers from the received webpush | 3142 | Encryption and server Crypto-Key headers from the received webpush |
3075 | 3143 | ||
3076 | Returns the decoded webpush as a `push notification dict`_. | 3144 | Returns the decoded webpush as a `push notification dict`_. |
3077 | """ | 3145 | """ |
3078 | if (not IMPL_HAS_ECE) or (not IMPL_HAS_CRYPTO): | 3146 | if (not IMPL_HAS_ECE) or (not IMPL_HAS_CRYPTO): |
3079 | raise NotImplementedError('To use the crypto tools, please install the webpush feature dependencies.') | 3147 | raise NotImplementedError( |
3080 | 3148 | 'To use the crypto tools, please install the webpush feature dependencies.') | |
3081 | salt = self.__decode_webpush_b64(encryption_header.split("salt=")[1].strip()) | 3149 | |
3082 | dhparams = self.__decode_webpush_b64(crypto_key_header.split("dh=")[1].split(";")[0].strip()) | 3150 | salt = self.__decode_webpush_b64( |
3083 | p256ecdsa = self.__decode_webpush_b64(crypto_key_header.split("p256ecdsa=")[1].strip()) | 3151 | encryption_header.split("salt=")[1].strip()) |
3084 | dec_key = ec.derive_private_key(decrypt_params['privkey'], ec.SECP256R1(), default_backend()) | 3152 | dhparams = self.__decode_webpush_b64( |
3153 | crypto_key_header.split("dh=")[1].split(";")[0].strip()) | ||
3154 | p256ecdsa = self.__decode_webpush_b64( | ||
3155 | crypto_key_header.split("p256ecdsa=")[1].strip()) | ||
3156 | dec_key = ec.derive_private_key( | ||
3157 | decrypt_params['privkey'], ec.SECP256R1(), default_backend()) | ||
3085 | decrypted = http_ece.decrypt( | 3158 | decrypted = http_ece.decrypt( |
3086 | data, | 3159 | data, |
3087 | salt = salt, | 3160 | salt=salt, |
3088 | key = p256ecdsa, | 3161 | key=p256ecdsa, |
3089 | private_key = dec_key, | 3162 | private_key=dec_key, |
3090 | dh = dhparams, | 3163 | dh=dhparams, |
3091 | auth_secret=decrypt_params['auth'], | 3164 | auth_secret=decrypt_params['auth'], |
3092 | keylabel = "P-256", | 3165 | keylabel="P-256", |
3093 | version = "aesgcm" | 3166 | version="aesgcm" |
3094 | ) | 3167 | ) |
3095 | 3168 | ||
3096 | return json.loads(decrypted.decode('utf-8'), object_hook = Mastodon.__json_hooks) | 3169 | return json.loads(decrypted.decode('utf-8'), object_hook=Mastodon.__json_hooks) |
3097 | 3170 | ||
3098 | ### | 3171 | ### |
3099 | # Blurhash utilities | 3172 | # Blurhash utilities |
3100 | ### | 3173 | ### |
3101 | def decode_blurhash(self, media_dict, out_size = (16, 16), size_per_component = True, return_linear = True): | 3174 | def decode_blurhash(self, media_dict, out_size=(16, 16), size_per_component=True, return_linear=True): |
3102 | """ | 3175 | """ |
3103 | Basic media-dict blurhash decoding. | 3176 | Basic media-dict blurhash decoding. |
3104 | 3177 | ||
3105 | out_size is the desired result size in pixels, either absolute or per blurhash | 3178 | out_size is the desired result size in pixels, either absolute or per blurhash |
3106 | component (this is the default). | 3179 | component (this is the default). |
3107 | 3180 | ||
3108 | By default, this function will return the image as linear RGB, ready for further | 3181 | By default, this function will return the image as linear RGB, ready for further |
3109 | scaling operations. If you want to display the image directly, set return_linear | 3182 | scaling operations. If you want to display the image directly, set return_linear |
3110 | to False. | 3183 | to False. |
3111 | 3184 | ||
3112 | Returns the decoded blurhash image as a three-dimensional list: [height][width][3], | 3185 | Returns the decoded blurhash image as a three-dimensional list: [height][width][3], |
3113 | with the last dimension being RGB colours. | 3186 | with the last dimension being RGB colours. |
3114 | 3187 | ||
3115 | For further info and tips for advanced usage, refer to the documentation for the | 3188 | For further info and tips for advanced usage, refer to the documentation for the |
3116 | blurhash module: https://github.com/halcy/blurhash-python | 3189 | blurhash module: https://github.com/halcy/blurhash-python |
3117 | """ | 3190 | """ |
3118 | if not IMPL_HAS_BLURHASH: | 3191 | if not IMPL_HAS_BLURHASH: |
3119 | raise NotImplementedError('To use the blurhash functions, please install the blurhash python module.') | 3192 | raise NotImplementedError( |
3193 | 'To use the blurhash functions, please install the blurhash Python module.') | ||
3120 | 3194 | ||
3121 | # Figure out what size to decode to | 3195 | # Figure out what size to decode to |
3122 | decode_components_x, decode_components_y = blurhash.components(media_dict["blurhash"]) | 3196 | decode_components_x, decode_components_y = blurhash.components( |
3197 | media_dict["blurhash"]) | ||
3123 | if size_per_component == False: | 3198 | if size_per_component == False: |
3124 | decode_size_x = out_size[0] | 3199 | decode_size_x = out_size[0] |
3125 | decode_size_y = out_size[1] | 3200 | decode_size_y = out_size[1] |
@@ -3128,11 +3203,12 @@ class Mastodon: | |||
3128 | decode_size_y = decode_components_y * out_size[1] | 3203 | decode_size_y = decode_components_y * out_size[1] |
3129 | 3204 | ||
3130 | # Decode | 3205 | # Decode |
3131 | decoded_image = blurhash.decode(media_dict["blurhash"], decode_size_x, decode_size_y, linear = return_linear) | 3206 | decoded_image = blurhash.decode( |
3207 | media_dict["blurhash"], decode_size_x, decode_size_y, linear=return_linear) | ||
3132 | 3208 | ||
3133 | # And that's pretty much it. | 3209 | # And that's pretty much it. |
3134 | return decoded_image | 3210 | return decoded_image |
3135 | 3211 | ||
3136 | ### | 3212 | ### |
3137 | # Pagination | 3213 | # Pagination |
3138 | ### | 3214 | ### |
@@ -3206,7 +3282,7 @@ class Mastodon: | |||
3206 | ### | 3282 | ### |
3207 | # Streaming | 3283 | # Streaming |
3208 | ### | 3284 | ### |
3209 | @api_version("1.1.0", "1.4.2", __DICT_VERSION_STATUS) | 3285 | @api_version("1.1.0", "1.4.2", __DICT_VERSION_STATUS) |
3210 | def stream_user(self, listener, run_async=False, timeout=__DEFAULT_STREAM_TIMEOUT, reconnect_async=False, reconnect_async_wait_sec=__DEFAULT_STREAM_RECONNECT_WAIT_SEC): | 3286 | def stream_user(self, listener, run_async=False, timeout=__DEFAULT_STREAM_TIMEOUT, reconnect_async=False, reconnect_async_wait_sec=__DEFAULT_STREAM_RECONNECT_WAIT_SEC): |
3211 | """ | 3287 | """ |
3212 | Streams events that are relevant to the authorized user, i.e. home | 3288 | Streams events that are relevant to the authorized user, i.e. home |
@@ -3233,11 +3309,12 @@ class Mastodon: | |||
3233 | """ | 3309 | """ |
3234 | Stream for all public statuses for the hashtag 'tag' seen by the connected | 3310 | Stream for all public statuses for the hashtag 'tag' seen by the connected |
3235 | instance. | 3311 | instance. |
3236 | 3312 | ||
3237 | Set local to True to only get local statuses. | 3313 | Set local to True to only get local statuses. |
3238 | """ | 3314 | """ |
3239 | if tag.startswith("#"): | 3315 | if tag.startswith("#"): |
3240 | raise MastodonIllegalArgumentError("Tag parameter should omit leading #") | 3316 | raise MastodonIllegalArgumentError( |
3317 | "Tag parameter should omit leading #") | ||
3241 | base = '/api/v1/streaming/hashtag' | 3318 | base = '/api/v1/streaming/hashtag' |
3242 | if local: | 3319 | if local: |
3243 | base += '/local' | 3320 | base += '/local' |
@@ -3247,28 +3324,29 @@ class Mastodon: | |||
3247 | def stream_list(self, id, listener, run_async=False, timeout=__DEFAULT_STREAM_TIMEOUT, reconnect_async=False, reconnect_async_wait_sec=__DEFAULT_STREAM_RECONNECT_WAIT_SEC): | 3324 | def stream_list(self, id, listener, run_async=False, timeout=__DEFAULT_STREAM_TIMEOUT, reconnect_async=False, reconnect_async_wait_sec=__DEFAULT_STREAM_RECONNECT_WAIT_SEC): |
3248 | """ | 3325 | """ |
3249 | Stream events for the current user, restricted to accounts on the given | 3326 | Stream events for the current user, restricted to accounts on the given |
3250 | list. | 3327 | list. |
3251 | """ | 3328 | """ |
3252 | id = self.__unpack_id(id) | 3329 | id = self.__unpack_id(id) |
3253 | return self.__stream("/api/v1/streaming/list?list={}".format(id), listener, run_async=run_async, timeout=timeout, reconnect_async=reconnect_async, reconnect_async_wait_sec=reconnect_async_wait_sec) | 3330 | return self.__stream("/api/v1/streaming/list?list={}".format(id), listener, run_async=run_async, timeout=timeout, reconnect_async=reconnect_async, reconnect_async_wait_sec=reconnect_async_wait_sec) |
3254 | 3331 | ||
3255 | @api_version("2.6.0", "2.6.0", __DICT_VERSION_STATUS) | 3332 | @api_version("2.6.0", "2.6.0", __DICT_VERSION_STATUS) |
3256 | def stream_direct(self, listener, run_async=False, timeout=__DEFAULT_STREAM_TIMEOUT, reconnect_async=False, reconnect_async_wait_sec=__DEFAULT_STREAM_RECONNECT_WAIT_SEC): | 3333 | def stream_direct(self, listener, run_async=False, timeout=__DEFAULT_STREAM_TIMEOUT, reconnect_async=False, reconnect_async_wait_sec=__DEFAULT_STREAM_RECONNECT_WAIT_SEC): |
3257 | """ | 3334 | """ |
3258 | Streams direct message events for the logged-in user, as conversation events. | 3335 | Streams direct message events for the logged-in user, as conversation events. |
3259 | """ | 3336 | """ |
3260 | return self.__stream('/api/v1/streaming/direct', listener, run_async=run_async, timeout=timeout, reconnect_async=reconnect_async, reconnect_async_wait_sec=reconnect_async_wait_sec) | 3337 | return self.__stream('/api/v1/streaming/direct', listener, run_async=run_async, timeout=timeout, reconnect_async=reconnect_async, reconnect_async_wait_sec=reconnect_async_wait_sec) |
3261 | 3338 | ||
3262 | @api_version("2.5.0", "2.5.0", "2.5.0") | 3339 | @api_version("2.5.0", "2.5.0", "2.5.0") |
3263 | def stream_healthy(self): | 3340 | def stream_healthy(self): |
3264 | """ | 3341 | """ |
3265 | Returns without True if streaming API is okay, False or raises an error otherwise. | 3342 | Returns without True if streaming API is okay, False or raises an error otherwise. |
3266 | """ | 3343 | """ |
3267 | api_okay = self.__api_request('GET', '/api/v1/streaming/health', base_url_override = self.__get_streaming_base(), parse=False) | 3344 | api_okay = self.__api_request( |
3345 | 'GET', '/api/v1/streaming/health', base_url_override=self.__get_streaming_base(), parse=False) | ||
3268 | if api_okay == b'OK': | 3346 | if api_okay == b'OK': |
3269 | return True | 3347 | return True |
3270 | return False | 3348 | return False |
3271 | 3349 | ||
3272 | ### | 3350 | ### |
3273 | # Internal helpers, dragons probably | 3351 | # Internal helpers, dragons probably |
3274 | ### | 3352 | ### |
@@ -3285,18 +3363,18 @@ class Mastodon: | |||
3285 | else: | 3363 | else: |
3286 | date_time_utc = date_time.astimezone(pytz.utc) | 3364 | date_time_utc = date_time.astimezone(pytz.utc) |
3287 | 3365 | ||
3288 | epoch_utc = datetime.datetime.utcfromtimestamp(0).replace(tzinfo=pytz.utc) | 3366 | epoch_utc = datetime.datetime.utcfromtimestamp( |
3367 | 0).replace(tzinfo=pytz.utc) | ||
3289 | 3368 | ||
3290 | return (date_time_utc - epoch_utc).total_seconds() | 3369 | return (date_time_utc - epoch_utc).total_seconds() |
3291 | 3370 | ||
3292 | def __get_logged_in_id(self): | 3371 | def __get_logged_in_id(self): |
3293 | """ | 3372 | """ |
3294 | Fetch the logged in users ID, with caching. ID is reset on calls to log_in. | 3373 | Fetch the logged in user's ID, with caching. ID is reset on calls to log_in. |
3295 | """ | 3374 | """ |
3296 | if self.__logged_in_id == None: | 3375 | if self.__logged_in_id == None: |
3297 | self.__logged_in_id = self.account_verify_credentials().id | 3376 | self.__logged_in_id = self.account_verify_credentials().id |
3298 | return self.__logged_in_id | 3377 | return self.__logged_in_id |
3299 | |||
3300 | 3378 | ||
3301 | @staticmethod | 3379 | @staticmethod |
3302 | def __json_allow_dict_attrs(json_object): | 3380 | def __json_allow_dict_attrs(json_object): |
@@ -3313,13 +3391,15 @@ class Mastodon: | |||
3313 | """ | 3391 | """ |
3314 | Parse dates in certain known json fields, if possible. | 3392 | Parse dates in certain known json fields, if possible. |
3315 | """ | 3393 | """ |
3316 | known_date_fields = ["created_at", "week", "day", "expires_at", "scheduled_at", "updated_at", "last_status_at", "starts_at", "ends_at", "published_at", "edited_at"] | 3394 | known_date_fields = ["created_at", "week", "day", "expires_at", "scheduled_at", |
3395 | "updated_at", "last_status_at", "starts_at", "ends_at", "published_at", "edited_at"] | ||
3317 | for k, v in json_object.items(): | 3396 | for k, v in json_object.items(): |
3318 | if k in known_date_fields: | 3397 | if k in known_date_fields: |
3319 | if v != None: | 3398 | if v != None: |
3320 | try: | 3399 | try: |
3321 | if isinstance(v, int): | 3400 | if isinstance(v, int): |
3322 | json_object[k] = datetime.datetime.fromtimestamp(v, pytz.utc) | 3401 | json_object[k] = datetime.datetime.fromtimestamp( |
3402 | v, pytz.utc) | ||
3323 | else: | 3403 | else: |
3324 | json_object[k] = dateutil.parser.parse(v) | 3404 | json_object[k] = dateutil.parser.parse(v) |
3325 | except: | 3405 | except: |
@@ -3338,7 +3418,7 @@ class Mastodon: | |||
3338 | if json_object[key].lower() == 'false': | 3418 | if json_object[key].lower() == 'false': |
3339 | json_object[key] = False | 3419 | json_object[key] = False |
3340 | return json_object | 3420 | return json_object |
3341 | 3421 | ||
3342 | @staticmethod | 3422 | @staticmethod |
3343 | def __json_strnum_to_bignum(json_object): | 3423 | def __json_strnum_to_bignum(json_object): |
3344 | """ | 3424 | """ |
@@ -3352,13 +3432,13 @@ class Mastodon: | |||
3352 | pass | 3432 | pass |
3353 | 3433 | ||
3354 | return json_object | 3434 | return json_object |
3355 | 3435 | ||
3356 | @staticmethod | 3436 | @staticmethod |
3357 | def __json_hooks(json_object): | 3437 | def __json_hooks(json_object): |
3358 | """ | 3438 | """ |
3359 | All the json hooks. Used in request parsing. | 3439 | All the json hooks. Used in request parsing. |
3360 | """ | 3440 | """ |
3361 | json_object = Mastodon.__json_strnum_to_bignum(json_object) | 3441 | json_object = Mastodon.__json_strnum_to_bignum(json_object) |
3362 | json_object = Mastodon.__json_date_parse(json_object) | 3442 | json_object = Mastodon.__json_date_parse(json_object) |
3363 | json_object = Mastodon.__json_truefalse_parse(json_object) | 3443 | json_object = Mastodon.__json_truefalse_parse(json_object) |
3364 | json_object = Mastodon.__json_allow_dict_attrs(json_object) | 3444 | json_object = Mastodon.__json_allow_dict_attrs(json_object) |
@@ -3371,7 +3451,8 @@ class Mastodon: | |||
3371 | every time instead of randomly doing different things on some systems | 3451 | every time instead of randomly doing different things on some systems |
3372 | and also it represents that time as the equivalent UTC time. | 3452 | and also it represents that time as the equivalent UTC time. |
3373 | """ | 3453 | """ |
3374 | isotime = datetime_val.astimezone(pytz.utc).strftime("%Y-%m-%dT%H:%M:%S%z") | 3454 | isotime = datetime_val.astimezone( |
3455 | pytz.utc).strftime("%Y-%m-%dT%H:%M:%S%z") | ||
3375 | if isotime[-2] != ":": | 3456 | if isotime[-2] != ":": |
3376 | isotime = isotime[:-2] + ":" + isotime[-2:] | 3457 | isotime = isotime[:-2] + ":" + isotime[-2:] |
3377 | return isotime | 3458 | return isotime |
@@ -3382,7 +3463,7 @@ class Mastodon: | |||
3382 | """ | 3463 | """ |
3383 | response = None | 3464 | response = None |
3384 | remaining_wait = 0 | 3465 | remaining_wait = 0 |
3385 | 3466 | ||
3386 | # "pace" mode ratelimiting: Assume constant rate of requests, sleep a little less long than it | 3467 | # "pace" mode ratelimiting: Assume constant rate of requests, sleep a little less long than it |
3387 | # would take to not hit the rate limit at that request rate. | 3468 | # would take to not hit the rate limit at that request rate. |
3388 | if do_ratelimiting and self.ratelimit_method == "pace": | 3469 | if do_ratelimiting and self.ratelimit_method == "pace": |
@@ -3394,7 +3475,8 @@ class Mastodon: | |||
3394 | time.sleep(to_next) | 3475 | time.sleep(to_next) |
3395 | else: | 3476 | else: |
3396 | time_waited = time.time() - self.ratelimit_lastcall | 3477 | time_waited = time.time() - self.ratelimit_lastcall |
3397 | time_wait = float(self.ratelimit_reset - time.time()) / float(self.ratelimit_remaining) | 3478 | time_wait = float(self.ratelimit_reset - |
3479 | time.time()) / float(self.ratelimit_remaining) | ||
3398 | remaining_wait = time_wait - time_waited | 3480 | remaining_wait = time_wait - time_waited |
3399 | 3481 | ||
3400 | if remaining_wait > 0: | 3482 | if remaining_wait > 0: |
@@ -3419,7 +3501,8 @@ class Mastodon: | |||
3419 | base_url = base_url_override | 3501 | base_url = base_url_override |
3420 | 3502 | ||
3421 | if self.debug_requests: | 3503 | if self.debug_requests: |
3422 | print('Mastodon: Request to endpoint "' + base_url + endpoint + '" using method "' + method + '".') | 3504 | print('Mastodon: Request to endpoint "' + base_url + |
3505 | endpoint + '" using method "' + method + '".') | ||
3423 | print('Parameters: ' + str(params)) | 3506 | print('Parameters: ' + str(params)) |
3424 | print('Headers: ' + str(headers)) | 3507 | print('Headers: ' + str(headers)) |
3425 | print('Files: ' + str(files)) | 3508 | print('Files: ' + str(files)) |
@@ -3440,61 +3523,74 @@ class Mastodon: | |||
3440 | kwargs['data'] = params | 3523 | kwargs['data'] = params |
3441 | else: | 3524 | else: |
3442 | kwargs['json'] = params | 3525 | kwargs['json'] = params |
3443 | 3526 | ||
3444 | # Block list with exactly three entries, matching on hashes of the instance API domain | 3527 | # Block list with exactly three entries, matching on hashes of the instance API domain |
3445 | # For more information, have a look at the docs | 3528 | # For more information, have a look at the docs |
3446 | if hashlib.sha256(",".join(base_url.split("//")[-1].split("/")[0].split(".")[-2:]).encode("utf-8")).hexdigest() in \ | 3529 | if hashlib.sha256(",".join(base_url.split("//")[-1].split("/")[0].split(".")[-2:]).encode("utf-8")).hexdigest() in \ |
3447 | [ | 3530 | [ |
3448 | "f3b50af8594eaa91dc440357a92691ff65dbfc9555226e9545b8e083dc10d2e1", | 3531 | "f3b50af8594eaa91dc440357a92691ff65dbfc9555226e9545b8e083dc10d2e1", |
3449 | "b96d2de9784efb5af0af56965b8616afe5469c06e7188ad0ccaee5c7cb8a56b6", | 3532 | "b96d2de9784efb5af0af56965b8616afe5469c06e7188ad0ccaee5c7cb8a56b6", |
3450 | "2dc0cbc89fad4873f665b78cc2f8b6b80fae4af9ac43c0d693edfda27275f517" | 3533 | "2dc0cbc89fad4873f665b78cc2f8b6b80fae4af9ac43c0d693edfda27275f517" |
3451 | ]: | 3534 | ]: |
3452 | raise Exception("Access denied.") | 3535 | raise Exception("Access denied.") |
3453 | 3536 | ||
3454 | response_object = self.session.request(method, base_url + endpoint, **kwargs) | 3537 | response_object = self.session.request( |
3538 | method, base_url + endpoint, **kwargs) | ||
3455 | except Exception as e: | 3539 | except Exception as e: |
3456 | raise MastodonNetworkError("Could not complete request: %s" % e) | 3540 | raise MastodonNetworkError( |
3541 | "Could not complete request: %s" % e) | ||
3457 | 3542 | ||
3458 | if response_object is None: | 3543 | if response_object is None: |
3459 | raise MastodonIllegalArgumentError("Illegal request.") | 3544 | raise MastodonIllegalArgumentError("Illegal request.") |
3460 | 3545 | ||
3461 | # Parse rate limiting headers | 3546 | # Parse rate limiting headers |
3462 | if 'X-RateLimit-Remaining' in response_object.headers and do_ratelimiting: | 3547 | if 'X-RateLimit-Remaining' in response_object.headers and do_ratelimiting: |
3463 | self.ratelimit_remaining = int(response_object.headers['X-RateLimit-Remaining']) | 3548 | self.ratelimit_remaining = int( |
3464 | self.ratelimit_limit = int(response_object.headers['X-RateLimit-Limit']) | 3549 | response_object.headers['X-RateLimit-Remaining']) |
3465 | 3550 | self.ratelimit_limit = int( | |
3551 | response_object.headers['X-RateLimit-Limit']) | ||
3552 | |||
3466 | # For gotosocial, we need an int representation, but for non-ints this would crash | 3553 | # For gotosocial, we need an int representation, but for non-ints this would crash |
3467 | try: | 3554 | try: |
3468 | ratelimit_intrep = str(int(response_object.headers['X-RateLimit-Reset'])) | 3555 | ratelimit_intrep = str( |
3556 | int(response_object.headers['X-RateLimit-Reset'])) | ||
3469 | except: | 3557 | except: |
3470 | ratelimit_intrep = None | 3558 | ratelimit_intrep = None |
3471 | 3559 | ||
3472 | try: | 3560 | try: |
3473 | if not ratelimit_intrep is None and ratelimit_intrep == response_object.headers['X-RateLimit-Reset']: | 3561 | if not ratelimit_intrep is None and ratelimit_intrep == response_object.headers['X-RateLimit-Reset']: |
3474 | self.ratelimit_reset = int(response_object.headers['X-RateLimit-Reset']) | 3562 | self.ratelimit_reset = int( |
3563 | response_object.headers['X-RateLimit-Reset']) | ||
3475 | else: | 3564 | else: |
3476 | ratelimit_reset_datetime = dateutil.parser.parse(response_object.headers['X-RateLimit-Reset']) | 3565 | ratelimit_reset_datetime = dateutil.parser.parse( |
3477 | self.ratelimit_reset = self.__datetime_to_epoch(ratelimit_reset_datetime) | 3566 | response_object.headers['X-RateLimit-Reset']) |
3567 | self.ratelimit_reset = self.__datetime_to_epoch( | ||
3568 | ratelimit_reset_datetime) | ||
3478 | 3569 | ||
3479 | # Adjust server time to local clock | 3570 | # Adjust server time to local clock |
3480 | if 'Date' in response_object.headers: | 3571 | if 'Date' in response_object.headers: |
3481 | server_time_datetime = dateutil.parser.parse(response_object.headers['Date']) | 3572 | server_time_datetime = dateutil.parser.parse( |
3482 | server_time = self.__datetime_to_epoch(server_time_datetime) | 3573 | response_object.headers['Date']) |
3574 | server_time = self.__datetime_to_epoch( | ||
3575 | server_time_datetime) | ||
3483 | server_time_diff = time.time() - server_time | 3576 | server_time_diff = time.time() - server_time |
3484 | self.ratelimit_reset += server_time_diff | 3577 | self.ratelimit_reset += server_time_diff |
3485 | self.ratelimit_lastcall = time.time() | 3578 | self.ratelimit_lastcall = time.time() |
3486 | except Exception as e: | 3579 | except Exception as e: |
3487 | raise MastodonRatelimitError("Rate limit time calculations failed: %s" % e) | 3580 | raise MastodonRatelimitError( |
3581 | "Rate limit time calculations failed: %s" % e) | ||
3488 | 3582 | ||
3489 | # Handle response | 3583 | # Handle response |
3490 | if self.debug_requests: | 3584 | if self.debug_requests: |
3491 | print('Mastodon: Response received with code ' + str(response_object.status_code) + '.') | 3585 | print('Mastodon: Response received with code ' + |
3586 | str(response_object.status_code) + '.') | ||
3492 | print('response headers: ' + str(response_object.headers)) | 3587 | print('response headers: ' + str(response_object.headers)) |
3493 | print('Response text content: ' + str(response_object.text)) | 3588 | print('Response text content: ' + str(response_object.text)) |
3494 | 3589 | ||
3495 | if not response_object.ok: | 3590 | if not response_object.ok: |
3496 | try: | 3591 | try: |
3497 | response = response_object.json(object_hook=self.__json_hooks) | 3592 | response = response_object.json( |
3593 | object_hook=self.__json_hooks) | ||
3498 | if isinstance(response, dict) and 'error' in response: | 3594 | if isinstance(response, dict) and 'error' in response: |
3499 | error_msg = response['error'] | 3595 | error_msg = response['error'] |
3500 | elif isinstance(response, str): | 3596 | elif isinstance(response, str): |
@@ -3535,28 +3631,29 @@ class Mastodon: | |||
3535 | elif response_object.status_code == 504: | 3631 | elif response_object.status_code == 504: |
3536 | ex_type = MastodonGatewayTimeoutError | 3632 | ex_type = MastodonGatewayTimeoutError |
3537 | elif response_object.status_code >= 500 and \ | 3633 | elif response_object.status_code >= 500 and \ |
3538 | response_object.status_code <= 511: | 3634 | response_object.status_code <= 511: |
3539 | ex_type = MastodonServerError | 3635 | ex_type = MastodonServerError |
3540 | else: | 3636 | else: |
3541 | ex_type = MastodonAPIError | 3637 | ex_type = MastodonAPIError |
3542 | 3638 | ||
3543 | raise ex_type( | 3639 | raise ex_type( |
3544 | 'Mastodon API returned error', | 3640 | 'Mastodon API returned error', |
3545 | response_object.status_code, | 3641 | response_object.status_code, |
3546 | response_object.reason, | 3642 | response_object.reason, |
3547 | error_msg) | 3643 | error_msg) |
3548 | 3644 | ||
3549 | if parse == True: | 3645 | if parse == True: |
3550 | try: | 3646 | try: |
3551 | response = response_object.json(object_hook=self.__json_hooks) | 3647 | response = response_object.json( |
3648 | object_hook=self.__json_hooks) | ||
3552 | except: | 3649 | except: |
3553 | raise MastodonAPIError( | 3650 | raise MastodonAPIError( |
3554 | "Could not parse response as JSON, response code was %s, " | 3651 | "Could not parse response as JSON, response code was %s, " |
3555 | "bad json content was '%s'" % (response_object.status_code, | 3652 | "bad json content was '%s'" % (response_object.status_code, |
3556 | response_object.content)) | 3653 | response_object.content)) |
3557 | else: | 3654 | else: |
3558 | response = response_object.content | 3655 | response = response_object.content |
3559 | 3656 | ||
3560 | # Parse link headers | 3657 | # Parse link headers |
3561 | if isinstance(response, list) and \ | 3658 | if isinstance(response, list) and \ |
3562 | 'Link' in response_object.headers and \ | 3659 | 'Link' in response_object.headers and \ |
@@ -3571,7 +3668,8 @@ class Mastodon: | |||
3571 | if url['rel'] == 'next': | 3668 | if url['rel'] == 'next': |
3572 | # Be paranoid and extract max_id specifically | 3669 | # Be paranoid and extract max_id specifically |
3573 | next_url = url['url'] | 3670 | next_url = url['url'] |
3574 | matchgroups = re.search(r"[?&]max_id=([^&]+)", next_url) | 3671 | matchgroups = re.search( |
3672 | r"[?&]max_id=([^&]+)", next_url) | ||
3575 | 3673 | ||
3576 | if matchgroups: | 3674 | if matchgroups: |
3577 | next_params = copy.deepcopy(params) | 3675 | next_params = copy.deepcopy(params) |
@@ -3596,9 +3694,10 @@ class Mastodon: | |||
3596 | if url['rel'] == 'prev': | 3694 | if url['rel'] == 'prev': |
3597 | # Be paranoid and extract since_id or min_id specifically | 3695 | # Be paranoid and extract since_id or min_id specifically |
3598 | prev_url = url['url'] | 3696 | prev_url = url['url'] |
3599 | 3697 | ||
3600 | # Old and busted (pre-2.6.0): since_id pagination | 3698 | # Old and busted (pre-2.6.0): since_id pagination |
3601 | matchgroups = re.search(r"[?&]since_id=([^&]+)", prev_url) | 3699 | matchgroups = re.search( |
3700 | r"[?&]since_id=([^&]+)", prev_url) | ||
3602 | if matchgroups: | 3701 | if matchgroups: |
3603 | prev_params = copy.deepcopy(params) | 3702 | prev_params = copy.deepcopy(params) |
3604 | prev_params['_pagination_method'] = method | 3703 | prev_params['_pagination_method'] = method |
@@ -3618,7 +3717,8 @@ class Mastodon: | |||
3618 | response[0]._pagination_prev = prev_params | 3717 | response[0]._pagination_prev = prev_params |
3619 | 3718 | ||
3620 | # New and fantastico (post-2.6.0): min_id pagination | 3719 | # New and fantastico (post-2.6.0): min_id pagination |
3621 | matchgroups = re.search(r"[?&]min_id=([^&]+)", prev_url) | 3720 | matchgroups = re.search( |
3721 | r"[?&]min_id=([^&]+)", prev_url) | ||
3622 | if matchgroups: | 3722 | if matchgroups: |
3623 | prev_params = copy.deepcopy(params) | 3723 | prev_params = copy.deepcopy(params) |
3624 | prev_params['_pagination_method'] = method | 3724 | prev_params['_pagination_method'] = method |
@@ -3642,7 +3742,7 @@ class Mastodon: | |||
3642 | def __get_streaming_base(self): | 3742 | def __get_streaming_base(self): |
3643 | """ | 3743 | """ |
3644 | Internal streaming API helper. | 3744 | Internal streaming API helper. |
3645 | 3745 | ||
3646 | Returns the correct URL for the streaming API. | 3746 | Returns the correct URL for the streaming API. |
3647 | """ | 3747 | """ |
3648 | instance = self.instance() | 3748 | instance = self.instance() |
@@ -3656,8 +3756,8 @@ class Mastodon: | |||
3656 | url = "http://" + parse.netloc | 3756 | url = "http://" + parse.netloc |
3657 | else: | 3757 | else: |
3658 | raise MastodonAPIError( | 3758 | raise MastodonAPIError( |
3659 | "Could not parse streaming api location returned from server: {}.".format( | 3759 | "Could not parse streaming api location returned from server: {}.".format( |
3660 | instance["urls"]["streaming_api"])) | 3760 | instance["urls"]["streaming_api"])) |
3661 | else: | 3761 | else: |
3662 | url = self.api_base_url | 3762 | url = self.api_base_url |
3663 | return url | 3763 | return url |
@@ -3676,20 +3776,22 @@ class Mastodon: | |||
3676 | # The streaming server can't handle two slashes in a path, so remove trailing slashes | 3776 | # The streaming server can't handle two slashes in a path, so remove trailing slashes |
3677 | if url[-1] == '/': | 3777 | if url[-1] == '/': |
3678 | url = url[:-1] | 3778 | url = url[:-1] |
3679 | 3779 | ||
3680 | # Connect function (called and then potentially passed to async handler) | 3780 | # Connect function (called and then potentially passed to async handler) |
3681 | def connect_func(): | 3781 | def connect_func(): |
3682 | headers = {"Authorization": "Bearer " + self.access_token} if self.access_token else {} | 3782 | headers = {"Authorization": "Bearer " + |
3783 | self.access_token} if self.access_token else {} | ||
3683 | if self.user_agent: | 3784 | if self.user_agent: |
3684 | headers['User-Agent'] = self.user_agent | 3785 | headers['User-Agent'] = self.user_agent |
3685 | connection = self.session.get(url + endpoint, headers = headers, data = params, stream = True, | 3786 | connection = self.session.get(url + endpoint, headers=headers, data=params, stream=True, |
3686 | timeout=(self.request_timeout, timeout)) | 3787 | timeout=(self.request_timeout, timeout)) |
3687 | 3788 | ||
3688 | if connection.status_code != 200: | 3789 | if connection.status_code != 200: |
3689 | raise MastodonNetworkError("Could not connect to streaming server: %s" % connection.reason) | 3790 | raise MastodonNetworkError( |
3791 | "Could not connect to streaming server: %s" % connection.reason) | ||
3690 | return connection | 3792 | return connection |
3691 | connection = None | 3793 | connection = None |
3692 | 3794 | ||
3693 | # Async stream handler | 3795 | # Async stream handler |
3694 | class __stream_handle(): | 3796 | class __stream_handle(): |
3695 | def __init__(self, connection, connect_func, reconnect_async, reconnect_async_wait_sec): | 3797 | def __init__(self, connection, connect_func, reconnect_async, reconnect_async_wait_sec): |
@@ -3700,7 +3802,7 @@ class Mastodon: | |||
3700 | self.reconnect_async = reconnect_async | 3802 | self.reconnect_async = reconnect_async |
3701 | self.reconnect_async_wait_sec = reconnect_async_wait_sec | 3803 | self.reconnect_async_wait_sec = reconnect_async_wait_sec |
3702 | self.reconnecting = False | 3804 | self.reconnecting = False |
3703 | 3805 | ||
3704 | def close(self): | 3806 | def close(self): |
3705 | self.closed = True | 3807 | self.closed = True |
3706 | if not self.connection is None: | 3808 | if not self.connection is None: |
@@ -3717,15 +3819,16 @@ class Mastodon: | |||
3717 | 3819 | ||
3718 | def _sleep_attentive(self): | 3820 | def _sleep_attentive(self): |
3719 | if self._thread != threading.current_thread(): | 3821 | if self._thread != threading.current_thread(): |
3720 | raise RuntimeError ("Illegal call from outside the stream_handle thread") | 3822 | raise RuntimeError( |
3823 | "Illegal call from outside the stream_handle thread") | ||
3721 | time_remaining = self.reconnect_async_wait_sec | 3824 | time_remaining = self.reconnect_async_wait_sec |
3722 | while time_remaining>0 and not self.closed: | 3825 | while time_remaining > 0 and not self.closed: |
3723 | time.sleep(0.5) | 3826 | time.sleep(0.5) |
3724 | time_remaining -= 0.5 | 3827 | time_remaining -= 0.5 |
3725 | 3828 | ||
3726 | def _threadproc(self): | 3829 | def _threadproc(self): |
3727 | self._thread = threading.current_thread() | 3830 | self._thread = threading.current_thread() |
3728 | 3831 | ||
3729 | # Run until closed or until error if not autoreconnecting | 3832 | # Run until closed or until error if not autoreconnecting |
3730 | while self.running: | 3833 | while self.running: |
3731 | if not self.connection is None: | 3834 | if not self.connection is None: |
@@ -3771,14 +3874,15 @@ class Mastodon: | |||
3771 | return 0 | 3874 | return 0 |
3772 | 3875 | ||
3773 | if run_async: | 3876 | if run_async: |
3774 | handle = __stream_handle(connection, connect_func, reconnect_async, reconnect_async_wait_sec) | 3877 | handle = __stream_handle( |
3878 | connection, connect_func, reconnect_async, reconnect_async_wait_sec) | ||
3775 | t = threading.Thread(args=(), target=handle._threadproc) | 3879 | t = threading.Thread(args=(), target=handle._threadproc) |
3776 | t.daemon = True | 3880 | t.daemon = True |
3777 | t.start() | 3881 | t.start() |
3778 | return handle | 3882 | return handle |
3779 | else: | 3883 | else: |
3780 | # Blocking, never returns (can only leave via exception) | 3884 | # Blocking, never returns (can only leave via exception) |
3781 | connection = connect_func() | 3885 | connection = connect_func() |
3782 | with closing(connection) as r: | 3886 | with closing(connection) as r: |
3783 | listener.handle_stream(r) | 3887 | listener.handle_stream(r) |
3784 | 3888 | ||
@@ -3795,14 +3899,14 @@ class Mastodon: | |||
3795 | 3899 | ||
3796 | if 'self' in params: | 3900 | if 'self' in params: |
3797 | del params['self'] | 3901 | del params['self'] |
3798 | 3902 | ||
3799 | param_keys = list(params.keys()) | 3903 | param_keys = list(params.keys()) |
3800 | for key in param_keys: | 3904 | for key in param_keys: |
3801 | if isinstance(params[key], bool) and params[key] == False: | 3905 | if isinstance(params[key], bool) and params[key] == False: |
3802 | params[key] = '0' | 3906 | params[key] = '0' |
3803 | if isinstance(params[key], bool) and params[key] == True: | 3907 | if isinstance(params[key], bool) and params[key] == True: |
3804 | params[key] = '1' | 3908 | params[key] = '1' |
3805 | 3909 | ||
3806 | for key in param_keys: | 3910 | for key in param_keys: |
3807 | if params[key] is None or key in exclude: | 3911 | if params[key] is None or key in exclude: |
3808 | del params[key] | 3912 | del params[key] |
@@ -3812,23 +3916,23 @@ class Mastodon: | |||
3812 | if isinstance(params[key], list): | 3916 | if isinstance(params[key], list): |
3813 | params[key + "[]"] = params[key] | 3917 | params[key + "[]"] = params[key] |
3814 | del params[key] | 3918 | del params[key] |
3815 | 3919 | ||
3816 | return params | 3920 | return params |
3817 | 3921 | ||
3818 | def __unpack_id(self, id, dateconv=False): | 3922 | def __unpack_id(self, id, dateconv=False): |
3819 | """ | 3923 | """ |
3820 | Internal object-to-id converter | 3924 | Internal object-to-id converter |
3821 | 3925 | ||
3822 | Checks if id is a dict that contains id and | 3926 | Checks if id is a dict that contains id and |
3823 | returns the id inside, otherwise just returns | 3927 | returns the id inside, otherwise just returns |
3824 | the id straight. | 3928 | the id straight. |
3825 | """ | 3929 | """ |
3826 | if isinstance(id, dict) and "id" in id: | 3930 | if isinstance(id, dict) and "id" in id: |
3827 | id = id["id"] | 3931 | id = id["id"] |
3828 | if dateconv and isinstance(id, datetime): | 3932 | if dateconv and isinstance(id, datetime): |
3829 | id = (int(id) << 16) * 1000 | 3933 | id = (int(id) << 16) * 1000 |
3830 | return id | 3934 | return id |
3831 | 3935 | ||
3832 | def __decode_webpush_b64(self, data): | 3936 | def __decode_webpush_b64(self, data): |
3833 | """ | 3937 | """ |
3834 | Re-pads and decodes urlsafe base64. | 3938 | Re-pads and decodes urlsafe base64. |
@@ -3837,7 +3941,7 @@ class Mastodon: | |||
3837 | if missing_padding != 0: | 3941 | if missing_padding != 0: |
3838 | data += '=' * (4 - missing_padding) | 3942 | data += '=' * (4 - missing_padding) |
3839 | return base64.urlsafe_b64decode(data) | 3943 | return base64.urlsafe_b64decode(data) |
3840 | 3944 | ||
3841 | def __get_token_expired(self): | 3945 | def __get_token_expired(self): |
3842 | """Internal helper for oauth code""" | 3946 | """Internal helper for oauth code""" |
3843 | return self._token_expired < datetime.datetime.now() | 3947 | return self._token_expired < datetime.datetime.now() |
@@ -3865,17 +3969,20 @@ class Mastodon: | |||
3865 | mime_type = mimetypes.guess_type(media_file)[0] | 3969 | mime_type = mimetypes.guess_type(media_file)[0] |
3866 | return mime_type | 3970 | return mime_type |
3867 | 3971 | ||
3868 | def __load_media_file(self, media_file, mime_type = None, file_name = None): | 3972 | def __load_media_file(self, media_file, mime_type=None, file_name=None): |
3869 | if isinstance(media_file, str) and os.path.isfile(media_file): | 3973 | if isinstance(media_file, str) and os.path.isfile(media_file): |
3870 | mime_type = self.__guess_type(media_file) | 3974 | mime_type = self.__guess_type(media_file) |
3871 | media_file = open(media_file, 'rb') | 3975 | media_file = open(media_file, 'rb') |
3872 | elif isinstance(media_file, str) and os.path.isfile(media_file): | 3976 | elif isinstance(media_file, str) and os.path.isfile(media_file): |
3873 | media_file = open(media_file, 'rb') | 3977 | media_file = open(media_file, 'rb') |
3874 | if mime_type is None: | 3978 | if mime_type is None: |
3875 | raise MastodonIllegalArgumentError('Could not determine mime type or data passed directly without mime type.') | 3979 | raise MastodonIllegalArgumentError( |
3980 | 'Could not determine mime type or data passed directly without mime type.') | ||
3876 | if file_name is None: | 3981 | if file_name is None: |
3877 | random_suffix = uuid.uuid4().hex | 3982 | random_suffix = uuid.uuid4().hex |
3878 | file_name = "mastodonpyupload_" + str(time.time()) + "_" + str(random_suffix) + mimetypes.guess_extension(mime_type) | 3983 | file_name = "mastodonpyupload_" + \ |
3984 | str(time.time()) + "_" + str(random_suffix) + \ | ||
3985 | mimetypes.guess_extension(mime_type) | ||
3879 | return (file_name, media_file, mime_type) | 3986 | return (file_name, media_file, mime_type) |
3880 | 3987 | ||
3881 | @staticmethod | 3988 | @staticmethod |
@@ -3895,10 +4002,12 @@ class Mastodon: | |||
3895 | class MastodonError(Exception): | 4002 | class MastodonError(Exception): |
3896 | """Base class for Mastodon.py exceptions""" | 4003 | """Base class for Mastodon.py exceptions""" |
3897 | 4004 | ||
4005 | |||
3898 | class MastodonVersionError(MastodonError): | 4006 | class MastodonVersionError(MastodonError): |
3899 | """Raised when a function is called that the version of Mastodon for which | 4007 | """Raised when a function is called that the version of Mastodon for which |
3900 | Mastodon.py was instantiated does not support""" | 4008 | Mastodon.py was instantiated does not support""" |
3901 | 4009 | ||
4010 | |||
3902 | class MastodonIllegalArgumentError(ValueError, MastodonError): | 4011 | class MastodonIllegalArgumentError(ValueError, MastodonError): |
3903 | """Raised when an incorrect parameter is passed to a function""" | 4012 | """Raised when an incorrect parameter is passed to a function""" |
3904 | pass | 4013 | pass |
@@ -3917,6 +4026,7 @@ class MastodonNetworkError(MastodonIOError): | |||
3917 | """Raised when network communication with the server fails""" | 4026 | """Raised when network communication with the server fails""" |
3918 | pass | 4027 | pass |
3919 | 4028 | ||
4029 | |||
3920 | class MastodonReadTimeout(MastodonNetworkError): | 4030 | class MastodonReadTimeout(MastodonNetworkError): |
3921 | """Raised when a stream times out""" | 4031 | """Raised when a stream times out""" |
3922 | pass | 4032 | pass |
@@ -3926,32 +4036,39 @@ class MastodonAPIError(MastodonError): | |||
3926 | """Raised when the mastodon API generates a response that cannot be handled""" | 4036 | """Raised when the mastodon API generates a response that cannot be handled""" |
3927 | pass | 4037 | pass |
3928 | 4038 | ||
4039 | |||
3929 | class MastodonServerError(MastodonAPIError): | 4040 | class MastodonServerError(MastodonAPIError): |
3930 | """Raised if the Server is malconfigured and returns a 5xx error code""" | 4041 | """Raised if the Server is malconfigured and returns a 5xx error code""" |
3931 | pass | 4042 | pass |
3932 | 4043 | ||
4044 | |||
3933 | class MastodonInternalServerError(MastodonServerError): | 4045 | class MastodonInternalServerError(MastodonServerError): |
3934 | """Raised if the Server returns a 500 error""" | 4046 | """Raised if the Server returns a 500 error""" |
3935 | pass | 4047 | pass |
3936 | 4048 | ||
4049 | |||
3937 | class MastodonBadGatewayError(MastodonServerError): | 4050 | class MastodonBadGatewayError(MastodonServerError): |
3938 | """Raised if the Server returns a 502 error""" | 4051 | """Raised if the Server returns a 502 error""" |
3939 | pass | 4052 | pass |
3940 | 4053 | ||
4054 | |||
3941 | class MastodonServiceUnavailableError(MastodonServerError): | 4055 | class MastodonServiceUnavailableError(MastodonServerError): |
3942 | """Raised if the Server returns a 503 error""" | 4056 | """Raised if the Server returns a 503 error""" |
3943 | pass | 4057 | pass |
3944 | 4058 | ||
4059 | |||
3945 | class MastodonGatewayTimeoutError(MastodonServerError): | 4060 | class MastodonGatewayTimeoutError(MastodonServerError): |
3946 | """Raised if the Server returns a 504 error""" | 4061 | """Raised if the Server returns a 504 error""" |
3947 | pass | 4062 | pass |
3948 | 4063 | ||
4064 | |||
3949 | class MastodonNotFoundError(MastodonAPIError): | 4065 | class MastodonNotFoundError(MastodonAPIError): |
3950 | """Raised when the mastodon API returns a 404 Not Found error""" | 4066 | """Raised when the Mastodon API returns a 404 Not Found error""" |
3951 | pass | 4067 | pass |
3952 | 4068 | ||
4069 | |||
3953 | class MastodonUnauthorizedError(MastodonAPIError): | 4070 | class MastodonUnauthorizedError(MastodonAPIError): |
3954 | """Raised when the mastodon API returns a 401 Unauthorized error | 4071 | """Raised when the Mastodon API returns a 401 Unauthorized error |
3955 | 4072 | ||
3956 | This happens when an OAuth token is invalid or has been revoked, | 4073 | This happens when an OAuth token is invalid or has been revoked, |
3957 | or when trying to access an endpoint that can't be used without | 4074 | or when trying to access an endpoint that can't be used without |
@@ -3963,7 +4080,7 @@ class MastodonRatelimitError(MastodonError): | |||
3963 | """Raised when rate limiting is set to manual mode and the rate limit is exceeded""" | 4080 | """Raised when rate limiting is set to manual mode and the rate limit is exceeded""" |
3964 | pass | 4081 | pass |
3965 | 4082 | ||
4083 | |||
3966 | class MastodonMalformedEventError(MastodonError): | 4084 | class MastodonMalformedEventError(MastodonError): |
3967 | """Raised when the server-sent event stream is malformed""" | 4085 | """Raised when the server-sent event stream is malformed""" |
3968 | pass | 4086 | pass |
3969 | |||
diff --git a/mastodon/streaming.py b/mastodon/streaming.py index 65acba8..2080908 100644 --- a/mastodon/streaming.py +++ b/mastodon/streaming.py | |||
@@ -1,6 +1,6 @@ | |||
1 | """ | 1 | """ |
2 | Handlers for the Streaming API: | 2 | Handlers for the Streaming API: |
3 | https://github.com/tootsuite/mastodon/blob/master/docs/Using-the-API/Streaming-API.md | 3 | https://github.com/mastodon/documentation/blob/master/content/en/methods/timelines/streaming.md |
4 | """ | 4 | """ |
5 | 5 | ||
6 | import json | 6 | import json |
@@ -14,6 +14,7 @@ from mastodon import Mastodon | |||
14 | from mastodon.Mastodon import MastodonMalformedEventError, MastodonNetworkError, MastodonReadTimeout | 14 | from mastodon.Mastodon import MastodonMalformedEventError, MastodonNetworkError, MastodonReadTimeout |
15 | from requests.exceptions import ChunkedEncodingError, ReadTimeout | 15 | from requests.exceptions import ChunkedEncodingError, ReadTimeout |
16 | 16 | ||
17 | |||
17 | class StreamListener(object): | 18 | class StreamListener(object): |
18 | """Callbacks for the streaming API. Create a subclass, override the on_xxx | 19 | """Callbacks for the streaming API. Create a subclass, override the on_xxx |
19 | methods for the kinds of events you're interested in, then pass an instance | 20 | methods for the kinds of events you're interested in, then pass an instance |
@@ -39,7 +40,7 @@ class StreamListener(object): | |||
39 | """There was a connection error, read timeout or other error fatal to | 40 | """There was a connection error, read timeout or other error fatal to |
40 | the streaming connection. The exception object about to be raised | 41 | the streaming connection. The exception object about to be raised |
41 | is passed to this function for reference. | 42 | is passed to this function for reference. |
42 | 43 | ||
43 | Note that the exception will be raised properly once you return from this | 44 | Note that the exception will be raised properly once you return from this |
44 | function, so if you are using this handler to reconnect, either never | 45 | function, so if you are using this handler to reconnect, either never |
45 | return or start a thread and then catch and ignore the exception. | 46 | return or start a thread and then catch and ignore the exception. |
@@ -55,7 +56,7 @@ class StreamListener(object): | |||
55 | contains the resulting conversation dict.""" | 56 | contains the resulting conversation dict.""" |
56 | pass | 57 | pass |
57 | 58 | ||
58 | def on_unknown_event(self, name, unknown_event = None): | 59 | def on_unknown_event(self, name, unknown_event=None): |
59 | """An unknown mastodon API event has been received. The name contains the event-name and unknown_event | 60 | """An unknown mastodon API event has been received. The name contains the event-name and unknown_event |
60 | contains the content of the unknown event. | 61 | contains the content of the unknown event. |
61 | 62 | ||
@@ -65,13 +66,12 @@ class StreamListener(object): | |||
65 | self.on_abort(exception) | 66 | self.on_abort(exception) |
66 | raise exception | 67 | raise exception |
67 | 68 | ||
68 | |||
69 | def handle_heartbeat(self): | 69 | def handle_heartbeat(self): |
70 | """The server has sent us a keep-alive message. This callback may be | 70 | """The server has sent us a keep-alive message. This callback may be |
71 | useful to carry out periodic housekeeping tasks, or just to confirm | 71 | useful to carry out periodic housekeeping tasks, or just to confirm |
72 | that the connection is still open.""" | 72 | that the connection is still open.""" |
73 | pass | 73 | pass |
74 | 74 | ||
75 | def handle_stream(self, response): | 75 | def handle_stream(self, response): |
76 | """ | 76 | """ |
77 | Handles a stream of events from the Mastodon server. When each event | 77 | Handles a stream of events from the Mastodon server. When each event |
@@ -87,7 +87,7 @@ class StreamListener(object): | |||
87 | event = {} | 87 | event = {} |
88 | line_buffer = bytearray() | 88 | line_buffer = bytearray() |
89 | try: | 89 | try: |
90 | for chunk in response.iter_content(chunk_size = 1): | 90 | for chunk in response.iter_content(chunk_size=1): |
91 | if chunk: | 91 | if chunk: |
92 | for chunk_part in chunk: | 92 | for chunk_part in chunk: |
93 | chunk_part = bytearray([chunk_part]) | 93 | chunk_part = bytearray([chunk_part]) |
@@ -95,7 +95,8 @@ class StreamListener(object): | |||
95 | try: | 95 | try: |
96 | line = line_buffer.decode('utf-8') | 96 | line = line_buffer.decode('utf-8') |
97 | except UnicodeDecodeError as err: | 97 | except UnicodeDecodeError as err: |
98 | exception = MastodonMalformedEventError("Malformed UTF-8") | 98 | exception = MastodonMalformedEventError( |
99 | "Malformed UTF-8") | ||
99 | self.on_abort(exception) | 100 | self.on_abort(exception) |
100 | six.raise_from( | 101 | six.raise_from( |
101 | exception, | 102 | exception, |
@@ -117,7 +118,8 @@ class StreamListener(object): | |||
117 | err | 118 | err |
118 | ) | 119 | ) |
119 | except MastodonReadTimeout as err: | 120 | except MastodonReadTimeout as err: |
120 | exception = MastodonReadTimeout("Timed out while reading from server."), | 121 | exception = MastodonReadTimeout( |
122 | "Timed out while reading from server."), | ||
121 | self.on_abort(exception) | 123 | self.on_abort(exception) |
122 | six.raise_from( | 124 | six.raise_from( |
123 | exception, | 125 | exception, |
@@ -141,7 +143,7 @@ class StreamListener(object): | |||
141 | else: | 143 | else: |
142 | event[key] = value | 144 | event[key] = value |
143 | return event | 145 | return event |
144 | 146 | ||
145 | def _dispatch(self, event): | 147 | def _dispatch(self, event): |
146 | try: | 148 | try: |
147 | name = event['event'] | 149 | name = event['event'] |
@@ -150,9 +152,11 @@ class StreamListener(object): | |||
150 | for_stream = json.loads(event['stream']) | 152 | for_stream = json.loads(event['stream']) |
151 | except: | 153 | except: |
152 | for_stream = None | 154 | for_stream = None |
153 | payload = json.loads(data, object_hook = Mastodon._Mastodon__json_hooks) | 155 | payload = json.loads( |
156 | data, object_hook=Mastodon._Mastodon__json_hooks) | ||
154 | except KeyError as err: | 157 | except KeyError as err: |
155 | exception = MastodonMalformedEventError('Missing field', err.args[0], event) | 158 | exception = MastodonMalformedEventError( |
159 | 'Missing field', err.args[0], event) | ||
156 | self.on_abort(exception) | 160 | self.on_abort(exception) |
157 | six.raise_from( | 161 | six.raise_from( |
158 | exception, | 162 | exception, |
@@ -170,7 +174,7 @@ class StreamListener(object): | |||
170 | # New mastodon API also supports event names with dots, | 174 | # New mastodon API also supports event names with dots, |
171 | # specifically, status_update. | 175 | # specifically, status_update. |
172 | handler_name = 'on_' + name.replace('.', '_') | 176 | handler_name = 'on_' + name.replace('.', '_') |
173 | 177 | ||
174 | # A generic way to handle unknown events to make legacy code more stable for future changes | 178 | # A generic way to handle unknown events to make legacy code more stable for future changes |
175 | handler = getattr(self, handler_name, self.on_unknown_event) | 179 | handler = getattr(self, handler_name, self.on_unknown_event) |
176 | try: | 180 | try: |
@@ -191,6 +195,7 @@ class StreamListener(object): | |||
191 | else: | 195 | else: |
192 | handler(name, payload) | 196 | handler(name, payload) |
193 | 197 | ||
198 | |||
194 | class CallbackStreamListener(StreamListener): | 199 | class CallbackStreamListener(StreamListener): |
195 | """ | 200 | """ |
196 | Simple callback stream handler class. | 201 | Simple callback stream handler class. |
@@ -198,7 +203,8 @@ class CallbackStreamListener(StreamListener): | |||
198 | Define an unknown_event_handler for new Mastodon API events. If not, the | 203 | Define an unknown_event_handler for new Mastodon API events. If not, the |
199 | listener will raise an error on new, not handled, events from the API. | 204 | listener will raise an error on new, not handled, events from the API. |
200 | """ | 205 | """ |
201 | def __init__(self, update_handler = None, local_update_handler = None, delete_handler = None, notification_handler = None, conversation_handler = None, unknown_event_handler = None, status_update_handler = None): | 206 | |
207 | def __init__(self, update_handler=None, local_update_handler=None, delete_handler=None, notification_handler=None, conversation_handler=None, unknown_event_handler=None, status_update_handler=None): | ||
202 | super(CallbackStreamListener, self).__init__() | 208 | super(CallbackStreamListener, self).__init__() |
203 | self.update_handler = update_handler | 209 | self.update_handler = update_handler |
204 | self.local_update_handler = local_update_handler | 210 | self.local_update_handler = local_update_handler |
@@ -211,29 +217,29 @@ class CallbackStreamListener(StreamListener): | |||
211 | def on_update(self, status): | 217 | def on_update(self, status): |
212 | if self.update_handler != None: | 218 | if self.update_handler != None: |
213 | self.update_handler(status) | 219 | self.update_handler(status) |
214 | 220 | ||
215 | try: | 221 | try: |
216 | if self.local_update_handler != None and not "@" in status["account"]["acct"]: | 222 | if self.local_update_handler != None and not "@" in status["account"]["acct"]: |
217 | self.local_update_handler(status) | 223 | self.local_update_handler(status) |
218 | except Exception as err: | 224 | except Exception as err: |
219 | six.raise_from( | 225 | six.raise_from( |
220 | MastodonMalformedEventError('received bad update', status), | 226 | MastodonMalformedEventError('received bad update', status), |
221 | err | 227 | err |
222 | ) | 228 | ) |
223 | 229 | ||
224 | def on_delete(self, deleted_id): | 230 | def on_delete(self, deleted_id): |
225 | if self.delete_handler != None: | 231 | if self.delete_handler != None: |
226 | self.delete_handler(deleted_id) | 232 | self.delete_handler(deleted_id) |
227 | 233 | ||
228 | def on_notification(self, notification): | 234 | def on_notification(self, notification): |
229 | if self.notification_handler != None: | 235 | if self.notification_handler != None: |
230 | self.notification_handler(notification) | 236 | self.notification_handler(notification) |
231 | 237 | ||
232 | def on_conversation(self, conversation): | 238 | def on_conversation(self, conversation): |
233 | if self.conversation_handler != None: | 239 | if self.conversation_handler != None: |
234 | self.conversation_handler(conversation) | 240 | self.conversation_handler(conversation) |
235 | 241 | ||
236 | def on_unknown_event(self, name, unknown_event = None): | 242 | def on_unknown_event(self, name, unknown_event=None): |
237 | if self.unknown_event_handler != None: | 243 | if self.unknown_event_handler != None: |
238 | self.unknown_event_handler(name, unknown_event) | 244 | self.unknown_event_handler(name, unknown_event) |
239 | else: | 245 | else: |
@@ -243,4 +249,4 @@ class CallbackStreamListener(StreamListener): | |||
243 | 249 | ||
244 | def on_status_update(self, status): | 250 | def on_status_update(self, status): |
245 | if self.status_update_handler != None: | 251 | if self.status_update_handler != None: |
246 | self.status_update_handler(status) \ No newline at end of file | 252 | self.status_update_handler(status) |