mirror of
https://gitverse.ru/kpa39l/chronicle.nixg.ru.git
synced 2026-09-29 18:05:07 +00:00
247 lines
7.3 KiB
Python
247 lines
7.3 KiB
Python
"""
|
|
Telegram Archiver - Main Application Entry Point.
|
|
|
|
Provides both FastAPI REST API and CLI interfaces for archiving
|
|
Telegram channels to local filesystem bundles.
|
|
"""
|
|
|
|
import asyncio
|
|
import time
|
|
from contextlib import asynccontextmanager
|
|
from pathlib import Path
|
|
from typing import AsyncGenerator
|
|
|
|
from fastapi import FastAPI, HTTPException, BackgroundTasks
|
|
from fastapi.responses import JSONResponse
|
|
|
|
from app.models import (
|
|
ArchiveRequest,
|
|
ArchiveResponse,
|
|
HealthCheck,
|
|
)
|
|
from app.telethon_client import TelethonArchiver
|
|
from app.archiver import ChannelArchiver
|
|
from app.logger import setup_logging, get_logger
|
|
from config import get_settings, get_required_settings, Settings
|
|
|
|
# Initialize logger
|
|
logger = get_logger(__name__)
|
|
|
|
# Global archiver client (shared across requests)
|
|
archiver_client: TelethonArchiver | None = None
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
"""Application lifespan manager."""
|
|
global archiver_client
|
|
|
|
# Startup
|
|
logger.info("Starting Telegram Archiver...")
|
|
|
|
try:
|
|
settings = get_required_settings()
|
|
setup_logging(
|
|
log_level=settings.log_level,
|
|
enable_otel=settings.otel_enabled,
|
|
)
|
|
|
|
archiver_client = TelethonArchiver(settings)
|
|
await archiver_client.connect()
|
|
|
|
logger.info("Telegram Archiver started successfully")
|
|
except Exception as e:
|
|
logger.warning(f"Could not initialize Telegram client: {e}")
|
|
logger.warning("Server will start but /archive endpoint will not work")
|
|
logger.warning("Create .env file with API_ID, API_HASH, PHONE to enable archiving")
|
|
|
|
yield
|
|
|
|
# Shutdown
|
|
logger.info("Shutting down Telegram Archiver...")
|
|
if archiver_client:
|
|
await archiver_client.disconnect()
|
|
logger.info("Telegram Archiver shutdown complete")
|
|
|
|
|
|
# Create FastAPI app
|
|
app = FastAPI(
|
|
title="Telegram Archiver",
|
|
description="Archive Telegram channels to local filesystem with media",
|
|
version="1.0.0",
|
|
lifespan=lifespan,
|
|
)
|
|
|
|
|
|
@app.get("/health", response_model=HealthCheck)
|
|
async def health_check() -> HealthCheck:
|
|
"""Health check endpoint."""
|
|
is_connected = archiver_client is not None and archiver_client._connected
|
|
return HealthCheck(
|
|
status="healthy" if is_connected else "disconnected",
|
|
version="1.0.0",
|
|
session_active=is_connected,
|
|
)
|
|
|
|
|
|
@app.post("/archive", response_model=ArchiveResponse)
|
|
async def archive_channel(request: ArchiveRequest) -> ArchiveResponse:
|
|
"""
|
|
Archive a Telegram channel to local filesystem.
|
|
|
|
Creates a bundle directory for each message with:
|
|
- index.md: Message content in Markdown with front-matter
|
|
- Media files: Photos, videos, documents (if under size limit)
|
|
|
|
Large files (>MAX_FILE_SIZE) are skipped and logged in 2big2get.md
|
|
"""
|
|
if not archiver_client:
|
|
raise HTTPException(
|
|
status_code=503,
|
|
detail="Archiver not initialized. Check server logs. Make sure .env file exists with API_ID, API_HASH, PHONE."
|
|
)
|
|
|
|
try:
|
|
logger.info(f"Archive request for channel: {request.channel}")
|
|
|
|
# Get settings and override output_dir if specified
|
|
settings = get_required_settings()
|
|
if request.output_dir:
|
|
settings.output_dir = Path(request.output_dir)
|
|
|
|
# Create archiver for this request
|
|
channel_archiver = ChannelArchiver(archiver_client, settings)
|
|
|
|
# Run archive process
|
|
result = await channel_archiver.archive_channel(
|
|
channel=request.channel,
|
|
limit=request.limit,
|
|
from_message_id=request.from_message_id,
|
|
force=request.force,
|
|
)
|
|
|
|
logger.info(f"Archive completed: {result['posts_archived']} posts")
|
|
|
|
return ArchiveResponse(**result)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Archive failed: {e}", exc_info=True)
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@app.get("/")
|
|
async def root() -> dict:
|
|
"""Root endpoint with API info."""
|
|
return {
|
|
"name": "Telegram Archiver",
|
|
"version": "1.0.0",
|
|
"description": "Archive Telegram channels to local filesystem",
|
|
"endpoints": {
|
|
"health": "GET /health",
|
|
"archive": "POST /archive",
|
|
"docs": "GET /docs",
|
|
},
|
|
}
|
|
|
|
|
|
# CLI entry point
|
|
def cli_main() -> None:
|
|
"""Command-line interface entry point."""
|
|
import click
|
|
|
|
@click.command()
|
|
@click.option(
|
|
"--channel",
|
|
"-c",
|
|
required=True,
|
|
help="Telegram channel username (with or without @)",
|
|
)
|
|
@click.option(
|
|
"--output-dir",
|
|
"-o",
|
|
type=click.Path(),
|
|
default=None,
|
|
help="Output directory for archived channel",
|
|
)
|
|
@click.option(
|
|
"--limit",
|
|
"-l",
|
|
type=int,
|
|
default=None,
|
|
help="Limit number of messages to archive",
|
|
)
|
|
@click.option(
|
|
"--from-message-id",
|
|
"-f",
|
|
type=int,
|
|
default=None,
|
|
help="Start archiving from this message ID",
|
|
)
|
|
@click.option(
|
|
"--force",
|
|
is_flag=True,
|
|
help="Force re-download of already archived posts",
|
|
)
|
|
@click.option(
|
|
"--env-file",
|
|
type=click.Path(exists=True),
|
|
default=".env",
|
|
help="Path to .env file",
|
|
)
|
|
def archive(
|
|
channel: str,
|
|
output_dir: str | None,
|
|
limit: int | None,
|
|
from_message_id: int | None,
|
|
force: bool,
|
|
env_file: str,
|
|
) -> None:
|
|
"""Archive a Telegram channel to local filesystem."""
|
|
# Load settings
|
|
settings = get_settings()
|
|
|
|
if output_dir:
|
|
settings.output_dir = Path(output_dir)
|
|
|
|
# Setup logging
|
|
setup_logging(log_level=settings.log_level)
|
|
|
|
# Run archive
|
|
async def run_archive() -> None:
|
|
async with TelethonArchiver(settings) as client:
|
|
archiver = ChannelArchiver(client, settings)
|
|
result = await archiver.archive_channel(
|
|
channel=channel,
|
|
limit=limit,
|
|
from_message_id=from_message_id,
|
|
force=force,
|
|
)
|
|
|
|
# Print summary
|
|
click.echo("\n" + "=" * 50)
|
|
click.echo("Archive Summary")
|
|
click.echo("=" * 50)
|
|
click.echo(f"Channel: {result['channel_title']} ({result['channel']})")
|
|
click.echo(f"Posts archived: {result['posts_archived']}")
|
|
click.echo(f"Posts skipped: {result['posts_skipped']}")
|
|
click.echo(f"Media downloaded: {result['media_downloaded']}")
|
|
click.echo(f"Media skipped (large): {result['media_skipped_large']}")
|
|
click.echo(f"Output path: {result['output_path']}")
|
|
click.echo(f"Log file: {result['log_file']}")
|
|
if result['large_files_report']:
|
|
click.echo(f"Large files report: {result['large_files_report']}")
|
|
click.echo(f"Duration: {result['duration_seconds']:.2f}s")
|
|
click.echo("=" * 50)
|
|
|
|
asyncio.run(run_archive())
|
|
|
|
archive()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# For development: run with uvicorn
|
|
# uvicorn app.main:app --reload
|
|
import uvicorn
|
|
|
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|