Repo: https://github.com/namelessdevelopers/eliminabot Last touched: Oct 2024 (1y4m dormant) Stack: Python, discord.py 2.x, SQLAlchemy 2.x, SQLite, Pydantic Scale: ~400-500 servers, hardcoded 32 shards Created: 2026-02-04
These are things that are silently failing or crashing right now.
File: elimina/db/guild.py → update_guild()
Impact: ~toggle and ~ignore commands do nothing. Completely broken.
# CURRENT (broken) — .append() returns None, json.dumps(None) = "null", AND result isn't assigned back
if enabled_channel:
json.dumps(json.loads(guild.toggled_channels).append(enabled_channel))Fix: Parse the list, mutate, serialize, assign back to the entity:
if enabled_channel:
channels = json.loads(guild.toggled_channels)
channels.append(enabled_channel)
guild.toggled_channels = json.dumps(channels)Same pattern for disabled_channel, ignored_bot, unignored_bot.
File: elimina/db/guild.py → transform_lists()
# CURRENT (broken) — loads toggled_channels into bots
bots = json.loads(guild.toggled_channels) # should be guild.ignored_botsFile: elimina/helpers/snipe.py
# CURRENT — declares type hint but never sets value
def __init__(self) -> None:
self.message: Optional[Message] # this is NOT assignment, just annotationFix: self.message: Optional[Message] = None
Same for EditSnipe.edited_message.
File: elimina/handlers/event_handler.py
# CURRENT — stores raw discord.Message, but snipe command accesses .message attribute
self.snipe_message[guild_id] = message # should wrap in Snipe()
# on_message_edit stores only `after`, but editsnipe needs both before and after
self.edit_snipe_message[guild_id] = afterFix:
# on_message_delete
snipe = Snipe()
snipe.message = message
self.snipe_message[guild_id] = snipe
# on_message_edit
edit_snipe = EditSnipe()
edit_snipe.message = before
edit_snipe.edited_message = after
self.edit_snipe_message[guild_id] = edit_snipeFile: elimina/handlers/event_handler.py
await ctx.send(snipe_embed) # WRONG — sends as content string
await ctx.send(embed=snipe_embed) # CORRECTFile: elimina/handlers/event_handler.py (snipe + editsnipe commands)
discord.py 2.x requires name= and value= as keyword args:
# CURRENT (broken)
snipe_embed.add_field(attachment.filename, attachment.proxy_url, inline=True)
# FIX
snipe_embed.add_field(name=attachment.filename, value=attachment.proxy_url, inline=True)File: elimina/handlers/event_handler.py → on_message()
discord.py 2.x changed positional delay to keyword-only:
return await message.delete(60) # BROKEN
return await message.delete(delay=60) # CORRECTFile: elimina/handlers/event_handler.py
guild_id = before.guild.id # WRONG — Guild object doesn't have .guild
guild_id = before.id # CORRECTFile: elimina/handlers/event_handler.py → on_message()
message.guild is None in DMs → message.guild.id crashes. Same for on_message_delete, on_message_edit.
Fix: Add early return at the top of each listener:
if not message.guild:
returnAlso add @commands.guild_only() decorator to all commands.
File: elimina/handlers/event_handler.py
# CURRENT — hardcoded, inconsistent with on_guild_join which uses config
await self.bot.get_guild(777063033301106728).get_channel(779045674557767680).send(...)
# FIX — use config values
await self.bot.get_guild(config.SUPPORT_SERVER_ID).get_channel(config.JOIN_LEAVE_CHANNEL).send(...)File: elimina/handlers/event_handler.py
# CURRENT — will KeyError if guild was never in the dict
del self.snipe_message[guild_id]
# FIX
self.snipe_message.pop(guild_id, None)
self.edit_snipe_message.pop(guild_id, None)File: elimina/handlers/error_handler.py
# CURRENT
command.get_cooldown_retry_after(ctx)
# FIX — use the error object directly
error.retry_afterFiles: elimina/db/__init__.py, elimina/db/guild.py
Currently using sync Session inside async def functions — this blocks the event loop on every DB call.
Changes:
- Replace
create_engine→create_async_engine(requiresaiosqlitedep) - Replace
Session(engine)→async with AsyncSession(engine) as session: - Replace
session.query(...)→await session.execute(select(...)) - Update
DB_URIformat:sqlite+aiosqlite:///db.sqlite3 - Add
aiosqliteto dependencies
File: elimina/db/guild.py
@cachetools.cached(cache) on get_whitelists() means the cache never clears. Any toggle/ignore change is invisible until restart.
Options (pick one):
- A) TTLCache —
cachetools.TTLCache(maxsize=1, ttl=60)— simple, slight delay - B) Manual invalidation — call
cache.clear()after everyupdate_guild/create_guild/delete_guild - C) In-memory dict managed by the bot — drop cachetools, maintain a dict updated on writes
Recommendation: Option B (manual invalidation). Simple, immediate, no weird stale data.
File: elimina/entities/guild.py, elimina/db/guild.py
The entity declares Mapped[List[int]] with JSON column type, but the code does manual json.loads/json.dumps everywhere. SQLAlchemy's JSON type handles serialization automatically.
Fix:
- Remove all manual
json.loads()/json.dumps()in DB functions - Let SQLAlchemy handle it natively through the JSON column type
- Remove
transform_lists()entirely — it becomes unnecessary
File: elimina/entities/guild.py
guild_table = Table(...) and class Guild(Base) both define the same table. The standalone Table object is never used. Delete it.
File: elimina/__init__.py
# CURRENT — hardcoded 32 shards for a 400-500 server bot (overkill, wastes resources)
shards=32,
# FIX — remove the kwarg entirely, let AutoShardedBot decide (or use regular Bot)Discord recommends sharding at 2,500+ guilds. For 400-500 servers, commands.Bot (no sharding) is fine. If you want to keep AutoShardedBot for future growth, just remove the shards=32 and let Discord's gateway tell you how many you need.
File: elimina/commands/mod.py
# CURRENT — manual role name check
for role in ctx.author.roles:
if role.name.lower() == "moderation":
has_moderation = TrueFix: Use discord.py's built-in decorator:
@commands.has_permissions(manage_messages=True)Drop the manual role iteration and SUPER_USERS check (handle super users via bot.is_owner() or a custom check).
File: elimina/handlers/event_handler.py
f"sniped by {ctx.author.name}#{ctx.author.discriminator}"Discord migrated to unique usernames. discriminator is "0" for most users now. Use ctx.author.display_name or just ctx.author.name.
Prevents DM invocations from crashing. Apply to every command in every cog.
File: elimina/__init__.py
intents=discord.Intents(53608189) # magic numberReplace with explicit intent construction for clarity:
intents = discord.Intents.default()
intents.message_content = True
intents.members = True # if neededCurrent issues:
- Stored per-guild, not per-channel (can only snipe the last deleted msg in the entire server)
- No TTL (deleted message from 3 hours ago is still snipeable)
- Snipe data lives on the EventHandler cog (coupling)
Proposed refactor:
- Store per-channel:
Dict[int, Dict[int, Snipe]]→{guild_id: {channel_id: Snipe}} - Add TTL: auto-expire after 60 seconds (match README claim)
- Move snipe storage to a dedicated service/manager class
snipe/editsnipecommands can stay on EventHandler or move to their own cog
Current state: Creates a temp voice channel, moves user to it, deletes the channel. Hacky but functional.
Known issues:
asyncio.sleep(time)doesn't survive bot restarts- Double
tempChannel.delete()(second one always fails) - No way to cancel the timer
Decision: Keep as-is per your call. Minor cleanup:
- Remove the second
delete()call and the 30s sleep - Add a try/except around the whole disconnect flow
- Maybe add a
~canceldctimercommand later
- Replace f-string logging with lazy
%sformatting (performance) - Add structured context (guild_id, channel_id) to log messages
- Consider using discord.py's built-in logging config
- Move
SUPER_USERSfromconstants.pyto config/env - Add
OWNER_IDSto Config forcommands.is_owner()support - Consider adding
LOG_LEVELto config
- Update command docs to reflect current state
- Fix "default is 5 seconds" vs "default: 15 seconds" inconsistency (help says 15, entity default is 5)
- Add setup/deployment instructions
| Phase | Status | Commits |
|---|---|---|
| Phase 1 (Critical Bugs) | ✅ DONE | f72b1e7..e721af4 |
| Phase 2 (Database Layer) | ✅ DONE | 934a8b0..eb6b623 |
| Phase 3 (discord.py Modern) | ✅ DONE | 7d400df..bb3e6f2 |
| Phase 4 (Refactor + Polish) | ✅ DONE | cf0274f..8e5c79d |
All phases completed 2026-02-04. 17 commits, not yet pushed.
# pyproject.toml additions
"aiosqlite>=0.20.0", # for async SQLite supportPhase 1: db/guild.py, helpers/snipe.py, handlers/event_handler.py, handlers/error_handler.py, commands/mod.py
Phase 2: db/__init__.py, db/guild.py, entities/guild.py, __init__.py, pyproject.toml
Phase 3: __init__.py, commands/mod.py, handlers/event_handler.py, handlers/error_handler.py, all command cogs (guild_only)
Phase 4: helpers/snipe.py, handlers/event_handler.py, commands/utility.py, config.py, constants.py, README.md
Let's get this thing working again. 🔧