553 lines
24 KiB
Python
553 lines
24 KiB
Python
posting_log = [] # in-memory log
|
|
|
|
|
|
import asyncio
|
|
import os
|
|
import logging
|
|
from datetime import datetime, time, timedelta
|
|
import aiosqlite
|
|
from typing import Optional
|
|
import io
|
|
|
|
from telegram import Update, Message, InputFile
|
|
from telegram.ext import ApplicationBuilder, ContextTypes, CommandHandler, MessageHandler, filters
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
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"))
|
|
CHANNEL_ID = os.environ.get("CHANNEL_ID") # @channelusername or -100<id>
|
|
|
|
SLOTS = [time(11, 0), time(16, 0), time(21, 0)]
|
|
|
|
async def init_db():
|
|
async with aiosqlite.connect(DB_PATH) as db:
|
|
await db.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS memes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
owner_file_id TEXT NOT NULL,
|
|
mime_type TEXT,
|
|
scheduled_ts INTEGER NOT NULL,
|
|
posted INTEGER DEFAULT 0,
|
|
created_ts INTEGER NOT NULL
|
|
)
|
|
"""
|
|
)
|
|
# Ensure preview_file_id column exists (migration)
|
|
async with db.execute("PRAGMA table_info(memes)") as cur:
|
|
cols = await cur.fetchall()
|
|
col_names = [c[1] for c in cols]
|
|
if 'preview_file_id' not in col_names:
|
|
await db.execute("ALTER TABLE memes ADD COLUMN preview_file_id TEXT")
|
|
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()."""
|
|
if after_dt is None:
|
|
after_dt = datetime.now()
|
|
# check same-day slots
|
|
today = after_dt.date()
|
|
for slot in SLOTS:
|
|
candidate = 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])
|
|
|
|
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:
|
|
row = await cur.fetchone()
|
|
return row[0] if row else None
|
|
|
|
async def schedule_meme(owner_file_id: str, mime_type: str) -> datetime:
|
|
async with aiosqlite.connect(DB_PATH) as db:
|
|
await init_db()
|
|
# 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()
|
|
else:
|
|
ref_dt = datetime.fromtimestamp(last_ts)
|
|
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)
|
|
preview_file_id = None
|
|
# context is not available here, so preview is best-effort: use owner_file_id for now
|
|
preview_file_id = owner_file_id
|
|
|
|
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),
|
|
)
|
|
await db.commit()
|
|
return next_dt
|
|
|
|
async def pop_due_memes_and_post(context: ContextTypes.DEFAULT_TYPE):
|
|
now_ts = int(datetime.now().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:
|
|
rows = await cur.fetchall()
|
|
for row in rows:
|
|
mid, file_id, mime = row
|
|
try:
|
|
sent = False
|
|
# Try video first when appropriate
|
|
if mime and mime.startswith("video"):
|
|
try:
|
|
await context.bot.send_video(CHANNEL_ID, file_id)
|
|
sent = True
|
|
except Exception as e_video:
|
|
logger.warning("send_video failed for id=%s: %s", mid, e_video)
|
|
if not sent:
|
|
# try as photo/animation
|
|
try:
|
|
await context.bot.send_photo(CHANNEL_ID, file_id)
|
|
sent = True
|
|
except Exception as e_photo:
|
|
logger.warning("send_photo failed for id=%s: %s", mid, e_photo)
|
|
# fallback to sending as document
|
|
try:
|
|
await context.bot.send_document(CHANNEL_ID, file_id)
|
|
sent = True
|
|
except Exception as e_doc:
|
|
logger.warning("send_document failed for id=%s: %s", mid, e_doc)
|
|
# raise the last exception to be caught below
|
|
raise e_doc
|
|
|
|
if sent:
|
|
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=' ')}")
|
|
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}")
|
|
if len(posting_log) > 100:
|
|
posting_log.pop(0)
|
|
async def scheduled(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
user_id = update.effective_user.id
|
|
if user_id != OWNER_ID:
|
|
await update.message.reply_text("Only the owner can use this command.")
|
|
return
|
|
|
|
async with aiosqlite.connect(DB_PATH) as db:
|
|
await init_db()
|
|
# Check if preview_file_id column exists to avoid sqlite errors on older DBs
|
|
async with db.execute("PRAGMA table_info(memes)") as pcur:
|
|
cols = await pcur.fetchall()
|
|
col_names = [c[1] for c in cols]
|
|
if 'preview_file_id' in col_names:
|
|
query = "SELECT id, scheduled_ts, mime_type, preview_file_id FROM memes WHERE posted=0 ORDER BY scheduled_ts ASC"
|
|
has_preview = True
|
|
else:
|
|
query = "SELECT id, scheduled_ts, mime_type FROM memes WHERE posted=0 ORDER BY scheduled_ts ASC"
|
|
has_preview = False
|
|
async with db.execute(query) as cur:
|
|
rows = await cur.fetchall()
|
|
|
|
if not rows:
|
|
await update.message.reply_text("No scheduled memes.")
|
|
return
|
|
|
|
# For each scheduled item, try to send a preview robustly (direct send, then download+reupload)
|
|
for row in rows:
|
|
if has_preview:
|
|
mid, ts, mtype, preview_id = row
|
|
else:
|
|
mid, ts, mtype = row
|
|
preview_id = None
|
|
|
|
# Fallback: if preview_id is missing/null, use owner_file_id
|
|
async with aiosqlite.connect(DB_PATH) as db:
|
|
await init_db()
|
|
async with db.execute("SELECT owner_file_id FROM memes WHERE id=?", (mid,)) as cur:
|
|
owner_row = await cur.fetchone()
|
|
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}"
|
|
|
|
sent = False
|
|
# Try direct sends with fallbacks
|
|
try:
|
|
if mtype and mtype.startswith('video'):
|
|
try:
|
|
if file_id:
|
|
await context.bot.send_video(update.effective_chat.id, file_id, caption=caption)
|
|
sent = True
|
|
except Exception as e: # direct video failed
|
|
logger.debug("scheduled: direct send_video failed for id=%s: %s", mid, e)
|
|
|
|
if not sent and file_id:
|
|
try:
|
|
await context.bot.send_photo(update.effective_chat.id, file_id, caption=caption)
|
|
sent = True
|
|
except Exception as e:
|
|
logger.debug("scheduled: direct send_photo failed for id=%s: %s", mid, e)
|
|
try:
|
|
await context.bot.send_document(update.effective_chat.id, file_id, caption=caption)
|
|
sent = True
|
|
except Exception as e2:
|
|
logger.debug("scheduled: direct send_document failed for id=%s: %s", mid, e2)
|
|
|
|
if not sent and file_id:
|
|
# Attempt download + reupload
|
|
try:
|
|
file = await context.bot.get_file(file_id)
|
|
bio = io.BytesIO()
|
|
await file.download(out=bio)
|
|
bio.seek(0)
|
|
if mtype and mtype.startswith('video'):
|
|
await context.bot.send_video(update.effective_chat.id, InputFile(bio, filename=f"meme_{mid}.mp4"), caption=caption)
|
|
else:
|
|
try:
|
|
await context.bot.send_photo(update.effective_chat.id, InputFile(bio, filename=f"meme_{mid}.jpg"), caption=caption)
|
|
except Exception:
|
|
bio.seek(0)
|
|
await context.bot.send_document(update.effective_chat.id, InputFile(bio, filename=f"meme_{mid}"), caption=caption)
|
|
sent = True
|
|
except Exception as e:
|
|
logger.debug("scheduled: download+reupload failed for id=%s: %s", mid, e)
|
|
|
|
except Exception as e:
|
|
logger.exception("Unexpected error while previewing scheduled id=%s: %s", mid, e)
|
|
|
|
if not sent:
|
|
# If all attempts fail, send a text placeholder
|
|
await update.message.reply_text(caption)
|
|
async def unschedule(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
user_id = update.effective_user.id
|
|
if user_id != OWNER_ID:
|
|
await update.message.reply_text("Only the owner can use this command.")
|
|
return
|
|
if not context.args or not all(arg.isdigit() for arg in context.args):
|
|
await update.message.reply_text("Usage: /unschedule <id1> <id2> ...")
|
|
return
|
|
meme_ids = [int(arg) for arg in context.args]
|
|
async with aiosqlite.connect(DB_PATH) as db:
|
|
await init_db()
|
|
for meme_id in meme_ids:
|
|
await db.execute("DELETE FROM memes WHERE id=? AND posted=0", (meme_id,))
|
|
await db.commit()
|
|
await update.message.reply_text(f"Unscheduled memes with IDs: {', '.join(str(mid) for mid in meme_ids)} (if they existed and were not posted yet).")
|
|
|
|
|
|
async def preview(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""Preview a scheduled meme by id. Tries direct send, then downloads and reuploads as a document if needed."""
|
|
user_id = update.effective_user.id
|
|
if user_id != OWNER_ID:
|
|
await update.message.reply_text("Only the owner can use this command.")
|
|
return
|
|
if not context.args or not context.args[0].isdigit():
|
|
await update.message.reply_text("Usage: /preview <id>")
|
|
return
|
|
meme_id = int(context.args[0])
|
|
# immediate ack so owner knows the command was received
|
|
try:
|
|
await update.message.reply_text(f"Previewing meme {meme_id}...")
|
|
except Exception:
|
|
logger.debug("Could not send ack reply for preview %s", meme_id)
|
|
async with aiosqlite.connect(DB_PATH) as db:
|
|
await init_db()
|
|
async with db.execute("SELECT owner_file_id, mime_type FROM memes WHERE id=?", (meme_id,)) as cur:
|
|
row = await cur.fetchone()
|
|
if not row:
|
|
await update.message.reply_text(f"No meme found with ID {meme_id}.")
|
|
return
|
|
file_id, mime = row
|
|
chat_id = update.effective_chat.id
|
|
# Try direct sends with fallbacks
|
|
try:
|
|
if mime and mime.startswith("video"):
|
|
await context.bot.send_video(chat_id, file_id, caption=f"Preview ID {meme_id}")
|
|
return
|
|
try:
|
|
await context.bot.send_photo(chat_id, file_id, caption=f"Preview ID {meme_id}")
|
|
return
|
|
except Exception as e_photo:
|
|
logger.debug("Direct send_photo failed for preview id=%s: %s", meme_id, e_photo)
|
|
# try send_document quick fallback
|
|
try:
|
|
await context.bot.send_document(chat_id, file_id, caption=f"Preview ID {meme_id}")
|
|
return
|
|
except Exception as e_doc:
|
|
logger.debug("Direct send_document failed for preview id=%s: %s", meme_id, e_doc)
|
|
# If direct fails, download and reupload
|
|
file = await context.bot.get_file(file_id)
|
|
bio = io.BytesIO()
|
|
await file.download(out=bio)
|
|
bio.seek(0)
|
|
# pick send method based on mime
|
|
if mime and mime.startswith("video"):
|
|
await context.bot.send_video(chat_id, InputFile(bio, filename=f"meme_{meme_id}.mp4"), caption=f"Preview ID {meme_id}")
|
|
else:
|
|
# try as photo first, then document
|
|
try:
|
|
await context.bot.send_photo(chat_id, InputFile(bio, filename=f"meme_{meme_id}.jpg"), caption=f"Preview ID {meme_id}")
|
|
except Exception:
|
|
bio.seek(0)
|
|
await context.bot.send_document(chat_id, InputFile(bio, filename=f"meme_{meme_id}"), caption=f"Preview ID {meme_id}")
|
|
except Exception as e:
|
|
logger.exception("Preview failed for id=%s: %s", meme_id, e)
|
|
await update.message.reply_text(f"Failed to preview meme {meme_id}: {type(e).__name__}: {e}")
|
|
|
|
async def logcmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
user_id = update.effective_user.id
|
|
if user_id != OWNER_ID:
|
|
await update.message.reply_text("Only the owner can use this command.")
|
|
return
|
|
if not posting_log:
|
|
await update.message.reply_text("No posting events yet.")
|
|
return
|
|
await update.message.reply_text("Last posting events:\n" + "\n".join(posting_log[-10:]))
|
|
|
|
async def periodic_poster(application):
|
|
while True:
|
|
try:
|
|
await pop_due_memes_and_post(application)
|
|
except Exception:
|
|
logger.exception("Error in poster loop")
|
|
await asyncio.sleep(30)
|
|
|
|
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
await update.message.reply_text("Hi! I schedule memes to the configured channel.")
|
|
|
|
async def helpcmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""Send a detailed help message with all commands."""
|
|
help_text = (
|
|
"""
|
|
<b>🤖 <u>Memebot Command Reference</u> 🤖</b>
|
|
|
|
<b>General:</b>
|
|
<b>/start</b> — Show a welcome message.
|
|
<b>/help</b> — Show this help message.
|
|
|
|
<b>Scheduling Memes:</b>
|
|
<b>Send a photo/video/animation</b> (as a DM to the bot):
|
|
Schedules it for the next available slot (11:00, 16:00, 21:00 IST).
|
|
<i>Example:</i> Send a meme to the bot in DM.
|
|
|
|
<b>/scheduled</b> — List all scheduled memes with previews and their IDs, times, and types.
|
|
|
|
<b>/unschedule <id1> [<id2> ...]</b> — Remove one or more memes from the schedule (by ID).
|
|
<i>Example:</i> <code>/unschedule 3 5 7</code>
|
|
|
|
<b>/postnow [id]</b> — Immediately post the next scheduled meme, or a specific meme by ID.
|
|
<i>Example:</i> <code>/postnow</code> or <code>/postnow 6</code>
|
|
|
|
<b>/preview <id></b> — Preview a scheduled meme by its ID.
|
|
<i>Example:</i> <code>/preview 4</code>
|
|
|
|
<b>/log</b> — Show the last 10 posting events (success/failure log).
|
|
|
|
<b>Advanced Scheduling:</b>
|
|
<b>/scheduleat id: <id> <HH:MM></b> — Reschedule a single meme to a specific time (24h, IST).
|
|
<i>Example:</i> <code>/scheduleat id: 6 16:20</code>
|
|
|
|
<b>/scheduleat ids: <start>-<end> <YYYY-MM-DD></b> — Reschedule a range of memes to a date, assigning slots (11:00, 16:00, 21:00 IST) in order.
|
|
<i>Example:</i> <code>/scheduleat ids: 5-10 2025-10-19</code>
|
|
|
|
<b>Notes:</b>
|
|
• <b>Only the owner</b> (set by OWNER_ID) can use admin commands.
|
|
• All times are in <b>IST (Asia/Kolkata)</b>.
|
|
• Meme IDs are shown in <b>/scheduled</b> previews.
|
|
• Use <b>/preview</b> to check a meme before posting.
|
|
|
|
<b>✨ Enjoy effortless meme scheduling! ✨</b>
|
|
"""
|
|
)
|
|
await update.message.reply_text(help_text, parse_mode="HTML", disable_web_page_preview=True)
|
|
|
|
async def handle_media(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
msg: Message = update.message
|
|
user_id = msg.from_user.id
|
|
if user_id != OWNER_ID:
|
|
await msg.reply_text("Sorry, only the owner can send memes to schedule.")
|
|
return
|
|
|
|
# Determine the best file id and mime
|
|
file_id = None
|
|
mime = None
|
|
if msg.photo:
|
|
# highest resolution
|
|
file = msg.photo[-1]
|
|
file_id = file.file_id
|
|
mime = 'image'
|
|
elif msg.video:
|
|
file_id = msg.video.file_id
|
|
mime = 'video'
|
|
elif msg.animation:
|
|
file_id = msg.animation.file_id
|
|
mime = 'image' # gifs treated as image
|
|
else:
|
|
await msg.reply_text("Please send a photo, animation (GIF) or video.")
|
|
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")
|
|
|
|
async def postnow(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
user_id = update.effective_user.id
|
|
if user_id != OWNER_ID:
|
|
await update.message.reply_text("Only the owner can use this command.")
|
|
return
|
|
|
|
# If an ID is provided, post that meme; else, post the next scheduled meme
|
|
meme_id = None
|
|
if context.args and context.args[0].isdigit():
|
|
meme_id = int(context.args[0])
|
|
|
|
async with aiosqlite.connect(DB_PATH) as db:
|
|
await init_db()
|
|
if meme_id is not None:
|
|
async with db.execute("SELECT id, owner_file_id, mime_type FROM memes WHERE posted=0 AND id=?", (meme_id,)) as cur:
|
|
row = await cur.fetchone()
|
|
if not row:
|
|
await update.message.reply_text(f"No scheduled meme with ID {meme_id} to post.")
|
|
return
|
|
else:
|
|
async with db.execute("SELECT id, owner_file_id, mime_type FROM memes WHERE posted=0 ORDER BY scheduled_ts ASC LIMIT 1") as cur:
|
|
row = await cur.fetchone()
|
|
if not row:
|
|
await update.message.reply_text("No scheduled memes to post.")
|
|
return
|
|
mid, file_id, mime = row
|
|
try:
|
|
if mime and mime.startswith("video"):
|
|
await context.bot.send_video(CHANNEL_ID, file_id)
|
|
else:
|
|
await context.bot.send_photo(CHANNEL_ID, file_id)
|
|
await db.execute("UPDATE memes SET posted=1 WHERE id=?", (mid,))
|
|
await db.commit()
|
|
await update.message.reply_text(f"Posted meme with ID {mid} to channel.")
|
|
except Exception as e:
|
|
await update.message.reply_text(f"Failed to post meme: {e}")
|
|
|
|
import re
|
|
|
|
async def scheduleat(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
user_id = update.effective_user.id
|
|
if user_id != OWNER_ID:
|
|
await update.message.reply_text("Only the owner can use this command.")
|
|
return
|
|
if not context.args or len(context.args) < 2:
|
|
await update.message.reply_text("Usage: /scheduleat id: <id> <HH:MM> or /scheduleat ids: <start>-<end> <YYYY-MM-DD>")
|
|
return
|
|
|
|
argstr = ' '.join(context.args)
|
|
# Single ID mode: /scheduleat id: 6 16:20
|
|
m_single = re.match(r'id:\s*(\d+)\s+(\d{2}):(\d{2})$', argstr)
|
|
# Range mode: /scheduleat ids: 5-10 2025-10-19
|
|
m_range = re.match(r'ids:\s*(\d+)-(\d+)\s+(\d{4}-\d{2}-\d{2})$', argstr)
|
|
|
|
if m_single:
|
|
meme_id = int(m_single.group(1))
|
|
hour = int(m_single.group(2))
|
|
minute = int(m_single.group(3))
|
|
# Validate time
|
|
if not (0 <= hour < 24 and 0 <= minute < 60):
|
|
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)
|
|
sched_ts = int(sched_dt.timestamp())
|
|
async with aiosqlite.connect(DB_PATH) as db:
|
|
await init_db()
|
|
await db.execute("UPDATE memes SET scheduled_ts=? WHERE id=? AND posted=0", (sched_ts, meme_id))
|
|
await db.commit()
|
|
await update.message.reply_text(f"Rescheduled meme ID {meme_id} for {sched_dt.strftime('%Y-%m-%d %H:%M')} IST.")
|
|
return
|
|
|
|
elif m_range:
|
|
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)
|
|
# 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))
|
|
updates = []
|
|
for idx, meme_id in enumerate(ids):
|
|
slot = slot_times[idx % len(slot_times)]
|
|
sched_dt = base_date.replace(hour=slot.hour, minute=slot.minute, second=0, microsecond=0)
|
|
sched_ts = int(sched_dt.timestamp())
|
|
updates.append((sched_ts, meme_id))
|
|
async with aiosqlite.connect(DB_PATH) as db:
|
|
await init_db()
|
|
for sched_ts, meme_id in updates:
|
|
await db.execute("UPDATE memes SET scheduled_ts=? WHERE id=? AND posted=0", (sched_ts, meme_id))
|
|
await db.commit()
|
|
await update.message.reply_text(f"Rescheduled memes IDs {start_id}-{end_id} for {date_str} in slots 11:00, 16:00, 21:00 IST (cycled).")
|
|
return
|
|
|
|
else:
|
|
await update.message.reply_text("Invalid format. Use /scheduleat id: <id> <HH:MM> or /scheduleat ids: <start>-<end> <YYYY-MM-DD>")
|
|
|
|
def main():
|
|
if not BOT_TOKEN:
|
|
raise SystemExit("Please set TELEGRAM_BOT_TOKEN environment variable")
|
|
if not OWNER_ID or OWNER_ID == 0:
|
|
raise SystemExit("Please set OWNER_ID environment variable to your Telegram user id")
|
|
if not CHANNEL_ID:
|
|
raise SystemExit("Please set CHANNEL_ID to target channel (username or id)")
|
|
|
|
# Initialize DB first
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(loop)
|
|
loop.run_until_complete(init_db())
|
|
|
|
app = ApplicationBuilder().token(BOT_TOKEN).build()
|
|
|
|
app.add_handler(CommandHandler('start', start))
|
|
app.add_handler(CommandHandler('help', helpcmd))
|
|
app.add_handler(CommandHandler('postnow', postnow))
|
|
app.add_handler(CommandHandler('scheduled', scheduled))
|
|
app.add_handler(CommandHandler('unschedule', unschedule))
|
|
app.add_handler(CommandHandler('preview', preview))
|
|
app.add_handler(CommandHandler('log', logcmd))
|
|
app.add_handler(CommandHandler('scheduleat', scheduleat))
|
|
media_filter = filters.ChatType.PRIVATE & (filters.PHOTO | filters.VIDEO | filters.ANIMATION)
|
|
app.add_handler(MessageHandler(media_filter, handle_media))
|
|
|
|
# run background poster using post_init hook
|
|
async def post_init(application):
|
|
asyncio.create_task(periodic_poster(application))
|
|
|
|
app.post_init = post_init
|
|
|
|
logger.info("Starting bot...")
|
|
app.run_polling()
|
|
|
|
if __name__ == '__main__':
|
|
main()
|