Files
stickerbot/bot.py
coding f634b884ec Initial commit: Telegram Sticker Bot Manager
Features:
- Create and manage sticker packs
- Add existing sticker sets to database
- Automatic image optimization (512px, WebP conversion)
- GIF to animated video sticker conversion (WebM, VP9)
- Background removal (local rembg + optional Remove.bg API)
- Hybrid API with retry logic and usage tracking
- Premium status detection
- SQLite database for persistence
- Docker containerization
- Complete OPENCODE.md documentation

Technical Stack:
- python-telegram-bot 22.7
- Pillow for image processing
- FFmpeg for video conversion
- rembg for local background removal
- aiosqlite for async database operations
2026-03-25 08:59:38 +00:00

1158 lines
42 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Telegram bot for managing sticker packs."""
import logging
from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton
from telegram.ext import (
Application,
CommandHandler,
MessageHandler,
CallbackQueryHandler,
ConversationHandler,
ContextTypes,
filters,
)
from config import BOT_TOKEN, OWNER_ID, PREMIUM_CHECK_INTERVAL, REMOVE_BG_API_KEY, logger
from database import db
from sticker_manager import (
check_owner,
check_premium_status,
get_main_menu_keyboard,
get_set_action_keyboard,
get_user_sets_keyboard,
create_sticker_set,
add_sticker_to_set,
add_video_sticker_to_set,
delete_sticker_from_set,
delete_sticker_set,
rename_sticker_set,
change_sticker_emoji,
schedule_premium_check,
add_existing_sticker_set,
)
from gif_processor import (
get_gif_info,
validate_gif_duration,
convert_gif_to_webm,
)
from background_remover import (
remove_background_local,
remove_background_api,
should_show_api_limit_warning,
validate_image_for_bg_removal,
MAX_API_CALLS_PER_MONTH,
)
# Conversation states
(MENU, CREATE_SET_NAME, CREATE_SET_TITLE, CREATE_SET_EMOJI, CREATE_SET_STICKER,
SELECT_SET, SET_ACTION, ADD_STICKER, ADD_STICKER_EMOJI, ADD_ANIMATION_EMOJI,
DELETE_STICKER, CHANGE_EMOJI, RENAME_SET, DELETE_CONFIRM,
GIF_DURATION_CONFIRM, GIF_CONVERTING, REMOVE_BG_CHOICE, REMOVE_BG_API_ERROR,
ADD_EXISTING_SET_NAME) = range(19)
# Data keys
CURRENT_SET = "current_set"
SET_NAME = "set_name"
SET_TITLE = "set_title"
STICKER_TO_CHANGE = "sticker_to_change"
PENDING_ANIMATION = "pending_animation"
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Send welcome message and show main menu."""
if not await check_owner(update):
await update.message.reply_text("⛔️ Dieser Bot ist nur für den Besitzer verfügbar.")
return ConversationHandler.END
# Check premium status on start
is_premium = await check_premium_status(update, context)
premium_text = "💎 Premium aktiv" if is_premium else "👤 Standard"
welcome_text = (
f"🎨 **Willkommen beim Sticker Bot Manager!**\n\n"
f"Status: {premium_text}\n\n"
"Mit diesem Bot kannst du deine Telegram Stickerpakete verwalten:\n\n"
"✅ **Neue Pakete erstellen**\n"
"✅ **Sticker hinzufügen** (Bilder & GIFs)\n"
"✅ **Sticker löschen**\n"
"✅ **Emoji ändern**\n"
"✅ **Pakete umbenennen**\n"
"✅ **Pakete löschen**\n\n"
"🎨 **Extras:**\n"
"• Automatische Bildoptimierung\n"
"• GIF → animierte Video-Sticker\n"
"• Hintergrundentfernung (optional)\n\n"
"Nutze /hilfe für Details.\n\n"
"Wähle eine Aktion:"
)
await update.message.reply_text(
welcome_text,
reply_markup=get_main_menu_keyboard(),
parse_mode="Markdown"
)
return MENU
async def menu_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Handle menu callbacks."""
query = update.callback_query
await query.answer()
data = query.data
if data == "create_set":
await query.edit_message_text(
"📝 **Neues Stickerpaket erstellen**\n\n"
"Schritt 1/4: Gib einen kurzen Namen ein\n"
"(nur Kleinbuchstaben, Unterstriche, Zahlen):"
)
return CREATE_SET_NAME
elif data == "list_sets":
keyboard, text = await get_user_sets_keyboard(update.effective_user.id)
await query.edit_message_text(
text,
reply_markup=keyboard,
parse_mode="Markdown"
)
return SELECT_SET
elif data == "help":
await query.edit_message_text(
"❓ **Hilfe**\n\n"
"**Funktionen:**\n"
"• Neues Stickerpaket erstellen\n"
"• Sticker zu Paket hinzufügen (Bilder & GIFs)\n"
"• Sticker aus Paket löschen\n"
"• Emoji eines Stickers ändern\n"
"• Paket umbenennen\n"
"• Paket löschen\n\n"
"**Tipps:**\n"
"• Bilder: 512x512 PNG/WebP (automatisch konvertiert)\n"
"• GIFs: Werden zu animierten Video-Stickern\n"
"• Max. 3 Sekunden für Animationen\n"
"• Nutze /start um zum Hauptmenü zurückzukehren",
reply_markup=InlineKeyboardMarkup([
[InlineKeyboardButton("⬅️ Zurück", callback_data="back_to_menu")]
])
)
return MENU
elif data == "add_existing":
await query.edit_message_text(
"📥 **Existierendes Stickerpaket hinzufügen**\n\n"
"Gib den Namen des Sticker-Pakets ein:\n\n"
"**Formate:**\n"
"• Kurzname: `meine_sticker` (wird automatisch ergänzt)\n"
"• Vollständig: `meine_sticker_by_botname`\n\n"
"Du findest den Namen im Sticker-Paket unter 'Info'."
)
return ADD_EXISTING_SET_NAME
elif data == "back_to_menu":
is_premium = await check_premium_status(update, context)
premium_text = "💎 Premium aktiv" if is_premium else "👤 Standard"
await query.edit_message_text(
f"🎨 **Sticker Bot Manager**\n"
f"Status: {premium_text}\n\n"
"Wähle eine Aktion:",
reply_markup=get_main_menu_keyboard(),
parse_mode="Markdown"
)
return MENU
return MENU
async def create_set_name(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Get sticker set name from user."""
name = update.message.text.strip().lower().replace(" ", "_")
# Validate name
if not all(c.isalnum() or c == "_" for c in name):
await update.message.reply_text(
"⚠️ Ungültiger Name. Nur Kleinbuchstaben, Zahlen und Unterstriche erlaubt.\n"
"Versuche es erneut:"
)
return CREATE_SET_NAME
if len(name) > 60:
await update.message.reply_text(
"⚠️ Name zu lang (max. 60 Zeichen). Versuche es erneut:"
)
return CREATE_SET_NAME
context.user_data[SET_NAME] = name
await update.message.reply_text(
"📝 **Schritt 2/4**\n\n"
"Gib nun einen Titel für das Stickerpaket ein:"
)
return CREATE_SET_TITLE
async def create_set_title(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Get sticker set title from user."""
title = update.message.text.strip()
if len(title) > 64:
await update.message.reply_text(
"⚠️ Titel zu lang (max. 64 Zeichen). Versuche es erneut:"
)
return CREATE_SET_TITLE
context.user_data[SET_TITLE] = title
await update.message.reply_text(
"🏷 **Schritt 3/4**\n\n"
"Gib ein Emoji für das erste Sticker ein (z.B. 🤖):"
)
return CREATE_SET_EMOJI
async def create_set_emoji(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Get emoji for first sticker."""
emoji = update.message.text.strip()
if len(emoji) > 2:
await update.message.reply_text(
"⚠️ Bitte gib nur ein Emoji ein. Versuche es erneut:"
)
return CREATE_SET_EMOJI
context.user_data["emoji"] = emoji
await update.message.reply_text(
"🖼 **Schritt 4/4**\n\n"
"Sende nun das erste Sticker-Bild:\n"
"(PNG oder WebP, 512x512 Pixel empfohlen)"
)
return CREATE_SET_STICKER
async def create_set_sticker(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Get first sticker and create the set with optional background removal."""
if not update.message.photo and not update.message.document:
await update.message.reply_text(
"⚠️ Bitte sende ein Bild (Foto oder Dokument)."
)
return CREATE_SET_STICKER
# Get file
if update.message.photo:
file = await update.message.photo[-1].get_file()
else:
file = await update.message.document.get_file()
name = context.user_data.get(SET_NAME)
title = context.user_data.get(SET_TITLE)
emoji = context.user_data.get("emoji", "🤖")
# Check if API key is configured
if REMOVE_BG_API_KEY:
# Get current API usage
current_usage = await db.get_monthly_api_usage("remove_bg")
is_limit_reached = await should_show_api_limit_warning(REMOVE_BG_API_KEY, current_usage)
# Show background removal options
button_text = f"☁️ Remove.bg API ({current_usage}/{MAX_API_CALLS_PER_MONTH})"
if is_limit_reached:
button_text += " ⚠️ Limit erreicht"
keyboard = InlineKeyboardMarkup([
[InlineKeyboardButton("🤖 Lokal entfernen (kostenlos)", callback_data="bg_local")],
[InlineKeyboardButton(button_text, callback_data="bg_api")],
[InlineKeyboardButton("⏭️ Ohne Entfernung", callback_data="bg_skip")],
])
await update.message.reply_text(
"🎨 **Hintergrund entfernen?**\n\n"
"Wähle eine Option:",
reply_markup=keyboard,
parse_mode="Markdown"
)
# Store file for later processing
context.user_data["pending_file"] = file
context.user_data["pending_name"] = name
context.user_data["pending_title"] = title
context.user_data["pending_emoji"] = emoji
return REMOVE_BG_CHOICE
else:
# No API key - process directly
await update.message.reply_text("⏳ Erstelle Stickerpaket...")
success, result = await create_sticker_set(
update, context, name, title, file, emoji
)
if success:
await update.message.reply_text(
f"✅ **Stickerpaket erstellt!**\n\n"
f"📦 Titel: {title}\n"
f"🔗 Name: `{result}`\n\n"
f"Nutze /start um weitere Aktionen durchzuführen.",
parse_mode="Markdown"
)
else:
await update.message.reply_text(
f"❌ **Fehler beim Erstellen:**\n{result}\n\n"
f"Mögliche Ursachen:\n"
f"• Name bereits vergeben\n"
f"• Ungültiges Bildformat\n"
f"• Bild zu groß/klein"
)
# Cleanup
context.user_data.pop(SET_NAME, None)
context.user_data.pop(SET_TITLE, None)
context.user_data.pop("emoji", None)
return ConversationHandler.END
async def bg_choice_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Handle background removal choice."""
query = update.callback_query
await query.answer()
data = query.data
file = context.user_data.get("pending_file")
name = context.user_data.get("pending_name")
title = context.user_data.get("pending_title")
emoji = context.user_data.get("pending_emoji")
if not file:
await query.edit_message_text("❌ Fehler: Kein Bild gefunden. Starte neu mit /start")
return ConversationHandler.END
if data == "bg_local":
# Local background removal
await query.edit_message_text("⏳ Entferne Hintergrund (lokal)...")
try:
# Download file
file_obj = await file.download_as_bytearray()
file_bytes = bytes(file_obj)
# Validate image
is_valid, error_msg = await validate_image_for_bg_removal(file_bytes)
if not is_valid:
await query.edit_message_text(f"❌ **Fehler:**\n{error_msg}\n\nVersuche es mit einem anderen Bild.")
return ConversationHandler.END
# Remove background
result = await remove_background_local(file_bytes)
if result:
await query.edit_message_text("⏳ Erstelle Stickerpaket...")
# Create sticker set with processed image
from image_processor import process_image_for_sticker
processed = await process_image_for_sticker(result)
if processed:
from telegram import InputSticker
from telegram.constants import StickerFormat
sticker = InputSticker(
sticker=processed,
format=StickerFormat.STATIC,
emoji_list=[emoji]
)
set_name = f"{name}_by_{context.bot.username}"
await context.bot.create_new_sticker_set(
user_id=update.effective_user.id,
name=set_name,
title=title,
stickers=[sticker]
)
await db.add_sticker_set(update.effective_user.id, set_name, title)
await query.edit_message_text(
f"✅ **Stickerpaket erstellt!**\n\n"
f"📦 Titel: {title}\n"
f"🔗 Name: `{set_name}`\n\n"
f"Nutze /start um weitere Aktionen durchzuführen.",
parse_mode="Markdown"
)
else:
await query.edit_message_text("❌ Fehler bei der Bildverarbeitung.")
else:
await query.edit_message_text(
"❌ **Hintergrundentfernung fehlgeschlagen**\n\n"
"Nutze /start um neu zu beginnen."
)
except Exception as e:
logger.error(f"Error in local bg removal: {e}")
await query.edit_message_text(f"❌ Fehler: {str(e)}")
return ConversationHandler.END
elif data == "bg_api":
# API background removal
await query.edit_message_text("⏳ Verbinde mit Remove.bg API...")
try:
# Download file
file_obj = await file.download_as_bytearray()
file_bytes = bytes(file_obj)
# Validate image
is_valid, error_msg = await validate_image_for_bg_removal(file_bytes)
if not is_valid:
await query.edit_message_text(f"❌ **Fehler:**\n{error_msg}\n\nVersuche es mit einem anderen Bild.")
return ConversationHandler.END
# Call API with retry logic
result, message = await remove_background_api(file_bytes, REMOVE_BG_API_KEY)
if result == "LIMIT_REACHED" or result == "API_ERROR":
# Show error with fallback options
current_usage = await db.get_monthly_api_usage("remove_bg")
keyboard = InlineKeyboardMarkup([
[InlineKeyboardButton("🤖 Stattdessen lokal entfernen", callback_data="bg_local")],
[InlineKeyboardButton("⏭️ Ohne Entfernung", callback_data="bg_skip")],
[InlineKeyboardButton("❌ Abbrechen", callback_data="bg_cancel")],
])
warning_text = ""
if result == "LIMIT_REACHED":
warning_text = f"⚠️ **API-Limit erreicht** ({current_usage}/{MAX_API_CALLS_PER_MONTH})\n\n"
else:
warning_text = f"⚠️ **API-Fehler:** {message}\n\n"
await query.edit_message_text(
warning_text +
"Was möchtest du stattdessen tun?",
reply_markup=keyboard,
parse_mode="Markdown"
)
return REMOVE_BG_API_ERROR
# Success - track API call
await db.track_api_call("remove_bg")
await query.edit_message_text("⏳ Erstelle Stickerpaket...")
# Process result
from image_processor import process_image_for_sticker
processed = await process_image_for_sticker(result)
if processed:
from telegram import InputSticker
from telegram.constants import StickerFormat
set_name = f"{name}_by_{context.bot.username}"
sticker = InputSticker(
sticker=processed,
format=StickerFormat.STATIC,
emoji_list=[emoji]
)
await context.bot.create_new_sticker_set(
user_id=update.effective_user.id,
name=set_name,
title=title,
stickers=[sticker]
)
await db.add_sticker_set(update.effective_user.id, set_name, title)
await query.edit_message_text(
f"✅ **Stickerpaket erstellt!**\n\n"
f"📦 Titel: {title}\n"
f"🔗 Name: `{set_name}`\n\n"
f"Hintergrund entfernt mit Remove.bg API\n"
f"Nutze /start um weitere Aktionen durchzuführen.",
parse_mode="Markdown"
)
else:
await query.edit_message_text("❌ Fehler bei der Bildverarbeitung.")
except Exception as e:
logger.error(f"Error in API bg removal: {e}")
await query.edit_message_text(f"❌ Fehler: {str(e)}")
return ConversationHandler.END
elif data == "bg_skip":
# Skip background removal
await query.edit_message_text("⏳ Erstelle Stickerpaket...")
try:
from telegram import InputSticker
from telegram.constants import StickerFormat
file_obj = await file.download_as_bytearray()
file_bytes = bytes(file_obj)
from image_processor import process_image_for_sticker
processed = await process_image_for_sticker(file_bytes)
if processed:
set_name = f"{name}_by_{context.bot.username}"
sticker = InputSticker(
sticker=processed,
format=StickerFormat.STATIC,
emoji_list=[emoji]
)
await context.bot.create_new_sticker_set(
user_id=update.effective_user.id,
name=set_name,
title=title,
stickers=[sticker]
)
await db.add_sticker_set(update.effective_user.id, set_name, title)
await query.edit_message_text(
f"✅ **Stickerpaket erstellt!**\n\n"
f"📦 Titel: {title}\n"
f"🔗 Name: `{set_name}`\n\n"
f"Nutze /start um weitere Aktionen durchzuführen.",
parse_mode="Markdown"
)
else:
await query.edit_message_text("❌ Fehler bei der Bildverarbeitung.")
except Exception as e:
logger.error(f"Error creating sticker set: {e}")
await query.edit_message_text(f"❌ Fehler: {str(e)}")
return ConversationHandler.END
elif data == "bg_cancel":
await query.edit_message_text("❌ Abgebrochen. Nutze /start zum Neustart.")
return ConversationHandler.END
return ConversationHandler.END
async def select_set_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Handle sticker set selection."""
query = update.callback_query
await query.answer()
data = query.data
if data.startswith("select_set:"):
set_name = data.split(":", 1)[1]
context.user_data[CURRENT_SET] = set_name
# Get set info from database
sets = await db.get_user_sticker_sets(update.effective_user.id)
set_info = next((s for s in sets if s['set_name'] == set_name), None)
title = set_info['set_title'] if set_info else set_name
await query.edit_message_text(
f"📦 **Stickerpaket**\n"
f"📝 Titel: {title}\n"
f"🔗 Name: `{set_name}`\n\n"
f"Was möchtest du tun?",
reply_markup=get_set_action_keyboard(set_name),
parse_mode="Markdown"
)
return SET_ACTION
elif data == "back_to_menu":
return await menu_callback(update, context)
return SELECT_SET
async def set_action_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Handle set action selection."""
query = update.callback_query
await query.answer()
data = query.data
set_name = context.user_data.get(CURRENT_SET)
if data.startswith("add_sticker:"):
await query.edit_message_text(
"🖼 Sende ein neues Sticker-Bild:\n\n"
"(PNG oder WebP, 512x512 Pixel empfohlen)"
)
return ADD_STICKER
elif data.startswith("delete_sticker:"):
await query.edit_message_text(
"🗑 Sende den Sticker, den du löschen möchtest:\n\n"
"(Klicke auf einen Sticker und wähle 'Weiterleiten' zu diesem Bot)"
)
return DELETE_STICKER
elif data.startswith("change_emoji:"):
await query.edit_message_text(
"🗑 Sende den Sticker, dessen Emoji du ändern möchtest:\n\n"
"(Klicke auf einen Sticker und wähle 'Weiterleiten')"
)
return CHANGE_EMOJI
elif data.startswith("rename_set:"):
await query.edit_message_text(
"✏️ Gib den neuen Titel für das Paket ein:"
)
return RENAME_SET
elif data.startswith("delete_set:"):
keyboard = InlineKeyboardMarkup([
[InlineKeyboardButton("✅ Ja, löschen", callback_data="confirm_delete")],
[InlineKeyboardButton("❌ Abbrechen", callback_data="cancel_delete")],
])
await query.edit_message_text(
f"⚠️ **Bist du sicher?**\n\n"
f"Das Stickerpaket wird unwiderruflich gelöscht!",
reply_markup=keyboard,
parse_mode="Markdown"
)
return DELETE_CONFIRM
elif data == "back_to_menu":
return await menu_callback(update, context)
return SET_ACTION
async def add_sticker_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Handle new sticker addition - including GIFs."""
set_name = context.user_data.get(CURRENT_SET)
# Check for GIF/Animation
if update.message.animation:
# It's a GIF/animation
animation = update.message.animation
# Download the file
file = await animation.get_file()
file_obj = await file.download_as_bytearray()
gif_bytes = bytes(file_obj)
# Get GIF info
gif_info = await get_gif_info(gif_bytes)
duration = gif_info.get('duration', 0)
# Check if duration exceeds limit
is_valid, error_msg = await validate_gif_duration(duration)
if not is_valid:
# GIF too long - ask user
context.user_data[PENDING_ANIMATION] = {
'bytes': gif_bytes,
'duration': duration
}
keyboard = InlineKeyboardMarkup([
[InlineKeyboardButton("✂️ Auf 3 Sekunden kürzen", callback_data="trim_gif")],
[InlineKeyboardButton("❌ Abbrechen", callback_data="cancel_gif")],
])
await update.message.reply_text(
f"🎬 **GIF zu lang**\n\n"
f"Dauer: {duration:.1f} Sekunden\n"
f"Maximal erlaubt: 3 Sekunden\n\n"
f"Was möchtest du tun?",
reply_markup=keyboard,
parse_mode="Markdown"
)
return GIF_DURATION_CONFIRM
# GIF is within limits - proceed with conversion
context.user_data[PENDING_ANIMATION] = {'bytes': gif_bytes, 'duration': duration}
await update.message.reply_text("🏷 Gib ein Emoji für den animierten Sticker ein (z.B. 😊):")
return ADD_ANIMATION_EMOJI
# Handle regular images
if not update.message.photo and not update.message.document:
await update.message.reply_text("⚠️ Bitte sende ein Bild oder GIF.")
return ADD_STICKER
# Get file
if update.message.photo:
file = await update.message.photo[-1].get_file()
else:
file = await update.message.document.get_file()
# Store file for next step (emoji selection)
context.user_data["pending_sticker"] = file
await update.message.reply_text(
"🏷 Gib ein Emoji für den neuen Sticker ein (z.B. 😊):"
)
return ADD_STICKER_EMOJI
async def gif_duration_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Handle GIF duration confirmation callback."""
query = update.callback_query
await query.answer()
if query.data == "trim_gif":
await query.edit_message_text(
"✂️ GIF wird auf 3 Sekunden gekürzt...\n\n"
"🏷 Gib ein Emoji für den animierten Sticker ein (z.B. 😊):"
)
return ADD_ANIMATION_EMOJI
elif query.data == "cancel_gif":
await query.edit_message_text("❌ GIF-Hinzufügen abgebrochen.")
context.user_data.pop(PENDING_ANIMATION, None)
return ConversationHandler.END
return GIF_DURATION_CONFIRM
async def add_animation_emoji(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Get emoji for animated sticker and process GIF."""
emoji = update.message.text.strip()
set_name = context.user_data.get(CURRENT_SET)
anim_data = context.user_data.get(PENDING_ANIMATION)
if not anim_data:
await update.message.reply_text("❌ Fehler: Kein GIF gefunden. Starte neu mit /start")
return ConversationHandler.END
gif_bytes = anim_data['bytes']
duration = anim_data['duration']
# Determine max duration
max_duration = 3.0 if duration > 3.0 else None
await update.message.reply_text("⏳ Konvertiere GIF zu Video Sticker...")
# Convert GIF to WebM
webm_bytes = await convert_gif_to_webm(gif_bytes, max_duration)
if not webm_bytes:
await update.message.reply_text(
"❌ **Fehler bei der Konvertierung**\n\n"
"Das GIF konnte nicht konvertiert werden.\n"
"Mögliche Ursachen:\n"
"• Datei zu groß\n"
"• Ungültiges Format\n"
"• Komprimierung fehlgeschlagen"
)
context.user_data.pop(PENDING_ANIMATION, None)
return ConversationHandler.END
await update.message.reply_text("⏳ Füge animierten Sticker hinzu...")
# Add as video sticker
success = await add_video_sticker_to_set(update, context, set_name, webm_bytes, emoji)
if success:
await update.message.reply_text(f"✅ Animierter Sticker hinzugefügt mit Emoji {emoji}!")
else:
await update.message.reply_text("❌ Fehler beim Hinzufügen des animierten Stickers.")
context.user_data.pop(PENDING_ANIMATION, None)
return ConversationHandler.END
async def add_existing_set_name(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Handle existing sticker set name input."""
set_name_input = update.message.text.strip()
# Auto-append _by_<bot_username> if not present
if "_by_" not in set_name_input:
bot_username = context.bot.username
set_name = f"{set_name_input}_by_{bot_username}"
else:
set_name = set_name_input
await update.message.reply_text(f"⏳ Suche Stickerpaket `{set_name}`...", parse_mode="Markdown")
# Try to add existing set
success, message = await add_existing_sticker_set(update, context, set_name)
if success:
await update.message.reply_text(
f"{message}\n\n"
f"Nutze /start um das Paket zu verwalten.",
parse_mode="Markdown"
)
else:
await update.message.reply_text(
f"❌ **Fehler:**\n{message}\n\n"
f"**Tipps:**\n"
f"• Prüfe den Namen (z.B. `meine_sticker` oder `meine_sticker_by_botname`)\n"
f"• Das Paket muss öffentlich sein\n"
f"• Du musst Besitzer des Pakets sein\n\n"
f"Versuche es erneut oder nutze /start.",
parse_mode="Markdown"
)
return ConversationHandler.END
async def add_sticker_emoji(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Get emoji for new sticker and add it."""
emoji = update.message.text.strip()
set_name = context.user_data.get(CURRENT_SET)
file = context.user_data.get("pending_sticker")
if not file:
await update.message.reply_text("❌ Fehler: Kein Sticker gefunden. Starte neu mit /start")
return ConversationHandler.END
await update.message.reply_text("⏳ Füge Sticker hinzu...")
success = await add_sticker_to_set(update, context, set_name, file, emoji)
if success:
await update.message.reply_text(f"✅ Sticker hinzugefügt mit Emoji {emoji}!")
else:
await update.message.reply_text("❌ Fehler beim Hinzufügen des Stickers.")
context.user_data.pop("pending_sticker", None)
return ConversationHandler.END
async def delete_sticker_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Handle sticker deletion."""
if not update.message.sticker:
await update.message.reply_text("⚠️ Bitte sende einen Sticker (nicht ein Bild).")
return DELETE_STICKER
sticker_file_id = update.message.sticker.file_id
await update.message.reply_text("⏳ Lösche Sticker...")
success = await delete_sticker_from_set(update, context, sticker_file_id)
if success:
await update.message.reply_text("✅ Sticker gelöscht!")
else:
await update.message.reply_text("❌ Fehler beim Löschen des Stickers.")
return ConversationHandler.END
async def change_emoji_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Handle sticker emoji change - step 1: get sticker."""
if not update.message.sticker:
await update.message.reply_text("⚠️ Bitte sende einen Sticker.")
return CHANGE_EMOJI
sticker_file_id = update.message.sticker.file_id
context.user_data[STICKER_TO_CHANGE] = sticker_file_id
await update.message.reply_text(
"🏷 Gib das neue Emoji für den Sticker ein (z.B. 😊):"
)
return CHANGE_EMOJI
async def change_emoji_input(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Handle new emoji input."""
new_emoji = update.message.text.strip()
sticker_file_id = context.user_data.get(STICKER_TO_CHANGE)
if not sticker_file_id:
await update.message.reply_text("❌ Fehler. Starte neu mit /start")
return ConversationHandler.END
await update.message.reply_text("⏳ Ändere Emoji...")
success = await change_sticker_emoji(update, context, sticker_file_id, new_emoji)
if success:
await update.message.reply_text(f"✅ Emoji geändert zu {new_emoji}!")
else:
await update.message.reply_text("❌ Fehler beim Ändern des Emojis.")
context.user_data.pop(STICKER_TO_CHANGE, None)
return ConversationHandler.END
async def rename_set_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Handle sticker set renaming."""
set_name = context.user_data.get(CURRENT_SET)
new_title = update.message.text.strip()
if len(new_title) > 64:
await update.message.reply_text("⚠️ Titel zu lang (max. 64 Zeichen). Versuche es erneut:")
return RENAME_SET
await update.message.reply_text("⏳ Benenne Paket um...")
success = await rename_sticker_set(update, context, set_name, new_title)
if success:
await update.message.reply_text(f"✅ Paket umbenannt in: {new_title}")
else:
await update.message.reply_text("❌ Fehler beim Umbenennen des Pakets.")
return ConversationHandler.END
async def delete_confirm_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Handle set deletion confirmation."""
query = update.callback_query
await query.answer()
if query.data == "confirm_delete":
set_name = context.user_data.get(CURRENT_SET)
await query.edit_message_text("⏳ Lösche Paket...")
success = await delete_sticker_set(update, context, set_name)
if success:
await query.edit_message_text("✅ Stickerpaket gelöscht!")
else:
await query.edit_message_text("❌ Fehler beim Löschen des Pakets.")
context.user_data.pop(CURRENT_SET, None)
return ConversationHandler.END
elif query.data == "cancel_delete":
set_name = context.user_data.get(CURRENT_SET)
sets = await db.get_user_sticker_sets(update.effective_user.id)
set_info = next((s for s in sets if s['set_name'] == set_name), None)
title = set_info['set_title'] if set_info else set_name
await query.edit_message_text(
f"📦 **Stickerpaket**\n"
f"📝 Titel: {title}\n"
f"🔗 Name: `{set_name}`\n\n"
f"Was möchtest du tun?",
reply_markup=get_set_action_keyboard(set_name),
parse_mode="Markdown"
)
return SET_ACTION
return ConversationHandler.END
async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Cancel conversation."""
await update.message.reply_text(
"❌ Aktion abgebrochen. Nutze /start zum Neustart."
)
# Cleanup
keys_to_remove = [SET_NAME, SET_TITLE, CURRENT_SET, STICKER_TO_CHANGE, PENDING_ANIMATION,
"pending_file", "pending_name", "pending_title", "pending_emoji", "emoji", "pending_sticker"]
for key in keys_to_remove:
context.user_data.pop(key, None)
return ConversationHandler.END
async def post_init(application: Application):
"""Initialize database and schedule jobs after bot startup."""
await db.init()
# Schedule premium check job
if application.job_queue:
application.job_queue.run_repeating(
schedule_premium_check,
interval=PREMIUM_CHECK_INTERVAL * 3600, # Convert to seconds
first=10 # First check after 10 seconds
)
logger.info(f"Scheduled premium check every {PREMIUM_CHECK_INTERVAL} hours")
def main():
"""Run the bot."""
application = Application.builder().token(BOT_TOKEN).build()
# Add post_init callback
application.post_init = post_init
conv_handler = ConversationHandler(
entry_points=[CommandHandler("start", start)],
states={
MENU: [
CallbackQueryHandler(menu_callback, pattern="^(create_set|list_sets|help|back_to_menu)$"),
],
CREATE_SET_NAME: [
MessageHandler(filters.TEXT & ~filters.COMMAND, create_set_name),
],
CREATE_SET_TITLE: [
MessageHandler(filters.TEXT & ~filters.COMMAND, create_set_title),
],
CREATE_SET_EMOJI: [
MessageHandler(filters.TEXT & ~filters.COMMAND, create_set_emoji),
],
CREATE_SET_STICKER: [
MessageHandler(filters.PHOTO | filters.Document.ALL, create_set_sticker),
],
SELECT_SET: [
CallbackQueryHandler(select_set_callback, pattern="^(select_set:|back_to_menu)$"),
],
SET_ACTION: [
CallbackQueryHandler(set_action_callback, pattern="^(add_sticker:|delete_sticker:|change_emoji:|rename_set:|delete_set:|back_to_menu)$"),
],
ADD_STICKER: [
MessageHandler(filters.PHOTO | filters.Document.ALL | filters.ANIMATION, add_sticker_handler),
],
ADD_STICKER_EMOJI: [
MessageHandler(filters.TEXT & ~filters.COMMAND, add_sticker_emoji),
],
GIF_DURATION_CONFIRM: [
CallbackQueryHandler(gif_duration_callback, pattern="^(trim_gif|cancel_gif)$"),
],
ADD_ANIMATION_EMOJI: [
MessageHandler(filters.TEXT & ~filters.COMMAND, add_animation_emoji),
],
DELETE_STICKER: [
MessageHandler(filters.Sticker.ALL, delete_sticker_handler),
],
CHANGE_EMOJI: [
MessageHandler(filters.Sticker.ALL, change_emoji_handler),
],
RENAME_SET: [
MessageHandler(filters.TEXT & ~filters.COMMAND, rename_set_handler),
],
DELETE_CONFIRM: [
CallbackQueryHandler(delete_confirm_callback, pattern="^(confirm_delete|cancel_delete)$"),
],
REMOVE_BG_CHOICE: [
CallbackQueryHandler(bg_choice_callback, pattern="^(bg_local|bg_api|bg_skip|bg_cancel)$"),
],
REMOVE_BG_API_ERROR: [
CallbackQueryHandler(bg_choice_callback, pattern="^(bg_local|bg_skip|bg_cancel)$"),
],
},
fallbacks=[CommandHandler("cancel", cancel)],
)
application.add_handler(conv_handler)
logger.info("Starting sticker bot...")
application.run_polling(allowed_updates=Update.ALL_TYPES)
if __name__ == "__main__":
main()
async def hilfe_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Send help message."""
if not await check_owner(update):
await update.message.reply_text("⛔️ Dieser Bot ist nur für den Besitzer verfügbar.")
return
help_text = (
"❓ **Hilfe - Sticker Bot Manager**\n\n"
"**📱 Befehle:**\n"
"• /start - Bot starten\n"
"• /hilfe - Diese Hilfe anzeigen\n"
"• /cancel - Aktuelle Aktion abbrechen\n\n"
"**🎨 Funktionen:**\n"
"1⃣ **Neues Stickerpaket**\n"
" - Name → Titel → Emoji → Bild\n"
" - Bilder werden automatisch optimiert\n\n"
"2⃣ **Sticker hinzufügen**\n"
" - Bilder: PNG, JPG, WebP (automatische Konvertierung)\n"
" - GIFs: Werden zu animierten Video-Stickern (max. 3s)\n"
" - Optional: Hintergrund automatisch entfernen\n\n"
"3⃣ **Sticker löschen**\n"
" - Sende den Sticker direkt an den Bot\n\n"
"4⃣ **Emoji ändern**\n"
" - Sticker senden → Neuen Emoji eingeben\n\n"
"5⃣ **Paket umbenennen**\n"
" - Neuen Titel eingeben (max. 64 Zeichen)\n\n"
"6⃣ **Paket löschen**\n"
" - ⚠️ Kann nicht rückgängig gemacht werden!\n\n"
"**💡 Tipps:**\n"
"• Bilder als Datei senden für beste Qualität\n"
"• Ideale Größe: 512x512 Pixel\n"
"• Max. 120 Sticker pro reguläres Paket\n"
"• Max. 200 Sticker pro Emoji-Paket\n\n"
"**🎨 Hintergrundentfernung:**\n"
"• Lokal: Kostenlos, direkt auf dem Server\n"
"• Remove.bg API: 50 Aufrufe/Monat gratis\n\n"
"Nutze /start zum Öffnen des Hauptmenüs."
)
await update.message.reply_text(help_text, parse_mode="Markdown")
def main():
"""Run the bot."""
application = Application.builder().token(BOT_TOKEN).build()
# Add post_init callback
application.post_init = post_init
# Add standalone command handlers
application.add_handler(CommandHandler("hilfe", hilfe_command))
conv_handler = ConversationHandler(
entry_points=[CommandHandler("start", start)],
states={
MENU: [
CallbackQueryHandler(menu_callback, pattern="^(create_set|list_sets|help|back_to_menu)$"),
],
CREATE_SET_NAME: [
MessageHandler(filters.TEXT & ~filters.COMMAND, create_set_name),
],
CREATE_SET_TITLE: [
MessageHandler(filters.TEXT & ~filters.COMMAND, create_set_title),
],
CREATE_SET_EMOJI: [
MessageHandler(filters.TEXT & ~filters.COMMAND, create_set_emoji),
],
CREATE_SET_STICKER: [
MessageHandler(filters.PHOTO | filters.Document.ALL, create_set_sticker),
],
SELECT_SET: [
CallbackQueryHandler(select_set_callback, pattern="^(select_set:|back_to_menu)$"),
],
SET_ACTION: [
CallbackQueryHandler(set_action_callback, pattern="^(add_sticker:|delete_sticker:|change_emoji:|rename_set:|delete_set:|back_to_menu)$"),
],
ADD_STICKER: [
MessageHandler(filters.PHOTO | filters.Document.ALL | filters.ANIMATION, add_sticker_handler),
],
ADD_STICKER_EMOJI: [
MessageHandler(filters.TEXT & ~filters.COMMAND, add_sticker_emoji),
],
GIF_DURATION_CONFIRM: [
CallbackQueryHandler(gif_duration_callback, pattern="^(trim_gif|cancel_gif)$"),
],
ADD_ANIMATION_EMOJI: [
MessageHandler(filters.TEXT & ~filters.COMMAND, add_animation_emoji),
],
DELETE_STICKER: [
MessageHandler(filters.Sticker.ALL, delete_sticker_handler),
],
CHANGE_EMOJI: [
MessageHandler(filters.Sticker.ALL, change_emoji_handler),
],
RENAME_SET: [
MessageHandler(filters.TEXT & ~filters.COMMAND, rename_set_handler),
],
DELETE_CONFIRM: [
CallbackQueryHandler(delete_confirm_callback, pattern="^(confirm_delete|cancel_delete)$"),
],
REMOVE_BG_CHOICE: [
CallbackQueryHandler(bg_choice_callback, pattern="^(bg_local|bg_api|bg_skip|bg_cancel)$"),
],
REMOVE_BG_API_ERROR: [
CallbackQueryHandler(bg_choice_callback, pattern="^(bg_local|bg_skip|bg_cancel)$"),
],
},
fallbacks=[CommandHandler("cancel", cancel)],
)
application.add_handler(conv_handler)
logger.info("Starting sticker bot...")
application.run_polling(allowed_updates=Update.ALL_TYPES)
if __name__ == "__main__":
main()