aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'mastodon/statuses.py')
-rw-r--r--mastodon/statuses.py424
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)
2from .versions import _DICT_VERSION_STATUS, _DICT_VERSION_CARD, _DICT_VERSION_CONTEXT, _DICT_VERSION_ACCOUNT, _DICT_VERSION_SCHEDULED_STATUS 2
3import collections
4
5from .versions import _DICT_VERSION_STATUS, _DICT_VERSION_CARD, _DICT_VERSION_CONTEXT, _DICT_VERSION_ACCOUNT, _DICT_VERSION_SCHEDULED_STATUS, \
6 _DICT_VERSION_STATUS_EDIT
7from .errors import MastodonIllegalArgumentError
3from .utility import api_version 8from .utility import api_version
4 9
5from .internals import Mastodon as Internals 10from .internals import Mastodon as Internals
6 11
7
8class Mastodon(Internals): 12class Mastodon(Internals):
9 ### 13 ###
10 # Reading data: Statuses 14 # Reading data: Statuses
@@ -105,4 +109,416 @@ class Mastodon(Internals):
105 id = self.__unpack_id(id) 109 id = self.__unpack_id(id)
106 url = '/api/v1/scheduled_statuses/{0}'.format(str(id)) 110 url = '/api/v1/scheduled_statuses/{0}'.format(str(id))
107 return self.__api_request('GET', url) 111 return self.__api_request('GET', url)
108 \ No newline at end of file 112
113 ###
114 # Writing data: Statuses
115 ###
116 def __status_internal(self, status, in_reply_to_id=None, media_ids=None,
117 sensitive=False, visibility=None, spoiler_text=None,
118 language=None, idempotency_key=None, content_type=None,
119 scheduled_at=None, poll=None, quote_id=None, edit=False):
120 if quote_id is not None:
121 if self.feature_set != "fedibird":
122 raise MastodonIllegalArgumentError('quote_id is only available with feature set fedibird')
123 quote_id = self.__unpack_id(quote_id)
124
125 if content_type is not None:
126 if self.feature_set != "pleroma":
127 raise MastodonIllegalArgumentError('content_type is only available with feature set pleroma')
128 # It would be better to read this from nodeinfo and cache, but this is easier
129 if not content_type in ["text/plain", "text/html", "text/markdown", "text/bbcode"]:
130 raise MastodonIllegalArgumentError('Invalid content type specified')
131
132 if in_reply_to_id is not None:
133 in_reply_to_id = self.__unpack_id(in_reply_to_id)
134
135 if scheduled_at is not None:
136 scheduled_at = self.__consistent_isoformat_utc(scheduled_at)
137
138 params_initial = locals()
139
140 # Validate poll/media exclusivity
141 if poll is not None:
142 if media_ids is not None and len(media_ids) != 0:
143 raise ValueError(
144 'Status can have media or poll attached - not both.')
145
146 # Validate visibility parameter
147 valid_visibilities = ['private', 'public', 'unlisted', 'direct']
148 if params_initial['visibility'] is None:
149 del params_initial['visibility']
150 else:
151 params_initial['visibility'] = params_initial['visibility'].lower()
152 if params_initial['visibility'] not in valid_visibilities:
153 raise ValueError('Invalid visibility value! Acceptable values are %s' % valid_visibilities)
154
155 if params_initial['language'] is None:
156 del params_initial['language']
157
158 if params_initial['sensitive'] is False:
159 del [params_initial['sensitive']]
160
161 headers = {}
162 if idempotency_key is not None:
163 headers['Idempotency-Key'] = idempotency_key
164
165 if media_ids is not None:
166 try:
167 media_ids_proper = []
168 if not isinstance(media_ids, (list, tuple)):
169 media_ids = [media_ids]
170 for media_id in media_ids:
171 media_ids_proper.append(self.__unpack_id(media_id))
172 except Exception as e:
173 raise MastodonIllegalArgumentError("Invalid media dict: %s" % e)
174
175 params_initial["media_ids"] = media_ids_proper
176
177 if params_initial['content_type'] is None:
178 del params_initial['content_type']
179
180 use_json = False
181 if poll is not None:
182 use_json = True
183
184 params = self.__generate_params(params_initial, ['idempotency_key', 'edit'])
185 if edit is None:
186 # Post
187 return self.__api_request('POST', '/api/v1/statuses', params, headers=headers, use_json=use_json)
188 else:
189 # Edit
190 return self.__api_request('PUT', '/api/v1/statuses/{0}'.format(str(self.__unpack_id(edit))), params, headers=headers, use_json=use_json)
191
192 @api_version("1.0.0", "2.8.0", _DICT_VERSION_STATUS)
193 def status_post(self, status, in_reply_to_id=None, media_ids=None,
194 sensitive=False, visibility=None, spoiler_text=None,
195 language=None, idempotency_key=None, content_type=None,
196 scheduled_at=None, poll=None, quote_id=None):
197 """
198 Post a status. Can optionally be in reply to another status and contain
199 media.
200
201 `media_ids` should be a list. (If it's not, the function will turn it
202 into one.) It can contain up to four pieces of media (uploaded via
203 :ref:`media_post() <media_post()>`). `media_ids` can also be the `media dicts`_ returned
204 by :ref:`media_post() <media_post()>` - they are unpacked automatically.
205
206 The `sensitive` boolean decides whether or not media attached to the post
207 should be marked as sensitive, which hides it by default on the Mastodon
208 web front-end.
209
210 The visibility parameter is a string value and accepts any of:
211 'direct' - post will be visible only to mentioned users
212 'private' - post will be visible only to followers
213 'unlisted' - post will be public but not appear on the public timeline
214 'public' - post will be public
215
216 If not passed in, visibility defaults to match the current account's
217 default-privacy setting (starting with Mastodon version 1.6) or its
218 locked setting - private if the account is locked, public otherwise
219 (for Mastodon versions lower than 1.6).
220
221 The `spoiler_text` parameter is a string to be shown as a warning before
222 the text of the status. If no text is passed in, no warning will be
223 displayed.
224
225 Specify `language` to override automatic language detection. The parameter
226 accepts all valid ISO 639-1 (2-letter) or for languages where that do not
227 have one, 639-3 (three letter) language codes.
228
229 You can set `idempotency_key` to a value to uniquely identify an attempt
230 at posting a status. Even if you call this function more than once,
231 if you call it with the same `idempotency_key`, only one status will
232 be created.
233
234 Pass a datetime as `scheduled_at` to schedule the toot for a specific time
235 (the time must be at least 5 minutes into the future). If this is passed,
236 status_post returns a :ref:`scheduled status dict <scheduled status dict>` instead.
237
238 Pass `poll` to attach a poll to the status. An appropriate object can be
239 constructed using :ref:`make_poll() <make_poll()>` . Note that as of Mastodon version
240 2.8.2, you can only have either media or a poll attached, not both at
241 the same time.
242
243 **Specific to "pleroma" feature set:**: Specify `content_type` to set
244 the content type of your post on Pleroma. It accepts 'text/plain' (default),
245 'text/markdown', 'text/html' and 'text/bbcode'. This parameter is not
246 supported on Mastodon servers, but will be safely ignored if set.
247
248 **Specific to "fedibird" feature set:**: The `quote_id` parameter is
249 a non-standard extension that specifies the id of a quoted status.
250
251 Returns a :ref:`status dict <status dict>` with the new status.
252 """
253 return self.__status_internal(
254 status,
255 in_reply_to_id,
256 media_ids,
257 sensitive,
258 visibility,
259 spoiler_text,
260 language,
261 idempotency_key,
262 content_type,
263 scheduled_at,
264 poll,
265 quote_id,
266 edit=None
267 )
268
269 @api_version("1.0.0", "2.8.0", _DICT_VERSION_STATUS)
270 def toot(self, status):
271 """
272 Synonym for :ref:`status_post() <status_post()>` that only takes the status text as input.
273
274 Usage in production code is not recommended.
275
276 Returns a :ref:`status dict <status dict>` with the new status.
277 """
278 return self.status_post(status)
279
280 @api_version("3.5.0", "3.5.0", _DICT_VERSION_STATUS)
281 def status_update(self, id, status = None, spoiler_text = None, sensitive = None, media_ids = None, poll = None):
282 """
283 Edit a status. The meanings of the fields are largely the same as in :ref:`status_post() <status_post()>`,
284 though not every field can be edited.
285
286 Note that editing a poll will reset the votes.
287 """
288 return self.__status_internal(
289 status = status,
290 media_ids = media_ids,
291 sensitive = sensitive,
292 spoiler_text = spoiler_text,
293 poll = poll,
294 edit = id
295 )
296
297 @api_version("3.5.0", "3.5.0", _DICT_VERSION_STATUS_EDIT)
298 def status_history(self, id):
299 """
300 Returns the edit history of a status as a list of :ref:`status edit dicts <status edit dicts>`, starting
301 from the original form. Note that this means that a status that has been edited
302 once will have *two* entries in this list, a status that has been edited twice
303 will have three, and so on.
304 """
305 id = self.__unpack_id(id)
306 return self.__api_request('GET', "/api/v1/statuses/{0}/history".format(str(id)))
307
308 def status_source(self, id):
309 """
310 Returns the source of a status for editing.
311
312 Return value is a dictionary containing exactly the parameters you could pass to
313 :ref:`status_update() <status_update()>` to change nothing about the status, except `status` is `text`
314 instead.
315 """
316 id = self.__unpack_id(id)
317 return self.__api_request('GET', "/api/v1/statuses/{0}/source".format(str(id)))
318
319 @api_version("1.0.0", "2.8.0", _DICT_VERSION_STATUS)
320 def status_reply(self, to_status, status, in_reply_to_id=None, media_ids=None,
321 sensitive=False, visibility=None, spoiler_text=None,
322 language=None, idempotency_key=None, content_type=None,
323 scheduled_at=None, poll=None, untag=False):
324 """
325 Helper function - acts like status_post, but prepends the name of all
326 the users that are being replied to to the status text and retains
327 CW and visibility if not explicitly overridden.
328
329 Set `untag` to True if you want the reply to only go to the user you
330 are replying to, removing every other mentioned user from the
331 conversation.
332 """
333 keyword_args = locals()
334 del keyword_args["self"]
335 del keyword_args["to_status"]
336 del keyword_args["untag"]
337
338 user_id = self.__get_logged_in_id()
339
340 # Determine users to mention
341 mentioned_accounts = collections.OrderedDict()
342 mentioned_accounts[to_status.account.id] = to_status.account.acct
343
344 if not untag:
345 for account in to_status.mentions:
346 if account.id != user_id and not account.id in mentioned_accounts.keys():
347 mentioned_accounts[account.id] = account.acct
348
349 # Join into one piece of text. The space is added inside because of self-replies.
350 status = "".join(map(lambda x: "@" + x + " ",
351 mentioned_accounts.values())) + status
352
353 # Retain visibility / cw
354 if visibility is None and 'visibility' in to_status:
355 visibility = to_status.visibility
356 if spoiler_text is None and 'spoiler_text' in to_status:
357 spoiler_text = to_status.spoiler_text
358
359 keyword_args["status"] = status
360 keyword_args["visibility"] = visibility
361 keyword_args["spoiler_text"] = spoiler_text
362 keyword_args["in_reply_to_id"] = to_status.id
363 return self.status_post(**keyword_args)
364
365 @api_version("1.0.0", "1.0.0", "1.0.0")
366 def status_delete(self, id):
367 """
368 Delete a status
369
370 Returns the now-deleted status, with an added "source" attribute that contains
371 the text that was used to compose this status (this can be used to power
372 "delete and redraft" functionality)
373 """
374 id = self.__unpack_id(id)
375 url = '/api/v1/statuses/{0}'.format(str(id))
376 return self.__api_request('DELETE', url)
377
378 @api_version("1.0.0", "2.0.0", _DICT_VERSION_STATUS)
379 def status_reblog(self, id, visibility=None):
380 """
381 Reblog / boost a status.
382
383 The visibility parameter functions the same as in :ref:`status_post() <status_post()>` and
384 allows you to reduce the visibility of a reblogged status.
385
386 Returns a :ref:`status dict <status dict>` with a new status that wraps around the reblogged one.
387 """
388 params = self.__generate_params(locals(), ['id'])
389 valid_visibilities = ['private', 'public', 'unlisted', 'direct']
390 if 'visibility' in params:
391 params['visibility'] = params['visibility'].lower()
392 if params['visibility'] not in valid_visibilities:
393 raise ValueError('Invalid visibility value! Acceptable '
394 'values are %s' % valid_visibilities)
395
396 id = self.__unpack_id(id)
397 url = '/api/v1/statuses/{0}/reblog'.format(str(id))
398 return self.__api_request('POST', url, params)
399
400 @api_version("1.0.0", "2.0.0", _DICT_VERSION_STATUS)
401 def status_unreblog(self, id):
402 """
403 Un-reblog a status.
404
405 Returns a :ref:`status dict <status dict>` with the status that used to be reblogged.
406 """
407 id = self.__unpack_id(id)
408 url = '/api/v1/statuses/{0}/unreblog'.format(str(id))
409 return self.__api_request('POST', url)
410
411 @api_version("1.0.0", "2.0.0", _DICT_VERSION_STATUS)
412 def status_favourite(self, id):
413 """
414 Favourite a status.
415
416 Returns a :ref:`status dict <status dict>` with the favourited status.
417 """
418 id = self.__unpack_id(id)
419 url = '/api/v1/statuses/{0}/favourite'.format(str(id))
420 return self.__api_request('POST', url)
421
422 @api_version("1.0.0", "2.0.0", _DICT_VERSION_STATUS)
423 def status_unfavourite(self, id):
424 """
425 Un-favourite a status.
426
427 Returns a :ref:`status dict <status dict>` with the un-favourited status.
428 """
429 id = self.__unpack_id(id)
430 url = '/api/v1/statuses/{0}/unfavourite'.format(str(id))
431 return self.__api_request('POST', url)
432
433 @api_version("1.4.0", "2.0.0", _DICT_VERSION_STATUS)
434 def status_mute(self, id):
435 """
436 Mute notifications for a status.
437
438 Returns a :ref:`status dict <status dict>` with the now muted status
439 """
440 id = self.__unpack_id(id)
441 url = '/api/v1/statuses/{0}/mute'.format(str(id))
442 return self.__api_request('POST', url)
443
444 @api_version("1.4.0", "2.0.0", _DICT_VERSION_STATUS)
445 def status_unmute(self, id):
446 """
447 Unmute notifications for a status.
448
449 Returns a :ref:`status dict <status dict>` with the status that used to be muted.
450 """
451 id = self.__unpack_id(id)
452 url = '/api/v1/statuses/{0}/unmute'.format(str(id))
453 return self.__api_request('POST', url)
454
455 @api_version("2.1.0", "2.1.0", _DICT_VERSION_STATUS)
456 def status_pin(self, id):
457 """
458 Pin a status for the logged-in user.
459
460 Returns a :ref:`status dict <status dict>` with the now pinned status
461 """
462 id = self.__unpack_id(id)
463 url = '/api/v1/statuses/{0}/pin'.format(str(id))
464 return self.__api_request('POST', url)
465
466 @api_version("2.1.0", "2.1.0", _DICT_VERSION_STATUS)
467 def status_unpin(self, id):
468 """
469 Unpin a pinned status for the logged-in user.
470
471 Returns a :ref:`status dict <status dict>` with the status that used to be pinned.
472 """
473 id = self.__unpack_id(id)
474 url = '/api/v1/statuses/{0}/unpin'.format(str(id))
475 return self.__api_request('POST', url)
476
477 @api_version("3.1.0", "3.1.0", _DICT_VERSION_STATUS)
478 def status_bookmark(self, id):
479 """
480 Bookmark a status as the logged-in user.
481
482 Returns a :ref:`status dict <status dict>` with the now bookmarked status
483 """
484 id = self.__unpack_id(id)
485 url = '/api/v1/statuses/{0}/bookmark'.format(str(id))
486 return self.__api_request('POST', url)
487
488 @api_version("3.1.0", "3.1.0", _DICT_VERSION_STATUS)
489 def status_unbookmark(self, id):
490 """
491 Unbookmark a bookmarked status for the logged-in user.
492
493 Returns a :ref:`status dict <status dict>` with the status that used to be bookmarked.
494 """
495 id = self.__unpack_id(id)
496 url = '/api/v1/statuses/{0}/unbookmark'.format(str(id))
497 return self.__api_request('POST', url)
498
499 ###
500 # Writing data: Scheduled statuses
501 ###
502 @api_version("2.7.0", "2.7.0", _DICT_VERSION_SCHEDULED_STATUS)
503 def scheduled_status_update(self, id, scheduled_at):
504 """
505 Update the scheduled time of a scheduled status.
506
507 New time must be at least 5 minutes into the future.
508
509 Returns a :ref:`scheduled status dict <scheduled status dict>`
510 """
511 scheduled_at = self.__consistent_isoformat_utc(scheduled_at)
512 id = self.__unpack_id(id)
513 params = self.__generate_params(locals(), ['id'])
514 url = '/api/v1/scheduled_statuses/{0}'.format(str(id))
515 return self.__api_request('PUT', url, params)
516
517 @api_version("2.7.0", "2.7.0", "2.7.0")
518 def scheduled_status_delete(self, id):
519 """
520 Deletes a scheduled status.
521 """
522 id = self.__unpack_id(id)
523 url = '/api/v1/scheduled_statuses/{0}'.format(str(id))
524 self.__api_request('DELETE', url)
Powered by cgit v1.2.3 (git 2.41.0)