Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f831f54
bump pycord
MattyTheHacker Dec 25, 2025
0a46604
Fix mypy and ruff errors
MattyTheHacker Dec 25, 2025
4ef4ffc
Improve error messages
MattyTheHacker Dec 25, 2025
b729375
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Dec 25, 2025
cd812b8
Merge main into bump-pycord
automatic-pr-updater[bot] Dec 26, 2025
abc320c
Bump again
MattyTheHacker Dec 26, 2025
056a138
Merge main into bump-pycord
automatic-pr-updater[bot] Dec 26, 2025
ec24dc2
Make command_name optional
MattyTheHacker Dec 28, 2025
0a28c61
Merge main into bump-pycord
automatic-pr-updater[bot] Dec 29, 2025
5c7a714
Merge main into bump-pycord
automatic-pr-updater[bot] Dec 30, 2025
2aac9e0
Fix py-cord bump
CarrotManMatt Dec 30, 2025
40b94a1
Merge main into bump-pycord
automatic-pr-updater[bot] Dec 30, 2025
5bec60f
Merge main into bump-pycord
automatic-pr-updater[bot] Jan 3, 2026
e1578b5
Merge main into bump-pycord
automatic-pr-updater[bot] Jan 5, 2026
aeaec86
Merge main into bump-pycord
automatic-pr-updater[bot] Jan 6, 2026
e5b8fe3
Merge main into bump-pycord
automatic-pr-updater[bot] Jan 13, 2026
fbdfe23
Merge main into bump-pycord
automatic-pr-updater[bot] Jan 13, 2026
c750c47
Merge main into bump-pycord
automatic-pr-updater[bot] Jan 20, 2026
74da541
Update utils/tex_bot_base_cog.py
MattyTheHacker Jan 20, 2026
d3a4c3c
Merge main into bump-pycord
automatic-pr-updater[bot] Jan 21, 2026
cf6c507
Merge main into bump-pycord
automatic-pr-updater[bot] Jan 23, 2026
9ac052d
Merge main into bump-pycord
automatic-pr-updater[bot] Jan 27, 2026
0f5291c
minor fix
MattyTheHacker Jan 27, 2026
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
16 changes: 10 additions & 6 deletions cogs/command_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,17 @@ async def on_application_command_error(
if isinstance(error, discord.ApplicationCommandInvokeError) and isinstance(
error.original, GuildDoesNotExistError
):
command_name: str = (
ctx.command.callback.__name__
if (
hasattr(ctx.command, "callback")
and not ctx.command.callback.__name__.startswith("_")
command_name: str | None = (
(
ctx.command.callback.__name__
if (
hasattr(ctx.command, "callback")
and not ctx.command.callback.__name__.startswith("_")
)
else ctx.command.qualified_name
)
else ctx.command.qualified_name
if ctx.command
else None
)
logger.critical(
" ".join(
Expand Down
4 changes: 3 additions & 1 deletion cogs/induct.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,9 @@ async def _perform_induction(
applicant_role, reason=INDUCT_AUDIT_MESSAGE
)

tex_emoji: discord.Emoji | None = self.bot.get_emoji(743218410409820213)
tex_emoji: discord.AppEmoji | discord.GuildEmoji | None = self.bot.get_emoji(
743218410409820213
)
if not tex_emoji:
tex_emoji = discord.utils.get(main_guild.emojis, name="TeX")

Expand Down
4 changes: 2 additions & 2 deletions cogs/kill.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class ConfirmKillView(View):
@discord.ui.button(
label="SHUTDOWN", style=discord.ButtonStyle.red, custom_id="shutdown_confirm"
)
async def confirm_shutdown_button_callback( # type: ignore[misc]
async def confirm_shutdown_button_callback(
self, _: discord.Button, interaction: discord.Interaction
) -> None:
"""When the shutdown button is pressed, delete the message."""
Expand All @@ -37,7 +37,7 @@ async def confirm_shutdown_button_callback( # type: ignore[misc]
@discord.ui.button(
label="CANCEL", style=discord.ButtonStyle.grey, custom_id="shutdown_cancel"
)
async def cancel_shutdown_button_callback( # type: ignore[misc]
async def cancel_shutdown_button_callback(
self, _: discord.Button, interaction: discord.Interaction
) -> None:
"""When the cancel button is pressed, delete the message."""
Expand Down
4 changes: 3 additions & 1 deletion cogs/make_applicant.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ async def _perform_make_applicant(
await applicant_member.add_roles(applicant_role, reason=AUDIT_MESSAGE)
logger.debug("Applicant role given to user %s", applicant_member)

tex_emoji: discord.Emoji | None = self.bot.get_emoji(743218410409820213)
tex_emoji: discord.AppEmoji | discord.GuildEmoji | None = self.bot.get_emoji(
743218410409820213
)
if not tex_emoji:
tex_emoji = discord.utils.get(main_guild.emojis, name="TeX")

Expand Down
7 changes: 7 additions & 0 deletions cogs/remind_me.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,13 @@ async def remind_me(

The "remind_me" command responds with the given message after the specified time.
"""
if not ctx.channel:
await self.command_send_error(
ctx,
message="Interaction channel was None while trying to set a reminder.",
)
return

parsed_time: tuple[time.struct_time, int] = parsedatetime.Calendar().parseDT(
delay, tzinfo=timezone.get_current_timezone()
)
Expand Down
8 changes: 3 additions & 5 deletions cogs/send_introduction_reminders.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@

import discord
import emoji
from discord import ui
from discord.ext import tasks
from discord.ui import View
from django.core.exceptions import ValidationError

import utils
Expand Down Expand Up @@ -185,7 +183,7 @@ async def send_introduction_reminders(self) -> None:
)[0],
)

class OptOutIntroductionRemindersView(View):
class OptOutIntroductionRemindersView(discord.ui.View):
"""
A discord.View containing a button to opt-in/out of introduction reminders.

Expand Down Expand Up @@ -227,13 +225,13 @@ async def send_error(
logging_message=logging_message,
)

@ui.button(
@discord.ui.button(
label="Opt-out of introduction reminders",
custom_id="opt_out_introduction_reminders_button",
style=discord.ButtonStyle.red,
emoji=discord.PartialEmoji.from_str(emoji.emojize(":no_good:", language="alias")),
)
async def opt_out_introduction_reminders_button_callback( # type: ignore[misc]
async def opt_out_introduction_reminders_button_callback(
self, button: discord.Button, interaction: discord.Interaction
) -> None:
"""
Expand Down
127 changes: 107 additions & 20 deletions cogs/stats/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Contains cog classes for any stats interactions."""

import math
import re
from typing import TYPE_CHECKING

import discord
Expand Down Expand Up @@ -83,32 +82,64 @@ async def channel_stats(
The "channel_stats" command sends a graph of the stats about messages sent in the given
channel.
"""
# NOTE: Shortcut accessors are placed at the top of the function so that the exceptions they raise are displayed before any further errors may be sent
main_guild: discord.Guild = self.bot.main_guild
if not ctx.channel or not isinstance(
ctx.channel, (discord.TextChannel, discord.DMChannel)
):
await self.command_send_error(
ctx,
message="Channel statistics cannot be sent in this channel.",
)
return

channel_id: int = ctx.channel_id
stats_channel: discord.TextChannel | None = None

if str_channel_id:
if not re.fullmatch(r"\A\d{17,20}\Z", str_channel_id):
if not str_channel_id:
if not isinstance(ctx.channel, discord.TextChannel):
await self.command_send_error(
ctx, message=f"{str_channel_id!r} is not a valid channel ID."
ctx,
message=(
"User did not provide a channel ID and the interaction channel "
"is not a text channel."
),
)
return
stats_channel = ctx.channel

channel_id = int(str_channel_id)
if not stats_channel:
try:
channel_id: int = int(str_channel_id)
except ValueError:
await self.command_send_error(
ctx,
message="The provided channel ID was not a valid integer.",
)
return

channel: discord.TextChannel | None = discord.utils.get(
main_guild.text_channels, id=channel_id
)
if not channel:
await self.command_send_error(
ctx, message=f"Text channel with ID {str(channel_id)!r} does not exist."
)
return
result_channel = ctx.bot.get_channel(channel_id)
if not result_channel:
await self.command_send_error(
ctx,
message="The provided channel ID was not valid or could not be found.",
)
return

if not isinstance(result_channel, discord.TextChannel):
await self.command_send_error(
ctx,
message=(
"The provided channel ID relates to a channel type "
"that is not supported."
),
)
return

stats_channel = result_channel

await ctx.defer(ephemeral=True)

message_counts: Mapping[str, int] = await get_channel_message_counts(channel=channel)
message_counts: Mapping[str, int] = await get_channel_message_counts(
channel=stats_channel
)

if math.ceil(max(message_counts.values()) / 15) < 1:
await self.command_send_error(
Expand All @@ -128,11 +159,11 @@ async def channel_stats(
amount_of_time_formatter(settings["STATISTICS_DAYS"].days, "day")
})"""
),
title=f"Most Active Roles in #{channel.name}",
filename=f"{channel.name}_channel_stats.png",
title=f"Most Active Roles in #{stats_channel.name}",
filename=f"{stats_channel.name}_channel_stats.png",
description=(
"Bar chart of the number of messages "
f"sent by different roles in {channel.mention}."
f"sent by different roles in {stats_channel.mention}."
),
extra_text=(
"Messages sent by members with multiple roles are counted once "
Expand All @@ -157,6 +188,26 @@ async def server_stats(self, ctx: "TeXBotApplicationContext") -> None:
main_guild: discord.Guild = self.bot.main_guild
guest_role: discord.Role = await self.bot.guest_role

if not ctx.channel:
await self.command_send_error(
ctx,
message=(
"Interaction channel was None while attempting to send server stats."
),
)
return

if isinstance(
ctx.channel, (discord.VoiceChannel, discord.ForumChannel, discord.CategoryChannel)
):
await self.command_send_error(
ctx,
message=(
"Server stats cannot be sent in a voice, forum, or category channel."
),
)
return

await ctx.defer(ephemeral=True)

message_counts: Mapping[str, Mapping[str, int]] = await get_server_message_counts(
Expand Down Expand Up @@ -248,6 +299,22 @@ async def user_stats(self, ctx: "TeXBotApplicationContext") -> None:
)
return

if not ctx.channel:
await self.command_send_error(
ctx,
message=("Interaction channel was None while attempting to send user stats."),
)
return

if isinstance(
ctx.channel, (discord.VoiceChannel, discord.ForumChannel, discord.CategoryChannel)
):
await self.command_send_error(
ctx,
message=("User stats cannot be sent in a voice, forum, or category channel."),
)
return

await ctx.defer(ephemeral=True)

message_counts: dict[str, int] = {"Total": 0}
Expand Down Expand Up @@ -314,6 +381,26 @@ async def left_member_stats(self, ctx: "TeXBotApplicationContext") -> None:
# NOTE: Shortcut accessors are placed at the top of the function so that the exceptions they raise are displayed before any further errors may be sent
main_guild: discord.Guild = self.bot.main_guild

if not ctx.channel:
await self.command_send_error(
ctx,
message=(
"Interaction channel was None while attempting to send left member stats."
),
)
return

if isinstance(
ctx.channel, (discord.VoiceChannel, discord.ForumChannel, discord.CategoryChannel)
):
await self.command_send_error(
ctx,
message=(
"Left member stats cannot be sent in a voice, forum, or category channel."
),
)
return

await ctx.defer(ephemeral=True)

left_member_counts: dict[str, int] = {
Expand Down
Loading
Loading