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
14 changes: 8 additions & 6 deletions counting/commands/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,8 @@ async def set_toggle_progress(self, ctx: commands.Context) -> None:
async def set_toggle_progress_delete(self, ctx: commands.Context) -> None:
"""Toggle whether the goal message is deleted after being sent."""
settings = await self.settings.get_guild_settings(ctx.guild)
toggle = not settings["toggle_progress_delete"]
await self.settings.update_guild(ctx.guild, "toggle_progress_delete", toggle)
toggle = not settings["toggle_goal_delete"]
await self.settings.update_guild(ctx.guild, "toggle_goal_delete", toggle)
await ctx.send(f"Goal message deletion is now {toggle and 'enabled' or 'disabled'}.")

@countingset.group(name="messages")
Expand Down Expand Up @@ -296,12 +296,12 @@ async def set_ruinrole(
if not (60 <= duration_seconds <= 30 * 86400):
return await ctx.send("Duration must be between 60 seconds and 30 days.")
except ValueError:
return await ctx.send(
"Invalid duration. Use a number followed by 's', 'm', 'h', or 'd'."
)
logger.error(
f"Invalid duration format '{duration}' provided by {ctx.author} in guild {ctx.guild.id}."
)
return await ctx.send(
"Invalid duration. Use a number followed by 's', 'm', 'h', or 'd'."
)
await asyncio.gather(
self.settings.update_guild(ctx.guild, "ruin_role_id", role.id),
self.settings.update_guild(ctx.guild, "ruin_role_duration", duration_seconds),
Expand Down Expand Up @@ -391,7 +391,9 @@ async def set_goal(
current_goals.append(goal)
current_goals.sort()
await self.settings.update_guild(ctx.guild, "goals", current_goals)
await ctx.send(f"Counting goal {goal} added. Current goals")
await ctx.send(
f"Counting goal {goal} added. Current goals: {', '.join(str(g) for g in current_goals)}."
)
else:
await ctx.send(f"Goal {goal} is already set.")
elif action.lower() == "remove":
Expand Down
56 changes: 54 additions & 2 deletions counting/commands/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@
SOFTWARE.
"""

import asyncio
from collections import defaultdict
from datetime import datetime
from typing import Optional

Expand Down Expand Up @@ -68,6 +66,60 @@ async def stats(self, ctx: commands.Context, user: Optional[discord.Member] = No
)
await ctx.send(f"Last counted: {time_str}\n{box(table, lang='prolog')}")

@counting.command(name="leaderboard", aliases=["lb"])
@commands.cooldown(1, 10, commands.BucketType.guild)
@commands.bot_has_permissions(embed_links=True)
async def leaderboard(self, ctx: commands.Context) -> None:
"""Show the counting leaderboard for the server.

Displays the top 15 users with the highest counts.
Please note that the leaderboard only includes users who have counted at least once.
"""
user_cache: dict = self.settings._user_cache
entries = []
for member in ctx.guild.members:
if member.bot:
continue
data = user_cache.get(member.id)
if data and data.get("count", 0) > 0:
entries.append((member, data["count"]))
if not entries:
return await ctx.send("No one has counted yet in this server.")
entries.sort(key=lambda x: x[1], reverse=True)
invoker_pos = next(
(i + 1 for i, (m, _) in enumerate(entries) if m.id == ctx.author.id), None
)
footer_base = f"Total counters: {len(entries)}"
if invoker_pos and invoker_pos > 15:
invoker_count = next(c for m, c in entries if m.id == ctx.author.id)
footer_base += (
f" · Your rank: #{invoker_pos} ({cf.humanize_number(invoker_count)} counts)"
)
per_page = 15
total_pages = max(1, -(-len(entries) // per_page))
pages = []
for page_num, i in enumerate(range(0, len(entries), per_page), start=1):
chunk = entries[i : i + per_page]
table_data = [
[i + rank, member.display_name, cf.humanize_number(count)]
for rank, (member, count) in enumerate(chunk, start=1)
]
table = tabulate(
table_data,
headers=["#", "User", "Count"],
tablefmt="simple",
stralign="left",
numalign="left",
)
embed = discord.Embed(
title=f"🏆 Counting Leaderboard — {ctx.guild.name}",
description=box(table, lang="prolog"),
color=await ctx.embed_color(),
)
embed.set_footer(text=f"Page {page_num}/{total_pages} · {footer_base}")
pages.append(embed)
await SimpleMenu(pages=pages, disable_after_timeout=True, timeout=120).start(ctx)

@counting.command(name="resetme", with_app_command=False)
@commands.cooldown(1, 360, commands.BucketType.user)
async def resetme(self, ctx: commands.Context) -> None:
Expand Down
42 changes: 26 additions & 16 deletions counting/event_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,19 +148,22 @@ async def on_message(self, message: discord.Message) -> None:
cleaned_goals = []
if isinstance(goals, list):
cleaned_goals = [int(g) for g in goals if isinstance(g, (int, float))]

if legacy_goal is not None:
migrated = False
if isinstance(legacy_goal, (int, float)) and legacy_goal not in cleaned_goals:
cleaned_goals.append(int(legacy_goal))
migrated = True
elif isinstance(legacy_goal, list):
for g in legacy_goal:
if isinstance(g, (int, float)) and g not in cleaned_goals:
cleaned_goals.append(int(g))
migrated = True
if migrated:
cleaned_goals = sorted(set(cleaned_goals))
await self.settings.update_guild(message.guild, "goals", cleaned_goals)
await self.settings.update_guild(message.guild, "goal", None)

if cleaned_goals:
cleaned_goals = sorted(set(cleaned_goals))
await self.settings.update_guild(message.guild, "goals", cleaned_goals)

if cleaned_goals and expected_count in cleaned_goals:
await self._handle_goal_reached(message, settings, expected_count)

Expand All @@ -186,7 +189,9 @@ async def on_message(self, message: discord.Message) -> None:
silent=settings["use_silent"],
)
elif settings["allow_ruin"]:
await self._handle_count_ruin(message, settings)
await self._handle_count_ruin(
message.channel, message.guild, message.author, settings
)
else:
response = settings["default_next_number_message"].format(
next_count=expected_count
Expand All @@ -195,26 +200,34 @@ async def on_message(self, message: discord.Message) -> None:
message, response, settings, settings["toggle_next_number_message"]
)
elif settings["allow_ruin"]:
await self._handle_count_ruin(message, settings)
await self._handle_count_ruin(message.channel, message.guild, message.author, settings)
else:
response = settings["default_next_number_message"].format(next_count=expected_count)
await handle_invalid_count(
message, response, settings, settings["toggle_next_number_message"]
)

async def _handle_count_ruin(self, message: discord.Message, settings: dict[str, Any]) -> None:
async def _handle_count_ruin(
self,
channel: discord.abc.Messageable,
guild: discord.Guild,
author: discord.Member | discord.Object,
settings: dict[str, Any],
) -> None:
old_count = settings["count"]
await asyncio.gather(
self.settings.update_guild(message.guild, "count", 0),
self.settings.update_guild(message.guild, "last_user_id", None),
self.settings.update_guild(guild, "count", 0),
self.settings.update_guild(guild, "last_user_id", None),
)
await assign_ruin_role(self.settings.config, message.author, message.guild, settings)
response = settings["ruin_message"].format(user=message.author.mention, count=old_count)
if isinstance(author, discord.Member):
await assign_ruin_role(self.settings.config, author, guild, settings)
author_mention = getattr(author, "mention", f"<@{author.id}>")
response = settings["ruin_message"].format(user=author_mention, count=old_count)
delete_after = (
settings["delete_after"] if settings.get("toggle_delete_after", False) else None
)
await send_message(
message.channel,
channel,
response,
delete_after=delete_after,
silent=settings["use_silent"],
Expand Down Expand Up @@ -251,10 +264,7 @@ async def on_raw_message_edit(self, payload: discord.RawMessageUpdateEvent) -> N

if settings["allow_ruin"]:
author = guild.get_member(author_id) or discord.Object(id=author_id)
await self._handle_count_ruin(
discord.Message(state=channel._state, channel=channel, data=payload.data),
settings,
)
await self._handle_count_ruin(channel, guild, author, settings)
elif settings["toggle_edit_message"]:
response = settings["default_edit_message"].format(next_count=settings["count"] + 1)
delete_after = (
Expand Down
4 changes: 2 additions & 2 deletions counting/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ async def handle_invalid_count(
await delete_message(message)
if send_response:
delete_after = (
settings["delete_after"] if settings.get("toggle_delete_after", True) else None
settings["delete_after"] if settings.get("toggle_delete_after", False) else None
)
await send_message(
message.channel,
Expand All @@ -105,7 +105,7 @@ async def assign_ruin_role(

role = guild.get_role(ruin_role_id)
if not role or role >= guild.me.top_role:
logger.warning(f"Cannot assign ruin role {role.name} in {guild.name} ({guild.id})")
logger.warning(f"Cannot assign ruin role {ruin_role_id} in {guild.name} ({guild.id})")
return

if any(r.id in excluded_role_ids for r in member.roles):
Expand Down
Loading