diff options
Diffstat (limited to 'mastodon/statuses.py')
-rw-r--r-- | mastodon/statuses.py | 424 |
1 files changed, 420 insertions, 4 deletions
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) | ||