2022-11-28 17:35:55 +00:00
|
|
|
import random
|
2023-11-04 17:18:39 +00:00
|
|
|
import re
|
2022-10-04 18:21:44 +01:00
|
|
|
import secrets
|
2023-11-04 17:18:39 +00:00
|
|
|
import typing
|
2022-11-06 21:35:14 +00:00
|
|
|
from datetime import datetime, timedelta
|
2022-10-04 18:21:44 +01:00
|
|
|
|
|
|
|
import discord
|
|
|
|
import orm
|
2022-11-06 21:35:14 +00:00
|
|
|
from discord.ui import View
|
2022-10-04 18:21:44 +01:00
|
|
|
|
2023-11-04 17:18:39 +00:00
|
|
|
from utils import (
|
|
|
|
TOKEN_LENGTH,
|
|
|
|
BannedStudentID,
|
|
|
|
Student,
|
|
|
|
VerifyCode,
|
|
|
|
console,
|
|
|
|
get_or_none,
|
|
|
|
send_verification_code,
|
|
|
|
)
|
2022-11-18 14:11:53 +00:00
|
|
|
|
2022-11-06 21:35:14 +00:00
|
|
|
if typing.TYPE_CHECKING:
|
|
|
|
from cogs.timetable import TimeTableCog
|
2022-10-04 18:21:44 +01:00
|
|
|
|
|
|
|
|
2022-11-06 21:35:14 +00:00
|
|
|
class VerifyView(View):
|
2022-10-04 18:21:44 +01:00
|
|
|
def __init__(self, ctx: discord.ApplicationContext):
|
|
|
|
self.ctx = ctx
|
|
|
|
super().__init__(timeout=300, disable_on_timeout=True)
|
|
|
|
|
2022-10-30 16:31:38 +00:00
|
|
|
@discord.ui.button(label="I have a verification code!", emoji="\U0001f4e7", custom_id="have")
|
2022-10-04 18:21:44 +01:00
|
|
|
async def have(self, _, interaction1: discord.Interaction):
|
|
|
|
class Modal(discord.ui.Modal):
|
|
|
|
def __init__(self):
|
|
|
|
super().__init__(
|
|
|
|
discord.ui.InputText(
|
|
|
|
custom_id="code",
|
|
|
|
label="Verification Code",
|
|
|
|
placeholder="e.g: " + secrets.token_hex(TOKEN_LENGTH),
|
2022-10-30 16:31:38 +00:00
|
|
|
min_length=TOKEN_LENGTH * 2,
|
|
|
|
max_length=TOKEN_LENGTH * 2,
|
2022-10-04 18:21:44 +01:00
|
|
|
),
|
|
|
|
title="Enter the verification code in your inbox",
|
|
|
|
)
|
|
|
|
|
|
|
|
async def callback(self, interaction: discord.Interaction):
|
|
|
|
await interaction.response.defer()
|
|
|
|
code = self.children[0].value
|
|
|
|
if not code: # timed out
|
|
|
|
self.stop()
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
existing: VerifyCode = await VerifyCode.objects.get(code=code)
|
|
|
|
except orm.NoMatch:
|
|
|
|
self.stop()
|
|
|
|
return await interaction.followup.send(
|
|
|
|
"\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:
|
|
|
|
self.stop()
|
2022-11-23 14:34:58 +00:00
|
|
|
return await interaction.user.kick(
|
2022-10-04 18:21:44 +01:00
|
|
|
reason=f"Attempted to verify with banned student ID {ban.student_id}"
|
2022-10-30 16:31:38 +00:00
|
|
|
f" (originally associated with account {ban.associated_account})"
|
2022-10-04 18:21:44 +01:00
|
|
|
)
|
2022-11-23 14:34:58 +00:00
|
|
|
await Student.objects.create(
|
2022-11-30 21:01:16 +00:00
|
|
|
id=existing.student_id, user_id=interaction.user.id, name=existing.name
|
2022-11-23 14:34:58 +00:00
|
|
|
)
|
2022-10-04 18:21:44 +01:00
|
|
|
await existing.delete()
|
|
|
|
role = discord.utils.find(lambda r: r.name.lower() == "verified", interaction.guild.roles)
|
2022-11-23 14:34:58 +00:00
|
|
|
member = await interaction.guild.fetch_member(interaction.user.id)
|
2022-10-04 18:21:44 +01:00
|
|
|
if role and role < interaction.guild.me.top_role:
|
|
|
|
await member.add_roles(role, reason="Verified")
|
2022-11-23 14:34:58 +00:00
|
|
|
try:
|
2022-11-30 21:01:16 +00:00
|
|
|
await member.edit(nick=f"{existing.name}", reason="Verified")
|
2022-11-23 14:34:58 +00:00
|
|
|
except discord.HTTPException:
|
|
|
|
pass
|
2022-10-04 18:21:44 +01:00
|
|
|
console.log(f"[green]{interaction.user} verified ({interaction.user.id}/{existing.student_id})")
|
|
|
|
self.stop()
|
|
|
|
return await interaction.followup.send(
|
2022-10-30 16:31:38 +00:00
|
|
|
"\N{white heavy check mark} Verification complete!", ephemeral=True
|
2022-10-04 18:21:44 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
await interaction1.response.send_modal(Modal())
|
|
|
|
self.disable_all_items()
|
|
|
|
await interaction1.edit_original_response(view=self)
|
|
|
|
await interaction1.delete_original_response(delay=1)
|
|
|
|
|
2022-10-30 16:31:38 +00:00
|
|
|
@discord.ui.button(label="Send me a verification code.", emoji="\U0001f4e5")
|
2022-10-04 18:21:44 +01:00
|
|
|
async def send(self, btn: discord.ui.Button, interaction1: discord.Interaction):
|
|
|
|
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,
|
|
|
|
),
|
2022-11-23 14:34:58 +00:00
|
|
|
discord.ui.InputText(
|
|
|
|
custom_id="name",
|
|
|
|
label="What is your name?",
|
|
|
|
placeholder="Nicknames are okay too.",
|
|
|
|
min_length=2,
|
|
|
|
max_length=32,
|
|
|
|
),
|
2022-10-04 18:21:44 +01:00
|
|
|
title="Enter your student ID number",
|
2022-10-30 16:31:38 +00:00
|
|
|
timeout=120,
|
2022-10-04 18:21:44 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
async def callback(self, interaction: discord.Interaction):
|
|
|
|
await interaction.response.defer()
|
2022-11-28 17:35:55 +00:00
|
|
|
st = self.children[0].value.strip()
|
2022-10-04 18:21:44 +01:00
|
|
|
if not st: # timed out
|
|
|
|
return
|
|
|
|
|
|
|
|
if not re.match(r"^B\d{6}$", st):
|
2022-10-06 09:45:42 +01:00
|
|
|
btn.disabled = False
|
2022-11-28 17:35:55 +00:00
|
|
|
return await interaction.followup.send(
|
|
|
|
"\N{cross mark} Invalid student ID - Failed to verify with regex."
|
|
|
|
" Please try again with a valid student ID. Make sure it is formatted as `BXXXXXX` "
|
2022-11-30 21:01:16 +00:00
|
|
|
"(e.g. `B{}`)".format("".join(str(random.randint(0, 9)) for _ in range(6))),
|
|
|
|
delete_after=60,
|
2022-11-28 17:35:55 +00:00
|
|
|
)
|
2022-10-04 18:21:44 +01:00
|
|
|
|
|
|
|
ex = await get_or_none(Student, id=st)
|
|
|
|
if ex:
|
2022-10-06 09:45:42 +01:00
|
|
|
btn.disabled = False
|
|
|
|
return await interaction.followup.send(
|
2022-11-30 21:01:16 +00:00
|
|
|
"\N{cross mark} Student ID is already associated.", delete_after=60
|
2022-10-04 18:21:44 +01:00
|
|
|
)
|
|
|
|
|
2022-10-06 09:45:42 +01:00
|
|
|
try:
|
|
|
|
_code = await send_verification_code(interaction.user, st)
|
|
|
|
except Exception as e:
|
|
|
|
return await interaction.followup.send(f"\N{cross mark} Failed to send email - {e}. Try again?")
|
2022-10-04 18:21:44 +01:00
|
|
|
console.log(f"Sending verification email to {interaction.user} ({interaction.user.id}/{st})...")
|
2022-11-23 14:34:58 +00:00
|
|
|
name = self.children[1].value
|
2023-02-23 11:08:57 +00:00
|
|
|
__code = await VerifyCode.objects.create(code=_code, bind=interaction.user.id, student_id=st, name=name)
|
2022-10-30 16:31:38 +00:00
|
|
|
console.log(
|
|
|
|
f"[green]Sent verification email to {interaction.user} ({interaction.user.id}/{st}): " f"{_code!r}"
|
|
|
|
)
|
2022-10-04 18:21:44 +01:00
|
|
|
await interaction.followup.send(
|
|
|
|
"\N{white heavy check mark} Verification email sent to your college email "
|
|
|
|
f"({st}@my.leedscitycollege.ac.uk)\n"
|
2022-11-28 18:06:56 +00:00
|
|
|
f"Once you get that email, copy the long hexadecimal code, and come back here, then press "
|
|
|
|
f"'I have a code'.\n\n"
|
2022-10-04 18:21:44 +01:00
|
|
|
f">>> If you don't know how to access your email, go to <https://gmail.com>, then "
|
|
|
|
f"sign in as `{st}@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,
|
|
|
|
)
|
|
|
|
|
2022-10-06 09:45:42 +01:00
|
|
|
modal = Modal()
|
2022-10-06 09:49:38 +01:00
|
|
|
await interaction1.response.send_modal(modal)
|
2022-10-04 18:21:44 +01:00
|
|
|
btn.disabled = True
|
|
|
|
await interaction1.edit_original_response(view=self)
|
2022-10-06 09:45:42 +01:00
|
|
|
await modal.wait()
|
|
|
|
await interaction1.edit_original_response(view=self)
|
2022-10-04 18:21:44 +01:00
|
|
|
|
2022-10-30 16:31:38 +00:00
|
|
|
@discord.ui.button(label="Why do I need a verification code?", emoji="\U0001f616")
|
2022-10-04 18:21:44 +01:00
|
|
|
async def why(self, _, interaction: discord.Interaction):
|
2022-10-30 16:31:38 +00:00
|
|
|
await interaction.response.defer(ephemeral=True)
|
2022-10-04 18:21:44 +01:00
|
|
|
await interaction.followup.send(
|
|
|
|
"In order to access this server, you need to enter your student ID.\n"
|
|
|
|
"We require this to make sure only **students** in our course can access the server.\n"
|
|
|
|
"Your B number (student ID) is found on your ID card (the one you use to scan into the building).\n"
|
|
|
|
"This is not invading your privacy, your B number is publicly visible, as it is the start of your email,"
|
|
|
|
" plus can be found on google chat.",
|
|
|
|
ephemeral=True,
|
2022-10-30 16:31:38 +00:00
|
|
|
delete_after=60,
|
2022-10-04 18:21:44 +01:00
|
|
|
)
|
2022-11-06 21:35:14 +00:00
|
|
|
|
|
|
|
|
|
|
|
class TimeTableDaySwitcherView(View):
|
|
|
|
def mod_date(self, by: int):
|
|
|
|
self.current_date += timedelta(days=by)
|
|
|
|
self.update_buttons()
|
|
|
|
|
|
|
|
def update_buttons(self):
|
|
|
|
def _format(d: datetime) -> str:
|
|
|
|
return d.strftime("(%A) %d/%m/%Y")
|
|
|
|
|
|
|
|
day_before = self.current_date + timedelta(days=-1)
|
|
|
|
day_after = self.current_date + timedelta(days=1)
|
2022-11-06 21:41:59 +00:00
|
|
|
for child in self.children:
|
|
|
|
# noinspection PyUnresolvedReferences
|
|
|
|
if child.custom_id == "day_before":
|
|
|
|
child.label = _format(day_before)
|
|
|
|
elif child.custom_id == "day_after":
|
|
|
|
child.label = _format(day_after)
|
|
|
|
else:
|
|
|
|
child.label = _format(self.current_date)
|
2022-11-06 21:35:14 +00:00
|
|
|
|
|
|
|
async def interaction_check(self, interaction: discord.Interaction) -> bool:
|
|
|
|
return interaction.user == self.user
|
|
|
|
|
2022-11-18 14:11:53 +00:00
|
|
|
@discord.ui.button(custom_id="day_before", emoji="\N{leftwards black arrow}")
|
2022-11-06 21:35:14 +00:00
|
|
|
async def day_before(self, _, interaction: discord.Interaction):
|
|
|
|
self.mod_date(-1)
|
2022-11-06 21:43:33 +00:00
|
|
|
await interaction.response.edit_message(content=self.cog.format_timetable_message(self.current_date), view=self)
|
2022-11-06 21:35:14 +00:00
|
|
|
|
2022-11-18 14:11:53 +00:00
|
|
|
@discord.ui.button(custom_id="custom_day", emoji="\N{tear-off calendar}", style=discord.ButtonStyle.primary)
|
2022-11-06 21:56:39 +00:00
|
|
|
async def current_day(self, _, interaction1: discord.Interaction):
|
|
|
|
self1 = self
|
|
|
|
|
|
|
|
class InputModal(discord.ui.Modal):
|
|
|
|
def __init__(self):
|
|
|
|
super().__init__(
|
|
|
|
discord.ui.InputText(
|
|
|
|
label="Date",
|
|
|
|
placeholder="DD/MM/YY",
|
|
|
|
min_length=6,
|
|
|
|
max_length=8,
|
|
|
|
required=True,
|
|
|
|
),
|
2022-11-18 14:11:53 +00:00
|
|
|
title="Date to view timetable of:",
|
2022-11-06 21:56:39 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
async def callback(self, interaction2: discord.Interaction):
|
|
|
|
try:
|
|
|
|
self1.current_date = datetime.strptime(self.children[0].value, "%d/%m/%y")
|
|
|
|
except ValueError:
|
|
|
|
await interaction2.response.send_message("Invalid date", ephemeral=True)
|
|
|
|
else:
|
|
|
|
self1.update_buttons()
|
|
|
|
await interaction2.response.edit_message(
|
2022-11-18 14:11:53 +00:00
|
|
|
content=self1.cog.format_timetable_message(self1.current_date), view=self1
|
2022-11-06 21:56:39 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
return await interaction1.response.send_modal(InputModal())
|
2022-11-06 21:35:14 +00:00
|
|
|
|
2022-11-18 14:11:53 +00:00
|
|
|
@discord.ui.button(custom_id="day_after", emoji="\N{black rightwards arrow}")
|
2022-11-06 21:42:45 +00:00
|
|
|
async def day_after(self, _, interaction: discord.Interaction):
|
2022-11-06 21:35:29 +00:00
|
|
|
self.mod_date(1)
|
2022-11-06 21:43:33 +00:00
|
|
|
await interaction.response.edit_message(content=self.cog.format_timetable_message(self.current_date), view=self)
|
2022-11-06 21:35:14 +00:00
|
|
|
|
2022-11-06 21:37:39 +00:00
|
|
|
def __init__(self, user: discord.User, instance: "TimeTableCog", date: datetime):
|
|
|
|
super().__init__(disable_on_timeout=True)
|
|
|
|
self.user = user
|
|
|
|
self.cog = instance
|
|
|
|
self.current_date = date
|
2022-11-07 15:49:16 +00:00
|
|
|
|
|
|
|
|
|
|
|
class SelectAssigneesView(discord.ui.View):
|
|
|
|
def __init__(self):
|
|
|
|
super().__init__()
|
|
|
|
self.users = []
|
|
|
|
|
|
|
|
@discord.ui.user_select(placeholder="Select some people...", min_values=0, max_values=20)
|
|
|
|
async def select_users(self, select: discord.ui.Select, interaction2: discord.Interaction):
|
2022-11-07 16:54:12 +00:00
|
|
|
await interaction2.response.defer()
|
2022-11-07 15:49:16 +00:00
|
|
|
self.disable_all_items()
|
|
|
|
self.users = select.values
|
|
|
|
await interaction2.edit_original_response(view=self)
|
|
|
|
self.stop()
|
|
|
|
|
|
|
|
@discord.ui.button(label="skip", style=discord.ButtonStyle.primary)
|
|
|
|
async def skip(self, _, interaction2: discord.Interaction):
|
2022-11-07 16:54:12 +00:00
|
|
|
await interaction2.response.defer()
|
2022-11-07 15:49:16 +00:00
|
|
|
self.disable_all_items()
|
|
|
|
await interaction2.edit_original_response(view=self)
|
|
|
|
self.stop()
|