Skip to content

Commit

Permalink
chore: code cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
martinszuc committed Dec 15, 2024
1 parent 9d98732 commit 3b573a2
Show file tree
Hide file tree
Showing 14 changed files with 732 additions and 252 deletions.
19 changes: 18 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
# [](/compare/v1.0.0...v) (2024-12-15)
# [](/compare/v1.1.0...v) (2024-12-15)


### Bug Fixes

* python version 7248367
* removed docker no-cache-dir a4ab127
* TTS Messages in docker 5443994


### Features

* Queue for announcements 4be33d8
* **test:** added multiple servers test case 707ec13



# [1.1.0](/compare/v1.0.0...v1.1.0) (2024-12-15)


### Bug Fixes
Expand Down
85 changes: 61 additions & 24 deletions src/cogs/help_cog.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# src/cogs/help_cog.py

import logging

import discord
Expand All @@ -9,25 +7,49 @@


class HelpCog(commands.Cog):
"""A Cog for handling the help command."""
"""
A Cog for handling the help command, providing users with information about available bot commands.
"""

def __init__(self, bot: commands.Bot):
"""
Initialize the HelpCog with the bot instance.
def __init__(self, bot):
Args:
bot (commands.Bot): The Discord bot instance.
"""
self.bot = bot
self.logger = logging.getLogger('DotaDiscordBot')
self.prefix = PREFIX

@commands.command(name="bot-help", aliases=['dota-help', 'dotahelp', 'pls', 'help'])
async def send_help(self, ctx):
"""Show available commands with examples."""
self.logger.info(f"Command '!bot-help' invoked by {ctx.author}")
@commands.command(
name="bot-help",
aliases=['dota-help', 'dotahelp', 'pls', 'help'],
brief="Displays the help message with available commands."
)
async def send_help(self, ctx: commands.Context) -> None:
"""
Sends an embedded help message detailing all available commands and their usage.
embed = discord.Embed(title="Dota Timer Bot - Help", color=0x00ff00)
Args:
ctx (commands.Context): The context in which the command was invoked.
"""
self.logger.info(f"Command '!bot-help' invoked by {ctx.author} in guild ID {ctx.guild.id}")

# Create an embed to structure the help message.
embed = discord.Embed(
title="Dota Timer Bot - Help",
color=0x00ff00,
description="List of available commands and their descriptions."
)

# Define command categories and their respective commands.
categories = {
"📅 **Game Timer**": [
{
"name": f"{self.prefix}start <countdown> [mode]",
"value": "Starts the game timer.\n**Example:** `{0}start 45` or `{0}start -10:00 turbo`".format(self.prefix)
"value": "Starts the game timer.\n**Example:** `{0}start 45` or `{0}start -10:00 turbo`".format(
self.prefix)
},
{
"name": f"{self.prefix}stop",
Expand All @@ -49,17 +71,20 @@ async def send_help(self, ctx):
"🛡️ **Roshan Timer**": [
{
"name": f"{self.prefix}rosh *(Aliases: `rs`, `rsdead`, `rs-dead`, `rsdied`, `rs-died`)*",
"value": "Logs Roshan's death and starts the respawn timer.\n**Example:** `{0}rosh`".format(self.prefix)
"value": "Logs Roshan's death and starts the respawn timer.\n**Example:** `{0}rosh`".format(
self.prefix)
},
{
"name": f"{self.prefix}cancel-rosh *(Aliases: `rsalive`, `rsback`, `rsb`)*",
"value": "Cancels the Roshan respawn timer if active.\n**Example:** `{0}cancel-rosh`".format(self.prefix)
"value": "Cancels the Roshan respawn timer if active.\n**Example:** `{0}cancel-rosh`".format(
self.prefix)
}
],
"🔮 **Glyph Timer**": [
{
"name": f"{self.prefix}glyph *(Alias: `g`)*",
"value": "Starts a 5-minute cooldown timer for the enemy's glyph.\n**Example:** `{0}glyph`".format(self.prefix)
"value": "Starts a 5-minute cooldown timer for the enemy's glyph.\n**Example:** `{0}glyph`".format(
self.prefix)
},
{
"name": f"{self.prefix}cancel-glyph *(Alias: `cg`)*",
Expand All @@ -69,7 +94,8 @@ async def send_help(self, ctx):
"🐉 **Tormentor Timer**": [
{
"name": f"{self.prefix}tormentor *(Aliases: `tm`, `torm`, `t`)*",
"value": "Logs Tormentor's death and starts the respawn timer.\n**Example:** `{0}tormentor`".format(self.prefix)
"value": "Logs Tormentor's death and starts the respawn timer.\n**Example:** `{0}tormentor`".format(
self.prefix)
},
{
"name": f"{self.prefix}cancel-torm *(Aliases: `ct`, `tormentorcancel`)*",
Expand All @@ -79,17 +105,20 @@ async def send_help(self, ctx):
"💬 **Mindful Messages**": [
{
"name": f"{self.prefix}enable-mindful *(Aliases: `enable-pma`, `pma`)*",
"value": "Enables periodic mindful messages to encourage positive play.\n**Example:** `{0}enable-mindful`".format(self.prefix)
"value": "Enables periodic mindful messages to encourage positive play.\n**Example:** `{0}enable-mindful`".format(
self.prefix)
},
{
"name": f"{self.prefix}disable-mindful *(Aliases: `disable-pma`, `no-pma`)*",
"value": "Disables the periodic mindful messages.\n**Example:** `{0}disable-mindful`".format(self.prefix)
"value": "Disables the periodic mindful messages.\n**Example:** `{0}disable-mindful`".format(
self.prefix)
}
],
"⚙️ **Custom Events**": [
{
"name": f"{self.prefix}add-event <type> <parameters>",
"value": "Adds a custom event.\n**Static:** `{0}add-event static 10:00 \"Siege Creep incoming!\"`\n**Periodic:** `{0}add-event periodic 05:00 02:00 20:00 \"Bounty Runes soon!\"`".format(self.prefix)
"value": "Adds a custom event.\n**Static:** `{0}add-event static 10:00 \"Siege Creep incoming!\"`\n**Periodic:** `{0}add-event periodic 05:00 02:00 20:00 \"Bounty Runes soon!\"`".format(
self.prefix)
},
{
"name": f"{self.prefix}remove-event <event_id>",
Expand All @@ -101,7 +130,8 @@ async def send_help(self, ctx):
},
{
"name": f"{self.prefix}reset-events",
"value": "Resets all events to their default values.\n**Example:** `{0}reset-events`".format(self.prefix)
"value": "Resets all events to their default values.\n**Example:** `{0}reset-events`".format(
self.prefix)
}
],
"ℹ️ **General**": [
Expand All @@ -112,16 +142,23 @@ async def send_help(self, ctx):
]
}

# Add each category and its commands to the embed as separate fields.
for category, commands_list in categories.items():
value = ""
field_value = ""
for cmd in commands_list:
value += f"**{cmd['name']}**\n{cmd['value']}\n\n"
embed.add_field(name=category, value=value, inline=False)
field_value += f"**{cmd['name']}**\n{cmd['value']}\n\n"
embed.add_field(name=category, value=field_value, inline=False)

# Send the embed to the context channel.
await ctx.send(embed=embed)
self.logger.info(f"Help message sent to {ctx.author}")
self.logger.info(f"Help message sent to {ctx.author} in guild ID {ctx.guild.id}")


async def setup(bot: commands.Bot) -> None:
"""
Function required for loading the Cog.
async def setup(bot):
"""Function required for loading the Cog."""
Args:
bot (commands.Bot): The Discord bot instance.
"""
await bot.add_cog(HelpCog(bot))
57 changes: 45 additions & 12 deletions src/communication/announcement.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,62 @@
import discord
import asyncio

import discord

from src.managers.tts_manager import TTSManager
from src.utils.config import logger


class Announcement:
"""A class to manage announcements in text and voice channels."""
"""
Manages announcements in both text and voice channels for a Discord server.
This class handles sending embed messages to text channels and managing
text-to-speech (TTS) announcements in voice channels via a queue system.
"""

def __init__(self):
"""
Initialize the Announcement manager.
Sets up the TTS manager, message queue, and initializes consumer task state.
"""
self.tts_manager = TTSManager()
self.queue = asyncio.Queue()
self.consumer_task = None
self._consumer_running = False

async def announce(self, game_timer, message):
async def announce(self, game_timer, message: str):
"""
Announce a message in both text and voice channels.
Sends an embed message to the text channel and queues the message for
TTS playback in the voice channel if connected.
Args:
game_timer: An instance containing game state and channel information.
message (str): The message to announce.
"""
# Announce in text channel
# Send the message to the text channel
await self._announce_text(game_timer, message)

# Instead of playing TTS immediately, put it into a queue
# if voice_client is connected and ready.
# Check if the voice client is connected before queuing the TTS message
if game_timer.voice_client and game_timer.voice_client.is_connected():
# Put the message into the queue for sequential playback
await self.queue.put((game_timer, message))
# Start the consumer if not already running
# Start the consumer task if it's not already running
if not self._consumer_running:
self._consumer_running = True
self.consumer_task = asyncio.create_task(self._message_consumer())
else:
logger.warning("Voice client is not connected; cannot announce message.")

async def _announce_text(self, game_timer, message):
"""Announce a message in the text channel."""
async def _announce_text(self, game_timer, message: str):
"""
Send an embed message to the designated text channel.
Args:
game_timer: An instance containing game state and channel information.
message (str): The message to send as an embed.
"""
if game_timer.channel:
try:
embed = discord.Embed(description=message, color=0x00ff00)
Expand All @@ -44,18 +68,27 @@ async def _announce_text(self, game_timer, message):
logger.warning("Cannot send message; text channel is not set.")

async def _message_consumer(self):
"""Continuously consume messages from the queue and play them sequentially."""
"""
Continuously process messages from the queue and play them in the voice channel.
This coroutine runs as a background task, ensuring that TTS messages are
played sequentially without overlapping.
"""
try:
while True:
game_timer, msg = await self.queue.get()
if game_timer.voice_client and game_timer.voice_client.is_connected():
try:
await self.tts_manager.play_tts(game_timer.voice_client, msg)
logger.info(f"Played TTS message in voice channel: {msg}")
except Exception as e:
logger.error(f"Error during voice announcement: {e}", exc_info=True)
# Mark the task as done
else:
logger.warning("Voice client disconnected while processing queue.")
# Indicate that the queued task is done
self.queue.task_done()
except asyncio.CancelledError:
logger.info("Message consumer task cancelled")
finally:
self._consumer_running = False
logger.debug("Message consumer has stopped running.")
59 changes: 48 additions & 11 deletions src/database.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,34 @@
# database.py

from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker, declarative_base

from src.utils.config import DATABASE_URL

# Create the SQLAlchemy engine
# Initialize the SQLAlchemy engine with the provided database URL.
# The 'check_same_thread' parameter is set to False to allow usage with multiple threads.
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})

# Create a configured "Session" class
# Create a configured "Session" class for database interactions.
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

# Base class for declarative class definitions
# Base class for declarative class definitions.
Base = declarative_base()


class ServerSettings(Base):
"""Model for storing server-specific settings."""
"""
Represents server-specific settings in the database.
Attributes:
id (int): Primary key for the server settings record.
server_id (str): Unique identifier for the Discord server.
prefix (str): Command prefix used by the bot in the server.
timer_channel (str): Name of the text channel designated for timer announcements.
voice_channel (str): Name of the voice channel designated for TTS announcements.
tts_language (str): Language setting for text-to-speech announcements.
mindful_messages_enabled (int): Flag indicating if mindful messages are enabled (1) or disabled (0).
"""
__tablename__ = "server_settings"

id = Column(Integer, primary_key=True, index=True)
server_id = Column(String, unique=True, index=True, nullable=False)
prefix = Column(String, default="!", nullable=False)
Expand All @@ -25,25 +37,50 @@ class ServerSettings(Base):
tts_language = Column(String, default="en-US-AriaNeural", nullable=False)
mindful_messages_enabled = Column(Integer, default=0, nullable=False)


class StaticEvent(Base):
"""Model for storing static events."""
"""
Represents static events in the database.
Attributes:
id (int): Primary key for the static event record.
guild_id (str): Identifier for the Discord guild/server.
mode (str): Game mode ('regular' or 'turbo') associated with the event.
time (int): Time in seconds when the event should trigger.
message (str): Message to announce when the event triggers.
"""
__tablename__ = "static_events"

id = Column(Integer, primary_key=True, index=True)
guild_id = Column(String, index=True, nullable=False)
mode = Column(String, index=True, nullable=False) # 'regular' or 'turbo'
time = Column(Integer, nullable=False) # Stored as seconds
message = Column(String, nullable=False)


class PeriodicEvent(Base):
"""Model for storing periodic events."""
"""
Represents periodic events in the database.
Attributes:
id (int): Primary key for the periodic event record.
guild_id (str): Identifier for the Discord guild/server.
mode (str): Game mode ('regular' or 'turbo') associated with the event.
start_time (int): Time in seconds when the event series starts.
interval (int): Interval in seconds between consecutive event triggers.
end_time (int): Time in seconds when the event series ends.
message (str): Message to announce when the event triggers.
"""
__tablename__ = "periodic_events"

id = Column(Integer, primary_key=True, index=True)
guild_id = Column(String, index=True, nullable=False)
mode = Column(String, index=True, nullable=False) # 'regular' or 'turbo'
start_time = Column(Integer, nullable=False) # in seconds
interval = Column(Integer, nullable=False) # in seconds
end_time = Column(Integer, nullable=False) # in seconds
interval = Column(Integer, nullable=False) # in seconds
end_time = Column(Integer, nullable=False) # in seconds
message = Column(String, nullable=False)

# Create all tables in the database

# Create all tables in the database based on the defined models.
Base.metadata.create_all(bind=engine)
Loading

0 comments on commit 3b573a2

Please sign in to comment.