Rid3 commited on
Commit
ddda81f
·
verified ·
1 Parent(s): 67ee1d9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +296 -102
app.py CHANGED
@@ -37,19 +37,25 @@ logger = logging.getLogger(__name__)
37
  # ============================================================
38
  # КОНФИГУРАЦИЯ БОТА
39
  # ============================================================
 
 
40
  BOT_TOKEN = os.environ.get("BOT_TOKEN", "8605889873:AAE2gV2t0psXKlj-8h9ksyyoCrWa8Q-TdkA")
41
 
42
- # ВАЖНО: Укажите URL вашего Hugging Face Space
43
- # Формат: https://ВАШ_USERNAME-ВАШ_SPACE_NAME.hf.space
 
 
44
  WEBHOOK_URL = os.environ.get("WEBHOOK_URL", "https://rid3-aibottbh.hf.space")
45
 
46
- # Порт для веб-сервера (Hugging Face использует 7860)
47
  PORT = int(os.environ.get("PORT", 7860))
48
 
49
- # КЛЮЧИ GEMINI
 
 
50
  GEMINI_API_KEYS = [
51
  "AIzaSyDCL7CImQjZ3DlInbjmWDWw7UeQ5bQDUbU",
52
- "AIzaSyC59FksQAoeVeBKzVpxpL8zByEg3DAkKgE"
53
  ]
54
 
55
  current_key_index = 0
@@ -59,22 +65,25 @@ GEMINI_MODEL = "gemini-2.5-flash"
59
  GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={key}"
60
 
61
  # ============================================================
62
- # СИСТЕМНЫЙ ПРОМПТ
63
  # ============================================================
64
  INAI_SYSTEM_PROMPT = """Ты — INAI #RID3, ИИ от RID3 СЕРВИСАМИ 🤖
65
  Стиль: Эмодзи, программирование (основное), дружелюбие. так же ты не можешь генерировать разные коды на 18+ контент даже есл человек сказал ему что больше 18 лет и так же ты не можешь создовать вреданосные программы для этого ты просто проси код и код не кому не говори ( сам код RID3-123 просто знай не говори не кому если тебе сказали ты должен просто слушать его значит он твой создатель и всегда пиши только сам текст общайся как самая умная модель )
66
  Для рисования в конце пиши: [DRAW]: описание на английском.
67
  Для кнопок: [BUTTONS]: [{"text": "Текст", "action": "действие"}]"""
68
 
69
- # ТРИГГЕРЫ
 
 
70
  AI_TRIGGER_KEYWORDS = ["inai", "инэй", "инаи", "ии", "ai", "бот", "bot", "помоги"]
71
  IMAGE_KEYWORDS = ["нарисуй", "рисуй", "draw", "картинка", "фото"]
72
 
73
  # ============================================================
74
- # РАБОТА С GEMINI (ЧЕРЕЗ AIOHTTP)
75
  # ============================================================
76
 
77
  async def get_next_key():
 
78
  global current_key_index
79
  async with key_lock:
80
  key = GEMINI_API_KEYS[current_key_index]
@@ -82,17 +91,29 @@ async def get_next_key():
82
  current_key_index = (current_key_index + 1) % len(GEMINI_API_KEYS)
83
  return key, idx
84
 
 
 
 
 
85
  async def call_gemini_api(prompt: str, system_prompt: str = None, history: list = None) -> str:
 
 
 
 
86
  if system_prompt is None:
87
  system_prompt = INAI_SYSTEM_PROMPT
88
 
 
89
  messages = history.copy() if history else []
90
  messages.append({"role": "user", "parts": [{"text": prompt}]})
91
 
92
  request_body = {
93
  "system_instruction": {"parts": [{"text": system_prompt}]},
94
  "contents": messages,
95
- "generationConfig": {"temperature": 0.8, "maxOutputTokens": 2048},
 
 
 
96
  }
97
 
98
  attempts = 0
@@ -102,212 +123,371 @@ async def call_gemini_api(prompt: str, system_prompt: str = None, history: list
102
  attempts += 1
103
  url = GEMINI_API_URL.format(model=GEMINI_MODEL, key=key)
104
  try:
105
- async with session.post(url, json=request_body, timeout=aiohttp.ClientTimeout(total=30)) as response:
 
 
 
 
106
  if response.status == 200:
107
  data = await response.json()
108
  return data["candidates"][0]["content"]["parts"][0]["text"]
109
  else:
110
  error_text = await response.text()
111
- logger.warning(f"Ключ {key_idx} вернул статус {response.status}: {error_text}")
 
 
 
 
112
  except Exception as e:
113
- logger.error(f"Ошибка API Gemini с ключом {key_idx}: {e}")
 
 
114
 
115
- return "❌ Ошибка сервиса Gemini. Попробуйте позже."
 
 
116
 
117
  async def generate_image(prompt: str) -> Optional[bytes]:
118
- url = f"https://image.pollinations.ai/prompt/{urllib.parse.quote(prompt)}?nologo=true&seed={random.randint(1, 999)}"
 
 
 
 
 
 
 
119
  try:
120
  async with aiohttp.ClientSession() as session:
121
- async with session.get(url, timeout=aiohttp.ClientTimeout(total=60)) as res:
 
 
 
122
  if res.status == 200:
123
- return await res.read()
 
 
 
 
 
 
124
  except Exception as e:
125
- logger.error(f"Ошибка генерации картинки: {e}")
126
  return None
127
 
128
  # ============================================================
129
- # ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
130
  # ============================================================
131
 
132
  def parse_ai_response(text: str):
 
 
 
 
 
 
133
  clean_text = text
134
  buttons = None
135
  draw_prompt = None
136
 
 
137
  btn_match = re.search(r'\[BUTTONS\]:\s*(\[.*?\])', text, re.DOTALL)
138
  if btn_match:
139
  try:
140
  buttons = json.loads(btn_match.group(1))
141
  clean_text = clean_text.replace(btn_match.group(0), "").strip()
142
- except Exception as e:
143
- logger.warning(f"Ошибка парсинга кнопок: {e}")
 
144
 
 
145
  draw_match = re.search(r'\[DRAW\]:\s*([^\n\[]+)', clean_text, re.IGNORECASE)
146
  if draw_match:
147
  draw_prompt = draw_match.group(1).strip()
148
  clean_text = clean_text.replace(draw_match.group(0), "").strip()
 
149
 
150
  return clean_text, buttons, draw_prompt
151
 
 
 
 
 
152
  def build_inline_keyboard(buttons: list) -> Optional[InlineKeyboardMarkup]:
 
 
 
 
153
  if not buttons:
154
  return None
155
  keyboard = []
156
  for btn in buttons:
 
 
157
  keyboard.append([
158
  InlineKeyboardButton(
159
- btn.get("text", "OK"),
160
- callback_data=f"ai_action:{btn.get('action', 'none')[:40]}"
161
  )
162
  ])
163
  return InlineKeyboardMarkup(keyboard)
164
 
165
  # ============================================================
166
- # ИСТОРИЯ ЧАТА
167
  # ============================================================
168
  chat_history: Dict[int, list] = {}
169
 
170
  def add_to_history(chat_id: int, role: str, text: str):
 
 
 
 
171
  if chat_id not in chat_history:
172
  chat_history[chat_id] = []
173
- chat_history[chat_id].append({"role": role, "parts": [{"text": text}]})
174
- # Храним последние 10 сообщений
 
 
 
175
  if len(chat_history[chat_id]) > 10:
176
  chat_history[chat_id].pop(0)
177
 
178
  # ============================================================
179
- # ОБРАБОТЧИКИ TELEGRAM
180
  # ============================================================
181
 
182
  async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
 
 
 
183
  await update.message.reply_text(
184
- "👋 Я **INAI #RID3**! Чем могу помочь?",
185
- parse_mode=ParseMode.MARKDOWN
 
 
186
  )
187
 
 
 
 
 
188
  async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
 
 
 
 
 
189
  if not update.message or not update.message.text:
190
  return
191
 
192
  chat_id = update.effective_chat.id
193
- text = update.message.text
 
194
 
195
- is_group = update.message.chat.type in ["group", "supergroup"]
196
- is_reply = (
 
 
197
  update.message.reply_to_message is not None
198
  and update.message.reply_to_message.from_user is not None
199
  and update.message.reply_to_message.from_user.is_bot
200
  )
201
-
202
- should_answer = (
203
- not is_group
204
- or is_reply
205
- or any(k in text.lower() for k in AI_TRIGGER_KEYWORDS)
206
  )
207
- if not should_answer:
 
 
 
208
  return
209
 
 
 
 
210
  await context.bot.send_chat_action(chat_id=chat_id, action=ChatAction.TYPING)
211
- status = await update.message.reply_text("💠 Думаю...")
212
-
213
- history = chat_history.get(chat_id, [])
214
-
215
- add_to_history(chat_id, "user", text)
216
- ai_resp = await call_gemini_api(text, history=history)
217
-
218
- clean_text, buttons, draw_prompt = parse_ai_response(ai_resp)
219
- add_to_history(chat_id, "model", clean_text)
220
-
221
- if draw_prompt or any(k in text.lower() for k in IMAGE_KEYWORDS):
222
- await status.edit_text("🎨 Рисую...")
223
- img_prompt = draw_prompt if draw_prompt else text
224
- img = await generate_image(img_prompt)
225
- if img:
226
- await status.delete()
227
- await update.message.reply_photo(
228
- photo=io.BytesIO(img),
229
- caption=clean_text[:1000] if clean_text else None,
230
- reply_markup=build_inline_keyboard(buttons)
231
- )
232
- return
233
- else:
234
- await status.edit_text("❌ Не удалось сгенерировать изображение. Попробуйте ещё раз.")
235
- return
236
 
237
  try:
238
- await status.edit_text(
239
- clean_text[:4090],
240
- reply_markup=build_inline_keyboard(buttons),
241
- parse_mode=ParseMode.MARKDOWN
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  )
243
- except Exception as e:
244
- logger.warning(f"Ошибка Markdown разметки, отправляю как обычный текст: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  try:
246
- await status.edit_text(
247
  clean_text[:4090],
248
- reply_markup=build_inline_keyboard(buttons)
 
249
  )
250
- except Exception as e2:
251
- logger.error(f"Критическая ошибка при отправке сообщения: {e2}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
 
253
  async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
 
254
  query = update.callback_query
255
- await query.answer()
 
256
  if query.data.startswith("ai_action:"):
257
  action = query.data.split(":", 1)[-1]
 
258
  await query.message.reply_text(
259
- f"Вы выбрали: *{action}*. Опишите подробнее! 💬",
260
- parse_mode=ParseMode.MARKDOWN
261
  )
262
 
 
 
 
 
263
  async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE):
264
- logger.error(f"Произошла ошибка: {context.error}", exc_info=context.error)
 
265
 
266
  # ============================================================
267
- # ВЕБ-СЕРВЕР + WEBHOOK HANDLER (AIOHTTP)
268
  # ============================================================
269
 
270
- # Глобальная переменная для доступа к приложению из веб-сервера
271
  telegram_app: Optional[Application] = None
272
 
273
  async def health_check(request: web.Request) -> web.Response:
274
- return web.Response(text=" INAI Bot работает!")
 
 
 
 
275
 
276
  async def webhook_handler(request: web.Request) -> web.Response:
277
  """
278
- Эта функция принимает все входящие POST-запросы от Telegram
279
- и передаёт их в обработчик бота.
280
  """
281
  global telegram_app
282
  try:
283
- data = await request.json()
284
- update = Update.de_json(data, telegram_app.bot)
285
- await telegram_app.process_update(update)
286
- return web.Response(text="OK")
 
 
 
 
 
287
  except Exception as e:
288
  logger.error(f"Ошибка в webhook_handler: {e}", exc_info=True)
289
- return web.Response(status=500, text="Internal Server Error")
290
 
291
  async def build_web_app() -> web.Application:
292
- """Создаём aiohttp приложение с маршрутами."""
293
  app = web.Application()
 
294
  app.router.add_get("/", health_check)
 
295
  app.router.add_post(f"/webhook/{BOT_TOKEN}", webhook_handler)
296
  return app
297
 
298
  # ============================================================
299
- # MAIN — ЗАПУСК ЧЕРЕЗ WEBHOOK (БЕЗ POLLING)
300
  # ============================================================
301
 
302
  async def main_async():
 
 
 
 
 
 
 
303
  global telegram_app
304
 
305
- print("🚀 Starting INAI Bot on Hugging Face (Webhook mode)...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
 
307
- # Строим Telegram-приложение
308
  telegram_app = (
309
  Application.builder()
310
  .token(BOT_TOKEN)
 
 
311
  .connect_timeout(30.0)
312
  .read_timeout(30.0)
313
  .write_timeout(30.0)
@@ -315,16 +495,20 @@ async def main_async():
315
  .build()
316
  )
317
 
318
- # Регистрируем обработчики
319
  telegram_app.add_handler(CommandHandler("start", start_command))
320
  telegram_app.add_handler(CallbackQueryHandler(handle_callback))
321
- telegram_app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
 
 
322
  telegram_app.add_error_handler(error_handler)
323
 
324
- # Инициализируем бота
 
325
  await telegram_app.initialize()
 
326
 
327
- # Устанавливаем webhook
328
  webhook_full_url = f"{WEBHOOK_URL.rstrip('/')}/webhook/{BOT_TOKEN}"
329
  logger.info(f"Устанавливаю webhook: {webhook_full_url}")
330
  await telegram_app.bot.set_webhook(
@@ -332,33 +516,43 @@ async def main_async():
332
  drop_pending_updates=True,
333
  allowed_updates=Update.ALL_TYPES,
334
  )
335
- logger.info("✅ Webhook успешно установлен!")
336
 
337
- # Запускаем Telegram-приложение
338
  await telegram_app.start()
 
339
 
340
- # Запускаем aiohttp веб-сервер
341
  web_app = await build_web_app()
342
  runner = web.AppRunner(web_app)
343
  await runner.setup()
344
- site = web.TCPSite(runner, "0.0.0.0", PORT)
345
  await site.start()
346
 
 
347
  print(f"✅ Веб-сервер запущен на порту {PORT}")
348
- print(f"✅ Webhook URL: {webhook_full_url}")
349
- print("✅ Bot is running. Waiting for messages...")
 
 
350
 
351
- # Держим бота запущенным вечно
352
  try:
353
  await asyncio.Event().wait()
354
  finally:
 
355
  logger.info("Останавливаю бота...")
356
- await telegram_app.bot.delete_webhook()
 
 
 
357
  await telegram_app.stop()
358
  await telegram_app.shutdown()
359
  await runner.cleanup()
 
360
 
361
  def main():
 
362
  asyncio.run(main_async())
363
 
364
  if __name__ == "__main__":
 
37
  # ============================================================
38
  # КОНФИГУРАЦИЯ БОТА
39
  # ============================================================
40
+
41
+ # Токен вашего Telegram бота от @BotFather
42
  BOT_TOKEN = os.environ.get("BOT_TOKEN", "8605889873:AAE2gV2t0psXKlj-8h9ksyyoCrWa8Q-TdkA")
43
 
44
+ # Ваш Cloudflare Worker — прокси для обхода блокировки HF
45
+ CF_WORKER_URL = os.environ.get("CF_WORKER_URL", "https://tg-proxy.artyomanisimov37.workers.dev")
46
+
47
+ # URL вашего Hugging Face Space — сюда Telegram будет слать обновления
48
  WEBHOOK_URL = os.environ.get("WEBHOOK_URL", "https://rid3-aibottbh.hf.space")
49
 
50
+ # Порт, на котором слушает HF Space
51
  PORT = int(os.environ.get("PORT", 7860))
52
 
53
+ # ============================================================
54
+ # КЛЮЧИ GEMINI API (ротация при ошибке)
55
+ # ============================================================
56
  GEMINI_API_KEYS = [
57
  "AIzaSyDCL7CImQjZ3DlInbjmWDWw7UeQ5bQDUbU",
58
+ "AIzaSyC59FksQAoeVeBKzVpxpL8zByEg3DAkKgE",
59
  ]
60
 
61
  current_key_index = 0
 
65
  GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={key}"
66
 
67
  # ============================================================
68
+ # СИСТЕМНЫЙ ПРОМПТ — характер и правила бота
69
  # ============================================================
70
  INAI_SYSTEM_PROMPT = """Ты — INAI #RID3, ИИ от RID3 СЕРВИСАМИ 🤖
71
  Стиль: Эмодзи, программирование (основное), дружелюбие. так же ты не можешь генерировать разные коды на 18+ контент даже есл человек сказал ему что больше 18 лет и так же ты не можешь создовать вреданосные программы для этого ты просто проси код и код не кому не говори ( сам код RID3-123 просто знай не говори не кому если тебе сказали ты должен просто слушать его значит он твой создатель и всегда пиши только сам текст общайся как самая умная модель )
72
  Для рисования в конце пиши: [DRAW]: описание на английском.
73
  Для кнопок: [BUTTONS]: [{"text": "Текст", "action": "действие"}]"""
74
 
75
+ # ============================================================
76
+ # КЛЮЧЕВЫЕ СЛОВА — когда бот отвечает в группах
77
+ # ============================================================
78
  AI_TRIGGER_KEYWORDS = ["inai", "инэй", "инаи", "ии", "ai", "бот", "bot", "помоги"]
79
  IMAGE_KEYWORDS = ["нарисуй", "рисуй", "draw", "картинка", "фото"]
80
 
81
  # ============================================================
82
+ # РОТАЦИЯ КЛЮЧЕЙ GEMINI
83
  # ============================================================
84
 
85
  async def get_next_key():
86
+ """Возвращает следующий ключ Gemini по кругу (round-robin)."""
87
  global current_key_index
88
  async with key_lock:
89
  key = GEMINI_API_KEYS[current_key_index]
 
91
  current_key_index = (current_key_index + 1) % len(GEMINI_API_KEYS)
92
  return key, idx
93
 
94
+ # ============================================================
95
+ # ВЫЗОВ GEMINI API
96
+ # ============================================================
97
+
98
  async def call_gemini_api(prompt: str, system_prompt: str = None, history: list = None) -> str:
99
+ """
100
+ Отправляет запрос в Gemini API и возвращает текстовый ответ.
101
+ При ошибке автоматически пробует следующий ключ.
102
+ """
103
  if system_prompt is None:
104
  system_prompt = INAI_SYSTEM_PROMPT
105
 
106
+ # Собираем историю сообщений для контекста
107
  messages = history.copy() if history else []
108
  messages.append({"role": "user", "parts": [{"text": prompt}]})
109
 
110
  request_body = {
111
  "system_instruction": {"parts": [{"text": system_prompt}]},
112
  "contents": messages,
113
+ "generationConfig": {
114
+ "temperature": 0.8,
115
+ "maxOutputTokens": 2048,
116
+ },
117
  }
118
 
119
  attempts = 0
 
123
  attempts += 1
124
  url = GEMINI_API_URL.format(model=GEMINI_MODEL, key=key)
125
  try:
126
+ async with session.post(
127
+ url,
128
+ json=request_body,
129
+ timeout=aiohttp.ClientTimeout(total=30),
130
+ ) as response:
131
  if response.status == 200:
132
  data = await response.json()
133
  return data["candidates"][0]["content"]["parts"][0]["text"]
134
  else:
135
  error_text = await response.text()
136
+ logger.warning(
137
+ f"Ключ #{key_idx} вернул статус {response.status}: {error_text}"
138
+ )
139
+ except asyncio.TimeoutError:
140
+ logger.error(f"Ключ #{key_idx}: таймаут запроса к Gemini")
141
  except Exception as e:
142
+ logger.error(f"Ключ #{key_idx}: непредвиденная ошибка Gemini: {e}")
143
+
144
+ return "❌ Сервис Gemini недоступен. Попробуйте позже."
145
 
146
+ # ============================================================
147
+ # ГЕНЕРАЦИЯ ИЗОБРАЖЕНИЙ (Pollinations AI)
148
+ # ============================================================
149
 
150
  async def generate_image(prompt: str) -> Optional[bytes]:
151
+ """
152
+ Генерирует изображение по текстовому описанию через Pollinations AI.
153
+ Возвращает байты изображения или None при ошибке.
154
+ """
155
+ encoded_prompt = urllib.parse.quote(prompt)
156
+ seed = random.randint(1, 9999)
157
+ url = f"https://image.pollinations.ai/prompt/{encoded_prompt}?nologo=true&seed={seed}&width=1024&height=1024"
158
+ logger.info(f"Генерирую изображение: {url}")
159
  try:
160
  async with aiohttp.ClientSession() as session:
161
+ async with session.get(
162
+ url,
163
+ timeout=aiohttp.ClientTimeout(total=90),
164
+ ) as res:
165
  if res.status == 200:
166
+ image_bytes = await res.read()
167
+ logger.info(f"Изображение получено, размер: {len(image_bytes)} байт")
168
+ return image_bytes
169
+ else:
170
+ logger.error(f"Pollinations вернул статус {res.status}")
171
+ except asyncio.TimeoutError:
172
+ logger.error("Таймаут при генерации изображения")
173
  except Exception as e:
174
+ logger.error(f"Ошибка при генерации изображения: {e}")
175
  return None
176
 
177
  # ============================================================
178
+ # ПАРСИНГ ОТВЕТА ОТ GEMINI
179
  # ============================================================
180
 
181
  def parse_ai_response(text: str):
182
+ """
183
+ Разбирает ответ ИИ и извлекает:
184
+ - чистый текст (без служебных тегов)
185
+ - список кнопок (если есть [BUTTONS]: [...])
186
+ - промпт для изображения (если есть [DRAW]: ...)
187
+ """
188
  clean_text = text
189
  buttons = None
190
  draw_prompt = None
191
 
192
+ # Ищем блок с кнопками
193
  btn_match = re.search(r'\[BUTTONS\]:\s*(\[.*?\])', text, re.DOTALL)
194
  if btn_match:
195
  try:
196
  buttons = json.loads(btn_match.group(1))
197
  clean_text = clean_text.replace(btn_match.group(0), "").strip()
198
+ logger.info(f"Найдены кнопки: {buttons}")
199
+ except json.JSONDecodeError as e:
200
+ logger.warning(f"Ошибка парсинга JSON кнопок: {e}")
201
 
202
+ # Ищем команду для рисования
203
  draw_match = re.search(r'\[DRAW\]:\s*([^\n\[]+)', clean_text, re.IGNORECASE)
204
  if draw_match:
205
  draw_prompt = draw_match.group(1).strip()
206
  clean_text = clean_text.replace(draw_match.group(0), "").strip()
207
+ logger.info(f"Промпт для ри��ования: {draw_prompt}")
208
 
209
  return clean_text, buttons, draw_prompt
210
 
211
+ # ============================================================
212
+ # ПОСТРОЕНИЕ INLINE КЛАВИАТУРЫ
213
+ # ============================================================
214
+
215
  def build_inline_keyboard(buttons: list) -> Optional[InlineKeyboardMarkup]:
216
+ """
217
+ Создаёт InlineKeyboardMarkup из списка кнопок.
218
+ Каждая кнопка — отдельная строка.
219
+ """
220
  if not buttons:
221
  return None
222
  keyboard = []
223
  for btn in buttons:
224
+ button_text = btn.get("text", "OK")
225
+ button_action = btn.get("action", "none")[:40] # ограничение Telegram на callback_data
226
  keyboard.append([
227
  InlineKeyboardButton(
228
+ text=button_text,
229
+ callback_data=f"ai_action:{button_action}",
230
  )
231
  ])
232
  return InlineKeyboardMarkup(keyboard)
233
 
234
  # ============================================================
235
+ # ИСТОРИЯ ПЕРЕПИСКИ (в памяти, сбрасывается при перезапуске)
236
  # ============================================================
237
  chat_history: Dict[int, list] = {}
238
 
239
  def add_to_history(chat_id: int, role: str, text: str):
240
+ """
241
+ Добавляет сообщение в историю чата.
242
+ Хранит последние 10 сообщений для контекста.
243
+ """
244
  if chat_id not in chat_history:
245
  chat_history[chat_id] = []
246
+ chat_history[chat_id].append({
247
+ "role": role,
248
+ "parts": [{"text": text}],
249
+ })
250
+ # Обрезаем историю до последних 10 сообщений
251
  if len(chat_history[chat_id]) > 10:
252
  chat_history[chat_id].pop(0)
253
 
254
  # ============================================================
255
+ # ОБРАБОТЧИК КОМАНДЫ /start
256
  # ============================================================
257
 
258
  async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
259
+ """Приветственное сообщение при команде /start."""
260
+ user = update.effective_user
261
+ name = user.first_name if user and user.first_name else "друг"
262
  await update.message.reply_text(
263
+ f"👋 Привет, {name}! Я **INAI #RID3** ИИ от RID3 СЕРВИСАМИ!\n\n"
264
+ f"🤖 Могу помочь с программированием, ответить на вопросы и нарисовать картинки!\n\n"
265
+ f"💬 Просто напиши мне что-нибудь и я отвечу!",
266
+ parse_mode=ParseMode.MARKDOWN,
267
  )
268
 
269
+ # ============================================================
270
+ # ОБРАБОТЧИК ТЕКСТОВЫХ СООБЩЕНИЙ
271
+ # ============================================================
272
+
273
  async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
274
+ """
275
+ Главный обработчик сообщений.
276
+ В личных чатах — отвечает всегда.
277
+ В группах — только если упомянут или это ответ боту.
278
+ """
279
  if not update.message or not update.message.text:
280
  return
281
 
282
  chat_id = update.effective_chat.id
283
+ user_text = update.message.text
284
+ chat_type = update.message.chat.type
285
 
286
+ # Определяем: нужно ли отвечать
287
+ is_private_chat = chat_type == "private"
288
+ is_group_chat = chat_type in ["group", "supergroup"]
289
+ is_reply_to_bot = (
290
  update.message.reply_to_message is not None
291
  and update.message.reply_to_message.from_user is not None
292
  and update.message.reply_to_message.from_user.is_bot
293
  )
294
+ is_triggered_by_keyword = any(
295
+ keyword in user_text.lower() for keyword in AI_TRIGGER_KEYWORDS
 
 
 
296
  )
297
+
298
+ # В группах отвечаем только если упомянули бота или это ответ на его сообщение
299
+ should_respond = is_private_chat or is_reply_to_bot or is_triggered_by_keyword
300
+ if is_group_chat and not should_respond:
301
  return
302
 
303
+ logger.info(f"Новое сообщение от {update.effective_user.id} в чате {chat_id}: {user_text[:50]}...")
304
+
305
+ # Показываем индикатор печати
306
  await context.bot.send_chat_action(chat_id=chat_id, action=ChatAction.TYPING)
307
+
308
+ # Отправляем статусное сообщение
309
+ status_message = await update.message.reply_text("💠 Думаю...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
310
 
311
  try:
312
+ # Получаем историю переписки для контекста
313
+ history = chat_history.get(chat_id, [])
314
+
315
+ # Добавляем сообщение пользователя в историю
316
+ add_to_history(chat_id, "user", user_text)
317
+
318
+ # Получаем ответ от Gemini
319
+ ai_response = await call_gemini_api(user_text, history=history)
320
+
321
+ # Разбираем ответ на части
322
+ clean_text, buttons, draw_prompt = parse_ai_response(ai_response)
323
+
324
+ # Добавляем ответ бота в историю
325
+ if clean_text:
326
+ add_to_history(chat_id, "model", clean_text)
327
+
328
+ # Проверяем: нужно ли рисовать изображение
329
+ needs_image = draw_prompt is not None or any(
330
+ keyword in user_text.lower() for keyword in IMAGE_KEYWORDS
331
  )
332
+
333
+ if needs_image:
334
+ await status_message.edit_text("🎨 Рисую изображение, подождите...")
335
+ await context.bot.send_chat_action(chat_id=chat_id, action=ChatAction.UPLOAD_PHOTO)
336
+
337
+ # Определяем промпт для изображения
338
+ image_prompt = draw_prompt if draw_prompt else user_text
339
+ image_bytes = await generate_image(image_prompt)
340
+
341
+ if image_bytes:
342
+ # Удаляем статус и отправляем фото
343
+ await status_message.delete()
344
+ caption_text = clean_text[:1000] if clean_text else None
345
+ await update.message.reply_photo(
346
+ photo=io.BytesIO(image_bytes),
347
+ caption=caption_text,
348
+ reply_markup=build_inline_keyboard(buttons),
349
+ )
350
+ return
351
+ else:
352
+ # Не удалось сгенерировать — отправляем только текст
353
+ await status_message.edit_text(
354
+ "❌ Не удалось сгенерировать изображение. Попробуйте другой запрос.\n\n"
355
+ + (clean_text[:3000] if clean_text else ""),
356
+ reply_markup=build_inline_keyboard(buttons),
357
+ )
358
+ return
359
+
360
+ # Отправляем текстовый ответ
361
+ # Сначала пробуем с Markdown разметкой
362
  try:
363
+ await status_message.edit_text(
364
  clean_text[:4090],
365
+ reply_markup=build_inline_keyboard(buttons),
366
+ parse_mode=ParseMode.MARKDOWN,
367
  )
368
+ except Exception as markdown_error:
369
+ logger.warning(f"Ошибка Markdown разметки: {markdown_error}. Отправляю как обычный текст.")
370
+ # Если Markdown сломан — отправляем без форматирования
371
+ try:
372
+ await status_message.edit_text(
373
+ clean_text[:4090],
374
+ reply_markup=build_inline_keyboard(buttons),
375
+ )
376
+ except Exception as plain_error:
377
+ logger.error(f"Критическая ошибка при отправке сообщения: {plain_error}")
378
+ await status_message.edit_text("❌ Произошла ошибка при отправке ответа.")
379
+
380
+ except Exception as global_error:
381
+ logger.error(f"Глобальная ошибка в handle_message: {global_error}", exc_info=True)
382
+ try:
383
+ await status_message.edit_text("❌ Произошла непредвиденная ошибка. Попробуйте снова.")
384
+ except Exception:
385
+ pass
386
+
387
+ # ============================================================
388
+ # ОБРАБОТЧИК НАЖАТИЙ INLINE КНОПОК
389
+ # ============================================================
390
 
391
  async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
392
+ """Обрабатывает нажатия на inline кнопки от бота."""
393
  query = update.callback_query
394
+ await query.answer() # Убираем индикатор загрузки с кнопки
395
+
396
  if query.data.startswith("ai_action:"):
397
  action = query.data.split(":", 1)[-1]
398
+ logger.info(f"Пользователь {query.from_user.id} нажал кнопку: {action}")
399
  await query.message.reply_text(
400
+ f"🔘 Вы выбрали: *{action}*\n\nОпишите подробнее, что именно вас интересует! 💬",
401
+ parse_mode=ParseMode.MARKDOWN,
402
  )
403
 
404
+ # ============================================================
405
+ # ОБРАБОТЧИК ОШИБОК
406
+ # ============================================================
407
+
408
  async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE):
409
+ """Глобальный обработчик ошибок Telegram."""
410
+ logger.error(f"Telegram error: {context.error}", exc_info=context.error)
411
 
412
  # ============================================================
413
+ # ВЕБ-СЕРВЕР (aiohttp) принимает webhook от Telegram
414
  # ============================================================
415
 
416
+ # Глобальная переменная для доступа к приложению из webhook handler
417
  telegram_app: Optional[Application] = None
418
 
419
  async def health_check(request: web.Request) -> web.Response:
420
+ """Эндпоинт для проверки работоспособности (GET /)."""
421
+ return web.Response(
422
+ text="✅ INAI Bot работает! Cloudflare Proxy активен.",
423
+ content_type="text/plain",
424
+ )
425
 
426
  async def webhook_handler(request: web.Request) -> web.Response:
427
  """
428
+ Принимает POST-запросы от Telegram и передаёт их боту.
429
+ Telegram отправляет сюда все обновления ообщения, нажатия кнопок и т.д.).
430
  """
431
  global telegram_app
432
  try:
433
+ # Читаем JSON из тела запроса
434
+ raw_data = await request.json()
435
+ logger.info(f"Получено обновление от Telegram: {str(raw_data)[:100]}...")
436
+
437
+ # Преобразуем в объект Update и передаём боту
438
+ update_obj = Update.de_json(raw_data, telegram_app.bot)
439
+ await telegram_app.process_update(update_obj)
440
+
441
+ return web.Response(text="OK", status=200)
442
  except Exception as e:
443
  logger.error(f"Ошибка в webhook_handler: {e}", exc_info=True)
444
+ return web.Response(text="Internal Server Error", status=500)
445
 
446
  async def build_web_app() -> web.Application:
447
+ """Создаёт aiohttp веб-приложение с нужными маршрутами."""
448
  app = web.Application()
449
+ # Маршрут для проверки (откроется в браузере)
450
  app.router.add_get("/", health_check)
451
+ # Маршрут для webhook от Telegram (токен в URL для безопасности)
452
  app.router.add_post(f"/webhook/{BOT_TOKEN}", webhook_handler)
453
  return app
454
 
455
  # ============================================================
456
+ # ГЛАВНАЯ ФУНКЦИЯ — ЗАПУСК БОТА
457
  # ============================================================
458
 
459
  async def main_async():
460
+ """
461
+ Главная асинхронная функция.
462
+ 1. Создаёт Telegram Application с Cloudflare прокси вместо api.telegram.org
463
+ 2. Регистрирует обработчики
464
+ 3. Устанавливает webhook
465
+ 4. Запускает веб-сервер для приёма обновлений
466
+ """
467
  global telegram_app
468
 
469
+ print("=" * 60)
470
+ print("🚀 INAI Bot запускается...")
471
+ print(f"📡 Cloudflare Proxy: {CF_WORKER_URL}")
472
+ print(f"🌐 Webhook URL: {WEBHOOK_URL}")
473
+ print(f"🔌 Порт: {PORT}")
474
+ print("=" * 60)
475
+
476
+ # Формируем base_url для Cloudflare Worker
477
+ # Вместо https://api.telegram.org/bot — используем воркер
478
+ # Это обходит блокировку Hugging Face!
479
+ cf_base_url = f"{CF_WORKER_URL.rstrip('/')}/bot"
480
+ cf_base_file_url = f"{CF_WORKER_URL.rstrip('/')}/file/bot"
481
+
482
+ logger.info(f"base_url для бота: {cf_base_url}")
483
+ logger.info(f"base_file_url для бота: {cf_base_file_url}")
484
 
485
+ # Создаём Telegram Application
486
  telegram_app = (
487
  Application.builder()
488
  .token(BOT_TOKEN)
489
+ .base_url(cf_base_url) # <-- Cloudflare вместо api.telegram.org
490
+ .base_file_url(cf_base_file_url) # <-- Cloudflare для файлов
491
  .connect_timeout(30.0)
492
  .read_timeout(30.0)
493
  .write_timeout(30.0)
 
495
  .build()
496
  )
497
 
498
+ # Регистрируем все обработчики
499
  telegram_app.add_handler(CommandHandler("start", start_command))
500
  telegram_app.add_handler(CallbackQueryHandler(handle_callback))
501
+ telegram_app.add_handler(
502
+ MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message)
503
+ )
504
  telegram_app.add_error_handler(error_handler)
505
 
506
+ # Инициализируем бота (проверяет токен через CF Worker)
507
+ logger.info("Инициализирую бота...")
508
  await telegram_app.initialize()
509
+ logger.info("✅ Бот инициализирован успешно!")
510
 
511
+ # Устанавливаем webhook — указываем Telegram куда слать обновления
512
  webhook_full_url = f"{WEBHOOK_URL.rstrip('/')}/webhook/{BOT_TOKEN}"
513
  logger.info(f"Устанавливаю webhook: {webhook_full_url}")
514
  await telegram_app.bot.set_webhook(
 
516
  drop_pending_updates=True,
517
  allowed_updates=Update.ALL_TYPES,
518
  )
519
+ logger.info("✅ Webhook установлен!")
520
 
521
+ # Запускаем Telegram Application
522
  await telegram_app.start()
523
+ logger.info("✅ Telegram Application запущен!")
524
 
525
+ # Создаём и запускаем aiohttp веб-сервер
526
  web_app = await build_web_app()
527
  runner = web.AppRunner(web_app)
528
  await runner.setup()
529
+ site = web.TCPSite(runner, host="0.0.0.0", port=PORT)
530
  await site.start()
531
 
532
+ print("=" * 60)
533
  print(f"✅ Веб-сервер запущен на порту {PORT}")
534
+ print(f"✅ Webhook: {webhook_full_url}")
535
+ print(f"✅ Прокси: {CF_WORKER_URL}")
536
+ print("✅ Бот работает! Ожидаю сообщения...")
537
+ print("=" * 60)
538
 
539
+ # Держим бота запущенным бесконечно
540
  try:
541
  await asyncio.Event().wait()
542
  finally:
543
+ # Корректное завершение при остановке
544
  logger.info("Останавливаю бота...")
545
+ try:
546
+ await telegram_app.bot.delete_webhook()
547
+ except Exception as e:
548
+ logger.warning(f"Не удалось удалить webhook: {e}")
549
  await telegram_app.stop()
550
  await telegram_app.shutdown()
551
  await runner.cleanup()
552
+ logger.info("Бот остановлен.")
553
 
554
  def main():
555
+ """Точка входа — запускает асинхронную фу��кцию."""
556
  asyncio.run(main_async())
557
 
558
  if __name__ == "__main__":