diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..73f92a8 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +main.db \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/cogs/verify.py b/cogs/verify.py new file mode 100644 index 0000000..375670f --- /dev/null +++ b/cogs/verify.py @@ -0,0 +1,109 @@ +import discord +import orm +from discord.ext import commands +from utils import send_verification_code, VerifyCode, Student +import config + + +class VerifyCog(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + + @commands.slash_command() + async def verify(self, ctx: discord.ApplicationContext, *, code: str = None): + """Verifies or generates a verification code""" + guild = self.bot.get_guild(config.guilds[0]) + # if ctx.guild is not None: + # return await ctx.respond("\N{cross mark} This command can only be run in my DMs!", ephemeral=True) + + try: + student: Student = await Student.objects.get(user_id=ctx.author.id) + return await ctx.respond(f"\N{cross mark} You're already verified as {student.id}!", ephemeral=True) + except orm.NoMatch: + pass + + if code is None: + class Modal(discord.ui.Modal): + def __init__(self): + super().__init__( + discord.ui.InputText( + custom_id="student_id", + label="What is your student ID", + placeholder="B...", + min_length=7, + max_length=7 + ), + title="Enter your student ID number" + ) + + async def callback(self, interaction: discord.Interaction): + await interaction.response.defer() + if not self.children[0].value: # timed out + return + _code = await send_verification_code( + ctx.author, + self.children[0].value + ) + __code = await VerifyCode.objects.create( + code=_code, + bind=ctx.author.id, + student_id=self.children[0].value + ) + await interaction.followup.send( + "\N{white heavy check mark} Verification email sent to your college email " + f"({self.children[0].value}@my.leedscitycollege.ac.uk)\n" + f"Once you get that email, run this command again, with the first option being the 16" + f" character code.\n\n" + f">>> If you don't know how to access your email, go to , then " + f"sign in as `{self.children[0].value}@leedscitycollege.ac.uk` (notice there's no `my.` " + f"prefix to sign into gmail), and you should be greeted by your inbox. The default password " + f"is your birthday, !, and the first three letters of your first or last name" + f" (for example, `John Doe`, born on the 1st of february 2006, would be either " + f"`01022006!Joh` or `01022006!Doe`).", + ephemeral=True + ) + return await ctx.send_modal(Modal()) + else: + try: + existing: VerifyCode = await VerifyCode.objects.get( + code=code + ) + except orm.NoMatch: + return await ctx.respond( + "\N{cross mark} Invalid or unknown verification code. Try again!", + ephemeral=True + ) + else: + await Student.objects.create( + id=existing.student_id, + user_id=ctx.author.id + ) + await existing.delete() + role = discord.utils.find(lambda r: r.name.lower() == "verified", guild.roles) + if role and role < guild.me.top_role: + member = await guild.fetch_member(ctx.author.id) + await member.add_roles(role, reason="Verified") + return await ctx.respond( + "\N{white heavy check mark} Verification complete!" + ) + + @commands.command(name="de-verify") + @commands.is_owner() + async def verification_del(self, ctx: commands.Context, *, user: discord.Member): + """Removes a user's verification status""" + await ctx.trigger_typing() + for code in await VerifyCode.objects.all(bind=user.id): + await code.delete() + usr = await Student.objects.first(user_id=user.id) + if usr: + await usr.delete() + + role = discord.utils.find(lambda r: r.name.lower() == "verified", ctx.guild.roles) + if role and role < ctx.me.top_role: + await user.remove_roles(role, reason=f"De-verified by {ctx.author}") + + return await ctx.reply(f"\N{white heavy check mark} De-verified {user}.") + + +def setup(bot): + bot.add_cog(VerifyCog(bot)) diff --git a/main.py b/main.py index 7b02667..e8babab 100644 --- a/main.py +++ b/main.py @@ -1,12 +1,16 @@ import discord from discord.ext import commands import config +from utils import registry bot = commands.Bot( commands.when_mentioned_or("h!"), - debug_guilds=config.guilds + debug_guilds=config.guilds, + allowed_mentions=discord.AllowedMentions.none() ) +bot.load_extension("cogs.verify") +bot.loop.run_until_complete(registry.create_all()) @bot.event diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4291bba --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +py-cord==2.1.3 +aiosmtplib==1.1.7 +orm[sqlite]==0.3.1 +httpx==0.23.0 \ No newline at end of file diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..9abc710 --- /dev/null +++ b/utils/__init__.py @@ -0,0 +1,2 @@ +from ._email import * +from .db import * diff --git a/utils/_email.py b/utils/_email.py new file mode 100644 index 0000000..91ea924 --- /dev/null +++ b/utils/_email.py @@ -0,0 +1,57 @@ +import secrets + +import discord + +import config +import aiosmtplib as smtp +from email.message import EmailMessage + +gmail_cfg = { + "addr": "smtp.gmail.com", + "username": config.email, + "password": config.email_password, + "port": 465 +} + + +async def send_verification_code( + user: discord.User, + student_number: str, + **kwargs +) -> str: + """Sends a verification code, returning said verification code, to the student.""" + code = secrets.token_hex(16) + text = f"Hey {user} ({student_number})! The code to join the hi^5 code is '{code}' - use " \ + f"'/verify {code}' in the bot's DMs to continue \N{dancer}\n\n~nex" + msg = EmailMessage() + msg["From"] = gmail_cfg["username"] + msg["To"] = f"{student_number}@my.leedscitycollege.ac.uk" + msg["Subject"] = "Server Verification" + msg.set_content(text) + + kwargs.setdefault( + "hostname", gmail_cfg["addr"] + ) + kwargs.setdefault( + "port", gmail_cfg["port"] + ) + kwargs.setdefault( + "use_tls", True + ) + kwargs.setdefault( + "username", gmail_cfg["username"] + ) + kwargs.setdefault( + "password", gmail_cfg["password"] + ) + kwargs.setdefault( + "start_tls", not kwargs["use_tls"] + ) + + assert kwargs["start_tls"] != kwargs["use_tls"] + + await smtp.send( + msg, + **kwargs + ) + return code diff --git a/utils/db.py b/utils/db.py new file mode 100644 index 0000000..8b838ce --- /dev/null +++ b/utils/db.py @@ -0,0 +1,38 @@ +import uuid +from typing import TYPE_CHECKING + +import orm +from databases import Database + + +registry = orm.ModelRegistry(Database("sqlite:///main.db")) + + +class VerifyCode(orm.Model): + registry = registry + tablename = "codes" + fields = { + "id": orm.Integer(primary_key=True), + "code": orm.String(min_length=8, max_length=64, unique=True), + "bind": orm.BigInteger(), + "student_id": orm.String(min_length=7, max_length=7) + } + if TYPE_CHECKING: + id: int + code: str + bind: int + student_id: str + + +class Student(orm.Model): + registry = registry + tablename = "students" + fields = { + "entry_id": orm.UUID(primary_key=True, default=uuid.uuid4), + "id": orm.String(min_length=7, max_length=7, unique=True), + "user_id": orm.BigInteger(unique=True), + } + if TYPE_CHECKING: + entry_id: uuid.UUID + id: str + user_id: int