First upload

This commit is contained in:
Gaëtan L. H.-F. 2023-06-26 15:30:31 +02:00
parent fb9294e46d
commit a6093bf7e5
5 changed files with 450 additions and 2 deletions

1
.gitignore vendored
View File

@ -160,3 +160,4 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
database.db

114
README.md
View File

@ -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/)** (linscription nest pas nécessaire, vous pouvez vous connecter avec votre compte GitHub).
* * *
<h2 align="center">discord account verification</h2>
<p align="center">Account verification bot for club elec's Discord server</p>
<p align="center">
<a href="#about">About</a>
<a href="#features">Features</a>
<a href="#deploy">Deploy</a>
<a href="#configuration">Configuration</a>
<a href="#license">License</a>
</p>
## 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

View File

@ -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

276
main.py Normal file
View File

@ -0,0 +1,276 @@
"""club elecs 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="Ladresse 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="Ladresse 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 daccéder à lensemble 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 à laide de votre adresse de courriel universitaire, vous pourrez débloquer votre accès à lensemble 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))} na 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 quil 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 dun 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))} na 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 quil 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)

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
discord.py==2.3.1