Spaces:
Runtime error
Runtime error
File size: 31,590 Bytes
dd499c1 92e6005 06b39f0 92e6005 dd499c1 92e6005 dd499c1 92e6005 dd499c1 06b39f0 dd499c1 92e6005 dd499c1 06b39f0 dd499c1 92e6005 06b39f0 dd499c1 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 92e6005 06b39f0 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 |
# app.py (Partial - showing key changes)
import os
import asyncio
import json
import logging
import sys
# --- Detailed Logging Setup ---
# ... (keep the detailed logging setup as before) ...
logger = logging.getLogger(__name__)
logging.getLogger('telethon').setLevel(logging.DEBUG)
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.")
# ... (rest of imports, env var loading) ...
# --- Session Name (CHANGE THIS OFTEN FOR DEBUGGING CONNECTION HANGS) ---
SESSION_VERSION = "v3_debug_run" # <<<<<<< CHANGE THIS TO A NEW VALUE
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)
# ... (rest of the script, including the main() function) ...
# --- Paths and Directory Setup ---
logger.info("DEBUG: Defining paths and ensuring directories...")
DATA_DIR = "data"
DOWNLOADS_DIR = "downloads" # Changed from TEMP_DIR to match Dockerfile
SESSION_DIR = "session" # Directory for Telethon session specified in Dockerfile
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)
# Dockerfile should create these, but Python checks are good for robustness
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).")
# Optional: Check writability, though chmod 777 in Dockerfile should cover this
if not os.access(dir_path, os.W_OK):
logger.warning(f"DEBUG: Directory '{dir_path}' might not be writable despite creation!")
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: {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() # Start with defaults
bot_config.update(loaded_values) # Override with 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()
# Always save after loading/initializing to ensure file exists with all keys and consistent format
save_bot_config(initial_load=True)
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) # Attempt to save a clean default config
except Exception as e:
logger.error(f"DEBUG: Critical error loading config, using defaults: {e}", exc_info=True)
bot_config = DEFAULT_CONFIG.copy()
# Avoid saving if a critical non-JSON error occurred during load, to prevent infinite loops if save also fails
if not os.path.exists(CONFIG_PATH): # Save only if file didn't exist
save_bot_config(initial_load=True)
def save_bot_config(initial_load=False):
global bot_config
if not initial_load: # Only log intent if it's a user-triggered save
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)
if not initial_load:
logger.info(f"DEBUG: Configuration saved successfully: {bot_config}")
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" # Add session version for uniqueness
output_path = os.path.join(DOWNLOADS_DIR, output_filename)
logger.debug(f"DEBUG: [ChatID: {chat_id}] Output path will be: {output_path}")
# Use loaded configuration
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)
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) #Pillow 10+
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: # Pillow 9.2.0+ textbbox
line_bbox = draw.textbbox((0, 0), line, font=font_to_use)
line_height = line_bbox[3] - line_bbox[1]
except AttributeError: # Fallback for older Pillow versions or basic font
(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") # Consider JPEG for smaller size if transparency not needed
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}] Received /start command from user {event.sender_id}")
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"
"**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.")
# ... (Other command handlers: /settemplate, /setfont, /viewconfig, /help_config, /setconfig, /create, /cancel)
# Add logger.info(f"DEBUG: [ChatID: {chat_id}] Received {command_name} command...") to each
@bot.on(events.NewMessage(pattern='/settemplate'))
async def set_template_handler(event):
chat_id = event.chat_id
logger.info(f"DEBUG: [ChatID: {chat_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}] 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}] 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}] 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}] Received /setconfig command. Key: '{key_to_set}', Value: '{value_str}'")
load_bot_config()
original_config_copy = json.dumps(bot_config) # For checking if value actually changed
updated = False
sub_key = None
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 # No specific validation, user responsible
updated = True
if updated:
if json.dumps(bot_config) != original_config_copy:
save_bot_config()
# Construct reply value more carefully
reply_value = value_str
if sub_key: # if it was a nested 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 (e.g., for sizes, coordinates).")
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}] 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}] 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 (state was 'idle' or no session).")
@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: # Initialize session if it doesn't exist
user_sessions[chat_id] = {'state': 'idle', 'data': {}}
logger.debug(f"DEBUG: [ChatID: {chat_id}] New user or session cleared, initialized session.")
# Let specific command handlers (defined with patterns) take precedence
# This generic handler processes messages based on user state.
if event.text and event.text.startswith(('/', '#', '.')): # Common command prefixes
logger.debug(f"DEBUG: [ChatID: {chat_id}] Message looks like a command ('{event.text[:20]}...'), 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}] 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}.")
# Handle font upload (as document)
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! It will be used for new images.")
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 based on its name or type. 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
# Handle image uploads for template or raw image
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! Remember to `/setfont` and use `/setconfig` if needed.")
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 for the image.")
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
# Handle text input for caption
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. Please try the `/create` command 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: # Try to delete "Processing..." message
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. Please check if the template and font are set correctly, and if the raw image is valid.")
logger.error(f"DEBUG: [ChatID: {chat_id}] Image generation failed or final_image_path is invalid.")
# Cleanup raw image and reset state
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'] = {} # Clear session data for this user
logger.info(f"DEBUG: [ChatID: {chat_id}] Post creation process complete. State set to 'idle'.")
return
# If message wasn't handled by specific states above and is not a command
if current_state != 'idle': # Only reply if in an active state and message wasn't processed
logger.debug(f"DEBUG: [ChatID: {chat_id}] Unhandled message in state '{current_state}'. Text: '{str(event.raw_text)[:30]}...'")
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() # Load or create initial config on startup
# --- Network Test (Optional but recommended for debugging connection hangs) ---
logger.info("DEBUG: Performing a simple network test to google.com...")
try:
import socket # Standard library, should be available
host_to_test = "google.com"
port_to_test = 80 # HTTP port
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 in the container.")
except Exception as ex:
logger.warning(f"DEBUG: Network test to {host_to_test}:{port_to_test} FAILED with other exception: {ex}. This might indicate broader network issues.")
logger.info("DEBUG: Network test completed.")
# --- End Network Test ---
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}")
# The actual connection happens here
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: # Specific catch for network related OS errors during start
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...") # Ensure this prints before loop starts
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.")
|