Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
## [clankwarden] -- 2026-06-04

### Fixes
- **`.clank cluster cleave` and `.clank taunt` no longer crash.** Both queried a
`chat_levels` table that only exists when a separate leveling bot shares the
database -- Clankwarden ships no leveling system, so the commands failed with
`relation "chat_levels" does not exist`. The level lookup is now best-effort:
when the table is absent the level-based protections/gates are simply skipped
(the commands remain mod-gated), and they work normally when the table is
present.
- **Scammer-report buttons no longer fail.** The "Clank", "False report (30m)",
and dehoist-alert "Clank" buttons ran the full containment path before
acknowledging the click, so when containment took longer than Discord's 3s
Expand Down
40 changes: 29 additions & 11 deletions cogs/clank.py
Original file line number Diff line number Diff line change
Expand Up @@ -1431,6 +1431,28 @@ async def _name_blacklist(self, guild_id: int) -> list[str]:
return []
return _as_str_list(s.get("name_blacklist"))

async def _chat_level(self, guild_id: int, user_id: int) -> int | None:
"""Best-effort chat level from a leveling bot that shares this database.

Clankwarden ships no leveling system, so the ``chat_levels`` table only
exists when another bot on the same DB provides it. Returns ``None`` when
the table is absent (so callers can tell "no leveling system" apart from
"level 0") and never lets a missing table crash a command."""
if getattr(self, "_chat_levels_missing", False):
return None
try:
val = await self.bot.db.fetch_val(
"SELECT level FROM chat_levels WHERE guild_id=$1 AND user_id=$2",
guild_id, user_id,
)
except Exception:
# Cache the absence so we do not re-run a failing query (and spam logs)
# on every cleave/clarion. A leveling bot would create the table at
# startup, before Clankwarden serves these admin commands.
self._chat_levels_missing = True
return None
return int(val) if val is not None else 0

async def _is_hunter(self, member: discord.Member | None, guild_id: int) -> bool:
"""True if ``member`` holds the configured clanker-hunter role.

Expand Down Expand Up @@ -7048,11 +7070,8 @@ async def cluster_cleave(self, ctx: DiscoContext, cluster_id: int) -> None:
if p.manage_roles or p.manage_messages or p.administrator:
skipped_protected.append(str(member))
continue
level = await self.bot.db.fetch_val(
"SELECT level FROM chat_levels WHERE guild_id=$1 AND user_id=$2",
ctx.guild.id, m_uid,
)
if level and int(level) >= 30:
level = await self._chat_level(ctx.guild.id, m_uid)
if level is not None and level >= 30:
skipped_protected.append(f"{member} (lvl {level})")
continue
to_clank.append(member)
Expand Down Expand Up @@ -8170,12 +8189,11 @@ async def clanker_clarion(self, ctx: DiscoContext, *, message: str) -> None:
await ctx.reply_error("You need Manage Roles permission.")
return

level_val = await self.bot.db.fetch_val(
"SELECT level FROM chat_levels WHERE guild_id=$1 AND user_id=$2",
ctx.guild.id, ctx.author.id,
)
level = int(level_val or 0)
if level < 15:
# The level gate only applies when a leveling bot shares this DB; with no
# chat_levels table (Clankwarden ships none) the command stays usable for
# the mods who already pass the Manage Roles check above.
level = await self._chat_level(ctx.guild.id, ctx.author.id)
if level is not None and level < 15:
await ctx.reply_error(f"Clarion requires level 15+. You are level {level}.")
return

Expand Down
Loading