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"", "Long Time": f"", "Short Date": f"", "Long Date": f"", "Short Date/Time": f"", "Long Date/Time": f"", "Relative": f"" } # 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"", "Long Time": f"", "Short Date": f"", "Long Date": f"", "Short Date/Time": f"", "Long Date/Time": f"", "Relative": f"" } 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)