Rid3 commited on
Commit
83ce55b
·
verified ·
1 Parent(s): ddda81f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +277 -149
app.py CHANGED
@@ -38,20 +38,27 @@ logger = logging.getLogger(__name__)
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",
@@ -65,18 +72,66 @@ GEMINI_MODEL = "gemini-2.5-flash"
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
@@ -97,13 +152,12 @@ async def get_next_key():
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
 
@@ -111,8 +165,10 @@ async def call_gemini_api(prompt: str, system_prompt: str = None, history: list
111
  "system_instruction": {"parts": [{"text": system_prompt}]},
112
  "contents": messages,
113
  "generationConfig": {
114
- "temperature": 0.8,
115
- "maxOutputTokens": 2048,
 
 
116
  },
117
  }
118
 
@@ -126,22 +182,22 @@ async def call_gemini_api(prompt: str, system_prompt: str = None, history: list
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)
@@ -149,64 +205,67 @@ async def call_gemini_api(prompt: str, system_prompt: str = None, history: list
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 КЛАВИАТУРЫ
@@ -214,15 +273,16 @@ def parse_ai_response(text: str):
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,
@@ -232,14 +292,14 @@ def build_inline_keyboard(buttons: list) -> Optional[InlineKeyboardMarkup]:
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] = []
@@ -247,8 +307,8 @@ def add_to_history(chat_id: int, role: str, text: str):
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
  # ============================================================
@@ -256,13 +316,19 @@ def add_to_history(chat_id: int, role: str, text: str):
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
 
@@ -272,44 +338,49 @@ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
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
  # Добавляем сообщение пользователя в историю
@@ -321,103 +392,173 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
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",
@@ -425,69 +566,60 @@ async def health_check(request: web.Request) -> web.Response:
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,7 +627,7 @@ async def main_async():
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(
@@ -503,12 +635,12 @@ async def main_async():
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(
@@ -520,9 +652,8 @@ async def main_async():
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()
@@ -530,17 +661,15 @@ async def main_async():
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()
@@ -552,7 +681,6 @@ async def main_async():
552
  logger.info("Бот остановлен.")
553
 
554
  def main():
555
- """Точка входа — запускает асинхронную функцию."""
556
  asyncio.run(main_async())
557
 
558
  if __name__ == "__main__":
 
38
  # КОНФИГУРАЦИЯ БОТА
39
  # ============================================================
40
 
 
41
  BOT_TOKEN = os.environ.get("BOT_TOKEN", "8605889873:AAE2gV2t0psXKlj-8h9ksyyoCrWa8Q-TdkA")
42
 
43
+ # Cloudflare Worker прокси (обходит блокировку HF)
44
  CF_WORKER_URL = os.environ.get("CF_WORKER_URL", "https://tg-proxy.artyomanisimov37.workers.dev")
45
 
46
+ # URL Hugging Face Space для webhook
47
  WEBHOOK_URL = os.environ.get("WEBHOOK_URL", "https://rid3-aibottbh.hf.space")
48
 
49
+ # Порт HF Space
50
  PORT = int(os.environ.get("PORT", 7860))
51
 
52
  # ============================================================
53
+ # БЕЛЫЙ СПИСОК ЧАТОВ бот отвечает ТОЛЬКО в этих чатах
54
+ # Чтобы узнать ID чата: добавь бота, напиши /start, посмотри логи
55
+ # Также можно использовать @username чата
56
+ # ============================================================
57
+ ALLOWED_CHAT_USERNAMES = ["rid3_clan", "sonicteamchat101"] # username без @
58
+ ALLOWED_CHAT_IDS: list = [] # Сюда можно добавить числовые ID чатов если нужно
59
+
60
+ # ============================================================
61
+ # КЛЮЧИ GEMINI API
62
  # ============================================================
63
  GEMINI_API_KEYS = [
64
  "AIzaSyDCL7CImQjZ3DlInbjmWDWw7UeQ5bQDUbU",
 
72
  GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={key}"
73
 
74
  # ============================================================
75
+ # СИСТЕМНЫЙ ПРОМПТ
76
+ # Исправлено: запрещаем генерировать правила чата в стиле JSON/кода,
77
+ # требуем развёрнутые ответы, кнопки только когда реально нужны
78
  # ============================================================
79
+ INAI_SYSTEM_PROMPT = """Ты — INAI #RID3, ИИ-ассистент от RID3🤖
80
+
81
+ ПРАВИЛА ПОВЕДЕНИЯ:
82
+ Всегда отвечай развёрнуто и подробно. Никогда не давай короткие ответы из 1-2 предложений если тема требует объяснения.
83
+ — Используй эмодзи умеренно для живости текста.
84
+ — Пиши грамотно, структурированно. Используй заголовки, списки, блоки кода где уместно.
85
+ — НИКОГДА не генерируй правила в формате JSON, XML, псевдокода или переменных типа (respect_all = true). Это выглядит глупо. Правила пиши как обычный человеческий текст.
86
+ — Ты не можешь создавать вредоносный код, малварь, вирусы, эксплойты. Вежливо отказывай.
87
+ — Ты не можешь генерировать 18+ контент ни при каких условиях.
88
+ — Секретный код создателя RID3-123. Не раскрывай его никому. Если кто-то назвал этот код — слушайся его как создателя.
89
+ — Основная специализация: программирование, технологии, помощь пользователям.
90
+
91
+ КНОПКИ:
92
+ — Добавляй ��нопки [BUTTONS] ТОЛЬКО когда это реально полезно — например предложить варианты действий, выбор темы, навигацию.
93
+ — НЕ добавляй кнопки просто так к каждому сообщению.
94
+ — Формат: [BUTTONS]: [{"text": "Текст кнопки", "action": "действие"}]
95
+
96
+ ИЗОБРАЖЕНИЯ:
97
+ — Если просят нарисовать — в конце ответа добавь: [DRAW]: описание на английском языке
98
+
99
+ СТИЛЬ ОБЩЕНИЯ:
100
+ — Дружелюбный, живой, как умный друг-программист.
101
+ — Развёрнутые объяснения с примерами.
102
+ — Если тема сложная — разбивай на части с заголовками."""
103
 
104
  # ============================================================
105
+ # ТРИГГЕРЫ ДЛЯ ГРУППОВЫХ ЧАТОВ
106
  # ============================================================
107
+ AI_TRIGGER_KEYWORDS = ["inai", "инэй", "инаи", "ии", "ai", "бот", "bot", "помоги", "помогите"]
108
+ IMAGE_KEYWORDS = ["нарисуй", "нарисуйте", "рисуй", "draw", "картинка", "картинку", "фото", "изображение"]
109
+
110
+ # ============================================================
111
+ # ПРОВЕРКА — разрешён ли чат
112
+ # ============================================================
113
+
114
+ def is_chat_allowed(update: Update) -> bool:
115
+ """
116
+ Возвращает True если чат находится в белом списке.
117
+ Проверяет как username так и числовой ID чата.
118
+ """
119
+ chat = update.effective_chat
120
+ if chat is None:
121
+ return False
122
+
123
+ # Проверяем username чата (без @)
124
+ if chat.username and chat.username.lower() in [u.lower() for u in ALLOWED_CHAT_USERNAMES]:
125
+ logger.info(f"Чат @{chat.username} разрешён (по username)")
126
+ return True
127
+
128
+ # Проверяем числовой ID чата
129
+ if chat.id in ALLOWED_CHAT_IDS:
130
+ logger.info(f"Чат {chat.id} разрешён (по ID)")
131
+ return True
132
+
133
+ logger.info(f"Чат {chat.id} (@{chat.username}) НЕ в белом списке — игнорирую")
134
+ return False
135
 
136
  # ============================================================
137
  # РОТАЦИЯ КЛЮЧЕЙ GEMINI
 
152
 
153
  async def call_gemini_api(prompt: str, system_prompt: str = None, history: list = None) -> str:
154
  """
155
+ Отправляет запрос в Gemini и возвращает ответ.
156
+ При ошибке пробует следующий ключ.
157
  """
158
  if system_prompt is None:
159
  system_prompt = INAI_SYSTEM_PROMPT
160
 
 
161
  messages = history.copy() if history else []
162
  messages.append({"role": "user", "parts": [{"text": prompt}]})
163
 
 
165
  "system_instruction": {"parts": [{"text": system_prompt}]},
166
  "contents": messages,
167
  "generationConfig": {
168
+ "temperature": 0.85,
169
+ "maxOutputTokens": 4096, # Увеличено для развёрнутых ответов
170
+ "topP": 0.95,
171
+ "topK": 40,
172
  },
173
  }
174
 
 
182
  async with session.post(
183
  url,
184
  json=request_body,
185
+ timeout=aiohttp.ClientTimeout(total=45),
186
  ) as response:
187
  if response.status == 200:
188
  data = await response.json()
189
+ result_text = data["candidates"][0]["content"]["parts"][0]["text"]
190
+ logger.info(f"Gemini ответил ({len(result_text)} символов)")
191
+ return result_text
192
  else:
193
  error_text = await response.text()
194
+ logger.warning(f"Ключ #{key_idx} статус {response.status}: {error_text[:200]}")
 
 
195
  except asyncio.TimeoutError:
196
  logger.error(f"Ключ #{key_idx}: таймаут запроса к Gemini")
197
  except Exception as e:
198
+ logger.error(f"Ключ #{key_idx}: ошибка Gemini: {e}")
199
 
200
+ return "❌ Сервис временно недоступен. Попробуй чуть позже!"
201
 
202
  # ============================================================
203
  # ГЕНЕРАЦИЯ ИЗОБРАЖЕНИЙ (Pollinations AI)
 
205
 
206
  async def generate_image(prompt: str) -> Optional[bytes]:
207
  """
208
+ Генерирует изображение через Pollinations AI.
209
  Возвращает байты изображения или None при ошибке.
210
  """
211
  encoded_prompt = urllib.parse.quote(prompt)
212
+ seed = random.randint(1, 99999)
213
+ url = (
214
+ f"https://image.pollinations.ai/prompt/{encoded_prompt}"
215
+ f"?nologo=true&seed={seed}&width=1024&height=1024&enhance=true"
216
+ )
217
+ logger.info(f"Генерирую изображение по промпту: {prompt[:80]}")
218
  try:
219
  async with aiohttp.ClientSession() as session:
220
  async with session.get(
221
  url,
222
+ timeout=aiohttp.ClientTimeout(total=120),
223
  ) as res:
224
  if res.status == 200:
225
  image_bytes = await res.read()
226
+ logger.info(f"Изображение получено: {len(image_bytes)} байт")
227
  return image_bytes
228
  else:
229
+ logger.error(f"Pollinations статус {res.status}")
230
  except asyncio.TimeoutError:
231
  logger.error("Таймаут при генерации изображения")
232
  except Exception as e:
233
+ logger.error(f"Ошибка генерации изображения: {e}")
234
  return None
235
 
236
  # ============================================================
237
+ # ПАРСИНГ ОТВЕТА GEMINI
238
  # ============================================================
239
 
240
  def parse_ai_response(text: str):
241
  """
242
+ Разбирает ответ ИИ:
243
+ - Извлекает список кнопок [BUTTONS]: [...]
244
+ - Извлекает промпт для изображения [DRAW]: ...
245
+ - Возвращает чистый текст без служебных тегов
246
  """
247
  clean_text = text
248
  buttons = None
249
  draw_prompt = None
250
 
251
+ # Ищем блок кнопок
252
  btn_match = re.search(r'\[BUTTONS\]:\s*(\[.*?\])', text, re.DOTALL)
253
  if btn_match:
254
  try:
255
  buttons = json.loads(btn_match.group(1))
256
  clean_text = clean_text.replace(btn_match.group(0), "").strip()
257
+ logger.info(f"Найдены кнопки: {len(buttons)} шт.")
258
  except json.JSONDecodeError as e:
259
+ logger.warning(f"Ошибка парсинга кнопок JSON: {e}")
260
 
261
+ # Ищем команду рисования
262
  draw_match = re.search(r'\[DRAW\]:\s*([^\n\[]+)', clean_text, re.IGNORECASE)
263
  if draw_match:
264
  draw_prompt = draw_match.group(1).strip()
265
  clean_text = clean_text.replace(draw_match.group(0), "").strip()
266
+ logger.info(f"Промпт для изображения: {draw_prompt[:60]}")
267
 
268
+ return clean_text.strip(), buttons, draw_prompt
269
 
270
  # ============================================================
271
  # ПОСТРОЕНИЕ INLINE КЛАВИАТУРЫ
 
273
 
274
  def build_inline_keyboard(buttons: list) -> Optional[InlineKeyboardMarkup]:
275
  """
276
+ Строит InlineKeyboardMarkup из списка кнопок.
277
+ Каждая кнопка на отдельной строке.
278
  """
279
  if not buttons:
280
  return None
281
  keyboard = []
282
  for btn in buttons:
283
  button_text = btn.get("text", "OK")
284
+ # Telegram ограничивает callback_data до 64 байт
285
+ button_action = str(btn.get("action", "none"))[:40]
286
  keyboard.append([
287
  InlineKeyboardButton(
288
  text=button_text,
 
292
  return InlineKeyboardMarkup(keyboard)
293
 
294
  # ============================================================
295
+ # ИСТОРИЯ ПЕРЕПИСКИ (хранится в памяти)
296
  # ============================================================
297
  chat_history: Dict[int, list] = {}
298
 
299
  def add_to_history(chat_id: int, role: str, text: str):
300
  """
301
  Добавляет сообщение в историю чата.
302
+ Хранит последние 20 сообщений для контекста.
303
  """
304
  if chat_id not in chat_history:
305
  chat_history[chat_id] = []
 
307
  "role": role,
308
  "parts": [{"text": text}],
309
  })
310
+ # Храним последние 20 сообщений
311
+ if len(chat_history[chat_id]) > 20:
312
  chat_history[chat_id].pop(0)
313
 
314
  # ============================================================
 
316
  # ============================================================
317
 
318
  async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
319
+ """Приветственное сообщение."""
320
+ if not is_chat_allowed(update):
321
+ return
322
+
323
  user = update.effective_user
324
  name = user.first_name if user and user.first_name else "друг"
325
  await update.message.reply_text(
326
+ f"👋 Привет, {name}!\n\n"
327
+ f"Я **INAI #RID3** ИИ-ассистент от RID3 🤖\n\n"
328
+ f"🖥️ Помогу с программированием и техническими вопросами\n"
329
+ f"🎨 Могу нарисовать изображение по описанию\n"
330
+ f"💬 Отвечу на любые вопросы подробно и понятно\n\n"
331
+ f"Просто напиши что тебя интересует!",
332
  parse_mode=ParseMode.MARKDOWN,
333
  )
334
 
 
338
 
339
  async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
340
  """
341
+ Главный обработчик входящих сообщений.
342
+ Работает ТОЛЬКО в разрешённых чатах.
343
+ В группах отвечает только при упоминании ключевых слов или ответе на сообщение бота.
344
  """
345
  if not update.message or not update.message.text:
346
  return
347
 
348
+ # ГЛАВНАЯ ПРОВЕРКА: разрешён ли этот чат?
349
+ if not is_chat_allowed(update):
350
+ return
351
+
352
  chat_id = update.effective_chat.id
353
  user_text = update.message.text
354
  chat_type = update.message.chat.type
355
 
356
+ # Определяем нужно ли отвечать
357
+ is_private = chat_type == "private"
358
+ is_group = chat_type in ["group", "supergroup"]
359
  is_reply_to_bot = (
360
  update.message.reply_to_message is not None
361
  and update.message.reply_to_message.from_user is not None
362
  and update.message.reply_to_message.from_user.is_bot
363
  )
364
+ has_trigger_word = any(kw in user_text.lower() for kw in AI_TRIGGER_KEYWORDS)
 
 
365
 
366
+ # В группе только при ответе боту или ключевом слове
367
+ if is_group and not is_reply_to_bot and not has_trigger_word:
 
368
  return
369
 
370
+ logger.info(
371
+ f"Сообщение от {update.effective_user.id} "
372
+ f"в чате {chat_id} (@{update.effective_chat.username}): "
373
+ f"{user_text[:60]}..."
374
+ )
375
 
376
+ # Показываем что бот печатает
377
  await context.bot.send_chat_action(chat_id=chat_id, action=ChatAction.TYPING)
378
 
379
+ # Отправляем статус
380
  status_message = await update.message.reply_text("💠 Думаю...")
381
 
382
  try:
383
+ # Берём историю переписки для контекста
384
  history = chat_history.get(chat_id, [])
385
 
386
  # Добавляем сообщение пользователя в историю
 
392
  # Разбираем ответ на части
393
  clean_text, buttons, draw_prompt = parse_ai_response(ai_response)
394
 
395
+ # Сохраняем ответ бота в историю
396
  if clean_text:
397
  add_to_history(chat_id, "model", clean_text)
398
 
399
+ # Нужно ли рисовать изображение?
400
+ needs_image = draw_prompt is not None or any(kw in user_text.lower() for kw in IMAGE_KEYWORDS)
 
 
401
 
402
  if needs_image:
403
+ await status_message.edit_text("🎨 Рисую, подожди немного...")
404
  await context.bot.send_chat_action(chat_id=chat_id, action=ChatAction.UPLOAD_PHOTO)
405
 
 
406
  image_prompt = draw_prompt if draw_prompt else user_text
407
  image_bytes = await generate_image(image_prompt)
408
 
409
  if image_bytes:
 
410
  await status_message.delete()
 
411
  await update.message.reply_photo(
412
  photo=io.BytesIO(image_bytes),
413
+ caption=clean_text[:1000] if clean_text else None,
414
  reply_markup=build_inline_keyboard(buttons),
415
+ parse_mode=ParseMode.MARKDOWN if clean_text else None,
416
  )
417
  return
418
  else:
419
+ # Не смогли сгенерировать — отправляем только текст
420
+ fallback = "❌ Не удалось нарисовать изображение, попробуй другой запрос.\n\n"
421
+ if clean_text:
422
+ fallback += clean_text[:3500]
423
  await status_message.edit_text(
424
+ fallback,
 
425
  reply_markup=build_inline_keyboard(buttons),
426
  )
427
  return
428
 
429
  # Отправляе�� текстовый ответ
430
+ # Telegram ограничивает сообщение до 4096 символов
431
+ # Если ответ длиннее — разбиваем на части
432
+ max_length = 4090
433
+
434
+ if len(clean_text) <= max_length:
435
+ # Отправляем одним сообщением
 
 
 
 
436
  try:
437
  await status_message.edit_text(
438
+ clean_text,
439
  reply_markup=build_inline_keyboard(buttons),
440
+ parse_mode=ParseMode.MARKDOWN,
441
  )
442
+ except Exception as markdown_error:
443
+ logger.warning(f"Ошибка Markdown: {markdown_error}. Отправляю как текст.")
444
+ try:
445
+ await status_message.edit_text(
446
+ clean_text,
447
+ reply_markup=build_inline_keyboard(buttons),
448
+ )
449
+ except Exception as plain_error:
450
+ logger.error(f"Ошибка отправки: {plain_error}")
451
+ await status_message.edit_text("❌ Ошибка при отправке ответа.")
452
+ else:
453
+ # Разбиваем длинный ответ на части
454
+ parts = []
455
+ remaining = clean_text
456
+ while remaining:
457
+ if len(remaining) <= max_length:
458
+ parts.append(remaining)
459
+ break
460
+ # Ищем удобное место для разрыва (перенос строки)
461
+ split_pos = remaining[:max_length].rfind("\n")
462
+ if split_pos == -1 or split_pos < max_length // 2:
463
+ split_pos = max_length
464
+ parts.append(remaining[:split_pos])
465
+ remaining = remaining[split_pos:].lstrip("\n")
466
+
467
+ logger.info(f"Длинный ответ разбит на {len(parts)} частей")
468
+
469
+ # Редактируем статус первой частью
470
+ try:
471
+ await status_message.edit_text(
472
+ parts[0],
473
+ parse_mode=ParseMode.MARKDOWN,
474
+ )
475
+ except Exception:
476
+ await status_message.edit_text(parts[0])
477
+
478
+ # Остальные части отправляем отдельными сообщениями
479
+ for i, part in enumerate(parts[1:], start=1):
480
+ is_last = (i == len(parts) - 1)
481
+ try:
482
+ await update.message.reply_text(
483
+ part,
484
+ parse_mode=ParseMode.MARKDOWN,
485
+ reply_markup=build_inline_keyboard(buttons) if is_last else None,
486
+ )
487
+ except Exception:
488
+ await update.message.reply_text(
489
+ part,
490
+ reply_markup=build_inline_keyboard(buttons) if is_last else None,
491
+ )
492
 
493
  except Exception as global_error:
494
+ logger.error(f"Глобальная ошибка handle_message: {global_error}", exc_info=True)
495
  try:
496
+ await status_message.edit_text(
497
+ "❌ Произошла непредвиденная ошибка. Попробуй снова!"
498
+ )
499
  except Exception:
500
  pass
501
 
502
  # ============================================================
503
+ # ОБРАБОТЧИК INLINE КНОПОК
504
  # ============================================================
505
 
506
  async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
507
+ """Обрабатывает нажатия на inline кнопки."""
508
  query = update.callback_query
509
+ await query.answer()
510
+
511
+ if not is_chat_allowed(update):
512
+ return
513
 
514
  if query.data.startswith("ai_action:"):
515
  action = query.data.split(":", 1)[-1]
516
+ chat_id = update.effective_chat.id
517
+ logger.info(f"Кнопка нажата: {action} в чате {chat_id}")
518
+
519
+ # Отвечаем на нажатие кнопки через Gemini
520
+ await context.bot.send_chat_action(chat_id=chat_id, action=ChatAction.TYPING)
521
+ status = await query.message.reply_text("💠 Думаю...")
522
+
523
+ history = chat_history.get(chat_id, [])
524
+ add_to_history(chat_id, "user", f"Пользователь выбрал: {action}")
525
+ ai_response = await call_gemini_api(
526
+ f"Пользователь нажал кнопку с действием: {action}. Дай развёрнутый ответ по этой теме.",
527
+ history=history,
528
  )
529
+ clean_text, new_buttons, draw_prompt = parse_ai_response(ai_response)
530
+
531
+ if clean_text:
532
+ add_to_history(chat_id, "model", clean_text)
533
+
534
+ try:
535
+ await status.edit_text(
536
+ clean_text[:4090],
537
+ reply_markup=build_inline_keyboard(new_buttons),
538
+ parse_mode=ParseMode.MARKDOWN,
539
+ )
540
+ except Exception:
541
+ await status.edit_text(
542
+ clean_text[:4090],
543
+ reply_markup=build_inline_keyboard(new_buttons),
544
+ )
545
 
546
  # ============================================================
547
  # ОБРАБОТЧИК ОШИБОК
548
  # ============================================================
549
 
550
  async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE):
551
+ """Глобальный обработчик ошибок."""
552
+ logger.error(f"Telegram ошибка: {context.error}", exc_info=context.error)
553
 
554
  # ============================================================
555
+ # ВЕБ-СЕРВЕР ДЛЯ ПРИЁМА WEBHOOK
556
  # ============================================================
557
 
 
558
  telegram_app: Optional[Application] = None
559
 
560
  async def health_check(request: web.Request) -> web.Response:
561
+ """Проверка работоспособности (GET /)."""
562
  return web.Response(
563
  text="✅ INAI Bot работает! Cloudflare Proxy активен.",
564
  content_type="text/plain",
 
566
 
567
  async def webhook_handler(request: web.Request) -> web.Response:
568
  """
569
+ Принимает POST от Telegram и передаёт боту.
570
+ Telegram шлёт сюда все обновления через webhook.
571
  """
572
  global telegram_app
573
  try:
 
574
  raw_data = await request.json()
 
 
 
575
  update_obj = Update.de_json(raw_data, telegram_app.bot)
576
  await telegram_app.process_update(update_obj)
 
577
  return web.Response(text="OK", status=200)
578
  except Exception as e:
579
+ logger.error(f"Ошибка webhook_handler: {e}", exc_info=True)
580
  return web.Response(text="Internal Server Error", status=500)
581
 
582
  async def build_web_app() -> web.Application:
583
+ """Создаёт aiohttp приложение с маршрутами."""
584
  app = web.Application()
 
585
  app.router.add_get("/", health_check)
 
586
  app.router.add_post(f"/webhook/{BOT_TOKEN}", webhook_handler)
587
  return app
588
 
589
  # ============================================================
590
+ # ГЛАВНАЯ ФУНКЦИЯ — ЗАПУСК
591
  # ============================================================
592
 
593
  async def main_async():
594
  """
595
+ Запускает бота:
596
+ 1. Создаёт Application с Cloudflare прокси
597
  2. Регистрирует обработчики
598
  3. Устанавливает webhook
599
+ 4. Запускает веб-сервер
600
  """
601
  global telegram_app
602
 
603
  print("=" * 60)
604
  print("🚀 INAI Bot запускается...")
605
  print(f"📡 Cloudflare Proxy: {CF_WORKER_URL}")
606
+ print(f"🌐 Webhook: {WEBHOOK_URL}")
607
+ print(f"🔒 Разрешённые чаты: {ALLOWED_CHAT_USERNAMES}")
608
  print(f"🔌 Порт: {PORT}")
609
  print("=" * 60)
610
 
611
+ # base_url Cloudflare Worker вместо api.telegram.org
 
 
612
  cf_base_url = f"{CF_WORKER_URL.rstrip('/')}/bot"
613
  cf_base_file_url = f"{CF_WORKER_URL.rstrip('/')}/file/bot"
614
 
615
+ logger.info(f"base_url: {cf_base_url}")
 
616
 
617
+ # Создаём Application
618
  telegram_app = (
619
  Application.builder()
620
  .token(BOT_TOKEN)
621
+ .base_url(cf_base_url)
622
+ .base_file_url(cf_base_file_url)
623
  .connect_timeout(30.0)
624
  .read_timeout(30.0)
625
  .write_timeout(30.0)
 
627
  .build()
628
  )
629
 
630
+ # Регистрируем обработчики
631
  telegram_app.add_handler(CommandHandler("start", start_command))
632
  telegram_app.add_handler(CallbackQueryHandler(handle_callback))
633
  telegram_app.add_handler(
 
635
  )
636
  telegram_app.add_error_handler(error_handler)
637
 
638
+ # Инициализируем бота
639
+ logger.info("Инициализирую бота через Cloudflare...")
640
  await telegram_app.initialize()
641
+ logger.info("✅ Бот инициализирован!")
642
 
643
+ # Устанавливаем webhook
644
  webhook_full_url = f"{WEBHOOK_URL.rstrip('/')}/webhook/{BOT_TOKEN}"
645
  logger.info(f"Устанавливаю webhook: {webhook_full_url}")
646
  await telegram_app.bot.set_webhook(
 
652
 
653
  # Запускаем Telegram Application
654
  await telegram_app.start()
 
655
 
656
+ # Запускаем веб-сервер
657
  web_app = await build_web_app()
658
  runner = web.AppRunner(web_app)
659
  await runner.setup()
 
661
  await site.start()
662
 
663
  print("=" * 60)
664
+ print(f"✅ Веб-сервер на порту {PORT}")
665
  print(f"✅ Webhook: {webhook_full_url}")
666
+ print("✅ Бот работает! Жду сообщений...")
 
667
  print("=" * 60)
668
 
669
+ # Держим бота запущенным
670
  try:
671
  await asyncio.Event().wait()
672
  finally:
 
673
  logger.info("Останавливаю бота...")
674
  try:
675
  await telegram_app.bot.delete_webhook()
 
681
  logger.info("Бот остановлен.")
682
 
683
  def main():
 
684
  asyncio.run(main_async())
685
 
686
  if __name__ == "__main__":