在 VPS 上打造全自动、高颜值的 iOS 限免 Telegram 频道

想法💡

  • 目的是不想闲置浪费我的服务器了,然后突然想做一个 Telegram 频道,专门推送 Apple App Store 的限时免费应用。

项目简介

本教程将帮你搭建一个专业级的 iOS 限免推送频道,实现以下功能:

准实时推送:每 2 分钟检测一次,发现限免立即通知

智能过滤:自动过滤 "降价 1 元" 等伪限免,只推真正的 0 元应用

精美排版:带封面图、交互按钮、Emoji 视觉引导

时间显示:显示推送时间和发现限免的时间

自动恢复:基于 Systemd 守护进程,崩溃自动重启

核心技术栈:Docker + RSSHub + Python + Systemd + Telegram Bot API

教程开始

第一部分:准备工作

硬件要求

  • 服务器:任意 VPS(本教程以 OVH 为例,1 核 1G 内存即可)

  • 系统:Debian 11/12 或 Ubuntu 20.04/22.04

  • 权限:Root

软件准备

  1. Telegram Bot Token:找 @BotFather 申请

  2. 频道 ID:创建频道,将 Bot 设为管理员,转发消息给 @getidsbot 获取

第二部分:部署私有 RSSHub

为避免公共 RSS 源不稳定,我们在本地搭建 RSSHub。

1. 安装 Docker

# 更新系统
sudo apt update && sudo apt upgrade -y

# 使用官方脚本安装 Docker(最稳定的方式)
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

# 启动 Docker
sudo systemctl start docker
sudo systemctl enable docker
  • 重要提示:安装过程中如果弹出 "Daemons using outdated libraries" 提示框,直接按回车即可。

2. 校准服务器时区

这一步非常关键,时间不对会导致 HTTPS 证书验证失败。

# 设置为中国时区
sudo timedatctl set-timezone Asia/Shanghai

# 同步网络时间
sudo apt install ntpdate -y
sudo ntpdate pool.ntp.org

# 验证时间(确保年份、日期、时间都正确)
date

3. 启动 RSSHub 容器

docker run -d \
  --name rsshub \
  -p 1200:1200 \
  --restart=always \
  diygod/rsshub:latest
  • 验证:执行 curl http://127.0.0.1:1200/appstore/xianmian,有 XML 数据返回即成功。

第三部分:部署 Python 推送脚本

1. 创建项目环境

mkdir -p /root/ios_notifier
cd /root/ios_notifier

# 安装 Python 虚拟环境
sudo apt install python3-venv python3-pip -y

# 创建虚拟环境
python3 -m venv venv
source venv/bin/activate

# 安装依赖库
pip install requests feedparser beautifulsoup4 lxml

2. 编写核心脚本

创建文件:nano /root/ios_notifier/main.py

完整代码如下(直接复制):

import feedparser
import requests
import sqlite3
import time
import re
import json
import sys
from bs4 import BeautifulSoup
from datetime import datetime
from time import mktime

# ================= 配置区域 (请根据实际情况修改) =================
# 1. 你的 Bot Token
BOT_TOKEN = '123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11' 

# 2. 你的频道 ID (必须带 -100)
CHANNEL_ID = '-100xxxxxxxxxx'

# 3. 频道用户名 (文案展示用)
CHANNEL_NAME = '@你的频道用户名' 

# 4. 频道链接 (按钮跳转用)
CHANNEL_LINK = 'https://t.me/你的频道用户名'

# 5. RSS 源地址
RSS_URL = 'https://appadvice.com/feed'

# 6. 轮询间隔 (秒) - 改成 5 分钟,减少无意义检测
CHECK_INTERVAL = 300 

# 7. 数据库路径
DB_PATH = '/root/ios_notifier/history.db'
# =============================================================

def init_db():
    """初始化数据库,用于防止重复推送"""
    with sqlite3.connect(DB_PATH) as conn:
        conn.execute('''CREATE TABLE IF NOT EXISTS sent_apps
                        (link TEXT PRIMARY KEY, title TEXT, date_added TEXT)''')

def is_sent(link):
    """检查是否已推送过"""
    with sqlite3.connect(DB_PATH) as conn:
        cursor = conn.execute("SELECT 1 FROM sent_apps WHERE link=?", (link,))
        return cursor.fetchone() is not None

def mark_as_sent(link, title):
    """标记为已推送"""
    with sqlite3.connect(DB_PATH) as conn:
        conn.execute("INSERT OR IGNORE INTO sent_apps (link, title, date_added) VALUES (?, ?, ?)", 
                     (link, title, datetime.now().isoformat()))

def extract_image(html_content):
    """从 HTML 描述中提取封面图"""
    try:
        soup = BeautifulSoup(html_content, 'lxml')
        img = soup.find('img')
        if img and img.get('src'):
            return img['src']
    except:
        return None
    return None

def clean_text(html_content):
    """清洗文本,移除 HTML 标签和原价行"""
    soup = BeautifulSoup(html_content, 'lxml')
    text = soup.get_text(separator='\n').strip()
    lines = [line.strip() for line in text.splitlines() if line.strip() and "原价" not in line]
    cleaned = '\n'.join(lines)
    return cleaned[:250] + "..." if len(cleaned) > 250 else cleaned

def send_telegram(entry):
    """构造并发送精美的 Telegram 消息"""
    title = entry.title
    link = entry.link
    description = entry.description
    
    image_url = extract_image(description)
    text_content = clean_text(description)
    
    # === 时间信息处理 ===
    now = datetime.now().strftime('%Y-%m-%d %H:%M')
    
    try:
        if hasattr(entry, 'published_parsed') and entry.published_parsed:
            published_time = datetime.fromtimestamp(mktime(entry.published_parsed))
            time_diff = datetime.now() - published_time
            
            hours_ago = int(time_diff.total_seconds() / 3600)
            if hours_ago < 1:
                time_ago_text = "刚刚发现"
            elif hours_ago < 24:
                time_ago_text = f"{hours_ago}小时前发现"
            else:
                days_ago = int(hours_ago / 24)
                time_ago_text = f"{days_ago}天前发现"
        else:
            time_ago_text = "最新发现"
    except:
        time_ago_text = "最新发现"
    
    # === 精美排版文案 ===
    caption = (
        f"<b>▎{title}</b>\n\n"
        f"{text_content}\n\n"
        f"<b>🕐 推送时间:</b> {now}\n"
        f"<b>⏱ 限免状态:</b> {time_ago_text}\n"
        f"<b>⚠️ 提醒:</b> 限免随时可能结束,尽快下载\n\n"
        f"<b>🔓 解锁方法:</b>\n"
        f"点击下方按钮,获取即永久 (Lifetime)\n\n"
        f"<b>🔔 限时活动,购买前请确认在限免期 🔔</b>\n\n"
        f"<b>🉐 可以不用,但是不能没有</b>\n\n"
        f"👇 👇 👇\n"
        f"<b>🧾 标签:</b> #iOS #限免\n"
        f"<b>📢 频道:</b> {CHANNEL_NAME}\n"
        f"<b>☝️ 消息怕错过?请收藏频道并开启推送! ☝️</b>"
    )

    # === 交互按钮 ===
    keyboard = {
        "inline_keyboard": [
            [{"text": "🍎 前往 App Store 下载", "url": link}],
            [{"text": "📨 转发给朋友一起领", "url": f"https://t.me/share/url?url={link}&text=快来!{title} 限免了!"}]
        ]
    }

    try:
        payload = {
            'chat_id': CHANNEL_ID,
            'caption': caption,
            'parse_mode': 'HTML',
            'reply_markup': json.dumps(keyboard)
        }
        
        if image_url:
            url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendPhoto"
            payload['photo'] = image_url
        else:
            url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage"
            payload['text'] = payload.pop('caption')
            
        requests.post(url, json=payload)
        return True
    except Exception as e:
        print(f"[错误] 发送失败: {e}")
        return False

def check_updates():
    """单次检查逻辑"""
    print(f"[{datetime.now().strftime('%H:%M:%S')}] 正在检查 App Store 更新...")
    try:
        feed = feedparser.parse(RSS_URL)
        if not feed.entries:
            print("RSS 源无数据")
            return

        for entry in feed.entries[:15]:
            # === 核心过滤逻辑(增强版)===
            # 1. 标题含"降价"直接跳过
            if "降价" in entry.title:
                mark_as_sent(entry.link, entry.title) 
                continue

            # 2. 过滤文章类内容(增强规则)
            title_lower = entry.title.lower()
            article_keywords = [
                "best", "top", "great", "apps for", "apps to",
                "how to", "tips", "guide", "review", "roundup",
                "collection", "games of", "essential", "new year",
                "help you", "keep", "continue", "jump into"
            ]
            
            if any(keyword in title_lower for keyword in article_keywords):
                # 静默跳过文章,不输出日志(减少刷屏)
                mark_as_sent(entry.link, entry.title)
                continue

            # 3. 多维度判断是否真的免费
            description_str = entry.description if hasattr(entry, 'description') else ""
            is_free = (
                "¥0.00" in description_str or 
                "现价: ¥0" in description_str or 
                "现价:¥0" in description_str or
                "免费" in entry.title or 
                "限免" in entry.title or
                ("free" in title_lower and "app" in title_lower) or
                "$0.00" in description_str or
                "price: $0" in description_str.lower() or
                "now free" in description_str.lower()
            )

            if not is_free:
                # 非免费的也静默跳过
                continue
            # ===================

            if not is_sent(entry.link):
                if send_telegram(entry):
                    print(f"✅ 推送成功: {entry.title}")
                    mark_as_sent(entry.link, entry.title)
                    time.sleep(3)
                
    except Exception as e:
        print(f"❌ 异常: {e}")

if __name__ == "__main__":
    init_db()
    print("--- 🚀 iOS 限免监控机器人已启动 ---")
    print(f"数据源: {RSS_URL}")
    print(f"检测频率: 每 {CHECK_INTERVAL} 秒 ({CHECK_INTERVAL//60} 分钟)")
    
    # === 守护进程主循环 ===
    while True:
        check_updates()
        sys.stdout.flush()
        time.sleep(CHECK_INTERVAL)

保存文件Ctrl+O → 回车 → Ctrl+X

第四部分:设置 Systemd 守护进程

将脚本注册为系统服务,实现开机自启和崩溃自愈。

1. 创建服务文件

nano /etc/systemd/system/ios_notifier.service

2. 粘贴以下配置

[Unit]
Description=iOS App Free Notifier Service
After=network.target docker.service

[Service]
User=root
WorkingDirectory=/root/ios_notifier
ExecStart=/root/ios_notifier/venv/bin/python3 /root/ios_notifier/main.py
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

保存Ctrl+O → 回车 → Ctrl+X

3. 启动服务

# 重载配置
sudo systemctl daemon-reload

# 开机自启
sudo systemctl enable ios_notifier

# 立即启动
sudo systemctl start ios_notifier

第五部分:验证与监控

实时查看日志

journalctl -u ios_notifier -f

预期输出

[12:00:00] 正在检查 App Store 更新...
✅ 推送成功: 某某 App
[12:02:00] 正在检查 App Store 更新...

查看服务状态

sudo systemctl status ios_notifier

看到绿色的 ● active (running) 即为成功。

强制测试推送

如果想立刻看到推送效果(而不是等新限免),可以清空数据库强制重发:

# 停止服务
sudo systemctl stop ios_notifier

# 删除历史记录
rm /root/ios_notifier/history.db

# 重启服务
sudo systemctl start ios_notifier

# 查看日志
journalctl -u ios_notifier -f

此时你的 Telegram 频道会立刻收到 5-10 条测试消息。

第六部分:故障排查指南

问题一:时间显示错误或证书报错

原因:服务器时间不对(穿越到了未来或过去)。

解决

sudo ntpdate pool.ntp.org
date  # 确认时间正确
sudo systemctl restart ios_notifier

问题二:日志显示但频道没收到消息

排查

  1. 检查 Bot Token 是否正确填写

  2. 检查 Channel ID 是否带 -100 前缀

  3. 确认 Bot 在频道中是管理员身份

  4. curl 测试 Token:

curl https://api.telegram.org/bot你的TOKEN/getMe

问题三:推送全是"降价"商品

解决:代码中已加入过滤逻辑,检查代码第 104-110 行的过滤条件。

成果展示

部署完成后,你的频道将实现:

每 2 分钟自动检测,有限免立即推送
显示推送时间和发现时间,让用户了解限免新鲜度
精美排版:封面图 + Emoji + 交互按钮
智能过滤:只推真正的 0 元限免
7x24 小时运行:Systemd 保证崩溃自动重启

这套方案部署在 VPS 上,内存占用不到 200MB,是利用闲置服务器资源的最佳实践!

附录:常用管理命令

# 查看日志
journalctl -u ios_notifier -f

# 停止服务
sudo systemctl stop ios_notifier

# 启动服务
sudo systemctl start ios_notifier

# 重启服务
sudo systemctl restart ios_notifier

# 查看状态
sudo systemctl status ios_notifier

# 修改检测频率
nano /root/ios_notifier/main.py
# 修改 CHECK_INTERVAL 的值,然后重启服务

📝 修改 main.py 的完整步骤

第一步:停止服务(防止修改时冲突)

sudo systemctl stop ios_notifier

为什么要先停?因为文件正在被 Python 进程读取,虽然强行改也行,但停掉更安全。

第二步:编辑文件

使用 nano 编辑器(最适合新手):

nano /root/ios_notifier/main.py

nano 编辑器基础操作:

操作

快捷键

移动光标

用方向键 ↑ ↓ ← →

删除一行

Ctrl + K

搜索文字

Ctrl + W,然后输入要找的内容(比如 BOT_TOKEN

保存文件

Ctrl + O,然后按回车

退出编辑器

Ctrl + X

第三步:常见修改项及位置

1. 修改 Bot Token 和频道 ID(第 11-21 行)

Ctrl + W,搜索 BOT_TOKEN,会跳到配置区:

BOT_TOKEN = '这里改成你的真实Token' 
CHANNEL_ID = '-100xxxxxxxxxx'  # 改成你的频道ID
CHANNEL_NAME = '@你的频道用户名'  # 改成真实用户名
CHANNEL_LINK = 'https://t.me/你的频道用户名'  # 改成真实链接

2. 修改检测频率(第 26 行)

CHECK_INTERVAL = 120  # 改成 60 = 1分钟,改成 300 = 5分钟

3. 修改推送文案(第 95-112 行)

搜索 caption =,可以修改 Emoji、标题、文案等。

第四步:保存并退出

  1. Ctrl + O(字母 O,不是数字 0)

  2. 屏幕底部会提示 File Name to Write: /root/ios_notifier/main.py

  3. 直接按回车

  4. Ctrl + X 退出编辑器

第五步:重启服务(让修改生效)

sudo systemctl start ios_notifier

或者用重启命令(效果一样):

sudo systemctl restart ios_notifier

第六步:验证修改是否生效

立刻查看日志,确认没有报错:

journalctl -u ios_notifier -f

正常输出示例:

[12:30:00] 正在检查 App Store 更新...
✅ 推送成功: 某某 App

如果报错(比如 SyntaxErrorValueError):

说明你改错了某个地方(比如引号没配对、缩进搞乱了)。

快速修复:

# 再次停止服务
sudo systemctl stop ios_notifier

# 重新编辑
nano /root/ios_notifier/main.py

# 修正错误后再重启
sudo systemctl start ios_notifier

🎯 快速操作速查表

步骤

命令

① 停止服务

sudo systemctl stop ios_notifier

② 编辑文件

nano /root/ios_notifier/main.py

③ 保存

Ctrl+O → 回车

④ 退出

Ctrl+X

⑤ 重启服务

sudo systemctl restart ios_notifier

⑥ 查看日志

journalctl -u ios_notifier -f

💡 进阶技巧:在线备份

每次修改前,建议先备份一下:

# 备份原文件(带日期后缀)
cp /root/ios_notifier/main.py /root/ios_notifier/main.py.backup.$(date +%Y%m%d)

# 如果改坏了,可以恢复
# cp /root/ios_notifier/main.py.backup.20250111 /root/ios_notifier/main.py

完美🎉

消息盒子

# 暂无消息 #

只显示最新10条未读和已读信息