Skip to content

Commit

Permalink
Merge pull request #32 from srobo/kjk/passwords
Browse files Browse the repository at this point in the history
Store passwords in a JSON file rather than in a channel
  • Loading branch information
raccube authored Aug 28, 2024
2 parents 485afa7 + c591c09 commit b78d805
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 43 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
log.log
.env
seen_posts.txt
passwords.json
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
37 changes: 23 additions & 14 deletions src/bot.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os
import json
import asyncio
import logging
from typing import Tuple, AsyncGenerator

import discord
from discord import app_commands
Expand All @@ -17,7 +17,6 @@
FEED_CHECK_INTERVAL,
ANNOUNCE_CHANNEL_NAME,
WELCOME_CATEGORY_NAME,
PASSWORDS_CHANNEL_NAME,
)
from src.commands.join import join
from src.commands.logs import logs
Expand All @@ -29,6 +28,7 @@
create_voice,
create_team_channel,
)
from src.commands.passwd import passwd


class BotClient(discord.Client):
Expand All @@ -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__(
Expand All @@ -64,8 +64,10 @@ def __init__(
team.add_command(create_team_channel)
team.add_command(export_team)
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()

async def setup_hook(self) -> None:
# This copies the global commands over to your guild.
Expand All @@ -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 (
Expand All @@ -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")
Expand All @@ -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:
Expand Down Expand Up @@ -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.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.upper()]
with open('passwords.json', 'w') as f:
json.dump(self.passwords, f)
12 changes: 6 additions & 6 deletions src/commands/join.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -75,15 +75,15 @@ 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.")


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 ""
39 changes: 39 additions & 0 deletions src/commands/passwd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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,
)
else:
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)
25 changes: 5 additions & 20 deletions src/commands/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
TEAM_LEADER_ROLE,
TEAM_CATEGORY_NAME,
TEAM_CHANNEL_PREFIX,
PASSWORDS_CHANNEL_NAME,
TEAM_VOICE_CATEGORY_NAME,
)

Expand All @@ -23,7 +22,8 @@
@app_commands.guild_only()
@app_commands.default_permissions()
class Team(app_commands.Group):
pass
def __init__(self) -> None:
super().__init__(description="Manage teams")


group = Team()
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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,
Expand All @@ -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:
Expand Down

0 comments on commit b78d805

Please sign in to comment.