Files
hmgrisen/bot/src/cogs/galtinn.py
T
Leander Furumo 0f90e9bbf8 make sure ephemeral messages are actually ephemeral
oopsie woopsie we leaked the auth link. the security team is working very hard to fix this
2024-04-12 04:11:21 +02:00

289 lines
9.3 KiB
Python

import asyncio
import secrets
import urllib.parse
from datetime import datetime
import discord
from cogs.utils import embed_templates
from cogs.utils import misc_utils
from discord import app_commands
from discord.ext import commands
from discord.ext import tasks
class Galtinn(commands.Cog):
"""Manage Galtinn membership and roles for users"""
def __init__(self, bot: commands.Bot):
"""
Parameters
----------
bot (commands.Bot): The bot instance
"""
self.bot = bot
self.membership_check.start()
self.verification_cleanup.start()
asyncio.create_task(self.listen_db())
async def init_db(self):
"""
Create the necessary tables for the cog to work
"""
await self.bot.db.execute(
"""
CREATE TABLE IF NOT EXISTS galtinn_verification (
discord_id BIGINT PRIMARY KEY,
challenge TEXT NOT NULL,
state TEXT NOT NULL,
expires TIMESTAMP DEFAULT (NOW() AT TIME ZONE 'utc') + INTERVAL '2 minutes'
);
"""
)
async def listen_db(self):
"""
Creates a listener for galitnn_auth_complete events from the database and processes them
"""
async def process_notification(conn, pid, channel, payload):
if channel != "galtinn_auth_complete":
return
self.bot.logger.info("Received galtinn_auth_complete event from database")
# Surely, no one would ever send a bad payload, right? RIGHT???
discord_id, galtinn_id = payload.split(" ")
# Fetch Discord user
user = self.bot.get_user(int(discord_id))
if not user:
self.bot.logger.info(
f"Could not find discord user with ID {discord_id} in cache. Fetching from API instead..."
)
user = await self.bot.fetch_user(int(discord_id))
if not user:
self.bot.logger.warning(f"Discord user with ID {payload} not found in cache or API. Ignoring event...")
return
# Fetch roles
# Give user roles
conn = await self.bot.db.acquire()
await conn.add_listener("galtinn_auth_complete", process_notification)
while True:
await asyncio.sleep(1)
def cog_unload(self):
self.bot.logger.info("Unloading cog")
self.membership_check.cancel()
self.verification_cleanup.cancel()
@tasks.loop(time=misc_utils.MIDNIGHT)
async def membership_check(self):
"""
Checks all registered user's membership status and assigns roles based on the status
"""
# Fetch galtinn organization roles
roles = {}
db_roles = await self.bot.db.fetch(
"""
SELECT *
FROM galtinn_roles;
"""
)
if db_roles:
for role_id, galtinn_org_id in list(db_roles):
role = self.bot.get_role(role_id)
if not role:
role = await self.bot.fetch_role(role_id)
if not role:
self.bot.logger.info(f"Role with ID {role_id} not found")
continue
roles[galtinn_org_id] = role
# Fetch registered users
# Fetch galtinn user info (membership status and orgs)
# Set roles
@membership_check.before_loop
async def before_membership_check(self):
"""
Make sure bot is ready before starting the membership check loop
"""
await self.bot.wait_until_ready()
galtinn_group = app_commands.Group(name="galtinn", description="Koble Galtinnbrukeren din til Discord")
@tasks.loop(minutes=2)
async def verification_cleanup(self):
"""
Clean up any pending verifications that have expired. This is in case the register command fails to do so
"""
results = await self.bot.db.fetch(
"""
SELECT discord_id, expires
FROM galtinn_verification;
"""
)
for result in results:
discord_id, expires = list(result)
if expires < datetime.utcnow():
await self.bot.db.execute(
"""
DELETE FROM galtinn_verification
WHERE discord_id = $1;
""",
discord_id,
)
@verification_cleanup.before_loop
async def before_verification_cleanup(self):
"""
Make sure bot is ready before starting the verification cleanup loop
"""
await self.bot.wait_until_ready()
@app_commands.checks.bot_has_permissions(embed_links=True)
@galtinn_group.command(name="registrer", description="Koble Galtinnbrukeren din til Discord")
async def register(self, interaction: discord.Interaction):
"""
Connect your Galtinn account to your Discord account
Parameters
----------
interaction (discord.Interaction): Slash command context object
"""
await interaction.response.defer(ephemeral=True)
# Check if user is already registered
# Check if user is already pending verification
verification = await self.bot.db.fetchrow(
"""
SELECT discord_id, challenge, state
FROM galtinn_verification
WHERE discord_id = $1;
""",
interaction.user.id,
)
if verification:
embed = embed_templates.error_warning("Du har allerede en pågående verifikasjon!")
await interaction.followup.send(embed=embed)
return
# Generate OAuth2 URL
challenge = f"{secrets.token_urlsafe(32)}" # TODO: generate challenge based on private key?
state = f"{secrets.token_urlsafe(32)}"
base_url = f"{self.bot.galtinn['api_url']}/oauth/authorize/"
params = {
"client_id": self.bot.galtinn["client_id"],
"scope": "openid profile email",
"response_type": "code",
"redirect_uri": self.bot.galtinn["redirect_uri"],
"code_challenge": challenge,
"state": state,
}
url = f"{base_url}?{urllib.parse.urlencode(params)}"
# Insert into verfications table
await self.bot.db.execute(
"""
INSERT INTO galtinn_verification
VALUES ($1, $2, $3);
""",
interaction.user.id,
challenge,
state,
)
self.bot.logger.info(f"Generated challenge for user {interaction.user.id}")
embed = discord.Embed(title="Koble Galtinnbrukeren din til Discord", color=discord.Color.orange())
embed.description = (
"Klikk på lenken under for å koble Galtinnbrukeren din til Discord\n\n"
+ f"{url}\n\nDu har 2 minutter på deg til å fullføre verifikasjonen."
)
await interaction.followup.send(embed=embed, ephemeral=True)
# Remove verification after 2 minutes
await asyncio.sleep(120)
await self.bot.db.execute(
"""
DELETE FROM galtinn_verification
WHERE discord_id = $1;
""",
interaction.user.id,
)
@app_commands.checks.bot_has_permissions(embed_links=True)
@galtinn_group.command(name="slett", description="Fjern koblingen mellom Galtinnbrukeren din og Discord")
async def delete(self, interaction: discord.Interaction):
"""
Remove the connection between your Galtinn user and Discord
Parameters
----------
interaction (discord.Interaction): Slash command context object
"""
await interaction.response.defer(ephemeral=True)
# Check if user exists
embed = discord.Embed(title="Fjern bruker", color=discord.Color.yellow())
embed.description = (
"Er du sikker på at du vil slette koblingen mellom Galtinnbrukeren din og Discord?\n\n"
+ "Merk at roller knyttet til Galtinn vil bli fjernet."
)
await interaction.followup.send(embed=embed, view=DeleteView(self.bot), ephemeral=True)
class DeleteView(discord.ui.View):
def __init__(self, bot: commands.Bot):
super().__init__()
self.bot = bot
@discord.ui.button(label="Ja, slett", style=discord.ButtonStyle.danger, custom_id="delete_yes")
async def delete_yes(self, interaction: discord.Interaction, button: discord.ui.Button):
self.bot.logger.info(f"Deleting user {interaction.user.id}")
# Remove Roles
# Delete user connection
embed = embed_templates.success("Du har slettet tilkoblingen til Galtinnbrukeren din!")
await interaction.message.edit(embed=embed, view=None, ephemeral=True)
@discord.ui.button(label="Avbryt", style=discord.ButtonStyle.primary, custom_id="delete_no")
async def delete_no(self, interaction: discord.Interaction, button: discord.ui.Button):
embed = embed_templates.error_warning("Sletting avbrutt")
await interaction.message.edit(embed=embed, view=None, ephemeral=True)
async def setup(bot: commands.Bot):
"""
Add the cog to the bot on extension load
Parameters
----------
bot (commands.Bot): Bot instance
"""
cog = Galtinn(bot)
await cog.init_db()
await bot.add_cog(cog)