Rid3 commited on
Commit
bcea0ff
·
verified ·
1 Parent(s): f8c6cc4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +160 -73
app.py CHANGED
@@ -37,9 +37,16 @@ logger = logging.getLogger(__name__)
37
  # ============================================================
38
  # КОНФИГУРАЦИЯ БОТА
39
  # ============================================================
40
- # РЕКОМЕНДАЦИЯ: В будущем лучше вынести токены в Secrets (переменные окружения) Hugging Face
41
  BOT_TOKEN = os.environ.get("BOT_TOKEN", "8605889873:AAE2gV2t0psXKlj-8h9ksyyoCrWa8Q-TdkA")
42
 
 
 
 
 
 
 
 
 
43
  # КЛЮЧИ GEMINI
44
  GEMINI_API_KEYS = [
45
  "AIzaSyDCL7CImQjZ3DlInbjmWDWw7UeQ5bQDUbU",
@@ -65,7 +72,7 @@ AI_TRIGGER_KEYWORDS = ["inai", "инэй", "инаи", "ии", "ai", "бот", "
65
  IMAGE_KEYWORDS = ["нарисуй", "рисуй", "draw", "картинка", "фото"]
66
 
67
  # ============================================================
68
- # РАБОТА С GEMINI И ИЗОБРАЖЕНИЯМИ (ЧЕРЕЗ AIOHTTP)
69
  # ============================================================
70
 
71
  async def get_next_key():
@@ -95,24 +102,24 @@ async def call_gemini_api(prompt: str, system_prompt: str = None, history: list
95
  key, key_idx = await get_next_key()
96
  attempts += 1
97
  url = GEMINI_API_URL.format(model=GEMINI_MODEL, key=key)
98
-
99
  try:
100
- async with session.post(url, json=request_body, timeout=30) as response:
101
  if response.status == 200:
102
  data = await response.json()
103
  return data["candidates"][0]["content"]["parts"][0]["text"]
104
  else:
105
- logger.warning(f"Ключ {key_idx} вернул статус {response.status}")
 
106
  except Exception as e:
107
- logger.error(f"Ошибка API Gemini: {e}")
108
-
109
  return "❌ Ошибка сервиса Gemini. Попробуйте позже."
110
 
111
  async def generate_image(prompt: str) -> Optional[bytes]:
112
- url = f"https://image.pollinations.ai/prompt/{urllib.parse.quote(prompt)}?nologo=true&seed={random.randint(1,999)}"
113
  try:
114
  async with aiohttp.ClientSession() as session:
115
- async with session.get(url, timeout=40) as res:
116
  if res.status == 200:
117
  return await res.read()
118
  except Exception as e:
@@ -133,7 +140,8 @@ def parse_ai_response(text: str):
133
  try:
134
  buttons = json.loads(btn_match.group(1))
135
  clean_text = clean_text.replace(btn_match.group(0), "").strip()
136
- except: pass
 
137
 
138
  draw_match = re.search(r'\[DRAW\]:\s*([^\n\[]+)', clean_text, re.IGNORECASE)
139
  if draw_match:
@@ -143,10 +151,16 @@ def parse_ai_response(text: str):
143
  return clean_text, buttons, draw_prompt
144
 
145
  def build_inline_keyboard(buttons: list) -> Optional[InlineKeyboardMarkup]:
146
- if not buttons: return None
 
147
  keyboard = []
148
  for btn in buttons:
149
- keyboard.append([InlineKeyboardButton(btn.get("text", "OK"), callback_data=f"ai_action:{btn.get('action', 'none')[:40]}")])
 
 
 
 
 
150
  return InlineKeyboardMarkup(keyboard)
151
 
152
  # ============================================================
@@ -155,125 +169,198 @@ def build_inline_keyboard(buttons: list) -> Optional[InlineKeyboardMarkup]:
155
  chat_history: Dict[int, list] = {}
156
 
157
  def add_to_history(chat_id: int, role: str, text: str):
158
- if chat_id not in chat_history: chat_history[chat_id] = []
 
159
  chat_history[chat_id].append({"role": role, "parts": [{"text": text}]})
160
  # Храним последние 10 сообщений
161
- if len(chat_history[chat_id]) > 10:
162
  chat_history[chat_id].pop(0)
163
 
164
  # ============================================================
165
- # ОБРАБОТЧИКИ
166
  # ============================================================
167
 
168
  async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
169
- await update.message.reply_text("👋 Я **INAI #RID3**! Чем помочь?", parse_mode=ParseMode.MARKDOWN)
 
 
 
170
 
171
  async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
172
- if not update.message or not update.message.text: return
173
-
 
174
  chat_id = update.effective_chat.id
175
  text = update.message.text
176
-
177
  is_group = update.message.chat.type in ["group", "supergroup"]
178
- is_reply = update.message.reply_to_message and update.message.reply_to_message.from_user.is_bot
179
-
180
- should_answer = not is_group or is_reply or any(k in text.lower() for k in AI_TRIGGER_KEYWORDS)
181
- if not should_answer: return
 
 
 
 
 
 
 
 
 
182
 
183
  await context.bot.send_chat_action(chat_id=chat_id, action=ChatAction.TYPING)
184
  status = await update.message.reply_text("💠 Думаю...")
185
-
186
- # Получаем историю без последнего сообщения пользователя
187
  history = chat_history.get(chat_id, [])
188
-
189
  add_to_history(chat_id, "user", text)
190
  ai_resp = await call_gemini_api(text, history=history)
191
-
192
  clean_text, buttons, draw_prompt = parse_ai_response(ai_resp)
193
  add_to_history(chat_id, "model", clean_text)
194
-
195
  if draw_prompt or any(k in text.lower() for k in IMAGE_KEYWORDS):
196
  await status.edit_text("🎨 Рисую...")
197
- img = await generate_image(draw_prompt if draw_prompt else text)
 
198
  if img:
199
  await status.delete()
200
  await update.message.reply_photo(
201
- photo=io.BytesIO(img),
202
- caption=clean_text[:1000],
203
  reply_markup=build_inline_keyboard(buttons)
204
  )
205
  return
 
 
 
206
 
207
- # Защита от падений из-за неправильного Markdown, сгенерированного ИИ
208
  try:
209
  await status.edit_text(
210
- clean_text[:4090],
211
- reply_markup=build_inline_keyboard(buttons),
212
  parse_mode=ParseMode.MARKDOWN
213
  )
214
  except Exception as e:
215
- logger.warning(f"Ошибка Markdown разметки. Отправляю как текст: {e}")
216
- await status.edit_text(
217
- clean_text[:4090],
218
- reply_markup=build_inline_keyboard(buttons)
219
- )
 
 
 
220
 
221
  async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
222
  query = update.callback_query
223
  await query.answer()
224
  if query.data.startswith("ai_action:"):
225
- await query.message.reply_text(f"Вы выбрали: {query.data.split(':')[-1]}. Опишите подробнее!")
 
 
 
 
226
 
227
  async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE):
228
- logger.error(f"Error: {context.error}")
229
 
230
  # ============================================================
231
- # ВЕБ-СЕРВЕР ДЛЯ HUGGING FACE (ПРЕДОТВРАЩАЕТ TIMEOUT ОШИБКИ)
232
  # ============================================================
233
- async def health_check(request):
234
- return web.Response(text="Бот INAI успешно работает!")
235
 
236
- async def start_web_server():
237
- app = web.Application()
238
- app.router.add_get('/', health_check)
239
- runner = web.AppRunner(app)
240
- await runner.setup()
241
- site = web.TCPSite(runner, '0.0.0.0', 7860)
242
- await site.start()
243
- logger.info("✅ Локальный веб-сервер запущен на порту 7860 (Hugging Face счастлив)")
244
 
245
- # Хук, который запускает веб-сервер одновременно с ботом
246
- async def post_init(application: Application):
247
- asyncio.create_task(start_web_server())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
 
249
  # ============================================================
250
- # MAIN - ЗАПУСК
251
  # ============================================================
252
 
253
- def main():
254
- print("🚀 Starting INAI Bot on Hugging Face...")
255
-
256
- # Настройка с МАКСИМАЛЬНЫМИ таймаутами
257
- application = (
 
 
258
  Application.builder()
259
  .token(BOT_TOKEN)
260
- .post_init(post_init) # Запуск веб-сервера для HF
261
- .connect_timeout(60.0)
262
- .read_timeout(60.0)
263
- .write_timeout(60.0)
264
- .get_updates_connect_timeout(60.0)
265
- .get_updates_read_timeout(60.0)
266
- .pool_timeout(60.0)
267
  .build()
268
  )
269
 
270
- application.add_handler(CommandHandler("start", start_command))
271
- application.add_handler(CallbackQueryHandler(handle_callback))
272
- application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
273
- application.add_error_handler(error_handler)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
 
 
 
275
  print("✅ Bot is running. Waiting for messages...")
276
- application.run_polling(drop_pending_updates=True)
 
 
 
 
 
 
 
 
 
 
 
 
277
 
278
  if __name__ == "__main__":
279
  main()
 
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
+
47
+ # Порт для веб-сервера (Hugging Face использует 7860)
48
+ PORT = int(os.environ.get("PORT", 7860))
49
+
50
  # КЛЮЧИ GEMINI
51
  GEMINI_API_KEYS = [
52
  "AIzaSyDCL7CImQjZ3DlInbjmWDWw7UeQ5bQDUbU",
 
72
  IMAGE_KEYWORDS = ["нарисуй", "рисуй", "draw", "картинка", "фото"]
73
 
74
  # ============================================================
75
+ # РАБОТА С GEMINI (ЧЕРЕЗ AIOHTTP)
76
  # ============================================================
77
 
78
  async def get_next_key():
 
102
  key, key_idx = await get_next_key()
103
  attempts += 1
104
  url = GEMINI_API_URL.format(model=GEMINI_MODEL, key=key)
 
105
  try:
106
+ async with session.post(url, json=request_body, timeout=aiohttp.ClientTimeout(total=30)) as response:
107
  if response.status == 200:
108
  data = await response.json()
109
  return data["candidates"][0]["content"]["parts"][0]["text"]
110
  else:
111
+ error_text = await response.text()
112
+ logger.warning(f"Ключ {key_idx} вернул статус {response.status}: {error_text}")
113
  except Exception as e:
114
+ logger.error(f"Ошибка API Gemini с ключом {key_idx}: {e}")
115
+
116
  return "❌ Ошибка сервиса Gemini. Попробуйте позже."
117
 
118
  async def generate_image(prompt: str) -> Optional[bytes]:
119
+ url = f"https://image.pollinations.ai/prompt/{urllib.parse.quote(prompt)}?nologo=true&seed={random.randint(1, 999)}"
120
  try:
121
  async with aiohttp.ClientSession() as session:
122
+ async with session.get(url, timeout=aiohttp.ClientTimeout(total=60)) as res:
123
  if res.status == 200:
124
  return await res.read()
125
  except Exception as e:
 
140
  try:
141
  buttons = json.loads(btn_match.group(1))
142
  clean_text = clean_text.replace(btn_match.group(0), "").strip()
143
+ except Exception as e:
144
+ logger.warning(f"Ошибка парсинга кнопок: {e}")
145
 
146
  draw_match = re.search(r'\[DRAW\]:\s*([^\n\[]+)', clean_text, re.IGNORECASE)
147
  if draw_match:
 
151
  return clean_text, buttons, draw_prompt
152
 
153
  def build_inline_keyboard(buttons: list) -> Optional[InlineKeyboardMarkup]:
154
+ if not buttons:
155
+ return None
156
  keyboard = []
157
  for btn in buttons:
158
+ keyboard.append([
159
+ InlineKeyboardButton(
160
+ btn.get("text", "OK"),
161
+ callback_data=f"ai_action:{btn.get('action', 'none')[:40]}"
162
+ )
163
+ ])
164
  return InlineKeyboardMarkup(keyboard)
165
 
166
  # ============================================================
 
169
  chat_history: Dict[int, list] = {}
170
 
171
  def add_to_history(chat_id: int, role: str, text: str):
172
+ if chat_id not in chat_history:
173
+ chat_history[chat_id] = []
174
  chat_history[chat_id].append({"role": role, "parts": [{"text": text}]})
175
  # Храним последние 10 сообщений
176
+ if len(chat_history[chat_id]) > 10:
177
  chat_history[chat_id].pop(0)
178
 
179
  # ============================================================
180
+ # ОБРАБОТЧИКИ TELEGRAM
181
  # ============================================================
182
 
183
  async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
184
+ await update.message.reply_text(
185
+ "👋 Я **INAI #RID3**! Чем могу помочь?",
186
+ parse_mode=ParseMode.MARKDOWN
187
+ )
188
 
189
  async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
190
+ if not update.message or not update.message.text:
191
+ return
192
+
193
  chat_id = update.effective_chat.id
194
  text = update.message.text
195
+
196
  is_group = update.message.chat.type in ["group", "supergroup"]
197
+ is_reply = (
198
+ update.message.reply_to_message is not None
199
+ and update.message.reply_to_message.from_user is not None
200
+ and update.message.reply_to_message.from_user.is_bot
201
+ )
202
+
203
+ should_answer = (
204
+ not is_group
205
+ or is_reply
206
+ or any(k in text.lower() for k in AI_TRIGGER_KEYWORDS)
207
+ )
208
+ if not should_answer:
209
+ return
210
 
211
  await context.bot.send_chat_action(chat_id=chat_id, action=ChatAction.TYPING)
212
  status = await update.message.reply_text("💠 Думаю...")
213
+
 
214
  history = chat_history.get(chat_id, [])
215
+
216
  add_to_history(chat_id, "user", text)
217
  ai_resp = await call_gemini_api(text, history=history)
218
+
219
  clean_text, buttons, draw_prompt = parse_ai_response(ai_resp)
220
  add_to_history(chat_id, "model", clean_text)
221
+
222
  if draw_prompt or any(k in text.lower() for k in IMAGE_KEYWORDS):
223
  await status.edit_text("🎨 Рисую...")
224
+ img_prompt = draw_prompt if draw_prompt else text
225
+ img = await generate_image(img_prompt)
226
  if img:
227
  await status.delete()
228
  await update.message.reply_photo(
229
+ photo=io.BytesIO(img),
230
+ caption=clean_text[:1000] if clean_text else None,
231
  reply_markup=build_inline_keyboard(buttons)
232
  )
233
  return
234
+ else:
235
+ await status.edit_text("❌ Не удалось сгенерировать изображение. Попробуйте ещё раз.")
236
+ return
237
 
 
238
  try:
239
  await status.edit_text(
240
+ clean_text[:4090],
241
+ reply_markup=build_inline_keyboard(buttons),
242
  parse_mode=ParseMode.MARKDOWN
243
  )
244
  except Exception as e:
245
+ logger.warning(f"Ошибка Markdown разметки, отправляю как обычный текст: {e}")
246
+ try:
247
+ await status.edit_text(
248
+ clean_text[:4090],
249
+ reply_markup=build_inline_keyboard(buttons)
250
+ )
251
+ except Exception as e2:
252
+ logger.error(f"Критическая ошибка при отправке сообщения: {e2}")
253
 
254
  async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
255
  query = update.callback_query
256
  await query.answer()
257
  if query.data.startswith("ai_action:"):
258
+ action = query.data.split(":", 1)[-1]
259
+ await query.message.reply_text(
260
+ f"Вы выбрали: *{action}*. Опишите подробнее! 💬",
261
+ parse_mode=ParseMode.MARKDOWN
262
+ )
263
 
264
  async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE):
265
+ logger.error(f"Произошла ошибка: {context.error}", exc_info=context.error)
266
 
267
  # ============================================================
268
+ # ВЕБ-СЕРВЕР + WEBHOOK HANDLER (AIOHTTP)
269
  # ============================================================
 
 
270
 
271
+ # Глобальная переменная для доступа к приложению из веб-сервера
272
+ telegram_app: Optional[Application] = None
 
 
 
 
 
 
273
 
274
+ async def health_check(request: web.Request) -> web.Response:
275
+ return web.Response(text="✅ INAI Bot работает!")
276
+
277
+ async def webhook_handler(request: web.Request) -> web.Response:
278
+ """
279
+ Эта функция принимает все входящие POST-запросы от Telegram
280
+ и передаёт их в обработчик бота.
281
+ """
282
+ global telegram_app
283
+ try:
284
+ data = await request.json()
285
+ update = Update.de_json(data, telegram_app.bot)
286
+ await telegram_app.process_update(update)
287
+ return web.Response(text="OK")
288
+ except Exception as e:
289
+ logger.error(f"Ошибка в webhook_handler: {e}", exc_info=True)
290
+ return web.Response(status=500, text="Internal Server Error")
291
+
292
+ async def build_web_app() -> web.Application:
293
+ """Создаём aiohttp приложение с маршрутами."""
294
+ app = web.Application()
295
+ app.router.add_get("/", health_check)
296
+ app.router.add_post(f"/webhook/{BOT_TOKEN}", webhook_handler)
297
+ return app
298
 
299
  # ============================================================
300
+ # MAIN ЗАПУСК ЧЕРЕЗ WEBHOOK (БЕЗ POLLING)
301
  # ============================================================
302
 
303
+ async def main_async():
304
+ global telegram_app
305
+
306
+ print("🚀 Starting INAI Bot on Hugging Face (Webhook mode)...")
307
+
308
+ # Строим Telegram-приложение
309
+ telegram_app = (
310
  Application.builder()
311
  .token(BOT_TOKEN)
312
+ .connect_timeout(30.0)
313
+ .read_timeout(30.0)
314
+ .write_timeout(30.0)
315
+ .pool_timeout(30.0)
 
 
 
316
  .build()
317
  )
318
 
319
+ # Регистрируем обработчики
320
+ telegram_app.add_handler(CommandHandler("start", start_command))
321
+ telegram_app.add_handler(CallbackQueryHandler(handle_callback))
322
+ telegram_app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
323
+ telegram_app.add_error_handler(error_handler)
324
+
325
+ # Инициализируем бота
326
+ await telegram_app.initialize()
327
+
328
+ # Устанавливаем webhook
329
+ webhook_full_url = f"{WEBHOOK_URL.rstrip('/')}/webhook/{BOT_TOKEN}"
330
+ logger.info(f"Устанавливаю webhook: {webhook_full_url}")
331
+ await telegram_app.bot.set_webhook(
332
+ url=webhook_full_url,
333
+ drop_pending_updates=True,
334
+ allowed_updates=Update.ALL_TYPES,
335
+ )
336
+ logger.info("✅ Webhook успешно установлен!")
337
+
338
+ # Запускаем Telegram-приложение
339
+ await telegram_app.start()
340
+
341
+ # Запускаем aiohttp веб-сервер
342
+ web_app = await build_web_app()
343
+ runner = web.AppRunner(web_app)
344
+ await runner.setup()
345
+ site = web.TCPSite(runner, "0.0.0.0", PORT)
346
+ await site.start()
347
 
348
+ print(f"✅ Веб-сервер запущен на порту {PORT}")
349
+ print(f"✅ Webhook URL: {webhook_full_url}")
350
  print("✅ Bot is running. Waiting for messages...")
351
+
352
+ # Держим бота запущенным вечно
353
+ try:
354
+ await asyncio.Event().wait()
355
+ finally:
356
+ logger.info("Останавливаю бота...")
357
+ await telegram_app.bot.delete_webhook()
358
+ await telegram_app.stop()
359
+ await telegram_app.shutdown()
360
+ await runner.cleanup()
361
+
362
+ def main():
363
+ asyncio.run(main_async())
364
 
365
  if __name__ == "__main__":
366
  main()