|
| 1 | +from typing import Optional, cast |
| 2 | + |
| 3 | +from discord import Color, Message, TextChannel, Thread, User |
| 4 | +from discord.ext import commands |
| 5 | +from discord.ext.commands import Bot, Cog, Context |
| 6 | + |
| 7 | +from commanderbot.core.commander_bot_base import CommanderBotBase |
| 8 | +from commanderbot.ext.stacktracer.stacktracer_data import StacktracerData |
| 9 | +from commanderbot.ext.stacktracer.stacktracer_guild_state import StacktracerGuildState |
| 10 | +from commanderbot.ext.stacktracer.stacktracer_json_store import StacktracerJsonStore |
| 11 | +from commanderbot.ext.stacktracer.stacktracer_options import StacktracerOptions |
| 12 | +from commanderbot.ext.stacktracer.stacktracer_state import StacktracerState |
| 13 | +from commanderbot.ext.stacktracer.stacktracer_store import StacktracerStore |
| 14 | +from commanderbot.lib import ( |
| 15 | + CogGuildStateManager, |
| 16 | + EventData, |
| 17 | + GuildContext, |
| 18 | + InMemoryDatabaseOptions, |
| 19 | + JsonFileDatabaseAdapter, |
| 20 | + JsonFileDatabaseOptions, |
| 21 | + UnsupportedDatabaseOptions, |
| 22 | + checks, |
| 23 | +) |
| 24 | + |
| 25 | + |
| 26 | +def _make_store(bot: Bot, cog: Cog, options: StacktracerOptions) -> StacktracerStore: |
| 27 | + db_options = options.database |
| 28 | + if isinstance(db_options, InMemoryDatabaseOptions): |
| 29 | + return StacktracerData() |
| 30 | + if isinstance(db_options, JsonFileDatabaseOptions): |
| 31 | + return StacktracerJsonStore( |
| 32 | + bot=bot, |
| 33 | + cog=cog, |
| 34 | + db=JsonFileDatabaseAdapter( |
| 35 | + options=db_options, |
| 36 | + serializer=lambda cache: cache.to_json(), |
| 37 | + deserializer=StacktracerData.from_data, |
| 38 | + ), |
| 39 | + ) |
| 40 | + raise UnsupportedDatabaseOptions(db_options) |
| 41 | + |
| 42 | + |
| 43 | +class StacktracerCog(Cog, name="commanderbot.ext.stacktracer"): |
| 44 | + """ |
| 45 | + Prints errors and stacktraces to a channel for staff to see. |
| 46 | +
|
| 47 | + Attributes |
| 48 | + ---------- |
| 49 | + bot |
| 50 | + The bot/client instance this cog is attached to. |
| 51 | + options |
| 52 | + Immutable, pre-defined settings that define core cog behaviour. |
| 53 | + store |
| 54 | + Abstracts the data storage and persistence of this cog. |
| 55 | + state |
| 56 | + Encapsulates the state and logic of this cog, for each guild. |
| 57 | + """ |
| 58 | + |
| 59 | + def __init__(self, bot: Bot, **options): |
| 60 | + self.bot: Bot = bot |
| 61 | + self.bot = bot |
| 62 | + self.options = StacktracerOptions.from_data(options) |
| 63 | + self.store: StacktracerStore = _make_store(bot, self, self.options) |
| 64 | + self.state = StacktracerState( |
| 65 | + bot=self.bot, |
| 66 | + cog=self, |
| 67 | + guilds=CogGuildStateManager( |
| 68 | + bot=self.bot, |
| 69 | + cog=self, |
| 70 | + factory=lambda guild: StacktracerGuildState( |
| 71 | + bot=bot, cog=self, guild=guild, store=self.store |
| 72 | + ), |
| 73 | + ), |
| 74 | + store=self.store, |
| 75 | + ) |
| 76 | + |
| 77 | + # Register error handlers with the bot core. |
| 78 | + if isinstance(bot, CommanderBotBase): |
| 79 | + bot.add_event_error_handler(self.handle_event_error) |
| 80 | + bot.add_command_error_handler(self.handle_command_error) |
| 81 | + |
| 82 | + async def handle_event_error( |
| 83 | + self, error: Exception, event_data: EventData, handled: bool |
| 84 | + ) -> Optional[bool]: |
| 85 | + return await self.state.handle_event_error(error, event_data, handled) |
| 86 | + |
| 87 | + async def handle_command_error( |
| 88 | + self, error: Exception, ctx: Context, handled: bool |
| 89 | + ) -> Optional[bool]: |
| 90 | + return await self.state.handle_command_error(error, ctx, handled) |
| 91 | + |
| 92 | + @Cog.listener() |
| 93 | + async def on_message_delete(self, message: Message): |
| 94 | + expected = f"{self.bot.command_prefix}stacktracer test" |
| 95 | + author = cast(User, message.author) |
| 96 | + if (message.content == expected) and await self.bot.is_owner(author): |
| 97 | + raise Exception("Testing the error logging configuration for events.") |
| 98 | + |
| 99 | + # @@ COMMANDS |
| 100 | + |
| 101 | + # @@ stacktracer |
| 102 | + |
| 103 | + @commands.group( |
| 104 | + name="stacktracer", |
| 105 | + brief="Manage error logging globally and across guilds.", |
| 106 | + ) |
| 107 | + @checks.is_guild_admin_or_bot_owner() |
| 108 | + async def cmd_stacktracer(self, ctx: GuildContext): |
| 109 | + if not ctx.invoked_subcommand: |
| 110 | + await ctx.send_help(self.cmd_stacktracer) |
| 111 | + |
| 112 | + @cmd_stacktracer.command( |
| 113 | + name="test", |
| 114 | + brief="Test the error logging configuration for commands.", |
| 115 | + ) |
| 116 | + async def cmd_stacktracer_test(self, ctx: GuildContext): |
| 117 | + raise Exception("Testing the error logging configuration for commands.") |
| 118 | + |
| 119 | + # @@ stacktracer global |
| 120 | + |
| 121 | + @cmd_stacktracer.group( |
| 122 | + name="global", |
| 123 | + brief="Manage global error logging.", |
| 124 | + ) |
| 125 | + @checks.is_owner() |
| 126 | + async def cmd_stacktracer_global(self, ctx: Context): |
| 127 | + if not ctx.invoked_subcommand: |
| 128 | + if ctx.subcommand_passed: |
| 129 | + await ctx.send_help(self.cmd_stacktracer_global) |
| 130 | + else: |
| 131 | + await self.state.show_global_log_options(ctx) |
| 132 | + |
| 133 | + @cmd_stacktracer_global.command( |
| 134 | + name="show", |
| 135 | + brief="Show the global error logging configuration.", |
| 136 | + ) |
| 137 | + async def cmd_stacktracer_global_show(self, ctx: Context): |
| 138 | + await self.state.show_global_log_options(ctx) |
| 139 | + |
| 140 | + @cmd_stacktracer_global.command( |
| 141 | + name="set", |
| 142 | + brief="Set the global error logging configuration.", |
| 143 | + ) |
| 144 | + async def cmd_stacktracer_global_set( |
| 145 | + self, |
| 146 | + ctx: Context, |
| 147 | + channel: TextChannel | Thread, |
| 148 | + stacktrace: Optional[bool], |
| 149 | + emoji: Optional[str], |
| 150 | + color: Optional[Color], |
| 151 | + ): |
| 152 | + await self.state.set_global_log_options( |
| 153 | + ctx, |
| 154 | + channel=channel, |
| 155 | + stacktrace=stacktrace, |
| 156 | + emoji=emoji, |
| 157 | + color=color, |
| 158 | + ) |
| 159 | + |
| 160 | + @cmd_stacktracer_global.command( |
| 161 | + name="remove", |
| 162 | + brief="Remove the global error logging configuration.", |
| 163 | + ) |
| 164 | + async def cmd_stacktracer_global_remove(self, ctx: Context): |
| 165 | + await self.state.remove_global_log_options(ctx) |
| 166 | + |
| 167 | + # @@ stacktracer guild |
| 168 | + |
| 169 | + @cmd_stacktracer.group( |
| 170 | + name="guild", |
| 171 | + brief="Manage error logging for this guild.", |
| 172 | + ) |
| 173 | + @checks.guild_only() |
| 174 | + async def cmd_stacktracer_guild(self, ctx: GuildContext): |
| 175 | + if not ctx.invoked_subcommand: |
| 176 | + if ctx.subcommand_passed: |
| 177 | + await ctx.send_help(self.cmd_stacktracer_guild) |
| 178 | + else: |
| 179 | + await self.state[ctx.guild].show_guild_log_options(ctx) |
| 180 | + |
| 181 | + @cmd_stacktracer_guild.command( |
| 182 | + name="show", |
| 183 | + brief="Show the error logging configuration for this guild.", |
| 184 | + ) |
| 185 | + async def cmd_stacktracer_guild_show(self, ctx: GuildContext): |
| 186 | + await self.state[ctx.guild].show_guild_log_options(ctx) |
| 187 | + |
| 188 | + @cmd_stacktracer_guild.command( |
| 189 | + name="set", |
| 190 | + brief="Set the error logging configuration for this guild.", |
| 191 | + ) |
| 192 | + async def cmd_stacktracer_guild_set( |
| 193 | + self, |
| 194 | + ctx: GuildContext, |
| 195 | + channel: TextChannel | Thread, |
| 196 | + stacktrace: Optional[bool], |
| 197 | + emoji: Optional[str], |
| 198 | + color: Optional[Color], |
| 199 | + ): |
| 200 | + await self.state[ctx.guild].set_guild_log_options( |
| 201 | + ctx, |
| 202 | + channel=channel, |
| 203 | + stacktrace=stacktrace, |
| 204 | + emoji=emoji, |
| 205 | + color=color, |
| 206 | + ) |
| 207 | + |
| 208 | + @cmd_stacktracer_guild.command( |
| 209 | + name="remove", |
| 210 | + brief="Remove the error logging configuration for this guild.", |
| 211 | + ) |
| 212 | + async def cmd_stacktracer_guild_remove(self, ctx: GuildContext): |
| 213 | + await self.state[ctx.guild].remove_guild_log_options(ctx) |
0 commit comments