diff options
-rw-r--r-- | docs/index.rst | 303 | ||||
-rw-r--r-- | mastodon/Mastodon.py | 1483 | ||||
-rw-r--r-- | mastodon/streaming.py | 50 |
3 files changed, 977 insertions, 859 deletions
diff --git a/docs/index.rst b/docs/index.rst index e555912..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/tootsuite/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 e84df6d..95a33b8 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,22 +510,22 @@ 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 | ||
489 | def auth_request_url(self, client_id=None, redirect_uris="urn:ietf:wg:oauth:2.0:oob", | 514 | def auth_request_url(self, client_id=None, redirect_uris="urn:ietf:wg:oauth:2.0:oob", |
490 | scopes=__DEFAULT_SCOPES, force_login=False): | 515 | scopes=__DEFAULT_SCOPES, force_login=False): |
491 | """ | 516 | """ |
492 | Returns the url that a client needs to request an oauth grant from the server. | 517 | Returns the URL that a client needs to request an OAuth grant from the server. |
493 | 518 | ||
494 | To log in with oauth, send your user to this URL. The user will then log in and | 519 | To log in with OAuth, send your user to this URL. The user will then log in and |
495 | get a code which you can pass to log_in. | 520 | get a code which you can pass to log_in. |
496 | 521 | ||
497 | scopes are as in `log_in()`_, redirect_uris is where the user should be redirected to | 522 | scopes are as in `log_in()`_, redirect_uris is where the user should be redirected to |
498 | after authentication. Note that redirect_uris must be one of the URLs given during | 523 | after authentication. Note that redirect_uris must be one of the URLs given during |
499 | app registration. When using urn:ietf:wg:oauth:2.0:oob, the code is simply displayed, | 524 | app registration. When using urn:ietf:wg:oauth:2.0:oob, the code is simply displayed, |
500 | otherwise it is added to the given URL as the "code" request parameter. | 525 | otherwise it is added to the given URL as the "code" request parameter. |
501 | 526 | ||
502 | Pass force_login if you want the user to always log in even when already logged | 527 | Pass force_login if you want the user to always log in even when already logged |
503 | into web mastodon (i.e. when registering multiple different accounts in an app). | 528 | into web Mastodon (i.e. when registering multiple different accounts in an app). |
504 | """ | 529 | """ |
505 | if client_id is None: | 530 | if client_id is None: |
506 | client_id = self.client_id | 531 | client_id = self.client_id |
@@ -523,18 +548,18 @@ class Mastodon: | |||
523 | scopes=__DEFAULT_SCOPES, to_file=None): | 548 | scopes=__DEFAULT_SCOPES, to_file=None): |
524 | """ | 549 | """ |
525 | Get the access token for a user. | 550 | Get the access token for a user. |
526 | 551 | ||
527 | The username is the e-mail used to log in into mastodon. | 552 | The username is the email address used to log in into Mastodon. |
528 | 553 | ||
529 | Can persist access token to file `to_file`, to be used in the constructor. | 554 | Can persist access token to file `to_file`, to be used in the constructor. |
530 | 555 | ||
531 | Handles password and OAuth-based authorization. | 556 | Handles password and OAuth-based authorization. |
532 | 557 | ||
533 | Will throw a `MastodonIllegalArgumentError` if the OAuth or the | 558 | Will throw a `MastodonIllegalArgumentError` if the OAuth or the |
534 | username / password credentials given are incorrect, and | 559 | username / password credentials given are incorrect, and |
535 | `MastodonAPIError` if all of the requested scopes were not granted. | 560 | `MastodonAPIError` if all of the requested scopes were not granted. |
536 | 561 | ||
537 | For OAuth2, obtain a code via having your user go to the url returned by | 562 | For OAuth 2, obtain a code via having your user go to the URL returned by |
538 | `auth_request_url()`_ and pass it as the code parameter. In this case, | 563 | `auth_request_url()`_ and pass it as the code parameter. In this case, |
539 | make sure to also pass the same redirect_uri parameter as you used when | 564 | make sure to also pass the same redirect_uri parameter as you used when |
540 | generating the auth request URL. | 565 | generating the auth request URL. |
@@ -542,31 +567,38 @@ class Mastodon: | |||
542 | Returns the access token as a string. | 567 | Returns the access token as a string. |
543 | """ | 568 | """ |
544 | if username is not None and password is not None: | 569 | if username is not None and password is not None: |
545 | params = self.__generate_params(locals(), ['scopes', 'to_file', 'code', 'refresh_token']) | 570 | params = self.__generate_params( |
571 | locals(), ['scopes', 'to_file', 'code', 'refresh_token']) | ||
546 | params['grant_type'] = 'password' | 572 | params['grant_type'] = 'password' |
547 | elif code is not None: | 573 | elif code is not None: |
548 | params = self.__generate_params(locals(), ['scopes', 'to_file', 'username', 'password', 'refresh_token']) | 574 | params = self.__generate_params( |
575 | locals(), ['scopes', 'to_file', 'username', 'password', 'refresh_token']) | ||
549 | params['grant_type'] = 'authorization_code' | 576 | params['grant_type'] = 'authorization_code' |
550 | elif refresh_token is not None: | 577 | elif refresh_token is not None: |
551 | params = self.__generate_params(locals(), ['scopes', 'to_file', 'username', 'password', 'code']) | 578 | params = self.__generate_params( |
579 | locals(), ['scopes', 'to_file', 'username', 'password', 'code']) | ||
552 | params['grant_type'] = 'refresh_token' | 580 | params['grant_type'] = 'refresh_token' |
553 | else: | 581 | else: |
554 | raise MastodonIllegalArgumentError('Invalid arguments given. username and password or code are required.') | 582 | raise MastodonIllegalArgumentError( |
583 | 'Invalid arguments given. username and password or code are required.') | ||
555 | 584 | ||
556 | params['client_id'] = self.client_id | 585 | params['client_id'] = self.client_id |
557 | params['client_secret'] = self.client_secret | 586 | params['client_secret'] = self.client_secret |
558 | params['scope'] = " ".join(scopes) | 587 | params['scope'] = " ".join(scopes) |
559 | 588 | ||
560 | try: | 589 | try: |
561 | response = self.__api_request('POST', '/oauth/token', params, do_ratelimiting=False) | 590 | response = self.__api_request( |
591 | 'POST', '/oauth/token', params, do_ratelimiting=False) | ||
562 | self.access_token = response['access_token'] | 592 | self.access_token = response['access_token'] |
563 | self.__set_refresh_token(response.get('refresh_token')) | 593 | self.__set_refresh_token(response.get('refresh_token')) |
564 | self.__set_token_expired(int(response.get('expires_in', 0))) | 594 | self.__set_token_expired(int(response.get('expires_in', 0))) |
565 | except Exception as e: | 595 | except Exception as e: |
566 | if username is not None or password is not None: | 596 | if username is not None or password is not None: |
567 | raise MastodonIllegalArgumentError('Invalid user name, password, or redirect_uris: %s' % e) | 597 | raise MastodonIllegalArgumentError( |
598 | 'Invalid user name, password, or redirect_uris: %s' % e) | ||
568 | elif code is not None: | 599 | elif code is not None: |
569 | raise MastodonIllegalArgumentError('Invalid access token or redirect_uris: %s' % e) | 600 | raise MastodonIllegalArgumentError( |
601 | 'Invalid access token or redirect_uris: %s' % e) | ||
570 | else: | 602 | else: |
571 | raise MastodonIllegalArgumentError('Invalid request: %s' % e) | 603 | raise MastodonIllegalArgumentError('Invalid request: %s' % e) |
572 | 604 | ||
@@ -574,7 +606,7 @@ class Mastodon: | |||
574 | for scope_set in self.__SCOPE_SETS.keys(): | 606 | for scope_set in self.__SCOPE_SETS.keys(): |
575 | if scope_set in received_scopes: | 607 | if scope_set in received_scopes: |
576 | received_scopes += self.__SCOPE_SETS[scope_set] | 608 | received_scopes += self.__SCOPE_SETS[scope_set] |
577 | 609 | ||
578 | if not set(scopes) <= set(received_scopes): | 610 | if not set(scopes) <= set(received_scopes): |
579 | raise MastodonAPIError( | 611 | raise MastodonAPIError( |
580 | 'Granted scopes "' + " ".join(received_scopes) + '" do not contain all of the requested scopes "' + " ".join(scopes) + '".') | 612 | 'Granted scopes "' + " ".join(received_scopes) + '" do not contain all of the requested scopes "' + " ".join(scopes) + '".') |
@@ -583,35 +615,35 @@ class Mastodon: | |||
583 | with open(to_file, 'w') as token_file: | 615 | with open(to_file, 'w') as token_file: |
584 | token_file.write(response['access_token'] + "\n") | 616 | token_file.write(response['access_token'] + "\n") |
585 | token_file.write(self.api_base_url + "\n") | 617 | token_file.write(self.api_base_url + "\n") |
586 | 618 | ||
587 | self.__logged_in_id = None | 619 | self.__logged_in_id = None |
588 | 620 | ||
589 | return response['access_token'] | 621 | return response['access_token'] |
590 | 622 | ||
591 | @api_version("2.7.0", "2.7.0", "2.7.0") | 623 | @api_version("2.7.0", "2.7.0", "2.7.0") |
592 | def create_account(self, username, password, email, agreement=False, reason=None, locale="en", scopes=__DEFAULT_SCOPES, to_file=None): | 624 | def create_account(self, username, password, email, agreement=False, reason=None, locale="en", scopes=__DEFAULT_SCOPES, to_file=None): |
593 | """ | 625 | """ |
594 | Creates a new user account with the given username, password and email. "agreement" | 626 | Creates a new user account with the given username, password and email. "agreement" |
595 | must be set to true (after showing the user the instances user agreement and having | 627 | must be set to true (after showing the user the instance's user agreement and having |
596 | them agree to it), "locale" specifies the language for the confirmation e-mail as an | 628 | them agree to it), "locale" specifies the language for the confirmation email as an |
597 | ISO 639-1 (two-letter) language code. `reason` can be used to specify why a user | 629 | ISO 639-1 (two-letter) language code. `reason` can be used to specify why a user |
598 | would like to join if approved-registrations mode is on. | 630 | would like to join if approved-registrations mode is on. |
599 | 631 | ||
600 | Does not require an access token, but does require a client grant. | 632 | Does not require an access token, but does require a client grant. |
601 | 633 | ||
602 | By default, this method is rate-limited by IP to 5 requests per 30 minutes. | 634 | By default, this method is rate-limited by IP to 5 requests per 30 minutes. |
603 | 635 | ||
604 | Returns an access token (just like log_in), which it can also persist to to_file, | 636 | Returns an access token (just like log_in), which it can also persist to to_file, |
605 | and sets it internally so that the user is now logged in. Note that this token | 637 | and sets it internally so that the user is now logged in. Note that this token |
606 | can only be used after the user has confirmed their e-mail. | 638 | can only be used after the user has confirmed their email. |
607 | """ | 639 | """ |
608 | params = self.__generate_params(locals(), ['to_file', 'scopes']) | 640 | params = self.__generate_params(locals(), ['to_file', 'scopes']) |
609 | params['client_id'] = self.client_id | 641 | params['client_id'] = self.client_id |
610 | params['client_secret'] = self.client_secret | 642 | params['client_secret'] = self.client_secret |
611 | 643 | ||
612 | if agreement == False: | 644 | if agreement == False: |
613 | del params['agreement'] | 645 | del params['agreement'] |
614 | 646 | ||
615 | # Step 1: Get a user-free token via oauth | 647 | # Step 1: Get a user-free token via oauth |
616 | try: | 648 | try: |
617 | oauth_params = {} | 649 | oauth_params = {} |
@@ -619,41 +651,43 @@ class Mastodon: | |||
619 | oauth_params['client_id'] = self.client_id | 651 | oauth_params['client_id'] = self.client_id |
620 | oauth_params['client_secret'] = self.client_secret | 652 | oauth_params['client_secret'] = self.client_secret |
621 | oauth_params['grant_type'] = 'client_credentials' | 653 | oauth_params['grant_type'] = 'client_credentials' |
622 | 654 | ||
623 | response = self.__api_request('POST', '/oauth/token', oauth_params, do_ratelimiting=False) | 655 | response = self.__api_request( |
656 | 'POST', '/oauth/token', oauth_params, do_ratelimiting=False) | ||
624 | temp_access_token = response['access_token'] | 657 | temp_access_token = response['access_token'] |
625 | except Exception as e: | 658 | except Exception as e: |
626 | raise MastodonIllegalArgumentError('Invalid request during oauth phase: %s' % e) | 659 | raise MastodonIllegalArgumentError( |
627 | 660 | 'Invalid request during oauth phase: %s' % e) | |
661 | |||
628 | # Step 2: Use that to create a user | 662 | # Step 2: Use that to create a user |
629 | try: | 663 | try: |
630 | response = self.__api_request('POST', '/api/v1/accounts', params, do_ratelimiting=False, | 664 | response = self.__api_request('POST', '/api/v1/accounts', params, do_ratelimiting=False, |
631 | access_token_override = temp_access_token) | 665 | access_token_override=temp_access_token) |
632 | self.access_token = response['access_token'] | 666 | self.access_token = response['access_token'] |
633 | self.__set_refresh_token(response.get('refresh_token')) | 667 | self.__set_refresh_token(response.get('refresh_token')) |
634 | self.__set_token_expired(int(response.get('expires_in', 0))) | 668 | self.__set_token_expired(int(response.get('expires_in', 0))) |
635 | except Exception as e: | 669 | except Exception as e: |
636 | raise MastodonIllegalArgumentError('Invalid request: %s' % e) | 670 | raise MastodonIllegalArgumentError('Invalid request: %s' % e) |
637 | 671 | ||
638 | # Step 3: Check scopes, persist, et cetera | 672 | # Step 3: Check scopes, persist, et cetera |
639 | received_scopes = response["scope"].split(" ") | 673 | received_scopes = response["scope"].split(" ") |
640 | for scope_set in self.__SCOPE_SETS.keys(): | 674 | for scope_set in self.__SCOPE_SETS.keys(): |
641 | if scope_set in received_scopes: | 675 | if scope_set in received_scopes: |
642 | received_scopes += self.__SCOPE_SETS[scope_set] | 676 | received_scopes += self.__SCOPE_SETS[scope_set] |
643 | 677 | ||
644 | if not set(scopes) <= set(received_scopes): | 678 | if not set(scopes) <= set(received_scopes): |
645 | raise MastodonAPIError( | 679 | raise MastodonAPIError( |
646 | 'Granted scopes "' + " ".join(received_scopes) + '" do not contain all of the requested scopes "' + " ".join(scopes) + '".') | 680 | 'Granted scopes "' + " ".join(received_scopes) + '" do not contain all of the requested scopes "' + " ".join(scopes) + '".') |
647 | 681 | ||
648 | if to_file is not None: | 682 | if to_file is not None: |
649 | with open(to_file, 'w') as token_file: | 683 | with open(to_file, 'w') as token_file: |
650 | token_file.write(response['access_token'] + "\n") | 684 | token_file.write(response['access_token'] + "\n") |
651 | token_file.write(self.api_base_url + "\n") | 685 | token_file.write(self.api_base_url + "\n") |
652 | 686 | ||
653 | self.__logged_in_id = None | 687 | self.__logged_in_id = None |
654 | 688 | ||
655 | return response['access_token'] | 689 | return response['access_token'] |
656 | 690 | ||
657 | ### | 691 | ### |
658 | # Reading data: Instances | 692 | # Reading data: Instances |
659 | ### | 693 | ### |
@@ -680,9 +714,9 @@ class Mastodon: | |||
680 | """ | 714 | """ |
681 | Retrieve activity stats about the instance. May be disabled by the instance administrator - throws | 715 | Retrieve activity stats about the instance. May be disabled by the instance administrator - throws |
682 | a MastodonNotFoundError in that case. | 716 | a MastodonNotFoundError in that case. |
683 | 717 | ||
684 | Activity is returned for 12 weeks going back from the current week. | 718 | Activity is returned for 12 weeks going back from the current week. |
685 | 719 | ||
686 | Returns a list of `activity dicts`_. | 720 | Returns a list of `activity dicts`_. |
687 | """ | 721 | """ |
688 | return self.__api_request('GET', '/api/v1/instance/activity') | 722 | return self.__api_request('GET', '/api/v1/instance/activity') |
@@ -692,7 +726,7 @@ class Mastodon: | |||
692 | """ | 726 | """ |
693 | Retrieve the instances that this instance knows about. May be disabled by the instance administrator - throws | 727 | Retrieve the instances that this instance knows about. May be disabled by the instance administrator - throws |
694 | a MastodonNotFoundError in that case. | 728 | a MastodonNotFoundError in that case. |
695 | 729 | ||
696 | Returns a list of URL strings. | 730 | Returns a list of URL strings. |
697 | """ | 731 | """ |
698 | return self.__api_request('GET', '/api/v1/instance/peers') | 732 | return self.__api_request('GET', '/api/v1/instance/peers') |
@@ -702,37 +736,39 @@ class Mastodon: | |||
702 | """ | 736 | """ |
703 | Basic health check. Returns True if healthy, False if not. | 737 | Basic health check. Returns True if healthy, False if not. |
704 | """ | 738 | """ |
705 | status = self.__api_request('GET', '/health', parse=False).decode("utf-8") | 739 | status = self.__api_request( |
740 | 'GET', '/health', parse=False).decode("utf-8") | ||
706 | return status in ["OK", "success"] | 741 | return status in ["OK", "success"] |
707 | 742 | ||
708 | @api_version("3.0.0", "3.0.0", "3.0.0") | 743 | @api_version("3.0.0", "3.0.0", "3.0.0") |
709 | def instance_nodeinfo(self, schema = "http://nodeinfo.diaspora.software/ns/schema/2.0"): | 744 | def instance_nodeinfo(self, schema="http://nodeinfo.diaspora.software/ns/schema/2.0"): |
710 | """ | 745 | """ |
711 | Retrieves the instances nodeinfo information. | 746 | Retrieves the instance's nodeinfo information. |
712 | 747 | ||
713 | For information on what the nodeinfo can contain, see the nodeinfo | 748 | For information on what the nodeinfo can contain, see the nodeinfo |
714 | specification: https://github.com/jhass/nodeinfo . By default, | 749 | specification: https://github.com/jhass/nodeinfo . By default, |
715 | Mastodon.py will try to retrieve the version 2.0 schema nodeinfo. | 750 | Mastodon.py will try to retrieve the version 2.0 schema nodeinfo. |
716 | 751 | ||
717 | To override the schema, specify the desired schema with the `schema` | 752 | To override the schema, specify the desired schema with the `schema` |
718 | parameter. | 753 | parameter. |
719 | """ | 754 | """ |
720 | links = self.__api_request('GET', '/.well-known/nodeinfo')["links"] | 755 | links = self.__api_request('GET', '/.well-known/nodeinfo')["links"] |
721 | 756 | ||
722 | schema_url = None | 757 | schema_url = None |
723 | for available_schema in links: | 758 | for available_schema in links: |
724 | if available_schema.rel == schema: | 759 | if available_schema.rel == schema: |
725 | schema_url = available_schema.href | 760 | schema_url = available_schema.href |
726 | 761 | ||
727 | if schema_url is None: | 762 | if schema_url is None: |
728 | raise MastodonIllegalArgumentError("Requested nodeinfo schema is not available.") | 763 | raise MastodonIllegalArgumentError( |
729 | 764 | "Requested nodeinfo schema is not available.") | |
765 | |||
730 | try: | 766 | try: |
731 | return self.__api_request('GET', schema_url, base_url_override="") | 767 | return self.__api_request('GET', schema_url, base_url_override="") |
732 | except MastodonNotFoundError: | 768 | except MastodonNotFoundError: |
733 | parse = urlparse(schema_url) | 769 | parse = urlparse(schema_url) |
734 | return self.__api_request('GET', parse.path + parse.params + parse.query + parse.fragment) | 770 | return self.__api_request('GET', parse.path + parse.params + parse.query + parse.fragment) |
735 | 771 | ||
736 | ### | 772 | ### |
737 | # Reading data: Timelines | 773 | # Reading data: Timelines |
738 | ## | 774 | ## |
@@ -741,30 +777,30 @@ class Mastodon: | |||
741 | """ | 777 | """ |
742 | Fetch statuses, most recent ones first. `timeline` can be 'home', 'local', 'public', | 778 | Fetch statuses, most recent ones first. `timeline` can be 'home', 'local', 'public', |
743 | 'tag/hashtag' or 'list/id'. See the following functions documentation for what those do. | 779 | 'tag/hashtag' or 'list/id'. See the following functions documentation for what those do. |
744 | 780 | ||
745 | The default timeline is the "home" timeline. | 781 | The default timeline is the "home" timeline. |
746 | 782 | ||
747 | Specify `only_media` to only get posts with attached media. Specify `local` to only get local statuses, | 783 | Specify `only_media` to only get posts with attached media. Specify `local` to only get local statuses, |
748 | and `remote` to only get remote statuses. Some options are mutually incompatible as dictated by logic. | 784 | and `remote` to only get remote statuses. Some options are mutually incompatible as dictated by logic. |
749 | 785 | ||
750 | May or may not require authentication depending on server settings and what is specifically requested. | 786 | May or may not require authentication depending on server settings and what is specifically requested. |
751 | 787 | ||
752 | Returns a list of `toot dicts`_. | 788 | Returns a list of `toot dicts`_. |
753 | """ | 789 | """ |
754 | if max_id != None: | 790 | if max_id != None: |
755 | max_id = self.__unpack_id(max_id, dateconv=True) | 791 | max_id = self.__unpack_id(max_id, dateconv=True) |
756 | 792 | ||
757 | if min_id != None: | 793 | if min_id != None: |
758 | min_id = self.__unpack_id(min_id, dateconv=True) | 794 | min_id = self.__unpack_id(min_id, dateconv=True) |
759 | 795 | ||
760 | if since_id != None: | 796 | if since_id != None: |
761 | since_id = self.__unpack_id(since_id, dateconv=True) | 797 | since_id = self.__unpack_id(since_id, dateconv=True) |
762 | 798 | ||
763 | params_initial = locals() | 799 | params_initial = locals() |
764 | 800 | ||
765 | if local == False: | 801 | if local == False: |
766 | del params_initial['local'] | 802 | del params_initial['local'] |
767 | 803 | ||
768 | if remote == False: | 804 | if remote == False: |
769 | del params_initial['remote'] | 805 | del params_initial['remote'] |
770 | 806 | ||
@@ -778,11 +814,11 @@ class Mastodon: | |||
778 | params = self.__generate_params(params_initial, ['timeline']) | 814 | params = self.__generate_params(params_initial, ['timeline']) |
779 | url = '/api/v1/timelines/{0}'.format(timeline) | 815 | url = '/api/v1/timelines/{0}'.format(timeline) |
780 | return self.__api_request('GET', url, params) | 816 | return self.__api_request('GET', url, params) |
781 | 817 | ||
782 | @api_version("1.0.0", "3.1.4", __DICT_VERSION_STATUS) | 818 | @api_version("1.0.0", "3.1.4", __DICT_VERSION_STATUS) |
783 | def timeline_home(self, max_id=None, min_id=None, since_id=None, limit=None, only_media=False, local=False, remote=False): | 819 | def timeline_home(self, max_id=None, min_id=None, since_id=None, limit=None, only_media=False, local=False, remote=False): |
784 | """ | 820 | """ |
785 | Convenience method: Fetches the logged-in users home timeline (i.e. followed users and self). Params as in `timeline()`. | 821 | Convenience method: Fetches the logged-in user's home timeline (i.e. followed users and self). Params as in `timeline()`. |
786 | 822 | ||
787 | Returns a list of `toot dicts`_. | 823 | Returns a list of `toot dicts`_. |
788 | """ | 824 | """ |
@@ -805,17 +841,18 @@ class Mastodon: | |||
805 | Returns a list of `toot dicts`_. | 841 | Returns a list of `toot dicts`_. |
806 | """ | 842 | """ |
807 | 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) | 843 | 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) |
808 | 844 | ||
809 | @api_version("1.0.0", "3.1.4", __DICT_VERSION_STATUS) | 845 | @api_version("1.0.0", "3.1.4", __DICT_VERSION_STATUS) |
810 | def timeline_hashtag(self, hashtag, local=False, max_id=None, min_id=None, since_id=None, limit=None, only_media=False, remote=False): | 846 | def timeline_hashtag(self, hashtag, local=False, max_id=None, min_id=None, since_id=None, limit=None, only_media=False, remote=False): |
811 | """ | 847 | """ |
812 | Convenience method: Fetch a timeline of toots with a given hashtag. The hashtag parameter | 848 | Convenience method: Fetch a timeline of toots with a given hashtag. The hashtag parameter |
813 | should not contain the leading #. Params as in `timeline()`. | 849 | should not contain the leading #. Params as in `timeline()`. |
814 | 850 | ||
815 | Returns a list of `toot dicts`_. | 851 | Returns a list of `toot dicts`_. |
816 | """ | 852 | """ |
817 | if hashtag.startswith("#"): | 853 | if hashtag.startswith("#"): |
818 | raise MastodonIllegalArgumentError("Hashtag parameter should omit leading #") | 854 | raise MastodonIllegalArgumentError( |
855 | "Hashtag parameter should omit leading #") | ||
819 | 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) | 856 | 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) |
820 | 857 | ||
821 | @api_version("2.1.0", "3.1.4", __DICT_VERSION_STATUS) | 858 | @api_version("2.1.0", "3.1.4", __DICT_VERSION_STATUS) |
@@ -831,22 +868,22 @@ class Mastodon: | |||
831 | @api_version("2.6.0", "2.6.0", __DICT_VERSION_CONVERSATION) | 868 | @api_version("2.6.0", "2.6.0", __DICT_VERSION_CONVERSATION) |
832 | def conversations(self, max_id=None, min_id=None, since_id=None, limit=None): | 869 | def conversations(self, max_id=None, min_id=None, since_id=None, limit=None): |
833 | """ | 870 | """ |
834 | Fetches a users conversations. | 871 | Fetches a user's conversations. |
835 | 872 | ||
836 | Returns a list of `conversation dicts`_. | 873 | Returns a list of `conversation dicts`_. |
837 | """ | 874 | """ |
838 | if max_id != None: | 875 | if max_id != None: |
839 | max_id = self.__unpack_id(max_id, dateconv=True) | 876 | max_id = self.__unpack_id(max_id, dateconv=True) |
840 | 877 | ||
841 | if min_id != None: | 878 | if min_id != None: |
842 | min_id = self.__unpack_id(min_id, dateconv=True) | 879 | min_id = self.__unpack_id(min_id, dateconv=True) |
843 | 880 | ||
844 | if since_id != None: | 881 | if since_id != None: |
845 | since_id = self.__unpack_id(since_id, dateconv=True) | 882 | since_id = self.__unpack_id(since_id, dateconv=True) |
846 | 883 | ||
847 | params = self.__generate_params(locals()) | 884 | params = self.__generate_params(locals()) |
848 | return self.__api_request('GET', "/api/v1/conversations/", params) | 885 | return self.__api_request('GET', "/api/v1/conversations/", params) |
849 | 886 | ||
850 | ### | 887 | ### |
851 | # Reading data: Statuses | 888 | # Reading data: Statuses |
852 | ### | 889 | ### |
@@ -873,7 +910,7 @@ class Mastodon: | |||
873 | 910 | ||
874 | This function is deprecated as of 3.0.0 and the endpoint does not | 911 | This function is deprecated as of 3.0.0 and the endpoint does not |
875 | exist anymore - you should just use the "card" field of the status dicts | 912 | exist anymore - you should just use the "card" field of the status dicts |
876 | instead. Mastodon.py will try to mimick the old behaviour, but this | 913 | instead. Mastodon.py will try to mimic the old behaviour, but this |
877 | is somewhat inefficient and not guaranteed to be the case forever. | 914 | is somewhat inefficient and not guaranteed to be the case forever. |
878 | 915 | ||
879 | Returns a `card dict`_. | 916 | Returns a `card dict`_. |
@@ -935,7 +972,7 @@ class Mastodon: | |||
935 | Returns a list of `scheduled toot dicts`_. | 972 | Returns a list of `scheduled toot dicts`_. |
936 | """ | 973 | """ |
937 | return self.__api_request('GET', '/api/v1/scheduled_statuses') | 974 | return self.__api_request('GET', '/api/v1/scheduled_statuses') |
938 | 975 | ||
939 | @api_version("2.7.0", "2.7.0", __DICT_VERSION_SCHEDULED_STATUS) | 976 | @api_version("2.7.0", "2.7.0", __DICT_VERSION_SCHEDULED_STATUS) |
940 | def scheduled_status(self, id): | 977 | def scheduled_status(self, id): |
941 | """ | 978 | """ |
@@ -946,7 +983,7 @@ class Mastodon: | |||
946 | id = self.__unpack_id(id) | 983 | id = self.__unpack_id(id) |
947 | url = '/api/v1/scheduled_statuses/{0}'.format(str(id)) | 984 | url = '/api/v1/scheduled_statuses/{0}'.format(str(id)) |
948 | return self.__api_request('GET', url) | 985 | return self.__api_request('GET', url) |
949 | 986 | ||
950 | ### | 987 | ### |
951 | # Reading data: Polls | 988 | # Reading data: Polls |
952 | ### | 989 | ### |
@@ -960,7 +997,7 @@ class Mastodon: | |||
960 | id = self.__unpack_id(id) | 997 | id = self.__unpack_id(id) |
961 | url = '/api/v1/polls/{0}'.format(str(id)) | 998 | url = '/api/v1/polls/{0}'.format(str(id)) |
962 | return self.__api_request('GET', url) | 999 | return self.__api_request('GET', url) |
963 | 1000 | ||
964 | ### | 1001 | ### |
965 | # Reading data: Notifications | 1002 | # Reading data: Notifications |
966 | ### | 1003 | ### |
@@ -973,7 +1010,7 @@ class Mastodon: | |||
973 | Parameter `exclude_types` is an array of the following `follow`, `favourite`, `reblog`, | 1010 | Parameter `exclude_types` is an array of the following `follow`, `favourite`, `reblog`, |
974 | `mention`, `poll`, `follow_request`. Specifying `mentions_only` is a deprecated way to | 1011 | `mention`, `poll`, `follow_request`. Specifying `mentions_only` is a deprecated way to |
975 | set `exclude_types` to all but mentions. | 1012 | set `exclude_types` to all but mentions. |
976 | 1013 | ||
977 | Can be passed an `id` to fetch a single notification. | 1014 | Can be passed an `id` to fetch a single notification. |
978 | 1015 | ||
979 | Returns a list of `notification dicts`_. | 1016 | Returns a list of `notification dicts`_. |
@@ -981,23 +1018,25 @@ class Mastodon: | |||
981 | if not mentions_only is None: | 1018 | if not mentions_only is None: |
982 | if not exclude_types is None: | 1019 | if not exclude_types is None: |
983 | if mentions_only: | 1020 | if mentions_only: |
984 | exclude_types = ["follow", "favourite", "reblog", "poll", "follow_request"] | 1021 | exclude_types = ["follow", "favourite", |
1022 | "reblog", "poll", "follow_request"] | ||
985 | else: | 1023 | else: |
986 | raise MastodonIllegalArgumentError('Cannot specify exclude_types when mentions_only is present') | 1024 | raise MastodonIllegalArgumentError( |
1025 | 'Cannot specify exclude_types when mentions_only is present') | ||
987 | del mentions_only | 1026 | del mentions_only |
988 | 1027 | ||
989 | if max_id != None: | 1028 | if max_id != None: |
990 | max_id = self.__unpack_id(max_id, dateconv=True) | 1029 | max_id = self.__unpack_id(max_id, dateconv=True) |
991 | 1030 | ||
992 | if min_id != None: | 1031 | if min_id != None: |
993 | min_id = self.__unpack_id(min_id, dateconv=True) | 1032 | min_id = self.__unpack_id(min_id, dateconv=True) |
994 | 1033 | ||
995 | if since_id != None: | 1034 | if since_id != None: |
996 | since_id = self.__unpack_id(since_id, dateconv=True) | 1035 | since_id = self.__unpack_id(since_id, dateconv=True) |
997 | 1036 | ||
998 | if account_id != None: | 1037 | if account_id != None: |
999 | account_id = self.__unpack_id(account_id) | 1038 | account_id = self.__unpack_id(account_id) |
1000 | 1039 | ||
1001 | if id is None: | 1040 | if id is None: |
1002 | params = self.__generate_params(locals(), ['id']) | 1041 | params = self.__generate_params(locals(), ['id']) |
1003 | return self.__api_request('GET', '/api/v1/notifications', params) | 1042 | return self.__api_request('GET', '/api/v1/notifications', params) |
@@ -1013,15 +1052,15 @@ class Mastodon: | |||
1013 | def account(self, id): | 1052 | def account(self, id): |
1014 | """ | 1053 | """ |
1015 | Fetch account information by user `id`. | 1054 | Fetch account information by user `id`. |
1016 | 1055 | ||
1017 | Does not require authentication for publicly visible accounts. | 1056 | Does not require authentication for publicly visible accounts. |
1018 | 1057 | ||
1019 | Returns a `user dict`_. | 1058 | Returns a `user dict`_. |
1020 | """ | 1059 | """ |
1021 | id = self.__unpack_id(id) | 1060 | id = self.__unpack_id(id) |
1022 | url = '/api/v1/accounts/{0}'.format(str(id)) | 1061 | url = '/api/v1/accounts/{0}'.format(str(id)) |
1023 | return self.__api_request('GET', url) | 1062 | return self.__api_request('GET', url) |
1024 | 1063 | ||
1025 | @api_version("1.0.0", "2.1.0", __DICT_VERSION_ACCOUNT) | 1064 | @api_version("1.0.0", "2.1.0", __DICT_VERSION_ACCOUNT) |
1026 | def account_verify_credentials(self): | 1065 | def account_verify_credentials(self): |
1027 | """ | 1066 | """ |
@@ -1034,12 +1073,12 @@ class Mastodon: | |||
1034 | @api_version("1.0.0", "2.1.0", __DICT_VERSION_ACCOUNT) | 1073 | @api_version("1.0.0", "2.1.0", __DICT_VERSION_ACCOUNT) |
1035 | def me(self): | 1074 | def me(self): |
1036 | """ | 1075 | """ |
1037 | Get this users account. Symonym for `account_verify_credentials()`, does exactly | 1076 | Get this user's account. Synonym for `account_verify_credentials()`, does exactly |
1038 | the same thing, just exists becase `account_verify_credentials()` has a confusing | 1077 | the same thing, just exists becase `account_verify_credentials()` has a confusing |
1039 | name. | 1078 | name. |
1040 | """ | 1079 | """ |
1041 | return self.account_verify_credentials() | 1080 | return self.account_verify_credentials() |
1042 | 1081 | ||
1043 | @api_version("1.0.0", "2.8.0", __DICT_VERSION_STATUS) | 1082 | @api_version("1.0.0", "2.8.0", __DICT_VERSION_STATUS) |
1044 | 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): | 1083 | 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): |
1045 | """ | 1084 | """ |
@@ -1049,7 +1088,7 @@ class Mastodon: | |||
1049 | included. | 1088 | included. |
1050 | 1089 | ||
1051 | If `only_media` is set, return only statuses with media attachments. | 1090 | If `only_media` is set, return only statuses with media attachments. |
1052 | If `pinned` is set, return only statuses that have been pinned. Note that | 1091 | If `pinned` is set, return only statuses that have been pinned. Note that |
1053 | as of Mastodon 2.1.0, this only works properly for instance-local users. | 1092 | as of Mastodon 2.1.0, this only works properly for instance-local users. |
1054 | If `exclude_replies` is set, filter out all statuses that are replies. | 1093 | If `exclude_replies` is set, filter out all statuses that are replies. |
1055 | If `exclude_reblogs` is set, filter out all statuses that are reblogs. | 1094 | If `exclude_reblogs` is set, filter out all statuses that are reblogs. |
@@ -1063,13 +1102,13 @@ class Mastodon: | |||
1063 | id = self.__unpack_id(id) | 1102 | id = self.__unpack_id(id) |
1064 | if max_id != None: | 1103 | if max_id != None: |
1065 | max_id = self.__unpack_id(max_id, dateconv=True) | 1104 | max_id = self.__unpack_id(max_id, dateconv=True) |
1066 | 1105 | ||
1067 | if min_id != None: | 1106 | if min_id != None: |
1068 | min_id = self.__unpack_id(min_id, dateconv=True) | 1107 | min_id = self.__unpack_id(min_id, dateconv=True) |
1069 | 1108 | ||
1070 | if since_id != None: | 1109 | if since_id != None: |
1071 | since_id = self.__unpack_id(since_id, dateconv=True) | 1110 | since_id = self.__unpack_id(since_id, dateconv=True) |
1072 | 1111 | ||
1073 | params = self.__generate_params(locals(), ['id']) | 1112 | params = self.__generate_params(locals(), ['id']) |
1074 | if pinned == False: | 1113 | if pinned == False: |
1075 | del params["pinned"] | 1114 | del params["pinned"] |
@@ -1093,13 +1132,13 @@ class Mastodon: | |||
1093 | id = self.__unpack_id(id) | 1132 | id = self.__unpack_id(id) |
1094 | if max_id != None: | 1133 | if max_id != None: |
1095 | max_id = self.__unpack_id(max_id, dateconv=True) | 1134 | max_id = self.__unpack_id(max_id, dateconv=True) |
1096 | 1135 | ||
1097 | if min_id != None: | 1136 | if min_id != None: |
1098 | min_id = self.__unpack_id(min_id, dateconv=True) | 1137 | min_id = self.__unpack_id(min_id, dateconv=True) |
1099 | 1138 | ||
1100 | if since_id != None: | 1139 | if since_id != None: |
1101 | since_id = self.__unpack_id(since_id, dateconv=True) | 1140 | since_id = self.__unpack_id(since_id, dateconv=True) |
1102 | 1141 | ||
1103 | params = self.__generate_params(locals(), ['id']) | 1142 | params = self.__generate_params(locals(), ['id']) |
1104 | url = '/api/v1/accounts/{0}/following'.format(str(id)) | 1143 | url = '/api/v1/accounts/{0}/following'.format(str(id)) |
1105 | return self.__api_request('GET', url, params) | 1144 | return self.__api_request('GET', url, params) |
@@ -1114,21 +1153,21 @@ class Mastodon: | |||
1114 | id = self.__unpack_id(id) | 1153 | id = self.__unpack_id(id) |
1115 | if max_id != None: | 1154 | if max_id != None: |
1116 | max_id = self.__unpack_id(max_id, dateconv=True) | 1155 | max_id = self.__unpack_id(max_id, dateconv=True) |
1117 | 1156 | ||
1118 | if min_id != None: | 1157 | if min_id != None: |
1119 | min_id = self.__unpack_id(min_id, dateconv=True) | 1158 | min_id = self.__unpack_id(min_id, dateconv=True) |
1120 | 1159 | ||
1121 | if since_id != None: | 1160 | if since_id != None: |
1122 | since_id = self.__unpack_id(since_id, dateconv=True) | 1161 | since_id = self.__unpack_id(since_id, dateconv=True) |
1123 | 1162 | ||
1124 | params = self.__generate_params(locals(), ['id']) | 1163 | params = self.__generate_params(locals(), ['id']) |
1125 | url = '/api/v1/accounts/{0}/followers'.format(str(id)) | 1164 | url = '/api/v1/accounts/{0}/followers'.format(str(id)) |
1126 | return self.__api_request('GET', url, params) | 1165 | return self.__api_request('GET', url, params) |
1127 | 1166 | ||
1128 | @api_version("1.0.0", "1.4.0", __DICT_VERSION_RELATIONSHIP) | 1167 | @api_version("1.0.0", "1.4.0", __DICT_VERSION_RELATIONSHIP) |
1129 | def account_relationships(self, id): | 1168 | def account_relationships(self, id): |
1130 | """ | 1169 | """ |
1131 | Fetch relationship (following, followed_by, blocking, follow requested) of | 1170 | Fetch relationship (following, followed_by, blocking, follow requested) of |
1132 | the logged in user to a given account. `id` can be a list. | 1171 | the logged in user to a given account. `id` can be a list. |
1133 | 1172 | ||
1134 | Returns a list of `relationship dicts`_. | 1173 | Returns a list of `relationship dicts`_. |
@@ -1148,92 +1187,92 @@ class Mastodon: | |||
1148 | Returns a list of `user dicts`_. | 1187 | Returns a list of `user dicts`_. |
1149 | """ | 1188 | """ |
1150 | params = self.__generate_params(locals()) | 1189 | params = self.__generate_params(locals()) |
1151 | 1190 | ||
1152 | if params["following"] == False: | 1191 | if params["following"] == False: |
1153 | del params["following"] | 1192 | del params["following"] |
1154 | 1193 | ||
1155 | return self.__api_request('GET', '/api/v1/accounts/search', params) | 1194 | return self.__api_request('GET', '/api/v1/accounts/search', params) |
1156 | 1195 | ||
1157 | @api_version("2.1.0", "2.1.0", __DICT_VERSION_LIST) | 1196 | @api_version("2.1.0", "2.1.0", __DICT_VERSION_LIST) |
1158 | def account_lists(self, id): | 1197 | def account_lists(self, id): |
1159 | """ | 1198 | """ |
1160 | Get all of the logged-in users lists which the specified user is | 1199 | Get all of the logged-in user's lists which the specified user is |
1161 | a member of. | 1200 | a member of. |
1162 | 1201 | ||
1163 | Returns a list of `list dicts`_. | 1202 | Returns a list of `list dicts`_. |
1164 | """ | 1203 | """ |
1165 | id = self.__unpack_id(id) | 1204 | id = self.__unpack_id(id) |
1166 | params = self.__generate_params(locals(), ['id']) | 1205 | params = self.__generate_params(locals(), ['id']) |
1167 | url = '/api/v1/accounts/{0}/lists'.format(str(id)) | 1206 | url = '/api/v1/accounts/{0}/lists'.format(str(id)) |
1168 | return self.__api_request('GET', url, params) | 1207 | return self.__api_request('GET', url, params) |
1169 | 1208 | ||
1170 | ### | 1209 | ### |
1171 | # Reading data: Featured hashtags | 1210 | # Reading data: Featured hashtags |
1172 | ### | 1211 | ### |
1173 | @api_version("3.0.0", "3.0.0", __DICT_VERSION_FEATURED_TAG) | 1212 | @api_version("3.0.0", "3.0.0", __DICT_VERSION_FEATURED_TAG) |
1174 | def featured_tags(self): | 1213 | def featured_tags(self): |
1175 | """ | 1214 | """ |
1176 | Return the hashtags the logged-in user has set to be featured on | 1215 | Return the hashtags the logged-in user has set to be featured on |
1177 | their profile as a list of `featured tag dicts`_. | 1216 | their profile as a list of `featured tag dicts`_. |
1178 | 1217 | ||
1179 | Returns a list of `featured tag dicts`_. | 1218 | Returns a list of `featured tag dicts`_. |
1180 | """ | 1219 | """ |
1181 | return self.__api_request('GET', '/api/v1/featured_tags') | 1220 | return self.__api_request('GET', '/api/v1/featured_tags') |
1182 | 1221 | ||
1183 | @api_version("3.0.0", "3.0.0", __DICT_VERSION_HASHTAG) | 1222 | @api_version("3.0.0", "3.0.0", __DICT_VERSION_HASHTAG) |
1184 | def featured_tag_suggestions(self): | 1223 | def featured_tag_suggestions(self): |
1185 | """ | 1224 | """ |
1186 | Returns the logged-in users 10 most commonly hashtags. | 1225 | Returns the logged-in user's 10 most commonly-used hashtags. |
1187 | 1226 | ||
1188 | Returns a list of `hashtag dicts`_. | 1227 | Returns a list of `hashtag dicts`_. |
1189 | """ | 1228 | """ |
1190 | return self.__api_request('GET', '/api/v1/featured_tags/suggestions') | 1229 | return self.__api_request('GET', '/api/v1/featured_tags/suggestions') |
1191 | 1230 | ||
1192 | ### | 1231 | ### |
1193 | # Reading data: Keyword filters | 1232 | # Reading data: Keyword filters |
1194 | ### | 1233 | ### |
1195 | @api_version("2.4.3", "2.4.3", __DICT_VERSION_FILTER) | 1234 | @api_version("2.4.3", "2.4.3", __DICT_VERSION_FILTER) |
1196 | def filters(self): | 1235 | def filters(self): |
1197 | """ | 1236 | """ |
1198 | Fetch all of the logged-in users filters. | 1237 | Fetch all of the logged-in user's filters. |
1199 | 1238 | ||
1200 | Returns a list of `filter dicts`_. Not paginated. | 1239 | Returns a list of `filter dicts`_. Not paginated. |
1201 | """ | 1240 | """ |
1202 | return self.__api_request('GET', '/api/v1/filters') | 1241 | return self.__api_request('GET', '/api/v1/filters') |
1203 | 1242 | ||
1204 | @api_version("2.4.3", "2.4.3", __DICT_VERSION_FILTER) | 1243 | @api_version("2.4.3", "2.4.3", __DICT_VERSION_FILTER) |
1205 | def filter(self, id): | 1244 | def filter(self, id): |
1206 | """ | 1245 | """ |
1207 | Fetches information about the filter with the specified `id`. | 1246 | Fetches information about the filter with the specified `id`. |
1208 | 1247 | ||
1209 | Returns a `filter dict`_. | 1248 | Returns a `filter dict`_. |
1210 | """ | 1249 | """ |
1211 | id = self.__unpack_id(id) | 1250 | id = self.__unpack_id(id) |
1212 | url = '/api/v1/filters/{0}'.format(str(id)) | 1251 | url = '/api/v1/filters/{0}'.format(str(id)) |
1213 | return self.__api_request('GET', url) | 1252 | return self.__api_request('GET', url) |
1214 | 1253 | ||
1215 | @api_version("2.4.3", "2.4.3", __DICT_VERSION_FILTER) | 1254 | @api_version("2.4.3", "2.4.3", __DICT_VERSION_FILTER) |
1216 | def filters_apply(self, objects, filters, context): | 1255 | def filters_apply(self, objects, filters, context): |
1217 | """ | 1256 | """ |
1218 | Helper function: Applies a list of filters to a list of either statuses | 1257 | Helper function: Applies a list of filters to a list of either statuses |
1219 | or notifications and returns only those matched by none. This function will | 1258 | or notifications and returns only those matched by none. This function will |
1220 | apply all filters that match the context provided in `context`, i.e. | 1259 | apply all filters that match the context provided in `context`, i.e. |
1221 | if you want to apply only notification-relevant filters, specify | 1260 | if you want to apply only notification-relevant filters, specify |
1222 | 'notifications'. Valid contexts are 'home', 'notifications', 'public' and 'thread'. | 1261 | 'notifications'. Valid contexts are 'home', 'notifications', 'public' and 'thread'. |
1223 | """ | 1262 | """ |
1224 | 1263 | ||
1225 | # Build filter regex | 1264 | # Build filter regex |
1226 | filter_strings = [] | 1265 | filter_strings = [] |
1227 | for keyword_filter in filters: | 1266 | for keyword_filter in filters: |
1228 | if not context in keyword_filter["context"]: | 1267 | if not context in keyword_filter["context"]: |
1229 | continue | 1268 | continue |
1230 | 1269 | ||
1231 | filter_string = re.escape(keyword_filter["phrase"]) | 1270 | filter_string = re.escape(keyword_filter["phrase"]) |
1232 | if keyword_filter["whole_word"] == True: | 1271 | if keyword_filter["whole_word"] == True: |
1233 | filter_string = "\\b" + filter_string + "\\b" | 1272 | filter_string = "\\b" + filter_string + "\\b" |
1234 | filter_strings.append(filter_string) | 1273 | filter_strings.append(filter_string) |
1235 | filter_re = re.compile("|".join(filter_strings), flags = re.IGNORECASE) | 1274 | filter_re = re.compile("|".join(filter_strings), flags=re.IGNORECASE) |
1236 | 1275 | ||
1237 | # Apply | 1276 | # Apply |
1238 | filter_results = [] | 1277 | filter_results = [] |
1239 | for filter_object in objects: | 1278 | for filter_object in objects: |
@@ -1246,7 +1285,7 @@ class Mastodon: | |||
1246 | if not filter_re.search(filter_text): | 1285 | if not filter_re.search(filter_text): |
1247 | filter_results.append(filter_object) | 1286 | filter_results.append(filter_object) |
1248 | return filter_results | 1287 | return filter_results |
1249 | 1288 | ||
1250 | ### | 1289 | ### |
1251 | # Reading data: Follow suggestions | 1290 | # Reading data: Follow suggestions |
1252 | ### | 1291 | ### |
@@ -1256,10 +1295,10 @@ class Mastodon: | |||
1256 | Fetch follow suggestions for the logged-in user. | 1295 | Fetch follow suggestions for the logged-in user. |
1257 | 1296 | ||
1258 | Returns a list of `user dicts`_. | 1297 | Returns a list of `user dicts`_. |
1259 | 1298 | ||
1260 | """ | 1299 | """ |
1261 | return self.__api_request('GET', '/api/v1/suggestions') | 1300 | return self.__api_request('GET', '/api/v1/suggestions') |
1262 | 1301 | ||
1263 | ### | 1302 | ### |
1264 | # Reading data: Follow suggestions | 1303 | # Reading data: Follow suggestions |
1265 | ### | 1304 | ### |
@@ -1269,27 +1308,27 @@ class Mastodon: | |||
1269 | Fetch the contents of the profile directory, if enabled on the server. | 1308 | Fetch the contents of the profile directory, if enabled on the server. |
1270 | 1309 | ||
1271 | Returns a list of `user dicts`_. | 1310 | Returns a list of `user dicts`_. |
1272 | 1311 | ||
1273 | """ | 1312 | """ |
1274 | return self.__api_request('GET', '/api/v1/directory') | 1313 | return self.__api_request('GET', '/api/v1/directory') |
1275 | 1314 | ||
1276 | ### | 1315 | ### |
1277 | # Reading data: Endorsements | 1316 | # Reading data: Endorsements |
1278 | ### | 1317 | ### |
1279 | @api_version("2.5.0", "2.5.0", __DICT_VERSION_ACCOUNT) | 1318 | @api_version("2.5.0", "2.5.0", __DICT_VERSION_ACCOUNT) |
1280 | def endorsements(self): | 1319 | def endorsements(self): |
1281 | """ | 1320 | """ |
1282 | Fetch list of users endorsemed by the logged-in user. | 1321 | Fetch list of users endorsed by the logged-in user. |
1283 | 1322 | ||
1284 | Returns a list of `user dicts`_. | 1323 | Returns a list of `user dicts`_. |
1285 | 1324 | ||
1286 | """ | 1325 | """ |
1287 | return self.__api_request('GET', '/api/v1/endorsements') | 1326 | return self.__api_request('GET', '/api/v1/endorsements') |
1288 | 1327 | ||
1289 | |||
1290 | ### | 1328 | ### |
1291 | # Reading data: Searching | 1329 | # Reading data: Searching |
1292 | ### | 1330 | ### |
1331 | |||
1293 | def __ensure_search_params_acceptable(self, account_id, offset, min_id, max_id): | 1332 | def __ensure_search_params_acceptable(self, account_id, offset, min_id, max_id): |
1294 | """ | 1333 | """ |
1295 | Internal Helper: Throw a MastodonVersionError if version is < 2.8.0 but parameters | 1334 | Internal Helper: Throw a MastodonVersionError if version is < 2.8.0 but parameters |
@@ -1297,8 +1336,9 @@ class Mastodon: | |||
1297 | """ | 1336 | """ |
1298 | if not account_id is None or not offset is None or not min_id is None or not max_id is None: | 1337 | if not account_id is None or not offset is None or not min_id is None or not max_id is None: |
1299 | if self.verify_minimum_version("2.8.0", cached=True) == False: | 1338 | if self.verify_minimum_version("2.8.0", cached=True) == False: |
1300 | raise MastodonVersionError("Advanced search parameters require Mastodon 2.8.0+") | 1339 | raise MastodonVersionError( |
1301 | 1340 | "Advanced search parameters require Mastodon 2.8.0+") | |
1341 | |||
1302 | @api_version("1.1.0", "2.8.0", __DICT_VERSION_SEARCHRESULT) | 1342 | @api_version("1.1.0", "2.8.0", __DICT_VERSION_SEARCHRESULT) |
1303 | def search(self, q, resolve=True, result_type=None, account_id=None, offset=None, min_id=None, max_id=None, exclude_unreviewed=True): | 1343 | def search(self, q, resolve=True, result_type=None, account_id=None, offset=None, min_id=None, max_id=None, exclude_unreviewed=True): |
1304 | """ | 1344 | """ |
@@ -1306,32 +1346,33 @@ class Mastodon: | |||
1306 | lookups if resolve is True. Full-text search is only enabled if | 1346 | lookups if resolve is True. Full-text search is only enabled if |
1307 | the instance supports it, and is restricted to statuses the logged-in | 1347 | the instance supports it, and is restricted to statuses the logged-in |
1308 | user wrote or was mentioned in. | 1348 | user wrote or was mentioned in. |
1309 | 1349 | ||
1310 | `result_type` can be one of "accounts", "hashtags" or "statuses", to only | 1350 | `result_type` can be one of "accounts", "hashtags" or "statuses", to only |
1311 | search for that type of object. | 1351 | search for that type of object. |
1312 | 1352 | ||
1313 | Specify `account_id` to only get results from the account with that id. | 1353 | Specify `account_id` to only get results from the account with that id. |
1314 | 1354 | ||
1315 | `offset`, `min_id` and `max_id` can be used to paginate. | 1355 | `offset`, `min_id` and `max_id` can be used to paginate. |
1316 | 1356 | ||
1317 | `exclude_unreviewed` can be used to restrict search results for hashtags to only | 1357 | `exclude_unreviewed` can be used to restrict search results for hashtags to only |
1318 | those that have been reviewed by moderators. It is on by default. | 1358 | those that have been reviewed by moderators. It is on by default. |
1319 | 1359 | ||
1320 | Will use search_v1 (no tag dicts in return values) on Mastodon versions before | 1360 | Will use search_v1 (no tag dicts in return values) on Mastodon versions before |
1321 | 2.4.1), search_v2 otherwise. Parameters other than resolve are only available | 1361 | 2.4.1), search_v2 otherwise. Parameters other than resolve are only available |
1322 | on Mastodon 2.8.0 or above - this function will throw a MastodonVersionError | 1362 | on Mastodon 2.8.0 or above - this function will throw a MastodonVersionError |
1323 | if you try to use them on versions before that. Note that the cached version | 1363 | if you try to use them on versions before that. Note that the cached version |
1324 | number will be used for this to avoid uneccesary requests. | 1364 | number will be used for this to avoid uneccesary requests. |
1325 | 1365 | ||
1326 | Returns a `search result dict`_, with tags as `hashtag dicts`_. | 1366 | Returns a `search result dict`_, with tags as `hashtag dicts`_. |
1327 | """ | 1367 | """ |
1328 | if self.verify_minimum_version("2.4.1", cached=True) == True: | 1368 | if self.verify_minimum_version("2.4.1", cached=True) == True: |
1329 | return self.search_v2(q, resolve=resolve, result_type=result_type, account_id=account_id, | 1369 | return self.search_v2(q, resolve=resolve, result_type=result_type, account_id=account_id, |
1330 | offset=offset, min_id=min_id, max_id=max_id) | 1370 | offset=offset, min_id=min_id, max_id=max_id) |
1331 | else: | 1371 | else: |
1332 | self.__ensure_search_params_acceptable(account_id, offset, min_id, max_id) | 1372 | self.__ensure_search_params_acceptable( |
1373 | account_id, offset, min_id, max_id) | ||
1333 | return self.search_v1(q, resolve=resolve) | 1374 | return self.search_v1(q, resolve=resolve) |
1334 | 1375 | ||
1335 | @api_version("1.1.0", "2.1.0", "2.1.0") | 1376 | @api_version("1.1.0", "2.1.0", "2.1.0") |
1336 | def search_v1(self, q, resolve=False): | 1377 | def search_v1(self, q, resolve=False): |
1337 | """ | 1378 | """ |
@@ -1350,43 +1391,44 @@ class Mastodon: | |||
1350 | """ | 1391 | """ |
1351 | Identical to `search_v1()`, except in that it returns tags as | 1392 | Identical to `search_v1()`, except in that it returns tags as |
1352 | `hashtag dicts`_, has more parameters, and resolves by default. | 1393 | `hashtag dicts`_, has more parameters, and resolves by default. |
1353 | 1394 | ||
1354 | For more details documentation, please see `search()` | 1395 | For more details documentation, please see `search()` |
1355 | 1396 | ||
1356 | Returns a `search result dict`_. | 1397 | Returns a `search result dict`_. |
1357 | """ | 1398 | """ |
1358 | self.__ensure_search_params_acceptable(account_id, offset, min_id, max_id) | 1399 | self.__ensure_search_params_acceptable( |
1400 | account_id, offset, min_id, max_id) | ||
1359 | params = self.__generate_params(locals()) | 1401 | params = self.__generate_params(locals()) |
1360 | 1402 | ||
1361 | if resolve == False: | 1403 | if resolve == False: |
1362 | del params["resolve"] | 1404 | del params["resolve"] |
1363 | 1405 | ||
1364 | if exclude_unreviewed == False or not self.verify_minimum_version("3.0.0", cached=True): | 1406 | if exclude_unreviewed == False or not self.verify_minimum_version("3.0.0", cached=True): |
1365 | del params["exclude_unreviewed"] | 1407 | del params["exclude_unreviewed"] |
1366 | 1408 | ||
1367 | if "result_type" in params: | 1409 | if "result_type" in params: |
1368 | params["type"] = params["result_type"] | 1410 | params["type"] = params["result_type"] |
1369 | del params["result_type"] | 1411 | del params["result_type"] |
1370 | 1412 | ||
1371 | return self.__api_request('GET', '/api/v2/search', params) | 1413 | return self.__api_request('GET', '/api/v2/search', params) |
1372 | 1414 | ||
1373 | ### | 1415 | ### |
1374 | # Reading data: Trends | 1416 | # Reading data: Trends |
1375 | ### | 1417 | ### |
1376 | @api_version("2.4.3", "3.0.0", __DICT_VERSION_HASHTAG) | 1418 | @api_version("2.4.3", "3.0.0", __DICT_VERSION_HASHTAG) |
1377 | def trends(self, limit = None): | 1419 | def trends(self, limit=None): |
1378 | """ | 1420 | """ |
1379 | Fetch trending-hashtag information, if the instance provides such information. | 1421 | Fetch trending-hashtag information, if the instance provides such information. |
1380 | 1422 | ||
1381 | Specify `limit` to limit how many results are returned (the maximum number | 1423 | Specify `limit` to limit how many results are returned (the maximum number |
1382 | of results is 10, the endpoint is not paginated). | 1424 | of results is 10, the endpoint is not paginated). |
1383 | 1425 | ||
1384 | Does not require authentication unless locked down by the administrator. | 1426 | Does not require authentication unless locked down by the administrator. |
1385 | 1427 | ||
1386 | Important versioning note: This endpoint does not exist for Mastodon versions | 1428 | Important versioning note: This endpoint does not exist for Mastodon versions |
1387 | between 2.8.0 (inclusive) and 3.0.0 (exclusive). | 1429 | between 2.8.0 (inclusive) and 3.0.0 (exclusive). |
1388 | 1430 | ||
1389 | Returns a list of `hashtag dicts`_, sorted by the instances trending algorithm, | 1431 | Returns a list of `hashtag dicts`_, sorted by the instance's trending algorithm, |
1390 | descending. | 1432 | descending. |
1391 | """ | 1433 | """ |
1392 | params = self.__generate_params(locals()) | 1434 | params = self.__generate_params(locals()) |
@@ -1399,7 +1441,7 @@ class Mastodon: | |||
1399 | def lists(self): | 1441 | def lists(self): |
1400 | """ | 1442 | """ |
1401 | Fetch a list of all the Lists by the logged-in user. | 1443 | Fetch a list of all the Lists by the logged-in user. |
1402 | 1444 | ||
1403 | Returns a list of `list dicts`_. | 1445 | Returns a list of `list dicts`_. |
1404 | """ | 1446 | """ |
1405 | return self.__api_request('GET', '/api/v1/lists') | 1447 | return self.__api_request('GET', '/api/v1/lists') |
@@ -1408,37 +1450,37 @@ class Mastodon: | |||
1408 | def list(self, id): | 1450 | def list(self, id): |
1409 | """ | 1451 | """ |
1410 | Fetch info about a specific list. | 1452 | Fetch info about a specific list. |
1411 | 1453 | ||
1412 | Returns a `list dict`_. | 1454 | Returns a `list dict`_. |
1413 | """ | 1455 | """ |
1414 | id = self.__unpack_id(id) | 1456 | id = self.__unpack_id(id) |
1415 | return self.__api_request('GET', '/api/v1/lists/{0}'.format(id)) | 1457 | return self.__api_request('GET', '/api/v1/lists/{0}'.format(id)) |
1416 | 1458 | ||
1417 | @api_version("2.1.0", "2.6.0", __DICT_VERSION_ACCOUNT) | 1459 | @api_version("2.1.0", "2.6.0", __DICT_VERSION_ACCOUNT) |
1418 | def list_accounts(self, id, max_id=None, min_id=None, since_id=None, limit=None): | 1460 | def list_accounts(self, id, max_id=None, min_id=None, since_id=None, limit=None): |
1419 | """ | 1461 | """ |
1420 | Get the accounts that are on the given list. | 1462 | Get the accounts that are on the given list. |
1421 | 1463 | ||
1422 | Returns a list of `user dicts`_. | 1464 | Returns a list of `user dicts`_. |
1423 | """ | 1465 | """ |
1424 | id = self.__unpack_id(id) | 1466 | id = self.__unpack_id(id) |
1425 | 1467 | ||
1426 | if max_id != None: | 1468 | if max_id != None: |
1427 | max_id = self.__unpack_id(max_id, dateconv=True) | 1469 | max_id = self.__unpack_id(max_id, dateconv=True) |
1428 | 1470 | ||
1429 | if min_id != None: | 1471 | if min_id != None: |
1430 | min_id = self.__unpack_id(min_id, dateconv=True) | 1472 | min_id = self.__unpack_id(min_id, dateconv=True) |
1431 | 1473 | ||
1432 | if since_id != None: | 1474 | if since_id != None: |
1433 | since_id = self.__unpack_id(since_id, dateconv=True) | 1475 | since_id = self.__unpack_id(since_id, dateconv=True) |
1434 | 1476 | ||
1435 | params = self.__generate_params(locals(), ['id']) | 1477 | params = self.__generate_params(locals(), ['id']) |
1436 | return self.__api_request('GET', '/api/v1/lists/{0}/accounts'.format(id)) | 1478 | return self.__api_request('GET', '/api/v1/lists/{0}/accounts'.format(id)) |
1437 | 1479 | ||
1438 | ### | 1480 | ### |
1439 | # Reading data: Mutes and Blocks | 1481 | # Reading data: Mutes and Blocks |
1440 | ### | 1482 | ### |
1441 | @api_version("1.1.0", "2.6.0", __DICT_VERSION_ACCOUNT) | 1483 | @api_version("1.1.0", "2.6.0", __DICT_VERSION_ACCOUNT) |
1442 | def mutes(self, max_id=None, min_id=None, since_id=None, limit=None): | 1484 | def mutes(self, max_id=None, min_id=None, since_id=None, limit=None): |
1443 | """ | 1485 | """ |
1444 | Fetch a list of users muted by the logged-in user. | 1486 | Fetch a list of users muted by the logged-in user. |
@@ -1447,13 +1489,13 @@ class Mastodon: | |||
1447 | """ | 1489 | """ |
1448 | if max_id != None: | 1490 | if max_id != None: |
1449 | max_id = self.__unpack_id(max_id, dateconv=True) | 1491 | max_id = self.__unpack_id(max_id, dateconv=True) |
1450 | 1492 | ||
1451 | if min_id != None: | 1493 | if min_id != None: |
1452 | min_id = self.__unpack_id(min_id, dateconv=True) | 1494 | min_id = self.__unpack_id(min_id, dateconv=True) |
1453 | 1495 | ||
1454 | if since_id != None: | 1496 | if since_id != None: |
1455 | since_id = self.__unpack_id(since_id, dateconv=True) | 1497 | since_id = self.__unpack_id(since_id, dateconv=True) |
1456 | 1498 | ||
1457 | params = self.__generate_params(locals()) | 1499 | params = self.__generate_params(locals()) |
1458 | return self.__api_request('GET', '/api/v1/mutes', params) | 1500 | return self.__api_request('GET', '/api/v1/mutes', params) |
1459 | 1501 | ||
@@ -1466,13 +1508,13 @@ class Mastodon: | |||
1466 | """ | 1508 | """ |
1467 | if max_id != None: | 1509 | if max_id != None: |
1468 | max_id = self.__unpack_id(max_id, dateconv=True) | 1510 | max_id = self.__unpack_id(max_id, dateconv=True) |
1469 | 1511 | ||
1470 | if min_id != None: | 1512 | if min_id != None: |
1471 | min_id = self.__unpack_id(min_id, dateconv=True) | 1513 | min_id = self.__unpack_id(min_id, dateconv=True) |
1472 | 1514 | ||
1473 | if since_id != None: | 1515 | if since_id != None: |
1474 | since_id = self.__unpack_id(since_id, dateconv=True) | 1516 | since_id = self.__unpack_id(since_id, dateconv=True) |
1475 | 1517 | ||
1476 | params = self.__generate_params(locals()) | 1518 | params = self.__generate_params(locals()) |
1477 | return self.__api_request('GET', '/api/v1/blocks', params) | 1519 | return self.__api_request('GET', '/api/v1/blocks', params) |
1478 | 1520 | ||
@@ -1485,9 +1527,9 @@ class Mastodon: | |||
1485 | Fetch a list of reports made by the logged-in user. | 1527 | Fetch a list of reports made by the logged-in user. |
1486 | 1528 | ||
1487 | Returns a list of `report dicts`_. | 1529 | Returns a list of `report dicts`_. |
1488 | 1530 | ||
1489 | Warning: This method has now finally been removed, and will not | 1531 | Warning: This method has now finally been removed, and will not |
1490 | work on mastodon versions 2.5.0 and above. | 1532 | work on Mastodon versions 2.5.0 and above. |
1491 | """ | 1533 | """ |
1492 | return self.__api_request('GET', '/api/v1/reports') | 1534 | return self.__api_request('GET', '/api/v1/reports') |
1493 | 1535 | ||
@@ -1503,13 +1545,13 @@ class Mastodon: | |||
1503 | """ | 1545 | """ |
1504 | if max_id != None: | 1546 | if max_id != None: |
1505 | max_id = self.__unpack_id(max_id, dateconv=True) | 1547 | max_id = self.__unpack_id(max_id, dateconv=True) |
1506 | 1548 | ||
1507 | if min_id != None: | 1549 | if min_id != None: |
1508 | min_id = self.__unpack_id(min_id, dateconv=True) | 1550 | min_id = self.__unpack_id(min_id, dateconv=True) |
1509 | 1551 | ||
1510 | if since_id != None: | 1552 | if since_id != None: |
1511 | since_id = self.__unpack_id(since_id, dateconv=True) | 1553 | since_id = self.__unpack_id(since_id, dateconv=True) |
1512 | 1554 | ||
1513 | params = self.__generate_params(locals()) | 1555 | params = self.__generate_params(locals()) |
1514 | return self.__api_request('GET', '/api/v1/favourites', params) | 1556 | return self.__api_request('GET', '/api/v1/favourites', params) |
1515 | 1557 | ||
@@ -1525,13 +1567,13 @@ class Mastodon: | |||
1525 | """ | 1567 | """ |
1526 | if max_id != None: | 1568 | if max_id != None: |
1527 | max_id = self.__unpack_id(max_id, dateconv=True) | 1569 | max_id = self.__unpack_id(max_id, dateconv=True) |
1528 | 1570 | ||
1529 | if min_id != None: | 1571 | if min_id != None: |
1530 | min_id = self.__unpack_id(min_id, dateconv=True) | 1572 | min_id = self.__unpack_id(min_id, dateconv=True) |
1531 | 1573 | ||
1532 | if since_id != None: | 1574 | if since_id != None: |
1533 | since_id = self.__unpack_id(since_id, dateconv=True) | 1575 | since_id = self.__unpack_id(since_id, dateconv=True) |
1534 | 1576 | ||
1535 | params = self.__generate_params(locals()) | 1577 | params = self.__generate_params(locals()) |
1536 | return self.__api_request('GET', '/api/v1/follow_requests', params) | 1578 | return self.__api_request('GET', '/api/v1/follow_requests', params) |
1537 | 1579 | ||
@@ -1547,13 +1589,13 @@ class Mastodon: | |||
1547 | """ | 1589 | """ |
1548 | if max_id != None: | 1590 | if max_id != None: |
1549 | max_id = self.__unpack_id(max_id, dateconv=True) | 1591 | max_id = self.__unpack_id(max_id, dateconv=True) |
1550 | 1592 | ||
1551 | if min_id != None: | 1593 | if min_id != None: |
1552 | min_id = self.__unpack_id(min_id, dateconv=True) | 1594 | min_id = self.__unpack_id(min_id, dateconv=True) |
1553 | 1595 | ||
1554 | if since_id != None: | 1596 | if since_id != None: |
1555 | since_id = self.__unpack_id(since_id, dateconv=True) | 1597 | since_id = self.__unpack_id(since_id, dateconv=True) |
1556 | 1598 | ||
1557 | params = self.__generate_params(locals()) | 1599 | params = self.__generate_params(locals()) |
1558 | return self.__api_request('GET', '/api/v1/domain_blocks', params) | 1600 | return self.__api_request('GET', '/api/v1/domain_blocks', params) |
1559 | 1601 | ||
@@ -1568,7 +1610,6 @@ class Mastodon: | |||
1568 | Does not require authentication unless locked down by the administrator. | 1610 | Does not require authentication unless locked down by the administrator. |
1569 | 1611 | ||
1570 | Returns a list of `emoji dicts`_. | 1612 | Returns a list of `emoji dicts`_. |
1571 | |||
1572 | """ | 1613 | """ |
1573 | return self.__api_request('GET', '/api/v1/custom_emojis') | 1614 | return self.__api_request('GET', '/api/v1/custom_emojis') |
1574 | 1615 | ||
@@ -1581,7 +1622,6 @@ class Mastodon: | |||
1581 | Fetch information about the current application. | 1622 | Fetch information about the current application. |
1582 | 1623 | ||
1583 | Returns an `application dict`_. | 1624 | Returns an `application dict`_. |
1584 | |||
1585 | """ | 1625 | """ |
1586 | return self.__api_request('GET', '/api/v1/apps/verify_credentials') | 1626 | return self.__api_request('GET', '/api/v1/apps/verify_credentials') |
1587 | 1627 | ||
@@ -1594,7 +1634,6 @@ class Mastodon: | |||
1594 | Fetch the current push subscription the logged-in user has for this app. | 1634 | Fetch the current push subscription the logged-in user has for this app. |
1595 | 1635 | ||
1596 | Returns a `push subscription dict`_. | 1636 | Returns a `push subscription dict`_. |
1597 | |||
1598 | """ | 1637 | """ |
1599 | return self.__api_request('GET', '/api/v1/push/subscription') | 1638 | return self.__api_request('GET', '/api/v1/push/subscription') |
1600 | 1639 | ||
@@ -1604,28 +1643,27 @@ class Mastodon: | |||
1604 | @api_version("2.8.0", "2.8.0", __DICT_VERSION_PREFERENCES) | 1643 | @api_version("2.8.0", "2.8.0", __DICT_VERSION_PREFERENCES) |
1605 | def preferences(self): | 1644 | def preferences(self): |
1606 | """ | 1645 | """ |
1607 | Fetch the users preferences, which can be used to set some default options. | 1646 | Fetch the user's preferences, which can be used to set some default options. |
1608 | As of 2.8.0, apps can only fetch, not update preferences. | 1647 | As of 2.8.0, apps can only fetch, not update preferences. |
1609 | 1648 | ||
1610 | Returns a `preference dict`_. | 1649 | Returns a `preference dict`_. |
1611 | |||
1612 | """ | 1650 | """ |
1613 | return self.__api_request('GET', '/api/v1/preferences') | 1651 | return self.__api_request('GET', '/api/v1/preferences') |
1614 | 1652 | ||
1615 | ## | 1653 | ## |
1616 | # Reading data: Announcements | 1654 | # Reading data: Announcements |
1617 | ## | 1655 | ## |
1618 | 1656 | ||
1619 | #/api/v1/announcements | 1657 | # /api/v1/announcements |
1620 | @api_version("3.1.0", "3.1.0", __DICT_VERSION_ANNOUNCEMENT) | 1658 | @api_version("3.1.0", "3.1.0", __DICT_VERSION_ANNOUNCEMENT) |
1621 | def announcements(self): | 1659 | def announcements(self): |
1622 | """ | 1660 | """ |
1623 | Fetch currently active annoucements. | 1661 | Fetch currently active announcements. |
1624 | 1662 | ||
1625 | Returns a list of `annoucement dicts`_. | 1663 | Returns a list of `announcement dicts`_. |
1626 | """ | 1664 | """ |
1627 | return self.__api_request('GET', '/api/v1/announcements') | 1665 | return self.__api_request('GET', '/api/v1/announcements') |
1628 | 1666 | ||
1629 | ## | 1667 | ## |
1630 | # Reading data: Read markers | 1668 | # Reading data: Read markers |
1631 | ## | 1669 | ## |
@@ -1634,15 +1672,15 @@ class Mastodon: | |||
1634 | """ | 1672 | """ |
1635 | Get the last-read-location markers for the specified timelines. Valid timelines | 1673 | Get the last-read-location markers for the specified timelines. Valid timelines |
1636 | are the same as in `timeline()`_ | 1674 | are the same as in `timeline()`_ |
1637 | 1675 | ||
1638 | Note that despite the singular name, `timeline` can be a list. | 1676 | Note that despite the singular name, `timeline` can be a list. |
1639 | 1677 | ||
1640 | Returns a dict of `read marker dicts`_, keyed by timeline name. | 1678 | Returns a dict of `read marker dicts`_, keyed by timeline name. |
1641 | """ | 1679 | """ |
1642 | if not isinstance(timeline, (list, tuple)): | 1680 | if not isinstance(timeline, (list, tuple)): |
1643 | timeline = [timeline] | 1681 | timeline = [timeline] |
1644 | params = self.__generate_params(locals()) | 1682 | params = self.__generate_params(locals()) |
1645 | 1683 | ||
1646 | return self.__api_request('GET', '/api/v1/markers', params) | 1684 | return self.__api_request('GET', '/api/v1/markers', params) |
1647 | 1685 | ||
1648 | ### | 1686 | ### |
@@ -1652,7 +1690,7 @@ class Mastodon: | |||
1652 | def bookmarks(self, max_id=None, min_id=None, since_id=None, limit=None): | 1690 | def bookmarks(self, max_id=None, min_id=None, since_id=None, limit=None): |
1653 | """ | 1691 | """ |
1654 | Get a list of statuses bookmarked by the logged-in user. | 1692 | Get a list of statuses bookmarked by the logged-in user. |
1655 | 1693 | ||
1656 | Returns a list of `toot dicts`_. | 1694 | Returns a list of `toot dicts`_. |
1657 | """ | 1695 | """ |
1658 | if max_id != None: | 1696 | if max_id != None: |
@@ -1666,7 +1704,7 @@ class Mastodon: | |||
1666 | 1704 | ||
1667 | params = self.__generate_params(locals()) | 1705 | params = self.__generate_params(locals()) |
1668 | return self.__api_request('GET', '/api/v1/bookmarks', params) | 1706 | return self.__api_request('GET', '/api/v1/bookmarks', params) |
1669 | 1707 | ||
1670 | ### | 1708 | ### |
1671 | # Writing data: Statuses | 1709 | # Writing data: Statuses |
1672 | ### | 1710 | ### |
@@ -1678,10 +1716,10 @@ class Mastodon: | |||
1678 | """ | 1716 | """ |
1679 | Post a status. Can optionally be in reply to another status and contain | 1717 | Post a status. Can optionally be in reply to another status and contain |
1680 | media. | 1718 | media. |
1681 | 1719 | ||
1682 | `media_ids` should be a list. (If it's not, the function will turn it | 1720 | `media_ids` should be a list. (If it's not, the function will turn it |
1683 | into one.) It can contain up to four pieces of media (uploaded via | 1721 | into one.) It can contain up to four pieces of media (uploaded via |
1684 | `media_post()`_). `media_ids` can also be the `media dicts`_ returned | 1722 | `media_post()`_). `media_ids` can also be the `media dicts`_ returned |
1685 | by `media_post()`_ - they are unpacked automatically. | 1723 | by `media_post()`_ - they are unpacked automatically. |
1686 | 1724 | ||
1687 | The `sensitive` boolean decides whether or not media attached to the post | 1725 | The `sensitive` boolean decides whether or not media attached to the post |
@@ -1717,44 +1755,48 @@ class Mastodon: | |||
1717 | 1755 | ||
1718 | Pass `poll` to attach a poll to the status. An appropriate object can be | 1756 | Pass `poll` to attach a poll to the status. An appropriate object can be |
1719 | constructed using `make_poll()`_ . Note that as of Mastodon version | 1757 | constructed using `make_poll()`_ . Note that as of Mastodon version |
1720 | 2.8.2, you can only have either media or a poll attached, not both at | 1758 | 2.8.2, you can only have either media or a poll attached, not both at |
1721 | the same time. | 1759 | the same time. |
1722 | 1760 | ||
1723 | **Specific to `pleroma` feature set:**: Specify `content_type` to set | 1761 | **Specific to "pleroma" feature set:**: Specify `content_type` to set |
1724 | the content type of your post on Pleroma. It accepts 'text/plain' (default), | 1762 | the content type of your post on Pleroma. It accepts 'text/plain' (default), |
1725 | 'text/markdown', 'text/html' and 'text/bbcode. This parameter is not | 1763 | 'text/markdown', 'text/html' and 'text/bbcode'. This parameter is not |
1726 | supported on Mastodon servers, but will be safely ignored if set. | 1764 | supported on Mastodon servers, but will be safely ignored if set. |
1727 | 1765 | ||
1728 | **Specific to `fedibird` feature set:**: The `quote_id` parameter is | 1766 | **Specific to "fedibird" feature set:**: The `quote_id` parameter is |
1729 | a non-standard extension that specifies the id of a quoted status. | 1767 | a non-standard extension that specifies the id of a quoted status. |
1730 | 1768 | ||
1731 | Returns a `toot dict`_ with the new status. | 1769 | Returns a `toot dict`_ with the new status. |
1732 | """ | 1770 | """ |
1733 | if quote_id != None: | 1771 | if quote_id != None: |
1734 | if self.feature_set != "fedibird": | 1772 | if self.feature_set != "fedibird": |
1735 | raise MastodonIllegalArgumentError('quote_id is only available with feature set fedibird') | 1773 | raise MastodonIllegalArgumentError( |
1774 | 'quote_id is only available with feature set fedibird') | ||
1736 | quote_id = self.__unpack_id(quote_id) | 1775 | quote_id = self.__unpack_id(quote_id) |
1737 | 1776 | ||
1738 | if content_type != None: | 1777 | if content_type != None: |
1739 | if self.feature_set != "pleroma": | 1778 | if self.feature_set != "pleroma": |
1740 | raise MastodonIllegalArgumentError('content_type is only available with feature set pleroma') | 1779 | raise MastodonIllegalArgumentError( |
1780 | 'content_type is only available with feature set pleroma') | ||
1741 | # It would be better to read this from nodeinfo and cache, but this is easier | 1781 | # It would be better to read this from nodeinfo and cache, but this is easier |
1742 | if not content_type in ["text/plain", "text/html", "text/markdown", "text/bbcode"]: | 1782 | if not content_type in ["text/plain", "text/html", "text/markdown", "text/bbcode"]: |
1743 | raise MastodonIllegalArgumentError('Invalid content type specified') | 1783 | raise MastodonIllegalArgumentError( |
1744 | 1784 | 'Invalid content type specified') | |
1785 | |||
1745 | if in_reply_to_id != None: | 1786 | if in_reply_to_id != None: |
1746 | in_reply_to_id = self.__unpack_id(in_reply_to_id) | 1787 | in_reply_to_id = self.__unpack_id(in_reply_to_id) |
1747 | 1788 | ||
1748 | if scheduled_at != None: | 1789 | if scheduled_at != None: |
1749 | scheduled_at = self.__consistent_isoformat_utc(scheduled_at) | 1790 | scheduled_at = self.__consistent_isoformat_utc(scheduled_at) |
1750 | 1791 | ||
1751 | params_initial = locals() | 1792 | params_initial = locals() |
1752 | 1793 | ||
1753 | # Validate poll/media exclusivity | 1794 | # Validate poll/media exclusivity |
1754 | if not poll is None: | 1795 | if not poll is None: |
1755 | if (not media_ids is None) and len(media_ids) != 0: | 1796 | if (not media_ids is None) and len(media_ids) != 0: |
1756 | raise ValueError('Status can have media or poll attached - not both.') | 1797 | raise ValueError( |
1757 | 1798 | 'Status can have media or poll attached - not both.') | |
1799 | |||
1758 | # Validate visibility parameter | 1800 | # Validate visibility parameter |
1759 | valid_visibilities = ['private', 'public', 'unlisted', 'direct'] | 1801 | valid_visibilities = ['private', 'public', 'unlisted', 'direct'] |
1760 | if params_initial['visibility'] == None: | 1802 | if params_initial['visibility'] == None: |
@@ -1763,7 +1805,7 @@ class Mastodon: | |||
1763 | params_initial['visibility'] = params_initial['visibility'].lower() | 1805 | params_initial['visibility'] = params_initial['visibility'].lower() |
1764 | if params_initial['visibility'] not in valid_visibilities: | 1806 | if params_initial['visibility'] not in valid_visibilities: |
1765 | raise ValueError('Invalid visibility value! Acceptable ' | 1807 | raise ValueError('Invalid visibility value! Acceptable ' |
1766 | 'values are %s' % valid_visibilities) | 1808 | 'values are %s' % valid_visibilities) |
1767 | 1809 | ||
1768 | if params_initial['language'] == None: | 1810 | if params_initial['language'] == None: |
1769 | del params_initial['language'] | 1811 | del params_initial['language'] |
@@ -1774,7 +1816,7 @@ class Mastodon: | |||
1774 | headers = {} | 1816 | headers = {} |
1775 | if idempotency_key != None: | 1817 | if idempotency_key != None: |
1776 | headers['Idempotency-Key'] = idempotency_key | 1818 | headers['Idempotency-Key'] = idempotency_key |
1777 | 1819 | ||
1778 | if media_ids is not None: | 1820 | if media_ids is not None: |
1779 | try: | 1821 | try: |
1780 | media_ids_proper = [] | 1822 | media_ids_proper = [] |
@@ -1796,7 +1838,7 @@ class Mastodon: | |||
1796 | use_json = True | 1838 | use_json = True |
1797 | 1839 | ||
1798 | params = self.__generate_params(params_initial, ['idempotency_key']) | 1840 | params = self.__generate_params(params_initial, ['idempotency_key']) |
1799 | return self.__api_request('POST', '/api/v1/statuses', params, headers = headers, use_json = use_json) | 1841 | return self.__api_request('POST', '/api/v1/statuses', params, headers=headers, use_json=use_json) |
1800 | 1842 | ||
1801 | @api_version("1.0.0", "2.8.0", __DICT_VERSION_STATUS) | 1843 | @api_version("1.0.0", "2.8.0", __DICT_VERSION_STATUS) |
1802 | def toot(self, status): | 1844 | def toot(self, status): |
@@ -1811,14 +1853,14 @@ class Mastodon: | |||
1811 | 1853 | ||
1812 | @api_version("1.0.0", "2.8.0", __DICT_VERSION_STATUS) | 1854 | @api_version("1.0.0", "2.8.0", __DICT_VERSION_STATUS) |
1813 | def status_reply(self, to_status, status, in_reply_to_id=None, media_ids=None, | 1855 | def status_reply(self, to_status, status, in_reply_to_id=None, media_ids=None, |
1814 | sensitive=False, visibility=None, spoiler_text=None, | 1856 | sensitive=False, visibility=None, spoiler_text=None, |
1815 | language=None, idempotency_key=None, content_type=None, | 1857 | language=None, idempotency_key=None, content_type=None, |
1816 | scheduled_at=None, poll=None, untag=False): | 1858 | scheduled_at=None, poll=None, untag=False): |
1817 | """ | 1859 | """ |
1818 | Helper function - acts like status_post, but prepends the name of all | 1860 | Helper function - acts like status_post, but prepends the name of all |
1819 | the users that are being replied to to the status text and retains | 1861 | the users that are being replied to to the status text and retains |
1820 | CW and visibility if not explicitly overridden. | 1862 | CW and visibility if not explicitly overridden. |
1821 | 1863 | ||
1822 | Set `untag` to True if you want the reply to only go to the user you | 1864 | Set `untag` to True if you want the reply to only go to the user you |
1823 | are replying to, removing every other mentioned user from the | 1865 | are replying to, removing every other mentioned user from the |
1824 | conversation. | 1866 | conversation. |
@@ -1827,52 +1869,53 @@ class Mastodon: | |||
1827 | del keyword_args["self"] | 1869 | del keyword_args["self"] |
1828 | del keyword_args["to_status"] | 1870 | del keyword_args["to_status"] |
1829 | del keyword_args["untag"] | 1871 | del keyword_args["untag"] |
1830 | 1872 | ||
1831 | user_id = self.__get_logged_in_id() | 1873 | user_id = self.__get_logged_in_id() |
1832 | 1874 | ||
1833 | # Determine users to mention | 1875 | # Determine users to mention |
1834 | mentioned_accounts = collections.OrderedDict() | 1876 | mentioned_accounts = collections.OrderedDict() |
1835 | mentioned_accounts[to_status.account.id] = to_status.account.acct | 1877 | mentioned_accounts[to_status.account.id] = to_status.account.acct |
1836 | 1878 | ||
1837 | if not untag: | 1879 | if not untag: |
1838 | for account in to_status.mentions: | 1880 | for account in to_status.mentions: |
1839 | if account.id != user_id and not account.id in mentioned_accounts.keys(): | 1881 | if account.id != user_id and not account.id in mentioned_accounts.keys(): |
1840 | mentioned_accounts[account.id] = account.acct | 1882 | mentioned_accounts[account.id] = account.acct |
1841 | 1883 | ||
1842 | # Join into one piece of text. The space is added inside because of self-replies. | 1884 | # Join into one piece of text. The space is added inside because of self-replies. |
1843 | status = "".join(map(lambda x: "@" + x + " ", mentioned_accounts.values())) + status | 1885 | status = "".join(map(lambda x: "@" + x + " ", |
1844 | 1886 | mentioned_accounts.values())) + status | |
1887 | |||
1845 | # Retain visibility / cw | 1888 | # Retain visibility / cw |
1846 | if visibility == None and 'visibility' in to_status: | 1889 | if visibility == None and 'visibility' in to_status: |
1847 | visibility = to_status.visibility | 1890 | visibility = to_status.visibility |
1848 | if spoiler_text == None and 'spoiler_text' in to_status: | 1891 | if spoiler_text == None and 'spoiler_text' in to_status: |
1849 | spoiler_text = to_status.spoiler_text | 1892 | spoiler_text = to_status.spoiler_text |
1850 | 1893 | ||
1851 | keyword_args["status"] = status | 1894 | keyword_args["status"] = status |
1852 | keyword_args["visibility"] = visibility | 1895 | keyword_args["visibility"] = visibility |
1853 | keyword_args["spoiler_text"] = spoiler_text | 1896 | keyword_args["spoiler_text"] = spoiler_text |
1854 | keyword_args["in_reply_to_id"] = to_status.id | 1897 | keyword_args["in_reply_to_id"] = to_status.id |
1855 | return self.status_post(**keyword_args) | 1898 | return self.status_post(**keyword_args) |
1856 | 1899 | ||
1857 | @api_version("2.8.0", "2.8.0", __DICT_VERSION_POLL) | 1900 | @api_version("2.8.0", "2.8.0", __DICT_VERSION_POLL) |
1858 | def make_poll(self, options, expires_in, multiple=False, hide_totals=False): | 1901 | def make_poll(self, options, expires_in, multiple=False, hide_totals=False): |
1859 | """ | 1902 | """ |
1860 | Generate a poll object that can be passed as the `poll` option when posting a status. | 1903 | Generate a poll object that can be passed as the `poll` option when posting a status. |
1861 | 1904 | ||
1862 | options is an array of strings with the poll options (Maximum, by default: 4), | 1905 | options is an array of strings with the poll options (Maximum, by default: 4), |
1863 | expires_in is the time in seconds for which the poll should be open. | 1906 | expires_in is the time in seconds for which the poll should be open. |
1864 | Set multiple to True to allow people to choose more than one answer. Set | 1907 | Set multiple to True to allow people to choose more than one answer. Set |
1865 | hide_totals to True to hide the results of the poll until it has expired. | 1908 | hide_totals to True to hide the results of the poll until it has expired. |
1866 | """ | 1909 | """ |
1867 | poll_params = locals() | 1910 | poll_params = locals() |
1868 | del poll_params["self"] | 1911 | del poll_params["self"] |
1869 | return poll_params | 1912 | return poll_params |
1870 | 1913 | ||
1871 | @api_version("1.0.0", "1.0.0", "1.0.0") | 1914 | @api_version("1.0.0", "1.0.0", "1.0.0") |
1872 | def status_delete(self, id): | 1915 | def status_delete(self, id): |
1873 | """ | 1916 | """ |
1874 | Delete a status | 1917 | Delete a status |
1875 | 1918 | ||
1876 | Returns the now-deleted status, with an added "source" attribute that contains | 1919 | Returns the now-deleted status, with an added "source" attribute that contains |
1877 | the text that was used to compose this status (this can be used to power | 1920 | the text that was used to compose this status (this can be used to power |
1878 | "delete and redraft" functionality) | 1921 | "delete and redraft" functionality) |
@@ -1885,7 +1928,7 @@ class Mastodon: | |||
1885 | def status_reblog(self, id, visibility=None): | 1928 | def status_reblog(self, id, visibility=None): |
1886 | """ | 1929 | """ |
1887 | Reblog / boost a status. | 1930 | Reblog / boost a status. |
1888 | 1931 | ||
1889 | The visibility parameter functions the same as in `status_post()`_ and | 1932 | The visibility parameter functions the same as in `status_post()`_ and |
1890 | allows you to reduce the visibility of a reblogged status. | 1933 | allows you to reduce the visibility of a reblogged status. |
1891 | 1934 | ||
@@ -1897,8 +1940,8 @@ class Mastodon: | |||
1897 | params['visibility'] = params['visibility'].lower() | 1940 | params['visibility'] = params['visibility'].lower() |
1898 | if params['visibility'] not in valid_visibilities: | 1941 | if params['visibility'] not in valid_visibilities: |
1899 | raise ValueError('Invalid visibility value! Acceptable ' | 1942 | raise ValueError('Invalid visibility value! Acceptable ' |
1900 | 'values are %s' % valid_visibilities) | 1943 | 'values are %s' % valid_visibilities) |
1901 | 1944 | ||
1902 | id = self.__unpack_id(id) | 1945 | id = self.__unpack_id(id) |
1903 | url = '/api/v1/statuses/{0}/reblog'.format(str(id)) | 1946 | url = '/api/v1/statuses/{0}/reblog'.format(str(id)) |
1904 | return self.__api_request('POST', url, params) | 1947 | return self.__api_request('POST', url, params) |
@@ -1935,7 +1978,7 @@ class Mastodon: | |||
1935 | id = self.__unpack_id(id) | 1978 | id = self.__unpack_id(id) |
1936 | url = '/api/v1/statuses/{0}/unfavourite'.format(str(id)) | 1979 | url = '/api/v1/statuses/{0}/unfavourite'.format(str(id)) |
1937 | return self.__api_request('POST', url) | 1980 | return self.__api_request('POST', url) |
1938 | 1981 | ||
1939 | @api_version("1.4.0", "2.0.0", __DICT_VERSION_STATUS) | 1982 | @api_version("1.4.0", "2.0.0", __DICT_VERSION_STATUS) |
1940 | def status_mute(self, id): | 1983 | def status_mute(self, id): |
1941 | """ | 1984 | """ |
@@ -1979,8 +2022,7 @@ class Mastodon: | |||
1979 | id = self.__unpack_id(id) | 2022 | id = self.__unpack_id(id) |
1980 | url = '/api/v1/statuses/{0}/unpin'.format(str(id)) | 2023 | url = '/api/v1/statuses/{0}/unpin'.format(str(id)) |
1981 | return self.__api_request('POST', url) | 2024 | return self.__api_request('POST', url) |
1982 | 2025 | ||
1983 | |||
1984 | @api_version("3.1.0", "3.1.0", __DICT_VERSION_STATUS) | 2026 | @api_version("3.1.0", "3.1.0", __DICT_VERSION_STATUS) |
1985 | def status_bookmark(self, id): | 2027 | def status_bookmark(self, id): |
1986 | """ | 2028 | """ |
@@ -2010,9 +2052,9 @@ class Mastodon: | |||
2010 | def scheduled_status_update(self, id, scheduled_at): | 2052 | def scheduled_status_update(self, id, scheduled_at): |
2011 | """ | 2053 | """ |
2012 | Update the scheduled time of a scheduled status. | 2054 | Update the scheduled time of a scheduled status. |
2013 | 2055 | ||
2014 | New time must be at least 5 minutes into the future. | 2056 | New time must be at least 5 minutes into the future. |
2015 | 2057 | ||
2016 | Returns a `scheduled toot dict`_ | 2058 | Returns a `scheduled toot dict`_ |
2017 | """ | 2059 | """ |
2018 | scheduled_at = self.__consistent_isoformat_utc(scheduled_at) | 2060 | scheduled_at = self.__consistent_isoformat_utc(scheduled_at) |
@@ -2020,7 +2062,7 @@ class Mastodon: | |||
2020 | params = self.__generate_params(locals(), ['id']) | 2062 | params = self.__generate_params(locals(), ['id']) |
2021 | url = '/api/v1/scheduled_statuses/{0}'.format(str(id)) | 2063 | url = '/api/v1/scheduled_statuses/{0}'.format(str(id)) |
2022 | return self.__api_request('PUT', url, params) | 2064 | return self.__api_request('PUT', url, params) |
2023 | 2065 | ||
2024 | @api_version("2.7.0", "2.7.0", "2.7.0") | 2066 | @api_version("2.7.0", "2.7.0", "2.7.0") |
2025 | def scheduled_status_delete(self, id): | 2067 | def scheduled_status_delete(self, id): |
2026 | """ | 2068 | """ |
@@ -2029,7 +2071,7 @@ class Mastodon: | |||
2029 | id = self.__unpack_id(id) | 2071 | id = self.__unpack_id(id) |
2030 | url = '/api/v1/scheduled_statuses/{0}'.format(str(id)) | 2072 | url = '/api/v1/scheduled_statuses/{0}'.format(str(id)) |
2031 | self.__api_request('DELETE', url) | 2073 | self.__api_request('DELETE', url) |
2032 | 2074 | ||
2033 | ### | 2075 | ### |
2034 | # Writing data: Polls | 2076 | # Writing data: Polls |
2035 | ### | 2077 | ### |
@@ -2037,45 +2079,44 @@ class Mastodon: | |||
2037 | def poll_vote(self, id, choices): | 2079 | def poll_vote(self, id, choices): |
2038 | """ | 2080 | """ |
2039 | Vote in the given poll. | 2081 | Vote in the given poll. |
2040 | 2082 | ||
2041 | `choices` is the index of the choice you wish to register a vote for | 2083 | `choices` is the index of the choice you wish to register a vote for |
2042 | (i.e. its index in the corresponding polls `options` field. In case | 2084 | (i.e. its index in the corresponding polls `options` field. In case |
2043 | of a poll that allows selection of more than one option, a list of | 2085 | of a poll that allows selection of more than one option, a list of |
2044 | indices can be passed. | 2086 | indices can be passed. |
2045 | 2087 | ||
2046 | You can only submit choices for any given poll once in case of | 2088 | You can only submit choices for any given poll once in case of |
2047 | single-option polls, or only once per option in case of multi-option | 2089 | single-option polls, or only once per option in case of multi-option |
2048 | polls. | 2090 | polls. |
2049 | 2091 | ||
2050 | Returns the updated `poll dict`_ | 2092 | Returns the updated `poll dict`_ |
2051 | """ | 2093 | """ |
2052 | id = self.__unpack_id(id) | 2094 | id = self.__unpack_id(id) |
2053 | if not isinstance(choices, list): | 2095 | if not isinstance(choices, list): |
2054 | choices = [choices] | 2096 | choices = [choices] |
2055 | params = self.__generate_params(locals(), ['id']) | 2097 | params = self.__generate_params(locals(), ['id']) |
2056 | 2098 | ||
2057 | url = '/api/v1/polls/{0}/votes'.format(id) | 2099 | url = '/api/v1/polls/{0}/votes'.format(id) |
2058 | self.__api_request('POST', url, params) | 2100 | self.__api_request('POST', url, params) |
2059 | 2101 | ||
2060 | |||
2061 | ### | 2102 | ### |
2062 | # Writing data: Notifications | 2103 | # Writing data: Notifications |
2063 | ### | 2104 | ### |
2105 | |||
2064 | @api_version("1.0.0", "1.0.0", "1.0.0") | 2106 | @api_version("1.0.0", "1.0.0", "1.0.0") |
2065 | def notifications_clear(self): | 2107 | def notifications_clear(self): |
2066 | """ | 2108 | """ |
2067 | Clear out a users notifications | 2109 | Clear out a user's notifications |
2068 | """ | 2110 | """ |
2069 | self.__api_request('POST', '/api/v1/notifications/clear') | 2111 | self.__api_request('POST', '/api/v1/notifications/clear') |
2070 | 2112 | ||
2071 | |||
2072 | @api_version("1.3.0", "2.9.2", "2.9.2") | 2113 | @api_version("1.3.0", "2.9.2", "2.9.2") |
2073 | def notifications_dismiss(self, id): | 2114 | def notifications_dismiss(self, id): |
2074 | """ | 2115 | """ |
2075 | Deletes a single notification | 2116 | Deletes a single notification |
2076 | """ | 2117 | """ |
2077 | id = self.__unpack_id(id) | 2118 | id = self.__unpack_id(id) |
2078 | 2119 | ||
2079 | if self.verify_minimum_version("2.9.2"): | 2120 | if self.verify_minimum_version("2.9.2"): |
2080 | url = '/api/v1/notifications/{0}/dismiss'.format(str(id)) | 2121 | url = '/api/v1/notifications/{0}/dismiss'.format(str(id)) |
2081 | self.__api_request('POST', url) | 2122 | self.__api_request('POST', url) |
@@ -2090,7 +2131,7 @@ class Mastodon: | |||
2090 | def conversations_read(self, id): | 2131 | def conversations_read(self, id): |
2091 | """ | 2132 | """ |
2092 | Marks a single conversation as read. | 2133 | Marks a single conversation as read. |
2093 | 2134 | ||
2094 | Returns the updated `conversation dict`_. | 2135 | Returns the updated `conversation dict`_. |
2095 | """ | 2136 | """ |
2096 | id = self.__unpack_id(id) | 2137 | id = self.__unpack_id(id) |
@@ -2112,10 +2153,10 @@ class Mastodon: | |||
2112 | """ | 2153 | """ |
2113 | id = self.__unpack_id(id) | 2154 | id = self.__unpack_id(id) |
2114 | params = self.__generate_params(locals()) | 2155 | params = self.__generate_params(locals()) |
2115 | 2156 | ||
2116 | if params["reblogs"] == None: | 2157 | if params["reblogs"] == None: |
2117 | del params["reblogs"] | 2158 | del params["reblogs"] |
2118 | 2159 | ||
2119 | url = '/api/v1/accounts/{0}/follow'.format(str(id)) | 2160 | url = '/api/v1/accounts/{0}/follow'.format(str(id)) |
2120 | return self.__api_request('POST', url, params) | 2161 | return self.__api_request('POST', url, params) |
2121 | 2162 | ||
@@ -2192,8 +2233,8 @@ class Mastodon: | |||
2192 | @api_version("1.1.1", "3.1.0", __DICT_VERSION_ACCOUNT) | 2233 | @api_version("1.1.1", "3.1.0", __DICT_VERSION_ACCOUNT) |
2193 | def account_update_credentials(self, display_name=None, note=None, | 2234 | def account_update_credentials(self, display_name=None, note=None, |
2194 | avatar=None, avatar_mime_type=None, | 2235 | avatar=None, avatar_mime_type=None, |
2195 | header=None, header_mime_type=None, | 2236 | header=None, header_mime_type=None, |
2196 | locked=None, bot=None, | 2237 | locked=None, bot=None, |
2197 | discoverable=None, fields=None): | 2238 | discoverable=None, fields=None): |
2198 | """ | 2239 | """ |
2199 | Update the profile for the currently logged-in user. | 2240 | Update the profile for the currently logged-in user. |
@@ -2202,51 +2243,53 @@ class Mastodon: | |||
2202 | 2243 | ||
2203 | `avatar` and 'header' are images. As with media uploads, it is possible to either | 2244 | `avatar` and 'header' are images. As with media uploads, it is possible to either |
2204 | pass image data and a mime type, or a filename of an image file, for either. | 2245 | pass image data and a mime type, or a filename of an image file, for either. |
2205 | 2246 | ||
2206 | `locked` specifies whether the user needs to manually approve follow requests. | 2247 | `locked` specifies whether the user needs to manually approve follow requests. |
2207 | 2248 | ||
2208 | `bot` specifies whether the user should be set to a bot. | 2249 | `bot` specifies whether the user should be set to a bot. |
2209 | 2250 | ||
2210 | `discoverable` specifies whether the user should appear in the user directory. | 2251 | `discoverable` specifies whether the user should appear in the user directory. |
2211 | 2252 | ||
2212 | `fields` can be a list of up to four name-value pairs (specified as tuples) to | 2253 | `fields` can be a list of up to four name-value pairs (specified as tuples) to |
2213 | appear as semi-structured information in the users profile. | 2254 | appear as semi-structured information in the user's profile. |
2214 | 2255 | ||
2215 | Returns the updated `user dict` of the logged-in user. | 2256 | Returns the updated `user dict` of the logged-in user. |
2216 | """ | 2257 | """ |
2217 | params_initial = collections.OrderedDict(locals()) | 2258 | params_initial = collections.OrderedDict(locals()) |
2218 | 2259 | ||
2219 | # Convert fields | 2260 | # Convert fields |
2220 | if fields != None: | 2261 | if fields != None: |
2221 | if len(fields) > 4: | 2262 | if len(fields) > 4: |
2222 | raise MastodonIllegalArgumentError('A maximum of four fields are allowed.') | 2263 | raise MastodonIllegalArgumentError( |
2223 | 2264 | 'A maximum of four fields are allowed.') | |
2265 | |||
2224 | fields_attributes = [] | 2266 | fields_attributes = [] |
2225 | for idx, (field_name, field_value) in enumerate(fields): | 2267 | for idx, (field_name, field_value) in enumerate(fields): |
2226 | params_initial['fields_attributes[' + str(idx) + '][name]'] = field_name | 2268 | params_initial['fields_attributes[' + |
2227 | params_initial['fields_attributes[' + str(idx) + '][value]'] = field_value | 2269 | str(idx) + '][name]'] = field_name |
2228 | 2270 | params_initial['fields_attributes[' + | |
2271 | str(idx) + '][value]'] = field_value | ||
2272 | |||
2229 | # Clean up params | 2273 | # Clean up params |
2230 | for param in ["avatar", "avatar_mime_type", "header", "header_mime_type", "fields"]: | 2274 | for param in ["avatar", "avatar_mime_type", "header", "header_mime_type", "fields"]: |
2231 | if param in params_initial: | 2275 | if param in params_initial: |
2232 | del params_initial[param] | 2276 | del params_initial[param] |
2233 | 2277 | ||
2234 | # Create file info | 2278 | # Create file info |
2235 | files = {} | 2279 | files = {} |
2236 | if not avatar is None: | 2280 | if not avatar is None: |
2237 | files["avatar"] = self.__load_media_file(avatar, avatar_mime_type) | 2281 | files["avatar"] = self.__load_media_file(avatar, avatar_mime_type) |
2238 | if not header is None: | 2282 | if not header is None: |
2239 | files["header"] = self.__load_media_file(header, header_mime_type) | 2283 | files["header"] = self.__load_media_file(header, header_mime_type) |
2240 | 2284 | ||
2241 | params = self.__generate_params(params_initial) | 2285 | params = self.__generate_params(params_initial) |
2242 | return self.__api_request('PATCH', '/api/v1/accounts/update_credentials', params, files=files) | 2286 | return self.__api_request('PATCH', '/api/v1/accounts/update_credentials', params, files=files) |
2243 | 2287 | ||
2244 | |||
2245 | @api_version("2.5.0", "2.5.0", __DICT_VERSION_RELATIONSHIP) | 2288 | @api_version("2.5.0", "2.5.0", __DICT_VERSION_RELATIONSHIP) |
2246 | def account_pin(self, id): | 2289 | def account_pin(self, id): |
2247 | """ | 2290 | """ |
2248 | Pin / endorse a user. | 2291 | Pin / endorse a user. |
2249 | 2292 | ||
2250 | Returns a `relationship dict`_ containing the updated relationship to the user. | 2293 | Returns a `relationship dict`_ containing the updated relationship to the user. |
2251 | """ | 2294 | """ |
2252 | id = self.__unpack_id(id) | 2295 | id = self.__unpack_id(id) |
@@ -2274,34 +2317,34 @@ class Mastodon: | |||
2274 | id = self.__unpack_id(id) | 2317 | id = self.__unpack_id(id) |
2275 | params = self.__generate_params(locals(), ["id"]) | 2318 | params = self.__generate_params(locals(), ["id"]) |
2276 | return self.__api_request('POST', '/api/v1/accounts/{0}/note'.format(str(id)), params) | 2319 | return self.__api_request('POST', '/api/v1/accounts/{0}/note'.format(str(id)), params) |
2277 | 2320 | ||
2278 | @api_version("3.3.0", "3.3.0", __DICT_VERSION_HASHTAG) | 2321 | @api_version("3.3.0", "3.3.0", __DICT_VERSION_HASHTAG) |
2279 | def account_featured_tags(self, id): | 2322 | def account_featured_tags(self, id): |
2280 | """ | 2323 | """ |
2281 | Get an accounts featured hashtags. | 2324 | Get an account's featured hashtags. |
2282 | 2325 | ||
2283 | Returns a list of `hashtag dicts`_ (NOT `featured tag dicts`_). | 2326 | Returns a list of `hashtag dicts`_ (NOT `featured tag dicts`_). |
2284 | """ | 2327 | """ |
2285 | id = self.__unpack_id(id) | 2328 | id = self.__unpack_id(id) |
2286 | return self.__api_request('GET', '/api/v1/accounts/{0}/featured_tags'.format(str(id))) | 2329 | return self.__api_request('GET', '/api/v1/accounts/{0}/featured_tags'.format(str(id))) |
2287 | 2330 | ||
2288 | ### | 2331 | ### |
2289 | # Writing data: Featured hashtags | 2332 | # Writing data: Featured hashtags |
2290 | ### | 2333 | ### |
2291 | @api_version("3.0.0", "3.0.0", __DICT_VERSION_FEATURED_TAG) | 2334 | @api_version("3.0.0", "3.0.0", __DICT_VERSION_FEATURED_TAG) |
2292 | def featured_tag_create(self, name): | 2335 | def featured_tag_create(self, name): |
2293 | """ | 2336 | """ |
2294 | Creates a new featured hashtag displayed on the logged-in users profile. | 2337 | Creates a new featured hashtag displayed on the logged-in user's profile. |
2295 | 2338 | ||
2296 | Returns a `featured tag dict`_ with the newly featured tag. | 2339 | Returns a `featured tag dict`_ with the newly featured tag. |
2297 | """ | 2340 | """ |
2298 | params = self.__generate_params(locals()) | 2341 | params = self.__generate_params(locals()) |
2299 | return self.__api_request('POST', '/api/v1/featured_tags', params) | 2342 | return self.__api_request('POST', '/api/v1/featured_tags', params) |
2300 | 2343 | ||
2301 | @api_version("3.0.0", "3.0.0", __DICT_VERSION_FEATURED_TAG) | 2344 | @api_version("3.0.0", "3.0.0", __DICT_VERSION_FEATURED_TAG) |
2302 | def featured_tag_delete(self, id): | 2345 | def featured_tag_delete(self, id): |
2303 | """ | 2346 | """ |
2304 | Deletes one of the logged-in users featured hashtags. | 2347 | Deletes one of the logged-in user's featured hashtags. |
2305 | """ | 2348 | """ |
2306 | id = self.__unpack_id(id) | 2349 | id = self.__unpack_id(id) |
2307 | url = '/api/v1/featured_tags/{0}'.format(str(id)) | 2350 | url = '/api/v1/featured_tags/{0}'.format(str(id)) |
@@ -2311,44 +2354,44 @@ class Mastodon: | |||
2311 | # Writing data: Keyword filters | 2354 | # Writing data: Keyword filters |
2312 | ### | 2355 | ### |
2313 | @api_version("2.4.3", "2.4.3", __DICT_VERSION_FILTER) | 2356 | @api_version("2.4.3", "2.4.3", __DICT_VERSION_FILTER) |
2314 | def filter_create(self, phrase, context, irreversible = False, whole_word = True, expires_in = None): | 2357 | def filter_create(self, phrase, context, irreversible=False, whole_word=True, expires_in=None): |
2315 | """ | 2358 | """ |
2316 | Creates a new keyword filter. `phrase` is the phrase that should be | 2359 | Creates a new keyword filter. `phrase` is the phrase that should be |
2317 | filtered out, `context` specifies from where to filter the keywords. | 2360 | filtered out, `context` specifies from where to filter the keywords. |
2318 | Valid contexts are 'home', 'notifications', 'public' and 'thread'. | 2361 | Valid contexts are 'home', 'notifications', 'public' and 'thread'. |
2319 | 2362 | ||
2320 | Set `irreversible` to True if you want the filter to just delete statuses | 2363 | Set `irreversible` to True if you want the filter to just delete statuses |
2321 | server side. This works only for the 'home' and 'notifications' contexts. | 2364 | server side. This works only for the 'home' and 'notifications' contexts. |
2322 | 2365 | ||
2323 | Set `whole_word` to False if you want to allow filter matches to | 2366 | Set `whole_word` to False if you want to allow filter matches to |
2324 | start or end within a word, not only at word boundaries. | 2367 | start or end within a word, not only at word boundaries. |
2325 | 2368 | ||
2326 | Set `expires_in` to specify for how many seconds the filter should be | 2369 | Set `expires_in` to specify for how many seconds the filter should be |
2327 | kept around. | 2370 | kept around. |
2328 | 2371 | ||
2329 | Returns the `filter dict`_ of the newly created filter. | 2372 | Returns the `filter dict`_ of the newly created filter. |
2330 | """ | 2373 | """ |
2331 | params = self.__generate_params(locals()) | 2374 | params = self.__generate_params(locals()) |
2332 | 2375 | ||
2333 | for context_val in context: | 2376 | for context_val in context: |
2334 | if not context_val in ['home', 'notifications', 'public', 'thread']: | 2377 | if not context_val in ['home', 'notifications', 'public', 'thread']: |
2335 | raise MastodonIllegalArgumentError('Invalid filter context.') | 2378 | raise MastodonIllegalArgumentError('Invalid filter context.') |
2336 | 2379 | ||
2337 | return self.__api_request('POST', '/api/v1/filters', params) | 2380 | return self.__api_request('POST', '/api/v1/filters', params) |
2338 | 2381 | ||
2339 | @api_version("2.4.3", "2.4.3", __DICT_VERSION_FILTER) | 2382 | @api_version("2.4.3", "2.4.3", __DICT_VERSION_FILTER) |
2340 | def filter_update(self, id, phrase = None, context = None, irreversible = None, whole_word = None, expires_in = None): | 2383 | def filter_update(self, id, phrase=None, context=None, irreversible=None, whole_word=None, expires_in=None): |
2341 | """ | 2384 | """ |
2342 | Updates the filter with the given `id`. Parameters are the same | 2385 | Updates the filter with the given `id`. Parameters are the same |
2343 | as in `filter_create()`. | 2386 | as in `filter_create()`. |
2344 | 2387 | ||
2345 | Returns the `filter dict`_ of the updated filter. | 2388 | Returns the `filter dict`_ of the updated filter. |
2346 | """ | 2389 | """ |
2347 | id = self.__unpack_id(id) | 2390 | id = self.__unpack_id(id) |
2348 | params = self.__generate_params(locals(), ['id']) | 2391 | params = self.__generate_params(locals(), ['id']) |
2349 | url = '/api/v1/filters/{0}'.format(str(id)) | 2392 | url = '/api/v1/filters/{0}'.format(str(id)) |
2350 | return self.__api_request('PUT', url, params) | 2393 | return self.__api_request('PUT', url, params) |
2351 | 2394 | ||
2352 | @api_version("2.4.3", "2.4.3", "2.4.3") | 2395 | @api_version("2.4.3", "2.4.3", "2.4.3") |
2353 | def filter_delete(self, id): | 2396 | def filter_delete(self, id): |
2354 | """ | 2397 | """ |
@@ -2357,7 +2400,7 @@ class Mastodon: | |||
2357 | id = self.__unpack_id(id) | 2400 | id = self.__unpack_id(id) |
2358 | url = '/api/v1/filters/{0}'.format(str(id)) | 2401 | url = '/api/v1/filters/{0}'.format(str(id)) |
2359 | self.__api_request('DELETE', url) | 2402 | self.__api_request('DELETE', url) |
2360 | 2403 | ||
2361 | ### | 2404 | ### |
2362 | # Writing data: Follow suggestions | 2405 | # Writing data: Follow suggestions |
2363 | ### | 2406 | ### |
@@ -2377,23 +2420,23 @@ class Mastodon: | |||
2377 | def list_create(self, title): | 2420 | def list_create(self, title): |
2378 | """ | 2421 | """ |
2379 | Create a new list with the given `title`. | 2422 | Create a new list with the given `title`. |
2380 | 2423 | ||
2381 | Returns the `list dict`_ of the created list. | 2424 | Returns the `list dict`_ of the created list. |
2382 | """ | 2425 | """ |
2383 | params = self.__generate_params(locals()) | 2426 | params = self.__generate_params(locals()) |
2384 | return self.__api_request('POST', '/api/v1/lists', params) | 2427 | return self.__api_request('POST', '/api/v1/lists', params) |
2385 | 2428 | ||
2386 | @api_version("2.1.0", "2.1.0", __DICT_VERSION_LIST) | 2429 | @api_version("2.1.0", "2.1.0", __DICT_VERSION_LIST) |
2387 | def list_update(self, id, title): | 2430 | def list_update(self, id, title): |
2388 | """ | 2431 | """ |
2389 | Update info about a list, where "info" is really the lists `title`. | 2432 | Update info about a list, where "info" is really the lists `title`. |
2390 | 2433 | ||
2391 | Returns the `list dict`_ of the modified list. | 2434 | Returns the `list dict`_ of the modified list. |
2392 | """ | 2435 | """ |
2393 | id = self.__unpack_id(id) | 2436 | id = self.__unpack_id(id) |
2394 | params = self.__generate_params(locals(), ['id']) | 2437 | params = self.__generate_params(locals(), ['id']) |
2395 | return self.__api_request('PUT', '/api/v1/lists/{0}'.format(id), params) | 2438 | return self.__api_request('PUT', '/api/v1/lists/{0}'.format(id), params) |
2396 | 2439 | ||
2397 | @api_version("2.1.0", "2.1.0", "2.1.0") | 2440 | @api_version("2.1.0", "2.1.0", "2.1.0") |
2398 | def list_delete(self, id): | 2441 | def list_delete(self, id): |
2399 | """ | 2442 | """ |
@@ -2401,61 +2444,63 @@ class Mastodon: | |||
2401 | """ | 2444 | """ |
2402 | id = self.__unpack_id(id) | 2445 | id = self.__unpack_id(id) |
2403 | self.__api_request('DELETE', '/api/v1/lists/{0}'.format(id)) | 2446 | self.__api_request('DELETE', '/api/v1/lists/{0}'.format(id)) |
2404 | 2447 | ||
2405 | @api_version("2.1.0", "2.1.0", "2.1.0") | 2448 | @api_version("2.1.0", "2.1.0", "2.1.0") |
2406 | def list_accounts_add(self, id, account_ids): | 2449 | def list_accounts_add(self, id, account_ids): |
2407 | """ | 2450 | """ |
2408 | Add the account(s) given in `account_ids` to the list. | 2451 | Add the account(s) given in `account_ids` to the list. |
2409 | """ | 2452 | """ |
2410 | id = self.__unpack_id(id) | 2453 | id = self.__unpack_id(id) |
2411 | 2454 | ||
2412 | if not isinstance(account_ids, list): | 2455 | if not isinstance(account_ids, list): |
2413 | account_ids = [account_ids] | 2456 | account_ids = [account_ids] |
2414 | account_ids = list(map(lambda x: self.__unpack_id(x), account_ids)) | 2457 | account_ids = list(map(lambda x: self.__unpack_id(x), account_ids)) |
2415 | 2458 | ||
2416 | params = self.__generate_params(locals(), ['id']) | 2459 | params = self.__generate_params(locals(), ['id']) |
2417 | self.__api_request('POST', '/api/v1/lists/{0}/accounts'.format(id), params) | 2460 | self.__api_request( |
2418 | 2461 | 'POST', '/api/v1/lists/{0}/accounts'.format(id), params) | |
2462 | |||
2419 | @api_version("2.1.0", "2.1.0", "2.1.0") | 2463 | @api_version("2.1.0", "2.1.0", "2.1.0") |
2420 | def list_accounts_delete(self, id, account_ids): | 2464 | def list_accounts_delete(self, id, account_ids): |
2421 | """ | 2465 | """ |
2422 | Remove the account(s) given in `account_ids` from the list. | 2466 | Remove the account(s) given in `account_ids` from the list. |
2423 | """ | 2467 | """ |
2424 | id = self.__unpack_id(id) | 2468 | id = self.__unpack_id(id) |
2425 | 2469 | ||
2426 | if not isinstance(account_ids, list): | 2470 | if not isinstance(account_ids, list): |
2427 | account_ids = [account_ids] | 2471 | account_ids = [account_ids] |
2428 | account_ids = list(map(lambda x: self.__unpack_id(x), account_ids)) | 2472 | account_ids = list(map(lambda x: self.__unpack_id(x), account_ids)) |
2429 | 2473 | ||
2430 | params = self.__generate_params(locals(), ['id']) | 2474 | params = self.__generate_params(locals(), ['id']) |
2431 | self.__api_request('DELETE', '/api/v1/lists/{0}/accounts'.format(id), params) | 2475 | self.__api_request( |
2432 | 2476 | 'DELETE', '/api/v1/lists/{0}/accounts'.format(id), params) | |
2477 | |||
2433 | ### | 2478 | ### |
2434 | # Writing data: Reports | 2479 | # Writing data: Reports |
2435 | ### | 2480 | ### |
2436 | @api_version("1.1.0", "2.5.0", __DICT_VERSION_REPORT) | 2481 | @api_version("1.1.0", "2.5.0", __DICT_VERSION_REPORT) |
2437 | def report(self, account_id, status_ids = None, comment = None, forward = False): | 2482 | def report(self, account_id, status_ids=None, comment=None, forward=False): |
2438 | """ | 2483 | """ |
2439 | Report statuses to the instances administrators. | 2484 | Report statuses to the instances administrators. |
2440 | 2485 | ||
2441 | Accepts a list of toot IDs associated with the report, and a comment. | 2486 | Accepts a list of toot IDs associated with the report, and a comment. |
2442 | 2487 | ||
2443 | Set forward to True to forward a report of a remote user to that users | 2488 | Set forward to True to forward a report of a remote user to that users |
2444 | instance as well as sending it to the instance local administrators. | 2489 | instance as well as sending it to the instance local administrators. |
2445 | 2490 | ||
2446 | Returns a `report dict`_. | 2491 | Returns a `report dict`_. |
2447 | """ | 2492 | """ |
2448 | account_id = self.__unpack_id(account_id) | 2493 | account_id = self.__unpack_id(account_id) |
2449 | 2494 | ||
2450 | if not status_ids is None: | 2495 | if not status_ids is None: |
2451 | if not isinstance(status_ids, list): | 2496 | if not isinstance(status_ids, list): |
2452 | status_ids = [status_ids] | 2497 | status_ids = [status_ids] |
2453 | status_ids = list(map(lambda x: self.__unpack_id(x), status_ids)) | 2498 | status_ids = list(map(lambda x: self.__unpack_id(x), status_ids)) |
2454 | 2499 | ||
2455 | params_initial = locals() | 2500 | params_initial = locals() |
2456 | if forward == False: | 2501 | if forward == False: |
2457 | del params_initial['forward'] | 2502 | del params_initial['forward'] |
2458 | 2503 | ||
2459 | params = self.__generate_params(params_initial) | 2504 | params = self.__generate_params(params_initial) |
2460 | return self.__api_request('POST', '/api/v1/reports/', params) | 2505 | return self.__api_request('POST', '/api/v1/reports/', params) |
2461 | 2506 | ||
@@ -2466,7 +2511,7 @@ class Mastodon: | |||
2466 | def follow_request_authorize(self, id): | 2511 | def follow_request_authorize(self, id): |
2467 | """ | 2512 | """ |
2468 | Accept an incoming follow request. | 2513 | Accept an incoming follow request. |
2469 | 2514 | ||
2470 | Returns the updated `relationship dict`_ for the requesting account. | 2515 | Returns the updated `relationship dict`_ for the requesting account. |
2471 | """ | 2516 | """ |
2472 | id = self.__unpack_id(id) | 2517 | id = self.__unpack_id(id) |
@@ -2477,7 +2522,7 @@ class Mastodon: | |||
2477 | def follow_request_reject(self, id): | 2522 | def follow_request_reject(self, id): |
2478 | """ | 2523 | """ |
2479 | Reject an incoming follow request. | 2524 | Reject an incoming follow request. |
2480 | 2525 | ||
2481 | Returns the updated `relationship dict`_ for the requesting account. | 2526 | Returns the updated `relationship dict`_ for the requesting account. |
2482 | """ | 2527 | """ |
2483 | id = self.__unpack_id(id) | 2528 | id = self.__unpack_id(id) |
@@ -2491,9 +2536,9 @@ class Mastodon: | |||
2491 | def media_post(self, media_file, mime_type=None, description=None, focus=None, file_name=None, thumbnail=None, thumbnail_mime_type=None, synchronous=False): | 2536 | def media_post(self, media_file, mime_type=None, description=None, focus=None, file_name=None, thumbnail=None, thumbnail_mime_type=None, synchronous=False): |
2492 | """ | 2537 | """ |
2493 | Post an image, video or audio file. `media_file` can either be data or | 2538 | Post an image, video or audio file. `media_file` can either be data or |
2494 | a file name. If data is passed directly, the mime type has to be specified | 2539 | a file name. If data is passed directly, the mime type has to be specified |
2495 | manually, otherwise, it is determined from the file name. `focus` should be a tuple | 2540 | manually, otherwise, it is determined from the file name. `focus` should be a tuple |
2496 | of floats between -1 and 1, giving the x and y coordinates of the images | 2541 | of floats between -1 and 1, giving the x and y coordinates of the images |
2497 | focus point for cropping (with the origin being the images center). | 2542 | focus point for cropping (with the origin being the images center). |
2498 | 2543 | ||
2499 | Throws a `MastodonIllegalArgumentError` if the mime type of the | 2544 | Throws a `MastodonIllegalArgumentError` if the mime type of the |
@@ -2516,22 +2561,27 @@ class Mastodon: | |||
2516 | "synchronous" to emulate the old behaviour. Not recommended, inefficient | 2561 | "synchronous" to emulate the old behaviour. Not recommended, inefficient |
2517 | and deprecated, you know the deal. | 2562 | and deprecated, you know the deal. |
2518 | """ | 2563 | """ |
2519 | files = {'file': self.__load_media_file(media_file, mime_type, file_name)} | 2564 | files = {'file': self.__load_media_file( |
2565 | media_file, mime_type, file_name)} | ||
2520 | 2566 | ||
2521 | if focus != None: | 2567 | if focus != None: |
2522 | focus = str(focus[0]) + "," + str(focus[1]) | 2568 | focus = str(focus[0]) + "," + str(focus[1]) |
2523 | 2569 | ||
2524 | if not thumbnail is None: | 2570 | if not thumbnail is None: |
2525 | if not self.verify_minimum_version("3.2.0"): | 2571 | if not self.verify_minimum_version("3.2.0"): |
2526 | raise MastodonVersionError('Thumbnail requires version > 3.2.0') | 2572 | raise MastodonVersionError( |
2527 | files["thumbnail"] = self.__load_media_file(thumbnail, thumbnail_mime_type) | 2573 | 'Thumbnail requires version > 3.2.0') |
2574 | files["thumbnail"] = self.__load_media_file( | ||
2575 | thumbnail, thumbnail_mime_type) | ||
2528 | 2576 | ||
2529 | # Disambiguate URL by version | 2577 | # Disambiguate URL by version |
2530 | if self.verify_minimum_version("3.1.4"): | 2578 | if self.verify_minimum_version("3.1.4"): |
2531 | ret_dict = self.__api_request('POST', '/api/v2/media', files = files, params={'description': description, 'focus': focus}) | 2579 | ret_dict = self.__api_request( |
2580 | 'POST', '/api/v2/media', files=files, params={'description': description, 'focus': focus}) | ||
2532 | else: | 2581 | else: |
2533 | ret_dict = self.__api_request('POST', '/api/v1/media', files = files, params={'description': description, 'focus': focus}) | 2582 | ret_dict = self.__api_request( |
2534 | 2583 | 'POST', '/api/v1/media', files=files, params={'description': description, 'focus': focus}) | |
2584 | |||
2535 | # Wait for processing? | 2585 | # Wait for processing? |
2536 | if synchronous: | 2586 | if synchronous: |
2537 | if self.verify_minimum_version("3.1.4"): | 2587 | if self.verify_minimum_version("3.1.4"): |
@@ -2540,36 +2590,40 @@ class Mastodon: | |||
2540 | ret_dict = self.media(ret_dict) | 2590 | ret_dict = self.media(ret_dict) |
2541 | time.sleep(1.0) | 2591 | time.sleep(1.0) |
2542 | except: | 2592 | except: |
2543 | raise MastodonAPIError("Attachment could not be processed") | 2593 | raise MastodonAPIError( |
2594 | "Attachment could not be processed") | ||
2544 | else: | 2595 | else: |
2545 | # Old version always waits | 2596 | # Old version always waits |
2546 | return ret_dict | 2597 | return ret_dict |
2547 | 2598 | ||
2548 | return ret_dict | 2599 | return ret_dict |
2549 | 2600 | ||
2550 | @api_version("2.3.0", "3.2.0", __DICT_VERSION_MEDIA) | 2601 | @api_version("2.3.0", "3.2.0", __DICT_VERSION_MEDIA) |
2551 | def media_update(self, id, description=None, focus=None, thumbnail=None, thumbnail_mime_type=None): | 2602 | def media_update(self, id, description=None, focus=None, thumbnail=None, thumbnail_mime_type=None): |
2552 | """ | 2603 | """ |
2553 | Update the metadata of the media file with the given `id`. `description` and | 2604 | Update the metadata of the media file with the given `id`. `description` and |
2554 | `focus` and `thumbnail` are as in `media_post()`_ . | 2605 | `focus` and `thumbnail` are as in `media_post()`_ . |
2555 | 2606 | ||
2556 | Returns the updated `media dict`_. | 2607 | Returns the updated `media dict`_. |
2557 | """ | 2608 | """ |
2558 | id = self.__unpack_id(id) | 2609 | id = self.__unpack_id(id) |
2559 | 2610 | ||
2560 | if focus != None: | 2611 | if focus != None: |
2561 | focus = str(focus[0]) + "," + str(focus[1]) | 2612 | focus = str(focus[0]) + "," + str(focus[1]) |
2562 | 2613 | ||
2563 | params = self.__generate_params(locals(), ['id', 'thumbnail', 'thumbnail_mime_type']) | 2614 | params = self.__generate_params( |
2615 | locals(), ['id', 'thumbnail', 'thumbnail_mime_type']) | ||
2564 | 2616 | ||
2565 | if not thumbnail is None: | 2617 | if not thumbnail is None: |
2566 | if not self.verify_minimum_version("3.2.0"): | 2618 | if not self.verify_minimum_version("3.2.0"): |
2567 | raise MastodonVersionError('Thumbnail requires version > 3.2.0') | 2619 | raise MastodonVersionError( |
2568 | files = {"thumbnail": self.__load_media_file(thumbnail, thumbnail_mime_type)} | 2620 | 'Thumbnail requires version > 3.2.0') |
2569 | return self.__api_request('PUT', '/api/v1/media/{0}'.format(str(id)), params, files = files) | 2621 | files = {"thumbnail": self.__load_media_file( |
2622 | thumbnail, thumbnail_mime_type)} | ||
2623 | return self.__api_request('PUT', '/api/v1/media/{0}'.format(str(id)), params, files=files) | ||
2570 | else: | 2624 | else: |
2571 | return self.__api_request('PUT', '/api/v1/media/{0}'.format(str(id)), params) | 2625 | return self.__api_request('PUT', '/api/v1/media/{0}'.format(str(id)), params) |
2572 | 2626 | ||
2573 | @api_version("3.1.4", "3.1.4", __DICT_VERSION_MEDIA) | 2627 | @api_version("3.1.4", "3.1.4", __DICT_VERSION_MEDIA) |
2574 | def media(self, id): | 2628 | def media(self, id): |
2575 | """ | 2629 | """ |
@@ -2605,124 +2659,125 @@ class Mastodon: | |||
2605 | def markers_set(self, timelines, last_read_ids): | 2659 | def markers_set(self, timelines, last_read_ids): |
2606 | """ | 2660 | """ |
2607 | Set the "last read" marker(s) for the given timeline(s) to the given id(s) | 2661 | Set the "last read" marker(s) for the given timeline(s) to the given id(s) |
2608 | 2662 | ||
2609 | Note that if you give an invalid timeline name, this will silently do nothing. | 2663 | Note that if you give an invalid timeline name, this will silently do nothing. |
2610 | 2664 | ||
2611 | Returns a dict with the updated `read marker dicts`_, keyed by timeline name. | 2665 | Returns a dict with the updated `read marker dicts`_, keyed by timeline name. |
2612 | """ | 2666 | """ |
2613 | if not isinstance(timelines, (list, tuple)): | 2667 | if not isinstance(timelines, (list, tuple)): |
2614 | timelines = [timelines] | 2668 | timelines = [timelines] |
2615 | 2669 | ||
2616 | if not isinstance(last_read_ids, (list, tuple)): | 2670 | if not isinstance(last_read_ids, (list, tuple)): |
2617 | last_read_ids = [last_read_ids] | 2671 | last_read_ids = [last_read_ids] |
2618 | 2672 | ||
2619 | if len(last_read_ids) != len(timelines): | 2673 | if len(last_read_ids) != len(timelines): |
2620 | raise MastodonIllegalArgumentError("Number of specified timelines and ids must be the same") | 2674 | raise MastodonIllegalArgumentError( |
2621 | 2675 | "Number of specified timelines and ids must be the same") | |
2676 | |||
2622 | params = collections.OrderedDict() | 2677 | params = collections.OrderedDict() |
2623 | for timeline, last_read_id in zip(timelines, last_read_ids): | 2678 | for timeline, last_read_id in zip(timelines, last_read_ids): |
2624 | params[timeline] = collections.OrderedDict() | 2679 | params[timeline] = collections.OrderedDict() |
2625 | params[timeline]["last_read_id"] = self.__unpack_id(last_read_id) | 2680 | params[timeline]["last_read_id"] = self.__unpack_id(last_read_id) |
2626 | 2681 | ||
2627 | return self.__api_request('POST', '/api/v1/markers', params, use_json=True) | 2682 | return self.__api_request('POST', '/api/v1/markers', params, use_json=True) |
2628 | 2683 | ||
2629 | ### | 2684 | ### |
2630 | # Writing data: Push subscriptions | 2685 | # Writing data: Push subscriptions |
2631 | ### | 2686 | ### |
2632 | @api_version("2.4.0", "2.4.0", __DICT_VERSION_PUSH) | 2687 | @api_version("2.4.0", "2.4.0", __DICT_VERSION_PUSH) |
2633 | def push_subscription_set(self, endpoint, encrypt_params, follow_events=None, | 2688 | def push_subscription_set(self, endpoint, encrypt_params, follow_events=None, |
2634 | favourite_events=None, reblog_events=None, | 2689 | favourite_events=None, reblog_events=None, |
2635 | mention_events=None, poll_events=None, | 2690 | mention_events=None, poll_events=None, |
2636 | follow_request_events=None): | 2691 | follow_request_events=None): |
2637 | """ | 2692 | """ |
2638 | Sets up or modifies the push subscription the logged-in user has for this app. | 2693 | Sets up or modifies the push subscription the logged-in user has for this app. |
2639 | 2694 | ||
2640 | `endpoint` is the endpoint URL mastodon should call for pushes. Note that mastodon | 2695 | `endpoint` is the endpoint URL mastodon should call for pushes. Note that mastodon |
2641 | requires https for this URL. `encrypt_params` is a dict with key parameters that allow | 2696 | requires https for this URL. `encrypt_params` is a dict with key parameters that allow |
2642 | the server to encrypt data for you: A public key `pubkey` and a shared secret `auth`. | 2697 | the server to encrypt data for you: A public key `pubkey` and a shared secret `auth`. |
2643 | You can generate this as well as the corresponding private key using the | 2698 | You can generate this as well as the corresponding private key using the |
2644 | `push_subscription_generate_keys()`_ function. | 2699 | `push_subscription_generate_keys()`_ function. |
2645 | 2700 | ||
2646 | The rest of the parameters controls what kind of events you wish to subscribe to. | 2701 | The rest of the parameters controls what kind of events you wish to subscribe to. |
2647 | 2702 | ||
2648 | Returns a `push subscription dict`_. | 2703 | Returns a `push subscription dict`_. |
2649 | """ | 2704 | """ |
2650 | endpoint = Mastodon.__protocolize(endpoint) | 2705 | endpoint = Mastodon.__protocolize(endpoint) |
2651 | 2706 | ||
2652 | push_pubkey_b64 = base64.b64encode(encrypt_params['pubkey']) | 2707 | push_pubkey_b64 = base64.b64encode(encrypt_params['pubkey']) |
2653 | push_auth_b64 = base64.b64encode(encrypt_params['auth']) | 2708 | push_auth_b64 = base64.b64encode(encrypt_params['auth']) |
2654 | 2709 | ||
2655 | params = { | 2710 | params = { |
2656 | 'subscription[endpoint]': endpoint, | 2711 | 'subscription[endpoint]': endpoint, |
2657 | 'subscription[keys][p256dh]': push_pubkey_b64, | 2712 | 'subscription[keys][p256dh]': push_pubkey_b64, |
2658 | 'subscription[keys][auth]': push_auth_b64 | 2713 | 'subscription[keys][auth]': push_auth_b64 |
2659 | } | 2714 | } |
2660 | 2715 | ||
2661 | if follow_events != None: | 2716 | if follow_events != None: |
2662 | params['data[alerts][follow]'] = follow_events | 2717 | params['data[alerts][follow]'] = follow_events |
2663 | 2718 | ||
2664 | if favourite_events != None: | 2719 | if favourite_events != None: |
2665 | params['data[alerts][favourite]'] = favourite_events | 2720 | params['data[alerts][favourite]'] = favourite_events |
2666 | 2721 | ||
2667 | if reblog_events != None: | 2722 | if reblog_events != None: |
2668 | params['data[alerts][reblog]'] = reblog_events | 2723 | params['data[alerts][reblog]'] = reblog_events |
2669 | 2724 | ||
2670 | if mention_events != None: | 2725 | if mention_events != None: |
2671 | params['data[alerts][mention]'] = mention_events | 2726 | params['data[alerts][mention]'] = mention_events |
2672 | 2727 | ||
2673 | if poll_events != None: | 2728 | if poll_events != None: |
2674 | params['data[alerts][poll]'] = poll_events | 2729 | params['data[alerts][poll]'] = poll_events |
2675 | 2730 | ||
2676 | if follow_request_events != None: | 2731 | if follow_request_events != None: |
2677 | params['data[alerts][follow_request]'] = follow_request_events | 2732 | params['data[alerts][follow_request]'] = follow_request_events |
2678 | 2733 | ||
2679 | # Canonicalize booleans | 2734 | # Canonicalize booleans |
2680 | params = self.__generate_params(params) | 2735 | params = self.__generate_params(params) |
2681 | 2736 | ||
2682 | return self.__api_request('POST', '/api/v1/push/subscription', params) | 2737 | return self.__api_request('POST', '/api/v1/push/subscription', params) |
2683 | 2738 | ||
2684 | @api_version("2.4.0", "2.4.0", __DICT_VERSION_PUSH) | 2739 | @api_version("2.4.0", "2.4.0", __DICT_VERSION_PUSH) |
2685 | def push_subscription_update(self, follow_events=None, | 2740 | def push_subscription_update(self, follow_events=None, |
2686 | favourite_events=None, reblog_events=None, | 2741 | favourite_events=None, reblog_events=None, |
2687 | mention_events=None, poll_events=None, | 2742 | mention_events=None, poll_events=None, |
2688 | follow_request_events=None): | 2743 | follow_request_events=None): |
2689 | """ | 2744 | """ |
2690 | Modifies what kind of events the app wishes to subscribe to. | 2745 | Modifies what kind of events the app wishes to subscribe to. |
2691 | 2746 | ||
2692 | Returns the updated `push subscription dict`_. | 2747 | Returns the updated `push subscription dict`_. |
2693 | """ | 2748 | """ |
2694 | params = {} | 2749 | params = {} |
2695 | 2750 | ||
2696 | if follow_events != None: | 2751 | if follow_events != None: |
2697 | params['data[alerts][follow]'] = follow_events | 2752 | params['data[alerts][follow]'] = follow_events |
2698 | 2753 | ||
2699 | if favourite_events != None: | 2754 | if favourite_events != None: |
2700 | params['data[alerts][favourite]'] = favourite_events | 2755 | params['data[alerts][favourite]'] = favourite_events |
2701 | 2756 | ||
2702 | if reblog_events != None: | 2757 | if reblog_events != None: |
2703 | params['data[alerts][reblog]'] = reblog_events | 2758 | params['data[alerts][reblog]'] = reblog_events |
2704 | 2759 | ||
2705 | if mention_events != None: | 2760 | if mention_events != None: |
2706 | params['data[alerts][mention]'] = mention_events | 2761 | params['data[alerts][mention]'] = mention_events |
2707 | 2762 | ||
2708 | if poll_events != None: | 2763 | if poll_events != None: |
2709 | params['data[alerts][poll]'] = poll_events | 2764 | params['data[alerts][poll]'] = poll_events |
2710 | 2765 | ||
2711 | if follow_request_events != None: | 2766 | if follow_request_events != None: |
2712 | params['data[alerts][follow_request]'] = follow_request_events | 2767 | params['data[alerts][follow_request]'] = follow_request_events |
2713 | 2768 | ||
2714 | # Canonicalize booleans | 2769 | # Canonicalize booleans |
2715 | params = self.__generate_params(params) | 2770 | params = self.__generate_params(params) |
2716 | 2771 | ||
2717 | return self.__api_request('PUT', '/api/v1/push/subscription', params) | 2772 | return self.__api_request('PUT', '/api/v1/push/subscription', params) |
2718 | 2773 | ||
2719 | @api_version("2.4.0", "2.4.0", "2.4.0") | 2774 | @api_version("2.4.0", "2.4.0", "2.4.0") |
2720 | def push_subscription_delete(self): | 2775 | def push_subscription_delete(self): |
2721 | """ | 2776 | """ |
2722 | Remove the current push subscription the logged-in user has for this app. | 2777 | Remove the current push subscription the logged-in user has for this app. |
2723 | """ | 2778 | """ |
2724 | self.__api_request('DELETE', '/api/v1/push/subscription') | 2779 | self.__api_request('DELETE', '/api/v1/push/subscription') |
2725 | 2780 | ||
2726 | ### | 2781 | ### |
2727 | # Writing data: Annoucements | 2782 | # Writing data: Annoucements |
2728 | ### | 2783 | ### |
@@ -2732,37 +2787,39 @@ class Mastodon: | |||
2732 | Set the given annoucement to read. | 2787 | Set the given annoucement to read. |
2733 | """ | 2788 | """ |
2734 | id = self.__unpack_id(id) | 2789 | id = self.__unpack_id(id) |
2735 | 2790 | ||
2736 | url = '/api/v1/announcements/{0}/dismiss'.format(str(id)) | 2791 | url = '/api/v1/announcements/{0}/dismiss'.format(str(id)) |
2737 | self.__api_request('POST', url) | 2792 | self.__api_request('POST', url) |
2738 | 2793 | ||
2739 | @api_version("3.1.0", "3.1.0", "3.1.0") | 2794 | @api_version("3.1.0", "3.1.0", "3.1.0") |
2740 | def announcement_reaction_create(self, id, reaction): | 2795 | def announcement_reaction_create(self, id, reaction): |
2741 | """ | 2796 | """ |
2742 | Add a reaction to an announcement. `reaction` can either be a unicode emoji | 2797 | Add a reaction to an announcement. `reaction` can either be a unicode emoji |
2743 | or the name of one of the instances custom emoji. | 2798 | or the name of one of the instances custom emoji. |
2744 | 2799 | ||
2745 | Will throw an API error if the reaction name is not one of the allowed things | 2800 | Will throw an API error if the reaction name is not one of the allowed things |
2746 | or when trying to add a reaction that the user has already added (adding a | 2801 | or when trying to add a reaction that the user has already added (adding a |
2747 | reaction that a different user added is legal and increments the count). | 2802 | reaction that a different user added is legal and increments the count). |
2748 | """ | 2803 | """ |
2749 | id = self.__unpack_id(id) | 2804 | id = self.__unpack_id(id) |
2750 | 2805 | ||
2751 | url = '/api/v1/announcements/{0}/reactions/{1}'.format(str(id), reaction) | 2806 | url = '/api/v1/announcements/{0}/reactions/{1}'.format( |
2807 | str(id), reaction) | ||
2752 | self.__api_request('PUT', url) | 2808 | self.__api_request('PUT', url) |
2753 | 2809 | ||
2754 | @api_version("3.1.0", "3.1.0", "3.1.0") | 2810 | @api_version("3.1.0", "3.1.0", "3.1.0") |
2755 | def announcement_reaction_delete(self, id, reaction): | 2811 | def announcement_reaction_delete(self, id, reaction): |
2756 | """ | 2812 | """ |
2757 | Remove a reaction to an announcement. | 2813 | Remove a reaction to an announcement. |
2758 | 2814 | ||
2759 | Will throw an API error if the reaction does not exist. | 2815 | Will throw an API error if the reaction does not exist. |
2760 | """ | 2816 | """ |
2761 | id = self.__unpack_id(id) | 2817 | id = self.__unpack_id(id) |
2762 | 2818 | ||
2763 | url = '/api/v1/announcements/{0}/reactions/{1}'.format(str(id), reaction) | 2819 | url = '/api/v1/announcements/{0}/reactions/{1}'.format( |
2820 | str(id), reaction) | ||
2764 | self.__api_request('DELETE', url) | 2821 | self.__api_request('DELETE', url) |
2765 | 2822 | ||
2766 | ### | 2823 | ### |
2767 | # Moderation API | 2824 | # Moderation API |
2768 | ### | 2825 | ### |
@@ -2770,7 +2827,7 @@ class Mastodon: | |||
2770 | 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): | 2827 | 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): |
2771 | """ | 2828 | """ |
2772 | Fetches a list of accounts that match given criteria. By default, local accounts are returned. | 2829 | Fetches a list of accounts that match given criteria. By default, local accounts are returned. |
2773 | 2830 | ||
2774 | * Set `remote` to True to get remote accounts, otherwise local accounts are returned (default: local accounts) | 2831 | * Set `remote` to True to get remote accounts, otherwise local accounts are returned (default: local accounts) |
2775 | * Set `by_domain` to a domain to get only accounts from that domain. | 2832 | * Set `by_domain` to a domain to get only accounts from that domain. |
2776 | * Set `status` to one of "active", "pending", "disabled", "silenced" or "suspended" to get only accounts with that moderation status (default: active) | 2833 | * Set `status` to one of "active", "pending", "disabled", "silenced" or "suspended" to get only accounts with that moderation status (default: active) |
@@ -2779,64 +2836,66 @@ class Mastodon: | |||
2779 | * Set `email` to an email to get only accounts with that email (this only works on local accounts). | 2836 | * Set `email` to an email to get only accounts with that email (this only works on local accounts). |
2780 | * 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). | 2837 | * 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). |
2781 | * Set `staff_only` to True to only get staff accounts (this only works on local accounts). | 2838 | * Set `staff_only` to True to only get staff accounts (this only works on local accounts). |
2782 | 2839 | ||
2783 | Note that setting the boolean parameters to False does not mean "give me users to which this does not apply" but | 2840 | Note that setting the boolean parameters to False does not mean "give me users to which this does not apply" but |
2784 | instead means "I do not care if users have this attribute". | 2841 | instead means "I do not care if users have this attribute". |
2785 | 2842 | ||
2786 | Returns a list of `admin account dicts`_. | 2843 | Returns a list of `admin account dicts`_. |
2787 | """ | 2844 | """ |
2788 | if max_id != None: | 2845 | if max_id != None: |
2789 | max_id = self.__unpack_id(max_id, dateconv=True) | 2846 | max_id = self.__unpack_id(max_id, dateconv=True) |
2790 | 2847 | ||
2791 | if min_id != None: | 2848 | if min_id != None: |
2792 | min_id = self.__unpack_id(min_id, dateconv=True) | 2849 | min_id = self.__unpack_id(min_id, dateconv=True) |
2793 | 2850 | ||
2794 | if since_id != None: | 2851 | if since_id != None: |
2795 | since_id = self.__unpack_id(since_id, dateconv=True) | 2852 | since_id = self.__unpack_id(since_id, dateconv=True) |
2796 | 2853 | ||
2797 | params = self.__generate_params(locals(), ['remote', 'status', 'staff_only']) | 2854 | params = self.__generate_params( |
2798 | 2855 | locals(), ['remote', 'status', 'staff_only']) | |
2856 | |||
2799 | if remote == True: | 2857 | if remote == True: |
2800 | params["remote"] = True | 2858 | params["remote"] = True |
2801 | 2859 | ||
2802 | mod_statuses = ["active", "pending", "disabled", "silenced", "suspended"] | 2860 | mod_statuses = ["active", "pending", |
2861 | "disabled", "silenced", "suspended"] | ||
2803 | if not status in mod_statuses: | 2862 | if not status in mod_statuses: |
2804 | raise ValueError("Invalid moderation status requested.") | 2863 | raise ValueError("Invalid moderation status requested.") |
2805 | 2864 | ||
2806 | if staff_only == True: | 2865 | if staff_only == True: |
2807 | params["staff"] = True | 2866 | params["staff"] = True |
2808 | 2867 | ||
2809 | for mod_status in mod_statuses: | 2868 | for mod_status in mod_statuses: |
2810 | if status == mod_status: | 2869 | if status == mod_status: |
2811 | params[status] = True | 2870 | params[status] = True |
2812 | 2871 | ||
2813 | return self.__api_request('GET', '/api/v1/admin/accounts', params) | 2872 | return self.__api_request('GET', '/api/v1/admin/accounts', params) |
2814 | 2873 | ||
2815 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_ADMIN_ACCOUNT) | 2874 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_ADMIN_ACCOUNT) |
2816 | def admin_account(self, id): | 2875 | def admin_account(self, id): |
2817 | """ | 2876 | """ |
2818 | Fetches a single `admin account dict`_ for the user with the given id. | 2877 | Fetches a single `admin account dict`_ for the user with the given id. |
2819 | 2878 | ||
2820 | Returns that dict. | 2879 | Returns that dict. |
2821 | """ | 2880 | """ |
2822 | id = self.__unpack_id(id) | 2881 | id = self.__unpack_id(id) |
2823 | return self.__api_request('GET', '/api/v1/admin/accounts/{0}'.format(id)) | 2882 | return self.__api_request('GET', '/api/v1/admin/accounts/{0}'.format(id)) |
2824 | 2883 | ||
2825 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_ADMIN_ACCOUNT) | 2884 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_ADMIN_ACCOUNT) |
2826 | def admin_account_enable(self, id): | 2885 | def admin_account_enable(self, id): |
2827 | """ | 2886 | """ |
2828 | Reenables login for a local account for which login has been disabled. | 2887 | Reenables login for a local account for which login has been disabled. |
2829 | 2888 | ||
2830 | Returns the updated `admin account dict`_. | 2889 | Returns the updated `admin account dict`_. |
2831 | """ | 2890 | """ |
2832 | id = self.__unpack_id(id) | 2891 | id = self.__unpack_id(id) |
2833 | return self.__api_request('POST', '/api/v1/admin/accounts/{0}/enable'.format(id)) | 2892 | return self.__api_request('POST', '/api/v1/admin/accounts/{0}/enable'.format(id)) |
2834 | 2893 | ||
2835 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_ADMIN_ACCOUNT) | 2894 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_ADMIN_ACCOUNT) |
2836 | def admin_account_approve(self, id): | 2895 | def admin_account_approve(self, id): |
2837 | """ | 2896 | """ |
2838 | Approves a pending account. | 2897 | Approves a pending account. |
2839 | 2898 | ||
2840 | Returns the updated `admin account dict`_. | 2899 | Returns the updated `admin account dict`_. |
2841 | """ | 2900 | """ |
2842 | id = self.__unpack_id(id) | 2901 | id = self.__unpack_id(id) |
@@ -2846,37 +2905,37 @@ class Mastodon: | |||
2846 | def admin_account_reject(self, id): | 2905 | def admin_account_reject(self, id): |
2847 | """ | 2906 | """ |
2848 | Rejects and deletes a pending account. | 2907 | Rejects and deletes a pending account. |
2849 | 2908 | ||
2850 | Returns the updated `admin account dict`_ for the account that is now gone. | 2909 | Returns the updated `admin account dict`_ for the account that is now gone. |
2851 | """ | 2910 | """ |
2852 | id = self.__unpack_id(id) | 2911 | id = self.__unpack_id(id) |
2853 | return self.__api_request('POST', '/api/v1/admin/accounts/{0}/reject'.format(id)) | 2912 | return self.__api_request('POST', '/api/v1/admin/accounts/{0}/reject'.format(id)) |
2854 | 2913 | ||
2855 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_ADMIN_ACCOUNT) | 2914 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_ADMIN_ACCOUNT) |
2856 | def admin_account_unsilence(self, id): | 2915 | def admin_account_unsilence(self, id): |
2857 | """ | 2916 | """ |
2858 | Unsilences an account. | 2917 | Unsilences an account. |
2859 | 2918 | ||
2860 | Returns the updated `admin account dict`_. | 2919 | Returns the updated `admin account dict`_. |
2861 | """ | 2920 | """ |
2862 | id = self.__unpack_id(id) | 2921 | id = self.__unpack_id(id) |
2863 | return self.__api_request('POST', '/api/v1/admin/accounts/{0}/unsilence'.format(id)) | 2922 | return self.__api_request('POST', '/api/v1/admin/accounts/{0}/unsilence'.format(id)) |
2864 | 2923 | ||
2865 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_ADMIN_ACCOUNT) | 2924 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_ADMIN_ACCOUNT) |
2866 | def admin_account_unsuspend(self, id): | 2925 | def admin_account_unsuspend(self, id): |
2867 | """ | 2926 | """ |
2868 | Unsuspends an account. | 2927 | Unsuspends an account. |
2869 | 2928 | ||
2870 | Returns the updated `admin account dict`_. | 2929 | Returns the updated `admin account dict`_. |
2871 | """ | 2930 | """ |
2872 | id = self.__unpack_id(id) | 2931 | id = self.__unpack_id(id) |
2873 | return self.__api_request('POST', '/api/v1/admin/accounts/{0}/unsuspend'.format(id)) | 2932 | return self.__api_request('POST', '/api/v1/admin/accounts/{0}/unsuspend'.format(id)) |
2874 | 2933 | ||
2875 | @api_version("3.3.0", "3.3.0", __DICT_VERSION_ADMIN_ACCOUNT) | 2934 | @api_version("3.3.0", "3.3.0", __DICT_VERSION_ADMIN_ACCOUNT) |
2876 | def admin_account_delete(self, id): | 2935 | def admin_account_delete(self, id): |
2877 | """ | 2936 | """ |
2878 | Delete a local user account. | 2937 | Delete a local user account. |
2879 | 2938 | ||
2880 | The deleted accounts `admin account dict`_. | 2939 | The deleted accounts `admin account dict`_. |
2881 | """ | 2940 | """ |
2882 | id = self.__unpack_id(id) | 2941 | id = self.__unpack_id(id) |
@@ -2886,7 +2945,7 @@ class Mastodon: | |||
2886 | def admin_account_unsensitive(self, id): | 2945 | def admin_account_unsensitive(self, id): |
2887 | """ | 2946 | """ |
2888 | Unmark an account as force-sensitive. | 2947 | Unmark an account as force-sensitive. |
2889 | 2948 | ||
2890 | Returns the updated `admin account dict`_. | 2949 | Returns the updated `admin account dict`_. |
2891 | """ | 2950 | """ |
2892 | id = self.__unpack_id(id) | 2951 | id = self.__unpack_id(id) |
@@ -2896,209 +2955,221 @@ class Mastodon: | |||
2896 | def admin_account_moderate(self, id, action=None, report_id=None, warning_preset_id=None, text=None, send_email_notification=True): | 2955 | def admin_account_moderate(self, id, action=None, report_id=None, warning_preset_id=None, text=None, send_email_notification=True): |
2897 | """ | 2956 | """ |
2898 | Perform a moderation action on an account. | 2957 | Perform a moderation action on an account. |
2899 | 2958 | ||
2900 | Valid actions are: | 2959 | Valid actions are: |
2901 | * "disable" - for a local user, disable login. | 2960 | * "disable" - for a local user, disable login. |
2902 | * "silence" - hide the users posts from all public timelines. | 2961 | * "silence" - hide the users posts from all public timelines. |
2903 | * "suspend" - irreversibly delete all the users posts, past and future. | 2962 | * "suspend" - irreversibly delete all the user's posts, past and future. |
2904 | * "sensitive" - forcce an accounts media visibility to always be sensitive. | 2963 | * "sensitive" - forcce an accounts media visibility to always be sensitive. |
2964 | |||
2905 | If no action is specified, the user is only issued a warning. | 2965 | If no action is specified, the user is only issued a warning. |
2906 | 2966 | ||
2907 | Specify the id of a report as `report_id` to close the report with this moderation action as the resolution. | 2967 | Specify the id of a report as `report_id` to close the report with this moderation action as the resolution. |
2908 | Specify `warning_preset_id` to use a warning preset as the notification text to the user, or `text` to specify text directly. | 2968 | Specify `warning_preset_id` to use a warning preset as the notification text to the user, or `text` to specify text directly. |
2909 | If both are specified, they are concatenated (preset first). Note that there is currently no API to retrieve or create | 2969 | If both are specified, they are concatenated (preset first). Note that there is currently no API to retrieve or create |
2910 | warning presets. | 2970 | warning presets. |
2911 | 2971 | ||
2912 | Set `send_email_notification` to False to not send the user an e-mail notification informing them of the moderation action. | 2972 | Set `send_email_notification` to False to not send the user an email notification informing them of the moderation action. |
2913 | """ | 2973 | """ |
2914 | if action is None: | 2974 | if action is None: |
2915 | action = "none" | 2975 | action = "none" |
2916 | 2976 | ||
2917 | if send_email_notification == False: | 2977 | if send_email_notification == False: |
2918 | send_email_notification = None | 2978 | send_email_notification = None |
2919 | 2979 | ||
2920 | id = self.__unpack_id(id) | 2980 | id = self.__unpack_id(id) |
2921 | if not report_id is None: | 2981 | if not report_id is None: |
2922 | report_id = self.__unpack_id(report_id) | 2982 | report_id = self.__unpack_id(report_id) |
2923 | 2983 | ||
2924 | params = self.__generate_params(locals(), ['id', 'action']) | 2984 | params = self.__generate_params(locals(), ['id', 'action']) |
2925 | 2985 | ||
2926 | params["type"] = action | 2986 | params["type"] = action |
2927 | 2987 | ||
2928 | self.__api_request('POST', '/api/v1/admin/accounts/{0}/action'.format(id), params) | 2988 | self.__api_request( |
2929 | 2989 | 'POST', '/api/v1/admin/accounts/{0}/action'.format(id), params) | |
2990 | |||
2930 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_REPORT) | 2991 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_REPORT) |
2931 | def admin_reports(self, resolved=False, account_id=None, target_account_id=None, max_id=None, min_id=None, since_id=None, limit=None): | 2992 | def admin_reports(self, resolved=False, account_id=None, target_account_id=None, max_id=None, min_id=None, since_id=None, limit=None): |
2932 | """ | 2993 | """ |
2933 | Fetches the list of reports. | 2994 | Fetches the list of reports. |
2934 | 2995 | ||
2935 | Set `resolved` to True to search for resolved reports. `account_id` and `target_account_id` | 2996 | Set `resolved` to True to search for resolved reports. `account_id` and `target_account_id` |
2936 | can be used to get reports filed by or about a specific user. | 2997 | can be used to get reports filed by or about a specific user. |
2937 | 2998 | ||
2938 | Returns a list of `report dicts`_. | 2999 | Returns a list of `report dicts`_. |
2939 | """ | 3000 | """ |
2940 | if max_id != None: | 3001 | if max_id != None: |
2941 | max_id = self.__unpack_id(max_id, dateconv=True) | 3002 | max_id = self.__unpack_id(max_id, dateconv=True) |
2942 | 3003 | ||
2943 | if min_id != None: | 3004 | if min_id != None: |
2944 | min_id = self.__unpack_id(min_id, dateconv=True) | 3005 | min_id = self.__unpack_id(min_id, dateconv=True) |
2945 | 3006 | ||
2946 | if since_id != None: | 3007 | if since_id != None: |
2947 | since_id = self.__unpack_id(since_id, dateconv=True) | 3008 | since_id = self.__unpack_id(since_id, dateconv=True) |
2948 | 3009 | ||
2949 | if not account_id is None: | 3010 | if not account_id is None: |
2950 | account_id = self.__unpack_id(account_id) | 3011 | account_id = self.__unpack_id(account_id) |
2951 | 3012 | ||
2952 | if not target_account_id is None: | 3013 | if not target_account_id is None: |
2953 | target_account_id = self.__unpack_id(target_account_id) | 3014 | target_account_id = self.__unpack_id(target_account_id) |
2954 | 3015 | ||
2955 | if resolved == False: | 3016 | if resolved == False: |
2956 | resolved = None | 3017 | resolved = None |
2957 | 3018 | ||
2958 | params = self.__generate_params(locals()) | 3019 | params = self.__generate_params(locals()) |
2959 | return self.__api_request('GET', '/api/v1/admin/reports', params) | 3020 | return self.__api_request('GET', '/api/v1/admin/reports', params) |
2960 | 3021 | ||
2961 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_REPORT) | 3022 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_REPORT) |
2962 | def admin_report(self, id): | 3023 | def admin_report(self, id): |
2963 | """ | 3024 | """ |
2964 | Fetches the report with the given id. | 3025 | Fetches the report with the given id. |
2965 | 3026 | ||
2966 | Returns a `report dict`_. | 3027 | Returns a `report dict`_. |
2967 | """ | 3028 | """ |
2968 | id = self.__unpack_id(id) | 3029 | id = self.__unpack_id(id) |
2969 | return self.__api_request('GET', '/api/v1/admin/reports/{0}'.format(id)) | 3030 | return self.__api_request('GET', '/api/v1/admin/reports/{0}'.format(id)) |
2970 | 3031 | ||
2971 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_REPORT) | 3032 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_REPORT) |
2972 | def admin_report_assign(self, id): | 3033 | def admin_report_assign(self, id): |
2973 | """ | 3034 | """ |
2974 | Assigns the given report to the logged-in user. | 3035 | Assigns the given report to the logged-in user. |
2975 | 3036 | ||
2976 | Returns the updated `report dict`_. | 3037 | Returns the updated `report dict`_. |
2977 | """ | 3038 | """ |
2978 | id = self.__unpack_id(id) | 3039 | id = self.__unpack_id(id) |
2979 | return self.__api_request('POST', '/api/v1/admin/reports/{0}/assign_to_self'.format(id)) | 3040 | return self.__api_request('POST', '/api/v1/admin/reports/{0}/assign_to_self'.format(id)) |
2980 | 3041 | ||
2981 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_REPORT) | 3042 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_REPORT) |
2982 | def admin_report_unassign(self, id): | 3043 | def admin_report_unassign(self, id): |
2983 | """ | 3044 | """ |
2984 | Unassigns the given report from the logged-in user. | 3045 | Unassigns the given report from the logged-in user. |
2985 | 3046 | ||
2986 | Returns the updated `report dict`_. | 3047 | Returns the updated `report dict`_. |
2987 | """ | 3048 | """ |
2988 | id = self.__unpack_id(id) | 3049 | id = self.__unpack_id(id) |
2989 | return self.__api_request('POST', '/api/v1/admin/reports/{0}/unassign'.format(id)) | 3050 | return self.__api_request('POST', '/api/v1/admin/reports/{0}/unassign'.format(id)) |
2990 | 3051 | ||
2991 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_REPORT) | 3052 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_REPORT) |
2992 | def admin_report_reopen(self, id): | 3053 | def admin_report_reopen(self, id): |
2993 | """ | 3054 | """ |
2994 | Reopens a closed report. | 3055 | Reopens a closed report. |
2995 | 3056 | ||
2996 | Returns the updated `report dict`_. | 3057 | Returns the updated `report dict`_. |
2997 | """ | 3058 | """ |
2998 | id = self.__unpack_id(id) | 3059 | id = self.__unpack_id(id) |
2999 | return self.__api_request('POST', '/api/v1/admin/reports/{0}/reopen'.format(id)) | 3060 | return self.__api_request('POST', '/api/v1/admin/reports/{0}/reopen'.format(id)) |
3000 | 3061 | ||
3001 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_REPORT) | 3062 | @api_version("2.9.1", "2.9.1", __DICT_VERSION_REPORT) |
3002 | def admin_report_resolve(self, id): | 3063 | def admin_report_resolve(self, id): |
3003 | """ | 3064 | """ |
3004 | Marks a report as resolved (without taking any action). | 3065 | Marks a report as resolved (without taking any action). |
3005 | 3066 | ||
3006 | Returns the updated `report dict`_. | 3067 | Returns the updated `report dict`_. |
3007 | """ | 3068 | """ |
3008 | id = self.__unpack_id(id) | 3069 | id = self.__unpack_id(id) |
3009 | return self.__api_request('POST', '/api/v1/admin/reports/{0}/resolve'.format(id)) | 3070 | return self.__api_request('POST', '/api/v1/admin/reports/{0}/resolve'.format(id)) |
3010 | 3071 | ||
3011 | ### | 3072 | ### |
3012 | # Push subscription crypto utilities | 3073 | # Push subscription crypto utilities |
3013 | ### | 3074 | ### |
3014 | def push_subscription_generate_keys(self): | 3075 | def push_subscription_generate_keys(self): |
3015 | """ | 3076 | """ |
3016 | Generates a private key, public key and shared secret for use in webpush subscriptions. | 3077 | Generates a private key, public key and shared secret for use in webpush subscriptions. |
3017 | 3078 | ||
3018 | Returns two dicts: One with the private key and shared secret and another with the | 3079 | Returns two dicts: One with the private key and shared secret and another with the |
3019 | public key and shared secret. | 3080 | public key and shared secret. |
3020 | """ | 3081 | """ |
3021 | if not IMPL_HAS_CRYPTO: | 3082 | if not IMPL_HAS_CRYPTO: |
3022 | raise NotImplementedError('To use the crypto tools, please install the webpush feature dependencies.') | 3083 | raise NotImplementedError( |
3023 | 3084 | 'To use the crypto tools, please install the webpush feature dependencies.') | |
3024 | push_key_pair = ec.generate_private_key(ec.SECP256R1(), default_backend()) | 3085 | |
3086 | push_key_pair = ec.generate_private_key( | ||
3087 | ec.SECP256R1(), default_backend()) | ||
3025 | push_key_priv = push_key_pair.private_numbers().private_value | 3088 | push_key_priv = push_key_pair.private_numbers().private_value |
3026 | 3089 | ||
3027 | crypto_ver = cryptography.__version__ | 3090 | crypto_ver = cryptography.__version__ |
3028 | if len(crypto_ver) < 5: | 3091 | if len(crypto_ver) < 5: |
3029 | crypto_ver += ".0" | 3092 | crypto_ver += ".0" |
3030 | if bigger_version(crypto_ver, "2.5.0") == crypto_ver: | 3093 | if bigger_version(crypto_ver, "2.5.0") == crypto_ver: |
3031 | push_key_pub = push_key_pair.public_key().public_bytes(serialization.Encoding.X962, serialization.PublicFormat.UncompressedPoint) | 3094 | push_key_pub = push_key_pair.public_key().public_bytes( |
3095 | serialization.Encoding.X962, serialization.PublicFormat.UncompressedPoint) | ||
3032 | else: | 3096 | else: |
3033 | push_key_pub = push_key_pair.public_key().public_numbers().encode_point() | 3097 | push_key_pub = push_key_pair.public_key().public_numbers().encode_point() |
3034 | push_shared_secret = os.urandom(16) | 3098 | push_shared_secret = os.urandom(16) |
3035 | 3099 | ||
3036 | priv_dict = { | 3100 | priv_dict = { |
3037 | 'privkey': push_key_priv, | 3101 | 'privkey': push_key_priv, |
3038 | 'auth': push_shared_secret | 3102 | 'auth': push_shared_secret |
3039 | } | 3103 | } |
3040 | 3104 | ||
3041 | pub_dict = { | 3105 | pub_dict = { |
3042 | 'pubkey': push_key_pub, | 3106 | 'pubkey': push_key_pub, |
3043 | 'auth': push_shared_secret | 3107 | 'auth': push_shared_secret |
3044 | } | 3108 | } |
3045 | 3109 | ||
3046 | return priv_dict, pub_dict | 3110 | return priv_dict, pub_dict |
3047 | 3111 | ||
3048 | @api_version("2.4.0", "2.4.0", __DICT_VERSION_PUSH_NOTIF) | 3112 | @api_version("2.4.0", "2.4.0", __DICT_VERSION_PUSH_NOTIF) |
3049 | def push_subscription_decrypt_push(self, data, decrypt_params, encryption_header, crypto_key_header): | 3113 | def push_subscription_decrypt_push(self, data, decrypt_params, encryption_header, crypto_key_header): |
3050 | """ | 3114 | """ |
3051 | Decrypts `data` received in a webpush request. Requires the private key dict | 3115 | Decrypts `data` received in a webpush request. Requires the private key dict |
3052 | from `push_subscription_generate_keys()`_ (`decrypt_params`) as well as the | 3116 | from `push_subscription_generate_keys()`_ (`decrypt_params`) as well as the |
3053 | Encryption and server Crypto-Key headers from the received webpush | 3117 | Encryption and server Crypto-Key headers from the received webpush |
3054 | 3118 | ||
3055 | Returns the decoded webpush as a `push notification dict`_. | 3119 | Returns the decoded webpush as a `push notification dict`_. |
3056 | """ | 3120 | """ |
3057 | if (not IMPL_HAS_ECE) or (not IMPL_HAS_CRYPTO): | 3121 | if (not IMPL_HAS_ECE) or (not IMPL_HAS_CRYPTO): |
3058 | raise NotImplementedError('To use the crypto tools, please install the webpush feature dependencies.') | 3122 | raise NotImplementedError( |
3059 | 3123 | 'To use the crypto tools, please install the webpush feature dependencies.') | |
3060 | salt = self.__decode_webpush_b64(encryption_header.split("salt=")[1].strip()) | 3124 | |
3061 | dhparams = self.__decode_webpush_b64(crypto_key_header.split("dh=")[1].split(";")[0].strip()) | 3125 | salt = self.__decode_webpush_b64( |
3062 | p256ecdsa = self.__decode_webpush_b64(crypto_key_header.split("p256ecdsa=")[1].strip()) | 3126 | encryption_header.split("salt=")[1].strip()) |
3063 | dec_key = ec.derive_private_key(decrypt_params['privkey'], ec.SECP256R1(), default_backend()) | 3127 | dhparams = self.__decode_webpush_b64( |
3128 | crypto_key_header.split("dh=")[1].split(";")[0].strip()) | ||
3129 | p256ecdsa = self.__decode_webpush_b64( | ||
3130 | crypto_key_header.split("p256ecdsa=")[1].strip()) | ||
3131 | dec_key = ec.derive_private_key( | ||
3132 | decrypt_params['privkey'], ec.SECP256R1(), default_backend()) | ||
3064 | decrypted = http_ece.decrypt( | 3133 | decrypted = http_ece.decrypt( |
3065 | data, | 3134 | data, |
3066 | salt = salt, | 3135 | salt=salt, |
3067 | key = p256ecdsa, | 3136 | key=p256ecdsa, |
3068 | private_key = dec_key, | 3137 | private_key=dec_key, |
3069 | dh = dhparams, | 3138 | dh=dhparams, |
3070 | auth_secret=decrypt_params['auth'], | 3139 | auth_secret=decrypt_params['auth'], |
3071 | keylabel = "P-256", | 3140 | keylabel="P-256", |
3072 | version = "aesgcm" | 3141 | version="aesgcm" |
3073 | ) | 3142 | ) |
3074 | 3143 | ||
3075 | return json.loads(decrypted.decode('utf-8'), object_hook = Mastodon.__json_hooks) | 3144 | return json.loads(decrypted.decode('utf-8'), object_hook=Mastodon.__json_hooks) |
3076 | 3145 | ||
3077 | ### | 3146 | ### |
3078 | # Blurhash utilities | 3147 | # Blurhash utilities |
3079 | ### | 3148 | ### |
3080 | def decode_blurhash(self, media_dict, out_size = (16, 16), size_per_component = True, return_linear = True): | 3149 | def decode_blurhash(self, media_dict, out_size=(16, 16), size_per_component=True, return_linear=True): |
3081 | """ | 3150 | """ |
3082 | Basic media-dict blurhash decoding. | 3151 | Basic media-dict blurhash decoding. |
3083 | 3152 | ||
3084 | out_size is the desired result size in pixels, either absolute or per blurhash | 3153 | out_size is the desired result size in pixels, either absolute or per blurhash |
3085 | component (this is the default). | 3154 | component (this is the default). |
3086 | 3155 | ||
3087 | By default, this function will return the image as linear RGB, ready for further | 3156 | By default, this function will return the image as linear RGB, ready for further |
3088 | scaling operations. If you want to display the image directly, set return_linear | 3157 | scaling operations. If you want to display the image directly, set return_linear |
3089 | to False. | 3158 | to False. |
3090 | 3159 | ||
3091 | Returns the decoded blurhash image as a three-dimensional list: [height][width][3], | 3160 | Returns the decoded blurhash image as a three-dimensional list: [height][width][3], |
3092 | with the last dimension being RGB colours. | 3161 | with the last dimension being RGB colours. |
3093 | 3162 | ||
3094 | For further info and tips for advanced usage, refer to the documentation for the | 3163 | For further info and tips for advanced usage, refer to the documentation for the |
3095 | blurhash module: https://github.com/halcy/blurhash-python | 3164 | blurhash module: https://github.com/halcy/blurhash-python |
3096 | """ | 3165 | """ |
3097 | if not IMPL_HAS_BLURHASH: | 3166 | if not IMPL_HAS_BLURHASH: |
3098 | raise NotImplementedError('To use the blurhash functions, please install the blurhash python module.') | 3167 | raise NotImplementedError( |
3168 | 'To use the blurhash functions, please install the blurhash Python module.') | ||
3099 | 3169 | ||
3100 | # Figure out what size to decode to | 3170 | # Figure out what size to decode to |
3101 | decode_components_x, decode_components_y = blurhash.components(media_dict["blurhash"]) | 3171 | decode_components_x, decode_components_y = blurhash.components( |
3172 | media_dict["blurhash"]) | ||
3102 | if size_per_component == False: | 3173 | if size_per_component == False: |
3103 | decode_size_x = out_size[0] | 3174 | decode_size_x = out_size[0] |
3104 | decode_size_y = out_size[1] | 3175 | decode_size_y = out_size[1] |
@@ -3107,11 +3178,12 @@ class Mastodon: | |||
3107 | decode_size_y = decode_components_y * out_size[1] | 3178 | decode_size_y = decode_components_y * out_size[1] |
3108 | 3179 | ||
3109 | # Decode | 3180 | # Decode |
3110 | decoded_image = blurhash.decode(media_dict["blurhash"], decode_size_x, decode_size_y, linear = return_linear) | 3181 | decoded_image = blurhash.decode( |
3182 | media_dict["blurhash"], decode_size_x, decode_size_y, linear=return_linear) | ||
3111 | 3183 | ||
3112 | # And that's pretty much it. | 3184 | # And that's pretty much it. |
3113 | return decoded_image | 3185 | return decoded_image |
3114 | 3186 | ||
3115 | ### | 3187 | ### |
3116 | # Pagination | 3188 | # Pagination |
3117 | ### | 3189 | ### |
@@ -3185,7 +3257,7 @@ class Mastodon: | |||
3185 | ### | 3257 | ### |
3186 | # Streaming | 3258 | # Streaming |
3187 | ### | 3259 | ### |
3188 | @api_version("1.1.0", "1.4.2", __DICT_VERSION_STATUS) | 3260 | @api_version("1.1.0", "1.4.2", __DICT_VERSION_STATUS) |
3189 | def stream_user(self, listener, run_async=False, timeout=__DEFAULT_STREAM_TIMEOUT, reconnect_async=False, reconnect_async_wait_sec=__DEFAULT_STREAM_RECONNECT_WAIT_SEC): | 3261 | def stream_user(self, listener, run_async=False, timeout=__DEFAULT_STREAM_TIMEOUT, reconnect_async=False, reconnect_async_wait_sec=__DEFAULT_STREAM_RECONNECT_WAIT_SEC): |
3190 | """ | 3262 | """ |
3191 | Streams events that are relevant to the authorized user, i.e. home | 3263 | Streams events that are relevant to the authorized user, i.e. home |
@@ -3212,11 +3284,12 @@ class Mastodon: | |||
3212 | """ | 3284 | """ |
3213 | Stream for all public statuses for the hashtag 'tag' seen by the connected | 3285 | Stream for all public statuses for the hashtag 'tag' seen by the connected |
3214 | instance. | 3286 | instance. |
3215 | 3287 | ||
3216 | Set local to True to only get local statuses. | 3288 | Set local to True to only get local statuses. |
3217 | """ | 3289 | """ |
3218 | if tag.startswith("#"): | 3290 | if tag.startswith("#"): |
3219 | raise MastodonIllegalArgumentError("Tag parameter should omit leading #") | 3291 | raise MastodonIllegalArgumentError( |
3292 | "Tag parameter should omit leading #") | ||
3220 | base = '/api/v1/streaming/hashtag' | 3293 | base = '/api/v1/streaming/hashtag' |
3221 | if local: | 3294 | if local: |
3222 | base += '/local' | 3295 | base += '/local' |
@@ -3226,28 +3299,29 @@ class Mastodon: | |||
3226 | 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): | 3299 | 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): |
3227 | """ | 3300 | """ |
3228 | Stream events for the current user, restricted to accounts on the given | 3301 | Stream events for the current user, restricted to accounts on the given |
3229 | list. | 3302 | list. |
3230 | """ | 3303 | """ |
3231 | id = self.__unpack_id(id) | 3304 | id = self.__unpack_id(id) |
3232 | 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) | 3305 | 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) |
3233 | 3306 | ||
3234 | @api_version("2.6.0", "2.6.0", __DICT_VERSION_STATUS) | 3307 | @api_version("2.6.0", "2.6.0", __DICT_VERSION_STATUS) |
3235 | def stream_direct(self, listener, run_async=False, timeout=__DEFAULT_STREAM_TIMEOUT, reconnect_async=False, reconnect_async_wait_sec=__DEFAULT_STREAM_RECONNECT_WAIT_SEC): | 3308 | def stream_direct(self, listener, run_async=False, timeout=__DEFAULT_STREAM_TIMEOUT, reconnect_async=False, reconnect_async_wait_sec=__DEFAULT_STREAM_RECONNECT_WAIT_SEC): |
3236 | """ | 3309 | """ |
3237 | Streams direct message events for the logged-in user, as conversation events. | 3310 | Streams direct message events for the logged-in user, as conversation events. |
3238 | """ | 3311 | """ |
3239 | 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) | 3312 | 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) |
3240 | 3313 | ||
3241 | @api_version("2.5.0", "2.5.0", "2.5.0") | 3314 | @api_version("2.5.0", "2.5.0", "2.5.0") |
3242 | def stream_healthy(self): | 3315 | def stream_healthy(self): |
3243 | """ | 3316 | """ |
3244 | Returns without True if streaming API is okay, False or raises an error otherwise. | 3317 | Returns without True if streaming API is okay, False or raises an error otherwise. |
3245 | """ | 3318 | """ |
3246 | api_okay = self.__api_request('GET', '/api/v1/streaming/health', base_url_override = self.__get_streaming_base(), parse=False) | 3319 | api_okay = self.__api_request( |
3320 | 'GET', '/api/v1/streaming/health', base_url_override=self.__get_streaming_base(), parse=False) | ||
3247 | if api_okay == b'OK': | 3321 | if api_okay == b'OK': |
3248 | return True | 3322 | return True |
3249 | return False | 3323 | return False |
3250 | 3324 | ||
3251 | ### | 3325 | ### |
3252 | # Internal helpers, dragons probably | 3326 | # Internal helpers, dragons probably |
3253 | ### | 3327 | ### |
@@ -3264,18 +3338,18 @@ class Mastodon: | |||
3264 | else: | 3338 | else: |
3265 | date_time_utc = date_time.astimezone(pytz.utc) | 3339 | date_time_utc = date_time.astimezone(pytz.utc) |
3266 | 3340 | ||
3267 | epoch_utc = datetime.datetime.utcfromtimestamp(0).replace(tzinfo=pytz.utc) | 3341 | epoch_utc = datetime.datetime.utcfromtimestamp( |
3342 | 0).replace(tzinfo=pytz.utc) | ||
3268 | 3343 | ||
3269 | return (date_time_utc - epoch_utc).total_seconds() | 3344 | return (date_time_utc - epoch_utc).total_seconds() |
3270 | 3345 | ||
3271 | def __get_logged_in_id(self): | 3346 | def __get_logged_in_id(self): |
3272 | """ | 3347 | """ |
3273 | Fetch the logged in users ID, with caching. ID is reset on calls to log_in. | 3348 | Fetch the logged in user's ID, with caching. ID is reset on calls to log_in. |
3274 | """ | 3349 | """ |
3275 | if self.__logged_in_id == None: | 3350 | if self.__logged_in_id == None: |
3276 | self.__logged_in_id = self.account_verify_credentials().id | 3351 | self.__logged_in_id = self.account_verify_credentials().id |
3277 | return self.__logged_in_id | 3352 | return self.__logged_in_id |
3278 | |||
3279 | 3353 | ||
3280 | @staticmethod | 3354 | @staticmethod |
3281 | def __json_allow_dict_attrs(json_object): | 3355 | def __json_allow_dict_attrs(json_object): |
@@ -3292,13 +3366,15 @@ class Mastodon: | |||
3292 | """ | 3366 | """ |
3293 | Parse dates in certain known json fields, if possible. | 3367 | Parse dates in certain known json fields, if possible. |
3294 | """ | 3368 | """ |
3295 | known_date_fields = ["created_at", "week", "day", "expires_at", "scheduled_at", "updated_at", "last_status_at", "starts_at", "ends_at", "published_at", "edited_at"] | 3369 | known_date_fields = ["created_at", "week", "day", "expires_at", "scheduled_at", |
3370 | "updated_at", "last_status_at", "starts_at", "ends_at", "published_at", "edited_at"] | ||
3296 | for k, v in json_object.items(): | 3371 | for k, v in json_object.items(): |
3297 | if k in known_date_fields: | 3372 | if k in known_date_fields: |
3298 | if v != None: | 3373 | if v != None: |
3299 | try: | 3374 | try: |
3300 | if isinstance(v, int): | 3375 | if isinstance(v, int): |
3301 | json_object[k] = datetime.datetime.fromtimestamp(v, pytz.utc) | 3376 | json_object[k] = datetime.datetime.fromtimestamp( |
3377 | v, pytz.utc) | ||
3302 | else: | 3378 | else: |
3303 | json_object[k] = dateutil.parser.parse(v) | 3379 | json_object[k] = dateutil.parser.parse(v) |
3304 | except: | 3380 | except: |
@@ -3317,7 +3393,7 @@ class Mastodon: | |||
3317 | if json_object[key].lower() == 'false': | 3393 | if json_object[key].lower() == 'false': |
3318 | json_object[key] = False | 3394 | json_object[key] = False |
3319 | return json_object | 3395 | return json_object |
3320 | 3396 | ||
3321 | @staticmethod | 3397 | @staticmethod |
3322 | def __json_strnum_to_bignum(json_object): | 3398 | def __json_strnum_to_bignum(json_object): |
3323 | """ | 3399 | """ |
@@ -3331,13 +3407,13 @@ class Mastodon: | |||
3331 | pass | 3407 | pass |
3332 | 3408 | ||
3333 | return json_object | 3409 | return json_object |
3334 | 3410 | ||
3335 | @staticmethod | 3411 | @staticmethod |
3336 | def __json_hooks(json_object): | 3412 | def __json_hooks(json_object): |
3337 | """ | 3413 | """ |
3338 | All the json hooks. Used in request parsing. | 3414 | All the json hooks. Used in request parsing. |
3339 | """ | 3415 | """ |
3340 | json_object = Mastodon.__json_strnum_to_bignum(json_object) | 3416 | json_object = Mastodon.__json_strnum_to_bignum(json_object) |
3341 | json_object = Mastodon.__json_date_parse(json_object) | 3417 | json_object = Mastodon.__json_date_parse(json_object) |
3342 | json_object = Mastodon.__json_truefalse_parse(json_object) | 3418 | json_object = Mastodon.__json_truefalse_parse(json_object) |
3343 | json_object = Mastodon.__json_allow_dict_attrs(json_object) | 3419 | json_object = Mastodon.__json_allow_dict_attrs(json_object) |
@@ -3350,7 +3426,8 @@ class Mastodon: | |||
3350 | every time instead of randomly doing different things on some systems | 3426 | every time instead of randomly doing different things on some systems |
3351 | and also it represents that time as the equivalent UTC time. | 3427 | and also it represents that time as the equivalent UTC time. |
3352 | """ | 3428 | """ |
3353 | isotime = datetime_val.astimezone(pytz.utc).strftime("%Y-%m-%dT%H:%M:%S%z") | 3429 | isotime = datetime_val.astimezone( |
3430 | pytz.utc).strftime("%Y-%m-%dT%H:%M:%S%z") | ||
3354 | if isotime[-2] != ":": | 3431 | if isotime[-2] != ":": |
3355 | isotime = isotime[:-2] + ":" + isotime[-2:] | 3432 | isotime = isotime[:-2] + ":" + isotime[-2:] |
3356 | return isotime | 3433 | return isotime |
@@ -3361,7 +3438,7 @@ class Mastodon: | |||
3361 | """ | 3438 | """ |
3362 | response = None | 3439 | response = None |
3363 | remaining_wait = 0 | 3440 | remaining_wait = 0 |
3364 | 3441 | ||
3365 | # "pace" mode ratelimiting: Assume constant rate of requests, sleep a little less long than it | 3442 | # "pace" mode ratelimiting: Assume constant rate of requests, sleep a little less long than it |
3366 | # would take to not hit the rate limit at that request rate. | 3443 | # would take to not hit the rate limit at that request rate. |
3367 | if do_ratelimiting and self.ratelimit_method == "pace": | 3444 | if do_ratelimiting and self.ratelimit_method == "pace": |
@@ -3373,7 +3450,8 @@ class Mastodon: | |||
3373 | time.sleep(to_next) | 3450 | time.sleep(to_next) |
3374 | else: | 3451 | else: |
3375 | time_waited = time.time() - self.ratelimit_lastcall | 3452 | time_waited = time.time() - self.ratelimit_lastcall |
3376 | time_wait = float(self.ratelimit_reset - time.time()) / float(self.ratelimit_remaining) | 3453 | time_wait = float(self.ratelimit_reset - |
3454 | time.time()) / float(self.ratelimit_remaining) | ||
3377 | remaining_wait = time_wait - time_waited | 3455 | remaining_wait = time_wait - time_waited |
3378 | 3456 | ||
3379 | if remaining_wait > 0: | 3457 | if remaining_wait > 0: |
@@ -3398,7 +3476,8 @@ class Mastodon: | |||
3398 | base_url = base_url_override | 3476 | base_url = base_url_override |
3399 | 3477 | ||
3400 | if self.debug_requests: | 3478 | if self.debug_requests: |
3401 | print('Mastodon: Request to endpoint "' + base_url + endpoint + '" using method "' + method + '".') | 3479 | print('Mastodon: Request to endpoint "' + base_url + |
3480 | endpoint + '" using method "' + method + '".') | ||
3402 | print('Parameters: ' + str(params)) | 3481 | print('Parameters: ' + str(params)) |
3403 | print('Headers: ' + str(headers)) | 3482 | print('Headers: ' + str(headers)) |
3404 | print('Files: ' + str(files)) | 3483 | print('Files: ' + str(files)) |
@@ -3419,61 +3498,74 @@ class Mastodon: | |||
3419 | kwargs['data'] = params | 3498 | kwargs['data'] = params |
3420 | else: | 3499 | else: |
3421 | kwargs['json'] = params | 3500 | kwargs['json'] = params |
3422 | 3501 | ||
3423 | # Block list with exactly three entries, matching on hashes of the instance API domain | 3502 | # Block list with exactly three entries, matching on hashes of the instance API domain |
3424 | # For more information, have a look at the docs | 3503 | # For more information, have a look at the docs |
3425 | if hashlib.sha256(",".join(base_url.split("//")[-1].split("/")[0].split(".")[-2:]).encode("utf-8")).hexdigest() in \ | 3504 | if hashlib.sha256(",".join(base_url.split("//")[-1].split("/")[0].split(".")[-2:]).encode("utf-8")).hexdigest() in \ |
3426 | [ | 3505 | [ |
3427 | "f3b50af8594eaa91dc440357a92691ff65dbfc9555226e9545b8e083dc10d2e1", | 3506 | "f3b50af8594eaa91dc440357a92691ff65dbfc9555226e9545b8e083dc10d2e1", |
3428 | "b96d2de9784efb5af0af56965b8616afe5469c06e7188ad0ccaee5c7cb8a56b6", | 3507 | "b96d2de9784efb5af0af56965b8616afe5469c06e7188ad0ccaee5c7cb8a56b6", |
3429 | "2dc0cbc89fad4873f665b78cc2f8b6b80fae4af9ac43c0d693edfda27275f517" | 3508 | "2dc0cbc89fad4873f665b78cc2f8b6b80fae4af9ac43c0d693edfda27275f517" |
3430 | ]: | 3509 | ]: |
3431 | raise Exception("Access denied.") | 3510 | raise Exception("Access denied.") |
3432 | 3511 | ||
3433 | response_object = self.session.request(method, base_url + endpoint, **kwargs) | 3512 | response_object = self.session.request( |
3513 | method, base_url + endpoint, **kwargs) | ||
3434 | except Exception as e: | 3514 | except Exception as e: |
3435 | raise MastodonNetworkError("Could not complete request: %s" % e) | 3515 | raise MastodonNetworkError( |
3516 | "Could not complete request: %s" % e) | ||
3436 | 3517 | ||
3437 | if response_object is None: | 3518 | if response_object is None: |
3438 | raise MastodonIllegalArgumentError("Illegal request.") | 3519 | raise MastodonIllegalArgumentError("Illegal request.") |
3439 | 3520 | ||
3440 | # Parse rate limiting headers | 3521 | # Parse rate limiting headers |
3441 | if 'X-RateLimit-Remaining' in response_object.headers and do_ratelimiting: | 3522 | if 'X-RateLimit-Remaining' in response_object.headers and do_ratelimiting: |
3442 | self.ratelimit_remaining = int(response_object.headers['X-RateLimit-Remaining']) | 3523 | self.ratelimit_remaining = int( |
3443 | self.ratelimit_limit = int(response_object.headers['X-RateLimit-Limit']) | 3524 | response_object.headers['X-RateLimit-Remaining']) |
3444 | 3525 | self.ratelimit_limit = int( | |
3526 | response_object.headers['X-RateLimit-Limit']) | ||
3527 | |||
3445 | # For gotosocial, we need an int representation, but for non-ints this would crash | 3528 | # For gotosocial, we need an int representation, but for non-ints this would crash |
3446 | try: | 3529 | try: |
3447 | ratelimit_intrep = str(int(response_object.headers['X-RateLimit-Reset'])) | 3530 | ratelimit_intrep = str( |
3531 | int(response_object.headers['X-RateLimit-Reset'])) | ||
3448 | except: | 3532 | except: |
3449 | ratelimit_intrep = None | 3533 | ratelimit_intrep = None |
3450 | 3534 | ||
3451 | try: | 3535 | try: |
3452 | if not ratelimit_intrep is None and ratelimit_intrep == response_object.headers['X-RateLimit-Reset']: | 3536 | if not ratelimit_intrep is None and ratelimit_intrep == response_object.headers['X-RateLimit-Reset']: |
3453 | self.ratelimit_reset = int(response_object.headers['X-RateLimit-Reset']) | 3537 | self.ratelimit_reset = int( |
3538 | response_object.headers['X-RateLimit-Reset']) | ||
3454 | else: | 3539 | else: |
3455 | ratelimit_reset_datetime = dateutil.parser.parse(response_object.headers['X-RateLimit-Reset']) | 3540 | ratelimit_reset_datetime = dateutil.parser.parse( |
3456 | self.ratelimit_reset = self.__datetime_to_epoch(ratelimit_reset_datetime) | 3541 | response_object.headers['X-RateLimit-Reset']) |
3542 | self.ratelimit_reset = self.__datetime_to_epoch( | ||
3543 | ratelimit_reset_datetime) | ||
3457 | 3544 | ||
3458 | # Adjust server time to local clock | 3545 | # Adjust server time to local clock |
3459 | if 'Date' in response_object.headers: | 3546 | if 'Date' in response_object.headers: |
3460 | server_time_datetime = dateutil.parser.parse(response_object.headers['Date']) | 3547 | server_time_datetime = dateutil.parser.parse( |
3461 | server_time = self.__datetime_to_epoch(server_time_datetime) | 3548 | response_object.headers['Date']) |
3549 | server_time = self.__datetime_to_epoch( | ||
3550 | server_time_datetime) | ||
3462 | server_time_diff = time.time() - server_time | 3551 | server_time_diff = time.time() - server_time |
3463 | self.ratelimit_reset += server_time_diff | 3552 | self.ratelimit_reset += server_time_diff |
3464 | self.ratelimit_lastcall = time.time() | 3553 | self.ratelimit_lastcall = time.time() |
3465 | except Exception as e: | 3554 | except Exception as e: |
3466 | raise MastodonRatelimitError("Rate limit time calculations failed: %s" % e) | 3555 | raise MastodonRatelimitError( |
3556 | "Rate limit time calculations failed: %s" % e) | ||
3467 | 3557 | ||
3468 | # Handle response | 3558 | # Handle response |
3469 | if self.debug_requests: | 3559 | if self.debug_requests: |
3470 | print('Mastodon: Response received with code ' + str(response_object.status_code) + '.') | 3560 | print('Mastodon: Response received with code ' + |
3561 | str(response_object.status_code) + '.') | ||
3471 | print('response headers: ' + str(response_object.headers)) | 3562 | print('response headers: ' + str(response_object.headers)) |
3472 | print('Response text content: ' + str(response_object.text)) | 3563 | print('Response text content: ' + str(response_object.text)) |
3473 | 3564 | ||
3474 | if not response_object.ok: | 3565 | if not response_object.ok: |
3475 | try: | 3566 | try: |
3476 | response = response_object.json(object_hook=self.__json_hooks) | 3567 | response = response_object.json( |
3568 | object_hook=self.__json_hooks) | ||
3477 | if isinstance(response, dict) and 'error' in response: | 3569 | if isinstance(response, dict) and 'error' in response: |
3478 | error_msg = response['error'] | 3570 | error_msg = response['error'] |
3479 | elif isinstance(response, str): | 3571 | elif isinstance(response, str): |
@@ -3514,28 +3606,29 @@ class Mastodon: | |||
3514 | elif response_object.status_code == 504: | 3606 | elif response_object.status_code == 504: |
3515 | ex_type = MastodonGatewayTimeoutError | 3607 | ex_type = MastodonGatewayTimeoutError |
3516 | elif response_object.status_code >= 500 and \ | 3608 | elif response_object.status_code >= 500 and \ |
3517 | response_object.status_code <= 511: | 3609 | response_object.status_code <= 511: |
3518 | ex_type = MastodonServerError | 3610 | ex_type = MastodonServerError |
3519 | else: | 3611 | else: |
3520 | ex_type = MastodonAPIError | 3612 | ex_type = MastodonAPIError |
3521 | 3613 | ||
3522 | raise ex_type( | 3614 | raise ex_type( |
3523 | 'Mastodon API returned error', | 3615 | 'Mastodon API returned error', |
3524 | response_object.status_code, | 3616 | response_object.status_code, |
3525 | response_object.reason, | 3617 | response_object.reason, |
3526 | error_msg) | 3618 | error_msg) |
3527 | 3619 | ||
3528 | if parse == True: | 3620 | if parse == True: |
3529 | try: | 3621 | try: |
3530 | response = response_object.json(object_hook=self.__json_hooks) | 3622 | response = response_object.json( |
3623 | object_hook=self.__json_hooks) | ||
3531 | except: | 3624 | except: |
3532 | raise MastodonAPIError( | 3625 | raise MastodonAPIError( |
3533 | "Could not parse response as JSON, response code was %s, " | 3626 | "Could not parse response as JSON, response code was %s, " |
3534 | "bad json content was '%s'" % (response_object.status_code, | 3627 | "bad json content was '%s'" % (response_object.status_code, |
3535 | response_object.content)) | 3628 | response_object.content)) |
3536 | else: | 3629 | else: |
3537 | response = response_object.content | 3630 | response = response_object.content |
3538 | 3631 | ||
3539 | # Parse link headers | 3632 | # Parse link headers |
3540 | if isinstance(response, list) and \ | 3633 | if isinstance(response, list) and \ |
3541 | 'Link' in response_object.headers and \ | 3634 | 'Link' in response_object.headers and \ |
@@ -3550,7 +3643,8 @@ class Mastodon: | |||
3550 | if url['rel'] == 'next': | 3643 | if url['rel'] == 'next': |
3551 | # Be paranoid and extract max_id specifically | 3644 | # Be paranoid and extract max_id specifically |
3552 | next_url = url['url'] | 3645 | next_url = url['url'] |
3553 | matchgroups = re.search(r"[?&]max_id=([^&]+)", next_url) | 3646 | matchgroups = re.search( |
3647 | r"[?&]max_id=([^&]+)", next_url) | ||
3554 | 3648 | ||
3555 | if matchgroups: | 3649 | if matchgroups: |
3556 | next_params = copy.deepcopy(params) | 3650 | next_params = copy.deepcopy(params) |
@@ -3575,9 +3669,10 @@ class Mastodon: | |||
3575 | if url['rel'] == 'prev': | 3669 | if url['rel'] == 'prev': |
3576 | # Be paranoid and extract since_id or min_id specifically | 3670 | # Be paranoid and extract since_id or min_id specifically |
3577 | prev_url = url['url'] | 3671 | prev_url = url['url'] |
3578 | 3672 | ||
3579 | # Old and busted (pre-2.6.0): since_id pagination | 3673 | # Old and busted (pre-2.6.0): since_id pagination |
3580 | matchgroups = re.search(r"[?&]since_id=([^&]+)", prev_url) | 3674 | matchgroups = re.search( |
3675 | r"[?&]since_id=([^&]+)", prev_url) | ||
3581 | if matchgroups: | 3676 | if matchgroups: |
3582 | prev_params = copy.deepcopy(params) | 3677 | prev_params = copy.deepcopy(params) |
3583 | prev_params['_pagination_method'] = method | 3678 | prev_params['_pagination_method'] = method |
@@ -3597,7 +3692,8 @@ class Mastodon: | |||
3597 | response[0]._pagination_prev = prev_params | 3692 | response[0]._pagination_prev = prev_params |
3598 | 3693 | ||
3599 | # New and fantastico (post-2.6.0): min_id pagination | 3694 | # New and fantastico (post-2.6.0): min_id pagination |
3600 | matchgroups = re.search(r"[?&]min_id=([^&]+)", prev_url) | 3695 | matchgroups = re.search( |
3696 | r"[?&]min_id=([^&]+)", prev_url) | ||
3601 | if matchgroups: | 3697 | if matchgroups: |
3602 | prev_params = copy.deepcopy(params) | 3698 | prev_params = copy.deepcopy(params) |
3603 | prev_params['_pagination_method'] = method | 3699 | prev_params['_pagination_method'] = method |
@@ -3621,7 +3717,7 @@ class Mastodon: | |||
3621 | def __get_streaming_base(self): | 3717 | def __get_streaming_base(self): |
3622 | """ | 3718 | """ |
3623 | Internal streaming API helper. | 3719 | Internal streaming API helper. |
3624 | 3720 | ||
3625 | Returns the correct URL for the streaming API. | 3721 | Returns the correct URL for the streaming API. |
3626 | """ | 3722 | """ |
3627 | instance = self.instance() | 3723 | instance = self.instance() |
@@ -3635,8 +3731,8 @@ class Mastodon: | |||
3635 | url = "http://" + parse.netloc | 3731 | url = "http://" + parse.netloc |
3636 | else: | 3732 | else: |
3637 | raise MastodonAPIError( | 3733 | raise MastodonAPIError( |
3638 | "Could not parse streaming api location returned from server: {}.".format( | 3734 | "Could not parse streaming api location returned from server: {}.".format( |
3639 | instance["urls"]["streaming_api"])) | 3735 | instance["urls"]["streaming_api"])) |
3640 | else: | 3736 | else: |
3641 | url = self.api_base_url | 3737 | url = self.api_base_url |
3642 | return url | 3738 | return url |
@@ -3655,20 +3751,22 @@ class Mastodon: | |||
3655 | # The streaming server can't handle two slashes in a path, so remove trailing slashes | 3751 | # The streaming server can't handle two slashes in a path, so remove trailing slashes |
3656 | if url[-1] == '/': | 3752 | if url[-1] == '/': |
3657 | url = url[:-1] | 3753 | url = url[:-1] |
3658 | 3754 | ||
3659 | # Connect function (called and then potentially passed to async handler) | 3755 | # Connect function (called and then potentially passed to async handler) |
3660 | def connect_func(): | 3756 | def connect_func(): |
3661 | headers = {"Authorization": "Bearer " + self.access_token} if self.access_token else {} | 3757 | headers = {"Authorization": "Bearer " + |
3758 | self.access_token} if self.access_token else {} | ||
3662 | if self.user_agent: | 3759 | if self.user_agent: |
3663 | headers['User-Agent'] = self.user_agent | 3760 | headers['User-Agent'] = self.user_agent |
3664 | connection = self.session.get(url + endpoint, headers = headers, data = params, stream = True, | 3761 | connection = self.session.get(url + endpoint, headers=headers, data=params, stream=True, |
3665 | timeout=(self.request_timeout, timeout)) | 3762 | timeout=(self.request_timeout, timeout)) |
3666 | 3763 | ||
3667 | if connection.status_code != 200: | 3764 | if connection.status_code != 200: |
3668 | raise MastodonNetworkError("Could not connect to streaming server: %s" % connection.reason) | 3765 | raise MastodonNetworkError( |
3766 | "Could not connect to streaming server: %s" % connection.reason) | ||
3669 | return connection | 3767 | return connection |
3670 | connection = None | 3768 | connection = None |
3671 | 3769 | ||
3672 | # Async stream handler | 3770 | # Async stream handler |
3673 | class __stream_handle(): | 3771 | class __stream_handle(): |
3674 | def __init__(self, connection, connect_func, reconnect_async, reconnect_async_wait_sec): | 3772 | def __init__(self, connection, connect_func, reconnect_async, reconnect_async_wait_sec): |
@@ -3679,7 +3777,7 @@ class Mastodon: | |||
3679 | self.reconnect_async = reconnect_async | 3777 | self.reconnect_async = reconnect_async |
3680 | self.reconnect_async_wait_sec = reconnect_async_wait_sec | 3778 | self.reconnect_async_wait_sec = reconnect_async_wait_sec |
3681 | self.reconnecting = False | 3779 | self.reconnecting = False |
3682 | 3780 | ||
3683 | def close(self): | 3781 | def close(self): |
3684 | self.closed = True | 3782 | self.closed = True |
3685 | if not self.connection is None: | 3783 | if not self.connection is None: |
@@ -3696,15 +3794,16 @@ class Mastodon: | |||
3696 | 3794 | ||
3697 | def _sleep_attentive(self): | 3795 | def _sleep_attentive(self): |
3698 | if self._thread != threading.current_thread(): | 3796 | if self._thread != threading.current_thread(): |
3699 | raise RuntimeError ("Illegal call from outside the stream_handle thread") | 3797 | raise RuntimeError( |
3798 | "Illegal call from outside the stream_handle thread") | ||
3700 | time_remaining = self.reconnect_async_wait_sec | 3799 | time_remaining = self.reconnect_async_wait_sec |
3701 | while time_remaining>0 and not self.closed: | 3800 | while time_remaining > 0 and not self.closed: |
3702 | time.sleep(0.5) | 3801 | time.sleep(0.5) |
3703 | time_remaining -= 0.5 | 3802 | time_remaining -= 0.5 |
3704 | 3803 | ||
3705 | def _threadproc(self): | 3804 | def _threadproc(self): |
3706 | self._thread = threading.current_thread() | 3805 | self._thread = threading.current_thread() |
3707 | 3806 | ||
3708 | # Run until closed or until error if not autoreconnecting | 3807 | # Run until closed or until error if not autoreconnecting |
3709 | while self.running: | 3808 | while self.running: |
3710 | if not self.connection is None: | 3809 | if not self.connection is None: |
@@ -3750,14 +3849,15 @@ class Mastodon: | |||
3750 | return 0 | 3849 | return 0 |
3751 | 3850 | ||
3752 | if run_async: | 3851 | if run_async: |
3753 | handle = __stream_handle(connection, connect_func, reconnect_async, reconnect_async_wait_sec) | 3852 | handle = __stream_handle( |
3853 | connection, connect_func, reconnect_async, reconnect_async_wait_sec) | ||
3754 | t = threading.Thread(args=(), target=handle._threadproc) | 3854 | t = threading.Thread(args=(), target=handle._threadproc) |
3755 | t.daemon = True | 3855 | t.daemon = True |
3756 | t.start() | 3856 | t.start() |
3757 | return handle | 3857 | return handle |
3758 | else: | 3858 | else: |
3759 | # Blocking, never returns (can only leave via exception) | 3859 | # Blocking, never returns (can only leave via exception) |
3760 | connection = connect_func() | 3860 | connection = connect_func() |
3761 | with closing(connection) as r: | 3861 | with closing(connection) as r: |
3762 | listener.handle_stream(r) | 3862 | listener.handle_stream(r) |
3763 | 3863 | ||
@@ -3774,14 +3874,14 @@ class Mastodon: | |||
3774 | 3874 | ||
3775 | if 'self' in params: | 3875 | if 'self' in params: |
3776 | del params['self'] | 3876 | del params['self'] |
3777 | 3877 | ||
3778 | param_keys = list(params.keys()) | 3878 | param_keys = list(params.keys()) |
3779 | for key in param_keys: | 3879 | for key in param_keys: |
3780 | if isinstance(params[key], bool) and params[key] == False: | 3880 | if isinstance(params[key], bool) and params[key] == False: |
3781 | params[key] = '0' | 3881 | params[key] = '0' |
3782 | if isinstance(params[key], bool) and params[key] == True: | 3882 | if isinstance(params[key], bool) and params[key] == True: |
3783 | params[key] = '1' | 3883 | params[key] = '1' |
3784 | 3884 | ||
3785 | for key in param_keys: | 3885 | for key in param_keys: |
3786 | if params[key] is None or key in exclude: | 3886 | if params[key] is None or key in exclude: |
3787 | del params[key] | 3887 | del params[key] |
@@ -3791,23 +3891,23 @@ class Mastodon: | |||
3791 | if isinstance(params[key], list): | 3891 | if isinstance(params[key], list): |
3792 | params[key + "[]"] = params[key] | 3892 | params[key + "[]"] = params[key] |
3793 | del params[key] | 3893 | del params[key] |
3794 | 3894 | ||
3795 | return params | 3895 | return params |
3796 | 3896 | ||
3797 | def __unpack_id(self, id, dateconv=False): | 3897 | def __unpack_id(self, id, dateconv=False): |
3798 | """ | 3898 | """ |
3799 | Internal object-to-id converter | 3899 | Internal object-to-id converter |
3800 | 3900 | ||
3801 | Checks if id is a dict that contains id and | 3901 | Checks if id is a dict that contains id and |
3802 | returns the id inside, otherwise just returns | 3902 | returns the id inside, otherwise just returns |
3803 | the id straight. | 3903 | the id straight. |
3804 | """ | 3904 | """ |
3805 | if isinstance(id, dict) and "id" in id: | 3905 | if isinstance(id, dict) and "id" in id: |
3806 | id = id["id"] | 3906 | id = id["id"] |
3807 | if dateconv and isinstance(id, datetime): | 3907 | if dateconv and isinstance(id, datetime): |
3808 | id = (int(id) << 16) * 1000 | 3908 | id = (int(id) << 16) * 1000 |
3809 | return id | 3909 | return id |
3810 | 3910 | ||
3811 | def __decode_webpush_b64(self, data): | 3911 | def __decode_webpush_b64(self, data): |
3812 | """ | 3912 | """ |
3813 | Re-pads and decodes urlsafe base64. | 3913 | Re-pads and decodes urlsafe base64. |
@@ -3816,7 +3916,7 @@ class Mastodon: | |||
3816 | if missing_padding != 0: | 3916 | if missing_padding != 0: |
3817 | data += '=' * (4 - missing_padding) | 3917 | data += '=' * (4 - missing_padding) |
3818 | return base64.urlsafe_b64decode(data) | 3918 | return base64.urlsafe_b64decode(data) |
3819 | 3919 | ||
3820 | def __get_token_expired(self): | 3920 | def __get_token_expired(self): |
3821 | """Internal helper for oauth code""" | 3921 | """Internal helper for oauth code""" |
3822 | return self._token_expired < datetime.datetime.now() | 3922 | return self._token_expired < datetime.datetime.now() |
@@ -3844,17 +3944,20 @@ class Mastodon: | |||
3844 | mime_type = mimetypes.guess_type(media_file)[0] | 3944 | mime_type = mimetypes.guess_type(media_file)[0] |
3845 | return mime_type | 3945 | return mime_type |
3846 | 3946 | ||
3847 | def __load_media_file(self, media_file, mime_type = None, file_name = None): | 3947 | def __load_media_file(self, media_file, mime_type=None, file_name=None): |
3848 | if isinstance(media_file, str) and os.path.isfile(media_file): | 3948 | if isinstance(media_file, str) and os.path.isfile(media_file): |
3849 | mime_type = self.__guess_type(media_file) | 3949 | mime_type = self.__guess_type(media_file) |
3850 | media_file = open(media_file, 'rb') | 3950 | media_file = open(media_file, 'rb') |
3851 | elif isinstance(media_file, str) and os.path.isfile(media_file): | 3951 | elif isinstance(media_file, str) and os.path.isfile(media_file): |
3852 | media_file = open(media_file, 'rb') | 3952 | media_file = open(media_file, 'rb') |
3853 | if mime_type is None: | 3953 | if mime_type is None: |
3854 | raise MastodonIllegalArgumentError('Could not determine mime type or data passed directly without mime type.') | 3954 | raise MastodonIllegalArgumentError( |
3955 | 'Could not determine mime type or data passed directly without mime type.') | ||
3855 | if file_name is None: | 3956 | if file_name is None: |
3856 | random_suffix = uuid.uuid4().hex | 3957 | random_suffix = uuid.uuid4().hex |
3857 | file_name = "mastodonpyupload_" + str(time.time()) + "_" + str(random_suffix) + mimetypes.guess_extension(mime_type) | 3958 | file_name = "mastodonpyupload_" + \ |
3959 | str(time.time()) + "_" + str(random_suffix) + \ | ||
3960 | mimetypes.guess_extension(mime_type) | ||
3858 | return (file_name, media_file, mime_type) | 3961 | return (file_name, media_file, mime_type) |
3859 | 3962 | ||
3860 | @staticmethod | 3963 | @staticmethod |
@@ -3874,10 +3977,12 @@ class Mastodon: | |||
3874 | class MastodonError(Exception): | 3977 | class MastodonError(Exception): |
3875 | """Base class for Mastodon.py exceptions""" | 3978 | """Base class for Mastodon.py exceptions""" |
3876 | 3979 | ||
3980 | |||
3877 | class MastodonVersionError(MastodonError): | 3981 | class MastodonVersionError(MastodonError): |
3878 | """Raised when a function is called that the version of Mastodon for which | 3982 | """Raised when a function is called that the version of Mastodon for which |
3879 | Mastodon.py was instantiated does not support""" | 3983 | Mastodon.py was instantiated does not support""" |
3880 | 3984 | ||
3985 | |||
3881 | class MastodonIllegalArgumentError(ValueError, MastodonError): | 3986 | class MastodonIllegalArgumentError(ValueError, MastodonError): |
3882 | """Raised when an incorrect parameter is passed to a function""" | 3987 | """Raised when an incorrect parameter is passed to a function""" |
3883 | pass | 3988 | pass |
@@ -3896,6 +4001,7 @@ class MastodonNetworkError(MastodonIOError): | |||
3896 | """Raised when network communication with the server fails""" | 4001 | """Raised when network communication with the server fails""" |
3897 | pass | 4002 | pass |
3898 | 4003 | ||
4004 | |||
3899 | class MastodonReadTimeout(MastodonNetworkError): | 4005 | class MastodonReadTimeout(MastodonNetworkError): |
3900 | """Raised when a stream times out""" | 4006 | """Raised when a stream times out""" |
3901 | pass | 4007 | pass |
@@ -3905,32 +4011,39 @@ class MastodonAPIError(MastodonError): | |||
3905 | """Raised when the mastodon API generates a response that cannot be handled""" | 4011 | """Raised when the mastodon API generates a response that cannot be handled""" |
3906 | pass | 4012 | pass |
3907 | 4013 | ||
4014 | |||
3908 | class MastodonServerError(MastodonAPIError): | 4015 | class MastodonServerError(MastodonAPIError): |
3909 | """Raised if the Server is malconfigured and returns a 5xx error code""" | 4016 | """Raised if the Server is malconfigured and returns a 5xx error code""" |
3910 | pass | 4017 | pass |
3911 | 4018 | ||
4019 | |||
3912 | class MastodonInternalServerError(MastodonServerError): | 4020 | class MastodonInternalServerError(MastodonServerError): |
3913 | """Raised if the Server returns a 500 error""" | 4021 | """Raised if the Server returns a 500 error""" |
3914 | pass | 4022 | pass |
3915 | 4023 | ||
4024 | |||
3916 | class MastodonBadGatewayError(MastodonServerError): | 4025 | class MastodonBadGatewayError(MastodonServerError): |
3917 | """Raised if the Server returns a 502 error""" | 4026 | """Raised if the Server returns a 502 error""" |
3918 | pass | 4027 | pass |
3919 | 4028 | ||
4029 | |||
3920 | class MastodonServiceUnavailableError(MastodonServerError): | 4030 | class MastodonServiceUnavailableError(MastodonServerError): |
3921 | """Raised if the Server returns a 503 error""" | 4031 | """Raised if the Server returns a 503 error""" |
3922 | pass | 4032 | pass |
3923 | 4033 | ||
4034 | |||
3924 | class MastodonGatewayTimeoutError(MastodonServerError): | 4035 | class MastodonGatewayTimeoutError(MastodonServerError): |
3925 | """Raised if the Server returns a 504 error""" | 4036 | """Raised if the Server returns a 504 error""" |
3926 | pass | 4037 | pass |
3927 | 4038 | ||
4039 | |||
3928 | class MastodonNotFoundError(MastodonAPIError): | 4040 | class MastodonNotFoundError(MastodonAPIError): |
3929 | """Raised when the mastodon API returns a 404 Not Found error""" | 4041 | """Raised when the Mastodon API returns a 404 Not Found error""" |
3930 | pass | 4042 | pass |
3931 | 4043 | ||
4044 | |||
3932 | class MastodonUnauthorizedError(MastodonAPIError): | 4045 | class MastodonUnauthorizedError(MastodonAPIError): |
3933 | """Raised when the mastodon API returns a 401 Unauthorized error | 4046 | """Raised when the Mastodon API returns a 401 Unauthorized error |
3934 | 4047 | ||
3935 | This happens when an OAuth token is invalid or has been revoked, | 4048 | This happens when an OAuth token is invalid or has been revoked, |
3936 | or when trying to access an endpoint that can't be used without | 4049 | or when trying to access an endpoint that can't be used without |
@@ -3942,7 +4055,7 @@ class MastodonRatelimitError(MastodonError): | |||
3942 | """Raised when rate limiting is set to manual mode and the rate limit is exceeded""" | 4055 | """Raised when rate limiting is set to manual mode and the rate limit is exceeded""" |
3943 | pass | 4056 | pass |
3944 | 4057 | ||
4058 | |||
3945 | class MastodonMalformedEventError(MastodonError): | 4059 | class MastodonMalformedEventError(MastodonError): |
3946 | """Raised when the server-sent event stream is malformed""" | 4060 | """Raised when the server-sent event stream is malformed""" |
3947 | pass | 4061 | pass |
3948 | |||
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) |