Skip to content

Refactor bot into cog and update tests#2

Open
UN7X wants to merge 5 commits intomasterfrom
aq5mun-codex/revamp-bot,-add-unit-tests,-and-integrate-setup-wizard
Open

Refactor bot into cog and update tests#2
UN7X wants to merge 5 commits intomasterfrom
aq5mun-codex/revamp-bot,-add-unit-tests,-and-integrate-setup-wizard

Conversation

@UN7X
Copy link
Copy Markdown
Owner

@UN7X UN7X commented Jun 8, 2025

Summary

  • reshape 7xbot.py into a cog to match the repo's modular structure
  • streamline command replies using embeds
  • add comprehensive unit tests for database helpers and commands

Testing

  • pytest -q

https://chatgpt.com/codex/tasks/task_e_684373bacd28832baf456a77e6db9559

Summary by Sourcery

Refactor the bot into a Cog-based architecture with separate modules for core functionality, music, utilities, games, economy, leveling, moderation, and admin features; introduce asyncpg-backed database integration, centralize configuration, and add unit tests and deployment configs.

New Features:

  • Restructure the bot into modular Cog classes (Core, Music, Utility, Fun, Games, Economy, Leveling, Moderation, AutoMod, Admin)
  • Introduce a Database class with asyncpg for persistent guild settings, economy, leveling, and moderation logs
  • Implement main.py as the new entry point with dynamic prefixes and automatic Cog loading

Enhancements:

  • Centralize configuration in a Config class and standardize command replies using embeds
  • Streamline server setup with an interactive setup wizard and dynamic per-guild prefixes
  • Provide comprehensive shop, points, and manual commands with standardized formatting and error handling

Build:

  • Update requirements.txt with new dependencies (psutil, asyncpg, aiohttp, etc.)

Deployment:

  • Add Heroku deployment support via Procfile and app.json

Tests:

  • Add pytest tests for helper functions (get_uptime, days_until_christmas), database load/save, setup wizard, shop logic, manual pages, and Core Cog commands

Chores:

  • Remove legacy monolithic 7xbot.py implementation and reorganize codebase into cogs directory

Summary by CodeRabbit

  • New Features

    • Introduced a modular Discord bot with advanced features, including moderation, economy, leveling, games, music playback, fun commands, auto-moderation, and utility/info commands.
    • Added dynamic command prefix support, paginated help system, and server-specific configurations.
    • Implemented owner/admin commands for bot management, cog reloading, and SQL/eval functionality.
    • Provided interactive setup wizard and shop system for server customization.
    • Enabled PostgreSQL database integration for persistent data storage.
    • Added comprehensive music playback with queue management and internet radio support.
    • Introduced detailed leveling system with XP tracking and leaderboards.
    • Added extensive economy system with wallet, bank, daily rewards, work, rob, gamble, and leaderboard commands.
    • Implemented multiple interactive games including rock-paper-scissors, number guessing, trivia, and tic-tac-toe.
    • Added auto-moderation features including spam detection, bad word filtering, caps and mention limits, with configuration commands.
    • Enhanced utility commands with server/user info, bot stats, prefix management, and a rich paginated help interface.
  • Bug Fixes

    • Not applicable.
  • Documentation

    • Added configuration and deployment files for Heroku, including environment variable setup and buildpacks.
    • Included sample cookies and Python version specification for environment consistency.
  • Tests

    • Added comprehensive test suite covering core bot features, command responses, and database operations.
  • Chores

    • Updated requirements with necessary dependencies for bot operation and feature support.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Jun 8, 2025

Reviewer's Guide

Restructured the monolithic Discord bot into a modular Cog-based architecture, extracted configuration and database logic into dedicated modules, introduced a main entrypoint class for dynamic Cog loading, updated dependency versions, and added comprehensive unit tests for core helper functions and commands.

Sequence Diagram: User Onboarding with Setup Wizard

sequenceDiagram
    actor User
    participant CoreCog
    participant Database

    User->>CoreCog: Sends command `7/setup`
    CoreCog->>User: "Starting setup wizard. Reply with 'cancel' to stop."
    CoreCog->>User: "Enter strike roles by mentioning them... or 'none':"
    User->>CoreCog: Replies with strike roles (e.g., @Role1 @Role2)
    CoreCog->>CoreCog: Parses strike roles
    CoreCog->>User: "Enable AI features? (yes/no):"
    User->>CoreCog: Replies with AI preference (e.g., yes)
    CoreCog->>CoreCog: Parses AI preference
    CoreCog->>User: "Economy type (regular/prankful/none):"
    User->>CoreCog: Replies with economy type (e.g., regular)
    CoreCog->>CoreCog: Parses economy type
    CoreCog->>Database: save_db(updated_config)
    Database-->>CoreCog: Success
    CoreCog->>User: "Setup complete."
Loading

Class Diagram: Core Cog and Helper Functions

classDiagram
    class CoreCog {
        +bot : commands.Bot
        +__init__(bot_)
        +on_guild_join(guild)
        +beta(ctx, option)
        +setup_cmd(ctx, args)  // Was setup_wizard
        +shop(ctx, args)
        +shop_buy(ctx, item)
        +points(ctx)
        +add(ctx, member, amount) // Points add
        +remove(ctx, member, amount) // Points remove
        +points_query(ctx, member) // Points query
        +manual(ctx, arg) // Was man_command
    }
    class commands.Cog
    CoreCog --|> commands.Cog

    class GlobalHelpers {
        <<Module: 7xbot.py>>
        +bot_start_time : datetime
        +db : dict
        +man_pages : dict
        +get_uptime(start_time) str
        +days_until_christmas() int
        +get_bot_info(dbot, ctx) dict
        +get_build_id() str
        +load_db(filename) dict
        +save_db(data, filename) None
        +get_server_config(guild_id) dict
        +get_shop_items_for_guild(guild_id) dict
        +check_points(user_id) int
        +set_points(user_id, pts) None
        +update_points(user_id, pts) None
    }
    CoreCog ..> GlobalHelpers : Uses utility functions
    CoreCog ..> Config : Uses for constants

    class Config {
        <<External>>
        +PRIMARY_COLOR
        +ERROR_COLOR
        +SUCCESS_COLOR
        +WARNING_COLOR
    }
Loading

Class Diagram: Music Cog and Associated Classes

classDiagram
    class MusicCog {
        +bot : commands.Bot
        +players : dict
        +session : aiohttp.ClientSession
        +check_empty_voice_task : asyncio.Task
        +__init__(bot)
        +cog_unload()
        +get_session() aiohttp.ClientSession
        +cleanup(guild) None
        +check_empty_voice_channels() None
        +get_player(ctx) MusicPlayer
        +is_valid_url(url) bool
        +is_audio_url(url) bool
        +join(ctx) None
        +play(ctx, query) None
        +radio(ctx, station_name) None
        +pause(ctx) None
        +resume(ctx) None
        +skip(ctx) None
        +queue_info(ctx) None
        +now_playing(ctx) None
        +volume(ctx, volume) None
        +stop(ctx) None
        +leave(ctx) None
        +ensure_voice(ctx) None
    }
    class commands.Cog
    MusicCog --|> commands.Cog
    MusicCog o-- MusicPlayer : players
    MusicCog ..> AudioSource : Creates
    MusicCog ..> RadioStation : Uses
    MusicCog ..> Config : Uses for constants

    class MusicPlayer {
        +bot : commands.Bot
        +guild : discord.Guild
        +channel : discord.TextChannel
        +cog : MusicCog
        +queue : asyncio.Queue
        +next : asyncio.Event
        +current : AudioSource
        +volume : float
        +now_playing : discord.Message
        +__init__(ctx)
        +player_loop() None
        +destroy(guild) None
    }

    class AudioSource {
        +requester : discord.Member
        +channel : discord.TextChannel
        +url : str
        +title : str
        +duration : str
        +thumbnail : str
        +uploader : str
        +stream_url : str
        +__init__(ctx, source, url, title, duration, thumbnail, uploader)
        +create_source(cls, ctx, url, title, duration, thumbnail, uploader) AudioSource
    }
    class discord.PCMVolumeTransformer
    AudioSource --|> discord.PCMVolumeTransformer

    class RadioStation {
        +STATIONS : dict
        +get_station(cls, name) dict
        +list_stations(cls) dict
    }

    class Config {
        <<External>>
        +PRIMARY_COLOR
        +ERROR_COLOR
    }
Loading

Class Diagram: AutoMod Cog

classDiagram
    class AutoModCog {
        +bot : commands.Bot
        +spam_tracker : dict
        +bad_words : list
        +__init__(bot)
        +on_message(message) None
        +check_spam(message) None
        +check_bad_words(message) None
        +check_caps(message) None
        +check_mentions(message) None
        +automod_settings(ctx, setting, value) None
        +add_bad_word(ctx, word) None
        +remove_bad_word(ctx, word) None
    }
    class commands.Cog
    AutoModCog --|> commands.Cog
    AutoModCog ..> Config : Uses for constants
    AutoModCog ..> DB : Uses bot.db.add_warning

    class DB {
        <<Assumed Interface>>
        +add_warning(guild_id, user_id, moderator_id, reason)
    }

    class Config {
        <<External>>
        +ERROR_COLOR
        +WARNING_COLOR
        +PRIMARY_COLOR
        +SUCCESS_COLOR
    }
Loading

File-Level Changes

Change Details Files
Refactored the bot into multiple Cog modules
  • Created separate Cogs for core commands, utility, fun, moderation, economy, games, music, leveling, automod, and admin
  • Converted command functions into Cog class methods and listeners
  • Updated 7xbot.py to only expose helpers and register the Core Cog
  • Implemented main.py with AdvancedBot to load all Cogs dynamically on startup
7xbot.py
main.py
cogs/music.py
cogs/utility.py
cogs/fun.py
cogs/moderation.py
cogs/economy.py
cogs/games.py
cogs/automod.py
cogs/admin.py
cogs/leveling.py
Extracted configuration and database logic into standalone modules
  • Moved environment-driven settings into a Config dataclass in config.py
  • Encapsulated asyncpg pool and CRUD methods in database.py
  • Refactored JSON-based helpers (load_db, save_db) and shop utilities into 7xbot.py
config.py
database.py
7xbot.py
Added comprehensive unit tests for helper functions and commands
  • Covered uptime and Christmas countdown helpers
  • Tested JSON DB load/save behavior and missing-file fallback
  • Verified setup_wizard flows (success, cancel, help)
  • Validated shop item selection, points update, man_command listing, and Beta info embed
tests/test_basic.py
Updated and pinned project dependencies
  • Pinned discord.py>=2.3.0, python-dotenv>=1.0.0 and added new libraries (psutil, asyncpg, yt-dlp, PyNaCl, wavelink, aiohttp, psycopg2-binary, ffmpeg-python, mutagen, pydub)
  • Removed redundant or unused imports from the monolithic script
requirements.txt

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jun 8, 2025

## Walkthrough

This update introduces a comprehensive Discord bot project with a modular architecture. It adds core infrastructure files, a main entry point, configuration management, PostgreSQL database integration, and a suite of cogs for moderation, music, fun, games, economy, leveling, admin, automod, and utility features. Extensive tests and deployment files are included.

## Changes

| File(s) / Path(s)                               | Change Summary                                                                                         |
|-------------------------------------------------|--------------------------------------------------------------------------------------------------------|
| .python-version                                 | Added file specifying Python 3.11 version for environment management.                                  |
| Procfile                                        | Added file to define Heroku worker process (`python main.py`).                                         |
| app.json                                        | Added Heroku deployment metadata, environment variables, add-ons, and buildpacks.                      |
| cookies.txt                                     | Added Netscape-format cookie file with YouTube cookies.                                                |
| requirements.txt                                | Updated: added and version-pinned dependencies for Discord, database, music, and utility libraries.    |
| config.py                                       | Added `Config` dataclass for centralized bot configuration from environment/defaults.                  |
| database.py                                     | Added `Database` class for async PostgreSQL operations, schema setup, and bot data access.             |
| main.py                                         | Added main bot file: defines `AdvancedBot`, cog loading, event handling, error management, and startup.|
| 7xbot.py                                        | Refactored: modularized bot helpers, simplified commands, added `Core` cog, removed legacy logic.      |
| cogs/admin.py                                   | Added admin cog: owner-only commands for cog management, eval, SQL, guild listing, and shutdown.       |
| cogs/automod.py                                 | Added automod cog: message listener, spam/bad word/caps/mention checks, and settings commands.         |
| cogs/economy.py                                 | Added economy cog: balance, daily, work, rob, gamble, deposit, withdraw, pay, leaderboard commands.    |
| cogs/fun.py                                     | Added fun cog: 8ball, flip, roll, choose, joke, meme, fact, quote, reverse, ascii art, poll commands.  |
| cogs/games.py                                   | Added games cog: rock-paper-scissors, number guessing, trivia, and tic-tac-toe commands.              |
| cogs/leveling.py                                | Added leveling cog: XP/level tracking, level/rank/leaderboard commands, message XP listener.           |
| cogs/moderation.py                              | Added moderation cog: kick, ban, unban, mute, unmute, warn, warnings, clear commands with checks.      |
| cogs/music.py                                   | Added music cog: music playback, queue, radio, pause, resume, skip, volume, and now playing commands.  |
| cogs/utility.py                                 | Added utility cog: help (paginated), ping, serverinfo, userinfo, avatar, botinfo, invite, prefix.      |
| tests/test_basic.py                             | Added test suite for `7xbot.py`: covers helpers, database, commands, shop, and manual/help features.   |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant Discord
    participant AdvancedBot
    participant Cog
    participant Database

    User->>Discord: Sends command message (e.g., !balance)
    Discord->>AdvancedBot: Forwards message event
    AdvancedBot->>Database: Fetch prefix for guild
    AdvancedBot->>Cog: Dispatches command to appropriate cog
    Cog->>Database: Query or update data (e.g., get balance)
    Database-->>Cog: Returns data/result
    Cog-->>AdvancedBot: Sends response
    AdvancedBot->>Discord: Sends embed/message to channel
sequenceDiagram
    participant Owner
    participant Discord
    participant AdvancedBot
    participant AdminCog

    Owner->>Discord: Sends admin command (e.g., !reload cog)
    Discord->>AdvancedBot: Forwards message event
    AdvancedBot->>AdminCog: Handles reload command
    AdminCog->>AdvancedBot: Loads/unloads/reloads extension
    AdminCog-->>AdvancedBot: Sends success/failure embed
    AdvancedBot->>Discord: Sends response to channel
Loading

Poem

A bunny hops through lines of code,
With cogs and configs neatly stowed.
From music, games, to leveling fun,
The bot now shines for everyone!
Tests and cookies, helpers too—
This warren’s work is nearly through.
Deploy, enjoy, and bunny cheers to you! 🐇✨


<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=UN7X/7xbot&utm_content=2):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 24

♻️ Duplicate comments (1)
cogs/economy.py (1)

238-244: Same concern for withdraw – see previous comment about consolidating balance updates.

🧹 Nitpick comments (27)
config.py (1)

30-42: Harden owner-id parsing & de-dupe entries.

int(owner) will raise if an env var contains non-numeric chars.
Also, duplicates across OWNER_ID & ADDITIONAL_OWNER_IDS aren’t filtered.
Consider safe parsing with str.isdigit() (or re.fullmatch(r'\d+')) and cast into a set before finally converting to list.

database.py (2)

2-6: Prune unused imports to silence Ruff F401 warnings.

asyncio, json, urlparse, Any aren’t referenced.

-import asyncio
-from typing import Optional, List, Dict, Any
-import json
-from urllib.parse import urlparse
+from typing import Optional, List, Dict
🧰 Tools
🪛 Ruff (0.11.9)

2-2: asyncio imported but unused

Remove unused import: asyncio

(F401)


3-3: typing.Any imported but unused

Remove unused import: typing.Any

(F401)


4-4: json imported but unused

Remove unused import: json

(F401)


6-6: urllib.parse.urlparse imported but unused

Remove unused import: urllib.parse.urlparse

(F401)


181-185: Wrap pool shutdown to avoid masking teardown errors.

If the pool is already closed or the event loop is stopping, await self.pool.close() might raise and prevent the bot from finishing cleanup. Surround with contextlib.suppress(Exception) or at least log & continue.

app.json (1)

25-26: Consider PostgreSQL plan limitations for production.

The hobby-dev plan has limitations (10,000 rows, 1GB storage) that may be insufficient for a production bot with leveling and economy features. Consider upgrading to a paid plan for production deployment.

cogs/leveling.py (3)

3-3: Remove unused import.

The asyncio import is not used in this file.

-import asyncio
🧰 Tools
🪛 Ruff (0.11.9)

3-3: asyncio imported but unused

Remove unused import: asyncio

(F401)


24-26: Simplify nested if statements.

The nested if statements can be combined for better readability.

-        if user_key in self.xp_cooldowns:
-            if now < self.xp_cooldowns[user_key]:
-                return
+        if user_key in self.xp_cooldowns and now < self.xp_cooldowns[user_key]:
+            return
🧰 Tools
🪛 Ruff (0.11.9)

24-25: Use a single if statement instead of nested if statements

Combine if statements using and

(SIM102)


32-33: Consider XP farming protection.

The current implementation only uses time-based cooldowns. Consider additional anti-farming measures:

  1. Message length requirements - Very short messages shouldn't award XP
  2. Channel restrictions - Exclude spam channels
  3. Rate limiting per channel - Prevent rapid-fire messaging

Example implementation:

         # Give XP
-        xp_gained = random.randint(10, self.bot.config.XP_PER_MESSAGE)
-        leveled_up = await self.bot.db.add_xp(message.author.id, message.guild.id, xp_gained)
+        # Anti-farming: require minimum message length
+        if len(message.content.strip()) < 5:
+            return
+            
+        xp_gained = random.randint(10, self.bot.config.XP_PER_MESSAGE)
+        leveled_up = await self.bot.db.add_xp(message.author.id, message.guild.id, xp_gained)
cogs/utility.py (2)

386-395: Split chained elif … : emoji = … statements for readability & PEP 8 compliance

Multiple statements on one line violate E701 and make future edits painful.

-elif "🛡️" in cog.description: emoji = "🛡️"
+elif "🛡️" in cog.description:
+    emoji = "🛡️"

Apply the same pattern to the other elif branches in this block.

🧰 Tools
🪛 Ruff (0.11.9)

387-387: Multiple statements on one line (colon)

(E701)


388-388: Multiple statements on one line (colon)

(E701)


389-389: Multiple statements on one line (colon)

(E701)


390-390: Multiple statements on one line (colon)

(E701)


391-391: Multiple statements on one line (colon)

(E701)


392-392: Multiple statements on one line (colon)

(E701)


393-393: Multiple statements on one line (colon)

(E701)


394-394: Multiple statements on one line (colon)

(E701)


316-407: help_command has grown into a 200-line monolith – extract helpers

Ruff/Pylint already flag 16 locals, 24 branches, 50+ statements (R0914/12/15).
Consider:

  1. _render_command_help(...)
  2. _render_category_help(...)
  3. _gather_cogs_data(...)

This will shrink cognitive load and improve testability.

🧰 Tools
🪛 Ruff (0.11.9)

387-387: Multiple statements on one line (colon)

(E701)


388-388: Multiple statements on one line (colon)

(E701)


389-389: Multiple statements on one line (colon)

(E701)


390-390: Multiple statements on one line (colon)

(E701)


391-391: Multiple statements on one line (colon)

(E701)


392-392: Multiple statements on one line (colon)

(E701)


393-393: Multiple statements on one line (colon)

(E701)


394-394: Multiple statements on one line (colon)

(E701)

🪛 Pylint (3.3.7)

[refactor] 316-316: Too many local variables (16/15)

(R0914)


[refactor] 316-316: Too many branches (24/12)

(R0912)


[refactor] 316-316: Too many statements (51/50)

(R0915)

tests/test_basic.py (1)

6-8: pytest import is unused

All assertions rely on the built-in assert keyword; the module itself is never referenced. Safe to drop:

-import pytest
🧰 Tools
🪛 Ruff (0.11.9)

6-6: pytest imported but unused

Remove unused import: pytest

(F401)

cogs/admin.py (2)

1-6: Prune unused imports

sys and traceback are never used. Remove them to satisfy Ruff F401.

-import sys
-import traceback
🧰 Tools
🪛 Ruff (0.11.9)

4-4: sys imported but unused

Remove unused import: sys

(F401)


5-5: traceback imported but unused

Remove unused import: traceback

(F401)


145-176: Unlimited SQL execution can brick the database or leak sensitive data

The sql command lets the owner run DROP DATABASE, long-running queries, or
dump user PII straight into Discord.

Minimum hardening:

  1. Reject mutating statements unless explicitly prefixed (e.g. !force flag).
  2. Truncate output to < 1900 chars to avoid “embed too large”.
  3. Wrap DB calls in asyncio.wait_for with a timeout.
-if len(output) > 1900:
-    output = output[:1890] + "…"

Also catch asyncpg.PostgresError separately to surface DB specifics.

cogs/economy.py (2)

4-6: Remove unused imports

datetime, timedelta, and asyncio are imported but never used, triggering Ruff F401.

-from datetime import datetime, timedelta
-import asyncio
🧰 Tools
🪛 Ruff (0.11.9)

4-4: datetime.datetime imported but unused

Remove unused import

(F401)


4-4: datetime.timedelta imported but unused

Remove unused import

(F401)


5-5: asyncio imported but unused

Remove unused import: asyncio

(F401)


200-206: Duplicate SQL path – favour single helper

deposit executes raw SQL while the rest of the cog relies on update_balance. This forks logic and increases maintenance cost (e.g., balance cache, audit-logging). Expose an transfer_balance(wallet_delta, bank_delta) helper in Database and call it here to DRY.

main.py (3)

3-8: Prune unused imports

asyncio and json are never referenced.

-import asyncio
-import json
🧰 Tools
🪛 Ruff (0.11.9)

3-3: asyncio imported but unused

Remove unused import: asyncio

(F401)


8-8: json imported but unused

Remove unused import: json

(F401)


130-133: Avoid printing raw messages in production

print() leaks potentially sensitive content to stdout. Replace with structured logging at DEBUG level.

🧰 Tools
🪛 Pylint (3.3.7)

[error] 133-133: Possibly using variable 'bot' before assignment

(E0606)


143-145: Use sys.exit for explicit termination

exit() is intended for the interactive interpreter; sys.exit(1) is clearer and suppresses pylint R1722.

-import logging.error("❌ DISCORD_TOKEN environment variable not found!")
-exit(1)
+logging.error("❌ DISCORD_TOKEN environment variable not found!")
+import sys; sys.exit(1)
🧰 Tools
🪛 Pylint (3.3.7)

[refactor] 144-144: Consider using 'sys.exit' instead

(R1722)

cogs/automod.py (2)

3-5: Drop unused modules

re and asyncio aren’t used anywhere in the cog.

-import re
-import asyncio
🧰 Tools
🪛 Ruff (0.11.9)

3-3: re imported but unused

Remove unused import: re

(F401)


4-4: asyncio imported but unused

Remove unused import: asyncio

(F401)


45-56: Unbounded spam_tracker growth

spam_tracker stores a list per user but never purges users after they leave or stay inactive, causing unbounded memory use on busy servers. Consider:

# after clearing old timestamps
if not self.spam_tracker[user_id]:
    self.spam_tracker.pop(user_id, None)
cogs/games.py (2)

60-120: Good game state management with proper cleanup.

The implementation correctly prevents multiple simultaneous games per user and ensures cleanup when games end. However, there's a minor issue with the hardcoded command prefix.

Fix the hardcoded command prefix:

-                return await ctx.send("❌ You don't have an active game! Start one with `!guess`")
+                return await ctx.send(f"❌ You don't have an active game! Start one with `{ctx.prefix}guess`")

129-155: Consider externalizing trivia questions for better maintainability.

The hardcoded trivia questions work for now, but consider moving them to a configuration file or database for easier updates and expansion.

Would you like me to help create a JSON file structure for storing trivia questions and update the code to load from it?

cogs/moderation.py (2)

129-129: Remove duplicate comment.

The comment "# Default 10 minutes" appears twice on the same line.

-            until = discord.utils.utcnow() + timedelta(minutes=10)  # Default 10 minutes  # Default 10 minutes
+            until = discord.utils.utcnow() + timedelta(minutes=10)  # Default 10 minutes

220-220: Rename unused loop variable.

The loop variable i is not used within the loop body.

-        for i, warning in enumerate(warnings[:10], 1):  # Show last 10 warnings
+        for _, warning in enumerate(warnings[:10], 1):  # Show last 10 warnings
🧰 Tools
🪛 Ruff (0.11.9)

220-220: Loop control variable i not used within loop body

Rename unused i to _i

(B007)

cogs/fun.py (1)

236-242: Incomplete ASCII art mapping limits functionality.

The ASCII art command only supports letters A-E. Users trying to convert text with other letters will get incomplete output.

Would you like me to help complete the ASCII art mapping for all letters, or implement a fallback for unsupported characters?

cogs/music.py (3)

5-13: Remove unused imports to silence Ruff F401 and reduce byte-code size

re, json, random, functools, itertools, deque, urlparse, and mimetypes are never referenced.
Prune them to keep the module lean and keep CI green.

-import re
-import json
-import random
-import itertools
-import functools
-from collections import deque
-from urllib.parse import urlparse
-import mimetypes
🧰 Tools
🪛 Ruff (0.11.9)

5-5: re imported but unused

Remove unused import: re

(F401)


6-6: json imported but unused

Remove unused import: json

(F401)


7-7: random imported but unused

Remove unused import: random

(F401)


10-10: functools imported but unused

Remove unused import: functools

(F401)


11-11: collections.deque imported but unused

Remove unused import: collections.deque

(F401)


12-12: urllib.parse.urlparse imported but unused

Remove unused import: urllib.parse.urlparse

(F401)


13-13: mimetypes imported but unused

Remove unused import: mimetypes

(F401)


199-210: Replace try/except/pass with contextlib.suppress

This block is exactly the pattern Ruff (SIM105) warns about.
Using contextlib.suppress is both clearer and marginally faster.

-        try:
-            await guild.voice_client.disconnect()
-        except AttributeError:
-            pass
-
-        try:
-            del self.players[guild.id]
-        except KeyError:
-            pass
+        from contextlib import suppress
+
+        with suppress(AttributeError):
+            await guild.voice_client.disconnect()
+
+        with suppress(KeyError):
+            del self.players[guild.id]
🧰 Tools
🪛 Ruff (0.11.9)

201-204: Use contextlib.suppress(AttributeError) instead of try-except-pass

Replace with contextlib.suppress(AttributeError)

(SIM105)


206-209: Use contextlib.suppress(KeyError) instead of try-except-pass

Replace with contextlib.suppress(KeyError)

(SIM105)


215-222: Flatten nested if for readability (Ruff SIM102)

-                if voice_client and voice_client.is_connected():
-                    # Check if there are any non-bot members in the voice channel
-                    if not any(member for member in voice_client.channel.members if not member.bot):
+                if (
+                    voice_client
+                    and voice_client.is_connected()
+                    and not any(m for m in voice_client.channel.members if not m.bot)
+                ):
🧰 Tools
🪛 Ruff (0.11.9)

218-220: Use a single if statement instead of nested if statements

(SIM102)

🪛 Pylint (3.3.7)

[refactor] 215-230: Too many nested blocks (6/5)

(R1702)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1d53ae1 and 6653706.

📒 Files selected for processing (19)
  • .python-version (1 hunks)
  • 7xbot.py (1 hunks)
  • Procfile (1 hunks)
  • app.json (1 hunks)
  • cogs/admin.py (1 hunks)
  • cogs/automod.py (1 hunks)
  • cogs/economy.py (1 hunks)
  • cogs/fun.py (1 hunks)
  • cogs/games.py (1 hunks)
  • cogs/leveling.py (1 hunks)
  • cogs/moderation.py (1 hunks)
  • cogs/music.py (1 hunks)
  • cogs/utility.py (1 hunks)
  • config.py (1 hunks)
  • cookies.txt (1 hunks)
  • database.py (1 hunks)
  • main.py (1 hunks)
  • requirements.txt (1 hunks)
  • tests/test_basic.py (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
main.py (11)
config.py (1)
  • Config (6-42)
database.py (5)
  • Database (8-184)
  • get_guild_prefix (83-88)
  • setup (17-81)
  • setup_guild (101-110)
  • close (181-184)
cogs/leveling.py (2)
  • level (54-85)
  • setup (155-156)
cogs/utility.py (2)
  • prefix (285-309)
  • setup (409-410)
cogs/admin.py (1)
  • setup (204-205)
cogs/automod.py (1)
  • setup (220-221)
cogs/fun.py (1)
  • setup (298-299)
cogs/games.py (1)
  • setup (323-324)
cogs/economy.py (1)
  • setup (318-319)
cogs/moderation.py (1)
  • setup (257-258)
cogs/music.py (1)
  • setup (580-581)
database.py (2)
cogs/utility.py (1)
  • prefix (285-309)
main.py (1)
  • close (122-128)
🪛 Pylint (3.3.7)
config.py

[refactor] 6-6: Too many instance attributes (13/7)

(R0902)

tests/test_basic.py

[refactor] 210-210: Too few public methods (1/2)

(R0903)

cogs/utility.py

[refactor] 316-316: Too many local variables (16/15)

(R0914)


[refactor] 316-316: Too many branches (24/12)

(R0912)


[refactor] 316-316: Too many statements (51/50)

(R0915)

main.py

[error] 133-133: Possibly using variable 'bot' before assignment

(E0606)


[refactor] 144-144: Consider using 'sys.exit' instead

(R1722)

cogs/admin.py

[error] 13-13: Method 'is_owner' has no argument

(E0211)


[error] 24-24: Too many positional arguments for method call

(E1121)


[error] 48-48: Too many positional arguments for method call

(E1121)


[error] 72-72: Too many positional arguments for method call

(E1121)


[error] 96-96: Too many positional arguments for method call

(E1121)


[error] 112-112: Too many positional arguments for method call

(E1121)


[error] 144-144: Too many positional arguments for method call

(E1121)


[error] 184-184: Too many positional arguments for method call

(E1121)

cogs/games.py

[refactor] 34-36: Too many boolean expressions in if statement (6/5)

(R0916)


[refactor] 127-127: Too many local variables (16/15)

(R0914)


[refactor] 214-214: Too many local variables (20/15)

(R0914)

cogs/music.py

[refactor] 15-15: Too many instance attributes (8/7)

(R0902)


[refactor] 18-18: Too many arguments (8/5)

(R0913)


[refactor] 18-18: Too many positional arguments (8/5)

(R0917)


[refactor] 30-30: Too many arguments (7/5)

(R0913)


[refactor] 15-15: Too few public methods (1/2)

(R0903)


[refactor] 96-96: Too many instance attributes (9/7)

(R0902)


[refactor] 215-230: Too many nested blocks (6/5)

(R1702)

🪛 Ruff (0.11.9)
tests/test_basic.py

6-6: pytest imported but unused

Remove unused import: pytest

(F401)

cogs/leveling.py

3-3: asyncio imported but unused

Remove unused import: asyncio

(F401)


24-25: Use a single if statement instead of nested if statements

Combine if statements using and

(SIM102)

cogs/utility.py

3-3: asyncio imported but unused

Remove unused import: asyncio

(F401)


4-4: aiohttp imported but unused

Remove unused import: aiohttp

(F401)


7-7: psutil imported but unused

Remove unused import: psutil

(F401)


8-8: os imported but unused

Remove unused import: os

(F401)


9-9: math imported but unused

Remove unused import: math

(F401)


387-387: Multiple statements on one line (colon)

(E701)


388-388: Multiple statements on one line (colon)

(E701)


389-389: Multiple statements on one line (colon)

(E701)


390-390: Multiple statements on one line (colon)

(E701)


391-391: Multiple statements on one line (colon)

(E701)


392-392: Multiple statements on one line (colon)

(E701)


393-393: Multiple statements on one line (colon)

(E701)


394-394: Multiple statements on one line (colon)

(E701)

main.py

3-3: asyncio imported but unused

Remove unused import: asyncio

(F401)


8-8: json imported but unused

Remove unused import: json

(F401)

cogs/economy.py

4-4: datetime.datetime imported but unused

Remove unused import

(F401)


4-4: datetime.timedelta imported but unused

Remove unused import

(F401)


5-5: asyncio imported but unused

Remove unused import: asyncio

(F401)

cogs/admin.py

4-4: sys imported but unused

Remove unused import: sys

(F401)


5-5: traceback imported but unused

Remove unused import: traceback

(F401)

cogs/games.py

274-274: Function definition does not bind loop variable current_player

(B023)

cogs/automod.py

3-3: re imported but unused

Remove unused import: re

(F401)


4-4: asyncio imported but unused

Remove unused import: asyncio

(F401)

cogs/moderation.py

28-31: Use contextlib.suppress(Exception) instead of try-except-pass

Replace with contextlib.suppress(Exception)

(SIM105)


30-30: Do not use bare except

(E722)


62-65: Use contextlib.suppress(Exception) instead of try-except-pass

Replace with contextlib.suppress(Exception)

(SIM105)


64-64: Do not use bare except

(E722)


185-188: Use contextlib.suppress(Exception) instead of try-except-pass

Replace with contextlib.suppress(Exception)

(SIM105)


187-187: Do not use bare except

(E722)


220-220: Loop control variable i not used within loop body

Rename unused i to _i

(B007)

database.py

2-2: asyncio imported but unused

Remove unused import: asyncio

(F401)


3-3: typing.Any imported but unused

Remove unused import: typing.Any

(F401)


4-4: json imported but unused

Remove unused import: json

(F401)


6-6: urllib.parse.urlparse imported but unused

Remove unused import: urllib.parse.urlparse

(F401)

cogs/fun.py

4-4: aiohttp imported but unused

Remove unused import: aiohttp

(F401)


5-5: asyncio imported but unused

Remove unused import: asyncio

(F401)


156-156: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


179-179: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


203-203: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)

cogs/music.py

5-5: re imported but unused

Remove unused import: re

(F401)


6-6: json imported but unused

Remove unused import: json

(F401)


7-7: random imported but unused

Remove unused import: random

(F401)


10-10: functools imported but unused

Remove unused import: functools

(F401)


11-11: collections.deque imported but unused

Remove unused import: collections.deque

(F401)


12-12: urllib.parse.urlparse imported but unused

Remove unused import: urllib.parse.urlparse

(F401)


13-13: mimetypes imported but unused

Remove unused import: mimetypes

(F401)


201-204: Use contextlib.suppress(AttributeError) instead of try-except-pass

Replace with contextlib.suppress(AttributeError)

(SIM105)


206-209: Use contextlib.suppress(KeyError) instead of try-except-pass

Replace with contextlib.suppress(KeyError)

(SIM105)


218-220: Use a single if statement instead of nested if statements

(SIM102)


224-227: Use contextlib.suppress(Exception) instead of try-except-pass

Replace with contextlib.suppress(Exception)

(SIM105)


226-226: Do not use bare except

(E722)


248-248: Do not use bare except

(E722)


264-264: Do not use bare except

(E722)

7xbot.py

164-164: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


211-211: Use key in dict instead of key in dict.keys()

Remove .keys()

(SIM118)

🔇 Additional comments (10)
database.py (1)

122-130: Guard against negative balance updates.

update_balance blindly adds amount; callers could pass negative numbers (e.g., cost deductions) and push users below zero. Either clamp at zero or add a CHECK (balance >= 0) constraint plus an in-code assertion.

Do downstream callers rely on negative updates? If not, add:

if amount < 0 and current_balance + amount < 0:
    raise ValueError("Insufficient funds")
.python-version (1)

1-1: LGTM! Python version specification is appropriate.

Python 3.11 is a good choice for modern Discord bots and aligns with the dependencies in requirements.txt.

Procfile (1)

1-1: LGTM! Correct Heroku worker configuration for Discord bot.

The worker process type is appropriate for long-running Discord bots on Heroku.

app.json (1)

28-35: LGTM! Buildpack configuration is appropriate.

The Python and FFmpeg buildpacks are correctly configured to support the bot's multimedia features (music cog).

cogs/leveling.py (1)

62-68:

Details

✅ Verification successful

Verify leveling formula consistency and potential overflow.

The leveling formula uses fourth power (level ** 4) which grows very rapidly. This could lead to:

  1. Integer overflow for high levels
  2. Inconsistency in XP calculations between different parts of the code

Let me verify the leveling formula implementation across the codebase:


🏁 Script executed:

#!/bin/bash
# Description: Check for consistent usage of leveling formula across the codebase
# Expected: All level calculations should use the same formula

# Search for level calculation patterns
rg -A 3 -B 1 "level.*\*\*.*4|level.*\^.*4|\*\*.*4.*level"

# Search for XP threshold calculations
rg -A 3 -B 1 "xp.*level|level.*xp" --type py

Length of output: 4082


Leveling formula consistency and overflow risk verified

  • Both cogs/leveling.py (next_level_xp = (level+1)4; current_level_xp = level4) and database.py (new_level = int(new_xp**(1/4))) use the same fourth-power formula.
  • Python’s integers are arbitrary-precision, so there’s no risk of integer overflow at any realistic level.

No changes required.

cogs/utility.py (1)

236-244: self.bot.start_time may be missing – verify it’s set at bot startup

uptime = datetime.utcnow() - self.bot.start_time will raise AttributeError
if start_time isn’t assigned (e.g. when running unit tests).

Make sure the main entry-point sets it, or guard here:

-start_time = getattr(self.bot, "start_time", datetime.utcnow())
-uptime = datetime.utcnow() - start_time
+start_time = getattr(self.bot, "start_time", datetime.utcnow())
+uptime = datetime.utcnow() - start_time
tests/test_basic.py (1)

9-17: Avoid manipulating sys.path & manual importlib plumbing in tests

Directly importing 7xbot.py bypasses the package import graph and can mask
packaging issues. Prefer:

import importlib
botmod = importlib.import_module("7xbot")

or install the project in editable mode during CI (pip install -e .).

cogs/games.py (1)

34-36: Boolean expression is appropriately structured.

While static analysis flagged this as complex, the win condition logic is clear and correctly implements the rock-paper-scissors rules.

🧰 Tools
🪛 Pylint (3.3.7)

[refactor] 34-36: Too many boolean expressions in if statement (6/5)

(R0916)

cogs/fun.py (1)

141-141:

Details

✅ Verification successful

Verify that bot has session attribute.

The code assumes self.bot.session exists. Ensure the bot instance has an aiohttp session initialized.

Let me check if the bot's session is properly initialized:


🏁 Script executed:

#!/bin/bash
# Check if bot.session is initialized in main.py or bot initialization
rg -A 5 "session.*=.*aiohttp" --type py
rg -A 5 "self\.session.*=.*ClientSession" --type py

Length of output: 1081


🏁 Script executed:

#!/bin/bash
# Show context around session initialization in main.py
rg -n -C 5 "self\.session = aiohttp\.ClientSession" --type py main.py

Length of output: 470


Session initialization confirmed

The self.bot.session property is set in your Bot’s setup_hook (main.py, line 49) via

self.session = aiohttp.ClientSession()

so using self.bot.session.get(…) in cogs/fun.py is valid. No changes needed.

cogs/music.py (1)

458-475: Accessing the private _queue attribute is brittle

asyncio.Queue does not guarantee that _queue remains stable.
Safer options:

  1. Maintain an explicit deque mirror when enqueuing items.
  2. Convert to list via list(player.queue._queue) wrapped in a try/except AttributeError guard.
  3. Switch to collections.deque or a custom queue that exposes the data.

At minimum, wrap access to _queue in a helper to localise the private-API dependency.

Comment on lines +5 to +10
.youtube.com TRUE / TRUE 1749307322 GPS 1
.youtube.com TRUE / TRUE 1783865541 PREF f4=4000000&f6=40000000&tz=Europe.Istanbul&f7=100
.youtube.com TRUE / TRUE 0 YSC DlJrprbNfnA
.youtube.com TRUE / TRUE 1764857524 VISITOR_INFO1_LIVE XscOorzO6Cs
.youtube.com TRUE / TRUE 1764857524 VISITOR_PRIVACY_METADATA CgJUUhIEGgAgVw%3D%3D
.youtube.com TRUE / TRUE 1764857524 __Secure-ROLLOUT_TOKEN CIanydmQofiWvwEQwoaf9L7fjQMYxpWc9b7fjQM%3D
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove real cookie values from source control immediately.

Committing live cookies (some flagged __Secure-*) risks session hijacking and violates the principle of not storing credentials/PII in-repo. Ship an empty placeholder or load cookies at runtime via secrets storage instead.

🤖 Prompt for AI Agents
In cookies.txt around lines 5 to 10, real cookie values are committed, which
risks session hijacking and exposes sensitive data. Remove all actual cookie
values from the file immediately. Replace them with empty placeholders or dummy
values, and modify the code to load real cookies securely at runtime from a
secrets management system or environment variables instead of storing them in
source control.

Comment on lines +8 to +10
DEFAULT_PREFIX: str = os.getenv('DEFAULT_PREFIX', '!')
OWNER_IDS: List[int] = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid mutable‐default-gotcha for OWNER_IDS.

OWNER_IDS: List[int] = None later mutates to a list in __post_init__. Use field(default_factory=list, init=False) to dodge mypy/Dataclass linter noise and inadvertent shared state.

-from typing import List
+from typing import List
+from dataclasses import dataclass, field
 ...
-    OWNER_IDS: List[int] = None
+    OWNER_IDS: List[int] = field(default_factory=list, init=False)
🤖 Prompt for AI Agents
In config.py around lines 8 to 10, the OWNER_IDS field is currently set to None
and later mutated to a list, which can cause mutable default issues and linter
warnings. Replace the OWNER_IDS declaration with a dataclass field using
default_factory=list and set init=False to ensure it initializes as an empty
list without shared mutable state or mypy/dataclass linter errors.

database.py Outdated
Comment on lines +11 to +16
self.connection_url = os.getenv('DATABASE_URL')

# Handle Heroku's updated DATABASE_URL format if needed
if self.connection_url and self.connection_url.startswith('postgres://'):
self.connection_url = self.connection_url.replace('postgres://', 'postgresql://', 1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fail fast when DATABASE_URL is missing.

asyncpg.create_pool(None) will explode later but the error surfaces far from the root cause. Detect early and raise a clear message.

-        self.connection_url = os.getenv('DATABASE_URL')
+        self.connection_url = os.getenv('DATABASE_URL')
+        if not self.connection_url:
+            raise RuntimeError("DATABASE_URL environment variable is not set.")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
self.connection_url = os.getenv('DATABASE_URL')
# Handle Heroku's updated DATABASE_URL format if needed
if self.connection_url and self.connection_url.startswith('postgres://'):
self.connection_url = self.connection_url.replace('postgres://', 'postgresql://', 1)
self.connection_url = os.getenv('DATABASE_URL')
if not self.connection_url:
raise RuntimeError("DATABASE_URL environment variable is not set.")
# Handle Heroku's updated DATABASE_URL format if needed
if self.connection_url and self.connection_url.startswith('postgres://'):
self.connection_url = self.connection_url.replace('postgres://', 'postgresql://', 1)
🤖 Prompt for AI Agents
In database.py around lines 11 to 16, the code does not check if the
DATABASE_URL environment variable is missing before using it. Add a check right
after retrieving DATABASE_URL from the environment, and if it is None or empty,
raise a clear and immediate exception indicating that DATABASE_URL is required.
This will fail fast and provide a clear error message instead of letting
asyncpg.create_pool fail later with a less clear error.

Comment on lines +1 to +14
discord.py>=2.3.0
python-dotenv>=1.0.0
g4f
psutil>=5.9.0
asyncpg>=0.28.0
yt-dlp>=2023.3.4
PyNaCl>=1.5.0
wavelink>=2.0.0
aiohttp>=3.8.0
async-timeout
psycopg2-binary
ffmpeg-python
mutagen
pydub
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider version pinning and potential dependency conflicts.

Several observations about the dependencies:

  1. Missing version constraints: async-timeout, psycopg2-binary, ffmpeg-python, mutagen, and pydub lack version specifications, which could lead to compatibility issues.

  2. Dual PostgreSQL drivers: Both asyncpg and psycopg2-binary are included. Consider if both are needed, as asyncpg is generally preferred for async operations.

  3. FFmpeg dependency: ffmpeg-python requires FFmpeg to be installed on the system, which is handled by the buildpack in app.json.

Consider pinning versions for all dependencies:

-async-timeout
-psycopg2-binary
-ffmpeg-python
-mutagen
-pydub
+async-timeout>=4.0.0
+psycopg2-binary>=2.9.0
+ffmpeg-python>=0.2.0
+mutagen>=1.47.0
+pydub>=0.25.0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
discord.py>=2.3.0
python-dotenv>=1.0.0
g4f
psutil>=5.9.0
asyncpg>=0.28.0
yt-dlp>=2023.3.4
PyNaCl>=1.5.0
wavelink>=2.0.0
aiohttp>=3.8.0
async-timeout
psycopg2-binary
ffmpeg-python
mutagen
pydub
discord.py>=2.3.0
python-dotenv>=1.0.0
g4f
psutil>=5.9.0
asyncpg>=0.28.0
yt-dlp>=2023.3.4
PyNaCl>=1.5.0
wavelink>=2.0.0
aiohttp>=3.8.0
-async-timeout
-psycopg2-binary
-ffmpeg-python
-mutagen
-pydub
+async-timeout>=4.0.0
+psycopg2-binary>=2.9.0
+ffmpeg-python>=0.2.0
+mutagen>=1.47.0
+pydub>=0.25.0
🤖 Prompt for AI Agents
In requirements.txt lines 1 to 14, several dependencies lack version
constraints, which can cause compatibility issues. Add explicit version pins for
async-timeout, psycopg2-binary, ffmpeg-python, mutagen, and pydub to ensure
consistent environments. Review whether both asyncpg and psycopg2-binary are
necessary; if only asyncpg is used for async DB operations, consider removing
psycopg2-binary. Confirm that ffmpeg-python's system dependency on FFmpeg is
documented or handled externally as noted.

{
"name": "Advanced Discord Bot",
"description": "A feature-rich Discord bot with moderation, music, economy, and more",
"repository": "https://github.com/yourusername/advanced-discord-bot",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Update the placeholder repository URL.

The repository URL contains a placeholder that needs to be updated with the actual repository URL.

-  "repository": "https://github.com/yourusername/advanced-discord-bot",
+  "repository": "https://github.com/UN7X/7xbot",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"repository": "https://github.com/yourusername/advanced-discord-bot",
"repository": "https://github.com/UN7X/7xbot",
🤖 Prompt for AI Agents
In app.json at line 4, the repository URL currently uses a placeholder value.
Replace "https://github.com/yourusername/advanced-discord-bot" with the actual
URL of the repository to ensure it correctly points to the project's GitHub
repository.

Comment on lines +156 to +157
except Exception as e:
await ctx.send("❌ Error fetching meme!")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove unused exception variables or log the errors.

The exception variables are assigned but never used. Either remove them or log the errors for debugging.

For all three occurrences:

-        except Exception as e:
-            await ctx.send("❌ Error fetching meme!")
+        except Exception:
+            await ctx.send("❌ Error fetching meme!")

Or if you want to log errors:

-        except Exception as e:
-            await ctx.send("❌ Error fetching meme!")
+        except Exception as e:
+            self.bot.logger.error(f"Error fetching meme: {e}")
+            await ctx.send("❌ Error fetching meme!")

Also applies to: 179-180, 203-204

🧰 Tools
🪛 Ruff (0.11.9)

156-156: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)

🤖 Prompt for AI Agents
In cogs/fun.py at lines 156-157, 179-180, and 203-204, the exception variable
'e' is assigned but not used. To fix this, either remove the 'as e' part from
the except clause if you do not intend to use the exception, or add logging of
the exception 'e' inside the except block to capture error details for debugging
purposes.

Comment on lines +4 to +5
import aiohttp
import asyncio
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove unused imports.

The aiohttp and asyncio imports are not used in this file.

-import aiohttp
-import asyncio
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import aiohttp
import asyncio
🧰 Tools
🪛 Ruff (0.11.9)

4-4: aiohttp imported but unused

Remove unused import: aiohttp

(F401)


5-5: asyncio imported but unused

Remove unused import: asyncio

(F401)

🤖 Prompt for AI Agents
In cogs/fun.py around lines 4 to 5, the imports for aiohttp and asyncio are not
used anywhere in the file. Remove these two import statements to clean up the
code and avoid unnecessary dependencies.

Comment on lines +242 to +266
async def is_valid_url(self, url):
"""Check if URL is valid and accessible."""
try:
session = await self.get_session()
async with session.head(url, timeout=5) as resp:
return resp.status == 200
except:
return False

async def is_audio_url(self, url):
"""Check if URL points to an audio file."""
try:
# Check file extension
audio_extensions = ['.mp3', '.wav', '.ogg', '.m4a', '.aac', '.flac', '.wma']
if any(url.lower().endswith(ext) for ext in audio_extensions):
return True

# Check MIME type
session = await self.get_session()
async with session.head(url, timeout=5) as resp:
content_type = resp.headers.get('content-type', '').lower()
return content_type.startswith('audio/')
except:
return False

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid bare except: – it hides programming errors

Catching everything (including KeyboardInterrupt, SystemExit, etc.) hampers debuggability.
Catch Exception at minimum and log the details.

-        except:
-            return False
+        except Exception:
+            return False

Apply the same change to is_audio_url.
Consider logging the exception to help diagnose network issues.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def is_valid_url(self, url):
"""Check if URL is valid and accessible."""
try:
session = await self.get_session()
async with session.head(url, timeout=5) as resp:
return resp.status == 200
except:
return False
async def is_audio_url(self, url):
"""Check if URL points to an audio file."""
try:
# Check file extension
audio_extensions = ['.mp3', '.wav', '.ogg', '.m4a', '.aac', '.flac', '.wma']
if any(url.lower().endswith(ext) for ext in audio_extensions):
return True
# Check MIME type
session = await self.get_session()
async with session.head(url, timeout=5) as resp:
content_type = resp.headers.get('content-type', '').lower()
return content_type.startswith('audio/')
except:
return False
async def is_valid_url(self, url):
"""Check if URL is valid and accessible."""
try:
session = await self.get_session()
async with session.head(url, timeout=5) as resp:
return resp.status == 200
except Exception:
return False
async def is_audio_url(self, url):
"""Check if URL points to an audio file."""
try:
# Check file extension
audio_extensions = ['.mp3', '.wav', '.ogg', '.m4a', '.aac', '.flac', '.wma']
if any(url.lower().endswith(ext) for ext in audio_extensions):
return True
# Check MIME type
session = await self.get_session()
async with session.head(url, timeout=5) as resp:
content_type = resp.headers.get('content-type', '').lower()
return content_type.startswith('audio/')
except Exception:
return False
🧰 Tools
🪛 Ruff (0.11.9)

248-248: Do not use bare except

(E722)


264-264: Do not use bare except

(E722)

🤖 Prompt for AI Agents
In cogs/music.py around lines 242 to 266, replace the bare except clauses in
both is_valid_url and is_audio_url methods with except Exception to avoid
catching system-exiting exceptions. Additionally, add logging of the caught
exceptions to help diagnose network or other runtime issues. This involves
importing the logging module if not already done, and logging the exception
details inside the except blocks before returning False.

Comment on lines +155 to +162
await ctx.send(prompt)
try:
current_index = strike_roles.index(current_role.name)
next_role_name = strike_roles[min(current_index + 1, len(strike_roles) - 1)]
next_role_obj = discord.utils.get(guild.roles, name=next_role_name)
except ValueError: # Should not happen if strike_roles is consistent
await ctx.send("An internal error occurred with role indexing.")
return

if not next_role_obj:
await ctx.send(f"The role for the next strike level was not found. Please check server roles.")
return

try:
if current_role:
await member.remove_roles(current_role)
await member.add_roles(next_role_obj)
await ctx.send(f"{member.mention} has been warned and given the role: {next_role_obj.name} for: {reason}")

# Timeout, kick, ban logic
if next_role_obj.name == "Time out warning 1":
await member.timeout(datetime.timedelta(minutes=10), reason=reason)
elif next_role_obj.name == "Time out warning 2":
await member.timeout(datetime.timedelta(hours=1), reason=reason)
elif next_role_obj.name == "Time out warning 3":
await member.timeout(datetime.timedelta(days=1), reason=reason)
elif next_role_obj.name == "Kick warning":
await member.kick(reason=f"Accumulated strikes: {reason}")
elif next_role_obj.name == "Banned":
await member.ban(reason=f"Accumulated strikes: {reason}")
# Audit log reason was missing, adding it back.
# await ctx.guild.audit_logs(reason=f"Warned {member.display_name}: {reason}") # This creates an entry, not what's usually done.
# Instead, the actions (add_roles, timeout, kick, ban) have their own audit log entries.
except discord.Forbidden:
await ctx.send(f"I don't have permissions to manage roles or perform actions on {member.mention}.")
except Exception as e:
await ctx.send(f"An error occurred: {e}")


@bot.command(help="Reverse the last warning of a user.", name="pardon")
@commands.has_permissions(manage_messages=True)
async def pardon(ctx, member: Optional[discord.Member] = None, *, args: Optional[str] = None):
if isinstance(member, str) and member.lower() == "help": # Check if first arg is 'help'
embed = discord.Embed(title="Pardon Command Help", description=pardon_explanation, color=0x00ff00)
await ctx.send(embed=embed)
return
if args and args.lower() == "help": # Check if args (reason part) is 'help'
embed = discord.Embed(title="Pardon Command Help", description=pardon_explanation, color=0x00ff00)
await ctx.send(embed=embed)
return
if not member:
await ctx.send("Please specify a member to pardon or type `7/pardon help`.")
return

guild = ctx.guild
# ... (rest of the pardon command logic, ensuring efficiency)
# Existing logic is fairly direct.
current_role = next(
(role for role in member.roles if role.name in strike_roles), None
)
if current_role is None:
await ctx.send(f"{member.mention} has no warnings to pardon.")
return

msg = await bot.wait_for(
"message",
check=lambda m: m.author == ctx.author and m.channel == ctx.channel,
timeout=60,
)
except asyncio.TimeoutError:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

setup_wizard leaks on tests because it calls the global bot

bot.wait_for here will raise AttributeError if the global has not been set (e.g., unit tests instantiating the wizard directly).

-            msg = await bot.wait_for(
+            msg = await ctx.bot.wait_for(

This removes the hidden dependency on the mutable global.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await ctx.send(prompt)
try:
current_index = strike_roles.index(current_role.name)
next_role_name = strike_roles[min(current_index + 1, len(strike_roles) - 1)]
next_role_obj = discord.utils.get(guild.roles, name=next_role_name)
except ValueError: # Should not happen if strike_roles is consistent
await ctx.send("An internal error occurred with role indexing.")
return
if not next_role_obj:
await ctx.send(f"The role for the next strike level was not found. Please check server roles.")
return
try:
if current_role:
await member.remove_roles(current_role)
await member.add_roles(next_role_obj)
await ctx.send(f"{member.mention} has been warned and given the role: {next_role_obj.name} for: {reason}")
# Timeout, kick, ban logic
if next_role_obj.name == "Time out warning 1":
await member.timeout(datetime.timedelta(minutes=10), reason=reason)
elif next_role_obj.name == "Time out warning 2":
await member.timeout(datetime.timedelta(hours=1), reason=reason)
elif next_role_obj.name == "Time out warning 3":
await member.timeout(datetime.timedelta(days=1), reason=reason)
elif next_role_obj.name == "Kick warning":
await member.kick(reason=f"Accumulated strikes: {reason}")
elif next_role_obj.name == "Banned":
await member.ban(reason=f"Accumulated strikes: {reason}")
# Audit log reason was missing, adding it back.
# await ctx.guild.audit_logs(reason=f"Warned {member.display_name}: {reason}") # This creates an entry, not what's usually done.
# Instead, the actions (add_roles, timeout, kick, ban) have their own audit log entries.
except discord.Forbidden:
await ctx.send(f"I don't have permissions to manage roles or perform actions on {member.mention}.")
except Exception as e:
await ctx.send(f"An error occurred: {e}")
@bot.command(help="Reverse the last warning of a user.", name="pardon")
@commands.has_permissions(manage_messages=True)
async def pardon(ctx, member: Optional[discord.Member] = None, *, args: Optional[str] = None):
if isinstance(member, str) and member.lower() == "help": # Check if first arg is 'help'
embed = discord.Embed(title="Pardon Command Help", description=pardon_explanation, color=0x00ff00)
await ctx.send(embed=embed)
return
if args and args.lower() == "help": # Check if args (reason part) is 'help'
embed = discord.Embed(title="Pardon Command Help", description=pardon_explanation, color=0x00ff00)
await ctx.send(embed=embed)
return
if not member:
await ctx.send("Please specify a member to pardon or type `7/pardon help`.")
return
guild = ctx.guild
# ... (rest of the pardon command logic, ensuring efficiency)
# Existing logic is fairly direct.
current_role = next(
(role for role in member.roles if role.name in strike_roles), None
)
if current_role is None:
await ctx.send(f"{member.mention} has no warnings to pardon.")
return
msg = await bot.wait_for(
"message",
check=lambda m: m.author == ctx.author and m.channel == ctx.channel,
timeout=60,
)
except asyncio.TimeoutError:
await ctx.send(prompt)
try:
msg = await ctx.bot.wait_for(
"message",
check=lambda m: m.author == ctx.author and m.channel == ctx.channel,
timeout=60,
)
except asyncio.TimeoutError:
🤖 Prompt for AI Agents
In 7xbot.py around lines 155 to 162, the code uses the global 'bot' object
directly in 'bot.wait_for', causing an AttributeError in tests where 'bot' is
not set. To fix this, refactor the 'setup_wizard' function to accept 'bot' as a
parameter and use this passed-in 'bot' instance instead of the global one,
removing the hidden dependency on the mutable global variable.

Comment on lines +19 to 21
bot: Optional[commands.Bot] = None
bot_start_time = datetime.now()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Global bot reference introduces race conditions

Relying on a module-level global that is mutated in Core.__init__ can break when:

  • Multiple bot instances are spawned (e.g., in tests).
  • Other tasks import this module before the Cog is initialised.

Prefer passing ctx.bot around or storing self.bot only.

-bot: Optional[commands.Bot] = None

Then replace global usages (bot.wait_for, etc.) with ctx.bot / self.bot.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In 7xbot.py around lines 19 to 21, avoid using the global variable 'bot' as it
causes race conditions when multiple bot instances exist or when the module is
imported before initialization. Instead, remove the global 'bot' variable and
ensure that bot references are accessed via 'self.bot' within classes or passed
explicitly as 'ctx.bot' in functions. Replace all global 'bot' usages like
'bot.wait_for' with 'self.bot.wait_for' or 'ctx.bot.wait_for' accordingly.

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @UN7X - I've reviewed your changes and they look great!

Here's what I looked at during the review
  • 🟡 General issues: 15 issues found
  • 🟢 Security: all looks good
  • 🟡 Testing: 6 issues found
  • 🟡 Complexity: 1 issue found
  • 🟢 Documentation: all looks good

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

)
async def join(self, ctx):
"""Join the user's voice channel."""
if not ctx.author.voice or not ctx.author.voice.channel:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): No check for bot's permissions to connect/speak in the voice channel.

Check the bot's 'connect' and 'speak' permissions in the target channel before joining to prevent permission errors and improve user experience.

except:
return False

async def is_audio_url(self, url):
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Audio file detection may not work for all stream URLs.

Since some streams lack standard extensions or MIME types, consider letting users override detection or displaying a warning instead of blocking playback.

Suggested implementation:

    async def is_audio_url(self, url, override=False):
        """Check if URL points to an audio file. If unsure, allow override or warn user."""
        try:
            session = await self.get_session()
            async with session.head(url, timeout=5) as resp:
                content_type = resp.headers.get("Content-Type", "")
                if "audio" in content_type:
                    return True
                # Fallback: check file extension
                if any(url.lower().endswith(ext) for ext in [".mp3", ".wav", ".ogg", ".flac", ".aac", ".m4a"]):
                    return True
            if override:
                return True  # Allow playback if user overrides
            # If detection fails, return None to indicate uncertainty
            return None
        except:
            if override:
                return True
            return None
  1. Update all calls to is_audio_url to handle the new return value (True, None, or False) and the override parameter.
  2. If is_audio_url returns None, display a warning to the user (e.g., "Warning: Unable to verify if this is an audio stream. Proceeding with playback.").
  3. Optionally, provide a way for users to force playback (e.g., a command flag or prompt).
  4. If you have a playback-blocking check based on is_audio_url, change it to only block on explicit False, not on None.

if player.queue.empty() and not player.current:
return await ctx.send("❌ The queue is empty.")

# Get up to 10 items from the queue
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Direct access to the private _queue attribute of asyncio.Queue.

Accessing _queue is not future-proof. Use public methods or maintain a separate list to avoid compatibility issues.

description="Stop playing and clear the queue",
usage="stop"
)
async def stop(self, ctx):
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Clearing the queue is not explicit.

Since cleanup only disconnects and deletes the player, the queue may not be cleared if the player persists. Explicitly clear the queue to prevent leftover items.

Suggested implementation:

    @commands.command(
        name="stop",
        description="Stop playing and clear the queue",
        usage="stop"
    )
    async def stop(self, ctx):
        """Stop playing and clear the queue."""
        voice_client = ctx.voice_client

        # Explicitly clear the queue to prevent leftover items
        player = self.get_player(ctx)
        if player and hasattr(player, "queue"):
            player.queue.clear()

If the get_player method or the queue attribute does not exist or is named differently, you will need to adjust the code to match your actual player/queue implementation. Make sure that player.queue.clear() is the correct way to clear the queue in your codebase.

await ctx.send("👋 Left the voice channel.")

@play.before_invoke
async def ensure_voice(self, ctx):
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): ensure_voice may connect the bot to a channel without checking permissions.

Please add checks for connect and speak permissions before attempting to join the voice channel to prevent permission errors.

Comment on lines +218 to +227
if voice_client and voice_client.is_connected():
# Check if there are any non-bot members in the voice channel
if not any(member for member in voice_client.channel.members if not member.bot):
await voice_client.disconnect()
if guild.id in self.players:
await self.cleanup(guild)
try:
await self.players[guild.id].channel.send("👋 Left the voice channel because everyone left.")
except:
pass
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Merge nested if conditions (merge-nested-ifs)

Suggested change
if voice_client and voice_client.is_connected():
# Check if there are any non-bot members in the voice channel
if not any(member for member in voice_client.channel.members if not member.bot):
await voice_client.disconnect()
if guild.id in self.players:
await self.cleanup(guild)
try:
await self.players[guild.id].channel.send("👋 Left the voice channel because everyone left.")
except:
pass
if voice_client and voice_client.is_connected() and not any(member for member in voice_client.channel.members if not member.bot):
await voice_client.disconnect()
if guild.id in self.players:
await self.cleanup(guild)
try:
await self.players[guild.id].channel.send("👋 Left the voice channel because everyone left.")
except:
pass


ExplanationToo much nesting can make code difficult to understand, and this is especially
true in Python, where there are no brackets to help out with the delineation of
different nesting levels.

Reading deeply nested code is confusing, since you have to keep track of which
conditions relate to which levels. We therefore strive to reduce nesting where
possible, and the situation where two if conditions can be combined using
and is an easy win.

level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('bot.log') if not os.getenv('HEROKU', False) else logging.StreamHandler(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Swap if/else branches of if expression to remove negation (swap-if-expression)

Suggested change
logging.FileHandler('bot.log') if not os.getenv('HEROKU', False) else logging.StreamHandler(),
logging.StreamHandler() if os.getenv('HEROKU', False) else logging.FileHandler('bot.log'),


ExplanationNegated conditions are more difficult to read than positive ones, so it is best
to avoid them where we can. By swapping the if and else conditions around we
can invert the condition and make it positive.

Comment on lines +100 to +102
if eco == "prankful":
return shop_items_prankful
return {}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): We've found these issues:

Suggested change
if eco == "prankful":
return shop_items_prankful
return {}
return shop_items_prankful if eco == "prankful" else {}

color=self.bot.config.PRIMARY_COLOR
)

guild_list = []
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (code-quality): Convert for loop into list comprehension (list-comprehension)

for char in text.upper():
if char in ascii_chars:
for i, line in enumerate(ascii_chars[char]):
lines[i] += line + ' '
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (code-quality): Use f-string instead of string concatenation (use-fstring-for-concatenation)

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
database.py (3)

2-3: Drop unused imports.

asyncio and urlparse are never referenced; keeping them only trips Ruff and future linters.

-import asyncio
-from urllib.parse import urlparse

Also applies to: 8-8

🧰 Tools
🪛 Ruff (0.11.9)

2-2: asyncio imported but unused

Remove unused import: asyncio

(F401)


27-31: Guard against concurrent writes to the JSON file.

Multiple async tasks calling _save_json() can interleave writes and corrupt database.json.
Consider:

  • an asyncio.Lock around the write,
  • or writing to a temp file and os.replace() it atomically.

Optional but prevents hard-to-debug data loss.


48-48: Tune the connection pool.

asyncpg.create_pool(self.connection_url) uses default sizes (min=10, max=10).
For small bots or Heroku hobby-tier DBs that’s overkill and may hit the connection limit. You can pass min_size=1, max_size=5 or read limits from config.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6653706 and e985be9.

📒 Files selected for processing (1)
  • database.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.11.9)
database.py

2-2: asyncio imported but unused

Remove unused import: asyncio

(F401)


8-8: urllib.parse.urlparse imported but unused

Remove unused import: urllib.parse.urlparse

(F401)

🔇 Additional comments (2)
database.py (2)

18-26: Confirm silent JSON fallback behaviour.

The code now falls back to local JSON storage when DATABASE_URL is absent.
If this is intentional for dev environments, great—otherwise you may re-introduce the “mystery data not persisted to Postgres” bug we discussed earlier. Please double-check the deployment expectation.


178-186: Validate balance mutations.

update_balance blindly applies amount; callers could push balances negative or overflow BIGINT.
Recommend clamping to >= 0 (or whatever rule you need) inside this method to keep invariants local.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant