From bbaab6af048c87442e3a0606bcc508c73d14acc2 Mon Sep 17 00:00:00 2001 From: sharath3589 Date: Wed, 22 Oct 2025 11:40:52 +0530 Subject: [PATCH] feat: schedule posts at IST times regardless of server timezone - All scheduling now uses IST (Asia/Kolkata) timezone - Posts happen at 11:00, 16:00, 21:00 IST regardless of server location - Improved datetime handling with timezone-aware operations - Updated all display times to show IST explicitly --- bot.py | 71 +++++++++++++++++++++++++++------------------------------- 1 file changed, 33 insertions(+), 38 deletions(-) diff --git a/bot.py b/bot.py index ac81726..d6abc2f 100644 --- a/bot.py +++ b/bot.py @@ -8,6 +8,7 @@ from datetime import datetime, time, timedelta import aiosqlite from typing import Optional import io +import pytz from telegram import Update, Message, InputFile from telegram.ext import ApplicationBuilder, ContextTypes, CommandHandler, MessageHandler, filters @@ -15,6 +16,9 @@ from telegram.ext import ApplicationBuilder, ContextTypes, CommandHandler, Messa logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) +# IST timezone +IST = pytz.timezone('Asia/Kolkata') + DB_PATH = os.environ.get("MEMEBOT_DB", "memes.db") BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN") OWNER_ID = int(os.environ.get("OWNER_ID", "0")) @@ -45,18 +49,27 @@ async def init_db(): await db.commit() async def compute_next_slot(after_dt: Optional[datetime] = None) -> datetime: - """Return the next slot datetime from after_dt (exclusive). If after_dt is None, use now().""" + """Return the next slot datetime from after_dt (exclusive). If after_dt is None, use now() in IST. + All calculations and returns are in IST timezone.""" if after_dt is None: - after_dt = datetime.now() - # check same-day slots + # Get current time in IST + after_dt = datetime.now(IST) + else: + # Ensure after_dt is timezone-aware and in IST + if after_dt.tzinfo is None: + after_dt = IST.localize(after_dt) + else: + after_dt = after_dt.astimezone(IST) + + # check same-day slots in IST today = after_dt.date() for slot in SLOTS: - candidate = datetime.combine(today, slot) + candidate = IST.localize(datetime.combine(today, slot)) if candidate > after_dt: return candidate # otherwise next day's first slot next_day = today + timedelta(days=1) - return datetime.combine(next_day, SLOTS[0]) + return IST.localize(datetime.combine(next_day, SLOTS[0])) async def get_last_scheduled_ts(db) -> Optional[int]: async with db.execute("SELECT scheduled_ts FROM memes WHERE posted=0 ORDER BY scheduled_ts DESC LIMIT 1") as cur: @@ -69,10 +82,11 @@ async def schedule_meme(owner_file_id: str, mime_type: str) -> datetime: # Always schedule after the latest scheduled meme, even if it's far in the future last_ts = await get_last_scheduled_ts(db) if last_ts is None: - # no pending memes, schedule relative to now - ref_dt = datetime.now() + # no pending memes, schedule relative to now in IST + ref_dt = datetime.now(IST) else: - ref_dt = datetime.fromtimestamp(last_ts) + # Convert timestamp to IST-aware datetime + ref_dt = datetime.fromtimestamp(last_ts, tz=IST) next_dt = await compute_next_slot(ref_dt) # Try to get a preview file_id (for photo: smallest size, for video: thumbnail, for animation: itself) @@ -82,13 +96,13 @@ async def schedule_meme(owner_file_id: str, mime_type: str) -> datetime: await db.execute( "INSERT INTO memes (owner_file_id, mime_type, scheduled_ts, created_ts, preview_file_id) VALUES (?, ?, ?, ?, ?)", - (owner_file_id, mime_type, int(next_dt.timestamp()), int(datetime.now().timestamp()), preview_file_id), + (owner_file_id, mime_type, int(next_dt.timestamp()), int(datetime.now(IST).timestamp()), preview_file_id), ) await db.commit() return next_dt async def pop_due_memes_and_post(context: ContextTypes.DEFAULT_TYPE): - now_ts = int(datetime.now().timestamp()) + now_ts = int(datetime.now(IST).timestamp()) async with aiosqlite.connect(DB_PATH) as db: await init_db() async with db.execute("SELECT id, owner_file_id, mime_type FROM memes WHERE posted=0 AND scheduled_ts<=? ORDER BY scheduled_ts ASC", (now_ts,)) as cur: @@ -124,12 +138,12 @@ async def pop_due_memes_and_post(context: ContextTypes.DEFAULT_TYPE): await db.execute("UPDATE memes SET posted=1 WHERE id=?", (mid,)) await db.commit() logger.info("Posted meme id=%s", mid) - posting_log.append(f"[SUCCESS] Posted meme id={mid} at {datetime.now().isoformat(sep=' ')}") + posting_log.append(f"[SUCCESS] Posted meme id={mid} at {datetime.now(IST).isoformat(sep=' ')}") if len(posting_log) > 100: posting_log.pop(0) except Exception as e: logger.exception("Failed to post meme id=%s: %s", mid, e) - posting_log.append(f"[FAIL] Meme id={mid} at {datetime.now().isoformat(sep=' ')}: {type(e).__name__}: {e}") + posting_log.append(f"[FAIL] Meme id={mid} at {datetime.now(IST).isoformat(sep=' ')}: {type(e).__name__}: {e}") if len(posting_log) > 100: posting_log.pop(0) async def scheduled(update: Update, context: ContextTypes.DEFAULT_TYPE): @@ -173,7 +187,7 @@ async def scheduled(update: Update, context: ContextTypes.DEFAULT_TYPE): owner_file_id = owner_row[0] if owner_row else None file_id = preview_id if preview_id else owner_file_id - caption = f"ID: {mid}, Time: {datetime.fromtimestamp(ts).isoformat(sep=' ')}, Type: {mtype}" + caption = f"ID: {mid}, Time: {datetime.fromtimestamp(ts, tz=IST).strftime('%Y-%m-%d %H:%M:%S IST')}, Type: {mtype}" sent = False # Try direct sends with fallbacks @@ -392,15 +406,8 @@ async def handle_media(update: Update, context: ContextTypes.DEFAULT_TYPE): return scheduled_dt = await schedule_meme(file_id, mime) - # Convert to IST (Asia/Kolkata) - try: - import pytz - ist = pytz.timezone('Asia/Kolkata') - scheduled_dt_ist = scheduled_dt.astimezone(ist) - except Exception: - # fallback: add 5:30 manually if pytz not available - scheduled_dt_ist = scheduled_dt + timedelta(hours=5, minutes=30) - await msg.reply_text(f"Scheduled for: {scheduled_dt_ist.strftime('%Y-%m-%d %H:%M:%S')} IST") + # scheduled_dt is already in IST timezone + await msg.reply_text(f"Scheduled for: {scheduled_dt.strftime('%Y-%m-%d %H:%M:%S IST')}") async def postnow(update: Update, context: ContextTypes.DEFAULT_TYPE): user_id = update.effective_user.id @@ -465,15 +472,8 @@ async def scheduleat(update: Update, context: ContextTypes.DEFAULT_TYPE): await update.message.reply_text("Invalid time format. Use 24h HH:MM.") return # Schedule meme at specified time today (IST) - from datetime import datetime, timedelta - try: - import pytz - ist = pytz.timezone('Asia/Kolkata') - now_ist = datetime.now(ist) - sched_dt = ist.localize(datetime(now_ist.year, now_ist.month, now_ist.day, hour, minute)) - except Exception: - now = datetime.now() - sched_dt = now.replace(hour=hour, minute=minute, second=0, microsecond=0) + timedelta(hours=5, minutes=30) + now_ist = datetime.now(IST) + sched_dt = IST.localize(datetime(now_ist.year, now_ist.month, now_ist.day, hour, minute)) sched_ts = int(sched_dt.timestamp()) async with aiosqlite.connect(DB_PATH) as db: await init_db() @@ -486,13 +486,8 @@ async def scheduleat(update: Update, context: ContextTypes.DEFAULT_TYPE): start_id = int(m_range.group(1)) end_id = int(m_range.group(2)) date_str = m_range.group(3) - from datetime import datetime, timedelta, time as dtime - try: - import pytz - ist = pytz.timezone('Asia/Kolkata') - base_date = ist.localize(datetime.strptime(date_str, '%Y-%m-%d')) - except Exception: - base_date = datetime.strptime(date_str, '%Y-%m-%d') + timedelta(hours=5, minutes=30) + from datetime import time as dtime + base_date = IST.localize(datetime.strptime(date_str, '%Y-%m-%d')) # Assign slots in order: 11:00, 16:00, 21:00, repeat slot_times = [dtime(11,0), dtime(16,0), dtime(21,0)] ids = list(range(start_id, end_id+1))