mirror of
https://github.com/driftywinds/discord-timestamp-bot.git
synced 2025-12-19 06:53:33 +00:00
367 lines
13 KiB
Python
367 lines
13 KiB
Python
import discord
|
|
from discord.ext import commands
|
|
from discord import app_commands
|
|
import datetime
|
|
import pytz
|
|
import re
|
|
import os
|
|
from typing import Optional, List
|
|
from dotenv import load_dotenv
|
|
|
|
# Load environment variables from .env file
|
|
load_dotenv()
|
|
|
|
# Bot setup
|
|
intents = discord.Intents.default()
|
|
intents.message_content = True
|
|
bot = commands.Bot(command_prefix='!', intents=intents)
|
|
|
|
class TimestampBot(commands.Bot):
|
|
def __init__(self):
|
|
super().__init__(command_prefix='!', intents=intents)
|
|
|
|
async def setup_hook(self):
|
|
# Sync slash commands
|
|
await self.tree.sync()
|
|
print(f"Synced slash commands for {self.user}")
|
|
|
|
bot = TimestampBot()
|
|
|
|
@bot.event
|
|
async def on_ready():
|
|
print(f'{bot.user} has landed!')
|
|
print(f'Bot is ready and can be used in DMs and servers!')
|
|
|
|
def parse_offset(offset_str: str) -> Optional[datetime.timezone]:
|
|
"""Parse UTC offset string like +05:30, -08:00, etc."""
|
|
match = re.match(r'^([+-])(\d{1,2}):?(\d{2})$', offset_str.strip())
|
|
if not match:
|
|
return None
|
|
|
|
sign, hours, minutes = match.groups()
|
|
total_minutes = int(hours) * 60 + int(minutes)
|
|
if sign == '-':
|
|
total_minutes = -total_minutes
|
|
|
|
return datetime.timezone(datetime.timedelta(minutes=total_minutes))
|
|
|
|
def parse_datetime_input(date_str: str, time_str: str = None) -> Optional[datetime.datetime]:
|
|
"""Parse date and optional time strings into datetime object."""
|
|
try:
|
|
# If time is not provided, assume the date string might contain time too
|
|
if time_str is None:
|
|
# Try parsing full datetime string
|
|
for fmt in ['%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M', '%m/%d/%Y %H:%M:%S',
|
|
'%m/%d/%Y %H:%M', '%d/%m/%Y %H:%M:%S', '%d/%m/%Y %H:%M']:
|
|
try:
|
|
return datetime.datetime.strptime(date_str, fmt)
|
|
except ValueError:
|
|
continue
|
|
|
|
# Try parsing just date
|
|
for fmt in ['%Y-%m-%d', '%m/%d/%Y', '%d/%m/%Y']:
|
|
try:
|
|
return datetime.datetime.strptime(date_str, fmt)
|
|
except ValueError:
|
|
continue
|
|
else:
|
|
# Parse date and time separately
|
|
date_part = None
|
|
for fmt in ['%Y-%m-%d', '%m/%d/%Y', '%d/%m/%Y']:
|
|
try:
|
|
date_part = datetime.datetime.strptime(date_str, fmt).date()
|
|
break
|
|
except ValueError:
|
|
continue
|
|
|
|
if date_part is None:
|
|
return None
|
|
|
|
time_part = None
|
|
for fmt in ['%H:%M:%S', '%H:%M', '%I:%M:%S %p', '%I:%M %p']:
|
|
try:
|
|
time_part = datetime.datetime.strptime(time_str, fmt).time()
|
|
break
|
|
except ValueError:
|
|
continue
|
|
|
|
if time_part is None:
|
|
return None
|
|
|
|
return datetime.datetime.combine(date_part, time_part)
|
|
|
|
return None
|
|
except:
|
|
return None
|
|
|
|
def get_timezone(timezone_str: str) -> Optional[datetime.tzinfo]:
|
|
"""Get timezone object from string."""
|
|
try:
|
|
return pytz.timezone(timezone_str)
|
|
except pytz.UnknownTimeZoneError:
|
|
tz = parse_offset(timezone_str)
|
|
if tz is None and timezone_str.upper() == 'UTC':
|
|
return pytz.UTC
|
|
return tz
|
|
|
|
async def timezone_autocomplete(interaction: discord.Interaction, current: str) -> List[app_commands.Choice[str]]:
|
|
"""Autocomplete for timezone field."""
|
|
common_timezones = [
|
|
"UTC", "America/New_York", "America/Chicago", "America/Denver", "America/Los_Angeles",
|
|
"Europe/London", "Europe/Paris", "Europe/Berlin", "Europe/Rome", "Europe/Amsterdam",
|
|
"Asia/Tokyo", "Asia/Shanghai", "Asia/Kolkata", "Asia/Dubai", "Australia/Sydney",
|
|
"America/Toronto", "America/Mexico_City", "Pacific/Auckland"
|
|
]
|
|
|
|
# Filter timezones based on current input
|
|
if current:
|
|
filtered = [tz for tz in common_timezones if current.lower() in tz.lower()]
|
|
else:
|
|
filtered = common_timezones[:10] # Show first 10 if no input
|
|
|
|
return [app_commands.Choice(name=tz, value=tz) for tz in filtered[:25]] # Discord limits to 25
|
|
|
|
@bot.tree.command(name="timestamp", description="Create Discord timestamps from date, time, and timezone")
|
|
@app_commands.describe(
|
|
date="Date in YYYY-MM-DD, MM/DD/YYYY, or DD/MM/YYYY format",
|
|
timezone="Timezone (IANA name like America/New_York or UTC offset like +05:30)",
|
|
time="Optional: Time in HH:MM, HH:MM:SS, or HH:MM AM/PM format"
|
|
)
|
|
@app_commands.autocomplete(timezone=timezone_autocomplete)
|
|
async def slash_timestamp(interaction: discord.Interaction, date: str, timezone: str, time: str = None):
|
|
"""Slash command version of timestamp creation."""
|
|
await interaction.response.defer(ephemeral=False)
|
|
|
|
# Parse timezone
|
|
tz = get_timezone(timezone)
|
|
if tz is None:
|
|
await interaction.followup.send(
|
|
f"❌ Unknown timezone: `{timezone}`\n"
|
|
"Use IANA names (e.g., `America/New_York`) or UTC offsets (e.g., `+05:30`, `-08:00`)",
|
|
ephemeral=True
|
|
)
|
|
return
|
|
|
|
# Parse datetime
|
|
dt = parse_datetime_input(date, time)
|
|
if dt is None:
|
|
await interaction.followup.send(
|
|
"❌ Could not parse date/time. Supported formats:\n"
|
|
"• `YYYY-MM-DD` (date only)\n"
|
|
"• `YYYY-MM-DD` with separate time field\n"
|
|
"• `MM/DD/YYYY` or `DD/MM/YYYY`\n"
|
|
"• Time: `HH:MM`, `HH:MM:SS`, or `HH:MM AM/PM`",
|
|
ephemeral=True
|
|
)
|
|
return
|
|
|
|
# Localize the datetime to the specified timezone
|
|
try:
|
|
if isinstance(tz, pytz.BaseTzInfo):
|
|
localized_dt = tz.localize(dt)
|
|
else:
|
|
localized_dt = dt.replace(tzinfo=tz)
|
|
except:
|
|
await interaction.followup.send("❌ Error applying timezone to the datetime.", ephemeral=True)
|
|
return
|
|
|
|
# Convert to Unix timestamp
|
|
unix_timestamp = int(localized_dt.timestamp())
|
|
|
|
# Create Discord timestamp formats
|
|
formats = {
|
|
"Short Time": f"<t:{unix_timestamp}:t>",
|
|
"Long Time": f"<t:{unix_timestamp}:T>",
|
|
"Short Date": f"<t:{unix_timestamp}:d>",
|
|
"Long Date": f"<t:{unix_timestamp}:D>",
|
|
"Short Date/Time": f"<t:{unix_timestamp}:f>",
|
|
"Long Date/Time": f"<t:{unix_timestamp}:F>",
|
|
"Relative": f"<t:{unix_timestamp}:R>"
|
|
}
|
|
|
|
# Create embed
|
|
embed = discord.Embed(
|
|
title="📅 Discord Timestamp Generated",
|
|
description=f"**Input:** {localized_dt.strftime('%Y-%m-%d %H:%M:%S %Z')}",
|
|
color=0x5865F2
|
|
)
|
|
|
|
# Add timestamp formats with previews
|
|
for name, timestamp in formats.items():
|
|
embed.add_field(
|
|
name=f"{name}: {timestamp}",
|
|
value=f"`{timestamp}`",
|
|
inline=False
|
|
)
|
|
|
|
embed.set_footer(text="💡 Copy any timestamp code above to use in your messages!")
|
|
|
|
await interaction.followup.send(embed=embed)
|
|
|
|
@bot.tree.command(name="timezones", description="List common timezone names for use with /timestamp")
|
|
async def slash_timezones(interaction: discord.Interaction):
|
|
"""Show common timezone names."""
|
|
embed = discord.Embed(
|
|
title="🌍 Common Timezone Names",
|
|
description="Here are commonly used IANA timezone names and UTC offsets:",
|
|
color=0x5865F2
|
|
)
|
|
|
|
embed.add_field(
|
|
name="🇺🇸 Americas",
|
|
value="• America/New_York (EST/EDT)\n• America/Chicago (CST/CDT)\n• America/Denver (MST/MDT)\n• America/Los_Angeles (PST/PDT)\n• America/Toronto",
|
|
inline=True
|
|
)
|
|
|
|
embed.add_field(
|
|
name="🇪🇺 Europe",
|
|
value="• Europe/London (GMT/BST)\n• Europe/Paris (CET/CEST)\n• Europe/Berlin\n• Europe/Amsterdam\n• Europe/Rome",
|
|
inline=True
|
|
)
|
|
|
|
embed.add_field(
|
|
name="🌏 Asia/Pacific",
|
|
value="• Asia/Tokyo (JST)\n• Asia/Shanghai (CST)\n• Asia/Kolkata (IST)\n• Australia/Sydney\n• Asia/Dubai",
|
|
inline=True
|
|
)
|
|
|
|
embed.add_field(
|
|
name="🕐 UTC Offsets",
|
|
value="You can also use UTC offsets:\n`+05:30`, `-08:00`, `+00:00`, `UTC`\n\nExample: `+05:30` for India Standard Time",
|
|
inline=False
|
|
)
|
|
|
|
embed.add_field(
|
|
name="💡 Usage Tips",
|
|
value="• Use `/timestamp` to create Discord timestamps\n• Start typing timezone names for autocomplete\n• Works in DMs and all server channels!",
|
|
inline=False
|
|
)
|
|
|
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
|
|
|
# Keep the original prefix commands for backward compatibility
|
|
@bot.command(name='timestamp', aliases=['ts'])
|
|
async def create_timestamp(ctx, *, input_text: str):
|
|
"""
|
|
Create a Discord timestamp from date, time, and timezone.
|
|
This is the legacy prefix command - use /timestamp for the modern slash command!
|
|
"""
|
|
parts = input_text.strip().split()
|
|
|
|
if len(parts) < 2:
|
|
await ctx.send("❌ Please provide at least a date and timezone.\n"
|
|
"**Tip:** Try the new `/timestamp` slash command for a better experience!\n"
|
|
"Examples:\n"
|
|
"`!timestamp 2024-12-25 America/New_York`\n"
|
|
"`!timestamp 2024-12-25 15:30 +05:30`")
|
|
return
|
|
|
|
timezone_str = parts[-1]
|
|
datetime_parts = parts[:-1]
|
|
|
|
tz = get_timezone(timezone_str)
|
|
if tz is None:
|
|
await ctx.send(f"❌ Unknown timezone: `{timezone_str}`\n"
|
|
"Use IANA names (e.g., `America/New_York`) or UTC offsets (e.g., `+05:30`, `-08:00`)")
|
|
return
|
|
|
|
dt = None
|
|
if len(datetime_parts) == 1:
|
|
dt = parse_datetime_input(datetime_parts[0])
|
|
elif len(datetime_parts) == 2:
|
|
dt = parse_datetime_input(datetime_parts[0], datetime_parts[1])
|
|
else:
|
|
dt = parse_datetime_input(' '.join(datetime_parts))
|
|
|
|
if dt is None:
|
|
await ctx.send("❌ Could not parse date/time. Supported formats:\n"
|
|
"• `YYYY-MM-DD` (date only)\n"
|
|
"• `YYYY-MM-DD HH:MM` or `YYYY-MM-DD HH:MM:SS`\n"
|
|
"• `MM/DD/YYYY` or `DD/MM/YYYY`")
|
|
return
|
|
|
|
try:
|
|
if isinstance(tz, pytz.BaseTzInfo):
|
|
localized_dt = tz.localize(dt)
|
|
else:
|
|
localized_dt = dt.replace(tzinfo=tz)
|
|
except:
|
|
await ctx.send("❌ Error applying timezone to the datetime.")
|
|
return
|
|
|
|
unix_timestamp = int(localized_dt.timestamp())
|
|
|
|
formats = {
|
|
"Short Time": f"<t:{unix_timestamp}:t>",
|
|
"Long Time": f"<t:{unix_timestamp}:T>",
|
|
"Short Date": f"<t:{unix_timestamp}:d>",
|
|
"Long Date": f"<t:{unix_timestamp}:D>",
|
|
"Short Date/Time": f"<t:{unix_timestamp}:f>",
|
|
"Long Date/Time": f"<t:{unix_timestamp}:F>",
|
|
"Relative": f"<t:{unix_timestamp}:R>"
|
|
}
|
|
|
|
embed = discord.Embed(
|
|
title="📅 Discord Timestamp Generated",
|
|
description=f"**Input:** {localized_dt.strftime('%Y-%m-%d %H:%M:%S %Z')}\n*Try `/timestamp` for the modern slash command experience!*",
|
|
color=0x5865F2
|
|
)
|
|
|
|
for name, timestamp in formats.items():
|
|
embed.add_field(
|
|
name=f"{name}: {timestamp}",
|
|
value=f"`{timestamp}`",
|
|
inline=False
|
|
)
|
|
|
|
embed.set_footer(text="💡 Copy any timestamp code above to use in your messages!")
|
|
await ctx.send(embed=embed)
|
|
|
|
@bot.command(name='timezones', aliases=['tz'])
|
|
async def list_common_timezones(ctx):
|
|
"""List common timezone names - use /timezones for the modern slash command!"""
|
|
embed = discord.Embed(
|
|
title="🌍 Common Timezone Names",
|
|
description="*Try `/timezones` for the modern slash command experience!*\n\nCommon IANA timezone names:",
|
|
color=0x5865F2
|
|
)
|
|
|
|
embed.add_field(
|
|
name="Americas",
|
|
value="• America/New_York\n• America/Chicago\n• America/Denver\n• America/Los_Angeles",
|
|
inline=True
|
|
)
|
|
|
|
embed.add_field(
|
|
name="Europe",
|
|
value="• Europe/London\n• Europe/Paris\n• Europe/Berlin\n• Europe/Amsterdam",
|
|
inline=True
|
|
)
|
|
|
|
embed.add_field(
|
|
name="Asia/Pacific",
|
|
value="• Asia/Tokyo\n• Asia/Shanghai\n• Asia/Kolkata\n• Australia/Sydney",
|
|
inline=True
|
|
)
|
|
|
|
await ctx.send(embed=embed)
|
|
|
|
@bot.tree.error
|
|
async def on_app_command_error(interaction: discord.Interaction, error: app_commands.AppCommandError):
|
|
"""Handle slash command errors."""
|
|
if isinstance(error, app_commands.CommandOnCooldown):
|
|
await interaction.response.send_message(f"Command is on cooldown. Try again in {error.retry_after:.2f} seconds.", ephemeral=True)
|
|
else:
|
|
await interaction.response.send_message("An error occurred while processing your command.", ephemeral=True)
|
|
print(f"Slash command error: {error}")
|
|
|
|
# Replace 'YOUR_BOT_TOKEN' with your actual bot token
|
|
if __name__ == "__main__":
|
|
token = os.getenv('DISCORD_BOT_TOKEN')
|
|
if not token:
|
|
print("❌ Error: DISCORD_BOT_TOKEN not found in environment variables!")
|
|
print("Please create a .env file with: DISCORD_BOT_TOKEN=your_token_here")
|
|
exit(1)
|
|
|
|
print("🤖 Starting Discord Timestamp Bot...")
|
|
bot.run(token) |