aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.rst8
-rw-r--r--mastodon/Mastodon.py2852
-rw-r--r--mastodon/accounts.py395
-rw-r--r--mastodon/admin.py572
-rw-r--r--mastodon/authentication.py22
-rw-r--r--mastodon/conversations.py43
-rw-r--r--mastodon/endorsements.py19
-rw-r--r--mastodon/errors.py (renamed from mastodon/error.py)0
-rw-r--r--mastodon/favourites.py52
-rw-r--r--mastodon/filters.py119
-rw-r--r--mastodon/hashtags.py52
-rw-r--r--mastodon/instance.py97
-rw-r--r--mastodon/internals.py4
-rw-r--r--mastodon/lists.py112
-rw-r--r--mastodon/media.py114
-rw-r--r--mastodon/notifications.py92
-rw-r--r--mastodon/polls.py61
-rw-r--r--mastodon/preferences.py71
-rw-r--r--mastodon/push.py201
-rw-r--r--mastodon/relationships.py136
-rw-r--r--mastodon/reports.py62
-rw-r--r--mastodon/search.py91
-rw-r--r--mastodon/statuses.py424
-rw-r--r--mastodon/streaming_endpoints.py75
-rw-r--r--mastodon/suggestions.py32
-rw-r--r--mastodon/timeline.py22
-rw-r--r--mastodon/trends.py72
-rw-r--r--mastodon/utility.py116
28 files changed, 3056 insertions, 2860 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index d83513c..6b5e832 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -4,8 +4,9 @@ version number. Breaking changes will be indicated by a change in the minor
4 4
5v1.8.0 (in progress) 5v1.8.0 (in progress)
6-------------------- 6--------------------
7* Overall: Support level is now 3.5.3 (last before 4.0.0) 7* Overall: Support level is now 3.5.5 (last before 4.0.0)
8* BREAKING CHANGE: Switch the base URL to None, throw an error when no base url is passed. Having mastosoc as default was sensible when there were only three mastodon servers. It is not sensible now and trips people up constantly. 8* BREAKING CHANGE: Switch the base URL to None, throw an error when no base url is passed. Having mastosoc as default was sensible when there were only three mastodon servers. It is not sensible now and trips people up constantly.
9* GENERAL WARNING: Currently, Mastodon.py does not support the "new" filter system. In the future, it will, and it will respect filters by default (i.e. remove filtered posts).
9* Fixed an issue with the fix for the Pleroma date bug (thanks adbenitez) 10* Fixed an issue with the fix for the Pleroma date bug (thanks adbenitez)
10* Added trending APIs (`trending_tags`, `trending_statuses`, `trending_links`, `admin_trending_tags`, `admin_trending_statuses`, `admin_trending_links`) 11* Added trending APIs (`trending_tags`, `trending_statuses`, `trending_links`, `admin_trending_tags`, `admin_trending_statuses`, `admin_trending_links`)
11* Added `lang` parameter and document what it does properly. 12* Added `lang` parameter and document what it does properly.
@@ -19,6 +20,11 @@ v1.8.0 (in progress)
19* Added `account_remove_from_followers` API 20* Added `account_remove_from_followers` API
20* Added `admin_accounts_v2` API 21* Added `admin_accounts_v2` API
21* FUTURE BREAKING CHANGE WARNING: For now, `admin_accounts` still calls v1, but this may change in the future. You are encouraged to use v2 instead. 22* FUTURE BREAKING CHANGE WARNING: For now, `admin_accounts` still calls v1, but this may change in the future. You are encouraged to use v2 instead.
23* Make the version comparisons a bit nicer (thanks eumiro)
24* Switch to more recent modules for datetime and timezone stuff (thanks eumiro)
25* Substantially restructure the documentation, making it a lot nicer to read
26* Substantially restructure the code, making it hopefully a lot easier to maintain
27 * This SHOULD NOT break anything. If this breaks your code, please let me know.
22 28
23v1.7.0 29v1.7.0
24------ 30------
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
diff --git a/mastodon/accounts.py b/mastodon/accounts.py
index 5ecdf93..219edac 100644
--- a/mastodon/accounts.py
+++ b/mastodon/accounts.py
@@ -1,5 +1,10 @@
1# accounts.py - account related endpoints
2
3import collections
4
5from .versions import _DICT_VERSION_ACCOUNT, _DICT_VERSION_STATUS, _DICT_VERSION_RELATIONSHIP, _DICT_VERSION_LIST, _DICT_VERSION_FAMILIAR_FOLLOWERS, _DICT_VERSION_HASHTAG
1from .defaults import _DEFAULT_SCOPES, _SCOPE_SETS 6from .defaults import _DEFAULT_SCOPES, _SCOPE_SETS
2from .error import MastodonIllegalArgumentError, MastodonAPIError 7from .errors import MastodonIllegalArgumentError, MastodonAPIError
3from .utility import api_version 8from .utility import api_version
4 9
5from .internals import Mastodon as Internals 10from .internals import Mastodon as Internals
@@ -105,3 +110,391 @@ class Mastodon(Internals):
105 Only available to the app that the user originally signed up with. 110 Only available to the app that the user originally signed up with.
106 """ 111 """
107 self.__api_request('POST', '/api/v1/emails/confirmations') 112 self.__api_request('POST', '/api/v1/emails/confirmations')
113
114 ###
115 # Reading data: Accounts
116 ###
117 @api_version("1.0.0", "1.0.0", _DICT_VERSION_ACCOUNT)
118 def account(self, id):
119 """
120 Fetch account information by user `id`.
121
122 Does not require authentication for publicly visible accounts.
123
124 Returns a :ref:`account dict <account dict>`.
125 """
126 id = self.__unpack_id(id)
127 url = '/api/v1/accounts/{0}'.format(str(id))
128 return self.__api_request('GET', url)
129
130 @api_version("1.0.0", "2.1.0", _DICT_VERSION_ACCOUNT)
131 def account_verify_credentials(self):
132 """
133 Fetch logged-in user's account information.
134
135 Returns a :ref:`account dict <account dict>` (Starting from 2.1.0, with an additional "source" field).
136 """
137 return self.__api_request('GET', '/api/v1/accounts/verify_credentials')
138
139 @api_version("1.0.0", "2.1.0", _DICT_VERSION_ACCOUNT)
140 def me(self):
141 """
142 Get this user's account. Synonym for `account_verify_credentials()`, does exactly
143 the same thing, just exists becase `account_verify_credentials()` has a confusing
144 name.
145 """
146 return self.account_verify_credentials()
147
148 @api_version("1.0.0", "2.8.0", _DICT_VERSION_STATUS)
149 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):
150 """
151 Fetch statuses by user `id`. Same options as :ref:`timeline() <timeline()>` are permitted.
152 Returned toots are from the perspective of the logged-in user, i.e.
153 all statuses visible to the logged-in user (including DMs) are
154 included.
155
156 If `only_media` is set, return only statuses with media attachments.
157 If `pinned` is set, return only statuses that have been pinned. Note that
158 as of Mastodon 2.1.0, this only works properly for instance-local users.
159 If `exclude_replies` is set, filter out all statuses that are replies.
160 If `exclude_reblogs` is set, filter out all statuses that are reblogs.
161 If `tagged` is set, return only statuses that are tagged with `tagged`. Only a single tag without a '#' is valid.
162
163 Does not require authentication for Mastodon versions after 2.7.0 (returns
164 publicly visible statuses in that case), for publicly visible accounts.
165
166 Returns a list of :ref:`status dicts <status dicts>`.
167 """
168 id = self.__unpack_id(id)
169 if max_id is not None:
170 max_id = self.__unpack_id(max_id, dateconv=True)
171
172 if min_id is not None:
173 min_id = self.__unpack_id(min_id, dateconv=True)
174
175 if since_id is not None:
176 since_id = self.__unpack_id(since_id, dateconv=True)
177
178 params = self.__generate_params(locals(), ['id'])
179 if not pinned:
180 del params["pinned"]
181 if not only_media:
182 del params["only_media"]
183 if not exclude_replies:
184 del params["exclude_replies"]
185 if not exclude_reblogs:
186 del params["exclude_reblogs"]
187
188 url = '/api/v1/accounts/{0}/statuses'.format(str(id))
189 return self.__api_request('GET', url, params)
190
191 @api_version("1.0.0", "2.6.0", _DICT_VERSION_ACCOUNT)
192 def account_following(self, id, max_id=None, min_id=None, since_id=None, limit=None):
193 """
194 Fetch users the given user is following.
195
196 Returns a list of :ref:`account dicts <account dicts>`.
197 """
198 id = self.__unpack_id(id)
199 if max_id is not None:
200 max_id = self.__unpack_id(max_id, dateconv=True)
201
202 if min_id is not None:
203 min_id = self.__unpack_id(min_id, dateconv=True)
204
205 if since_id is not None:
206 since_id = self.__unpack_id(since_id, dateconv=True)
207
208 params = self.__generate_params(locals(), ['id'])
209 url = '/api/v1/accounts/{0}/following'.format(str(id))
210 return self.__api_request('GET', url, params)
211
212 @api_version("1.0.0", "2.6.0", _DICT_VERSION_ACCOUNT)
213 def account_followers(self, id, max_id=None, min_id=None, since_id=None, limit=None):
214 """
215 Fetch users the given user is followed by.
216
217 Returns a list of :ref:`account dicts <account dicts>`.
218 """
219 id = self.__unpack_id(id)
220 if max_id is not None:
221 max_id = self.__unpack_id(max_id, dateconv=True)
222
223 if min_id is not None:
224 min_id = self.__unpack_id(min_id, dateconv=True)
225
226 if since_id is not None:
227 since_id = self.__unpack_id(since_id, dateconv=True)
228
229 params = self.__generate_params(locals(), ['id'])
230 url = '/api/v1/accounts/{0}/followers'.format(str(id))
231 return self.__api_request('GET', url, params)
232
233 @api_version("1.0.0", "1.4.0", _DICT_VERSION_RELATIONSHIP)
234 def account_relationships(self, id):
235 """
236 Fetch relationship (following, followed_by, blocking, follow requested) of
237 the logged in user to a given account. `id` can be a list.
238
239 Returns a list of :ref:`relationship dicts <relationship dicts>`.
240 """
241 id = self.__unpack_id(id)
242 params = self.__generate_params(locals())
243 return self.__api_request('GET', '/api/v1/accounts/relationships',
244 params)
245
246 @api_version("1.0.0", "2.3.0", _DICT_VERSION_ACCOUNT)
247 def account_search(self, q, limit=None, following=False):
248 """
249 Fetch matching accounts. Will lookup an account remotely if the search term is
250 in the username@domain format and not yet in the database. Set `following` to
251 True to limit the search to users the logged-in user follows.
252
253 Returns a list of :ref:`account dicts <account dicts>`.
254 """
255 params = self.__generate_params(locals())
256
257 if params["following"] == False:
258 del params["following"]
259
260 return self.__api_request('GET', '/api/v1/accounts/search', params)
261
262 @api_version("2.1.0", "2.1.0", _DICT_VERSION_LIST)
263 def account_lists(self, id):
264 """
265 Get all of the logged-in user's lists which the specified user is
266 a member of.
267
268 Returns a list of :ref:`list dicts <list dicts>`.
269 """
270 id = self.__unpack_id(id)
271 params = self.__generate_params(locals(), ['id'])
272 url = '/api/v1/accounts/{0}/lists'.format(str(id))
273 return self.__api_request('GET', url, params)
274
275 @api_version("3.4.0", "3.4.0", _DICT_VERSION_ACCOUNT)
276 def account_lookup(self, acct):
277 """
278 Look up an account from user@instance form (@instance allowed but not required for
279 local accounts). Will only return accounts that the instance already knows about,
280 and not do any webfinger requests. Use `account_search` if you need to resolve users
281 through webfinger from remote.
282
283 Returns an :ref:`account dict <account dict>`.
284 """
285 return self.__api_request('GET', '/api/v1/accounts/lookup', self.__generate_params(locals()))
286
287 @api_version("3.5.0", "3.5.0", _DICT_VERSION_FAMILIAR_FOLLOWERS)
288 def account_familiar_followers(self, id):
289 """
290 Find followers for the account given by id (can be a list) that also follow the
291 logged in account.
292
293 Returns a list of :ref:`familiar follower dicts <familiar follower dicts>`
294 """
295 if not isinstance(id, list):
296 id = [id]
297 for i in range(len(id)):
298 id[i] = self.__unpack_id(id[i])
299 return self.__api_request('GET', '/api/v1/accounts/familiar_followers', {'id': id}, use_json=True)
300
301 ###
302 # Writing data: Accounts
303 ###
304 @api_version("1.0.0", "3.3.0", _DICT_VERSION_RELATIONSHIP)
305 def account_follow(self, id, reblogs=True, notify=False):
306 """
307 Follow a user.
308
309 Set `reblogs` to False to hide boosts by the followed user.
310 Set `notify` to True to get a notification every time the followed user posts.
311
312 Returns a :ref:`relationship dict <relationship dict>` containing the updated relationship to the user.
313 """
314 id = self.__unpack_id(id)
315 params = self.__generate_params(locals(), ["id"])
316
317 if params["reblogs"] is None:
318 del params["reblogs"]
319
320 url = '/api/v1/accounts/{0}/follow'.format(str(id))
321 return self.__api_request('POST', url, params)
322
323 @api_version("1.0.0", "2.1.0", _DICT_VERSION_ACCOUNT)
324 def follows(self, uri):
325 """
326 Follow a remote user by uri (username@domain).
327
328 Returns a :ref:`account dict <account dict>`.
329 """
330 params = self.__generate_params(locals())
331 return self.__api_request('POST', '/api/v1/follows', params)
332
333 @api_version("1.0.0", "1.4.0", _DICT_VERSION_RELATIONSHIP)
334 def account_unfollow(self, id):
335 """
336 Unfollow a user.
337
338 Returns a :ref:`relationship dict <relationship dict>` containing the updated relationship to the user.
339 """
340 id = self.__unpack_id(id)
341 return self.__api_request('POST', '/api/v1/accounts/{0}/unfollow'.format(str(id)))
342
343 @api_version("3.5.0", "3.5.0", _DICT_VERSION_RELATIONSHIP)
344 def account_remove_from_followers(self, id):
345 """
346 Remove a user from the logged in users followers (i.e. make them unfollow the logged in
347 user / "softblock" them).
348
349 Returns a :ref:`relationship dict <relationship dict>` reflecting the updated following status.
350 """
351 id = self.__unpack_id(id)
352 return self.__api_request('POST', '/api/v1/accounts/{0}/remove_from_followers'.format(str(id)))
353
354
355 @api_version("1.0.0", "1.4.0", _DICT_VERSION_RELATIONSHIP)
356 def account_block(self, id):
357 """
358 Block a user.
359
360 Returns a :ref:`relationship dict <relationship dict>` containing the updated relationship to the user.
361 """
362 id = self.__unpack_id(id)
363 url = '/api/v1/accounts/{0}/block'.format(str(id))
364 return self.__api_request('POST', url)
365
366 @api_version("1.0.0", "1.4.0", _DICT_VERSION_RELATIONSHIP)
367 def account_unblock(self, id):
368 """
369 Unblock a user.
370
371 Returns a :ref:`relationship dict <relationship dict>` containing the updated relationship to the user.
372 """
373 id = self.__unpack_id(id)
374 url = '/api/v1/accounts/{0}/unblock'.format(str(id))
375 return self.__api_request('POST', url)
376
377 @api_version("1.1.0", "2.4.3", _DICT_VERSION_RELATIONSHIP)
378 def account_mute(self, id, notifications=True, duration=None):
379 """
380 Mute a user.
381
382 Set `notifications` to False to receive notifications even though the user is
383 muted from timelines. Pass a `duration` in seconds to have Mastodon automatically
384 lift the mute after that many seconds.
385
386 Returns a :ref:`relationship dict <relationship dict>` containing the updated relationship to the user.
387 """
388 id = self.__unpack_id(id)
389 params = self.__generate_params(locals(), ['id'])
390 url = '/api/v1/accounts/{0}/mute'.format(str(id))
391 return self.__api_request('POST', url, params)
392
393 @api_version("1.1.0", "1.4.0", _DICT_VERSION_RELATIONSHIP)
394 def account_unmute(self, id):
395 """
396 Unmute a user.
397
398 Returns a :ref:`relationship dict <relationship dict>` containing the updated relationship to the user.
399 """
400 id = self.__unpack_id(id)
401 url = '/api/v1/accounts/{0}/unmute'.format(str(id))
402 return self.__api_request('POST', url)
403
404 @api_version("1.1.1", "3.1.0", _DICT_VERSION_ACCOUNT)
405 def account_update_credentials(self, display_name=None, note=None,
406 avatar=None, avatar_mime_type=None,
407 header=None, header_mime_type=None,
408 locked=None, bot=None,
409 discoverable=None, fields=None):
410 """
411 Update the profile for the currently logged-in user.
412
413 `note` is the user's bio.
414
415 `avatar` and 'header' are images. As with media uploads, it is possible to either
416 pass image data and a mime type, or a filename of an image file, for either.
417
418 `locked` specifies whether the user needs to manually approve follow requests.
419
420 `bot` specifies whether the user should be set to a bot.
421
422 `discoverable` specifies whether the user should appear in the user directory.
423
424 `fields` can be a list of up to four name-value pairs (specified as tuples) to
425 appear as semi-structured information in the user's profile.
426
427 Returns the updated `account dict` of the logged-in user.
428 """
429 params_initial = collections.OrderedDict(locals())
430
431 # Convert fields
432 if fields is not None:
433 if len(fields) > 4:
434 raise MastodonIllegalArgumentError(
435 'A maximum of four fields are allowed.')
436
437 fields_attributes = []
438 for idx, (field_name, field_value) in enumerate(fields):
439 params_initial['fields_attributes[' +
440 str(idx) + '][name]'] = field_name
441 params_initial['fields_attributes[' +
442 str(idx) + '][value]'] = field_value
443
444 # Clean up params
445 for param in ["avatar", "avatar_mime_type", "header", "header_mime_type", "fields"]:
446 if param in params_initial:
447 del params_initial[param]
448
449 # Create file info
450 files = {}
451 if avatar is not None:
452 files["avatar"] = self.__load_media_file(avatar, avatar_mime_type)
453 if header is not None:
454 files["header"] = self.__load_media_file(header, header_mime_type)
455
456 params = self.__generate_params(params_initial)
457 return self.__api_request('PATCH', '/api/v1/accounts/update_credentials', params, files=files)
458
459 @api_version("2.5.0", "2.5.0", _DICT_VERSION_RELATIONSHIP)
460 def account_pin(self, id):
461 """
462 Pin / endorse a user.
463
464 Returns a :ref:`relationship dict <relationship dict>` containing the updated relationship to the user.
465 """
466 id = self.__unpack_id(id)
467 url = '/api/v1/accounts/{0}/pin'.format(str(id))
468 return self.__api_request('POST', url)
469
470 @api_version("2.5.0", "2.5.0", _DICT_VERSION_RELATIONSHIP)
471 def account_unpin(self, id):
472 """
473 Unpin / un-endorse a user.
474
475 Returns a :ref:`relationship dict <relationship dict>` containing the updated relationship to the user.
476 """
477 id = self.__unpack_id(id)
478 url = '/api/v1/accounts/{0}/unpin'.format(str(id))
479 return self.__api_request('POST', url)
480
481 @api_version("3.2.0", "3.2.0", _DICT_VERSION_RELATIONSHIP)
482 def account_note_set(self, id, comment):
483 """
484 Set a note (visible to the logged in user only) for the given account.
485
486 Returns a :ref:`status dict <status dict>` with the `note` updated.
487 """
488 id = self.__unpack_id(id)
489 params = self.__generate_params(locals(), ["id"])
490 return self.__api_request('POST', '/api/v1/accounts/{0}/note'.format(str(id)), params)
491
492 @api_version("3.3.0", "3.3.0", _DICT_VERSION_HASHTAG)
493 def account_featured_tags(self, id):
494 """
495 Get an account's featured hashtags.
496
497 Returns a list of :ref:`hashtag dicts <hashtag dicts>` (NOT `featured tag dicts`_).
498 """
499 id = self.__unpack_id(id)
500 return self.__api_request('GET', '/api/v1/accounts/{0}/featured_tags'.format(str(id)))
diff --git a/mastodon/admin.py b/mastodon/admin.py
new file mode 100644
index 0000000..e2f5f20
--- /dev/null
+++ b/mastodon/admin.py
@@ -0,0 +1,572 @@
1# admin.py - admin / moderation endpoints
2
3from .versions import _DICT_VERSION_ADMIN_ACCOUNT, _DICT_VERSION_REPORT, _DICT_VERSION_HASHTAG, _DICT_VERSION_STATUS, _DICT_VERSION_CARD, \
4 _DICT_VERSION_ADMIN_DOMAIN_BLOCK, _DICT_VERSION_ADMIN_MEASURE, _DICT_VERSION_ADMIN_DIMENSION, _DICT_VERSION_ADMIN_RETENTION
5from .errors import MastodonIllegalArgumentError
6from .utility import api_version
7
8from .internals import Mastodon as Internals
9
10class Mastodon(Internals):
11 ###
12 # Moderation API
13 ###
14 @api_version("2.9.1", "4.0.0", _DICT_VERSION_ADMIN_ACCOUNT)
15 def admin_accounts_v2(self, origin=None, by_domain=None, status=None, username=None, display_name=None, email=None, ip=None,
16 permissions=None, invited_by=None, role_ids=None, max_id=None, min_id=None, since_id=None, limit=None):
17 """
18 Fetches a list of accounts that match given criteria. By default, local accounts are returned.
19
20 * Set `origin` to "local" or "remote" to get only local or remote accounts.
21 * Set `by_domain` to a domain to get only accounts from that domain.
22 * Set `status` to one of "active", "pending", "disabled", "silenced" or "suspended" to get only accounts with that moderation status (default: active)
23 * Set `username` to a string to get only accounts whose username contains this string.
24 * Set `display_name` to a string to get only accounts whose display name contains this string.
25 * Set `email` to an email to get only accounts with that email (this only works on local accounts).
26 * 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).
27 * Set `permissions` to "staff" to only get accounts with staff permissions.
28 * Set `invited_by` to an account id to get only accounts invited by this user.
29 * Set `role_ids` to a list of role IDs to get only accounts with those roles.
30
31 Returns a list of :ref:`admin account dicts <admin account dicts>`.
32 """
33 if max_id is not None:
34 max_id = self.__unpack_id(max_id, dateconv=True)
35
36 if min_id is not None:
37 min_id = self.__unpack_id(min_id, dateconv=True)
38
39 if since_id is not None:
40 since_id = self.__unpack_id(since_id, dateconv=True)
41
42 if role_ids is not None:
43 if not isinstance(role_ids, list):
44 role_ids = [role_ids]
45 role_ids = list(map(self.__unpack_id, role_ids))
46
47 if invited_by is not None:
48 invited_by = self.__unpack_id(invited_by)
49
50 if permissions is not None and not permissions in ["staff"]:
51 raise MastodonIllegalArgumentError("Permissions must be staff if passed")
52
53 if origin is not None and not origin in ["local", "remote"]:
54 raise MastodonIllegalArgumentError("Origin must be local or remote")
55
56 if status is not None and not status in ["active", "pending", "disabled", "silenced", "suspended"]:
57 raise MastodonIllegalArgumentError("Status must be local or active, pending, disabled, silenced or suspended")
58
59 if not by_domain is None:
60 by_domain = self.__deprotocolize(by_domain)
61
62 params = self.__generate_params(locals())
63 return self.__api_request('GET', '/api/v2/admin/accounts', params)
64
65 @api_version("2.9.1", "2.9.1", _DICT_VERSION_ADMIN_ACCOUNT)
66 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):
67 """
68 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.
69
70 !!!!! 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. !!!!!
71 """
72 return self.admin_accounts_v1(
73 remote=remote,
74 by_domain=by_domain,
75 status=status,
76 username=username,
77 display_name=display_name,
78 email=email,
79 ip=ip,
80 staff_only=staff_only,
81 max_id=max_id,
82 min_id=min_id,
83 since_id=since_id
84 )
85
86 @api_version("2.9.1", "2.9.1", _DICT_VERSION_ADMIN_ACCOUNT)
87 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):
88 """
89 Fetches a list of accounts that match given criteria. By default, local accounts are returned.
90
91 * Set `remote` to True to get remote accounts, otherwise local accounts are returned (default: local accounts)
92 * Set `by_domain` to a domain to get only accounts from that domain.
93 * Set `status` to one of "active", "pending", "disabled", "silenced" or "suspended" to get only accounts with that moderation status (default: active)
94 * Set `username` to a string to get only accounts whose username contains this string.
95 * Set `display_name` to a string to get only accounts whose display name contains this string.
96 * Set `email` to an email to get only accounts with that email (this only works on local accounts).
97 * 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).
98 * Set `staff_only` to True to only get staff accounts (this only works on local accounts).
99
100 Note that setting the boolean parameters to False does not mean "give me users to which this does not apply" but
101 instead means "I do not care if users have this attribute".
102
103 Deprecated in Mastodon version 3.5.0.
104
105 Returns a list of :ref:`admin account dicts <admin account dicts>`.
106 """
107 if max_id is not None:
108 max_id = self.__unpack_id(max_id, dateconv=True)
109
110 if min_id is not None:
111 min_id = self.__unpack_id(min_id, dateconv=True)
112
113 if since_id is not None:
114 since_id = self.__unpack_id(since_id, dateconv=True)
115
116 params = self.__generate_params(locals(), ['remote', 'status', 'staff_only'])
117
118 if remote:
119 params["remote"] = True
120
121 mod_statuses = ["active", "pending", "disabled", "silenced", "suspended"]
122 if not status in mod_statuses:
123 raise ValueError("Invalid moderation status requested.")
124
125 if staff_only:
126 params["staff"] = True
127
128 for mod_status in mod_statuses:
129 if status == mod_status:
130 params[status] = True
131
132 if not by_domain is None:
133 by_domain = self.__deprotocolize(by_domain)
134
135 return self.__api_request('GET', '/api/v1/admin/accounts', params)
136
137 @api_version("2.9.1", "2.9.1", _DICT_VERSION_ADMIN_ACCOUNT)
138 def admin_account(self, id):
139 """
140 Fetches a single :ref:`admin account dict <admin account dict>` for the user with the given id.
141
142 Returns that dict.
143 """
144 id = self.__unpack_id(id)
145 return self.__api_request('GET', '/api/v1/admin/accounts/{0}'.format(id))
146
147 @api_version("2.9.1", "2.9.1", _DICT_VERSION_ADMIN_ACCOUNT)
148 def admin_account_enable(self, id):
149 """
150 Reenables login for a local account for which login has been disabled.
151
152 Returns the updated :ref:`admin account dict <admin account dict>`.
153 """
154 id = self.__unpack_id(id)
155 return self.__api_request('POST', '/api/v1/admin/accounts/{0}/enable'.format(id))
156
157 @api_version("2.9.1", "2.9.1", _DICT_VERSION_ADMIN_ACCOUNT)
158 def admin_account_approve(self, id):
159 """
160 Approves a pending account.
161
162 Returns the updated :ref:`admin account dict <admin account dict>`.
163 """
164 id = self.__unpack_id(id)
165 return self.__api_request('POST', '/api/v1/admin/accounts/{0}/approve'.format(id))
166
167 @api_version("2.9.1", "2.9.1", _DICT_VERSION_ADMIN_ACCOUNT)
168 def admin_account_reject(self, id):
169 """
170 Rejects and deletes a pending account.
171
172 Returns the updated :ref:`admin account dict <admin account dict>` for the account that is now gone.
173 """
174 id = self.__unpack_id(id)
175 return self.__api_request('POST', '/api/v1/admin/accounts/{0}/reject'.format(id))
176
177 @api_version("2.9.1", "2.9.1", _DICT_VERSION_ADMIN_ACCOUNT)
178 def admin_account_unsilence(self, id):
179 """
180 Unsilences an account.
181
182 Returns the updated :ref:`admin account dict <admin account dict>`.
183 """
184 id = self.__unpack_id(id)
185 return self.__api_request('POST', '/api/v1/admin/accounts/{0}/unsilence'.format(id))
186
187 @api_version("2.9.1", "2.9.1", _DICT_VERSION_ADMIN_ACCOUNT)
188 def admin_account_unsuspend(self, id):
189 """
190 Unsuspends an account.
191
192 Returns the updated :ref:`admin account dict <admin account dict>`.
193 """
194 id = self.__unpack_id(id)
195 return self.__api_request('POST', '/api/v1/admin/accounts/{0}/unsuspend'.format(id))
196
197 @api_version("3.3.0", "3.3.0", _DICT_VERSION_ADMIN_ACCOUNT)
198 def admin_account_delete(self, id):
199 """
200 Delete a local user account.
201
202 The deleted accounts :ref:`admin account dict <admin account dict>`.
203 """
204 id = self.__unpack_id(id)
205 return self.__api_request('DELETE', '/api/v1/admin/accounts/{0}'.format(id))
206
207 @api_version("3.3.0", "3.3.0", _DICT_VERSION_ADMIN_ACCOUNT)
208 def admin_account_unsensitive(self, id):
209 """
210 Unmark an account as force-sensitive.
211
212 Returns the updated :ref:`admin account dict <admin account dict>`.
213 """
214 id = self.__unpack_id(id)
215 return self.__api_request('POST', '/api/v1/admin/accounts/{0}/unsensitive'.format(id))
216
217 @api_version("2.9.1", "2.9.1", "2.9.1")
218 def admin_account_moderate(self, id, action=None, report_id=None, warning_preset_id=None, text=None, send_email_notification=True):
219 """
220 Perform a moderation action on an account.
221
222 Valid actions are:
223 * "disable" - for a local user, disable login.
224 * "silence" - hide the users posts from all public timelines.
225 * "suspend" - irreversibly delete all the user's posts, past and future.
226 * "sensitive" - forcce an accounts media visibility to always be sensitive.
227
228 If no action is specified, the user is only issued a warning.
229
230 Specify the id of a report as `report_id` to close the report with this moderation action as the resolution.
231 Specify `warning_preset_id` to use a warning preset as the notification text to the user, or `text` to specify text directly.
232 If both are specified, they are concatenated (preset first). Note that there is currently no API to retrieve or create
233 warning presets.
234
235 Set `send_email_notification` to False to not send the user an email notification informing them of the moderation action.
236 """
237 if action is None:
238 action = "none"
239
240 if not send_email_notification:
241 send_email_notification = None
242
243 id = self.__unpack_id(id)
244 if report_id is not None:
245 report_id = self.__unpack_id(report_id)
246
247 params = self.__generate_params(locals(), ['id', 'action'])
248
249 params["type"] = action
250
251 self.__api_request(
252 'POST', '/api/v1/admin/accounts/{0}/action'.format(id), params)
253
254 @api_version("2.9.1", "2.9.1", _DICT_VERSION_REPORT)
255 def admin_reports(self, resolved=False, account_id=None, target_account_id=None, max_id=None, min_id=None, since_id=None, limit=None):
256 """
257 Fetches the list of reports.
258
259 Set `resolved` to True to search for resolved reports. `account_id` and `target_account_id`
260 can be used to get reports filed by or about a specific user.
261
262 Returns a list of :ref:`report dicts <report dicts>`.
263 """
264 if max_id is not None:
265 max_id = self.__unpack_id(max_id, dateconv=True)
266
267 if min_id is not None:
268 min_id = self.__unpack_id(min_id, dateconv=True)
269
270 if since_id is not None:
271 since_id = self.__unpack_id(since_id, dateconv=True)
272
273 if account_id is not None:
274 account_id = self.__unpack_id(account_id)
275
276 if target_account_id is not None:
277 target_account_id = self.__unpack_id(target_account_id)
278
279 if not resolved:
280 resolved = None
281
282 params = self.__generate_params(locals())
283 return self.__api_request('GET', '/api/v1/admin/reports', params)
284
285 @api_version("2.9.1", "2.9.1", _DICT_VERSION_REPORT)
286 def admin_report(self, id):
287 """
288 Fetches the report with the given id.
289
290 Returns a :ref:`report dict <report dict>`.
291 """
292 id = self.__unpack_id(id)
293 return self.__api_request('GET', '/api/v1/admin/reports/{0}'.format(id))
294
295 @api_version("2.9.1", "2.9.1", _DICT_VERSION_REPORT)
296 def admin_report_assign(self, id):
297 """
298 Assigns the given report to the logged-in user.
299
300 Returns the updated :ref:`report dict <report dict>`.
301 """
302 id = self.__unpack_id(id)
303 return self.__api_request('POST', '/api/v1/admin/reports/{0}/assign_to_self'.format(id))
304
305 @api_version("2.9.1", "2.9.1", _DICT_VERSION_REPORT)
306 def admin_report_unassign(self, id):
307 """
308 Unassigns the given report from the logged-in user.
309
310 Returns the updated :ref:`report dict <report dict>`.
311 """
312 id = self.__unpack_id(id)
313 return self.__api_request('POST', '/api/v1/admin/reports/{0}/unassign'.format(id))
314
315 @api_version("2.9.1", "2.9.1", _DICT_VERSION_REPORT)
316 def admin_report_reopen(self, id):
317 """
318 Reopens a closed report.
319
320 Returns the updated :ref:`report dict <report dict>`.
321 """
322 id = self.__unpack_id(id)
323 return self.__api_request('POST', '/api/v1/admin/reports/{0}/reopen'.format(id))
324
325 @api_version("2.9.1", "2.9.1", _DICT_VERSION_REPORT)
326 def admin_report_resolve(self, id):
327 """
328 Marks a report as resolved (without taking any action).
329
330 Returns the updated :ref:`report dict <report dict>`.
331 """
332 id = self.__unpack_id(id)
333 return self.__api_request('POST', '/api/v1/admin/reports/{0}/resolve'.format(id))
334
335 @api_version("3.5.0", "3.5.0", _DICT_VERSION_HASHTAG)
336 def admin_trending_tags(self, limit=None):
337 """
338 Admin version of :ref:`trending_tags() <trending_tags()>`. Includes unapproved tags.
339
340 Returns a list of :ref:`hashtag dicts <hashtag dicts>`, sorted by the instance's trending algorithm,
341 descending.
342 """
343 params = self.__generate_params(locals())
344 return self.__api_request('GET', '/api/v1/admin/trends/tags', params)
345
346 @api_version("3.5.0", "3.5.0", _DICT_VERSION_STATUS)
347 def admin_trending_statuses(self):
348 """
349 Admin version of :ref:`trending_statuses() <trending_statuses()>`. Includes unapproved tags.
350
351 Returns a list of :ref:`status dicts <status dicts>`, sorted by the instance's trending algorithm,
352 descending.
353 """
354 params = self.__generate_params(locals())
355 return self.__api_request('GET', '/api/v1/admin/trends/statuses', params)
356
357 @api_version("3.5.0", "3.5.0", _DICT_VERSION_CARD)
358 def admin_trending_links(self):
359 """
360 Admin version of :ref:`trending_links() <trending_links()>`. Includes unapproved tags.
361
362 Returns a list of :ref:`card dicts <card dicts>`, sorted by the instance's trending algorithm,
363 descending.
364 """
365 params = self.__generate_params(locals())
366 return self.__api_request('GET', '/api/v1/admin/trends/links', params)
367
368 @api_version("4.0.0", "4.0.0", _DICT_VERSION_ADMIN_DOMAIN_BLOCK)
369 def admin_domain_blocks(self, id=None, limit:int=None):
370 """
371 Fetches a list of blocked domains. Requires scope `admin:read:domain_blocks`.
372
373 Provide an `id` to fetch a specific domain block based on its database id.
374
375 Returns a list of :ref:`admin domain block dicts <admin domain block dicts>`, raises a `MastodonAPIError` if the specified block does not exist.
376 """
377 if id is not None:
378 id = self.__unpack_id(id)
379 return self.__api_request('GET', '/api/v1/admin/domain_blocks/{0}'.format(id))
380 else:
381 params = self.__generate_params(locals(),['limit'])
382 return self.__api_request('GET', '/api/v1/admin/domain_blocks/', params)
383
384 @api_version("4.0.0", "4.0.0", _DICT_VERSION_ADMIN_DOMAIN_BLOCK)
385 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):
386 """
387 Perform a moderation action on a domain. Requires scope `admin:write:domain_blocks`.
388
389 Valid severities are:
390 * "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.
391 * "suspend" - deny interactions with this instance going forward. This action is reversible.
392 * "limit" - generally used with reject_media=true to force reject media from an instance without silencing or suspending..
393
394 If no action is specified, the domain is only silenced.
395 `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.
396 `reject_media` will not download remote media on to your local instance media storage.
397 `reject_reports` ignores all reports from the remote instance.
398 `private_comment` sets a private admin comment for the domain.
399 `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.
400 `obfuscate` censors some part of the domain name. Useful if the domain name contains unwanted words like slurs.
401
402 Returns the new domain block as an :ref:`admin domain block dict <admin domain block dict>`.
403 """
404 if domain is None:
405 raise AttributeError("Must provide a domain to block a domain")
406 params = self.__generate_params(locals())
407 return self.__api_request('POST', '/api/v1/admin/domain_blocks/', params)
408
409 @api_version("4.0.0", "4.0.0", _DICT_VERSION_ADMIN_DOMAIN_BLOCK)
410 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):
411 """
412 Modify existing moderation action on a domain. Requires scope `admin:write:domain_blocks`.
413
414 Valid severities are:
415 * "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.
416 * "suspend" - deny interactions with this instance going forward. This action is reversible.
417 * "limit" - generally used with reject_media=true to force reject media from an instance without silencing or suspending.
418
419 If no action is specified, the domain is only silenced.
420 `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.
421 `reject_media` will not download remote media on to your local instance media storage.
422 `reject_reports` ignores all reports from the remote instance.
423 `private_comment` sets a private admin comment for the domain.
424 `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.
425 `obfuscate` censors some part of the domain name. Useful if the domain name contains unwanted words like slurs.
426
427 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.
428 """
429 if id is None:
430 raise AttributeError("Must provide an id to modify the existing moderation actions on a given domain.")
431 id = self.__unpack_id(id)
432 params = self.__generate_params(locals(), ["id"])
433 return self.__api_request('PUT', '/api/v1/admin/domain_blocks/{0}'.format(id), params)
434
435 @api_version("4.0.0", "4.0.0", _DICT_VERSION_ADMIN_DOMAIN_BLOCK)
436 def admin_delete_domain_block(self, id=None):
437 """
438 Removes moderation action against a given domain. Requires scope `admin:write:domain_blocks`.
439
440 Provide an `id` to remove a specific domain block based on its database id.
441
442 Raises a `MastodonAPIError` if the specified block does not exist.
443 """
444 if id is not None:
445 id = self.__unpack_id(id)
446 self.__api_request('DELETE', '/api/v1/admin/domain_blocks/{0}'.format(id))
447 else:
448 raise AttributeError("You must provide an id of an existing domain block to remove it.")
449
450 @api_version("3.5.0", "3.5.0", _DICT_VERSION_ADMIN_MEASURE)
451 def admin_measures(self, start_at, end_at, active_users=False, new_users=False, interactions=False, opened_reports = False, resolved_reports=False,
452 tag_accounts=None, tag_uses=None, tag_servers=None, instance_accounts=None, instance_media_attachments=None, instance_reports=None,
453 instance_statuses=None, instance_follows=None, instance_followers=None):
454 """
455 Retrieves numerical instance information for the time period (at day granularity) between `start_at` and `end_at`.
456
457 * `active_users`: Pass true to retrieve the number of active users on your instance within the time period
458 * `new_users`: Pass true to retrieve the number of users who joined your instance within the time period
459 * `interactions`: Pass true to retrieve the number of interactions (favourites, boosts, replies) on local statuses within the time period
460 * `opened_reports`: Pass true to retrieve the number of reports filed within the time period
461 * `resolved_reports` = Pass true to retrieve the number of reports resolved within the time period
462 * `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
463 * `tag_uses`: Pass a tag ID to get the number of statuses which used that tag within the time period
464 * `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
465 * `instance_accounts`: Pass a domain to get the number of accounts originating from that remote domain within the time period
466 * `instance_media_attachments`: Pass a domain to get the amount of space used by media attachments from that remote domain within the time period
467 * `instance_reports`: Pass a domain to get the number of reports filed against accounts from that remote domain within the time period
468 * `instance_statuses`: Pass a domain to get the number of statuses originating from that remote domain within the time period
469 * `instance_follows`: Pass a domain to get the number of accounts from a remote domain followed by that local user within the time period
470 * `instance_followers`: Pass a domain to get the number of local accounts followed by accounts from that remote domain within the time period
471
472 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
473 might take a long time to compute and, in fact, time out.
474
475 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
476 release.
477
478 Returns a list of :ref:`admin measure dicts <admin measure dicts>`.
479 """
480 params_init = locals()
481 keys = []
482 for key in ["active_users", "new_users", "interactions", "opened_reports", "resolved_reports"]:
483 if params_init[key] == True:
484 keys.append(key)
485
486 params = {}
487 for key in ["tag_accounts", "tag_uses", "tag_servers"]:
488 if params_init[key] is not None:
489 keys.append(key)
490 params[key] = {"id": self.__unpack_id(params_init[key])}
491 for key in ["instance_accounts", "instance_media_attachments", "instance_reports", "instance_statuses", "instance_follows", "instance_followers"]:
492 if params_init[key] is not None:
493 keys.append(key)
494 params[key] = {"domain": Mastodon.__deprotocolize(params_init[key]).split("/")[0]}
495
496 if len(keys) == 0:
497 raise MastodonIllegalArgumentError("Must request at least one metric.")
498
499 params["keys"] = keys
500 params["start_at"] = self.__consistent_isoformat_utc(start_at)
501 params["end_at"] = self.__consistent_isoformat_utc(end_at)
502
503 return self.__api_request('POST', '/api/v1/admin/measures', params, use_json=True)
504
505 @api_version("3.5.0", "3.5.0", _DICT_VERSION_ADMIN_DIMENSION)
506 def admin_dimensions(self, start_at, end_at, limit=None, languages=False, sources=False, servers=False, space_usage=False, software_versions=False,
507 tag_servers=None, tag_languages=None, instance_accounts=None, instance_languages=None):
508 """
509 Retrieves primarily categorical instance information for the time period (at day granularity) between `start_at` and `end_at`.
510
511 * `languages`: Pass true to get the most-used languages on this server
512 * `sources`: Pass true to get the most-used client apps on this server
513 * `servers`: Pass true to get the remote servers with the most statuses
514 * `space_usage`: Pass true to get the how much space is used by different components your software stack
515 * `software_versions`: Pass true to get the version numbers for your software stack
516 * `tag_servers`: Pass a tag ID to get the most-common servers for statuses including a trending tag
517 * `tag_languages`: Pass a tag ID to get the most-used languages for statuses including a trending tag
518 * `instance_accounts`: Pass a domain to get the most-followed accounts from a remote server
519 * `instance_languages`: Pass a domain to get the most-used languages from a remote server
520
521 Pass `limit` to set how many results you want on queries where that makes sense.
522
523 This API call is relatively expensive - watch your servers load if you want to get a lot of statistical data.
524
525 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
526 release.
527
528 Returns a list of :ref:`admin dimension dicts <admin dimension dicts>`.
529 """
530 params_init = locals()
531 keys = []
532 for key in ["languages", "sources", "servers", "space_usage", "software_versions"]:
533 if params_init[key] == True:
534 keys.append(key)
535
536 params = {}
537 for key in ["tag_servers", "tag_languages"]:
538 if params_init[key] is not None:
539 keys.append(key)
540 params[key] = {"id": self.__unpack_id(params_init[key])}
541 for key in ["instance_accounts", "instance_languages"]:
542 if params_init[key] is not None:
543 keys.append(key)
544 params[key] = {"domain": Mastodon.__deprotocolize(params_init[key]).split("/")[0]}
545
546 if len(keys) == 0:
547 raise MastodonIllegalArgumentError("Must request at least one dimension.")
548
549 params["keys"] = keys
550 if limit is not None:
551 params["limit"] = limit
552 params["start_at"] = self.__consistent_isoformat_utc(start_at)
553 params["end_at"] = self.__consistent_isoformat_utc(end_at)
554
555 return self.__api_request('POST', '/api/v1/admin/dimensions', params, use_json=True)
556
557 @api_version("3.5.0", "3.5.0", _DICT_VERSION_ADMIN_RETENTION)
558 def admin_retention(self, start_at, end_at, frequency="day"):
559 """
560 Gets user retention statistics (at `frequency` - "day" or "month" - granularity) between `start_at` and `end_at`.
561
562 Returns a list of :ref:`admin retention dicts <admin retention dicts>`
563 """
564 if not frequency in ["day", "month"]:
565 raise MastodonIllegalArgumentError("Frequency must be day or month")
566
567 params = {
568 "start_at": self.__consistent_isoformat_utc(start_at),
569 "end_at": self.__consistent_isoformat_utc(end_at),
570 "frequency": frequency
571 }
572 return self.__api_request('POST', '/api/v1/admin/retention', params) \ No newline at end of file
diff --git a/mastodon/authentication.py b/mastodon/authentication.py
index b7f15c9..d4cb283 100644
--- a/mastodon/authentication.py
+++ b/mastodon/authentication.py
@@ -1,3 +1,5 @@
1# authentication.py - app and user creation, login, oauth, getting app info, and the constructor
2
1import requests 3import requests
2from requests.models import urlencode 4from requests.models import urlencode
3import datetime 5import datetime
@@ -5,9 +7,11 @@ import os
5import time 7import time
6import collections 8import collections
7 9
8from .error import MastodonIllegalArgumentError, MastodonNetworkError, MastodonVersionError, MastodonAPIError 10from .errors import MastodonIllegalArgumentError, MastodonNetworkError, MastodonVersionError, MastodonAPIError
11from .versions import _DICT_VERSION_APPLICATION
9from .defaults import _DEFAULT_SCOPES, _SCOPE_SETS, _DEFAULT_TIMEOUT 12from .defaults import _DEFAULT_SCOPES, _SCOPE_SETS, _DEFAULT_TIMEOUT
10from .utility import parse_version_string 13from .utility import parse_version_string, api_version
14
11from .internals import Mastodon as Internals 15from .internals import Mastodon as Internals
12 16
13class Mastodon(Internals): 17class Mastodon(Internals):
@@ -367,4 +371,16 @@ class Mastodon(Internals):
367 371
368 # We are now logged out, clear token and logged in id 372 # We are now logged out, clear token and logged in id
369 self.access_token = None 373 self.access_token = None
370 self.__logged_in_id = None \ No newline at end of file 374 self.__logged_in_id = None
375
376 ###
377 # Reading data: Apps
378 ###
379 @api_version("2.0.0", "2.7.2", _DICT_VERSION_APPLICATION)
380 def app_verify_credentials(self):
381 """
382 Fetch information about the current application.
383
384 Returns an :ref:`application dict <application dict>`.
385 """
386 return self.__api_request('GET', '/api/v1/apps/verify_credentials')
diff --git a/mastodon/conversations.py b/mastodon/conversations.py
new file mode 100644
index 0000000..ba3ee61
--- /dev/null
+++ b/mastodon/conversations.py
@@ -0,0 +1,43 @@
1# conversations.py - conversation endpoints
2
3from .versions import _DICT_VERSION_CONVERSATION
4from .utility import api_version
5
6from .internals import Mastodon as Internals
7
8class Mastodon(Internals):
9 ###
10 # Reading data: Conversations
11 ###
12 @api_version("2.6.0", "2.6.0", _DICT_VERSION_CONVERSATION)
13 def conversations(self, max_id=None, min_id=None, since_id=None, limit=None):
14 """
15 Fetches a user's conversations.
16
17 Returns a list of :ref:`conversation dicts <conversation dicts>`.
18 """
19 if max_id is not None:
20 max_id = self.__unpack_id(max_id, dateconv=True)
21
22 if min_id is not None:
23 min_id = self.__unpack_id(min_id, dateconv=True)
24
25 if since_id is not None:
26 since_id = self.__unpack_id(since_id, dateconv=True)
27
28 params = self.__generate_params(locals())
29 return self.__api_request('GET', "/api/v1/conversations/", params)
30
31 ###
32 # Writing data: Conversations
33 ###
34 @api_version("2.6.0", "2.6.0", _DICT_VERSION_CONVERSATION)
35 def conversations_read(self, id):
36 """
37 Marks a single conversation as read.
38
39 Returns the updated :ref:`conversation dict <conversation dict>`.
40 """
41 id = self.__unpack_id(id)
42 url = '/api/v1/conversations/{0}/read'.format(str(id))
43 return self.__api_request('POST', url)
diff --git a/mastodon/endorsements.py b/mastodon/endorsements.py
new file mode 100644
index 0000000..b6c27a0
--- /dev/null
+++ b/mastodon/endorsements.py
@@ -0,0 +1,19 @@
1# notifications.py - endorsement endpoints
2
3from .versions import _DICT_VERSION_ACCOUNT
4from .utility import api_version
5
6from .internals import Mastodon as Internals
7
8class Mastodon(Internals):
9 ###
10 # Reading data: Endorsements
11 ###
12 @api_version("2.5.0", "2.5.0", _DICT_VERSION_ACCOUNT)
13 def endorsements(self):
14 """
15 Fetch list of users endorsed by the logged-in user.
16
17 Returns a list of :ref:`account dicts <account dicts>`.
18 """
19 return self.__api_request('GET', '/api/v1/endorsements')
diff --git a/mastodon/error.py b/mastodon/errors.py
index 85cc313..85cc313 100644
--- a/mastodon/error.py
+++ b/mastodon/errors.py
diff --git a/mastodon/favourites.py b/mastodon/favourites.py
new file mode 100644
index 0000000..9d86424
--- /dev/null
+++ b/mastodon/favourites.py
@@ -0,0 +1,52 @@
1# favourites.py - favourites and also bookmarks
2
3from .versions import _DICT_VERSION_STATUS
4from .utility import api_version
5
6from .internals import Mastodon as Internals
7
8class Mastodon(Internals):
9 ###
10 # Reading data: Favourites
11 ###
12 @api_version("1.0.0", "2.6.0", _DICT_VERSION_STATUS)
13 def favourites(self, max_id=None, min_id=None, since_id=None, limit=None):
14 """
15 Fetch the logged-in user's favourited statuses.
16
17 Returns a list of :ref:`status dicts <status dicts>`.
18 """
19 if max_id is not None:
20 max_id = self.__unpack_id(max_id, dateconv=True)
21
22 if min_id is not None:
23 min_id = self.__unpack_id(min_id, dateconv=True)
24
25 if since_id is not None:
26 since_id = self.__unpack_id(since_id, dateconv=True)
27
28 params = self.__generate_params(locals())
29 return self.__api_request('GET', '/api/v1/favourites', params)
30
31 ###
32 # Reading data: Bookmarks
33 ###
34 @api_version("3.1.0", "3.1.0", _DICT_VERSION_STATUS)
35 def bookmarks(self, max_id=None, min_id=None, since_id=None, limit=None):
36 """
37 Get a list of statuses bookmarked by the logged-in user.
38
39 Returns a list of :ref:`status dicts <status dicts>`.
40 """
41 if max_id is not None:
42 max_id = self.__unpack_id(max_id, dateconv=True)
43
44 if min_id is not None:
45 min_id = self.__unpack_id(min_id, dateconv=True)
46
47 if since_id is not None:
48 since_id = self.__unpack_id(since_id, dateconv=True)
49
50 params = self.__generate_params(locals())
51 return self.__api_request('GET', '/api/v1/bookmarks', params)
52 \ No newline at end of file
diff --git a/mastodon/filters.py b/mastodon/filters.py
new file mode 100644
index 0000000..5f373d4
--- /dev/null
+++ b/mastodon/filters.py
@@ -0,0 +1,119 @@
1# filters.py - Filter-related endpoints
2
3import re
4
5from .versions import _DICT_VERSION_FILTER
6from .errors import MastodonIllegalArgumentError
7from .utility import api_version
8
9from .internals import Mastodon as Internals
10
11class Mastodon(Internals):
12 ###
13 # Reading data: Keyword filters
14 ###
15 @api_version("2.4.3", "2.4.3", _DICT_VERSION_FILTER)
16 def filters(self):
17 """
18 Fetch all of the logged-in user's filters.
19
20 Returns a list of :ref:`filter dicts <filter dicts>`. Not paginated.
21 """
22 return self.__api_request('GET', '/api/v1/filters')
23
24 @api_version("2.4.3", "2.4.3", _DICT_VERSION_FILTER)
25 def filter(self, id):
26 """
27 Fetches information about the filter with the specified `id`.
28
29 Returns a :ref:`filter dict <filter dict>`.
30 """
31 id = self.__unpack_id(id)
32 url = '/api/v1/filters/{0}'.format(str(id))
33 return self.__api_request('GET', url)
34
35 @api_version("2.4.3", "2.4.3", _DICT_VERSION_FILTER)
36 def filters_apply(self, objects, filters, context):
37 """
38 Helper function: Applies a list of filters to a list of either statuses
39 or notifications and returns only those matched by none. This function will
40 apply all filters that match the context provided in `context`, i.e.
41 if you want to apply only notification-relevant filters, specify
42 'notifications'. Valid contexts are 'home', 'notifications', 'public' and 'thread'.
43 """
44
45 # Build filter regex
46 filter_strings = []
47 for keyword_filter in filters:
48 if not context in keyword_filter["context"]:
49 continue
50
51 filter_string = re.escape(keyword_filter["phrase"])
52 if keyword_filter["whole_word"]:
53 filter_string = "\\b" + filter_string + "\\b"
54 filter_strings.append(filter_string)
55 filter_re = re.compile("|".join(filter_strings), flags=re.IGNORECASE)
56
57 # Apply
58 filter_results = []
59 for filter_object in objects:
60 filter_status = filter_object
61 if "status" in filter_object:
62 filter_status = filter_object["status"]
63 filter_text = filter_status["content"]
64 filter_text = re.sub(r"<.*?>", " ", filter_text)
65 filter_text = re.sub(r"\s+", " ", filter_text).strip()
66 if not filter_re.search(filter_text):
67 filter_results.append(filter_object)
68 return filter_results
69
70 ###
71 # Writing data: Keyword filters
72 ###
73 @api_version("2.4.3", "2.4.3", _DICT_VERSION_FILTER)
74 def filter_create(self, phrase, context, irreversible=False, whole_word=True, expires_in=None):
75 """
76 Creates a new keyword filter. `phrase` is the phrase that should be
77 filtered out, `context` specifies from where to filter the keywords.
78 Valid contexts are 'home', 'notifications', 'public' and 'thread'.
79
80 Set `irreversible` to True if you want the filter to just delete statuses
81 server side. This works only for the 'home' and 'notifications' contexts.
82
83 Set `whole_word` to False if you want to allow filter matches to
84 start or end within a word, not only at word boundaries.
85
86 Set `expires_in` to specify for how many seconds the filter should be
87 kept around.
88
89 Returns the :ref:`filter dict <filter dict>` of the newly created filter.
90 """
91 params = self.__generate_params(locals())
92
93 for context_val in context:
94 if not context_val in ['home', 'notifications', 'public', 'thread']:
95 raise MastodonIllegalArgumentError('Invalid filter context.')
96
97 return self.__api_request('POST', '/api/v1/filters', params)
98
99 @api_version("2.4.3", "2.4.3", _DICT_VERSION_FILTER)
100 def filter_update(self, id, phrase=None, context=None, irreversible=None, whole_word=None, expires_in=None):
101 """
102 Updates the filter with the given `id`. Parameters are the same
103 as in `filter_create()`.
104
105 Returns the :ref:`filter dict <filter dict>` of the updated filter.
106 """
107 id = self.__unpack_id(id)
108 params = self.__generate_params(locals(), ['id'])
109 url = '/api/v1/filters/{0}'.format(str(id))
110 return self.__api_request('PUT', url, params)
111
112 @api_version("2.4.3", "2.4.3", "2.4.3")
113 def filter_delete(self, id):
114 """
115 Deletes the filter with the given `id`.
116 """
117 id = self.__unpack_id(id)
118 url = '/api/v1/filters/{0}'.format(str(id))
119 self.__api_request('DELETE', url) \ No newline at end of file
diff --git a/mastodon/hashtags.py b/mastodon/hashtags.py
new file mode 100644
index 0000000..89e8cac
--- /dev/null
+++ b/mastodon/hashtags.py
@@ -0,0 +1,52 @@
1# hashtags.py - hashtag and featured-hashtag endpoints
2
3from .versions import _DICT_VERSION_FEATURED_TAG, _DICT_VERSION_HASHTAG
4from .utility import api_version
5
6from .internals import Mastodon as Internals
7
8class Mastodon(Internals):
9 ###
10 # Reading data: Featured hashtags
11 ###
12 @api_version("3.0.0", "3.0.0", _DICT_VERSION_FEATURED_TAG)
13 def featured_tags(self):
14 """
15 Return the hashtags the logged-in user has set to be featured on
16 their profile as a list of :ref:`featured tag dicts <featured tag dicts>`.
17
18 Returns a list of :ref:`featured tag dicts <featured tag dicts>`.
19 """
20 return self.__api_request('GET', '/api/v1/featured_tags')
21
22 @api_version("3.0.0", "3.0.0", _DICT_VERSION_HASHTAG)
23 def featured_tag_suggestions(self):
24 """
25 Returns the logged-in user's 10 most commonly-used hashtags.
26
27 Returns a list of :ref:`hashtag dicts <hashtag dicts>`.
28 """
29 return self.__api_request('GET', '/api/v1/featured_tags/suggestions')
30
31 ###
32 # Writing data: Featured hashtags
33 ###
34 @api_version("3.0.0", "3.0.0", _DICT_VERSION_FEATURED_TAG)
35 def featured_tag_create(self, name):
36 """
37 Creates a new featured hashtag displayed on the logged-in user's profile.
38
39 Returns a :ref:`featured tag dict <featured tag dict>` with the newly featured tag.
40 """
41 params = self.__generate_params(locals())
42 return self.__api_request('POST', '/api/v1/featured_tags', params)
43
44 @api_version("3.0.0", "3.0.0", _DICT_VERSION_FEATURED_TAG)
45 def featured_tag_delete(self, id):
46 """
47 Deletes one of the logged-in user's featured hashtags.
48 """
49 id = self.__unpack_id(id)
50 url = '/api/v1/featured_tags/{0}'.format(str(id))
51 self.__api_request('DELETE', url)
52 \ No newline at end of file
diff --git a/mastodon/instance.py b/mastodon/instance.py
index 88445d1..dfbbefb 100644
--- a/mastodon/instance.py
+++ b/mastodon/instance.py
@@ -1,5 +1,7 @@
1from .versions import _DICT_VERSION_INSTANCE, _DICT_VERSION_ACTIVITY 1# instance.py - instance-level endpoints, directory, emoji, announcements
2from .error import MastodonIllegalArgumentError, MastodonNotFoundError 2
3from .versions import _DICT_VERSION_INSTANCE, _DICT_VERSION_ACTIVITY, _DICT_VERSION_ACCOUNT, _DICT_VERSION_EMOJI, _DICT_VERSION_ANNOUNCEMENT
4from .errors import MastodonIllegalArgumentError, MastodonNotFoundError
3from .utility import api_version 5from .utility import api_version
4from .compat import urlparse 6from .compat import urlparse
5 7
@@ -94,3 +96,94 @@ class Mastodon(Internals):
94 Returns a list of `id` + `text` dicts, same as the `rules` field in the :ref:`instance dicts <instance dicts>`. 96 Returns a list of `id` + `text` dicts, same as the `rules` field in the :ref:`instance dicts <instance dicts>`.
95 """ 97 """
96 return self.__api_request('GET', '/api/v1/instance/rules') 98 return self.__api_request('GET', '/api/v1/instance/rules')
99
100 ###
101 # Reading data: Directory
102 ###
103 @api_version("3.0.0", "3.0.0", _DICT_VERSION_ACCOUNT)
104 def directory(self, offset=None, limit=None, order=None, local=None):
105 """
106 Fetch the contents of the profile directory, if enabled on the server.
107
108 `offset` how many accounts to skip before returning results. Default 0.
109
110 `limit` how many accounts to load. Default 40.
111
112 `order` "active" to sort by most recently posted statuses (default) or
113 "new" to sort by most recently created profiles.
114
115 `local` True to return only local accounts.
116
117 Returns a list of :ref:`account dicts <account dicts>`.
118
119 """
120 params = self.__generate_params(locals())
121 return self.__api_request('GET', '/api/v1/directory', params)
122
123 ###
124 # Reading data: Emoji
125 ###
126 @api_version("2.1.0", "2.1.0", _DICT_VERSION_EMOJI)
127 def custom_emojis(self):
128 """
129 Fetch the list of custom emoji the instance has installed.
130
131 Does not require authentication unless locked down by the administrator.
132
133 Returns a list of :ref:`emoji dicts <emoji dicts>`.
134 """
135 return self.__api_request('GET', '/api/v1/custom_emojis')
136
137 ##
138 # Reading data: Announcements
139 ##
140 @api_version("3.1.0", "3.1.0", _DICT_VERSION_ANNOUNCEMENT)
141 def announcements(self):
142 """
143 Fetch currently active announcements.
144
145 Returns a list of :ref:`announcement dicts <announcement dicts>`.
146 """
147 return self.__api_request('GET', '/api/v1/announcements')
148
149 ###
150 # Writing data: Annoucements
151 ###
152 @api_version("3.1.0", "3.1.0", "3.1.0")
153 def announcement_dismiss(self, id):
154 """
155 Set the given annoucement to read.
156 """
157 id = self.__unpack_id(id)
158
159 url = '/api/v1/announcements/{0}/dismiss'.format(str(id))
160 self.__api_request('POST', url)
161
162 @api_version("3.1.0", "3.1.0", "3.1.0")
163 def announcement_reaction_create(self, id, reaction):
164 """
165 Add a reaction to an announcement. `reaction` can either be a unicode emoji
166 or the name of one of the instances custom emoji.
167
168 Will throw an API error if the reaction name is not one of the allowed things
169 or when trying to add a reaction that the user has already added (adding a
170 reaction that a different user added is legal and increments the count).
171 """
172 id = self.__unpack_id(id)
173
174 url = '/api/v1/announcements/{0}/reactions/{1}'.format(
175 str(id), reaction)
176 self.__api_request('PUT', url)
177
178 @api_version("3.1.0", "3.1.0", "3.1.0")
179 def announcement_reaction_delete(self, id, reaction):
180 """
181 Remove a reaction to an announcement.
182
183 Will throw an API error if the reaction does not exist.
184 """
185 id = self.__unpack_id(id)
186
187 url = '/api/v1/announcements/{0}/reactions/{1}'.format(
188 str(id), reaction)
189 self.__api_request('DELETE', url)
diff --git a/mastodon/internals.py b/mastodon/internals.py
index 0e77421..aa82553 100644
--- a/mastodon/internals.py
+++ b/mastodon/internals.py
@@ -1,3 +1,5 @@
1# internals.py - many internal helpers
2
1import datetime 3import datetime
2from contextlib import closing 4from contextlib import closing
3import mimetypes 5import mimetypes
@@ -14,7 +16,7 @@ import base64
14import os 16import os
15 17
16from .utility import AttribAccessDict, AttribAccessList, parse_version_string 18from .utility import AttribAccessDict, AttribAccessList, parse_version_string
17from .error import MastodonNetworkError, MastodonIllegalArgumentError, MastodonRatelimitError, MastodonNotFoundError, \ 19from .errors import MastodonNetworkError, MastodonIllegalArgumentError, MastodonRatelimitError, MastodonNotFoundError, \
18 MastodonUnauthorizedError, MastodonInternalServerError, MastodonBadGatewayError, MastodonServiceUnavailableError, \ 20 MastodonUnauthorizedError, MastodonInternalServerError, MastodonBadGatewayError, MastodonServiceUnavailableError, \
19 MastodonGatewayTimeoutError, MastodonServerError, MastodonAPIError, MastodonMalformedEventError 21 MastodonGatewayTimeoutError, MastodonServerError, MastodonAPIError, MastodonMalformedEventError
20from .compat import urlparse, magic, PurePath 22from .compat import urlparse, magic, PurePath
diff --git a/mastodon/lists.py b/mastodon/lists.py
new file mode 100644
index 0000000..0882133
--- /dev/null
+++ b/mastodon/lists.py
@@ -0,0 +1,112 @@
1# list.py - list endpoints
2
3from .versions import _DICT_VERSION_LIST, _DICT_VERSION_ACCOUNT
4from .utility import api_version
5
6from .internals import Mastodon as Internals
7
8class Mastodon(Internals):
9 ###
10 # Reading data: Lists
11 ###
12 @api_version("2.1.0", "2.1.0", _DICT_VERSION_LIST)
13 def lists(self):
14 """
15 Fetch a list of all the Lists by the logged-in user.
16
17 Returns a list of :ref:`list dicts <list dicts>`.
18 """
19 return self.__api_request('GET', '/api/v1/lists')
20
21 @api_version("2.1.0", "2.1.0", _DICT_VERSION_LIST)
22 def list(self, id):
23 """
24 Fetch info about a specific list.
25
26 Returns a :ref:`list dict <list dict>`.
27 """
28 id = self.__unpack_id(id)
29 return self.__api_request('GET', '/api/v1/lists/{0}'.format(id))
30
31 @api_version("2.1.0", "2.6.0", _DICT_VERSION_ACCOUNT)
32 def list_accounts(self, id, max_id=None, min_id=None, since_id=None, limit=None):
33 """
34 Get the accounts that are on the given list.
35
36 Returns a list of :ref:`account dicts <account dicts>`.
37 """
38 id = self.__unpack_id(id)
39
40 if max_id is not None:
41 max_id = self.__unpack_id(max_id, dateconv=True)
42
43 if min_id is not None:
44 min_id = self.__unpack_id(min_id, dateconv=True)
45
46 if since_id is not None:
47 since_id = self.__unpack_id(since_id, dateconv=True)
48
49 params = self.__generate_params(locals(), ['id'])
50 return self.__api_request('GET', '/api/v1/lists/{0}/accounts'.format(id))
51
52 ###
53 # Writing data: Lists
54 ###
55 @api_version("2.1.0", "2.1.0", _DICT_VERSION_LIST)
56 def list_create(self, title):
57 """
58 Create a new list with the given `title`.
59
60 Returns the :ref:`list dict <list dict>` of the created list.
61 """
62 params = self.__generate_params(locals())
63 return self.__api_request('POST', '/api/v1/lists', params)
64
65 @api_version("2.1.0", "2.1.0", _DICT_VERSION_LIST)
66 def list_update(self, id, title):
67 """
68 Update info about a list, where "info" is really the lists `title`.
69
70 Returns the :ref:`list dict <list dict>` of the modified list.
71 """
72 id = self.__unpack_id(id)
73 params = self.__generate_params(locals(), ['id'])
74 return self.__api_request('PUT', '/api/v1/lists/{0}'.format(id), params)
75
76 @api_version("2.1.0", "2.1.0", "2.1.0")
77 def list_delete(self, id):
78 """
79 Delete a list.
80 """
81 id = self.__unpack_id(id)
82 self.__api_request('DELETE', '/api/v1/lists/{0}'.format(id))
83
84 @api_version("2.1.0", "2.1.0", "2.1.0")
85 def list_accounts_add(self, id, account_ids):
86 """
87 Add the account(s) given in `account_ids` to the list.
88 """
89 id = self.__unpack_id(id)
90
91 if not isinstance(account_ids, list):
92 account_ids = [account_ids]
93 account_ids = list(map(lambda x: self.__unpack_id(x), account_ids))
94
95 params = self.__generate_params(locals(), ['id'])
96 self.__api_request(
97 'POST', '/api/v1/lists/{0}/accounts'.format(id), params)
98
99 @api_version("2.1.0", "2.1.0", "2.1.0")
100 def list_accounts_delete(self, id, account_ids):
101 """
102 Remove the account(s) given in `account_ids` from the list.
103 """
104 id = self.__unpack_id(id)
105
106 if not isinstance(account_ids, list):
107 account_ids = [account_ids]
108 account_ids = list(map(lambda x: self.__unpack_id(x), account_ids))
109
110 params = self.__generate_params(locals(), ['id'])
111 self.__api_request(
112 'DELETE', '/api/v1/lists/{0}/accounts'.format(id), params) \ No newline at end of file
diff --git a/mastodon/media.py b/mastodon/media.py
new file mode 100644
index 0000000..3c815fb
--- /dev/null
+++ b/mastodon/media.py
@@ -0,0 +1,114 @@
1# admin.py - admin / moderation endpoints
2
3import time
4
5from .versions import _DICT_VERSION_MEDIA
6from .errors import MastodonVersionError, MastodonAPIError
7from .utility import api_version
8
9from .internals import Mastodon as Internals
10
11class Mastodon(Internals):
12 ###
13 # Reading data: Media
14 ###
15 @api_version("3.1.4", "3.1.4", _DICT_VERSION_MEDIA)
16 def media(self, id):
17 """
18 Get the updated JSON for one non-attached / in progress media upload belonging
19 to the logged-in user.
20 """
21 id = self.__unpack_id(id)
22 return self.__api_request('GET', '/api/v1/media/{0}'.format(str(id)))
23
24 ###
25 # Writing data: Media
26 ###
27 @api_version("1.0.0", "3.2.0", _DICT_VERSION_MEDIA)
28 def media_post(self, media_file, mime_type=None, description=None, focus=None, file_name=None, thumbnail=None, thumbnail_mime_type=None, synchronous=False):
29 """
30 Post an image, video or audio file. `media_file` can either be data or
31 a file name. If data is passed directly, the mime type has to be specified
32 manually, otherwise, it is determined from the file name. `focus` should be a tuple
33 of floats between -1 and 1, giving the x and y coordinates of the images
34 focus point for cropping (with the origin being the images center).
35
36 Throws a `MastodonIllegalArgumentError` if the mime type of the
37 passed data or file can not be determined properly.
38
39 `file_name` can be specified to upload a file with the given name,
40 which is ignored by Mastodon, but some other Fediverse server software
41 will display it. If no name is specified, a random name will be generated.
42 The filename of a file specified in media_file will be ignored.
43
44 Starting with Mastodon 3.2.0, `thumbnail` can be specified in the same way as `media_file`
45 to upload a custom thumbnail image for audio and video files.
46
47 Returns a :ref:`media dict <media dict>`. This contains the id that can be used in
48 status_post to attach the media file to a toot.
49
50 When using the v2 API (post Mastodon version 3.1.4), the `url` in the
51 returned dict will be `null`, since attachments are processed
52 asynchronously. You can fetch an updated dict using `media`. Pass
53 "synchronous" to emulate the old behaviour. Not recommended, inefficient
54 and deprecated, will eat your API quota, you know the deal.
55 """
56 files = {'file': self.__load_media_file(
57 media_file, mime_type, file_name)}
58
59 if focus is not None:
60 focus = str(focus[0]) + "," + str(focus[1])
61
62 if thumbnail is not None:
63 if not self.verify_minimum_version("3.2.0", cached=True):
64 raise MastodonVersionError('Thumbnail requires version > 3.2.0')
65 files["thumbnail"] = self.__load_media_file(
66 thumbnail, thumbnail_mime_type)
67
68 # Disambiguate URL by version
69 if self.verify_minimum_version("3.1.4", cached=True):
70 ret_dict = self.__api_request(
71 'POST', '/api/v2/media', files=files, params={'description': description, 'focus': focus})
72 else:
73 ret_dict = self.__api_request(
74 'POST', '/api/v1/media', files=files, params={'description': description, 'focus': focus})
75
76 # Wait for processing?
77 if synchronous:
78 if self.verify_minimum_version("3.1.4"):
79 while not "url" in ret_dict or ret_dict.url is None:
80 try:
81 ret_dict = self.media(ret_dict)
82 time.sleep(5.0)
83 except:
84 raise MastodonAPIError("Attachment could not be processed")
85 else:
86 # Old version always waits
87 return ret_dict
88
89 return ret_dict
90
91 @api_version("2.3.0", "3.2.0", _DICT_VERSION_MEDIA)
92 def media_update(self, id, description=None, focus=None, thumbnail=None, thumbnail_mime_type=None):
93 """
94 Update the metadata of the media file with the given `id`. `description` and
95 `focus` and `thumbnail` are as in :ref:`media_post() <media_post()>` .
96
97 Returns the updated :ref:`media dict <media dict>`.
98 """
99 id = self.__unpack_id(id)
100
101 if focus is not None:
102 focus = str(focus[0]) + "," + str(focus[1])
103
104 params = self.__generate_params(
105 locals(), ['id', 'thumbnail', 'thumbnail_mime_type'])
106
107 if thumbnail is not None:
108 if not self.verify_minimum_version("3.2.0", cached=True):
109 raise MastodonVersionError('Thumbnail requires version > 3.2.0')
110 files = {"thumbnail": self.__load_media_file(
111 thumbnail, thumbnail_mime_type)}
112 return self.__api_request('PUT', '/api/v1/media/{0}'.format(str(id)), params, files=files)
113 else:
114 return self.__api_request('PUT', '/api/v1/media/{0}'.format(str(id)), params)
diff --git a/mastodon/notifications.py b/mastodon/notifications.py
new file mode 100644
index 0000000..f65b3fb
--- /dev/null
+++ b/mastodon/notifications.py
@@ -0,0 +1,92 @@
1# notifications.py - notification endpoints
2
3from .versions import _DICT_VERSION_NOTIFICATION
4from .errors import MastodonIllegalArgumentError
5from .utility import api_version
6
7from .internals import Mastodon as Internals
8
9class Mastodon(Internals):
10 ###
11 # Reading data: Notifications
12 ###
13 @api_version("1.0.0", "3.5.0", _DICT_VERSION_NOTIFICATION)
14 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):
15 """
16 Fetch notifications (mentions, favourites, reblogs, follows) for the logged-in
17 user. Pass `account_id` to get only notifications originating from the given account.
18
19 There are different types of notifications:
20 * `follow` - A user followed the logged in user
21 * `follow_request` - A user has requested to follow the logged in user (for locked accounts)
22 * `favourite` - A user favourited a post by the logged in user
23 * `reblog` - A user reblogged a post by the logged in user
24 * `mention` - A user mentioned the logged in user
25 * `poll` - A poll the logged in user created or voted in has ended
26 * `update` - A status the logged in user has reblogged (and only those, as of 4.0.0) has been edited
27 * `status` - A user that the logged in user has enabned notifications for has enabled `notify` (see :ref:`account_follow() <account_follow()>`)
28 * `admin.sign_up` - For accounts with appropriate permissions (TODO: document which those are when adding the permission API): A new user has signed up
29 * `admin.report` - For accounts with appropriate permissions (TODO: document which those are when adding the permission API): A new report has been received
30 Parameters `exclude_types` and `types` are array of these types, specifying them will in- or exclude the
31 types of notifications given. It is legal to give both parameters at the same tine, the result will then
32 be the intersection of the results of both filters. Specifying `mentions_only` is a deprecated way to set
33 `exclude_types` to all but mentions.
34
35 Can be passed an `id` to fetch a single notification.
36
37 Returns a list of :ref:`notification dicts <notification dicts>`.
38 """
39 if mentions_only is not None:
40 if exclude_types is None and types is None:
41 if mentions_only:
42 if self.verify_minimum_version("3.5.0", cached=True):
43 types = ["mention"]
44 else:
45 exclude_types = ["follow", "favourite", "reblog", "poll", "follow_request"]
46 else:
47 raise MastodonIllegalArgumentError('Cannot specify exclude_types/types when mentions_only is present')
48 del mentions_only
49
50 if max_id is not None:
51 max_id = self.__unpack_id(max_id, dateconv=True)
52
53 if min_id is not None:
54 min_id = self.__unpack_id(min_id, dateconv=True)
55
56 if since_id is not None:
57 since_id = self.__unpack_id(since_id, dateconv=True)
58
59 if account_id is not None:
60 account_id = self.__unpack_id(account_id)
61
62 if id is None:
63 params = self.__generate_params(locals(), ['id'])
64 return self.__api_request('GET', '/api/v1/notifications', params)
65 else:
66 id = self.__unpack_id(id)
67 url = '/api/v1/notifications/{0}'.format(str(id))
68 return self.__api_request('GET', url)
69
70 ###
71 # Writing data: Notifications
72 ###
73 @api_version("1.0.0", "1.0.0", "1.0.0")
74 def notifications_clear(self):
75 """
76 Clear out a user's notifications
77 """
78 self.__api_request('POST', '/api/v1/notifications/clear')
79
80 @api_version("1.3.0", "2.9.2", "2.9.2")
81 def notifications_dismiss(self, id):
82 """
83 Deletes a single notification
84 """
85 id = self.__unpack_id(id)
86
87 if self.verify_minimum_version("2.9.2", cached=True):
88 url = '/api/v1/notifications/{0}/dismiss'.format(str(id))
89 self.__api_request('POST', url)
90 else:
91 params = self.__generate_params(locals())
92 self.__api_request('POST', '/api/v1/notifications/dismiss', params)
diff --git a/mastodon/polls.py b/mastodon/polls.py
new file mode 100644
index 0000000..d974531
--- /dev/null
+++ b/mastodon/polls.py
@@ -0,0 +1,61 @@
1# polls.py - poll related endpoints and tooling
2
3from .versions import _DICT_VERSION_POLL
4from .utility import api_version
5
6from .internals import Mastodon as Internals
7
8class Mastodon(Internals):
9 ###
10 # Reading data: Polls
11 ###
12 @api_version("2.8.0", "2.8.0", _DICT_VERSION_POLL)
13 def poll(self, id):
14 """
15 Fetch information about the poll with the given id
16
17 Returns a :ref:`poll dict <poll dict>`.
18 """
19 id = self.__unpack_id(id)
20 url = '/api/v1/polls/{0}'.format(str(id))
21 return self.__api_request('GET', url)
22
23 ###
24 # Writing data: Polls
25 ###
26 @api_version("2.8.0", "2.8.0", _DICT_VERSION_POLL)
27 def poll_vote(self, id, choices):
28 """
29 Vote in the given poll.
30
31 `choices` is the index of the choice you wish to register a vote for
32 (i.e. its index in the corresponding polls `options` field. In case
33 of a poll that allows selection of more than one option, a list of
34 indices can be passed.
35
36 You can only submit choices for any given poll once in case of
37 single-option polls, or only once per option in case of multi-option
38 polls.
39
40 Returns the updated :ref:`poll dict <poll dict>`
41 """
42 id = self.__unpack_id(id)
43 if not isinstance(choices, list):
44 choices = [choices]
45 params = self.__generate_params(locals(), ['id'])
46
47 url = '/api/v1/polls/{0}/votes'.format(id)
48 self.__api_request('POST', url, params)
49
50 def make_poll(self, options, expires_in, multiple=False, hide_totals=False):
51 """
52 Generate a poll object that can be passed as the `poll` option when posting a status.
53
54 options is an array of strings with the poll options (Maximum, by default: 4),
55 expires_in is the time in seconds for which the poll should be open.
56 Set multiple to True to allow people to choose more than one answer. Set
57 hide_totals to True to hide the results of the poll until it has expired.
58 """
59 poll_params = locals()
60 del poll_params["self"]
61 return poll_params \ No newline at end of file
diff --git a/mastodon/preferences.py b/mastodon/preferences.py
new file mode 100644
index 0000000..5e7435f
--- /dev/null
+++ b/mastodon/preferences.py
@@ -0,0 +1,71 @@
1# preferences.py - user preferences, markers
2
3import collections
4
5from .versions import _DICT_VERSION_PREFERENCES, _DICT_VERSION_MARKER
6from .errors import MastodonIllegalArgumentError
7from .utility import api_version
8
9from .internals import Mastodon as Internals
10
11class Mastodon(Internals):
12 ###
13 # Reading data: Preferences
14 ###
15 @api_version("2.8.0", "2.8.0", _DICT_VERSION_PREFERENCES)
16 def preferences(self):
17 """
18 Fetch the user's preferences, which can be used to set some default options.
19 As of 2.8.0, apps can only fetch, not update preferences.
20
21 Returns a :ref:`preference dict <preference dict>`.
22 """
23 return self.__api_request('GET', '/api/v1/preferences')
24
25 ##
26 # Reading data: Read markers
27 ##
28 @api_version("3.0.0", "3.0.0", _DICT_VERSION_MARKER)
29 def markers_get(self, timeline=["home"]):
30 """
31 Get the last-read-location markers for the specified timelines. Valid timelines
32 are the same as in :ref:`timeline() <timeline()>`
33
34 Note that despite the singular name, `timeline` can be a list.
35
36 Returns a dict of :ref:`read marker dicts <read marker dicts>`, keyed by timeline name.
37 """
38 if not isinstance(timeline, (list, tuple)):
39 timeline = [timeline]
40 params = self.__generate_params(locals())
41
42 return self.__api_request('GET', '/api/v1/markers', params)
43
44 ##
45 # Writing data: Read markers
46 ##
47 @api_version("3.0.0", "3.0.0", _DICT_VERSION_MARKER)
48 def markers_set(self, timelines, last_read_ids):
49 """
50 Set the "last read" marker(s) for the given timeline(s) to the given id(s)
51
52 Note that if you give an invalid timeline name, this will silently do nothing.
53
54 Returns a dict with the updated :ref:`read marker dicts <read marker dicts>`, keyed by timeline name.
55 """
56 if not isinstance(timelines, (list, tuple)):
57 timelines = [timelines]
58
59 if not isinstance(last_read_ids, (list, tuple)):
60 last_read_ids = [last_read_ids]
61
62 if len(last_read_ids) != len(timelines):
63 raise MastodonIllegalArgumentError("Number of specified timelines and ids must be the same")
64
65 params = collections.OrderedDict()
66 for timeline, last_read_id in zip(timelines, last_read_ids):
67 params[timeline] = collections.OrderedDict()
68 params[timeline]["last_read_id"] = self.__unpack_id(last_read_id)
69
70 return self.__api_request('POST', '/api/v1/markers', params, use_json=True)
71 \ No newline at end of file
diff --git a/mastodon/push.py b/mastodon/push.py
new file mode 100644
index 0000000..def348a
--- /dev/null
+++ b/mastodon/push.py
@@ -0,0 +1,201 @@
1# push.py - webpush endpoints and tooling
2
3import base64
4import os
5import json
6
7from .versions import _DICT_VERSION_PUSH, _DICT_VERSION_PUSH_NOTIF
8from .errors import MastodonIllegalArgumentError
9from .utility import api_version
10from .compat import IMPL_HAS_CRYPTO, ec, serialization, default_backend
11from .compat import IMPL_HAS_ECE, http_ece
12
13from .internals import Mastodon as Internals
14
15class Mastodon(Internals):
16 ###
17 # Reading data: Webpush subscriptions
18 ###
19 @api_version("2.4.0", "2.4.0", _DICT_VERSION_PUSH)
20 def push_subscription(self):
21 """
22 Fetch the current push subscription the logged-in user has for this app.
23
24 Returns a :ref:`push subscription dict <push subscription dict>`.
25 """
26 return self.__api_request('GET', '/api/v1/push/subscription')
27
28 ###
29 # Writing data: Push subscriptions
30 ###
31 @api_version("2.4.0", "2.4.0", _DICT_VERSION_PUSH)
32 def push_subscription_set(self, endpoint, encrypt_params, follow_events=None,
33 favourite_events=None, reblog_events=None,
34 mention_events=None, poll_events=None,
35 follow_request_events=None, status_events=None, policy='all'):
36 """
37 Sets up or modifies the push subscription the logged-in user has for this app.
38
39 `endpoint` is the endpoint URL mastodon should call for pushes. Note that mastodon
40 requires https for this URL. `encrypt_params` is a dict with key parameters that allow
41 the server to encrypt data for you: A public key `pubkey` and a shared secret `auth`.
42 You can generate this as well as the corresponding private key using the
43 :ref:`push_subscription_generate_keys() <push_subscription_generate_keys()>` function.
44
45 `policy` controls what sources will generate webpush events. Valid values are
46 `all`, `none`, `follower` and `followed`.
47
48 The rest of the parameters controls what kind of events you wish to subscribe to.
49
50 Returns a :ref:`push subscription dict <push subscription dict>`.
51 """
52 if not policy in ['all', 'none', 'follower', 'followed']:
53 raise MastodonIllegalArgumentError("Valid values for policy are 'all', 'none', 'follower' or 'followed'.")
54
55 endpoint = Mastodon.__protocolize(endpoint)
56
57 push_pubkey_b64 = base64.b64encode(encrypt_params['pubkey'])
58 push_auth_b64 = base64.b64encode(encrypt_params['auth'])
59
60 params = {
61 'subscription[endpoint]': endpoint,
62 'subscription[keys][p256dh]': push_pubkey_b64,
63 'subscription[keys][auth]': push_auth_b64,
64 'policy': policy
65 }
66
67 if follow_events is not None:
68 params['data[alerts][follow]'] = follow_events
69
70 if favourite_events is not None:
71 params['data[alerts][favourite]'] = favourite_events
72
73 if reblog_events is not None:
74 params['data[alerts][reblog]'] = reblog_events
75
76 if mention_events is not None:
77 params['data[alerts][mention]'] = mention_events
78
79 if poll_events is not None:
80 params['data[alerts][poll]'] = poll_events
81
82 if follow_request_events is not None:
83 params['data[alerts][follow_request]'] = follow_request_events
84
85 if follow_request_events is not None:
86 params['data[alerts][status]'] = status_events
87
88 # Canonicalize booleans
89 params = self.__generate_params(params)
90
91 return self.__api_request('POST', '/api/v1/push/subscription', params)
92
93 @api_version("2.4.0", "2.4.0", _DICT_VERSION_PUSH)
94 def push_subscription_update(self, follow_events=None,
95 favourite_events=None, reblog_events=None,
96 mention_events=None, poll_events=None,
97 follow_request_events=None):
98 """
99 Modifies what kind of events the app wishes to subscribe to.
100
101 Returns the updated :ref:`push subscription dict <push subscription dict>`.
102 """
103 params = {}
104
105 if follow_events is not None:
106 params['data[alerts][follow]'] = follow_events
107
108 if favourite_events is not None:
109 params['data[alerts][favourite]'] = favourite_events
110
111 if reblog_events is not None:
112 params['data[alerts][reblog]'] = reblog_events
113
114 if mention_events is not None:
115 params['data[alerts][mention]'] = mention_events
116
117 if poll_events is not None:
118 params['data[alerts][poll]'] = poll_events
119
120 if follow_request_events is not None:
121 params['data[alerts][follow_request]'] = follow_request_events
122
123 # Canonicalize booleans
124 params = self.__generate_params(params)
125
126 return self.__api_request('PUT', '/api/v1/push/subscription', params)
127
128 @api_version("2.4.0", "2.4.0", "2.4.0")
129 def push_subscription_delete(self):
130 """
131 Remove the current push subscription the logged-in user has for this app.
132 """
133 self.__api_request('DELETE', '/api/v1/push/subscription')
134
135 ###
136 # Push subscription crypto utilities
137 ###
138 def push_subscription_generate_keys(self):
139 """
140 Generates a private key, public key and shared secret for use in webpush subscriptions.
141
142 Returns two dicts: One with the private key and shared secret and another with the
143 public key and shared secret.
144 """
145 if not IMPL_HAS_CRYPTO:
146 raise NotImplementedError(
147 'To use the crypto tools, please install the webpush feature dependencies.')
148
149 push_key_pair = ec.generate_private_key(ec.SECP256R1(), default_backend())
150 push_key_priv = push_key_pair.private_numbers().private_value
151 try:
152 push_key_pub = push_key_pair.public_key().public_bytes(
153 serialization.Encoding.X962,
154 serialization.PublicFormat.UncompressedPoint,
155 )
156 except:
157 push_key_pub = push_key_pair.public_key().public_numbers().encode_point()
158
159 push_shared_secret = os.urandom(16)
160
161 priv_dict = {
162 'privkey': push_key_priv,
163 'auth': push_shared_secret
164 }
165
166 pub_dict = {
167 'pubkey': push_key_pub,
168 'auth': push_shared_secret
169 }
170
171 return priv_dict, pub_dict
172
173 @api_version("2.4.0", "2.4.0", _DICT_VERSION_PUSH_NOTIF)
174 def push_subscription_decrypt_push(self, data, decrypt_params, encryption_header, crypto_key_header):
175 """
176 Decrypts `data` received in a webpush request. Requires the private key dict
177 from :ref:`push_subscription_generate_keys() <push_subscription_generate_keys()>` (`decrypt_params`) as well as the
178 Encryption and server Crypto-Key headers from the received webpush
179
180 Returns the decoded webpush as a :ref:`push notification dict <push notification dict>`.
181 """
182 if (not IMPL_HAS_ECE) or (not IMPL_HAS_CRYPTO):
183 raise NotImplementedError(
184 'To use the crypto tools, please install the webpush feature dependencies.')
185
186 salt = self.__decode_webpush_b64(encryption_header.split("salt=")[1].strip())
187 dhparams = self.__decode_webpush_b64(crypto_key_header.split("dh=")[1].split(";")[0].strip())
188 p256ecdsa = self.__decode_webpush_b64(crypto_key_header.split("p256ecdsa=")[1].strip())
189 dec_key = ec.derive_private_key(decrypt_params['privkey'], ec.SECP256R1(), default_backend())
190 decrypted = http_ece.decrypt(
191 data,
192 salt=salt,
193 key=p256ecdsa,
194 private_key=dec_key,
195 dh=dhparams,
196 auth_secret=decrypt_params['auth'],
197 keylabel="P-256",
198 version="aesgcm"
199 )
200
201 return json.loads(decrypted.decode('utf-8'), object_hook=Mastodon.__json_hooks)
diff --git a/mastodon/relationships.py b/mastodon/relationships.py
new file mode 100644
index 0000000..9cf15db
--- /dev/null
+++ b/mastodon/relationships.py
@@ -0,0 +1,136 @@
1# relationships.py - endpoints for user and domain blocks and mutes as well as follow requests
2
3from .versions import _DICT_VERSION_ACCOUNT, _DICT_VERSION_RELATIONSHIP
4from .utility import api_version
5
6from .internals import Mastodon as Internals
7
8class Mastodon(Internals):
9 ###
10 # Reading data: Mutes and Blocks
11 ###
12 @api_version("1.1.0", "2.6.0", _DICT_VERSION_ACCOUNT)
13 def mutes(self, max_id=None, min_id=None, since_id=None, limit=None):
14 """
15 Fetch a list of users muted by the logged-in user.
16
17 Returns a list of :ref:`account dicts <account dicts>`.
18 """
19 if max_id is not None:
20 max_id = self.__unpack_id(max_id, dateconv=True)
21
22 if min_id is not None:
23 min_id = self.__unpack_id(min_id, dateconv=True)
24
25 if since_id is not None:
26 since_id = self.__unpack_id(since_id, dateconv=True)
27
28 params = self.__generate_params(locals())
29 return self.__api_request('GET', '/api/v1/mutes', params)
30
31 @api_version("1.0.0", "2.6.0", _DICT_VERSION_ACCOUNT)
32 def blocks(self, max_id=None, min_id=None, since_id=None, limit=None):
33 """
34 Fetch a list of users blocked by the logged-in user.
35
36 Returns a list of :ref:`account dicts <account dicts>`.
37 """
38 if max_id is not None:
39 max_id = self.__unpack_id(max_id, dateconv=True)
40
41 if min_id is not None:
42 min_id = self.__unpack_id(min_id, dateconv=True)
43
44 if since_id is not None:
45 since_id = self.__unpack_id(since_id, dateconv=True)
46
47 params = self.__generate_params(locals())
48 return self.__api_request('GET', '/api/v1/blocks', params)
49
50 ###
51 # Reading data: Follow requests
52 ###
53 @api_version("1.0.0", "2.6.0", _DICT_VERSION_ACCOUNT)
54 def follow_requests(self, max_id=None, min_id=None, since_id=None, limit=None):
55 """
56 Fetch the logged-in user's incoming follow requests.
57
58 Returns a list of :ref:`account dicts <account dicts>`.
59 """
60 if max_id is not None:
61 max_id = self.__unpack_id(max_id, dateconv=True)
62
63 if min_id is not None:
64 min_id = self.__unpack_id(min_id, dateconv=True)
65
66 if since_id is not None:
67 since_id = self.__unpack_id(since_id, dateconv=True)
68
69 params = self.__generate_params(locals())
70 return self.__api_request('GET', '/api/v1/follow_requests', params)
71
72 ###
73 # Reading data: Domain blocks
74 ###
75 @api_version("1.4.0", "2.6.0", "1.4.0")
76 def domain_blocks(self, max_id=None, min_id=None, since_id=None, limit=None):
77 """
78 Fetch the logged-in user's blocked domains.
79
80 Returns a list of blocked domain URLs (as strings, without protocol specifier).
81 """
82 if max_id is not None:
83 max_id = self.__unpack_id(max_id, dateconv=True)
84
85 if min_id is not None:
86 min_id = self.__unpack_id(min_id, dateconv=True)
87
88 if since_id is not None:
89 since_id = self.__unpack_id(since_id, dateconv=True)
90
91 params = self.__generate_params(locals())
92 return self.__api_request('GET', '/api/v1/domain_blocks', params)
93
94 ###
95 # Writing data: Follow requests
96 ###
97 @api_version("1.0.0", "3.0.0", _DICT_VERSION_RELATIONSHIP)
98 def follow_request_authorize(self, id):
99 """
100 Accept an incoming follow request.
101
102 Returns the updated :ref:`relationship dict <relationship dict>` for the requesting account.
103 """
104 id = self.__unpack_id(id)
105 url = '/api/v1/follow_requests/{0}/authorize'.format(str(id))
106 return self.__api_request('POST', url)
107
108 @api_version("1.0.0", "3.0.0", _DICT_VERSION_RELATIONSHIP)
109 def follow_request_reject(self, id):
110 """
111 Reject an incoming follow request.
112
113 Returns the updated :ref:`relationship dict <relationship dict>` for the requesting account.
114 """
115 id = self.__unpack_id(id)
116 url = '/api/v1/follow_requests/{0}/reject'.format(str(id))
117 return self.__api_request('POST', url)
118
119 ###
120 # Writing data: Domain blocks
121 ###
122 @api_version("1.4.0", "1.4.0", "1.4.0")
123 def domain_block(self, domain=None):
124 """
125 Add a block for all statuses originating from the specified domain for the logged-in user.
126 """
127 params = self.__generate_params(locals())
128 self.__api_request('POST', '/api/v1/domain_blocks', params)
129
130 @api_version("1.4.0", "1.4.0", "1.4.0")
131 def domain_unblock(self, domain=None):
132 """
133 Remove a domain block for the logged-in user.
134 """
135 params = self.__generate_params(locals())
136 self.__api_request('DELETE', '/api/v1/domain_blocks', params) \ No newline at end of file
diff --git a/mastodon/reports.py b/mastodon/reports.py
new file mode 100644
index 0000000..3d6380e
--- /dev/null
+++ b/mastodon/reports.py
@@ -0,0 +1,62 @@
1
2# reports.py - report endpoints
3
4from .versions import _DICT_VERSION_REPORT
5from .errors import MastodonVersionError
6from .utility import api_version
7
8from .internals import Mastodon as Internals
9
10class Mastodon(Internals):
11 ###
12 # Reading data: Reports
13 ###
14 @api_version("1.1.0", "1.1.0", _DICT_VERSION_REPORT)
15 def reports(self):
16 """
17 Fetch a list of reports made by the logged-in user.
18
19 Returns a list of :ref:`report dicts <report dicts>`.
20
21 Warning: This method has now finally been removed, and will not
22 work on Mastodon versions 2.5.0 and above.
23 """
24 if self.verify_minimum_version("2.5.0", cached=True):
25 raise MastodonVersionError("API removed in Mastodon 2.5.0")
26 return self.__api_request('GET', '/api/v1/reports')
27
28 ###
29 # Writing data: Reports
30 ###
31 @api_version("1.1.0", "3.5.0", _DICT_VERSION_REPORT)
32 def report(self, account_id, status_ids=None, comment=None, forward=False, category=None, rule_ids=None):
33 """
34 Report statuses to the instances administrators.
35
36 Accepts a list of toot IDs associated with the report, and a comment.
37
38 Starting with Mastodon 3.5.0, you can also pass a `category` (one out of
39 "spam", "violation" or "other") and `rule_ids` (a list of rule IDs corresponding
40 to the rules returned by the :ref:`instance() <instance()>` API).
41
42 Set `forward` to True to forward a report of a remote user to that users
43 instance as well as sending it to the instance local administrators.
44
45 Returns a :ref:`report dict <report dict>`.
46 """
47 if category is not None and not category in ["spam", "violation", "other"]:
48 raise MastodonIllegalArgumentError("Invalid report category (must be spam, violation or other)")
49
50 account_id = self.__unpack_id(account_id)
51
52 if status_ids is not None:
53 if not isinstance(status_ids, list):
54 status_ids = [status_ids]
55 status_ids = list(map(lambda x: self.__unpack_id(x), status_ids))
56
57 params_initial = locals()
58 if not forward:
59 del params_initial['forward']
60
61 params = self.__generate_params(params_initial)
62 return self.__api_request('POST', '/api/v1/reports/', params) \ No newline at end of file
diff --git a/mastodon/search.py b/mastodon/search.py
new file mode 100644
index 0000000..b77002f
--- /dev/null
+++ b/mastodon/search.py
@@ -0,0 +1,91 @@
1# search.py - search endpoints
2
3from .versions import _DICT_VERSION_SEARCHRESULT
4from .errors import MastodonVersionError
5from .utility import api_version
6
7from .internals import Mastodon as Internals
8
9class Mastodon(Internals):
10 ###
11 # Reading data: Searching
12 ###
13 def __ensure_search_params_acceptable(self, account_id, offset, min_id, max_id):
14 """
15 Internal Helper: Throw a MastodonVersionError if version is < 2.8.0 but parameters
16 for search that are available only starting with 2.8.0 are specified.
17 """
18 if any(item is not None for item in (account_id, offset, min_id, max_id)):
19 if not self.verify_minimum_version("2.8.0", cached=True):
20 raise MastodonVersionError("Advanced search parameters require Mastodon 2.8.0+")
21
22 @api_version("1.1.0", "2.8.0", _DICT_VERSION_SEARCHRESULT)
23 def search(self, q, resolve=True, result_type=None, account_id=None, offset=None, min_id=None, max_id=None, exclude_unreviewed=True):
24 """
25 Fetch matching hashtags, accounts and statuses. Will perform webfinger
26 lookups if resolve is True. Full-text search is only enabled if
27 the instance supports it, and is restricted to statuses the logged-in
28 user wrote or was mentioned in.
29
30 `result_type` can be one of "accounts", "hashtags" or "statuses", to only
31 search for that type of object.
32
33 Specify `account_id` to only get results from the account with that id.
34
35 `offset`, `min_id` and `max_id` can be used to paginate.
36
37 `exclude_unreviewed` can be used to restrict search results for hashtags to only
38 those that have been reviewed by moderators. It is on by default. When using the
39 v1 search API (pre 2.4.1), it is ignored.
40
41 Will use search_v1 (no tag dicts in return values) on Mastodon versions before
42 2.4.1), search_v2 otherwise. Parameters other than resolve are only available
43 on Mastodon 2.8.0 or above - this function will throw a MastodonVersionError
44 if you try to use them on versions before that. Note that the cached version
45 number will be used for this to avoid uneccesary requests.
46
47 Returns a :ref:`search result dict <search result dict>`, with tags as `hashtag dicts`_.
48 """
49 if self.verify_minimum_version("2.4.1", cached=True):
50 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)
51 else:
52 self.__ensure_search_params_acceptable(account_id, offset, min_id, max_id)
53 return self.search_v1(q, resolve=resolve)
54
55 @api_version("1.1.0", "2.1.0", "2.1.0")
56 def search_v1(self, q, resolve=False):
57 """
58 Identical to `search_v2()`, except in that it does not return
59 tags as :ref:`hashtag dicts <hashtag dicts>`.
60
61 Returns a :ref:`search result dict <search result dict>`.
62 """
63 params = self.__generate_params(locals())
64 if not resolve:
65 del params['resolve']
66 return self.__api_request('GET', '/api/v1/search', params)
67
68 @api_version("2.4.1", "2.8.0", _DICT_VERSION_SEARCHRESULT)
69 def search_v2(self, q, resolve=True, result_type=None, account_id=None, offset=None, min_id=None, max_id=None, exclude_unreviewed=True):
70 """
71 Identical to `search_v1()`, except in that it returns tags as
72 :ref:`hashtag dicts <hashtag dicts>`, has more parameters, and resolves by default.
73
74 For more details documentation, please see `search()`
75
76 Returns a :ref:`search result dict <search result dict>`.
77 """
78 self.__ensure_search_params_acceptable(account_id, offset, min_id, max_id)
79 params = self.__generate_params(locals())
80
81 if not resolve:
82 del params["resolve"]
83
84 if not exclude_unreviewed or not self.verify_minimum_version("3.0.0", cached=True):
85 del params["exclude_unreviewed"]
86
87 if "result_type" in params:
88 params["type"] = params["result_type"]
89 del params["result_type"]
90
91 return self.__api_request('GET', '/api/v2/search', params)
diff --git a/mastodon/statuses.py b/mastodon/statuses.py
index ae891d5..36a7d2b 100644
--- a/mastodon/statuses.py
+++ b/mastodon/statuses.py
@@ -1,10 +1,14 @@
1 1# statuses.py - status endpoints (regular and scheduled)
2from .versions import _DICT_VERSION_STATUS, _DICT_VERSION_CARD, _DICT_VERSION_CONTEXT, _DICT_VERSION_ACCOUNT, _DICT_VERSION_SCHEDULED_STATUS 2
3import collections
4
5from .versions import _DICT_VERSION_STATUS, _DICT_VERSION_CARD, _DICT_VERSION_CONTEXT, _DICT_VERSION_ACCOUNT, _DICT_VERSION_SCHEDULED_STATUS, \
6 _DICT_VERSION_STATUS_EDIT
7from .errors import MastodonIllegalArgumentError
3from .utility import api_version 8from .utility import api_version
4 9
5from .internals import Mastodon as Internals 10from .internals import Mastodon as Internals
6 11
7
8class Mastodon(Internals): 12class Mastodon(Internals):
9 ### 13 ###
10 # Reading data: Statuses 14 # Reading data: Statuses
@@ -105,4 +109,416 @@ class Mastodon(Internals):
105 id = self.__unpack_id(id) 109 id = self.__unpack_id(id)
106 url = '/api/v1/scheduled_statuses/{0}'.format(str(id)) 110 url = '/api/v1/scheduled_statuses/{0}'.format(str(id))
107 return self.__api_request('GET', url) 111 return self.__api_request('GET', url)
108 \ No newline at end of file 112
113 ###
114 # Writing data: Statuses
115 ###
116 def __status_internal(self, status, in_reply_to_id=None, media_ids=None,
117 sensitive=False, visibility=None, spoiler_text=None,
118 language=None, idempotency_key=None, content_type=None,
119 scheduled_at=None, poll=None, quote_id=None, edit=False):
120 if quote_id is not None:
121 if self.feature_set != "fedibird":
122 raise MastodonIllegalArgumentError('quote_id is only available with feature set fedibird')
123 quote_id = self.__unpack_id(quote_id)
124
125 if content_type is not None:
126 if self.feature_set != "pleroma":
127 raise MastodonIllegalArgumentError('content_type is only available with feature set pleroma')
128 # It would be better to read this from nodeinfo and cache, but this is easier
129 if not content_type in ["text/plain", "text/html", "text/markdown", "text/bbcode"]:
130 raise MastodonIllegalArgumentError('Invalid content type specified')
131
132 if in_reply_to_id is not None:
133 in_reply_to_id = self.__unpack_id(in_reply_to_id)
134
135 if scheduled_at is not None:
136 scheduled_at = self.__consistent_isoformat_utc(scheduled_at)
137
138 params_initial = locals()
139
140 # Validate poll/media exclusivity
141 if poll is not None:
142 if media_ids is not None and len(media_ids) != 0:
143 raise ValueError(
144 'Status can have media or poll attached - not both.')
145
146 # Validate visibility parameter
147 valid_visibilities = ['private', 'public', 'unlisted', 'direct']
148 if params_initial['visibility'] is None:
149 del params_initial['visibility']
150 else:
151 params_initial['visibility'] = params_initial['visibility'].lower()
152 if params_initial['visibility'] not in valid_visibilities:
153 raise ValueError('Invalid visibility value! Acceptable values are %s' % valid_visibilities)
154
155 if params_initial['language'] is None:
156 del params_initial['language']
157
158 if params_initial['sensitive'] is False:
159 del [params_initial['sensitive']]
160
161 headers = {}
162 if idempotency_key is not None:
163 headers['Idempotency-Key'] = idempotency_key
164
165 if media_ids is not None:
166 try:
167 media_ids_proper = []
168 if not isinstance(media_ids, (list, tuple)):
169 media_ids = [media_ids]
170 for media_id in media_ids:
171 media_ids_proper.append(self.__unpack_id(media_id))
172 except Exception as e:
173 raise MastodonIllegalArgumentError("Invalid media dict: %s" % e)
174
175 params_initial["media_ids"] = media_ids_proper
176
177 if params_initial['content_type'] is None:
178 del params_initial['content_type']
179
180 use_json = False
181 if poll is not None:
182 use_json = True
183
184 params = self.__generate_params(params_initial, ['idempotency_key', 'edit'])
185 if edit is None:
186 # Post
187 return self.__api_request('POST', '/api/v1/statuses', params, headers=headers, use_json=use_json)
188 else:
189 # Edit
190 return self.__api_request('PUT', '/api/v1/statuses/{0}'.format(str(self.__unpack_id(edit))), params, headers=headers, use_json=use_json)
191
192 @api_version("1.0.0", "2.8.0", _DICT_VERSION_STATUS)
193 def status_post(self, status, in_reply_to_id=None, media_ids=None,
194 sensitive=False, visibility=None, spoiler_text=None,
195 language=None, idempotency_key=None, content_type=None,
196 scheduled_at=None, poll=None, quote_id=None):
197 """
198 Post a status. Can optionally be in reply to another status and contain
199 media.
200
201 `media_ids` should be a list. (If it's not, the function will turn it
202 into one.) It can contain up to four pieces of media (uploaded via
203 :ref:`media_post() <media_post()>`). `media_ids` can also be the `media dicts`_ returned
204 by :ref:`media_post() <media_post()>` - they are unpacked automatically.
205
206 The `sensitive` boolean decides whether or not media attached to the post
207 should be marked as sensitive, which hides it by default on the Mastodon
208 web front-end.
209
210 The visibility parameter is a string value and accepts any of:
211 'direct' - post will be visible only to mentioned users
212 'private' - post will be visible only to followers
213 'unlisted' - post will be public but not appear on the public timeline
214 'public' - post will be public
215
216 If not passed in, visibility defaults to match the current account's
217 default-privacy setting (starting with Mastodon version 1.6) or its
218 locked setting - private if the account is locked, public otherwise
219 (for Mastodon versions lower than 1.6).
220
221 The `spoiler_text` parameter is a string to be shown as a warning before
222 the text of the status. If no text is passed in, no warning will be
223 displayed.
224
225 Specify `language` to override automatic language detection. The parameter
226 accepts all valid ISO 639-1 (2-letter) or for languages where that do not
227 have one, 639-3 (three letter) language codes.
228
229 You can set `idempotency_key` to a value to uniquely identify an attempt
230 at posting a status. Even if you call this function more than once,
231 if you call it with the same `idempotency_key`, only one status will
232 be created.
233
234 Pass a datetime as `scheduled_at` to schedule the toot for a specific time
235 (the time must be at least 5 minutes into the future). If this is passed,
236 status_post returns a :ref:`scheduled status dict <scheduled status dict>` instead.
237
238 Pass `poll` to attach a poll to the status. An appropriate object can be
239 constructed using :ref:`make_poll() <make_poll()>` . Note that as of Mastodon version
240 2.8.2, you can only have either media or a poll attached, not both at
241 the same time.
242
243 **Specific to "pleroma" feature set:**: Specify `content_type` to set
244 the content type of your post on Pleroma. It accepts 'text/plain' (default),
245 'text/markdown', 'text/html' and 'text/bbcode'. This parameter is not
246 supported on Mastodon servers, but will be safely ignored if set.
247
248 **Specific to "fedibird" feature set:**: The `quote_id` parameter is
249 a non-standard extension that specifies the id of a quoted status.
250
251 Returns a :ref:`status dict <status dict>` with the new status.
252 """
253 return self.__status_internal(
254 status,
255 in_reply_to_id,
256 media_ids,
257 sensitive,
258 visibility,
259 spoiler_text,
260 language,
261 idempotency_key,
262 content_type,
263 scheduled_at,
264 poll,
265 quote_id,
266 edit=None
267 )
268
269 @api_version("1.0.0", "2.8.0", _DICT_VERSION_STATUS)
270 def toot(self, status):
271 """
272 Synonym for :ref:`status_post() <status_post()>` that only takes the status text as input.
273
274 Usage in production code is not recommended.
275
276 Returns a :ref:`status dict <status dict>` with the new status.
277 """
278 return self.status_post(status)
279
280 @api_version("3.5.0", "3.5.0", _DICT_VERSION_STATUS)
281 def status_update(self, id, status = None, spoiler_text = None, sensitive = None, media_ids = None, poll = None):
282 """
283 Edit a status. The meanings of the fields are largely the same as in :ref:`status_post() <status_post()>`,
284 though not every field can be edited.
285
286 Note that editing a poll will reset the votes.
287 """
288 return self.__status_internal(
289 status = status,
290 media_ids = media_ids,
291 sensitive = sensitive,
292 spoiler_text = spoiler_text,
293 poll = poll,
294 edit = id
295 )
296
297 @api_version("3.5.0", "3.5.0", _DICT_VERSION_STATUS_EDIT)
298 def status_history(self, id):
299 """
300 Returns the edit history of a status as a list of :ref:`status edit dicts <status edit dicts>`, starting
301 from the original form. Note that this means that a status that has been edited
302 once will have *two* entries in this list, a status that has been edited twice
303 will have three, and so on.
304 """
305 id = self.__unpack_id(id)
306 return self.__api_request('GET', "/api/v1/statuses/{0}/history".format(str(id)))
307
308 def status_source(self, id):
309 """
310 Returns the source of a status for editing.
311
312 Return value is a dictionary containing exactly the parameters you could pass to
313 :ref:`status_update() <status_update()>` to change nothing about the status, except `status` is `text`
314 instead.
315 """
316 id = self.__unpack_id(id)
317 return self.__api_request('GET', "/api/v1/statuses/{0}/source".format(str(id)))
318
319 @api_version("1.0.0", "2.8.0", _DICT_VERSION_STATUS)
320 def status_reply(self, to_status, status, in_reply_to_id=None, media_ids=None,
321 sensitive=False, visibility=None, spoiler_text=None,
322 language=None, idempotency_key=None, content_type=None,
323 scheduled_at=None, poll=None, untag=False):
324 """
325 Helper function - acts like status_post, but prepends the name of all
326 the users that are being replied to to the status text and retains
327 CW and visibility if not explicitly overridden.
328
329 Set `untag` to True if you want the reply to only go to the user you
330 are replying to, removing every other mentioned user from the
331 conversation.
332 """
333 keyword_args = locals()
334 del keyword_args["self"]
335 del keyword_args["to_status"]
336 del keyword_args["untag"]
337
338 user_id = self.__get_logged_in_id()
339
340 # Determine users to mention
341 mentioned_accounts = collections.OrderedDict()
342 mentioned_accounts[to_status.account.id] = to_status.account.acct
343
344 if not untag:
345 for account in to_status.mentions:
346 if account.id != user_id and not account.id in mentioned_accounts.keys():
347 mentioned_accounts[account.id] = account.acct
348
349 # Join into one piece of text. The space is added inside because of self-replies.
350 status = "".join(map(lambda x: "@" + x + " ",
351 mentioned_accounts.values())) + status
352
353 # Retain visibility / cw
354 if visibility is None and 'visibility' in to_status:
355 visibility = to_status.visibility
356 if spoiler_text is None and 'spoiler_text' in to_status:
357 spoiler_text = to_status.spoiler_text
358
359 keyword_args["status"] = status
360 keyword_args["visibility"] = visibility
361 keyword_args["spoiler_text"] = spoiler_text
362 keyword_args["in_reply_to_id"] = to_status.id
363 return self.status_post(**keyword_args)
364
365 @api_version("1.0.0", "1.0.0", "1.0.0")
366 def status_delete(self, id):
367 """
368 Delete a status
369
370 Returns the now-deleted status, with an added "source" attribute that contains
371 the text that was used to compose this status (this can be used to power
372 "delete and redraft" functionality)
373 """
374 id = self.__unpack_id(id)
375 url = '/api/v1/statuses/{0}'.format(str(id))
376 return self.__api_request('DELETE', url)
377
378 @api_version("1.0.0", "2.0.0", _DICT_VERSION_STATUS)
379 def status_reblog(self, id, visibility=None):
380 """
381 Reblog / boost a status.
382
383 The visibility parameter functions the same as in :ref:`status_post() <status_post()>` and
384 allows you to reduce the visibility of a reblogged status.
385
386 Returns a :ref:`status dict <status dict>` with a new status that wraps around the reblogged one.
387 """
388 params = self.__generate_params(locals(), ['id'])
389 valid_visibilities = ['private', 'public', 'unlisted', 'direct']
390 if 'visibility' in params:
391 params['visibility'] = params['visibility'].lower()
392 if params['visibility'] not in valid_visibilities:
393 raise ValueError('Invalid visibility value! Acceptable '
394 'values are %s' % valid_visibilities)
395
396 id = self.__unpack_id(id)
397 url = '/api/v1/statuses/{0}/reblog'.format(str(id))
398 return self.__api_request('POST', url, params)
399
400 @api_version("1.0.0", "2.0.0", _DICT_VERSION_STATUS)
401 def status_unreblog(self, id):
402 """
403 Un-reblog a status.
404
405 Returns a :ref:`status dict <status dict>` with the status that used to be reblogged.
406 """
407 id = self.__unpack_id(id)
408 url = '/api/v1/statuses/{0}/unreblog'.format(str(id))
409 return self.__api_request('POST', url)
410
411 @api_version("1.0.0", "2.0.0", _DICT_VERSION_STATUS)
412 def status_favourite(self, id):
413 """
414 Favourite a status.
415
416 Returns a :ref:`status dict <status dict>` with the favourited status.
417 """
418 id = self.__unpack_id(id)
419 url = '/api/v1/statuses/{0}/favourite'.format(str(id))
420 return self.__api_request('POST', url)
421
422 @api_version("1.0.0", "2.0.0", _DICT_VERSION_STATUS)
423 def status_unfavourite(self, id):
424 """
425 Un-favourite a status.
426
427 Returns a :ref:`status dict <status dict>` with the un-favourited status.
428 """
429 id = self.__unpack_id(id)
430 url = '/api/v1/statuses/{0}/unfavourite'.format(str(id))
431 return self.__api_request('POST', url)
432
433 @api_version("1.4.0", "2.0.0", _DICT_VERSION_STATUS)
434 def status_mute(self, id):
435 """
436 Mute notifications for a status.
437
438 Returns a :ref:`status dict <status dict>` with the now muted status
439 """
440 id = self.__unpack_id(id)
441 url = '/api/v1/statuses/{0}/mute'.format(str(id))
442 return self.__api_request('POST', url)
443
444 @api_version("1.4.0", "2.0.0", _DICT_VERSION_STATUS)
445 def status_unmute(self, id):
446 """
447 Unmute notifications for a status.
448
449 Returns a :ref:`status dict <status dict>` with the status that used to be muted.
450 """
451 id = self.__unpack_id(id)
452 url = '/api/v1/statuses/{0}/unmute'.format(str(id))
453 return self.__api_request('POST', url)
454
455 @api_version("2.1.0", "2.1.0", _DICT_VERSION_STATUS)
456 def status_pin(self, id):
457 """
458 Pin a status for the logged-in user.
459
460 Returns a :ref:`status dict <status dict>` with the now pinned status
461 """
462 id = self.__unpack_id(id)
463 url = '/api/v1/statuses/{0}/pin'.format(str(id))
464 return self.__api_request('POST', url)
465
466 @api_version("2.1.0", "2.1.0", _DICT_VERSION_STATUS)
467 def status_unpin(self, id):
468 """
469 Unpin a pinned status for the logged-in user.
470
471 Returns a :ref:`status dict <status dict>` with the status that used to be pinned.
472 """
473 id = self.__unpack_id(id)
474 url = '/api/v1/statuses/{0}/unpin'.format(str(id))
475 return self.__api_request('POST', url)
476
477 @api_version("3.1.0", "3.1.0", _DICT_VERSION_STATUS)
478 def status_bookmark(self, id):
479 """
480 Bookmark a status as the logged-in user.
481
482 Returns a :ref:`status dict <status dict>` with the now bookmarked status
483 """
484 id = self.__unpack_id(id)
485 url = '/api/v1/statuses/{0}/bookmark'.format(str(id))
486 return self.__api_request('POST', url)
487
488 @api_version("3.1.0", "3.1.0", _DICT_VERSION_STATUS)
489 def status_unbookmark(self, id):
490 """
491 Unbookmark a bookmarked status for the logged-in user.
492
493 Returns a :ref:`status dict <status dict>` with the status that used to be bookmarked.
494 """
495 id = self.__unpack_id(id)
496 url = '/api/v1/statuses/{0}/unbookmark'.format(str(id))
497 return self.__api_request('POST', url)
498
499 ###
500 # Writing data: Scheduled statuses
501 ###
502 @api_version("2.7.0", "2.7.0", _DICT_VERSION_SCHEDULED_STATUS)
503 def scheduled_status_update(self, id, scheduled_at):
504 """
505 Update the scheduled time of a scheduled status.
506
507 New time must be at least 5 minutes into the future.
508
509 Returns a :ref:`scheduled status dict <scheduled status dict>`
510 """
511 scheduled_at = self.__consistent_isoformat_utc(scheduled_at)
512 id = self.__unpack_id(id)
513 params = self.__generate_params(locals(), ['id'])
514 url = '/api/v1/scheduled_statuses/{0}'.format(str(id))
515 return self.__api_request('PUT', url, params)
516
517 @api_version("2.7.0", "2.7.0", "2.7.0")
518 def scheduled_status_delete(self, id):
519 """
520 Deletes a scheduled status.
521 """
522 id = self.__unpack_id(id)
523 url = '/api/v1/scheduled_statuses/{0}'.format(str(id))
524 self.__api_request('DELETE', url)
diff --git a/mastodon/streaming_endpoints.py b/mastodon/streaming_endpoints.py
new file mode 100644
index 0000000..9ff72f5
--- /dev/null
+++ b/mastodon/streaming_endpoints.py
@@ -0,0 +1,75 @@
1 # relationships.py - endpoints for user and domain blocks and mutes as well as follow requests
2
3from .versions import _DICT_VERSION_STATUS
4from .errors import MastodonIllegalArgumentError
5from .defaults import _DEFAULT_STREAM_TIMEOUT, _DEFAULT_STREAM_RECONNECT_WAIT_SEC
6from .utility import api_version
7
8from .internals import Mastodon as Internals
9
10class Mastodon(Internals):
11 ###
12 # Streaming
13 ###
14 @api_version("1.1.0", "1.4.2", _DICT_VERSION_STATUS)
15 def stream_user(self, listener, run_async=False, timeout=_DEFAULT_STREAM_TIMEOUT, reconnect_async=False, reconnect_async_wait_sec=_DEFAULT_STREAM_RECONNECT_WAIT_SEC):
16 """
17 Streams events that are relevant to the authorized user, i.e. home
18 timeline and notifications.
19 """
20 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)
21
22 @api_version("1.1.0", "1.4.2", _DICT_VERSION_STATUS)
23 def stream_public(self, listener, run_async=False, timeout=_DEFAULT_STREAM_TIMEOUT, reconnect_async=False, reconnect_async_wait_sec=_DEFAULT_STREAM_RECONNECT_WAIT_SEC):
24 """
25 Streams public events.
26 """
27 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)
28
29 @api_version("1.1.0", "1.4.2", _DICT_VERSION_STATUS)
30 def stream_local(self, listener, run_async=False, timeout=_DEFAULT_STREAM_TIMEOUT, reconnect_async=False, reconnect_async_wait_sec=_DEFAULT_STREAM_RECONNECT_WAIT_SEC):
31 """
32 Streams local public events.
33 """
34 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)
35
36 @api_version("1.1.0", "1.4.2", _DICT_VERSION_STATUS)
37 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):
38 """
39 Stream for all public statuses for the hashtag 'tag' seen by the connected
40 instance.
41
42 Set local to True to only get local statuses.
43 """
44 if tag.startswith("#"):
45 raise MastodonIllegalArgumentError("Tag parameter should omit leading #")
46 base = '/api/v1/streaming/hashtag'
47 if local:
48 base += '/local'
49 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)
50
51 @api_version("2.1.0", "2.1.0", _DICT_VERSION_STATUS)
52 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):
53 """
54 Stream events for the current user, restricted to accounts on the given
55 list.
56 """
57 id = self.__unpack_id(id)
58 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)
59
60 @api_version("2.6.0", "2.6.0", _DICT_VERSION_STATUS)
61 def stream_direct(self, listener, run_async=False, timeout=_DEFAULT_STREAM_TIMEOUT, reconnect_async=False, reconnect_async_wait_sec=_DEFAULT_STREAM_RECONNECT_WAIT_SEC):
62 """
63 Streams direct message events for the logged-in user, as conversation events.
64 """
65 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)
66
67 @api_version("2.5.0", "2.5.0", "2.5.0")
68 def stream_healthy(self):
69 """
70 Returns without True if streaming API is okay, False or raises an error otherwise.
71 """
72 api_okay = self.__api_request('GET', '/api/v1/streaming/health', base_url_override=self.__get_streaming_base(), parse=False)
73 if api_okay in [b'OK', b'success']:
74 return True
75 return False
diff --git a/mastodon/suggestions.py b/mastodon/suggestions.py
new file mode 100644
index 0000000..ab55993
--- /dev/null
+++ b/mastodon/suggestions.py
@@ -0,0 +1,32 @@
1# suggestions.py - follow suggestion endpoints
2
3from .versions import _DICT_VERSION_ACCOUNT
4from .utility import api_version
5
6from .internals import Mastodon as Internals
7
8class Mastodon(Internals):
9 ###
10 # Reading data: Follow suggestions
11 ###
12 @api_version("2.4.3", "2.4.3", _DICT_VERSION_ACCOUNT)
13 def suggestions(self):
14 """
15 Fetch follow suggestions for the logged-in user.
16
17 Returns a list of :ref:`account dicts <account dicts>`.
18
19 """
20 return self.__api_request('GET', '/api/v1/suggestions')
21
22 ###
23 # Writing data: Follow suggestions
24 ###
25 @api_version("2.4.3", "2.4.3", _DICT_VERSION_ACCOUNT)
26 def suggestion_delete(self, account_id):
27 """
28 Remove the user with the given `account_id` from the follow suggestions.
29 """
30 account_id = self.__unpack_id(account_id)
31 url = '/api/v1/suggestions/{0}'.format(str(account_id))
32 self.__api_request('DELETE', url) \ No newline at end of file
diff --git a/mastodon/timeline.py b/mastodon/timeline.py
index b5a4068..04406ed 100644
--- a/mastodon/timeline.py
+++ b/mastodon/timeline.py
@@ -1,5 +1,7 @@
1# timeline.py - endpoints for reading various different timelines
2
1from .versions import _DICT_VERSION_STATUS, _DICT_VERSION_CONVERSATION 3from .versions import _DICT_VERSION_STATUS, _DICT_VERSION_CONVERSATION
2from .error import MastodonIllegalArgumentError, MastodonNotFoundError 4from .errors import MastodonIllegalArgumentError, MastodonNotFoundError
3from .utility import api_version 5from .utility import api_version
4 6
5from .internals import Mastodon as Internals 7from .internals import Mastodon as Internals
@@ -101,21 +103,3 @@ class Mastodon(Internals):
101 id = self.__unpack_id(id) 103 id = self.__unpack_id(id)
102 return self.timeline('list/{0}'.format(id), max_id=max_id, min_id=min_id, since_id=since_id, limit=limit, only_media=only_media, local=local, remote=remote) 104 return self.timeline('list/{0}'.format(id), max_id=max_id, min_id=min_id, since_id=since_id, limit=limit, only_media=only_media, local=local, remote=remote)
103 105
104 @api_version("2.6.0", "2.6.0", _DICT_VERSION_CONVERSATION)
105 def conversations(self, max_id=None, min_id=None, since_id=None, limit=None):
106 """
107 Fetches a user's conversations.
108
109 Returns a list of :ref:`conversation dicts <conversation dicts>`.
110 """
111 if max_id is not None:
112 max_id = self.__unpack_id(max_id, dateconv=True)
113
114 if min_id is not None:
115 min_id = self.__unpack_id(min_id, dateconv=True)
116
117 if since_id is not None:
118 since_id = self.__unpack_id(since_id, dateconv=True)
119
120 params = self.__generate_params(locals())
121 return self.__api_request('GET', "/api/v1/conversations/", params)
diff --git a/mastodon/trends.py b/mastodon/trends.py
new file mode 100644
index 0000000..09a0ae5
--- /dev/null
+++ b/mastodon/trends.py
@@ -0,0 +1,72 @@
1# trends.py - trend-related endpoints
2
3from .versions import _DICT_VERSION_HASHTAG, _DICT_VERSION_STATUS, _DICT_VERSION_CARD
4from .utility import api_version
5
6from .internals import Mastodon as Internals
7
8class Mastodon(Internals):
9 ###
10 # Reading data: Trends
11 ###
12 @api_version("2.4.3", "3.5.0", _DICT_VERSION_HASHTAG)
13 def trends(self, limit=None):
14 """
15 Alias for :ref:`trending_tags() <trending_tags()>`
16 """
17 return self.trending_tags(limit=limit)
18
19 @api_version("3.5.0", "3.5.0", _DICT_VERSION_HASHTAG)
20 def trending_tags(self, limit=None, lang=None):
21 """
22 Fetch trending-hashtag information, if the instance provides such information.
23
24 Specify `limit` to limit how many results are returned (the maximum number
25 of results is 10, the endpoint is not paginated).
26
27 Does not require authentication unless locked down by the administrator.
28
29 Important versioning note: This endpoint does not exist for Mastodon versions
30 between 2.8.0 (inclusive) and 3.0.0 (exclusive).
31
32 Pass `lang` to override the global locale parameter, which may affect trend ordering.
33
34 Returns a list of :ref:`hashtag dicts <hashtag dicts>`, sorted by the instance's trending algorithm,
35 descending.
36 """
37 params = self.__generate_params(locals())
38 if self.verify_minimum_version("3.5.0", cached=True):
39 # Starting 3.5.0, old version is deprecated
40 return self.__api_request('GET', '/api/v1/trends/tags', params)
41 else:
42 return self.__api_request('GET', '/api/v1/trends', params)
43
44 @api_version("3.5.0", "3.5.0", _DICT_VERSION_STATUS)
45 def trending_statuses(self):
46 """
47 Fetch trending-status information, if the instance provides such information.
48
49 Specify `limit` to limit how many results are returned (the maximum number
50 of results is 10, the endpoint is not paginated).
51
52 Pass `lang` to override the global locale parameter, which may affect trend ordering.
53
54 Returns a list of :ref:`status dicts <status dicts>`, sorted by the instances's trending algorithm,
55 descending.
56 """
57 params = self.__generate_params(locals())
58 return self.__api_request('GET', '/api/v1/trends/statuses', params)
59
60 @api_version("3.5.0", "3.5.0", _DICT_VERSION_CARD)
61 def trending_links(self):
62 """
63 Fetch trending-link information, if the instance provides such information.
64
65 Specify `limit` to limit how many results are returned (the maximum number
66 of results is 10, the endpoint is not paginated).
67
68 Returns a list of :ref:`card dicts <card dicts>`, sorted by the instances's trending algorithm,
69 descending.
70 """
71 params = self.__generate_params(locals())
72 return self.__api_request('GET', '/api/v1/trends/links', params)
diff --git a/mastodon/utility.py b/mastodon/utility.py
index 53980b6..2510dca 100644
--- a/mastodon/utility.py
+++ b/mastodon/utility.py
@@ -1,10 +1,13 @@
1# utility.py - utility functions, externally usable 1# utility.py - utility functions, externally usable
2 2
3import re 3import re
4from decorator import decorate
5from .error import MastodonVersionError, MastodonAPIError
6import dateutil 4import dateutil
7import datetime 5import datetime
6import copy
7
8from decorator import decorate
9from .errors import MastodonVersionError, MastodonAPIError
10from .compat import IMPL_HAS_BLURHASH, blurhash
8 11
9# Module level: 12# Module level:
10 13
@@ -141,3 +144,112 @@ class Mastodon():
141 return datetime.datetime.fromtimestamp(epoch_time) 144 return datetime.datetime.fromtimestamp(epoch_time)
142 else: 145 else:
143 raise MastodonAPIError("No server time in response.") 146 raise MastodonAPIError("No server time in response.")
147
148 ###
149 # Blurhash utilities
150 ###
151 def decode_blurhash(self, media_dict, out_size=(16, 16), size_per_component=True, return_linear=True):
152 """
153 Basic media-dict blurhash decoding.
154
155 out_size is the desired result size in pixels, either absolute or per blurhash
156 component (this is the default).
157
158 By default, this function will return the image as linear RGB, ready for further
159 scaling operations. If you want to display the image directly, set return_linear
160 to False.
161
162 Returns the decoded blurhash image as a three-dimensional list: [height][width][3],
163 with the last dimension being RGB colours.
164
165 For further info and tips for advanced usage, refer to the documentation for the
166 blurhash module: https://github.com/halcy/blurhash-python
167 """
168 if not IMPL_HAS_BLURHASH:
169 raise NotImplementedError(
170 'To use the blurhash functions, please install the blurhash Python module.')
171
172 # Figure out what size to decode to
173 decode_components_x, decode_components_y = blurhash.components(media_dict["blurhash"])
174 if size_per_component:
175 decode_size_x = decode_components_x * out_size[0]
176 decode_size_y = decode_components_y * out_size[1]
177 else:
178 decode_size_x = out_size[0]
179 decode_size_y = out_size[1]
180
181 # Decode
182 decoded_image = blurhash.decode(media_dict["blurhash"], decode_size_x, decode_size_y, linear=return_linear)
183
184 # And that's pretty much it.
185 return decoded_image
186
187 ###
188 # Pagination
189 ###
190 def fetch_next(self, previous_page):
191 """
192 Fetches the next page of results of a paginated request. Pass in the
193 previous page in its entirety, or the pagination information dict
194 returned as a part of that pages last status ('_pagination_next').
195
196 Returns the next page or None if no further data is available.
197 """
198 if isinstance(previous_page, list) and len(previous_page) != 0:
199 if hasattr(previous_page, '_pagination_next'):
200 params = copy.deepcopy(previous_page._pagination_next)
201 else:
202 return None
203 else:
204 params = copy.deepcopy(previous_page)
205
206 method = params['_pagination_method']
207 del params['_pagination_method']
208
209 endpoint = params['_pagination_endpoint']
210 del params['_pagination_endpoint']
211
212 return self.__api_request(method, endpoint, params)
213
214 def fetch_previous(self, next_page):
215 """
216 Fetches the previous page of results of a paginated request. Pass in the
217 previous page in its entirety, or the pagination information dict
218 returned as a part of that pages first status ('_pagination_prev').
219
220 Returns the previous page or None if no further data is available.
221 """
222 if isinstance(next_page, list) and len(next_page) != 0:
223 if hasattr(next_page, '_pagination_prev'):
224 params = copy.deepcopy(next_page._pagination_prev)
225 else:
226 return None
227 else:
228 params = copy.deepcopy(next_page)
229
230 method = params['_pagination_method']
231 del params['_pagination_method']
232
233 endpoint = params['_pagination_endpoint']
234 del params['_pagination_endpoint']
235
236 return self.__api_request(method, endpoint, params)
237
238 def fetch_remaining(self, first_page):
239 """
240 Fetches all the remaining pages of a paginated request starting from a
241 first page and returns the entire set of results (including the first page
242 that was passed in) as a big list.
243
244 Be careful, as this might generate a lot of requests, depending on what you are
245 fetching, and might cause you to run into rate limits very quickly.
246 """
247 first_page = copy.deepcopy(first_page)
248
249 all_pages = []
250 current_page = first_page
251 while current_page is not None and len(current_page) > 0:
252 all_pages.extend(current_page)
253 current_page = self.fetch_next(current_page)
254
255 return all_pages
Powered by cgit v1.2.3 (git 2.41.0)