aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'mastodon/Mastodon.py')
-rw-r--r--mastodon/Mastodon.py2852
1 files changed, 25 insertions, 2827 deletions
diff --git a/mastodon/Mastodon.py b/mastodon/Mastodon.py
index 0ded1cf..23e4d9e 100644
--- a/mastodon/Mastodon.py
+++ b/mastodon/Mastodon.py
@@ -26,7 +26,7 @@ from .utility import parse_version_string, max_version, api_version
26from .utility import AttribAccessDict, AttribAccessDict 26from .utility import AttribAccessDict, AttribAccessDict
27from .utility import Mastodon as Utility 27from .utility import Mastodon as Utility
28 28
29from .error import * 29from .errors import *
30from .versions import _DICT_VERSION_APPLICATION, _DICT_VERSION_MENTION, _DICT_VERSION_MEDIA, _DICT_VERSION_ACCOUNT, _DICT_VERSION_POLL, \ 30from .versions import _DICT_VERSION_APPLICATION, _DICT_VERSION_MENTION, _DICT_VERSION_MEDIA, _DICT_VERSION_ACCOUNT, _DICT_VERSION_POLL, \
31 _DICT_VERSION_STATUS, _DICT_VERSION_INSTANCE, _DICT_VERSION_HASHTAG, _DICT_VERSION_EMOJI, _DICT_VERSION_RELATIONSHIP, \ 31 _DICT_VERSION_STATUS, _DICT_VERSION_INSTANCE, _DICT_VERSION_HASHTAG, _DICT_VERSION_EMOJI, _DICT_VERSION_RELATIONSHIP, \
32 _DICT_VERSION_NOTIFICATION, _DICT_VERSION_CONTEXT, _DICT_VERSION_LIST, _DICT_VERSION_CARD, _DICT_VERSION_SEARCHRESULT, \ 32 _DICT_VERSION_NOTIFICATION, _DICT_VERSION_CONTEXT, _DICT_VERSION_LIST, _DICT_VERSION_CARD, _DICT_VERSION_SEARCHRESULT, \
@@ -45,11 +45,33 @@ from .accounts import Mastodon as Accounts
45from .instance import Mastodon as Instance 45from .instance import Mastodon as Instance
46from .timeline import Mastodon as Timeline 46from .timeline import Mastodon as Timeline
47from .statuses import Mastodon as Statuses 47from .statuses import Mastodon as Statuses
48from .media import Mastodon as Media
49from .polls import Mastodon as Polls
50from .notifications import Mastodon as Notifications
51from .conversations import Mastodon as Conversations
52from .hashtags import Mastodon as Hashtags
53from .filters import Mastodon as Filters
54from .suggestions import Mastodon as Suggestions
55from .endorsements import Mastodon as Endorsements
56from .relationships import Mastodon as Relationships
57from .lists import Mastodon as Lists
58from .trends import Mastodon as Trends
59from .search import Mastodon as Search
60from .favourites import Mastodon as Favourites
61from .reports import Mastodon as Reports
62from .preferences import Mastodon as Preferences
63from .push import Mastodon as Push
64from .admin import Mastodon as Admin
65from .streaming_endpoints import Mastodon as Streaming
48 66
49## 67###
50# The actual Mastodon class 68# The actual Mastodon class
69#
70# Almost all code is now imported from smaller files to make editing a bit more pleasant
51### 71###
52class Mastodon(Utility, Authentication, Accounts, Instance, Timeline, Statuses): 72class Mastodon(Utility, Authentication, Accounts, Instance, Timeline, Statuses, Polls, Notifications, Hashtags,
73 Filters, Suggestions, Endorsements, Relationships, Lists, Trends, Search, Favourites, Reports,
74 Preferences, Push, Admin, Conversations, Media, Streaming):
53 """ 75 """
54 Thorough and easy to use Mastodon 76 Thorough and easy to use Mastodon
55 API wrapper in Python. 77 API wrapper in Python.
@@ -65,2827 +87,3 @@ class Mastodon(Utility, Authentication, Accounts, Instance, Timeline, Statuses):
65 Retrieve the maximum version of Mastodon supported by this version of Mastodon.py 87 Retrieve the maximum version of Mastodon supported by this version of Mastodon.py
66 """ 88 """
67 return Mastodon.__SUPPORTED_MASTODON_VERSION 89 return Mastodon.__SUPPORTED_MASTODON_VERSION
68
69 ###
70 # Reading data: Polls
71 ###
72 @api_version("2.8.0", "2.8.0", _DICT_VERSION_POLL)
73 def poll(self, id):
74 """
75 Fetch information about the poll with the given id
76
77 Returns a :ref:`poll dict <poll dict>`.
78 """
79 id = self.__unpack_id(id)
80 url = '/api/v1/polls/{0}'.format(str(id))
81 return self.__api_request('GET', url)
82
83 ###
84 # Reading data: Notifications
85 ###
86 @api_version("1.0.0", "3.5.0", _DICT_VERSION_NOTIFICATION)
87 def notifications(self, id=None, account_id=None, max_id=None, min_id=None, since_id=None, limit=None, exclude_types=None, types=None, mentions_only=None):
88 """
89 Fetch notifications (mentions, favourites, reblogs, follows) for the logged-in
90 user. Pass `account_id` to get only notifications originating from the given account.
91
92 There are different types of notifications:
93 * `follow` - A user followed the logged in user
94 * `follow_request` - A user has requested to follow the logged in user (for locked accounts)
95 * `favourite` - A user favourited a post by the logged in user
96 * `reblog` - A user reblogged a post by the logged in user
97 * `mention` - A user mentioned the logged in user
98 * `poll` - A poll the logged in user created or voted in has ended
99 * `update` - A status the logged in user has reblogged (and only those, as of 4.0.0) has been edited
100 * `status` - A user that the logged in user has enabned notifications for has enabled `notify` (see :ref:`account_follow() <account_follow()>`)
101 * `admin.sign_up` - For accounts with appropriate permissions (TODO: document which those are when adding the permission API): A new user has signed up
102 * `admin.report` - For accounts with appropriate permissions (TODO: document which those are when adding the permission API): A new report has been received
103 Parameters `exclude_types` and `types` are array of these types, specifying them will in- or exclude the
104 types of notifications given. It is legal to give both parameters at the same tine, the result will then
105 be the intersection of the results of both filters. Specifying `mentions_only` is a deprecated way to set
106 `exclude_types` to all but mentions.
107
108 Can be passed an `id` to fetch a single notification.
109
110 Returns a list of :ref:`notification dicts <notification dicts>`.
111 """
112 if mentions_only is not None:
113 if exclude_types is None and types is None:
114 if mentions_only:
115 if self.verify_minimum_version("3.5.0", cached=True):
116 types = ["mention"]
117 else:
118 exclude_types = ["follow", "favourite", "reblog", "poll", "follow_request"]
119 else:
120 raise MastodonIllegalArgumentError('Cannot specify exclude_types/types when mentions_only is present')
121 del mentions_only
122
123 if max_id is not None:
124 max_id = self.__unpack_id(max_id, dateconv=True)
125
126 if min_id is not None:
127 min_id = self.__unpack_id(min_id, dateconv=True)
128
129 if since_id is not None:
130 since_id = self.__unpack_id(since_id, dateconv=True)
131
132 if account_id is not None:
133 account_id = self.__unpack_id(account_id)
134
135 if id is None:
136 params = self.__generate_params(locals(), ['id'])
137 return self.__api_request('GET', '/api/v1/notifications', params)
138 else:
139 id = self.__unpack_id(id)
140 url = '/api/v1/notifications/{0}'.format(str(id))
141 return self.__api_request('GET', url)
142
143 ###
144 # Reading data: Accounts
145 ###
146 @api_version("1.0.0", "1.0.0", _DICT_VERSION_ACCOUNT)
147 def account(self, id):
148 """
149 Fetch account information by user `id`.
150
151 Does not require authentication for publicly visible accounts.
152
153 Returns a :ref:`account dict <account dict>`.
154 """
155 id = self.__unpack_id(id)
156 url = '/api/v1/accounts/{0}'.format(str(id))
157 return self.__api_request('GET', url)
158
159 @api_version("1.0.0", "2.1.0", _DICT_VERSION_ACCOUNT)
160 def account_verify_credentials(self):
161 """
162 Fetch logged-in user's account information.
163
164 Returns a :ref:`account dict <account dict>` (Starting from 2.1.0, with an additional "source" field).
165 """
166 return self.__api_request('GET', '/api/v1/accounts/verify_credentials')
167
168 @api_version("1.0.0", "2.1.0", _DICT_VERSION_ACCOUNT)
169 def me(self):
170 """
171 Get this user's account. Synonym for `account_verify_credentials()`, does exactly
172 the same thing, just exists becase `account_verify_credentials()` has a confusing
173 name.
174 """
175 return self.account_verify_credentials()
176
177 @api_version("1.0.0", "2.8.0", _DICT_VERSION_STATUS)
178 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):
179 """
180 Fetch statuses by user `id`. Same options as :ref:`timeline() <timeline()>` are permitted.
181 Returned toots are from the perspective of the logged-in user, i.e.
182 all statuses visible to the logged-in user (including DMs) are
183 included.
184
185 If `only_media` is set, return only statuses with media attachments.
186 If `pinned` is set, return only statuses that have been pinned. Note that
187 as of Mastodon 2.1.0, this only works properly for instance-local users.
188 If `exclude_replies` is set, filter out all statuses that are replies.
189 If `exclude_reblogs` is set, filter out all statuses that are reblogs.
190 If `tagged` is set, return only statuses that are tagged with `tagged`. Only a single tag without a '#' is valid.
191
192 Does not require authentication for Mastodon versions after 2.7.0 (returns
193 publicly visible statuses in that case), for publicly visible accounts.
194
195 Returns a list of :ref:`status dicts <status dicts>`.
196 """
197 id = self.__unpack_id(id)
198 if max_id is not None:
199 max_id = self.__unpack_id(max_id, dateconv=True)
200
201 if min_id is not None:
202 min_id = self.__unpack_id(min_id, dateconv=True)
203
204 if since_id is not None:
205 since_id = self.__unpack_id(since_id, dateconv=True)
206
207 params = self.__generate_params(locals(), ['id'])
208 if not pinned:
209 del params["pinned"]
210 if not only_media:
211 del params["only_media"]
212 if not exclude_replies:
213 del params["exclude_replies"]
214 if not exclude_reblogs:
215 del params["exclude_reblogs"]
216
217 url = '/api/v1/accounts/{0}/statuses'.format(str(id))
218 return self.__api_request('GET', url, params)
219
220 @api_version("1.0.0", "2.6.0", _DICT_VERSION_ACCOUNT)
221 def account_following(self, id, max_id=None, min_id=None, since_id=None, limit=None):
222 """
223 Fetch users the given user is following.
224
225 Returns a list of :ref:`account dicts <account dicts>`.
226 """
227 id = self.__unpack_id(id)
228 if max_id is not None:
229 max_id = self.__unpack_id(max_id, dateconv=True)
230
231 if min_id is not None:
232 min_id = self.__unpack_id(min_id, dateconv=True)
233
234 if since_id is not None:
235 since_id = self.__unpack_id(since_id, dateconv=True)
236
237 params = self.__generate_params(locals(), ['id'])
238 url = '/api/v1/accounts/{0}/following'.format(str(id))
239 return self.__api_request('GET', url, params)
240
241 @api_version("1.0.0", "2.6.0", _DICT_VERSION_ACCOUNT)
242 def account_followers(self, id, max_id=None, min_id=None, since_id=None, limit=None):
243 """
244 Fetch users the given user is followed by.
245
246 Returns a list of :ref:`account dicts <account dicts>`.
247 """
248 id = self.__unpack_id(id)
249 if max_id is not None:
250 max_id = self.__unpack_id(max_id, dateconv=True)
251
252 if min_id is not None:
253 min_id = self.__unpack_id(min_id, dateconv=True)
254
255 if since_id is not None:
256 since_id = self.__unpack_id(since_id, dateconv=True)
257
258 params = self.__generate_params(locals(), ['id'])
259 url = '/api/v1/accounts/{0}/followers'.format(str(id))
260 return self.__api_request('GET', url, params)
261
262 @api_version("1.0.0", "1.4.0", _DICT_VERSION_RELATIONSHIP)
263 def account_relationships(self, id):
264 """
265 Fetch relationship (following, followed_by, blocking, follow requested) of
266 the logged in user to a given account. `id` can be a list.
267
268 Returns a list of :ref:`relationship dicts <relationship dicts>`.
269 """
270 id = self.__unpack_id(id)
271 params = self.__generate_params(locals())
272 return self.__api_request('GET', '/api/v1/accounts/relationships',
273 params)
274
275 @api_version("1.0.0", "2.3.0", _DICT_VERSION_ACCOUNT)
276 def account_search(self, q, limit=None, following=False):
277 """
278 Fetch matching accounts. Will lookup an account remotely if the search term is
279 in the username@domain format and not yet in the database. Set `following` to
280 True to limit the search to users the logged-in user follows.
281
282 Returns a list of :ref:`account dicts <account dicts>`.
283 """
284 params = self.__generate_params(locals())
285
286 if params["following"] == False:
287 del params["following"]
288
289 return self.__api_request('GET', '/api/v1/accounts/search', params)
290
291 @api_version("2.1.0", "2.1.0", _DICT_VERSION_LIST)
292 def account_lists(self, id):
293 """
294 Get all of the logged-in user's lists which the specified user is
295 a member of.
296
297 Returns a list of :ref:`list dicts <list dicts>`.
298 """
299 id = self.__unpack_id(id)
300 params = self.__generate_params(locals(), ['id'])
301 url = '/api/v1/accounts/{0}/lists'.format(str(id))
302 return self.__api_request('GET', url, params)
303
304 @api_version("3.4.0", "3.4.0", _DICT_VERSION_ACCOUNT)
305 def account_lookup(self, acct):
306 """
307 Look up an account from user@instance form (@instance allowed but not required for
308 local accounts). Will only return accounts that the instance already knows about,
309 and not do any webfinger requests. Use `account_search` if you need to resolve users
310 through webfinger from remote.
311
312 Returns an :ref:`account dict <account dict>`.
313 """
314 return self.__api_request('GET', '/api/v1/accounts/lookup', self.__generate_params(locals()))
315
316 @api_version("3.5.0", "3.5.0", _DICT_VERSION_FAMILIAR_FOLLOWERS)
317 def account_familiar_followers(self, id):
318 """
319 Find followers for the account given by id (can be a list) that also follow the
320 logged in account.
321
322 Returns a list of :ref:`familiar follower dicts <familiar follower dicts>`
323 """
324 if not isinstance(id, list):
325 id = [id]
326 for i in range(len(id)):
327 id[i] = self.__unpack_id(id[i])
328 return self.__api_request('GET', '/api/v1/accounts/familiar_followers', {'id': id}, use_json=True)
329
330 ###
331 # Reading data: Featured hashtags
332 ###
333 @api_version("3.0.0", "3.0.0", _DICT_VERSION_FEATURED_TAG)
334 def featured_tags(self):
335 """
336 Return the hashtags the logged-in user has set to be featured on
337 their profile as a list of :ref:`featured tag dicts <featured tag dicts>`.
338
339 Returns a list of :ref:`featured tag dicts <featured tag dicts>`.
340 """
341 return self.__api_request('GET', '/api/v1/featured_tags')
342
343 @api_version("3.0.0", "3.0.0", _DICT_VERSION_HASHTAG)
344 def featured_tag_suggestions(self):
345 """
346 Returns the logged-in user's 10 most commonly-used hashtags.
347
348 Returns a list of :ref:`hashtag dicts <hashtag dicts>`.
349 """
350 return self.__api_request('GET', '/api/v1/featured_tags/suggestions')
351
352 ###
353 # Reading data: Keyword filters
354 ###
355 @api_version("2.4.3", "2.4.3", _DICT_VERSION_FILTER)
356 def filters(self):
357 """
358 Fetch all of the logged-in user's filters.
359
360 Returns a list of :ref:`filter dicts <filter dicts>`. Not paginated.
361 """
362 return self.__api_request('GET', '/api/v1/filters')
363
364 @api_version("2.4.3", "2.4.3", _DICT_VERSION_FILTER)
365 def filter(self, id):
366 """
367 Fetches information about the filter with the specified `id`.
368
369 Returns a :ref:`filter dict <filter dict>`.
370 """
371 id = self.__unpack_id(id)
372 url = '/api/v1/filters/{0}'.format(str(id))
373 return self.__api_request('GET', url)
374
375 @api_version("2.4.3", "2.4.3", _DICT_VERSION_FILTER)
376 def filters_apply(self, objects, filters, context):
377 """
378 Helper function: Applies a list of filters to a list of either statuses
379 or notifications and returns only those matched by none. This function will
380 apply all filters that match the context provided in `context`, i.e.
381 if you want to apply only notification-relevant filters, specify
382 'notifications'. Valid contexts are 'home', 'notifications', 'public' and 'thread'.
383 """
384
385 # Build filter regex
386 filter_strings = []
387 for keyword_filter in filters:
388 if not context in keyword_filter["context"]:
389 continue
390
391 filter_string = re.escape(keyword_filter["phrase"])
392 if keyword_filter["whole_word"]:
393 filter_string = "\\b" + filter_string + "\\b"
394 filter_strings.append(filter_string)
395 filter_re = re.compile("|".join(filter_strings), flags=re.IGNORECASE)
396
397 # Apply
398 filter_results = []
399 for filter_object in objects:
400 filter_status = filter_object
401 if "status" in filter_object:
402 filter_status = filter_object["status"]
403 filter_text = filter_status["content"]
404 filter_text = re.sub(r"<.*?>", " ", filter_text)
405 filter_text = re.sub(r"\s+", " ", filter_text).strip()
406 if not filter_re.search(filter_text):
407 filter_results.append(filter_object)
408 return filter_results
409
410 ###
411 # Reading data: Follow suggestions
412 ###
413 @api_version("2.4.3", "2.4.3", _DICT_VERSION_ACCOUNT)
414 def suggestions(self):
415 """
416 Fetch follow suggestions for the logged-in user.
417
418 Returns a list of :ref:`account dicts <account dicts>`.
419
420 """
421 return self.__api_request('GET', '/api/v1/suggestions')
422
423 ###
424 # Reading data: Follow suggestions
425 ###
426 @api_version("3.0.0", "3.0.0", _DICT_VERSION_ACCOUNT)
427 def directory(self, offset=None, limit=None, order=None, local=None):
428 """
429 Fetch the contents of the profile directory, if enabled on the server.
430
431 `offset` how many accounts to skip before returning results. Default 0.
432
433 `limit` how many accounts to load. Default 40.
434
435 `order` "active" to sort by most recently posted statuses (default) or
436 "new" to sort by most recently created profiles.
437
438 `local` True to return only local accounts.
439
440 Returns a list of :ref:`account dicts <account dicts>`.
441
442 """
443 params = self.__generate_params(locals())
444 return self.__api_request('GET', '/api/v1/directory', params)
445
446 ###
447 # Reading data: Endorsements
448 ###
449 @api_version("2.5.0", "2.5.0", _DICT_VERSION_ACCOUNT)
450 def endorsements(self):
451 """
452 Fetch list of users endorsed by the logged-in user.
453
454 Returns a list of :ref:`account dicts <account dicts>`.
455
456 """
457 return self.__api_request('GET', '/api/v1/endorsements')
458
459 ###
460 # Reading data: Searching
461 ###
462
463 def __ensure_search_params_acceptable(self, account_id, offset, min_id, max_id):
464 """
465 Internal Helper: Throw a MastodonVersionError if version is < 2.8.0 but parameters
466 for search that are available only starting with 2.8.0 are specified.
467 """
468 if any(item is not None for item in (account_id, offset, min_id, max_id)):
469 if not self.verify_minimum_version("2.8.0", cached=True):
470 raise MastodonVersionError("Advanced search parameters require Mastodon 2.8.0+")
471
472 @api_version("1.1.0", "2.8.0", _DICT_VERSION_SEARCHRESULT)
473 def search(self, q, resolve=True, result_type=None, account_id=None, offset=None, min_id=None, max_id=None, exclude_unreviewed=True):
474 """
475 Fetch matching hashtags, accounts and statuses. Will perform webfinger
476 lookups if resolve is True. Full-text search is only enabled if
477 the instance supports it, and is restricted to statuses the logged-in
478 user wrote or was mentioned in.
479
480 `result_type` can be one of "accounts", "hashtags" or "statuses", to only
481 search for that type of object.
482
483 Specify `account_id` to only get results from the account with that id.
484
485 `offset`, `min_id` and `max_id` can be used to paginate.
486
487 `exclude_unreviewed` can be used to restrict search results for hashtags to only
488 those that have been reviewed by moderators. It is on by default. When using the
489 v1 search API (pre 2.4.1), it is ignored.
490
491 Will use search_v1 (no tag dicts in return values) on Mastodon versions before
492 2.4.1), search_v2 otherwise. Parameters other than resolve are only available
493 on Mastodon 2.8.0 or above - this function will throw a MastodonVersionError
494 if you try to use them on versions before that. Note that the cached version
495 number will be used for this to avoid uneccesary requests.
496
497 Returns a :ref:`search result dict <search result dict>`, with tags as `hashtag dicts`_.
498 """
499 if self.verify_minimum_version("2.4.1", cached=True):
500 return self.search_v2(q, resolve=resolve, result_type=result_type, account_id=account_id, offset=offset, min_id=min_id, max_id=max_id, exclude_unreviewed=exclude_unreviewed)
501 else:
502 self.__ensure_search_params_acceptable(
503 account_id, offset, min_id, max_id)
504 return self.search_v1(q, resolve=resolve)
505
506 @api_version("1.1.0", "2.1.0", "2.1.0")
507 def search_v1(self, q, resolve=False):
508 """
509 Identical to `search_v2()`, except in that it does not return
510 tags as :ref:`hashtag dicts <hashtag dicts>`.
511
512 Returns a :ref:`search result dict <search result dict>`.
513 """
514 params = self.__generate_params(locals())
515 if not resolve:
516 del params['resolve']
517 return self.__api_request('GET', '/api/v1/search', params)
518
519 @api_version("2.4.1", "2.8.0", _DICT_VERSION_SEARCHRESULT)
520 def search_v2(self, q, resolve=True, result_type=None, account_id=None, offset=None, min_id=None, max_id=None, exclude_unreviewed=True):
521 """
522 Identical to `search_v1()`, except in that it returns tags as
523 :ref:`hashtag dicts <hashtag dicts>`, has more parameters, and resolves by default.
524
525 For more details documentation, please see `search()`
526
527 Returns a :ref:`search result dict <search result dict>`.
528 """
529 self.__ensure_search_params_acceptable(
530 account_id, offset, min_id, max_id)
531 params = self.__generate_params(locals())
532
533 if not resolve:
534 del params["resolve"]
535
536 if not exclude_unreviewed or not self.verify_minimum_version("3.0.0", cached=True):
537 del params["exclude_unreviewed"]
538
539 if "result_type" in params:
540 params["type"] = params["result_type"]
541 del params["result_type"]
542
543 return self.__api_request('GET', '/api/v2/search', params)
544
545 ###
546 # Reading data: Trends
547 ###
548 @api_version("2.4.3", "3.5.0", _DICT_VERSION_HASHTAG)
549 def trends(self, limit=None):
550 """
551 Alias for :ref:`trending_tags() <trending_tags()>`
552 """
553 return self.trending_tags(limit=limit)
554
555 @api_version("3.5.0", "3.5.0", _DICT_VERSION_HASHTAG)
556 def trending_tags(self, limit=None, lang=None):
557 """
558 Fetch trending-hashtag information, if the instance provides such information.
559
560 Specify `limit` to limit how many results are returned (the maximum number
561 of results is 10, the endpoint is not paginated).
562
563 Does not require authentication unless locked down by the administrator.
564
565 Important versioning note: This endpoint does not exist for Mastodon versions
566 between 2.8.0 (inclusive) and 3.0.0 (exclusive).
567
568 Pass `lang` to override the global locale parameter, which may affect trend ordering.
569
570 Returns a list of :ref:`hashtag dicts <hashtag dicts>`, sorted by the instance's trending algorithm,
571 descending.
572 """
573 params = self.__generate_params(locals())
574 if self.verify_minimum_version("3.5.0", cached=True):
575 # Starting 3.5.0, old version is deprecated
576 return self.__api_request('GET', '/api/v1/trends/tags', params)
577 else:
578 return self.__api_request('GET', '/api/v1/trends', params)
579
580 @api_version("3.5.0", "3.5.0", _DICT_VERSION_STATUS)
581 def trending_statuses(self):
582 """
583 Fetch trending-status information, if the instance provides such information.
584
585 Specify `limit` to limit how many results are returned (the maximum number
586 of results is 10, the endpoint is not paginated).
587
588 Pass `lang` to override the global locale parameter, which may affect trend ordering.
589
590 Returns a list of :ref:`status dicts <status dicts>`, sorted by the instances's trending algorithm,
591 descending.
592 """
593 params = self.__generate_params(locals())
594 return self.__api_request('GET', '/api/v1/trends/statuses', params)
595
596 @api_version("3.5.0", "3.5.0", _DICT_VERSION_CARD)
597 def trending_links(self):
598 """
599 Fetch trending-link information, if the instance provides such information.
600
601 Specify `limit` to limit how many results are returned (the maximum number
602 of results is 10, the endpoint is not paginated).
603
604 Returns a list of :ref:`card dicts <card dicts>`, sorted by the instances's trending algorithm,
605 descending.
606 """
607 params = self.__generate_params(locals())
608 return self.__api_request('GET', '/api/v1/trends/links', params)
609
610 ###
611 # Reading data: Lists
612 ###
613 @api_version("2.1.0", "2.1.0", _DICT_VERSION_LIST)
614 def lists(self):
615 """
616 Fetch a list of all the Lists by the logged-in user.
617
618 Returns a list of :ref:`list dicts <list dicts>`.
619 """
620 return self.__api_request('GET', '/api/v1/lists')
621
622 @api_version("2.1.0", "2.1.0", _DICT_VERSION_LIST)
623 def list(self, id):
624 """
625 Fetch info about a specific list.
626
627 Returns a :ref:`list dict <list dict>`.
628 """
629 id = self.__unpack_id(id)
630 return self.__api_request('GET', '/api/v1/lists/{0}'.format(id))
631
632 @api_version("2.1.0", "2.6.0", _DICT_VERSION_ACCOUNT)
633 def list_accounts(self, id, max_id=None, min_id=None, since_id=None, limit=None):
634 """
635 Get the accounts that are on the given list.
636
637 Returns a list of :ref:`account dicts <account dicts>`.
638 """
639 id = self.__unpack_id(id)
640
641 if max_id is not None:
642 max_id = self.__unpack_id(max_id, dateconv=True)
643
644 if min_id is not None:
645 min_id = self.__unpack_id(min_id, dateconv=True)
646
647 if since_id is not None:
648 since_id = self.__unpack_id(since_id, dateconv=True)
649
650 params = self.__generate_params(locals(), ['id'])
651 return self.__api_request('GET', '/api/v1/lists/{0}/accounts'.format(id))
652
653 ###
654 # Reading data: Mutes and Blocks
655 ###
656 @api_version("1.1.0", "2.6.0", _DICT_VERSION_ACCOUNT)
657 def mutes(self, max_id=None, min_id=None, since_id=None, limit=None):
658 """
659 Fetch a list of users muted by the logged-in user.
660
661 Returns a list of :ref:`account dicts <account dicts>`.
662 """
663 if max_id is not None:
664 max_id = self.__unpack_id(max_id, dateconv=True)
665
666 if min_id is not None:
667 min_id = self.__unpack_id(min_id, dateconv=True)
668
669 if since_id is not None:
670 since_id = self.__unpack_id(since_id, dateconv=True)
671
672 params = self.__generate_params(locals())
673 return self.__api_request('GET', '/api/v1/mutes', params)
674
675 @api_version("1.0.0", "2.6.0", _DICT_VERSION_ACCOUNT)
676 def blocks(self, max_id=None, min_id=None, since_id=None, limit=None):
677 """
678 Fetch a list of users blocked by the logged-in user.
679
680 Returns a list of :ref:`account dicts <account dicts>`.
681 """
682 if max_id is not None:
683 max_id = self.__unpack_id(max_id, dateconv=True)
684
685 if min_id is not None:
686 min_id = self.__unpack_id(min_id, dateconv=True)
687
688 if since_id is not None:
689 since_id = self.__unpack_id(since_id, dateconv=True)
690
691 params = self.__generate_params(locals())
692 return self.__api_request('GET', '/api/v1/blocks', params)
693
694 ###
695 # Reading data: Reports
696 ###
697 @api_version("1.1.0", "1.1.0", _DICT_VERSION_REPORT)
698 def reports(self):
699 """
700 Fetch a list of reports made by the logged-in user.
701
702 Returns a list of :ref:`report dicts <report dicts>`.
703
704 Warning: This method has now finally been removed, and will not
705 work on Mastodon versions 2.5.0 and above.
706 """
707 if self.verify_minimum_version("2.5.0", cached=True):
708 raise MastodonVersionError("API removed in Mastodon 2.5.0")
709 return self.__api_request('GET', '/api/v1/reports')
710
711 ###
712 # Reading data: Favourites
713 ###
714 @api_version("1.0.0", "2.6.0", _DICT_VERSION_STATUS)
715 def favourites(self, max_id=None, min_id=None, since_id=None, limit=None):
716 """
717 Fetch the logged-in user's favourited statuses.
718
719 Returns a list of :ref:`status dicts <status dicts>`.
720 """
721 if max_id is not None:
722 max_id = self.__unpack_id(max_id, dateconv=True)
723
724 if min_id is not None:
725 min_id = self.__unpack_id(min_id, dateconv=True)
726
727 if since_id is not None:
728 since_id = self.__unpack_id(since_id, dateconv=True)
729
730 params = self.__generate_params(locals())
731 return self.__api_request('GET', '/api/v1/favourites', params)
732
733 ###
734 # Reading data: Follow requests
735 ###
736 @api_version("1.0.0", "2.6.0", _DICT_VERSION_ACCOUNT)
737 def follow_requests(self, max_id=None, min_id=None, since_id=None, limit=None):
738 """
739 Fetch the logged-in user's incoming follow requests.
740
741 Returns a list of :ref:`account dicts <account dicts>`.
742 """
743 if max_id is not None:
744 max_id = self.__unpack_id(max_id, dateconv=True)
745
746 if min_id is not None:
747 min_id = self.__unpack_id(min_id, dateconv=True)
748
749 if since_id is not None:
750 since_id = self.__unpack_id(since_id, dateconv=True)
751
752 params = self.__generate_params(locals())
753 return self.__api_request('GET', '/api/v1/follow_requests', params)
754
755 ###
756 # Reading data: Domain blocks
757 ###
758 @api_version("1.4.0", "2.6.0", "1.4.0")
759 def domain_blocks(self, max_id=None, min_id=None, since_id=None, limit=None):
760 """
761 Fetch the logged-in user's blocked domains.
762
763 Returns a list of blocked domain URLs (as strings, without protocol specifier).
764 """
765 if max_id is not None:
766 max_id = self.__unpack_id(max_id, dateconv=True)
767
768 if min_id is not None:
769 min_id = self.__unpack_id(min_id, dateconv=True)
770
771 if since_id is not None:
772 since_id = self.__unpack_id(since_id, dateconv=True)
773
774 params = self.__generate_params(locals())
775 return self.__api_request('GET', '/api/v1/domain_blocks', params)
776
777 ###
778 # Reading data: Emoji
779 ###
780 @api_version("2.1.0", "2.1.0", _DICT_VERSION_EMOJI)
781 def custom_emojis(self):
782 """
783 Fetch the list of custom emoji the instance has installed.
784
785 Does not require authentication unless locked down by the administrator.
786
787 Returns a list of :ref:`emoji dicts <emoji dicts>`.
788 """
789 return self.__api_request('GET', '/api/v1/custom_emojis')
790
791 ###
792 # Reading data: Apps
793 ###
794 @api_version("2.0.0", "2.7.2", _DICT_VERSION_APPLICATION)
795 def app_verify_credentials(self):
796 """
797 Fetch information about the current application.
798
799 Returns an :ref:`application dict <application dict>`.
800 """
801 return self.__api_request('GET', '/api/v1/apps/verify_credentials')
802
803 ###
804 # Reading data: Webpush subscriptions
805 ###
806 @api_version("2.4.0", "2.4.0", _DICT_VERSION_PUSH)
807 def push_subscription(self):
808 """
809 Fetch the current push subscription the logged-in user has for this app.
810
811 Returns a :ref:`push subscription dict <push subscription dict>`.
812 """
813 return self.__api_request('GET', '/api/v1/push/subscription')
814
815 ###
816 # Reading data: Preferences
817 ###
818 @api_version("2.8.0", "2.8.0", _DICT_VERSION_PREFERENCES)
819 def preferences(self):
820 """
821 Fetch the user's preferences, which can be used to set some default options.
822 As of 2.8.0, apps can only fetch, not update preferences.
823
824 Returns a :ref:`preference dict <preference dict>`.
825 """
826 return self.__api_request('GET', '/api/v1/preferences')
827
828 ##
829 # Reading data: Announcements
830 ##
831
832 # /api/v1/announcements
833 @api_version("3.1.0", "3.1.0", _DICT_VERSION_ANNOUNCEMENT)
834 def announcements(self):
835 """
836 Fetch currently active announcements.
837
838 Returns a list of :ref:`announcement dicts <announcement dicts>`.
839 """
840 return self.__api_request('GET', '/api/v1/announcements')
841
842 ##
843 # Reading data: Read markers
844 ##
845 @api_version("3.0.0", "3.0.0", _DICT_VERSION_MARKER)
846 def markers_get(self, timeline=["home"]):
847 """
848 Get the last-read-location markers for the specified timelines. Valid timelines
849 are the same as in :ref:`timeline() <timeline()>`
850
851 Note that despite the singular name, `timeline` can be a list.
852
853 Returns a dict of :ref:`read marker dicts <read marker dicts>`, keyed by timeline name.
854 """
855 if not isinstance(timeline, (list, tuple)):
856 timeline = [timeline]
857 params = self.__generate_params(locals())
858
859 return self.__api_request('GET', '/api/v1/markers', params)
860
861 ###
862 # Reading data: Bookmarks
863 ###
864 @api_version("3.1.0", "3.1.0", _DICT_VERSION_STATUS)
865 def bookmarks(self, max_id=None, min_id=None, since_id=None, limit=None):
866 """
867 Get a list of statuses bookmarked by the logged-in user.
868
869 Returns a list of :ref:`status dicts <status dicts>`.
870 """
871 if max_id is not None:
872 max_id = self.__unpack_id(max_id, dateconv=True)
873
874 if min_id is not None:
875 min_id = self.__unpack_id(min_id, dateconv=True)
876
877 if since_id is not None:
878 since_id = self.__unpack_id(since_id, dateconv=True)
879
880 params = self.__generate_params(locals())
881 return self.__api_request('GET', '/api/v1/bookmarks', params)
882
883 ###
884 # Writing data: Statuses
885 ###
886 def __status_internal(self, status, in_reply_to_id=None, media_ids=None,
887 sensitive=False, visibility=None, spoiler_text=None,
888 language=None, idempotency_key=None, content_type=None,
889 scheduled_at=None, poll=None, quote_id=None, edit=False):
890 if quote_id is not None:
891 if self.feature_set != "fedibird":
892 raise MastodonIllegalArgumentError('quote_id is only available with feature set fedibird')
893 quote_id = self.__unpack_id(quote_id)
894
895 if content_type is not None:
896 if self.feature_set != "pleroma":
897 raise MastodonIllegalArgumentError('content_type is only available with feature set pleroma')
898 # It would be better to read this from nodeinfo and cache, but this is easier
899 if not content_type in ["text/plain", "text/html", "text/markdown", "text/bbcode"]:
900 raise MastodonIllegalArgumentError('Invalid content type specified')
901
902 if in_reply_to_id is not None:
903 in_reply_to_id = self.__unpack_id(in_reply_to_id)
904
905 if scheduled_at is not None:
906 scheduled_at = self.__consistent_isoformat_utc(scheduled_at)
907
908 params_initial = locals()
909
910 # Validate poll/media exclusivity
911 if poll is not None:
912 if media_ids is not None and len(media_ids) != 0:
913 raise ValueError(
914 'Status can have media or poll attached - not both.')
915
916 # Validate visibility parameter
917 valid_visibilities = ['private', 'public', 'unlisted', 'direct']
918 if params_initial['visibility'] is None:
919 del params_initial['visibility']
920 else:
921 params_initial['visibility'] = params_initial['visibility'].lower()
922 if params_initial['visibility'] not in valid_visibilities:
923 raise ValueError('Invalid visibility value! Acceptable values are %s' % valid_visibilities)
924
925 if params_initial['language'] is None:
926 del params_initial['language']
927
928 if params_initial['sensitive'] is False:
929 del [params_initial['sensitive']]
930
931 headers = {}
932 if idempotency_key is not None:
933 headers['Idempotency-Key'] = idempotency_key
934
935 if media_ids is not None:
936 try:
937 media_ids_proper = []
938 if not isinstance(media_ids, (list, tuple)):
939 media_ids = [media_ids]
940 for media_id in media_ids:
941 media_ids_proper.append(self.__unpack_id(media_id))
942 except Exception as e:
943 raise MastodonIllegalArgumentError("Invalid media dict: %s" % e)
944
945 params_initial["media_ids"] = media_ids_proper
946
947 if params_initial['content_type'] is None:
948 del params_initial['content_type']
949
950 use_json = False
951 if poll is not None:
952 use_json = True
953
954 params = self.__generate_params(params_initial, ['idempotency_key', 'edit'])
955 if edit is None:
956 # Post
957 return self.__api_request('POST', '/api/v1/statuses', params, headers=headers, use_json=use_json)
958 else:
959 # Edit
960 return self.__api_request('PUT', '/api/v1/statuses/{0}'.format(str(self.__unpack_id(edit))), params, headers=headers, use_json=use_json)
961
962 @api_version("1.0.0", "2.8.0", _DICT_VERSION_STATUS)
963 def status_post(self, status, in_reply_to_id=None, media_ids=None,
964 sensitive=False, visibility=None, spoiler_text=None,
965 language=None, idempotency_key=None, content_type=None,
966 scheduled_at=None, poll=None, quote_id=None):
967 """
968 Post a status. Can optionally be in reply to another status and contain
969 media.
970
971 `media_ids` should be a list. (If it's not, the function will turn it
972 into one.) It can contain up to four pieces of media (uploaded via
973 :ref:`media_post() <media_post()>`). `media_ids` can also be the `media dicts`_ returned
974 by :ref:`media_post() <media_post()>` - they are unpacked automatically.
975
976 The `sensitive` boolean decides whether or not media attached to the post
977 should be marked as sensitive, which hides it by default on the Mastodon
978 web front-end.
979
980 The visibility parameter is a string value and accepts any of:
981 'direct' - post will be visible only to mentioned users
982 'private' - post will be visible only to followers
983 'unlisted' - post will be public but not appear on the public timeline
984 'public' - post will be public
985
986 If not passed in, visibility defaults to match the current account's
987 default-privacy setting (starting with Mastodon version 1.6) or its
988 locked setting - private if the account is locked, public otherwise
989 (for Mastodon versions lower than 1.6).
990
991 The `spoiler_text` parameter is a string to be shown as a warning before
992 the text of the status. If no text is passed in, no warning will be
993 displayed.
994
995 Specify `language` to override automatic language detection. The parameter
996 accepts all valid ISO 639-1 (2-letter) or for languages where that do not
997 have one, 639-3 (three letter) language codes.
998
999 You can set `idempotency_key` to a value to uniquely identify an attempt
1000 at posting a status. Even if you call this function more than once,
1001 if you call it with the same `idempotency_key`, only one status will
1002 be created.
1003
1004 Pass a datetime as `scheduled_at` to schedule the toot for a specific time
1005 (the time must be at least 5 minutes into the future). If this is passed,
1006 status_post returns a :ref:`scheduled status dict <scheduled status dict>` instead.
1007
1008 Pass `poll` to attach a poll to the status. An appropriate object can be
1009 constructed using :ref:`make_poll() <make_poll()>` . Note that as of Mastodon version
1010 2.8.2, you can only have either media or a poll attached, not both at
1011 the same time.
1012
1013 **Specific to "pleroma" feature set:**: Specify `content_type` to set
1014 the content type of your post on Pleroma. It accepts 'text/plain' (default),
1015 'text/markdown', 'text/html' and 'text/bbcode'. This parameter is not
1016 supported on Mastodon servers, but will be safely ignored if set.
1017
1018 **Specific to "fedibird" feature set:**: The `quote_id` parameter is
1019 a non-standard extension that specifies the id of a quoted status.
1020
1021 Returns a :ref:`status dict <status dict>` with the new status.
1022 """
1023 return self.__status_internal(
1024 status,
1025 in_reply_to_id,
1026 media_ids,
1027 sensitive,
1028 visibility,
1029 spoiler_text,
1030 language,
1031 idempotency_key,
1032 content_type,
1033 scheduled_at,
1034 poll,
1035 quote_id,
1036 edit=None
1037 )
1038
1039 @api_version("1.0.0", "2.8.0", _DICT_VERSION_STATUS)
1040 def toot(self, status):
1041 """
1042 Synonym for :ref:`status_post() <status_post()>` that only takes the status text as input.
1043
1044 Usage in production code is not recommended.
1045
1046 Returns a :ref:`status dict <status dict>` with the new status.
1047 """
1048 return self.status_post(status)
1049
1050 @api_version("3.5.0", "3.5.0", _DICT_VERSION_STATUS)
1051 def status_update(self, id, status = None, spoiler_text = None, sensitive = None, media_ids = None, poll = None):
1052 """
1053 Edit a status. The meanings of the fields are largely the same as in :ref:`status_post() <status_post()>`,
1054 though not every field can be edited.
1055
1056 Note that editing a poll will reset the votes.
1057 """
1058 return self.__status_internal(
1059 status = status,
1060 media_ids = media_ids,
1061 sensitive = sensitive,
1062 spoiler_text = spoiler_text,
1063 poll = poll,
1064 edit = id
1065 )
1066
1067 @api_version("3.5.0", "3.5.0", _DICT_VERSION_STATUS_EDIT)
1068 def status_history(self, id):
1069 """
1070 Returns the edit history of a status as a list of :ref:`status edit dicts <status edit dicts>`, starting
1071 from the original form. Note that this means that a status that has been edited
1072 once will have *two* entries in this list, a status that has been edited twice
1073 will have three, and so on.
1074 """
1075 id = self.__unpack_id(id)
1076 return self.__api_request('GET', "/api/v1/statuses/{0}/history".format(str(id)))
1077
1078 def status_source(self, id):
1079 """
1080 Returns the source of a status for editing.
1081
1082 Return value is a dictionary containing exactly the parameters you could pass to
1083 :ref:`status_update() <status_update()>` to change nothing about the status, except `status` is `text`
1084 instead.
1085 """
1086 id = self.__unpack_id(id)
1087 return self.__api_request('GET', "/api/v1/statuses/{0}/source".format(str(id)))
1088
1089 @api_version("1.0.0", "2.8.0", _DICT_VERSION_STATUS)
1090 def status_reply(self, to_status, status, in_reply_to_id=None, media_ids=None,
1091 sensitive=False, visibility=None, spoiler_text=None,
1092 language=None, idempotency_key=None, content_type=None,
1093 scheduled_at=None, poll=None, untag=False):
1094 """
1095 Helper function - acts like status_post, but prepends the name of all
1096 the users that are being replied to to the status text and retains
1097 CW and visibility if not explicitly overridden.
1098
1099 Set `untag` to True if you want the reply to only go to the user you
1100 are replying to, removing every other mentioned user from the
1101 conversation.
1102 """
1103 keyword_args = locals()
1104 del keyword_args["self"]
1105 del keyword_args["to_status"]
1106 del keyword_args["untag"]
1107
1108 user_id = self.__get_logged_in_id()
1109
1110 # Determine users to mention
1111 mentioned_accounts = collections.OrderedDict()
1112 mentioned_accounts[to_status.account.id] = to_status.account.acct
1113
1114 if not untag:
1115 for account in to_status.mentions:
1116 if account.id != user_id and not account.id in mentioned_accounts.keys():
1117 mentioned_accounts[account.id] = account.acct
1118
1119 # Join into one piece of text. The space is added inside because of self-replies.
1120 status = "".join(map(lambda x: "@" + x + " ",
1121 mentioned_accounts.values())) + status
1122
1123 # Retain visibility / cw
1124 if visibility is None and 'visibility' in to_status:
1125 visibility = to_status.visibility
1126 if spoiler_text is None and 'spoiler_text' in to_status:
1127 spoiler_text = to_status.spoiler_text
1128
1129 keyword_args["status"] = status
1130 keyword_args["visibility"] = visibility
1131 keyword_args["spoiler_text"] = spoiler_text
1132 keyword_args["in_reply_to_id"] = to_status.id
1133 return self.status_post(**keyword_args)
1134
1135 @api_version("2.8.0", "2.8.0", _DICT_VERSION_POLL)
1136 def make_poll(self, options, expires_in, multiple=False, hide_totals=False):
1137 """
1138 Generate a poll object that can be passed as the `poll` option when posting a status.
1139
1140 options is an array of strings with the poll options (Maximum, by default: 4),
1141 expires_in is the time in seconds for which the poll should be open.
1142 Set multiple to True to allow people to choose more than one answer. Set
1143 hide_totals to True to hide the results of the poll until it has expired.
1144 """
1145 poll_params = locals()
1146 del poll_params["self"]
1147 return poll_params
1148
1149 @api_version("1.0.0", "1.0.0", "1.0.0")
1150 def status_delete(self, id):
1151 """
1152 Delete a status
1153
1154 Returns the now-deleted status, with an added "source" attribute that contains
1155 the text that was used to compose this status (this can be used to power
1156 "delete and redraft" functionality)
1157 """
1158 id = self.__unpack_id(id)
1159 url = '/api/v1/statuses/{0}'.format(str(id))
1160 return self.__api_request('DELETE', url)
1161
1162 @api_version("1.0.0", "2.0.0", _DICT_VERSION_STATUS)
1163 def status_reblog(self, id, visibility=None):
1164 """
1165 Reblog / boost a status.
1166
1167 The visibility parameter functions the same as in :ref:`status_post() <status_post()>` and
1168 allows you to reduce the visibility of a reblogged status.
1169
1170 Returns a :ref:`status dict <status dict>` with a new status that wraps around the reblogged one.
1171 """
1172 params = self.__generate_params(locals(), ['id'])
1173 valid_visibilities = ['private', 'public', 'unlisted', 'direct']
1174 if 'visibility' in params:
1175 params['visibility'] = params['visibility'].lower()
1176 if params['visibility'] not in valid_visibilities:
1177 raise ValueError('Invalid visibility value! Acceptable '
1178 'values are %s' % valid_visibilities)
1179
1180 id = self.__unpack_id(id)
1181 url = '/api/v1/statuses/{0}/reblog'.format(str(id))
1182 return self.__api_request('POST', url, params)
1183
1184 @api_version("1.0.0", "2.0.0", _DICT_VERSION_STATUS)
1185 def status_unreblog(self, id):
1186 """
1187 Un-reblog a status.
1188
1189 Returns a :ref:`status dict <status dict>` with the status that used to be reblogged.
1190 """
1191 id = self.__unpack_id(id)
1192 url = '/api/v1/statuses/{0}/unreblog'.format(str(id))
1193 return self.__api_request('POST', url)
1194
1195 @api_version("1.0.0", "2.0.0", _DICT_VERSION_STATUS)
1196 def status_favourite(self, id):
1197 """
1198 Favourite a status.
1199
1200 Returns a :ref:`status dict <status dict>` with the favourited status.
1201 """
1202 id = self.__unpack_id(id)
1203 url = '/api/v1/statuses/{0}/favourite'.format(str(id))
1204 return self.__api_request('POST', url)
1205
1206 @api_version("1.0.0", "2.0.0", _DICT_VERSION_STATUS)
1207 def status_unfavourite(self, id):
1208 """
1209 Un-favourite a status.
1210
1211 Returns a :ref:`status dict <status dict>` with the un-favourited status.
1212 """
1213 id = self.__unpack_id(id)
1214 url = '/api/v1/statuses/{0}/unfavourite'.format(str(id))
1215 return self.__api_request('POST', url)
1216
1217 @api_version("1.4.0", "2.0.0", _DICT_VERSION_STATUS)
1218 def status_mute(self, id):
1219 """
1220 Mute notifications for a status.
1221
1222 Returns a :ref:`status dict <status dict>` with the now muted status
1223 """
1224 id = self.__unpack_id(id)
1225 url = '/api/v1/statuses/{0}/mute'.format(str(id))
1226 return self.__api_request('POST', url)
1227
1228 @api_version("1.4.0", "2.0.0", _DICT_VERSION_STATUS)
1229 def status_unmute(self, id):
1230 """
1231 Unmute notifications for a status.
1232
1233 Returns a :ref:`status dict <status dict>` with the status that used to be muted.
1234 """
1235 id = self.__unpack_id(id)
1236 url = '/api/v1/statuses/{0}/unmute'.format(str(id))
1237 return self.__api_request('POST', url)
1238
1239 @api_version("2.1.0", "2.1.0", _DICT_VERSION_STATUS)
1240 def status_pin(self, id):
1241 """
1242 Pin a status for the logged-in user.
1243
1244 Returns a :ref:`status dict <status dict>` with the now pinned status
1245 """
1246 id = self.__unpack_id(id)
1247 url = '/api/v1/statuses/{0}/pin'.format(str(id))
1248 return self.__api_request('POST', url)
1249
1250 @api_version("2.1.0", "2.1.0", _DICT_VERSION_STATUS)
1251 def status_unpin(self, id):
1252 """
1253 Unpin a pinned status for the logged-in user.
1254
1255 Returns a :ref:`status dict <status dict>` with the status that used to be pinned.
1256 """
1257 id = self.__unpack_id(id)
1258 url = '/api/v1/statuses/{0}/unpin'.format(str(id))
1259 return self.__api_request('POST', url)
1260
1261 @api_version("3.1.0", "3.1.0", _DICT_VERSION_STATUS)
1262 def status_bookmark(self, id):
1263 """
1264 Bookmark a status as the logged-in user.
1265
1266 Returns a :ref:`status dict <status dict>` with the now bookmarked status
1267 """
1268 id = self.__unpack_id(id)
1269 url = '/api/v1/statuses/{0}/bookmark'.format(str(id))
1270 return self.__api_request('POST', url)
1271
1272 @api_version("3.1.0", "3.1.0", _DICT_VERSION_STATUS)
1273 def status_unbookmark(self, id):
1274 """
1275 Unbookmark a bookmarked status for the logged-in user.
1276
1277 Returns a :ref:`status dict <status dict>` with the status that used to be bookmarked.
1278 """
1279 id = self.__unpack_id(id)
1280 url = '/api/v1/statuses/{0}/unbookmark'.format(str(id))
1281 return self.__api_request('POST', url)
1282
1283 ###
1284 # Writing data: Scheduled statuses
1285 ###
1286 @api_version("2.7.0", "2.7.0", _DICT_VERSION_SCHEDULED_STATUS)
1287 def scheduled_status_update(self, id, scheduled_at):
1288 """
1289 Update the scheduled time of a scheduled status.
1290
1291 New time must be at least 5 minutes into the future.
1292
1293 Returns a :ref:`scheduled status dict <scheduled status dict>`
1294 """
1295 scheduled_at = self.__consistent_isoformat_utc(scheduled_at)
1296 id = self.__unpack_id(id)
1297 params = self.__generate_params(locals(), ['id'])
1298 url = '/api/v1/scheduled_statuses/{0}'.format(str(id))
1299 return self.__api_request('PUT', url, params)
1300
1301 @api_version("2.7.0", "2.7.0", "2.7.0")
1302 def scheduled_status_delete(self, id):
1303 """
1304 Deletes a scheduled status.
1305 """
1306 id = self.__unpack_id(id)
1307 url = '/api/v1/scheduled_statuses/{0}'.format(str(id))
1308 self.__api_request('DELETE', url)
1309
1310 ###
1311 # Writing data: Polls
1312 ###
1313 @api_version("2.8.0", "2.8.0", _DICT_VERSION_POLL)
1314 def poll_vote(self, id, choices):
1315 """
1316 Vote in the given poll.
1317
1318 `choices` is the index of the choice you wish to register a vote for
1319 (i.e. its index in the corresponding polls `options` field. In case
1320 of a poll that allows selection of more than one option, a list of
1321 indices can be passed.
1322
1323 You can only submit choices for any given poll once in case of
1324 single-option polls, or only once per option in case of multi-option
1325 polls.
1326
1327 Returns the updated :ref:`poll dict <poll dict>`
1328 """
1329 id = self.__unpack_id(id)
1330 if not isinstance(choices, list):
1331 choices = [choices]
1332 params = self.__generate_params(locals(), ['id'])
1333
1334 url = '/api/v1/polls/{0}/votes'.format(id)
1335 self.__api_request('POST', url, params)
1336
1337 ###
1338 # Writing data: Notifications
1339 ###
1340
1341 @api_version("1.0.0", "1.0.0", "1.0.0")
1342 def notifications_clear(self):
1343 """
1344 Clear out a user's notifications
1345 """
1346 self.__api_request('POST', '/api/v1/notifications/clear')
1347
1348 @api_version("1.3.0", "2.9.2", "2.9.2")
1349 def notifications_dismiss(self, id):
1350 """
1351 Deletes a single notification
1352 """
1353 id = self.__unpack_id(id)
1354
1355 if self.verify_minimum_version("2.9.2", cached=True):
1356 url = '/api/v1/notifications/{0}/dismiss'.format(str(id))
1357 self.__api_request('POST', url)
1358 else:
1359 params = self.__generate_params(locals())
1360 self.__api_request('POST', '/api/v1/notifications/dismiss', params)
1361
1362 ###
1363 # Writing data: Conversations
1364 ###
1365 @api_version("2.6.0", "2.6.0", _DICT_VERSION_CONVERSATION)
1366 def conversations_read(self, id):
1367 """
1368 Marks a single conversation as read.
1369
1370 Returns the updated :ref:`conversation dict <conversation dict>`.
1371 """
1372 id = self.__unpack_id(id)
1373 url = '/api/v1/conversations/{0}/read'.format(str(id))
1374 return self.__api_request('POST', url)
1375
1376 ###
1377 # Writing data: Accounts
1378 ###
1379 @api_version("1.0.0", "3.3.0", _DICT_VERSION_RELATIONSHIP)
1380 def account_follow(self, id, reblogs=True, notify=False):
1381 """
1382 Follow a user.
1383
1384 Set `reblogs` to False to hide boosts by the followed user.
1385 Set `notify` to True to get a notification every time the followed user posts.
1386
1387 Returns a :ref:`relationship dict <relationship dict>` containing the updated relationship to the user.
1388 """
1389 id = self.__unpack_id(id)
1390 params = self.__generate_params(locals(), ["id"])
1391
1392 if params["reblogs"] is None:
1393 del params["reblogs"]
1394
1395 url = '/api/v1/accounts/{0}/follow'.format(str(id))
1396 return self.__api_request('POST', url, params)
1397
1398 @api_version("1.0.0", "2.1.0", _DICT_VERSION_ACCOUNT)
1399 def follows(self, uri):
1400 """
1401 Follow a remote user by uri (username@domain).
1402
1403 Returns a :ref:`account dict <account dict>`.
1404 """
1405 params = self.__generate_params(locals())
1406 return self.__api_request('POST', '/api/v1/follows', params)
1407
1408 @api_version("1.0.0", "1.4.0", _DICT_VERSION_RELATIONSHIP)
1409 def account_unfollow(self, id):
1410 """
1411 Unfollow a user.
1412
1413 Returns a :ref:`relationship dict <relationship dict>` containing the updated relationship to the user.
1414 """
1415 id = self.__unpack_id(id)
1416 return self.__api_request('POST', '/api/v1/accounts/{0}/unfollow'.format(str(id)))
1417
1418 @api_version("3.5.0", "3.5.0", _DICT_VERSION_RELATIONSHIP)
1419 def account_remove_from_followers(self, id):
1420 """
1421 Remove a user from the logged in users followers (i.e. make them unfollow the logged in
1422 user / "softblock" them).
1423
1424 Returns a :ref:`relationship dict <relationship dict>` reflecting the updated following status.
1425 """
1426 id = self.__unpack_id(id)
1427 return self.__api_request('POST', '/api/v1/accounts/{0}/remove_from_followers'.format(str(id)))
1428
1429
1430 @api_version("1.0.0", "1.4.0", _DICT_VERSION_RELATIONSHIP)
1431 def account_block(self, id):
1432 """
1433 Block a user.
1434
1435 Returns a :ref:`relationship dict <relationship dict>` containing the updated relationship to the user.
1436 """
1437 id = self.__unpack_id(id)
1438 url = '/api/v1/accounts/{0}/block'.format(str(id))
1439 return self.__api_request('POST', url)
1440
1441 @api_version("1.0.0", "1.4.0", _DICT_VERSION_RELATIONSHIP)
1442 def account_unblock(self, id):
1443 """
1444 Unblock a user.
1445
1446 Returns a :ref:`relationship dict <relationship dict>` containing the updated relationship to the user.
1447 """
1448 id = self.__unpack_id(id)
1449 url = '/api/v1/accounts/{0}/unblock'.format(str(id))
1450 return self.__api_request('POST', url)
1451
1452 @api_version("1.1.0", "2.4.3", _DICT_VERSION_RELATIONSHIP)
1453 def account_mute(self, id, notifications=True, duration=None):
1454 """
1455 Mute a user.
1456
1457 Set `notifications` to False to receive notifications even though the user is
1458 muted from timelines. Pass a `duration` in seconds to have Mastodon automatically
1459 lift the mute after that many seconds.
1460
1461 Returns a :ref:`relationship dict <relationship dict>` containing the updated relationship to the user.
1462 """
1463 id = self.__unpack_id(id)
1464 params = self.__generate_params(locals(), ['id'])
1465 url = '/api/v1/accounts/{0}/mute'.format(str(id))
1466 return self.__api_request('POST', url, params)
1467
1468 @api_version("1.1.0", "1.4.0", _DICT_VERSION_RELATIONSHIP)
1469 def account_unmute(self, id):
1470 """
1471 Unmute a user.
1472
1473 Returns a :ref:`relationship dict <relationship dict>` containing the updated relationship to the user.
1474 """
1475 id = self.__unpack_id(id)
1476 url = '/api/v1/accounts/{0}/unmute'.format(str(id))
1477 return self.__api_request('POST', url)
1478
1479 @api_version("1.1.1", "3.1.0", _DICT_VERSION_ACCOUNT)
1480 def account_update_credentials(self, display_name=None, note=None,
1481 avatar=None, avatar_mime_type=None,
1482 header=None, header_mime_type=None,
1483 locked=None, bot=None,
1484 discoverable=None, fields=None):
1485 """
1486 Update the profile for the currently logged-in user.
1487
1488 `note` is the user's bio.
1489
1490 `avatar` and 'header' are images. As with media uploads, it is possible to either
1491 pass image data and a mime type, or a filename of an image file, for either.
1492
1493 `locked` specifies whether the user needs to manually approve follow requests.
1494
1495 `bot` specifies whether the user should be set to a bot.
1496
1497 `discoverable` specifies whether the user should appear in the user directory.
1498
1499 `fields` can be a list of up to four name-value pairs (specified as tuples) to
1500 appear as semi-structured information in the user's profile.
1501
1502 Returns the updated `account dict` of the logged-in user.
1503 """
1504 params_initial = collections.OrderedDict(locals())
1505
1506 # Convert fields
1507 if fields is not None:
1508 if len(fields) > 4:
1509 raise MastodonIllegalArgumentError(
1510 'A maximum of four fields are allowed.')
1511
1512 fields_attributes = []
1513 for idx, (field_name, field_value) in enumerate(fields):
1514 params_initial['fields_attributes[' +
1515 str(idx) + '][name]'] = field_name
1516 params_initial['fields_attributes[' +
1517 str(idx) + '][value]'] = field_value
1518
1519 # Clean up params
1520 for param in ["avatar", "avatar_mime_type", "header", "header_mime_type", "fields"]:
1521 if param in params_initial:
1522 del params_initial[param]
1523
1524 # Create file info
1525 files = {}
1526 if avatar is not None:
1527 files["avatar"] = self.__load_media_file(avatar, avatar_mime_type)
1528 if header is not None:
1529 files["header"] = self.__load_media_file(header, header_mime_type)
1530
1531 params = self.__generate_params(params_initial)
1532 return self.__api_request('PATCH', '/api/v1/accounts/update_credentials', params, files=files)
1533
1534 @api_version("2.5.0", "2.5.0", _DICT_VERSION_RELATIONSHIP)
1535 def account_pin(self, id):
1536 """
1537 Pin / endorse a user.
1538
1539 Returns a :ref:`relationship dict <relationship dict>` containing the updated relationship to the user.
1540 """
1541 id = self.__unpack_id(id)
1542 url = '/api/v1/accounts/{0}/pin'.format(str(id))
1543 return self.__api_request('POST', url)
1544
1545 @api_version("2.5.0", "2.5.0", _DICT_VERSION_RELATIONSHIP)
1546 def account_unpin(self, id):
1547 """
1548 Unpin / un-endorse a user.
1549
1550 Returns a :ref:`relationship dict <relationship dict>` containing the updated relationship to the user.
1551 """
1552 id = self.__unpack_id(id)
1553 url = '/api/v1/accounts/{0}/unpin'.format(str(id))
1554 return self.__api_request('POST', url)
1555
1556 @api_version("3.2.0", "3.2.0", _DICT_VERSION_RELATIONSHIP)
1557 def account_note_set(self, id, comment):
1558 """
1559 Set a note (visible to the logged in user only) for the given account.
1560
1561 Returns a :ref:`status dict <status dict>` with the `note` updated.
1562 """
1563 id = self.__unpack_id(id)
1564 params = self.__generate_params(locals(), ["id"])
1565 return self.__api_request('POST', '/api/v1/accounts/{0}/note'.format(str(id)), params)
1566
1567 @api_version("3.3.0", "3.3.0", _DICT_VERSION_HASHTAG)
1568 def account_featured_tags(self, id):
1569 """
1570 Get an account's featured hashtags.
1571
1572 Returns a list of :ref:`hashtag dicts <hashtag dicts>` (NOT `featured tag dicts`_).
1573 """
1574 id = self.__unpack_id(id)
1575 return self.__api_request('GET', '/api/v1/accounts/{0}/featured_tags'.format(str(id)))
1576
1577 ###
1578 # Writing data: Featured hashtags
1579 ###
1580 @api_version("3.0.0", "3.0.0", _DICT_VERSION_FEATURED_TAG)
1581 def featured_tag_create(self, name):
1582 """
1583 Creates a new featured hashtag displayed on the logged-in user's profile.
1584
1585 Returns a :ref:`featured tag dict <featured tag dict>` with the newly featured tag.
1586 """
1587 params = self.__generate_params(locals())
1588 return self.__api_request('POST', '/api/v1/featured_tags', params)
1589
1590 @api_version("3.0.0", "3.0.0", _DICT_VERSION_FEATURED_TAG)
1591 def featured_tag_delete(self, id):
1592 """
1593 Deletes one of the logged-in user's featured hashtags.
1594 """
1595 id = self.__unpack_id(id)
1596 url = '/api/v1/featured_tags/{0}'.format(str(id))
1597 self.__api_request('DELETE', url)
1598
1599 ###
1600 # Writing data: Keyword filters
1601 ###
1602 @api_version("2.4.3", "2.4.3", _DICT_VERSION_FILTER)
1603 def filter_create(self, phrase, context, irreversible=False, whole_word=True, expires_in=None):
1604 """
1605 Creates a new keyword filter. `phrase` is the phrase that should be
1606 filtered out, `context` specifies from where to filter the keywords.
1607 Valid contexts are 'home', 'notifications', 'public' and 'thread'.
1608
1609 Set `irreversible` to True if you want the filter to just delete statuses
1610 server side. This works only for the 'home' and 'notifications' contexts.
1611
1612 Set `whole_word` to False if you want to allow filter matches to
1613 start or end within a word, not only at word boundaries.
1614
1615 Set `expires_in` to specify for how many seconds the filter should be
1616 kept around.
1617
1618 Returns the :ref:`filter dict <filter dict>` of the newly created filter.
1619 """
1620 params = self.__generate_params(locals())
1621
1622 for context_val in context:
1623 if not context_val in ['home', 'notifications', 'public', 'thread']:
1624 raise MastodonIllegalArgumentError('Invalid filter context.')
1625
1626 return self.__api_request('POST', '/api/v1/filters', params)
1627
1628 @api_version("2.4.3", "2.4.3", _DICT_VERSION_FILTER)
1629 def filter_update(self, id, phrase=None, context=None, irreversible=None, whole_word=None, expires_in=None):
1630 """
1631 Updates the filter with the given `id`. Parameters are the same
1632 as in `filter_create()`.
1633
1634 Returns the :ref:`filter dict <filter dict>` of the updated filter.
1635 """
1636 id = self.__unpack_id(id)
1637 params = self.__generate_params(locals(), ['id'])
1638 url = '/api/v1/filters/{0}'.format(str(id))
1639 return self.__api_request('PUT', url, params)
1640
1641 @api_version("2.4.3", "2.4.3", "2.4.3")
1642 def filter_delete(self, id):
1643 """
1644 Deletes the filter with the given `id`.
1645 """
1646 id = self.__unpack_id(id)
1647 url = '/api/v1/filters/{0}'.format(str(id))
1648 self.__api_request('DELETE', url)
1649
1650 ###
1651 # Writing data: Follow suggestions
1652 ###
1653 @api_version("2.4.3", "2.4.3", _DICT_VERSION_ACCOUNT)
1654 def suggestion_delete(self, account_id):
1655 """
1656 Remove the user with the given `account_id` from the follow suggestions.
1657 """
1658 account_id = self.__unpack_id(account_id)
1659 url = '/api/v1/suggestions/{0}'.format(str(account_id))
1660 self.__api_request('DELETE', url)
1661
1662 ###
1663 # Writing data: Lists
1664 ###
1665 @api_version("2.1.0", "2.1.0", _DICT_VERSION_LIST)
1666 def list_create(self, title):
1667 """
1668 Create a new list with the given `title`.
1669
1670 Returns the :ref:`list dict <list dict>` of the created list.
1671 """
1672 params = self.__generate_params(locals())
1673 return self.__api_request('POST', '/api/v1/lists', params)
1674
1675 @api_version("2.1.0", "2.1.0", _DICT_VERSION_LIST)
1676 def list_update(self, id, title):
1677 """
1678 Update info about a list, where "info" is really the lists `title`.
1679
1680 Returns the :ref:`list dict <list dict>` of the modified list.
1681 """
1682 id = self.__unpack_id(id)
1683 params = self.__generate_params(locals(), ['id'])
1684 return self.__api_request('PUT', '/api/v1/lists/{0}'.format(id), params)
1685
1686 @api_version("2.1.0", "2.1.0", "2.1.0")
1687 def list_delete(self, id):
1688 """
1689 Delete a list.
1690 """
1691 id = self.__unpack_id(id)
1692 self.__api_request('DELETE', '/api/v1/lists/{0}'.format(id))
1693
1694 @api_version("2.1.0", "2.1.0", "2.1.0")
1695 def list_accounts_add(self, id, account_ids):
1696 """
1697 Add the account(s) given in `account_ids` to the list.
1698 """
1699 id = self.__unpack_id(id)
1700
1701 if not isinstance(account_ids, list):
1702 account_ids = [account_ids]
1703 account_ids = list(map(lambda x: self.__unpack_id(x), account_ids))
1704
1705 params = self.__generate_params(locals(), ['id'])
1706 self.__api_request(
1707 'POST', '/api/v1/lists/{0}/accounts'.format(id), params)
1708
1709 @api_version("2.1.0", "2.1.0", "2.1.0")
1710 def list_accounts_delete(self, id, account_ids):
1711 """
1712 Remove the account(s) given in `account_ids` from the list.
1713 """
1714 id = self.__unpack_id(id)
1715
1716 if not isinstance(account_ids, list):
1717 account_ids = [account_ids]
1718 account_ids = list(map(lambda x: self.__unpack_id(x), account_ids))
1719
1720 params = self.__generate_params(locals(), ['id'])
1721 self.__api_request(
1722 'DELETE', '/api/v1/lists/{0}/accounts'.format(id), params)
1723
1724 ###
1725 # Writing data: Reports
1726 ###
1727 @api_version("1.1.0", "3.5.0", _DICT_VERSION_REPORT)
1728 def report(self, account_id, status_ids=None, comment=None, forward=False, category=None, rule_ids=None):
1729 """
1730 Report statuses to the instances administrators.
1731
1732 Accepts a list of toot IDs associated with the report, and a comment.
1733
1734 Starting with Mastodon 3.5.0, you can also pass a `category` (one out of
1735 "spam", "violation" or "other") and `rule_ids` (a list of rule IDs corresponding
1736 to the rules returned by the :ref:`instance() <instance()>` API).
1737
1738 Set `forward` to True to forward a report of a remote user to that users
1739 instance as well as sending it to the instance local administrators.
1740
1741 Returns a :ref:`report dict <report dict>`.
1742 """
1743 if category is not None and not category in ["spam", "violation", "other"]:
1744 raise MastodonIllegalArgumentError("Invalid report category (must be spam, violation or other)")
1745
1746 account_id = self.__unpack_id(account_id)
1747
1748 if status_ids is not None:
1749 if not isinstance(status_ids, list):
1750 status_ids = [status_ids]
1751 status_ids = list(map(lambda x: self.__unpack_id(x), status_ids))
1752
1753 params_initial = locals()
1754 if not forward:
1755 del params_initial['forward']
1756
1757 params = self.__generate_params(params_initial)
1758 return self.__api_request('POST', '/api/v1/reports/', params)
1759
1760 ###
1761 # Writing data: Follow requests
1762 ###
1763 @api_version("1.0.0", "3.0.0", _DICT_VERSION_RELATIONSHIP)
1764 def follow_request_authorize(self, id):
1765 """
1766 Accept an incoming follow request.
1767
1768 Returns the updated :ref:`relationship dict <relationship dict>` for the requesting account.
1769 """
1770 id = self.__unpack_id(id)
1771 url = '/api/v1/follow_requests/{0}/authorize'.format(str(id))
1772 return self.__api_request('POST', url)
1773
1774 @api_version("1.0.0", "3.0.0", _DICT_VERSION_RELATIONSHIP)
1775 def follow_request_reject(self, id):
1776 """
1777 Reject an incoming follow request.
1778
1779 Returns the updated :ref:`relationship dict <relationship dict>` for the requesting account.
1780 """
1781 id = self.__unpack_id(id)
1782 url = '/api/v1/follow_requests/{0}/reject'.format(str(id))
1783 return self.__api_request('POST', url)
1784
1785 ###
1786 # Writing data: Media
1787 ###
1788 @api_version("1.0.0", "3.2.0", _DICT_VERSION_MEDIA)
1789 def media_post(self, media_file, mime_type=None, description=None, focus=None, file_name=None, thumbnail=None, thumbnail_mime_type=None, synchronous=False):
1790 """
1791 Post an image, video or audio file. `media_file` can either be data or
1792 a file name. If data is passed directly, the mime type has to be specified
1793 manually, otherwise, it is determined from the file name. `focus` should be a tuple
1794 of floats between -1 and 1, giving the x and y coordinates of the images
1795 focus point for cropping (with the origin being the images center).
1796
1797 Throws a `MastodonIllegalArgumentError` if the mime type of the
1798 passed data or file can not be determined properly.
1799
1800 `file_name` can be specified to upload a file with the given name,
1801 which is ignored by Mastodon, but some other Fediverse server software
1802 will display it. If no name is specified, a random name will be generated.
1803 The filename of a file specified in media_file will be ignored.
1804
1805 Starting with Mastodon 3.2.0, `thumbnail` can be specified in the same way as `media_file`
1806 to upload a custom thumbnail image for audio and video files.
1807
1808 Returns a :ref:`media dict <media dict>`. This contains the id that can be used in
1809 status_post to attach the media file to a toot.
1810
1811 When using the v2 API (post Mastodon version 3.1.4), the `url` in the
1812 returned dict will be `null`, since attachments are processed
1813 asynchronously. You can fetch an updated dict using `media`. Pass
1814 "synchronous" to emulate the old behaviour. Not recommended, inefficient
1815 and deprecated, you know the deal.
1816 """
1817 files = {'file': self.__load_media_file(
1818 media_file, mime_type, file_name)}
1819
1820 if focus is not None:
1821 focus = str(focus[0]) + "," + str(focus[1])
1822
1823 if thumbnail is not None:
1824 if not self.verify_minimum_version("3.2.0", cached=True):
1825 raise MastodonVersionError(
1826 'Thumbnail requires version > 3.2.0')
1827 files["thumbnail"] = self.__load_media_file(
1828 thumbnail, thumbnail_mime_type)
1829
1830 # Disambiguate URL by version
1831 if self.verify_minimum_version("3.1.4", cached=True):
1832 ret_dict = self.__api_request(
1833 'POST', '/api/v2/media', files=files, params={'description': description, 'focus': focus})
1834 else:
1835 ret_dict = self.__api_request(
1836 'POST', '/api/v1/media', files=files, params={'description': description, 'focus': focus})
1837
1838 # Wait for processing?
1839 if synchronous:
1840 if self.verify_minimum_version("3.1.4"):
1841 while not "url" in ret_dict or ret_dict.url is None:
1842 try:
1843 ret_dict = self.media(ret_dict)
1844 time.sleep(1.0)
1845 except:
1846 raise MastodonAPIError(
1847 "Attachment could not be processed")
1848 else:
1849 # Old version always waits
1850 return ret_dict
1851
1852 return ret_dict
1853
1854 @api_version("2.3.0", "3.2.0", _DICT_VERSION_MEDIA)
1855 def media_update(self, id, description=None, focus=None, thumbnail=None, thumbnail_mime_type=None):
1856 """
1857 Update the metadata of the media file with the given `id`. `description` and
1858 `focus` and `thumbnail` are as in :ref:`media_post() <media_post()>` .
1859
1860 Returns the updated :ref:`media dict <media dict>`.
1861 """
1862 id = self.__unpack_id(id)
1863
1864 if focus is not None:
1865 focus = str(focus[0]) + "," + str(focus[1])
1866
1867 params = self.__generate_params(
1868 locals(), ['id', 'thumbnail', 'thumbnail_mime_type'])
1869
1870 if thumbnail is not None:
1871 if not self.verify_minimum_version("3.2.0", cached=True):
1872 raise MastodonVersionError(
1873 'Thumbnail requires version > 3.2.0')
1874 files = {"thumbnail": self.__load_media_file(
1875 thumbnail, thumbnail_mime_type)}
1876 return self.__api_request('PUT', '/api/v1/media/{0}'.format(str(id)), params, files=files)
1877 else:
1878 return self.__api_request('PUT', '/api/v1/media/{0}'.format(str(id)), params)
1879
1880 @api_version("3.1.4", "3.1.4", _DICT_VERSION_MEDIA)
1881 def media(self, id):
1882 """
1883 Get the updated JSON for one non-attached / in progress media upload belonging
1884 to the logged-in user.
1885 """
1886 id = self.__unpack_id(id)
1887 return self.__api_request('GET', '/api/v1/media/{0}'.format(str(id)))
1888
1889 ###
1890 # Writing data: Domain blocks
1891 ###
1892 @api_version("1.4.0", "1.4.0", "1.4.0")
1893 def domain_block(self, domain=None):
1894 """
1895 Add a block for all statuses originating from the specified domain for the logged-in user.
1896 """
1897 params = self.__generate_params(locals())
1898 self.__api_request('POST', '/api/v1/domain_blocks', params)
1899
1900 @api_version("1.4.0", "1.4.0", "1.4.0")
1901 def domain_unblock(self, domain=None):
1902 """
1903 Remove a domain block for the logged-in user.
1904 """
1905 params = self.__generate_params(locals())
1906 self.__api_request('DELETE', '/api/v1/domain_blocks', params)
1907
1908 ##
1909 # Writing data: Read markers
1910 ##
1911 @api_version("3.0.0", "3.0.0", _DICT_VERSION_MARKER)
1912 def markers_set(self, timelines, last_read_ids):
1913 """
1914 Set the "last read" marker(s) for the given timeline(s) to the given id(s)
1915
1916 Note that if you give an invalid timeline name, this will silently do nothing.
1917
1918 Returns a dict with the updated :ref:`read marker dicts <read marker dicts>`, keyed by timeline name.
1919 """
1920 if not isinstance(timelines, (list, tuple)):
1921 timelines = [timelines]
1922
1923 if not isinstance(last_read_ids, (list, tuple)):
1924 last_read_ids = [last_read_ids]
1925
1926 if len(last_read_ids) != len(timelines):
1927 raise MastodonIllegalArgumentError(
1928 "Number of specified timelines and ids must be the same")
1929
1930 params = collections.OrderedDict()
1931 for timeline, last_read_id in zip(timelines, last_read_ids):
1932 params[timeline] = collections.OrderedDict()
1933 params[timeline]["last_read_id"] = self.__unpack_id(last_read_id)
1934
1935 return self.__api_request('POST', '/api/v1/markers', params, use_json=True)
1936
1937 ###
1938 # Writing data: Push subscriptions
1939 ###
1940 @api_version("2.4.0", "2.4.0", _DICT_VERSION_PUSH)
1941 def push_subscription_set(self, endpoint, encrypt_params, follow_events=None,
1942 favourite_events=None, reblog_events=None,
1943 mention_events=None, poll_events=None,
1944 follow_request_events=None, status_events=None, policy='all'):
1945 """
1946 Sets up or modifies the push subscription the logged-in user has for this app.
1947
1948 `endpoint` is the endpoint URL mastodon should call for pushes. Note that mastodon
1949 requires https for this URL. `encrypt_params` is a dict with key parameters that allow
1950 the server to encrypt data for you: A public key `pubkey` and a shared secret `auth`.
1951 You can generate this as well as the corresponding private key using the
1952 :ref:`push_subscription_generate_keys() <push_subscription_generate_keys()>` function.
1953
1954 `policy` controls what sources will generate webpush events. Valid values are
1955 `all`, `none`, `follower` and `followed`.
1956
1957 The rest of the parameters controls what kind of events you wish to subscribe to.
1958
1959 Returns a :ref:`push subscription dict <push subscription dict>`.
1960 """
1961 if not policy in ['all', 'none', 'follower', 'followed']:
1962 raise MastodonIllegalArgumentError("Valid values for policy are 'all', 'none', 'follower' or 'followed'.")
1963
1964 endpoint = Mastodon.__protocolize(endpoint)
1965
1966 push_pubkey_b64 = base64.b64encode(encrypt_params['pubkey'])
1967 push_auth_b64 = base64.b64encode(encrypt_params['auth'])
1968
1969 params = {
1970 'subscription[endpoint]': endpoint,
1971 'subscription[keys][p256dh]': push_pubkey_b64,
1972 'subscription[keys][auth]': push_auth_b64,
1973 'policy': policy
1974 }
1975
1976 if follow_events is not None:
1977 params['data[alerts][follow]'] = follow_events
1978
1979 if favourite_events is not None:
1980 params['data[alerts][favourite]'] = favourite_events
1981
1982 if reblog_events is not None:
1983 params['data[alerts][reblog]'] = reblog_events
1984
1985 if mention_events is not None:
1986 params['data[alerts][mention]'] = mention_events
1987
1988 if poll_events is not None:
1989 params['data[alerts][poll]'] = poll_events
1990
1991 if follow_request_events is not None:
1992 params['data[alerts][follow_request]'] = follow_request_events
1993
1994 if follow_request_events is not None:
1995 params['data[alerts][status]'] = status_events
1996
1997 # Canonicalize booleans
1998 params = self.__generate_params(params)
1999
2000 return self.__api_request('POST', '/api/v1/push/subscription', params)
2001
2002 @api_version("2.4.0", "2.4.0", _DICT_VERSION_PUSH)
2003 def push_subscription_update(self, follow_events=None,
2004 favourite_events=None, reblog_events=None,
2005 mention_events=None, poll_events=None,
2006 follow_request_events=None):
2007 """
2008 Modifies what kind of events the app wishes to subscribe to.
2009
2010 Returns the updated :ref:`push subscription dict <push subscription dict>`.
2011 """
2012 params = {}
2013
2014 if follow_events is not None:
2015 params['data[alerts][follow]'] = follow_events
2016
2017 if favourite_events is not None:
2018 params['data[alerts][favourite]'] = favourite_events
2019
2020 if reblog_events is not None:
2021 params['data[alerts][reblog]'] = reblog_events
2022
2023 if mention_events is not None:
2024 params['data[alerts][mention]'] = mention_events
2025
2026 if poll_events is not None:
2027 params['data[alerts][poll]'] = poll_events
2028
2029 if follow_request_events is not None:
2030 params['data[alerts][follow_request]'] = follow_request_events
2031
2032 # Canonicalize booleans
2033 params = self.__generate_params(params)
2034
2035 return self.__api_request('PUT', '/api/v1/push/subscription', params)
2036
2037 @api_version("2.4.0", "2.4.0", "2.4.0")
2038 def push_subscription_delete(self):
2039 """
2040 Remove the current push subscription the logged-in user has for this app.
2041 """
2042 self.__api_request('DELETE', '/api/v1/push/subscription')
2043
2044 ###
2045 # Writing data: Annoucements
2046 ###
2047 @api_version("3.1.0", "3.1.0", "3.1.0")
2048 def announcement_dismiss(self, id):
2049 """
2050 Set the given annoucement to read.
2051 """
2052 id = self.__unpack_id(id)
2053
2054 url = '/api/v1/announcements/{0}/dismiss'.format(str(id))
2055 self.__api_request('POST', url)
2056
2057 @api_version("3.1.0", "3.1.0", "3.1.0")
2058 def announcement_reaction_create(self, id, reaction):
2059 """
2060 Add a reaction to an announcement. `reaction` can either be a unicode emoji
2061 or the name of one of the instances custom emoji.
2062
2063 Will throw an API error if the reaction name is not one of the allowed things
2064 or when trying to add a reaction that the user has already added (adding a
2065 reaction that a different user added is legal and increments the count).
2066 """
2067 id = self.__unpack_id(id)
2068
2069 url = '/api/v1/announcements/{0}/reactions/{1}'.format(
2070 str(id), reaction)
2071 self.__api_request('PUT', url)
2072
2073 @api_version("3.1.0", "3.1.0", "3.1.0")
2074 def announcement_reaction_delete(self, id, reaction):
2075 """
2076 Remove a reaction to an announcement.
2077
2078 Will throw an API error if the reaction does not exist.
2079 """
2080 id = self.__unpack_id(id)
2081
2082 url = '/api/v1/announcements/{0}/reactions/{1}'.format(
2083 str(id), reaction)
2084 self.__api_request('DELETE', url)
2085
2086 ###
2087 # Moderation API
2088 ###
2089 @api_version("2.9.1", "4.0.0", _DICT_VERSION_ADMIN_ACCOUNT)
2090 def admin_accounts_v2(self, origin=None, by_domain=None, status=None, username=None, display_name=None, email=None, ip=None,
2091 permissions=None, invited_by=None, role_ids=None, max_id=None, min_id=None, since_id=None, limit=None):
2092 """
2093 Fetches a list of accounts that match given criteria. By default, local accounts are returned.
2094
2095 * Set `origin` to "local" or "remote" to get only local or remote accounts.
2096 * Set `by_domain` to a domain to get only accounts from that domain.
2097 * Set `status` to one of "active", "pending", "disabled", "silenced" or "suspended" to get only accounts with that moderation status (default: active)
2098 * Set `username` to a string to get only accounts whose username contains this string.
2099 * Set `display_name` to a string to get only accounts whose display name contains this string.
2100 * Set `email` to an email to get only accounts with that email (this only works on local accounts).
2101 * 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).
2102 * Set `permissions` to "staff" to only get accounts with staff permissions.
2103 * Set `invited_by` to an account id to get only accounts invited by this user.
2104 * Set `role_ids` to a list of role IDs to get only accounts with those roles.
2105
2106 Returns a list of :ref:`admin account dicts <admin account dicts>`.
2107 """
2108 if max_id is not None:
2109 max_id = self.__unpack_id(max_id, dateconv=True)
2110
2111 if min_id is not None:
2112 min_id = self.__unpack_id(min_id, dateconv=True)
2113
2114 if since_id is not None:
2115 since_id = self.__unpack_id(since_id, dateconv=True)
2116
2117 if role_ids is not None:
2118 if not isinstance(role_ids, list):
2119 role_ids = [role_ids]
2120 role_ids = list(map(self.__unpack_id, role_ids))
2121
2122 if invited_by is not None:
2123 invited_by = self.__unpack_id(invited_by)
2124
2125 if permissions is not None and not permissions in ["staff"]:
2126 raise MastodonIllegalArgumentError("Permissions must be staff if passed")
2127
2128 if origin is not None and not origin in ["local", "remote"]:
2129 raise MastodonIllegalArgumentError("Origin must be local or remote")
2130
2131 if status is not None and not status in ["active", "pending", "disabled", "silenced", "suspended"]:
2132 raise MastodonIllegalArgumentError("Status must be local or active, pending, disabled, silenced or suspended")
2133
2134 if not by_domain is None:
2135 by_domain = self.__deprotocolize(by_domain)
2136
2137 params = self.__generate_params(locals())
2138 return self.__api_request('GET', '/api/v2/admin/accounts', params)
2139
2140 @api_version("2.9.1", "2.9.1", _DICT_VERSION_ADMIN_ACCOUNT)
2141 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):
2142 """
2143 Currently a synonym for admin_accounts_v1, now deprecated. You are strongly encouraged to use admin_accounts_v2 instead, since this one is kind of bad.
2144
2145 !!!!! This function may be switched to calling the v2 API in the future. This is your warning. If you want to keep using v1, use it explicitly. !!!!!
2146 """
2147 return self.admin_accounts_v1(
2148 remote=remote,
2149 by_domain=by_domain,
2150 status=status,
2151 username=username,
2152 display_name=display_name,
2153 email=email,
2154 ip=ip,
2155 staff_only=staff_only,
2156 max_id=max_id,
2157 min_id=min_id,
2158 since_id=since_id
2159 )
2160
2161 @api_version("2.9.1", "2.9.1", _DICT_VERSION_ADMIN_ACCOUNT)
2162 def admin_accounts_v1(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):
2163 """
2164 Fetches a list of accounts that match given criteria. By default, local accounts are returned.
2165
2166 * Set `remote` to True to get remote accounts, otherwise local accounts are returned (default: local accounts)
2167 * Set `by_domain` to a domain to get only accounts from that domain.
2168 * Set `status` to one of "active", "pending", "disabled", "silenced" or "suspended" to get only accounts with that moderation status (default: active)
2169 * Set `username` to a string to get only accounts whose username contains this string.
2170 * Set `display_name` to a string to get only accounts whose display name contains this string.
2171 * Set `email` to an email to get only accounts with that email (this only works on local accounts).
2172 * 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).
2173 * Set `staff_only` to True to only get staff accounts (this only works on local accounts).
2174
2175 Note that setting the boolean parameters to False does not mean "give me users to which this does not apply" but
2176 instead means "I do not care if users have this attribute".
2177
2178 Deprecated in Mastodon version 3.5.0.
2179
2180 Returns a list of :ref:`admin account dicts <admin account dicts>`.
2181 """
2182 if max_id is not None:
2183 max_id = self.__unpack_id(max_id, dateconv=True)
2184
2185 if min_id is not None:
2186 min_id = self.__unpack_id(min_id, dateconv=True)
2187
2188 if since_id is not None:
2189 since_id = self.__unpack_id(since_id, dateconv=True)
2190
2191 params = self.__generate_params(locals(), ['remote', 'status', 'staff_only'])
2192
2193 if remote:
2194 params["remote"] = True
2195
2196 mod_statuses = ["active", "pending", "disabled", "silenced", "suspended"]
2197 if not status in mod_statuses:
2198 raise ValueError("Invalid moderation status requested.")
2199
2200 if staff_only:
2201 params["staff"] = True
2202
2203 for mod_status in mod_statuses:
2204 if status == mod_status:
2205 params[status] = True
2206
2207 if not by_domain is None:
2208 by_domain = self.__deprotocolize(by_domain)
2209
2210 return self.__api_request('GET', '/api/v1/admin/accounts', params)
2211
2212 @api_version("2.9.1", "2.9.1", _DICT_VERSION_ADMIN_ACCOUNT)
2213 def admin_account(self, id):
2214 """
2215 Fetches a single :ref:`admin account dict <admin account dict>` for the user with the given id.
2216
2217 Returns that dict.
2218 """
2219 id = self.__unpack_id(id)
2220 return self.__api_request('GET', '/api/v1/admin/accounts/{0}'.format(id))
2221
2222 @api_version("2.9.1", "2.9.1", _DICT_VERSION_ADMIN_ACCOUNT)
2223 def admin_account_enable(self, id):
2224 """
2225 Reenables login for a local account for which login has been disabled.
2226
2227 Returns the updated :ref:`admin account dict <admin account dict>`.
2228 """
2229 id = self.__unpack_id(id)
2230 return self.__api_request('POST', '/api/v1/admin/accounts/{0}/enable'.format(id))
2231
2232 @api_version("2.9.1", "2.9.1", _DICT_VERSION_ADMIN_ACCOUNT)
2233 def admin_account_approve(self, id):
2234 """
2235 Approves a pending account.
2236
2237 Returns the updated :ref:`admin account dict <admin account dict>`.
2238 """
2239 id = self.__unpack_id(id)
2240 return self.__api_request('POST', '/api/v1/admin/accounts/{0}/approve'.format(id))
2241
2242 @api_version("2.9.1", "2.9.1", _DICT_VERSION_ADMIN_ACCOUNT)
2243 def admin_account_reject(self, id):
2244 """
2245 Rejects and deletes a pending account.
2246
2247 Returns the updated :ref:`admin account dict <admin account dict>` for the account that is now gone.
2248 """
2249 id = self.__unpack_id(id)
2250 return self.__api_request('POST', '/api/v1/admin/accounts/{0}/reject'.format(id))
2251
2252 @api_version("2.9.1", "2.9.1", _DICT_VERSION_ADMIN_ACCOUNT)
2253 def admin_account_unsilence(self, id):
2254 """
2255 Unsilences an account.
2256
2257 Returns the updated :ref:`admin account dict <admin account dict>`.
2258 """
2259 id = self.__unpack_id(id)
2260 return self.__api_request('POST', '/api/v1/admin/accounts/{0}/unsilence'.format(id))
2261
2262 @api_version("2.9.1", "2.9.1", _DICT_VERSION_ADMIN_ACCOUNT)
2263 def admin_account_unsuspend(self, id):
2264 """
2265 Unsuspends an account.
2266
2267 Returns the updated :ref:`admin account dict <admin account dict>`.
2268 """
2269 id = self.__unpack_id(id)
2270 return self.__api_request('POST', '/api/v1/admin/accounts/{0}/unsuspend'.format(id))
2271
2272 @api_version("3.3.0", "3.3.0", _DICT_VERSION_ADMIN_ACCOUNT)
2273 def admin_account_delete(self, id):
2274 """
2275 Delete a local user account.
2276
2277 The deleted accounts :ref:`admin account dict <admin account dict>`.
2278 """
2279 id = self.__unpack_id(id)
2280 return self.__api_request('DELETE', '/api/v1/admin/accounts/{0}'.format(id))
2281
2282 @api_version("3.3.0", "3.3.0", _DICT_VERSION_ADMIN_ACCOUNT)
2283 def admin_account_unsensitive(self, id):
2284 """
2285 Unmark an account as force-sensitive.
2286
2287 Returns the updated :ref:`admin account dict <admin account dict>`.
2288 """
2289 id = self.__unpack_id(id)
2290 return self.__api_request('POST', '/api/v1/admin/accounts/{0}/unsensitive'.format(id))
2291
2292 @api_version("2.9.1", "2.9.1", "2.9.1")
2293 def admin_account_moderate(self, id, action=None, report_id=None, warning_preset_id=None, text=None, send_email_notification=True):
2294 """
2295 Perform a moderation action on an account.
2296
2297 Valid actions are:
2298 * "disable" - for a local user, disable login.
2299 * "silence" - hide the users posts from all public timelines.
2300 * "suspend" - irreversibly delete all the user's posts, past and future.
2301 * "sensitive" - forcce an accounts media visibility to always be sensitive.
2302
2303 If no action is specified, the user is only issued a warning.
2304
2305 Specify the id of a report as `report_id` to close the report with this moderation action as the resolution.
2306 Specify `warning_preset_id` to use a warning preset as the notification text to the user, or `text` to specify text directly.
2307 If both are specified, they are concatenated (preset first). Note that there is currently no API to retrieve or create
2308 warning presets.
2309
2310 Set `send_email_notification` to False to not send the user an email notification informing them of the moderation action.
2311 """
2312 if action is None:
2313 action = "none"
2314
2315 if not send_email_notification:
2316 send_email_notification = None
2317
2318 id = self.__unpack_id(id)
2319 if report_id is not None:
2320 report_id = self.__unpack_id(report_id)
2321
2322 params = self.__generate_params(locals(), ['id', 'action'])
2323
2324 params["type"] = action
2325
2326 self.__api_request(
2327 'POST', '/api/v1/admin/accounts/{0}/action'.format(id), params)
2328
2329 @api_version("2.9.1", "2.9.1", _DICT_VERSION_REPORT)
2330 def admin_reports(self, resolved=False, account_id=None, target_account_id=None, max_id=None, min_id=None, since_id=None, limit=None):
2331 """
2332 Fetches the list of reports.
2333
2334 Set `resolved` to True to search for resolved reports. `account_id` and `target_account_id`
2335 can be used to get reports filed by or about a specific user.
2336
2337 Returns a list of :ref:`report dicts <report dicts>`.
2338 """
2339 if max_id is not None:
2340 max_id = self.__unpack_id(max_id, dateconv=True)
2341
2342 if min_id is not None:
2343 min_id = self.__unpack_id(min_id, dateconv=True)
2344
2345 if since_id is not None:
2346 since_id = self.__unpack_id(since_id, dateconv=True)
2347
2348 if account_id is not None:
2349 account_id = self.__unpack_id(account_id)
2350
2351 if target_account_id is not None:
2352 target_account_id = self.__unpack_id(target_account_id)
2353
2354 if not resolved:
2355 resolved = None
2356
2357 params = self.__generate_params(locals())
2358 return self.__api_request('GET', '/api/v1/admin/reports', params)
2359
2360 @api_version("2.9.1", "2.9.1", _DICT_VERSION_REPORT)
2361 def admin_report(self, id):
2362 """
2363 Fetches the report with the given id.
2364
2365 Returns a :ref:`report dict <report dict>`.
2366 """
2367 id = self.__unpack_id(id)
2368 return self.__api_request('GET', '/api/v1/admin/reports/{0}'.format(id))
2369
2370 @api_version("2.9.1", "2.9.1", _DICT_VERSION_REPORT)
2371 def admin_report_assign(self, id):
2372 """
2373 Assigns the given report to the logged-in user.
2374
2375 Returns the updated :ref:`report dict <report dict>`.
2376 """
2377 id = self.__unpack_id(id)
2378 return self.__api_request('POST', '/api/v1/admin/reports/{0}/assign_to_self'.format(id))
2379
2380 @api_version("2.9.1", "2.9.1", _DICT_VERSION_REPORT)
2381 def admin_report_unassign(self, id):
2382 """
2383 Unassigns the given report from the logged-in user.
2384
2385 Returns the updated :ref:`report dict <report dict>`.
2386 """
2387 id = self.__unpack_id(id)
2388 return self.__api_request('POST', '/api/v1/admin/reports/{0}/unassign'.format(id))
2389
2390 @api_version("2.9.1", "2.9.1", _DICT_VERSION_REPORT)
2391 def admin_report_reopen(self, id):
2392 """
2393 Reopens a closed report.
2394
2395 Returns the updated :ref:`report dict <report dict>`.
2396 """
2397 id = self.__unpack_id(id)
2398 return self.__api_request('POST', '/api/v1/admin/reports/{0}/reopen'.format(id))
2399
2400 @api_version("2.9.1", "2.9.1", _DICT_VERSION_REPORT)
2401 def admin_report_resolve(self, id):
2402 """
2403 Marks a report as resolved (without taking any action).
2404
2405 Returns the updated :ref:`report dict <report dict>`.
2406 """
2407 id = self.__unpack_id(id)
2408 return self.__api_request('POST', '/api/v1/admin/reports/{0}/resolve'.format(id))
2409
2410 @api_version("3.5.0", "3.5.0", _DICT_VERSION_HASHTAG)
2411 def admin_trending_tags(self, limit=None):
2412 """
2413 Admin version of :ref:`trending_tags() <trending_tags()>`. Includes unapproved tags.
2414
2415 Returns a list of :ref:`hashtag dicts <hashtag dicts>`, sorted by the instance's trending algorithm,
2416 descending.
2417 """
2418 params = self.__generate_params(locals())
2419 return self.__api_request('GET', '/api/v1/admin/trends/tags', params)
2420
2421 @api_version("3.5.0", "3.5.0", _DICT_VERSION_STATUS)
2422 def admin_trending_statuses(self):
2423 """
2424 Admin version of :ref:`trending_statuses() <trending_statuses()>`. Includes unapproved tags.
2425
2426 Returns a list of :ref:`status dicts <status dicts>`, sorted by the instance's trending algorithm,
2427 descending.
2428 """
2429 params = self.__generate_params(locals())
2430 return self.__api_request('GET', '/api/v1/admin/trends/statuses', params)
2431
2432 @api_version("3.5.0", "3.5.0", _DICT_VERSION_CARD)
2433 def admin_trending_links(self):
2434 """
2435 Admin version of :ref:`trending_links() <trending_links()>`. Includes unapproved tags.
2436
2437 Returns a list of :ref:`card dicts <card dicts>`, sorted by the instance's trending algorithm,
2438 descending.
2439 """
2440 params = self.__generate_params(locals())
2441 return self.__api_request('GET', '/api/v1/admin/trends/links', params)
2442
2443 @api_version("4.0.0", "4.0.0", _DICT_VERSION_ADMIN_DOMAIN_BLOCK)
2444 def admin_domain_blocks(self, id=None, limit:int=None):
2445 """
2446 Fetches a list of blocked domains. Requires scope `admin:read:domain_blocks`.
2447
2448 Provide an `id` to fetch a specific domain block based on its database id.
2449
2450 Returns a list of :ref:`admin domain block dicts <admin domain block dicts>`, raises a `MastodonAPIError` if the specified block does not exist.
2451 """
2452 if id is not None:
2453 id = self.__unpack_id(id)
2454 return self.__api_request('GET', '/api/v1/admin/domain_blocks/{0}'.format(id))
2455 else:
2456 params = self.__generate_params(locals(),['limit'])
2457 return self.__api_request('GET', '/api/v1/admin/domain_blocks/', params)
2458
2459 @api_version("4.0.0", "4.0.0", _DICT_VERSION_ADMIN_DOMAIN_BLOCK)
2460 def admin_create_domain_block(self, domain:str, severity:str=None, reject_media:bool=None, reject_reports:bool=None, private_comment:str=None, public_comment:str=None, obfuscate:bool=None):
2461 """
2462 Perform a moderation action on a domain. Requires scope `admin:write:domain_blocks`.
2463
2464 Valid severities are:
2465 * "silence" - hide all posts from federated timelines and do not show notifications to local users from the remote instance's users unless they are following the remote user.
2466 * "suspend" - deny interactions with this instance going forward. This action is reversible.
2467 * "limit" - generally used with reject_media=true to force reject media from an instance without silencing or suspending..
2468
2469 If no action is specified, the domain is only silenced.
2470 `domain` is the domain to block. Note that using the top level domain will also imapct all subdomains. ie, example.com will also impact subdomain.example.com.
2471 `reject_media` will not download remote media on to your local instance media storage.
2472 `reject_reports` ignores all reports from the remote instance.
2473 `private_comment` sets a private admin comment for the domain.
2474 `public_comment` sets a publicly available comment for this domain, which will be available to local users and may be available to everyone depending on your settings.
2475 `obfuscate` censors some part of the domain name. Useful if the domain name contains unwanted words like slurs.
2476
2477 Returns the new domain block as an :ref:`admin domain block dict <admin domain block dict>`.
2478 """
2479 if domain is None:
2480 raise AttributeError("Must provide a domain to block a domain")
2481 params = self.__generate_params(locals())
2482 return self.__api_request('POST', '/api/v1/admin/domain_blocks/', params)
2483
2484 @api_version("4.0.0", "4.0.0", _DICT_VERSION_ADMIN_DOMAIN_BLOCK)
2485 def admin_update_domain_block(self, id, severity:str=None, reject_media:bool=None, reject_reports:bool=None, private_comment:str=None, public_comment:str=None, obfuscate:bool=None):
2486 """
2487 Modify existing moderation action on a domain. Requires scope `admin:write:domain_blocks`.
2488
2489 Valid severities are:
2490 * "silence" - hide all posts from federated timelines and do not show notifications to local users from the remote instance's users unless they are following the remote user.
2491 * "suspend" - deny interactions with this instance going forward. This action is reversible.
2492 * "limit" - generally used with reject_media=true to force reject media from an instance without silencing or suspending.
2493
2494 If no action is specified, the domain is only silenced.
2495 `domain` is the domain to block. Note that using the top level domain will also imapct all subdomains. ie, example.com will also impact subdomain.example.com.
2496 `reject_media` will not download remote media on to your local instance media storage.
2497 `reject_reports` ignores all reports from the remote instance.
2498 `private_comment` sets a private admin comment for the domain.
2499 `public_comment` sets a publicly available comment for this domain, which will be available to local users and may be available to everyone depending on your settings.
2500 `obfuscate` censors some part of the domain name. Useful if the domain name contains unwanted words like slurs.
2501
2502 Returns the modified domain block as an :ref:`admin domain block dict <admin domain block dict>`, raises a `MastodonAPIError` if the specified block does not exist.
2503 """
2504 if id is None:
2505 raise AttributeError("Must provide an id to modify the existing moderation actions on a given domain.")
2506 id = self.__unpack_id(id)
2507 params = self.__generate_params(locals(), ["id"])
2508 return self.__api_request('PUT', '/api/v1/admin/domain_blocks/{0}'.format(id), params)
2509
2510 @api_version("4.0.0", "4.0.0", _DICT_VERSION_ADMIN_DOMAIN_BLOCK)
2511 def admin_delete_domain_block(self, id=None):
2512 """
2513 Removes moderation action against a given domain. Requires scope `admin:write:domain_blocks`.
2514
2515 Provide an `id` to remove a specific domain block based on its database id.
2516
2517 Raises a `MastodonAPIError` if the specified block does not exist.
2518 """
2519 if id is not None:
2520 id = self.__unpack_id(id)
2521 self.__api_request('DELETE', '/api/v1/admin/domain_blocks/{0}'.format(id))
2522 else:
2523 raise AttributeError("You must provide an id of an existing domain block to remove it.")
2524
2525 @api_version("3.5.0", "3.5.0", _DICT_VERSION_ADMIN_MEASURE)
2526 def admin_measures(self, start_at, end_at, active_users=False, new_users=False, interactions=False, opened_reports = False, resolved_reports=False,
2527 tag_accounts=None, tag_uses=None, tag_servers=None, instance_accounts=None, instance_media_attachments=None, instance_reports=None,
2528 instance_statuses=None, instance_follows=None, instance_followers=None):
2529 """
2530 Retrieves numerical instance information for the time period (at day granularity) between `start_at` and `end_at`.
2531
2532 * `active_users`: Pass true to retrieve the number of active users on your instance within the time period
2533 * `new_users`: Pass true to retrieve the number of users who joined your instance within the time period
2534 * `interactions`: Pass true to retrieve the number of interactions (favourites, boosts, replies) on local statuses within the time period
2535 * `opened_reports`: Pass true to retrieve the number of reports filed within the time period
2536 * `resolved_reports` = Pass true to retrieve the number of reports resolved within the time period
2537 * `tag_accounts`: Pass a tag ID to get the number of accounts which used that tag in at least one status within the time period
2538 * `tag_uses`: Pass a tag ID to get the number of statuses which used that tag within the time period
2539 * `tag_servers`: Pass a tag ID to to get the number of remote origin servers for statuses which used that tag within the time period
2540 * `instance_accounts`: Pass a domain to get the number of accounts originating from that remote domain within the time period
2541 * `instance_media_attachments`: Pass a domain to get the amount of space used by media attachments from that remote domain within the time period
2542 * `instance_reports`: Pass a domain to get the number of reports filed against accounts from that remote domain within the time period
2543 * `instance_statuses`: Pass a domain to get the number of statuses originating from that remote domain within the time period
2544 * `instance_follows`: Pass a domain to get the number of accounts from a remote domain followed by that local user within the time period
2545 * `instance_followers`: Pass a domain to get the number of local accounts followed by accounts from that remote domain within the time period
2546
2547 This API call is relatively expensive - watch your servers load if you want to get a lot of statistical data. Especially the instance_statuses stats
2548 might take a long time to compute and, in fact, time out.
2549
2550 There is currently no way to get tag IDs implemented in Mastodon.py, because the Mastodon public API does not implement one. This will be fixed in a future
2551 release.
2552
2553 Returns a list of :ref:`admin measure dicts <admin measure dicts>`.
2554 """
2555 params_init = locals()
2556 keys = []
2557 for key in ["active_users", "new_users", "interactions", "opened_reports", "resolved_reports"]:
2558 if params_init[key] == True:
2559 keys.append(key)
2560
2561 params = {}
2562 for key in ["tag_accounts", "tag_uses", "tag_servers"]:
2563 if params_init[key] is not None:
2564 keys.append(key)
2565 params[key] = {"id": self.__unpack_id(params_init[key])}
2566 for key in ["instance_accounts", "instance_media_attachments", "instance_reports", "instance_statuses", "instance_follows", "instance_followers"]:
2567 if params_init[key] is not None:
2568 keys.append(key)
2569 params[key] = {"domain": Mastodon.__deprotocolize(params_init[key]).split("/")[0]}
2570
2571 if len(keys) == 0:
2572 raise MastodonIllegalArgumentError("Must request at least one metric.")
2573
2574 params["keys"] = keys
2575 params["start_at"] = self.__consistent_isoformat_utc(start_at)
2576 params["end_at"] = self.__consistent_isoformat_utc(end_at)
2577
2578 return self.__api_request('POST', '/api/v1/admin/measures', params, use_json=True)
2579
2580 @api_version("3.5.0", "3.5.0", _DICT_VERSION_ADMIN_DIMENSION)
2581 def admin_dimensions(self, start_at, end_at, limit=None, languages=False, sources=False, servers=False, space_usage=False, software_versions=False,
2582 tag_servers=None, tag_languages=None, instance_accounts=None, instance_languages=None):
2583 """
2584 Retrieves primarily categorical instance information for the time period (at day granularity) between `start_at` and `end_at`.
2585
2586 * `languages`: Pass true to get the most-used languages on this server
2587 * `sources`: Pass true to get the most-used client apps on this server
2588 * `servers`: Pass true to get the remote servers with the most statuses
2589 * `space_usage`: Pass true to get the how much space is used by different components your software stack
2590 * `software_versions`: Pass true to get the version numbers for your software stack
2591 * `tag_servers`: Pass a tag ID to get the most-common servers for statuses including a trending tag
2592 * `tag_languages`: Pass a tag ID to get the most-used languages for statuses including a trending tag
2593 * `instance_accounts`: Pass a domain to get the most-followed accounts from a remote server
2594 * `instance_languages`: Pass a domain to get the most-used languages from a remote server
2595
2596 Pass `limit` to set how many results you want on queries where that makes sense.
2597
2598 This API call is relatively expensive - watch your servers load if you want to get a lot of statistical data.
2599
2600 There is currently no way to get tag IDs implemented in Mastodon.py, because the Mastodon public API does not implement one. This will be fixed in a future
2601 release.
2602
2603 Returns a list of :ref:`admin dimension dicts <admin dimension dicts>`.
2604 """
2605 params_init = locals()
2606 keys = []
2607 for key in ["languages", "sources", "servers", "space_usage", "software_versions"]:
2608 if params_init[key] == True:
2609 keys.append(key)
2610
2611 params = {}
2612 for key in ["tag_servers", "tag_languages"]:
2613 if params_init[key] is not None:
2614 keys.append(key)
2615 params[key] = {"id": self.__unpack_id(params_init[key])}
2616 for key in ["instance_accounts", "instance_languages"]:
2617 if params_init[key] is not None:
2618 keys.append(key)
2619 params[key] = {"domain": Mastodon.__deprotocolize(params_init[key]).split("/")[0]}
2620
2621 if len(keys) == 0:
2622 raise MastodonIllegalArgumentError("Must request at least one dimension.")
2623
2624 params["keys"] = keys
2625 if limit is not None:
2626 params["limit"] = limit
2627 params["start_at"] = self.__consistent_isoformat_utc(start_at)
2628 params["end_at"] = self.__consistent_isoformat_utc(end_at)
2629
2630 return self.__api_request('POST', '/api/v1/admin/dimensions', params, use_json=True)
2631
2632 @api_version("3.5.0", "3.5.0", _DICT_VERSION_ADMIN_RETENTION)
2633 def admin_retention(self, start_at, end_at, frequency="day"):
2634 """
2635 Gets user retention statistics (at `frequency` - "day" or "month" - granularity) between `start_at` and `end_at`.
2636
2637 Returns a list of :ref:`admin retention dicts <admin retention dicts>`
2638 """
2639 if not frequency in ["day", "month"]:
2640 raise MastodonIllegalArgumentError("Frequency must be day or month")
2641
2642 params = {
2643 "start_at": self.__consistent_isoformat_utc(start_at),
2644 "end_at": self.__consistent_isoformat_utc(end_at),
2645 "frequency": frequency
2646 }
2647 return self.__api_request('POST', '/api/v1/admin/retention', params)
2648
2649 ###
2650 # Push subscription crypto utilities
2651 ###
2652 def push_subscription_generate_keys(self):
2653 """
2654 Generates a private key, public key and shared secret for use in webpush subscriptions.
2655
2656 Returns two dicts: One with the private key and shared secret and another with the
2657 public key and shared secret.
2658 """
2659 if not IMPL_HAS_CRYPTO:
2660 raise NotImplementedError(
2661 'To use the crypto tools, please install the webpush feature dependencies.')
2662
2663 push_key_pair = ec.generate_private_key(ec.SECP256R1(), default_backend())
2664 push_key_priv = push_key_pair.private_numbers().private_value
2665 try:
2666 push_key_pub = push_key_pair.public_key().public_bytes(
2667 serialization.Encoding.X962,
2668 serialization.PublicFormat.UncompressedPoint,
2669 )
2670 except:
2671 push_key_pub = push_key_pair.public_key().public_numbers().encode_point()
2672
2673 push_shared_secret = os.urandom(16)
2674
2675 priv_dict = {
2676 'privkey': push_key_priv,
2677 'auth': push_shared_secret
2678 }
2679
2680 pub_dict = {
2681 'pubkey': push_key_pub,
2682 'auth': push_shared_secret
2683 }
2684
2685 return priv_dict, pub_dict
2686
2687 @api_version("2.4.0", "2.4.0", _DICT_VERSION_PUSH_NOTIF)
2688 def push_subscription_decrypt_push(self, data, decrypt_params, encryption_header, crypto_key_header):
2689 """
2690 Decrypts `data` received in a webpush request. Requires the private key dict
2691 from :ref:`push_subscription_generate_keys() <push_subscription_generate_keys()>` (`decrypt_params`) as well as the
2692 Encryption and server Crypto-Key headers from the received webpush
2693
2694 Returns the decoded webpush as a :ref:`push notification dict <push notification dict>`.
2695 """
2696 if (not IMPL_HAS_ECE) or (not IMPL_HAS_CRYPTO):
2697 raise NotImplementedError(
2698 'To use the crypto tools, please install the webpush feature dependencies.')
2699
2700 salt = self.__decode_webpush_b64(encryption_header.split("salt=")[1].strip())
2701 dhparams = self.__decode_webpush_b64(crypto_key_header.split("dh=")[1].split(";")[0].strip())
2702 p256ecdsa = self.__decode_webpush_b64(crypto_key_header.split("p256ecdsa=")[1].strip())
2703 dec_key = ec.derive_private_key(decrypt_params['privkey'], ec.SECP256R1(), default_backend())
2704 decrypted = http_ece.decrypt(
2705 data,
2706 salt=salt,
2707 key=p256ecdsa,
2708 private_key=dec_key,
2709 dh=dhparams,
2710 auth_secret=decrypt_params['auth'],
2711 keylabel="P-256",
2712 version="aesgcm"
2713 )
2714
2715 return json.loads(decrypted.decode('utf-8'), object_hook=Mastodon.__json_hooks)
2716
2717 ###
2718 # Blurhash utilities
2719 ###
2720 def decode_blurhash(self, media_dict, out_size=(16, 16), size_per_component=True, return_linear=True):
2721 """
2722 Basic media-dict blurhash decoding.
2723
2724 out_size is the desired result size in pixels, either absolute or per blurhash
2725 component (this is the default).
2726
2727 By default, this function will return the image as linear RGB, ready for further
2728 scaling operations. If you want to display the image directly, set return_linear
2729 to False.
2730
2731 Returns the decoded blurhash image as a three-dimensional list: [height][width][3],
2732 with the last dimension being RGB colours.
2733
2734 For further info and tips for advanced usage, refer to the documentation for the
2735 blurhash module: https://github.com/halcy/blurhash-python
2736 """
2737 if not IMPL_HAS_BLURHASH:
2738 raise NotImplementedError(
2739 'To use the blurhash functions, please install the blurhash Python module.')
2740
2741 # Figure out what size to decode to
2742 decode_components_x, decode_components_y = blurhash.components(media_dict["blurhash"])
2743 if size_per_component:
2744 decode_size_x = decode_components_x * out_size[0]
2745 decode_size_y = decode_components_y * out_size[1]
2746 else:
2747 decode_size_x = out_size[0]
2748 decode_size_y = out_size[1]
2749
2750 # Decode
2751 decoded_image = blurhash.decode(media_dict["blurhash"], decode_size_x, decode_size_y, linear=return_linear)
2752
2753 # And that's pretty much it.
2754 return decoded_image
2755
2756 ###
2757 # Pagination
2758 ###
2759 def fetch_next(self, previous_page):
2760 """
2761 Fetches the next page of results of a paginated request. Pass in the
2762 previous page in its entirety, or the pagination information dict
2763 returned as a part of that pages last status ('_pagination_next').
2764
2765 Returns the next page or None if no further data is available.
2766 """
2767 if isinstance(previous_page, list) and len(previous_page) != 0:
2768 if hasattr(previous_page, '_pagination_next'):
2769 params = copy.deepcopy(previous_page._pagination_next)
2770 else:
2771 return None
2772 else:
2773 params = copy.deepcopy(previous_page)
2774
2775 method = params['_pagination_method']
2776 del params['_pagination_method']
2777
2778 endpoint = params['_pagination_endpoint']
2779 del params['_pagination_endpoint']
2780
2781 return self.__api_request(method, endpoint, params)
2782
2783 def fetch_previous(self, next_page):
2784 """
2785 Fetches the previous page of results of a paginated request. Pass in the
2786 previous page in its entirety, or the pagination information dict
2787 returned as a part of that pages first status ('_pagination_prev').
2788
2789 Returns the previous page or None if no further data is available.
2790 """
2791 if isinstance(next_page, list) and len(next_page) != 0:
2792 if hasattr(next_page, '_pagination_prev'):
2793 params = copy.deepcopy(next_page._pagination_prev)
2794 else:
2795 return None
2796 else:
2797 params = copy.deepcopy(next_page)
2798
2799 method = params['_pagination_method']
2800 del params['_pagination_method']
2801
2802 endpoint = params['_pagination_endpoint']
2803 del params['_pagination_endpoint']
2804
2805 return self.__api_request(method, endpoint, params)
2806
2807 def fetch_remaining(self, first_page):
2808 """
2809 Fetches all the remaining pages of a paginated request starting from a
2810 first page and returns the entire set of results (including the first page
2811 that was passed in) as a big list.
2812
2813 Be careful, as this might generate a lot of requests, depending on what you are
2814 fetching, and might cause you to run into rate limits very quickly.
2815 """
2816 first_page = copy.deepcopy(first_page)
2817
2818 all_pages = []
2819 current_page = first_page
2820 while current_page is not None and len(current_page) > 0:
2821 all_pages.extend(current_page)
2822 current_page = self.fetch_next(current_page)
2823
2824 return all_pages
2825
2826 ###
2827 # Streaming
2828 ###
2829 @api_version("1.1.0", "1.4.2", _DICT_VERSION_STATUS)
2830 def stream_user(self, listener, run_async=False, timeout=_DEFAULT_STREAM_TIMEOUT, reconnect_async=False, reconnect_async_wait_sec=_DEFAULT_STREAM_RECONNECT_WAIT_SEC):
2831 """
2832 Streams events that are relevant to the authorized user, i.e. home
2833 timeline and notifications.
2834 """
2835 return self.__stream('/api/v1/streaming/user', listener, run_async=run_async, timeout=timeout, reconnect_async=reconnect_async, reconnect_async_wait_sec=reconnect_async_wait_sec)
2836
2837 @api_version("1.1.0", "1.4.2", _DICT_VERSION_STATUS)
2838 def stream_public(self, listener, run_async=False, timeout=_DEFAULT_STREAM_TIMEOUT, reconnect_async=False, reconnect_async_wait_sec=_DEFAULT_STREAM_RECONNECT_WAIT_SEC):
2839 """
2840 Streams public events.
2841 """
2842 return self.__stream('/api/v1/streaming/public', listener, run_async=run_async, timeout=timeout, reconnect_async=reconnect_async, reconnect_async_wait_sec=reconnect_async_wait_sec)
2843
2844 @api_version("1.1.0", "1.4.2", _DICT_VERSION_STATUS)
2845 def stream_local(self, listener, run_async=False, timeout=_DEFAULT_STREAM_TIMEOUT, reconnect_async=False, reconnect_async_wait_sec=_DEFAULT_STREAM_RECONNECT_WAIT_SEC):
2846 """
2847 Streams local public events.
2848 """
2849 return self.__stream('/api/v1/streaming/public/local', listener, run_async=run_async, timeout=timeout, reconnect_async=reconnect_async, reconnect_async_wait_sec=reconnect_async_wait_sec)
2850
2851 @api_version("1.1.0", "1.4.2", _DICT_VERSION_STATUS)
2852 def stream_hashtag(self, tag, listener, local=False, run_async=False, timeout=_DEFAULT_STREAM_TIMEOUT, reconnect_async=False, reconnect_async_wait_sec=_DEFAULT_STREAM_RECONNECT_WAIT_SEC):
2853 """
2854 Stream for all public statuses for the hashtag 'tag' seen by the connected
2855 instance.
2856
2857 Set local to True to only get local statuses.
2858 """
2859 if tag.startswith("#"):
2860 raise MastodonIllegalArgumentError(
2861 "Tag parameter should omit leading #")
2862 base = '/api/v1/streaming/hashtag'
2863 if local:
2864 base += '/local'
2865 return self.__stream("{}?tag={}".format(base, tag), listener, run_async=run_async, timeout=timeout, reconnect_async=reconnect_async, reconnect_async_wait_sec=reconnect_async_wait_sec)
2866
2867 @api_version("2.1.0", "2.1.0", _DICT_VERSION_STATUS)
2868 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):
2869 """
2870 Stream events for the current user, restricted to accounts on the given
2871 list.
2872 """
2873 id = self.__unpack_id(id)
2874 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)
2875
2876 @api_version("2.6.0", "2.6.0", _DICT_VERSION_STATUS)
2877 def stream_direct(self, listener, run_async=False, timeout=_DEFAULT_STREAM_TIMEOUT, reconnect_async=False, reconnect_async_wait_sec=_DEFAULT_STREAM_RECONNECT_WAIT_SEC):
2878 """
2879 Streams direct message events for the logged-in user, as conversation events.
2880 """
2881 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)
2882
2883 @api_version("2.5.0", "2.5.0", "2.5.0")
2884 def stream_healthy(self):
2885 """
2886 Returns without True if streaming API is okay, False or raises an error otherwise.
2887 """
2888 api_okay = self.__api_request('GET', '/api/v1/streaming/health', base_url_override=self.__get_streaming_base(), parse=False)
2889 if api_okay in [b'OK', b'success']:
2890 return True
2891 return False
Powered by cgit v1.2.3 (git 2.41.0)