Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 8 additions & 4 deletions .github/workflows/workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,15 @@ jobs:
run: uv python install ${{ env.LATEST_PYTHON_VERSION }}

- name: Install dependencies
run: uv sync --locked --all-extras --dev
run: uv sync --locked --all-extras --group dev

- name: Run tests with coverage
run: uv run --no-sync pytest tests/unit
uses: nick-fields/retry@v3
with:
timeout_minutes: 2
max_attempts: 3
command: uv run --no-sync pytest tests/unit
shell: bash

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
Expand All @@ -63,8 +68,7 @@ jobs:
run: uv python install ${{ env.LATEST_PYTHON_VERSION }}

- name: Install dependencies
run: uv sync --locked --all-extras --dev
shell: bash
run: uv sync --locked --all-extras --group dev

- name: Run integration tests
uses: nick-fields/retry@v3
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,6 @@ cython_debug/
# Custom
/junit.xml
*.prof

# Local logs
/logs/*
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
repos:
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.10.0
rev: 0.10.3
hooks:
- id: uv-lock
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ COPY --chmod=555 uv.lock ${WORKDIR}
COPY --chmod=555 .env ${WORKDIR}

RUN set -ex && \
mkdir -p ${LOG_DIRECTORY} && \
mkdir -p "${LOG_DIRECTORY}" && \
uv sync --frozen --no-dev && \
uv cache clean && \
chown -R botuser:botuser ${WORKDIR}
chown -R botuser:botuser "${WORKDIR}"

USER botuser
414 changes: 292 additions & 122 deletions README.md

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions assets/DiscordBot-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
110 changes: 53 additions & 57 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,66 +1,66 @@
[project]
name = "DiscordBot"
version = "3.0.1"
description = "A Bot for Discord"
version = "3.0.2"
description = "A simple Discord bot with OpenAI support and server administration tools"
urls.Repository = "https://github.com/ddc/DiscordBot"
urls.Homepage = "https://github.com/ddc/DiscordBot"
license = {text = "MIT"}
license = { text = "MIT" }
readme = "README.md"
authors = [
{name = "Daniel Costa", email = "ddcsoftwares@proton.me"},
{ name = "Daniel Costa", email = "ddcsoftwares@proton.me" },
]
maintainers = [
{name = "Daniel Costa"},
{ name = "Daniel Costa" },
]
keywords = [
"python", "python3", "python-3",
"DiscordBot", "discord-bots", "bot",
"discord-py", "discord"
"python", "python3", "python-3",
"DiscordBot", "discord-bots", "bot",
"discord-py", "discord"
]
classifiers = [
"Topic :: Communications :: Chat",
"Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.14",
"Operating System :: OS Independent",
"Environment :: Other Environment",
"Intended Audience :: Developers",
"Natural Language :: English",
"Topic :: Communications :: Chat",
"Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.14",
"Operating System :: OS Independent",
"Environment :: Other Environment",
"Intended Audience :: Developers",
"Natural Language :: English",
]
requires-python = ">=3.14"
dependencies = [
"alembic>=1.18.4",
"beautifulsoup4>=4.14.3",
"better-profanity>=0.7.0",
"ddcdatabases[postgres]>=3.0.10",
"discord-py>=2.6.4",
"gTTS>=2.5.4",
"openai>=2.20.0",
"PyNaCl>=1.6.2",
"pythonLogs>=6.0.2",
"alembic>=1.18.4",
"beautifulsoup4>=4.14.3",
"better-profanity>=0.7.0",
"ddcdatabases[postgres]>=3.0.10",
"discord-py>=2.6.4",
"gTTS>=2.5.4",
"openai>=2.21.0",
"PyNaCl>=1.6.2",
"pythonLogs>=6.0.2",
]

[dependency-groups]
dev = [
"pytest-asyncio>=1.3.0",
"pytest-cov>=7.0.0",
"testcontainers[postgres]>=4.14.1",
"poethepoet>=0.41.0",
"ruff>=0.15.0",
"black>=26.1.0",
"pytest-asyncio>=1.3.0",
"pytest-cov>=7.0.0",
"testcontainers[postgres]>=4.14.1",
"poethepoet>=0.41.0",
"ruff>=0.15.1",
]

[tool.poe.tasks]
linter.shell = "uv run ruff check --fix . && uv run black ."
profile.sequence = ["linter", {shell = "uv run python -m cProfile -o cprofile_unit.prof -m pytest tests/unit --no-cov"}]
profile-integration.sequence = ["linter", {shell = "uv run python -m cProfile -o cprofile_integration.prof -m pytest tests/integration --no-cov"}]
test.sequence = ["linter", {shell = "uv run pytest"}]
test-integration.sequence = ["linter", {shell = "uv run pytest tests/integration --no-cov"}]
linter.shell = "uv run ruff check --fix . && uv run ruff format ."
profile = "uv run python -m cProfile -o cprofile_unit.prof -m pytest tests/unit --no-cov"
profile-integration = "uv run python -m cProfile -o cprofile_integration.prof -m pytest tests/integration --no-cov"
test = "uv run pytest tests/unit"
test-integration = "uv run pytest tests/integration --no-cov"
hadolint.shell = "docker run --rm -i -v $(pwd)/.hadolint.yaml:/.config/hadolint.yaml:ro hadolint/hadolint < Dockerfile"
test-docker.shell = "uv run pytest tests/docker -v --no-cov"
test-docker = "uv run pytest tests/docker -v --no-cov"
migration = "uv run --frozen alembic upgrade head"
tests.sequence = ["test", "test-integration", "hadolint", "test-docker"]
updatedev.sequence = ["linter", {shell = "uv lock --upgrade && uv sync --all-extras --group dev"}]
migration.shell = "uv run --frozen alembic upgrade head"

[tool.pytest.ini_options]
addopts = "-v --import-mode=importlib --cov --cov-report=term --cov-report=xml --junitxml=junit.xml"
Expand All @@ -70,35 +70,31 @@ testpaths = ["tests/unit"]
asyncio_mode = "strict"
asyncio_default_fixture_loop_scope = "function"
markers = [
"integration: marks tests as integration tests",
"docker: Docker and compose file tests",
"integration: marks tests as integration tests",
"docker: Docker and compose file tests",
]
[tool.coverage.run]
omit = [
"tests/*",
"*/__init__.py",
"tests/*",
"*/__init__.py",
]

[tool.coverage.report]
show_missing = true
skip_covered = false
exclude_lines = [
"pragma: no cover",
"def __repr__",
"if self.debug:",
"if settings.DEBUG",
"raise AssertionError",
"raise NotImplementedError",
"if 0:",
"if __name__ == .__main__.:",
"class .*\\bProtocol\\):",
"@(abc\\.)?abstractmethod",
"pragma: no cover",
"def __repr__",
"if self.debug:",
"if settings.DEBUG",
"raise AssertionError",
"raise NotImplementedError",
"if 0:",
"if __name__ == .__main__.:",
"class .*\\bProtocol\\):",
"@(abc\\.)?abstractmethod",
]

[tool.black]
line-length = 120
skip-string-normalization = true

[tool.ruff]
line-length = 120
target-version = "py314"
Expand Down
2 changes: 1 addition & 1 deletion src/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ async def main() -> None:


def run_bot() -> None:
print(messages.BOT_STARTING.format(variables.TIME_BEFORE_START))
print(messages.bot_starting(variables.TIME_BEFORE_START))
time.sleep(variables.TIME_BEFORE_START)

try:
Expand Down
4 changes: 2 additions & 2 deletions src/bot/cogs/admin/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ async def botgame(self, ctx: commands.Context, *, game: str) -> None:
await self.bot.change_presence(activity=activity)

# Create success embed
embed = self._create_admin_embed(f"```{messages.BOT_ANNOUNCE_PLAYING.format(game)}```")
embed = self._create_admin_embed(f"```{messages.bot_announce_playing(game)}```")
embed.set_author(
name=self.bot.user.display_name,
icon_url=self.bot.user.avatar.url if self.bot.user.avatar else None,
Expand All @@ -58,7 +58,7 @@ async def _warn_about_bg_activity_timer(self, ctx: commands.Context) -> None:
"""Show warning if background activity timer will override the game status."""
bg_activity_timer = self.bot.settings["bot"]["BGActivityTimer"]
if bg_activity_timer and bg_activity_timer > 0:
bg_task_warning = messages.BG_TASK_WARNING.format(bg_activity_timer)
bg_task_warning = messages.bg_task_warning(bg_activity_timer)
warning_embed = self._create_admin_embed(bg_task_warning)
await bot_utils.send_embed(ctx, warning_embed, False)

Expand Down
16 changes: 8 additions & 8 deletions src/bot/cogs/admin/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ async def config_pfilter(ctx: commands.Context, *, subcommand_passed: str) -> No
case _:
raise commands.BadArgument(message="BadArgument")

msg = messages.CONFIG_PFILTER.format(status.upper(), channel.name)
msg = messages.config_pfilter(status.upper(), channel.name)
embed = discord.Embed(description=msg, color=color)
await bot_utils.send_embed(ctx, embed)
return None
Expand Down Expand Up @@ -273,7 +273,7 @@ async def config_list(ctx: commands.Context) -> None:

# Format channel names
if pf:
channel_names_lst = [channel['channel_name'] for channel in pf]
channel_names_lst = [channel["channel_name"] for channel in pf]
channel_names = "\n".join(channel_names_lst)
else:
channel_names = messages.NO_CHANNELS_LISTED
Expand Down Expand Up @@ -314,7 +314,7 @@ async def config_list(ctx: commands.Context) -> None:
inline=True,
)
embed.add_field(
name=f"🎭 {messages.CONFIG_BOT_WORD_REACTIONS.format(ctx.guild.name)}",
name=f"🎭 {messages.CONFIG_BOT_WORD_REACTIONS}",
value=f"{on}" if sc["bot_word_reactions"] else f"{off}",
inline=True,
)
Expand Down Expand Up @@ -412,9 +412,9 @@ async def _handle_update(
# Set updating state and disable all buttons
self._updating = True
for item in self.children:
if hasattr(item, 'disabled'):
if hasattr(item, "disabled"):
item.disabled = True
if hasattr(item, 'style'):
if hasattr(item, "style"):
item.style = discord.ButtonStyle.gray

# Defer the response to allow editing the original message
Expand Down Expand Up @@ -466,7 +466,7 @@ async def _handle_update(
async def _restore_buttons(self):
"""Restore button states and colors."""
for item in self.children:
if hasattr(item, 'disabled'):
if hasattr(item, "disabled"):
item.disabled = False

# Restore original button colors
Expand Down Expand Up @@ -500,7 +500,7 @@ async def _create_updated_embed(self):

# Format channel names
if pf:
channel_names_lst = [channel['channel_name'] for channel in pf]
channel_names_lst = [channel["channel_name"] for channel in pf]
channel_names = "\n".join(channel_names_lst)
else:
channel_names = messages.NO_CHANNELS_LISTED
Expand Down Expand Up @@ -543,7 +543,7 @@ async def _create_updated_embed(self):
inline=True,
)
embed.add_field(
name=f"🎭 {messages.CONFIG_BOT_WORD_REACTIONS.format(self.ctx.guild.name)}",
name=f"🎭 {messages.CONFIG_BOT_WORD_REACTIONS}",
value=f"{on}" if self.server_config["bot_word_reactions"] else f"{off}",
inline=True,
)
Expand Down
3 changes: 1 addition & 2 deletions src/bot/cogs/admin/custom_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,7 @@ async def remove_custom_command(ctx: commands.Context, cmd_name: str) -> None:
command_names = {cmd["name"] for cmd in server_commands}
if cmd_name not in command_names:
error_msg = (
f"{messages.CUSTOM_COMMAND_UNABLE_REMOVE}\n"
f"{messages.COMMAND_NOT_FOUND}: {chat_formatting.inline(cmd_name)}"
f"{messages.CUSTOM_COMMAND_UNABLE_REMOVE}\n{messages.COMMAND_NOT_FOUND}: {chat_formatting.inline(cmd_name)}"
)
return await bot_utils.send_error_msg(ctx, error_msg)

Expand Down
2 changes: 1 addition & 1 deletion src/bot/cogs/dice_rolls.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ async def roll_results(self, ctx: commands.Context) -> None:
server_rolls = await dice_rolls_dal.get_all_server_rolls(ctx.guild.id, dice_size)

if not server_rolls:
return await bot_utils.send_error_msg(ctx, messages.NO_DICE_SIZE_ROLLS.format(dice_size))
return await bot_utils.send_error_msg(ctx, messages.no_dice_size_rolls(dice_size))

embed = self._create_results_embed(ctx, server_rolls, dice_size)
await bot_utils.send_embed(ctx, embed)
Expand Down
32 changes: 16 additions & 16 deletions src/bot/cogs/events/on_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,22 +55,22 @@ def __init__(self, bot: Bot) -> None:
self.bot = bot
self.command_logger = CommandLogger(bot)

@self.bot.event
async def on_command(ctx: commands.Context) -> None:
"""Handle command execution event.

Called when a valid command gets executed successfully.
This is useful for logging command usage and monitoring.

Args:
ctx: The command context containing information about the command execution
"""
try:
# Log command execution for monitoring purposes
self.command_logger.log_command_execution(ctx)
# Future: Add command usage statistics, rate limiting, etc.
except Exception as e:
self.bot.log.error(f"Error in on_command event: {e}")
@commands.Cog.listener()
async def on_command(self, ctx: commands.Context) -> None:
"""Handle command execution event.

Called when a valid command gets executed successfully.
This is useful for logging command usage and monitoring.

Args:
ctx: The command context containing information about the command execution
"""
try:
# Log command execution for monitoring purposes
self.command_logger.log_command_execution(ctx)
# Future: Add command usage statistics, rate limiting, etc.
except Exception as e:
self.bot.log.error(f"Error in on_command event: {e}")


async def setup(bot: Bot) -> None:
Expand Down
Loading