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
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: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ parsedatetime = "==2.6"
pymongo = {extras = ["srv"], version = "*"} # Required by motor
python-dateutil = "==2.8.2"
python-dotenv = "==1.0.0"
uvloop = {version = ">=0.19.0", markers = "sys_platform != 'win32'"}
lottie = {version = "==0.7.0", extras = ["pdf"]}
requests = "==2.31.0"

Expand Down
281 changes: 175 additions & 106 deletions Pipfile.lock

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion bot.py
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
22 changes: 8 additions & 14 deletions core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import typing
from datetime import datetime, timezone
from difflib import get_close_matches
from distutils.util import strtobool as _stb # pylint: disable=import-error
from itertools import takewhile, zip_longest
from urllib import parse

Expand Down Expand Up @@ -56,15 +55,12 @@
def strtobool(val):
if isinstance(val, bool):
return val
try:
return _stb(str(val))
except ValueError:
val = val.lower()
if val == "enable":
return 1
if val == "disable":
return 0
raise
val = str(val).lower()
if val in ("y", "yes", "on", "1", "true", "t", "enable"):
return 1
if val in ("n", "no", "off", "0", "false", "f", "disable"):
return 0
raise ValueError(f"invalid truth value {val}")


class User(commands.MemberConverter):
Expand Down Expand Up @@ -373,7 +369,7 @@ def create_not_found_embed(word, possibilities, name, n=2, cutoff=0.6) -> discor

def parse_alias(alias, *, split=True):
def encode_alias(m):
return "\x1aU" + base64.b64encode(m.group(1).encode()).decode() + "\x1aU"
return "\x1AU" + base64.b64encode(m.group(1).encode()).decode() + "\x1AU"

def decode_alias(m):
return base64.b64decode(m.group(1).encode()).decode()
Expand All @@ -395,7 +391,7 @@ def decode_alias(m):
iterate = [alias]

for a in iterate:
a = re.sub("\x1aU(.+?)\x1aU", decode_alias, a)
a = re.sub(r"\x1AU(.+?)\x1AU", decode_alias, a)
if a[0] == a[-1] == '"':
a = a[1:-1]
aliases.append(a)
Expand Down Expand Up @@ -617,7 +613,6 @@ def return_or_truncate(text, max_length):
class AcceptButton(discord.ui.Button):
def __init__(self, custom_id: str, emoji: str):
super().__init__(style=discord.ButtonStyle.gray, emoji=emoji, custom_id=custom_id)
self.view: ConfirmThreadCreationView

async def callback(self, interaction: discord.Interaction):
self.view.value = True
Expand All @@ -628,7 +623,6 @@ async def callback(self, interaction: discord.Interaction):
class DenyButton(discord.ui.Button):
def __init__(self, custom_id: str, emoji: str):
super().__init__(style=discord.ButtonStyle.gray, emoji=emoji, custom_id=custom_id)
self.view: ConfirmThreadCreationView

async def callback(self, interaction: discord.Interaction):
self.view.value = False
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1,
discord.py[speed]==2.6.3; python_version >= '3.8'
dnspython==2.8.0; python_version >= '3.10'
emoji==2.8.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
frozenlist==1.7.0; python_version >= '3.9'
frozenlist==1.8.0; python_version >= '3.9'
idna==3.10; python_version >= '3.6'
isodate==0.6.1
lottie[pdf]==0.7.0; python_version >= '3'
Expand All @@ -37,6 +37,7 @@ requests==2.31.0; python_version >= '3.7'
six==1.17.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'
tinycss2==1.4.0; python_version >= '3.8'
urllib3==2.5.0; python_version >= '3.9'
uvloop==0.21.0; sys_platform != 'win32'
webencodings==0.5.1
yarl==1.21.0; python_version >= '3.9'
zstandard==0.25.0; python_version >= '3.9'