🤖 Adding a Cog¶
This page documents how to add a new Discord.py cog to the Magikal bot safely.
It is based on the current live bot structure.
Source files checked¶
The current live bot loads cogs from:
/home/magikalbot/magikal-bot/bot/main.py
Cogs live in:
/home/magikalbot/magikal-bot/bot/cogs/
A recent current-pattern cog used for reference:
/home/magikalbot/magikal-bot/bot/cogs/tempvc_v2.py
Current cog loading list¶
The bot startup extension list is in bot/main.py.
Current loaded extensions include:
bot.cogs.moderationbot.cogs.autorolesbot.cogs.reaction_rolesbot.cogs.tempvc_v2bot.cogs.basicbot.cogs.admin_configbot.cogs.diag_storagebot.cogs.ticketsbot.cogs.shipinfo_uexbot.cogs.market_uexbot.cogs.embed_toolsbot.cogs.rsi_profilebot.cogs.welcomebot.cogs.diag_commandsbot.cogs.help_auditorbot.cogs.help_panelbot.cogs.event_modalbot.cogs.automodbot.cogs.modlog_eventsbot.cogs.voice_eventsbot.cogs.bankingbot.cogs.recruit_configbot.cogs.eventsbot.cogs.xpbot.cogs.command_dumpbot.cogs.roles_configbot.cogs.rsi_statusbot.cogs.ai_assistant
Keep the load list intentional
Do not add a cog to initial_extensions unless it should load automatically on the live bot.
Experimental or unfinished cogs should not be silently added to startup.
Basic cog pattern¶
A normal cog should use the Discord.py extension pattern:
class ExampleCog(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.config_storage = bot.config_storage
async def setup(bot: commands.Bot):
await bot.add_cog(ExampleCog(bot))
Recommended imports¶
A typical cog starts with:
import discord
import structlog
from discord.ext import commands
from discord import app_commands
from ..utils.hasher import make_pid
from ..utils.embeds import MagikalEmbeds
logger = structlog.get_logger(__name__)
Only import what the cog actually needs.
Store bot references clearly¶
In __init__, cache the bot and shared adapters:
def __init__(self, bot: commands.Bot):
self.bot = bot
self.config_storage = bot.config_storage
If the cog needs runtime state, use the bot state adapter:
self.state_storage = bot.state_storage
State storage is an adapter
Do not hardcode assumptions about whether state is Postgres, Redis, or in-memory.
Use the adapter exposed by bot.state_storage.
Config access pattern¶
Normal cogs should use:
self.config_storage
or:
self.bot.config_storage
Avoid direct database sessions in normal cogs.
Good:
settings = await self.config_storage.get_tempvc_afk_settings(guild_id)
Avoid in new cogs unless explicitly approved:
self.db = bot.database_manager
async with self.db.session_factory() as session:
...
Optional config helper pattern¶
For cogs that need to support slightly different storage method shapes, a helper can be used.
Pattern seen in current TempVC:
async def _cfg(self, method: str, *args, **kwargs):
fn = getattr(self.config_storage, method, None)
if fn is None:
raise AttributeError(f"config_storage missing {method}")
res = fn(*args, **kwargs)
return await res if inspect.isawaitable(res) else res
Keep this simple.
Do not use a fallback to raw database manager unless there is a clear reason.
State helper pattern¶
For cogs that use runtime state:
async def _state(self, method: str, *args, **kwargs):
fn = getattr(self.state_storage, method, None)
if fn is None:
return None
res = fn(*args, **kwargs)
return await res if inspect.isawaitable(res) else res
Use this only where the cog genuinely needs state storage.
Slash commands¶
Use slash commands for modern user-facing interactions.
Example shape:
@app_commands.command(name="example", description="Short clear description.")
@app_commands.guild_only()
async def example(self, interaction: discord.Interaction):
await interaction.response.send_message("Done.", ephemeral=True)
For admin-only slash commands, use suitable permissions:
@app_commands.default_permissions(manage_guild=True)
@app_commands.guild_only()
Text commands¶
Text commands still exist in the bot.
Example shape:
@commands.command(name="example")
@commands.guild_only()
async def example(self, ctx: commands.Context):
await ctx.reply("Done.")
Use text commands where the existing feature area already uses text commands or where prefix command behaviour is required.
Persistent views¶
If a cog registers a persistent Discord view, create the cog first, then register the view.
Pattern seen in TempVC:
async def setup(bot: commands.Bot):
cog = TempVCV2Cog(bot)
await bot.add_cog(cog)
bot.add_view(ControlPanelView(cog))
Persistent view rule
Persistent views must be registered during setup so buttons keep working after bot restart.
Make sure the view does not depend on stale local-only state.
Logging¶
Use structlog.
Pattern:
import structlog
logger = structlog.get_logger(__name__)
Use stable event names:
logger.info(event="example.started")
logger.warning(event="example.failed", error=str(e))
Do not log raw Discord IDs or Discord objects.
Privacy-safe IDs¶
Use:
make_pid(guild_id, user_id)
Good:
logger.info(
event="example.action",
guild_pid=make_pid(guild.id, guild.id),
user_pid=make_pid(guild.id, member.id),
actor_pid=make_pid(guild.id, interaction.user.id),
)
Avoid:
logger.info(event="example.action", user_id=member.id)
Embeds¶
Prefer the existing embed helper where possible:
MagikalEmbeds.success(...)
MagikalEmbeds.warning(...)
MagikalEmbeds.error(...)
This keeps bot messages consistent.
Error handling¶
Handle expected Discord errors cleanly.
Good:
try:
await interaction.followup.send(...)
except discord.HTTPException as e:
logger.warning(event="example.followup_failed", error=str(e))
Do not expose raw tracebacks to users.
Adding the cog to startup¶
After creating the new cog file, add it to initial_extensions in:
bot/main.py
Example:
"bot.cogs.example",
Keep the list tidy.
Do not add a half-finished cog to live startup.
Restart and check¶
After adding a cog, restart the bot:
sudo systemctl restart magikal-bot
Check status:
sudo systemctl status magikal-bot --no-pager
Check logs:
sudo journalctl -u magikal-bot -n 120 --no-pager
Look for:
cog.loaded
or:
cog.load_failed
Slash command sync¶
If the cog adds slash commands, sync may be required.
The bot has existing command sync helpers.
Common check:
sudo journalctl -u magikal-bot -n 200 --no-pager | grep -i slash
Admin command sync may also be available from Discord depending on current bot command setup.
Testing checklist¶
Before calling the cog done:
- bot starts cleanly
- cog logs as loaded
- no traceback in logs
- commands appear where expected
- command permissions are correct
- guild-only commands do not work in DMs
- embeds look consistent
- logs do not contain raw Discord IDs
- database writes go through the approved storage layer
- any new config has docs or panel support planned
Red flags¶
Stop and review if the cog:
- needs a new database table
- changes
bot/database.py - needs Alembic
- uses direct DB sessions
- stores raw Discord IDs
- introduces a new storage backend
- adds persistent views
- creates background tasks
- touches Temp VC, tickets, moderation, or panel config
- changes permission gates
- logs user data
Rule¶
A new cog should be small, load cleanly, use config_storage, log safely, and avoid new architecture unless approved.