Edit on GitHub

tickets_plus.cogs.override

General powertools for the bot owner.

This module contains the override cog, which contains commands that are only available to the bot owner. This includes commands to reload cogs, restart the bot, and pull from git. The last command is to be used very carefully, as changes to the database schema will require a database migration.

Typical usage example:
from tickets_plus import bot
bot_instance = bot.TicketsPlusBot(...)
await bot_instance.load_extension("tickets_plus.cogs.override")
  1"""General powertools for the bot owner.
  2
  3This module contains the override cog, which contains commands that are only
  4available to the bot owner.
  5This includes commands to reload cogs, restart the bot, and pull from git.
  6The last command is to be used very carefully, as changes to the database
  7schema will require a database migration.
  8
  9Typical usage example:
 10    ```py
 11    from tickets_plus import bot
 12    bot_instance = bot.TicketsPlusBot(...)
 13    await bot_instance.load_extension("tickets_plus.cogs.override")
 14    ```
 15"""
 16# License: EPL-2.0
 17# SPDX-License-Identifier: EPL-2.0
 18# Copyright (c) 2021-present The Tickets+ Contributors
 19# This Source Code may also be made available under the following
 20# Secondary Licenses when the conditions for such availability set forth
 21# in the Eclipse Public License, v. 2.0 are satisfied: GPL-3.0-only OR
 22# If later approved by the Initial Contributor, GPL-3.0-or-later.
 23
 24import asyncio
 25import logging
 26import os
 27
 28import discord
 29from discord import app_commands
 30from discord.ext import commands
 31
 32from tickets_plus import bot, cogs
 33from tickets_plus.database import config, const
 34from tickets_plus.ext import checks, views
 35
 36_CNFG = config.MiniConfig()
 37"""Submodule private global constant for the config."""
 38
 39
 40@app_commands.guilds(_CNFG.getitem("dev_guild_id"))
 41class Overrides(commands.GroupCog, name="override", description="Owner override commands."):
 42    """Owner override commands.
 43
 44    This class contains commands that are only available to the bot owner.
 45    These commands are used to reload cogs, restart the bot, and pull from git.
 46    The commands are only available in the development guild, as specified in
 47    the config.
 48    """
 49
 50    def __init__(self, bot_instance: bot.TicketsPlusBot):
 51        """Initializes the cog.
 52
 53        This method initializes the cog.
 54        It also sets the bot instance as a private attribute.
 55        And finally initializes the superclass.
 56
 57        Args:
 58            bot_instance: The bot instance.
 59        """
 60        self._bt = bot_instance
 61        super().__init__()
 62        logging.info("Loaded %s", self.__class__.__name__)
 63
 64    @app_commands.command(name="reload", description="Reloads the bot's cogs.")
 65    @checks.is_owner_check()
 66    @app_commands.describe(sync="Syncs the tree after reloading cogs.")
 67    async def reload(self, ctx: discord.Interaction, sync: bool = False) -> None:
 68        """Reloads the bot's cogs.
 69
 70        This command reloads all cogs in the EXTENSIONS list.
 71        Reloads are atomic, so if one fails, it rolls back.
 72        We can just import this submodule and iterate over the EXTENSIONS list.
 73        You can also sync the tree after reloading cogs. Though this is not
 74        to be used very often, as it has low rate limits.
 75
 76        Args:
 77            ctx: The interaction context.
 78            sync: Whether to sync the tree after reloading cogs.
 79        """
 80        await ctx.response.send_message("Reloading cogs...")
 81        logging.info("Reloading cogs...")
 82        for extension in cogs.EXTENSIONS:
 83            await self._bt.reload_extension(extension)
 84        await ctx.followup.send("Reloaded cogs.")
 85        logging.info("Finished reloading cogs.")
 86        if sync:
 87            await self._bt.tree.sync()
 88            dev_guild = self._bt.get_guild(_CNFG.getitem("dev_guild_id"))
 89            await self._bt.tree.sync(guild=dev_guild)
 90            logging.info("Finished syncing tree.")
 91
 92    @app_commands.command(name="close", description="Closes the bot.")
 93    @checks.is_owner_check()
 94    async def close(self, ctx: discord.Interaction) -> None:
 95        """Closes the bot.
 96
 97        If used with a process manager, this will restart the bot.
 98        If used without a process manager, this will close the bot.
 99
100        Args:
101            ctx: The interaction context.
102        """
103        await ctx.response.send_message("Closing...")
104        logging.info("Closing...")
105        await self._bt.close()
106
107    @app_commands.command(name="pull", description="Pulls the latest changes from the git repo. DANGEROUS!")
108    @checks.is_owner_check()
109    async def pull(self, ctx: discord.Interaction) -> None:
110        """Pulls the latest changes from the git repo.
111
112        This command pulls the latest changes from the git repo.
113        This is a dangerous command, as it can break the bot.
114        If you are not sure what you are doing, don't use this command.
115
116        Args:
117            ctx: The interaction context.
118        """
119        confr = views.Confirm()
120        emd = discord.Embed(
121            title="Pull from git",
122            description=("Are you sure you want to pull from git?\n"
123                         "This is a dangerous command, as it can break the bot.\n"
124                         "If you are not sure what you are doing, abort now."),
125            color=discord.Color.red(),
126        )
127        await ctx.response.send_message(embed=emd, view=confr)
128        mgs = await ctx.original_response()
129        await confr.wait()
130        if confr.value is None:
131            emd = discord.Embed(
132                title="Pull from git",
133                description="Timed out.",
134                color=discord.Color.red(),
135            )
136            await ctx.followup.edit_message(mgs.id, embed=emd)
137            return
138        if not confr.value:
139            emd = discord.Embed(title="Pull from git", description="Cancelled.", color=discord.Color.orange())
140            await ctx.followup.edit_message(mgs.id, embed=emd)
141            return
142        emd = discord.Embed(
143            title="Pull from git",
144            description="Confirmed.",
145            color=discord.Color.green(),
146        )
147        await ctx.followup.edit_message(mgs.id, embed=emd)
148        await ctx.followup.send("Pulling latest changes...")
149        logging.info("Pulling latest changes...")
150        pull = await asyncio.create_subprocess_shell(
151            "git pull",
152            stdout=asyncio.subprocess.PIPE,
153            stderr=asyncio.subprocess.PIPE,
154            cwd=const.PROG_DIR,
155        )
156        stdo, stdr = await pull.communicate()
157        if stdo:
158            await ctx.followup.send(f"[stdout]\n{stdo.decode()}")
159            logging.info("[stdout]\n%s", stdo.decode())
160
161        if stdr:
162            await ctx.followup.send(f"[stderr]\n{stdr.decode()}")
163            logging.info("[stderr]\n%s", stdr.decode())
164
165        await ctx.followup.send("Finished pulling latest changes.\n"
166                                "Restart bot or reload cogs to apply changes.")
167
168    @app_commands.command(name="logs", description="Sends the logs.")
169    @checks.is_owner_check()
170    @app_commands.describe(id_no="Log ID (0 for latest log)")
171    async def logs(self, ctx: discord.Interaction, id_no: int = 0) -> None:
172        """Sends the logs.
173
174        This command sends the logs to the user who invoked the command.
175        The logs are sent as a file attachment.
176        It is possible to specify a log ID, which will send a specific log.
177        If no log ID is specified, the latest log will be sent.
178
179        Args:
180            ctx: The interaction context.
181            id_no: The log ID.
182        """
183        await ctx.response.defer(thinking=True)
184        logging.info("Sending logs to %s...", str(ctx.user))
185        filename = f"bot.log{"."+str(id_no) if id_no else ""}"
186        file_path = os.path.join(const.PROG_DIR, "log", filename)
187        try:
188            await ctx.user.send(file=discord.File(fp=file_path))
189        except FileNotFoundError:
190            await ctx.followup.send("Specified log not found.")
191            logging.info("Specified log not found.")
192            return
193        await ctx.followup.send("Sent logs.")
194        logging.info("Logs sent.")
195
196    @app_commands.command(name="config", description="Sends the guild config.")
197    @checks.is_owner_check()
198    @app_commands.describe(guid="Guild ID")
199    async def config(self, ctx: discord.Interaction, guid: str) -> None:
200        """Sends the config.
201
202        This command sends the config to the user who invoked the command.
203        I don't really know if it works with the new db system.
204        It is required to specify a guild ID. This is because the config
205        is guild-specific.
206
207        Args:
208            ctx: The interaction context.
209            guid: The guild ID.
210        """
211        guid_id = int(guid)
212        await ctx.response.defer(thinking=True)
213        logging.info("Sending config to %s...", str(ctx.user))
214        async with self._bt.get_connection() as conn:
215            guild_confg = await conn.get_guild(guid_id)
216            emd = discord.Embed(
217                title="OVERRIDE: Config",
218                description=f"CONFIG FOR GUILD: {guid_id}",
219                color=discord.Color.random(),
220            )
221            emd.add_field(name="OPEN MESSAGE", value=guild_confg.open_message)
222            emd.add_field(name="STAFF TEAM NAME", value=guild_confg.staff_team_name)
223            emd.add_field(name="FIRST AUTO-CLOSE", value=guild_confg.first_autoclose)
224            emd.add_field(name="MSG DISCOVERY", value=guild_confg.msg_discovery)
225            emd.add_field(name="STRIP BUTTONS", value=guild_confg.strip_buttons)
226        await ctx.user.send(embed=emd)
227        await ctx.followup.send("Sent config.")
228        logging.info("Config sent.")
229
230    @commands.command(name="sync", description="Syncs the tree.")
231    @commands.is_owner()
232    async def sync(self, ctx: commands.Context) -> None:
233        """Syncs the tree.
234
235        This command syncs the tree.
236        It is not recommended to use this command often, as it has low rate
237        limits. It is the only non-slash command in this bot.
238
239        Args:
240            ctx: The command context.
241        """
242        await ctx.send("Syncing...")
243        logging.info("Syncing...")
244        await self._bt.tree.sync()
245        dev_guild = self._bt.get_guild(_CNFG.getitem("dev_guild_id"))
246        await self._bt.tree.sync(guild=dev_guild)
247        await ctx.send("Synced.")
248        logging.info("Synced.")
249
250
251async def setup(bot_instance: bot.TicketsPlusBot):
252    """Sets up the overrides.
253
254    We add the override cog to the bot.
255
256    Args:
257        bot_instance: The bot.
258    """
259    await bot_instance.add_cog(Overrides(bot_instance))
@app_commands.guilds(_CNFG.getitem('dev_guild_id'))
class Overrides(discord.ext.commands.cog.GroupCog):
 41@app_commands.guilds(_CNFG.getitem("dev_guild_id"))
 42class Overrides(commands.GroupCog, name="override", description="Owner override commands."):
 43    """Owner override commands.
 44
 45    This class contains commands that are only available to the bot owner.
 46    These commands are used to reload cogs, restart the bot, and pull from git.
 47    The commands are only available in the development guild, as specified in
 48    the config.
 49    """
 50
 51    def __init__(self, bot_instance: bot.TicketsPlusBot):
 52        """Initializes the cog.
 53
 54        This method initializes the cog.
 55        It also sets the bot instance as a private attribute.
 56        And finally initializes the superclass.
 57
 58        Args:
 59            bot_instance: The bot instance.
 60        """
 61        self._bt = bot_instance
 62        super().__init__()
 63        logging.info("Loaded %s", self.__class__.__name__)
 64
 65    @app_commands.command(name="reload", description="Reloads the bot's cogs.")
 66    @checks.is_owner_check()
 67    @app_commands.describe(sync="Syncs the tree after reloading cogs.")
 68    async def reload(self, ctx: discord.Interaction, sync: bool = False) -> None:
 69        """Reloads the bot's cogs.
 70
 71        This command reloads all cogs in the EXTENSIONS list.
 72        Reloads are atomic, so if one fails, it rolls back.
 73        We can just import this submodule and iterate over the EXTENSIONS list.
 74        You can also sync the tree after reloading cogs. Though this is not
 75        to be used very often, as it has low rate limits.
 76
 77        Args:
 78            ctx: The interaction context.
 79            sync: Whether to sync the tree after reloading cogs.
 80        """
 81        await ctx.response.send_message("Reloading cogs...")
 82        logging.info("Reloading cogs...")
 83        for extension in cogs.EXTENSIONS:
 84            await self._bt.reload_extension(extension)
 85        await ctx.followup.send("Reloaded cogs.")
 86        logging.info("Finished reloading cogs.")
 87        if sync:
 88            await self._bt.tree.sync()
 89            dev_guild = self._bt.get_guild(_CNFG.getitem("dev_guild_id"))
 90            await self._bt.tree.sync(guild=dev_guild)
 91            logging.info("Finished syncing tree.")
 92
 93    @app_commands.command(name="close", description="Closes the bot.")
 94    @checks.is_owner_check()
 95    async def close(self, ctx: discord.Interaction) -> None:
 96        """Closes the bot.
 97
 98        If used with a process manager, this will restart the bot.
 99        If used without a process manager, this will close the bot.
100
101        Args:
102            ctx: The interaction context.
103        """
104        await ctx.response.send_message("Closing...")
105        logging.info("Closing...")
106        await self._bt.close()
107
108    @app_commands.command(name="pull", description="Pulls the latest changes from the git repo. DANGEROUS!")
109    @checks.is_owner_check()
110    async def pull(self, ctx: discord.Interaction) -> None:
111        """Pulls the latest changes from the git repo.
112
113        This command pulls the latest changes from the git repo.
114        This is a dangerous command, as it can break the bot.
115        If you are not sure what you are doing, don't use this command.
116
117        Args:
118            ctx: The interaction context.
119        """
120        confr = views.Confirm()
121        emd = discord.Embed(
122            title="Pull from git",
123            description=("Are you sure you want to pull from git?\n"
124                         "This is a dangerous command, as it can break the bot.\n"
125                         "If you are not sure what you are doing, abort now."),
126            color=discord.Color.red(),
127        )
128        await ctx.response.send_message(embed=emd, view=confr)
129        mgs = await ctx.original_response()
130        await confr.wait()
131        if confr.value is None:
132            emd = discord.Embed(
133                title="Pull from git",
134                description="Timed out.",
135                color=discord.Color.red(),
136            )
137            await ctx.followup.edit_message(mgs.id, embed=emd)
138            return
139        if not confr.value:
140            emd = discord.Embed(title="Pull from git", description="Cancelled.", color=discord.Color.orange())
141            await ctx.followup.edit_message(mgs.id, embed=emd)
142            return
143        emd = discord.Embed(
144            title="Pull from git",
145            description="Confirmed.",
146            color=discord.Color.green(),
147        )
148        await ctx.followup.edit_message(mgs.id, embed=emd)
149        await ctx.followup.send("Pulling latest changes...")
150        logging.info("Pulling latest changes...")
151        pull = await asyncio.create_subprocess_shell(
152            "git pull",
153            stdout=asyncio.subprocess.PIPE,
154            stderr=asyncio.subprocess.PIPE,
155            cwd=const.PROG_DIR,
156        )
157        stdo, stdr = await pull.communicate()
158        if stdo:
159            await ctx.followup.send(f"[stdout]\n{stdo.decode()}")
160            logging.info("[stdout]\n%s", stdo.decode())
161
162        if stdr:
163            await ctx.followup.send(f"[stderr]\n{stdr.decode()}")
164            logging.info("[stderr]\n%s", stdr.decode())
165
166        await ctx.followup.send("Finished pulling latest changes.\n"
167                                "Restart bot or reload cogs to apply changes.")
168
169    @app_commands.command(name="logs", description="Sends the logs.")
170    @checks.is_owner_check()
171    @app_commands.describe(id_no="Log ID (0 for latest log)")
172    async def logs(self, ctx: discord.Interaction, id_no: int = 0) -> None:
173        """Sends the logs.
174
175        This command sends the logs to the user who invoked the command.
176        The logs are sent as a file attachment.
177        It is possible to specify a log ID, which will send a specific log.
178        If no log ID is specified, the latest log will be sent.
179
180        Args:
181            ctx: The interaction context.
182            id_no: The log ID.
183        """
184        await ctx.response.defer(thinking=True)
185        logging.info("Sending logs to %s...", str(ctx.user))
186        filename = f"bot.log{"."+str(id_no) if id_no else ""}"
187        file_path = os.path.join(const.PROG_DIR, "log", filename)
188        try:
189            await ctx.user.send(file=discord.File(fp=file_path))
190        except FileNotFoundError:
191            await ctx.followup.send("Specified log not found.")
192            logging.info("Specified log not found.")
193            return
194        await ctx.followup.send("Sent logs.")
195        logging.info("Logs sent.")
196
197    @app_commands.command(name="config", description="Sends the guild config.")
198    @checks.is_owner_check()
199    @app_commands.describe(guid="Guild ID")
200    async def config(self, ctx: discord.Interaction, guid: str) -> None:
201        """Sends the config.
202
203        This command sends the config to the user who invoked the command.
204        I don't really know if it works with the new db system.
205        It is required to specify a guild ID. This is because the config
206        is guild-specific.
207
208        Args:
209            ctx: The interaction context.
210            guid: The guild ID.
211        """
212        guid_id = int(guid)
213        await ctx.response.defer(thinking=True)
214        logging.info("Sending config to %s...", str(ctx.user))
215        async with self._bt.get_connection() as conn:
216            guild_confg = await conn.get_guild(guid_id)
217            emd = discord.Embed(
218                title="OVERRIDE: Config",
219                description=f"CONFIG FOR GUILD: {guid_id}",
220                color=discord.Color.random(),
221            )
222            emd.add_field(name="OPEN MESSAGE", value=guild_confg.open_message)
223            emd.add_field(name="STAFF TEAM NAME", value=guild_confg.staff_team_name)
224            emd.add_field(name="FIRST AUTO-CLOSE", value=guild_confg.first_autoclose)
225            emd.add_field(name="MSG DISCOVERY", value=guild_confg.msg_discovery)
226            emd.add_field(name="STRIP BUTTONS", value=guild_confg.strip_buttons)
227        await ctx.user.send(embed=emd)
228        await ctx.followup.send("Sent config.")
229        logging.info("Config sent.")
230
231    @commands.command(name="sync", description="Syncs the tree.")
232    @commands.is_owner()
233    async def sync(self, ctx: commands.Context) -> None:
234        """Syncs the tree.
235
236        This command syncs the tree.
237        It is not recommended to use this command often, as it has low rate
238        limits. It is the only non-slash command in this bot.
239
240        Args:
241            ctx: The command context.
242        """
243        await ctx.send("Syncing...")
244        logging.info("Syncing...")
245        await self._bt.tree.sync()
246        dev_guild = self._bt.get_guild(_CNFG.getitem("dev_guild_id"))
247        await self._bt.tree.sync(guild=dev_guild)
248        await ctx.send("Synced.")
249        logging.info("Synced.")

Owner override commands.

This class contains commands that are only available to the bot owner. These commands are used to reload cogs, restart the bot, and pull from git. The commands are only available in the development guild, as specified in the config.

Overrides(bot_instance: tickets_plus.bot.TicketsPlusBot)
51    def __init__(self, bot_instance: bot.TicketsPlusBot):
52        """Initializes the cog.
53
54        This method initializes the cog.
55        It also sets the bot instance as a private attribute.
56        And finally initializes the superclass.
57
58        Args:
59            bot_instance: The bot instance.
60        """
61        self._bt = bot_instance
62        super().__init__()
63        logging.info("Loaded %s", self.__class__.__name__)

Initializes the cog.

This method initializes the cog. It also sets the bot instance as a private attribute. And finally initializes the superclass.

Arguments:
  • bot_instance: The bot instance.
reload = <discord.app_commands.commands.Command object>
close = <discord.app_commands.commands.Command object>
pull = <discord.app_commands.commands.Command object>
logs = <discord.app_commands.commands.Command object>
config = <discord.app_commands.commands.Command object>
sync = <discord.ext.commands.core.Command object>
async def setup(bot_instance: tickets_plus.bot.TicketsPlusBot):
252async def setup(bot_instance: bot.TicketsPlusBot):
253    """Sets up the overrides.
254
255    We add the override cog to the bot.
256
257    Args:
258        bot_instance: The bot.
259    """
260    await bot_instance.add_cog(Overrides(bot_instance))

Sets up the overrides.

We add the override cog to the bot.

Arguments:
  • bot_instance: The bot.