Initial commit: MIB bot

This commit is contained in:
Галингер Р.С.
2026-07-07 17:55:52 +07:00
commit a355dccd45
17 changed files with 8988 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
.env
env-org
venv/
.venv/
__pycache__/
*.pyc
bot_database.db
tmp/
gitea-mcp
.idea/
*.zip
*.tar.gz
downloads/
img/
+150
View File
@@ -0,0 +1,150 @@
# 🤖 Telegram Multi-Tool Bot
![Python](https://img.shields.io/badge/Python-3.10+-blue)
![aiogram](https://img.shields.io/badge/aiogram-3.x-green)
![License](https://img.shields.io/badge/license-MIT-lightgrey)
Многофункциональный 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
View File
@@ -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)
Executable
+250
View File
@@ -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
View File
@@ -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()
+63
View File
@@ -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
View File
@@ -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
View File
@@ -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
Executable
+15
View File
@@ -0,0 +1,15 @@
Я - М.И.Б (МультиИнструмент-Бот), и я здесь, чтобы помочь тебе с твоими задачами.
Я помогу получить твой ID пользователя. Отправь мне /get_id.
Скачать файл с Яндекс Диска по команде /ydf.
Могу рассказать о погоде в твоем или другом городе. Просто отправь команду /weather с названием города.
Также, по команде /res, я могу восстановить эти (gjqltv gbnm gbdj) крокозябры в нормальные слова (пойдем пить пиво).
Также я умею модерировать стикеры (режим АнтитДудо). Если их будет много, то я позабочусь, чтобы они вас не беспокоили. Если вдруг вы попадете под мои “лапы”, просто напишите мне /info_ban и я все тебе расскажу.
Кроме того, я использую SpeechKit для обработки твоих голосовых сообщений. Чтобы в твоем чате все ленивые смогли прочесть сообщение и не слушать его. И вы могли лучше понять друг друга.
Если у вас возникнут вопросы или потребуется помощь, пожалуйста, не стесняйтесь обращаться ко мне.
Executable
+106
View File
@@ -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
View File
@@ -0,0 +1,5 @@
Кто не слушает мудрость старших, тот платит собственной глупостью!
Пренебрежение предупреждающим советом - путь к неприятностям!
Не уважие к мудрым словам - первый шаг к своей гибели!
Кто игнорирует предупреждение, тот неизбежно столкнется с неприятностями!
Подчинение мудрости - ключ к избежанию наказаний!
+16
View File
@@ -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" }
}
}
+6
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+17
View File
@@ -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
+33
View File
@@ -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
}"
+63
View File
@@ -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