Skip to content

Add event cogs#14

Open
aaravmaloo wants to merge 4 commits intoTheCodeVerseHub:masterfrom
aaravmaloo:master
Open

Add event cogs#14
aaravmaloo wants to merge 4 commits intoTheCodeVerseHub:masterfrom
aaravmaloo:master

Conversation

@aaravmaloo
Copy link
Contributor

Summary

  • add seasonal events/live-ops cog with join, daily check-ins, leaderboard, and admin controls
  • add automatic start/end transitions with announcements and winner summaries
  • add events tables to the main SQLite DB and wire the cog into bot startup

Testing

  • Not run (not requested)

Notes

  • Event times accept YYYY-MM-DD HH:MM or ISO 8601; timezone defaults to UTC if omitted

@HyScript7 HyScript7 requested a review from youngcoder45 March 10, 2026 13:40
Copy link
Contributor

@youngcoder45 youngcoder45 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good but will think before merging

async with aiosqlite.connect(DB_PATH) as db:
cursor = await db.execute(
"SELECT event_id, name, description, start_at, end_at, status, announcement_channel_id "
"FROM events WHERE guild_id = ? AND status = 'active'",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bro what

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new seasonal events / live-ops system to the Discord bot, backed by SQLite tables and loaded as a new cog at startup.

Changes:

  • Adds new events, event_participants, and event_actions tables to the main SQLite DB initialization.
  • Introduces cogs/events.py implementing /event commands (create/join/checkin/leaderboard/admin controls) plus an automatic status transition task loop.
  • Wires the new cog into bot.py extension loading.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.

File Description
utils/codebuddy_database.py Creates SQLite tables needed for events/live-ops.
cogs/events.py New seasonal events cog with commands, leaderboard, and automatic start/end announcements.
bot.py Loads the new cogs.events extension during startup.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +350 to +365
async with aiosqlite.connect(DB_PATH) as db:
cursor = await db.execute(
"SELECT last_checkin FROM event_participants WHERE event_id = ? AND user_id = ?",
(event_id, ctx.author.id)
)
row = await cursor.fetchone()
if row and row[0] == today:
return await ctx.reply("You already checked in today. Try again tomorrow.")
await db.execute(
"UPDATE event_participants SET last_checkin = ?, last_activity = ? WHERE event_id = ? AND user_id = ?",
(today, _utcnow().isoformat(), event_id, ctx.author.id)
)
await db.commit()

await self._add_points(event_id, ctx.author.id, 1, "daily_checkin")
await ctx.reply("Check-in recorded. +1 point.")
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/event checkin updates last_checkin in one DB connection/transaction, then calls _add_points() which opens a second connection and performs separate writes. If the second step fails (e.g., DB locked), users can end up with a check-in recorded but no point awarded. Consider doing the check-in update + points increment + action insert in a single transaction/connection for atomicity.

Copilot uses AI. Check for mistakes.
import discord
from discord import app_commands
from discord.ext import commands, tasks
from datetime import datetime, timezone, timedelta
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timedelta is imported from datetime but never used in this module. Removing unused imports helps keep the file tidy and avoids confusion about intended time calculations.

Suggested change
from datetime import datetime, timezone, timedelta
from datetime import datetime, timezone

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +10

DB_PATH = "botdata.db"


Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This cog defines its own DB_PATH = "botdata.db" instead of reusing the shared DB path constant. Other cogs that use the main SQLite DB import DB_PATH from utils.codebuddy_database (e.g., cogs/counting.py:5, cogs/tod.py:6). To avoid divergence if the DB location changes, import and use the shared DB_PATH here too (and drop the local constant).

Suggested change
DB_PATH = "botdata.db"
from utils.codebuddy_database import DB_PATH

Copilot uses AI. Check for mistakes.
Comment on lines +197 to +206
@tasks.loop(minutes=1)
async def event_tick(self):
try:
rows = await self._fetch_events()
guild_ids = sorted({row[1] for row in rows})
for guild_id in guild_ids:
await self._sync_guild(guild_id)
except Exception:
return

Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

event_tick swallows all exceptions and returns without logging. This can silently break automatic transitions (and make DB lock/parse issues impossible to diagnose). Log the exception with traceback (and ideally continue per-guild rather than aborting the whole tick) instead of silently returning.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to raise this as well, but since nothing else in this bot logs anyway, I decided not to comment on it.

Comment on lines +52 to +60
async def _fetch_events(self, guild_id: Optional[int] = None) -> List[tuple]:
query = "SELECT event_id, guild_id, name, description, start_at, end_at, status, announcement_channel_id FROM events"
params: tuple = ()
if guild_id is not None:
query += " WHERE guild_id = ?"
params = (guild_id,)
async with aiosqlite.connect(DB_PATH) as db:
cursor = await db.execute(query, params)
return await cursor.fetchall()
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All DB accesses here use the default SQLite timeout and no retry/backoff. In this repo, at least cogs/counting.py explicitly uses aiosqlite.connect(DB_PATH, timeout=30.0) and retries on database is locked, which suggests the main DB sees concurrent access. Using the default timeout here can cause frequent OperationalError: database is locked during commands/ticks. Consider using the same timeout (and possibly a small retry) for these connections.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +61
async def _fetch_events(self, guild_id: Optional[int] = None) -> List[tuple]:
query = "SELECT event_id, guild_id, name, description, start_at, end_at, status, announcement_channel_id FROM events"
params: tuple = ()
if guild_id is not None:
query += " WHERE guild_id = ?"
params = (guild_id,)
async with aiosqlite.connect(DB_PATH) as db:
cursor = await db.execute(query, params)
return await cursor.fetchall()

Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_fetch_events() selects all rows from events (including ended / cancelled) and event_tick calls it every minute, then _sync_guild re-fetches per guild. As the table grows, this becomes unnecessary work each tick. Restrict the query to statuses that can transition (e.g., scheduled/active) and/or fetch once per tick and group in-memory to avoid the extra per-guild queries.

Copilot uses AI. Check for mistakes.
Comment on lines +90 to +97
"FROM events WHERE guild_id = ? AND status = 'scheduled'",
(guild_id,)
)
rows = await cursor.fetchall()
if not rows:
return None
rows_sorted = sorted(rows, key=lambda r: datetime.fromisoformat(r[3]))
return rows_sorted[0]
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_get_next_event() loads all scheduled events and sorts them in Python. This is less efficient than letting SQLite do it (ORDER BY start_at LIMIT 1), and it also risks raising ValueError in the sort key if any row has a non-ISO start_at string (the schema doesn't enforce format). Prefer an ordered query that returns only the next row, which removes both issues.

Suggested change
"FROM events WHERE guild_id = ? AND status = 'scheduled'",
(guild_id,)
)
rows = await cursor.fetchall()
if not rows:
return None
rows_sorted = sorted(rows, key=lambda r: datetime.fromisoformat(r[3]))
return rows_sorted[0]
"FROM events WHERE guild_id = ? AND status = 'scheduled' "
"ORDER BY start_at ASC LIMIT 1",
(guild_id,)
)
row = await cursor.fetchone()
return row

Copilot uses AI. Check for mistakes.
end_at TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'scheduled',
announcement_channel_id INTEGER,
created_by INTEGER,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is created_by nullable?
As far as I know, it shouldn't be possible for an event to create itself autonomously like some sort of cosmic deity.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

created_by is currently nullable mainly as a safeguard for edge cases where an event might be inserted programmatically (for example during migrations, imports, or automated maintenance tasks) where a user context might not exist.


def __init__(self, bot: commands.Bot):
self.bot = bot
self.event_tick.start()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Is there a reason why this is in the constructor? Why not do this in cog_load?


@event_group.command(name="award", description="Award points to a participant.")
@app_commands.describe(user="User to award", points="Points to add", reason="Reason for the award")
@commands.has_permissions(manage_guild=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is manage_guild the right permission here? Guild settings aren't modified here either.
I'd again consider using manage_events, since the points aren't used outside of this cog. (Correct me if I am wrong.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

manage_events would be the more appropriate permission here since the command only affects event-related data and doesn't modify broader guild settings. I'll switch the checks to manage_events.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. Also check whether the tick task initialization couldn't be moved to cog_load.
I'll approve changes after you push.

""")

await db.execute("""
CREATE TABLE IF NOT EXISTS event_actions (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd consider renaming this table, as it is not immediately apparent that it logs point additions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants