understanding commited on
Commit
92e6005
·
verified ·
1 Parent(s): 8c532b0

Update app.py

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