Conversation
…essential packages for flexibility in dependency management.
Reviewer's GuideRestructured 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 WizardsequenceDiagram
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."
Class Diagram: Core Cog and Helper FunctionsclassDiagram
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
}
Class Diagram: Music Cog and Associated ClassesclassDiagram
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
}
Class Diagram: AutoMod CogclassDiagram
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
}
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
## 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 channelsequenceDiagram
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
Poem
|
There was a problem hiding this comment.
Actionable comments posted: 24
♻️ Duplicate comments (1)
cogs/economy.py (1)
238-244: Same concern forwithdraw– 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 acrossOWNER_ID&ADDITIONAL_OWNER_IDSaren’t filtered.
Consider safe parsing withstr.isdigit()(orre.fullmatch(r'\d+')) and cast into asetbefore finally converting tolist.database.py (2)
2-6: Prune unused imports to silence Ruff F401 warnings.
asyncio,json,urlparse,Anyaren’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:
asyncioimported but unusedRemove unused import:
asyncio(F401)
3-3:
typing.Anyimported but unusedRemove unused import:
typing.Any(F401)
4-4:
jsonimported but unusedRemove unused import:
json(F401)
6-6:
urllib.parse.urlparseimported but unusedRemove 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 withcontextlib.suppress(Exception)or at least log & continue.app.json (1)
25-26: Consider PostgreSQL plan limitations for production.The
hobby-devplan 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
asyncioimport is not used in this file.-import asyncio🧰 Tools
🪛 Ruff (0.11.9)
3-3:
asyncioimported but unusedRemove 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
ifstatement instead of nestedifstatementsCombine
ifstatements usingand(SIM102)
32-33: Consider XP farming protection.The current implementation only uses time-based cooldowns. Consider additional anti-farming measures:
- Message length requirements - Very short messages shouldn't award XP
- Channel restrictions - Exclude spam channels
- 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 chainedelif … : emoji = …statements for readability & PEP 8 complianceMultiple 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
elifbranches 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_commandhas grown into a 200-line monolith – extract helpersRuff/Pylint already flag 16 locals, 24 branches, 50+ statements (R0914/12/15).
Consider:
_render_command_help(...)_render_category_help(...)_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:pytestimport is unusedAll assertions rely on the built-in
assertkeyword; the module itself is never referenced. Safe to drop:-import pytest🧰 Tools
🪛 Ruff (0.11.9)
6-6:
pytestimported but unusedRemove unused import:
pytest(F401)
cogs/admin.py (2)
1-6: Prune unused imports
sysandtracebackare never used. Remove them to satisfy Ruff F401.-import sys -import traceback🧰 Tools
🪛 Ruff (0.11.9)
4-4:
sysimported but unusedRemove unused import:
sys(F401)
5-5:
tracebackimported but unusedRemove unused import:
traceback(F401)
145-176: Unlimited SQL execution can brick the database or leak sensitive dataThe
sqlcommand lets the owner runDROP DATABASE, long-running queries, or
dump user PII straight into Discord.Minimum hardening:
- Reject mutating statements unless explicitly prefixed (e.g.
!forceflag).- Truncate
outputto < 1900 chars to avoid “embed too large”.- Wrap DB calls in
asyncio.wait_forwith a timeout.-if len(output) > 1900: - output = output[:1890] + "…"Also catch
asyncpg.PostgresErrorseparately to surface DB specifics.cogs/economy.py (2)
4-6: Remove unused imports
datetime,timedelta, andasyncioare imported but never used, triggering Ruff F401.-from datetime import datetime, timedelta -import asyncio🧰 Tools
🪛 Ruff (0.11.9)
4-4:
datetime.datetimeimported but unusedRemove unused import
(F401)
4-4:
datetime.timedeltaimported but unusedRemove unused import
(F401)
5-5:
asyncioimported but unusedRemove unused import:
asyncio(F401)
200-206: Duplicate SQL path – favour single helper
depositexecutes raw SQL while the rest of the cog relies onupdate_balance. This forks logic and increases maintenance cost (e.g., balance cache, audit-logging). Expose antransfer_balance(wallet_delta, bank_delta)helper inDatabaseand call it here to DRY.main.py (3)
3-8: Prune unused imports
asyncioandjsonare never referenced.-import asyncio -import json🧰 Tools
🪛 Ruff (0.11.9)
3-3:
asyncioimported but unusedRemove unused import:
asyncio(F401)
8-8:
jsonimported but unusedRemove unused import:
json(F401)
130-133: Avoid printing raw messages in production
print()leaks potentially sensitive content to stdout. Replace with structured logging atDEBUGlevel.🧰 Tools
🪛 Pylint (3.3.7)
[error] 133-133: Possibly using variable 'bot' before assignment
(E0606)
143-145: Usesys.exitfor 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
reandasyncioaren’t used anywhere in the cog.-import re -import asyncio🧰 Tools
🪛 Ruff (0.11.9)
3-3:
reimported but unusedRemove unused import:
re(F401)
4-4:
asyncioimported but unusedRemove unused import:
asyncio(F401)
45-56: Unboundedspam_trackergrowth
spam_trackerstores 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
iis 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
inot used within loop bodyRename unused
ito_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, andmimetypesare 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:
reimported but unusedRemove unused import:
re(F401)
6-6:
jsonimported but unusedRemove unused import:
json(F401)
7-7:
randomimported but unusedRemove unused import:
random(F401)
10-10:
functoolsimported but unusedRemove unused import:
functools(F401)
11-11:
collections.dequeimported but unusedRemove unused import:
collections.deque(F401)
12-12:
urllib.parse.urlparseimported but unusedRemove unused import:
urllib.parse.urlparse(F401)
13-13:
mimetypesimported but unusedRemove unused import:
mimetypes(F401)
199-210: Replace try/except/pass withcontextlib.suppressThis block is exactly the pattern Ruff (SIM105) warns about.
Usingcontextlib.suppressis 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 oftry-except-passReplace with
contextlib.suppress(AttributeError)(SIM105)
206-209: Use
contextlib.suppress(KeyError)instead oftry-except-passReplace with
contextlib.suppress(KeyError)(SIM105)
215-222: Flatten nestediffor 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
ifstatement instead of nestedifstatements(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
📒 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_balanceblindly addsamount; callers could pass negative numbers (e.g., cost deductions) and push users below zero. Either clamp at zero or add aCHECK (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:
- Integer overflow for high levels
- 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 pyLength 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_timemay be missing – verify it’s set at bot startup
uptime = datetime.utcnow() - self.bot.start_timewill raiseAttributeError
ifstart_timeisn’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_timetests/test_basic.py (1)
9-17: Avoid manipulatingsys.path& manualimportlibplumbing in testsDirectly importing
7xbot.pybypasses 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.sessionexists. 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 pyLength 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.pyLength of output: 470
Session initialization confirmed
The
self.bot.sessionproperty is set in your Bot’ssetup_hook(main.py, line 49) viaself.session = aiohttp.ClientSession()so using
self.bot.session.get(…)incogs/fun.pyis valid. No changes needed.cogs/music.py (1)
458-475: Accessing the private_queueattribute is brittle
asyncio.Queuedoes not guarantee that_queueremains stable.
Safer options:
- Maintain an explicit
dequemirror when enqueuing items.- Convert to list via
list(player.queue._queue)wrapped in a try/except AttributeError guard.- Switch to
collections.dequeor a custom queue that exposes the data.At minimum, wrap access to
_queuein a helper to localise the private-API dependency.
| .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 |
There was a problem hiding this comment.
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.
| DEFAULT_PREFIX: str = os.getenv('DEFAULT_PREFIX', '!') | ||
| OWNER_IDS: List[int] = None | ||
|
|
There was a problem hiding this comment.
🛠️ 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
| 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) | ||
|
|
There was a problem hiding this comment.
🛠️ 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.
| 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.
| 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 |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Consider version pinning and potential dependency conflicts.
Several observations about the dependencies:
-
Missing version constraints:
async-timeout,psycopg2-binary,ffmpeg-python,mutagen, andpydublack version specifications, which could lead to compatibility issues. -
Dual PostgreSQL drivers: Both
asyncpgandpsycopg2-binaryare included. Consider if both are needed, asasyncpgis generally preferred for async operations. -
FFmpeg dependency:
ffmpeg-pythonrequires FFmpeg to be installed on the system, which is handled by the buildpack inapp.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.
| 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", |
There was a problem hiding this comment.
🛠️ 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.
| "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.
| except Exception as e: | ||
| await ctx.send("❌ Error fetching meme!") |
There was a problem hiding this comment.
🛠️ 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.
| import aiohttp | ||
| import asyncio |
There was a problem hiding this comment.
🛠️ 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.
| 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.
| 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 | ||
|
|
There was a problem hiding this comment.
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 FalseApply 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.
| 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.
| 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: |
There was a problem hiding this comment.
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.
| 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.
| bot: Optional[commands.Bot] = None | ||
| bot_start_time = datetime.now() | ||
|
|
There was a problem hiding this comment.
🛠️ 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] = NoneThen 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.
There was a problem hiding this comment.
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
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: |
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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- Update all calls to
is_audio_urlto handle the new return value (True,None, orFalse) and theoverrideparameter. - If
is_audio_urlreturnsNone, display a warning to the user (e.g., "Warning: Unable to verify if this is an audio stream. Proceeding with playback."). - Optionally, provide a way for users to force playback (e.g., a command flag or prompt).
- If you have a playback-blocking check based on
is_audio_url, change it to only block on explicitFalse, not onNone.
| if player.queue.empty() and not player.current: | ||
| return await ctx.send("❌ The queue is empty.") | ||
|
|
||
| # Get up to 10 items from the queue |
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
suggestion (code-quality): Merge nested if conditions (merge-nested-ifs)
| 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 | |
Explanation
Too much nesting can make code difficult to understand, and this is especiallytrue 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(), |
There was a problem hiding this comment.
suggestion (code-quality): Swap if/else branches of if expression to remove negation (swap-if-expression)
| 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'), |
Explanation
Negated conditions are more difficult to read than positive ones, so it is bestto avoid them where we can. By swapping the
if and else conditions around wecan invert the condition and make it positive.
| if eco == "prankful": | ||
| return shop_items_prankful | ||
| return {} |
There was a problem hiding this comment.
suggestion (code-quality): We've found these issues:
- Lift code into else after jump in control flow (
reintroduce-else) - Replace if statement with if expression (
assign-if-exp)
| if eco == "prankful": | |
| return shop_items_prankful | |
| return {} | |
| return shop_items_prankful if eco == "prankful" else {} |
| color=self.bot.config.PRIMARY_COLOR | ||
| ) | ||
|
|
||
| guild_list = [] |
There was a problem hiding this comment.
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 + ' ' |
There was a problem hiding this comment.
issue (code-quality): Use f-string instead of string concatenation (use-fstring-for-concatenation)
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (3)
database.py (3)
2-3: Drop unused imports.
asyncioandurlparseare never referenced; keeping them only trips Ruff and future linters.-import asyncio -from urllib.parse import urlparseAlso applies to: 8-8
🧰 Tools
🪛 Ruff (0.11.9)
2-2:
asyncioimported but unusedRemove unused import:
asyncio(F401)
27-31: Guard against concurrent writes to the JSON file.Multiple async tasks calling
_save_json()can interleave writes and corruptdatabase.json.
Consider:
- an
asyncio.Lockaround 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 passmin_size=1, max_size=5or read limits from config.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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_URLis 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_balanceblindly appliesamount; callers could push balances negative or overflowBIGINT.
Recommend clamping to>= 0(or whatever rule you need) inside this method to keep invariants local.
Summary
7xbot.pyinto a cog to match the repo's modular structureTesting
pytest -qhttps://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:
Enhancements:
Build:
Deployment:
Tests:
Chores:
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Tests
Chores