first work version

This commit is contained in:
vvzvlad
2025-02-01 16:47:46 +03:00
parent 81b0f029e8
commit 6e367b3ac9
11 changed files with 354 additions and 113 deletions
+29
View File
@@ -0,0 +1,29 @@
name: Docker Image CI
on:
workflow_dispatch:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GHCR_PAT }}
- name: Build the Docker image
run: docker build . --file Dockerfile --tag ghcr.io/${{ github.repository }}:latest --tag ghcr.io/${{ github.repository }}:${{ github.sha }}
- name: Push Docker images
run: |
docker push ghcr.io/${{ github.repository }}:latest
docker push ghcr.io/${{ github.repository }}:${{ github.sha }}
+4
View File
@@ -0,0 +1,4 @@
__pycache__/
.env
.DS_Store
+10
View File
@@ -0,0 +1,10 @@
FROM python:3.11-slim
WORKDIR /app
RUN mkdir -p data
RUN apt-get update && apt-get install -y curl libgl1 && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY *.py .
CMD ["uvicorn", "api_server:app", "--host", "0.0.0.0", "--port", "8000"]
+45
View File
@@ -0,0 +1,45 @@
Create API ID/HASH:
1)Login at https://my.telegram.org/apps
2)API development tools
3)Create new application
4)Copy App api_id and api_hash
Get session:
1)python3 -m venv .venv
2)source .venv/bin/activate
3)pip install pyrogram
4)Run ```TG_API_ID=290389758 TG_API_HASH=c22987sdfnkjjhd37efa5f0 python3 session_generator.py```
5)Enter phone number, get code in telegram, enter code, and copy session string:
```
Enter phone number or bot token: +7 993 850 5104
Is "+7 993 850 5104" correct? (y/N): y
The confirmation code has been sent via Telegram app
Enter confirmation code: 69267
The two-step verification is enabled and a password is required
Password hint: None
Enter password (empty to recover): Passport-Vegan-Scale6
Your session string:
========================================
AgG7QBoANg0YVmwZTZmqadO4MJQdnaRRnXwSYpbbkGf49aATvTZj-yKcvdH8IsIDbwp00PbWFcbSjzoPsmiUK8BF80yVmML7iEQptBZrRTLsnoxmeglD-I1dqcAB2ufkxQDM_40y5KAHiFvzAzXhVngQo8W7u3ZPQXb_DTcfogXXePPBQAyV20cuwGrsArv-R39ssSWnFueGnB21Y_cTXTQAAAAFPEaL8AA
========================================
Use session on ENV variable TG_SESSION_STRING in docker-compose.yml
```
6)Set session ENV variable in docker-compose.yml file:
```
pyrogram_bridge:
image: ghcr.io/vvzvlad/pyrogram-bridge:latest
container_name: pyrogram-bridge
environment:
TG_API_ID: 290389758
TG_API_HASH: c22987sdfnkjjhd37efa5f0
TG_SESSION_STRING: "AgG7QBoANg0YVmwZTZmqadO4MJQdn............FPEaL8AA"
TZ: Europe/Moscow
volumes:
....
```
+43
View File
@@ -0,0 +1,43 @@
from fastapi import FastAPI, HTTPException, Response
from telegram_client import TelegramClient
from config import get_settings
import logging
import json
logger = logging.getLogger(__name__)
app = FastAPI(title="PyroTg Bridge")
client = TelegramClient()
settings = get_settings()
@app.on_event("startup")
async def startup():
await client.start()
@app.on_event("shutdown")
async def shutdown():
await client.stop()
@app.get("/post/{channel}/{post_id}")
async def get_post(channel: str, post_id: int):
try:
data = await client.get_post(channel, post_id)
return Response(
content=json.dumps(data),
media_type="application/json"
)
except Exception as e:
logger.error(f"API error: {str(e)}")
raise HTTPException(
status_code=404,
detail={
"error": "post_retrieval_error",
"message": str(e)
}
) from e
@app.get("/status")
async def health_check():
return {
"status": "ok",
"connected": client.client.is_connected
}
-113
View File
@@ -1,113 +0,0 @@
# Telegram RSS Bridge Architecture
## System Components
### 1. Web Parser (Main Bridge)
- Existing web-based parser for public Telegram content
- Fallback mechanism to Pyro Parser when content not found
### 2. Pyro Parser (Backup Service)
- Pyrogram-based client for private content
- HTTP API server for post retrieval
- Session management for Telegram auth
### 3. HTTP API Interface
- Endpoints:
- `GET /post/{channel}/{post_id}` - Get single post
- `GET /channel/{channel}` - (Future) Get channel feed
- Response format:
```json
{
"id": 123,
"date": "2023-01-01T00:00:00",
"text": "post content",
"media": [],
"views": 100,
"reactions": [] // Future extension
}
```
## Service Structure
```text
pyro_parser/
├── telegram_client.py # Pyrogram wrapper + session management
├── api_server.py # FastAPI/Falcon server
├── schemas.py # Data models/Pydantic schemas
└── config.py # Environment configuration
```
## Key Features
1. **Pyrogram Client**
- Async Telegram client with session persistence
- Automatic reconnection
- Rate limiting handling
- Error tracking for API calls
2. **Content Processing**
- Raw post transformation
- Media links extraction
- Text cleanup utilities
- (Future) Reactions parser
## Environment Configuration
```bash
# Core settings
TG_API_ID=123456
TG_API_HASH=abcdef123456
SESSION_PATH=/data/session.file
# Server config
API_HOST=0.0.0.0
API_PORT=8000
REQUEST_TIMEOUT=30
```
## Docker Setup
```yaml
services:
pyro_parser:
image: ghcr.io/vvzvlad/pyrotg-bridge:latest
environment:
TG_API_ID: ${TG_API_ID}
TG_API_HASH: ${TG_API_HASH}
volumes:
- ./sessions:/data
ports:
- "8000:8000"
```
## CI/CD Integration
```yaml
# .github/workflows/deploy.yml
name: Deploy
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ghcr.io/vvzvlad/pyrotg-bridge:latest
```
## Extension Points
1. **Post Processing**
- Media download hooks
- Link preview generators
- Content filters
2. **API Features**
- Pagination params
- Field selection
- Webhook support
3. **Monitoring**
- /status endpoint
- Basic request metrics
- Error logging
+20
View File
@@ -0,0 +1,20 @@
import os
TG_API_ID = int(os.getenv("TG_API_ID"))
TG_API_HASH = os.getenv("TG_API_HASH")
SESSION_PATH = os.getenv("SESSION_PATH", "session.file")
API_HOST = os.getenv("API_HOST", "0.0.0.0")
API_PORT = int(os.getenv("API_PORT", 8000))
REQUEST_TIMEOUT = int(os.getenv("REQUEST_TIMEOUT", 30))
SESSION_STRING = os.getenv("TG_SESSION_STRING", "")
def get_settings():
return {
"tg_api_id": TG_API_ID,
"tg_api_hash": TG_API_HASH,
"session_path": SESSION_PATH,
"api_host": API_HOST,
"api_port": API_PORT,
"request_timeout": REQUEST_TIMEOUT,
"session_string": SESSION_STRING
}
+25
View File
@@ -0,0 +1,25 @@
services:
pyrogram_bridge:
image: ghcr.io/vvzvlad/pyrogram-bridge:latest
container_name: pyrogram-bridge
environment:
TG_API_ID: ${TG_API_ID}
TG_API_HASH: ${TG_API_HASH}
TG_SESSION_STRING: ${TG_SESSION_STRING}
TZ: Europe/Moscow
volumes:
- ./sessions:/data
ports:
- "8000:8000"
restart: always
logging:
driver: "json-file"
options:
max-file: 5
max-size: 10m
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/status"]
interval: 30s
timeout: 10s
retries: 3
start_period: 20s
+5
View File
@@ -0,0 +1,5 @@
fastapi
uvicorn
pyrogram
tgcrypto
pillow>=10.0.0
+27
View File
@@ -0,0 +1,27 @@
import os
import asyncio
from pyrogram import Client
async def generate_session():
api_id = os.getenv("TG_API_ID")
api_hash = os.getenv("TG_API_HASH")
if not api_id or not api_hash:
print("Error: Set TG_API_ID and TG_API_HASH environment variables")
return
async with Client(
name=":memory:",
api_id=int(api_id),
api_hash=api_hash,
in_memory=True
) as client:
session = await client.export_session_string()
print("\nYour session string:")
print("=" * 40)
print(session)
print("=" * 40)
print("\nUser session on ENV variable TG_SESSION_STRING in docker-compose.yml")
if __name__ == "__main__":
asyncio.run(generate_session())
+146
View File
@@ -0,0 +1,146 @@
import logging
from pyrogram import Client, errors
from pyrogram.types import Message
from config import get_settings
import base64
import os
logger = logging.getLogger(__name__)
settings = get_settings()
class TelegramClient:
def __init__(self):
if settings["session_string"]:
self.client = Client(
name="pyro_bridge",
session_string=settings["session_string"],
api_id=settings["tg_api_id"],
api_hash=settings["tg_api_hash"],
in_memory=True
)
else:
self.client = Client(
name="pyro_bridge",
api_id=settings["tg_api_id"],
api_hash=settings["tg_api_hash"],
workdir=settings["session_path"],
in_memory=True
)
async def start(self):
if not self.client.is_connected:
await self.client.start()
logger.info("Telegram client connected")
async def stop(self):
if self.client.is_connected:
await self.client.stop()
logger.info("Telegram client disconnected")
async def _parse_message(self, message: Message) -> dict:
# Extract text from different sources
text = message.text or ""
# Try to get caption for media messages
if not text and hasattr(message, "caption"):
text = message.caption or ""
# Try to get text from other attributes
if not text and hasattr(message, "action"):
text = str(message.action) # For service messages
# Parse media and add video markers
media = []
if message.media:
media_obj = message.media
if hasattr(message, "video"):
media.append(await self._parse_media(message.video))
else:
if isinstance(media_obj, list):
for m in media_obj:
media.append(await self._parse_media(m))
elif hasattr(media_obj, "photo"):
media.append(await self._parse_media(media_obj.photo))
else:
media.append(await self._parse_media(media_obj))
video_count = sum(1 for m in media if m.get("type") == "video")
if video_count > 0:
text = f"[{'Video' if video_count == 1 else f'{video_count} Videos'}] {text}" if text else f"[{'Video' if video_count == 1 else f'{video_count} Videos'}]"
# Always add reactions if present
reactions_text = ""
if getattr(message, "reactions", None):
reactions_text = "\nReactions: " + ", ".join(
f"{r.emoji}({r.count})"
for r in message.reactions.reactions
)
text += reactions_text
return {
"id": message.id,
"date": message.date.isoformat(),
"text": text,
"media": media,
"views": message.views or 0,
"reactions": [r.emoji for r in message.reactions.reactions] if getattr(message, "reactions", None) else []
}
async def _parse_media(self, media_obj) -> dict:
# Handle different media types
media_type = media_obj.__class__.__name__.lower()
file_id = getattr(media_obj, "file_id", "")
thumbnail_base64 = None
# Special cases
if media_type == "photofile":
file_id = getattr(media_obj, "photo_file_id", file_id)
elif media_type == "webpage":
return {"type": "webpage", "url": getattr(media_obj, "url", "")}
elif media_type == "video":
# Download and encode thumbnail
if media_obj.thumbs:
try:
thumb_path = await self.client.download_media(media_obj.thumbs[0].file_id)
with open(thumb_path, "rb") as image_file:
thumbnail_base64 = base64.b64encode(image_file.read()).decode("utf-8")
os.remove(thumb_path)
except Exception as e:
logger.error(f"Thumbnail error: {e.__class__.__name__} - {str(e)}")
return {
"type": "video",
"url": file_id,
"size": media_obj.file_size,
"duration": media_obj.duration,
"thumbnail": thumbnail_base64
}
return {
"type": media_type,
"url": str(file_id),
"size": getattr(media_obj, "file_size", None),
"thumbnail": thumbnail_base64
}
async def get_post(self, channel: str, post_id: int) -> dict:
try:
message = await self.client.get_messages(
chat_id=channel,
message_ids=post_id
)
if not message:
raise ValueError("Message not found")
# Debug raw message structure
print("Raw message object: %s", message)
print("Message attributes: %s", dir(message))
print("Message media type: %s", type(message.media) if message.media else None)
return await self._parse_message(message)
except errors.RPCError as e:
logger.error(f"Telegram API error: {e.__class__.__name__} - {e}")
raise
except Exception as e:
logger.error(f"Unexpected error in get_post: {str(e)}")
raise