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