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