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.")