Initial commit: MIB bot
This commit is contained in:
+14
@@ -0,0 +1,14 @@
|
||||
.env
|
||||
env-org
|
||||
venv/
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
bot_database.db
|
||||
tmp/
|
||||
gitea-mcp
|
||||
.idea/
|
||||
*.zip
|
||||
*.tar.gz
|
||||
downloads/
|
||||
img/
|
||||
@@ -0,0 +1,150 @@
|
||||
# 🤖 Telegram Multi-Tool Bot
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
Многофункциональный Telegram-бот на базе **aiogram 3**, который умеет:
|
||||
|
||||
- 🌤️ Показывать погоду
|
||||
- 📥 Скачивать файлы с Яндекс.Диска
|
||||
- 🎤 Распознавать голосовые сообщения (Yandex SpeechKit)
|
||||
- 🔄 Восстанавливать раскладку клавиатуры
|
||||
- 🚫 Защищать чат от спама
|
||||
- 🗂️ Кешировать файлы через SQLite
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Возможности
|
||||
|
||||
### 📌 Команды
|
||||
|
||||
| Команда | Описание |
|
||||
|--------------|--------|
|
||||
| `/start` | Приветствие |
|
||||
| `/help` | Информация о боте |
|
||||
| `/get_id` | Узнать свой ID |
|
||||
| `/weather` | Погода (`/weather Москва`) |
|
||||
| `/ydf` | Скачать файл с Яндекс.Диска |
|
||||
| `/res` | Восстановить раскладку |
|
||||
| `/info_ban` | Информация о бане |
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Установка
|
||||
|
||||
```bash
|
||||
git clone <your-repo>
|
||||
cd bot
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Настройка
|
||||
|
||||
Создай `.env`:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Заполни:
|
||||
|
||||
```env
|
||||
BOT_TOKEN=your_bot_token
|
||||
|
||||
YSK_API_KEY=your_yandex_speechkit_key
|
||||
API_WEATHER=your_openweather_key
|
||||
|
||||
ACCESS_KEY=your_s3_access_key
|
||||
SECRET_KEY=your_s3_secret_key
|
||||
BUCKET_NAME=your_bucket
|
||||
|
||||
PROXY_URL=socks5://login:password@host:port
|
||||
|
||||
DB_PATH=bot_database.db
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Прокси
|
||||
|
||||
Поддерживаются:
|
||||
|
||||
- HTTP
|
||||
- SOCKS5 (рекомендуется)
|
||||
|
||||
Пример:
|
||||
|
||||
```env
|
||||
PROXY_URL=socks5://user:pass@127.0.0.1:1080
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ▶️ Запуск
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ Структура проекта
|
||||
|
||||
```
|
||||
.
|
||||
├── main.py
|
||||
├── config.py
|
||||
├── database.py
|
||||
├── functions.py
|
||||
├── antispam.py
|
||||
├── download_files.py
|
||||
├── yandex_cloud_sk.py
|
||||
├── switching_en_ru.py
|
||||
├── requirements.txt
|
||||
├── .env.example
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎤 SpeechKit
|
||||
|
||||
Используется библиотека:
|
||||
|
||||
```
|
||||
yandex-speechkit
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ Безопасность
|
||||
|
||||
- ❌ Не коммить `.env`
|
||||
- 🔑 Перевыпускай ключи при утечке
|
||||
- 🔒 Храни секреты только в окружении
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Особенности
|
||||
|
||||
- Асинхронный код
|
||||
- Поддержка прокси
|
||||
- Безопасная обработка голосовых
|
||||
- Кеширование файлов
|
||||
- SQLite база данных
|
||||
|
||||
---
|
||||
|
||||
## 📈 Планы
|
||||
|
||||
- Redis
|
||||
- Docker
|
||||
- Web UI
|
||||
|
||||
---
|
||||
|
||||
## 👨💻 Автор
|
||||
|
||||
Сделано с ❤️
|
||||
Executable
+125
@@ -0,0 +1,125 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
|
||||
from config import bot
|
||||
from database import get_user_data, get_user_data_ban, save_user_data, save_user_data_ban
|
||||
|
||||
logger = logging.getLogger("antispam")
|
||||
|
||||
STICKER_LIMIT = 15
|
||||
STICKER_WINDOW_SECONDS = 60
|
||||
STICKER_BAN_SECONDS = 300
|
||||
|
||||
COMMAND_WARNING_AT = 3
|
||||
COMMAND_MUTE_AT = 5
|
||||
COMMAND_WINDOW_SECONDS = 60
|
||||
COMMAND_MUTE_SECONDS = 600
|
||||
|
||||
|
||||
async def anti_DuDo(user_id: int, message, now: float) -> None:
|
||||
chat_id = message.chat.id
|
||||
username = message.from_user.username or message.from_user.full_name
|
||||
user_info = get_user_data(user_id, chat_id)
|
||||
|
||||
if not user_info:
|
||||
save_user_data(
|
||||
user_id=user_id,
|
||||
chat_id=chat_id,
|
||||
count=0,
|
||||
start_time=now,
|
||||
ban_trigger=False,
|
||||
ban_time=0,
|
||||
username=username,
|
||||
)
|
||||
user_info = get_user_data(user_id, chat_id)
|
||||
|
||||
count = user_info[2]
|
||||
start_time = user_info[3]
|
||||
ban_trigger = bool(user_info[4])
|
||||
ban_time = user_info[5]
|
||||
|
||||
if username != user_info[6]:
|
||||
save_user_data(user_id, chat_id, username=username)
|
||||
|
||||
if ban_trigger:
|
||||
if now - ban_time >= STICKER_BAN_SECONDS:
|
||||
save_user_data(
|
||||
user_id,
|
||||
chat_id,
|
||||
count=0,
|
||||
start_time=now,
|
||||
ban_trigger=False,
|
||||
ban_time=0,
|
||||
username=username,
|
||||
)
|
||||
await bot.send_message(chat_id, f"Надеюсь, {username}, твое злобное дудо остыло...")
|
||||
ban_trigger = False
|
||||
count = 0
|
||||
else:
|
||||
try:
|
||||
await message.delete()
|
||||
except TelegramBadRequest:
|
||||
pass
|
||||
return
|
||||
|
||||
if now - start_time >= STICKER_WINDOW_SECONDS:
|
||||
count = 0
|
||||
start_time = now
|
||||
|
||||
count += 1
|
||||
save_user_data(user_id, chat_id, count=count, start_time=start_time, username=username)
|
||||
|
||||
if count >= STICKER_LIMIT:
|
||||
save_user_data(user_id, chat_id, ban_trigger=True, ban_time=now, username=username)
|
||||
await bot.send_message(
|
||||
chat_id,
|
||||
(
|
||||
f"⚠️ {username}, отдохни 5 минут, лови АнтиДудо!\n\n"
|
||||
"Хочешь узнать время своего бана — жми /info_ban"
|
||||
),
|
||||
)
|
||||
try:
|
||||
await message.delete()
|
||||
except TelegramBadRequest:
|
||||
pass
|
||||
|
||||
|
||||
async def call_verification(user_id: int, message, command: str) -> None:
|
||||
now = time.time()
|
||||
chat_id = message.chat.id
|
||||
username = message.from_user.username or message.from_user.full_name
|
||||
|
||||
user_ban_info = get_user_data_ban(user_id, chat_id, command)
|
||||
if not user_ban_info:
|
||||
save_user_data_ban(user_id, chat_id, command, count=1, ban_time_inf=now)
|
||||
return
|
||||
|
||||
count = user_ban_info[3]
|
||||
last_time = user_ban_info[4]
|
||||
|
||||
if now - last_time >= COMMAND_WINDOW_SECONDS:
|
||||
count = 0
|
||||
|
||||
count += 1
|
||||
save_user_data_ban(user_id, chat_id, command, count=count, ban_time_inf=now)
|
||||
|
||||
if count == COMMAND_WARNING_AT:
|
||||
await bot.send_message(chat_id, f"⚠️ {username}, не злоупотребляй командой /{command}")
|
||||
|
||||
if count >= COMMAND_MUTE_AT:
|
||||
member = await bot.get_chat_member(chat_id, user_id)
|
||||
if member.status in ["administrator", "creator"]:
|
||||
await bot.send_message(
|
||||
chat_id,
|
||||
f"❗️ {username} нельзя замутить: статус {member.status}.",
|
||||
)
|
||||
return
|
||||
|
||||
await bot.restrict_chat_member(chat_id, user_id, until_date=int(now + COMMAND_MUTE_SECONDS))
|
||||
await bot.send_message(
|
||||
chat_id,
|
||||
f"{username} был замучен на 10 минут за злоупотребление командой /{command}.",
|
||||
)
|
||||
save_user_data_ban(user_id, chat_id, command, count=0, ban_time_inf=now)
|
||||
@@ -0,0 +1,250 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
import aiohttp
|
||||
from aiogram import Bot
|
||||
from aiogram.client.session.aiohttp import AiohttpSession
|
||||
from dotenv import load_dotenv
|
||||
|
||||
try:
|
||||
from aiohttp_socks import ProxyConnector
|
||||
except ImportError: # pragma: no cover
|
||||
ProxyConnector = None
|
||||
|
||||
load_dotenv()
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger("config")
|
||||
|
||||
BOT_TOKEN = os.getenv("BOT_TOKEN", "").strip()
|
||||
YSK_API_KEY = os.getenv("YSK_API_KEY", "").strip()
|
||||
API_WEATHER = os.getenv("API_WEATHER", "").strip()
|
||||
|
||||
ACCESS_KEY = os.getenv("ACCESS_KEY", "").strip()
|
||||
SECRET_KEY = os.getenv("SECRET_KEY", "").strip()
|
||||
BUCKET_NAME = os.getenv("BUCKET_NAME", "").strip()
|
||||
|
||||
DB_PATH = os.getenv("DB_PATH", "bot_database.db").strip() or "bot_database.db"
|
||||
|
||||
# Можно использовать старую переменную PROXY_URL или новую PROXY_URLS.
|
||||
# Примеры:
|
||||
# PROXY_URL=socks5://127.0.0.1:12553
|
||||
# PROXY_URLS=socks5://127.0.0.1:12553,socks5://127.0.0.1:12554
|
||||
_PROXY_RAW = (
|
||||
os.getenv("PROXY_URLS", "").strip()
|
||||
or os.getenv("PROXY_URL", "").strip()
|
||||
)
|
||||
|
||||
|
||||
class NetworkUnavailableError(RuntimeError):
|
||||
"""Нет рабочего подключения ни через прокси, ни напрямую."""
|
||||
|
||||
|
||||
def _parse_proxy_urls(raw_value: str) -> list[str]:
|
||||
"""Разбирает список прокси из .env.
|
||||
|
||||
Поддерживаются разделители:
|
||||
- запятая
|
||||
- точка с запятой
|
||||
- перенос строки
|
||||
|
||||
Важно: AiohttpSession принимает только один proxy URL за раз,
|
||||
поэтому список нужно разбирать и проверять по очереди.
|
||||
"""
|
||||
if not raw_value:
|
||||
return []
|
||||
|
||||
items = []
|
||||
for item in re.split(r"[,;\r\n]+", raw_value):
|
||||
proxy = item.strip().strip("'\"")
|
||||
if proxy:
|
||||
items.append(proxy)
|
||||
|
||||
# Убираем дубли, но сохраняем порядок.
|
||||
result = []
|
||||
seen = set()
|
||||
for proxy in items:
|
||||
if proxy not in seen:
|
||||
result.append(proxy)
|
||||
seen.add(proxy)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
PROXY_URLS = _parse_proxy_urls(_PROXY_RAW)
|
||||
|
||||
# Оставляем PROXY_URL для обратной совместимости со старым кодом.
|
||||
# Но новый код должен брать именно get_active_proxy_url().
|
||||
PROXY_URL = PROXY_URLS[0] if PROXY_URLS else None
|
||||
|
||||
_ACTIVE_PROXY_URL: str | None = None
|
||||
_CONNECTION_READY = False
|
||||
|
||||
if not BOT_TOKEN:
|
||||
raise RuntimeError("BOT_TOKEN is not set. Put it into .env")
|
||||
|
||||
|
||||
def _mask_proxy(proxy_url: str | None) -> str:
|
||||
"""Прячет логин/пароль в логах."""
|
||||
if not proxy_url:
|
||||
return "напрямую"
|
||||
|
||||
return re.sub(
|
||||
r"(://)([^:/@\s]+):([^/@\s]+)@",
|
||||
r"\1***:***@",
|
||||
proxy_url,
|
||||
)
|
||||
|
||||
|
||||
def _is_socks_proxy(proxy_url: str | None) -> bool:
|
||||
if not proxy_url:
|
||||
return False
|
||||
return proxy_url.lower().startswith(("socks4://", "socks5://", "socks5h://"))
|
||||
|
||||
|
||||
def get_active_proxy_url() -> str | None:
|
||||
"""Возвращает прокси, который реально прошёл проверку.
|
||||
|
||||
Если подключение выбрано напрямую, вернёт None.
|
||||
Это нужно для других модулей, которые используют aiohttp.
|
||||
"""
|
||||
return _ACTIVE_PROXY_URL
|
||||
|
||||
|
||||
def ensure_connection_ready() -> None:
|
||||
"""Защита от сетевых вызовов до setup_bot_connection()."""
|
||||
if not _CONNECTION_READY:
|
||||
raise NetworkUnavailableError(
|
||||
"Подключение к Telegram не настроено. Сначала вызовите setup_bot_connection()."
|
||||
)
|
||||
|
||||
|
||||
def create_aiohttp_session(timeout: aiohttp.ClientTimeout | None = None) -> aiohttp.ClientSession:
|
||||
"""Создаёт aiohttp-сессию с учётом активного прокси.
|
||||
|
||||
Почему так:
|
||||
- aiogram.AiohttpSession умеет SOCKS-прокси сам;
|
||||
- обычный aiohttp не должен получать socks5:// через параметр proxy=;
|
||||
- для SOCKS в aiohttp нужен aiohttp_socks.ProxyConnector.
|
||||
"""
|
||||
ensure_connection_ready()
|
||||
|
||||
proxy_url = get_active_proxy_url()
|
||||
if not proxy_url:
|
||||
return aiohttp.ClientSession(timeout=timeout)
|
||||
|
||||
if _is_socks_proxy(proxy_url):
|
||||
if ProxyConnector is None:
|
||||
raise RuntimeError(
|
||||
"Для SOCKS-прокси нужен пакет aiohttp-socks. Установите: pip install aiohttp-socks"
|
||||
)
|
||||
connector = ProxyConnector.from_url(proxy_url)
|
||||
return aiohttp.ClientSession(timeout=timeout, connector=connector)
|
||||
|
||||
# Для HTTP/HTTPS-прокси aiohttp использует параметр proxy= в конкретном запросе.
|
||||
return aiohttp.ClientSession(timeout=timeout)
|
||||
|
||||
|
||||
def get_aiohttp_request_kwargs() -> dict:
|
||||
"""Аргументы для session.get/post у обычного aiohttp.
|
||||
|
||||
SOCKS-прокси уже встроен в connector внутри create_aiohttp_session(),
|
||||
поэтому proxy= для socks5:// здесь не передаём.
|
||||
"""
|
||||
proxy_url = get_active_proxy_url()
|
||||
if proxy_url and not _is_socks_proxy(proxy_url):
|
||||
return {"proxy": proxy_url}
|
||||
return {}
|
||||
|
||||
|
||||
async def _close_session_safely(session: AiohttpSession | None) -> None:
|
||||
if session is None:
|
||||
return
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
await session.close()
|
||||
|
||||
|
||||
async def _build_checked_session(proxy_url: str | None) -> AiohttpSession:
|
||||
"""Создаёт сессию и проверяет связь с Telegram методом get_me()."""
|
||||
session = AiohttpSession(proxy=proxy_url) if proxy_url else AiohttpSession()
|
||||
test_bot = Bot(token=BOT_TOKEN, session=session)
|
||||
|
||||
try:
|
||||
await test_bot.get_me()
|
||||
except Exception:
|
||||
await _close_session_safely(session)
|
||||
raise
|
||||
|
||||
return session
|
||||
|
||||
|
||||
async def setup_bot_connection() -> bool:
|
||||
"""Подбирает рабочее подключение для bot.
|
||||
|
||||
Алгоритм:
|
||||
1. Проверить все прокси из .env по очереди.
|
||||
2. Если ни один не подошёл — попробовать прямое подключение.
|
||||
3. Если прямое подключение тоже не работает — вернуть False.
|
||||
|
||||
Функция не бросает исключение наружу из-за нерабочей сети/прокси.
|
||||
main.py сможет аккуратно завершить работу без traceback.
|
||||
"""
|
||||
global _ACTIVE_PROXY_URL, _CONNECTION_READY
|
||||
|
||||
_CONNECTION_READY = False
|
||||
|
||||
for proxy_url in PROXY_URLS:
|
||||
shown_proxy = _mask_proxy(proxy_url)
|
||||
logger.info("Пробую подключиться через прокси %s", shown_proxy)
|
||||
|
||||
try:
|
||||
checked_session = await _build_checked_session(proxy_url)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Связь через прокси %s не удалась: %s. Пробую следующий вариант подключения.",
|
||||
shown_proxy,
|
||||
exc,
|
||||
)
|
||||
continue
|
||||
|
||||
old_session = bot.session
|
||||
bot.session = checked_session
|
||||
await _close_session_safely(old_session)
|
||||
|
||||
_ACTIVE_PROXY_URL = proxy_url
|
||||
_CONNECTION_READY = True
|
||||
logger.info("Подключение через прокси %s успешно.", shown_proxy)
|
||||
return True
|
||||
|
||||
if PROXY_URLS:
|
||||
logger.warning("Ни один прокси не помог. Пробую подключиться напрямую.")
|
||||
else:
|
||||
logger.info("Прокси не указаны. Пробую подключиться напрямую.")
|
||||
|
||||
try:
|
||||
checked_session = await _build_checked_session(None)
|
||||
except Exception as exc:
|
||||
_ACTIVE_PROXY_URL = None
|
||||
_CONNECTION_READY = False
|
||||
logger.error("Прямое подключение не удалось: %s", exc)
|
||||
logger.error("Нет возможности соединиться с Telegram. Завершаю работу.")
|
||||
return False
|
||||
|
||||
old_session = bot.session
|
||||
bot.session = checked_session
|
||||
await _close_session_safely(old_session)
|
||||
|
||||
_ACTIVE_PROXY_URL = None
|
||||
_CONNECTION_READY = True
|
||||
logger.info("Прямое подключение успешно.")
|
||||
return True
|
||||
|
||||
|
||||
# Важно: не создаём AiohttpSession(proxy=PROXY_URL) на импорте.
|
||||
# Если PROXY_URL кривой, программа должна не падать при импорте,
|
||||
# а аккуратно проверить все варианты в setup_bot_connection().
|
||||
bot = Bot(token=BOT_TOKEN, session=AiohttpSession())
|
||||
Executable
+222
@@ -0,0 +1,222 @@
|
||||
import logging
|
||||
import sqlite3
|
||||
from contextlib import closing
|
||||
|
||||
from config import DB_PATH
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger("database")
|
||||
|
||||
|
||||
def create_connection() -> sqlite3.Connection:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
|
||||
def create_tables() -> None:
|
||||
with closing(create_connection()) as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute(
|
||||
'''
|
||||
CREATE TABLE IF NOT EXISTS file_ids (
|
||||
file_key TEXT PRIMARY KEY,
|
||||
file_id TEXT
|
||||
)
|
||||
'''
|
||||
)
|
||||
|
||||
cursor.execute(
|
||||
'''
|
||||
CREATE TABLE IF NOT EXISTS user_data (
|
||||
user_id INTEGER,
|
||||
chat_id INTEGER,
|
||||
count INTEGER NOT NULL DEFAULT 0,
|
||||
start_time REAL NOT NULL DEFAULT 0,
|
||||
ban_trigger INTEGER NOT NULL DEFAULT 0,
|
||||
ban_time REAL NOT NULL DEFAULT 0,
|
||||
username TEXT,
|
||||
PRIMARY KEY (user_id, chat_id)
|
||||
)
|
||||
'''
|
||||
)
|
||||
|
||||
cursor.execute(
|
||||
'''
|
||||
CREATE TABLE IF NOT EXISTS user_data_ban (
|
||||
user_id INTEGER,
|
||||
chat_id INTEGER,
|
||||
command TEXT,
|
||||
count INTEGER NOT NULL DEFAULT 0,
|
||||
ban_time_inf REAL NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (user_id, chat_id, command)
|
||||
)
|
||||
'''
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
|
||||
|
||||
def get_file_id(file_key: str) -> str | None:
|
||||
try:
|
||||
with closing(create_connection()) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT file_id FROM file_ids WHERE file_key = ?", (file_key,))
|
||||
row = cursor.fetchone()
|
||||
return row["file_id"] if row and row["file_id"] else None
|
||||
except Exception as exc:
|
||||
logger.error("Ошибка при получении file_id: %s", exc)
|
||||
return None
|
||||
|
||||
|
||||
def save_file_id(file_key: str, file_id: str | None) -> None:
|
||||
try:
|
||||
with closing(create_connection()) as conn:
|
||||
cursor = conn.cursor()
|
||||
if file_id is None:
|
||||
cursor.execute("DELETE FROM file_ids WHERE file_key = ?", (file_key,))
|
||||
else:
|
||||
cursor.execute(
|
||||
"REPLACE INTO file_ids (file_key, file_id) VALUES (?, ?)",
|
||||
(file_key, file_id),
|
||||
)
|
||||
conn.commit()
|
||||
except Exception as exc:
|
||||
logger.error("Ошибка при сохранении file_id: %s", exc)
|
||||
|
||||
|
||||
def get_user_data(user_id: int, chat_id: int):
|
||||
try:
|
||||
with closing(create_connection()) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"SELECT * FROM user_data WHERE user_id = ? AND chat_id = ?",
|
||||
(user_id, chat_id),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
return tuple(row) if row else None
|
||||
except Exception as exc:
|
||||
logger.error("Ошибка при получении данных пользователя: %s", exc)
|
||||
return None
|
||||
|
||||
|
||||
def save_user_data(
|
||||
user_id: int,
|
||||
chat_id: int,
|
||||
count: int | None = None,
|
||||
start_time: float | None = None,
|
||||
ban_trigger: bool | None = None,
|
||||
ban_time: float | None = None,
|
||||
username: str | None = None,
|
||||
) -> None:
|
||||
try:
|
||||
with closing(create_connection()) as conn:
|
||||
cursor = conn.cursor()
|
||||
existing = get_user_data(user_id, chat_id)
|
||||
|
||||
if existing is None:
|
||||
cursor.execute(
|
||||
'''
|
||||
INSERT INTO user_data (user_id, chat_id, count, start_time, ban_trigger, ban_time, username)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
''',
|
||||
(
|
||||
user_id,
|
||||
chat_id,
|
||||
count or 0,
|
||||
start_time or 0,
|
||||
int(bool(ban_trigger)) if ban_trigger is not None else 0,
|
||||
ban_time or 0,
|
||||
username,
|
||||
),
|
||||
)
|
||||
else:
|
||||
updates = []
|
||||
params = []
|
||||
if count is not None:
|
||||
updates.append("count = ?")
|
||||
params.append(count)
|
||||
if start_time is not None:
|
||||
updates.append("start_time = ?")
|
||||
params.append(start_time)
|
||||
if ban_trigger is not None:
|
||||
updates.append("ban_trigger = ?")
|
||||
params.append(int(bool(ban_trigger)))
|
||||
if ban_time is not None:
|
||||
updates.append("ban_time = ?")
|
||||
params.append(ban_time)
|
||||
if username is not None:
|
||||
updates.append("username = ?")
|
||||
params.append(username)
|
||||
|
||||
if updates:
|
||||
params.extend([user_id, chat_id])
|
||||
cursor.execute(
|
||||
f"UPDATE user_data SET {', '.join(updates)} WHERE user_id = ? AND chat_id = ?",
|
||||
params,
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
except Exception as exc:
|
||||
logger.error("Ошибка при сохранении данных пользователя: %s", exc)
|
||||
|
||||
|
||||
def get_user_data_ban(user_id: int, chat_id: int, command: str):
|
||||
try:
|
||||
with closing(create_connection()) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"SELECT * FROM user_data_ban WHERE user_id = ? AND chat_id = ? AND command = ?",
|
||||
(user_id, chat_id, command),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
return tuple(row) if row else None
|
||||
except Exception as exc:
|
||||
logger.error("Ошибка при получении данных о блокировке пользователя: %s", exc)
|
||||
return None
|
||||
|
||||
|
||||
def save_user_data_ban(
|
||||
user_id: int,
|
||||
chat_id: int,
|
||||
command: str,
|
||||
count: int | None = None,
|
||||
ban_time_inf: float | None = None,
|
||||
) -> None:
|
||||
try:
|
||||
with closing(create_connection()) as conn:
|
||||
cursor = conn.cursor()
|
||||
existing = get_user_data_ban(user_id, chat_id, command)
|
||||
|
||||
if existing is None:
|
||||
cursor.execute(
|
||||
'''
|
||||
INSERT INTO user_data_ban (user_id, chat_id, command, count, ban_time_inf)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''',
|
||||
(user_id, chat_id, command, count or 0, ban_time_inf or 0),
|
||||
)
|
||||
else:
|
||||
updates = []
|
||||
params = []
|
||||
if count is not None:
|
||||
updates.append("count = ?")
|
||||
params.append(count)
|
||||
if ban_time_inf is not None:
|
||||
updates.append("ban_time_inf = ?")
|
||||
params.append(ban_time_inf)
|
||||
|
||||
if updates:
|
||||
params.extend([user_id, chat_id, command])
|
||||
cursor.execute(
|
||||
f"UPDATE user_data_ban SET {', '.join(updates)} WHERE user_id = ? AND chat_id = ? AND command = ?",
|
||||
params,
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
except Exception as exc:
|
||||
logger.error("Ошибка при сохранении данных о блокировке пользователя: %s", exc)
|
||||
|
||||
|
||||
create_tables()
|
||||
Executable
+63
@@ -0,0 +1,63 @@
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlencode, unquote
|
||||
|
||||
import aiohttp
|
||||
|
||||
from config import create_aiohttp_session, get_aiohttp_request_kwargs
|
||||
|
||||
DOWNLOAD_DIR = Path("downloads")
|
||||
DOWNLOAD_DIR.mkdir(exist_ok=True)
|
||||
|
||||
|
||||
def sanitize_filename(filename: str) -> str:
|
||||
filename = os.path.basename(filename).strip()
|
||||
filename = re.sub(r"[\\/:*?\"<>|]+", "_", filename)
|
||||
return filename or "downloaded_file"
|
||||
|
||||
|
||||
async def download_yandex_files(public_url: str, progress_callback=None) -> str:
|
||||
base_url = "https://cloud-api.yandex.net/v1/disk/public/resources/download?"
|
||||
final_url = base_url + urlencode({"public_key": public_url})
|
||||
|
||||
timeout = aiohttp.ClientTimeout(total=None, sock_connect=30, sock_read=300)
|
||||
|
||||
async with create_aiohttp_session(timeout=timeout) as session:
|
||||
request_kwargs = get_aiohttp_request_kwargs()
|
||||
|
||||
async with session.get(final_url, **request_kwargs) as response:
|
||||
response.raise_for_status()
|
||||
payload = await response.json()
|
||||
download_url = payload["href"]
|
||||
|
||||
async with session.get(download_url, **request_kwargs) as response:
|
||||
response.raise_for_status()
|
||||
|
||||
content_disposition = response.headers.get("Content-Disposition", "")
|
||||
filename = download_url.split("/")[-1]
|
||||
|
||||
if "filename*" in content_disposition:
|
||||
try:
|
||||
encoded = content_disposition.split("filename*=")[1].strip()
|
||||
parts = encoded.split("''", 1)
|
||||
if len(parts) == 2:
|
||||
filename = unquote(parts[1], encoding=parts[0] or "utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
filename = sanitize_filename(filename)
|
||||
suffix = Path(filename).suffix or ".bin"
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix, dir=DOWNLOAD_DIR) as tmp_file:
|
||||
total_size = int(response.headers.get("Content-Length", 0))
|
||||
downloaded_size = 0
|
||||
|
||||
async for chunk in response.content.iter_chunked(1024 * 64):
|
||||
tmp_file.write(chunk)
|
||||
downloaded_size += len(chunk)
|
||||
if progress_callback:
|
||||
await progress_callback(downloaded_size, total_size)
|
||||
|
||||
return tmp_file.name
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
BOT_TOKEN=your_telegram_bot_token
|
||||
YSK_API_KEY=your_yandex_speechkit_api_key
|
||||
API_WEATHER=your_openweather_api_key
|
||||
ACCESS_KEY=your_yandex_object_storage_access_key
|
||||
SECRET_KEY=your_yandex_object_storage_secret_key
|
||||
BUCKET_NAME=ydf-storage
|
||||
PROXY_URL=http://user:password@host:port
|
||||
# Примеры:
|
||||
# PROXY_URL=socks5://user:password@host:port
|
||||
# PROXY_URL=http://user:password@host:port
|
||||
Executable
+282
@@ -0,0 +1,282 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import aiohttp
|
||||
import boto3
|
||||
from aiogram.exceptions import TelegramBadRequest, TelegramRetryAfter
|
||||
from aiogram.types import FSInputFile
|
||||
|
||||
from config import ACCESS_KEY, API_WEATHER, BUCKET_NAME, SECRET_KEY, bot
|
||||
from database import get_file_id, get_user_data, save_file_id
|
||||
from download_files import download_yandex_files
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger("functions")
|
||||
|
||||
|
||||
async def send_photo_with_caption(chat_id: int, file_key: str, file_path: str, caption: str) -> None:
|
||||
file_id = get_file_id(file_key)
|
||||
|
||||
if file_id:
|
||||
try:
|
||||
await bot.send_photo(chat_id, file_id, caption=caption)
|
||||
return
|
||||
except Exception as exc:
|
||||
logger.error("Ошибка при отправке файла по ID: %s", exc)
|
||||
|
||||
try:
|
||||
sent_message = await bot.send_photo(chat_id, FSInputFile(file_path), caption=caption)
|
||||
save_file_id(file_key, sent_message.photo[-1].file_id)
|
||||
except Exception as exc:
|
||||
logger.error("Ошибка при загрузке файла: %s", exc)
|
||||
|
||||
|
||||
async def info_ban_status(message) -> None:
|
||||
user_id = message.from_user.id
|
||||
chat_id = message.chat.id
|
||||
user_info = get_user_data(user_id, chat_id)
|
||||
|
||||
if not user_info or not bool(user_info[4]):
|
||||
await send_photo_with_caption(chat_id, "clear_robot", "img/clear_robot.jpg", "Не переживай, ты чист и невинен!")
|
||||
return
|
||||
|
||||
username = user_info[6] or message.from_user.full_name
|
||||
ban_until = user_info[5] + 300
|
||||
ban_until_txt = time.strftime("%d %b %Y %H:%M:%S", time.localtime(ban_until))
|
||||
await send_photo_with_caption(
|
||||
chat_id,
|
||||
"time_robot",
|
||||
"img/time_robot.jpg",
|
||||
f"{username}\nТвой бан на стикеры и гифки будет снят {ban_until_txt}",
|
||||
)
|
||||
|
||||
|
||||
def escape_markdown(text: str) -> str:
|
||||
return re.sub(r"([_\*\[\]\(\)~`>#\+\-=\|{}\.!])", r"\\\1", text)
|
||||
|
||||
|
||||
async def edit_message_with_retry(message, text: str, **kwargs):
|
||||
while True:
|
||||
try:
|
||||
return await message.edit_text(text, **kwargs)
|
||||
except TelegramRetryAfter as exc:
|
||||
logger.warning("Превышен лимит на редактирование сообщений. Жду %s сек.", exc.retry_after)
|
||||
await asyncio.sleep(exc.retry_after)
|
||||
except TelegramBadRequest as exc:
|
||||
logger.error("Ошибка редактирования сообщения: %s", exc)
|
||||
return None
|
||||
|
||||
|
||||
async def send_message_with_retry(message, text: str, **kwargs) -> None:
|
||||
while True:
|
||||
try:
|
||||
await message.answer(text, **kwargs)
|
||||
return
|
||||
except TelegramRetryAfter as exc:
|
||||
logger.warning("Превышен лимит на отправку сообщений. Жду %s сек.", exc.retry_after)
|
||||
await asyncio.sleep(exc.retry_after)
|
||||
|
||||
|
||||
def get_wind_direction(deg: float) -> str:
|
||||
if deg >= 337.5 or deg < 22.5:
|
||||
return "Север"
|
||||
if deg < 67.5:
|
||||
return "Северо-восток"
|
||||
if deg < 112.5:
|
||||
return "Восток"
|
||||
if deg < 157.5:
|
||||
return "Юго-восток"
|
||||
if deg < 202.5:
|
||||
return "Юг"
|
||||
if deg < 247.5:
|
||||
return "Юго-запад"
|
||||
if deg < 292.5:
|
||||
return "Запад"
|
||||
return "Северо-запад"
|
||||
|
||||
|
||||
async def get_weather(message) -> None:
|
||||
if len(message.text.split(maxsplit=1)) == 1:
|
||||
await bot.send_message(message.chat.id, "Пожалуйста, укажите город.")
|
||||
return
|
||||
|
||||
city = message.text.split(maxsplit=1)[1].strip()
|
||||
url = "https://api.openweathermap.org/data/2.5/weather"
|
||||
params = {"q": city, "appid": API_WEATHER, "units": "metric", "lang": "ru"}
|
||||
|
||||
timeout = aiohttp.ClientTimeout(total=30, sock_connect=15, sock_read=15)
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with session.get(url, params=params) as response:
|
||||
if response.status == 404:
|
||||
await bot.send_message(message.chat.id, "Город не найден. Пожалуйста, уточните запрос.")
|
||||
return
|
||||
response.raise_for_status()
|
||||
weather_data = await response.json()
|
||||
|
||||
weather_description = weather_data["weather"][0]["description"]
|
||||
temperature = weather_data["main"]["temp"]
|
||||
feels_like = weather_data["main"]["feels_like"]
|
||||
temp_min = weather_data["main"]["temp_min"]
|
||||
temp_max = weather_data["main"]["temp_max"]
|
||||
humidity = weather_data["main"]["humidity"]
|
||||
pressure = int(weather_data["main"]["pressure"] / 1.333)
|
||||
wind_speed = weather_data["wind"]["speed"]
|
||||
wind_deg = weather_data["wind"].get("deg", 0)
|
||||
wind_direction = get_wind_direction(wind_deg)
|
||||
rain_1h = weather_data.get("rain", {}).get("1h", 0)
|
||||
clouds_all = weather_data["clouds"]["all"]
|
||||
visibility = weather_data.get("visibility", 0)
|
||||
sunrise_time = datetime.fromtimestamp(weather_data["sys"]["sunrise"]).strftime("%H:%M")
|
||||
sunset_time = datetime.fromtimestamp(weather_data["sys"]["sunset"]).strftime("%H:%M")
|
||||
|
||||
weather_message = (
|
||||
f"🌆 Погода в городе {city}:\n\n"
|
||||
f"☁️ Описание: {weather_description}\n"
|
||||
f"🌡️ Температура: {temperature}°C (ощущается как {feels_like}°C)\n"
|
||||
f"📉 Минимальная температура: {temp_min}°C\n"
|
||||
f"📈 Максимальная температура: {temp_max}°C\n"
|
||||
f"💧 Влажность: {humidity}%\n"
|
||||
f"🌬️ Давление: {pressure} мм рт.ст\n"
|
||||
f"💨 Скорость ветра: {wind_speed} м/с, направление: {wind_direction}\n"
|
||||
f"🌧️ Осадки за последний час: {rain_1h} мм\n"
|
||||
f"☁️ Облачность: {clouds_all}%\n"
|
||||
f"👁️ Видимость: {visibility} м\n"
|
||||
f"🌅 Восход: {sunrise_time}, закат: {sunset_time}"
|
||||
)
|
||||
await bot.send_message(message.chat.id, weather_message)
|
||||
except aiohttp.ClientResponseError as exc:
|
||||
logger.error("HTTP ошибка погоды: %s", exc)
|
||||
await bot.send_message(message.chat.id, "При получении данных произошла ошибка, попробуйте еще раз.")
|
||||
except aiohttp.ClientError as exc:
|
||||
logger.error("Ошибка запроса погоды: %s", exc)
|
||||
await bot.send_message(message.chat.id, "Не удалось связаться с погодным сервисом.")
|
||||
except Exception as exc:
|
||||
logger.exception("Неожиданная ошибка погоды: %s", exc)
|
||||
await bot.send_message(message.chat.id, "Произошла ошибка при обработке погоды.")
|
||||
|
||||
|
||||
def _build_s3_client():
|
||||
endpoint_url = "https://storage.yandexcloud.net"
|
||||
return boto3.client(
|
||||
"s3",
|
||||
endpoint_url=endpoint_url,
|
||||
aws_access_key_id=ACCESS_KEY,
|
||||
aws_secret_access_key=SECRET_KEY,
|
||||
)
|
||||
|
||||
|
||||
def is_valid_yandex_public_link(value: str) -> bool:
|
||||
value = value.strip().lower()
|
||||
return (
|
||||
value.startswith("https://disk.yandex.")
|
||||
or value.startswith("http://disk.yandex.")
|
||||
or value.startswith("https://yadi.sk/")
|
||||
or value.startswith("http://yadi.sk/")
|
||||
)
|
||||
|
||||
|
||||
async def download_yd(message) -> None:
|
||||
text = (message.text or "").strip()
|
||||
|
||||
parts = text.split(maxsplit=1)
|
||||
command_part = parts[0] if parts else ""
|
||||
argument = parts[1].strip() if len(parts) > 1 else ""
|
||||
|
||||
command_name = command_part.split("@", 1)[0].lower()
|
||||
|
||||
if command_name != "/ydf":
|
||||
await message.answer("Некорректная команда.")
|
||||
return
|
||||
|
||||
if not argument:
|
||||
await message.answer("Не указана ссылка. Используйте: /ydf [ссылка]")
|
||||
return
|
||||
|
||||
files_down = argument
|
||||
|
||||
if files_down.startswith("(new)"):
|
||||
files_down = files_down.replace("(new)", "", 1).strip()
|
||||
if not files_down:
|
||||
await message.answer("После (new) нужно указать ссылку: /ydf (new) [ссылка]")
|
||||
return
|
||||
save_file_id(files_down, None)
|
||||
|
||||
if not is_valid_yandex_public_link(files_down):
|
||||
await message.answer("Похоже, это не публичная ссылка Яндекс.Диска.")
|
||||
return
|
||||
|
||||
file_id = get_file_id(files_down)
|
||||
if file_id:
|
||||
await message.answer_document(
|
||||
file_id,
|
||||
caption="Файл отправлен из кэша Telegram. Для повторной загрузки: /ydf (new) [ссылка]",
|
||||
)
|
||||
return
|
||||
|
||||
initial_message = await message.answer("Найдена ссылка, начинаю загрузку")
|
||||
last_message_text = "Найдена ссылка, начинаю загрузку"
|
||||
|
||||
async def progress_callback(downloaded_size: int, total_size: int) -> None:
|
||||
nonlocal last_message_text
|
||||
if total_size <= 0:
|
||||
return
|
||||
|
||||
percent = int(downloaded_size / total_size * 100)
|
||||
new_message_text = f"Загрузка файла: {percent}%"
|
||||
if new_message_text != last_message_text and percent % 5 == 0:
|
||||
await edit_message_with_retry(initial_message, new_message_text)
|
||||
last_message_text = new_message_text
|
||||
|
||||
try:
|
||||
file_name = await download_yandex_files(files_down, progress_callback=progress_callback)
|
||||
except aiohttp.ClientResponseError as exc:
|
||||
if exc.status == 404:
|
||||
await initial_message.edit_text("Ссылка не найдена или недоступна для скачивания.")
|
||||
else:
|
||||
await initial_message.edit_text(f"Ошибка загрузки: HTTP {exc.status}")
|
||||
return
|
||||
except aiohttp.ClientError:
|
||||
await initial_message.edit_text("Ошибка сети при обращении к Яндекс.Диску.")
|
||||
return
|
||||
except Exception:
|
||||
logger.exception("Ошибка при скачивании файла")
|
||||
await initial_message.edit_text("Произошла ошибка при скачивании файла.")
|
||||
return
|
||||
|
||||
try:
|
||||
if os.path.getsize(file_name) < 50 * 1024 * 1024:
|
||||
document = FSInputFile(file_name)
|
||||
response = await message.answer_document(document)
|
||||
save_file_id(files_down, response.document.file_id)
|
||||
else:
|
||||
s3 = await asyncio.to_thread(_build_s3_client)
|
||||
key_name = os.path.basename(file_name)
|
||||
await asyncio.to_thread(s3.upload_file, file_name, BUCKET_NAME, key_name)
|
||||
|
||||
endpoint_url = "https://storage.yandexcloud.net"
|
||||
file_url = urljoin(endpoint_url, f"/{BUCKET_NAME}/{key_name}")
|
||||
escaped_file_name = escape_markdown(key_name)
|
||||
escaped_file_url = escape_markdown(file_url)
|
||||
|
||||
await send_message_with_retry(
|
||||
message,
|
||||
f"Ваш файл доступен для скачивания [{escaped_file_name}]({escaped_file_url})",
|
||||
parse_mode="MarkdownV2",
|
||||
)
|
||||
finally:
|
||||
try:
|
||||
os.remove(file_name)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
try:
|
||||
await initial_message.delete()
|
||||
except TelegramBadRequest:
|
||||
pass
|
||||
@@ -0,0 +1,15 @@
|
||||
Я - М.И.Б (МультиИнструмент-Бот), и я здесь, чтобы помочь тебе с твоими задачами.
|
||||
|
||||
Я помогу получить твой ID пользователя. Отправь мне /get_id.
|
||||
|
||||
Скачать файл с Яндекс Диска по команде /ydf.
|
||||
|
||||
Могу рассказать о погоде в твоем или другом городе. Просто отправь команду /weather с названием города.
|
||||
|
||||
Также, по команде /res, я могу восстановить эти (gjqltv gbnm gbdj) крокозябры в нормальные слова (пойдем пить пиво).
|
||||
|
||||
Также я умею модерировать стикеры (режим АнтитДудо). Если их будет много, то я позабочусь, чтобы они вас не беспокоили. Если вдруг вы попадете под мои “лапы”, просто напишите мне /info_ban и я все тебе расскажу.
|
||||
|
||||
Кроме того, я использую SpeechKit для обработки твоих голосовых сообщений. Чтобы в твоем чате все ленивые смогли прочесть сообщение и не слушать его. И вы могли лучше понять друг друга.
|
||||
|
||||
Если у вас возникнут вопросы или потребуется помощь, пожалуйста, не стесняйтесь обращаться ко мне.
|
||||
@@ -0,0 +1,106 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
|
||||
from aiogram import Dispatcher, F, types
|
||||
from aiogram.filters import Command
|
||||
|
||||
from antispam import anti_DuDo, call_verification
|
||||
from config import bot, setup_bot_connection
|
||||
from functions import download_yd, get_weather, info_ban_status, send_photo_with_caption
|
||||
from switching_en_ru import convert_layout
|
||||
from yandex_cloud_sk import get_text_from_speech
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger("main")
|
||||
|
||||
router = Dispatcher()
|
||||
|
||||
|
||||
@router.message(Command("start"))
|
||||
async def welcome_start(message: types.Message):
|
||||
caption = (
|
||||
f"Приветствую тебя {message.from_user.username or message.from_user.full_name}\n"
|
||||
"Для получения дополнительной информации отправь мне /help"
|
||||
)
|
||||
await send_photo_with_caption(message.chat.id, "welcome_robot", "img/welcome_robot.jpeg", caption)
|
||||
await call_verification(message.from_user.id, message, "start")
|
||||
|
||||
|
||||
@router.message(Command("help"))
|
||||
async def welcome_help(message: types.Message):
|
||||
with open("info.txt", "r", encoding="utf-8") as file:
|
||||
info = file.read()
|
||||
await message.reply(f"Привет {message.from_user.username or message.from_user.full_name}.\n{info}")
|
||||
await call_verification(message.from_user.id, message, "help")
|
||||
|
||||
|
||||
@router.message(Command("get_id"))
|
||||
async def get_me_id(message: types.Message):
|
||||
await message.answer(f"ID пользователя: {message.from_user.id}")
|
||||
await call_verification(message.from_user.id, message, "get_id")
|
||||
|
||||
|
||||
@router.message(Command("weather"))
|
||||
async def weather(message: types.Message):
|
||||
await get_weather(message)
|
||||
await call_verification(message.from_user.id, message, "weather")
|
||||
|
||||
|
||||
@router.message(Command("ydf"))
|
||||
async def yandex_down_files(message: types.Message):
|
||||
await download_yd(message)
|
||||
await call_verification(message.from_user.id, message, "ydf")
|
||||
|
||||
|
||||
@router.message(Command("res"))
|
||||
async def restoring_layout(message: types.Message):
|
||||
if message.reply_to_message and message.reply_to_message.text:
|
||||
restored_text = convert_layout(message.reply_to_message.text)
|
||||
await message.answer(restored_text)
|
||||
else:
|
||||
await message.answer("Пожалуйста, ответьте командой /res на сообщение с текстом.")
|
||||
await call_verification(message.from_user.id, message, "res")
|
||||
|
||||
|
||||
@router.message(Command("info_ban"))
|
||||
async def info_ban(message: types.Message):
|
||||
await info_ban_status(message)
|
||||
await call_verification(message.from_user.id, message, "info_ban")
|
||||
|
||||
|
||||
@router.message(F.sticker | F.animation)
|
||||
async def handle_sticker(message: types.Message):
|
||||
user_id = message.from_user.id
|
||||
now = time.time()
|
||||
await anti_DuDo(user_id, message, now)
|
||||
|
||||
|
||||
@router.message(F.voice)
|
||||
async def handle_voice(message: types.Message):
|
||||
voice: types.Voice = message.voice
|
||||
voice_file = await bot.get_file(voice.file_id)
|
||||
file_base_url = f"https://api.telegram.org/file/bot{bot.token}/{voice_file.file_path}"
|
||||
|
||||
try:
|
||||
text = await get_text_from_speech(file_base_url)
|
||||
if not text:
|
||||
text = "Не удалось распознать текст."
|
||||
except Exception as exc:
|
||||
logger.error("Ошибка при распознавании текста: %s", exc)
|
||||
text = "Произошла ошибка при обработке голосового сообщения."
|
||||
|
||||
await message.answer(f"{message.from_user.username or message.from_user.full_name} изрек:\n{text}")
|
||||
|
||||
|
||||
async def main():
|
||||
connected = await setup_bot_connection()
|
||||
if not connected:
|
||||
# Завершаем работу штатно: без traceback и без падения процесса.
|
||||
return
|
||||
|
||||
await router.start_polling(bot)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
Кто не слушает мудрость старших, тот платит собственной глупостью!
|
||||
Пренебрежение предупреждающим советом - путь к неприятностям!
|
||||
Не уважие к мудрым словам - первый шаг к своей гибели!
|
||||
Кто игнорирует предупреждение, тот неизбежно столкнется с неприятностями!
|
||||
Подчинение мудрости - ключ к избежанию наказаний!
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"references": {
|
||||
"mib-new": "."
|
||||
},
|
||||
"mcp": {
|
||||
"gitea": {
|
||||
"type": "local",
|
||||
"command": ["./gitea-mcp", "-t", "stdio", "--host", "https://git.hidosi.ru", "--token", "${GITEA_TOKEN}"],
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"permission": {
|
||||
"bash": { "git *": "allow", "*": "ask" }
|
||||
}
|
||||
}
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
aiogram>=3.4,<4.0
|
||||
aiohttp>=3.9,<4.0
|
||||
python-dotenv>=1.0,<2.0
|
||||
boto3>=1.34,<2.0
|
||||
yandex-speechkit
|
||||
aiohttp-socks>=0.8,<1.0
|
||||
+7611
File diff suppressed because it is too large
Load Diff
Executable
+17
@@ -0,0 +1,17 @@
|
||||
def convert_layout(text):
|
||||
english_chars = "qwertyuiop[]asdfghjkl;'zxcvbnm,.`"
|
||||
russian_chars = "йцукенгшщзхъфывапролджэячсмитьбюё"
|
||||
|
||||
converted_text = ""
|
||||
|
||||
for char in text:
|
||||
if char.lower() in english_chars:
|
||||
char_index = english_chars.index(char.lower())
|
||||
converted_char = russian_chars[char_index]
|
||||
if char.isupper():
|
||||
converted_char = converted_char.upper()
|
||||
converted_text += converted_char
|
||||
else:
|
||||
converted_text += char
|
||||
|
||||
return converted_text
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
API_KEY="${GENAPI_KEY:-}"
|
||||
BASE_URL="${GENAPI_BASE_URL:-https://proxy.gen-api.ru/v1}"
|
||||
MODEL="${GENAPI_MODEL:-gpt-5.4-nano}"
|
||||
PROMPT="${1:-Напиши одно слово: работает}"
|
||||
MAX_TOKENS="${MAX_TOKENS:-80}"
|
||||
TEMPERATURE="${TEMPERATURE:-0.2}"
|
||||
|
||||
if [ -z "$API_KEY" ]; then
|
||||
echo "Ошибка: не задан GENAPI_KEY"
|
||||
echo "Пример:"
|
||||
echo " export GENAPI_KEY='твoй_ключ'"
|
||||
echo " bash test_genapi_nano.sh 'Привет'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
curl -sS -X POST "$BASE_URL/chat/completions" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"model\": \"$MODEL\",
|
||||
\"messages\": [
|
||||
{
|
||||
\"role\": \"user\",
|
||||
\"content\": \"$PROMPT\"
|
||||
}
|
||||
],
|
||||
\"temperature\": $TEMPERATURE,
|
||||
\"max_tokens\": $MAX_TOKENS
|
||||
}"
|
||||
Executable
+63
@@ -0,0 +1,63 @@
|
||||
import asyncio
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import aiohttp
|
||||
from speechkit import configure_credentials, creds, model_repository
|
||||
from speechkit.stt import AudioProcessingType
|
||||
|
||||
from config import YSK_API_KEY, create_aiohttp_session, get_aiohttp_request_kwargs
|
||||
|
||||
if YSK_API_KEY:
|
||||
configure_credentials(
|
||||
yandex_credentials=creds.YandexCredentials(api_key=YSK_API_KEY)
|
||||
)
|
||||
|
||||
|
||||
def _transcribe_file_sync(file_path: str) -> str | None:
|
||||
model = model_repository.recognition_model()
|
||||
model.model = "general"
|
||||
model.language = "ru-RU"
|
||||
model.audio_processing_type = AudioProcessingType.Full
|
||||
|
||||
result = model.transcribe_file(file_path)
|
||||
for res in result:
|
||||
normalized_text = getattr(res, "normalized_text", None)
|
||||
if normalized_text:
|
||||
return normalized_text
|
||||
return None
|
||||
|
||||
|
||||
async def get_text_from_speech(file_url: str) -> str | None:
|
||||
temp_path = None
|
||||
timeout = aiohttp.ClientTimeout(total=300, sock_connect=30, sock_read=300)
|
||||
|
||||
try:
|
||||
async with create_aiohttp_session(timeout=timeout) as session:
|
||||
async with session.get(file_url, **get_aiohttp_request_kwargs()) as response:
|
||||
response.raise_for_status()
|
||||
content = await response.read()
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".ogg") as temp_file:
|
||||
temp_file.write(content)
|
||||
temp_path = temp_file.name
|
||||
|
||||
return await asyncio.to_thread(_transcribe_file_sync, temp_path)
|
||||
finally:
|
||||
if temp_path:
|
||||
try:
|
||||
Path(temp_path).unlink(missing_ok=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def synthesize(text: str) -> str:
|
||||
model = model_repository.synthesis_model()
|
||||
model.voice = "lera"
|
||||
result = model.synthesize(text, raw_format=False)
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".ogg") as temp_file:
|
||||
output_path = temp_file.name
|
||||
|
||||
result.export(output_path, "ogg")
|
||||
return output_path
|
||||
Reference in New Issue
Block a user