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))
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.
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.
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.