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

Moderation streams #1051

Merged
merged 2 commits into from
Jan 2, 2020
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,27 @@ Unreleased
* Parameters ``mod_note`` and ``reason_id`` to
:meth:`.ThingModerationMixin.remove` to optionally apply a removal reason on
removal
* Add :class:`.SubredditModerationStream` to enable moderation streams
* Attribute ``stream`` to :class:`.SubredditModeration` to interact with new
moderation streams
* Add :meth:`.SubredditModerationStream.edited` to allow streaming
of :meth:`.SubredditModeration.edited`
* Add :meth:`.SubredditModerationStream.log` to allow streaming
of :meth:`.SubredditModeration.log`
* Add :meth:`.SubredditModerationStream.modmail_conversations` to allow
streaming of :meth:`.Modmail.conversations`
* Add :meth:`.SubredditModerationStream.modqueue` to allow streaming
of :meth:`.SubredditModeration.modqueue`
* Add :meth:`.SubredditModerationStream.reports` to allow streaming
of :meth:`.SubredditModeration.reports`
* Add :meth:`.SubredditModerationStream.spam` to allow streaming
of :meth:`.SubredditModeration.spam`
* Add :meth:`.SubredditModerationStream.unmoderated` to allow streaming
of :meth:`.SubredditModeration.unmoderated`
* Add :meth:`.SubredditModerationStream.unread` to allow streaming
of :meth:`.SubredditModeration.unread`
* Parameter ``exclude_before`` to :func:`.stream_generator` to allow
:meth:`.SubredditModerationStream.modmail_conversations` to work
* Parameters ``allowable_content`` and ``max_emojis`` to
:meth:`~.SubredditRedditorFlairTemplates.add`,
:meth:`~.SubredditLinkFlairTemplates.add`, and
Expand Down
1 change: 1 addition & 0 deletions docs/code_overview/other.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ instances of them bound to an attribute of one of the PRAW models.
other/subredditfilters
other/subredditquarantine
other/subredditstream
other/subredditmoderationstream
other/subredditstylesheet
other/subredditwidgets
other/subredditwiki
Expand Down
5 changes: 5 additions & 0 deletions docs/code_overview/other/subredditmoderationstream.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
SubredditModerationStream
=========================

.. autoclass:: praw.models.reddit.subreddit.SubredditModerationStream
:inherited-members:
205 changes: 205 additions & 0 deletions praw/models/reddit/subreddit.py
Original file line number Diff line number Diff line change
Expand Up @@ -1552,6 +1552,7 @@ def __init__(self, subreddit):

"""
self.subreddit = subreddit
self._stream = None

def accept_invite(self):
"""Accept an invitation as a moderator of the community."""
Expand Down Expand Up @@ -1657,6 +1658,23 @@ def modqueue(self, only=None, **generator_kwargs):
**generator_kwargs
)

@property
def stream(self):
"""Provide an instance of :class:`.SubredditModerationStream`.

Streams can be used to indefinitely retrieve Moderator only items from
:class:`.SubredditModeration` made to moderated subreddits, like:

.. code:: python

for log in reddit.subreddit('mod').mod.stream.log():
print("Mod: {}, Subreddit: {}".format(log.mod, log.subreddit))

"""
if self._stream is None:
self._stream = SubredditModerationStream(self.subreddit)
return self._stream

@cachedproperty
def removal_reasons(self):
"""Provide an instance of :class:`.SubredditRemovalReasons`.
Expand Down Expand Up @@ -1879,6 +1897,193 @@ def update(self, **settings):
)


class SubredditModerationStream:
"""Provides moderator streams."""

def __init__(self, subreddit):
"""Create a SubredditModerationStream instance.

:param subreddit: The moderated subreddit associated with the streams.

"""
self.subreddit = subreddit

def edited(self, only=None, **stream_options):
"""Yield edited comments and submissions as they become available.

:param only: If specified, one of ``'comments'``, or ``'submissions'``
to yield only results of that type.

Keyword arguments are passed to :func:`.stream_generator`.

For example, to retrieve all new edited submissions/comments made
to all moderated subreddits, try:

.. code:: python

for item in reddit.subreddit('mod').mod.stream.edited():
print(item)

"""
return stream_generator(
self.subreddit.mod.edited, only=only, **stream_options
)

def log(self, action=None, mod=None, **stream_options):
"""Yield moderator log entries as they become available.

:param action: If given, only return log entries for the specified
action.
:param mod: If given, only return log entries for actions made by the
passed in Redditor.

For example, to retrieve all new mod actions made to all moderated
subreddits, try:

.. code:: python

for log in reddit.subreddit('mod').mod.stream.log():
print("Mod: {}, Subreddit: {}".format(log.mod, log.subreddit))

"""
return stream_generator(
self.subreddit.mod.log,
action=action,
mod=mod,
attribute_name="id",
**stream_options
)

def modmail_conversations(
self, other_subreddits=None, sort=None, state=None, **stream_options
):
"""Yield unread new modmail messages as they become available.

:param other_subreddits: A list of :class:`.Subreddit` instances for
which to fetch conversations (default: None).
:param sort: Can be one of: mod, recent, unread, user
(default: recent).
:param state: Can be one of: all, archived, highlighted, inprogress,
mod, new, notifications, (default: all). "all" does not include
internal or archived conversations.

Keyword arguments are passed to :func:`.stream_generator`.

To print new mail in the unread modmail queue try:

.. code:: python

for message in reddit.subreddit('all').mod.stream.modmail_conversations():
print("From: {}, To: {}".format(message.owner, message.participant))

""" # noqa: E501
if self.subreddit == "mod":
self.subreddit = self.subreddit._reddit.subreddit("all")
return stream_generator(
self.subreddit.modmail.conversations,
other_subreddits=other_subreddits,
sort=sort,
state=state,
attribute_name="id",
exclude_before=True,
**stream_options
)

def modqueue(self, only=None, **stream_options):
"""Yield comments/submissions in the modqueue as they become available.

:param only: If specified, one of ``'comments'``, or ``'submissions'``
to yield only results of that type.

Keyword arguments are passed to :func:`.stream_generator`.

To print all new modqueue items try:

.. code:: python

for item in reddit.subreddit('mod').mod.stream.modqueue():
print(item)

"""
return stream_generator(
self.subreddit.mod.modqueue, only=only, **stream_options
)

def reports(self, only=None, **stream_options):
"""Yield reported comments and submissions as they become available.

:param only: If specified, one of ``'comments'``, or ``'submissions'``
to yield only results of that type.

Keyword arguments are passed to :func:`.stream_generator`.

To print new user and mod report reasons in the report queue try:

.. code:: python

for item in reddit.subreddit('mod').mod.stream.reports():
print(item)

"""
return stream_generator(
self.subreddit.mod.reports, only=only, **stream_options
)

def spam(self, only=None, **stream_options):
"""Yield spam comments and submissions as they become available.

:param only: If specified, one of ``'comments'``, or ``'submissions'``
to yield only results of that type.

Keyword arguments are passed to :func:`.stream_generator`.

To print new items in the spam queue try:

.. code:: python

for item in reddit.subreddit('mod').mod.stream.spam():
print(item)

"""
return stream_generator(
self.subreddit.mod.spam, only=only, **stream_options
)

def unmoderated(self, **stream_options):
"""Yield unmoderated submissions as they become available.

Keyword arguments are passed to :func:`.stream_generator`.

To print new items in the unmoderated queue try:

.. code:: python

for item in reddit.subreddit('mod').mod.stream.unmoderated():
print(item)

"""
return stream_generator(
self.subreddit.mod.unmoderated, **stream_options
)

def unread(self, **stream_options):
"""Yield unread old modmail messages as they become available.

Keyword arguments are passed to :func:`.stream_generator`.

See ``inbox`` for all messages.

To print new mail in the unread modmail queue try:

.. code:: python

for message in reddit.subreddit('mod').mod.stream.unread():
print("From: {}, To: {}".format(message.author, message.dest))

"""
return stream_generator(self.subreddit.mod.unread, **stream_options)


class SubredditQuarantine:
"""Provides subreddit quarantine related methods."""

Expand Down
16 changes: 7 additions & 9 deletions praw/models/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ def stream_generator(
pause_after: Optional[int] = None,
skip_existing: bool = False,
attribute_name: str = "fullname",
exclude_before: bool = False,
**function_kwargs: Any
) -> Generator[Any, None, None]:
"""Yield new items from ListingGenerators and ``None`` when paused.
Expand All @@ -105,6 +106,9 @@ def stream_generator(

:param attribute_name: The field to use as an id (default: "fullname").

:param exclude_before: When True does not pass ``params`` to ``functions``
(default: False).

Additional keyword arguments will be passed to ``function``.

.. note:: This function internally uses an exponential delay with jitter
Expand Down Expand Up @@ -177,15 +181,9 @@ def stream_generator(
if before_attribute is None:
limit -= without_before_counter
without_before_counter = (without_before_counter + 1) % 30
for item in reversed(
list(
function(
limit=limit,
params={"before": before_attribute},
**function_kwargs
)
)
):
if not exclude_before:
function_kwargs["params"] = {"before": before_attribute}
jarhill0 marked this conversation as resolved.
Show resolved Hide resolved
for item in reversed(list(function(limit=limit, **function_kwargs))):
attribute = getattr(item, attribute_name)
if attribute in seen_attributes:
continue
Expand Down
2 changes: 2 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ def filter_access_token(interaction, current_cassette):
"test_subreddit user_agent username refresh_token"
LilSpazJoekp marked this conversation as resolved.
Show resolved Hide resolved
).split()
}


placeholders["basic_auth"] = b64_string(
"{}:{}".format(placeholders["client_id"], placeholders["client_secret"])
)
Expand Down
Loading