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

697 lines
25 KiB
Python

"""Provides classes for interacting with moderator notes."""
from itertools import islice
from typing import TYPE_CHECKING, Any, Generator, List, Optional, Tuple, Union
from ..const import API_PATH
from .base import PRAWBase
from .listing.generator import ListingGenerator
from .reddit.comment import Comment
from .reddit.redditor import Redditor
from .reddit.submission import Submission
if TYPE_CHECKING: # pragma: no cover
import praw.models
RedditorType = Union[Redditor, str]
SubredditType = Union["praw.models.Subreddit", str]
ThingType = Union[Comment, Submission]
class BaseModNotes:
"""Provides base methods to interact with moderator notes."""
def __init__(
self,
reddit: "praw.Reddit",
):
"""Initialize a :class:`.BaseModNotes` instance.
:param reddit: An instance of :class:`.Reddit`.
"""
self._reddit = reddit
def _all_generator(
self,
redditor: RedditorType,
subreddit: SubredditType,
**generator_kwargs: Any,
):
PRAWBase._safely_add_arguments(
arguments=generator_kwargs,
key="params",
subreddit=subreddit,
user=redditor,
)
return ListingGenerator(self._reddit, API_PATH["mod_notes"], **generator_kwargs)
def _bulk_generator(
self, redditors: List[RedditorType], subreddits: List[SubredditType]
) -> Generator["praw.models.ModNote", None, None]:
subreddits_iter = iter(subreddits)
redditors_iter = iter(redditors)
while True:
subreddits_chunk = list(islice(subreddits_iter, 500))
users_chunk = list(islice(redditors_iter, 500))
if not any([subreddits_chunk, users_chunk]):
break
params = {
"subreddits": ",".join(map(str, subreddits_chunk)),
"users": ",".join(map(str, users_chunk)),
}
response = self._reddit.get(API_PATH["mod_notes_bulk"], params=params)
for note_dict in response["mod_notes"]:
yield self._reddit._objector.objectify(note_dict)
def _ensure_attribute(self, error_message: str, **attributes: Any) -> Any:
attribute, _value = attributes.popitem()
value = _value or getattr(self, attribute, None)
if value is None:
raise TypeError(error_message)
return value
def _notes(
self,
all_notes: bool,
redditors: List[RedditorType],
subreddits: List[SubredditType],
**generator_kwargs: Any,
) -> Generator["praw.models.ModNote", None, None]:
if all_notes:
for subreddit in subreddits:
for redditor in redditors:
yield from self._all_generator(
redditor, subreddit, **generator_kwargs
)
else:
yield from self._bulk_generator(redditors, subreddits)
def create(
self,
*,
label: Optional[str] = None,
note: str,
redditor: Optional[RedditorType] = None,
subreddit: Optional[SubredditType] = None,
thing: Optional[Union[Comment, Submission, str]] = None,
**other_settings: Any,
) -> "praw.models.ModNote":
"""Create a :class:`.ModNote` for a redditor in the specified subreddit.
:param label: The label for the note. As of this writing, this can be one of the
following: ``"ABUSE_WARNING"``, ``"BAN"``, ``"BOT_BAN"``,
``"HELPFUL_USER"``, ``"PERMA_BAN"``, ``"SOLID_CONTRIBUTOR"``,
``"SPAM_WARNING"``, ``"SPAM_WATCH"``, or ``None`` (default: ``None``).
:param note: The content of the note. As of this writing, this is limited to 250
characters.
:param redditor: The redditor to create the note for (default: ``None``).
.. note::
This parameter is required if ``thing`` is not provided or this is not
called from a :class:`.Redditor` instance (e.g.,
``reddit.redditor.notes``).
:param subreddit: The subreddit associated with the note (default: ``None``).
.. note::
This parameter is required if ``thing`` is not provided or this is not
called from a :class:`.Subreddit` instance (e.g.,
``reddit.subreddit.mod``).
:param thing: Either the fullname of a comment/submission, a :class:`.Comment`,
or a :class:`.Submission` to associate with the note.
:param other_settings: Additional keyword arguments can be provided to handle
new parameters as Reddit introduces them.
:returns: The new :class:`.ModNote` object.
For example, to create a note for u/spez in r/test:
.. code-block:: python
reddit.subreddit("test").mod.notes.create(
label="HELPFUL_USER", note="Test note", redditor="spez"
)
# or
reddit.redditor("spez").mod.notes.create(
label="HELPFUL_USER", note="Test note", subreddit="test"
)
# or
reddit.notes.create(
label="HELPFUL_USER", note="Test note", redditor="spez", subreddit="test"
)
"""
reddit_id = None
if thing:
if isinstance(thing, str):
reddit_id = thing
# this is to minimize the number of requests
if not (
getattr(self, "redditor", redditor)
and getattr(self, "subreddit", subreddit)
):
# only fetch if we are missing either redditor or subreddit
thing = next(self._reddit.info(fullnames=[thing]))
else:
reddit_id = thing.fullname
redditor = getattr(self, "redditor", redditor) or thing.author
subreddit = getattr(self, "subreddit", subreddit) or thing.subreddit
redditor = self._ensure_attribute(
"Either the 'redditor' or 'thing' parameters must be provided or this"
" method must be called from a Redditor instance (e.g., 'redditor.notes').",
redditor=redditor,
)
subreddit = self._ensure_attribute(
"Either the 'subreddit' or 'thing' parameters must be provided or this"
" method must be called from a Subreddit instance (e.g.,"
" 'subreddit.mod.notes').",
subreddit=subreddit,
)
data = {
"user": str(redditor),
"subreddit": str(subreddit),
"note": note,
}
if label:
data["label"] = label
if reddit_id:
data["reddit_id"] = reddit_id
data.update(other_settings)
return self._reddit.post(API_PATH["mod_notes"], data=data)
def delete(
self,
*,
delete_all: bool = False,
note_id: Optional[str] = None,
redditor: Optional[RedditorType] = None,
subreddit: Optional[SubredditType] = None,
):
"""Delete note(s) for a redditor.
:param delete_all: When ``True``, delete all notes for the specified redditor in
the specified subreddit (default: ``False``).
.. note::
This will make a request for each note.
:param note_id: The ID of the note to delete. This parameter is ignored if
``delete_all`` is ``True``.
:param redditor: The redditor to delete the note(s) for (default: ``None``). Can
be a :class:`.Redditor` instance or a redditor name.
.. note::
This parameter is required if this method is **not** called from a
:class:`.Redditor` instance (e.g., ``redditor.notes``).
:param subreddit: The subreddit to delete the note(s) from (default: ``None``).
Can be a :class:`.Subreddit` instance or a subreddit name.
.. note::
This parameter is required if this method is **not** called from a
:class:`.Subreddit` instance (e.g., ``reddit.subreddit.mod``).
For example, to delete a note with the ID
``"ModNote_d324b280-5ecc-435d-8159-3e259e84e339"``, try:
.. code-block:: python
reddit.subreddit("test").mod.notes.delete(
note_id="ModNote_d324b280-5ecc-435d-8159-3e259e84e339", redditor="spez"
)
# or
reddit.redditor("spez").notes.delete(
note_id="ModNote_d324b280-5ecc-435d-8159-3e259e84e339", subreddit="test"
)
# or
reddit.notes.delete(
note_id="ModNote_d324b280-5ecc-435d-8159-3e259e84e339",
subreddit="test",
redditor="spez",
)
To delete all notes for u/spez, try:
.. code-block:: python
reddit.subreddit("test").mod.notes.delete(delete_all=True, redditor="spez")
# or
reddit.redditor("spez").notes.delete(delete_all=True, subreddit="test")
# or
reddit.notes.delete(delete_all=True, subreddit="test", redditor="spez")
"""
redditor = self._ensure_attribute(
"Either the 'redditor' parameter must be provided or this method must be"
" called from a Redditor instance (e.g., 'redditor.notes').",
redditor=redditor,
)
subreddit = self._ensure_attribute(
"Either the 'subreddit' parameter must be provided or this method must be"
" called from a Subreddit instance (e.g., 'subreddit.mod.notes').",
subreddit=subreddit,
)
if not delete_all and note_id is None:
msg = "Either 'note_id' or 'delete_all' must be provided."
raise TypeError(msg)
if delete_all:
for note in self._notes(True, [redditor], [subreddit]):
note.delete()
else:
params = {
"user": str(redditor),
"subreddit": str(subreddit),
"note_id": note_id,
}
self._reddit.delete(API_PATH["mod_notes"], params=params)
class RedditorModNotes(BaseModNotes):
"""Provides methods to interact with moderator notes at the redditor level.
.. 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}")
"""
def __init__(self, reddit: "praw.Reddit", redditor: RedditorType):
"""Initialize a :class:`.RedditorModNotes` instance.
:param reddit: An instance of :class:`.Reddit`.
:param redditor: An instance of :class:`.Redditor`.
"""
super().__init__(reddit)
self.redditor = redditor
def subreddits(
self,
*subreddits: SubredditType,
all_notes: Optional[bool] = None,
**generator_kwargs: Any,
) -> Generator["praw.models.ModNote", None, None]:
"""Return notes for this :class:`.Redditor` from one or more subreddits.
:param subreddits: One or more subreddits to retrieve the notes from. Must be
either a :class:`.Subreddit` or a subreddit name.
:param all_notes: Whether to return all notes or only the latest note (default:
``True`` if only one subreddit is provided otherwise ``False``).
.. note::
Setting this to ``True`` will result in a request for each subreddit.
:returns: A generator that yields the most recent :class:`.ModNote` (or ``None``
if this redditor doesn't have any notes) per subreddit in their relative
order. If ``all_notes`` is ``True``, this will yield all notes or ``None``
from each subreddit for this redditor.
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}")
For example, the latest note for u/spez from r/test and r/redditdev can be
iterated through like so:
.. code-block:: python
redditor = reddit.redditor("spez")
subreddit = reddit.subreddit("redditdev")
for note in redditor.notes.subreddits("test", subreddit):
print(f"{note.label}: {note.note}")
For example, **all** the notes for u/spez in r/test and r/redditdev can be
iterated through like so:
.. code-block:: python
redditor = reddit.redditor("spez")
subreddit = reddit.subreddit("redditdev")
for note in redditor.notes.subreddits("test", subreddit, all_notes=True):
print(f"{note.label}: {note.note}")
"""
if len(subreddits) == 0:
msg = "At least 1 subreddit must be provided."
raise ValueError(msg)
if all_notes is None:
all_notes = len(subreddits) == 1
return self._notes(
all_notes,
[self.redditor] * len(subreddits),
list(subreddits),
**generator_kwargs,
)
class SubredditModNotes(BaseModNotes):
"""Provides methods to interact with moderator notes at the subreddit level.
.. note::
The authenticated user must be a moderator of this subreddit.
For example, all the notes for u/spez in r/test can be iterated through like so:
.. code-block:: python
subreddit = reddit.subreddit("test")
for note in subreddit.mod.notes.redditors("spez"):
print(f"{note.label}: {note.note}")
"""
def __init__(self, reddit: "praw.Reddit", subreddit: SubredditType):
"""Initialize a :class:`.SubredditModNotes` instance.
:param reddit: An instance of :class:`.Reddit`.
:param subreddit: An instance of :class:`.Subreddit`.
"""
super().__init__(reddit)
self.subreddit = subreddit
def redditors(
self,
*redditors: RedditorType,
all_notes: Optional[bool] = None,
**generator_kwargs: Any,
) -> Generator["praw.models.ModNote", None, None]:
"""Return notes from this :class:`.Subreddit` for one or more redditors.
:param redditors: One or more redditors to retrieve notes for. Must be either a
:class:`.Redditor` or a redditor name.
:param all_notes: Whether to return all notes or only the latest note (default:
``True`` if only one redditor is provided otherwise ``False``).
.. note::
Setting this to ``True`` will result in a request for each redditor.
:returns: A generator that yields the most recent :class:`.ModNote` (or ``None``
if the user doesn't have any notes in this subreddit) per redditor in their
relative order. If ``all_notes`` is ``True``, this will yield all notes for
each redditor.
For example, all the notes for u/spez in r/test can be iterated through like so:
.. code-block:: python
subreddit = reddit.subreddit("test")
for note in subreddit.mod.notes.redditors("spez"):
print(f"{note.label}: {note.note}")
For example, the latest note for u/spez and u/bboe from r/test can be iterated
through like so:
.. code-block:: python
subreddit = reddit.subreddit("test")
redditor = reddit.redditor("bboe")
for note in subreddit.mod.notes.redditors("spez", redditor):
print(f"{note.label}: {note.note}")
For example, **all** the notes for both u/spez and u/bboe in r/test can be
iterated through like so:
.. code-block:: python
subreddit = reddit.subreddit("test")
redditor = reddit.redditor("bboe")
for note in subreddit.mod.notes.redditors("spez", redditor, all_notes=True):
print(f"{note.label}: {note.note}")
"""
if len(redditors) == 0:
msg = "At least 1 redditor must be provided."
raise ValueError(msg)
if all_notes is None:
all_notes = len(redditors) == 1
return self._notes(
all_notes,
list(redditors),
[self.subreddit] * len(redditors),
**generator_kwargs,
)
class RedditModNotes(BaseModNotes):
"""Provides methods to interact with moderator notes at a global level.
.. note::
The authenticated user must be a moderator of the provided subreddit(s).
For example, the latest note for u/spez in r/redditdev and r/test, and for u/bboe in
r/redditdev can be iterated through like so:
.. code-block:: python
redditor = reddit.redditor("bboe")
subreddit = reddit.subreddit("redditdev")
pairs = [(subreddit, "spez"), ("test", "spez"), (subreddit, redditor)]
for note in reddit.notes(pairs=pairs):
print(f"{note.label}: {note.note}")
"""
def __call__(
self,
*,
all_notes: bool = False,
pairs: Optional[List[Tuple[SubredditType, RedditorType]]] = None,
redditors: Optional[List[RedditorType]] = None,
subreddits: Optional[List[SubredditType]] = None,
things: Optional[List[ThingType]] = None,
**generator_kwargs: Any,
) -> Generator["praw.models.ModNote", None, None]:
"""Get note(s) for each subreddit/user pair, or ``None`` if they don't have any.
:param all_notes: Whether to return all notes or only the latest note for each
subreddit/redditor pair (default: ``False``).
.. note::
Setting this to ``True`` will result in a request for each unique
subreddit/redditor pair. If ``subreddits`` and ``redditors`` are
provided, this will make a request equivalent to number of redditors
multiplied by the number of subreddits.
:param pairs: A list of subreddit/redditor tuples.
.. note::
Required if ``subreddits``, ``redditors``, nor ``things`` are provided.
:param redditors: A list of redditors to return notes for. This parameter is
used in tandem with ``subreddits`` to get notes from multiple subreddits for
each of the provided redditors.
.. note::
Required if ``items`` or ``things`` is not provided or if ``subreddits``
**is** provided.
:param subreddits: A list of subreddits to return notes for. This parameter is
used in tandem with ``redditors`` to get notes for multiple redditors from
each of the provided subreddits.
.. note::
Required if ``items`` or ``things`` is not provided or if ``redditors``
**is** provided.
:param things: A list of comments and/or submissions to return notes for.
:param generator_kwargs: Additional keyword arguments passed to the generator.
This parameter is ignored when ``all_notes`` is ``False``.
:returns: A generator that yields the most recent :class:`.ModNote` (or ``None``
if the user doesn't have any notes) per entry in their relative order. If
``all_notes`` is ``True``, this will yield all notes for each entry.
.. note::
This method will merge the subreddits and redditors provided from ``pairs``,
``redditors``, ``subreddits``, and ``things``.
.. note::
This method accepts :class:`.Redditor` instances or redditor names and
:class:`.Subreddit` instances or subreddit names where applicable.
For example, the latest note for u/spez in r/redditdev and r/test, and for
u/bboe in r/redditdev can be iterated through like so:
.. code-block:: python
redditor = reddit.redditor("bboe")
subreddit = reddit.subreddit("redditdev")
pairs = [(subreddit, "spez"), ("test", "spez"), (subreddit, redditor)]
for note in reddit.notes(pairs=pairs):
print(f"{note.label}: {note.note}")
For example, **all** the notes for u/spez and u/bboe in r/announcements,
r/redditdev, and r/test can be iterated through like so:
.. code-block:: python
redditor = reddit.redditor("bboe")
subreddit = reddit.subreddit("redditdev")
for note in reddit.notes(
redditors=["spez", redditor],
subreddits=["announcements", subreddit, "test"],
all_notes=True,
):
print(f"{note.label}: {note.note}")
For example, the latest note for the authors of the last 5 comments and
submissions from r/test can be iterated through like so:
.. code-block:: python
submissions = list(reddit.subreddit("test").new(limit=5))
comments = list(reddit.subreddit("test").comments(limit=5))
for note in reddit.notes(things=submissions + comments):
print(f"{note.label}: {note.note}")
.. note::
Setting ``all_notes`` to ``True`` will make a request for each redditor and
subreddit combination. The previous example will make 6 requests.
"""
if pairs is None:
pairs = []
if redditors is None:
redditors = []
if subreddits is None:
subreddits = []
if things is None:
things = []
if not (pairs + redditors + subreddits + things):
msg = "Either the 'pairs', 'redditors', 'subreddits', or 'things' parameters must be provided."
raise TypeError(msg)
if (
len(redditors) * len(subreddits) == 0
and len(redditors) + len(subreddits) > 0
):
raise TypeError(
"'redditors' must be non-empty if 'subreddits' is not empty."
if len(subreddits) > 0
else "'subreddits' must be non-empty if 'redditors' is not empty."
)
merged_redditors = []
merged_subreddits = []
items = (
pairs
+ [
(subreddit, redditor)
for redditor in set(redditors)
for subreddit in set(subreddits)
]
+ things
)
for item in items:
if isinstance(item, (Comment, Submission)):
merged_redditors.append(item.author.name)
merged_subreddits.append(item.subreddit.display_name)
elif isinstance(item, Tuple):
subreddit, redditor = item
merged_redditors.append(redditor)
merged_subreddits.append(subreddit)
else:
msg = f"Cannot get subreddit and author fields from type {type(item)}"
raise ValueError(msg)
return self._notes(
all_notes, merged_redditors, merged_subreddits, **generator_kwargs
)
def things(
self,
*things: ThingType,
all_notes: Optional[bool] = None,
**generator_kwargs: Any,
) -> Generator["praw.models.ModNote", None, None]:
"""Return notes associated with the author of a :class:`.Comment` or :class:`.Submission`.
:param things: One or more things to return notes on. Must be a
:class:`.Comment` or :class:`.Submission`.
:param all_notes: Whether to return all notes, or only the latest (default:
``True`` if only one thing is provided otherwise ``False``).
.. note::
Setting this to ``True`` will result in a request for each thing.
:returns: A generator that yields the most recent :class:`.ModNote` (or ``None``
if the user doesn't have any notes) per entry in their relative order. If
``all_notes`` is ``True``, this will yield all notes for each entry.
For example, to get the latest note for the authors of the top 25 submissions in
r/test:
.. code-block:: python
submissions = reddit.subreddit("test").top(limit=25)
for note in reddit.notes.things(*submissions):
print(f"{note.label}: {note.note}")
For example, to get the latest note for the authors of the last 25 comments in
r/test:
.. code-block:: python
comments = reddit.subreddit("test").comments(limit=25)
for note in reddit.notes.things(*comments):
print(f"{note.label}: {note.note}")
"""
subreddits = []
redditors = []
for thing in things:
subreddits.append(thing.subreddit)
redditors.append(thing.author)
if all_notes is None:
all_notes = len(things) == 1
return self._notes(all_notes, redditors, subreddits, **generator_kwargs)