Spaces:
Runtime error
Runtime error
Update app.py
Browse files
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 |
-
|
|
|
|
|
9 |
try:
|
|
|
10 |
from dotenv import load_dotenv
|
11 |
-
|
|
|
|
|
|
|
|
|
12 |
except ImportError:
|
13 |
-
|
|
|
|
|
|
|
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 |
-
|
21 |
-
|
|
|
|
|
22 |
try:
|
23 |
API_ID = int(API_ID)
|
|
|
24 |
except ValueError:
|
25 |
-
|
26 |
-
exit(1)
|
27 |
|
28 |
-
#
|
29 |
-
|
30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
31 |
|
32 |
|
33 |
-
# --- Paths and
|
|
|
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 |
-
#
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
os.makedirs(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
49 |
|
50 |
-
# --- Default Configuration
|
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 |
-
|
68 |
-
bot_config.
|
|
|
|
|
69 |
else:
|
|
|
70 |
bot_config = DEFAULT_CONFIG.copy()
|
71 |
-
|
72 |
-
|
|
|
|
|
|
|
|
|
|
|
73 |
except Exception as e:
|
74 |
-
|
75 |
bot_config = DEFAULT_CONFIG.copy()
|
76 |
-
|
|
|
|
|
|
|
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 |
-
|
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 |
-
|
92 |
-
|
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 |
-
|
120 |
-
|
|
|
121 |
if not font_to_use:
|
122 |
try:
|
|
|
123 |
font_to_use = ImageFont.truetype("arial.ttf", cfg_font_size)
|
|
|
124 |
except IOError:
|
125 |
-
|
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 |
-
|
|
|
|
|
142 |
return output_path
|
143 |
|
144 |
except FileNotFoundError as fnf_err:
|
145 |
-
|
146 |
return None
|
147 |
except Exception as e:
|
148 |
-
|
149 |
-
import traceback
|
150 |
-
traceback.print_exc()
|
151 |
return None
|
152 |
|
153 |
-
# --- Bot Event Handlers ---
|
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 |
-
|
|
|
|
|
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
|
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:
|
282 |
-
|
|
|
|
|
|
|
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
|
|
|
|
|
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 |
-
|
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 |
-
|
|
|
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:
|
362 |
-
|
|
|
|
|
|
|
|
|
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):
|
|
|
|
|
371 |
else:
|
372 |
-
await event.reply("❌ Sorry, something went wrong while creating the image.")
|
|
|
373 |
|
374 |
-
|
|
|
|
|
|
|
375 |
user_sessions[chat_id]['state'] = 'idle'
|
376 |
-
user_sessions[chat_id]['data'] = {}
|
|
|
377 |
return
|
378 |
|
379 |
-
|
|
|
|
|
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 |
-
|
385 |
-
load_bot_config()
|
386 |
|
|
|
|
|
387 |
try:
|
388 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
389 |
await bot.start(bot_token=BOT_TOKEN)
|
390 |
-
|
|
|
391 |
me = await bot.get_me()
|
392 |
-
|
|
|
|
|
393 |
await bot.run_until_disconnected()
|
|
|
|
|
|
|
394 |
except Exception as e:
|
395 |
-
|
396 |
-
import traceback
|
397 |
-
traceback.print_exc()
|
398 |
finally:
|
399 |
-
|
400 |
if bot.is_connected():
|
401 |
-
|
402 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
403 |
|
404 |
if __name__ == '__main__':
|
405 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|