Skip to content
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s
# v4.2.0

Upgraded discord.py to version 2.6.3, added support for CV2.
Forwarded messages now properly show in threads, rather then showing as an empty embed.

### Fixed
- Make Modmail keep working when typing is disabled due to a outage caused by Discord.
Expand All @@ -18,6 +19,8 @@ Upgraded discord.py to version 2.6.3, added support for CV2.
- Eliminated duplicate logs and notes.
- Addressed inconsistent use of `logkey` after ticket restoration.
- Fixed issues with identifying the user who sent internal messages.
- Solved an ancient bug where closing with words like `evening` wouldnt work.
- Fixed the command from being included in the reply in rare conditions.

### Added
Commands:
Expand Down
1 change: 0 additions & 1 deletion bot.py
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!

Original file line number Diff line number Diff line change
Expand Up @@ -2035,4 +2035,3 @@ def main():

if __name__ == "__main__":
main()

56 changes: 50 additions & 6 deletions cogs/utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -1363,7 +1363,18 @@ async def permissions_add(
key = self.bot.modmail_guild.get_member(value)
if key is not None:
logger.info("Granting %s access to Modmail category.", key.name)
await self.bot.main_category.set_permissions(key, read_messages=True)
try:
await self.bot.main_category.set_permissions(key, read_messages=True)
except discord.Forbidden:
warn = discord.Embed(
title="Missing Permissions",
color=self.bot.error_color,
description=(
"I couldn't update the Modmail category permissions. "
"Please grant me 'Manage Channels' and 'Manage Roles' for this category."
),
)
await ctx.send(embed=warn)

embed = discord.Embed(
title="Success",
Expand Down Expand Up @@ -1454,17 +1465,50 @@ async def permissions_remove(
if level > PermissionLevel.REGULAR:
if value == -1:
logger.info("Denying @everyone access to Modmail category.")
await self.bot.main_category.set_permissions(
self.bot.modmail_guild.default_role, read_messages=False
)
try:
await self.bot.main_category.set_permissions(
self.bot.modmail_guild.default_role, read_messages=False
)
except discord.Forbidden:
warn = discord.Embed(
title="Missing Permissions",
color=self.bot.error_color,
description=(
"I couldn't update the Modmail category permissions. "
"Please grant me 'Manage Channels' and 'Manage Roles' for this category."
),
)
await ctx.send(embed=warn)
elif isinstance(user_or_role, discord.Role):
logger.info("Denying %s access to Modmail category.", user_or_role.name)
await self.bot.main_category.set_permissions(user_or_role, overwrite=None)
try:
await self.bot.main_category.set_permissions(user_or_role, overwrite=None)
except discord.Forbidden:
warn = discord.Embed(
title="Missing Permissions",
color=self.bot.error_color,
description=(
"I couldn't update the Modmail category permissions. "
"Please grant me 'Manage Channels' and 'Manage Roles' for this category."
),
)
await ctx.send(embed=warn)
else:
member = self.bot.modmail_guild.get_member(value)
if member is not None and member != self.bot.modmail_guild.me:
logger.info("Denying %s access to Modmail category.", member.name)
await self.bot.main_category.set_permissions(member, overwrite=None)
try:
await self.bot.main_category.set_permissions(member, overwrite=None)
except discord.Forbidden:
warn = discord.Embed(
title="Missing Permissions",
color=self.bot.error_color,
description=(
"I couldn't update the Modmail category permissions. "
"Please grant me 'Manage Channels' and 'Manage Roles' for this category."
),
)
await ctx.send(embed=warn)

embed = discord.Embed(
title="Success",
Expand Down
17 changes: 14 additions & 3 deletions core/paginator.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,20 @@ async def run(self) -> typing.Optional[Message]:
if not self.running:
await self.show_page(self.current)

if self.view is not None:
await self.view.wait()

# Don't block command execution while waiting for the View timeout.
# Schedule the wait-and-close sequence in the background so the command
# returns immediately (prevents typing indicator from hanging).
if self.view is not None:

async def _wait_and_close():
try:
await self.view.wait()
finally:
await self.close(delete=False)

# Fire and forget
self.ctx.bot.loop.create_task(_wait_and_close())
else:
await self.close(delete=False)

async def close(
Expand Down
63 changes: 62 additions & 1 deletion core/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,30 @@ def __init__(self, dt: datetime.datetime, now: datetime.datetime = None):
async def ensure_constraints(
self, ctx: Context, uft: UserFriendlyTime, now: datetime.datetime, remaining: str
) -> None:
# Strip stray connector words like "in", "to", or "at" that may
# remain when the natural language parser isolates the time token
# positioned at the end (e.g. "in 10m" leaves "in" before the token).
if isinstance(remaining, str):
cleaned = remaining.strip(" ,.!")
stray_tokens = {
"in",
"to",
"at",
"me",
# also treat vague times of day as stray tokens when they are the only leftover word
"evening",
"night",
"midnight",
"morning",
"afternoon",
"tonight",
"noon",
"today",
"tomorrow",
}
if cleaned.lower() in stray_tokens:
remaining = ""

if self.dt < now:
raise commands.BadArgument("This time is in the past.")

Expand Down Expand Up @@ -199,6 +223,26 @@ async def convert(self, ctx: Context, argument: str, *, now=None) -> FriendlyTim
if now is None:
now = ctx.message.created_at

# Heuristic: If the user provides only certain single words that are commonly
# used as salutations or vague times of day, interpret them as a message
# rather than a schedule. This avoids accidental scheduling when the intent
# is a short message (e.g. '?close evening'). Explicit scheduling still works
# via 'in 2h', '2m30s', 'at 8pm', etc.
if argument.strip().lower() in {
"evening",
"night",
"midnight",
"morning",
"afternoon",
"tonight",
"noon",
"today",
"tomorrow",
}:
result = FriendlyTimeResult(now)
await result.ensure_constraints(ctx, self, now, argument)
return result

match = regex.match(argument)
if match is not None and match.group(0):
data = {k: int(v) for k, v in match.groupdict(default=0).items()}
Expand Down Expand Up @@ -245,7 +289,10 @@ async def convert(self, ctx: Context, argument: str, *, now=None) -> FriendlyTim
if not status.hasDateOrTime:
raise commands.BadArgument('Invalid time provided, try e.g. "tomorrow" or "3 days".')

if begin not in (0, 1) and end != len(argument):
# If the parsed time token is embedded in the text but only followed by
# trailing punctuation/whitespace, treat it as if it's positioned at the end.
trailing = argument[end:].strip(" ,.!")
if begin not in (0, 1) and trailing != "":
raise commands.BadArgument(
"Time is either in an inappropriate location, which "
"must be either at the end or beginning of your input, "
Expand All @@ -260,6 +307,20 @@ async def convert(self, ctx: Context, argument: str, *, now=None) -> FriendlyTim
if status.accuracy == pdt.pdtContext.ACU_HALFDAY:
dt = dt.replace(day=now.day + 1)

# Heuristic: If the matched time string is a vague time-of-day (e.g.,
# 'evening', 'morning', 'afternoon', 'night') and there's additional
# non-punctuation text besides that token, assume the user intended a
# closing message rather than scheduling. This avoids cases like
# '?close Have a good evening!' being treated as a scheduled close.
vague_tod = {"evening", "morning", "afternoon", "night"}
matched_text = dt_string.strip().strip('"').rstrip(" ,.!").lower()
pre_text = argument[:begin].strip(" ,.!")
post_text = argument[end:].strip(" ,.!")
if matched_text in vague_tod and (pre_text or post_text):
result = FriendlyTimeResult(now)
await result.ensure_constraints(ctx, self, now, argument)
return result

result = FriendlyTimeResult(dt.replace(tzinfo=datetime.timezone.utc), now)
remaining = ""

Expand Down