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