Skip to content

Commit 42070be

Browse files
committed
fix: ignore typing failures
Make Modmail keep working when typing is disabled/outage
1 parent 4c7b601 commit 42070be

File tree

7 files changed

+67
-18
lines changed

7 files changed

+67
-18
lines changed

bot.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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 & 1 deletion
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,10 +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()
463+
# Fire and forget typing; do not block on failures
464+
async with safe_typing(ctx):
465+
pass
432466
return await func(self, ctx, *args, **kwargs)
433467

434468
return wrapper

diff-summary.txt

Whitespace-only changes.

0 commit comments

Comments
 (0)