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