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