Birthdays have more timezones and ping actually pings

This commit is contained in:
Lilac-Rose
2025-10-18 08:24:42 +02:00
parent feb77f4637
commit 20f2dcc5f3
3 changed files with 339 additions and 4 deletions

314
birthday/birthday.py Normal file
View File

@@ -0,0 +1,314 @@
import discord
from discord.ext import commands, tasks
from discord import app_commands
import sqlite3
from datetime import datetime, timezone
import asyncio
import pytz
import os
from dotenv import load_dotenv
load_dotenv()
ADMIN_ROLE_ID = int(os.getenv("ADMIN_ROLE_ID"), 0)
class Birthday(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.db_path = os.path.join(os.path.dirname(__file__), "birthdays.db")
self._init_db()
self.check_birthdays.start()
def cog_unload(self):
self.check_birthdays.cancel()
def _init_db(self):
"""Initialize SQLite database."""
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute("""
CREATE TABLE IF NOT EXISTS birthdays (
user_id INTEGER PRIMARY KEY,
birthday TEXT NOT NULL,
timezone TEXT NOT NULL
)
""")
c.execute("""
CREATE TABLE IF NOT EXISTS guild_settings (
guild_id INTEGER PRIMARY KEY,
channel_id INTEGER
)
""")
conn.commit()
conn.close()
async def timezone_autocomplete(self, interaction: discord.Interaction, current: str):
all_timezones = [
("UTC", "UTC"),
# North America
("US Eastern (America/New_York)", "America/New_York"),
("US Central (America/Chicago)", "America/Chicago"),
("US Mountain (America/Denver)", "America/Denver"),
("US Pacific (America/Los_Angeles)", "America/Los_Angeles"),
("Alaska (America/Anchorage)", "America/Anchorage"),
("Hawaii (Pacific/Honolulu)", "Pacific/Honolulu"),
("Canada Atlantic (America/Halifax)", "America/Halifax"),
("Canada Central (America/Winnipeg)", "America/Winnipeg"),
("Canada Mountain (America/Edmonton)", "America/Edmonton"),
("Canada Pacific (America/Vancouver)", "America/Vancouver"),
("Canada Eastern (America/Toronto)", "America/Toronto"),
("Newfoundland (America/St_Johns)", "America/St_Johns"),
("Mexico City (America/Mexico_City)", "America/Mexico_City"),
# South America
("Brazil (America/Sao_Paulo)", "America/Sao_Paulo"),
("Argentina (America/Argentina/Buenos_Aires)", "America/Argentina/Buenos_Aires"),
("Chile (America/Santiago)", "America/Santiago"),
("Colombia (America/Bogota)", "America/Bogota"),
("Peru (America/Lima)", "America/Lima"),
("Venezuela (America/Caracas)", "America/Caracas"),
# Europe
("UK (Europe/London)", "Europe/London"),
("Ireland (Europe/Dublin)", "Europe/Dublin"),
("Portugal (Europe/Lisbon)", "Europe/Lisbon"),
("Spain (Europe/Madrid)", "Europe/Madrid"),
("France (Europe/Paris)", "Europe/Paris"),
("Netherlands (Europe/Amsterdam)", "Europe/Amsterdam"),
("Belgium (Europe/Brussels)", "Europe/Brussels"),
("Germany (Europe/Berlin)", "Europe/Berlin"),
("Switzerland (Europe/Zurich)", "Europe/Zurich"),
("Italy (Europe/Rome)", "Europe/Rome"),
("Austria (Europe/Vienna)", "Europe/Vienna"),
("Poland (Europe/Warsaw)", "Europe/Warsaw"),
("Czech Republic (Europe/Prague)", "Europe/Prague"),
("Greece (Europe/Athens)", "Europe/Athens"),
("Turkey (Europe/Istanbul)", "Europe/Istanbul"),
("Romania (Europe/Bucharest)", "Europe/Bucharest"),
("Sweden (Europe/Stockholm)", "Europe/Stockholm"),
("Norway (Europe/Oslo)", "Europe/Oslo"),
("Denmark (Europe/Copenhagen)", "Europe/Copenhagen"),
("Finland (Europe/Helsinki)", "Europe/Helsinki"),
("Russia Moscow (Europe/Moscow)", "Europe/Moscow"),
("Russia Yekaterinburg (Asia/Yekaterinburg)", "Asia/Yekaterinburg"),
("Russia Novosibirsk (Asia/Novosibirsk)", "Asia/Novosibirsk"),
("Russia Vladivostok (Asia/Vladivostok)", "Asia/Vladivostok"),
# Middle East & Central Asia
("UAE/Dubai (Asia/Dubai)", "Asia/Dubai"),
("Saudi Arabia (Asia/Riyadh)", "Asia/Riyadh"),
("Israel (Asia/Jerusalem)", "Asia/Jerusalem"),
("Iran (Asia/Tehran)", "Asia/Tehran"),
("Pakistan (Asia/Karachi)", "Asia/Karachi"),
("Afghanistan (Asia/Kabul)", "Asia/Kabul"),
("Kazakhstan (Asia/Almaty)", "Asia/Almaty"),
# South Asia
("India (Asia/Kolkata)", "Asia/Kolkata"),
("Sri Lanka (Asia/Colombo)", "Asia/Colombo"),
("Bangladesh (Asia/Dhaka)", "Asia/Dhaka"),
("Nepal (Asia/Kathmandu)", "Asia/Kathmandu"),
# Southeast Asia
("Thailand (Asia/Bangkok)", "Asia/Bangkok"),
("Vietnam (Asia/Ho_Chi_Minh)", "Asia/Ho_Chi_Minh"),
("Myanmar (Asia/Yangon)", "Asia/Yangon"),
("Malaysia (Asia/Kuala_Lumpur)", "Asia/Kuala_Lumpur"),
("Singapore (Asia/Singapore)", "Asia/Singapore"),
("Indonesia West (Asia/Jakarta)", "Asia/Jakarta"),
("Indonesia Central (Asia/Makassar)", "Asia/Makassar"),
("Indonesia East (Asia/Jayapura)", "Asia/Jayapura"),
("Philippines (Asia/Manila)", "Asia/Manila"),
# East Asia
("China (Asia/Shanghai)", "Asia/Shanghai"),
("Hong Kong (Asia/Hong_Kong)", "Asia/Hong_Kong"),
("Taiwan (Asia/Taipei)", "Asia/Taipei"),
("Japan (Asia/Tokyo)", "Asia/Tokyo"),
("Korea (Asia/Seoul)", "Asia/Seoul"),
("Mongolia (Asia/Ulaanbaatar)", "Asia/Ulaanbaatar"),
# Oceania
("Australia Western (Australia/Perth)", "Australia/Perth"),
("Australia Central (Australia/Adelaide)", "Australia/Adelaide"),
("Australia Eastern (Australia/Sydney)", "Australia/Sydney"),
("Australia Queensland (Australia/Brisbane)", "Australia/Brisbane"),
("New Zealand (Pacific/Auckland)", "Pacific/Auckland"),
("Fiji (Pacific/Fiji)", "Pacific/Fiji"),
("Papua New Guinea (Pacific/Port_Moresby)", "Pacific/Port_Moresby"),
# Africa
("South Africa (Africa/Johannesburg)", "Africa/Johannesburg"),
("Egypt (Africa/Cairo)", "Africa/Cairo"),
("Nigeria (Africa/Lagos)", "Africa/Lagos"),
("Kenya (Africa/Nairobi)", "Africa/Nairobi"),
("Morocco (Africa/Casablanca)", "Africa/Casablanca"),
("Ethiopia (Africa/Addis_Ababa)", "Africa/Addis_Ababa"),
]
# Filter timezones based on what user is typing
filtered = [
app_commands.Choice(name=name, value=value)
for name, value in all_timezones
if current.lower() in name.lower()
]
return filtered[:25] # Discord limit of 25 at a time
@app_commands.command(name="setbirthday", description="Set your birthday timezone.")
@app_commands.describe(date="Your birthday (MM-DD)", timezone="Your timezone, search your city/country to find it!")
@app_commands.autocomplete(timezone=timezone_autocomplete)
async def setbirthday(self, interaction: discord.Interaction, date: str, timezone: str):
try:
datetime.strptime(date, "%m-%d")
except ValueError:
await interaction.response.send_message("Invalid date format! Use MM-DD.", ephemeral=True)
return
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute("""
INSERT INTO birthdays (user_id, birthday, timezone)
VALUES (?, ?, ?)
ON CONFLICT(user_id) DO UPDATE SET birthday=excluded.birthday, timezone=excluded.timezone
""", (interaction.user.id, date, timezone))
conn.commit()
conn.close()
await interaction.response.send_message(f"🎂 Birthday set to '{date}' in timezone '{timezone}'!", ephemeral=True)
@app_commands.command(name="setbirthdaychannel", description="Set the channel for birthday announcements.")
@app_commands.describe(channel="Channel where birthday announcements will be sent.")
async def setbirthdaychannel(self, interaction: discord.Interaction, channel: discord.TextChannel):
member = interaction.user
if not any(role.id == ADMIN_ROLE_ID for role in member.roles):
await interaction.response.send_message("You do not have permission to use this command.", ephemeral=True)
return
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute("""
INSERT INTO guild_settings (guild_id, channel_id)
VALUES (?, ?)
ON CONFLICT(guild_id) DO UPDATE SET channel_id=excluded.channel_id
""", (interaction.guild.id, channel.id))
conn.commit()
conn.close()
await interaction.response.send_message(f"Birthday announcements will be sent in {channel.mention}")
@app_commands.command(name="listbirthdays", description="List all birthdays or birthdays for a specific month.")
@app_commands.describe(month="Optional: Specify a month (1-12) to see birthdays for that month")
async def listbirthdays(self, interaction: discord.Interaction, month: int = None):
if month is not None and (month < 1 or month > 12):
await interaction.response.send_message("Invalid month! Please use a number between 1 and 12.", ephemeral=True)
return
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute("SELECT user_id, birthday from birthdays")
rows = c.fetchall()
conn.close()
if not rows:
await interaction.response.send_message("No birthdays have been set yet!", ephemeral=True)
return
now = datetime.now(timezone.utc)
if month is not None:
# Filter by specific month
birthdays_list = []
for user_id, date_str in rows:
bday_month, day = map(int, date_str.split("-"))
if bday_month == month:
display_date = datetime(now.year, bday_month, day, tzinfo=timezone.utc)
birthdays_list.append((user_id, display_date, day))
if not birthdays_list:
month_name = datetime(now.year, month, 1).strftime('%B')
await interaction.response.send_message(f"No birthdays in {month_name}!", ephemeral=True)
return
# Sort by day of month
birthdays_list.sort(key=lambda x: x[2])
month_name = datetime(now.year, month, 1).strftime('%B')
lines = []
for user_id, date, _ in birthdays_list:
user = interaction.guild.get_member(user_id)
name = user.display_name if user else f"User {user_id}"
lines.append(f"**{name}** - {date.strftime('%B %d')}")
embed = discord.Embed(
title=f"🎂 Birthdays in {month_name}",
description="\n".join(lines),
color=discord.Color.magenta()
)
await interaction.response.send_message(embed=embed)
else:
# Show all birthdays organized by month
birthdays_by_month = {}
for user_id, date_str in rows:
bday_month, day = map(int, date_str.split("-"))
if bday_month not in birthdays_by_month:
birthdays_by_month[bday_month] = []
display_date = datetime(now.year, bday_month, day, tzinfo=timezone.utc)
birthdays_by_month[bday_month].append((user_id, display_date, day))
# Sort each month by day
for month_num in birthdays_by_month:
birthdays_by_month[month_num].sort(key=lambda x: x[2])
# Build the embed description
lines = []
for month_num in sorted(birthdays_by_month.keys()):
month_name = datetime(now.year, month_num, 1).strftime('%B')
lines.append(f"\n**{month_name}**")
for user_id, date, _ in birthdays_by_month[month_num]:
user = interaction.guild.get_member(user_id)
name = user.display_name if user else f"User {user_id}"
lines.append(f"{name} - {date.strftime('%B %d')}")
embed = discord.Embed(
title="🎂 All Birthdays",
description="\n".join(lines),
color=discord.Color.magenta()
)
await interaction.response.send_message(embed=embed)
@tasks.loop(minutes=1)
async def check_birthdays(self):
now_utc = datetime.now(timezone.utc).replace(second = 0, microsecond = 0)
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute("SELECT user_id, birthday, timezone FROM birthdays")
users = c.fetchall()
for user_id, date_str, timezone_str in users:
try:
tz = pytz.timezone(timezone_str)
except pytz.UnknownTimeZoneError:
continue
now_local = datetime.now(pytz.utc).astimezone(tz)
if now_local.hour == 0 and now_local.minute == 0:
month, day = map(int, date_str.split("-"))
if month == now_local.month and day == now_local.day:
for guild in self.bot.guilds:
member = guild.get_member(user_id)
if not member:
continue
c.execute("SELECT channel_id FROM guild_settings WHERE guild_id=?", (guild.id,))
row = c.fetchone()
if not row:
continue
channel_id = row[0]
channel= guild.get_channel(channel_id)
if channel:
try:
await channel.send(f"🎉 Happy Birthday, {member.mention}! 🎂")
except Exception:
pass
conn.close()
@check_birthdays.before_loop
async def before_check_birthdays(self):
await self.bot.wait_until_ready()
async def setup(bot):
await bot.add_cog(Birthday(bot))

5
bot.py
View File

@@ -68,6 +68,8 @@ async def on_ready():
await load_cogs("sparkle")
await load_cogs("image")
await load_cogs("suggestion")
await load_cogs("birthday")
# Sync slash commands after loading cogs
@@ -97,7 +99,8 @@ async def reload(ctx):
await load_cogs("sparkle")
await load_cogs("image")
await load_cogs("suggestion")
await load_cogs("birthday")
try:
synced = await bot.tree.sync()
await ctx.send(f"Cogs reloaded successfully! Synced {len(synced)} slash commands.")

View File

@@ -1,14 +1,32 @@
import discord
from discord.ext import commands
from discord import app_commands
import time
class Ping(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
@app_commands.command(name="ping", description="Replies with Pong!")
@app_commands.command(name="ping", description="Check the bot's latency")
async def ping(self, interaction: discord.Interaction):
await interaction.response.send_message("Pong!")
# Measure API latency (time to respond to the interaction)
start_time = time.perf_counter()
await interaction.response.send_message("Pinging...")
end_time = time.perf_counter()
api_latency = round((end_time - start_time) * 1000)
# Get WebSocket latency
ws_latency = round(self.bot.latency * 1000)
# Edit the message with the results
embed = discord.Embed(
title="Pong!",
color=discord.Color.green()
)
embed.add_field(name="WebSocket Latency", value=f"{ws_latency}ms", inline=True)
embed.add_field(name="API Latency", value=f"{api_latency}ms", inline=True)
await interaction.edit_original_response(content=None, embed=embed)
async def setup(bot: commands.Bot):
await bot.add_cog(Ping(bot))