diff options
Diffstat (limited to 'mastodon')
-rw-r--r-- | mastodon/Mastodon.py | 2852 | ||||
-rw-r--r-- | mastodon/accounts.py | 395 | ||||
-rw-r--r-- | mastodon/admin.py | 572 | ||||
-rw-r--r-- | mastodon/authentication.py | 22 | ||||
-rw-r--r-- | mastodon/conversations.py | 43 | ||||
-rw-r--r-- | mastodon/endorsements.py | 19 | ||||
-rw-r--r-- | mastodon/errors.py (renamed from mastodon/error.py) | 0 | ||||
-rw-r--r-- | mastodon/favourites.py | 52 | ||||
-rw-r--r-- | mastodon/filters.py | 119 | ||||
-rw-r--r-- | mastodon/hashtags.py | 52 | ||||
-rw-r--r-- | mastodon/instance.py | 97 | ||||
-rw-r--r-- | mastodon/internals.py | 4 | ||||
-rw-r--r-- | mastodon/lists.py | 112 | ||||
-rw-r--r-- | mastodon/media.py | 114 | ||||
-rw-r--r-- | mastodon/notifications.py | 92 | ||||
-rw-r--r-- | mastodon/polls.py | 61 | ||||
-rw-r--r-- | mastodon/preferences.py | 71 | ||||
-rw-r--r-- | mastodon/push.py | 201 | ||||
-rw-r--r-- | mastodon/relationships.py | 136 | ||||
-rw-r--r-- | mastodon/reports.py | 62 | ||||
-rw-r--r-- | mastodon/search.py | 91 | ||||
-rw-r--r-- | mastodon/statuses.py | 424 | ||||
-rw-r--r-- | mastodon/streaming_endpoints.py | 75 | ||||
-rw-r--r-- | mastodon/suggestions.py | 32 | ||||
-rw-r--r-- | mastodon/timeline.py | 22 | ||||
-rw-r--r-- | mastodon/trends.py | 72 | ||||
-rw-r--r-- | mastodon/utility.py | 116 |
27 files changed, 3049 insertions, 2859 deletions
diff --git a/mastodon/Mastodon.py b/mastodon/Mastodon.py index 0ded1cf..23e4d9e 100644 --- a/mastodon/Mastodon.py +++ b/mastodon/Mastodon.py | |||
@@ -26,7 +26,7 @@ from .utility import parse_version_string, max_version, api_version | |||
26 | from .utility import AttribAccessDict, AttribAccessDict | 26 | from .utility import AttribAccessDict, AttribAccessDict |
27 | from .utility import Mastodon as Utility | 27 | from .utility import Mastodon as Utility |
28 | 28 | ||
29 | from .error import * | 29 | from .errors import * |
30 | from .versions import _DICT_VERSION_APPLICATION, _DICT_VERSION_MENTION, _DICT_VERSION_MEDIA, _DICT_VERSION_ACCOUNT, _DICT_VERSION_POLL, \ | 30 | from .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 | |||
45 | from .instance import Mastodon as Instance | 45 | from .instance import Mastodon as Instance |
46 | from .timeline import Mastodon as Timeline | 46 | from .timeline import Mastodon as Timeline |
47 | from .statuses import Mastodon as Statuses | 47 | from .statuses import Mastodon as Statuses |
48 | from .media import Mastodon as Media | ||
49 | from .polls import Mastodon as Polls | ||
50 | from .notifications import Mastodon as Notifications | ||
51 | from .conversations import Mastodon as Conversations | ||
52 | from .hashtags import Mastodon as Hashtags | ||
53 | from .filters import Mastodon as Filters | ||
54 | from .suggestions import Mastodon as Suggestions | ||
55 | from .endorsements import Mastodon as Endorsements | ||
56 | from .relationships import Mastodon as Relationships | ||
57 | from .lists import Mastodon as Lists | ||
58 | from .trends import Mastodon as Trends | ||
59 | from .search import Mastodon as Search | ||
60 | from .favourites import Mastodon as Favourites | ||
61 | from .reports import Mastodon as Reports | ||
62 | from .preferences import Mastodon as Preferences | ||
63 | from .push import Mastodon as Push | ||
64 | from .admin import Mastodon as Admin | ||
65 | from .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 | ### |
52 | class Mastodon(Utility, Authentication, Accounts, Instance, Timeline, Statuses): | 72 | class 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 | |||
3 | import collections | ||
4 | |||
5 | from .versions import _DICT_VERSION_ACCOUNT, _DICT_VERSION_STATUS, _DICT_VERSION_RELATIONSHIP, _DICT_VERSION_LIST, _DICT_VERSION_FAMILIAR_FOLLOWERS, _DICT_VERSION_HASHTAG | ||
1 | from .defaults import _DEFAULT_SCOPES, _SCOPE_SETS | 6 | from .defaults import _DEFAULT_SCOPES, _SCOPE_SETS |
2 | from .error import MastodonIllegalArgumentError, MastodonAPIError | 7 | from .errors import MastodonIllegalArgumentError, MastodonAPIError |
3 | from .utility import api_version | 8 | from .utility import api_version |
4 | 9 | ||
5 | from .internals import Mastodon as Internals | 10 | from .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 | |||
3 | from .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 | ||
5 | from .errors import MastodonIllegalArgumentError | ||
6 | from .utility import api_version | ||
7 | |||
8 | from .internals import Mastodon as Internals | ||
9 | |||
10 | class 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 | |||
1 | import requests | 3 | import requests |
2 | from requests.models import urlencode | 4 | from requests.models import urlencode |
3 | import datetime | 5 | import datetime |
@@ -5,9 +7,11 @@ import os | |||
5 | import time | 7 | import time |
6 | import collections | 8 | import collections |
7 | 9 | ||
8 | from .error import MastodonIllegalArgumentError, MastodonNetworkError, MastodonVersionError, MastodonAPIError | 10 | from .errors import MastodonIllegalArgumentError, MastodonNetworkError, MastodonVersionError, MastodonAPIError |
11 | from .versions import _DICT_VERSION_APPLICATION | ||
9 | from .defaults import _DEFAULT_SCOPES, _SCOPE_SETS, _DEFAULT_TIMEOUT | 12 | from .defaults import _DEFAULT_SCOPES, _SCOPE_SETS, _DEFAULT_TIMEOUT |
10 | from .utility import parse_version_string | 13 | from .utility import parse_version_string, api_version |
14 | |||
11 | from .internals import Mastodon as Internals | 15 | from .internals import Mastodon as Internals |
12 | 16 | ||
13 | class Mastodon(Internals): | 17 | class 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 | |||
3 | from .versions import _DICT_VERSION_CONVERSATION | ||
4 | from .utility import api_version | ||
5 | |||
6 | from .internals import Mastodon as Internals | ||
7 | |||
8 | class 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 | |||
3 | from .versions import _DICT_VERSION_ACCOUNT | ||
4 | from .utility import api_version | ||
5 | |||
6 | from .internals import Mastodon as Internals | ||
7 | |||
8 | class 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 | |||
3 | from .versions import _DICT_VERSION_STATUS | ||
4 | from .utility import api_version | ||
5 | |||
6 | from .internals import Mastodon as Internals | ||
7 | |||
8 | class 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 | |||
3 | import re | ||
4 | |||
5 | from .versions import _DICT_VERSION_FILTER | ||
6 | from .errors import MastodonIllegalArgumentError | ||
7 | from .utility import api_version | ||
8 | |||
9 | from .internals import Mastodon as Internals | ||
10 | |||
11 | class 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 | |||
3 | from .versions import _DICT_VERSION_FEATURED_TAG, _DICT_VERSION_HASHTAG | ||
4 | from .utility import api_version | ||
5 | |||
6 | from .internals import Mastodon as Internals | ||
7 | |||
8 | class 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 @@ | |||
1 | from .versions import _DICT_VERSION_INSTANCE, _DICT_VERSION_ACTIVITY | 1 | # instance.py - instance-level endpoints, directory, emoji, announcements |
2 | from .error import MastodonIllegalArgumentError, MastodonNotFoundError | 2 | |
3 | from .versions import _DICT_VERSION_INSTANCE, _DICT_VERSION_ACTIVITY, _DICT_VERSION_ACCOUNT, _DICT_VERSION_EMOJI, _DICT_VERSION_ANNOUNCEMENT | ||
4 | from .errors import MastodonIllegalArgumentError, MastodonNotFoundError | ||
3 | from .utility import api_version | 5 | from .utility import api_version |
4 | from .compat import urlparse | 6 | from .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 | |||
1 | import datetime | 3 | import datetime |
2 | from contextlib import closing | 4 | from contextlib import closing |
3 | import mimetypes | 5 | import mimetypes |
@@ -14,7 +16,7 @@ import base64 | |||
14 | import os | 16 | import os |
15 | 17 | ||
16 | from .utility import AttribAccessDict, AttribAccessList, parse_version_string | 18 | from .utility import AttribAccessDict, AttribAccessList, parse_version_string |
17 | from .error import MastodonNetworkError, MastodonIllegalArgumentError, MastodonRatelimitError, MastodonNotFoundError, \ | 19 | from .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 |
20 | from .compat import urlparse, magic, PurePath | 22 | from .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 | |||
3 | from .versions import _DICT_VERSION_LIST, _DICT_VERSION_ACCOUNT | ||
4 | from .utility import api_version | ||
5 | |||
6 | from .internals import Mastodon as Internals | ||
7 | |||
8 | class 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 | |||
3 | import time | ||
4 | |||
5 | from .versions import _DICT_VERSION_MEDIA | ||
6 | from .errors import MastodonVersionError, MastodonAPIError | ||
7 | from .utility import api_version | ||
8 | |||
9 | from .internals import Mastodon as Internals | ||
10 | |||
11 | class 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 | |||
3 | from .versions import _DICT_VERSION_NOTIFICATION | ||
4 | from .errors import MastodonIllegalArgumentError | ||
5 | from .utility import api_version | ||
6 | |||
7 | from .internals import Mastodon as Internals | ||
8 | |||
9 | class 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 | |||
3 | from .versions import _DICT_VERSION_POLL | ||
4 | from .utility import api_version | ||
5 | |||
6 | from .internals import Mastodon as Internals | ||
7 | |||
8 | class 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 | |||
3 | import collections | ||
4 | |||
5 | from .versions import _DICT_VERSION_PREFERENCES, _DICT_VERSION_MARKER | ||
6 | from .errors import MastodonIllegalArgumentError | ||
7 | from .utility import api_version | ||
8 | |||
9 | from .internals import Mastodon as Internals | ||
10 | |||
11 | class 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 | |||
3 | import base64 | ||
4 | import os | ||
5 | import json | ||
6 | |||
7 | from .versions import _DICT_VERSION_PUSH, _DICT_VERSION_PUSH_NOTIF | ||
8 | from .errors import MastodonIllegalArgumentError | ||
9 | from .utility import api_version | ||
10 | from .compat import IMPL_HAS_CRYPTO, ec, serialization, default_backend | ||
11 | from .compat import IMPL_HAS_ECE, http_ece | ||
12 | |||
13 | from .internals import Mastodon as Internals | ||
14 | |||
15 | class 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 | |||
3 | from .versions import _DICT_VERSION_ACCOUNT, _DICT_VERSION_RELATIONSHIP | ||
4 | from .utility import api_version | ||
5 | |||
6 | from .internals import Mastodon as Internals | ||
7 | |||
8 | class 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 | |||
4 | from .versions import _DICT_VERSION_REPORT | ||
5 | from .errors import MastodonVersionError | ||
6 | from .utility import api_version | ||
7 | |||
8 | from .internals import Mastodon as Internals | ||
9 | |||
10 | class 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 | |||
3 | from .versions import _DICT_VERSION_SEARCHRESULT | ||
4 | from .errors import MastodonVersionError | ||
5 | from .utility import api_version | ||
6 | |||
7 | from .internals import Mastodon as Internals | ||
8 | |||
9 | class 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) | |
2 | from .versions import _DICT_VERSION_STATUS, _DICT_VERSION_CARD, _DICT_VERSION_CONTEXT, _DICT_VERSION_ACCOUNT, _DICT_VERSION_SCHEDULED_STATUS | 2 | |
3 | import collections | ||
4 | |||
5 | from .versions import _DICT_VERSION_STATUS, _DICT_VERSION_CARD, _DICT_VERSION_CONTEXT, _DICT_VERSION_ACCOUNT, _DICT_VERSION_SCHEDULED_STATUS, \ | ||
6 | _DICT_VERSION_STATUS_EDIT | ||
7 | from .errors import MastodonIllegalArgumentError | ||
3 | from .utility import api_version | 8 | from .utility import api_version |
4 | 9 | ||
5 | from .internals import Mastodon as Internals | 10 | from .internals import Mastodon as Internals |
6 | 11 | ||
7 | |||
8 | class Mastodon(Internals): | 12 | class 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 | |||
3 | from .versions import _DICT_VERSION_STATUS | ||
4 | from .errors import MastodonIllegalArgumentError | ||
5 | from .defaults import _DEFAULT_STREAM_TIMEOUT, _DEFAULT_STREAM_RECONNECT_WAIT_SEC | ||
6 | from .utility import api_version | ||
7 | |||
8 | from .internals import Mastodon as Internals | ||
9 | |||
10 | class 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 | |||
3 | from .versions import _DICT_VERSION_ACCOUNT | ||
4 | from .utility import api_version | ||
5 | |||
6 | from .internals import Mastodon as Internals | ||
7 | |||
8 | class 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 | |||
1 | from .versions import _DICT_VERSION_STATUS, _DICT_VERSION_CONVERSATION | 3 | from .versions import _DICT_VERSION_STATUS, _DICT_VERSION_CONVERSATION |
2 | from .error import MastodonIllegalArgumentError, MastodonNotFoundError | 4 | from .errors import MastodonIllegalArgumentError, MastodonNotFoundError |
3 | from .utility import api_version | 5 | from .utility import api_version |
4 | 6 | ||
5 | from .internals import Mastodon as Internals | 7 | from .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 | |||
3 | from .versions import _DICT_VERSION_HASHTAG, _DICT_VERSION_STATUS, _DICT_VERSION_CARD | ||
4 | from .utility import api_version | ||
5 | |||
6 | from .internals import Mastodon as Internals | ||
7 | |||
8 | class 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 | ||
3 | import re | 3 | import re |
4 | from decorator import decorate | ||
5 | from .error import MastodonVersionError, MastodonAPIError | ||
6 | import dateutil | 4 | import dateutil |
7 | import datetime | 5 | import datetime |
6 | import copy | ||
7 | |||
8 | from decorator import decorate | ||
9 | from .errors import MastodonVersionError, MastodonAPIError | ||
10 | from .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 | ||