Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for mod notes #201

Merged
merged 1 commit into from Oct 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGES.rst
Expand Up @@ -14,6 +14,26 @@ Unreleased
- :meth:`.SubredditCollectionsModeration.create` keyword argument ``display_layout`` for
specifying a display layout when creating a :class:`.Collection`.
- :attr:`~.Message.parent` to get the parent of a :class:`.Message`.
- :class:`.ModNote` to represent a moderator note.
- :meth:`.ModNote.delete` to delete a single moderator note.
- :class:`.RedditModNotes` to interact with moderator notes from a :class:`.Reddit`
instance. This provides the ability to create and fetch notes for one or more
redditors from one or more subreddits.
- :class:`.RedditorModNotes` to interact with moderator notes from a :class:`.Redditor`
instance.
- :meth:`.RedditorModNotes.subreddits` to obtain moderator notes from multiple
subreddits for a single redditor.
- :class:`.SubredditModNotes` to interact with moderator notes from a
:class:`.Subreddit` instance.
- :meth:`.SubredditModNotes.redditors` to obtain moderator notes for multiple redditors
from a single subreddit.
- :meth:`~.BaseModNotes.create` to create a moderator note.
- :attr:`.Redditor.notes` to interact with :class:`.RedditorModNotes`.
- :attr:`.SubredditModeration.notes` to interact with :class:`.SubredditModNotes`.
- :meth:`~.ModNoteMixin.create_note` create a moderator note from a :class:`.Comment` or
:class:`.Submission`.
- :meth:`~.ModNoteMixin.author_notes` to view the moderator notes for the author of a
:class:`.Comment` or :class:`.Submission`.

**Changed**

Expand Down
2 changes: 2 additions & 0 deletions asyncpraw/endpoints.py
Expand Up @@ -108,6 +108,8 @@
"mentions": "message/mentions",
"message": "message/messages/{id}/",
"messages": "message/messages/",
"mod_notes": "api/mod/notes",
"mod_notes_bulk": "api/mod/notes/recent",
"moderated": "user/{user}/moderated_subreddits/",
"moderator_messages": "r/{subreddit}/message/moderator/",
"moderator_unread": "r/{subreddit}/message/moderator/unread/",
Expand Down
2 changes: 2 additions & 0 deletions asyncpraw/models/__init__.py
Expand Up @@ -11,6 +11,8 @@
from .listing.generator import ListingGenerator
from .listing.listing import Listing, ModeratorListing, ModmailConversationsListing
from .mod_action import ModAction
from .mod_note import ModNote
from .mod_notes import RedditModNotes, RedditorModNotes, SubredditModNotes
from .preferences import Preferences
from .reddit.collections import Collection
from .reddit.comment import Comment
Expand Down
23 changes: 18 additions & 5 deletions asyncpraw/models/listing/generator.py
Expand Up @@ -3,7 +3,7 @@
from typing import TYPE_CHECKING, Any, AsyncIterator, Dict, Optional, Union

from ..base import AsyncPRAWBase
from .listing import FlairListing
from .listing import FlairListing, ModNoteListing

if TYPE_CHECKING: # pragma: no cover
import asyncpraw
Expand Down Expand Up @@ -66,15 +66,28 @@ async def __anext__(self) -> Any:
self.yielded += 1
return self._listing[self._list_index - 1]

def _extract_sublist(self, listing):
if isinstance(listing, list):
return listing[1] # for submission duplicates
elif isinstance(listing, dict):
classes = [FlairListing, ModNoteListing]

for listing_type in classes:
if listing_type.CHILD_ATTRIBUTE in listing:
return listing_type(self._reddit, listing)
else:
raise ValueError(
"The generator returned a dictionary Async PRAW didn't"
" recognize. File a bug report at Async PRAW."
)
return listing

async def _next_batch(self):
if self._exhausted:
raise StopAsyncIteration()

self._listing = await self._reddit.get(self.url, params=self.params)
if isinstance(self._listing, list):
self._listing = self._listing[1] # for submission duplicates
elif isinstance(self._listing, dict):
self._listing = FlairListing(self._reddit, self._listing)
self._listing = self._extract_sublist(self._listing)
self._list_index = 0

if not self._listing:
Expand Down
13 changes: 13 additions & 0 deletions asyncpraw/models/listing/listing.py
Expand Up @@ -41,6 +41,19 @@ class ModeratorListing(Listing):
CHILD_ATTRIBUTE = "moderators"


class ModNoteListing(Listing):
"""Special Listing for handling :class:`.ModNote` lists."""

CHILD_ATTRIBUTE = "mod_notes"

@property
def after(self) -> Optional[Any]:
"""Return the next attribute or None."""
if not getattr(self, "has_next_page", True):
return None
return getattr(self, "end_cursor", None)


class ModmailConversationsListing(Listing):
"""Special Listing for handling :class:`.ModmailConversation` lists."""

Expand Down
70 changes: 70 additions & 0 deletions asyncpraw/models/mod_note.py
@@ -0,0 +1,70 @@
"""Provide the ModNote class."""

from ..endpoints import API_PATH
from .base import AsyncPRAWBase


class ModNote(AsyncPRAWBase):
"""Represent a moderator note.

.. include:: ../../typical_attributes.rst

=============== ====================================================================
Attribute Description
=============== ====================================================================
``action`` If this note represents a moderator action, this field indicates the
type of action. For example, ``"banuser"`` if the action was banning
a user.
``created_at`` Time the moderator note was created, represented in `Unix Time`_.
``description`` If this note represents a moderator action, this field indicates the
description of the action. For example, if the action was banning
the user, this is the ban reason.
``details`` If this note represents a moderator action, this field indicates the
details of the action. For example, if the action was banning the
user, this is the duration of the ban.
``id`` The ID of the moderator note.
``label`` The label applied to the note, currently one of:
``"ABUSE_WARNING"``, ``"BAN"``, ``"BOT_BAN"``, ``"HELPFUL_USER"``,
``"PERMA_BAN"``, ``"SOLID_CONTRIBUTOR"``, ``"SPAM_WARNING"``,
``"SPAM_WATCH"``, or ``None``.
``moderator`` The moderator who created the note.
``note`` The text of the note.
``reddit_id`` The fullname of the object this note is attributed to, or ``None``
if not set. If this note represents a moderators action, this is the
fullname of the object the action was performed on.
``subreddit`` The subreddit this note belongs to.
``type`` The type of note, currently one of: ``"APPROVAL"``, ``"BAN"``,
``"CONTENT_CHANGE"``, ``"INVITE"``, ``"MUTE"``, ``"NOTE"``,
``"REMOVAL"``, or ``"SPAM"``.
``user`` The redditor the note is for.
=============== ====================================================================

.. _unix time: https://en.wikipedia.org/wiki/Unix_time

"""

def __eq__(self, other):
"""Return whether the other instance equals the current."""
if isinstance(other, self.__class__):
return self.id == other.id
if isinstance(other, str):
return self.id == other
return super().__eq__(other)

async def delete(self):
"""Delete this note.

For example, to delete the last note for u/spez from r/test, try:

.. code-block:: python

for note in reddit.subreddit("test").mod.notes("spez"):
note.delete()

"""
params = {
"user": str(self.user),
"subreddit": str(self.subreddit),
"note_id": self.id,
}
await self._reddit.delete(API_PATH["mod_notes"], params=params)