Skip to content

Commit

Permalink
Merge pull request #28 from praw-dev/invited_moderators
Browse files Browse the repository at this point in the history
Add invited moderators endpoint
  • Loading branch information
LilSpazJoekp committed Jan 19, 2021
2 parents b33ad52 + 5877dfc commit 2d3b287
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGES.rst
Expand Up @@ -9,6 +9,7 @@ Unreleased
* Ability to submit image galleries with :meth:`.submit_gallery`.
* Ability to pass a gallery url to :meth:`.Reddit.submission`.
* Ability to specify modmail mute duration.
* Add method :meth:`.invited` to get invited moderators of a subreddit.
* Added :meth:`.Reddit.close` to close the requestor session.
* Ability to use :class:`.Reddit` as an asynchronous context manager that automatically
closes the requestor session on exit.
Expand Down
1 change: 1 addition & 0 deletions asyncpraw/endpoints.py
Expand Up @@ -72,6 +72,7 @@
"list_banned": "r/{subreddit}/about/banned/",
"list_contributor": "r/{subreddit}/about/contributors/",
"list_moderator": "r/{subreddit}/about/moderators/",
"list_invited_moderator": "/api/v1/{subreddit}/moderators_invited",
"list_muted": "r/{subreddit}/about/muted/",
"list_wikibanned": "r/{subreddit}/about/wikibanned/",
"list_wikicontributor": "r/{subreddit}/about/wikicontributors/",
Expand Down
2 changes: 1 addition & 1 deletion asyncpraw/models/__init__.py
Expand Up @@ -7,7 +7,7 @@
from .list.trophy import TrophyList
from .listing.domain import DomainListing
from .listing.generator import ListingGenerator
from .listing.listing import Listing
from .listing.listing import Listing, ModeratorListing
from .mod_action import ModAction
from .preferences import Preferences
from .reddit.collections import Collection
Expand Down
6 changes: 6 additions & 0 deletions asyncpraw/models/listing/listing.py
Expand Up @@ -33,3 +33,9 @@ class FlairListing(Listing):
def after(self) -> Optional[Any]:
"""Return the next attribute or None."""
return getattr(self, "next", None)


class ModeratorListing(Listing):
"""Special Listing for handling moderator lists."""

CHILD_ATTRIBUTE = "moderators"
30 changes: 30 additions & 0 deletions asyncpraw/models/reddit/subreddit.py
Expand Up @@ -2887,6 +2887,36 @@ async def invite(self, redditor, permissions=None, **other_settings):
url = API_PATH["friend"].format(subreddit=self.subreddit)
await self.subreddit._reddit.post(url, data=data)

def invited(self, redditor=None, **generator_kwargs):
"""Return a :class:`.ListingGenerator` for Redditors invited to be moderators.
:param redditor: When provided, return a list containing at most one
:class:`~.Redditor` instance. This is useful to confirm if a relationship
exists, or to fetch the metadata associated with a particular relationship
(default: None).
Additional keyword arguments are passed in the initialization of
:class:`.ListingGenerator`.
.. note::
Unlike other usages of :class:`.ListingGenerator`, ``limit`` has no effect
in the quantity returned. This endpoint always returns moderators in batches
of 25 at a time regardless of what ``limit`` is set to.
Usage:
.. code-block:: python
subreddit = await reddit.subreddit("NAME")
async for invited_mod in subreddit.moderator.invited():
print(invited_mod)
"""
generator_kwargs["params"] = {"username": redditor} if redditor else None
url = API_PATH["list_invited_moderator"].format(subreddit=self.subreddit)
return ListingGenerator(self.subreddit._reddit, url, **generator_kwargs)

async def leave(self):
"""Abdicate the moderator position (use with care).
Expand Down
14 changes: 14 additions & 0 deletions asyncpraw/objector.py
Expand Up @@ -118,6 +118,20 @@ def _objectify_dict(self, data):
parser = self.parsers[self._reddit.config.kinds["comment"]]
elif "collection_id" in data.keys():
parser = self.parsers["Collection"]
elif {"moderators", "moderatorIds", "allUsersLoaded", "subredditId"}.issubset(
data
):
data = snake_case_keys(data)
moderators = []
for mod_id in data["moderator_ids"]:
mod = snake_case_keys(data["moderators"][mod_id])
mod["mod_permissions"] = list(mod["mod_permissions"].keys())
moderators.append(mod)
data["moderators"] = moderators
parser = self.parsers["moderator-list"]
elif "username" in data.keys():
data["name"] = data.pop("username")
parser = self.parsers[self._reddit.config.kinds["redditor"]]
else:
if "user" in data:
parser = self.parsers[self._reddit.config.kinds["redditor"]]
Expand Down
1 change: 1 addition & 0 deletions asyncpraw/reddit.py
Expand Up @@ -436,6 +436,7 @@ def _prepare_objector(self):
"image": models.ImageWidget,
"menu": models.Menu,
"modaction": models.ModAction,
"moderator-list": models.ModeratorListing,
"moderators": models.ModeratorsWidget,
"more": models.MoreComments,
"post-flair": models.PostFlairWidget,
Expand Down
@@ -0,0 +1,111 @@
{
"interactions": [
{
"request": {
"body": [
[
"grant_type",
"refresh_token"
],
[
"refresh_token",
"<REFRESH_TOKEN>"
]
],
"headers": {
"AUTHORIZATION": [
"Basic <BASIC_AUTH>"
],
"Accept-Encoding": [
"identity"
],
"Connection": [
"close"
],
"User-Agent": [
"<USER_AGENT> Async PRAW/7.1.1.dev0 asyncprawcore/1.5.0"
]
},
"method": "POST",
"uri": "https://www.reddit.com/api/v1/access_token"
},
"response": {
"body": {
"string": "{\"access_token\": \"<ACCESS_TOKEN>\", \"token_type\": \"bearer\", \"expires_in\": 3600, \"scope\": \"account creddits edit flair history identity livemanage modconfig modcontributors modflair modlog modmail modothers modposts modself modtraffic modwiki mysubreddits privatemessages read report save structuredstyles submit subscribe vote wikiedit wikiread\"}"
},
"headers": {
"Accept-Ranges": "bytes",
"Connection": "close",
"Content-Length": "370",
"Content-Type": "application/json; charset=UTF-8",
"Date": "Mon, 18 Jan 2021 04:11:01 GMT",
"Server": "snooserv",
"Set-Cookie": "edgebucket=FH0aPNSSzuVo1ci6Zu; Domain=reddit.com; Max-Age=63071999; Path=/; secure",
"Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload",
"Via": "1.1 varnish",
"X-Moose": "majestic",
"cache-control": "max-age=0, must-revalidate",
"x-content-type-options": "nosniff",
"x-frame-options": "SAMEORIGIN",
"x-xss-protection": "1; mode=block"
},
"status": {
"code": 200,
"message": "OK"
},
"url": "https://www.reddit.com/api/v1/access_token"
}
},
{
"request": {
"body": null,
"headers": {
"Accept-Encoding": [
"identity"
],
"Authorization": [
"bearer <ACCESS_TOKEN>"
],
"User-Agent": [
"<USER_AGENT> Async PRAW/7.1.1.dev0 asyncprawcore/1.5.0"
]
},
"method": "GET",
"uri": "https://oauth.reddit.com/api/v1/<TEST_SUBREDDIT>/moderators_invited?limit=100&raw_json=1"
},
"response": {
"body": {
"string": "{\"after\": null, \"moderators\": {\"t2_3c96t\": {\"username\": \"DubTeeDub\", \"accountIcon\": \"https://styles.redditmedia.com/t5_21g9si/styles/profileIcon_snoo9d063b51-1bb4-4814-b3d9-b51f27626261-headshot.png?width=256\\u0026height=256\\u0026crop=256:256,smart\\u0026frame=1\\u0026s=342c612498e798627a05d5ea1cecc095c8dda841\", \"authorFlairText\": \"\", \"moddedAtUTC\": 1560998690, \"modPermissions\": {\"wiki\": true, \"all\": true, \"chat_operator\": true, \"chat_config\": true, \"posts\": true, \"access\": true, \"mail\": true, \"config\": true, \"flair\": true}, \"iconSize\": [256, 256], \"postKarma\": 697457, \"id\": \"t2_3c96t\"}}, \"moderatorIds\": [\"t2_3c96t\"], \"allUsersLoaded\": true, \"subredditId\": \"t5_3deqz\", \"before\": null}"
},
"headers": {
"Accept-Ranges": "bytes",
"Connection": "keep-alive",
"Content-Length": "690",
"Content-Type": "application/json; charset=UTF-8",
"Date": "Mon, 18 Jan 2021 04:11:01 GMT",
"Server": "snooserv",
"Set-Cookie": "csv=1; Max-Age=63072000; Domain=.reddit.com; Path=/; Secure; SameSite=None",
"Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload",
"Via": "1.1 varnish",
"X-Moose": "majestic",
"cache-control": "private, s-maxage=0, max-age=0, must-revalidate, no-store, max-age=0, must-revalidate",
"expires": "-1",
"x-content-type-options": "nosniff",
"x-frame-options": "SAMEORIGIN",
"x-ratelimit-remaining": "544.0",
"x-ratelimit-reset": "539",
"x-ratelimit-used": "56",
"x-reddit-decoy-snail": "3314,900",
"x-xss-protection": "1; mode=block"
},
"status": {
"code": 200,
"message": "OK"
},
"url": "https://oauth.reddit.com/api/v1/<TEST_SUBREDDIT>/moderators_invited?limit=100&raw_json=1"
}
}
],
"recorded_at": "2021-01-17T22:11:01",
"version": 1
}
11 changes: 11 additions & 0 deletions tests/integration/models/reddit/test_subreddit.py
Expand Up @@ -36,6 +36,7 @@
Subreddit,
SubredditMessage,
WikiPage,
ListingGenerator,
)

from ... import IntegrationTest
Expand Down Expand Up @@ -1671,6 +1672,16 @@ async def test_moderator_invite__no_perms(self, _):
await subreddit.moderator.invite(self.REDDITOR, permissions=[])
assert self.REDDITOR not in await subreddit.moderator()

@mock.patch("asyncio.sleep", return_value=None)
async def test_moderator_invited_moderators(self, _):
self.reddit.read_only = False
subreddit = await self.reddit.subreddit(pytest.placeholders.test_subreddit)
with self.use_cassette():
invited = subreddit.moderator.invited()
assert isinstance(invited, ListingGenerator)
async for moderator in invited:
assert isinstance(moderator, Redditor)

@mock.patch("asyncio.sleep", return_value=None)
async def test_moderator_leave(self, _):
self.reddit.read_only = False
Expand Down

0 comments on commit 2d3b287

Please sign in to comment.