Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Database loading checks #28

Draft
wants to merge 39 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
33b2b71
Update README.md
LucasPlacentino Nov 16, 2022
46f709e
add memory limit to the container
LucasPlacentino Nov 26, 2022
f7fe2ec
Merge pull request #23 from bepolytech/main
OscarVsp Feb 16, 2023
d271086
add missing information about admin command
OscarVsp Feb 16, 2023
3acc68b
change disnake version from 2.6 to 2.7
OscarVsp Feb 16, 2023
6c08c4b
removed frozenlist since it had compiler issu with python 3.11
OscarVsp Feb 16, 2023
80e1829
added the feedback command information
OscarVsp Feb 16, 2023
2f62547
Revert "add missing information about admin command"
OscarVsp Feb 16, 2023
f835bfa
Revert "added the feedback command information"
OscarVsp Feb 16, 2023
c4ebc10
added missing information about discord commands
OscarVsp Feb 16, 2023
1767f24
added member count and percent
OscarVsp Feb 16, 2023
5de4cc3
added member count to server info and /stats command
OscarVsp Feb 16, 2023
3c7ae17
added /stats admin command info
OscarVsp Feb 16, 2023
a023214
change disnake to 2.8
OscarVsp Feb 17, 2023
a03683d
change black type to python3
OscarVsp Dec 5, 2023
14e3413
added way to send error log message without cmd context
OscarVsp Dec 5, 2023
a992859
added check and error msg for database loading
OscarVsp Dec 5, 2023
68f2679
Bump aiohttp from 3.8.3 to 3.9.0
dependabot[bot] Dec 5, 2023
755f8d1
sync fix to dev - MergePR #34 from bepolytech/main
LucasPlacentino Dec 5, 2023
e0cf852
update python image to 3.11.6-alpine- Update Dockerfile
LucasPlacentino Dec 5, 2023
230cb85
bump asyncpg, frozenlist, multidict, yarl to latest- Update requireme…
LucasPlacentino Dec 5, 2023
985a963
bump async-timeout to latest - Update requirements.txt
LucasPlacentino Dec 5, 2023
a5ce1fd
Merge remote-tracking branch 'origin/dependabot/pip/aiohttp-3.9.0' in…
OscarVsp Dec 6, 2023
1f5eed9
fix: filter names to remove unwanted characters
LucasPlacentino Mar 30, 2024
b701705
rename user choice during update_all_guilds
LucasPlacentino Mar 30, 2024
89bcf1b
add option for admin to choose to force rename during `/update` to al…
LucasPlacentino Mar 30, 2024
d1858bd
Merge branch '19-update-to-disnake-270' into update-to-python-3-11-6-…
LucasPlacentino Apr 1, 2024
003f908
Merge pull request #37 from bepolytech/update-to-python-3-11-6-alpine
LucasPlacentino Apr 1, 2024
fa88dd7
resolve merge conflict into bepolytech/dev
LucasPlacentino Apr 1, 2024
0097784
Merge branch 'dev' into update-option-rename
LucasPlacentino Apr 1, 2024
30eea13
Merge pull request #44 from bepolytech/update-option-rename
LucasPlacentino Apr 1, 2024
d33c40a
Merge pull request #43 from bepolytech/fix-numbers-in-names
LucasPlacentino Apr 1, 2024
3481cc1
fix typos
LucasPlacentino Apr 1, 2024
5a60293
fix typo
LucasPlacentino Apr 1, 2024
edc8307
Merge pull request #45 from bepolytech/19-update-to-disnake-270
LucasPlacentino Apr 1, 2024
97cb6b9
Update README.md
LucasPlacentino Apr 17, 2024
70f7a27
Better auto complete match by lowering the text
OscarVsp Apr 26, 2024
08af6f3
Better exception handler and logs
OscarVsp Apr 26, 2024
4e52af6
better logs
OscarVsp Apr 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ repos:
hooks:
- id: black
language: python
types: [python]
types: [python3]
args: ["--line-length=120"]

- repo: https://github.com/PyCQA/autoflake
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#FROM python:3
FROM python:3.8.6-alpine
FROM python:3.11.6-alpine
# use python:3.11.0rc2-slim for less vulnerabilities ? (from `docker scan`)
# use python:3.8.6 for no pip dependencies build errors ?
# use python:alpine for reduced size
Expand Down
20 changes: 16 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

This is a small discord bot written in python using the [disnake library](https://github.com/DisnakeDev/disnake) to make a registration system for ULB discord servers.

The bot checks that a user is a ULB student by verifying their ULB email adress using a one-time generated token sent to their email adress. It then gives them the role and adds their Discord user ID and ULB email adress to a database. The user will is then automatically verified on every server that the bot is running. The bot also has a rename functionality (optional, per server), names are extracted from the email adress.
**_[Version 1]_**: The bot checks that a user is a ULB student by verifying their ULB email adress using a one-time generated token sent to their email adress. It then gives them the role and adds their Discord user ID and ULB email adress to a database. The user will is then automatically verified on every server that the bot is running. The bot also has a rename functionality (optional, per server), names are extracted from the email adress.

# ➕ Add the bot to your server

Expand Down Expand Up @@ -196,16 +196,20 @@ To see the bot logs when running with docker in detached mode (`-d`), use the [d

* `/setup`

(Admin permission needed) When adding the bot to a new server, you need to set the @ULB role with the command `/setup`. This command also allows you to choose if you want to force the registered member to get renamed with their real name or not (yes by default).
**[Admin permission needed]** When adding the bot to a new server, you need to set the @ULB role with the command `/setup`. This command also allows you to choose if you want to force the registered member to get renamed with their real name or not (yes by default).

* `/info`

(Admin permission needed) Get current server information (@ULB role, if rename is enabled, and checks for permission conflicts).
**[Admin permission needed]** Get current server information (@ULB role, if rename is enabled, and checks for permission conflicts).

* `/ulb`

Once the ULB role is set, when a new user joins the server, either they are already registered (from another of your servers) in which case they will get the `@ULB` role and get renamed, or they are not registered yet and will receive a DM message with the instructions to register themselves using the `/ulb` command.

* `/feedback`

Send feedback directly from discord.

### Admin server

* `/user add`
Expand All @@ -224,9 +228,17 @@ Edit info of a user.

Delete a user.

* `/server info`

Get information about a guild (ULB role, number of registered members, ...)

* `/stats`

Get statistics about the bot usage (number of configured servers, number of registered users, ...)

* `/update`

This forces a total update of the database and of all the servers. Since the bot already does this automatically at startup and after each reconnection, the only normal usecase for this would be if you manually add an entry (server or user) to the google sheet instead of using the `/user add` command above, we don't recommend manually editing the google sheet.
This forces a total update of the database and of all the servers. Since the bot already does this automatically at startup and after each reconnection, the only normal usecase for this would be if you manually add an entry (server or user) to the google sheet instead of using the `/user add` command above, we don't recommend manually editing the google sheet. It also includes an option (`/update Non`) to not rename users (on servers where it is enabled) when running this command (default: do **not** force rename on force update).

## 👤 Author

Expand Down
28 changes: 20 additions & 8 deletions bot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,24 @@ def load_commands(self) -> None:
)
self.cog_not_loaded.append(extension)

async def send_error_log(self, interaction: ApplicationCommandInteraction, error: Exception):
async def send_error_log(self, tb: str):

n = len(tb) // 4050

#Logs need to be diveded into multiple embed due to size limitation
# TODO Check if we can use a list of embeds and one message
# TODO Make it dynamic base on the message size from the lib (check library version, maybe need to upgrade)
for i in range(n):
await self.log_channel.send(embed=disnake.Embed(description=f"```python\n{tb[4050*i:4050*(i+1)]}```"))
await self.log_channel.send(embed=disnake.Embed(description=f"```python\n{tb[4050*n:]}```"))

async def send_cmd_error_log(self, interaction: ApplicationCommandInteraction, error: Exception):
tb = self.tracebackEx(error)
logging.error(
f"{error} raised on command /{interaction.application_command.name} from {interaction.guild.name+'#'+interaction.channel.name if interaction.guild else 'DM'} by {interaction.author.name}.\n{tb}"
)

#Send error msg to the user
await interaction.send(
content=self.owner.mention,
embed=disnake.Embed(
Expand All @@ -110,17 +123,16 @@ async def send_error_log(self, interaction: ApplicationCommandInteraction, error
),
delete_after=10,
)

#Send logs to admins
await self.log_channel.send(
embed=disnake.Embed(title=f":x: __** ERROR**__ :x:", description=f"```{error}```").add_field(
name=f"Raised on command :",
value=f"**/{interaction.application_command.name}:{interaction.id}** from {interaction.guild.name+'#'+interaction.channel.name if interaction.guild else 'DM'} by {interaction.author.mention} at {interaction.created_at} with options\n```{interaction.filled_options}```"
+ (f" and target\n``'{interaction.target}``'." if interaction.target else "."),
)
)
n = len(tb) // 4050
for i in range(n):
await self.log_channel.send(embed=disnake.Embed(description=f"```python\n{tb[4050*i:4050*(i+1)]}```"))
await self.log_channel.send(embed=disnake.Embed(description=f"```python\n{tb[4050*n:]}```"))
await self.send_error_log(tb)

async def on_slash_command(self, interaction: disnake.ApplicationCommandInteraction) -> None:
logging.trace(
Expand All @@ -138,13 +150,13 @@ async def on_message_command(self, interaction: disnake.MessageCommandInteractio
)

async def on_slash_command_error(self, interaction: ApplicationCommandInteraction, error: Exception) -> None:
await self.send_error_log(interaction, error)
await self.send_cmd_error_log(interaction, error)

async def on_user_command_error(self, interaction: disnake.UserCommandInteraction, error: Exception) -> None:
await self.send_error_log(interaction, error)
await self.send_cmd_error_log(interaction, error)

async def on_message_command_error(self, interaction: disnake.MessageCommandInteraction, error: Exception) -> None:
await self.send_error_log(interaction, error)
await self.send_cmd_error_log(interaction, error)

async def on_slash_command_completion(self, interaction: disnake.ApplicationCommandInteraction) -> None:
logging.trace(
Expand Down
98 changes: 52 additions & 46 deletions classes/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def loaded(cls) -> bool:
return cls._loaded

@classmethod
def load(cls, bot: Bot) -> None:
async def load(cls, bot: Bot) -> bool:
"""Load the data from the google sheet.

Returns
Expand All @@ -104,33 +104,37 @@ def load(cls, bot: Bot) -> None:
- Guild: `Dict[disnake.Guild, disnake.Role]`
- Users: `Dict[disnake.User, UlbUser]]`
"""
# First time this is call, we need to load the credentials and the sheet
if not cls._sheet:
cred_dict = {}
cred_dict["type"] = os.getenv("GS_TYPE")
cred_dict["project_id"] = os.getenv("GS_PROJECT_ID")
cred_dict["auth_uri"] = os.getenv("GS_AUTHOR_URI")
cred_dict["token_uri"] = os.getenv("GS_TOKEN_URI")
cred_dict["auth_provider_x509_cert_url"] = os.getenv("GS_AUTH_PROV")
cred_dict["client_x509_cert_url"] = os.getenv("GS_CLIENT_CERT_URL")
cred_dict["private_key"] = os.getenv("GS_PRIVATE_KEY").replace(
"\\n", "\n"
) # Python add a '\' before any '\n' when loading a str
cred_dict["private_key_id"] = os.getenv("GS_PRIVATE_KEY_ID")
cred_dict["client_email"] = os.getenv("GS_CLIENT_EMAIL")
cred_dict["client_id"] = int(os.getenv("GS_CLIENT_ID"))
creds = ServiceAccountCredentials.from_json_keyfile_dict(cred_dict, cls._scope)
cls._client = gspread.authorize(creds)
logging.info("[Database] Google sheet credentials loaded.")

# Open google sheet
cls._sheet = cls._client.open_by_url(os.getenv("GOOGLE_SHEET_URL"))
cls._users_ws = cls._sheet.worksheet("users")
cls._guilds_ws = cls._sheet.worksheet("guilds")

logging.info("[Database] Spreadsheed loaded")

logging.info("[Database] Loading data...")
try:
# First time this is call, we need to load the credentials and the sheet
if not cls._sheet:
cred_dict = {}
cred_dict["type"] = os.getenv("GS_TYPE")
cred_dict["project_id"] = os.getenv("GS_PROJECT_ID")
cred_dict["auth_uri"] = os.getenv("GS_AUTHOR_URI")
cred_dict["token_uri"] = os.getenv("GS_TOKEN_URI")
cred_dict["auth_provider_x509_cert_url"] = os.getenv("GS_AUTH_PROV")
cred_dict["client_x509_cert_url"] = os.getenv("GS_CLIENT_CERT_URL")
cred_dict["private_key"] = os.getenv("GS_PRIVATE_KEY").replace(
"\\n", "\n"
) # Python add a '\' before any '\n' when loading a str
cred_dict["private_key_id"] = os.getenv("GS_PRIVATE_KEY_ID")
cred_dict["client_email"] = os.getenv("GS_CLIENT_EMAIL")
cred_dict["client_id"] = int(os.getenv("GS_CLIENT_ID"))
creds = ServiceAccountCredentials.from_json_keyfile_dict(cred_dict, cls._scope)
cls._client = gspread.authorize(creds)
logging.info("[Database] Google sheet credentials loaded.")

# Open google sheet
cls._sheet = cls._client.open_by_url(os.getenv("GOOGLE_SHEET_URL"))
cls._users_ws = cls._sheet.worksheet("users")
cls._guilds_ws = cls._sheet.worksheet("guilds")

logging.info("[Database:load] Spreadsheed loaded")
except (ValueError, gspread.exceptions.SpreadsheetNotFound, gspread.exceptions.WorksheetNotFound) as err:
await bot.send_error_log(bot.tracebackEx(err))
return

logging.info("[Database:load] Loading data...")

# Load guilds
cls.ulb_guilds = {}
Expand All @@ -142,28 +146,30 @@ def load(cls, bot: Bot) -> None:
if role:
cls.ulb_guilds.setdefault(guild, UlbGuild(role, rename))
logging.trace(
f"[Database] Role {role.name}:{role.id} loaded from guild {guild.name}:{guild.id} with {rename=}"
f"[Database:load] Role {role.name}:{role.id} loaded from guild {guild.name}:{guild.id} with {rename=}"
)
else:
logging.warning(
f"[Database] Not able to find role from id={guild_data.get('role_id', int)} in guild {guild.name}:{guild.id}."
f"[Database:load] Not able to find role from id={guild_data.get('role_id', int)} in guild {guild.name}:{guild.id}."
)
else:
logging.warning(f"[GoogleSheet] Not able to find guild from id={guild_data.get('guild_id', int)}.")
logging.info(f"[Database] Found {len(cls.ulb_guilds)} guilds.")
logging.warning(f"[Database:load] Not able to find guild from id={guild_data.get('guild_id', int)}.")
logging.info(f"[Database:load] Found {len(cls.ulb_guilds)} guilds.")

# Load users
cls.ulb_users = {}
not_found_counter = 0
for user_data in cls._users_ws.get_all_records():
user = bot.get_user(user_data.get("user_id", int))
if user:
cls.ulb_users.setdefault(user, UlbUser(user_data.get("name", str), user_data.get("email", str)))
logging.trace(
f"[Database] User {user.name}:{user.id} loaded with name={user_data.get('name')} and email={user_data.get('email')}"
f"[Database:load] User {user.name}:{user.id} loaded with name={user_data.get('name')} and email={user_data.get('email')}"
)
else:
not_found_counter += 1
logging.warning(f"[Database] Not able to find user from id={user_data.get('user_id',int)}.")
logging.info(f"[Database] Found {len(cls.ulb_users)} users.")
logging.info(f"[Database:load] {len(cls.ulb_users)} users found, {not_found_counter} not found.")

cls._loaded = True

Expand All @@ -183,15 +189,15 @@ async def _set_user_task(cls, user_id: int, name: str, email: str):
user_cell: gspread.cell.Cell = cls._users_ws.find(str(user_id), in_column=1)
await asyncio.sleep(0.1)
if user_cell:
logging.debug(f"[Database] {user_id=} found")
logging.trace(f"[Database:_set_user_task] {user_id=} found")
cls._users_ws.update_cell(user_cell.row, 2, name)
await asyncio.sleep(0.1)
cls._users_ws.update_cell(user_cell.row, 3, email)
logging.info(f"[Database] {user_id=} updated with {name=} and {email=}")
logging.info(f"[Database:_set_user_task] {user_id=} updated with {name=} and {email=}")
else:
logging.debug(f"[Database] {user_id=} not found")
logging.trace(f"[Database:_set_user_task] {user_id=} not found")
cls._users_ws.append_row(values=[str(user_id), name, email])
logging.info(f"[Database] {user_id=} added with {name=} and {email=}")
logging.info(f"[Database:_set_user_task] {user_id=} added with {name=} and {email=}")

@classmethod
def set_user(cls, user: disnake.User, name: str, email: str):
Expand Down Expand Up @@ -224,10 +230,10 @@ async def _delete_user_task(cls, user_id: int):
"""
user_cell: gspread.cell.Cell = cls._users_ws.find(str(user_id), in_column=1)
await asyncio.sleep(0.1)
logging.trace(f"[Database] {user_id=} found")
logging.trace(f"[Database:_delete_user_task] {user_id=} found")
cls._users_ws.delete_row(user_cell.row)
await asyncio.sleep(0.1)
logging.info(f"[Database] {user_id=} deleted.")
logging.info(f"[Database:_delete_user_task] {user_id=} deleted.")

@classmethod
def delete_user(cls, user: disnake.User):
Expand Down Expand Up @@ -261,14 +267,14 @@ async def _set_guild_task(cls, guild_id: int, role_id: int, rename: bool):
guild_cell: gspread.cell.Cell = cls._guilds_ws.find(str(guild_id), in_column=1)
await asyncio.sleep(0.1)
if guild_cell:
logging.debug(f"[Database] {guild_id=} found.")
logging.trace(f"[Database:_set_guild_task] {guild_id=} found.")
cls._guilds_ws.update_cell(guild_cell.row, 2, str(role_id))
cls._guilds_ws.update_cell(guild_cell.row, 3, rename)
logging.info(f"[Database] {guild_id=} update with {role_id=} and {rename=}.")
logging.info(f"[Database:_set_guild_task] {guild_id=} update with {role_id=} and {rename=}.")
else:
logging.debug(f"[Database] {guild_id=} not found.")
logging.trace(f"[Database:_set_guild_task] {guild_id=} not found.")
cls._guilds_ws.append_row(values=[str(guild_id), str(role_id), rename])
logging.info(f"[Database] {guild_id=} added with {role_id=} and {rename=}.")
logging.info(f"[Database:_set_guild_task] {guild_id=} added with {role_id=} and {rename=}.")

@classmethod
def set_guild(cls, guild: disnake.Guild, role: disnake.Role, rename: bool):
Expand Down Expand Up @@ -299,10 +305,10 @@ async def _delete_guild_task(cls, guild_id: int):
"""
guild_cell: gspread.cell.Cell = cls._guilds_ws.find(str(guild_id), in_column=1)
await asyncio.sleep(0.1)
logging.trace(f"[Database] {guild_id=} found")
logging.trace(f"[Database:_delete_guild_task] {guild_id=} found")
cls._guilds_ws.delete_row(guild_cell.row)
await asyncio.sleep(0.1)
logging.info(f"[Database] {guild_id=} deleted.")
logging.info(f"[Database:_delete_guild_task] {guild_id=} deleted.")

@classmethod
def delete_guild(cls, guild: disnake.Guild):
Expand Down
6 changes: 5 additions & 1 deletion classes/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from typing import Dict
from typing import List
from typing import Tuple
from string import ascii_letters

import disnake
from disnake.ext import commands
Expand Down Expand Up @@ -519,7 +520,10 @@ async def _register_user_step(self, inter: disnake.ModalInteraction) -> None:
The modal interaction that triggered the step
"""
# Extract name and store the user
name = " ".join([name.title() for name in self.email.split("@")[0].split(".")])
allowed_chars = set(ascii_letters + " ") # or set('abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ')
unfiltered_name = " ".join([name.title() for name in self.email.split("@")[0].split(".")])
name = "".join(filter(allowed_chars.__contains__, unfiltered_name)) # remove all characters that aren't ASCII letters or a space

logging.trace(f"[RegistrationForm] [User:{self.target.id}] Extracted name from email= {name}")
Database.set_user(self.target, name, self.email)
await self._stop()
Expand Down
Loading
Loading