· 6 years ago · Nov 18, 2019, 01:04 PM
1"""Provide the Subreddit class."""
2# pylint: disable=too-many-lines
3from copy import deepcopy
4from json import dumps, loads
5from os.path import basename, dirname, join
6from urllib.parse import urljoin
7
8from prawcore import Redirect
9import websocket
10
11from ...const import API_PATH, JPEG_HEADER
12from ...exceptions import APIException, ClientException
13from ...util.cache import cachedproperty
14from ..util import permissions_string, stream_generator
15from ..listing.generator import ListingGenerator
16from ..listing.mixins import SubredditListingMixin
17from .base import RedditBase
18from .emoji import SubredditEmoji
19from .mixins import FullnameMixin, MessageableMixin
20from .modmail import ModmailConversation
21from .widgets import SubredditWidgets, WidgetEncoder
22from .wikipage import WikiPage
23
24
25class Subreddit(
26 MessageableMixin, SubredditListingMixin, FullnameMixin, RedditBase
27):
28 """A class for Subreddits.
29
30 To obtain an instance of this class for subreddit ``/r/redditdev`` execute:
31
32 .. code:: python
33
34 subreddit = reddit.subreddit('redditdev')
35
36 While ``/r/all`` is not a real subreddit, it can still be treated like
37 one. The following outputs the titles of the 25 hottest submissions in
38 ``/r/all``:
39
40 .. code:: python
41
42 for submission in reddit.subreddit('all').hot(limit=25):
43 print(submission.title)
44
45 Multiple subreddits can be combined like so:
46
47 .. code:: python
48
49 for submission in reddit.subreddit('redditdev+learnpython').top('all'):
50 print(submission)
51
52 Subreddits can be filtered from combined listings as follows. Note that
53 these filters are ignored by certain methods, including
54 :attr:`~praw.models.Subreddit.comments`,
55 :meth:`~praw.models.Subreddit.gilded`, and
56 :meth:`.SubredditStream.comments`.
57
58 .. code:: python
59
60 for submission in reddit.subreddit('all-redditdev').new():
61 print(submission)
62
63 **Typical Attributes**
64
65 This table describes attributes that typically belong to objects of this
66 class. Since attributes are dynamically provided (see
67 :ref:`determine-available-attributes-of-an-object`), there is not a
68 guarantee that these attributes will always be present, nor is this list
69 comprehensive in any way.
70
71 ========================== ===============================================
72 Attribute Description
73 ========================== ===============================================
74 ``can_assign_link_flair`` Whether users can assign their own link flair.
75 ``can_assign_user_flair`` Whether users can assign their own user flair.
76 ``created_utc`` Time the subreddit was created, represented in
77 `Unix Time`_.
78 ``description`` Subreddit description, in Markdown.
79 ``description_html`` Subreddit description, in HTML.
80 ``display_name`` Name of the subreddit.
81 ``id`` ID of the subreddit.
82 ``name`` Fullname of the subreddit.
83 ``over18`` Whether the subreddit is NSFW.
84 ``public_description`` Description of the subreddit, shown in searches
85 and on the "You must be invited to visit this
86 community" page (if applicable).
87 ``spoilers_enabled`` Whether the spoiler tag feature is enabled.
88 ``subscribers`` Count of subscribers.
89 ``user_is_banned`` Whether the authenticated user is banned.
90 ``user_is_moderator`` Whether the authenticated user is a moderator.
91 ``user_is_subscriber`` Whether the authenticated user is subscribed.
92 ========================== ===============================================
93
94
95 .. _Unix Time: https://en.wikipedia.org/wiki/Unix_time
96
97 """
98
99 # pylint: disable=too-many-public-methods
100
101 STR_FIELD = "display_name"
102 MESSAGE_PREFIX = "#"
103
104 @staticmethod
105 def _create_or_update(
106 _reddit,
107 allow_images=None,
108 allow_post_crossposts=None,
109 allow_top=None,
110 collapse_deleted_comments=None,
111 comment_score_hide_mins=None,
112 description=None,
113 domain=None,
114 exclude_banned_modqueue=None,
115 header_hover_text=None,
116 hide_ads=None,
117 lang=None,
118 key_color=None,
119 link_type=None,
120 name=None,
121 over_18=None,
122 public_description=None,
123 public_traffic=None,
124 show_media=None,
125 show_media_preview=None,
126 spam_comments=None,
127 spam_links=None,
128 spam_selfposts=None,
129 spoilers_enabled=None,
130 sr=None,
131 submit_link_label=None,
132 submit_text=None,
133 submit_text_label=None,
134 subreddit_type=None,
135 suggested_comment_sort=None,
136 title=None,
137 wiki_edit_age=None,
138 wiki_edit_karma=None,
139 wikimode=None,
140 **other_settings
141 ):
142 # pylint: disable=invalid-name,too-many-locals,too-many-arguments
143 model = {
144 "allow_images": allow_images,
145 "allow_post_crossposts": allow_post_crossposts,
146 "allow_top": allow_top,
147 "collapse_deleted_comments": collapse_deleted_comments,
148 "comment_score_hide_mins": comment_score_hide_mins,
149 "description": description,
150 "domain": domain,
151 "exclude_banned_modqueue": exclude_banned_modqueue,
152 "header-title": header_hover_text, # Remap here - better name
153 "hide_ads": hide_ads,
154 "key_color": key_color,
155 "lang": lang,
156 "link_type": link_type,
157 "name": name,
158 "over_18": over_18,
159 "public_description": public_description,
160 "public_traffic": public_traffic,
161 "show_media": show_media,
162 "show_media_preview": show_media_preview,
163 "spam_comments": spam_comments,
164 "spam_links": spam_links,
165 "spam_selfposts": spam_selfposts,
166 "spoilers_enabled": spoilers_enabled,
167 "sr": sr,
168 "submit_link_label": submit_link_label,
169 "submit_text": submit_text,
170 "submit_text_label": submit_text_label,
171 "suggested_comment_sort": suggested_comment_sort,
172 "title": title,
173 "type": subreddit_type,
174 "wiki_edit_age": wiki_edit_age,
175 "wiki_edit_karma": wiki_edit_karma,
176 "wikimode": wikimode,
177 }
178
179 model.update(other_settings)
180
181 _reddit.post(API_PATH["site_admin"], data=model)
182
183 @staticmethod
184 def _subreddit_list(subreddit, other_subreddits):
185 if other_subreddits:
186 return ",".join(
187 [str(subreddit)] + [str(x) for x in other_subreddits]
188 )
189 return str(subreddit)
190
191 @property
192 def _kind(self):
193 """Return the class's kind."""
194 return self._reddit.config.kinds["subreddit"]
195
196 @cachedproperty
197 def banned(self):
198 """Provide an instance of :class:`.SubredditRelationship`.
199
200 For example to ban a user try:
201
202 .. code-block:: python
203
204 reddit.subreddit('SUBREDDIT').banned.add('NAME', ban_reason='...')
205
206 To list the banned users along with any notes, try:
207
208 .. code-block:: python
209
210 for ban in reddit.subreddit('SUBREDDIT').banned():
211 print('{}: {}'.format(ban, ban.note))
212
213 """
214 return SubredditRelationship(self, "banned")
215
216 @cachedproperty
217 def collections(self):
218 r"""Provide an instance of :class:`.SubredditCollections`.
219
220 To see the permalinks of all :class:`.Collection`\ s that belong to
221 a subreddit, try:
222
223 .. code-block:: python
224
225 for collection in reddit.subreddit('SUBREDDIT').collections:
226 print(collection.permalink)
227
228 To get a specific :class:`.Collection` by its UUID or permalink,
229 use one of the following:
230
231 .. code-block:: python
232
233 collection = reddit.subreddit('SUBREDDIT').collections('some_uuid')
234 collection = reddit.subreddit('SUBREDDIT').collections(
235 permalink='https://reddit.com/r/SUBREDDIT/collection/some_uuid')
236
237 """
238 return self._subreddit_collections_class(self._reddit, self)
239
240 @cachedproperty
241 def contributor(self):
242 """Provide an instance of :class:`.ContributorRelationship`.
243
244 Contributors are also known as approved submitters.
245
246 To add a contributor try:
247
248 .. code-block:: python
249
250 reddit.subreddit('SUBREDDIT').contributor.add('NAME')
251
252 """
253 return ContributorRelationship(self, "contributor")
254
255 @cachedproperty
256 def emoji(self):
257 """Provide an instance of :class:`.SubredditEmoji`.
258
259 This attribute can be used to discover all emoji for a subreddit:
260
261 .. code:: python
262
263 for emoji in reddit.subreddit('iama').emoji:
264 print(emoji)
265
266 A single emoji can be lazily retrieved via:
267
268 .. code:: python
269
270 reddit.subreddit('blah').emoji['emoji_name']
271
272 .. note:: Attempting to access attributes of an nonexistent emoji will
273 result in a :class:`.ClientException`.
274
275 """
276 return SubredditEmoji(self)
277
278 @cachedproperty
279 def filters(self):
280 """Provide an instance of :class:`.SubredditFilters`."""
281 return SubredditFilters(self)
282
283 @cachedproperty
284 def flair(self):
285 """Provide an instance of :class:`.SubredditFlair`.
286
287 Use this attribute for interacting with a subreddit's flair. For
288 example to list all the flair for a subreddit which you have the
289 ``flair`` moderator permission on try:
290
291 .. code-block:: python
292
293 for flair in reddit.subreddit('NAME').flair():
294 print(flair)
295
296 Flair templates can be interacted with through this attribute via:
297
298 .. code-block:: python
299
300 for template in reddit.subreddit('NAME').flair.templates:
301 print(template)
302
303 """
304 return SubredditFlair(self)
305
306 @cachedproperty
307 def mod(self):
308 """Provide an instance of :class:`.SubredditModeration`."""
309 return SubredditModeration(self)
310
311 @cachedproperty
312 def moderator(self):
313 """Provide an instance of :class:`.ModeratorRelationship`.
314
315 For example to add a moderator try:
316
317 .. code-block:: python
318
319 reddit.subreddit('SUBREDDIT').moderator.add('NAME')
320
321 To list the moderators along with their permissions try:
322
323 .. code-block:: python
324
325 for moderator in reddit.subreddit('SUBREDDIT').moderator():
326 print('{}: {}'.format(moderator, moderator.mod_permissions))
327
328 """
329 return ModeratorRelationship(self, "moderator")
330
331 @cachedproperty
332 def modmail(self):
333 """Provide an instance of :class:`.Modmail`."""
334 return Modmail(self)
335
336 @cachedproperty
337 def muted(self):
338 """Provide an instance of :class:`.SubredditRelationship`."""
339 return SubredditRelationship(self, "muted")
340
341 @cachedproperty
342 def quaran(self):
343 """Provide an instance of :class:`.SubredditQuarantine`.
344
345 This property is named ``quaran`` because ``quarantine`` is a
346 Subreddit attribute returned by Reddit to indicate whether or not a
347 Subreddit is quarantined.
348
349 """
350 return SubredditQuarantine(self)
351
352 @cachedproperty
353 def stream(self):
354 """Provide an instance of :class:`.SubredditStream`.
355
356 Streams can be used to indefinitely retrieve new comments made to a
357 subreddit, like:
358
359 .. code:: python
360
361 for comment in reddit.subreddit('iama').stream.comments():
362 print(comment)
363
364 Additionally, new submissions can be retrieved via the stream. In the
365 following example all submissions are fetched via the special subreddit
366 ``all``:
367
368 .. code:: python
369
370 for submission in reddit.subreddit('all').stream.submissions():
371 print(submission)
372
373 """
374 return SubredditStream(self)
375
376 @cachedproperty
377 def stylesheet(self):
378 """Provide an instance of :class:`.SubredditStylesheet`."""
379 return SubredditStylesheet(self)
380
381 @cachedproperty
382 def widgets(self):
383 """Provide an instance of :class:`.SubredditWidgets`.
384
385 **Example usage**
386
387 Get all sidebar widgets:
388
389 .. code-block:: python
390
391 for widget in reddit.subreddit('redditdev').widgets.sidebar:
392 print(widget)
393
394 Get ID card widget:
395
396 .. code-block:: python
397
398 print(reddit.subreddit('redditdev').widgets.id_card)
399
400 """
401 return SubredditWidgets(self)
402
403 @cachedproperty
404 def wiki(self):
405 """Provide an instance of :class:`.SubredditWiki`.
406
407 This attribute can be used to discover all wikipages for a subreddit:
408
409 .. code:: python
410
411 for wikipage in reddit.subreddit('iama').wiki:
412 print(wikipage)
413
414 To fetch the content for a given wikipage try:
415
416 .. code:: python
417
418 wikipage = reddit.subreddit('iama').wiki['proof']
419 print(wikipage.content_md)
420
421 """
422 return SubredditWiki(self)
423
424 def __init__(self, reddit, display_name=None, _data=None):
425 """Initialize a Subreddit instance.
426
427 :param reddit: An instance of :class:`~.Reddit`.
428 :param display_name: The name of the subreddit.
429
430 .. note:: This class should not be initialized directly. Instead obtain
431 an instance via: ``reddit.subreddit('subreddit_name')``
432
433 """
434 if bool(display_name) == bool(_data):
435 raise TypeError(
436 "Either `display_name` or `_data` must be provided."
437 )
438 super(Subreddit, self).__init__(reddit, _data=_data)
439 if display_name:
440 self.display_name = display_name
441 self._path = API_PATH["subreddit"].format(subreddit=self)
442
443 def _fetch_info(self):
444 return ("subreddit_about", {"subreddit": self}, None)
445
446 def _fetch_data(self):
447 name, fields, params = self._fetch_info()
448 path = API_PATH[name].format(**fields)
449 return self._reddit.request("GET", path, params)
450
451 def _fetch(self):
452 data = self._fetch_data()
453 data = data["data"]
454 other = type(self)(self._reddit, _data=data)
455 self.__dict__.update(other.__dict__)
456 self._fetched = True
457
458 def _submit_media(self, data, timeout, without_websockets):
459 """Submit and return an `image`, `video`, or `videogif`.
460
461 This is a helper method for submitting posts that are not link posts or
462 self posts.
463 """
464 response = self._reddit.post(API_PATH["submit"], data=data)
465
466 # About the websockets:
467 #
468 # Reddit responds to this request with only two fields: a link to
469 # the user's /submitted page, and a websocket URL. We can use the
470 # websocket URL to get a link to the new post once it is created.
471 #
472 # An important note to PRAW contributors or anyone who would
473 # wish to step through this section with a debugger: This block
474 # of code is NOT debugger-friendly. If there is *any*
475 # significant time between the POST request just above this
476 # comment and the creation of the websocket connection just
477 # below, the code will become stuck in an infinite loop at the
478 # socket.recv() call. I believe this is because only one message is
479 # sent over the websocket, and if the client doesn't connect
480 # soon enough, it will miss the message and get stuck forever
481 # waiting for another.
482 #
483 # So if you need to debug this section of code, please let the
484 # websocket creation happen right after the POST request,
485 # otherwise you will have trouble.
486
487 if not isinstance(response, dict):
488 raise ClientException(
489 "Something went wrong with your post: {!r}".format(response)
490 )
491
492 if without_websockets:
493 return
494
495 try:
496 socket = websocket.create_connection(
497 response["json"]["data"]["websocket_url"], timeout=timeout
498 )
499 ws_update = loads(socket.recv())
500 socket.close()
501 except websocket.WebSocketTimeoutException:
502 raise ClientException(
503 "Websocket error. Check your media file. "
504 "Your post may still have been created."
505 )
506 url = ws_update["payload"]["redirect"]
507 return self._reddit.submission(url=url)
508
509 def _upload_media(self, media_path):
510 """Upload media and return its URL. Uses undocumented endpoint."""
511 if media_path is None:
512 media_path = join(
513 dirname(dirname(dirname(__file__))), "images", "PRAW logo.png"
514 )
515
516 file_name = basename(media_path).lower()
517 mime_type = {
518 "png": "image/png",
519 "mov": "video/quicktime",
520 "mp4": "video/mp4",
521 "jpg": "image/jpeg",
522 "jpeg": "image/jpeg",
523 "gif": "image/gif",
524 }.get(
525 file_name.rpartition(".")[2], "image/jpeg"
526 ) # default to JPEG
527 img_data = {"filepath": file_name, "mimetype": mime_type}
528
529 url = API_PATH["media_asset"]
530 # until we learn otherwise, assume this request always succeeds
531 upload_lease = self._reddit.post(url, data=img_data)["args"]
532 upload_url = "https:{}".format(upload_lease["action"])
533 upload_data = {
534 item["name"]: item["value"] for item in upload_lease["fields"]
535 }
536
537 with open(media_path, "rb") as media:
538 response = self._reddit._core._requestor._http.post(
539 upload_url, data=upload_data, files={"file": media}
540 )
541 response.raise_for_status()
542
543 return upload_url + "/" + upload_data["key"]
544
545 def random(self):
546 """Return a random Submission.
547
548 Returns ``None`` on subreddits that do not support the random feature.
549 One example, at the time of writing, is /r/wallpapers.
550 """
551 url = API_PATH["subreddit_random"].format(subreddit=self)
552 try:
553 self._reddit.get(url, params={"unique": self._reddit._next_unique})
554 except Redirect as redirect:
555 path = redirect.path
556 try:
557 return self._submission_class(
558 self._reddit, url=urljoin(self._reddit.config.reddit_url, path)
559 )
560 except ClientException:
561 return None
562
563 def rules(self):
564 """Return rules for the subreddit.
565
566 For example to show the rules of ``/r/redditdev`` try:
567
568 .. code:: python
569
570 reddit.subreddit('redditdev').rules()
571
572 """
573 return self._reddit.get(API_PATH["rules"].format(subreddit=self))
574
575 def search(
576 self,
577 query,
578 sort="relevance",
579 syntax="lucene",
580 time_filter="all",
581 **generator_kwargs
582 ):
583 """Return a ListingGenerator for items that match ``query``.
584
585 :param query: The query string to search for.
586 :param sort: Can be one of: relevance, hot, top, new,
587 comments. (default: relevance).
588 :param syntax: Can be one of: cloudsearch, lucene, plain
589 (default: lucene).
590 :param time_filter: Can be one of: all, day, hour, month, week, year
591 (default: all).
592
593 For more information on building a search query see:
594 https://www.reddit.com/wiki/search
595
596 For example to search all subreddits for ``praw`` try:
597
598 .. code:: python
599
600 for submission in reddit.subreddit('all').search('praw'):
601 print(submission.title)
602
603 """
604 self._validate_time_filter(time_filter)
605 not_all = self.display_name.lower() != "all"
606 self._safely_add_arguments(
607 generator_kwargs,
608 "params",
609 q=query,
610 restrict_sr=not_all,
611 sort=sort,
612 syntax=syntax,
613 t=time_filter,
614 )
615 url = API_PATH["search"].format(subreddit=self)
616 return ListingGenerator(self._reddit, url, **generator_kwargs)
617
618 def sticky(self, number=1):
619 """Return a Submission object for a sticky of the subreddit.
620
621 :param number: Specify which sticky to return. 1 appears at the top
622 (default: 1).
623
624 Raises ``prawcore.NotFound`` if the sticky does not exist.
625
626 """
627 url = API_PATH["about_sticky"].format(subreddit=self)
628 try:
629 self._reddit.get(url, params={"num": number})
630 except Redirect as redirect:
631 path = redirect.path
632 return self._submission_class(
633 self._reddit, url=urljoin(self._reddit.config.reddit_url, path)
634 )
635
636 def submit(
637 self,
638 title,
639 selftext=None,
640 url=None,
641 flair_id=None,
642 flair_text=None,
643 resubmit=True,
644 send_replies=True,
645 nsfw=False,
646 spoiler=False,
647 collection_id=None,
648 ):
649 """Add a submission to the subreddit.
650
651 :param title: The title of the submission.
652 :param selftext: The markdown formatted content for a ``text``
653 submission. Use an empty string, ``''``, to make a title-only
654 submission.
655 :param url: The URL for a ``link`` submission.
656 :param collection_id: The UUID of a :class:`.Collection` to add the
657 newly-submitted post to.
658 :param flair_id: The flair template to select (default: None).
659 :param flair_text: If the template's ``flair_text_editable`` value is
660 True, this value will set a custom text (default: None).
661 :param resubmit: When False, an error will occur if the URL has already
662 been submitted (default: True).
663 :param send_replies: When True, messages will be sent to the submission
664 author when comments are made to the submission (default: True).
665 :param nsfw: Whether or not the submission should be marked NSFW
666 (default: False).
667 :param spoiler: Whether or not the submission should be marked as
668 a spoiler (default: False).
669 :returns: A :class:`~.Submission` object for the newly created
670 submission.
671
672 Either ``selftext`` or ``url`` can be provided, but not both.
673
674 For example to submit a URL to ``/r/reddit_api_test`` do:
675
676 .. code:: python
677
678 title = 'PRAW documentation'
679 url = 'https://praw.readthedocs.io'
680 reddit.subreddit('reddit_api_test').submit(title, url=url)
681
682 .. note ::
683
684 For submitting images, videos, and videogifs,
685 see :meth:`.submit_image` and :meth:`.submit_video`.
686
687 """
688 if (bool(selftext) or selftext == "") == bool(url):
689 raise TypeError("Either `selftext` or `url` must be provided.")
690
691 data = {
692 "sr": str(self),
693 "resubmit": bool(resubmit),
694 "sendreplies": bool(send_replies),
695 "title": title,
696 "nsfw": bool(nsfw),
697 "spoiler": bool(spoiler),
698 }
699 for key, value in (
700 ("flair_id", flair_id),
701 ("flair_text", flair_text),
702 ("collection_id", collection_id),
703 ):
704 if value is not None:
705 data[key] = value
706 if selftext is not None:
707 data.update(kind="self", text=selftext)
708 else:
709 data.update(kind="link", url=url)
710
711 return self._reddit.post(API_PATH["submit"], data=data)
712
713 def submit_image(
714 self,
715 title,
716 image_path,
717 flair_id=None,
718 flair_text=None,
719 resubmit=True,
720 send_replies=True,
721 nsfw=False,
722 spoiler=False,
723 timeout=10,
724 collection_id=None,
725 without_websockets=False,
726 ):
727 """Add an image submission to the subreddit.
728
729 :param title: The title of the submission.
730 :param image_path: The path to an image, to upload and post.
731 :param collection_id: The UUID of a :class:`.Collection` to add the
732 newly-submitted post to.
733 :param flair_id: The flair template to select (default: None).
734 :param flair_text: If the template's ``flair_text_editable`` value is
735 True, this value will set a custom text (default: None).
736 :param resubmit: When False, an error will occur if the URL has already
737 been submitted (default: True).
738 :param send_replies: When True, messages will be sent to the submission
739 author when comments are made to the submission (default: True).
740 :param nsfw: Whether or not the submission should be marked NSFW
741 (default: False).
742 :param spoiler: Whether or not the submission should be marked as
743 a spoiler (default: False).
744 :param timeout: Specifies a particular timeout, in seconds. Use to
745 avoid "Websocket error" exceptions (default: 10).
746 :param without_websockets: Set to ``True`` to disable use of WebSockets
747 (see note below for an explanation). If ``True``, this method
748 doesn't return anything. (default: ``False``).
749
750 :returns: A :class:`.Submission` object for the newly created
751 submission, unless ``without_websockets`` is ``True``.
752
753 .. note::
754
755 Reddit's API uses WebSockets to respond with the link of the
756 newly created post. If this fails, the method will raise
757 :class:`.ClientException`. Occasionally, the Reddit post will still
758 be created. More often, there is an error with the image file. If
759 you frequently get exceptions but successfully created posts, try
760 setting the ``timeout`` parameter to a value above 10.
761
762 To disable the use of WebSockets, set ``without_websockets=True``.
763 This will make the method return ``None``, though the post will
764 still be created. You may wish to do this if you are running your
765 program in a restricted network environment, or using a proxy
766 that doesn't support WebSockets connections.
767
768 For example to submit an image to ``/r/reddit_api_test`` do:
769
770 .. code:: python
771
772 title = 'My favorite picture'
773 image = '/path/to/image.png'
774 reddit.subreddit('reddit_api_test').submit_image(title, image)
775
776 """
777 data = {
778 "sr": str(self),
779 "resubmit": bool(resubmit),
780 "sendreplies": bool(send_replies),
781 "title": title,
782 "nsfw": bool(nsfw),
783 "spoiler": bool(spoiler),
784 }
785 for key, value in (
786 ("flair_id", flair_id),
787 ("flair_text", flair_text),
788 ("collection_id", collection_id),
789 ):
790 if value is not None:
791 data[key] = value
792 data.update(kind="image", url=self._upload_media(image_path))
793 return self._submit_media(
794 data, timeout, without_websockets=without_websockets
795 )
796
797 def submit_video(
798 self,
799 title,
800 video_path,
801 videogif=False,
802 thumbnail_path=None,
803 flair_id=None,
804 flair_text=None,
805 resubmit=True,
806 send_replies=True,
807 nsfw=False,
808 spoiler=False,
809 timeout=10,
810 collection_id=None,
811 without_websockets=False,
812 ):
813 """Add a video or videogif submission to the subreddit.
814
815 :param title: The title of the submission.
816 :param video_path: The path to a video, to upload and post.
817 :param videogif: A ``bool`` value. If ``True``, the video is
818 uploaded as a videogif, which is essentially a silent video
819 (default: ``False``).
820 :param thumbnail_path: (Optional) The path to an image, to be uploaded
821 and used as the thumbnail for this video. If not provided, the
822 PRAW logo will be used as the thumbnail.
823 :param collection_id: The UUID of a :class:`.Collection` to add the
824 newly-submitted post to.
825 :param flair_id: The flair template to select (default: ``None``).
826 :param flair_text: If the template's ``flair_text_editable`` value is
827 True, this value will set a custom text (default: ``None``).
828 :param resubmit: When False, an error will occur if the URL has already
829 been submitted (default: ``True``).
830 :param send_replies: When True, messages will be sent to the submission
831 author when comments are made to the submission
832 (default: ``True``).
833 :param nsfw: Whether or not the submission should be marked NSFW
834 (default: False).
835 :param spoiler: Whether or not the submission should be marked as
836 a spoiler (default: False).
837 :param timeout: Specifies a particular timeout, in seconds. Use to
838 avoid "Websocket error" exceptions (default: 10).
839 :param without_websockets: Set to ``True`` to disable use of WebSockets
840 (see note below for an explanation). If ``True``, this method
841 doesn't return anything. (default: ``False``).
842
843 :returns: A :class:`.Submission` object for the newly created
844 submission, unless ``without_websockets`` is ``True``.
845
846 .. note::
847
848 Reddit's API uses WebSockets to respond with the link of the
849 newly created post. If this fails, the method will raise
850 :class:`.ClientException`. Occasionally, the Reddit post will still
851 be created. More often, there is an error with the video file. If
852 you frequently get exceptions but successfully created posts, try
853 setting the ``timeout`` parameter to a value above 10.
854
855 To disable the use of WebSockets, set ``without_websockets=True``.
856 This will make the method return ``None``, though the post will
857 still be created. You may wish to do this if you are running your
858 program in a restricted network environment, or using a proxy
859 that doesn't support WebSockets connections.
860
861 For example to submit a video to ``/r/reddit_api_test`` do:
862
863 .. code:: python
864
865 title = 'My favorite movie'
866 video = '/path/to/video.mp4'
867 reddit.subreddit('reddit_api_test').submit_video(title, video)
868
869 """
870 data = {
871 "sr": str(self),
872 "resubmit": bool(resubmit),
873 "sendreplies": bool(send_replies),
874 "title": title,
875 "nsfw": bool(nsfw),
876 "spoiler": bool(spoiler),
877 }
878 for key, value in (
879 ("flair_id", flair_id),
880 ("flair_text", flair_text),
881 ("collection_id", collection_id),
882 ):
883 if value is not None:
884 data[key] = value
885 data.update(
886 kind="videogif" if videogif else "video",
887 url=self._upload_media(video_path),
888 # if thumbnail_path is None, it uploads the PRAW logo
889 video_poster_url=self._upload_media(thumbnail_path),
890 )
891 return self._submit_media(
892 data, timeout, without_websockets=without_websockets
893 )
894
895 def subscribe(self, other_subreddits=None):
896 """Subscribe to the subreddit.
897
898 :param other_subreddits: When provided, also subscribe to the provided
899 list of subreddits.
900
901 """
902 data = {
903 "action": "sub",
904 "skip_inital_defaults": True,
905 "sr_name": self._subreddit_list(self, other_subreddits),
906 }
907 self._reddit.post(API_PATH["subscribe"], data=data)
908
909 def traffic(self):
910 """Return a dictionary of the subreddit's traffic statistics.
911
912 Raises ``prawcore.NotFound`` when the traffic stats aren't available to
913 the authenticated user, that is, they are not public and the
914 authenticated user is not a moderator of the subreddit.
915
916 """
917 return self._reddit.get(
918 API_PATH["about_traffic"].format(subreddit=self)
919 )
920
921 def unsubscribe(self, other_subreddits=None):
922 """Unsubscribe from the subreddit.
923
924 :param other_subreddits: When provided, also unsubscribe to the
925 provided list of subreddits.
926
927 """
928 data = {
929 "action": "unsub",
930 "sr_name": self._subreddit_list(self, other_subreddits),
931 }
932 self._reddit.post(API_PATH["subscribe"], data=data)
933
934
935WidgetEncoder._subreddit_class = Subreddit
936
937
938class SubredditFilters:
939 """Provide functions to interact with the special Subreddit's filters.
940
941 Members of this class should be utilized via ``Subreddit.filters``. For
942 example to add a filter run:
943
944 .. code:: python
945
946 reddit.subreddit('all').filters.add('subreddit_name')
947
948 """
949
950 def __init__(self, subreddit):
951 """Create a SubredditFilters instance.
952
953 :param subreddit: The special subreddit whose filters to work with.
954
955 As of this writing filters can only be used with the special subreddits
956 ``all`` and ``mod``.
957
958 """
959 self.subreddit = subreddit
960
961 def __iter__(self):
962 """Iterate through the special subreddit's filters.
963
964 This method should be invoked as:
965
966 .. code:: python
967
968 for subreddit in reddit.subreddit('NAME').filters:
969 ...
970
971 """
972 url = API_PATH["subreddit_filter_list"].format(
973 special=self.subreddit, user=self.subreddit._reddit.user.me()
974 )
975 params = {"unique": self.subreddit._reddit._next_unique}
976 response_data = self.subreddit._reddit.get(url, params=params)
977 for subreddit in response_data.subreddits:
978 yield subreddit
979
980 def add(self, subreddit):
981 """Add ``subreddit`` to the list of filtered subreddits.
982
983 :param subreddit: The subreddit to add to the filter list.
984
985 Items from subreddits added to the filtered list will no longer be
986 included when obtaining listings for ``/r/all``.
987
988 Alternatively, you can filter a subreddit temporarily from a special
989 listing in a manner like so:
990
991 .. code:: python
992
993 reddit.subreddit('all-redditdev-learnpython')
994
995 Raises ``prawcore.NotFound`` when calling on a non-special subreddit.
996
997 """
998 url = API_PATH["subreddit_filter"].format(
999 special=self.subreddit,
1000 user=self.subreddit._reddit.user.me(),
1001 subreddit=subreddit,
1002 )
1003 self.subreddit._reddit.request(
1004 "PUT", url, data={"model": dumps({"name": str(subreddit)})}
1005 )
1006
1007 def remove(self, subreddit):
1008 """Remove ``subreddit`` from the list of filtered subreddits.
1009
1010 :param subreddit: The subreddit to remove from the filter list.
1011
1012 Raises ``prawcore.NotFound`` when calling on a non-special subreddit.
1013
1014 """
1015 url = API_PATH["subreddit_filter"].format(
1016 special=self.subreddit,
1017 user=self.subreddit._reddit.user.me(),
1018 subreddit=str(subreddit),
1019 )
1020 self.subreddit._reddit.request("DELETE", url, data={})
1021
1022
1023class SubredditFlair:
1024 """Provide a set of functions to interact with a Subreddit's flair."""
1025
1026 @cachedproperty
1027 def link_templates(self):
1028 """Provide an instance of :class:`.SubredditLinkFlairTemplates`.
1029
1030 Use this attribute for interacting with a subreddit's link flair
1031 templates. For example to list all the link flair templates for a
1032 subreddit which you have the ``flair`` moderator permission on try:
1033
1034 .. code-block:: python
1035
1036 for template in reddit.subreddit('NAME').flair.link_templates:
1037 print(template)
1038
1039 """
1040 return SubredditLinkFlairTemplates(self.subreddit)
1041
1042 @cachedproperty
1043 def templates(self):
1044 """Provide an instance of :class:`.SubredditRedditorFlairTemplates`.
1045
1046 Use this attribute for interacting with a subreddit's flair
1047 templates. For example to list all the flair templates for a subreddit
1048 which you have the ``flair`` moderator permission on try:
1049
1050 .. code-block:: python
1051
1052 for template in reddit.subreddit('NAME').flair.templates:
1053 print(template)
1054
1055 """
1056 return SubredditRedditorFlairTemplates(self.subreddit)
1057
1058 def __call__(self, redditor=None, **generator_kwargs):
1059 """Return a generator for Redditors and their associated flair.
1060
1061 :param redditor: When provided, yield at most a single
1062 :class:`~.Redditor` instance (default: None).
1063
1064 This method is intended to be used like:
1065
1066 .. code-block:: python
1067
1068 for flair in reddit.subreddit('NAME').flair(limit=None):
1069 print(flair)
1070
1071 """
1072 Subreddit._safely_add_arguments(
1073 generator_kwargs, "params", name=redditor
1074 )
1075 generator_kwargs.setdefault("limit", None)
1076 url = API_PATH["flairlist"].format(subreddit=self.subreddit)
1077 return ListingGenerator(
1078 self.subreddit._reddit, url, **generator_kwargs
1079 )
1080
1081 def __init__(self, subreddit):
1082 """Create a SubredditFlair instance.
1083
1084 :param subreddit: The subreddit whose flair to work with.
1085
1086 """
1087 self.subreddit = subreddit
1088
1089 def configure(
1090 self,
1091 position="right",
1092 self_assign=False,
1093 link_position="left",
1094 link_self_assign=False,
1095 **settings
1096 ):
1097 """Update the subreddit's flair configuration.
1098
1099 :param position: One of left, right, or False to disable (default:
1100 right).
1101 :param self_assign: (boolean) Permit self assignment of user flair
1102 (default: False).
1103 :param link_position: One of left, right, or False to disable
1104 (default: left).
1105 :param link_self_assign: (boolean) Permit self assignment
1106 of link flair (default: False).
1107
1108 Additional keyword arguments can be provided to handle new settings as
1109 Reddit introduces them.
1110
1111 """
1112 data = {
1113 "flair_enabled": bool(position),
1114 "flair_position": position or "right",
1115 "flair_self_assign_enabled": self_assign,
1116 "link_flair_position": link_position or "",
1117 "link_flair_self_assign_enabled": link_self_assign,
1118 }
1119 data.update(settings)
1120 url = API_PATH["flairconfig"].format(subreddit=self.subreddit)
1121 self.subreddit._reddit.post(url, data=data)
1122
1123 def delete(self, redditor):
1124 """Delete flair for a Redditor.
1125
1126 :param redditor: A redditor name (e.g., ``'spez'``) or
1127 :class:`~.Redditor` instance.
1128
1129 .. note:: To delete the flair of many Redditors at once, please see
1130 :meth:`~praw.models.reddit.subreddit.SubredditFlair.update`.
1131
1132 """
1133 url = API_PATH["deleteflair"].format(subreddit=self.subreddit)
1134 self.subreddit._reddit.post(url, data={"name": str(redditor)})
1135
1136 def delete_all(self):
1137 """Delete all Redditor flair in the Subreddit.
1138
1139 :returns: List of dictionaries indicating the success or failure of
1140 each delete.
1141
1142 """
1143 return self.update(x["user"] for x in self())
1144
1145 def set(
1146 self, redditor=None, text="", css_class="", flair_template_id=None
1147 ):
1148 """Set flair for a Redditor.
1149
1150 :param redditor: (Required) A redditor name (e.g., ``'spez'``) or
1151 :class:`~.Redditor` instance.
1152 :param text: The flair text to associate with the Redditor or
1153 Submission (default: '').
1154 :param css_class: The css class to associate with the flair html
1155 (default: ''). Use either this or ``flair_template_id``.
1156 :param flair_template_id: The ID of the flair template to be used
1157 (default: ``None``). Use either this or ``css_class``.
1158
1159 This method can only be used by an authenticated user who is a
1160 moderator of the associated Subreddit.
1161
1162 For example:
1163
1164 .. code:: python
1165
1166 reddit.subreddit('redditdev').flair.set('bboe', 'PRAW author',
1167 css_class='mods')
1168 template = '6bd28436-1aa7-11e9-9902-0e05ab0fad46'
1169 reddit.subreddit('redditdev').flair.set('spez', 'Reddit CEO',
1170 flair_template_id=template)
1171
1172 """
1173 if css_class and flair_template_id is not None:
1174 raise TypeError(
1175 "Parameter `css_class` cannot be used in "
1176 "conjunction with `flair_template_id`."
1177 )
1178 data = {"name": str(redditor), "text": text}
1179 if flair_template_id is not None:
1180 data["flair_template_id"] = flair_template_id
1181 url = API_PATH["select_flair"].format(subreddit=self.subreddit)
1182 else:
1183 data["css_class"] = css_class
1184 url = API_PATH["flair"].format(subreddit=self.subreddit)
1185 self.subreddit._reddit.post(url, data=data)
1186
1187 def update(self, flair_list, text="", css_class=""):
1188 """Set or clear the flair for many Redditors at once.
1189
1190 :param flair_list: Each item in this list should be either: the name of
1191 a Redditor, an instance of :class:`.Redditor`, or a dictionary
1192 mapping keys ``user``, ``flair_text``, and ``flair_css_class`` to
1193 their respective values. The ``user`` key should map to a Redditor,
1194 as described above. When a dictionary isn't provided, or the
1195 dictionary is missing one of ``flair_text``, or ``flair_css_class``
1196 attributes the default values will come from the the following
1197 arguments.
1198
1199 :param text: The flair text to use when not explicitly provided in
1200 ``flair_list`` (default: '').
1201 :param css_class: The css class to use when not explicitly provided in
1202 ``flair_list`` (default: '').
1203 :returns: List of dictionaries indicating the success or failure of
1204 each update.
1205
1206 For example to clear the flair text, and set the ``praw`` flair css
1207 class on a few users try:
1208
1209 .. code:: python
1210
1211 subreddit.flair.update(['bboe', 'spez', 'spladug'],
1212 css_class='praw')
1213
1214 """
1215 lines = []
1216 for item in flair_list:
1217 if isinstance(item, dict):
1218 fmt_data = (
1219 str(item["user"]),
1220 item.get("flair_text", text),
1221 item.get("flair_css_class", css_class),
1222 )
1223 else:
1224 fmt_data = (str(item), text, css_class)
1225 lines.append('"{}","{}","{}"'.format(*fmt_data))
1226
1227 response = []
1228 url = API_PATH["flaircsv"].format(subreddit=self.subreddit)
1229 while lines:
1230 data = {"flair_csv": "\n".join(lines[:100])}
1231 response.extend(self.subreddit._reddit.post(url, data=data))
1232 lines = lines[100:]
1233 return response
1234
1235
1236class SubredditFlairTemplates:
1237 """Provide functions to interact with a Subreddit's flair templates."""
1238
1239 @staticmethod
1240 def flair_type(is_link):
1241 """Return LINK_FLAIR or USER_FLAIR depending on ``is_link`` value."""
1242 return "LINK_FLAIR" if is_link else "USER_FLAIR"
1243
1244 def __init__(self, subreddit):
1245 """Create a SubredditFlairTemplate instance.
1246
1247 :param subreddit: The subreddit whose flair templates to work with.
1248
1249 .. note:: This class should not be initialized directly. Instead obtain
1250 an instance via:
1251 ``reddit.subreddit('subreddit_name').flair.templates`` or
1252 ``reddit.subreddit('subreddit_name').flair.link_templates``.
1253
1254 """
1255 self.subreddit = subreddit
1256
1257 def _add(
1258 self,
1259 text,
1260 css_class="",
1261 text_editable=False,
1262 is_link=None,
1263 background_color=None,
1264 text_color=None,
1265 mod_only=None,
1266 max_emojis=10,
1267 ):
1268 url = API_PATH["flairtemplate_v2"].format(subreddit=self.subreddit)
1269 data = {
1270 "css_class": css_class,
1271 "background_color": background_color,
1272 "text_color": text_color,
1273 "flair_type": self.flair_type(is_link),
1274 "text": text,
1275 "text_editable": bool(text_editable),
1276 "mod_only": bool(mod_only),
1277 }
1278 self.subreddit._reddit.post(url, data=data)
1279
1280 def _clear(self, is_link=None):
1281 url = API_PATH["flairtemplateclear"].format(subreddit=self.subreddit)
1282 self.subreddit._reddit.post(
1283 url, data={"flair_type": self.flair_type(is_link)}
1284 )
1285
1286 def delete(self, template_id):
1287 """Remove a flair template provided by ``template_id``.
1288
1289 For example, to delete the first Redditor flair template listed, try:
1290
1291 .. code-block:: python
1292
1293 template_info = list(subreddit.flair.templates)[0]
1294 subreddit.flair.templates.delete(template_info['id'])
1295
1296 """
1297 url = API_PATH["flairtemplatedelete"].format(subreddit=self.subreddit)
1298 self.subreddit._reddit.post(
1299 url, data={"flair_template_id": template_id}
1300 )
1301
1302 def update(
1303 self,
1304 template_id,
1305 text,
1306 css_class="",
1307 text_editable=False,
1308 background_color=None,
1309 text_color=None,
1310 mod_only=None,
1311 max_emojis=10,
1312 ):
1313 """Update the flair template provided by ``template_id``.
1314
1315 :param template_id: The flair template to update. If not valid then
1316 a new flair template will be made.
1317 :param text: The flair template's new text (required).
1318 :param css_class: The flair template's new css_class (default: '').
1319 :param text_editable: (boolean) Indicate if the flair text can be
1320 modified for each Redditor that sets it (default: False).
1321 :param background_color: The flair template's new background color,
1322 as a hex color.
1323 :param text_color: The flair template's new text color, either
1324 ``'light'`` or ``'dark'``.
1325 :param mod_only: (boolean) Indicate if the flair can only be used by
1326 moderators.
1327
1328 For example to make a user flair template text_editable, try:
1329
1330 .. code-block:: python
1331
1332 template_info = list(subreddit.flair.templates)[0]
1333 subreddit.flair.templates.update(
1334 template_info['id'],
1335 template_info['flair_text'],
1336 text_editable=True)
1337
1338 .. note::
1339
1340 Any parameters not provided will be set to default values (usually
1341 ``None`` or ``False``) on Reddit's end.
1342
1343 """
1344 url = API_PATH["flairtemplate_v2"].format(subreddit=self.subreddit)
1345 data = {
1346 "flair_template_id": template_id,
1347 "text": text,
1348 "css_class": css_class,
1349 "background_color": background_color,
1350 "text_color": text_color,
1351 "text_editable": text_editable,
1352 "mod_only": mod_only,
1353 "max_emojis":max_emojis,
1354 }
1355 self.subreddit._reddit.post(url, data=data)
1356
1357
1358class SubredditRedditorFlairTemplates(SubredditFlairTemplates):
1359 """Provide functions to interact with Redditor flair templates."""
1360
1361 def __iter__(self):
1362 """Iterate through the user flair templates.
1363
1364 For example:
1365
1366 .. code-block:: python
1367
1368 for template in reddit.subreddit('NAME').flair.templates:
1369 print(template)
1370
1371
1372 """
1373 url = API_PATH["user_flair"].format(subreddit=self.subreddit)
1374 params = {"unique": self.subreddit._reddit._next_unique}
1375 for template in self.subreddit._reddit.get(url, params=params):
1376 yield template
1377
1378 def add(
1379 self,
1380 text,
1381 css_class="",
1382 text_editable=False,
1383 background_color=None,
1384 text_color=None,
1385 mod_only=None,
1386 max_emojis=10,
1387 ):
1388 """Add a Redditor flair template to the associated subreddit.
1389
1390 :param text: The flair template's text (required).
1391 :param css_class: The flair template's css_class (default: '').
1392 :param text_editable: (boolean) Indicate if the flair text can be
1393 modified for each Redditor that sets it (default: False).
1394 :param background_color: The flair template's new background color,
1395 as a hex color.
1396 :param text_color: The flair template's new text color, either
1397 ``'light'`` or ``'dark'``.
1398 :param mod_only: (boolean) Indicate if the flair can only be used by
1399 moderators.
1400
1401 For example, to add an editable Redditor flair try:
1402
1403 .. code-block:: python
1404
1405 reddit.subreddit('NAME').flair.templates.add(
1406 css_class='praw', text_editable=True)
1407
1408 """
1409 self._add(
1410 text,
1411 css_class=css_class,
1412 text_editable=text_editable,
1413 is_link=False,
1414 background_color=background_color,
1415 text_color=text_color,
1416 mod_only=mod_only,
1417 max_emojis=max_emojis,
1418 )
1419
1420 def clear(self):
1421 """Remove all Redditor flair templates from the subreddit.
1422
1423 For example:
1424
1425 .. code-block:: python
1426
1427 reddit.subreddit('NAME').flair.templates.clear()
1428
1429 """
1430 self._clear(is_link=False)
1431
1432
1433class SubredditLinkFlairTemplates(SubredditFlairTemplates):
1434 """Provide functions to interact with link flair templates."""
1435
1436 def __iter__(self):
1437 """Iterate through the link flair templates.
1438
1439 For example:
1440
1441 .. code-block:: python
1442
1443 for template in reddit.subreddit('NAME').flair.link_templates:
1444 print(template)
1445
1446
1447 """
1448 url = API_PATH["link_flair"].format(subreddit=self.subreddit)
1449 for template in self.subreddit._reddit.get(url):
1450 yield template
1451
1452 def add(
1453 self,
1454 text,
1455 css_class="",
1456 text_editable=False,
1457 background_color=None,
1458 text_color=None,
1459 mod_only=None,
1460 max_emojis=10,
1461 ):
1462 """Add a link flair template to the associated subreddit.
1463
1464 :param text: The flair template's text (required).
1465 :param css_class: The flair template's css_class (default: '').
1466 :param text_editable: (boolean) Indicate if the flair text can be
1467 modified for each Redditor that sets it (default: False).
1468 :param background_color: The flair template's new background color,
1469 as a hex color.
1470 :param text_color: The flair template's new text color, either
1471 ``'light'`` or ``'dark'``.
1472 :param mod_only: (boolean) Indicate if the flair can only be used by
1473 moderators.
1474
1475 For example, to add an editable link flair try:
1476
1477 .. code-block:: python
1478
1479 reddit.subreddit('NAME').flair.link_templates.add(
1480 css_class='praw', text_editable=True)
1481
1482 """
1483 self._add(
1484 text,
1485 css_class=css_class,
1486 text_editable=text_editable,
1487 is_link=True,
1488 background_color=background_color,
1489 text_color=text_color,
1490 mod_only=mod_only,
1491 max_emojis=max_emojis,
1492 )
1493
1494 def clear(self):
1495 """Remove all link flair templates from the subreddit.
1496
1497 For example:
1498
1499 .. code-block:: python
1500
1501 reddit.subreddit('NAME').flair.link_templates.clear()
1502
1503 """
1504 self._clear(is_link=True)
1505
1506
1507class SubredditModeration:
1508 """Provides a set of moderation functions to a Subreddit."""
1509
1510 @staticmethod
1511 def _handle_only(only, generator_kwargs):
1512 if only is not None:
1513 if only == "submissions":
1514 only = "links"
1515 RedditBase._safely_add_arguments(
1516 generator_kwargs, "params", only=only
1517 )
1518
1519 def __init__(self, subreddit):
1520 """Create a SubredditModeration instance.
1521
1522 :param subreddit: The subreddit to moderate.
1523
1524 """
1525 self.subreddit = subreddit
1526
1527 def accept_invite(self):
1528 """Accept an invitation as a moderator of the community."""
1529 url = API_PATH["accept_mod_invite"].format(subreddit=self.subreddit)
1530 self.subreddit._reddit.post(url)
1531
1532 def edited(self, only=None, **generator_kwargs):
1533 """Return a ListingGenerator for edited comments and submissions.
1534
1535 :param only: If specified, one of ``'comments'``, or ``'submissions'``
1536 to yield only results of that type.
1537
1538 Additional keyword arguments are passed in the initialization of
1539 :class:`.ListingGenerator`.
1540
1541 To print all items in the edited queue try:
1542
1543 .. code:: python
1544
1545 for item in reddit.subreddit('mod').mod.edited(limit=None):
1546 print(item)
1547
1548 """
1549 self._handle_only(only, generator_kwargs)
1550 return ListingGenerator(
1551 self.subreddit._reddit,
1552 API_PATH["about_edited"].format(subreddit=self.subreddit),
1553 **generator_kwargs
1554 )
1555
1556 def inbox(self, **generator_kwargs):
1557 """Return a ListingGenerator for moderator messages.
1558
1559 Additional keyword arguments are passed in the initialization of
1560 :class:`.ListingGenerator`.
1561
1562 See ``unread`` for unread moderator messages.
1563
1564 To print the last 5 moderator mail messages and their replies, try:
1565
1566 .. code:: python
1567
1568 for message in reddit.subreddit('mod').mod.inbox(limit=5):
1569 print("From: {}, Body: {}".format(message.author, message.body))
1570 for reply in message.replies:
1571 print("From: {}, Body: {}".format(reply.author, reply.body))
1572
1573 """
1574 return ListingGenerator(
1575 self.subreddit._reddit,
1576 API_PATH["moderator_messages"].format(subreddit=self.subreddit),
1577 **generator_kwargs
1578 )
1579
1580 def log(self, action=None, mod=None, **generator_kwargs):
1581 """Return a ListingGenerator for moderator log entries.
1582
1583 :param action: If given, only return log entries for the specified
1584 action.
1585 :param mod: If given, only return log entries for actions made by the
1586 passed in Redditor.
1587
1588 To print the moderator and subreddit of the last 5 modlog entries try:
1589
1590 .. code:: python
1591
1592 for log in reddit.subreddit('mod').mod.log(limit=5):
1593 print("Mod: {}, Subreddit: {}".format(log.mod, log.subreddit))
1594
1595 """
1596 params = {"mod": str(mod) if mod else mod, "type": action}
1597 Subreddit._safely_add_arguments(generator_kwargs, "params", **params)
1598 return ListingGenerator(
1599 self.subreddit._reddit,
1600 API_PATH["about_log"].format(subreddit=self.subreddit),
1601 **generator_kwargs
1602 )
1603
1604 def modqueue(self, only=None, **generator_kwargs):
1605 """Return a ListingGenerator for comments/submissions in the modqueue.
1606
1607 :param only: If specified, one of ``'comments'``, or ``'submissions'``
1608 to yield only results of that type.
1609
1610 Additional keyword arguments are passed in the initialization of
1611 :class:`.ListingGenerator`.
1612
1613 To print all modqueue items try:
1614
1615 .. code:: python
1616
1617 for item in reddit.subreddit('mod').mod.modqueue(limit=None):
1618 print(item)
1619
1620 """
1621 self._handle_only(only, generator_kwargs)
1622 return ListingGenerator(
1623 self.subreddit._reddit,
1624 API_PATH["about_modqueue"].format(subreddit=self.subreddit),
1625 **generator_kwargs
1626 )
1627
1628 def reports(self, only=None, **generator_kwargs):
1629 """Return a ListingGenerator for reported comments and submissions.
1630
1631 :param only: If specified, one of ``'comments'``, or ``'submissions'``
1632 to yield only results of that type.
1633
1634 Additional keyword arguments are passed in the initialization of
1635 :class:`.ListingGenerator`.
1636
1637 To print the user and mod report reasons in the report queue try:
1638
1639 .. code:: python
1640
1641 for reported_item in reddit.subreddit('mod').mod.reports():
1642 print("User Reports: {}".format(reported_item.user_reports))
1643 print("Mod Reports: {}".format(reported_item.mod_reports))
1644
1645 """
1646 self._handle_only(only, generator_kwargs)
1647 return ListingGenerator(
1648 self.subreddit._reddit,
1649 API_PATH["about_reports"].format(subreddit=self.subreddit),
1650 **generator_kwargs
1651 )
1652
1653 def settings(self):
1654 """Return a dictionary of the subreddit's current settings."""
1655 url = API_PATH["subreddit_settings"].format(subreddit=self.subreddit)
1656 return self.subreddit._reddit.get(url)["data"]
1657
1658 def spam(self, only=None, **generator_kwargs):
1659 """Return a ListingGenerator for spam comments and submissions.
1660
1661 :param only: If specified, one of ``'comments'``, or ``'submissions'``
1662 to yield only results of that type.
1663
1664 Additional keyword arguments are passed in the initialization of
1665 :class:`.ListingGenerator`.
1666
1667 To print the items in the spam queue try:
1668
1669 .. code:: python
1670
1671 for item in reddit.subreddit('mod').mod.spam():
1672 print(item)
1673
1674 """
1675 self._handle_only(only, generator_kwargs)
1676 return ListingGenerator(
1677 self.subreddit._reddit,
1678 API_PATH["about_spam"].format(subreddit=self.subreddit),
1679 **generator_kwargs
1680 )
1681
1682 def unmoderated(self, **generator_kwargs):
1683 """Return a ListingGenerator for unmoderated submissions.
1684
1685 Additional keyword arguments are passed in the initialization of
1686 :class:`.ListingGenerator`.
1687
1688 To print the items in the unmoderated queue try:
1689
1690 .. code:: python
1691
1692 for item in reddit.subreddit('mod').mod.unmoderated():
1693 print(item)
1694
1695 """
1696 return ListingGenerator(
1697 self.subreddit._reddit,
1698 API_PATH["about_unmoderated"].format(subreddit=self.subreddit),
1699 **generator_kwargs
1700 )
1701
1702 def unread(self, **generator_kwargs):
1703 """Return a ListingGenerator for unread moderator messages.
1704
1705 Additional keyword arguments are passed in the initialization of
1706 :class:`.ListingGenerator`.
1707
1708 See ``inbox`` for all messages.
1709
1710 To print the mail in the unread modmail queue try:
1711
1712 .. code:: python
1713
1714 for message in reddit.subreddit('mod').mod.unread():
1715 print("From: {}, To: {}".format(message.author, message.dest))
1716
1717 """
1718 return ListingGenerator(
1719 self.subreddit._reddit,
1720 API_PATH["moderator_unread"].format(subreddit=self.subreddit),
1721 **generator_kwargs
1722 )
1723
1724 def update(self, **settings):
1725 """Update the subreddit's settings.
1726
1727 :param allow_images: Allow users to upload images using the native
1728 image hosting. Only applies to link-only subreddits.
1729 :param allow_post_crossposts: Allow users to crosspost submissions from
1730 other subreddits.
1731 :param allow_top: Allow the subreddit to appear on ``/r/all`` as well
1732 as the default and trending lists.
1733 :param collapse_deleted_comments: Collapse deleted and removed comments
1734 on comments pages by default.
1735 :param comment_score_hide_mins: The number of minutes to hide comment
1736 scores.
1737 :param description: Shown in the sidebar of your subreddit.
1738 :param disable_contributor_requests: Specifies whether redditors may
1739 send automated modmail messages requesting approval as a submitter.
1740 :type disable_contributor_requests: bool
1741 :param domain: Domain name with a cname that points to
1742 {subreddit}.reddit.com.
1743 :param exclude_banned_modqueue: Exclude posts by site-wide banned users
1744 from modqueue/unmoderated.
1745 :param header_hover_text: The text seen when hovering over the snoo.
1746 :param hide_ads: Don't show ads within this subreddit. Only applies to
1747 gold-user only subreddits.
1748 :param key_color: A 6-digit rgb hex color (e.g. ``'#AABBCC'``), used as
1749 a thematic color for your subreddit on mobile.
1750 :param lang: A valid IETF language tag (underscore separated).
1751 :param link_type: The types of submissions users can make.
1752 One of ``any``, ``link``, ``self``.
1753 :param over_18: Viewers must be over 18 years old (i.e. NSFW).
1754 :param public_description: Public description blurb. Appears in search
1755 results and on the landing page for private subreddits.
1756 :param public_traffic: Make the traffic stats page public.
1757 :param restrict_commenting: Specifies whether approved users have the
1758 ability to comment.
1759 :type restrict_commenting: bool
1760 :param restrict_posting: Specifies whether approved users have the
1761 ability to submit posts.
1762 :type restrict_posting: bool
1763 :param show_media: Show thumbnails on submissions.
1764 :param show_media_preview: Expand media previews on comments pages.
1765 :param spam_comments: Spam filter strength for comments.
1766 One of ``all``, ``low``, ``high``.
1767 :param spam_links: Spam filter strength for links.
1768 One of ``all``, ``low``, ``high``.
1769 :param spam_selfposts: Spam filter strength for selfposts.
1770 One of ``all``, ``low``, ``high``.
1771 :param spoilers_enabled: Enable marking posts as containing spoilers.
1772 :param sr: The fullname of the subreddit whose settings will be
1773 updated.
1774 :param submit_link_label: Custom label for submit link button
1775 (None for default).
1776 :param submit_text: Text to show on submission page.
1777 :param submit_text_label: Custom label for submit text post button
1778 (None for default).
1779 :param subreddit_type: One of ``archived``, ``employees_only``,
1780 ``gold_only``, ``gold_restricted``, ``private``, ``public``,
1781 ``restricted``.
1782 :param suggested_comment_sort: All comment threads will use this
1783 sorting method by default. Leave None, or choose one of
1784 ``confidence``, ``controversial``, ``new``, ``old``, ``qa``,
1785 ``random``, ``top``.
1786 :param title: The title of the subreddit.
1787 :param wiki_edit_age: Account age, in days, required to edit and create
1788 wiki pages.
1789 :param wiki_edit_karma: Subreddit karma required to edit and create
1790 wiki pages.
1791 :param wikimode: One of ``anyone``, ``disabled``, ``modonly``.
1792
1793 Additional keyword arguments can be provided to handle new settings as
1794 Reddit introduces them.
1795
1796 Settings that are documented here and aren't explicitly set by you in a
1797 call to :meth:`.SubredditModeration.update` should retain their current
1798 value. If they do not please file a bug.
1799
1800 .. warning:: Undocumented settings, or settings that were very recently
1801 documented, may not retain their current value when
1802 updating. This often occurs when Reddit adds a new setting
1803 but forgets to add that setting to the API endpoint that
1804 is used to fetch the current settings.
1805
1806 """
1807 current_settings = self.settings()
1808 fullname = current_settings.pop("subreddit_id")
1809
1810 # These attributes come out using different names than they go in.
1811 remap = {
1812 "allow_top": "default_set",
1813 "lang": "language",
1814 "link_type": "content_options",
1815 }
1816 for (new, old) in remap.items():
1817 current_settings[new] = current_settings.pop(old)
1818
1819 current_settings.update(settings)
1820 return Subreddit._create_or_update(
1821 _reddit=self.subreddit._reddit, sr=fullname, **current_settings
1822 )
1823
1824
1825class SubredditQuarantine:
1826 """Provides subreddit quarantine related methods."""
1827
1828 def __init__(self, subreddit):
1829 """Create a SubredditQuarantine instance.
1830
1831 :param subreddit: The subreddit associated with the quarantine.
1832
1833 """
1834 self.subreddit = subreddit
1835
1836 def opt_in(self):
1837 """Permit your user access to the quarantined subreddit.
1838
1839 Usage:
1840
1841 .. code:: python
1842
1843 subreddit = reddit.subreddit('QUESTIONABLE')
1844 next(subreddit.hot()) # Raises prawcore.Forbidden
1845
1846 subreddit.quaran.opt_in()
1847 next(subreddit.hot()) # Returns Submission
1848
1849 """
1850 data = {"sr_name": self.subreddit}
1851 try:
1852 self.subreddit._reddit.post(
1853 API_PATH["quarantine_opt_in"], data=data
1854 )
1855 except Redirect:
1856 pass
1857
1858 def opt_out(self):
1859 """Remove access to the quarantined subreddit.
1860
1861 Usage:
1862
1863 .. code:: python
1864
1865 subreddit = reddit.subreddit('QUESTIONABLE')
1866 next(subreddit.hot()) # Returns Submission
1867
1868 subreddit.quaran.opt_out()
1869 next(subreddit.hot()) # Raises prawcore.Forbidden
1870
1871 """
1872 data = {"sr_name": self.subreddit}
1873 try:
1874 self.subreddit._reddit.post(
1875 API_PATH["quarantine_opt_out"], data=data
1876 )
1877 except Redirect:
1878 pass
1879
1880
1881class SubredditRelationship:
1882 """Represents a relationship between a redditor and subreddit.
1883
1884 Instances of this class can be iterated through in order to discover the
1885 Redditors that make up the relationship.
1886
1887 For example, banned users of a subreddit can be iterated through like so:
1888
1889 .. code-block:: python
1890
1891 for ban in reddit.subreddit('redditdev').banned():
1892 print('{}: {}'.format(ban, ban.note))
1893
1894 """
1895
1896 def __call__(self, redditor=None, **generator_kwargs):
1897 """Return a generator for Redditors belonging to this relationship.
1898
1899 :param redditor: When provided, yield at most a single
1900 :class:`~.Redditor` instance. This is useful to confirm if a
1901 relationship exists, or to fetch the metadata associated with a
1902 particular relationship (default: None).
1903
1904 Additional keyword arguments are passed in the initialization of
1905 :class:`.ListingGenerator`.
1906
1907 """
1908 Subreddit._safely_add_arguments(
1909 generator_kwargs, "params", user=redditor
1910 )
1911 url = API_PATH["list_{}".format(self.relationship)].format(
1912 subreddit=self.subreddit
1913 )
1914 return ListingGenerator(
1915 self.subreddit._reddit, url, **generator_kwargs
1916 )
1917
1918 def __init__(self, subreddit, relationship):
1919 """Create a SubredditRelationship instance.
1920
1921 :param subreddit: The subreddit for the relationship.
1922 :param relationship: The name of the relationship.
1923
1924 """
1925 self.relationship = relationship
1926 self.subreddit = subreddit
1927
1928 def add(self, redditor, **other_settings):
1929 """Add ``redditor`` to this relationship.
1930
1931 :param redditor: A redditor name (e.g., ``'spez'``) or
1932 :class:`~.Redditor` instance.
1933
1934 """
1935 data = {"name": str(redditor), "type": self.relationship}
1936 data.update(other_settings)
1937 url = API_PATH["friend"].format(subreddit=self.subreddit)
1938 self.subreddit._reddit.post(url, data=data)
1939
1940 def remove(self, redditor):
1941 """Remove ``redditor`` from this relationship.
1942
1943 :param redditor: A redditor name (e.g., ``'spez'``) or
1944 :class:`~.Redditor` instance.
1945
1946 """
1947 data = {"name": str(redditor), "type": self.relationship}
1948 url = API_PATH["unfriend"].format(subreddit=self.subreddit)
1949 self.subreddit._reddit.post(url, data=data)
1950
1951
1952class ContributorRelationship(SubredditRelationship):
1953 """Provides methods to interact with a Subreddit's contributors.
1954
1955 Contributors are also known as approved submitters.
1956
1957 Contributors of a subreddit can be iterated through like so:
1958
1959 .. code-block:: python
1960
1961 for contributor in reddit.subreddit('redditdev').contributor():
1962 print(contributor)
1963
1964 """
1965
1966 def leave(self):
1967 """Abdicate the contributor position."""
1968 self.subreddit._reddit.post(
1969 API_PATH["leavecontributor"], data={"id": self.subreddit.fullname}
1970 )
1971
1972
1973class ModeratorRelationship(SubredditRelationship):
1974 """Provides methods to interact with a Subreddit's moderators.
1975
1976 Moderators of a subreddit can be iterated through like so:
1977
1978 .. code-block:: python
1979
1980 for moderator in reddit.subreddit('redditdev').moderator():
1981 print(moderator)
1982
1983 """
1984
1985 PERMISSIONS = {"access", "config", "flair", "mail", "posts", "wiki"}
1986
1987 @staticmethod
1988 def _handle_permissions(permissions, other_settings):
1989 other_settings = deepcopy(other_settings) if other_settings else {}
1990 other_settings["permissions"] = permissions_string(
1991 permissions, ModeratorRelationship.PERMISSIONS
1992 )
1993 return other_settings
1994
1995 def __call__(self, redditor=None): # pylint: disable=arguments-differ
1996 """Return a list of Redditors who are moderators.
1997
1998 :param redditor: When provided, return a list containing at most one
1999 :class:`~.Redditor` instance. This is useful to confirm if a
2000 relationship exists, or to fetch the metadata associated with a
2001 particular relationship (default: None).
2002
2003 .. note:: Unlike other relationship callables, this relationship is not
2004 paginated. Thus it simply returns the full list, rather than
2005 an iterator for the results.
2006
2007 To be used like:
2008
2009 .. code:: python
2010
2011 moderators = reddit.subreddit('nameofsub').moderator()
2012
2013 For example, to list the moderators along with their permissions try:
2014
2015 .. code:: python
2016
2017 for moderator in reddit.subreddit('SUBREDDIT').moderator():
2018 print('{}: {}'.format(moderator, moderator.mod_permissions))
2019
2020
2021 """
2022 params = {} if redditor is None else {"user": redditor}
2023 url = API_PATH["list_{}".format(self.relationship)].format(
2024 subreddit=self.subreddit
2025 )
2026 return self.subreddit._reddit.get(url, params=params)
2027
2028 # pylint: disable=arguments-differ
2029 def add(self, redditor, permissions=None, **other_settings):
2030 """Add or invite ``redditor`` to be a moderator of the subreddit.
2031
2032 :param redditor: A redditor name (e.g., ``'spez'``) or
2033 :class:`~.Redditor` instance.
2034 :param permissions: When provided (not ``None``), permissions should be
2035 a list of strings specifying which subset of permissions to
2036 grant. An empty list ``[]`` indicates no permissions, and when not
2037 provided ``None``, indicates full permissions.
2038
2039 An invite will be sent unless the user making this call is an admin
2040 user.
2041
2042 For example, to invite ``'spez'`` with ``'posts'`` and ``'mail'``
2043 permissions to ``'/r/test/``, try:
2044
2045 .. code:: python
2046
2047 reddit.subreddit('test').moderator.add('spez', ['posts', 'mail'])
2048
2049 """
2050 other_settings = self._handle_permissions(permissions, other_settings)
2051 super(ModeratorRelationship, self).add(redditor, **other_settings)
2052
2053 # pylint: enable=arguments-differ
2054
2055 def invite(self, redditor, permissions=None, **other_settings):
2056 """Invite ``redditor`` to be a moderator of the subreddit.
2057
2058 :param redditor: A redditor name (e.g., ``'spez'``) or
2059 :class:`~.Redditor` instance.
2060 :param permissions: When provided (not ``None``), permissions should be
2061 a list of strings specifying which subset of permissions to
2062 grant. An empty list ``[]`` indicates no permissions, and when not
2063 provided ``None``, indicates full permissions.
2064
2065 For example, to invite ``'spez'`` with ``'posts'`` and ``'mail'``
2066 permissions to ``'/r/test/``, try:
2067
2068 .. code:: python
2069
2070 reddit.subreddit('test').moderator.invite('spez', ['posts', 'mail'])
2071
2072 """
2073 data = self._handle_permissions(permissions, other_settings)
2074 data.update({"name": str(redditor), "type": "moderator_invite"})
2075 url = API_PATH["friend"].format(subreddit=self.subreddit)
2076 self.subreddit._reddit.post(url, data=data)
2077
2078 def leave(self):
2079 """Abdicate the moderator position (use with care).
2080
2081 For example:
2082
2083 .. code:: python
2084
2085 reddit.subreddit('subredditname').moderator.leave()
2086
2087 """
2088 self.remove(self.subreddit._reddit.config.username)
2089
2090 def remove_invite(self, redditor):
2091 """Remove the moderator invite for ``redditor``.
2092
2093 :param redditor: A redditor name (e.g., ``'spez'``) or
2094 :class:`~.Redditor` instance.
2095
2096 For example:
2097
2098 .. code:: python
2099
2100 reddit.subreddit('subredditname').moderator.remove_invite('spez')
2101
2102 """
2103 data = {"name": str(redditor), "type": "moderator_invite"}
2104 url = API_PATH["unfriend"].format(subreddit=self.subreddit)
2105 self.subreddit._reddit.post(url, data=data)
2106
2107 def update(self, redditor, permissions=None):
2108 """Update the moderator permissions for ``redditor``.
2109
2110 :param redditor: A redditor name (e.g., ``'spez'``) or
2111 :class:`~.Redditor` instance.
2112 :param permissions: When provided (not ``None``), permissions should be
2113 a list of strings specifying which subset of permissions to
2114 grant. An empty list ``[]`` indicates no permissions, and when not
2115 provided, ``None``, indicates full permissions.
2116
2117 For example, to add all permissions to the moderator, try:
2118
2119 .. code:: python
2120
2121 subreddit.moderator.update('spez')
2122
2123 To remove all permissions from the moderator, try:
2124
2125 .. code:: python
2126
2127 subreddit.moderator.update('spez', [])
2128
2129 """
2130 url = API_PATH["setpermissions"].format(subreddit=self.subreddit)
2131 data = self._handle_permissions(
2132 permissions, {"name": str(redditor), "type": "moderator"}
2133 )
2134 self.subreddit._reddit.post(url, data=data)
2135
2136 def update_invite(self, redditor, permissions=None):
2137 """Update the moderator invite permissions for ``redditor``.
2138
2139 :param redditor: A redditor name (e.g., ``'spez'``) or
2140 :class:`~.Redditor` instance.
2141 :param permissions: When provided (not ``None``), permissions should be
2142 a list of strings specifying which subset of permissions to
2143 grant. An empty list ``[]`` indicates no permissions, and when not
2144 provided, ``None``, indicates full permissions.
2145
2146 For example, to grant the flair and mail permissions to the moderator
2147 invite, try:
2148
2149 .. code:: python
2150
2151 subreddit.moderator.update_invite('spez', ['flair', 'mail'])
2152
2153 """
2154 url = API_PATH["setpermissions"].format(subreddit=self.subreddit)
2155 data = self._handle_permissions(
2156 permissions, {"name": str(redditor), "type": "moderator_invite"}
2157 )
2158 self.subreddit._reddit.post(url, data=data)
2159
2160
2161class Modmail:
2162 """Provides modmail functions for a subreddit."""
2163
2164 def __call__(self, id=None, mark_read=False): # noqa: D207, D301
2165 """Return an individual conversation.
2166
2167 :param id: A reddit base36 conversation ID, e.g., ``2gmz``.
2168 :param mark_read: If True, conversation is marked as read
2169 (default: False).
2170
2171 For example:
2172
2173 .. code:: python
2174
2175 reddit.subreddit('redditdev').modmail('2gmz', mark_read=True)
2176
2177 To print all messages from a conversation as Markdown source:
2178
2179 .. code:: python
2180
2181 conversation = reddit.subreddit('redditdev').modmail('2gmz', \
2182mark_read=True)
2183 for message in conversation.messages:
2184 print(message.body_markdown)
2185
2186 ``ModmailConversation.user`` is a special instance of
2187 :class:`.Redditor` with extra attributes describing the non-moderator
2188 user's recent posts, comments, and modmail messages within the
2189 subreddit, as well as information on active bans and mutes. This
2190 attribute does not exist on internal moderator discussions.
2191
2192 For example, to print the user's ban status:
2193
2194 .. code:: python
2195
2196 conversation = reddit.subreddit('redditdev').modmail('2gmz', \
2197mark_read=True)
2198 print(conversation.user.ban_status)
2199
2200 To print a list of recent submissions by the user:
2201
2202 .. code:: python
2203
2204 conversation = reddit.subreddit('redditdev').modmail('2gmz', \
2205mark_read=True)
2206 print(conversation.user.recent_posts)
2207
2208 """
2209 # pylint: disable=invalid-name,redefined-builtin
2210 return ModmailConversation(
2211 self.subreddit._reddit, id=id, mark_read=mark_read
2212 )
2213
2214 def __init__(self, subreddit):
2215 """Construct an instance of the Modmail object."""
2216 self.subreddit = subreddit
2217
2218 def _build_subreddit_list(self, other_subreddits):
2219 """Return a comma-separated list of subreddit display names."""
2220 subreddits = [self.subreddit] + (other_subreddits or [])
2221 return ",".join(str(subreddit) for subreddit in subreddits)
2222
2223 def bulk_read(self, other_subreddits=None, state=None):
2224 """Mark conversations for subreddit(s) as read.
2225
2226 Due to server-side restrictions, 'all' is not a valid subreddit for
2227 this method. Instead, use :meth:`~.Modmail.subreddits` to get a list of
2228 subreddits using the new modmail.
2229
2230 :param other_subreddits: A list of :class:`.Subreddit` instances for
2231 which to mark conversations (default: None).
2232 :param state: Can be one of: all, archived, highlighted, inprogress,
2233 mod, new, notifications, (default: all). "all" does not include
2234 internal or archived conversations.
2235 :returns: A list of :class:`.ModmailConversation` instances that were
2236 marked read.
2237
2238 For example, to mark all notifications for a subreddit as read:
2239
2240 .. code:: python
2241
2242 subreddit = reddit.subreddit('redditdev')
2243 subreddit.modmail.bulk_read(state='notifications')
2244
2245 """
2246 params = {"entity": self._build_subreddit_list(other_subreddits)}
2247 if state:
2248 params["state"] = state
2249 response = self.subreddit._reddit.post(
2250 API_PATH["modmail_bulk_read"], params=params
2251 )
2252 return [
2253 self(conversation_id)
2254 for conversation_id in response["conversation_ids"]
2255 ]
2256
2257 def conversations(
2258 self,
2259 after=None,
2260 limit=None,
2261 other_subreddits=None,
2262 sort=None,
2263 state=None,
2264 ): # noqa: D207, D301
2265 """Generate :class:`.ModmailConversation` objects for subreddit(s).
2266
2267 :param after: A base36 modmail conversation id. When provided, the
2268 listing begins after this conversation (default: None).
2269 :param limit: The maximum number of conversations to fetch. If None,
2270 the server-side default is 25 at the time of writing
2271 (default: None).
2272 :param other_subreddits: A list of :class:`.Subreddit` instances for
2273 which to fetch conversations (default: None).
2274 :param sort: Can be one of: mod, recent, unread, user
2275 (default: recent).
2276 :param state: Can be one of: all, archived, highlighted, inprogress,
2277 mod, new, notifications, (default: all). "all" does not include
2278 internal or archived conversations.
2279
2280
2281 For example:
2282
2283 .. code:: python
2284
2285 conversations = reddit.subreddit('all').modmail.conversations(\
2286state='mod')
2287
2288 """
2289 params = {}
2290 if self.subreddit != "all":
2291 params["entity"] = self._build_subreddit_list(other_subreddits)
2292
2293 for name, value in {
2294 "after": after,
2295 "limit": limit,
2296 "sort": sort,
2297 "state": state,
2298 }.items():
2299 if value:
2300 params[name] = value
2301
2302 response = self.subreddit._reddit.get(
2303 API_PATH["modmail_conversations"], params=params
2304 )
2305 for conversation_id in response["conversationIds"]:
2306 data = {
2307 "conversation": response["conversations"][conversation_id],
2308 "messages": response["messages"],
2309 }
2310 yield ModmailConversation.parse(
2311 data, self.subreddit._reddit, convert_objects=False
2312 )
2313
2314 def create(self, subject, body, recipient, author_hidden=False):
2315 """Create a new modmail conversation.
2316
2317 :param subject: The message subject. Cannot be empty.
2318 :param body: The message body. Cannot be empty.
2319 :param recipient: The recipient; a username or an instance of
2320 :class:`.Redditor`.
2321 :param author_hidden: When True, author is hidden from non-moderators
2322 (default: False).
2323 :returns: A :class:`.ModmailConversation` object for the newly created
2324 conversation.
2325
2326 .. code:: python
2327
2328 subreddit = reddit.subreddit('redditdev')
2329 redditor = reddit.redditor('bboe')
2330 subreddit.modmail.create('Subject', 'Body', redditor)
2331
2332 """
2333 data = {
2334 "body": body,
2335 "isAuthorHidden": author_hidden,
2336 "srName": self.subreddit,
2337 "subject": subject,
2338 "to": recipient,
2339 }
2340 return self.subreddit._reddit.post(
2341 API_PATH["modmail_conversations"], data=data
2342 )
2343
2344 def subreddits(self):
2345 """Yield subreddits using the new modmail that the user moderates.
2346
2347 For example:
2348
2349 .. code:: python
2350
2351 subreddits = reddit.subreddit('all').modmail.subreddits()
2352
2353 """
2354 response = self.subreddit._reddit.get(API_PATH["modmail_subreddits"])
2355 for value in response["subreddits"].values():
2356 subreddit = self.subreddit._reddit.subreddit(value["display_name"])
2357 subreddit.last_updated = value["lastUpdated"]
2358 yield subreddit
2359
2360 def unread_count(self):
2361 """Return unread conversation count by conversation state.
2362
2363 At time of writing, possible states are: archived, highlighted,
2364 inprogress, mod, new, notifications.
2365
2366 :returns: A dict mapping conversation states to unread counts.
2367
2368 For example, to print the count of unread moderator discussions:
2369
2370 .. code:: python
2371
2372 subreddit = reddit.subreddit('redditdev')
2373 unread_counts = subreddit.modmail.unread_count()
2374 print(unread_counts['mod'])
2375
2376 """
2377 return self.subreddit._reddit.get(API_PATH["modmail_unread_count"])
2378
2379
2380class SubredditStream:
2381 """Provides submission and comment streams."""
2382
2383 def __init__(self, subreddit):
2384 """Create a SubredditStream instance.
2385
2386 :param subreddit: The subreddit associated with the streams.
2387
2388 """
2389 self.subreddit = subreddit
2390
2391 def comments(self, **stream_options):
2392 """Yield new comments as they become available.
2393
2394 Comments are yielded oldest first. Up to 100 historical comments will
2395 initially be returned.
2396
2397 Keyword arguments are passed to :func:`.stream_generator`.
2398
2399 For example, to retrieve all new comments made to the ``iama``
2400 subreddit, try:
2401
2402 .. code:: python
2403
2404 for comment in reddit.subreddit('iama').stream.comments():
2405 print(comment)
2406
2407 To only retreive new submissions starting when the stream is
2408 created, pass `skip_existing=True`:
2409
2410 .. code:: python
2411
2412 subreddit = reddit.subreddit('iama')
2413 for comment in subreddit.stream.comments(skip_existing=True):
2414 print(comment)
2415
2416 """
2417 return stream_generator(self.subreddit.comments, **stream_options)
2418
2419 def submissions(self, **stream_options):
2420 """Yield new submissions as they become available.
2421
2422 Submissions are yielded oldest first. Up to 100 historical submissions
2423 will initially be returned.
2424
2425 Keyword arguments are passed to :func:`.stream_generator`.
2426
2427 For example to retrieve all new submissions made to all of Reddit, try:
2428
2429 .. code:: python
2430
2431 for submission in reddit.subreddit('all').stream.submissions():
2432 print(submission)
2433
2434 """
2435 return stream_generator(self.subreddit.new, **stream_options)
2436
2437
2438class SubredditStylesheet:
2439 """Provides a set of stylesheet functions to a Subreddit."""
2440
2441 def __call__(self):
2442 """Return the subreddit's stylesheet.
2443
2444 To be used as:
2445
2446 .. code:: python
2447
2448 stylesheet = reddit.subreddit('SUBREDDIT').stylesheet()
2449
2450 """
2451 url = API_PATH["about_stylesheet"].format(subreddit=self.subreddit)
2452 return self.subreddit._reddit.get(url)
2453
2454 def __init__(self, subreddit):
2455 """Create a SubredditStylesheet instance.
2456
2457 :param subreddit: The subreddit associated with the stylesheet.
2458
2459 An instance of this class is provided as:
2460
2461 .. code:: python
2462
2463 reddit.subreddit('SUBREDDIT').stylesheet
2464
2465 """
2466 self.subreddit = subreddit
2467
2468 def _update_structured_styles(self, style_data):
2469 url = API_PATH["structured_styles"].format(subreddit=self.subreddit)
2470 self.subreddit._reddit.patch(url, style_data)
2471
2472 def _upload_image(self, image_path, data):
2473 with open(image_path, "rb") as image:
2474 header = image.read(len(JPEG_HEADER))
2475 image.seek(0)
2476 data["img_type"] = "jpg" if header == JPEG_HEADER else "png"
2477 url = API_PATH["upload_image"].format(subreddit=self.subreddit)
2478 response = self.subreddit._reddit.post(
2479 url, data=data, files={"file": image}
2480 )
2481 if response["errors"]:
2482 error_type = response["errors"][0]
2483 error_value = response.get("errors_values", [""])[0]
2484 assert error_type in [
2485 "BAD_CSS_NAME",
2486 "IMAGE_ERROR",
2487 ], "Please file a bug with PRAW"
2488 raise APIException(error_type, error_value, None)
2489 return response
2490
2491 def _upload_style_asset(self, image_path, image_type):
2492 data = {"imagetype": image_type, "filepath": basename(image_path)}
2493 data["mimetype"] = "image/jpeg"
2494 if image_path.lower().endswith(".png"):
2495 data["mimetype"] = "image/png"
2496 url = API_PATH["style_asset_lease"].format(subreddit=self.subreddit)
2497
2498 upload_lease = self.subreddit._reddit.post(url, data=data)[
2499 "s3UploadLease"
2500 ]
2501 upload_data = {
2502 item["name"]: item["value"] for item in upload_lease["fields"]
2503 }
2504 upload_url = "https:{}".format(upload_lease["action"])
2505
2506 with open(image_path, "rb") as image:
2507 response = self.subreddit._reddit._core._requestor._http.post(
2508 upload_url, data=upload_data, files={"file": image}
2509 )
2510 response.raise_for_status()
2511
2512 return "{}/{}".format(upload_url, upload_data["key"])
2513
2514 def delete_banner(self):
2515 """Remove the current subreddit (redesign) banner image.
2516
2517 Succeeds even if there is no banner image.
2518
2519 For example:
2520
2521 .. code:: python
2522
2523 reddit.subreddit('SUBREDDIT').stylesheet.delete_banner()
2524
2525 """
2526 data = {"bannerBackgroundImage": ""}
2527 self._update_structured_styles(data)
2528
2529 def delete_banner_additional_image(self):
2530 """Remove the current subreddit (redesign) banner additional image.
2531
2532 Succeeds even if there is no additional image. Will also delete any
2533 configured hover image.
2534
2535 For example:
2536
2537 .. code:: python
2538
2539 reddit.subreddit('SUBREDDIT').stylesheet.delete_banner_additional_image()
2540
2541 """
2542 data = {
2543 "bannerPositionedImage": "",
2544 "secondaryBannerPositionedImage": "",
2545 }
2546 self._update_structured_styles(data)
2547
2548 def delete_banner_hover_image(self):
2549 """Remove the current subreddit (redesign) banner hover image.
2550
2551 Succeeds even if there is no hover image.
2552
2553 For example:
2554
2555 .. code:: python
2556
2557 reddit.subreddit('SUBREDDIT').stylesheet.delete_banner_hover_image()
2558
2559 """
2560 data = {"secondaryBannerPositionedImage": ""}
2561 self._update_structured_styles(data)
2562
2563 def delete_header(self):
2564 """Remove the current subreddit header image.
2565
2566 Succeeds even if there is no header image.
2567
2568 For example:
2569
2570 .. code:: python
2571
2572 reddit.subreddit('SUBREDDIT').stylesheet.delete_header()
2573
2574 """
2575 url = API_PATH["delete_sr_header"].format(subreddit=self.subreddit)
2576 self.subreddit._reddit.post(url)
2577
2578 def delete_image(self, name):
2579 """Remove the named image from the subreddit.
2580
2581 Succeeds even if the named image does not exist.
2582
2583 For example:
2584
2585 .. code:: python
2586
2587 reddit.subreddit('SUBREDDIT').stylesheet.delete_image('smile')
2588
2589 """
2590 url = API_PATH["delete_sr_image"].format(subreddit=self.subreddit)
2591 self.subreddit._reddit.post(url, data={"img_name": name})
2592
2593 def delete_mobile_header(self):
2594 """Remove the current subreddit mobile header.
2595
2596 Succeeds even if there is no mobile header.
2597
2598 For example:
2599
2600 .. code:: python
2601
2602 reddit.subreddit('SUBREDDIT').stylesheet.delete_mobile_header()
2603
2604 """
2605 url = API_PATH["delete_sr_header"].format(subreddit=self.subreddit)
2606 self.subreddit._reddit.post(url)
2607
2608 def delete_mobile_icon(self):
2609 """Remove the current subreddit mobile icon.
2610
2611 Succeeds even if there is no mobile icon.
2612
2613 For example:
2614
2615 .. code:: python
2616
2617 reddit.subreddit('SUBREDDIT').stylesheet.delete_mobile_icon()
2618
2619 """
2620 url = API_PATH["delete_sr_icon"].format(subreddit=self.subreddit)
2621 self.subreddit._reddit.post(url)
2622
2623 def update(self, stylesheet, reason=None):
2624 """Update the subreddit's stylesheet.
2625
2626 :param stylesheet: The CSS for the new stylesheet.
2627
2628 For example:
2629
2630 .. code:: python
2631
2632 reddit.subreddit('SUBREDDIT').stylesheet.update(
2633 'p { color: green; }', 'color text green')
2634
2635 """
2636 data = {
2637 "op": "save",
2638 "reason": reason,
2639 "stylesheet_contents": stylesheet,
2640 }
2641 url = API_PATH["subreddit_stylesheet"].format(subreddit=self.subreddit)
2642 self.subreddit._reddit.post(url, data=data)
2643
2644 def upload(self, name, image_path):
2645 """Upload an image to the Subreddit.
2646
2647 :param name: The name to use for the image. If an image already exists
2648 with the same name, it will be replaced.
2649 :param image_path: A path to a jpeg or png image.
2650 :returns: A dictionary containing a link to the uploaded image under
2651 the key ``img_src``.
2652
2653 Raises ``prawcore.TooLarge`` if the overall request body is too large.
2654
2655 Raises :class:`.APIException` if there are other issues with the
2656 uploaded image. Unfortunately the exception info might not be very
2657 specific, so try through the website with the same image to see what
2658 the problem actually might be.
2659
2660 For example:
2661
2662 .. code:: python
2663
2664 reddit.subreddit('SUBREDDIT').stylesheet.upload('smile', 'img.png')
2665
2666 """
2667 return self._upload_image(
2668 image_path, {"name": name, "upload_type": "img"}
2669 )
2670
2671 def upload_banner(self, image_path):
2672 """Upload an image for the subreddit's (redesign) banner image.
2673
2674 :param image_path: A path to a jpeg or png image.
2675
2676 Raises ``prawcore.TooLarge`` if the overall request body is too large.
2677
2678 Raises :class:`.APIException` if there are other issues with the
2679 uploaded image. Unfortunately the exception info might not be very
2680 specific, so try through the website with the same image to see what
2681 the problem actually might be.
2682
2683 For example:
2684
2685 .. code:: python
2686
2687 reddit.subreddit('SUBREDDIT').stylesheet.upload_banner('banner.png')
2688
2689 """
2690 image_type = "bannerBackgroundImage"
2691 image_url = self._upload_style_asset(image_path, image_type)
2692 self._update_structured_styles({image_type: image_url})
2693
2694 def upload_banner_additional_image(self, image_path, align=None):
2695 """Upload an image for the subreddit's (redesign) additional image.
2696
2697 :param image_path: A path to a jpeg or png image.
2698 :param align: Either ``left``, ``centered``, or ``right``. (default:
2699 ``left``).
2700
2701 Raises ``prawcore.TooLarge`` if the overall request body is too large.
2702
2703 Raises :class:`.APIException` if there are other issues with the
2704 uploaded image. Unfortunately the exception info might not be very
2705 specific, so try through the website with the same image to see what
2706 the problem actually might be.
2707
2708 For example:
2709
2710 .. code:: python
2711
2712 reddit.subreddit('SUBREDDIT').stylesheet.upload_banner_additional_image('banner.png')
2713
2714 """
2715 alignment = {}
2716 if align is not None:
2717 if align not in {"left", "centered", "right"}:
2718 raise ValueError(
2719 "align argument must be either "
2720 "`left`, `centered`, or `right`"
2721 )
2722 alignment["bannerPositionedImagePosition"] = align
2723
2724 image_type = "bannerPositionedImage"
2725 image_url = self._upload_style_asset(image_path, image_type)
2726 style_data = {image_type: image_url}
2727 if alignment:
2728 style_data.update(alignment)
2729 self._update_structured_styles(style_data)
2730
2731 def upload_banner_hover_image(self, image_path):
2732 """Upload an image for the subreddit's (redesign) additional image.
2733
2734 :param image_path: A path to a jpeg or png image.
2735
2736 Fails if the Subreddit does not have an additional image defined
2737
2738 Raises ``prawcore.TooLarge`` if the overall request body is too large.
2739
2740 Raises :class:`.APIException` if there are other issues with the
2741 uploaded image. Unfortunately the exception info might not be very
2742 specific, so try through the website with the same image to see what
2743 the problem actually might be.
2744
2745 For example:
2746
2747 .. code:: python
2748
2749 reddit.subreddit('SUBREDDIT').stylesheet.upload_banner_hover_image('banner.png')
2750
2751 """
2752 image_type = "secondaryBannerPositionedImage"
2753 image_url = self._upload_style_asset(image_path, image_type)
2754 self._update_structured_styles({image_type: image_url})
2755
2756 def upload_header(self, image_path):
2757 """Upload an image to be used as the Subreddit's header image.
2758
2759 :param image_path: A path to a jpeg or png image.
2760 :returns: A dictionary containing a link to the uploaded image under
2761 the key ``img_src``.
2762
2763 Raises ``prawcore.TooLarge`` if the overall request body is too large.
2764
2765 Raises :class:`.APIException` if there are other issues with the
2766 uploaded image. Unfortunately the exception info might not be very
2767 specific, so try through the website with the same image to see what
2768 the problem actually might be.
2769
2770 For example:
2771
2772 .. code:: python
2773
2774 reddit.subreddit('SUBREDDIT').stylesheet.upload_header('header.png')
2775
2776 """
2777 return self._upload_image(image_path, {"upload_type": "header"})
2778
2779 def upload_mobile_header(self, image_path):
2780 """Upload an image to be used as the Subreddit's mobile header.
2781
2782 :param image_path: A path to a jpeg or png image.
2783 :returns: A dictionary containing a link to the uploaded image under
2784 the key ``img_src``.
2785
2786 Raises ``prawcore.TooLarge`` if the overall request body is too large.
2787
2788 Raises :class:`.APIException` if there are other issues with the
2789 uploaded image. Unfortunately the exception info might not be very
2790 specific, so try through the website with the same image to see what
2791 the problem actually might be.
2792
2793 For example:
2794
2795 .. code:: python
2796
2797 reddit.subreddit('SUBREDDIT').stylesheet.upload_mobile_header(
2798 'header.png')
2799
2800 """
2801 return self._upload_image(image_path, {"upload_type": "banner"})
2802
2803 def upload_mobile_icon(self, image_path):
2804 """Upload an image to be used as the Subreddit's mobile icon.
2805
2806 :param image_path: A path to a jpeg or png image.
2807 :returns: A dictionary containing a link to the uploaded image under
2808 the key ``img_src``.
2809
2810 Raises ``prawcore.TooLarge`` if the overall request body is too large.
2811
2812 Raises :class:`.APIException` if there are other issues with the
2813 uploaded image. Unfortunately the exception info might not be very
2814 specific, so try through the website with the same image to see what
2815 the problem actually might be.
2816
2817 For example:
2818
2819 .. code:: python
2820
2821 reddit.subreddit('SUBREDDIT').stylesheet.upload_mobile_icon(
2822 'icon.png')
2823
2824 """
2825 return self._upload_image(image_path, {"upload_type": "icon"})
2826
2827
2828class SubredditWiki:
2829 """Provides a set of wiki functions to a Subreddit."""
2830
2831 def __getitem__(self, page_name):
2832 """Lazily return the WikiPage for the subreddit named ``page_name``.
2833
2834 This method is to be used to fetch a specific wikipage, like so:
2835
2836 .. code:: python
2837
2838 wikipage = reddit.subreddit('iama').wiki['proof']
2839 print(wikipage.content_md)
2840
2841 """
2842 return WikiPage(
2843 self.subreddit._reddit, self.subreddit, page_name.lower()
2844 )
2845
2846 def __init__(self, subreddit):
2847 """Create a SubredditWiki instance.
2848
2849 :param subreddit: The subreddit whose wiki to work with.
2850
2851 """
2852 self.banned = SubredditRelationship(subreddit, "wikibanned")
2853 self.contributor = SubredditRelationship(subreddit, "wikicontributor")
2854 self.subreddit = subreddit
2855
2856 def __iter__(self):
2857 """Iterate through the pages of the wiki.
2858
2859 This method is to be used to discover all wikipages for a subreddit:
2860
2861 .. code:: python
2862
2863 for wikipage in reddit.subreddit('iama').wiki:
2864 print(wikipage)
2865
2866 """
2867 response = self.subreddit._reddit.get(
2868 API_PATH["wiki_pages"].format(subreddit=self.subreddit),
2869 params={"unique": self.subreddit._reddit._next_unique},
2870 )
2871 for page_name in response["data"]:
2872 yield WikiPage(self.subreddit._reddit, self.subreddit, page_name)
2873
2874 def create(self, name, content, reason=None, **other_settings):
2875 """Create a new wiki page.
2876
2877 :param name: The name of the new WikiPage. This name will be
2878 normalized.
2879 :param content: The content of the new WikiPage.
2880 :param reason: (Optional) The reason for the creation.
2881 :param other_settings: Additional keyword arguments to pass.
2882
2883 To create the wiki page ``'praw_test'`` in ``'/r/test'`` try:
2884
2885 .. code:: python
2886
2887 reddit.subreddit('test').wiki.create(
2888 'praw_test', 'wiki body text', reason='PRAW Test Creation')
2889
2890 """
2891 name = name.replace(" ", "_").lower()
2892 new = WikiPage(self.subreddit._reddit, self.subreddit, name)
2893 new.edit(content=content, reason=reason, **other_settings)
2894 return new
2895
2896 def revisions(self, **generator_kwargs):
2897 """Return a generator for recent wiki revisions.
2898
2899 Additional keyword arguments are passed in the initialization of
2900 :class:`.ListingGenerator`.
2901
2902 To view the wiki revisions for ``'praw_test'`` in ``'/r/test'`` try:
2903
2904 .. code:: python
2905
2906 for item in reddit.subreddit('test').wiki['praw_test'].revisions():
2907 print(item)
2908
2909 """
2910 url = API_PATH["wiki_revisions"].format(subreddit=self.subreddit)
2911 return WikiPage._revision_generator(
2912 self.subreddit, url, generator_kwargs
2913 )