Loading

监听 电报订阅频道消息 telegram,转送到企业微信、wxpusher

先看效果:
telegram频道有新的消息。手机就收到:





安装依赖(Linux)

python3 -m venv /opt/tg-relay-venv
source /opt/tg-relay-venv/bin/activate
pip install --upgrade pip
pip install telethon==1.36.0 requests
配置文件 .env,【 cat /opt/tg-relay/.env】
# Telegram MTProto(必须) (填你自己的账号信息,从https://my.telegram.org/apps获取)
TG_API_ID=""
TG_API_HASH=""
TG_SESSION_NAME="tg_relay_session"   # session文件名(会生成 tg_relay_session.session)

# 监听频道(必须):逗号分隔,支持 @xxx 或 xxx(公开频道username)
TG_CHANNELS="@MjjShareChannel"

# 推送渠道:WECOM 或 WXPUSHER(二选一)
PUSH_PROVIDER="WXPUSHER"

# 企业微信群机器人 webhook(PUSH_PROVIDER=WECOM 时必填)
WECOM_WEBHOOK="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=123456"

# WxPusher(PUSH_PROVIDER=WXPUSHER 时必填)
WXPUSHER_APP_TOKEN="123456"
# 主题(Topics)
WXPUSHER_TOPIC_IDS="123456"


# 限频(可选)
RATE_LIMIT_PER_MIN=30
# 每分钟最多推送条数,默认 30

主程序:tg_to_wechat_telethon.py  【vim /opt/tg-relay/tg_to_wechat_telethon.py】
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
import time
import json
import signal
import asyncio
import requests
from datetime import datetime
from typing import List

from telethon import TelegramClient, events
from telethon.tl.types import Message


# ========== 读取 .env ==========
def load_env(path: str):
    if not os.path.exists(path):
        return
    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            s = line.strip()
            if not s or s.startswith("#") or "=" not in s:
                continue
            k, v = s.split("=", 1)
            os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))


# ========== 限频器 ==========
class RateLimiter:
    def __init__(self, per_min: int):
        self.per_min = max(1, per_min)
        self.bucket: List[float] = []

    async def acquire(self):
        now = time.time()
        self.bucket = [t for t in self.bucket if t >= now - 60]
        if len(self.bucket) >= self.per_min:
            await asyncio.sleep((self.bucket[0] + 60) - now)
        self.bucket.append(time.time())


# ========== 去重 ==========
class Deduper:
    def __init__(self, state_file: str):
        self.file = state_file
        self.seen = set()
        self._load()

    def _load(self):
        if not os.path.exists(self.file):
            return
        try:
            with open(self.file, "r", encoding="utf-8") as f:
                data = json.load(f)
            for c, m in data.get("seen", []):
                self.seen.add((int(c), int(m)))
        except Exception:
            pass

    def _save(self):
        tmp = self.file + ".tmp"
        with open(tmp, "w", encoding="utf-8") as f:
            json.dump({"seen": list(self.seen)[-5000:]}, f)
        os.replace(tmp, self.file)

    def is_new(self, chat_id: int, msg_id: int) -> bool:
        key = (chat_id, msg_id)
        if key in self.seen:
            return False
        self.seen.add(key)
        if len(self.seen) % 50 == 0:
            self._save()
        return True

# ========== WxPusher HTML 推送(仅 topicIds) ==========
def wxpusher_send_html(app_token: str, topic_ids: List[int], html: str, summary: str = ""):
    payload = {
        "appToken": app_token,
        "content": html,
        "summary": summary[:20],
        "contentType": 2,
        "topicIds": topic_ids,
        "verifyPayType": 0
    }
    r = requests.post(
        "https://wxpusher.zjiecode.com/api/send/message",
        json=payload,
        timeout=20
    )
    r.raise_for_status()
    data = r.json()
    if data.get("code") != 1000:
        raise RuntimeError(f"WxPusher error: {data}")


# ========== HTML 内容格式化 ==========
def format_html(msg: Message, channel_title: str) -> str:
    dt = msg.date.astimezone().strftime("%Y-%m-%d %H:%M:%S")
    text = (msg.raw_text or "").strip()

    media = "文本"
    if msg.photo:
        media = "图片"
    elif msg.video:
        media = "视频"
    elif msg.document:
        media = "文件"
    elif msg.voice:
        media = "语音"

    html = f"""
<h3> TG频道:{channel_title}</h3>
<p><b>时间:</b>{dt}</p>
<p><b>类型:</b>{media}</p>
<hr/>
<p style="white-space:pre-wrap;">{text}</p>
""".strip()

    return html


# ========== 主程序 ==========
async def main():
    load_env(os.environ.get("ENV_FILE", "/opt/tg-relay/.env"))

    api_id = os.environ.get("TG_API_ID")
    api_hash = os.environ.get("TG_API_HASH")
    session_name = os.environ.get("TG_SESSION_NAME", "tg_relay_session")

    channels = [c.strip() for c in os.environ.get("TG_CHANNELS", "").split(",") if c.strip()]
    channels = [c if c.startswith("@") else "@" + c for c in channels]

    app_token = os.environ.get("WXPUSHER_APP_TOKEN")
    topic_ids = [int(x) for x in os.environ.get("WXPUSHER_TOPIC_IDS", "").split(",") if x.strip().isdigit()]

    rate_limit = int(os.environ.get("RATE_LIMIT_PER_MIN", "30"))

    if not all([api_id, api_hash, channels, app_token, topic_ids]):
        raise SystemExit("❌ .env 配置不完整")

    limiter = RateLimiter(rate_limit)
    deduper = Deduper("/opt/tg-relay/state_seen.json")

    client = TelegramClient(session_name, int(api_id), api_hash)

    stop_event = asyncio.Event()
    signal.signal(signal.SIGTERM, lambda *_: stop_event.set())
    signal.signal(signal.SIGINT, lambda *_: stop_event.set())

    await client.start()
    me = await client.get_me()
    print(f"[OK] 已登录:{me.username or me.id}")
    print(f"[OK] 监听频道:{channels}")
    print(f"[OK] WxPusher topicIds:{topic_ids}")

    @client.on(events.NewMessage(chats=channels))
    async def handler(event):
        msg = event.message
        if not deduper.is_new(event.chat_id, msg.id):
            return

        await limiter.acquire()

        chat = await event.get_chat()
        title = getattr(chat, "title", None) or getattr(chat, "username", str(event.chat_id))
        html = format_html(msg, title)

        summary_text = (msg.raw_text or "").strip()
        summary_text = " ".join(summary_text.split())  # 把换行/多空格压成单空格,适合当“标题/摘要”

        if not summary_text:
            if msg.photo:
                summary_text = "【图片】"
            elif msg.video:
                summary_text = "【视频】"
            elif msg.document:
                summary_text = "【文件】"
            elif msg.voice:
                summary_text = "【语音】"
            else:
                summary_text = "【新消息】"



        wxpusher_send_html(
            app_token=app_token,
            topic_ids=topic_ids,
            html=html,
            #summary=f"{title} 新消息"
            summary=summary_text
        )

        print(f"[SENT] {title} #{msg.id}")

    await stop_event.wait()
    await client.disconnect()

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())






4)首次登录(只需要做一次)
第一次运行必须在一个能交互输入的终端执行一次,生成session等信息(后面 systemd 就能后台跑)。
cd /opt/tg-relay
source /opt/tg-relay-venv/bin/activate
ENV_FILE=/opt/tg-relay/.env python /opt/tg-relay/tg_to_wechat_telethon.py
它会提示你输入:
手机号(带国家码)、Telegram 验证码、若开启 2FA,还会要密码
成功后会生成:tg_relay_session.session 文件(非常重要,等于“登录凭证”)

5)systemd 后台守护(长期稳定)

创建:/etc/systemd/system/tg-relay.service
[Unit]
Description=Telegram Channel Relay to WeChat (Telethon)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
WorkingDirectory=/opt/tg-relay
Environment=ENV_FILE=/opt/tg-relay/.env
ExecStart=/opt/tg-relay-venv/bin/python /opt/tg-relay/tg_to_wechat_telethon.py
Restart=always
RestartSec=3
User=root

# 安全加固(可选但推荐)
NoNewPrivileges=true
PrivateTmp=true

[Install]
WantedBy=multi-user.target
启动:
systemctl daemon-reload
systemctl enable --now tg-relay
#实时看日志
journalctl -u tg-relay -f

重启:
systemctl restart tg-relay













posted @ 2026-02-03 22:12  LungGiyo  阅读(2)  评论(0)    收藏  举报