mirror of
https://github.com/driftywinds/discord-timestamp-bot.git
synced 2025-12-19 15:03:32 +00:00
initialize bot
This commit is contained in:
27
Dockerfile
Normal file
27
Dockerfile
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Use Python 3.11 slim image
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Create a non-root user for security
|
||||||
|
RUN useradd --create-home --shell /bin/bash botuser
|
||||||
|
|
||||||
|
# Copy requirements file first (for better caching)
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
RUN pip install --no-cache-dir --upgrade pip && \
|
||||||
|
pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy the bot script
|
||||||
|
COPY bot.py .
|
||||||
|
|
||||||
|
# Change ownership of the app directory to the bot user
|
||||||
|
RUN chown -R botuser:botuser /app
|
||||||
|
|
||||||
|
# Switch to non-root user
|
||||||
|
USER botuser
|
||||||
|
|
||||||
|
# Run the bot
|
||||||
|
CMD ["python3", "bot.py"]
|
||||||
367
bot.py
Normal file
367
bot.py
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
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)
|
||||||
36
dockerignore
Normal file
36
dockerignore
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Ignore environment files (security)
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
# Ignore version control
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Ignore Python cache
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# Ignore IDE files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Ignore logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# Ignore documentation
|
||||||
|
README.md
|
||||||
|
*.md
|
||||||
|
|
||||||
|
# Ignore Docker files (not needed in container)
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
|
.dockerignore
|
||||||
5
example.env
Normal file
5
example.env
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Discord Bot Configuration
|
||||||
|
DISCORD_BOT_TOKEN=your_discord_bot_token_here
|
||||||
|
|
||||||
|
# Optional: Set log level (DEBUG, INFO, WARNING, ERROR)
|
||||||
|
# LOG_LEVEL=INFO
|
||||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
discord.py
|
||||||
|
pytz
|
||||||
|
python-dotenv
|
||||||
Reference in New Issue
Block a user