• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

discord-modmail / modmail / 11041384318

25 Sep 2024 09:27PM UTC coverage: 71.46% (-1.5%) from 72.981%
11041384318

Pull #138

github

web-flow
Merge aafb3e91a into df5dbb94a
Pull Request #138: update lint config

569 of 1096 branches covered (51.92%)

4 of 4 new or added lines in 4 files covered. (100.0%)

55 existing lines in 3 files now uncovered.

2559 of 3581 relevant lines covered (71.46%)

0.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.5
/modmail/extensions/utils/error_handler.py
1
import logging
1✔
2
import re
1✔
3
import typing
1✔
4

5
import discord
1✔
6
import discord.errors
1✔
7
from discord.ext import commands
1✔
8

9
from modmail.bot import ModmailBot
1✔
10
from modmail.log import ModmailLogger
1✔
11
from modmail.utils import responses
1✔
12
from modmail.utils.cogs import BotModes, ExtMetadata, ModmailCog
1✔
13
from modmail.utils.extensions import BOT_MODE
1✔
14

15

16
logger: ModmailLogger = logging.getLogger(__name__)
1✔
17

18
EXT_METADATA = ExtMetadata()
1✔
19

20
ERROR_COLOUR = responses.DEFAULT_FAILURE_COLOUR
1✔
21

22
ERROR_TITLE_REGEX = re.compile(r"((?<=[a-z])[A-Z]|(?<=[a-zA-Z])[A-Z](?=[a-z]))")
1✔
23

24
ANY_DEV_MODE = BOT_MODE & (BotModes.DEVELOP.value + BotModes.PLUGIN_DEV.value)
1✔
25

26
MAYBE_DM_ON_PERM_ERROR = True
1✔
27

28

29
class ErrorHandler(ModmailCog, name="Error Handler"):
1✔
30
    """Handles all errors across the bot."""
31

32
    def __init__(self, bot: ModmailBot):
1✔
33
        self.bot = bot
1✔
34

35
    @staticmethod
1✔
36
    def error_embed(title: str, message: str) -> discord.Embed:
1✔
37
        """Create an error embed with an error colour and reason and return it."""
38
        return discord.Embed(title=title, description=message, colour=ERROR_COLOUR)
1✔
39

40
    @staticmethod
1✔
41
    def get_title_from_name(error: typing.Union[Exception, str]) -> str:
1✔
42
        """
43
        Return a message dervived from the exception class name.
44

45
        Eg NSFWChannelRequired returns NSFW Channel Required
46
        """
47
        if not isinstance(error, str):
1✔
48
            error = type(error).__name__
1✔
49
        return re.sub(ERROR_TITLE_REGEX, r" \1", error)
1✔
50

51
    @staticmethod
1✔
52
    def _reset_command_cooldown(ctx: commands.Context) -> bool:
1✔
53
        if return_value := ctx.command.is_on_cooldown(ctx):
1✔
54
            ctx.command.reset_cooldown(ctx)
1✔
55
        return return_value
1✔
56

57
    async def handle_user_input_error(
1✔
58
        self,
59
        ctx: commands.Context,
60
        error: commands.UserInputError,
61
        reset_cooldown: bool = True,
62
    ) -> discord.Embed:
63
        """Handling deferred from main error handler to handle UserInputErrors."""
64
        if reset_cooldown:
1✔
65
            self._reset_command_cooldown(ctx)
1✔
66
        msg = None
1✔
67
        if isinstance(error, commands.BadUnionArgument):
1✔
68
            msg = self.get_title_from_name(str(error))
1✔
69
        title = self.get_title_from_name(error)
1✔
70
        return self.error_embed(title, msg or str(error))
1✔
71

72
    async def handle_bot_missing_perms(
1✔
73
        self, ctx: commands.Context, error: commands.BotMissingPermissions
74
    ) -> bool:
75
        """Handles bot missing permissions by dming the user if they have a permission which may be able to fix this."""  # noqa: E501
76
        embed = self.error_embed("Permissions Failure", str(error))
1✔
77
        bot_perms = ctx.channel.permissions_for(ctx.me)
1✔
78
        if bot_perms >= discord.Permissions(send_messages=True, embed_links=True):
1✔
79
            await ctx.send(embeds=[embed])
1✔
80
            return True
1✔
81
        elif bot_perms >= discord.Permissions(send_messages=True):
1✔
82
            # make a message as similar to the embed, using as few permissions as possible
83
            # this is the only place we send a standard message instead of an embed
84
            # so no helper methods are necessary
85
            await ctx.send(
1✔
86
                "**Permissions Failure**\n\n"
87
                "I am missing the permissions required to properly execute your command."
88
            )
89
            # intentionally skipping setting responded to True, since we want to attempt to dm the user
90
            logger.warning(
1✔
91
                f"Missing partial required permissions for {ctx.channel}. "
92
                "I am able to send messages, but not embeds."
93
            )
94
        else:
95
            logger.error(f"Unable to send an error message to channel {ctx.channel}")
1✔
96

97
        if MAYBE_DM_ON_PERM_ERROR or ANY_DEV_MODE:
1!
98
            # non-general permissions
99
            perms = discord.Permissions(
1✔
100
                administrator=True,
101
                manage_channels=True,
102
                manage_roles=True,
103
                manage_threads=True,
104
            )
105
            if perms.value & ctx.channel.permissions_for(ctx.author).value:
1✔
106
                logger.info(
1✔
107
                    f"Attempting to dm {ctx.author} since they have a permission which may be able "
108
                    "to give the bot send message permissions."
109
                )
110
                try:
1✔
111
                    await ctx.author.send(embeds=[embed])
1✔
112
                except discord.Forbidden:
1✔
113
                    logger.notice("Also encountered an error when trying to reply in dms.")
1✔
114
                    return False
1✔
115
                else:
116
                    return True
1✔
117

118
    async def handle_check_failure(
1✔
119
        self, ctx: commands.Context, error: commands.CheckFailure
120
    ) -> typing.Optional[discord.Embed]:
121
        """Handle CheckFailures seperately given that there are many of them."""
122
        title = "Check Failure"
1✔
123
        if isinstance(error, commands.CheckAnyFailure):
1✔
124
            title = self.get_title_from_name(error.checks[-1])
1✔
125
        elif isinstance(error, commands.PrivateMessageOnly):
1✔
126
            title = "DMs Only"
1✔
127
        elif isinstance(error, commands.NoPrivateMessage):
1✔
128
            title = "Server Only"
1✔
129
        elif isinstance(error, commands.BotMissingPermissions):
1✔
130
            # defer handling BotMissingPermissions to a method
131
            # the error could be that the bot is unable to send messages, which would cause
132
            # the error handling to fail
133
            await self.handle_bot_missing_perms(ctx, error)
1✔
134
            return None
1✔
135
        else:
136
            title = self.get_title_from_name(error)
1✔
137
        embed = self.error_embed(title, str(error))
1✔
138
        return embed
1✔
139

140
    async def handle_command_invoke_error(
1✔
141
        self, ctx: commands.Context, error: commands.CommandInvokeError
142
    ) -> typing.Optional[discord.Embed]:
143
        """Formulate an embed for a generic error handler."""
144
        if isinstance(error.original, discord.Forbidden):
1!
145
            logger.warn(f"Permissions error occurred in {ctx.command}.")
1✔
146
            await self.handle_bot_missing_perms(ctx, error.original)
1✔
147
            return None
1✔
148

149
        # todo: this should properly handle plugin errors and note that they are not bot bugs
150
        # todo: this should log somewhere else since this is a bot bug.
151
        # generic error
UNCOV
152
        logger.error(f'Error occurred in command "{ctx.command}".', exc_info=error.original)
×
UNCOV
153
        if ctx.command.cog.__module__.startswith("modmail.plugins"):
×
154
            # plugin msg
UNCOV
155
            title = "Plugin Internal Error Occurred"
×
UNCOV
156
            msg = (
×
157
                "Something went wrong internally in the plugin contributed command you were trying "
158
                "to execute. Please report this error and what you were trying to do to the "
159
                "respective plugin developers.\n\n**PLEASE NOTE**: Modmail developers will not help "
160
                "you with this issue and will refer you to the plugin developers."
161
            )
162
        else:
163
            # built in command msg
UNCOV
164
            title = "Internal Error"
×
UNCOV
165
            msg = (
×
166
                "Something went wrong internally in the command you were trying to execute. "
167
                "Please report this error and what you were trying to do to the bot developers."
168
            )
UNCOV
169
        logger.debug(ctx.command.callback.__module__)
×
UNCOV
170
        return self.error_embed(title, msg)
×
171

172
    @ModmailCog.listener()
1✔
173
    async def on_command_error(self, ctx: commands.Context, error: commands.CommandError) -> None:
1✔
174
        """Activates when a command raises an error."""
175
        if getattr(error, "handled", False):
1✔
176
            logging.debug(f"Command {ctx.command} had its error already handled locally, ignoring.")
1✔
177
            return
1✔
178

179
        if isinstance(error, commands.CommandNotFound):
1✔
180
            # ignore every time the user inputs a message that starts with our prefix but isn't a command
181
            # this will be modified in the future to support prefilled commands
182
            if ANY_DEV_MODE:
1!
183
                logger.trace(error)
1✔
184
            return
1✔
185

186
        logger.trace(error)
1✔
187

188
        embed: typing.Optional[discord.Embed] = None
1✔
189
        should_respond = True
1✔
190

191
        if isinstance(error, commands.UserInputError):
1✔
192
            embed = await self.handle_user_input_error(ctx, error)
1✔
193
        elif isinstance(error, commands.CheckFailure):
1✔
194
            embed = await self.handle_check_failure(ctx, error)
1✔
195
            # handle_check_failure may send its own error if its a BotMissingPermissions error.
196
            if embed is None:
1✔
197
                should_respond = False
1✔
198
        elif isinstance(error, commands.ConversionError):
1✔
199
            pass
1✔
200
        elif isinstance(error, commands.DisabledCommand):
1!
UNCOV
201
            logger.debug("")
×
UNCOV
202
            if ctx.command.hidden:
×
UNCOV
203
                should_respond = False
×
204
            else:
UNCOV
205
                msg = f"Command `{ctx.invoked_with}` is disabled."
×
UNCOV
206
                if reason := ctx.command.extras.get("disabled_reason", None):
×
UNCOV
207
                    msg += f"\nReason: {reason}"
×
UNCOV
208
                embed = self.error_embed("Command Disabled", msg)
×
209

210
        elif isinstance(error, commands.CommandInvokeError):
1!
211
            embed = await self.handle_command_invoke_error(ctx, error)
1✔
212
            if embed is None:
1✔
213
                should_respond = False
1✔
214

215
        # TODO: this has a fundamental problem with any BotMissingPermissions error
216
        # if the issue is the bot does not have permissions to send embeds or send messages...
217
        # yeah, problematic.
218

219
        if not should_respond:
1✔
220
            logger.debug(
1✔
221
                "Not responding to error since should_respond is falsey because either "
222
                "the embed has already been sent or belongs to a hidden command and thus should be hidden."
223
            )
224
            return
1✔
225

226
        if embed is None:
1✔
227
            embed = self.error_embed(self.get_title_from_name(error), str(error))
1✔
228

229
        await ctx.send(embeds=[embed])
1✔
230

231

232
def setup(bot: ModmailBot) -> None:
1✔
233
    """Add the error handler to the bot."""
234
    bot.add_cog(ErrorHandler(bot))
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc