Date: 2026-02-14 Scope: Full codebase review - code quality, security, modernization, correctness Files Reviewed: 32 Python files, Dockerfile, pyproject.toml, CI workflows, .env
- Critical Issues
- Security Vulnerabilities
- Dependency Issues
- Code Quality Issues
- Architectural Issues
- Discord.py Migration Issues
- Database Issues
- Performance Issues
- CI/CD Issues
- Docker Issues
- Code Style & Conventions
- Per-File Findings
- Modernization Roadmap
File: pyproject.toml:7
Severity: Critical
Description: discord.py 1.7.3 was released in 2021 and is no longer maintained. The library has since released v2.x with breaking changes. The pinned version has known bugs, missing features (slash commands, modals, buttons), and no security updates.
Impact: No access to modern Discord features, potential security vulnerabilities, community support is gone.
Fix: Migrate to discord.py 2.x (or the maintained py-cord / nextcord fork). This is a major undertaking as v2 changed intents, event handlers, avatar_url -> avatar.url, cog loading, and many other APIs.
File: pyproject.toml:8
Severity: Critical
Description: aiohttp < 3.8 pins to a version from 2021. Current aiohttp is 3.11+. This blocks security patches and compatibility with modern Python.
Impact: Known CVEs in old aiohttp versions. Incompatible with Python 3.12+.
Fix: Remove the upper bound pin after migrating to discord.py 2.x (which supports modern aiohttp).
Status: Fixed in Step 3. Migrated both user_db_conn.py and cache_db_conn.py from sqlite3 to aiosqlite. All methods are now async, all callers updated to await.
File: tle/cogs/duel.py:456-479 (draw_offers), tle/cogs/starboard.py (locks)
Severity: Critical
Description: Duel draw offers (self.draw_offers = {}) are stored only in memory. If the bot restarts during a duel, draw state is lost. Similarly, starboard guild locks accumulate without cleanup.
Impact: Data loss on restart, inconsistent game state, potential memory leak from unbounded lock dict.
Status: Starboard locks bounded with LRU dict (maxsize=256) in Step 4. Duel draw persistence deferred to Step 9 (schema-breaking).
Status: Fixed in Step 3. All f-string SQL interpolation replaced with parameterized ? placeholders.
Status: Fixed in Step 3. Added _VALID_TABLES and _VALID_COLUMNS allowlist validation before formatting.
File: tle/cogs/meta.py:28-43
Severity: Medium
Description: git_history() runs subprocess.Popen(['git', ...]) with communicate() but no timeout. A hanging git process would block indefinitely.
Fix: Add timeout=10 to communicate().
File: tle/util/codeforces_common.py:334-338, 390-396
Severity: Low
Description: parse_rating() calls int(arg) without range validation. A user could pass extremely large numbers. Similarly, rating filter args like r>=99999999 are accepted.
Fix: Validate ratings are within reasonable bounds (e.g., 0-5000).
Status: Fixed in Steps 1 and 3. Replaced with await bot.close() + sys.exit(0) in Step 1. DB connections are now closed on shutdown via bot.close override in Step 3.
File: pyproject.toml:10-18
Severity: High
Description: numpy, pandas, matplotlib, seaborn, pillow, pycairo, PyGObject, aiocache have no version constraints. A pip install could pull incompatible major versions.
Fix: Pin to compatible ranges: numpy>=1.24,<3, matplotlib>=3.7,<4, etc.
File: tle/__main__.py
Severity: Medium
Description: The bot reads from .env but relies on the shell environment or Docker to load it. There's no python-dotenv in dependencies, meaning python -m tle without Docker won't load .env.
Fix: Add python-dotenv and call load_dotenv() in __main__.py, or document that .env is only for Docker.
File: pyproject.toml:4
Severity: Low
Description: The code uses str | int union syntax (constants.py:32) which requires Python 3.10+. The requires-python should reflect this.
Fix: Change to requires-python = ">= 3.10" or use Union[str, int] from typing.
Severity: Medium
Description: No requirements.txt, poetry.lock, or uv.lock exists. Builds are not reproducible.
Fix: Add a lock file mechanism. Consider migrating to uv or poetry for deterministic builds.
Severity: High
Description: Almost no functions have type hints. The few that exist are incomplete (e.g., user_ids: [str] at user_db_conn.py:1060 is invalid - should be list[str]).
Fix: Add type annotations incrementally. Enable mypy or pyright in CI.
Severity: Medium Files: Multiple Examples:
codeforces.py:332:delta // 100 + 3(array index offset)duel.py:120-124:_DUEL_INVALIDATE_TIME = 120(seconds? minutes?)cache_system2.py:42-45:30 * 60,5 * 60,20 * 60(reload delays)codeforces.py:41:_GITGUD_NO_SKIP_TIME = 3 * 60(minutes, but reads as seconds)rating_calculator.py:54:MAX = 6144(unexplained bound) Fix: Extract to well-named constants with units in the name (e.g.,_CONTEST_RELOAD_DELAY_SECONDS = 1800).
Severity: Medium Examples:
handles.py_update_ranks()- ~100 lines handling role updates, rank detection, embed creationcontests.py_watch_rated_vc()- ~75 lines with sequential API calls and rating calculationsgraphs.py_plot_rating()- ~60 lines combining data fetching, transformation, and plotting Fix: Break into smaller, focused functions with single responsibilities.
Severity: Medium Description: Three different patterns coexist:
- Custom exception +
@send_error_ifdecorator (most cogs) - Bare
except: pass(logging.py:61-62) - No error handling at all (some cache operations)
Fix: Standardize on the decorator pattern. Never use bare
except.
Status: Fixed. The deactivated/ directory and cses_scraper.py have been deleted.
File: tle/util/discord_common.py:103
Description: """Decorator that wraps a corouting asuch that it is executed only once.""" - "corouting asuch" should be "coroutine such".
Fix: Fix the docstring.
File: tle/util/discord_common.py:138
Description: presence_task contains a while True loop with asyncio.sleep(10 * 60) inside a task that itself uses Waiter.fixed_delay(5 * 60). The inner loop means the waiter never fires after the first run.
Fix: Remove the inner while True loop and let the task framework handle repetition.
Status: Fixed in Step 4. Services (user_db, cache2, event_sys) are now attached to the bot instance during initialize(bot, nodb). Cogs access them via self.bot.user_db, etc. Module-level globals kept as aliases for utility code.
Status: Fixed in Step 4. All cog methods access services through self.bot.* instead of importing globals. Module-level functions that can't use self.bot still use cf_common.* aliases.
Status: Fixed in Step 4. Split cache_system2.py (850 lines) into tle/util/cache/ package with focused modules: _common.py, contest.py, problem.py, problemset.py, rating_changes.py, ranklist.py, cache_system.py. Old file replaced with backward-compat re-export shim.
Severity: Medium
Description: Cog methods mix data fetching, business logic, embed creation, and response sending in single functions. For example, contests.py's ranklist command fetches standings, formats tables, creates embeds, and paginates all in one method.
Fix: Extract business logic into service classes. Keep cogs thin - only parsing input and sending output.
File: tle/util/tasks.py (251 lines)
Severity: Low
Description: A custom task framework was built instead of using discord.ext.tasks. While more flexible, it's non-standard and adds maintenance burden.
Fix: Evaluate whether discord.py 2.x's improved tasks.loop can replace this. If not, keep but document thoroughly.
These issues must be addressed when migrating from discord.py 1.7.3 to 2.x:
File: tle/util/discord_common.py:50
Description: user.avatar_url -> user.display_avatar.url in v2.
Fix: Update all avatar_url references.
File: tle/util/discord_common.py:19
Description: discord.Embed.Empty -> None in v2.
Fix: Replace with None.
File: All cog files (e.g., codeforces.py:618)
Description: def setup(bot): bot.add_cog(...) must become async def setup(bot): await bot.add_cog(...) in v2.
Fix: Make all setup() functions async.
File: tle/__main__.py:88
Description: bot.load_extension(...) must be awaited in v2.
Fix: Move extension loading into an async setup hook.
File: tle/__main__.py:82-83
Description: v2 requires intents.message_content = True for reading message content. The current code only enables intents.members.
Fix: Add intents.message_content = True.
File: tle/cogs/meta.py (guilds command)
Description: guild.icon_url -> guild.icon.url in v2 (with None check).
Fix: Update with null safety.
Severity: Medium
Description: The bot uses only prefix commands (;). Modern Discord bots should support slash commands for discoverability.
Fix: Add hybrid commands or migrate to app commands after v2 migration.
File: tle/util/db/user_db_conn.py:238-288
Severity: High
Description: Database migrations are inline if old_exists and not migrated: blocks in create_tables(). This doesn't scale and makes schema evolution error-prone.
Fix: Use Alembic, or at minimum a versioned migration system with a schema_version table.
Status: Improved in Step 3. Simple writes use async with conn:. Methods with conditional rollback logic kept explicit commit()/rollback() since context managers can't express that pattern.
Status: Fixed in Step 3. Now raises ValueError on non-identifier column names instead of silently dropping them.
Status: Fixed in Step 3. Now uses cursor-level row_factory instead of mutating conn.row_factory.
File: tle/util/db/user_db_conn.py
Severity: Medium
Description: user_id is stored as TEXT in user_handle but as INTEGER in duelist. Discord IDs are large integers (snowflakes). The inconsistency requires casting at boundaries (e.g., int(user_id) at line 547).
Fix: Standardize on INTEGER for all Discord IDs.
Status: Fixed in Step 3. bot.close is overridden in __main__.py to close both user_db and cache_db connections on shutdown.
File: tle/util/db/user_db_conn.py:107
Description: The duelist and duel tables have no guild_id column. A user's duel rating is shared across all guilds the bot serves. There's even a TODO comment acknowledging this.
Fix: Add guild_id to duel tables with migration.
Status: Fixed in Step 4. Replaced sequential loop with asyncio.gather(). Rate limit still respected via shared @cf_ratelimit decorator on _query_api.
File: tle/util/cache_system2.py
Severity: Medium
Description: The problem cache loads all ~10,000+ CF problems into memory as Python objects. Combined with the contest cache and ranklist cache, memory usage can be significant.
Fix: Consider lazy loading or database-backed queries for less frequently accessed data.
File: tle/util/ranklist/rating_calculator.py:53-65
Description: Uses numpy FFT to precompute seeds for all possible ratings (array size 12288). While mathematically elegant, this is heavy for what could be a simpler computation, especially for small contests.
Fix: Profile and consider a direct computation for small contest sizes.
Status: Fixed in Step 4. Replaced temp file with io.BytesIO() in-memory buffer. Added plt.close() to prevent matplotlib memory leak.
File: tle/util/discord_common.py:139-145
Description: bot.get_all_members() iterates ALL members in ALL guilds every 10 minutes to pick one random member. For large bots, this could be thousands of members.
Fix: Cache the member list or use random.choice(guild.members) on a single guild.
File: .github/workflows/lint.yaml:13-14, build.yaml:13
Severity: Medium
Description: Uses actions/checkout@v3 and actions/setup-python@v4. Current versions are v4 and v5 respectively.
Fix: Update to latest action versions.
Severity: High
Description: Despite having pytest as an optional dependency, there are no tests and no test job in CI.
Fix: Add tests and a test job. See TESTING_PLAN.md.
File: .github/workflows/lint.yaml:16-17
Severity: Low
Description: Lint only runs on 3.11. Should test on all supported versions, or at minimum on the declared minimum version.
Fix: Add matrix for supported Python versions.
File: .github/workflows/lint.yaml:18
Description: pip install ruff installs the latest version. A new ruff release with new rules could break CI.
Fix: Pin ruff version: pip install ruff==0.x.y.
Severity: Medium
Description: No dependency vulnerability scanning (e.g., pip-audit, safety, Dependabot).
Fix: Add pip-audit step to CI and enable Dependabot.
File: Dockerfile:3-8
Severity: Medium
Description: build-essential, cmake, gcc, meson are installed and remain in the final image. These are only needed for compiling Python packages.
Fix: Use multi-stage build: compile in builder stage, copy only runtime dependencies to final stage.
Severity: Low
Description: No HEALTHCHECK instruction. Docker/orchestrators can't monitor bot health.
Fix: Add a health check (e.g., HTTP endpoint or file touch).
Severity: Medium
Description: No USER instruction. The bot runs as root inside the container.
Fix: Add RUN useradd -m botuser and USER botuser.
File: Dockerfile:18
Severity: Low
Description: No .dockerignore file. COPY . . includes .git/, .env, data/, IDE files, etc.
Fix: Create .dockerignore with .git, .env, data/, logs/, __pycache__/, .vscode/, .idea/.
File: Dockerfile:16
Severity: Low
Description: pip install --no-cache-dir . installs the package in the workdir. This is fine, but the subsequent COPY . . overwrites the installed package with source. The bot runs via python -m tle which uses the source directly.
Fix: Either install properly and remove source, or don't install and just run from source (current effective behavior).
Description: Mix of f-strings, .format(), and % formatting across the codebase.
Fix: Standardize on f-strings everywhere.
Description: Some admin commands use _ prefix (_nogud, _invalidate), others don't (cache). No clear convention.
Fix: Document convention and apply consistently.
Description: Most modules have no docstring explaining their purpose. Fix: Add docstrings to all modules.
Description: Some files import discord before stdlib, others follow PEP 8 ordering.
Fix: Enable ruff's import sorting rules (I ruleset).
Status: Fixed in Step 4. All paths in tle/constants.py migrated from os.path.join to pathlib.Path / operator.
| Line | Issue | Severity |
|---|---|---|
| 38 | plt.rcParams set globally during import-time setup |
Low |
| 85 | Hardcoded command prefix ; - should be configurable |
Low |
| 86 | Path('tle', 'cogs') uses relative path - breaks if CWD changes |
Medium |
| 88 | bot.load_extension() is sync - needs async in discord.py 2.x |
High |
| 104 | asyncio.create_task(discord_common.presence(bot)) - task not awaited or tracked |
Low |
| Line | Issue | Severity |
|---|---|---|
| 3 | DATA_DIR = 'data' - relative path, breaks if CWD changes |
Medium |
| 23-27 | ALL_DIRS is a generator expression - can only be iterated once |
Medium |
| 32 | `str | intreturn type requires Python 3.10+ butrequires-python >= 3.9` |
| 46-47 | _DEFAULT_COLOR and _DEFAULT_STAR use leading underscore but are imported by other modules |
Low |
| Line | Issue | Severity |
|---|---|---|
| ~360 | Global _session initialized lazily, not thread-safe |
Medium |
| ~375-407 | Rate limiter retries 3 times with 1s delay - doesn't handle 429 properly | Medium |
| ~532 | Chunk size 10000 for batch user info - no adaptive sizing | Low |
| ~625 | _resolve_redirect() only handles specific redirect patterns |
Low |
| Various | All NamedTuples use camelCase (matching CF API) - Pythonic would be snake_case | Low |
| Line | Issue | Severity |
|---|---|---|
| 31 | active_groups = defaultdict(set) - race condition in async context |
Medium |
| 72-73 | active set captured at decoration time, not dynamically |
Low |
| 131 | handles: [str] is not valid type annotation (should be list[str]) |
Low |
| 272 | member_identifier[-2:] == '#0' - hardcoded Discord discriminator removal hack |
Medium |
| 351 | self.dhi = 10**10 - magic number for max timestamp |
Low |
| Line | Issue | Severity |
|---|---|---|
| 50 | user.avatar_url - deprecated in discord.py 2.x |
High |
| 103 | Typo in docstring: "corouting asuch" | Low |
| 138 | while True inside task function - waiter never re-fires |
Medium |
| 143-144 | constants.TLE_PURGATORY compared to role.name - fails if configured as role ID |
Medium |
| Line | Issue | Severity |
|---|---|---|
| 20 | CONTEST_BLACKLIST = {1308, 1309, 1431, 1432} - hardcoded, should be configurable |
Low |
| 70-77 | reload_now() race: checks locked() then acts - TOCTOU |
Medium |
| ~249 | Problem name collisions silently overwrite data | Medium |
| Line | Issue | Severity |
|---|---|---|
isidentifier() filter can silently drop columns |
||
| 107 | TODO: duel tables not guild-aware (since original code) | High |
| 238-288 | Migration code mixed with schema creation | Medium |
_insert_one/_insert_many use .format() for SQL |
||
_fetchone mutates shared conn.row_factory |
||
| 369 | Tuple unpacking assumes column order c_id, issue_time = res |
Low |
| Line | Issue | Severity |
|---|---|---|
| ~147 | max(random.randrange()...) for weighted random - unclear intent |
Low |
| ~332 | delta // 100 + 3 - magic index offset |
Low |
| ~573-619 | teamrate has nested normalization with magic math |
Low |
| Line | Issue | Severity |
|---|---|---|
| ~174-202 | All guild tasks rescheduled on any contest update | Medium |
| ~188 | json.loads(before) without try/except |
Medium |
| ~364-397 | Duplicated CF/IOI standings formatting logic | Low |
| ~752-826 | _watch_rated_vc() is very long, sequential async calls |
Medium |
| Line | Issue | Severity |
|---|---|---|
| ~244 | 5-minute asyncio.sleep() blocking pattern |
Medium |
| ~456-479 | draw_offers dict not persisted |
High |
| ~571-572 | No cleanup of expired draw offers | Medium |
| ~213-216 | Double randrange loop for problem selection is inefficient |
Low |
| Line | Issue | Severity |
|---|---|---|
| ~670 | Parameter validation after parsing (should validate early) | Low |
| ~737-744 | Color generation uses assert instead of proper error handling |
Medium |
| ~774-789 | Figure size hardcoded (1500, 500) |
Low |
| Line | Issue | Severity |
|---|---|---|
| ~359-420 | maybe_add_trusted_role() with deeply nested try/except |
Medium |
| ~607-609 | Font loading with no error handling | Low |
| ~775-776 | zip() without strict=True could silently truncate |
Medium |
| ~1162-1228 | grandfather() is 66 lines with high cyclomatic complexity |
Medium |
| Line | Issue | Severity |
|---|---|---|
| ~61-62 | Bare except: pass silences all errors |
Medium |
| ~45-52 | Silent attribute access errors | Low |
| N/A | No backpressure on log queue | Low |
| Line | Issue | Severity |
|---|---|---|
os._exit(42) bypasses cleanup |
||
os._exit(0) bypasses cleanup |
| Line | Issue | Severity |
|---|---|---|
| ~~~97~~ | self.locks.setdefault() creates unbounded dict growth |
_BoundedLockDict(maxsize=256) |
| ~66-67 | Incomplete image type checking | Low |
| Line | Issue | Severity |
|---|---|---|
io.BytesIO() |
||
plt.close() after figure creation - memory leak |
plt.close() |
|
| N/A | Hardcoded font path with no fallback | Low |
| Line | Issue | Severity |
|---|---|---|
| N/A | Event listeners accumulate without cleanup for removed cogs | Low |
| Line | Issue | Severity |
|---|---|---|
| N/A | Uses reaction-based pagination (deprecated Discord pattern) | Low |
- Rotate the bot token
- Fix
os._exit()calls to use proper shutdown - Add subprocess timeout to
git_history() - Fix bare
exceptin logging cog - Pin all dependency versions
- Create
.dockerignore Close CSES scraper session properly (or remove)(DONE - removed)- Fix
ALL_DIRSgenerator-consumed-once bug in constants.py
- Update
discord.pyto 2.x - Remove
aiohttp < 3.8pin - Fix all deprecated APIs (avatar_url, Embed.Empty, etc.)
- Make cog
setup()functions async - Add
message_contentintent - Move extension loading to async setup hook
- Update
requires-pythonto>= 3.10
Migrate from(DONE)sqlite3toaiosqliteStandardize transaction management (context managers)(DONE where appropriate)- Standardize
user_idtypes to INTEGER (deferred to Step 9) - Implement proper migration system (versioned SQL files)
- Move migration code out of
create_tables() - Add guild_id to duel tables
Replace global singletons with dependency injection via bot instance(DONE)Split(DONE)cache_system2.pyinto separate modules per cache- Extract business logic from cogs into service classes
Use(DONE)io.BytesIO()for plot generation instead of temp files- Add proper type annotations throughout
- Replace custom task framework with discord.py 2.x tasks (evaluate feasibility)
- Add hybrid commands (prefix + slash)
- Implement button/select-based pagination (replace reaction pagination)
- Add modals for complex input (handle registration)
- Use embeds with components for interactive features (duel accept/decline)
- Add unit tests (see TESTING_PLAN.md)
- Add integration tests
- Update GitHub Actions versions
- Pin ruff version
- Add
pip-auditsecurity scanning - Add type checking (mypy/pyright) to CI
- Add test coverage reporting
- Multi-stage Docker build
- Run as non-root user
- Add health check
- Consider Docker Compose for development
- Add secrets management