diff --git a/CHANGES.rst b/CHANGES.rst index 0c4d8ed4..6a1c4ce4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,9 @@ Unreleased - :class:`.UserSubreddit` for the ``subreddit`` attribute of :class:`.Redditor`. - :meth:`.Reddit.username_available` checks if a username is available. +- :meth:`.trusted` to retrieve a :class:`.RedditorList` of trusted users. +- :meth:`.trust` to add a user to the trusted list. +- :meth:`.distrust` to remove a user from the trusted list. **Changed** diff --git a/asyncpraw/endpoints.py b/asyncpraw/endpoints.py index fa88767e..93d37989 100644 --- a/asyncpraw/endpoints.py +++ b/asyncpraw/endpoints.py @@ -14,6 +14,7 @@ "about_unmoderated": "r/{subreddit}/about/unmoderated/", "accept_mod_invite": "r/{subreddit}/api/accept_moderator_invite", "add_subreddit_rule": "api/add_subreddit_rule", + "add_whitelisted": "api/add_whitelisted", "approve": "api/approve/", "award_thing": "api/v2/gold/gild", "block": "api/block", @@ -143,6 +144,7 @@ "removal_reason": "api/v1/{subreddit}/removal_reasons/{id}", "removal_reasons_list": "api/v1/{subreddit}/removal_reasons", "remove_subreddit_rule": "api/remove_subreddit_rule", + "remove_whitelisted": "api/remove_whitelisted", "remove": "api/remove/", "reorder_subreddit_rules": "api/reorder_subreddit_rules", "report": "api/report/", @@ -184,6 +186,7 @@ "subscribe": "api/subscribe/", "suggested_sort": "api/set_suggested_sort/", "trophies": "api/v1/user/{user}/trophies", + "trusted": "prefs/trusted", "uncollapse": "api/uncollapse_message/", "unfriend": "r/{subreddit}/api/unfriend/", "unhide": "api/unhide/", diff --git a/asyncpraw/models/reddit/redditor.py b/asyncpraw/models/reddit/redditor.py index 7e07a88d..34e735f9 100644 --- a/asyncpraw/models/reddit/redditor.py +++ b/asyncpraw/models/reddit/redditor.py @@ -191,9 +191,36 @@ async def block(self): redditor = await reddit.redditor("spez") await redditor.block() + .. note:: + + Blocking a trusted user will remove that user from your trusted list. + + .. seealso:: + + :meth:`.trust` + """ await self._reddit.post(API_PATH["block_user"], params={"name": self.name}) + async def distrust(self): + """Remove the Redditor from your whitelist of trusted users. + + For example, to remove Redditor ``spez`` from your whitelist: + + .. code-block:: python + + redditor = await reddit.redditor("spez") + await redditor.distrust() + + .. seealso:: + + :meth:`.trust` + + """ + await self._reddit.post( + API_PATH["remove_whitelisted"], data={"name": self.name} + ) + async def friend(self, note: str = None): """Friend the Redditor. @@ -312,6 +339,46 @@ async def trophies(self) -> List["asyncpraw.models.Trophy"]: """ return list(await self._reddit.get(API_PATH["trophies"].format(user=self))) + async def trust(self): + """Add the Redditor to your whitelist of trusted users. + + Trusted users will always be able to send you PMs. + + Example usage: + + .. code-block:: python + + redditor = await reddit.redditor("AaronSw") + await redditor.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: + await reddit.user.preferences.update(accept_pms="everyone") + # Only accept private messages from trusted users: + await 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` + + """ + await self._reddit.post(API_PATH["add_whitelisted"], data={"name": self.name}) + async def unblock(self): """Unblock the Redditor. diff --git a/asyncpraw/models/user.py b/asyncpraw/models/user.py index 32e76521..06ec259e 100644 --- a/asyncpraw/models/user.py +++ b/asyncpraw/models/user.py @@ -173,3 +173,18 @@ def subreddits( return ListingGenerator( self._reddit, API_PATH["my_subreddits"], **generator_kwargs ) + + async def trusted(self) -> List["asyncpraw.models.Redditor"]: + """Return a RedditorList of trusted Redditors. + + To display the usernames of your trusted users and the times at which you + decided to trust them, try: + + .. code-block:: python + + trusted_users = reddit.user.trusted() + for user in trusted_users: + print(f"User: {user.name}, time: {user.date}") + + """ + return await self._reddit.get(API_PATH["trusted"]) diff --git a/tests/integration/cassettes/TestRedditorListings.test_trust_and_distrust.json b/tests/integration/cassettes/TestRedditorListings.test_trust_and_distrust.json new file mode 100644 index 00000000..6091d2ff --- /dev/null +++ b/tests/integration/cassettes/TestRedditorListings.test_trust_and_distrust.json @@ -0,0 +1,238 @@ +{ + "interactions": [ + { + "request": { + "body": [ + [ + "grant_type", + "password" + ], + [ + "password", + "" + ], + [ + "username", + "" + ] + ], + "headers": { + "AUTHORIZATION": [ + "Basic " + ], + "Accept-Encoding": [ + "identity" + ], + "Connection": [ + "close" + ], + "User-Agent": [ + " Async PRAW/7.2.1.dev0 asyncprawcore/2.2.0" + ] + }, + "method": "POST", + "uri": "https://www.reddit.com/api/v1/access_token" + }, + "response": { + "body": { + "string": "{\"access_token\": \"\", \"token_type\": \"bearer\", \"expires_in\": 3600, \"scope\": \"*\"}" + }, + "headers": { + "Accept-Ranges": "bytes", + "Cache-Control": "max-age=0, must-revalidate", + "Connection": "close", + "Content-Length": "121", + "Content-Type": "application/json; charset=UTF-8", + "Date": "Wed, 16 Jun 2021 23:11:00 GMT", + "Server": "snooserv", + "Set-Cookie": "edgebucket=HLr1AELfgZhlrI9pfX; Domain=reddit.com; Max-Age=63071999; Path=/; secure", + "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", + "Via": "1.1 varnish", + "X-Clacks-Overhead": "GNU Terry Pratchett", + "X-Moose": "majestic", + "x-content-type-options": "nosniff", + "x-frame-options": "SAMEORIGIN", + "x-ratelimit-remaining": "278", + "x-ratelimit-reset": "540", + "x-ratelimit-used": "22", + "x-xss-protection": "1; mode=block" + }, + "status": { + "code": 200, + "message": "OK" + }, + "url": "https://www.reddit.com/api/v1/access_token" + } + }, + { + "request": { + "body": [ + [ + "api_type", + "json" + ], + [ + "name", + "PyAPITestUser3" + ] + ], + "headers": { + "Accept-Encoding": [ + "identity" + ], + "Authorization": [ + "bearer " + ], + "User-Agent": [ + " Async PRAW/7.2.1.dev0 asyncprawcore/2.2.0" + ] + }, + "method": "POST", + "uri": "https://oauth.reddit.com/api/add_whitelisted?raw_json=1" + }, + "response": { + "body": { + "string": "{\"json\": {\"errors\": []}}" + }, + "headers": { + "Accept-Ranges": "bytes", + "Cache-Control": "private, s-maxage=0, max-age=0, must-revalidate, no-store, max-age=0, must-revalidate", + "Connection": "keep-alive", + "Content-Length": "24", + "Content-Type": "application/json; charset=UTF-8", + "Date": "Wed, 16 Jun 2021 23:11:01 GMT", + "Expires": "-1", + "Server": "snooserv", + "Set-Cookie": "session_tracker=fnrcrrppgpcrgmmrqe.0.1623885061043.Z0FBQUFBQmd5b1VGMHpzWVRIT2U4U0NVVi1HbkRZYmIwcWQ0UG9YV1pqWE0tTHI4eFE2TGd5ZWZuY0ZOWk5iYm5OMUR3MV83RmFHdWx6MEs4WVNBTmxUMFN4VDU1dDN5QVNYSzVsTUhiRGl2b0RJbjNBYjF0dUdfN0FtcU1LNU1HYWhJbUUxX0IycUU; Domain=reddit.com; Max-Age=7199; Path=/; expires=Thu, 17-Jun-2021 01:11:01 GMT; secure", + "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", + "Via": "1.1 varnish", + "X-Clacks-Overhead": "GNU Terry Pratchett", + "X-Moose": "majestic", + "x-content-type-options": "nosniff", + "x-frame-options": "SAMEORIGIN", + "x-ratelimit-remaining": "598.0", + "x-ratelimit-reset": "539", + "x-ratelimit-used": "2", + "x-ua-compatible": "IE=edge", + "x-xss-protection": "1; mode=block" + }, + "status": { + "code": 200, + "message": "OK" + }, + "url": "https://oauth.reddit.com/api/add_whitelisted?raw_json=1" + } + }, + { + "request": { + "body": null, + "headers": { + "Accept-Encoding": [ + "identity" + ], + "Authorization": [ + "bearer " + ], + "User-Agent": [ + " Async PRAW/7.2.1.dev0 asyncprawcore/2.2.0" + ] + }, + "method": "GET", + "uri": "https://oauth.reddit.com/prefs/trusted?raw_json=1" + }, + "response": { + "body": { + "string": "{\"kind\": \"UserList\", \"data\": {\"children\": [{\"date\": 1623885061.0, \"rel_id\": \"r9_2no6bp\", \"name\": \"PyAPITestUser3\", \"id\": \"t2_6c1xj\"}]}}" + }, + "headers": { + "Accept-Ranges": "bytes", + "Cache-Control": "private, s-maxage=0, max-age=0, must-revalidate, no-store, max-age=0, must-revalidate", + "Connection": "keep-alive", + "Content-Length": "135", + "Content-Type": "application/json; charset=UTF-8", + "Date": "Wed, 16 Jun 2021 23:11:01 GMT", + "Expires": "-1", + "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-Clacks-Overhead": "GNU Terry Pratchett", + "X-Moose": "majestic", + "x-content-type-options": "nosniff", + "x-frame-options": "SAMEORIGIN", + "x-ratelimit-remaining": "597.0", + "x-ratelimit-reset": "539", + "x-ratelimit-used": "3", + "x-ua-compatible": "IE=edge", + "x-xss-protection": "1; mode=block" + }, + "status": { + "code": 200, + "message": "OK" + }, + "url": "https://oauth.reddit.com/prefs/trusted?raw_json=1" + } + }, + { + "request": { + "body": [ + [ + "api_type", + "json" + ], + [ + "name", + "PyAPITestUser3" + ] + ], + "headers": { + "Accept-Encoding": [ + "identity" + ], + "Authorization": [ + "bearer " + ], + "User-Agent": [ + " Async PRAW/7.2.1.dev0 asyncprawcore/2.2.0" + ] + }, + "method": "POST", + "uri": "https://oauth.reddit.com/api/remove_whitelisted?raw_json=1" + }, + "response": { + "body": { + "string": "{}" + }, + "headers": { + "Accept-Ranges": "bytes", + "Cache-Control": "private, s-maxage=0, max-age=0, must-revalidate, no-store, max-age=0, must-revalidate", + "Connection": "keep-alive", + "Content-Length": "2", + "Content-Type": "application/json; charset=UTF-8", + "Date": "Wed, 16 Jun 2021 23:11:01 GMT", + "Expires": "-1", + "Server": "snooserv", + "Set-Cookie": "session_tracker=fnrcrrppgpcrgmmrqe.0.1623885061325.Z0FBQUFBQmd5b1VGWUNyTzZVYkpfLXVWa3NfcUt2ZlZqb1FzWDNqdGdBMlVUQ0NuWTFtVWFlcnc1M0tKVDZSeWdpeW1vWWp2d29nMXJreDlFY3phSlZFNU5XQXRDWDk2TEVrb2NNZ2Q5VmpMWlRtamJMcnN6N1lnNTV0TWpXU2MySEN4Sk9MRlZ0UVg; Domain=reddit.com; Max-Age=7199; Path=/; expires=Thu, 17-Jun-2021 01:11:01 GMT; secure", + "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", + "Via": "1.1 varnish", + "X-Clacks-Overhead": "GNU Terry Pratchett", + "X-Moose": "majestic", + "x-content-type-options": "nosniff", + "x-frame-options": "SAMEORIGIN", + "x-ratelimit-remaining": "596.0", + "x-ratelimit-reset": "539", + "x-ratelimit-used": "4", + "x-ua-compatible": "IE=edge", + "x-xss-protection": "1; mode=block" + }, + "status": { + "code": 200, + "message": "OK" + }, + "url": "https://oauth.reddit.com/api/remove_whitelisted?raw_json=1" + } + } + ], + "recorded_at": "2021-06-16T18:11:01", + "version": 1 +} diff --git a/tests/integration/cassettes/TestRedditorListings.test_trust_blocked_user.json b/tests/integration/cassettes/TestRedditorListings.test_trust_blocked_user.json new file mode 100644 index 00000000..119b908c --- /dev/null +++ b/tests/integration/cassettes/TestRedditorListings.test_trust_blocked_user.json @@ -0,0 +1,184 @@ +{ + "interactions": [ + { + "request": { + "body": [ + [ + "grant_type", + "password" + ], + [ + "password", + "" + ], + [ + "username", + "" + ] + ], + "headers": { + "AUTHORIZATION": [ + "Basic " + ], + "Accept-Encoding": [ + "identity" + ], + "Connection": [ + "close" + ], + "User-Agent": [ + " Async PRAW/7.2.1.dev0 asyncprawcore/2.2.0" + ] + }, + "method": "POST", + "uri": "https://www.reddit.com/api/v1/access_token" + }, + "response": { + "body": { + "string": "{\"access_token\": \"\", \"token_type\": \"bearer\", \"expires_in\": 3600, \"scope\": \"*\"}" + }, + "headers": { + "Accept-Ranges": "bytes", + "Cache-Control": "max-age=0, must-revalidate", + "Connection": "close", + "Content-Length": "121", + "Content-Type": "application/json; charset=UTF-8", + "Date": "Wed, 16 Jun 2021 23:11:06 GMT", + "Server": "snooserv", + "Set-Cookie": "edgebucket=bfpLCXgQxI5jwcbAYB; Domain=reddit.com; Max-Age=63071999; Path=/; secure", + "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", + "Via": "1.1 varnish", + "X-Clacks-Overhead": "GNU Terry Pratchett", + "X-Moose": "majestic", + "x-content-type-options": "nosniff", + "x-frame-options": "SAMEORIGIN", + "x-ratelimit-remaining": "275", + "x-ratelimit-reset": "535", + "x-ratelimit-used": "25", + "x-xss-protection": "1; mode=block" + }, + "status": { + "code": 200, + "message": "OK" + }, + "url": "https://www.reddit.com/api/v1/access_token" + } + }, + { + "request": { + "body": [ + [ + "api_type", + "json" + ] + ], + "headers": { + "Accept-Encoding": [ + "identity" + ], + "Authorization": [ + "bearer " + ], + "User-Agent": [ + " Async PRAW/7.2.1.dev0 asyncprawcore/2.2.0" + ] + }, + "method": "POST", + "uri": "https://oauth.reddit.com/api/block_user/?name=kn0thing&raw_json=1" + }, + "response": { + "body": { + "string": "{\"date\": 1623885066.0, \"icon_img\": \"https://styles.redditmedia.com/t5_3jgyq/styles/profileIcon_snoo55cb4e85-6b74-4219-9663-715859823828-headshot.png?width=256&height=256&crop=256:256,smart&s=41cc600375da6e0558114b762082c882d6e84138\", \"id\": \"t2_1wh0\", \"name\": \"kn0thing\"}" + }, + "headers": { + "Accept-Ranges": "bytes", + "Cache-Control": "private, s-maxage=0, max-age=0, must-revalidate, no-store, max-age=0, must-revalidate", + "Connection": "keep-alive", + "Content-Length": "270", + "Content-Type": "application/json; charset=UTF-8", + "Date": "Wed, 16 Jun 2021 23:11:06 GMT", + "Expires": "-1", + "Server": "snooserv", + "Set-Cookie": "session_tracker=mdnienprncokbqbfre.0.1623885066193.Z0FBQUFBQmd5b1VLc045VGZBSWJGLXVpZ1FZQzNfUlV1NElqSTM2Y3c5d2RTRmYzekZQeU1PbWdoYS1oRWxlV2tZZExIQ0hqQ2FKNVlyWlpGeFRXSFVGZXdFdXFhWmtzUEFQYnA5OEs0YXRLOEctYmlTOVZqYzJOd0tOQVVvWWExa1BwRU5EcC1LNkU; Domain=reddit.com; Max-Age=7199; Path=/; expires=Thu, 17-Jun-2021 01:11:06 GMT; secure", + "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", + "Via": "1.1 varnish", + "X-Clacks-Overhead": "GNU Terry Pratchett", + "X-Moose": "majestic", + "x-content-type-options": "nosniff", + "x-frame-options": "SAMEORIGIN", + "x-ratelimit-remaining": "595.0", + "x-ratelimit-reset": "534", + "x-ratelimit-used": "5", + "x-ua-compatible": "IE=edge", + "x-xss-protection": "1; mode=block" + }, + "status": { + "code": 200, + "message": "OK" + }, + "url": "https://oauth.reddit.com/api/block_user/?name=kn0thing&raw_json=1" + } + }, + { + "request": { + "body": [ + [ + "api_type", + "json" + ], + [ + "name", + "kn0thing" + ] + ], + "headers": { + "Accept-Encoding": [ + "identity" + ], + "Authorization": [ + "bearer " + ], + "User-Agent": [ + " Async PRAW/7.2.1.dev0 asyncprawcore/2.2.0" + ] + }, + "method": "POST", + "uri": "https://oauth.reddit.com/api/add_whitelisted?raw_json=1" + }, + "response": { + "body": { + "string": "{\"json\": {\"errors\": [[\"CANT_WHITELIST_AN_ENEMY\", \"You can't add a blocked user as a trusted user.\", \"name\"]]}}" + }, + "headers": { + "Accept-Ranges": "bytes", + "Cache-Control": "private, s-maxage=0, max-age=0, must-revalidate, no-store, max-age=0, must-revalidate", + "Connection": "keep-alive", + "Content-Length": "110", + "Content-Type": "application/json; charset=UTF-8", + "Date": "Wed, 16 Jun 2021 23:11:06 GMT", + "Expires": "-1", + "Server": "snooserv", + "Set-Cookie": "session_tracker=mdnienprncokbqbfre.0.1623885066383.Z0FBQUFBQmd5b1VLS1FWaXVpWXZwYzhudWJMc1htWF9lb20yWXpqTWloWDRCbzFqVW5jcTFUbEdkT3FOUHRwR2JhWHE0ZFVBcjB0cENWR1VmS1VyRm9fQzc0LUw4RjduLXJFaUZ1Sm1hYkNqYVFyLVR5cnJ6M1Nub3MwUmZaaGxoQ2RndmRmNnNva0I; Domain=reddit.com; Max-Age=7199; Path=/; expires=Thu, 17-Jun-2021 01:11:06 GMT; secure", + "Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload", + "Via": "1.1 varnish", + "X-Clacks-Overhead": "GNU Terry Pratchett", + "X-Moose": "majestic", + "x-content-type-options": "nosniff", + "x-frame-options": "SAMEORIGIN", + "x-ratelimit-remaining": "594.0", + "x-ratelimit-reset": "534", + "x-ratelimit-used": "6", + "x-ua-compatible": "IE=edge", + "x-xss-protection": "1; mode=block" + }, + "status": { + "code": 200, + "message": "OK" + }, + "url": "https://oauth.reddit.com/api/add_whitelisted?raw_json=1" + } + } + ], + "recorded_at": "2021-06-16T18:11:06", + "version": 1 +} diff --git a/tests/integration/models/reddit/test_redditor.py b/tests/integration/models/reddit/test_redditor.py index fc3f9919..0f2be5be 100644 --- a/tests/integration/models/reddit/test_redditor.py +++ b/tests/integration/models/reddit/test_redditor.py @@ -307,6 +307,23 @@ async def test_top(self): items = await self.async_list(redditor.top()) assert len(items) == 100 + async def test_trust_and_distrust(self): + self.reddit.read_only = False + with self.use_cassette(): + redditor = await self.reddit.redditor("PyAPITestUser3") + await redditor.trust() + redditor = (await self.reddit.user.trusted())[0] + await redditor.distrust() + + async def test_trust_blocked_user(self): + self.reddit.read_only = False + with self.use_cassette(): + redditor = await self.reddit.redditor("kn0thing") + await redditor.block() + with pytest.raises(RedditAPIException) as excinfo: + await redditor.trust() + assert "CANT_WHITELIST_AN_ENEMY" == excinfo.value.error_type + async def test_upvoted(self): self.reddit.read_only = False with self.use_cassette():