From 57c9c361e97883840f84d5a1e623a90e6405d445 Mon Sep 17 00:00:00 2001 From: "Karina J. Kwiatek" Date: Sat, 17 Aug 2024 13:06:59 +0200 Subject: [PATCH 1/7] Store passwords in a JSON file rather than in a channel --- .gitignore | 1 + src/bot.py | 37 +++++++++++++++++++++++-------------- src/commands/join.py | 10 +++++----- src/commands/team.py | 42 ++++++++++++++++++++++++------------------ 4 files changed, 53 insertions(+), 37 deletions(-) diff --git a/.gitignore b/.gitignore index 7686201..b26dfd7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ log.log .env seen_posts.txt +passwords.json diff --git a/src/bot.py b/src/bot.py index 3e94cb3..7e4e840 100644 --- a/src/bot.py +++ b/src/bot.py @@ -1,7 +1,7 @@ import os +import json import asyncio import logging -from typing import Tuple, AsyncGenerator import discord from discord import app_commands @@ -17,12 +17,12 @@ FEED_CHECK_INTERVAL, ANNOUNCE_CHANNEL_NAME, WELCOME_CATEGORY_NAME, - PASSWORDS_CHANNEL_NAME, ) from src.commands.join import join from src.commands.logs import logs from src.commands.team import ( Team, + passwd, new_team, delete_team, export_team, @@ -39,7 +39,7 @@ class BotClient(discord.Client): volunteer_role: discord.Role welcome_category: discord.CategoryChannel announce_channel: discord.TextChannel - passwords_channel: discord.TextChannel + passwords: dict[str, str] feed_channel: discord.TextChannel def __init__( @@ -63,9 +63,11 @@ def __init__( team.add_command(create_voice) team.add_command(create_team_channel) team.add_command(export_team) + team.add_command(passwd) self.tree.add_command(team, guild=self.guild) self.tree.add_command(join, guild=self.guild) self.tree.add_command(logs, guild=self.guild) + self.load_passwords() async def setup_hook(self) -> None: # This copies the global commands over to your guild. @@ -86,7 +88,6 @@ async def on_ready(self) -> None: volunteer_role = discord.utils.get(guild.roles, name=VOLUNTEER_ROLE) welcome_category = discord.utils.get(guild.categories, name=WELCOME_CATEGORY_NAME) announce_channel = discord.utils.get(guild.text_channels, name=ANNOUNCE_CHANNEL_NAME) - passwords_channel = discord.utils.get(guild.text_channels, name=PASSWORDS_CHANNEL_NAME) feed_channel = discord.utils.get(guild.text_channels, name=FEED_CHANNEL_NAME) if ( @@ -95,7 +96,6 @@ async def on_ready(self) -> None: or volunteer_role is None or welcome_category is None or announce_channel is None - or passwords_channel is None or feed_channel is None ): logging.error("Roles and channels are not set up") @@ -106,7 +106,6 @@ async def on_ready(self) -> None: self.volunteer_role = volunteer_role self.welcome_category = welcome_category self.announce_channel = announce_channel - self.passwords_channel = passwords_channel self.feed_channel = feed_channel async def on_member_join(self, member: discord.Member) -> None: @@ -152,19 +151,29 @@ async def check_for_new_blog_posts(self) -> None: async def before_check_for_new_blog_posts(self) -> None: await self.wait_until_ready() - async def load_passwords(self) -> AsyncGenerator[Tuple[str, str], None]: + def load_passwords(self) -> None: """ Returns a mapping from role name to the password for that role. - Reads from the first message of the channel named {PASSWORDS_CHANNEL_NAME}. The format should be as follows: ``` teamname:password ``` """ - message: discord.Message - async for message in self.passwords_channel.history(limit=100, oldest_first=True): - if message.content.startswith('```'): - content: str = message.content.replace('`', '').strip() - team, password = content.split(':') - yield team.strip(), password.strip() + try: + with open('passwords.json') as f: + self.passwords = json.load(f) + except (json.JSONDecodeError, FileNotFoundError): + with open('passwords.json', 'w') as f: + f.write('{}') + self.passwords = {} + + def set_password(self, tla: str, password: str) -> None: + self.passwords[tla] = password + with open('passwords.json', 'w') as f: + json.dump(self.passwords, f) + + def remove_password(self, tla: str) -> None: + del self.passwords[tla] + with open('passwords.json', 'w') as f: + json.dump(self.passwords, f) diff --git a/src/commands/join.py b/src/commands/join.py index 0db50d2..b2c3a33 100644 --- a/src/commands/join.py +++ b/src/commands/join.py @@ -36,7 +36,7 @@ async def join(interaction: discord.Interaction["BotClient"], password: str) -> or not channel.name.startswith(CHANNEL_PREFIX)): return - chosen_team = await find_team(interaction.client, member, password) + chosen_team = find_team(interaction.client, member, password) if chosen_team: if chosen_team == SPECIAL_TEAM: role_name = SPECIAL_ROLE @@ -78,12 +78,12 @@ async def join(interaction: discord.Interaction["BotClient"], password: str) -> await interaction.response.send_message("Incorrect password.", ephemeral=True) -async def find_team(client: "BotClient", member: discord.Member, entered: str) -> str | None: - async for team_name, password in client.load_passwords(): - if password in entered.lower(): +def find_team(client: "BotClient", member: discord.Member, entered: str) -> str | None: + for team_name, password in client.passwords.items(): + if entered == password: client.logger.info( f"'{member.name}' entered the correct password for {team_name}", ) # Password was correct! return team_name - return None + return "" diff --git a/src/commands/team.py b/src/commands/team.py index 9678d9d..bb59daf 100644 --- a/src/commands/team.py +++ b/src/commands/team.py @@ -83,16 +83,10 @@ async def new_team(interaction: discord.interactions.Interaction["BotClient"], t category=category, overwrites=permissions(interaction.client, role) ) - await _save_password(guild, tla, password) + interaction.client.set_password(tla, password) await interaction.response.send_message(f"{role.mention} and {channel.mention} created!", ephemeral=True) -async def _save_password(guild: discord.Guild, tla: str, password: str) -> None: - channel: discord.TextChannel | None = discord.utils.get(guild.text_channels, name=PASSWORDS_CHANNEL_NAME) - if channel is not None: - await channel.send(f"```\n{tla.upper()}:{password}\n```") - - @group.command( # type:ignore[arg-type] name='delete', description='Deletes a role and channel for a team', @@ -128,6 +122,7 @@ async def delete_team(interaction: discord.interactions.Interaction["BotClient"] await channel.delete(reason=reason) await role.delete(reason=reason) + interaction.client.remove_password(tla) if isinstance(interaction.channel, discord.abc.GuildChannel) and not interaction.channel.name.startswith(f"{TEAM_CHANNEL_PREFIX}{tla.lower()}"): await interaction.edit_original_response(content=f"Team {tla.upper()} has been deleted") @@ -200,16 +195,6 @@ async def create_team_channel( await interaction.response.send_message(f"{new_channel.mention} created!", ephemeral=True) -async def _find_password( - team_tla: str, - interaction: discord.interactions.Interaction["BotClient"], -) -> str: - async for team_name, password in interaction.client.load_passwords(): - if team_name == team_tla: - return password - return "" - - async def _export_team( team_tla: str, only_teams: bool, @@ -220,7 +205,7 @@ async def _export_team( if main_channel is None and not isinstance(main_channel, discord.abc.GuildChannel): raise app_commands.AppCommandError("Invalid TLA") - password = await _find_password(team_tla, interaction) + password = interaction.client.passwords[team_tla] commands = [f"/team new tla:{team_tla} name:{main_channel.topic} password:{password}"] if not only_teams: @@ -266,3 +251,24 @@ async def export_team( output = output + await _export_team(tla, only_teams, guild, interaction) output = output + "\n```" await interaction.followup.send(content=output, ephemeral=True) + + +@group.command( # type:ignore[arg-type] + name='passwd', + description='Outputs or changes the password of a given team', +) +@app_commands.describe( + tla='Three Letter Acronym (e.g. SRZ)', + new_password='New password', +) +async def passwd( + interaction: discord.interactions.Interaction["BotClient"], + tla: str, + new_password: str | None = None, +) -> None: + if new_password: + interaction.client.set_password(tla, new_password) + await interaction.response.send_message(f"The password for {tla.upper()} has been changed.", ephemeral=True) + else: + password = interaction.client.passwords[tla] + await interaction.response.send_message(f"The password for {tla.upper()} is `{password}`", ephemeral=True) From 646a56c6a1b38b05b12f91c1d1bb01a72be9ecc4 Mon Sep 17 00:00:00 2001 From: "Karina J. Kwiatek" Date: Sat, 17 Aug 2024 13:08:45 +0200 Subject: [PATCH 2/7] Make incorrect password message not ephemeral This way admins can see failed attempts --- src/commands/join.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/join.py b/src/commands/join.py index b2c3a33..9c8c30c 100644 --- a/src/commands/join.py +++ b/src/commands/join.py @@ -75,7 +75,7 @@ async def join(interaction: discord.Interaction["BotClient"], password: str) -> f"deleted channel '{channel.name}' because verification has completed.", ) else: - await interaction.response.send_message("Incorrect password.", ephemeral=True) + await interaction.response.send_message("Incorrect password.") def find_team(client: "BotClient", member: discord.Member, entered: str) -> str | None: From cebd70b49ee1cd27cb4dd92cfa383961ccdc3874 Mon Sep 17 00:00:00 2001 From: "Karina J. Kwiatek" Date: Sat, 17 Aug 2024 13:16:11 +0200 Subject: [PATCH 3/7] Ensure TLAs are stored in upper-case --- src/bot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bot.py b/src/bot.py index 7e4e840..d82bfc1 100644 --- a/src/bot.py +++ b/src/bot.py @@ -169,11 +169,11 @@ def load_passwords(self) -> None: self.passwords = {} def set_password(self, tla: str, password: str) -> None: - self.passwords[tla] = password + self.passwords[tla.upper()] = password with open('passwords.json', 'w') as f: json.dump(self.passwords, f) def remove_password(self, tla: str) -> None: - del self.passwords[tla] + del self.passwords[tla.upper()] with open('passwords.json', 'w') as f: json.dump(self.passwords, f) From c19b3e701783df6c87556a7eab681ddff59069d7 Mon Sep 17 00:00:00 2001 From: "Karina J. Kwiatek" Date: Sat, 17 Aug 2024 13:55:37 +0200 Subject: [PATCH 4/7] Add the option to show all passwords --- src/commands/team.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/commands/team.py b/src/commands/team.py index bb59daf..359d7c0 100644 --- a/src/commands/team.py +++ b/src/commands/team.py @@ -255,7 +255,7 @@ async def export_team( @group.command( # type:ignore[arg-type] name='passwd', - description='Outputs or changes the password of a given team', + description='Outputs or changes team passwords', ) @app_commands.describe( tla='Three Letter Acronym (e.g. SRZ)', @@ -263,10 +263,15 @@ async def export_team( ) async def passwd( interaction: discord.interactions.Interaction["BotClient"], - tla: str, + tla: str | None = None, new_password: str | None = None, ) -> None: - if new_password: + if tla is None: + await interaction.response.send_message( + '\n'.join([f"**{team}:** {password}" for team, password in interaction.client.passwords.items()]), + ephemeral=True, + ) + elif new_password: interaction.client.set_password(tla, new_password) await interaction.response.send_message(f"The password for {tla.upper()} has been changed.", ephemeral=True) else: From b37d2dfb234282d3a9f4456a9da06aa9197aee64 Mon Sep 17 00:00:00 2001 From: "Karina J. Kwiatek" Date: Mon, 19 Aug 2024 11:49:09 +0200 Subject: [PATCH 5/7] Move team password command out of `/team` This allows its permissions to be set independently of `/team`. --- src/bot.py | 4 ++-- src/commands/passwd.py | 38 ++++++++++++++++++++++++++++++++++++++ src/commands/team.py | 30 ++---------------------------- 3 files changed, 42 insertions(+), 30 deletions(-) create mode 100644 src/commands/passwd.py diff --git a/src/bot.py b/src/bot.py index d82bfc1..b5a32c8 100644 --- a/src/bot.py +++ b/src/bot.py @@ -22,13 +22,13 @@ from src.commands.logs import logs from src.commands.team import ( Team, - passwd, new_team, delete_team, export_team, create_voice, create_team_channel, ) +from src.commands.passwd import passwd class BotClient(discord.Client): @@ -63,8 +63,8 @@ def __init__( team.add_command(create_voice) team.add_command(create_team_channel) team.add_command(export_team) - team.add_command(passwd) self.tree.add_command(team, guild=self.guild) + self.tree.add_command(passwd, guild=self.guild) self.tree.add_command(join, guild=self.guild) self.tree.add_command(logs, guild=self.guild) self.load_passwords() diff --git a/src/commands/passwd.py b/src/commands/passwd.py new file mode 100644 index 0000000..5c097e5 --- /dev/null +++ b/src/commands/passwd.py @@ -0,0 +1,38 @@ +from typing import TYPE_CHECKING + +import discord +from discord import app_commands + +if TYPE_CHECKING: + from src.bot import BotClient + +@app_commands.command( # type:ignore[arg-type] + name='passwd', + description='Outputs or changes team passwords', +) +@app_commands.describe( + tla='Three Letter Acronym (e.g. SRZ)', + new_password='New password', +) +async def passwd( + interaction: discord.interactions.Interaction["BotClient"], + tla: str | None = None, + new_password: str | None = None, +) -> None: + if tla is None: + await interaction.response.send_message( + '\n'.join([f"**{team}:** {password}" for team, password in interaction.client.passwords.items()]), + ephemeral=True, + ) + if new_password is not None: + if not interaction.user.guild_permissions.administrator: + await interaction.response.send_message( + "You do not have permission to change team passwords.", + ephemeral=True + ) + return + interaction.client.set_password(tla, new_password) + await interaction.response.send_message(f"The password for {tla.upper()} has been changed.", ephemeral=True) + else: + password = interaction.client.passwords[tla] + await interaction.response.send_message(f"The password for {tla.upper()} is `{password}`", ephemeral=True) diff --git a/src/commands/team.py b/src/commands/team.py index 359d7c0..70853f1 100644 --- a/src/commands/team.py +++ b/src/commands/team.py @@ -13,7 +13,6 @@ TEAM_LEADER_ROLE, TEAM_CATEGORY_NAME, TEAM_CHANNEL_PREFIX, - PASSWORDS_CHANNEL_NAME, TEAM_VOICE_CATEGORY_NAME, ) @@ -23,7 +22,8 @@ @app_commands.guild_only() @app_commands.default_permissions() class Team(app_commands.Group): - pass + def __init__(self): + super().__init__(description="Manage teams") group = Team() @@ -251,29 +251,3 @@ async def export_team( output = output + await _export_team(tla, only_teams, guild, interaction) output = output + "\n```" await interaction.followup.send(content=output, ephemeral=True) - - -@group.command( # type:ignore[arg-type] - name='passwd', - description='Outputs or changes team passwords', -) -@app_commands.describe( - tla='Three Letter Acronym (e.g. SRZ)', - new_password='New password', -) -async def passwd( - interaction: discord.interactions.Interaction["BotClient"], - tla: str | None = None, - new_password: str | None = None, -) -> None: - if tla is None: - await interaction.response.send_message( - '\n'.join([f"**{team}:** {password}" for team, password in interaction.client.passwords.items()]), - ephemeral=True, - ) - elif new_password: - interaction.client.set_password(tla, new_password) - await interaction.response.send_message(f"The password for {tla.upper()} has been changed.", ephemeral=True) - else: - password = interaction.client.passwords[tla] - await interaction.response.send_message(f"The password for {tla.upper()} is `{password}`", ephemeral=True) From 2826826fa5b1db31689a80b6e936a90c961f849c Mon Sep 17 00:00:00 2001 From: "Karina J. Kwiatek" Date: Mon, 19 Aug 2024 11:49:13 +0200 Subject: [PATCH 6/7] Adjust README.md to match new team management setup --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5cc4c24..34f6aec 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,11 @@ For development, see `script/requirements.txt` - Create a role named `Verified` which can see the base channels (i.e. #general) - Create a role named `Unverified Volunteer` which can see the volunteer onboarding channel. - Create a role named `Blueshirt`. -- Create a new channel category called 'welcome', block all users from reading this category in its permissions. -- Create another channel, visible only to the admins, named '#role-passwords', enter in it 1 message per role in the form `role : password`. Special case: for the `Unverified Volunteer` role, please use the role name `Team SRZ`. -- Create each role named `Team {role}`. +- Create a role named `Team Supervisor`. +- Create a new channel category called `welcome`, block all users from reading this category in its permissions. +- Create channel categories called `Team Channels` and `Team Voice Channels`. +- Create a channel named `#blog`, block all users from sending messages in it. +- Create teams using the `/team new` command. And voilĂ , any new users should automatically get their role assigned once they enter the correct password. @@ -35,3 +37,4 @@ And voilĂ , any new users should automatically get their role assigned once they 4. `pip install -r requirements.txt` 5. `python src/main.py` 6. In the server settings, ensure the `/join` command cannot be used by the `Verified` role +7. Ensure the `/passwd` commands can only be used by `Blueshirt`s From c591c09f95e4f243b4e19c0bd8e407c552103034 Mon Sep 17 00:00:00 2001 From: "Karina J. Kwiatek" Date: Mon, 19 Aug 2024 12:05:28 +0200 Subject: [PATCH 7/7] Fix lint/type errors --- src/commands/passwd.py | 23 ++++++++++++----------- src/commands/team.py | 2 +- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/commands/passwd.py b/src/commands/passwd.py index 5c097e5..27c79de 100644 --- a/src/commands/passwd.py +++ b/src/commands/passwd.py @@ -24,15 +24,16 @@ async def passwd( '\n'.join([f"**{team}:** {password}" for team, password in interaction.client.passwords.items()]), ephemeral=True, ) - if new_password is not None: - if not interaction.user.guild_permissions.administrator: - await interaction.response.send_message( - "You do not have permission to change team passwords.", - ephemeral=True - ) - return - interaction.client.set_password(tla, new_password) - await interaction.response.send_message(f"The password for {tla.upper()} has been changed.", ephemeral=True) else: - password = interaction.client.passwords[tla] - await interaction.response.send_message(f"The password for {tla.upper()} is `{password}`", ephemeral=True) + if new_password is not None: + if isinstance(interaction.user, discord.Member) and not interaction.user.guild_permissions.administrator: + await interaction.response.send_message( + "You do not have permission to change team passwords.", + ephemeral=True + ) + return + interaction.client.set_password(tla, new_password) + await interaction.response.send_message(f"The password for {tla.upper()} has been changed.", ephemeral=True) + else: + password = interaction.client.passwords[tla] + await interaction.response.send_message(f"The password for {tla.upper()} is `{password}`", ephemeral=True) diff --git a/src/commands/team.py b/src/commands/team.py index 70853f1..2029c1b 100644 --- a/src/commands/team.py +++ b/src/commands/team.py @@ -22,7 +22,7 @@ @app_commands.guild_only() @app_commands.default_permissions() class Team(app_commands.Group): - def __init__(self): + def __init__(self) -> None: super().__init__(description="Manage teams")