From 5123b6f1985adb62e9cb8e790f4c43319f1ddf46 Mon Sep 17 00:00:00 2001 From: Zevaryx Date: Wed, 20 Apr 2022 00:33:32 -0600 Subject: [PATCH] Add subreddit command --- jarvis/cogs/reddit.py | 147 ++++++++++++++++++++++++++++++++++++++++++ jarvis/config.py | 1 + poetry.lock | 142 +++++++++++++++++++++++++++++++++++++--- pyproject.toml | 1 + 4 files changed, 283 insertions(+), 8 deletions(-) create mode 100644 jarvis/cogs/reddit.py diff --git a/jarvis/cogs/reddit.py b/jarvis/cogs/reddit.py new file mode 100644 index 0000000..debd164 --- /dev/null +++ b/jarvis/cogs/reddit.py @@ -0,0 +1,147 @@ +"""JARVIS Reddit cog.""" +import asyncio +import logging + +from asyncpraw import Reddit +from asyncprawcore.exceptions import Forbidden, NotFound +from dis_snek import InteractionContext, Permissions, Scale, Snake +from dis_snek.client.utils.misc_utils import get +from dis_snek.models.discord.channel import ChannelTypes, GuildText +from dis_snek.models.discord.components import ActionRow, Select, SelectOption +from dis_snek.models.snek.application_commands import ( + OptionTypes, + SlashCommand, + slash_option, +) +from dis_snek.models.snek.command import check +from jarvis_core.db import q +from jarvis_core.db.models import Subreddit, SubredditFollow + +from jarvis import const +from jarvis.config import JarvisConfig +from jarvis.utils.permissions import admin_or_permissions + +DEFAULT_USER_AGENT = f"python:JARVIS-Tasks:{const.__version__} (by u/zevaryx)" + + +class RedditCog(Scale): + """JARVIS Reddit Cog.""" + + def __init__(self, bot: Snake): + self.bot = bot + self.logger = logging.getLogger(__name__) + config = JarvisConfig.from_yaml() + config.reddit["user_agent"] = config.reddit.get("user_agent", DEFAULT_USER_AGENT) + self.api = Reddit(**config.reddit) + + reddit = SlashCommand(name="reddit", description="Manage Reddit follows") + + @reddit.subcommand(sub_cmd_name="follow", sub_cmd_description="Follow a Subreddit") + @slash_option( + name="name", + description="Subreddit display name", + opt_type=OptionTypes.STRING, + required=True, + ) + @slash_option( + name="channel", + description="Channel to post to", + opt_type=OptionTypes.CHANNEL, + channel_types=[ChannelTypes.GUILD_TEXT], + required=True, + ) + @check(admin_or_permissions(Permissions.MANAGE_GUILD)) + async def _reddit_follow(self, ctx: InteractionContext, name: str, channel: GuildText) -> None: + name = name.replace("r/", "") + if len(name) > 20: + await ctx.send("Invalid Subreddit name", ephemeral=True) + return + + if not isinstance(channel, GuildText): + await ctx.send("Channel must be a text channel", ephemeral=True) + return + + try: + subreddit = await self.api.subreddit(name) + except (NotFound, Forbidden) as e: + self.logger.debug(f"Subreddit {name} raised {e.__class__.__name__} on add") + await ctx.send("Subreddit may be private, quarantined, or nonexistent.", ephemeral=True) + return + + sr = await Subreddit.find_one(q(display_name=subreddit.display_name)) + if not sr: + sr = Subreddit(display_name=subreddit.display_name, over18=subreddit.over18) + await sr.commit() + + srf = SubredditFollow( + display_name=subreddit.display_name, + channel=channel.id, + guild=ctx.guild.id, + admin=ctx.author.id, + ) + await srf.commit() + + await ctx.send(f"Now following `r/{name}` in {channel.mention}") + + @reddit.subcommand(sub_cmd_name="unfollow", sub_cmd_description="Unfollow Subreddits") + @check(admin_or_permissions(Permissions.MANAGE_GUILD)) + async def _subreddit_unfollow(self, ctx: InteractionContext) -> None: + subs = SubredditFollow.find(q(guild=ctx.guild.id)) + subreddits = [] + async for sub in subs: + subreddits.append(sub) + if not subreddits: + await ctx.send("You need to follow a Subreddit first", ephemeral=True) + return + + options = [] + names = [] + for idx, subreddit in enumerate(subreddits): + sub = await Subreddit.find_one(q(display_name=subreddit.display_name)) + names.append(sub.display_name) + option = SelectOption(label=sub.display_name, value=str(idx)) + options.append(option) + + select = Select( + options=options, custom_id="to_delete", min_values=1, max_values=len(subreddits) + ) + + components = [ActionRow(select)] + block = "\n".join(x for x in names) + message = await ctx.send( + content=f"You are following the following subreddits:\n```\n{block}\n```\n\n" + "Please choose subreddits to unfollow", + components=components, + ) + + try: + context = await self.bot.wait_for_component( + check=lambda x: ctx.author.id == x.context.author.id, + messages=message, + timeout=60 * 5, + ) + for to_delete in context.context.values: + follow = get(subreddits, guild=ctx.guild.id, display_name=names[int(to_delete)]) + try: + await follow.delete() + except Exception: + self.logger.debug("Ignoring deletion error") + for row in components: + for component in row.components: + component.disabled = True + + block = "\n".join(names[int(x)] for x in context.context.values) + await context.context.edit_origin( + content=f"Unfollowed the following:\n```\n{block}\n```", components=components + ) + except asyncio.TimeoutError: + for row in components: + for component in row.components: + component.disabled = True + await message.edit(components=components) + + +def setup(bot: Snake) -> None: + """Add RedditCog to JARVIS""" + if JarvisConfig.from_yaml().reddit: + RedditCog(bot) diff --git a/jarvis/config.py b/jarvis/config.py index 2aec603..0c68d65 100644 --- a/jarvis/config.py +++ b/jarvis/config.py @@ -21,6 +21,7 @@ class JarvisConfig(CConfig): "gitlab_token": None, "max_messages": 1000, "twitter": None, + "reddit": None, } diff --git a/poetry.lock b/poetry.lock index 86c0f8b..aa9396a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -12,6 +12,14 @@ caio = ">=0.9.0,<0.10.0" [package.extras] develop = ["aiomisc", "asynctest", "pytest", "pytest-cov"] +[[package]] +name = "aiofiles" +version = "0.6.0" +description = "File support for asyncio." +category = "main" +optional = false +python-versions = "*" + [[package]] name = "aiohttp" version = "3.8.1" @@ -43,6 +51,25 @@ python-versions = ">=3.6" [package.dependencies] frozenlist = ">=1.1.0" +[[package]] +name = "aiosqlite" +version = "0.17.0" +description = "asyncio bridge to the standard sqlite3 module" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing_extensions = ">=3.7.2" + +[[package]] +name = "async-generator" +version = "1.10" +description = "Async generators and context managers for Python 3.5+" +category = "main" +optional = false +python-versions = ">=3.5" + [[package]] name = "async-timeout" version = "4.0.2" @@ -51,6 +78,61 @@ category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "asyncio-extras" +version = "1.3.2" +description = "Asynchronous generators, context managers and more for asyncio" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +async-generator = ">=1.3" + +[package.extras] +doc = ["sphinx-autodoc-typehints"] +test = ["pytest", "pytest-asyncio", "pytest-cov"] + +[[package]] +name = "asyncpraw" +version = "7.5.0" +description = "Async PRAW, an abbreviation for `Asynchronous Python Reddit API Wrapper`, is a python package that allows for simple access to reddit's API." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +aiofiles = "<=0.6.0" +aiosqlite = "<=0.17.0" +asyncio-extras = "<=1.3.2" +asyncprawcore = ">=2.1,<3" +update-checker = ">=0.18" + +[package.extras] +ci = ["coveralls"] +dev = ["packaging", "pre-commit", "sphinx", "sphinx-rtd-theme", "sphinxcontrib-trio", "asynctest (>=0.13.0)", "mock (>=0.8)", "pytest (>=2.7.3)", "pytest-asyncio", "pytest-vcr", "testfixtures (>4.13.2,<7)", "vcrpy (==4.1.1)"] +lint = ["pre-commit", "sphinx", "sphinx-rtd-theme", "sphinxcontrib-trio"] +readthedocs = ["sphinx", "sphinx-rtd-theme", "sphinxcontrib-trio"] +test = ["asynctest (>=0.13.0)", "mock (>=0.8)", "pytest (>=2.7.3)", "pytest-asyncio", "pytest-vcr", "testfixtures (>4.13.2,<7)", "vcrpy (==4.1.1)"] + +[[package]] +name = "asyncprawcore" +version = "2.3.0" +description = "Low-level asynchronous communication layer for Async PRAW 7+." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +aiohttp = "*" +yarl = "*" + +[package.extras] +ci = ["coveralls"] +dev = ["black", "flake8", "flynt", "pre-commit", "pydocstyle", "asynctest (>=0.13.0)", "mock (>=0.8)", "pytest", "pytest-vcr", "testfixtures (>4.13.2,<7)", "vcrpy (==4.0.2)"] +lint = ["black", "flake8", "flynt", "pre-commit", "pydocstyle"] +test = ["asynctest (>=0.13.0)", "mock (>=0.8)", "pytest", "pytest-vcr", "testfixtures (>4.13.2,<7)", "vcrpy (==4.0.2)"] + [[package]] name = "attrs" version = "21.4.0" @@ -183,15 +265,15 @@ python-versions = ">=3.5" [[package]] name = "jarvis-core" -version = "0.7.0" -description = "" +version = "0.8.0" +description = "JARVIS core" category = "main" optional = false python-versions = "^3.10" develop = false [package.dependencies] -dis-snek = "*" +aiohttp = "^3.8.1" motor = "^2.5.1" orjson = "^3.6.6" pytz = "^2022.1" @@ -202,7 +284,7 @@ umongo = "^3.1.0" type = "git" url = "https://git.zevaryx.com/stark-industries/jarvis/jarvis-core.git" reference = "main" -resolved_reference = "15521e73fb84fb4282a484ff0c9bb88ed1d144ae" +resolved_reference = "02b60a29ebbab91d9ad4cf876eb10c2aea6f2231" [[package]] name = "marshmallow" @@ -333,11 +415,11 @@ python-versions = ">=3.10" aiohttp = {version = "3.8.1", markers = "python_version >= \"3.6\""} aiosignal = {version = "1.2.0", markers = "python_version >= \"3.6\""} async-timeout = {version = "4.0.2", markers = "python_version >= \"3.6\""} -attrs = {version = "21.4.0", markers = "python_version >= \"3.6\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.5.0\" and python_version >= \"3.6\""} +attrs = {version = "21.4.0", markers = "python_version >= \"3.6\""} certifi = {version = "2021.10.8", markers = "python_version >= \"2.7\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.6.0\""} -charset-normalizer = {version = "2.0.12", markers = "python_full_version >= \"3.6.0\" and python_version >= \"3.6\""} +charset-normalizer = {version = "2.0.12", markers = "python_full_version >= \"3.6.0\""} frozenlist = {version = "1.3.0", markers = "python_version >= \"3.7\""} -idna = {version = "3.3", markers = "python_version >= \"3.6\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.6.0\" and python_version >= \"3.6\""} +idna = {version = "3.3", markers = "python_full_version >= \"3.6.0\""} multidict = {version = "6.0.2", markers = "python_version >= \"3.7\""} pycryptodome = {version = "3.14.1", markers = "python_version >= \"2.7\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.5.0\""} requests = {version = "2.27.1", markers = "python_version >= \"2.7\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.6.0\""} @@ -610,6 +692,22 @@ mongomock = ["mongomock"] motor = ["motor (>=2.0,<3.0)"] txmongo = ["txmongo (>=19.2.0)"] +[[package]] +name = "update-checker" +version = "0.18.0" +description = "A python module that will check for package updates." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +requests = ">=2.3.0" + +[package.extras] +dev = ["black", "flake8", "pytest (>=2.7.3)"] +lint = ["black", "flake8"] +test = ["pytest (>=2.7.3)"] + [[package]] name = "urllib3" version = "1.26.8" @@ -638,13 +736,17 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "ed0f4a3c6383dfad0b1aa4c2a45e14283972734400b9b29095a04dfb7d14eb7b" +content-hash = "2397e36142e58e3a9a9215a6dae4e37c054fba46f168fb945ec26974bb9c2f21" [metadata.files] aiofile = [ {file = "aiofile-3.7.4-py3-none-any.whl", hash = "sha256:0e2a524e4714efda47ce8964b13d4da94cf553411f9f6da813df615a4cd73d95"}, {file = "aiofile-3.7.4.tar.gz", hash = "sha256:0aefa1d91d000d3a20a515d153db2ebf713076c7c94edf2fca85d3d83316abc5"}, ] +aiofiles = [ + {file = "aiofiles-0.6.0-py3-none-any.whl", hash = "sha256:bd3019af67f83b739f8e4053c6c0512a7f545b9a8d91aaeab55e6e0f9d123c27"}, + {file = "aiofiles-0.6.0.tar.gz", hash = "sha256:e0281b157d3d5d59d803e3f4557dcc9a3dff28a4dd4829a9ff478adae50ca092"}, +] aiohttp = [ {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8"}, {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8"}, @@ -723,10 +825,30 @@ aiosignal = [ {file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"}, {file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"}, ] +aiosqlite = [ + {file = "aiosqlite-0.17.0-py3-none-any.whl", hash = "sha256:6c49dc6d3405929b1d08eeccc72306d3677503cc5e5e43771efc1e00232e8231"}, + {file = "aiosqlite-0.17.0.tar.gz", hash = "sha256:f0e6acc24bc4864149267ac82fb46dfb3be4455f99fe21df82609cc6e6baee51"}, +] +async-generator = [ + {file = "async_generator-1.10-py3-none-any.whl", hash = "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b"}, + {file = "async_generator-1.10.tar.gz", hash = "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"}, +] async-timeout = [ {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, ] +asyncio-extras = [ + {file = "asyncio_extras-1.3.2-py3-none-any.whl", hash = "sha256:839568ba07c3470c9aa2c441aa2417c108f7d3755862bc2bd39d69b524303993"}, + {file = "asyncio_extras-1.3.2.tar.gz", hash = "sha256:084b62bebc19c6ba106d438a274bbb5566941c469128cd4af1a85f00a2c81f8d"}, +] +asyncpraw = [ + {file = "asyncpraw-7.5.0-py3-none-any.whl", hash = "sha256:b40f3db3464077a7a7e30a89181ba15ba4c5bc550dc2642e815b235f42ad8eb2"}, + {file = "asyncpraw-7.5.0.tar.gz", hash = "sha256:61aabf05052472d8b29e0f0500a6ec8b483129374d36dad286d94e4b6864572d"}, +] +asyncprawcore = [ + {file = "asyncprawcore-2.3.0-py3-none-any.whl", hash = "sha256:46c52e6cfe91801a8c9490a0ee29a85cbc6713ccc535d5c704d448aee9729e5b"}, + {file = "asyncprawcore-2.3.0.tar.gz", hash = "sha256:2a4a2d1ca7f78c8fa7d4903e6bd18cfe96742ad1f167b59473f64be0e7060d5d"}, +] attrs = [ {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, @@ -1390,6 +1512,10 @@ umongo = [ {file = "umongo-3.1.0-py2.py3-none-any.whl", hash = "sha256:f6913027651ae673d71aaf54285f9ebf1e49a3f57662e526d029ba72e1a3fcd5"}, {file = "umongo-3.1.0.tar.gz", hash = "sha256:20c72f09edae931285c22c1928862af35b90ec639a4dac2dbf015aaaac00e931"}, ] +update-checker = [ + {file = "update_checker-0.18.0-py3-none-any.whl", hash = "sha256:cbba64760a36fe2640d80d85306e8fe82b6816659190993b7bdabadee4d4bbfd"}, + {file = "update_checker-0.18.0.tar.gz", hash = "sha256:6a2d45bb4ac585884a6b03f9eade9161cedd9e8111545141e9aa9058932acb13"}, +] urllib3 = [ {file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"}, {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, diff --git a/pyproject.toml b/pyproject.toml index 247125e..ae18795 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ pastypy = "^1.0.1" dateparser = "^1.1.1" aiofile = "^3.7.4" molter = "^0.11.0" +asyncpraw = "^7.5.0" [build-system] requires = ["poetry-core>=1.0.0"]