Phase 0 MVP: Telegram Archiver fully functional (992 posts tested)

This commit is contained in:
2026-03-06 22:15:50 +03:00
parent bb8c7766b4
commit b76e27afcb
14 changed files with 1171 additions and 134 deletions
+94 -31
View File
@@ -64,10 +64,11 @@ class TelethonArchiver:
session=self.settings.session_path,
api_id=self.settings.api_id,
api_hash=self.settings.api_hash,
device_model="Telegram Archiver",
device_model="Desktop (Ubuntu)", # Helps with code delivery
system_version="Ubuntu 22.04",
app_version="1.0.0",
lang_code="en",
system_lang_code="en",
lang_code="ru", # Russian language
system_lang_code="ru",
)
await self.client.connect()
@@ -83,26 +84,58 @@ class TelethonArchiver:
async def _authenticate(self) -> None:
"""Handle the authentication flow."""
try:
# Send code request
await self.client.send_code_request(self.settings.phone)
logger.info(f"Code sent to {self.settings.phone}")
# Try app code first
logger.info("Requesting auth code via Telegram app...")
sent_code = await self.client.send_code_request(
self.settings.phone,
force_sms=False
)
logger.info(f"Code request sent. Type: {sent_code.type}")
# Get code from user (in MVP, we'll use input())
code = input("Enter the code you received: ")
# Get code from user
print("\n" + "=" * 50)
print("AUTHENTICATION REQUIRED")
print("=" * 50)
print("Check your Telegram app (NOT SMS) for the code.")
print("Look for a message from 'Telegram' with the code.")
print("=" * 50)
print("\nOptions:")
print(" 1. Enter the code from Telegram app")
print(" 2. Type 'sms' to receive code via SMS")
print(" 3. Press Ctrl+C to cancel")
print("=" * 50)
user_input = input("\nEnter code (or 'sms'): ").strip()
# If user requests SMS
if user_input.lower() == 'sms':
logger.info("Requesting SMS code...")
print("\n📱 Requesting SMS code...")
sent_code = await self.client.send_code_request(
self.settings.phone,
force_sms=True
)
print("SMS sent! Enter the code:")
user_input = input("SMS Code: ").strip()
code = user_input
try:
await self.client.sign_in(
phone=self.settings.phone,
code=code
code=code,
phone_code_hash=sent_code.phone_code_hash
)
except SessionPasswordNeededError:
# 2FA is enabled
logger.info("2FA enabled. Enter password:")
print("\n🔐 2FA enabled")
password = input("2FA Password: ")
await self.client.sign_in(password=password)
except FloodWaitError as e:
logger.error(f"Flood wait: must wait {e.seconds} seconds")
print(f"\n⚠️ You must wait {e.seconds} seconds before trying again.")
raise
except Exception as e:
logger.error(f"Authentication failed: {e}")
@@ -165,24 +198,33 @@ class TelethonArchiver:
# Iterate messages (newest first)
message_count = 0
async for message in self.client.iter_messages(
entity,
limit=limit,
min_id=from_message_id, # Messages with ID > from_message_id
):
yield message
message_count += 1
error_count = 0
# Build iter_messages kwargs
iter_kwargs = {"limit": limit}
if from_message_id:
iter_kwargs["min_id"] = from_message_id
async for message in self.client.iter_messages(entity, **iter_kwargs):
try:
yield message
message_count += 1
if message_count % 100 == 0:
logger.debug(f"Fetched {message_count} messages...")
if message_count % 50 == 0:
logger.info(f"Progress: {message_count} messages fetched...")
except Exception as e:
error_count += 1
logger.error(f"Error processing message {message.id}: {e}")
continue # Skip problematic messages
logger.info(f"Finished fetching. Total messages: {message_count}")
logger.info(f"Finished fetching. Total: {message_count}, Errors: {error_count}")
async def download_media(
self,
message: Message,
output_path: Path,
max_size: Optional[int] = None,
timeout: int = 60, # Timeout in seconds
) -> list[dict]:
"""
Download all media from a message.
@@ -191,6 +233,7 @@ class TelethonArchiver:
message: Telegram message with media
output_path: Directory to save media files
max_size: Maximum file size to download (bytes)
timeout: Timeout for each file download in seconds
Returns:
List of dicts with file info (filename, type, size, path, is_too_large)
@@ -204,20 +247,40 @@ class TelethonArchiver:
# Ensure output directory exists
output_path.mkdir(parents=True, exist_ok=True)
# Handle different media types
if isinstance(message.media, MessageMediaPhoto):
# Photo
info = await self._download_photo(message, output_path, max_size)
if info:
media_info.append(info)
try:
# Handle different media types with timeout
if isinstance(message.media, MessageMediaPhoto):
# Photo
info = await asyncio.wait_for(
self._download_photo(message, output_path, max_size),
timeout=timeout
)
if info:
media_info.append(info)
elif isinstance(message.media, MessageMediaDocument):
# Document, video, audio, etc.
info = await self._download_document(message, output_path, max_size)
if info:
media_info.append(info)
elif isinstance(message.media, MessageMediaDocument):
# Document, video, audio, etc.
info = await asyncio.wait_for(
self._download_document(message, output_path, max_size),
timeout=timeout
)
if info:
media_info.append(info)
# Note: Other media types (geo, poll, etc.) are not downloadable
except asyncio.TimeoutError:
logger.warning(f"Timeout downloading media for message {message.id}")
# Add info about skipped file
media_info.append({
"filename": "timeout_skipped.bin",
"type": "timeout",
"caption": None,
"size": 0,
"path": None,
"is_too_large": False,
"timeout": True,
})
except Exception as e:
logger.error(f"Error downloading media for message {message.id}: {e}")
return media_info