Skip to content

Commit

Permalink
Merge pull request #7 from Tibo-Ulens/config
Browse files Browse the repository at this point in the history
Add per-server configuration
  • Loading branch information
Tibo-Ulens authored Dec 22, 2022
2 parents 9ab801f + cadb28c commit 5646da7
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 47 deletions.
12 changes: 6 additions & 6 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@
- [ ] Automatically fetch a list of all courses for the current period instead
of needing to use `/course add`

## Configuration

- [ ] Add a `/setup` command or a set of `/config {option}` commands to allow
per-server configuration of stuff like verification channels, what
courses to scrape, etc.

# Done

## General
Expand All @@ -21,3 +15,9 @@
- [x] Add a github action to deploy to dockerhub or ghcr
- [x] Update the docker compose file to pull from the container repository
instead of building locally

## Configuration

- [x] Add a `/setup` command or a set of `/config {option}` commands to allow
per-server configuration of stuff like verification channels, what
courses to scrape, etc.
32 changes: 32 additions & 0 deletions alembic/versions/ce233a03ec25_create_config_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""create_config_table
Revision ID: ce233a03ec25
Revises: 6f04bc4e4691
Create Date: 2022-12-22 04:10:27.310547
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = "ce233a03ec25"
down_revision = "6f04bc4e4691"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.create_table(
"config",
sa.Column("guild_id", sa.Text, primary_key=True),
sa.Column("verified_role", sa.Text, nullable=True),
sa.Column("verification_channel", sa.Text, nullable=True),
sa.UniqueConstraint("guild_id", name="unique_guild_id"),
sa.UniqueConstraint("verified_role", name="unique_verified_role"),
sa.UniqueConstraint("verification_channel", name="unique_verification_channel"),
)


def downgrade() -> None:
op.drop_table("config")
15 changes: 3 additions & 12 deletions bot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
import redis.asyncio as redis
from redis.asyncio import Redis

from bot.constants import GUILD_ID


logger = logging.getLogger("bot")

Expand All @@ -25,6 +23,7 @@ async def create(cls) -> "Bot":
"""Create and return a new bot instance"""

intents = discord.Intents.default()
intents.message_content = True
intents.bans = False

intents.dm_messages = False
Expand All @@ -48,12 +47,6 @@ async def create(cls) -> "Bot":

return cls(command_prefix="$", intents=intents, redis=redis_conn)

async def setup_hook(self):
"""Sync slash commands to guild"""

self.tree.copy_global_to(guild=GUILD_ID)
await self.tree.sync()

async def load_extensions(self) -> None:
"""Load all enabled extensions"""

Expand All @@ -70,8 +63,6 @@ async def add_cog(self, cog: commands.Cog) -> None:
await super().add_cog(cog)

async def close(self) -> None:
"""Close the Discord connection"""

# Remove all extensions and cogs
for ext in list(self.extensions):
with suppress(Exception):
Expand All @@ -83,8 +74,8 @@ async def close(self) -> None:
logger.info(f"Removing cog {cog}")
self.remove_cog(cog)

logger.info("Closing Postgres connection")
self.pg_conn.close()
logger.info("Closing redis connection")
await self.redis.close()

logger.info("Closing bot client...")

Expand Down
8 changes: 0 additions & 8 deletions bot/constants.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import datetime
import logging
import os
import textwrap
import math

import discord

logger = logging.getLogger("bot")

Expand All @@ -18,12 +16,6 @@
SMTP_USER = secret.readline().rstrip("\n")
SMTP_PASSWORD = secret.readline().rstrip("\n")

GUILD_ID = discord.Object(id=int(os.environ.get("GUILD_ID")))

VERIFIED_ROLE = os.environ.get("VERIFIED_ROLE")

VERIFY_CHANNEL = os.environ.get("VERIFY_CHANNEL")

DRIVE_LINKS = {
"Grondslagen van de Psychologie": "https://drive.google.com/drive/folders/10ZKbgdHg49_DRjH5TtPtZ7sZjQt6GmMD?usp=sharing",
"Kwalitatieve Data Analyse": "https://drive.google.com/drive/folders/10EKAngz_VfQzSQ1RKgZbvKOZ61SmEmMZ?usp=sharing",
Expand Down
62 changes: 62 additions & 0 deletions bot/extensions/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from discord import app_commands, Interaction, Role, TextChannel
from discord.ext import commands
from discord.ext.commands import Cog, command, Context
import logging

from bot.bot import Bot
from bot.models.config import Config as ConfigModel
from bot import constants


logger = logging.getLogger("bot")


class Config(Cog):
config_group = app_commands.Group(name="config", description="bot configuration")

def __init__(self, bot: Bot) -> None:
self.bot = bot

@command(name="freud_sync")
@commands.guild_only()
@commands.is_owner()
async def sync(self, ctx: Context):
ctx.bot.tree.copy_global_to(guild=ctx.guild)
synced = await ctx.bot.tree.sync(guild=ctx.guild)

logger.info(f"synced {len(synced)} commands to {ctx.guild.name}")
await ctx.reply(f"synced {len(synced)} commands to the current guild")

@app_commands.guild_only()
@config_group.command(
name="verified_role",
description="Set the role to be applied to members once they have been verified",
)
@app_commands.describe(role="The role to be applied")
async def set_verified_role(self, ia: Interaction, role: Role):
guild_config = await ConfigModel.get_or_create(ia.guild_id)

guild_config.verified_role = str(role.id)
await guild_config.save()

logger.info(f"set verified role to {role.id} for guild {ia.guild_id}")
await ia.response.send_message(f"set verified role to <@&{role.id}>")

@app_commands.guild_only()
@config_group.command(
name="verification_channel",
description="Set the channel in which the /verify command can be used",
)
@app_commands.describe(channel="The channel to select")
async def set_verification_channel(self, ia: Interaction, channel: TextChannel):
guild_config = await ConfigModel.get_or_create(ia.guild_id)

guild_config.verification_channel = str(channel.id)
await guild_config.save()

logger.info(f"set verification channel to {channel.id} for guild {ia.guild_id}")
await ia.response.send_message(f"set verification channel to <#{channel.id}>")


async def setup(bot: Bot):
await bot.add_cog(Config(bot))
56 changes: 45 additions & 11 deletions bot/extensions/verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@
from bot import constants
from bot.bot import Bot
from bot.models.profile import Profile
from bot.models.config import Config


EMAIL_REGEX = re.compile(r"^[^\s@]+@ugent\.be$")
CODE_REGEX = re.compile(r"^['|<]?([a-z0-9]{32})[>|']?$")

EMAIL_MESSAGE = "From: [email protected]\nTo: {to}\nSubject: Psychology Discord Verification Code\n\nYour verification code for the psychology discord server is '{code}'"
EMAIL_MESSAGE = "From: {from_}\nTo: {to}\nSubject: Psychology Discord Verification Code\n\nYour verification code for the psychology discord server is '{code}'"


logger = logging.getLogger("bot")
Expand All @@ -29,7 +30,7 @@ def __init__(self, bot: Bot) -> None:

@staticmethod
def send_confirmation_email(to: str, code: str):
message = EMAIL_MESSAGE.format(to=to, code=code)
message = EMAIL_MESSAGE.format(from_=constants.SMTP_USER, to=to, code=code)

email_logger.info(f"sending email to {to}...")
server = smtplib.SMTP("smtp.gmail.com", 587)
Expand Down Expand Up @@ -141,29 +142,62 @@ async def verify_code(self, iactn: Interaction, code: str):

return

profile.confirmation_code = None
await profile.save()

user = iactn.user

logger.info(f"[{author_id}] {iactn.user.name} verified succesfully")
await user.add_roles(
discord.utils.get(user.guild.roles, name=constants.VERIFIED_ROLE)
)

config = await Config.get(iactn.guild_id)
if config is None:
logger.error(f"no config for guild {iactn.guild_id} exists yet")
await iactn.response.send_message(
"The bot has not been set up properly yet, please notify a server admin"
)
return

verified_role = config.verified_role
if verified_role is None:
logger.error(f"no verified role for guild {iactn.guild_id} exists yet")
await iactn.response.send_message(
"The bot has not been set up properly yet, please notify a server admin"
)
return

await user.add_roles(discord.utils.get(user.guild.roles, id=int(verified_role)))
await iactn.response.send_message(
"You have verified succesfully! Welcome to the psychology server"
"You have verified succesfully! Welcome to the server"
)

profile.confirmation_code = None
await profile.save()

@app_commands.command(
name="verify", description="Verify that you are a true UGentStudent"
)
@app_commands.describe(argument="Your UGent email or verification code")
async def verify(self, iactn: Interaction, argument: str):
author_id = iactn.user.id

if str(iactn.channel_id) != constants.VERIFY_CHANNEL:
config = await Config.get(iactn.guild_id)
if config is None:
logger.error(f"no config for guild {iactn.guild_id} exists yet")
await iactn.response.send_message(
"The bot has not been set up properly yet, please notify a server admin"
)
return

verification_channel = config.verification_channel
if verification_channel is None:
logger.error(
f"no verification channel for guild {iactn.guild_id} exists yet"
)
await iactn.response.send_message(
"The bot has not been set up properly yet, please notify a server admin"
)
return

if str(iactn.channel_id) != verification_channel:
await iactn.response.send_message(
f"This command can only be used in <#{constants.VERIFY_CHANNEL}>",
f"This command can only be used in <#{verification_channel}>",
ephemeral=True,
)
return
Expand Down
7 changes: 5 additions & 2 deletions bot/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@

class Model:
@classmethod
async def create(cls, **kwargs):
async def create(cls, **kwargs) -> "Model":
"""Create a new row"""

async with session_factory() as session:
session.add(cls(**kwargs))
instance = cls(**kwargs)
session.add(instance)
await session.commit()

return instance

async def save(self):
"""Save an updated row"""

Expand Down
52 changes: 52 additions & 0 deletions bot/models/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import logging
from typing import Optional
from sqlalchemy import Column, Text
from sqlalchemy.future import select
from sqlalchemy.orm import Query

from bot.models import Base, Model, session_factory


logger = logging.getLogger("models")


class Config(Base, Model):
__tablename__ = "config"

guild_id = Column(Text, primary_key=True)
verified_role = Column(Text, unique=True, nullable=False)
verification_channel = Column(Text, unique=True, nullable=False)

def __repr__(self) -> str:
return f"<{self.__class__.__name__}> guild_id: {self.guild_id} verified_role: {self.verified_role} verification_channel: {self.verification_channel}"

@classmethod
async def get(cls, id_: int) -> Optional["Config"]:
"""Find a config given its guild ID"""

async with session_factory() as session:
result: Query = await session.execute(
select(cls).where(cls.guild_id == str(id_))
)

r = result.first()
if r is None:
return None
else:
return r[0]

@classmethod
async def get_or_create(cls, id_: int) -> "Config":
"""Find a config given its guild ID, or create an empty config if it does not exist"""

async with session_factory() as session:
result: Query = await session.execute(
select(cls).where(cls.guild_id == str(id_))
)

r = result.first()
if r is None:
logger.info(f"created new config for guild {id_}")
return await Config.create(guild_id=str(id_), verified_role=None)
else:
return r[0]
8 changes: 0 additions & 8 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ services:

- CACHE_HOST=freud_bot_cache
- CACHE_PORT=6379

- GUILD_ID=983088370509053983
- VERIFY_CHANNEL=983088370966212609
- VERIFIED_ROLE=Verified
logging:
options:
max-size: "4096m"
Expand All @@ -34,10 +30,6 @@ services:

- CACHE_HOST=freud_bot_cache
- CACHE_PORT=6379

- GUILD_ID=983088370509053983
- VERIFY_CHANNEL=983088370966212609
- VERIFIED_ROLE=Verified
logging:
options:
max-size: "4096m"
Expand Down

0 comments on commit 5646da7

Please sign in to comment.