From 89daf7c762f9015de220cd66d5ee932b2060a870 Mon Sep 17 00:00:00 2001 From: EEKIM10 Date: Tue, 4 Oct 2022 16:20:01 +0100 Subject: [PATCH] Add student ID moderation --- .idea/the-hi5-group.iml | 2 +- cogs/mod.py | 63 +++++++++++++++++++++++++++++++++++++++++ cogs/verify.py | 20 +++++++++++-- main.py | 1 + requirements.txt | 2 +- utils/db.py | 42 ++++++++++++++++++++++++++- 6 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 cogs/mod.py diff --git a/.idea/the-hi5-group.iml b/.idea/the-hi5-group.iml index 74d515a..8e90ca4 100644 --- a/.idea/the-hi5-group.iml +++ b/.idea/the-hi5-group.iml @@ -4,7 +4,7 @@ - + \ No newline at end of file diff --git a/cogs/mod.py b/cogs/mod.py new file mode 100644 index 0000000..397c590 --- /dev/null +++ b/cogs/mod.py @@ -0,0 +1,63 @@ +import discord +from discord.ext import commands +from utils import Student, get_or_none, BannedStudentID +import orm + + +class Mod(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @commands.user_command(name="Ban Account's B Number") + @discord.default_permissions(ban_members=True) + async def ban_student_id(self, ctx: discord.ApplicationContext, member: discord.Member): + """Bans a student ID from registering. Also bans an account associated with it.""" + await ctx.defer(ephemeral=True) + student_id = await get_or_none(Student, user_id=member.id) + if student_id is None: + return await ctx.respond("\N{cross mark} Unknown B number (is the user verified yet?)", ephemeral=True) + + ban = await get_or_none(BannedStudentID, student_id=student_id.id) + if ban is None: + ban = await BannedStudentID.objects.create( + student_id=student_id.id, + associated_account=member.id, + ) + + await member.ban(reason=f"Banned ID {ban.student_id} by {ctx.author}") + return await ctx.respond( + f"\N{white heavy check mark} Banned {ban.student_id} (and {member.mention})", + ephemeral=True + ) + + @commands.slash_command(name="unban-student-number") + @discord.default_permissions(ban_members=True) + async def unban_student_id(self, ctx: discord.ApplicationContext, student_id: str): + """Unbans a student ID and the account associated with it.""" + student_id = student_id.upper() + ban = await get_or_none(BannedStudentID, student_id=student_id) + if not ban: + return await ctx.respond("\N{cross mark} That student ID isn't banned.") + await ctx.defer() + user_id = ban.associated_account + await ban.delete() + if not user_id: + return await ctx.respond(f"\N{white heavy checkmark} Unbanned {student_id}. No user to unban.") + else: + try: + await ctx.guild.unban( + discord.Object(user_id), + reason=f"Unbanned by {ctx.author}" + ) + except discord.HTTPException as e: + return await ctx.respond( + f"\N{white heavy check mark} Unbanned {student_id}. Failed to unban {user_id} - HTTP {e.status}." + ) + else: + return await ctx.respond( + f"\N{white heavy check mark} Unbanned {student_id}. Unbanned {user_id}." + ) + + +def setup(bot): + bot.add_cog(Mod(bot)) diff --git a/cogs/verify.py b/cogs/verify.py index af3cbe0..ae9e562 100644 --- a/cogs/verify.py +++ b/cogs/verify.py @@ -2,7 +2,7 @@ import discord import orm import re from discord.ext import commands -from utils import send_verification_code, VerifyCode, Student, console +from utils import send_verification_code, VerifyCode, Student, console, get_or_none, BannedStudentID import config @@ -45,7 +45,17 @@ class VerifyCog(commands.Cog): return if not re.match(r"^B\d{6}$", st): - return await interaction.response.send_message("\N{cross mark} Invalid student ID.") + return await interaction.response.send_message( + "\N{cross mark} Invalid student ID.", + delete_after=60 + ) + + ex = await get_or_none(Student, id=st) + if ex: + return await interaction.response.send_message( + "\N{cross mark} Student ID is already associated.", + delete_after=60 + ) _code = await send_verification_code(ctx.author, st) console.log(f"Sending verification email to {ctx.author} ({ctx.author.id}/{st})...") @@ -74,6 +84,12 @@ class VerifyCog(commands.Cog): "\N{cross mark} Invalid or unknown verification code. Try again!", ephemeral=True ) else: + ban = await get_or_none(BannedStudentID, student_id=existing.student_id) + if ban is not None: + return await ctx.author.ban( + reason=f"Attempted to verify with banned student ID {ban.student_id}" + f" (originally associated with account {ban.associated_account})" + ) 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) diff --git a/main.py b/main.py index e676d9f..29fc520 100644 --- a/main.py +++ b/main.py @@ -9,6 +9,7 @@ bot = commands.Bot( ) bot.load_extension("jishaku") bot.load_extension("cogs.verify") +bot.load_extension("cogs.mod") bot.loop.run_until_complete(registry.create_all()) diff --git a/requirements.txt b/requirements.txt index 2051fe0..0b6f6ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -py-cord==2.1.3 +py-cord==2.2.0 aiosmtplib==1.1.7 orm[sqlite]==0.3.1 httpx==0.23.0 diff --git a/utils/db.py b/utils/db.py index 5d72ff9..cbfaae8 100644 --- a/utils/db.py +++ b/utils/db.py @@ -1,5 +1,6 @@ +import datetime import uuid -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional, TypeVar import orm from databases import Database @@ -9,9 +10,32 @@ from pathlib import Path os.chdir(Path(__file__).parent.parent) +__all__ = [ + "registry", + "get_or_none" +] +_models = [ + "VerifyCode", + "Student", + "BannedStudentID" +] +__all__ += _models + +T = TypeVar('T') +T_co = TypeVar("T_co", covariant=True) + + registry = orm.ModelRegistry(Database("sqlite:///main.db")) +async def get_or_none(model: T, **kw) -> Optional[T_co]: + """Returns none or the required thing.""" + try: + return await model.objects.get(**kw) + except orm.NoMatch: + return + + class VerifyCode(orm.Model): registry = registry tablename = "codes" @@ -40,3 +64,19 @@ class Student(orm.Model): entry_id: uuid.UUID id: str user_id: int + + +class BannedStudentID(orm.Model): + registry = registry + tablename = "banned" + fields = { + "entry_id": orm.UUID(primary_key=True, default=uuid.uuid4), + "student_id": orm.String(min_length=7, max_length=7, unique=True), + "associated_account": orm.BigInteger(default=None), + "banned_at_timestamp": orm.Float(default=lambda: datetime.datetime.utcnow().timestamp()) + } + if TYPE_CHECKING: + entry_id: uuid.UUID + student_id: str + associated_account: Optional[int] + banned_at_timestamp: float