diff --git a/README.md b/README.md index efd615f..9de2cb2 100644 --- a/README.md +++ b/README.md @@ -10,28 +10,7 @@ ## Get session -1)```python3 -m venv .venv && source .venv/bin/activate``` -2)```pip install pyrogram``` -3)Run ```TG_API_ID=290389758 TG_API_HASH=c22987sdfnkjjhd37efa5f0 python3 session_generator.py``` -4)Enter phone number, get code in telegram, enter code, and copy session string: - -```text -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: +1) Сreate docker-compose.yml (or, recommended, create stack in portainer) with full configuration and mount the same data directory: ```docker-compose volumes: @@ -44,7 +23,6 @@ services: environment: TG_API_ID: 290389758 TG_API_HASH: c22987sdfnkjjhd37efa5f0 - TG_SESSION_STRING: "AgG7QBoANg0YVmwZTZmqadO4MJQdn............FPEaL8AA" PYROGRAM_BRIDGE_URL: https://pgbridge.example.com API_PORT: 80 TOKEN: "1234567890" @@ -59,6 +37,51 @@ services: traefik.http.routers.pgbridge.tls: true ``` +2) Run docker-compose up -d or start stack in portainer + +3) Enter in container: + +```bash +docker exec -it pyrogram-bridge /bin/bash +``` + +4) Run bridge in interactive mode: + +```bash +python3 api_server.py +``` + +5) Enter phone number, get code in telegram, enter code, and copy session string. The session file will be saved in your local ./data directory. + +```text +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 +``` + +Session file will be saved in your data directory, in docker compose case — /var/lib/docker/volumes/pyrogram_bridge/_data/pyro_bridge.session. + +6) Restart bridge container: + +```bash +docker restart pyrogram-bridge +``` + + + + + + + + + + + + PYROGRAM_BRIDGE_URL - url to rss bridge, used for generate absolute url to media TOKEN - optional, if set, will be used to check if user has access to rss feed. If token is set, rss url will be https://pgbridge.example.com/rss/DragorWW_space/1234567890 Use this if you rss bridge access all world, otherwise your bridge can be used by many people and telegram will inevitably be sanctioned for botting. @@ -91,6 +114,7 @@ This creates an unnecessary load, so we do something else: we request limit*2 an Exclusion flags are a way to filter channel content based on pre-defined (by me) criteria. It's not a universal regexp-based filtering engine, for example, but it does 99% of my tasks of filtering the content of some toxic tg channels (mostly with fresh memes). There are several flags: + - video - presence of video and small text in the post - stream - words like "стрим", "livestream" - donat - word "донат" and its variations diff --git a/config.py b/config.py index 8acfc42..00b0fe1 100644 --- a/config.py +++ b/config.py @@ -1,13 +1,18 @@ import os def get_settings(): + tg_api_id = os.getenv("TG_API_ID") + tg_api_hash = os.getenv("TG_API_HASH") + if not tg_api_id or not tg_api_hash: + print("TG_API_ID and TG_API_HASH must be set") + os._exit(1) + return { - "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") or "session.file", + "tg_api_id": int(tg_api_id), + "tg_api_hash": tg_api_hash, + "session_path": os.getenv("SESSION_PATH", "data") or "data", "api_host": os.getenv("API_HOST", "0.0.0.0"), "api_port": int(os.getenv("API_PORT") or 8000), - "session_string": os.getenv("TG_SESSION_STRING", ""), "pyrogram_bridge_url": os.getenv("PYROGRAM_BRIDGE_URL", ""), "log_level": os.getenv("LOG_LEVEL", "INFO"), "debug": os.getenv("DEBUG", "False") == "True", diff --git a/rss_generator.py b/rss_generator.py index be5b533..727922f 100644 --- a/rss_generator.py +++ b/rss_generator.py @@ -16,7 +16,7 @@ if not logger.handlers: logger.addHandler(handler) -async def create_time_based_media_groups(messages, merge_seconds: int = 5): +async def _create_time_based_media_groups(messages, merge_seconds: int = 5): messages_sorted = sorted(messages, key=lambda x: x.date) cluster = [] last_msg_date = None @@ -59,7 +59,7 @@ async def create_time_based_media_groups(messages, merge_seconds: int = 5): return messages -async def create_messages_groups(messages): +async def _create_messages_groups(messages): """ Process messages into formatted posts, handling media groups """ @@ -70,16 +70,14 @@ async def create_messages_groups(messages): for message in messages: try: # Skip service messages about pinned posts - if message.service and 'PINNED_MESSAGE' in str(message.service): - continue + if message.service and 'PINNED_MESSAGE' in str(message.service): continue if message.media_group_id: if message.media_group_id not in media_groups: media_groups[message.media_group_id] = [] media_groups[message.media_group_id].append(message) else: - # Single message becomes its own processing group - processing_groups.append([message]) + processing_groups.append([message]) # Single message becomes its own processing group except Exception as e: logger.error(f"message_processing_error: channel {message.chat.username}, message_id {message.id}, error {str(e)}") @@ -91,25 +89,20 @@ async def create_messages_groups(messages): processing_groups.append(media_group) # Sort processing groups by date of first message in each group - processing_groups.sort( - key=lambda group: group[0].date, - reverse=True - ) + processing_groups.sort( key=lambda group: group[0].date, reverse=True ) return processing_groups -async def trim_messages_groups(messages_groups, limit): - # Remove the oldest group (the one with the lowest message id based on the first message's date) - if messages_groups: - _removed_group = messages_groups.pop() +async def _trim_messages_groups(messages_groups, limit): + if messages_groups: # Remove the oldest group (the one with the lowest message id based on the first message's date) + messages_groups.pop() - # Trim groups if they exceed the specified limit - if len(messages_groups) > limit: + if len(messages_groups) > limit: # Trim groups if they exceed the specified limit messages_groups = messages_groups[:limit] return messages_groups -async def render_messages_groups(messages_groups, post_parser, exclude_flags: str | None = None): +async def _render_messages_groups(messages_groups, post_parser, exclude_flags: str | None = None): """ Render message groups into HTML format Args: @@ -256,10 +249,10 @@ async def generate_channel_rss(channel: str, # Process messages into groups and render them if Config['time_based_merge']: - messages = await create_time_based_media_groups(messages, merge_seconds) - message_groups = await create_messages_groups(messages) - message_groups = await trim_messages_groups(message_groups, limit) - final_posts = await render_messages_groups(message_groups, post_parser, exclude_flags) + messages = await _create_time_based_media_groups(messages, merge_seconds) + message_groups = await _create_messages_groups(messages) + message_groups = await _trim_messages_groups(message_groups, limit) + final_posts = await _render_messages_groups(message_groups, post_parser, exclude_flags) # Generate feed entries for post in final_posts: @@ -337,10 +330,10 @@ async def generate_channel_html(channel: str, # Process messages into groups and render them if Config['time_based_merge']: - messages = await create_time_based_media_groups(messages, merge_seconds) - message_groups = await create_messages_groups(messages) - message_groups = await trim_messages_groups(message_groups, limit) - final_posts = await render_messages_groups(message_groups, post_parser, exclude_flags) + messages = await _create_time_based_media_groups(messages, merge_seconds) + message_groups = await _create_messages_groups(messages) + message_groups = await _trim_messages_groups(message_groups, limit) + final_posts = await _render_messages_groups(message_groups, post_parser, exclude_flags) # Generate HTML content html_posts = [post['html'] for post in final_posts] diff --git a/session_generator.py b/session_generator.py deleted file mode 100644 index 7c5793b..0000000 --- a/session_generator.py +++ /dev/null @@ -1,27 +0,0 @@ -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()) \ No newline at end of file diff --git a/telegram_client.py b/telegram_client.py index 156fb4f..63d74aa 100644 --- a/telegram_client.py +++ b/telegram_client.py @@ -1,4 +1,5 @@ import logging +import os from pyrogram import Client from config import get_settings @@ -7,27 +8,30 @@ 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 - ) + self._ensure_session_directory() + self.client = Client( + name="pyro_bridge", + api_id=settings["tg_api_id"], + api_hash=settings["tg_api_hash"], + workdir=settings["session_path"], + ) + + def _ensure_session_directory(self): + try: + os.makedirs(settings["session_path"], exist_ok=True) + logger.debug(f'Session directory created/verified: {settings["session_path"]}') + except Exception as e: + logger.error(f'Failed to create session directory {settings["session_path"]}: {str(e)}') + raise async def start(self): - if not self.client.is_connected: - await self.client.start() - logger.info("Telegram client connected") + try: + if not self.client.is_connected: + await self.client.start() + logger.info("Telegram client connected successfully") + except Exception as e: + logger.error(f"Failed to start Telegram client: {str(e)}") + raise async def stop(self): if self.client.is_connected: