"""Provide the Redditor class.""" from __future__ import annotations from json import dumps from typing import TYPE_CHECKING, Any, Generator from ...const import API_PATH from ...util import _deprecate_args from ...util.cache import cachedproperty from ..listing.mixins import RedditorListingMixin from ..util import stream_generator from .base import RedditBase from .mixins import FullnameMixin, MessageableMixin if TYPE_CHECKING: # pragma: no cover import praw.models class Redditor(MessageableMixin, RedditorListingMixin, FullnameMixin, RedditBase): """A class representing the users of Reddit. .. include:: ../../typical_attributes.rst .. note:: Shadowbanned accounts are treated the same as non-existent accounts, meaning that they will not have any attributes. .. note:: Suspended/banned accounts will only return the ``name`` and ``is_suspended`` attributes. =================================== ================================================ Attribute Description =================================== ================================================ ``comment_karma`` The comment karma for the :class:`.Redditor`. ``comments`` Provide an instance of :class:`.SubListing` for comment access. ``submissions`` Provide an instance of :class:`.SubListing` for submission access. ``created_utc`` Time the account was created, represented in `Unix Time`_. ``has_verified_email`` Whether or not the :class:`.Redditor` has verified their email. ``icon_img`` The url of the Redditors' avatar. ``id`` The ID of the :class:`.Redditor`. ``is_employee`` Whether or not the :class:`.Redditor` is a Reddit employee. ``is_friend`` Whether or not the :class:`.Redditor` is friends with the authenticated user. ``is_mod`` Whether or not the :class:`.Redditor` mods any subreddits. ``is_gold`` Whether or not the :class:`.Redditor` has active Reddit Premium status. ``is_suspended`` Whether or not the :class:`.Redditor` is currently suspended. ``link_karma`` The link karma for the :class:`.Redditor`. ``name`` The Redditor's username. ``subreddit`` If the :class:`.Redditor` has created a user-subreddit, provides a dictionary of additional attributes. See below. ``subreddit["banner_img"]`` The URL of the user-subreddit banner. ``subreddit["name"]`` The fullname of the user-subreddit. ``subreddit["over_18"]`` Whether or not the user-subreddit is NSFW. ``subreddit["public_description"]`` The public description of the user-subreddit. ``subreddit["subscribers"]`` The number of users subscribed to the user-subreddit. ``subreddit["title"]`` The title of the user-subreddit. =================================== ================================================ .. _unix time: https://en.wikipedia.org/wiki/Unix_time """ STR_FIELD = "name" @classmethod def from_data(cls, reddit: praw.Reddit, data: dict[str, Any]) -> Redditor | None: """Return an instance of :class:`.Redditor`, or ``None`` from ``data``.""" if data == "[deleted]": return None return cls(reddit, data) @cachedproperty def notes(self) -> praw.models.RedditorModNotes: """Provide an instance of :class:`.RedditorModNotes`. This provides an interface for managing moderator notes for a redditor. .. note:: The authenticated user must be a moderator of the provided subreddit(s). For example, all the notes for u/spez in r/test can be iterated through like so: .. code-block:: python redditor = reddit.redditor("spez") for note in redditor.notes.subreddits("test"): print(f"{note.label}: {note.note}") """ from praw.models.mod_notes import RedditorModNotes return RedditorModNotes(self._reddit, self) @cachedproperty def stream(self) -> praw.models.reddit.redditor.RedditorStream: """Provide an instance of :class:`.RedditorStream`. Streams can be used to indefinitely retrieve new comments made by a redditor, like: .. code-block:: python for comment in reddit.redditor("spez").stream.comments(): print(comment) Additionally, new submissions can be retrieved via the stream. In the following example all submissions are fetched via the redditor u/spez: .. code-block:: python for submission in reddit.redditor("spez").stream.submissions(): print(submission) """ return RedditorStream(self) @property def _kind(self) -> str: """Return the class's kind.""" return self._reddit.config.kinds["redditor"] @property def _path(self) -> str: return API_PATH["user"].format(user=self) def __init__( self, reddit: praw.Reddit, name: str | None = None, fullname: str | None = None, _data: dict[str, Any] | None = None, ): """Initialize a :class:`.Redditor` instance. :param reddit: An instance of :class:`.Reddit`. :param name: The name of the redditor. :param fullname: The fullname of the redditor, starting with ``t2_``. Exactly one of ``name``, ``fullname`` or ``_data`` must be provided. """ if (name, fullname, _data).count(None) != 2: msg = "Exactly one of 'name', 'fullname', or '_data' must be provided." raise TypeError(msg) if _data: assert ( # noqa: PT018 isinstance(_data, dict) and "name" in _data ), "Please file a bug with PRAW." self._listing_use_sort = True if name: self.name = name elif fullname: self._fullname = fullname super().__init__(reddit, _data=_data, _extra_attribute_to_check="_fullname") def __setattr__(self, name: str, value: Any): """Objectify the subreddit attribute.""" if name == "subreddit" and value: from .user_subreddit import UserSubreddit value = UserSubreddit(reddit=self._reddit, _data=value) super().__setattr__(name, value) def _fetch(self): data = self._fetch_data() data = data["data"] other = type(self)(self._reddit, _data=data) self.__dict__.update(other.__dict__) super()._fetch() def _fetch_info(self): if hasattr(self, "_fullname"): self.name = self._fetch_username(self._fullname) return "user_about", {"user": self.name}, None def _fetch_username(self, fullname: str): return self._reddit.get(API_PATH["user_by_fullname"], params={"ids": fullname})[ fullname ]["name"] def _friend(self, *, data: dict[str, Any], method: str): url = API_PATH["friend_v1"].format(user=self) self._reddit.request(data=dumps(data), method=method, path=url) def block(self): """Block the :class:`.Redditor`. For example, to block :class:`.Redditor` u/spez: .. code-block:: python reddit.redditor("spez").block() .. note:: Blocking a trusted user will remove that user from your trusted list. .. seealso:: :meth:`.trust` """ self._reddit.post(API_PATH["block_user"], params={"name": self.name}) def distrust(self): """Remove the :class:`.Redditor` from your whitelist of trusted users. For example, to remove :class:`.Redditor` u/spez from your whitelist: .. code-block:: python reddit.redditor("spez").distrust() .. seealso:: :meth:`.trust` """ self._reddit.post(API_PATH["remove_whitelisted"], data={"name": self.name}) @_deprecate_args("note") def friend(self, *, note: str = None): """Friend the :class:`.Redditor`. :param note: A note to save along with the relationship. Requires Reddit Premium (default: ``None``). Calling this method subsequent times will update the note. For example, to friend u/spez: .. code-block:: python reddit.redditor("spez").friend() To add a note to the friendship (requires Reddit Premium): .. code-block:: python reddit.redditor("spez").friend(note="My favorite admin") """ self._friend(data={"note": note} if note else {}, method="PUT") def friend_info(self) -> praw.models.Redditor: """Return a :class:`.Redditor` instance with specific friend-related attributes. :returns: A :class:`.Redditor` instance with fields ``date``, ``id``, and possibly ``note`` if the authenticated user has Reddit Premium. For example, to get the friendship information of :class:`.Redditor` u/spez: .. code-block:: python info = reddit.redditor("spez").friend_info friend_data = info.date """ return self._reddit.get(API_PATH["friend_v1"].format(user=self)) @_deprecate_args("months") def gild(self, *, months: int = 1): """Gild the :class:`.Redditor`. :param months: Specifies the number of months to gild up to 36 (default: ``1``). For example, to gild :class:`.Redditor` u/spez for 1 month: .. code-block:: python reddit.redditor("spez").gild(months=1) """ if months < 1 or months > 36: msg = "months must be between 1 and 36" raise TypeError(msg) self._reddit.post( API_PATH["gild_user"].format(username=self), data={"months": months} ) def moderated(self) -> list[praw.models.Subreddit]: """Return a list of the redditor's moderated subreddits. :returns: A list of :class:`.Subreddit` objects. Return ``[]`` if the redditor has no moderated subreddits. :raises: ``prawcore.ServerError`` in certain circumstances. See the note below. .. note:: The redditor's own user profile subreddit will not be returned, but other user profile subreddits they moderate will be returned. Usage: .. code-block:: python for subreddit in reddit.redditor("spez").moderated(): print(subreddit.display_name) print(subreddit.title) .. note:: A ``prawcore.ServerError`` exception may be raised if the redditor moderates a large number of subreddits. If that happens, try switching to :ref:`read-only mode `. For example, .. code-block:: python reddit.read_only = True for subreddit in reddit.redditor("reddit").moderated(): print(str(subreddit)) It is possible that requests made in read-only mode will also raise a ``prawcore.ServerError`` exception. When used in read-only mode, this method does not retrieve information about subreddits that require certain special permissions to access, e.g., private subreddits and premium-only subreddits. .. seealso:: :meth:`.User.moderator_subreddits` """ return self._reddit.get(API_PATH["moderated"].format(user=self)) or [] def multireddits(self) -> list[praw.models.Multireddit]: """Return a list of the redditor's public multireddits. For example, to to get :class:`.Redditor` u/spez's multireddits: .. code-block:: python multireddits = reddit.redditor("spez").multireddits() """ return self._reddit.get(API_PATH["multireddit_user"].format(user=self)) def trophies(self) -> list[praw.models.Trophy]: """Return a list of the redditor's trophies. :returns: A list of :class:`.Trophy` objects. Return ``[]`` if the redditor has no trophies. :raises: :class:`.RedditAPIException` if the redditor doesn't exist. Usage: .. code-block:: python for trophy in reddit.redditor("spez").trophies(): print(trophy.name) print(trophy.description) """ return list(self._reddit.get(API_PATH["trophies"].format(user=self))) def trust(self): """Add the :class:`.Redditor` to your whitelist of trusted users. Trusted users will always be able to send you PMs. Example usage: .. code-block:: python reddit.redditor("AaronSw").trust() Use the ``accept_pms`` parameter of :meth:`.Preferences.update` to toggle your ``accept_pms`` setting between ``"everyone"`` and ``"whitelisted"``. For example: .. code-block:: python # Accept private messages from everyone: reddit.user.preferences.update(accept_pms="everyone") # Only accept private messages from trusted users: reddit.user.preferences.update(accept_pms="whitelisted") You may trust a user even if your ``accept_pms`` setting is switched to ``"everyone"``. .. note:: You are allowed to have a user on your blocked list and your friends list at the same time. However, you cannot trust a user who is on your blocked list. .. seealso:: - :meth:`.distrust` - :meth:`.Preferences.update` - :meth:`.trusted` """ self._reddit.post(API_PATH["add_whitelisted"], data={"name": self.name}) def unblock(self): """Unblock the :class:`.Redditor`. For example, to unblock :class:`.Redditor` u/spez: .. code-block:: python reddit.redditor("spez").unblock() """ data = { "container": self._reddit.user.me().fullname, "name": str(self), "type": "enemy", } url = API_PATH["unfriend"].format(subreddit="all") self._reddit.post(url, data=data) def unfriend(self): """Unfriend the :class:`.Redditor`. For example, to unfriend :class:`.Redditor` u/spez: .. code-block:: python reddit.redditor("spez").unfriend() """ self._friend(data={"id": str(self)}, method="DELETE") class RedditorStream: """Provides submission and comment streams.""" def __init__(self, redditor: praw.models.Redditor): """Initialize a :class:`.RedditorStream` instance. :param redditor: The redditor associated with the streams. """ self.redditor = redditor def comments( self, **stream_options: str | int | dict[str, str] ) -> Generator[praw.models.Comment, None, None]: """Yield new comments as they become available. Comments are yielded oldest first. Up to 100 historical comments will initially be returned. Keyword arguments are passed to :func:`.stream_generator`. For example, to retrieve all new comments made by redditor u/spez, try: .. code-block:: python for comment in reddit.redditor("spez").stream.comments(): print(comment) """ return stream_generator(self.redditor.comments.new, **stream_options) def submissions( self, **stream_options: str | int | dict[str, str] ) -> Generator[praw.models.Submission, None, None]: """Yield new submissions as they become available. Submissions are yielded oldest first. Up to 100 historical submissions will initially be returned. Keyword arguments are passed to :func:`.stream_generator`. For example, to retrieve all new submissions made by redditor u/spez, try: .. code-block:: python for submission in reddit.redditor("spez").stream.submissions(): print(submission) """ return stream_generator(self.redditor.submissions.new, **stream_options)