Spaces:
Runtime error
Runtime error
File size: 32,103 Bytes
f0391dc 92e6005 06b39f0 92e6005 f0391dc 92e6005 f0391dc 92e6005 dd499c1 06b39f0 dd499c1 92e6005 f0391dc 06b39f0 dd499c1 f0391dc 92e6005 06b39f0 92e6005 06b39f0 f0391dc 92e6005 06b39f0 92e6005 f0391dc 92e6005 06b39f0 92e6005 06b39f0 f0391dc 06b39f0 92e6005 06b39f0 92e6005 f0391dc 92e6005 06b39f0 92e6005 06b39f0 f0391dc 92e6005 f0391dc 06b39f0 92e6005 06b39f0 f0391dc 92e6005 06b39f0 92e6005 06b39f0 f0391dc 92e6005 06b39f0 f0391dc 92e6005 f0391dc 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 f0391dc 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 f0391dc 92e6005 06b39f0 92e6005 f0391dc 06b39f0 92e6005 06b39f0 92e6005 f0391dc 06b39f0 f0391dc 06b39f0 92e6005 06b39f0 92e6005 f0391dc 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 f0391dc 06b39f0 f0391dc 06b39f0 92e6005 06b39f0 f0391dc 06b39f0 f0391dc 06b39f0 92e6005 f0391dc 92e6005 06b39f0 92e6005 f0391dc 06b39f0 92e6005 06b39f0 f0391dc 06b39f0 f0391dc 06b39f0 f0391dc 06b39f0 f0391dc 06b39f0 f0391dc 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 f0391dc 92e6005 06b39f0 92e6005 06b39f0 f0391dc 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 f0391dc 06b39f0 92e6005 06b39f0 92e6005 06b39f0 f0391dc 06b39f0 92e6005 06b39f0 f0391dc 06b39f0 92e6005 f0391dc 92e6005 06b39f0 f0391dc 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 f0391dc 92e6005 06b39f0 92e6005 06b39f0 f0391dc 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 f0391dc 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 f0391dc 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 f0391dc 06b39f0 92e6005 06b39f0 f0391dc 92e6005 f0391dc 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 f0391dc 92e6005 06b39f0 f0391dc 92e6005 06b39f0 f0391dc 06b39f0 92e6005 06b39f0 92e6005 f0391dc 06b39f0 92e6005 06b39f0 f0391dc 92e6005 f0391dc 92e6005 f0391dc 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 f0391dc 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 f0391dc 92e6005 06b39f0 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 |
print("DEBUG: app.py script execution starting...")
import os
import asyncio
import json
import logging
import sys # For stdout logging and sys.exit
# --- Detailed Logging Setup ---
print("DEBUG: Setting up logging...")
logging.basicConfig(
level=logging.DEBUG, # Capture all DEBUG, INFO, WARNING, ERROR, CRITICAL
format='%(asctime)s - %(name)s - %(levelname)s - %(module)s:%(lineno)d - %(message)s',
stream=sys.stdout, # Ensure logs go to standard output for Hugging Face
force=True # In case logging was already configured by another lib (unlikely here)
)
logger = logging.getLogger(__name__) # Get a logger for this module
logging.getLogger('telethon').setLevel(logging.DEBUG) # Max verbosity for Telethon
print("DEBUG: Logging setup complete.")
logger.debug("DEBUG: Importing Telethon...")
from telethon import TelegramClient, events # type: ignore
import telethon # For version checking
from telethon.tl import types # For DocumentAttributeFilename
logger.info(f"DEBUG: Using Telethon version: {telethon.__version__}") # Log the version
logger.debug("DEBUG: Telethon imported.")
logger.debug("DEBUG: Importing Pillow...")
from PIL import Image, ImageDraw, ImageFont
logger.debug("DEBUG: Pillow imported.")
# --- Configuration Loading and Environment Variable Check ---
logger.info("DEBUG: Loading environment variables...")
try:
logger.debug("DEBUG: Attempting to import dotenv...")
from dotenv import load_dotenv
logger.debug("DEBUG: dotenv imported. Attempting to load .env file...")
if load_dotenv():
logger.info("DEBUG: .env file loaded successfully.")
else:
logger.info("DEBUG: No .env file found or it's empty, relying on pre-set environment variables.")
except ImportError:
logger.info("DEBUG: python-dotenv not installed, relying on pre-set environment variables.")
except Exception as e:
logger.error(f"DEBUG: Error loading .env using dotenv: {e}")
API_ID = os.environ.get('API_ID')
API_HASH = os.environ.get('API_HASH')
BOT_TOKEN = os.environ.get('BOT_TOKEN')
logger.info(f"DEBUG: API_ID loaded: {'Set' if API_ID else 'NOT SET'}")
logger.info(f"DEBUG: API_HASH loaded: {'Set' if API_HASH else 'NOT SET'}")
logger.info(f"DEBUG: BOT_TOKEN loaded: {'Set' if BOT_TOKEN else 'NOT SET'}")
if not all([API_ID, API_HASH, BOT_TOKEN]):
logger.critical("CRITICAL ERROR: API_ID, API_HASH, and BOT_TOKEN environment variables must be set in Hugging Face Secrets.")
logger.critical("Bot cannot start. Please set these secrets in your Space settings.")
sys.exit(1) # Exit script if critical env vars are missing
try:
API_ID = int(API_ID)
logger.info(f"DEBUG: API_ID successfully converted to int: {str(API_ID)[:4]}... (masked for log)")
except ValueError:
logger.critical(f"CRITICAL ERROR: API_ID ('{os.environ.get('API_ID')}') must be an integer.")
sys.exit(1)
# --- Session Name (CHANGE THIS OFTEN FOR DEBUGGING CONNECTION HANGS) ---
SESSION_VERSION = "v4_fresh_20250513_0130" # <<<<<<< CHANGE THIS TO A NEW UNIQUE STRING
SESSION_NAME = f'session/image_bot_session_{SESSION_VERSION}'
logger.info(f"DEBUG: Using session name: {SESSION_NAME}")
logger.debug("DEBUG: Initializing TelegramClient (bot variable)...")
try:
bot = TelegramClient(SESSION_NAME, API_ID, API_HASH)
logger.info(f"DEBUG: TelegramClient initialized successfully with session '{SESSION_NAME}'.")
except Exception as e:
logger.critical(f"CRITICAL ERROR: Failed to initialize TelegramClient: {e}", exc_info=True)
sys.exit(1)
# --- Paths and Directory Setup ---
logger.info("DEBUG: Defining paths and ensuring directories...")
DATA_DIR = "data"
DOWNLOADS_DIR = "downloads"
SESSION_DIR = "session"
CONFIG_FILENAME = "config.json"
TEMPLATE_FILENAME = "user_template.png"
USER_FONT_FILENAME = "user_font.ttf"
CONFIG_PATH = os.path.join(DATA_DIR, CONFIG_FILENAME)
TEMPLATE_PATH = os.path.join(DATA_DIR, TEMPLATE_FILENAME)
USER_FONT_PATH = os.path.join(DATA_DIR, USER_FONT_FILENAME)
for dir_path in [DATA_DIR, DOWNLOADS_DIR, SESSION_DIR]:
logger.debug(f"DEBUG: Ensuring directory exists: {dir_path}")
try:
os.makedirs(dir_path, exist_ok=True)
logger.info(f"DEBUG: Directory '{dir_path}' ensured (created if didn't exist).")
if not os.access(dir_path, os.W_OK):
logger.warning(f"DEBUG: Directory '{dir_path}' might not be writable despite creation (check Dockerfile chmod and persistent storage mount options)!")
except Exception as e:
logger.error(f"DEBUG: Error ensuring directory '{dir_path}': {e}", exc_info=True)
logger.info("DEBUG: Paths defined and directories ensured.")
# --- Default Configuration ---
DEFAULT_CONFIG = {
"raw_image_box": {"x": 40, "y": 40, "width": 1000, "height": 780},
"caption_area": {"x": 60, "y": 870},
"caption_font_size": 45,
"caption_color": "white",
"line_spacing": 10
}
bot_config = {}
logger.debug(f"DEBUG: Default config defined: {DEFAULT_CONFIG}")
# --- Config Management ---
def load_bot_config():
global bot_config
logger.info(f"DEBUG: Attempting to load bot configuration from {CONFIG_PATH}...")
try:
if os.path.exists(CONFIG_PATH):
with open(CONFIG_PATH, 'r') as f:
loaded_values = json.load(f)
logger.info(f"DEBUG: Successfully read from {CONFIG_PATH}.")
bot_config = DEFAULT_CONFIG.copy()
bot_config.update(loaded_values)
logger.info("DEBUG: Merged loaded config with defaults.")
else:
logger.info(f"DEBUG: Config file {CONFIG_PATH} not found. Using defaults.")
bot_config = DEFAULT_CONFIG.copy()
save_bot_config(initial_load=True) # Ensure file exists with all keys
logger.info(f"DEBUG: Final configuration after load/init: {bot_config}")
except json.JSONDecodeError as e:
logger.error(f"DEBUG: Error decoding JSON from {CONFIG_PATH}. Using defaults and overwriting. Error: {e}", exc_info=True)
bot_config = DEFAULT_CONFIG.copy()
save_bot_config(initial_load=True)
except Exception as e:
logger.error(f"DEBUG: Critical error loading config, using defaults: {e}", exc_info=True)
bot_config = DEFAULT_CONFIG.copy()
if not os.path.exists(CONFIG_PATH):
save_bot_config(initial_load=True)
def save_bot_config(initial_load=False):
global bot_config
if not initial_load:
logger.info(f"DEBUG: Attempting to save bot configuration to {CONFIG_PATH}...")
try:
with open(CONFIG_PATH, 'w') as f:
json.dump(bot_config, f, indent=4)
# Avoid logging full config on every save during initial_load to reduce noise
if not initial_load:
logger.info(f"DEBUG: Configuration saved successfully.")
except Exception as e:
logger.error(f"DEBUG: Error saving config to {CONFIG_PATH}: {e}", exc_info=True)
# --- User State Management ---
user_sessions = {}
logger.debug("DEBUG: User sessions initialized as empty dict.")
# --- Helper Functions (Image Processing) ---
async def generate_image_with_frame(raw_image_path, frame_template_path, caption_text, chat_id):
logger.info(f"DEBUG: [ChatID: {chat_id}] Starting image generation. Raw: '{raw_image_path}', Template: '{frame_template_path}', Caption: '{caption_text[:30]}...'")
output_filename = f"final_image_{chat_id}_{SESSION_VERSION}.png"
output_path = os.path.join(DOWNLOADS_DIR, output_filename)
logger.debug(f"DEBUG: [ChatID: {chat_id}] Output path will be: {output_path}")
cfg_raw_box = bot_config.get("raw_image_box", DEFAULT_CONFIG["raw_image_box"])
cfg_caption_area = bot_config.get("caption_area", DEFAULT_CONFIG["caption_area"])
cfg_font_size = bot_config.get("caption_font_size", DEFAULT_CONFIG["caption_font_size"])
cfg_caption_color = bot_config.get("caption_color", DEFAULT_CONFIG["caption_color"])
cfg_line_spacing = bot_config.get("line_spacing", DEFAULT_CONFIG["line_spacing"])
logger.debug(f"DEBUG: [ChatID: {chat_id}] Using image processing config: raw_box={cfg_raw_box}, caption_area={cfg_caption_area}, font_size={cfg_font_size}")
try:
logger.debug(f"DEBUG: [ChatID: {chat_id}] Opening raw image: {raw_image_path}")
raw_img = Image.open(raw_image_path).convert("RGBA")
logger.debug(f"DEBUG: [ChatID: {chat_id}] Opening frame template image: {frame_template_path}")
frame_template_img = Image.open(frame_template_path).convert("RGBA")
raw_img_copy = raw_img.copy()
logger.debug(f"DEBUG: [ChatID: {chat_id}] Resizing raw image to fit box: {cfg_raw_box['width']}x{cfg_raw_box['height']}")
raw_img_copy.thumbnail((cfg_raw_box['width'], cfg_raw_box['height']), Image.LANCZOS)
final_img = frame_template_img.copy()
paste_x = cfg_raw_box['x'] + (cfg_raw_box['width'] - raw_img_copy.width) // 2
paste_y = cfg_raw_box['y'] + (cfg_raw_box['height'] - raw_img_copy.height) // 2
logger.debug(f"DEBUG: [ChatID: {chat_id}] Pasting raw image at ({paste_x}, {paste_y}) on template.")
final_img.paste(raw_img_copy, (paste_x, paste_y), raw_img_copy if raw_img_copy.mode == 'RGBA' else None)
draw = ImageDraw.Draw(final_img)
font_to_use = None
logger.debug(f"DEBUG: [ChatID: {chat_id}] Attempting to load user font: {USER_FONT_PATH} with size {cfg_font_size}")
if os.path.exists(USER_FONT_PATH):
try:
font_to_use = ImageFont.truetype(USER_FONT_PATH, cfg_font_size)
logger.info(f"DEBUG: [ChatID: {chat_id}] User font loaded successfully.")
except IOError as e:
logger.warning(f"DEBUG: [ChatID: {chat_id}] User font at '{USER_FONT_PATH}' found but couldn't be loaded: {e}. Using fallback.")
if not font_to_use:
try:
logger.debug(f"DEBUG: [ChatID: {chat_id}] Attempting to load 'arial.ttf' as fallback.")
font_to_use = ImageFont.truetype("arial.ttf", cfg_font_size) # Common system font
logger.info(f"DEBUG: [ChatID: {chat_id}] Fallback font 'arial.ttf' loaded.")
except IOError:
logger.warning(f"DEBUG: [ChatID: {chat_id}] Arial.ttf not found, using Pillow's load_default(). This may look very basic.")
font_to_use = ImageFont.load_default().font_variant(size=cfg_font_size)
logger.debug(f"DEBUG: [ChatID: {chat_id}] Drawing caption text...")
lines = caption_text.split('\n')
current_y = cfg_caption_area['y']
for i, line in enumerate(lines):
try:
line_bbox = draw.textbbox((0, 0), line, font=font_to_use)
line_height = line_bbox[3] - line_bbox[1]
except AttributeError:
(text_width, text_height) = draw.textsize(line, font=font_to_use)
line_height = text_height
logger.debug(f"DEBUG: [ChatID: {chat_id}] Drawing line {i+1}/{len(lines)}: '{line[:20]}...' at y={current_y}, height={line_height}")
draw.text((cfg_caption_area['x'], current_y), line, font=font_to_use, fill=cfg_caption_color)
current_y += line_height + cfg_line_spacing
logger.info(f"DEBUG: [ChatID: {chat_id}] Saving final image to {output_path}")
final_img.convert("RGB").save(output_path, "PNG")
logger.info(f"DEBUG: [ChatID: {chat_id}] Image generation complete: {output_path}")
return output_path
except FileNotFoundError as fnf_err:
logger.error(f"DEBUG: [ChatID: {chat_id}] File not found during image processing. Template: {frame_template_path}, Raw: {raw_image_path}. Error: {fnf_err}", exc_info=True)
return None
except Exception as e:
logger.error(f"DEBUG: [ChatID: {chat_id}] Critical error during image processing: {e}", exc_info=True)
return None
# --- Bot Event Handlers ---
@bot.on(events.NewMessage(pattern='/start'))
async def start_handler(event):
chat_id = event.chat_id
logger.info(f"DEBUG: [ChatID: {chat_id}, User: {event.sender_id}] Received /start command.")
user_sessions[chat_id] = {'state': 'idle', 'data': {}}
start_message = (
"Welcome! This bot helps you create images with a template.\n\n"
"**Initial Setup (if first time or after a reset):**\n"
"1. `/settemplate` - Upload your frame template image (PNG/JPG).\n"
"2. `/setfont` - Upload your .ttf font file for captions.\n"
"3. `/help_config` - Learn how to set layout parameters.\n"
"4. `/setconfig <key> <value>` - Adjust layout as needed.\n\n"
"**Regular Use:**\n"
"`/create` - Start creating a new image.\n"
"`/viewconfig` - See current layout settings.\n"
"`/cancel` - Cancel current operation.\n\n"
f"**DEBUG INFO:** Session Suffix: `{SESSION_VERSION}`\n"
"**Note on Hosting:** The `data/` and `session/` directories should ideally use persistent storage on your hosting platform. "
"If using ephemeral storage (common on free tiers), your uploaded template, font, config, and session might be lost on restarts, requiring setup again."
)
await event.reply(start_message)
logger.info(f"DEBUG: [ChatID: {chat_id}] Sent welcome message for /start.")
@bot.on(events.NewMessage(pattern='/settemplate'))
async def set_template_handler(event):
chat_id = event.chat_id
logger.info(f"DEBUG: [ChatID: {chat_id}, User: {event.sender_id}] Received /settemplate command.")
user_sessions[chat_id] = {'state': 'awaiting_template_image', 'data': {}}
await event.reply("Please send your frame template image (e.g., a PNG or JPG).")
@bot.on(events.NewMessage(pattern='/setfont'))
async def set_font_handler(event):
chat_id = event.chat_id
logger.info(f"DEBUG: [ChatID: {chat_id}, User: {event.sender_id}] Received /setfont command.")
user_sessions[chat_id] = {'state': 'awaiting_font_file', 'data': {}}
await event.reply("Please send your `.ttf` font file as a document/file.")
@bot.on(events.NewMessage(pattern='/viewconfig'))
async def view_config_handler(event):
chat_id = event.chat_id
logger.info(f"DEBUG: [ChatID: {chat_id}, User: {event.sender_id}] Received /viewconfig command.")
load_bot_config() # Ensure config is fresh
config_str = "Current Bot Configuration:\n"
config_str += f"- Font: {'User font set (' + USER_FONT_FILENAME + ')' if os.path.exists(USER_FONT_PATH) else 'Using fallback font (Arial or Pillow default)'}\n"
for key, value in bot_config.items():
config_str += f"- {key}: {json.dumps(value)}\n"
await event.reply(config_str)
@bot.on(events.NewMessage(pattern='/help_config'))
async def help_config_handler(event):
chat_id = event.chat_id
logger.info(f"DEBUG: [ChatID: {chat_id}, User: {event.sender_id}] Received /help_config command.")
await event.reply(
"**Configuration Commands (`/setconfig <key> <value>`):**\n"
"`raw_image_box_x <num>` (e.g., 40)\n"
"`raw_image_box_y <num>`\n"
"`raw_image_box_width <num>`\n"
"`raw_image_box_height <num>`\n"
"`caption_area_x <num>`\n"
"`caption_area_y <num>`\n"
"`caption_font_size <num>` (e.g., 45)\n"
"`caption_color <name_or_hex>` (e.g., white or #FFFFFF)\n"
"`line_spacing <num>` (e.g., 10)\n\n"
"Example: `/setconfig caption_font_size 50`\n"
"Use `/viewconfig` to see current values. "
"These settings define how the raw image and caption are placed on your template."
)
@bot.on(events.NewMessage(pattern=r'/setconfig (\w+) (.+)'))
async def set_config_handler(event):
chat_id = event.chat_id
key_to_set = event.pattern_match.group(1).strip()
value_str = event.pattern_match.group(2).strip()
logger.info(f"DEBUG: [ChatID: {chat_id}, User: {event.sender_id}] Received /setconfig command. Key: '{key_to_set}', Value: '{value_str}'")
load_bot_config()
original_config_copy = json.dumps(bot_config)
updated = False
sub_key = None # Initialize to avoid UnboundLocalError if key doesn't match known patterns
try:
if key_to_set.startswith("raw_image_box_"):
sub_key = key_to_set.replace("raw_image_box_", "")
if "raw_image_box" in bot_config and sub_key in bot_config["raw_image_box"]:
bot_config["raw_image_box"][sub_key] = int(value_str)
updated = True
elif key_to_set.startswith("caption_area_"):
sub_key = key_to_set.replace("caption_area_", "")
if "caption_area" in bot_config and sub_key in bot_config["caption_area"]:
bot_config["caption_area"][sub_key] = int(value_str)
updated = True
elif key_to_set in ["caption_font_size", "line_spacing"]:
bot_config[key_to_set] = int(value_str)
updated = True
elif key_to_set == "caption_color":
bot_config[key_to_set] = value_str
updated = True
if updated:
if json.dumps(bot_config) != original_config_copy:
save_bot_config()
reply_value = value_str
if sub_key:
if key_to_set.startswith("raw_image_box_"):
reply_value = bot_config.get("raw_image_box", {}).get(sub_key, value_str)
elif key_to_set.startswith("caption_area_"):
reply_value = bot_config.get("caption_area", {}).get(sub_key, value_str)
else:
reply_value = bot_config.get(key_to_set, value_str)
await event.reply(f"Configuration updated: {key_to_set} = {reply_value}")
logger.info(f"DEBUG: [ChatID: {chat_id}] Config '{key_to_set}' updated to '{reply_value}'.")
else:
await event.reply(f"Value for {key_to_set} is already {value_str}. No change made.")
logger.info(f"DEBUG: [ChatID: {chat_id}] Config '{key_to_set}' already '{value_str}'. No change.")
else:
await event.reply(f"Unknown or non-configurable key: '{key_to_set}'. See `/help_config`.")
logger.warning(f"DEBUG: [ChatID: {chat_id}] Unknown config key attempted: '{key_to_set}'.")
except ValueError:
await event.reply(f"Invalid value for '{key_to_set}'. Please provide a number where expected.")
logger.warning(f"DEBUG: [ChatID: {chat_id}] Invalid value for config key '{key_to_set}': '{value_str}'.")
except Exception as e:
await event.reply(f"Error setting config: {e}")
logger.error(f"DEBUG: [ChatID: {chat_id}] Error setting config '{key_to_set}': {e}", exc_info=True)
@bot.on(events.NewMessage(pattern='/create'))
async def create_post_handler(event):
chat_id = event.chat_id
logger.info(f"DEBUG: [ChatID: {chat_id}, User: {event.sender_id}] Received /create command.")
if not os.path.exists(TEMPLATE_PATH):
logger.warning(f"DEBUG: [ChatID: {chat_id}] /create failed: Template not found at {TEMPLATE_PATH}.")
await event.reply("No template found. Please set one using `/settemplate` first.")
return
if not os.path.exists(USER_FONT_PATH):
logger.warning(f"DEBUG: [ChatID: {chat_id}] /create warning: User font not found at {USER_FONT_PATH}. Fallback will be used.")
await event.reply("Warning: No user font found (use `/setfont`). A fallback font will be attempted, but results may vary.")
user_sessions[chat_id] = {'state': 'awaiting_raw_image_for_create', 'data': {}}
await event.reply("Please send the raw image you want to use.")
logger.debug(f"DEBUG: [ChatID: {chat_id}] State set to 'awaiting_raw_image_for_create'.")
@bot.on(events.NewMessage(pattern='/cancel'))
async def cancel_handler(event):
chat_id = event.chat_id
logger.info(f"DEBUG: [ChatID: {chat_id}, User: {event.sender_id}] Received /cancel command.")
if chat_id in user_sessions and user_sessions[chat_id]['state'] != 'idle':
temp_raw_path = user_sessions[chat_id].get('data', {}).get('raw_image_path')
if temp_raw_path and os.path.exists(temp_raw_path):
try:
os.remove(temp_raw_path)
logger.info(f"DEBUG: [ChatID: {chat_id}] Deleted temp raw image: {temp_raw_path}")
except OSError as e:
logger.error(f"DEBUG: [ChatID: {chat_id}] Error deleting temp file {temp_raw_path}: {e}")
user_sessions[chat_id] = {'state': 'idle', 'data': {}}
await event.reply("Operation cancelled.")
logger.info(f"DEBUG: [ChatID: {chat_id}] Operation cancelled, state reset to 'idle'.")
else:
await event.reply("Nothing to cancel.")
logger.info(f"DEBUG: [ChatID: {chat_id}] /cancel received but nothing to cancel.")
@bot.on(events.NewMessage)
async def message_handler(event): # pylint: disable=too-many-branches, too-many-statements
chat_id = event.chat_id
if chat_id not in user_sessions:
user_sessions[chat_id] = {'state': 'idle', 'data': {}}
if event.text and event.text.startswith(('/', '#', '.')):
logger.debug(f"DEBUG: [ChatID: {chat_id}, User: {event.sender_id}] Message ('{event.text[:20]}...') looks like a command, letting patterned handlers manage it.")
return
current_state = user_sessions.get(chat_id, {}).get('state', 'idle')
session_data = user_sessions.get(chat_id, {}).get('data', {})
logger.debug(f"DEBUG: [ChatID: {chat_id}, User: {event.sender_id}] Message received. Current state: '{current_state}'. Message text (first 30 chars): '{str(event.raw_text)[:30]}...' Is photo: {event.photo is not None}. Is document: {event.document is not None}.")
if event.document and current_state == 'awaiting_font_file':
logger.info(f"DEBUG: [ChatID: {chat_id}] Document received in 'awaiting_font_file' state.")
doc_attrs = event.document.attributes
filename = "unknown_document"
for attr in doc_attrs:
if isinstance(attr, types.DocumentAttributeFilename):
filename = attr.file_name
break
logger.debug(f"DEBUG: [ChatID: {chat_id}] Document filename: '{filename}', MIME type: '{event.document.mime_type}'")
is_ttf = False
if filename.lower().endswith('.ttf'):
is_ttf = True
elif hasattr(event.document, 'mime_type') and \
('font' in event.document.mime_type or 'ttf' in event.document.mime_type or 'opentype' in event.document.mime_type):
is_ttf = True
if is_ttf:
await event.reply("Downloading font file...")
logger.info(f"DEBUG: [ChatID: {chat_id}] Downloading TTF font: {filename}")
try:
await bot.download_media(event.message.document, USER_FONT_PATH)
user_sessions[chat_id]['state'] = 'idle'
await event.reply(f"Font saved as '{USER_FONT_FILENAME}' in `data/` directory!")
logger.info(f"DEBUG: [ChatID: {chat_id}] Font saved successfully. State set to 'idle'.")
except Exception as e:
await event.reply(f"Sorry, I couldn't save the font. Error: {e}")
logger.error(f"DEBUG: [ChatID: {chat_id}] Error saving font: {e}", exc_info=True)
user_sessions[chat_id]['state'] = 'idle'
else:
await event.reply("That doesn't look like a .ttf font file. Please send a valid .ttf font file, or use /cancel.")
logger.warning(f"DEBUG: [ChatID: {chat_id}] Received document is not a TTF: {filename}")
return
if event.photo:
logger.info(f"DEBUG: [ChatID: {chat_id}] Photo received in state '{current_state}'.")
if current_state == 'awaiting_template_image':
await event.reply("Downloading template image...")
logger.info(f"DEBUG: [ChatID: {chat_id}] Downloading template image.")
try:
await bot.download_media(event.message.photo, TEMPLATE_PATH)
user_sessions[chat_id]['state'] = 'idle'
await event.reply(f"Template saved as '{TEMPLATE_FILENAME}' in `data/` directory!")
logger.info(f"DEBUG: [ChatID: {chat_id}] Template saved. State set to 'idle'.")
except Exception as e:
await event.reply(f"Couldn't save template image. Error: {e}")
logger.error(f"DEBUG: [ChatID: {chat_id}] Error saving template: {e}", exc_info=True)
user_sessions[chat_id]['state'] = 'idle'
return
elif current_state == 'awaiting_raw_image_for_create':
raw_img_filename = f"raw_{chat_id}_{event.message.id}_{SESSION_VERSION}.jpg"
raw_img_temp_path = os.path.join(DOWNLOADS_DIR, raw_img_filename)
await event.reply("Downloading raw image...")
logger.info(f"DEBUG: [ChatID: {chat_id}] Downloading raw image to {raw_img_temp_path}.")
try:
await bot.download_media(event.message.photo, raw_img_temp_path)
session_data['raw_image_path'] = raw_img_temp_path
user_sessions[chat_id]['state'] = 'awaiting_caption_for_create'
await event.reply("Raw image received. Now, please send the caption text.")
logger.info(f"DEBUG: [ChatID: {chat_id}] Raw image saved. State set to 'awaiting_caption_for_create'.")
except Exception as e:
await event.reply(f"Couldn't save raw image. Error: {e}")
logger.error(f"DEBUG: [ChatID: {chat_id}] Error saving raw image: {e}", exc_info=True)
user_sessions[chat_id]['state'] = 'idle'
return
if event.text and not event.text.startswith('/') and current_state == 'awaiting_caption_for_create':
caption_text = event.text
raw_image_path = session_data.get('raw_image_path')
logger.info(f"DEBUG: [ChatID: {chat_id}] Caption text received: '{caption_text[:30]}...'. Raw image path: {raw_image_path}")
if not raw_image_path or not os.path.exists(raw_image_path):
logger.error(f"DEBUG: [ChatID: {chat_id}] Raw image path not found or file missing: {raw_image_path}. State reset.")
await event.reply("Error: Raw image data not found. Try `/create` again.")
user_sessions[chat_id]['state'] = 'idle'
return
processing_msg = await event.reply("⏳ Processing your image, please wait...")
logger.info(f"DEBUG: [ChatID: {chat_id}] Starting image processing with caption.")
final_image_path = await generate_image_with_frame(raw_image_path, TEMPLATE_PATH, caption_text, chat_id)
try:
logger.debug(f"DEBUG: [ChatID: {chat_id}] Attempting to delete 'Processing...' message.")
await bot.delete_messages(chat_id, processing_msg)
except Exception as e: logger.warning(f"DEBUG: [ChatID: {chat_id}] Could not delete 'Processing...' message: {e}")
if final_image_path and os.path.exists(final_image_path):
logger.info(f"DEBUG: [ChatID: {chat_id}] Final image created: {final_image_path}. Sending to user.")
try:
await bot.send_file(chat_id, final_image_path, caption="🖼️ Here's your generated image!")
logger.info(f"DEBUG: [ChatID: {chat_id}] Final image sent successfully.")
except Exception as e:
await event.reply(f"Sorry, couldn't send the final image. Error: {e}")
logger.error(f"DEBUG: [ChatID: {chat_id}] Error sending final image: {e}", exc_info=True)
finally:
if os.path.exists(final_image_path):
logger.debug(f"DEBUG: [ChatID: {chat_id}] Deleting final image from server: {final_image_path}")
os.remove(final_image_path)
else:
await event.reply("❌ Sorry, something went wrong while creating the image.")
logger.error(f"DEBUG: [ChatID: {chat_id}] Image generation failed or final_image_path is invalid.")
if os.path.exists(raw_image_path):
logger.debug(f"DEBUG: [ChatID: {chat_id}] Deleting temporary raw image: {raw_image_path}")
os.remove(raw_image_path)
user_sessions[chat_id]['state'] = 'idle'
user_sessions[chat_id]['data'] = {}
logger.info(f"DEBUG: [ChatID: {chat_id}] Post creation process complete. State set to 'idle'.")
return
if current_state != 'idle':
logger.debug(f"DEBUG: [ChatID: {chat_id}, User: {event.sender_id}] Unhandled message in state '{current_state}'. Text: '{str(event.raw_text)[:30]}...'")
# Avoid replying to every single message if it's not a command and not in a specific waiting state
# This can be noisy. Only reply if a specific input was expected.
# if current_state not in ['awaiting_template_image', 'awaiting_font_file', 'awaiting_raw_image_for_create', 'awaiting_caption_for_create']:
# pass # Do nothing for other states if non-command text
# else: # If in a specific waiting state, then the message was unexpected.
# await event.reply(f"I'm currently waiting for: `{current_state}`. If you're stuck, try `/cancel`.")
# --- Main Bot Execution ---
async def main():
logger.info("===== Bot main() function starting =====")
load_bot_config()
logger.info("DEBUG: Performing a simple network test to google.com...")
try:
import socket
host_to_test = "google.com"; port_to_test = 80
socket.create_connection((host_to_test, port_to_test), timeout=10)
logger.info(f"DEBUG: Network test to {host_to_test}:{port_to_test} was SUCCESSFUL.")
except OSError as e:
logger.warning(f"DEBUG: Network test to {host_to_test}:{port_to_test} FAILED - OSError: {e}. This might indicate broader network issues.")
except Exception as ex:
logger.warning(f"DEBUG: Network test to {host_to_test}:{port_to_test} FAILED with other exception: {ex}.")
logger.info("DEBUG: Network test completed.")
try:
logger.info(f"DEBUG: Attempting to connect to Telegram with bot token...")
logger.info(f"DEBUG: Using API_ID: {str(API_ID)[:4]}... (masked), Session: {SESSION_NAME}")
await bot.start(bot_token=BOT_TOKEN)
logger.info("SUCCESS: Bot connected to Telegram successfully!")
me = await bot.get_me()
logger.info(f"Bot User Info: Logged in as: @{me.username} (ID: {me.id})")
logger.info("Bot is now running and listening for messages...")
await bot.run_until_disconnected()
except OSError as e:
logger.critical(f"CRITICAL ERROR: OSError during bot.start() or connection phase: {e}. This often indicates network connectivity problems from the container to Telegram servers.", exc_info=True)
except Exception as e:
logger.critical(f"CRITICAL ERROR: An unhandled exception occurred while starting or running the bot: {e}", exc_info=True)
finally:
logger.info("===== Bot main() function ending (or ended due to error) =====")
if bot.is_connected():
logger.info("DEBUG: Bot is connected, attempting to disconnect...")
try:
await bot.disconnect()
logger.info("DEBUG: Bot disconnected successfully.")
except Exception as e:
logger.error(f"DEBUG: Error during bot disconnect: {e}", exc_info=True)
else:
logger.info("DEBUG: Bot was not connected (or already disconnected).")
logger.info("Bot has stopped.")
if __name__ == '__main__':
logger.info("DEBUG: Script is being run directly (__name__ == '__main__').")
print("DEBUG: Starting asyncio event loop...")
try:
asyncio.run(main())
except KeyboardInterrupt:
logger.info("DEBUG: Bot stopped by KeyboardInterrupt (Ctrl+C).")
except Exception as e:
logger.critical(f"DEBUG: Critical error running asyncio event loop: {e}", exc_info=True)
finally:
print("DEBUG: Asyncio event loop finished.")
|