2026-02-01 09:31:38 +01:00

495 lines
17 KiB
Python

"""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 <read_only_application>`. 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)