diff --git a/.editorconfig b/.editorconfig index a882442..de71f36 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,3 +5,9 @@ end_of_line = lf insert_final_newline = true indent_style = space indent_size = 4 + +[*.yml] +indent_size = 2 + +[*.md] +indent_size = 2 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..30a2d55 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + pylint: + runs-on: ubuntu-latest + steps: + - name: Git checkout + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v3 + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Run pylint + run: pylint . + + autopep8: + runs-on: ubuntu-latest + steps: + - name: Git checkout + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v3 + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Run autopep8 + run: autopep8 --diff . diff --git a/.gitignore b/.gitignore index 4a51de0..d2993d5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # Byte-compiled / optimized / DLL files -__pycache__/ +*/__pycache__/ *.py[cod] *$py.class @@ -12,9 +12,7 @@ build/ develop-eggs/ dist/ downloads/ -cogs/__pycache__ .vscode/ -noncommands/__pycache__ eggs/ .eggs/ lib/ @@ -27,7 +25,7 @@ share/python-wheels/ *.egg-info/ .installed.cfg *.egg -config.yaml +config.toml MANIFEST # PyInstaller @@ -144,14 +142,6 @@ cython_debug/ .idea/* *.pyc *.pyc -/cogs/__pycache__/fun.cpython-37.pyc -/cogs/__pycache__/general.cpython-37.pyc -/cogs/__pycache__/help.cpython-37.pyc -/cogs/__pycache__/moderation.cpython-37.pyc -/cogs/__pycache__/owner.cpython-37.pyc -/cogs/__pycache__/template.cpython-37.pyc -/noncommands/__pycache__/imchecker.cpython-37.pyc -/noncommands/__pycache__/reminderLoop.cpython-37.pyc newperson.png *.pyc */*.pyc diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..c87ca2d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + "recommendations": [ + "ms-python.python", + "EditorConfig.EditorConfig", + "bungcip.better-toml", + "esbenp.prettier-vscode", + "vivaxy.vscode-conventional-commits", + "eamodio.gitlens" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 3cb2271..15fa23b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { - "python.analysis.extraPaths": ["./cogs"], - "python.formatting.autopep8Args": ["--max-line-length", "120"] + "python.formatting.provider": "autopep8", + "python.linting.pylintEnabled": true, + "python.linting.enabled": true } diff --git a/README.md b/README.md index fafd4db..dccc849 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,10 @@ You should see something similar to this result if you are using Windows Command Command Line Output -You can set up a python virtual environment that uses python 3.9 by running this command on windows +You can set up a python virtual environment by running this command on windows ```pwsh -&"C:\Program Files\Python39\python.exe" -m venv .venv +python -m venv .venv ``` ### 2. Getting a Discord Bot @@ -86,25 +86,18 @@ Click the Administrator Box, so it looks as shown: ### 4. Config File -Now go to your IDE or Text Editor, and make a copy of `config template.yaml`. Save the copy as `config.yaml`. This will be the bots config file, and is important to running on discord. +Now go to your IDE or Text Editor, and make a copy of `config_template.toml`. Save the copy as `config.toml`. This will be the bots config file, and is important to running on discord. DO NOT WRITE ON THE TEMPLATE -```yaml -token: "BOT_TOKEN" -application_id: "APPLICATION_ID" -owners: - - OWNER_ID -blacklist: - - 000000000000000000 -main_color: 0xD75BF4 -error: 0xE02B2B -success: 0x42F56C -warning: 0xF59E42 -info: 0x4299F5 +```toml +token = "BOT_TOKEN" +application_id = "APPLICATION_ID" +owners = ["OWNER_ID"] +blacklist = [] ``` -On the `config.yaml` file, you will be changing four lines: +On the `config.toml` file, you will be changing four lines: - `token` - `application_id` diff --git a/bot.py b/bot.py index a37c4f5..331f3b7 100644 --- a/bot.py +++ b/bot.py @@ -1,99 +1,115 @@ -import os -import platform -import random -import sys -import nextcord -import yaml -from apscheduler.schedulers.asyncio import AsyncIOScheduler -from apscheduler.triggers.cron import CronTrigger -from nextcord.ext import commands, tasks -from nextcord.ext.commands import Bot - -from noncommands import auto_code_block,quotes - - -with open("config.yaml") as file: - config = yaml.load(file, Loader=yaml.FullLoader) - -intents = nextcord.Intents.default().all() - -bot = Bot(intents=intents, command_prefix="!") - -scheduler = AsyncIOScheduler() -toSchedule = quotes.Quotes(bot) -autoCodeBlock = auto_code_block.AutoCodeBlock(bot) - -# The code in this even is executed when the bot is ready -@bot.event -async def on_ready(): - print(f"Logged in as {bot.user.name}") - print(f"nextcord.py API version: {nextcord.__version__}") - print(f"Python version: {platform.python_version()}") - print(f"Running on: {platform.system()} {platform.release()} ({os.name})") - print("-------------------") - await bot.change_presence(activity=nextcord.Game("/help")) - -# Removes the default help command of nextcord.py to be able to create our custom help command. -bot.remove_command("help") - -if __name__ == "__main__": - for file in os.listdir("./cogs"): - if file.endswith(".py"): - extension = file[:-3] - try: - bot.load_extension(f"cogs.{extension}") - print(f"Loaded extension '{extension}'") - except Exception as e: - exception = f"{type(e).__name__}: {e}" - print(f"Failed to load extension {extension}\n{exception}") - -# The code in this event is executed every time someone sends a message, with or without the prefix -@bot.event -async def on_message(message): - # Ignores if a command is being executed by a bot or by the bot itself - if message.author == bot.user or message.author.bot: - return - # Ignores if a command is being executed by a blacklisted user - - if message.author.id in config["blacklist"]: - return - - await autoCodeBlock.check_message(message) - - await bot.process_commands(message) - -# The code in this event is executed every time a command has been *successfully* executed -@bot.event -async def on_command_completion(ctx): - fullCommandName = ctx.command.qualified_name - split = fullCommandName.split(" ") - executedCommand = str(split[0]) - print( - f"Executed {executedCommand} command in {ctx.guild.name} (ID: {ctx.message.guild.id}) by {ctx.message.author} (ID: {ctx.message.author.id})") - -# The code in this event is executed every time a valid commands catches an error -@bot.event -async def on_command_error(context, error): - if isinstance(error, commands.CommandOnCooldown): - minutes, seconds = divmod(error.retry_after, 60) - hours, minutes = divmod(minutes, 60) - hours = hours % 24 - embed = nextcord.Embed( - title="Hey, please slow down!", - description=f"You can use this command again in {f'{round(hours)} hours' if round(hours) > 0 else ''} {f'{round(minutes)} minutes' if round(minutes) > 0 else ''} {f'{round(seconds)} seconds' if round(seconds) > 0 else ''}.", - color=config["error"] - ) - await context.send(embed=embed) - elif isinstance(error, commands.MissingPermissions): - embed = nextcord.Embed( - title="Error!", - description="You are missing the permission `" + ", ".join( - error.missing_perms) + "` to execute this command!", - color=config["error"] - ) - await context.send(embed=embed) - raise error - -scheduler.add_job(toSchedule.dailyQuote, CronTrigger(hour="8",minute="0",second="0",day_of_week="0-4",timezone="EST")) -scheduler.start() -bot.run(config["token"]) \ No newline at end of file +""" +This is the main file that runs the bot. +""" + + +import os +import platform +import nextcord +from apscheduler.schedulers.asyncio import AsyncIOScheduler +from apscheduler.triggers.cron import CronTrigger +from nextcord.ext import commands +from nextcord.ext.commands import Bot + +from noncommands import auto_code_block, quotes +from constants import ERROR_COLOR, config + + +intents = nextcord.Intents.default().all() + +bot = Bot(intents=intents, command_prefix="!") + +scheduler = AsyncIOScheduler() +toSchedule = quotes.Quotes(bot) +autoCodeBlock = auto_code_block.AutoCodeBlock(bot) + + +@bot.event +async def on_ready(): + """ + The code in this even is executed when the bot is ready + """ + + print(f"Logged in as {bot.user.name}") + print(f"nextcord.py API version: {nextcord.__version__}") + print(f"Python version: {platform.python_version()}") + print(f"Running on: {platform.system()} {platform.release()} ({os.name})") + print("-------------------") + await bot.change_presence(activity=nextcord.Game("/help")) + +# Removes the default help command of nextcord.py to be able to create our custom help command. +bot.remove_command("help") + +if __name__ == "__main__": + for file in os.listdir("./cogs"): + if file.endswith(".py") and not file.startswith("_"): + extension = file[:-3] + try: + bot.load_extension(f"cogs.{extension}") + print(f"Loaded extension '{extension}'") + except Exception as e: + exception = f"{type(e).__name__}: {e}" + print(f"Failed to load extension {extension}\n{exception}") + + +@bot.event +async def on_message(message): + """ + The code in this event is executed every time someone sends a message, with or without the prefix + """ + + # Ignores if a command is being executed by a bot or by the bot itself + if message.author == bot.user or message.author.bot: + return + # Ignores if a command is being executed by a blacklisted user + + if message.author.id in config["blacklist"]: + return + + await autoCodeBlock.check_message(message) + + await bot.process_commands(message) + + +@bot.event +async def on_command_completion(ctx): + """ + The code in this event is executed every time a command has been *successfully* executed + """ + full_command_name = ctx.command.qualified_name + split = full_command_name.split(" ") + executed_command = str(split[0]) + print( + f"Executed {executed_command} command in {ctx.guild.name} (ID: {ctx.message.guild.id}) by {ctx.message.author} (ID: {ctx.message.author.id})") + + +@bot.event +async def on_command_error(context, error): + """ + The code in this event is executed every time a valid commands catches an error + """ + + if isinstance(error, commands.CommandOnCooldown): + minutes, seconds = divmod(error.retry_after, 60) + hours, minutes = divmod(minutes, 60) + hours = hours % 24 + embed = nextcord.Embed( + title="Hey, please slow down!", + description=f"You can use this command again in {f'{round(hours)} hours' if round(hours) > 0 else ''} {f'{round(minutes)} minutes' if round(minutes) > 0 else ''} {f'{round(seconds)} seconds' if round(seconds) > 0 else ''}.", + color=ERROR_COLOR + ) + await context.send(embed=embed) + elif isinstance(error, commands.MissingPermissions): + embed = nextcord.Embed( + title="Error!", + description="You are missing the permission `" + ", ".join( + error.missing_perms) + "` to execute this command!", + color=ERROR_COLOR + ) + await context.send(embed=embed) + raise error + +scheduler.add_job(toSchedule.daily_quote, + CronTrigger(hour="8", minute="0", second="0", day_of_week="0-4", timezone="EST")) +scheduler.start() +bot.run(config["token"]) diff --git a/cogs/__init__.py b/cogs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cogs/channel.py b/cogs/channel.py index 5a08ef3..4afa472 100644 --- a/cogs/channel.py +++ b/cogs/channel.py @@ -1,23 +1,22 @@ +""" +This cog adds commands related to the creation and deletion of class channels. +""" + + import asyncio from collections import OrderedDict -from pathlib import Path from string import ascii_lowercase import sys from typing import Coroutine, TypedDict, Generator import json -import yaml +import re from nextcord.ext.commands import Cog from nextcord.ext.application_checks import has_permissions from nextcord.utils import find -import re import nextcord from nextcord import Interaction, SlashOption, PermissionOverwrite, Permissions, Colour, Embed, Attachment -with open("config.yaml") as file: - config = yaml.load(file, Loader=yaml.FullLoader) - - # classes we do not want to create channels for course_blacklist = ['106', '146', '388'] @@ -98,7 +97,7 @@ async def create_category(category_name, interaction: Interaction): } try: return await guild.create_category(category_name, overwrites=overwrites) - except: + except Exception: print("Issue with ", category_name, ". Error: ", sys.exc_info()[0]) @@ -112,13 +111,16 @@ async def create_channel(channel_name: str, category: nextcord.CategoryChannel, async def create_role(interaction: Interaction, role_name: str, permissions: Permissions = Permissions.none(), color=Colour.default()): """ - creates a role with specified permissions, with specifed name. + creates a role with specified permissions, with specified name. """ return await interaction.guild.create_role(name=role_name, permissions=permissions, colour=color) async def create_role_for_category(interaction: Interaction, category: nextcord.CategoryChannel, term: str): + """ + creates a role for a category and returns role object + """ role_name = f"{category.name.replace('-', ' ')} {term}" role = await create_role(interaction, role_name, color=nextcord.Colour.blue()) # gives basic permissions to a role for its assigned channel @@ -142,6 +144,10 @@ def reaction_emoji() -> Generator[str, None, None]: class ChannelManager(Cog, name="channelmanager"): + """ + This cog adds commands related to the creation and deletion of class channels. + """ + def __init__(self, bot): self.bot = bot @@ -153,9 +159,9 @@ async def import_classes(self, interaction: Interaction, file: Attachment = Slas """ try: - json = await read_class_json(file) - except Exception as e: - await interaction.response.send_message(f"Error: {e}", ephemeral=True) + json_data = await read_class_json(file) + except Exception as exception: + await interaction.response.send_message(f"Error: {exception}", ephemeral=True) return await interaction.response.defer() @@ -166,7 +172,7 @@ async def import_classes(self, interaction: Interaction, file: Attachment = Slas courses_by_category_name: OrderedDict[str, list[Section]] = OrderedDict() - for section in json["classes"]: + for section in json_data["classes"]: # first 3 numbers only (not L1,L2,L3) course_number = section["course"][0:3] @@ -218,7 +224,7 @@ async def import_classes(self, interaction: Interaction, file: Attachment = Slas coroutines.append(create_channel(channel_name, category, interaction, description)) channels_count += 1 - coroutines.append(create_role_for_category(interaction, category, json["term"])) + coroutines.append(create_role_for_category(interaction, category, json_data["term"])) roles_count += 1 coroutines.append( @@ -316,6 +322,8 @@ async def carl_class_roles(self, interaction: Interaction): await interaction.followup.send(embed=embed) -# And then we finally add the cog to the bot so that it can load, unload, reload and use it's content. def setup(bot): + """ + Add the cog to the bot so that it can load, unload, reload and use it's content. + """ bot.add_cog(ChannelManager(bot)) diff --git a/cogs/fun.py b/cogs/fun.py index eda732d..8cb5467 100644 --- a/cogs/fun.py +++ b/cogs/fun.py @@ -1,32 +1,29 @@ -import asyncio -import os +""" +This cog add fun little random things. +""" + + import random -import sys import aiohttp -import aiofiles -import hashlib -import yaml import requests -import uuid import inspirobot -import uwuify -import json import nextcord -from typing import Optional from nextcord.ext import commands -from nextcord import Interaction, SlashOption, ChannelType -from nextcord.abc import GuildChannel +from nextcord import Interaction, SlashOption -with open("config.yaml") as file: - config = yaml.load(file, Loader=yaml.FullLoader) +from constants import ERROR_COLOR, MAIN_COLOR, SUCCESS_COLOR class Fun(commands.Cog, name="fun"): + """ + This cog add fun little random things. + """ + def __init__(self, bot): self.bot = bot - + @nextcord.slash_command(name="dadjoke", description="Get one of the classics") async def dadjoke(self, interaction: Interaction, searchterm: str = SlashOption(description="A term to try and find a dadjoke about", default="", required=False)): """ @@ -40,7 +37,7 @@ async def dadjoke(self, interaction: Interaction, searchterm: str = SlashOption( await interaction.response.send_message(random.choice(json["results"])["joke"]) except: await interaction.response.send_message("I don't think I've heard a good one about that yet. Try something else.") - + @nextcord.slash_command(name="xkcd", description="Get an xkcd comic.") async def xkcd(self, interaction: Interaction, comicnumber: int = SlashOption(description="A specific xkcd comic, like '1' to get the first comic", default="", required=False)): """ @@ -55,7 +52,7 @@ async def xkcd(self, interaction: Interaction, comicnumber: int = SlashOption(de await interaction.response.send_message(r.json()['img']) except: await interaction.response.send_message("I can't find that xkcd comic, try another.") - + @nextcord.slash_command(name="iswanted", description="See if someone is on the FBI's most wanted list.") async def iswanted(self, interaction: Interaction, name: str = SlashOption(description="The name of the person you want to check", required=True)): """ @@ -68,7 +65,7 @@ async def iswanted(self, interaction: Interaction, name: str = SlashOption(descr await interaction.response.send_message(name + " might be wanted by the FBI:\n" + url) except: await interaction.response.send_message("No one with that name is currently wanted by the FBI") - + @nextcord.slash_command(name="eightball", description="Ask any yes/no question and get an answer.") async def eight_ball(self, interaction: Interaction, question: str = SlashOption(description="The question you want to ask", required=True)): """ @@ -82,7 +79,7 @@ async def eight_ball(self, interaction: Interaction, question: str = SlashOption embed = nextcord.Embed( title=f"**My Answer to '{question}':**", description=f"{answers[random.randint(0, len(answers) - 1)]}", - color=config["success"] + color=SUCCESS_COLOR ) embed.set_footer( text=f"Question asked by: {interaction.user}" @@ -100,16 +97,16 @@ async def randomfact(self, interaction: Interaction): async with session.get("https://uselessfacts.jsph.pl/random.json?language=en") as request: if request.status == 200: data = await request.json() - embed = nextcord.Embed(description=data["text"], color=config["main_color"]) + embed = nextcord.Embed(description=data["text"], color=MAIN_COLOR) await interaction.response.send_message(embed=embed) else: embed = nextcord.Embed( title="Error!", description="There is something wrong with the API, please try again later", - color=config["error"] + color=ERROR_COLOR ) await interaction.response.send_message(embed=embed) - + @nextcord.slash_command(name="inspire", description="Get an inspirational poster courtesy of https://inspirobot.me/") async def inspire(self, interaction: Interaction): """ @@ -117,7 +114,7 @@ async def inspire(self, interaction: Interaction): """ quote = inspirobot.generate() await interaction.response.send_message(quote.url) - + @nextcord.slash_command(name="wisdom", description="Get some wisdom courtesy of https://inspirobot.me/") async def wisdom(self, interaction: Interaction): """ @@ -127,7 +124,7 @@ async def wisdom(self, interaction: Interaction): res = "" for quote in flow: res += quote.text + "\n" - + await interaction.response.send_message(res) @nextcord.slash_command(name="advice", description="Get some advice.") @@ -138,5 +135,9 @@ async def advice(self, interaction: Interaction): r = requests.get("https://api.adviceslip.com/advice") await interaction.response.send_message(r.json()['slip']['advice']) + def setup(bot): - bot.add_cog(Fun(bot)) \ No newline at end of file + """ + Add the cog to the bot so that it can load, unload, reload and use it's content. + """ + bot.add_cog(Fun(bot)) diff --git a/cogs/general.py b/cogs/general.py index 7a093b4..092505f 100644 --- a/cogs/general.py +++ b/cogs/general.py @@ -1,21 +1,22 @@ -import os +""" +This cog adds meta commands having to do with the bot itself. +""" + + import platform -import sys -import json -import yaml -import random -from nextcord.ext import commands -from noncommands import summarizer, quotes import nextcord -from typing import Optional from nextcord.ext import commands -from nextcord import Interaction, SlashOption, ChannelType -from nextcord.abc import GuildChannel +from nextcord import Interaction, SlashOption + +from noncommands import summarizer +from constants import config, SUCCESS_COLOR -with open("config.yaml") as file: - config = yaml.load(file, Loader=yaml.FullLoader) -class general(commands.Cog, name="general"): +class General(commands.Cog, name="general"): + """ + This cog adds meta commands having to do with the bot itself. + """ + def __init__(self, bot): self.bot = bot @@ -26,7 +27,7 @@ async def info(self, interaction: Interaction): """ embed = nextcord.Embed( description="The server's most helpful member.", - color=config["success"] + color=SUCCESS_COLOR ) embed.set_author( name="Bot Information" @@ -66,10 +67,10 @@ async def serverinfo(self, interaction: Interaction): embed = nextcord.Embed( title="**Server Name:**", description=f"{server}", - color=config["success"] + color=SUCCESS_COLOR ) - if server.icon != None: + if server.icon is not None: embed.set_thumbnail( url=server.icon.url ) @@ -100,7 +101,7 @@ async def ping(self, interaction: Interaction): [No Arguments] Check if the bot is alive. """ embed = nextcord.Embed( - color=config["success"] + color=SUCCESS_COLOR ) embed.add_field( name="Pong!", @@ -110,7 +111,7 @@ async def ping(self, interaction: Interaction): embed.set_footer( text=f"Pong request by {interaction.user}" ) - await interaction.response.send_message(embed=embed) + await interaction.response.send_message(embed=embed) @nextcord.slash_command(name="invite", description="Get the invite link of the bot to be able to invite it to another server.") async def invite(self, interaction: Interaction): @@ -119,7 +120,6 @@ async def invite(self, interaction: Interaction): """ await interaction.response.send_message(f"Invite me by clicking here: https://discordapp.com/oauth2/authorize?&client_id={config['application_id']}&scope=bot&permissions=8") - @nextcord.slash_command(name="tldrchannel", description="Get a TLDR of X number of past messages on the channel.") async def tldrchannel(self, interaction: Interaction, number: int = SlashOption(description="The number of past messages to summarize", required=True, min_value=5, max_value=200)): """ @@ -129,20 +129,23 @@ async def tldrchannel(self, interaction: Interaction, number: int = SlashOption( messages = await interaction.channel.history(limit=number).flatten() text = ". ".join([m.content for m in messages]) text = text.replace(".. ", ". ") - embed = summarizer.getSummaryText(config, text) + embed = summarizer.get_summary_text(text) await interaction.response.send_message(embed=embed) - + @nextcord.slash_command(name="tldr", description="Get a TLDR of a web page.") async def tldr(self, interaction: Interaction, url: str = SlashOption(description="The URL of the web page to summarize", required=True)): """ [URL] Get a TLDR a web page. """ try: - await interaction.response.send_message(embed=summarizer.getSummaryUrl(config, url)) + await interaction.response.send_message(embed=summarizer.get_summary_url(url)) except: await interaction.response.send_message("There's something odd about that link. Either they won't let me read it or you sent it wrongly.") def setup(bot): - bot.add_cog(general(bot)) + """ + Add the cog to the bot so that it can load, unload, reload and use it's content. + """ + bot.add_cog(General(bot)) diff --git a/cogs/help.py b/cogs/help.py index 9019706..e677e3d 100644 --- a/cogs/help.py +++ b/cogs/help.py @@ -1,28 +1,32 @@ -import os -import sys -import yaml -import nextcord -from typing import Optional -from nextcord.ext import commands -from nextcord import Interaction, SlashOption, ChannelType -from nextcord.abc import GuildChannel - - -with open("config.yaml") as file: - config = yaml.load(file, Loader=yaml.FullLoader) - -class Help(commands.Cog, name="help"): - def __init__(self, bot): - self.bot = bot - - @nextcord.slash_command(name="help", description="How do I find out what all the commands are?") - async def help(self, interaction: Interaction): - """ - How do I find out what all the commands are? - """ - - await interaction.response.send_message("This bot uses slash commands! Type a `/` into the chat, click my icon, and you should see all the commands I can take!") - - -def setup(bot): - bot.add_cog(Help(bot)) +""" +This cog adds a help command. +""" + + +import nextcord +from nextcord.ext import commands +from nextcord import Interaction + + +class Help(commands.Cog, name="help"): + """ + This cog adds a help command. + """ + + def __init__(self, bot): + self.bot = bot + + @nextcord.slash_command(name="help", description="How do I find out what all the commands are?") + async def help(self, interaction: Interaction): + """ + How do I find out what all the commands are? + """ + + await interaction.response.send_message("This bot uses slash commands! Type a `/` into the chat, click my icon, and you should see all the commands I can take!") + + +def setup(bot): + """ + Add the cog to the bot so that it can load, unload, reload and use it's content. + """ + bot.add_cog(Help(bot)) diff --git a/cogs/quotes.py b/cogs/quotes.py index d9380b9..a18147a 100644 --- a/cogs/quotes.py +++ b/cogs/quotes.py @@ -1,19 +1,20 @@ -import os -import sys -import yaml -from nextcord.ext import commands -from noncommands import quotes +""" +This cog adds commands for getting and creating quotes. +""" + + import nextcord -from typing import Optional from nextcord.ext import commands -from nextcord import Interaction, SlashOption, ChannelType -from nextcord.abc import GuildChannel +from nextcord import Interaction, SlashOption + +from noncommands import quotes -with open("config.yaml") as file: - config = yaml.load(file, Loader=yaml.FullLoader) -# Here we name the cog and create a new class for the cog. class Quotes(commands.Cog, name="quotes"): + """ + This cog adds commands for getting and creating quotes. + """ + def __init__(self, bot): self.bot = bot @@ -23,8 +24,8 @@ async def quote(self, interaction: Interaction, keyword: str = SlashOption(descr """ [(Optional) Search text] Searches CS quotes by keyword, or search one at random. """ - quoteClass = quotes.Quotes(self.bot) - random_quote = await quoteClass.quote(keyword) + quote_class = quotes.Quotes(self.bot) + random_quote = await quote_class.quote(keyword) await interaction.response.send_message(random_quote) @nextcord.slash_command(name="newquote", description="Creates a new quote to be put into the list of CS quotes.") @@ -32,11 +33,14 @@ async def newquote(self, interaction: Interaction, quote: str = SlashOption(desc """ [(Required) Quote] Creates a new quote to be put into the list of CS quotes. """ - quoteClass = quotes.Quotes(self.bot) - newquote = await quoteClass.newquote(quote) + quote_class = quotes.Quotes(self.bot) + newquote = await quote_class.newquote(quote) await interaction.response.send_message("Quote submitted! Quote: " + newquote) - -# And then we finally add the cog to the bot so that it can load, unload, reload and use it's content. + + def setup(bot): + """ + Add the cog to the bot so that it can load, unload, reload and use it's content. + """ bot.add_cog(Quotes(bot)) diff --git a/cogs/ratemyprofessor.py b/cogs/ratemyprofessor.py index fbefaee..87bd7a0 100644 --- a/cogs/ratemyprofessor.py +++ b/cogs/ratemyprofessor.py @@ -1,28 +1,31 @@ -import os -import sys -import yaml -from nextcord.ext import commands +""" +This cog adds a command for accessing ratemyprofessor +""" + + import ratemyprofessor import nextcord -from typing import Optional from nextcord.ext import commands -from nextcord import Interaction, SlashOption, ChannelType -from nextcord.abc import GuildChannel +from nextcord import SlashOption +from constants import SUCCESS_COLOR + +PROF_IMAGES = { + "William Sverdlik": "https://www.emich.edu/computer-science/images/faculty/w-sverdlik.jpg", + "Zenia Bahorski": "https://www.emich.edu/computer-science/images/faculty/zbahorski.jpg", + "Andrii Kashliev": "https://www.emich.edu/computer-science/images/faculty/andreii-kashliev.jpg", + "Krish Narayanan": "https://www.emich.edu/computer-science/images/faculty/k-narayanan.jpg", + "Siyuan Jiang": "https://www.emich.edu/computer-science/images/faculty/s-jiang.jpg", +} -with open("config.yaml") as file: - config = yaml.load(file, Loader=yaml.FullLoader) class RateMyProfessor(commands.Cog, name="rate my professor"): + """ + This cog adds a command for accessing ratemyprofessor + """ + def __init__(self, bot): self.bot = bot - self.profImages = { - "William Sverdlik": "https://www.emich.edu/computer-science/images/faculty/w-sverdlik.jpg", - "Zenia Bahorski": "https://www.emich.edu/computer-science/images/faculty/zbahorski.jpg", - "Andrii Kashliev": "https://www.emich.edu/computer-science/images/faculty/andreii-kashliev.jpg", - "Krish Narayanan": "https://www.emich.edu/computer-science/images/faculty/k-narayanan.jpg", - "Siyuan Jiang": "https://www.emich.edu/computer-science/images/faculty/s-jiang.jpg", - } @nextcord.slash_command(name="rmp", description="Check out what RateMyProfessor has to say about a professor!") async def rmp(self, interaction: nextcord.Interaction, professorname: str = SlashOption(name="professorname", description="The name of the professor you want to look up", required=True)): @@ -33,23 +36,27 @@ async def rmp(self, interaction: nextcord.Interaction, professorname: str = Slas EMU = ratemyprofessor.get_school_by_name("Eastern Michigan University") prof = ratemyprofessor.get_professor_by_school_and_name(EMU, professorname) - ratingsBest = sorted([rating for rating in prof.get_ratings() if rating.comment], key=lambda rating: (rating.rating, rating.date)) - bestRating = ratingsBest[-1] + ratings_best = sorted([rating for rating in prof.get_ratings() if rating.comment], + key=lambda rating: (rating.rating, rating.date)) + best_rating = ratings_best[-1] - ratingsWorst = sorted([rating for rating in prof.get_ratings() if rating.comment], key=lambda rating: (-rating.rating, rating.date)) - worstRating = ratingsWorst[-1] + ratings_worst = sorted([rating for rating in prof.get_ratings() if rating.comment], + key=lambda rating: (-rating.rating, rating.date)) + worst_rating = ratings_worst[-1] - profEmbed = self.buildProfEmbed(prof) - bestembed = self.buildRatingEmbed(nextcord.Embed(title=f"Best Rating for {prof.name}", color=config["success"]), bestRating) - worstembed = self.buildRatingEmbed(nextcord.Embed(title=f"Worst Rating for {prof.name}",color=config["success"]), worstRating) - - await interaction.response.send_message(embed=profEmbed) - await interaction.followup.send(embed=bestembed) - await interaction.followup.send(embed=worstembed) + prof_embed = self.build_prof_embed(prof) + best_embed = self.build_rating_embed(nextcord.Embed(title=f"Best Rating for {prof.name}", color=SUCCESS_COLOR), + best_rating) + worst_embed = self.build_rating_embed(nextcord.Embed(title=f"Worst Rating for {prof.name}", color=SUCCESS_COLOR), + worst_rating) + + await interaction.response.send_message(embed=prof_embed) + await interaction.followup.send(embed=best_embed) + await interaction.followup.send(embed=worst_embed) except: await interaction.response.send_message(f"Could not find professor '{professorname}'. Try only using a last name or check your spelling!") - def buildRatingEmbed(self, embed, rating): + def build_rating_embed(self, embed, rating): if rating.rating: embed.add_field( name="Rating", @@ -93,16 +100,16 @@ def buildRatingEmbed(self, embed, rating): ) return embed - - def buildProfEmbed(self, prof): + + def build_prof_embed(self, prof): embed = nextcord.Embed( title=prof, - color=config["success"] + color=SUCCESS_COLOR ) - if prof.name in self.profImages: - embed.set_image(url=self.profImages[prof.name]) - + if prof.name in PROF_IMAGES: + embed.set_image(url=PROF_IMAGES[prof.name]) + embed.add_field( name="Department", value=prof.department, @@ -141,5 +148,6 @@ def buildProfEmbed(self, prof): return embed + def setup(bot): bot.add_cog(RateMyProfessor(bot)) diff --git a/cogs/template.py b/cogs/template.py index 4276dc1..319f79d 100644 --- a/cogs/template.py +++ b/cogs/template.py @@ -1,29 +1,38 @@ -# import os -# import sys -# import yaml -# import nextcord -# from typing import Optional -# from nextcord.ext import commands -# from nextcord import Interaction, SlashOption, ChannelType -# from nextcord.abc import GuildChannel - - -# with open("config.yaml") as file: -# config = yaml.load(file, Loader=yaml.FullLoader) - -# # Here we name the cog and create a new class for the cog. -# class Template(commands.Cog, name="template"): -# def __init__(self, bot): -# self.bot = bot - - # # Here you can just add your own commands, you'll always need to provide "self" as first parameter. - # @nextcord.slash_command(name="template", description="This is a testing command that does nothing.") - # async def testcommand(self, context): - # """ - # [No Arguments] This is a testing command that does nothing. - # """ - # await context.send("I'll tell you when you're older. Move along now, child.") - -# # And then we finally add the cog to the bot so that it can load, unload, reload and use it's content. -# def setup(bot): -# bot.add_cog(Template(bot)) +# """ +# This is a template for a new cog. +# """ +# +# +# import os +# import sys +# import nextcord +# from typing import Optional +# from nextcord.ext import commands +# from nextcord import Interaction, SlashOption, ChannelType +# from nextcord.abc import GuildChannel +# +# from constants import config +# +# +# class Template(commands.Cog, name="template"): # Here we name the cog and create a new class for the cog. +# """ +# This is a template for a new cog. +# """ +# +# def __init__(self, bot): +# self.bot = bot +# +# # Here you can just add your own commands, you'll always need to provide "self" as first parameter. +# @nextcord.slash_command(name="template", description="This is a testing command that does nothing.") +# async def testcommand(self, context): +# """ +# [No Arguments] This is a testing command that does nothing. +# """ +# await context.send("I'll tell you when you're older. Move along now, child.") +# +# +# def setup(bot): +# """ +# Add the cog to the bot so that it can load, unload, reload and use it's content. +# """ +# bot.add_cog(Template(bot)) diff --git a/config template.yaml b/config template.yaml deleted file mode 100644 index 0822fcb..0000000 --- a/config template.yaml +++ /dev/null @@ -1,11 +0,0 @@ -token: "BOT_TOKEN" -application_id: "APPLICATION_ID" -owners: - - OWNER_ID -blacklist: - - 000000000000000000 -main_color: 0xD75BF4 -error: 0xE02B2B -success: 0x42F56C -warning: 0xF59E42 -info: 0x4299F5 diff --git a/config_template.toml b/config_template.toml new file mode 100644 index 0000000..80c4aea --- /dev/null +++ b/config_template.toml @@ -0,0 +1,12 @@ +# Fill in the your bot's token application ID and owner ID +token = "BOT_TOKEN" +application_id = "APPLICATION_ID" +owners = ["OWNER_ID"] # TODO are we using this? +blacklist = [] + +# Uncomment the following lines to enable the database +# [db] +# host = "DATABASE_HOSTENAME" +# user = "DATABASE_USERNAME" +# password = "DATABASE_PASSWORD" +# database = "DATABASE_NAME" diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..848d8e4 --- /dev/null +++ b/constants.py @@ -0,0 +1,42 @@ +""" +Various constants used throughout the bot. +""" + +from typing import Optional, TypedDict +import tomllib +from nextcord import Color + +MAIN_COLOR = Color(0xD75BF4) +ERROR_COLOR = Color(0xE02B2B) +SUCCESS_COLOR = Color(0x42F56C) +WARNING_COLOR = Color(0xF59E42) +INFO_COLOR = Color(0x4299F5) + + +class DBConfig(TypedDict): + """ + The database config from the config file. + """ + + host: str + user: str + password: str + database: str + + +class Config(TypedDict): + """ + The config file. + """ + + token: str + application_id: str + owners: list[int] + blacklist: list[int] + db: Optional[DBConfig] + + +config: Config + +with open("config.toml", "rb") as f: + config = tomllib.load(f) diff --git a/noncommands/__init__.py b/noncommands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/noncommands/auto_code_block.py b/noncommands/auto_code_block.py index a9a8e9d..f727d1e 100644 --- a/noncommands/auto_code_block.py +++ b/noncommands/auto_code_block.py @@ -1,6 +1,9 @@ import re import asyncio +CODE_REGEX = re.compile(r"{.*;.*}", re.DOTALL) + + class AutoCodeBlock: def __init__(self, bot): self.bot = bot @@ -8,11 +11,9 @@ def __init__(self, bot): async def check_message(self, message): message_text = message.content if self.looks_like_unformatted_code(message_text): - probable_code = message_text.replace("```", "") - reply = "It seems like that message might contain some unformatted code, I did my best to format it for you. If this is unwanted, react to this message within 2 minutes." reply += "\n```java\n" - reply += probable_code + reply += message_text reply += "\n```\n" reply += "(If you are curious how to do this, check out https://www.codegrepper.com/code-examples/whatever/discord+syntax+highlighting)" reply += f'\nOriginal Message sent by: {message.author.mention}' @@ -28,11 +29,9 @@ def check(reaction, user): await msg.delete() except asyncio.exceptions.TimeoutError: await msg.clear_reactions() - pass - - def looks_like_unformatted_code(self, text): - return re.search('\{*.\}', text) and ("```" not in text or "```\n" in text) - - - + def looks_like_unformatted_code(self, text): + """ + Returns True if the text looks like it might contain unformatted code. + """ + return CODE_REGEX.search(text) and "```" not in text diff --git a/noncommands/quotes.py b/noncommands/quotes.py index 649baf3..9d974c3 100644 --- a/noncommands/quotes.py +++ b/noncommands/quotes.py @@ -1,27 +1,26 @@ -import json -import random -import yaml -import os import mysql.connector -with open("config.yaml") as file: - config = yaml.load(file, Loader=yaml.FullLoader) +from constants import config + class Quotes: def __init__(self, bot): - self.bot=bot + self.bot = bot - async def dailyQuote(self): + async def daily_quote(self): channel = self.bot.get_channel(707293854507991172) dailyquote = await Quotes.quote(self.bot, "") await channel.send("Daily Quote:\n" + dailyquote) async def quote(self, keywords): + if config["db"] is None: + return "Database not configured" + mydb = mysql.connector.connect( - host=config["dbhost"], - user=config["dbuser"], - password=config["dbpassword"], - database=config["databasename"], + host=config["db"]["host"], + user=config["db"]["user"], + password=config["db"]["password"], + database=config["db"]["database"], autocommit=True, use_unicode=True ) @@ -39,11 +38,14 @@ async def quote(self, keywords): return quote[0] async def newquote(self, quote): + if config["db"] is None: + return "Database not configured" + mydb = mysql.connector.connect( - host=config["dbhost"], - user=config["dbuser"], - password=config["dbpassword"], - database=config["databasename"], + host=config["db"]["host"], + user=config["db"]["user"], + password=config["db"]["password"], + database=config["db"]["database"], autocommit=True, use_unicode=True ) @@ -54,5 +56,5 @@ async def newquote(self, quote): mydb.commit() mycursor.close() mydb.close() - - return quote \ No newline at end of file + + return quote diff --git a/noncommands/summarizer.py b/noncommands/summarizer.py index 67c3e8c..b0cc890 100644 --- a/noncommands/summarizer.py +++ b/noncommands/summarizer.py @@ -2,151 +2,156 @@ from sklearn.decomposition import TruncatedSVD import pandas as pd import nltk -import re import numpy as np -from nltk.corpus import stopwords +from nltk.corpus import stopwords from nltk.tokenize import word_tokenize from trafilatura import bare_extraction import trafilatura import nextcord + +from constants import SUCCESS_COLOR + nltk.download('punkt') nltk.download('stopwords') -def scoreSent(sent, scoreMatrix, scoreCol): + +def score_sent(sent, score_matrix, score_col): score = 0 for word in sent.split(" "): - if word in scoreMatrix.index: - filt = scoreMatrix.filter(items=[word], axis='index') - score += filt[scoreCol].values[0] + if word in score_matrix.index: + filt = score_matrix.filter(items=[word], axis='index') + score += filt[score_col].values[0] return score/len(sent) -def filterStopwords(sent): +def filter_stopwords(sent): stop_words = set(stopwords.words('english')) word_tokens = word_tokenize(sent) return " ".join([w for w in word_tokens if w not in stop_words]) -def getSummarySpread(filePath, numSent): - - f = open(filePath, "r") + +def get_summary_spread(file_path, num_sent): + + f = open(file_path, "r") text = f.read() text = " ".join(text.split()) for i in range(100): text = text.replace("[" + str(i) + "]", "") - + doc = nltk.tokenize.sent_tokenize(text) - docFilt = [filterStopwords(s) for s in doc] + doc_filt = [filter_stopwords(s) for s in doc] vectorizer = CountVectorizer() - bag_of_words = vectorizer.fit_transform(docFilt) + bag_of_words = vectorizer.fit_transform(doc_filt) - svd = TruncatedSVD(n_components = numSent) + svd = TruncatedSVD(n_components=num_sent) lsa = svd.fit_transform(bag_of_words) - col = ["topic" + str(i) for i in range(numSent)] + col = ["topic" + str(i) for i in range(num_sent)] - absCol = ["abs_topic" + str(i) for i in range(numSent)] + abs_col = ["abs_topic" + str(i) for i in range(num_sent)] topic_encoded_df = pd.DataFrame(lsa, columns=col) - topic_encoded_df["docFilt"] = docFilt + topic_encoded_df["docFilt"] = doc_filt topic_encoded_df["doc"] = doc dictionary = vectorizer.get_feature_names_out() - encoding_matrix=pd.DataFrame(svd.components_,index=col,columns=dictionary).T + encoding_matrix = pd.DataFrame(svd.components_, index=col, columns=dictionary).T - for i in range(numSent): - encoding_matrix[absCol[i]] = np.abs(encoding_matrix[col[i]]) + for i in range(num_sent): + encoding_matrix[abs_col[i]] = np.abs(encoding_matrix[col[i]]) cl = dict() - for c in absCol: + for c in abs_col: cl[c] = [] - - for c in absCol: + for c in abs_col: for s in doc: - cl[c].append([s, scoreSent(s, encoding_matrix, c)]) + cl[c].append([s, score_sent(s, encoding_matrix, c)]) chosen = [] - for c in absCol: + for c in abs_col: s = [d for d in sorted(cl[c], key=lambda x: x[1]) if d[0] not in [f[0] for f in chosen]][::-1] chosen.append(s[0]) -def getSummaryMono(text, numSent): + +def get_summary_mono(text, num_sent): text = " ".join(text.split()) for i in range(100): text = text.replace("[" + str(i) + "]", "") - + doc = nltk.tokenize.sent_tokenize(text) - docFilt = [filterStopwords(s) for s in doc] + doc_filt = [filter_stopwords(s) for s in doc] vectorizer = CountVectorizer() - bag_of_words = vectorizer.fit_transform(docFilt) + bag_of_words = vectorizer.fit_transform(doc_filt) - svd = TruncatedSVD(n_components = 1) + svd = TruncatedSVD(n_components=1) lsa = svd.fit_transform(bag_of_words) col = ["topic1"] - absCol = ["abs_topic1"] + abs_col = ["abs_topic1"] topic_encoded_df = pd.DataFrame(lsa, columns=col) - topic_encoded_df["docFilt"] = docFilt + topic_encoded_df["docFilt"] = doc_filt topic_encoded_df["doc"] = doc dictionary = vectorizer.get_feature_names_out() - encoding_matrix=pd.DataFrame(svd.components_,index=col,columns=dictionary).T + encoding_matrix = pd.DataFrame(svd.components_, index=col, columns=dictionary).T for i in range(len(col)): - encoding_matrix[absCol[i]] = np.abs(encoding_matrix[col[i]]) + encoding_matrix[abs_col[i]] = np.abs(encoding_matrix[col[i]]) cl = dict() - for c in absCol: + for c in abs_col: cl[c] = [] - for c in absCol: + for c in abs_col: for s in doc: - cl[c].append([s, scoreSent(s, encoding_matrix, c)]) + cl[c].append([s, score_sent(s, encoding_matrix, c)]) chosen = [] - - for i in range(numSent): - for c in absCol: + + for i in range(num_sent): + for c in abs_col: s = [d for d in sorted(cl[c], key=lambda x: x[1]) if d[0] not in [f[0] for f in chosen]][::-1] chosen.append(s[0]) return [i[0] for i in chosen] -def getSummaryUrl(config, url): - numSent = 5 +def get_summary_url(url): + num_sent = 5 downloaded = trafilatura.fetch_url(url) article = bare_extraction(downloaded) embed = nextcord.Embed( - color=config["success"] + color=SUCCESS_COLOR ) embed.add_field( - name = "Title:", - value = article["title"], - inline = True + name="Title:", + value=article["title"], + inline=True ) embed.add_field( name="Summary:", - value="\n".join(getSummaryMono(article["text"], numSent)), - inline = False + value="\n".join(get_summary_mono(article["text"], num_sent)), + inline=False ) return embed -def getSummaryText(config, text): - numSent = 5 + +def get_summary_text(text): + num_sent = 5 embed = nextcord.Embed( - color=config["success"] + color=SUCCESS_COLOR ) embed.add_field( name="Summary:", - value="\n".join(getSummaryMono(text, numSent)), - inline = False + value="\n".join(get_summary_mono(text, num_sent)), + inline=False ) - return embed \ No newline at end of file + return embed diff --git a/pebble-python-config.json b/pebble-python-config.json new file mode 100644 index 0000000..b718b60 --- /dev/null +++ b/pebble-python-config.json @@ -0,0 +1,5 @@ +{ + "main": "bot.py", + "requirementsFile": "requirements.txt", + "pythonVersion": "3.11" +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e2a2bad --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[tool.pylint.main] +ignore = [".venv"] +recursive = true + +[tool.pylint.messages_control] +disable = [ + "line-too-long", # disable because we use autopep8 + "broad-except", # because in many cases we want the program to continue even if there is an exception +] + +[tool.autopep8] +max_line_length = 120 +recursive = true +exit_code = true diff --git a/requirements.txt b/requirements.txt index b8de83d..6226c30 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,15 @@ -aiohttp -apscheduler -asyncio -nextcord -pyyaml -aiofiles -schedule -sklearn -pandas -nltk -numpy -trafilatura -inspiro -uwuify -RateMyProfessorAPI -mysql-connector \ No newline at end of file +aiohttp +apscheduler +asyncio +nextcord +schedule +scikit-learn +pandas +nltk +numpy +trafilatura +inspiro +RateMyProfessorAPI +mysql-connector +pylint +autopep8