From a6093bf7e544b40eda24f922ce04374fc2a71076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20L=2E=20H=2E-F?= Date: Mon, 26 Jun 2023 15:30:31 +0200 Subject: [PATCH] First upload --- .gitignore | 1 + README.md | 114 ++++++++++- discord-account-verification.service | 60 ++++++ main.py | 276 +++++++++++++++++++++++++++ requirements.txt | 1 + 5 files changed, 450 insertions(+), 2 deletions(-) create mode 100644 discord-account-verification.service create mode 100644 main.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index 5d381cc..6d7ebab 100644 --- a/.gitignore +++ b/.gitignore @@ -160,3 +160,4 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +database.db diff --git a/README.md b/README.md index b5297e3..a4111bc 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,113 @@ -# discord-account-verification +[![](https://upload.wikimedia.org/wikipedia/commons/thumb/b/bb/Gitea_Logo.svg/48px-Gitea_Logo.svg.png)](https://forge.collabore.fr) + +![English:](https://upload.wikimedia.org/wikipedia/commons/thumb/7/77/Flag_of_the_United_States_and_United_Kingdom.png/20px-Flag_of_the_United_States_and_United_Kingdom.png) **club elec** uses **Gitea** for the development of its free softwares. Our GitHub repositories are only mirrors. +If you want to work with us, **fork us on [collabore forge](https://forge.collabore.fr/)** (no registration needed, you can sign in with your GitHub account). + +![Français :](https://upload.wikimedia.org/wikipedia/commons/thumb/b/bc/Flag_of_France_(1794%E2%80%931815%2C_1830%E2%80%931974%2C_2020%E2%80%93present).svg/20px-Flag_of_France_(1794%E2%80%931815%2C_1830%E2%80%931974%2C_2020%E2%80%93present).svg.png) **club elec** utilise **Gitea** pour le développement de ses logiciels libres. Nos dépôts GitHub ne sont que des miroirs. +Si vous souhaitez travailler avec nous, **forkez-nous sur [collabore forge](https://forge.collabore.fr/)** (l’inscription n’est pas nécessaire, vous pouvez vous connecter avec votre compte GitHub). +* * * + +

discord account verification

+

Account verification bot for club elec's Discord server

+

+ About • + Features • + Deploy • + Configuration • + License +

+ +## About + +[club elec](https://clubelec.insset.fr) needed a tool to validate the Discord accounts of people joining its Discord server. +Validating student accounts manually is a time-consuming operation requiring the presence of an administrator. +This Discord bot was therefore created to allow newcomers to easily validate their Discord account by receiving a verification code on their university email address, saving everyone time. + +## Features + +- ✅ **Easy** to use +- ✅ **Receive** a validation code by **email** +- ✅ **Easy** to deploy +- ✨ Using **Discord interactions** + +## Deploy + +We have deployed discord account verification on a server running Debian 11. + +**Please adapt these steps to your configuration, ...** +*We do not describe the usual server configuration steps.* + +### Install required packages + +``` +apt install python3-pip python3-venv nginx +``` + +### Create `discord-account-verification` user + +``` +groupadd discord-account-verification +``` + +``` +useradd -r -s /sbin/nologin -g discord-account-verification discord-account-verification +``` + +### Retrieve sources + +``` +mkdir /opt/discord-account-verification +``` + +``` +chown discord-account-verification:discord-account-verification /opt/discord-account-verification +``` + +``` +cd /opt/discord-account-verification +``` + +``` +runuser -u discord-account-verification -- git clone https://forge.collabore.fr/ClubElecINSSET/discord-account-verification . +``` + +### Create Python virtual environment + +``` +runuser -u discord-account-verification -- virtualenv .env +``` + +### Install Python dependencies + +``` +runuser -u discord-account-verification -- .env/bin/pip install -r requirements.txt +``` + +### Install systemd service + +``` +cp discord-account-verification.service /etc/systemd/system/ +``` + +### Enable and start systemd service + +``` +systemctl enable discord-account-verification +``` + +``` +systemctl start discord-account-verification +``` + +## Configuration + +To configure discord account verification, please modify the configurations of the systemd service according to your needs. + +## License + +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License along with this program. If not, see http://www.gnu.org/licenses/. -Account verification bot for club elec's Discord server \ No newline at end of file diff --git a/discord-account-verification.service b/discord-account-verification.service new file mode 100644 index 0000000..ab9faf8 --- /dev/null +++ b/discord-account-verification.service @@ -0,0 +1,60 @@ +[Unit] +Description=discord account verification Account verification bot for club elec's Discord server +After=network.target + +[Service] +Type=exec + +# environment variables +Environment=HOME=/opt/discord-account-verification/ +Environment=BOT_TOKEN= +Environment=PUBLIC_CHANNEL= +Environment=DATABASE_PATH=/opt/discord-account-verification/database.db + +# working directory and exec +WorkingDirectory=/opt/discord-account-verification +ExecStart=/opt/discord-account-verification/.env/bin/python3 main.py +ExecStop=/usr/bin/kill -9 $MAINPID +Restart=on-failure +RestartSec=10s +User=discord-account-verification +Group=discord-account-verification + +# filesystem +TemporaryFileSystem=/:ro +BindReadOnlyPaths=/lib/ /lib64/ /usr/lib/ /usr/lib64/ /opt/discord-account-verification/ +BindReadOnlyPaths=/usr/bin/python3 /usr/bin/kill +BindPaths=/opt/discord-account-verification/database.db +PrivateTmp=true +PrivateDevices=true +ProtectControlGroups=true +ProtectKernelModules=true +ProtectKernelTunables=true +ProtectKernelLogs=true +ReadWritePaths= + +# network +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 + +# misc +SystemCallArchitectures=native +SystemCallFilter= +NoNewPrivileges=true +RestrictRealtime=true +MemoryDenyWriteExecute=true +ProtectKernelLogs=true +LockPersonality=true +ProtectHostname=true +RemoveIPC=true +RestrictSUIDSGID=true +ProtectClock=true +ProtectProc=invisible + +# capabilities +RestrictNamespaces=yes +CapabilityBoundingSet= +AmbientCapabilities= + +[Install] +WantedBy=multi-user.target + diff --git a/main.py b/main.py new file mode 100644 index 0000000..b66a43a --- /dev/null +++ b/main.py @@ -0,0 +1,276 @@ +"""club elec’s Discord server account verification""" + +import os +import discord +from discord import app_commands +from discord.ext import commands +import random +import smtplib +from email.mime.text import MIMEText +import datetime +import re + +import sqlite3 +import traceback + +bot_token: str = os.getenv("BOT_TOKEN", "") +public_channel: int = int(os.getenv("PUBLIC_CHANNEL", "")) +database_path: str = os.getenv("DATABASE_PATH", "./database.db") + + +conn: sqlite3.Connection = sqlite3.connect(database_path) +c: sqlite3.Cursor = conn.cursor() + +c.execute( + "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, email TEXT, timestamp TIMESTAMP)" +) +conn.commit() + + +def generate_code() -> str: + return str(random.randint(000000, 999999)) + + +def send_email(email: str, code: str) -> None: + msg = MIMEText( + f"Votre code de vérification pour valider votre compte Discord sur le serveur Discord du club elec est {code}." + ) + msg["Subject"] = "Code de vérification pour le serveur Discord du club elec" + msg["From"] = "no-reply@discord.clubelec.org" + msg["To"] = email + + with smtplib.SMTP("localhost") as smtp: + smtp.send_message(msg) + + +class MyClient(discord.Client): + def __init__(self) -> None: + intents = discord.Intents.default() + intents.members = True + super().__init__(intents=intents) + self.tree = app_commands.CommandTree(self) + + async def on_ready(self): + print(f"Logged in as {self.user} (ID: {self.user.id})") + print("------") + + async def setup_hook(self) -> None: + await self.tree.sync() + + +class EmailModal(discord.ui.Modal, title="Vérification de votre compte"): + email = discord.ui.TextInput( + label="Adresse de courriel", + placeholder="ane.onyme@(etud.)u-picardie.fr", + required=True, + ) + + async def on_submit(self, interaction: discord.Interaction): + if self.email.value.endswith("@u-picardie.fr") or self.email.value.endswith( + "@etud.u-picardie.fr" + ): + email_already_registered = c.execute( + "SELECT EXISTS(SELECT 1 FROM users WHERE email = :email)", + {"email": self.email.value}, + ).fetchone()[0] + if email_already_registered: + await interaction.response.edit_message( + content="L’adresse de courriel que vous avez entrée a déjà été utilisée pour valider un compte.\nSi vous avez changé de compte et souhaitez valider un autre compte, veuillez contacter un administrateur de ce serveur Discord.", + view=None, + ) + else: + code = generate_code() + send_email(self.email.value, code) + await interaction.response.edit_message( + content="Veuillez vérifier votre boîte de courriels UPJV, un message contenant un code de vérification vous a été envoyé.\nUne fois en possession de ce code, veuillez cliquer sur le bouton ci-dessous", + view=CodeSentView(self.email.value, code), + ) + else: + await interaction.response.edit_message( + content="L’adresse de courriel que vous avez entrée ne correspond pas à une adresse UPJV en `@u-picardie.fr` ou en `@etud.u-picardie.fr`.", + view=None, + ) + + async def on_error( + self, interaction: discord.Interaction, error: Exception + ) -> None: + await interaction.response.edit_message( + content="Oups... :sob:\nUne erreur est survenue.", view=None + ) + traceback.print_exception(type(error), error, error.__traceback__) + + +class CodeModal(discord.ui.Modal, title="Entrez votre code de vérification"): + def __init__(self, email: str, code: str) -> None: + super().__init__() + self.email = email + self.code = code + + typed_code = discord.ui.TextInput( + label="Code reçu par courriel", placeholder="000000", required=True + ) + + async def on_submit(self, interaction: discord.Interaction): + if self.typed_code.value == self.code: + c.execute( + "INSERT INTO users (id, email, timestamp) VALUES (?, ?, ?)", + (interaction.user.id, self.email, datetime.datetime.now()), + ) + conn.commit() + role = discord.utils.get(interaction.guild.roles, name="Vérifiés") + await interaction.user.add_roles(role) + await interaction.response.edit_message( + content="Bravo !\nVous avez validé votre compte avec succès. :partying_face: :partying_face: :partying_face:\nLe rôle `Vérifié`, vous permettant d’accéder à l’ensemble des fonctionnalités de ce serveur Discord, vous a été ajouté.\n", + view=None, + ) + + async def on_error( + self, interaction: discord.Interaction, error: Exception + ) -> None: + await interaction.response.edit_message( + content="Oups... :sob:\nUne erreur est survenue.", + view=None, + ) + traceback.print_exception(type(error), error, error.__traceback__) + + +class CodeSentView(discord.ui.View): + def __init__(self, email: str, code: str) -> None: + super().__init__() + self.email = email + self.code = code + + @discord.ui.button( + label="Entrer mon code de vérification", style=discord.ButtonStyle.green + ) + async def confirm( + self, interaction: discord.Interaction, button: discord.ui.Button + ): + await interaction.response.send_modal(CodeModal(self.email, self.code)) + + +class EmailModalView(discord.ui.View): + def __init__(self) -> None: + super().__init__() + + @discord.ui.button( + label="Entrer mon adresse de courriel universitaire", + style=discord.ButtonStyle.green, + ) + async def confirm( + self, interaction: discord.Interaction, button: discord.ui.Button + ): + await interaction.response.send_modal(EmailModal()) + + +client = MyClient() + + +@client.tree.command(name="verify", description="Vérification de votre compte") +async def verify(interaction: discord.Interaction) -> None: + is_verified = c.execute( + "SELECT EXISTS(SELECT 1 FROM users WHERE id = :id)", {"id": interaction.user.id} + ).fetchone()[0] + if is_verified: + await interaction.response.send_message( + "Votre compte est déjà vérifié.", ephemeral=True + ) + else: + await interaction.response.send_message( + "En vérifiant votre compte Discord à l’aide de votre adresse de courriel universitaire, vous pourrez débloquer votre accès à l’ensemble des fonctionnalités de ce serveur Discord.\nCliquez sur le bouton ci-dessous pour commencer.", + view=EmailModalView(), + ephemeral=True, + ) + + +@client.tree.command(name="whois", description="Qui est cette personne ?") +@app_commands.checks.has_permissions(manage_messages=True) +async def whois(interaction: discord.Interaction, message: str) -> None: + matches = re.findall(r"<@!?([0-9]{15,20})>", message) + if matches: + match = matches[0] + get_email = c.execute("SELECT email FROM users WHERE id = :id", {"id": match}) + email = get_email.fetchone() + if email: + username = email[0].split("@").replace(".", " ") + await interaction.response.send_message( + f"{interaction.guild.get_member(int(match))} a pour adresse de courriel : {username}", + ephemeral=True, + ) + else: + await interaction.response.send_message( + f"{interaction.guild.get_member(int(match))} n’a pas vérifié son compte, il est donc impossible de trouver son adresse de courriel.", + ephemeral=True, + ) + else: + await interaction.response.send_message( + "Hum... Il semble qu’il y a un souci avec votre demande...\nVeuillez vérifier si vous avez correctement mentionné un utilisateur.", + ephemeral=True, + ) + + +@whois.error +async def whois_error(interaction: discord.Interaction, error) -> None: + await interaction.response.send_message( + f"Une erreur est survenue : {error}", ephemeral=True + ) + + +@client.tree.command( + name="unverify", description="Suppression de la vérification d’un compte" +) +@app_commands.checks.has_permissions(manage_messages=True) +async def unverify(interaction: discord.Interaction, id: str) -> None: + matches = re.findall(r"<@!?([0-9]{15,20})>", id) + if matches: + match = matches[0] + get_user = c.execute("SELECT email FROM users WHERE id = :id", {"id": match}) + user = get_user.fetchone() + if user: + delete_user = c.execute("DELETE FROM users WHERE id = :id", {"id": match}) + conn.commit() + role = discord.utils.get(interaction.guild.roles, name="Vérifié") + member = interaction.guild.get_member(int(match)) + await member.remove_roles(role) + await interaction.response.send_message( + f"La vérification du compte de {interaction.guild.get_member(int(match))} a été supprimé avec succès.", + ephemeral=True, + ) + else: + await interaction.response.send_message( + f"{interaction.guild.get_member(int(match))} n’a pas vérifié son compte, il est donc impossible de supprimer sa vérification.", + ephemeral=True, + ) + else: + await interaction.response.send_message( + "Hum... Il semble qu’il y a un souci avec votre demande...\nVeuillez vérifier si vous avez correctement mentionné un utilisateur.", + ephemeral=True, + ) + + +@unverify.error +async def unverify_error(interaction: discord.Interaction, error) -> None: + await interaction.response.send_message( + f"Une erreur est survenue : {error}", ephemeral=True + ) + + +@client.event +async def on_member_remove(member) -> None: + get_user = c.execute("SELECT email FROM users WHERE id = :id", {"id": member.id}) + user = get_user.fetchone() + if user: + delete_user = c.execute("DELETE FROM users WHERE id = :id", {"id": member.id}) + conn.commit() + + +@client.event +async def on_member_join(member) -> None: + channel = client.get_channel(public_channel) + await channel.send( + f"Coucou {member.mention}, je suis club elec security, le bot de vérification de comptes Discord missionné par ce serveur.\nSi vous êtes étudiant·e ou personnel, vous pouvez cliquer sur le bouton ci-dessous pour valider votre compte avec votre courriel universitaire. Vous pouvez aussi taper la commande `/verify`. Laissez-vous guider, je suis un gentil petit bot !\nSi vous ne faites pas partie de l'UPJV, vous pouvez vous présenter dans ce salon afin que nous puissons valider votre compte manuellement et vous donner les autorisations adéquates.\nMerci et à très vite. :grin:", + view=EmailModalView(), + ) + + +client.run(bot_token) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a60219e --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +discord.py==2.3.1