Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ DISCORD_TOKEN=your_discord_bot_token_here
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
POSTGRES_DB=veka_bot
POSTGRES_USER=veka_user
POSTGRES_USER=veka_bot_user
POSTGRES_PASSWORD=your_secure_password_here
DATABASE_URL=postgresql://veka_user:your_secure_password_here@postgres:5432/veka_bot
DATABASE_URL=postgresql://veka_bot_user:your_secure_password_here@postgres:5432/veka_bot

# Access Control (comma-separated Discord user IDs)
ADMIN_IDS=123456789,987654321
Expand All @@ -25,6 +25,9 @@ MARKETPLACE_CHANNEL_ID=
# Operational alerts channel (Discord channel ID)
ADMIN_ALERT_CHANNEL_ID=

# Mass unban log channel (Discord channel ID, falls back to LOGS_CHANNEL_ID if unset)
MASSUNBAN_LOG_CHANNEL_ID=

# Radio Configuration
RADIO_STREAM_URL=https://www.youtube.com/watch?v=jfKfPfyJRdk
RADIO_VOICE_CHANNEL_ID=
Expand Down
113 changes: 111 additions & 2 deletions AGENTS.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ dev = [
]

[tool.ruff]
target-version = "py312"
target-version = "py313"
line-length = 120

[tool.ruff.lint]
Expand All @@ -36,7 +36,7 @@ quote-style = "single"
docstring-code-format = true

[tool.mypy]
python_version = "3.12"
python_version = "3.13"
ignore_missing_imports = true
check_untyped_defs = true
warn_redundant_casts = true
Expand Down
46 changes: 42 additions & 4 deletions src/cogs/admin/massunban.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ def __init__(self, bot: commands.Bot) -> None:

async def cog_load(self) -> None:
"""Resume incomplete jobs on startup."""
self.bot.loop.create_task(self._resume_incomplete_jobs())
asyncio.create_task(self._resume_incomplete_jobs())

async def _resume_incomplete_jobs(self) -> None:
"""Wait for bot to be ready, then scan for resumable jobs."""
Expand All @@ -218,7 +218,7 @@ async def _resume_incomplete_jobs(self) -> None:
job['id'],
)
self._active_jobs[job['id']] = False
self.bot.loop.create_task(self._execute_job(job['id'], resumed=True))
asyncio.create_task(self._execute_job(job['id'], resumed=True))
except Exception:
logger.error('Failed to resume job %s', job['id'], exc_info=True)

Expand Down Expand Up @@ -775,7 +775,7 @@ async def _process_unban_item(self, guild: nextcord.Guild, item: dict, job: dict

# Check if already unbanned
try:
await guild.fetch_ban(user_id)
await guild.fetch_ban(nextcord.Object(user_id))
except nextcord.NotFound:
# Already unbanned
result['status'] = 'skipped'
Expand Down Expand Up @@ -940,7 +940,7 @@ async def _handle_rate_limit_pause(self, job_id: int, result: dict) -> None:
logger.warning('Failed to send rate limit notification for job %s', job_id, exc_info=True)

# Schedule resume using asyncio task (not call_later)
self.bot.loop.create_task(self._scheduled_resume(job_id, retry_after_secs))
asyncio.create_task(self._scheduled_resume(job_id, retry_after_secs))

async def _scheduled_resume(self, job_id: int, delay: float) -> None:
"""Wait then resume a rate-limited job, checking for cancellation first."""
Expand Down Expand Up @@ -1022,6 +1022,44 @@ async def _complete_job(self, job_id: int, guild: nextcord.Guild) -> None:
except Exception:
logger.error('Failed to complete job %s', job_id, exc_info=True)

async def _build_preview_embed(
self,
job_id: int,
ban_count: int,
start_dt: datetime,
end_dt: datetime,
banned_by: nextcord.Member | None,
reason: str,
no_audit_count: int,
) -> nextcord.Embed:
embed = await veka_embed(
title='Mass Unban — Confirm',
description=f'**{ban_count} ban(s)** match your filters.',
contributor_source=__name__,
)
embed.add_field(name='Job ID', value=str(job_id), inline=False)
embed.add_field(
name='Date Range',
value=f'{start_dt.strftime("%Y-%m-%d %H:%M:%S UTC")} → {end_dt.strftime("%Y-%m-%d %H:%M:%S UTC")}',
inline=False,
)
if banned_by:
embed.add_field(name='Moderator Filter', value=banned_by.mention, inline=False)
if reason:
embed.add_field(name='Reason', value=reason, inline=False)
if no_audit_count > 0:
embed.add_field(
name='Note',
value=f'{no_audit_count} ban(s) have no audit history and will be included.',
inline=False,
)
embed.add_field(
name='Confirm',
value='This action will unban all matched users. Double confirmation required.',
inline=False,
)
return embed

async def _cancel_job(self, job_id: int, _runner_id: int) -> None:
"""Cancel a running job."""
self._active_jobs[job_id] = True
Expand Down
5 changes: 3 additions & 2 deletions src/cogs/marketplace/marketplace.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import datetime
import logging
from datetime import UTC

import nextcord
from nextcord.ext import commands
Expand Down Expand Up @@ -94,7 +95,7 @@ async def mp_post(

category_id = category_record['id']

listing_id = f'MP{int(datetime.datetime.utcnow().timestamp())}'
listing_id = f'MP{int(datetime.datetime.now(UTC).timestamp())}'

# Ensure user exists in db
await db.execute('INSERT INTO users (discord_id) VALUES ($1) ON CONFLICT DO NOTHING', str(interaction.user.id))
Expand Down Expand Up @@ -156,7 +157,7 @@ async def mp_post(
'category': category,
'status': 'active',
'image_url': image_url or None,
'listing_created_at': datetime.datetime.utcnow().isoformat(),
'listing_created_at': datetime.datetime.now(UTC).isoformat(),
}
)

Expand Down
7 changes: 4 additions & 3 deletions src/cogs/marketplace/reviews.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,10 +335,11 @@ async def bump_slash(self, interaction: nextcord.Interaction, listing_id: str):
await safe_send(interaction, embed=embed, ephemeral=True)
return

from datetime import datetime, timedelta
from datetime import UTC, datetime, timedelta

if listing['bumped_at'] and listing['bumped_at'] > datetime.utcnow() - timedelta(hours=24):
time_left = listing['bumped_at'] + timedelta(hours=24) - datetime.utcnow()
now = datetime.now(UTC)
if listing['bumped_at'] and listing['bumped_at'] > now - timedelta(hours=24):
time_left = listing['bumped_at'] + timedelta(hours=24) - now
hours = int(time_left.total_seconds() // 3600)
embed = await error_embed(
'Too Soon',
Expand Down
4 changes: 2 additions & 2 deletions src/cogs/marketplace_enhanced.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"""

import logging
from datetime import datetime, timedelta
from datetime import UTC, datetime, timedelta
from decimal import Decimal

import nextcord
Expand Down Expand Up @@ -240,7 +240,7 @@ async def offer_slash(self, interaction: nextcord.Interaction, listing_id: str,
await safe_send(interaction, embed=embed, ephemeral=True)
return

expires = datetime.utcnow() + timedelta(days=3)
expires = datetime.now(UTC) + timedelta(days=3)
await db.execute(
"""INSERT INTO marketplace_offers
(listing_id, buyer_id, offered_price, message, expires_at)
Expand Down
6 changes: 3 additions & 3 deletions src/cogs/portfolio/portfolio_manager.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import logging
from datetime import datetime
from datetime import UTC, datetime

import nextcord
import validators
Expand Down Expand Up @@ -48,7 +48,7 @@ async def portfolio_add_slash(

tag_list = [t.strip() for t in tags.split(',') if t.strip()] if tags else []

project_id = f'proj-{int(datetime.utcnow().timestamp())}'
project_id = f'proj-{int(datetime.now(UTC).timestamp())}'
user = await get_or_create_user(str(interaction.user.id))

await db.execute(
Expand Down Expand Up @@ -269,7 +269,7 @@ def check(m):
tags_raw = (await self.bot.wait_for('message', check=check, timeout=60)).content
tag_list = [t.strip() for t in tags_raw.split(',') if t.strip()]

project_id = f'proj-{int(datetime.utcnow().timestamp())}'
project_id = f'proj-{int(datetime.now(UTC).timestamp())}'
user = await get_or_create_user(str(ctx.author.id))

await db.execute(
Expand Down
6 changes: 3 additions & 3 deletions src/cogs/radio/radio.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import asyncio
import logging
import time
from datetime import datetime
from datetime import UTC, datetime

import nextcord
from nextcord.ext import commands, tasks
Expand Down Expand Up @@ -137,7 +137,7 @@ async def _auto_join(self):

self._voice_client = await channel.connect(self_deaf=True) # type: ignore[call-arg]
self._play_stream()
self._started_at = datetime.utcnow()
self._started_at = datetime.now(UTC)
self._auto_started = True
self._manual_stop = False
logger.info('Radio started in channel %s', channel.name)
Expand Down Expand Up @@ -231,7 +231,7 @@ def _get_uptime(self) -> str:
"""Format uptime since radio started."""
if not self._started_at:
return 'Not started'
delta = datetime.utcnow() - self._started_at
delta = datetime.now(UTC) - self._started_at
hours, remainder = divmod(int(delta.total_seconds()), 3600)
minutes, seconds = divmod(remainder, 60)
if hours:
Expand Down
7 changes: 2 additions & 5 deletions src/cogs/resources/feeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,9 @@ async def feed_latest(

entries = entries[:5]

for idx, entry in enumerate(entries):
for _idx, entry in enumerate(entries):
embed = await self.create_feed_embed(entry, category)
if idx == 0:
await interaction.followup.send(embed=embed)
else:
await interaction.channel.send(embed=embed)
await interaction.followup.send(embed=embed)

async def create_feed_embed(self, entry: dict, category: str) -> nextcord.Embed:
embed = await info_embed(
Expand Down
4 changes: 2 additions & 2 deletions src/cogs/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,8 @@ def _resolve_member_name(self, user_id: str, guild: nextcord.Guild | None = None
member = guild.get_member(uid)
if member:
return member.display_name
member = self.bot.get_user(uid)
return member.display_name if member else f'User {uid}'
user = self.bot.get_user(uid)
return user.display_name if user else f'User {uid}'

# ============================================================
# Commands — Most Streamed
Expand Down
Loading