Skip to content

Commit eebcd7a

Browse files
authored
fix: ignore typing failures (#3389)
* fix: ignore typing failures Make Modmail keep working when typing is disabled/outage * fix: only surpress failures * chore: sync local edits before push
1 parent 4c7b601 commit eebcd7a

File tree

8 files changed

+100
-21
lines changed

8 files changed

+100
-21
lines changed

CHANGELOG.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,37 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
66
This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html);
77
however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/modmail-dev/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section.
88

9+
# v4.2.0
10+
11+
Upgraded discord.py to version 2.6.3, added support for CV2.
12+
13+
### Fixed
14+
- Make Modmail keep working when typing is disabled due to a outage caused by Discord.
15+
- Resolved an issue where forwarded messages appeared as empty embeds.
16+
- Fixed internal message handling and restoration processes.
17+
- Corrected a bug in the unsnooze functionality.
18+
- Eliminated duplicate logs and notes.
19+
- Addressed inconsistent use of `logkey` after ticket restoration.
20+
- Fixed issues with identifying the user who sent internal messages.
21+
22+
### Added
23+
Commands:
24+
* `snooze`: Initiates a snooze action.
25+
* `snoozed`: Displays snoozed items.
26+
* `unsnooze`: Reverses the snooze action.
27+
* `clearsnoozed`: Clears all snoozed items.
28+
29+
Configuration Options:
30+
* `max_snooze_time`: Sets the maximum duration for snooze.
31+
* `snooze_title`: Customizes the title for snooze notifications.
32+
* `snooze_text`: Customizes the text for snooze notifications.
33+
* `unsnooze_text`: Customizes the text for unsnooze notifications.
34+
* `unsnooze_notify_channel`: Specifies the channel for unsnooze notifications.
35+
* `thread_min_characters`: Minimum number of characters required.
36+
* `thread_min_characters_title`: Title shown when the message is too short.
37+
* `thread_min_characters_response`: Response shown to the user if their message is too short.
38+
* `thread_min_characters_footer`: Footer displaying the minimum required characters.
39+
940
# v4.1.2
1041

1142
### Fixed

bot.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "4.1.2"
1+
__version__ = "4.2.0"
22

33

44
import asyncio
@@ -1403,7 +1403,10 @@ async def on_typing(self, channel, user, _):
14031403
thread = await self.threads.find(recipient=user)
14041404

14051405
if thread:
1406-
await thread.channel.typing()
1406+
try:
1407+
await thread.channel.typing()
1408+
except Exception:
1409+
pass
14071410
else:
14081411
if not self.config.get("mod_typing"):
14091412
return
@@ -1413,7 +1416,10 @@ async def on_typing(self, channel, user, _):
14131416
for user in thread.recipients:
14141417
if await self.is_blocked(user):
14151418
continue
1416-
await user.typing()
1419+
try:
1420+
await user.typing()
1421+
except Exception:
1422+
pass
14171423

14181424
async def handle_reaction_events(self, payload):
14191425
user = self.get_user(payload.user_id)
@@ -1720,7 +1726,10 @@ async def on_command_error(
17201726
return
17211727

17221728
if isinstance(exception, (commands.BadArgument, commands.BadUnionArgument)):
1723-
await context.typing()
1729+
try:
1730+
await context.typing()
1731+
except Exception:
1732+
pass
17241733
await context.send(embed=discord.Embed(color=self.error_color, description=str(exception)))
17251734
elif isinstance(exception, commands.CommandNotFound):
17261735
logger.warning("CommandNotFound: %s", exception)

cogs/modmail.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,7 +1210,8 @@ async def logs(self, ctx, *, user: User = None):
12101210
`user` may be a user ID, mention, or name.
12111211
"""
12121212

1213-
await ctx.typing()
1213+
async with safe_typing(ctx):
1214+
pass
12141215

12151216
if not user:
12161217
thread = ctx.thread
@@ -1342,7 +1343,8 @@ async def logs_search(self, ctx, limit: Optional[int] = None, *, query):
13421343
Provide a `limit` to specify the maximum number of logs the bot should find.
13431344
"""
13441345

1345-
await ctx.typing()
1346+
async with safe_typing(ctx):
1347+
pass
13461348

13471349
entries = await self.bot.api.search_by_text(query, limit)
13481350

@@ -1371,7 +1373,7 @@ async def reply(self, ctx, *, msg: str = ""):
13711373

13721374
ctx.message.content = msg
13731375

1374-
async with ctx.typing():
1376+
async with safe_typing(ctx):
13751377
await ctx.thread.reply(ctx.message)
13761378

13771379
@commands.command(aliases=["formatreply"])
@@ -1393,7 +1395,7 @@ async def freply(self, ctx, *, msg: str = ""):
13931395
msg, channel=ctx.channel, recipient=ctx.thread.recipient, author=ctx.message.author
13941396
)
13951397
ctx.message.content = msg
1396-
async with ctx.typing():
1398+
async with safe_typing(ctx):
13971399
await ctx.thread.reply(ctx.message)
13981400

13991401
@commands.command(aliases=["formatanonreply"])
@@ -1415,7 +1417,7 @@ async def fareply(self, ctx, *, msg: str = ""):
14151417
msg, channel=ctx.channel, recipient=ctx.thread.recipient, author=ctx.message.author
14161418
)
14171419
ctx.message.content = msg
1418-
async with ctx.typing():
1420+
async with safe_typing(ctx):
14191421
await ctx.thread.reply(ctx.message, anonymous=True)
14201422

14211423
@commands.command(aliases=["formatplainreply"])
@@ -1437,7 +1439,7 @@ async def fpreply(self, ctx, *, msg: str = ""):
14371439
msg, channel=ctx.channel, recipient=ctx.thread.recipient, author=ctx.message.author
14381440
)
14391441
ctx.message.content = msg
1440-
async with ctx.typing():
1442+
async with safe_typing(ctx):
14411443
await ctx.thread.reply(ctx.message, plain=True)
14421444

14431445
@commands.command(aliases=["formatplainanonreply"])
@@ -1459,7 +1461,7 @@ async def fpareply(self, ctx, *, msg: str = ""):
14591461
msg, channel=ctx.channel, recipient=ctx.thread.recipient, author=ctx.message.author
14601462
)
14611463
ctx.message.content = msg
1462-
async with ctx.typing():
1464+
async with safe_typing(ctx):
14631465
await ctx.thread.reply(ctx.message, anonymous=True, plain=True)
14641466

14651467
@commands.command(aliases=["anonreply", "anonymousreply"])
@@ -1476,7 +1478,7 @@ async def areply(self, ctx, *, msg: str = ""):
14761478
and `anon_tag` config variables to do so.
14771479
"""
14781480
ctx.message.content = msg
1479-
async with ctx.typing():
1481+
async with safe_typing(ctx):
14801482
await ctx.thread.reply(ctx.message, anonymous=True)
14811483

14821484
@commands.command(aliases=["plainreply"])
@@ -1490,7 +1492,7 @@ async def preply(self, ctx, *, msg: str = ""):
14901492
automatically embedding image URLs.
14911493
"""
14921494
ctx.message.content = msg
1493-
async with ctx.typing():
1495+
async with safe_typing(ctx):
14941496
await ctx.thread.reply(ctx.message, plain=True)
14951497

14961498
@commands.command(aliases=["plainanonreply", "plainanonymousreply"])
@@ -1504,7 +1506,7 @@ async def pareply(self, ctx, *, msg: str = ""):
15041506
automatically embedding image URLs.
15051507
"""
15061508
ctx.message.content = msg
1507-
async with ctx.typing():
1509+
async with safe_typing(ctx):
15081510
await ctx.thread.reply(ctx.message, anonymous=True, plain=True)
15091511

15101512
@commands.group(invoke_without_command=True)
@@ -1517,7 +1519,7 @@ async def note(self, ctx, *, msg: str = ""):
15171519
Useful for noting context.
15181520
"""
15191521
ctx.message.content = msg
1520-
async with ctx.typing():
1522+
async with safe_typing(ctx):
15211523
msg = await ctx.thread.note(ctx.message)
15221524
await msg.pin()
15231525

@@ -1529,7 +1531,7 @@ async def note_persistent(self, ctx, *, msg: str = ""):
15291531
Take a persistent note about the current user.
15301532
"""
15311533
ctx.message.content = msg
1532-
async with ctx.typing():
1534+
async with safe_typing(ctx):
15331535
msg = await ctx.thread.note(ctx.message, persistent=True)
15341536
await msg.pin()
15351537
await self.bot.api.create_note(recipient=ctx.thread.recipient, message=ctx.message, message_id=msg.id)

cogs/plugins.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from core import checks
2121
from core.models import PermissionLevel, getLogger
2222
from core.paginator import EmbedPaginatorSession
23-
from core.utils import trigger_typing, truncate
23+
from core.utils import trigger_typing, truncate, safe_typing
2424

2525
logger = getLogger(__name__)
2626

@@ -484,7 +484,7 @@ async def update_plugin(self, ctx, plugin_name):
484484
embed = discord.Embed(description="Plugin is not installed.", color=self.bot.error_color)
485485
return await ctx.send(embed=embed)
486486

487-
async with ctx.typing():
487+
async with safe_typing(ctx):
488488
embed = discord.Embed(
489489
description=f"Successfully updated {plugin.name}.", color=self.bot.main_color
490490
)

cogs/utility.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from core.utils import trigger_typing, truncate, safe_typing
12
import asyncio
23
import inspect
34
import os

core/thread.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1457,11 +1457,14 @@ def lottie_to_png(data):
14571457
):
14581458
logger.info("Sending a message to %s when DM disabled is set.", self.recipient)
14591459

1460+
# Best-effort typing: never block message delivery if typing fails
14601461
try:
14611462
await destination.typing()
14621463
except discord.NotFound:
14631464
logger.warning("Channel not found.")
14641465
raise
1466+
except (discord.Forbidden, discord.HTTPException, Exception) as e:
1467+
logger.warning("Unable to send typing to %s: %s. Continuing without typing.", destination, e)
14651468

14661469
if not from_mod and not note:
14671470
mentions = await self.get_notifications()

core/utils.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import base64
22
import functools
3+
import contextlib
34
import re
45
import typing
56
from datetime import datetime, timezone
@@ -34,6 +35,7 @@
3435
"normalize_alias",
3536
"format_description",
3637
"trigger_typing",
38+
"safe_typing",
3739
"escape_code_block",
3840
"tryint",
3941
"get_top_role",
@@ -425,11 +427,42 @@ def format_description(i, names):
425427
)
426428

427429

430+
class _SafeTyping:
431+
"""Best-effort typing context manager.
432+
433+
Suppresses errors from Discord's typing endpoint so core flows continue
434+
when typing is disabled or experiencing outages.
435+
"""
436+
437+
def __init__(self, target):
438+
# target can be a Context or any Messageable (channel/DM/user)
439+
self._target = target
440+
self._cm = None
441+
442+
async def __aenter__(self):
443+
try:
444+
self._cm = self._target.typing()
445+
return await self._cm.__aenter__()
446+
except Exception:
447+
# typing is best-effort; ignore any failure
448+
self._cm = None
449+
450+
async def __aexit__(self, exc_type, exc, tb):
451+
if self._cm is not None:
452+
with contextlib.suppress(Exception):
453+
return await self._cm.__aexit__(exc_type, exc, tb)
454+
455+
456+
def safe_typing(target):
457+
return _SafeTyping(target)
458+
459+
428460
def trigger_typing(func):
429461
@functools.wraps(func)
430462
async def wrapper(self, ctx: commands.Context, *args, **kwargs):
431-
await ctx.typing()
432-
return await func(self, ctx, *args, **kwargs)
463+
# Keep typing active for the duration of the command; suppress failures
464+
async with safe_typing(ctx):
465+
return await func(self, ctx, *args, **kwargs)
433466

434467
return wrapper
435468

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ extend-exclude = '''
2121

2222
[tool.poetry]
2323
name = 'Modmail'
24-
version = '4.1.2'
24+
version = '4.2.0'
2525
description = "Modmail is similar to Reddit's Modmail, both in functionality and purpose. It serves as a shared inbox for server staff to communicate with their users in a seamless way."
2626
license = 'AGPL-3.0-only'
2727
authors = [

0 commit comments

Comments
 (0)