diff --git a/.vscode/settings.json b/.vscode/settings.json index 941b5b7..a5b8b80 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,6 @@ { "python.analysis.typeCheckingMode": "basic", - "python.analysis.extraPaths": [ - "starbot" - ], + "python.analysis.extraPaths": ["nightly", "starbot"], "python.testing.pytestEnabled": false, "python.testing.unittestEnabled": true, "search.exclude": { diff --git a/docker-compose.yml b/docker-compose.yml index 8dba3ab..29ed882 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,3 +24,22 @@ services: window: 120s tty: true stop_signal: SIGINT + nightly: + build: + context: nightly + volumes: + - "./nightly/nightly:/app/nightly:ro" + env_file: + - .secrets/nightly.env + deploy: + resources: + limits: + cpus: '2' + memory: 200M + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + window: 120s + tty: true + stop_signal: SIGINT diff --git a/nightly/Dockerfile b/nightly/Dockerfile new file mode 100644 index 0000000..1d23338 --- /dev/null +++ b/nightly/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.11 as base +WORKDIR /app/ +COPY requirements.txt requirements.txt +RUN pip --no-cache-dir install -r requirements.txt +CMD ["python3", "-m", "nightly"] +COPY nightly nightly diff --git a/nightly/nightly/__main__.py b/nightly/nightly/__main__.py new file mode 100644 index 0000000..3b1b4fe --- /dev/null +++ b/nightly/nightly/__main__.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +import asyncio +import importlib +import os + +import discord +from discord.ext import commands + +from .bot import NightlyBot + + +async def _main(): + token = os.getenv("DISCORD_TOKEN") + if token is None: + print( + """\ +DISCORD_TOKEN environment variable is not set +edit .secrets/nightly.env with the following content (substitute with the token): +DISCORD_TOKEN = \ +""" + ) + exit(1) + bot = NightlyBot() + await bot.login(token) + yield bot + await bot.load_extension("nightly.night") + await bot.connect() + yield bot + + +async def aclose(client: discord.Client): + if not client.is_closed(): + await client.change_presence(status=discord.Status.offline) + await client.close() + + +def close(client: discord.Client, loop: asyncio.AbstractEventLoop): + loop.run_until_complete(aclose(client)) + + +def main(): + loop = asyncio.new_event_loop() + gen = _main() + bot: commands.Bot = loop.run_until_complete(anext(gen)) + discord.utils.setup_logging() + + async def complete(): + return await anext(gen) + + task = loop.create_task(complete()) + try: + loop.run_until_complete(task) + except (KeyboardInterrupt, InterruptedError, RuntimeError): + try: + close(bot, loop) + finally: + loop.run_until_complete(task) + + +if __name__ == "__main__": + main() diff --git a/nightly/nightly/bot.py b/nightly/nightly/bot.py new file mode 100644 index 0000000..8e15de3 --- /dev/null +++ b/nightly/nightly/bot.py @@ -0,0 +1,10 @@ +import discord +from discord.ext import commands + + +class NightlyBot(commands.Bot): + def __init__(self) -> None: + super().__init__( + command_prefix="🌟", + intents=discord.Intents(message_content=True, guild_messages=True, guild_reactions=True, guilds=True), + ) diff --git a/nightly/nightly/night.py b/nightly/nightly/night.py new file mode 100644 index 0000000..ff62eb0 --- /dev/null +++ b/nightly/nightly/night.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +import re + +import discord +from aiohttp import ClientSession +from discord.ext import commands + + +class Night(commands.Cog): + @commands.hybrid_command() + @commands.is_owner() + async def reload(self, ctx: commands.Context): + print("reload") + bot: commands.Bot = ctx.bot + try: + await bot.reload_extension("nightly.night") + except commands.ExtensionNotLoaded: + await ctx.reply("not loaded") + print("reloaded") + await ctx.reply("reloaded") + + @commands.hybrid_command() + @commands.is_owner() + async def sync(self, ctx: commands.Context): + await ctx.bot.tree.sync() + print("synced") + await ctx.reply("synced") + + @commands.hybrid_command() + async def leetcode(self, ctx: commands.Context, url: str): + match = re.search(r"/problems/([a-z0-1\-]*)", url) + if not match: + await ctx.reply("invalid url") + return + name = match.group(1) + async with ClientSession() as session: + async with session.post( + "https://leetcode.com/graphql/", + json={ + "query": "\n query consolePanelConfig($titleSlug: String!) {\n question(titleSlug: $titleSlug) {\n questionId\n questionFrontendId\n questionTitle\n enableDebugger\n enableRunCode\n enableSubmit\n enableTestMode\n exampleTestcaseList\n metaData\n }\n}\n ", + "variables": {"titleSlug": name}, + "operationName": "consolePanelConfig", + }, + ) as response: + if not response.ok: + await ctx.reply("error (idk)") + return + match await response.json(): + case {"data": {"question": {"questionFrontendId": qfi, "questionTitle": qt}}}: + title = f"{qfi}. {qt}" + print(title) + case _: + await ctx.reply("error (leetcode format wrong)") + return + if not isinstance(ctx.channel, discord.TextChannel): + await ctx.reply("not available outside text channels") + return + thread = await ctx.channel.create_thread(name=title, type=discord.ChannelType.public_thread) + message = await thread.send(f"https://leetcode.com/problems/{name}/") + if not ctx.bot_permissions.manage_messages: + await ctx.reply("cannot pin messages (missing permissions)", mention_author=False) + return + await message.pin() + + +async def setup(bot: commands.Bot): + global cog + cog = Night() + await bot.add_cog(cog) + + +async def teardown(bot: commands.Bot): + global cog + await bot.remove_cog(cog.qualified_name) + del cog + print("torn down") diff --git a/nightly/pyproject.toml b/nightly/pyproject.toml new file mode 100644 index 0000000..e69de29 diff --git a/nightly/requirements.txt b/nightly/requirements.txt new file mode 100644 index 0000000..ff1b845 --- /dev/null +++ b/nightly/requirements.txt @@ -0,0 +1,2 @@ +aiohttp>=3.7.4,<4 +discord.py~=2.3.2