""" Sending files to users """ from pathlib import Path from pyrogram import Client from pyrogram.types import Message from typing import Optional import aiohttp import aiofiles import logging logger = logging.getLogger(__name__) async def download_thumbnail(url: str, output_path: str) -> Optional[str]: """ Download thumbnail from URL Args: url: Thumbnail URL output_path: Path to save Returns: Path to downloaded file or None """ try: async with aiohttp.ClientSession() as session: async with session.get(url) as response: if response.status == 200: async with aiofiles.open(output_path, 'wb') as f: async for chunk in response.content.iter_chunked(8192): await f.write(chunk) return output_path except Exception as e: logger.warning(f"Failed to download thumbnail: {e}") return None async def send_file_to_user( client: Client, chat_id: int, file_path: str, caption: Optional[str] = None, thumbnail: Optional[str] = None ) -> bool: """ Send file to user Args: client: Pyrogram client chat_id: Chat ID file_path: Path to file caption: File caption thumbnail: Path to thumbnail or URL Returns: True if successful, False otherwise """ thumbnail_path = None try: file = Path(file_path) if not file.exists(): logger.error(f"File not found: {file_path}") return False # Maximum file size for Telegram (2GB) max_size = 2 * 1024 * 1024 * 1024 file_size = file.stat().st_size # If file is larger than 2GB, split into parts if file_size > max_size: logger.info(f"File too large ({file_size / (1024*1024*1024):.2f} GB), splitting into parts...") return await send_large_file_in_parts( client, chat_id, file_path, caption, thumbnail ) # Process thumbnail (can be URL or file path) if thumbnail: if thumbnail.startswith(('http://', 'https://')): # This is a URL - download thumbnail thumbnail_path = f"downloads/thumb_{file.stem}.jpg" downloaded = await download_thumbnail(thumbnail, thumbnail_path) if downloaded: thumbnail_path = downloaded else: thumbnail_path = None # Don't use thumbnail if download failed else: # This is a file path thumb_file = Path(thumbnail) if thumb_file.exists(): thumbnail_path = thumbnail else: thumbnail_path = None # Determine file type if file.suffix.lower() in ['.mp4', '.avi', '.mov', '.mkv', '.webm']: # Video - if no thumbnail, try to generate one if not thumbnail_path: from bot.utils.file_processor import generate_thumbnail thumbnail_path_temp = f"downloads/thumb_{file.stem}.jpg" if await generate_thumbnail(str(file), thumbnail_path_temp): thumbnail_path = thumbnail_path_temp await client.send_video( chat_id=chat_id, video=str(file), caption=caption, thumb=thumbnail_path ) elif file.suffix.lower() in ['.jpg', '.jpeg', '.png', '.gif', '.webp']: # Image await client.send_photo( chat_id=chat_id, photo=str(file), caption=caption ) elif file.suffix.lower() in ['.mp3', '.wav', '.ogg', '.m4a', '.flac']: # Audio await client.send_audio( chat_id=chat_id, audio=str(file), caption=caption, thumb=thumbnail_path ) else: # Document await client.send_document( chat_id=chat_id, document=str(file), caption=caption, thumb=thumbnail_path ) logger.info(f"File sent to user {chat_id}: {file_path}") return True except Exception as e: logger.error(f"Error sending file: {e}", exc_info=True) return False finally: # Delete temporary thumbnail if it was downloaded if thumbnail_path and thumbnail_path.startswith("downloads/thumb_"): try: thumb_file = Path(thumbnail_path) if thumb_file.exists(): thumb_file.unlink() logger.debug(f"Temporary thumbnail deleted: {thumbnail_path}") except Exception as e: logger.warning(f"Failed to delete temporary thumbnail: {e}") async def send_large_file_in_parts( client: Client, chat_id: int, file_path: str, caption: Optional[str] = None, thumbnail: Optional[str] = None ) -> bool: """ Send large file in parts Args: client: Pyrogram client chat_id: Chat ID file_path: Path to file caption: File caption thumbnail: Path to thumbnail or URL Returns: True if successful, False otherwise """ from bot.utils.file_splitter import split_file, delete_file_parts, get_part_info parts = [] try: # Split file into parts parts = await split_file(file_path) part_info = get_part_info(parts) total_parts = part_info["total_parts"] logger.info(f"Sending file in parts: {total_parts} parts") # Send each part for part_num, part_path in enumerate(parts, 1): part_caption = None if caption: part_caption = f"{caption}\n\nšŸ“¦ Part {part_num} of {total_parts}" else: part_caption = f"šŸ“¦ Part {part_num} of {total_parts}" # Send thumbnail only with first part part_thumbnail = thumbnail if part_num == 1 else None try: await client.send_document( chat_id=chat_id, document=str(part_path), caption=part_caption, thumb=part_thumbnail ) logger.info(f"Sent part {part_num}/{total_parts}") except Exception as e: logger.error(f"Error sending part {part_num}: {e}", exc_info=True) # Continue sending other parts continue logger.info(f"File sent in parts to user {chat_id}") return True except Exception as e: logger.error(f"Error sending large file in parts: {e}", exc_info=True) return False finally: # Delete file parts after sending if parts: await delete_file_parts(parts) logger.debug("File parts deleted") async def delete_file(file_path: str, max_retries: int = 3) -> bool: """ Delete file with retries Args: file_path: Path to file max_retries: Maximum number of retries Returns: True if successful """ import asyncio file = Path(file_path) if not file.exists(): return True # File already doesn't exist for attempt in range(max_retries): try: file.unlink() logger.info(f"File deleted: {file_path}") return True except PermissionError as e: # File may be locked by another process if attempt < max_retries - 1: wait_time = (attempt + 1) * 0.5 # Exponential backoff logger.warning(f"File locked, retrying in {wait_time}s: {file_path}") await asyncio.sleep(wait_time) else: logger.error(f"Failed to delete file after {max_retries} attempts (locked): {file_path}") # Add to cleanup queue for background cleanup task from bot.utils.file_cleanup import add_file_to_cleanup_queue add_file_to_cleanup_queue(str(file_path)) return False except Exception as e: if attempt < max_retries - 1: wait_time = (attempt + 1) * 0.5 logger.warning(f"Error deleting file, retrying in {wait_time}s: {e}") await asyncio.sleep(wait_time) else: logger.error(f"Error deleting file after {max_retries} attempts: {e}", exc_info=True) # Add to cleanup queue for background cleanup task from bot.utils.file_cleanup import add_file_to_cleanup_queue add_file_to_cleanup_queue(str(file_path)) return False return False