diff --git a/.dockerignore b/.dockerignore
index 4bc78bf..f091260 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,49 +1,49 @@
-# 忽略环境变量文件
+# Ignore environment variable files
.env
.env.example
-# 忽略数据库文件
+# Ignore database files
*.db
db/forward.db1
-# 忽略 Python 生成的缓存文件
+# Ignore Python generated cache files
**/__pycache__/
*.py[cod]
*$py.class
-# 忽略虚拟环境
+# Ignore virtual environments
**/venv/
**/env/
**/ENV/
-# 忽略 Telethon 会话文件
+# Ignore Telethon session files
*.session
-*.session-journal
+*.session-journal
-# 忽略 IDE 配置文件
+# Ignore IDE configuration files
.idea/
ufb/.idea
-# 忽略示例和临时配置文件
+# Ignore example and temporary configuration files
/example
/config/*
ufb/config/*
-# 忽略无用的图片和测试目录
+# Ignore unused image and test directories
**/test/
**/images/
-# 忽略 RSS 相关数据和临时文件
+# Ignore RSS related data and temporary files
/rss/media/*
/rss/data/*
-# 忽略日志文件
+# Ignore log files
logs/*
-# 忽略临时文件夹
+# Ignore temporary folders
/temp/*
-# 额外忽略 `.git` 和 Docker 相关文件,防止意外复制
+# Additionally ignore .git and Docker related files to prevent accidental copying
.git
.gitignore
.dockerignore
diff --git a/.env.example b/.env.example
index fa1fef5..617bfba 100644
--- a/.env.example
+++ b/.env.example
@@ -1,141 +1,140 @@
-######### 必填项 #########
-# Telegram API 配置 (从 https://my.telegram.org/apps 获取)
+######### Required #########
+# Telegram API configuration (obtain from https://my.telegram.org/apps)
API_ID=
API_HASH=
-# 用户账号登录用的手机号 (格式如: +8613812345678)
+# Phone number for user account login (format: +8613812345678)
PHONE_NUMBER=
# Bot Token
BOT_TOKEN=
-# 用户ID (从 @userinfobot 获取)
+# User ID (obtain from @userinfobot)
USER_ID=
-################ 以下均为可选项 ##################
+################ All below are optional ##################
-# 管理员列表(此处填user_id,留空默认上方的USER_ID,多个用户用逗号分隔)
+# Admin list (enter user_id here, leave empty to default to USER_ID above, separate multiple users with commas)
ADMINS=
-# bot消息删除时间 (秒),0表示立即删除, -1表示不删除
+# Bot message deletion timeout (seconds), 0 means delete immediately, -1 means do not delete
BOT_MESSAGE_DELETE_TIMEOUT=300
-# 是否自动删除用户发送的指令消息 (true/false)
+# Whether to automatically delete command messages sent by users (true/false)
USER_MESSAGE_DELETE_ENABLE=false
-# 默认最大媒体文件大小限制(单位:MB)
+# Default maximum media file size limit (unit: MB)
DEFAULT_MAX_MEDIA_SIZE=15
-# 默认时区
+# Default timezone
DEFAULT_TIMEZONE=Asia/Shanghai
-# 自动更新数据库中聊天窗口名字时间 (24小时制)
+# Time to auto-update chat names in the database (24-hour format)
CHAT_UPDATE_TIME=03:00
-# 数据库配置
+# Database configuration
DATABASE_URL=sqlite:///./db/forward.db
-######### UI 布局配置 #########
+######### UI Layout Configuration #########
AI_MODELS_PER_PAGE=10
KEYWORDS_PER_PAGE=10
PUSH_CHANNEL_PER_PAGE=10
-# 总结列表(行)
-SUMMARY_TIME_ROWS=10
-# 总结列表(列)
+# Summary list (rows)
+SUMMARY_TIME_ROWS=10
+# Summary list (columns)
SUMMARY_TIME_COLS=6
-# 延迟时间列表(行)
+# Delay time list (rows)
DELAY_TIME_ROWS=10
-# 延迟时间列表(列)
+# Delay time list (columns)
DELAY_TIME_COLS=6
-# 媒体大小列表(行)
+# Media size list (rows)
MEDIA_SIZE_ROWS=10
-# 媒体大小列表(列)
+# Media size list (columns)
MEDIA_SIZE_COLS=6
-# 媒体扩展名列表(行)
+# Media extension list (rows)
MEDIA_EXTENSIONS_ROWS=10
-# 媒体扩展名列表(列)
+# Media extension list (columns)
MEDIA_EXTENSIONS_COLS=6
-# 每页显示的规则数量
+# Number of rules displayed per page
RULES_PER_PAGE=20
- ######### AI设置 #########
+ ######### AI Settings #########
-# 默认AI模型
+# Default AI model
DEFAULT_AI_MODEL=gemini-2.0-flash
# OpenAi API Key
OPENAI_API_KEY=your_openai_api_key
-# 留空使用官方接口 https://api.openai.com/v1
-OPENAI_API_BASE=
+# Leave empty to use the official API https://api.openai.com/v1
+OPENAI_API_BASE=
# Claude API Key
CLAUDE_API_KEY=your_claude_api_key
-# 留空使用官方接口
+# Leave empty to use the official API
CLAUDE_API_BASE=
# Gemini API Key
-# 默认使用官方接口
+# Uses the official API by default
GEMINI_API_KEY=your_gemini_api_key
-# 兼容OpenAI接口标准的第三方API Base,如官方的:https://generativelanguage.googleapis.com/v1beta
+# Third-party API Base compatible with OpenAI API standard, e.g. the official one: https://generativelanguage.googleapis.com/v1beta
GEMINI_API_BASE=
# DeepSeek API Key
DEEPSEEK_API_KEY=your_deepseek_api_key
-# 留空使用官方接口 https://api.deepseek.com/v1
-DEEPSEEK_API_BASE=
+# Leave empty to use the official API https://api.deepseek.com/v1
+DEEPSEEK_API_BASE=
# Qwen API Key
QWEN_API_KEY=your_qwen_api_key
-# 留空使用官方接口 https://dashscope.aliyuncs.com/compatible-mode/v1
-QWEN_API_BASE=
+# Leave empty to use the official API https://dashscope.aliyuncs.com/compatible-mode/v1
+QWEN_API_BASE=
# Grok API Key
GROK_API_KEY=your_grok_api_key
-# 留空使用官方接口 https://api.x.ai/v1
-GROK_API_BASE=
+# Leave empty to use the official API https://api.x.ai/v1
+GROK_API_BASE=
-# 默认AI提示词
-DEFAULT_AI_PROMPT=请尊重原意,保持原有格式不变,用简体中文重写下面的内容:
+# Default AI prompt
+DEFAULT_AI_PROMPT=Please respect the original meaning, keep the original format unchanged, and rewrite the following content in Simplified Chinese:
-# 默认AI总结提示词
-DEFAULT_SUMMARY_PROMPT=请总结以下频道/群组24小时内的消息。
-# 默认总结时间 (24小时制)
+# Default AI summary prompt
+DEFAULT_SUMMARY_PROMPT=Please summarize the messages from the following channel/group within the past 24 hours.
+# Default summary time (24-hour format)
DEFAULT_SUMMARY_TIME=07:00
-# AI总结每次爬取消息数量
+# Number of messages fetched per AI summary batch
SUMMARY_BATCH_SIZE=20
-# AI总结每次爬取消息间隔时间(秒)
+# Interval between AI summary message fetches (seconds)
SUMMARY_BATCH_DELAY=2
-######### RSS配置 #########
-# 是否启用RSS功能 (true/false)
+######### RSS Configuration #########
+# Whether to enable RSS functionality (true/false)
RSS_ENABLED=false
-# RSS基础访问URL
+# RSS base access URL
RSS_BASE_URL=
-# RSS媒体文件基础URL
+# RSS media file base URL
RSS_MEDIA_BASE_URL=
-######### 扩展内容 #########
+######### Extensions #########
-# 是否开启与通用论坛屏蔽插件服务端的同步服务 (true/false)
+# Whether to enable sync service with universal forum blocker plugin server (true/false)
UFB_ENABLED=false
-# 服务端地址
+# Server address
UFB_SERVER_URL=
-# 用户API_KEY
+# User API_KEY
UFB_TOKEN=
-
diff --git a/.gitignore b/.gitignore
index 5b728e1..f78ddd1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,7 @@
-# 环境变量文件
+# Environment variable files
.env
-# 数据库文件
+# Database files
*.db
# Python
@@ -9,14 +9,14 @@ __pycache__/
*.py[cod]
*$py.class
-# 虚拟环境
+# Virtual environments
venv/
env/
ENV/
-# Telethon session 文件
+# Telethon session files
*.session
-*.session-journal
+*.session-journal
/.idea
/example
/config
@@ -27,7 +27,7 @@ ufb/config/config.json
db/forward.db1
handlers/bot_handler copy.py
/temp
-使用场景示例.md
+usage_scenario_examples.md
/rss/media
/rss/data
diff --git a/Dockerfile b/Dockerfile
index 977292f..052273a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,13 +1,13 @@
FROM python:3.11-slim
-# 设置工作目录
+# Set working directory
WORKDIR /app
-# 设置Docker日志配置
+# Set Docker log configuration
ENV DOCKER_LOG_MAX_SIZE=10m
ENV DOCKER_LOG_MAX_FILE=3
-# 安装系统依赖
+# Install system dependencies
RUN apt-get update && apt-get install -y \
tzdata \
&& ln -fs /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
@@ -17,18 +17,18 @@ RUN apt-get update && apt-get install -y \
python3-dev \
&& rm -rf /var/lib/apt/lists/*
-# 复制依赖文件并安装
+# Copy dependency files and install
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
-# 创建临时文件目录
+# Create temporary file directory
RUN mkdir -p /app/temp
-# 复制应用代码
+# Copy application code
COPY . .
-# 设置环境变量
+# Set environment variables
ENV PYTHONUNBUFFERED=1
-# 启动命令
+# Start command
CMD ["python", "main.py"]
\ No newline at end of file
diff --git a/README.md b/README.md
index 962dbf8..01228d6 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@

-
Telegram 转发器 | Telegram Forwarder
+
Telegram Forwarder
---
@@ -12,109 +12,109 @@
-## 📖 简介
-Telegram 转发器是一个强大的消息转发工具,只需要你的账号加入频道/群聊即可以将指定聊天中的消息转发到其他聊天,不需要bot进入对应的频道/群组即可监听。可用于信息流整合过滤,消息提醒,内容收藏等多种场景, 不受转发/复制禁止的限制。此外,利用 Apprise 强大的推送功能,你可以轻松将消息分发至聊天软件、邮件、短信、Webhooks、APIs 等各种平台。
-
-## ✨ 特性
-
-- 🔄 **多源转发**:支持从多个来源转发到指定目标
-- 🔍 **关键词过滤**:支持白名单和黑名单模式
-- 📝 **正则匹配**:支持正则表达式匹配目标文本
-- 📋 **内容修改**:支持多种方式修改消息内容
-- 🤖 **AI 处理**:支持使用各大厂商的AI接口
-- 📹 **媒体过滤**:支持过滤指定类型的媒体文件
-- 📰 **RSS订阅**:支持RSS订阅
-- 📢 **多平台推送**:支持通过Apprise推送到多个平台
-
-## 📋 目录
-
-- [📖 简介](#-简介)
-- [✨ 特性](#-特性)
-- [🚀 快速开始](#-快速开始)
- - [1️⃣ 准备工作](#1️⃣-准备工作)
- - [2️⃣ 配置环境](#2️⃣-配置环境)
- - [3️⃣ 启动服务](#3️⃣-启动服务)
- - [4️⃣ 更新](#4️⃣-更新)
-- [📚 使用指南](#-使用指南)
- - [🌟 基础使用示例](#-基础使用示例)
- - [🔧 特殊使用场景示例](#-特殊使用场景示例)
-- [🛠️ 功能详解](#️-功能详解)
- - [⚡ 过滤流程](#-过滤流程)
- - [⚙️ 设置说明](#️-设置说明)
- - [主设置说明](#主设置说明)
- - [媒体设置说明](#媒体设置说明)
- - [🤖 AI功能](#-ai功能)
- - [配置说明](#配置)
- - [自定义模型](#自定义模型)
- - [AI处理能力](#ai-处理)
- - [定时总结功能](#定时总结)
- - [📢 推送功能](#-推送功能)
- - [设置说明](#设置说明)
- - [📰 RSS订阅](#-RSS订阅)
- - [启用RSS功能](#启用rss功能)
- - [访问RSS仪表盘](#访问rss仪表盘)
- - [Nginx配置](#nginx配置)
- - [RSS配置说明](#rss配置管理)
- - [特殊设置项](#特殊设置项)
- - [注意事项](#注意事项)
-
-- [🎯 特殊功能](#-特殊功能)
- - [🔗 链接转发功能](#-链接转发功能)
-- [📝 命令列表](#-命令列表)
-- [💐 致谢](#-致谢)
-- [☕ 捐赠](#-捐赠)
-- [📄 开源协议](#-开源协议)
-
-
-
-## 🚀 快速开始
-
-### 1️⃣ 准备工作
-
-1. 获取 Telegram API 凭据:
- - 访问 https://my.telegram.org/apps
- - 创建一个应用获取 `API_ID` 和 `API_HASH`
-
-2. 获取机器人 Token:
- - 与 @BotFather 对话创建机器人
- - 获取机器人的 `BOT_TOKEN`
-
-3. 获取用户 ID:
- - 与 @userinfobot 对话获取你的 `USER_ID`
-
-### 2️⃣ 配置环境
-
-新建文件夹
+## 📖 Introduction
+Telegram Forwarder is a powerful message forwarding tool. As long as your account has joined a channel/group, it can forward messages from specified chats to other chats without requiring the bot to be in the corresponding channel/group for monitoring. It can be used for information stream aggregation and filtering, message notifications, content bookmarking, and many other scenarios, without being restricted by forwarding/copying limitations. Additionally, leveraging the powerful push capabilities of Apprise, you can easily distribute messages to chat apps, email, SMS, Webhooks, APIs, and various other platforms.
+
+## ✨ Features
+
+- 🔄 **Multi-source Forwarding**: Support forwarding from multiple sources to a specified target
+- 🔍 **Keyword Filtering**: Support whitelist and blacklist modes
+- 📝 **Regex Matching**: Support regular expression matching for target text
+- 📋 **Content Modification**: Support multiple ways to modify message content
+- 🤖 **AI Processing**: Support AI APIs from various major providers
+- 📹 **Media Filtering**: Support filtering specified types of media files
+- 📰 **RSS Subscription**: Support RSS subscription
+- 📢 **Multi-platform Push**: Support pushing to multiple platforms via Apprise
+
+## 📋 Table of Contents
+
+- [📖 Introduction](#-introduction)
+- [✨ Features](#-features)
+- [🚀 Quick Start](#-quick-start)
+ - [1️⃣ Prerequisites](#1️⃣-prerequisites)
+ - [2️⃣ Configure Environment](#2️⃣-configure-environment)
+ - [3️⃣ Start Service](#3️⃣-start-service)
+ - [4️⃣ Update](#4️⃣-update)
+- [📚 User Guide](#-user-guide)
+ - [🌟 Basic Usage Example](#-basic-usage-example)
+ - [🔧 Special Use Case Examples](#-special-use-case-examples)
+- [🛠️ Feature Details](#️-feature-details)
+ - [⚡ Filtering Process](#-filtering-process)
+ - [⚙️ Settings Description](#️-settings-description)
+ - [Main Settings Description](#main-settings-description)
+ - [Media Settings Description](#media-settings-description)
+ - [🤖 AI Features](#-ai-features)
+ - [Configuration](#configuration)
+ - [Custom Models](#custom-models)
+ - [AI Processing](#ai-processing)
+ - [Scheduled Summary](#scheduled-summary)
+ - [📢 Push Feature](#-push-feature)
+ - [Settings Description](#settings-description)
+ - [📰 RSS Subscription](#-rss-subscription)
+ - [Enable RSS Feature](#enable-rss-feature)
+ - [Access RSS Dashboard](#access-rss-dashboard)
+ - [Nginx Configuration](#nginx-configuration)
+ - [RSS Configuration Management](#rss-configuration-management)
+ - [Special Settings](#special-settings)
+ - [Notes](#notes)
+
+- [🎯 Special Features](#-special-features)
+ - [🔗 Link Forwarding Feature](#-link-forwarding-feature)
+- [📝 Command List](#-command-list)
+- [💐 Acknowledgments](#-acknowledgments)
+- [☕ Donate](#-donate)
+- [📄 License](#-license)
+
+
+
+## 🚀 Quick Start
+
+### 1️⃣ Prerequisites
+
+1. Obtain Telegram API credentials:
+ - Visit https://my.telegram.org/apps
+ - Create an application to get `API_ID` and `API_HASH`
+
+2. Get bot Token:
+ - Chat with @BotFather to create a bot
+ - Obtain the bot's `BOT_TOKEN`
+
+3. Get user ID:
+ - Chat with @userinfobot to get your `USER_ID`
+
+### 2️⃣ Configure Environment
+
+Create a new directory
```bash
mkdir ./TelegramForwarder && cd ./TelegramForwarder
```
-下载仓库的 [**docker-compose.yml**](https://github.com/Heavrnl/TelegramForwarder/blob/main/docker-compose.yml) 到目录下
+Download the repository's [**docker-compose.yml**](https://github.com/Heavrnl/TelegramForwarder/blob/main/docker-compose.yml) to the directory
-接着下载或复制仓库的 **[.env.example](./.env.example)** 文件,填入必填项,然后重命名为`.env`
+Then download or copy the repository's **[.env.example](./.env.example)** file, fill in the required fields, and rename it to `.env`
```bash
wget https://raw.githubusercontent.com/Heavrnl/TelegramForwarder/refs/heads/main/.env.example -O .env
```
-### 3️⃣ 启动服务
+### 3️⃣ Start Service
-首次运行(需要验证):
+First run (requires verification):
```bash
docker-compose run -it telegram-forwarder
```
-CTRL+C 退出容器
+CTRL+C to exit the container
-修改 docker-compose.yml 文件,修改 `stdin_open: false` 和 `tty: false`
+Modify the docker-compose.yml file, set `stdin_open: false` and `tty: false`
-后台运行:
+Run in background:
```bash
docker-compose up -d
```
-### 4️⃣ 更新
-注意:docker-compose运行不需要拉取仓库源码,除非你打算自己build,否则只需要在项目目录执行以下命令即可更新。
+### 4️⃣ Update
+Note: Running with docker-compose does not require pulling the repository source code. Unless you plan to build it yourself, you only need to execute the following commands in the project directory to update.
```bash
docker-compose down
```
@@ -124,289 +124,289 @@ docker-compose pull
```bash
docker-compose up -d
```
-## 📚 使用指南
+## 📚 User Guide
-### 🌟 基础使用示例
+### 🌟 Basic Usage Example
-假设订阅了频道 "TG 新闻" (https://t.me/tgnews) 和 "TG 阅读" (https://t.me/tgread) ,但想过滤掉一些不感兴趣的内容:
+Suppose you've subscribed to channels "TG News" (https://t.me/tgnews) and "TG Read" (https://t.me/tgread), but want to filter out some uninteresting content:
-1. 创建一个 Telegram 群组/频道(例如:"My TG Filter")
-2. 将机器人添加到群组/频道,并设置为管理员
-3. 在**新创建**的群组/频道中发送命令:
+1. Create a Telegram group/channel (e.g., "My TG Filter")
+2. Add the bot to the group/channel and set it as admin
+3. Send commands in the **newly created** group/channel:
```bash
- /bind https://t.me/tgnews 或者 /bind "TG 新闻"
- /bind https://t.me/tgread 或者 /bind "TG 阅读"
+ /bind https://t.me/tgnews or /bind "TG News"
+ /bind https://t.me/tgread or /bind "TG Read"
```
-4. 设置消息处理模式:
+4. Set message processing mode:
```bash
/settings
```
- 选择要操作的对应频道的规则,根据喜好设置
-
- 详细设置说明请查看 [🛠️ 功能详解](#️-功能详解)
+ Select the rule for the corresponding channel and configure according to your preferences
-5. 添加屏蔽关键词:
+ For detailed settings, see [🛠️ Feature Details](#️-feature-details)
+
+5. Add blocked keywords:
```bash
- /add 广告 推广 '这是 广告'
+ /add ad promotion 'this is an ad'
```
-6. 如果发现转发的消息格式有问题(比如有多余的符号),可以使用正则表达式处理:
+6. If you find formatting issues with forwarded messages (e.g., extra symbols), you can use regex to handle them:
```bash
/replace \*\*
```
- 这会删除消息中的所有 `**` 符号
+ This will remove all `**` symbols from messages
->注意:以上增删改查操作,只对第一个绑定的规则生效,示例里是TG 新闻。若想对TG 阅读进行操作,需要先使用`/settings(/s)`,选择TG 阅读,再点击"应用当前规则",就可以对此进行增删改查操作了。也可以使用`/add_all(/aa)`,`/replace_all(/ra)`等指令同时对两条规则生效
+>Note: The above add/remove/modify/query operations only apply to the first bound rule, which is TG News in this example. To operate on TG Read, you need to first use `/settings(/s)`, select TG Read, then click "Apply current rule" to perform add/remove/modify/query operations on it. You can also use `/add_all(/aa)`, `/replace_all(/ra)` and similar commands to apply to both rules simultaneously.
-这样,你就能收到经过过滤和格式化的频道消息了
+This way, you'll receive filtered and formatted channel messages.
-### 🔧 特殊使用场景示例
+### 🔧 Special Use Case Examples
-#### 1. TG 频道的部分消息由于文字嵌入链接,点击会让你确认再跳转,例如 NodeSeek 的官方通知频道
+#### 1. Some messages in TG channels have text embedded with links, clicking them requires confirmation before redirecting, e.g., NodeSeek's official notification channel
-频道的原始消息格式
+Original message format from the channel
```markdown
-[**贴子标题**](https://www.nodeseek.com/post-xxxx-1)
-```
-可以对通知频道的转发规则 **依次** 使用以下指令:
+[**Post Title**](https://www.nodeseek.com/post-xxxx-1)
+```
+You can use the following commands **sequentially** on the notification channel's forwarding rule:
```plaintext
/replace \*\*
/replace \[(?:\[([^\]]+)\])?([^\]]+)\]\(([^)]+)\) [\1]\2\n(\3)
/replace \[\]\s*
-```
-最终所有转发的消息都会变成以下格式,这样直接点击链接就无需确认跳转:
+```
+All forwarded messages will then become the following format, allowing direct link clicks without confirmation:
```plaintext
-贴子标题
+Post Title
(https://www.nodeseek.com/post-xxxx-1)
-```
+```
---
-#### 2. 监听用户消息格式不美观,可优化消息显示方式
+#### 2. Monitored user messages have unattractive formatting, can optimize message display
-**依次** 使用以下指令:
+Use the following commands **sequentially**:
```plaintext
/r ^(?=.)
/r (?<=.)(?=$)
-```
-然后设置消息格式为 **HTML**,这样监听用户消息时,消息格式就会美观很多:
+```
+Then set the message format to **HTML**, which will make monitored user messages look much better:
-
+
---
-#### 3. 同步规则操作
+#### 3. Sync rule operations
-在 **设置菜单** 中开启 **"同步规则"**,并选择 **目标规则**,当前规则的所有操作将同步到选定的规则。
+Enable **"Sync rules"** in the **settings menu** and select the **target rule**. All operations on the current rule will be synced to the selected rule.
-适用于以下场景:
-- 不想在当前窗口处理规则
-- 需要同时操作多个规则
+Applicable scenarios:
+- Don't want to manage rules in the current window
+- Need to operate on multiple rules simultaneously
-如果当前规则仅用于同步而不需实际生效,可将 **"是否启用规则"** 设置为 **"否"**。
+If the current rule is only for syncing and doesn't need to take effect, you can set **"Enable rule"** to **"No"**.
---
-#### 4. 如何转发到收藏夹 (Saved Messages)
-> 不推荐,操作比较繁琐
-1. 在你的 bot 管理的任意群组或频道中发送以下命令:
+#### 4. How to forward to Saved Messages
+> Not recommended, the process is quite tedious
+1. In any group or channel managed by your bot, send the following command:
```bash
- /bind https://t.me/tgnews 你的用户名(即展示的名称)
- ```
+ /bind https://t.me/tgnews Your Username (i.e., display name)
+ ```
-2. 随意新建一个规则,并进行以下设置:
- - **开启同步功能**,同步到 **转发收藏夹的规则**
- - **转发模式** 选择 **"用户模式"**
- - **禁用规则**(将规则”是否启用规则“设置为关闭)
+2. Create any new rule and configure the following:
+ - **Enable sync feature**, sync to the **forward-to-saved-messages rule**
+ - **Forwarding mode** select **"User mode"**
+ - **Disable rule** (set "Enable rule" to off)
-这样,你就可以在其他规则中管理收藏夹的规则,所有操作都会同步到 **转发收藏夹** 规则中。
+This way, you can manage the saved messages rule from other rules, and all operations will be synced to the **forward-to-saved-messages** rule.
-## 🛠️ 功能详解
+## 🛠️ Feature Details
-### ⚡ 过滤流程
-首先要清楚消息过滤顺序,括号里对应设置里的选项:
+### ⚡ Filtering Process
+First, understand the message filtering order (options in parentheses correspond to settings):

-### ⚙️ 设置说明
-| 主设置界面 | AI设置界面 | 媒体设置界面 |
+### ⚙️ Settings Description
+| Main Settings Interface | AI Settings Interface | Media Settings Interface |
|---------|------|------|
|  |  |  |
-#### 主设置说明
-以下对设置选项进行说明
-| 设置选项 | 说明 |
+#### Main Settings Description
+The following describes the settings options
+| Setting Option | Description |
|---------|------|
-| 应用当前规则 | 选择后,关键字指令(/add,/remove_keyword,/list_keyword等)和替换指令(/replace,/list_replace等)的增删改查导入导出将作用于当前规则 |
-| 是否启用规则 | 选择后,当前规则将被启用,否则将被禁用 |
-| 当前关键字添加模式 | 点击可切换黑/白名单模式,由于黑白名单是分开处理的,需要手动切换,注意,此时关键字的增删改查都和这里的模式有关,如果要使用指令对当前规则的白名单进行增删改查操作,请确保这里的模式是白名单 |
-| 过滤关键字时是否附带发送者名称和ID | 启用后,过滤关键字时会包含发送者名称和ID信息(不会添加到实际消息中),可用于针对特定用户进行过滤 |
-| 处理模式 | 可切换编辑/转发模式。编辑模式下会直接修改原消息;转发模式下会将处理后的消息转发到目标聊天。注意:编辑模式仅适用于你是管理员的且原消息是频道消息或群组中自己发送的消息 |
-| 过滤模式 | 可切换仅黑名单/仅白名单/先黑后白/先白后黑模式。由于黑白名单分开存储,可根据需要选择不同的过滤方式 |
-| 转发模式 | 可切换用户/机器人模式。用户模式下使用用户账号转发消息;机器人模式下使用机器人账号发送消息 |
-| 替换模式 | 启用后将根据已设置的替换规则对消息进行处理 |
-| 消息格式 | 可切换Markdown/HTML格式,在最终发送阶段生效,一般使用默认的Markdown即可 |
-| 预览模式 | 可切换开启/关闭/跟随原消息。开启后会预览消息中的第一个链接,默认跟随原消息的预览状态 |
-| 原始发送者/原始链接/发送时间 | 启用后会在消息发送时添加这些信息,默认关闭,可在"其他设置"菜单中设置自定义模板 |
-| 延时处理 | 启用后会按设定的延迟时间重新获取原消息内容,再开始处理流程,适用于频繁修改消息的频道/群组,可在 config/delay_time.txt 中添加自定义延迟时间 |
-| 删除原始消息 | 启用后会删除原消息,使用前请确认是否有删除权限 |
-| 评论区直达按钮 | 启用后在转发后的消息下发添加评论区直达按钮,前提是原消息有评论区 |
-| 同步到其他规则 | 启用后会同步当前规则的操作到其他规则,除了"是否启用规则"和"开启同步"其他设置都会同步 |
-
-#### 媒体设置说明
-| 设置选项 | 说明 |
+| Apply current rule | After selection, keyword commands (/add, /remove_keyword, /list_keyword, etc.) and replace commands (/replace, /list_replace, etc.) add/remove/modify/query/import/export operations will apply to the current rule |
+| Enable rule | After selection, the current rule will be enabled; otherwise it will be disabled |
+| Current keyword add mode | Click to switch between blacklist/whitelist mode. Since blacklist and whitelist are processed separately, you need to switch manually. Note: keyword add/remove/modify/query operations are related to this mode. To use commands for add/remove/modify/query on the current rule's whitelist, make sure the mode here is set to whitelist |
+| Include sender name and ID when filtering keywords | When enabled, keyword filtering will include sender name and ID information (not added to the actual message), which can be used to filter specific users |
+| Processing mode | Switch between edit/forward mode. In edit mode, the original message is modified directly; in forward mode, the processed message is forwarded to the target chat. Note: edit mode only works when you are an admin and the original message is a channel message or a message you sent in a group |
+| Filter mode | Switch between blacklist only/whitelist only/blacklist then whitelist/whitelist then blacklist modes. Since blacklist and whitelist are stored separately, choose different filtering methods as needed |
+| Forwarding mode | Switch between user/bot mode. In user mode, the user account forwards messages; in bot mode, the bot account sends messages |
+| Replace mode | When enabled, messages will be processed according to configured replace rules |
+| Message format | Switch between Markdown/HTML format, takes effect at the final sending stage. Generally use the default Markdown |
+| Preview mode | Switch between on/off/follow original message. When on, previews the first link in the message. Default follows the original message's preview state |
+| Original sender/Original link/Send time | When enabled, this information is added when sending messages. Default off, custom templates can be set in the "Other settings" menu |
+| Delayed processing | When enabled, the original message content is re-fetched after the set delay time before starting the processing flow. Suitable for channels/groups that frequently edit messages. Custom delay times can be added in config/delay_time.txt |
+| Delete original message | When enabled, the original message is deleted. Please confirm you have deletion permissions before use |
+| Comment section shortcut button | When enabled, a comment section shortcut button is added below the forwarded message, provided the original message has a comment section |
+| Sync to other rules | When enabled, operations on the current rule are synced to other rules. All settings are synced except "Enable rule" and "Enable sync" |
+
+#### Media Settings Description
+| Setting Option | Description |
|---------|------|
-| 媒体类型过滤 | 启用后会过滤掉非选中的媒体类型 |
-| 选择的媒体类型 | 选择要**屏蔽**的媒体类型,注意:Telegram对媒体文件的分类是固定的,主要就是这几种,图片 (photo),文档 (document),视频 (video),音频 (audio),语音 (voice),其中所有不属于图片、视频、音频、语音的文件都会被归类为"文档"类型。比如病毒文件(.exe)、压缩包(.zip)、文本文件(.txt)等,在 Telegram 中都属于"文档"类型。 |
-| 媒体大小过滤 | 启用后会过滤掉超过设置大小的媒体 |
-| 媒体大小限制 | 设置媒体大小限制,单位:MB,可在 config/media_size.txt 中添加自定义大小 |
-| 媒体大小超限时发送提醒 | 启用后媒体超限会发送提醒消息 |
-| 媒体扩展名过滤 | 启用后会过滤掉选中的媒体扩展名 |
-| 媒体扩展名过滤模式 | 切换黑/白名单模式 |
-| 选择的媒体扩展名 | 选择要过滤的的媒体扩展名,可在 config/media_extensions.txt 中添加自定义扩展名 |
-| 放行文本 | 开启后过滤媒体时不会屏蔽整条消息,而是单独转发文本 |
-
-#### 其他设置说明
-
-其他设置菜单中整合了常用的几个指令,使其可以在界面直接交互,包括:
-- 复制规则
-- 复制关键字
-- 复制替换规则
-- 清除关键字
-- 清除替换规则
-- 删除规则
-
-其中清除关键字、清除替换规则、删除规则可以对其他规则生效
-
-同时你可以在这里设置自定义模板,包括:用户信息模板、时间模板、原始链接模板
-| 设置选项 | 说明 |
+| Media type filter | When enabled, non-selected media types will be filtered out |
+| Selected media types | Select media types to **block**. Note: Telegram's classification of media files is fixed, mainly these types: photo, document, video, audio, voice. All files that don't belong to photo, video, audio, or voice categories are classified as "document" type. For example, executable files (.exe), archives (.zip), text files (.txt) are all classified as "document" type in Telegram |
+| Media size filter | When enabled, media exceeding the set size will be filtered out |
+| Media size limit | Set media size limit in MB. Custom sizes can be added in config/media_size.txt |
+| Send notification when media exceeds size limit | When enabled, a notification message is sent when media exceeds the limit |
+| Media extension filter | When enabled, selected media extensions will be filtered out |
+| Media extension filter mode | Switch between blacklist/whitelist mode |
+| Selected media extensions | Select media extensions to filter. Custom extensions can be added in config/media_extensions.txt |
+| Pass through text | When enabled, filtering media won't block the entire message; text will be forwarded separately |
+
+#### Other Settings Description
+
+The other settings menu integrates several commonly used commands for direct UI interaction, including:
+- Copy rule
+- Copy keywords
+- Copy replace rules
+- Clear keywords
+- Clear replace rules
+- Delete rule
+
+Clear keywords, clear replace rules, and delete rule can also be applied to other rules.
+
+You can also set custom templates here, including: user info template, time template, original link template
+| Setting Option | Description |
|---------|------|
-|反转黑名单| 启用后,将把黑名单当成白名单处理,若使用先白后黑模式,黑名单会作为第二重白名单处理|
-|反转白名单| 启用后,将把白名单当成黑名单处理,若使用先白后黑模式,白名单会作为第二重黑名单处理|
+| Invert blacklist | When enabled, the blacklist is treated as a whitelist. In whitelist-then-blacklist mode, the blacklist acts as a second-level whitelist |
+| Invert whitelist | When enabled, the whitelist is treated as a blacklist. In whitelist-then-blacklist mode, the whitelist acts as a second-level blacklist |
-结合“先 X 后 X”模式,可实现双层黑/白名单机制。例如,反转黑名单后,“先白后黑”中的黑名单将变为第二层级的白名单,适用于监听特定用户并筛选其特殊关键词等多种场景。
+Combined with "X then X" modes, a dual-layer blacklist/whitelist mechanism can be achieved. For example, after inverting the blacklist, the blacklist in "whitelist then blacklist" mode becomes a second-level whitelist, suitable for monitoring specific users and filtering their special keywords, among other scenarios.
-### 🤖 AI功能
+### 🤖 AI Features
-项目内置了各大厂商的AI接口,可以帮你:
-- 自动翻译外语内容
-- 定时总结群组消息
-- 智能过滤广告信息
-- 自动为内容打标签
+The project has built-in AI APIs from various major providers that can help you:
+- Automatically translate foreign language content
+- Scheduled group message summaries
+- Intelligently filter advertisements
+- Automatically tag content
....
-
-#### 配置
-1. 在 `.env` 文件中配置你的 AI 接口:
+#### Configuration
+
+1. Configure your AI API in the `.env` file:
```ini
# OpenAI API
OPENAI_API_KEY=your_key
-OPENAI_API_BASE= # 可选,默认官方接口
+OPENAI_API_BASE= # Optional, defaults to official API
# Claude API
CLAUDE_API_KEY=your_key
-# 其他支持的接口...
+# Other supported APIs...
```
-#### 自定义模型
+#### Custom Models
-没找到想要的模型名字?在 `config/ai_models.json` 中添加即可。
+Can't find the model name you want? Add it in `config/ai_models.json`.
-#### AI 处理
+#### AI Processing
-AI处理提示词中可以使用以下格式:
-- `{source_message_context:数字}` - 获取源聊天窗口最新的指定数量消息
-- `{target_message_context:数字}` - 获取目标聊天窗口最新的指定数量消息
-- `{source_message_time:数字}` - 获取源聊天窗口最近指定分钟数的消息
-- `{target_message_time:数字}` - 获取目标聊天窗口最近指定分钟数的消息
+The following formats can be used in AI processing prompts:
+- `{source_message_context:number}` - Get the latest specified number of messages from the source chat window
+- `{target_message_context:number}` - Get the latest specified number of messages from the target chat window
+- `{source_message_time:number}` - Get messages from the source chat window within the specified number of minutes
+- `{target_message_time:number}` - Get messages from the target chat window within the specified number of minutes
-提示词示例:
+Prompt example:
-前置:开启AI处理后再次执行关键词过滤,把“#不转发”添加到过滤关键字中
+Prerequisite: After enabling AI processing, perform keyword filtering again. Add "#donotforward" to the filter keywords.
```
-这是一个资讯整合频道,从多个源获取消息,现在你要判断新资讯是否和历史资讯内容重复了,若重复,则只需要回复“#不转发”,否则请返回新资讯的原文并保持格式。
-记住,你只能返回“#不转发”或者新资讯的原文。
-以下是历史资讯:{target_message_context:10}
-以下是新资讯:
+This is a news aggregation channel that collects messages from multiple sources. You need to determine whether the new article duplicates existing articles. If it's a duplicate, just reply "#donotforward". Otherwise, return the original text of the new article while preserving the format.
+Remember, you can only return "#donotforward" or the original text of the new article.
+Here are the historical articles: {target_message_context:10}
+Here is the new article:
```
-#### 定时总结
+#### Scheduled Summary
-开启定时总结后,机器人会在指定时间(默认每天早上 7 点)自动总结过去 24 小时的消息。
+After enabling scheduled summary, the bot will automatically summarize messages from the past 24 hours at the specified time (default: 7 AM daily).
-- 可在 `config/summary_time.txt` 中添加多个总结时间点
-- 在 `.env` 中设置默认时区
-- 自定义总结的提示词
+- Multiple summary time points can be added in `config/summary_time.txt`
+- Set the default timezone in `.env`
+- Customize the summary prompt
-> 注意:总结功能会消耗较多的 API 额度,请根据需要开启。
+> Note: The summary feature consumes a significant amount of API quota. Please enable it based on your needs.
-### 📢 推送功能
+### 📢 Push Feature
-除了telegram内部消息转发外,项目还集成了Apprise,利用其强大的推送功能,你可以轻松将消息分发至聊天软件、邮件、短信、Webhooks、APIs 等各种平台。
+In addition to internal Telegram message forwarding, the project also integrates Apprise. Leveraging its powerful push capabilities, you can easily distribute messages to chat apps, email, SMS, Webhooks, APIs, and various other platforms.
-| 推送设置主界面 | 推送设置子界面 |
+| Push Settings Main Interface | Push Settings Sub-interface |
|---------|------|
|  |  |
-#### 设置说明
+#### Settings Description
-| 设置选项 | 说明 |
+| Setting Option | Description |
|---------|------|
-| 只转发到推送配置 | 开启后跳过转发过滤器,直接跳到推送过滤器 |
-| 媒体发送方式 | 支持两种模式: - 单个:每个媒体文件单独推送一条消息 - 全部:将所有媒体文件合并到一条消息中推送 具体使用哪种模式取决于目标平台是否支持一次推送多个附件 |
+| Only forward to push configuration | When enabled, skips the forwarding filter and goes directly to the push filter |
+| Media sending method | Supports two modes: - Single: Each media file is pushed as a separate message - All: All media files are combined into one message for pushing Which mode to use depends on whether the target platform supports pushing multiple attachments at once |
-### 如何添加推送配置?
-完整的推送平台列表和配置格式请参考 [Apprise Wiki](https://github.com/caronc/apprise/wiki)
+### How to add push configuration?
+For the complete list of push platforms and configuration formats, refer to [Apprise Wiki](https://github.com/caronc/apprise/wiki)
-**示例:使用 ntfy.sh 推送**
+**Example: Push using ntfy.sh**
-* 假设你想推送到 ntfy.sh 上的一个名为 `my_topic` 的主题。
-* 根据 Apprise Wiki,其格式为 `ntfy://ntfy.sh/你的主题名`。
-* 那么,你需要添加的配置 URL 就是:
+* Suppose you want to push to a topic named `my_topic` on ntfy.sh.
+* According to Apprise Wiki, the format is `ntfy://ntfy.sh/your_topic_name`.
+* The configuration URL you need to add is:
```
ntfy://ntfy.sh/my_topic
```
-## 📰 RSS订阅
+## 📰 RSS Subscription
-项目集成了将Telegram消息转换为RSS Feed的功能,可以轻松地将Telegram频道/群组内容转为标准RSS格式,方便通过RSS阅读器跟踪。
+The project integrates functionality to convert Telegram messages into RSS Feeds, making it easy to convert Telegram channel/group content into standard RSS format for tracking via RSS readers.
-### 启用RSS功能
+### Enable RSS Feature
-1. 在 `.env` 文件中配置RSS相关参数:
+1. Configure RSS related parameters in the `.env` file:
```ini
- # RSS配置
- # 是否启用RSS功能 (true/false)
+ # RSS Configuration
+ # Whether to enable RSS functionality (true/false)
RSS_ENABLED=true
- # RSS基础访问URL,留空则使用默认的访问URL(例如:https://rss.example.com)
+ # RSS base access URL, leave empty to use the default access URL (e.g., https://rss.example.com)
RSS_BASE_URL=
- # RSS媒体文件基础URL,留空则使用默认的访问URL(例如:https://media.example.com)
+ # RSS media file base URL, leave empty to use the default access URL (e.g., https://media.example.com)
RSS_MEDIA_BASE_URL=
```
-2. docker-compose.yml取消注释
+2. Uncomment in docker-compose.yml
```
- # 如果需要使用 RSS 功能,请取消以下注释
+ # If you need to use RSS functionality, uncomment the following
ports:
- 9804:8000
```
-3. 重启服务以启用RSS功能:
+3. Restart the service to enable RSS functionality:
```bash
docker-compose restart
```
-> 注意:旧版本用户需要用新的docker-compose.yml文件重新部署:[docker-compose.yml](./docker-compose.yml)
-### 访问RSS仪表盘
+> Note: Users of older versions need to redeploy with the new docker-compose.yml file: [docker-compose.yml](./docker-compose.yml)
+### Access RSS Dashboard
-浏览器访问 `http://你的服务器地址:9804/`
+Access `http://your_server_address:9804/` in your browser
-### Nginx配置
+### Nginx Configuration
```
location / {
proxy_pass http://127.0.0.1:9804;
@@ -420,138 +420,138 @@ AI处理提示词中可以使用以下格式:
}
```
-### RSS配置管理
+### RSS Configuration Management
-相关界面
+Related interfaces
-| 登录界面 | Dashboard界面 | 新建/编辑配置界面 |
+| Login Interface | Dashboard Interface | Create/Edit Configuration Interface |
|---------|------|------|
|  |  |  |
-### 新建/编辑配置界面说明
-| 设置选项 | 说明 |
+### Create/Edit Configuration Interface Description
+| Setting Option | Description |
|---------|------|
-| 规则ID | 选择现有的一个转发规则,用于生成RSS订阅 |
-| 复制已有配置 | 选择现有的一个RSS配置,复制它的配置到当前表单|
-|订阅源标题| 设置订阅源标题 |
-|自动填充| 点击后自动根据规则的源聊天窗口名字生成订阅源标题 |
-|订阅源描述| 设置订阅源描述 |
-|语言| 占位,暂无特殊功能 |
-|最大条目数| 设置RSS订阅源的最大条目数,默认50,对于媒体比较多的聊天源,请根据硬盘实际硬盘大小设置 |
-|使用 AI 提取标题和内容| 启用后,将使用AI服务自动分析消息,提取标题和内容和整理格式,AI模型请在bot中设置,不受bot中“是否开启 AI 处理”选项影响,此选项开启后和下面所有配置互斥 |
-|AI 提取提示词| 设置AI提取标题和内容的提示词,如需自定义,请务必让AI返回以下json格式内容:`{ "title": "标题", "content": "正文内容" }` |
-|自动提取标题| 启用后,由预设好的正则表达式自动提取标题 |
-|自动提取内容| 启用后,由预设好的正则表达式自动提取内容 |
-|自动将 Markdown 转换为 HTML| 启用后,将使用相关库自动将Telegram中的Markdown格式转换为标准HTML,如需自行处理,请在bot中使用 `/replace` 自行替换 |
-|启用自定义标题提取正则表达式| 启用后,将使用自定义正则表达式提取标题 |
-|启用自定义内容提取正则表达式| 启用后,将使用自定义正则表达式提取内容 |
-|优先级| 设置正则表达式的执行顺序,数字越小优先级越高。系统会按优先级从高到低依次执行正则表达式,**前一个正则表达式提取的结果会作为下一个的输入**,直到完成所有提取 |
-|正则表达式测试| 可用于测试当前正则表达式是否匹配目标文本 |
+| Rule ID | Select an existing forwarding rule to generate RSS subscription |
+| Copy existing configuration | Select an existing RSS configuration and copy its settings to the current form |
+| Subscription source title | Set the subscription source title |
+| Auto-fill | Click to automatically generate a subscription source title based on the rule's source chat window name |
+| Subscription source description | Set the subscription source description |
+| Language | Placeholder, no special function currently |
+| Maximum entries | Set the maximum number of entries for the RSS subscription source, default 50. For chat sources with lots of media, set according to actual disk size |
+| Use AI to extract title and content | When enabled, AI service will automatically analyze messages, extract titles and content, and organize formatting. Set the AI model in the bot; this is not affected by the "Enable AI processing" option in the bot. When this option is enabled, it is mutually exclusive with all configurations below |
+| AI extraction prompt | Set the prompt for AI title and content extraction. If customizing, make sure AI returns content in the following JSON format: `{ "title": "title", "content": "body content" }` |
+| Auto-extract title | When enabled, titles are automatically extracted using preset regular expressions |
+| Auto-extract content | When enabled, content is automatically extracted using preset regular expressions |
+| Auto-convert Markdown to HTML | When enabled, Markdown format in Telegram will be automatically converted to standard HTML using relevant libraries. For manual handling, use `/replace` in the bot for custom replacements |
+| Enable custom title extraction regex | When enabled, custom regular expressions will be used to extract titles |
+| Enable custom content extraction regex | When enabled, custom regular expressions will be used to extract content |
+| Priority | Set the execution order of regular expressions; lower numbers mean higher priority. The system executes regex from highest to lowest priority, where **the result of the previous regex becomes the input for the next one**, until all extractions are complete |
+| Regex test | Can be used to test whether the current regular expression matches the target text |
-### 特殊说明
-- 若只开启自动提取标题,而不开启自动提取内容,则内容会是包含提取了标题的完整的Telegram消息内容
-- 若内容处理选项和正则表达式配置都为空,会自动匹配前20个字符作为标题,内容则为原始消息
+### Special Notes
+- If only auto-extract title is enabled without auto-extract content, the content will be the complete Telegram message including the extracted title
+- If content processing options and regex configurations are both empty, the first 20 characters are automatically matched as the title, and the content is the original message
-### 特殊设置项
-若在.env中开启`RSS_ENABLED=true`,则会在bot的设置中会新增一个`只转发到RSS`的选项,启用后,消息经过各种处理后会在RSS过滤器处理后中断,不会执行转发/编辑
+### Special Settings
+If `RSS_ENABLED=true` is set in .env, a new "Only forward to RSS" option will appear in the bot's settings. When enabled, messages will be interrupted at the RSS filter after going through various processing, and will not execute forwarding/editing
-### 注意事项
+### Notes
-- 没有找回密码功能,请妥善保管你的账号密码
+- There is no password recovery feature; please keep your credentials safe
-## 🎯 特殊功能
+## 🎯 Special Features
-### 🔗 链接转发功能
+### 🔗 Link Forwarding Feature
-向bot发送消息链接,即可把那条消息转发到当前聊天窗口,无视禁止转发和复制的限制(项目自身功能已无视转发和复制限制
+Send a message link to the bot, and it will forward that message to the current chat window, bypassing restrictions on forwarding and copying (the project's own functionality already bypasses forwarding and copying restrictions).
-### 🔄 与通用论坛屏蔽插件联动
+### 🔄 Integration with Universal Forum Blocker Plugin
> https://github.com/heavrnl/universalforumblock
-确保.env文件中已配置相关参数,在已经绑定好的聊天窗口中使用`/ufb_bind <论坛域名>`,即可实现三端联动屏蔽,使用`/ufb_item_change`切换要同步当前域名的主页关键字/主页用户名/内容页关键字/内容页用户名
+Make sure the relevant parameters are configured in the .env file. In a chat window that has already been bound, use `/ufb_bind ` to achieve three-way synchronized blocking. Use `/ufb_item_change` to switch between syncing the current domain's homepage keywords/homepage usernames/content page keywords/content page usernames.
-## 📝 命令列表
+## 📝 Command List
```bash
-命令列表
-
-基础命令
-/start - 开始使用
-/help(/h) - 显示此帮助信息
-
-绑定和设置
-/bind(/b) <源聊天链接或名称> [目标聊天链接或名称] - 绑定源聊天
-/settings(/s) [规则ID] - 管理转发规则
-/changelog(/cl) - 查看更新日志
-
-转发规则管理
-/copy_rule(/cr) <源规则ID> [目标规则ID] - 复制指定规则的所有设置到当前规则或目标规则ID
-/delete_rule(/dr) <规则ID> [规则ID] [规则ID] ... - 删除指定规则
-/list_rule(/lr) - 列出所有转发规则
-
-关键字管理
-/add(/a) <关键字> [关键字] ["关 键 字"] ['关 键 字'] ... - 添加普通关键字
-/add_regex(/ar) <正则表达式> [正则表达式] [正则表达式] ... - 添加正则表达式
-/add_all(/aa) <关键字> [关键字] [关键字] ... - 添加普通关键字到当前频道绑定的所有规则
-/add_regex_all(/ara) <正则表达式> [正则表达式] [正则表达式] ... - 添加正则关键字到所有规则
-/list_keyword(/lk) - 列出所有关键字
-/remove_keyword(/rk) <关键词> ["关 键 字"] ['关 键 字'] ... - 删除关键字
-/remove_keyword_by_id(/rkbi) [ID] [ID] ... - 按ID删除关键字
-/remove_all_keyword(/rak) <关键词> ["关 键 字"] ['关 键 字'] ... - 删除当前频道绑定的所有规则的指定关键字
-/clear_all_keywords(/cak) - 清除当前规则的所有关键字
-/clear_all_keywords_regex(/cakr) - 清除当前规则的所有正则关键字
-/copy_keywords(/ck) <规则ID> - 复制指定规则的关键字到当前规则
-/copy_keywords_regex(/ckr) <规则ID> - 复制指定规则的正则关键字到当前规则
-/copy_replace(/crp) <规则ID> - 复制指定规则的替换规则到当前规则
-/copy_rule(/cr) <规则ID> - 复制指定规则的所有设置到当前规则(包括关键字、正则、替换规则、媒体设置等)
-
-替换规则管理
-/replace(/r) <正则表达式> [替换内容] - 添加替换规则
-/replace_all(/ra) <正则表达式> [替换内容] - 添加替换规则到所有规则
-/list_replace(/lrp) - 列出所有替换规则
-/remove_replace(/rr) <序号> - 删除替换规则
-/clear_all_replace(/car) - 清除当前规则的所有替换规则
-/copy_replace(/crp) <规则ID> - 复制指定规则的替换规则到当前规则
-
-导入导出
-/export_keyword(/ek) - 导出当前规则的关键字
-/export_replace(/er) - 导出当前规则的替换规则
-/import_keyword(/ik) <同时发送文件> - 导入普通关键字
-/import_regex_keyword(/irk) <同时发送文件> - 导入正则关键字
-/import_replace(/ir) <同时发送文件> - 导入替换规则
-
-RSS相关
-/delete_rss_user(/dru) [用户名] - 删除RSS用户
-
-UFB相关
-/ufb_bind(/ub) <域名> - 绑定UFB域名
-/ufb_unbind(/uu) - 解绑UFB域名
-/ufb_item_change(/uic) - 切换UFB同步配置类型
-
-提示
-• 括号内为命令的简写形式
-• 尖括号 <> 表示必填参数
-• 方括号 [] 表示可选参数
-• 导入命令需要同时发送文件
+Command List
+
+Basic Commands
+/start - Get started
+/help(/h) - Show this help information
+
+Binding and Settings
+/bind(/b) [target chat link or name] - Bind source chat
+/settings(/s) [rule ID] - Manage forwarding rules
+/changelog(/cl) - View changelog
+
+Forwarding Rule Management
+/copy_rule(/cr) [target rule ID] - Copy all settings from specified rule to current rule or target rule ID
+/delete_rule(/dr) [rule ID] [rule ID] ... - Delete specified rules
+/list_rule(/lr) - List all forwarding rules
+
+Keyword Management
+/add(/a) [keyword] ["key word"] ['key word'] ... - Add plain keywords
+/add_regex(/ar) [regex] [regex] ... - Add regular expressions
+/add_all(/aa) [keyword] [keyword] ... - Add plain keywords to all rules bound to current channel
+/add_regex_all(/ara) [regex] [regex] ... - Add regex keywords to all rules
+/list_keyword(/lk) - List all keywords
+/remove_keyword(/rk) ["key word"] ['key word'] ... - Remove keywords
+/remove_keyword_by_id(/rkbi) [ID] [ID] ... - Remove keywords by ID
+/remove_all_keyword(/rak) ["key word"] ['key word'] ... - Remove specified keyword from all rules bound to current channel
+/clear_all_keywords(/cak) - Clear all keywords of current rule
+/clear_all_keywords_regex(/cakr) - Clear all regex keywords of current rule
+/copy_keywords(/ck) - Copy keywords from specified rule to current rule
+/copy_keywords_regex(/ckr) - Copy regex keywords from specified rule to current rule
+/copy_replace(/crp) - Copy replace rules from specified rule to current rule
+/copy_rule(/cr) - Copy all settings from specified rule to current rule (including keywords, regex, replace rules, media settings, etc.)
+
+Replace Rule Management
+/replace(/r) [replacement] - Add replace rule
+/replace_all(/ra) [replacement] - Add replace rule to all rules
+/list_replace(/lrp) - List all replace rules
+/remove_replace(/rr) - Remove replace rule
+/clear_all_replace(/car) - Clear all replace rules of current rule
+/copy_replace(/crp) - Copy replace rules from specified rule to current rule
+
+Import/Export
+/export_keyword(/ek) - Export keywords of current rule
+/export_replace(/er) - Export replace rules of current rule
+/import_keyword(/ik) - Import plain keywords
+/import_regex_keyword(/irk) - Import regex keywords
+/import_replace(/ir) - Import replace rules
+
+RSS Related
+/delete_rss_user(/dru) [username] - Delete RSS user
+
+UFB Related
+/ufb_bind(/ub) - Bind UFB domain
+/ufb_unbind(/uu) - Unbind UFB domain
+/ufb_item_change(/uic) - Switch UFB sync configuration type
+
+Tips
+• Content in parentheses is the shorthand form of the command
+• Angle brackets <> indicate required parameters
+• Square brackets [] indicate optional parameters
+• Import commands require attaching a file
```
-## 💐 致谢
+## 💐 Acknowledgments
- [Apprise](https://github.com/caronc/apprise)
- [Telethon](https://github.com/LonamiWebs/Telethon)
-## ☕ 捐赠
+## ☕ Donate
-如果你觉得这个项目对你有帮助,欢迎通过以下方式请我喝杯咖啡:
+If you find this project helpful, feel free to buy me a coffee through the following:
[](https://ko-fi.com/0heavrnl)
-## 📄 开源协议
+## 📄 License
-本项目采用 [GPL-3.0](LICENSE) 开源协议,详细信息请参阅 [LICENSE](LICENSE) 文件。
+This project is licensed under the [GPL-3.0](LICENSE) license. For details, please refer to the [LICENSE](LICENSE) file.
diff --git a/ai/__init__.py b/ai/__init__.py
index bcfb7d6..59ad6f5 100644
--- a/ai/__init__.py
+++ b/ai/__init__.py
@@ -10,23 +10,23 @@
from utils.settings import load_ai_models
from utils.constants import DEFAULT_AI_MODEL
-# 获取日志记录器
+# Get logger
logger = logging.getLogger(__name__)
async def get_ai_provider(model=None):
- """获取AI提供者实例"""
+ """Get AI provider instance"""
if not model:
model = DEFAULT_AI_MODEL
-
- # 加载提供商配置(使用dict格式)
+
+ # Load provider configuration (using dict format)
providers_config = load_ai_models(type="dict")
-
- # 根据模型名称选择对应的提供者
+
+ # Select corresponding provider based on model name
provider = None
-
- # 遍历配置中的每个提供商
+
+ # Iterate through each provider in configuration
for provider_name, models_list in providers_config.items():
- # 检查完全匹配
+ # Check for exact match
if model in models_list:
if provider_name == "openai":
provider = OpenAIProvider()
@@ -41,9 +41,9 @@ async def get_ai_provider(model=None):
elif provider_name == "claude":
provider = ClaudeProvider()
break
-
+
if not provider:
- raise ValueError(f"不支持的模型: {model}")
+ raise ValueError(f"Unsupported model: {model}")
return provider
diff --git a/ai/base.py b/ai/base.py
index 7bd535c..92df68d 100644
--- a/ai/base.py
+++ b/ai/base.py
@@ -2,29 +2,29 @@
from typing import Optional, Dict, Any, List
class BaseAIProvider(ABC):
- """AI提供者的基类"""
-
+ """Base class for AI providers"""
+
@abstractmethod
- async def process_message(self,
- message: str,
+ async def process_message(self,
+ message: str,
prompt: Optional[str] = None,
images: Optional[List[Dict[str, str]]] = None,
**kwargs) -> str:
"""
- 处理消息的抽象方法
-
+ Abstract method for processing messages
+
Args:
- message: 要处理的消息内容
- prompt: 可选的提示词
- images: 可选的图片列表,每个图片是一个字典,包含data和mime_type
- **kwargs: 其他参数
-
+ message: The message content to process
+ prompt: Optional prompt text
+ images: Optional list of images, each image is a dict containing data and mime_type
+ **kwargs: Other parameters
+
Returns:
- str: 处理后的消息
+ str: The processed message
"""
pass
-
+
@abstractmethod
async def initialize(self, **kwargs) -> None:
- """初始化AI提供者"""
+ """Initialize AI provider"""
pass
\ No newline at end of file
diff --git a/ai/claude_provider.py b/ai/claude_provider.py
index dfcabbe..e4dd6b9 100644
--- a/ai/claude_provider.py
+++ b/ai/claude_provider.py
@@ -11,54 +11,54 @@ def __init__(self):
self.client = None
self.model = None
self.default_model = 'claude-3-5-sonnet-latest'
-
+
async def initialize(self, **kwargs):
- """初始化Claude客户端"""
+ """Initialize Claude client"""
api_key = os.getenv('CLAUDE_API_KEY')
if not api_key:
- raise ValueError("未设置CLAUDE_API_KEY环境变量")
-
- # 检查是否配置了自定义API基础URL
+ raise ValueError("CLAUDE_API_KEY environment variable is not set")
+
+ # Check if a custom API base URL is configured
api_base = os.getenv('CLAUDE_API_BASE', '').strip()
if api_base:
- logger.info(f"使用自定义Claude API基础URL: {api_base}")
+ logger.info(f"Using custom Claude API base URL: {api_base}")
self.client = anthropic.Anthropic(
api_key=api_key,
base_url=api_base
)
else:
- # 使用默认URL
+ # Use default URL
self.client = anthropic.Anthropic(api_key=api_key)
-
+
self.model = kwargs.get('model', self.default_model)
-
- async def process_message(self,
- message: str,
+
+ async def process_message(self,
+ message: str,
prompt: Optional[str] = None,
images: Optional[List[Dict[str, str]]] = None,
**kwargs) -> str:
- """处理消息"""
+ """Process message"""
try:
if not self.client:
await self.initialize(**kwargs)
-
- # 构建消息列表
+
+ # Build message list
messages = []
if prompt:
messages.append({"role": "system", "content": prompt})
-
- # 如果有图片,需要添加到消息中
+
+ # If there are images, add them to the message
if images and len(images) > 0:
- # 构建包含图片的内容列表
+ # Build content list containing images
content = []
-
- # 添加文本
+
+ # Add text
content.append({
"type": "text",
"text": message
})
-
- # 添加每张图片
+
+ # Add each image
for img in images:
content.append({
"type": "image",
@@ -68,27 +68,27 @@ async def process_message(self,
"data": img["data"]
}
})
- logger.info(f"已添加一张类型为 {img['mime_type']} 的图片,大小约 {len(img['data']) // 1000} KB")
-
- # 添加用户消息
+ logger.info(f"Added an image of type {img['mime_type']}, size approximately {len(img['data']) // 1000} KB")
+
+ # Add user message
messages.append({"role": "user", "content": content})
else:
- # 没有图片,只添加文本
+ # No images, only add text
messages.append({"role": "user", "content": message})
-
- # 使用流式输出 - 按照官方文档正确实现
+
+ # Use streaming output - correctly implemented per official documentation
with self.client.messages.stream(
model=self.model,
max_tokens=4096,
messages=messages
) as stream:
- # 使用专用的text_stream迭代器直接获取文本
+ # Use dedicated text_stream iterator to directly get text
full_response = ""
for text in stream.text_stream:
full_response += text
-
+
return full_response
-
+
except Exception as e:
- logger.error(f"Claude API 调用失败: {str(e)}")
- return f"AI处理失败: {str(e)}"
\ No newline at end of file
+ logger.error(f"Claude API call failed: {str(e)}")
+ return f"AI processing failed: {str(e)}"
diff --git a/ai/gemini_provider.py b/ai/gemini_provider.py
index 8d6e32f..f4bcba9 100644
--- a/ai/gemini_provider.py
+++ b/ai/gemini_provider.py
@@ -1,6 +1,6 @@
from typing import Optional, List, Dict
import google.generativeai as genai
-# 移除对不存在的模块的导入
+# Remove import of non-existent module
# from google.genai import types
from .base import BaseAIProvider
from .openai_base_provider import OpenAIBaseProvider
@@ -11,46 +11,46 @@
logger = logging.getLogger(__name__)
class GeminiOpenAIProvider(OpenAIBaseProvider):
- """使用OpenAI兼容接口的Gemini提供者"""
+ """Gemini provider using OpenAI-compatible interface"""
def __init__(self):
super().__init__(
env_prefix='GEMINI',
default_model='gemini-pro',
- default_api_base='' # API_BASE必须在环境变量中提供
+ default_api_base='' # API_BASE must be provided in environment variables
)
class GeminiProvider(BaseAIProvider):
def __init__(self):
self.model = None
- self.model_name = None # 添加model_name属性
+ self.model_name = None # Add model_name attribute
self.provider = None
-
+
async def initialize(self, **kwargs):
- """初始化Gemini客户端"""
- # 检查是否配置了GEMINI_API_BASE,如果有则使用兼容OpenAI的接口
+ """Initialize Gemini client"""
+ # Check if GEMINI_API_BASE is configured, if so use OpenAI-compatible interface
api_base = os.getenv('GEMINI_API_BASE', '').strip()
-
+
if api_base:
- logger.info(f"检测到GEMINI_API_BASE环境变量: {api_base},使用兼容OpenAI的接口")
+ logger.info(f"Detected GEMINI_API_BASE environment variable: {api_base}, using OpenAI-compatible interface")
self.provider = GeminiOpenAIProvider()
await self.provider.initialize(**kwargs)
return
-
- # 原来的Gemini API初始化代码
+
+ # Original Gemini API initialization code
api_key = os.getenv('GEMINI_API_KEY')
if not api_key:
- raise ValueError("未设置GEMINI_API_KEY环境变量")
+ raise ValueError("GEMINI_API_KEY environment variable is not set")
- # 使用传入的model参数,如果没有才使用默认值
- if not self.model_name: # 如果model_name还没设置
+ # Use the passed model parameter, only use default if not provided
+ if not self.model_name: # If model_name is not set yet
self.model_name = kwargs.get('model')
-
- if not self.model_name: # 如果kwargs中也没有model
- self.model_name = 'gemini-pro' # 最后才使用默认值
-
- logger.info(f"初始化Gemini模型: {self.model_name}")
-
- # 配置安全设置 - 只使用基本类别
+
+ if not self.model_name: # If model is not in kwargs either
+ self.model_name = 'gemini-pro' # Only then use default value
+
+ logger.info(f"Initializing Gemini model: {self.model_name}")
+
+ # Configure safety settings - only use basic categories
safety_settings = [
{
"category": "HARM_CATEGORY_HARASSMENT",
@@ -69,87 +69,87 @@ async def initialize(self, **kwargs):
"threshold": "BLOCK_NONE"
}
]
-
+
genai.configure(api_key=api_key)
- # 使用self.model_name初始化模型
+ # Initialize model using self.model_name
self.model = genai.GenerativeModel(
model_name=self.model_name,
safety_settings=safety_settings
)
-
- async def process_message(self,
- message: str,
+
+ async def process_message(self,
+ message: str,
prompt: Optional[str] = None,
images: Optional[List[Dict[str, str]]] = None,
**kwargs) -> str:
- """处理消息"""
+ """Process message"""
try:
if not self.provider and not self.model:
await self.initialize(**kwargs)
-
- # 如果使用的是OpenAI兼容接口,则调用该接口的处理方法
+
+ # If using OpenAI-compatible interface, call its processing method
if self.provider:
return await self.provider.process_message(message, prompt, images, **kwargs)
-
- # 使用Gemini API的流式处理
- logger.info(f"实际使用的Gemini模型: {self.model_name}")
- # 组合提示词和消息
+ # Stream processing using Gemini API
+ logger.info(f"Actual Gemini model in use: {self.model_name}")
+
+ # Combine prompt and message
if prompt:
user_message = f"{prompt}\n\n{message}"
else:
user_message = message
-
- # 检查是否有图片
+
+ # Check if there are images
if images and len(images) > 0:
try:
- # 使用MultimodalContent添加图片
+ # Use MultimodalContent to add images
contents = []
- # 添加文本
+ # Add text
contents.append({"role": "user", "parts": [{"text": user_message}]})
-
- # 对每张图片进行处理
+
+ # Process each image
for img in images:
try:
- # 直接添加图片字节到模型的输入
+ # Directly add image bytes to model input
image_part = {
"inline_data": {
"mime_type": img["mime_type"],
- "data": img["data"] # 使用原始base64数据
+ "data": img["data"] # Use original base64 data
}
}
contents[0]["parts"].append(image_part)
- logger.info(f"已添加一张类型为 {img['mime_type']} 的图片,大小约 {len(img['data']) // 1000} KB")
+ logger.info(f"Added an image of type {img['mime_type']}, size approximately {len(img['data']) // 1000} KB")
except Exception as img_error:
- logger.error(f"处理单张图片时出错: {str(img_error)}")
-
- # 使用流式输出 - 不设置额外参数,使用默认值
+ logger.error(f"Error processing a single image: {str(img_error)}")
+
+ # Use streaming output - no extra parameters, use defaults
response_stream = self.model.generate_content(
contents,
stream=True
)
except Exception as e:
- logger.error(f"Gemini处理带图片消息时出错: {str(e)}")
- # 如果处理图片失败,尝试只用文本
+ logger.error(f"Error processing message with images in Gemini: {str(e)}")
+ # If image processing fails, try with text only
response_stream = self.model.generate_content(
[{"role": "user", "parts": [{"text": user_message}]}],
stream=True
)
else:
- # 无图片,使用流式输出
+ # No images, use streaming output
response_stream = self.model.generate_content(
[{"role": "user", "parts": [{"text": user_message}]}],
stream=True
)
-
- # 收集完整响应
+
+ # Collect complete response
full_response = ""
for chunk in response_stream:
if hasattr(chunk, 'text'):
full_response += chunk.text
-
+
return full_response
-
+
except Exception as e:
- logger.error(f"Gemini处理消息时出错: {str(e)}")
- return f"AI处理失败: {str(e)}"
\ No newline at end of file
+ logger.error(f"Error processing message in Gemini: {str(e)}")
+ return f"AI processing failed: {str(e)}"
diff --git a/ai/openai_base_provider.py b/ai/openai_base_provider.py
index fc34dc1..57b8454 100644
--- a/ai/openai_base_provider.py
+++ b/ai/openai_base_provider.py
@@ -10,12 +10,12 @@ class OpenAIBaseProvider(BaseAIProvider):
def __init__(self, env_prefix: str = 'OPENAI', default_model: str = 'gpt-4o-mini',
default_api_base: str = 'https://api.openai.com/v1'):
"""
- 初始化基础OpenAI格式提供者
+ Initialize base OpenAI-format provider
Args:
- env_prefix: 环境变量前缀,如 'OPENAI', 'GROK', 'DEEPSEEK', 'QWEN'
- default_model: 默认模型名称
- default_api_base: 默认API基础URL
+ env_prefix: Environment variable prefix, e.g. 'OPENAI', 'GROK', 'DEEPSEEK', 'QWEN'
+ default_model: Default model name
+ default_api_base: Default API base URL
"""
super().__init__()
self.env_prefix = env_prefix
@@ -25,11 +25,11 @@ def __init__(self, env_prefix: str = 'OPENAI', default_model: str = 'gpt-4o-mini
self.model = None
async def initialize(self, **kwargs) -> None:
- """初始化OpenAI客户端"""
+ """Initialize OpenAI client"""
try:
api_key = os.getenv(f'{self.env_prefix}_API_KEY')
if not api_key:
- raise ValueError(f"未设置 {self.env_prefix}_API_KEY 环境变量")
+ raise ValueError(f"{self.env_prefix}_API_KEY environment variable is not set")
api_base = os.getenv(f'{self.env_prefix}_API_BASE', '').strip() or self.default_api_base
@@ -39,10 +39,10 @@ async def initialize(self, **kwargs) -> None:
)
self.model = kwargs.get('model', self.default_model)
- logger.info(f"初始化OpenAI模型: {self.model}")
+ logger.info(f"Initializing OpenAI model: {self.model}")
except Exception as e:
- error_msg = f"初始化 {self.env_prefix} 客户端时出错: {str(e)}"
+ error_msg = f"Error initializing {self.env_prefix} client: {str(e)}"
logger.error(error_msg, exc_info=True)
raise
@@ -51,7 +51,7 @@ async def process_message(self,
prompt: Optional[str] = None,
images: Optional[List[Dict[str, str]]] = None,
**kwargs) -> str:
- """处理消息"""
+ """Process message"""
try:
if not self.client:
await self.initialize(**kwargs)
@@ -60,18 +60,18 @@ async def process_message(self,
if prompt:
messages.append({"role": "system", "content": prompt})
- # 如果有图片,需要添加到消息中
+ # If there are images, add them to the message
if images and len(images) > 0:
- # 创建包含文本和图片的内容数组
+ # Create content array containing text and images
content = []
- # 添加文本
+ # Add text
content.append({
"type": "text",
"text": message
})
- # 添加每张图片
+ # Add each image
for img in images:
content.append({
"type": "image_url",
@@ -79,23 +79,23 @@ async def process_message(self,
"url": f"data:{img['mime_type']};base64,{img['data']}"
}
})
- logger.info(f"已添加一张类型为 {img['mime_type']} 的图片,大小约 {len(img['data']) // 1000} KB")
+ logger.info(f"Added an image of type {img['mime_type']}, size approximately {len(img['data']) // 1000} KB")
messages.append({"role": "user", "content": content})
else:
- # 没有图片,只添加文本
+ # No images, only add text
messages.append({"role": "user", "content": message})
- logger.info(f"实际使用的OpenAI模型: {self.model}")
+ logger.info(f"Actual OpenAI model in use: {self.model}")
- # 所有模型统一使用流式调用
+ # All models use streaming calls uniformly
completion = await self.client.chat.completions.create(
model=self.model,
messages=messages,
stream=True
)
- # 收集所有内容
+ # Collect all content
collected_content = ""
collected_reasoning = ""
@@ -105,21 +105,21 @@ async def process_message(self,
delta = chunk.choices[0].delta
- # 处理思考内容(如果存在)
+ # Process thinking content (if present)
if hasattr(delta, 'reasoning_content') and delta.reasoning_content is not None:
collected_reasoning += delta.reasoning_content
- # 处理回答内容
+ # Process response content
if hasattr(delta, 'content') and delta.content is not None:
collected_content += delta.content
- # 如果没有内容但有思考过程,可能是思考模型只返回了思考过程
+ # If no content but has thinking process, the thinking model may have only returned the thinking process
if not collected_content and collected_reasoning:
- logger.warning("模型只返回了思考过程,没有最终回答")
- return "模型未能生成有效回答"
+ logger.warning("Model only returned thinking process, no final answer")
+ return "Model failed to generate a valid answer"
return collected_content
except Exception as e:
- logger.error(f"{self.env_prefix} API 调用失败: {str(e)}", exc_info=True)
- return f"AI处理失败: {str(e)}"
+ logger.error(f"{self.env_prefix} API call failed: {str(e)}", exc_info=True)
+ return f"AI processing failed: {str(e)}"
diff --git a/ai/openai_provider.py b/ai/openai_provider.py
index fba9eb1..92b4633 100644
--- a/ai/openai_provider.py
+++ b/ai/openai_provider.py
@@ -15,32 +15,32 @@ def __init__(self):
default_api_base='https://api.openai.com/v1'
)
- async def process_message(self,
- message: str,
+ async def process_message(self,
+ message: str,
prompt: Optional[str] = None,
images: Optional[List[Dict[str, str]]] = None,
**kwargs) -> str:
- """处理消息"""
+ """Process message"""
try:
if not self.client:
await self.initialize(**kwargs)
-
+
messages = []
if prompt:
messages.append({"role": "system", "content": prompt})
-
- # 如果有图片,需要添加到消息中
+
+ # If there are images, add them to the message
if images and len(images) > 0:
- # 创建包含文本和图片的内容数组
+ # Create content array containing text and images
content = []
-
- # 添加文本
+
+ # Add text
content.append({
"type": "text",
"text": message
})
-
- # 添加每张图片
+
+ # Add each image
for img in images:
content.append({
"type": "image_url",
@@ -48,19 +48,19 @@ async def process_message(self,
"url": f"data:{img['mime_type']};base64,{img['data']}"
}
})
-
+
messages.append({"role": "user", "content": content})
else:
- # 没有图片,只添加文本
+ # No images, only add text
messages.append({"role": "user", "content": message})
-
+
response = await self.client.chat.completions.create(
model=self.model,
messages=messages
)
-
+
return response.choices[0].message.content
-
+
except Exception as e:
- logger.error(f"OpenAI处理消息时出错: {str(e)}", exc_info=True)
- return f"AI处理失败: {str(e)}"
\ No newline at end of file
+ logger.error(f"Error processing message in OpenAI: {str(e)}", exc_info=True)
+ return f"AI processing failed: {str(e)}"
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index 758803a..55b543a 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -2,7 +2,7 @@ services:
telegram-forwarder:
image: heavrnl/telegramforwarder:latest
container_name: telegram-forwarder
- # 如果需要使用 RSS 功能,请取消以下注释
+ # If you need to use RSS functionality, uncomment the following
# ports:
# - 9804:8000
restart: unless-stopped
diff --git a/enums/enums.py b/enums/enums.py
index 2dffe48..36040e8 100644
--- a/enums/enums.py
+++ b/enums/enums.py
@@ -1,6 +1,6 @@
import enum
-# 四个模式,仅黑名单,仅白名单,先黑名单后白名单,先白名单后黑名单
+# Four modes: blacklist only, whitelist only, blacklist then whitelist, whitelist then blacklist
class ForwardMode(enum.Enum):
WHITELIST = 'whitelist'
BLACKLIST = 'blacklist'
@@ -11,7 +11,7 @@ class ForwardMode(enum.Enum):
class PreviewMode(enum.Enum):
ON = 'on'
OFF = 'off'
- FOLLOW = 'follow' # 跟随原消息的预览设置
+ FOLLOW = 'follow' # Follow the original message's preview setting
class MessageMode(enum.Enum):
MARKDOWN = 'Markdown'
diff --git a/filters/ai_filter.py b/filters/ai_filter.py
index e67af97..f20a5e1 100644
--- a/filters/ai_filter.py
+++ b/filters/ai_filter.py
@@ -17,18 +17,18 @@
class AIFilter(BaseFilter):
"""
- AI处理过滤器,使用AI处理消息文本
+ AI processing filter, uses AI to process message text
"""
-
+
async def _process(self, context):
"""
- 使用AI处理消息文本
-
+ Use AI to process message text
+
Args:
- context: 消息上下文
-
+ context: Message context
+
Returns:
- bool: 是否继续处理
+ bool: Whether to continue processing
"""
rule = context.rule
message_text = context.message_text
@@ -37,294 +37,294 @@ async def _process(self, context):
try:
if not rule.is_ai:
- logger.info("AI处理未开启,返回原始消息")
+ logger.info("AI processing not enabled, returning original message")
return True
- # 处理媒体组消息
+ # Process media group messages
if context.is_media_group:
logger.info(f"is_media_group: {context.is_media_group}")
-
- # 获取需要上传的图片文件
+
+ # Get image files that need to be uploaded
image_files = []
has_media_to_process = False
-
+
if rule.enable_ai_upload_image:
- # 检查是否有已下载的媒体文件
+ # Check if there are already downloaded media files
if context.media_files:
- # 已经下载好的文件,需要读取到内存
+ # Files already downloaded, need to read into memory
for file_path in context.media_files:
try:
- # 检查文件是否存在
+ # Check if file exists
if not os.path.exists(file_path):
- logger.warning(f"文件不存在: {file_path}")
+ logger.warning(f"File does not exist: {file_path}")
continue
-
- # 读取文件内容
+
+ # Read file content
with open(file_path, 'rb') as f:
file_content = f.read()
-
- # 获取MIME类型
+
+ # Get MIME type
mime_type = mimetypes.guess_type(file_path)[0] or "image/jpeg"
-
- # 保存到内存图片列表
+
+ # Save to in-memory image list
image_files.append({
"data": base64.b64encode(file_content).decode('utf-8'),
"mime_type": mime_type
})
- logger.info(f"已加载图片到内存,类型: {mime_type},大小: {len(file_content) // 1024} KB")
+ logger.info(f"Loaded image into memory, type: {mime_type}, size: {len(file_content) // 1024} KB")
except Exception as e:
- logger.error(f"读取文件到内存时出错: {str(e)}")
-
+ logger.error(f"Error reading file into memory: {str(e)}")
+
has_media_to_process = len(image_files) > 0
- logger.info(f"已加载 {len(image_files)} 个文件到内存")
-
- # 如果没有已下载的文件,但有媒体组消息,则直接下载到内存
+ logger.info(f"Loaded {len(image_files)} files into memory")
+
+ # If no downloaded files but there are media group messages, download directly to memory
elif context.is_media_group and context.media_group_messages:
- logger.info(f"检测到媒体组消息: {len(context.media_group_messages)}条,直接下载到内存")
- # 下载媒体组中的图片到内存
+ logger.info(f"Detected media group messages: {len(context.media_group_messages)} messages, downloading directly to memory")
+ # Download images from the media group to memory
for msg in context.media_group_messages:
if msg.photo or (msg.document and hasattr(msg.document, 'mime_type') and msg.document.mime_type.startswith('image/')):
try:
- # 创建内存缓冲区
+ # Create memory buffer
buffer = io.BytesIO()
- # 直接下载到内存缓冲区
+ # Download directly to memory buffer
await msg.download_media(file=buffer)
- # 获取图片内容
+ # Get image content
buffer.seek(0)
content = buffer.read()
-
- # 获取MIME类型
- mime_type = "image/jpeg" # 默认类型
+
+ # Get MIME type
+ mime_type = "image/jpeg" # Default type
if msg.photo:
mime_type = "image/jpeg"
elif msg.document and hasattr(msg.document, 'mime_type'):
mime_type = msg.document.mime_type
-
- # 保存到内存图片列表
+
+ # Save to in-memory image list
image_files.append({
"data": base64.b64encode(content).decode('utf-8'),
"mime_type": mime_type
})
- logger.info(f"已下载媒体组图片到内存,类型: {mime_type},大小: {len(content) // 1024} KB")
+ logger.info(f"Downloaded media group image to memory, type: {mime_type}, size: {len(content) // 1024} KB")
except Exception as e:
- logger.error(f"下载媒体组图片到内存时出错: {str(e)}")
-
+ logger.error(f"Error downloading media group image to memory: {str(e)}")
+
has_media_to_process = len(image_files) > 0
- logger.info(f"共下载了 {len(image_files)} 张图片到内存")
-
- # 检查单条消息是否有媒体并下载到内存
+ logger.info(f"Downloaded {len(image_files)} images to memory in total")
+
+ # Check if single message has media and download to memory
elif event.message and event.message.media:
- logger.info("检测到单条消息有媒体,下载到内存")
+ logger.info("Detected single message has media, downloading to memory")
try:
- # 创建内存缓冲区
+ # Create memory buffer
buffer = io.BytesIO()
- # 直接下载到内存
+ # Download directly to memory
await event.message.download_media(file=buffer)
- # 获取图片内容
+ # Get image content
buffer.seek(0)
content = buffer.read()
-
- # 获取MIME类型
- mime_type = "image/jpeg" # 默认类型
+
+ # Get MIME type
+ mime_type = "image/jpeg" # Default type
if hasattr(event.message.media, 'photo'):
mime_type = "image/jpeg"
elif hasattr(event.message.media, 'document') and hasattr(event.message.media.document, 'mime_type'):
mime_type = event.message.media.document.mime_type
-
- # 保存到内存图片列表
+
+ # Save to in-memory image list
image_files.append({
"data": base64.b64encode(content).decode('utf-8'),
"mime_type": mime_type
})
has_media_to_process = True
- logger.info(f"已下载单条消息媒体到内存,类型: {mime_type},大小: {len(content) // 1024} KB")
+ logger.info(f"Downloaded single message media to memory, type: {mime_type}, size: {len(content) // 1024} KB")
except Exception as e:
- logger.error(f"下载单条消息媒体到内存时出错: {str(e)}")
-
- # 如果有消息文本或图片,使用AI处理
+ logger.error(f"Error downloading single message media to memory: {str(e)}")
+
+ # If there is message text or images, use AI to process
if context.message_text or has_media_to_process:
try:
- # 确保即使没有文本也能处理图片
- text_to_process = context.message_text if context.message_text else "[图片消息]"
-
- logger.info(f"开始AI处理,文本长度: {len(text_to_process)},图片数量: {len(image_files)}")
+ # Ensure images can be processed even without text
+ text_to_process = context.message_text if context.message_text else "[Image message]"
+
+ logger.info(f"Starting AI processing, text length: {len(text_to_process)}, image count: {len(image_files)}")
processed_text = await _ai_handle(text_to_process, rule, image_files)
context.message_text = processed_text
-
- # 如果需要在AI处理后再次检查关键字
+
+ # If keyword check after AI processing is needed
logger.info(f"rule.is_keyword_after_ai:{rule.is_keyword_after_ai}")
if rule.is_keyword_after_ai:
should_forward = await check_keywords(rule, processed_text, event)
-
+
if not should_forward:
- logger.info('AI处理后的文本未通过关键字检查,取消转发')
+ logger.info('Text after AI processing did not pass keyword check, canceling forwarding')
context.should_forward = False
return False
except Exception as e:
- logger.error(f'AI处理消息时出错: {str(e)}')
- context.errors.append(f"AI处理错误: {str(e)}")
- # 即使AI处理失败,仍然继续处理
- return True
+ logger.error(f'Error during AI message processing: {str(e)}')
+ context.errors.append(f"AI processing error: {str(e)}")
+ # Even if AI processing fails, continue processing
+ return True
finally:
pass
async def _ai_handle(message: str, rule, image_files=None) -> str:
- """使用AI处理消息
-
+ """Use AI to process message
+
Args:
- message: 原始消息文本
- rule: 转发规则对象,包含AI相关设置
- image_files: 需要上传的图片文件路径列表或内存中的图片数据
-
+ message: Original message text
+ rule: Forwarding rule object, contains AI-related settings
+ image_files: List of image file paths to upload or in-memory image data
+
Returns:
- str: 处理后的消息文本
+ str: Processed message text
"""
try:
if not rule.is_ai:
- logger.info("AI处理未开启,返回原始消息")
+ logger.info("AI processing not enabled, returning original message")
return message
- # 先读取数据库,如果ai模型为空,则使用.env中的默认模型
+ # First read the database, if ai model is empty, use the default model from .env
if not rule.ai_model:
rule.ai_model = DEFAULT_AI_MODEL
- logger.info(f"使用默认AI模型: {rule.ai_model}")
+ logger.info(f"Using default AI model: {rule.ai_model}")
else:
- logger.info(f"使用规则配置的AI模型: {rule.ai_model}")
-
+ logger.info(f"Using rule-configured AI model: {rule.ai_model}")
+
provider = await get_ai_provider(rule.ai_model)
-
+
if not rule.ai_prompt:
rule.ai_prompt = DEFAULT_AI_PROMPT
- logger.info("使用默认AI提示词")
+ logger.info("Using default AI prompt")
else:
- logger.info("使用规则配置的AI提示词")
-
- # 处理特殊提示词格式
+ logger.info("Using rule-configured AI prompt")
+
+ # Process special prompt format
prompt = rule.ai_prompt
if prompt:
- # 处理聊天记录提示词
-
- # 匹配源聊天和目标聊天的context格式
+ # Process chat history prompt
+
+ # Match source chat and target chat context format
source_context_match = re.search(r'\{source_message_context:(\d+)\}', prompt)
target_context_match = re.search(r'\{target_message_context:(\d+)\}', prompt)
- # 匹配源聊天和目标聊天的time格式
+ # Match source chat and target chat time format
source_time_match = re.search(r'\{source_message_time:(\d+)\}', prompt)
target_time_match = re.search(r'\{target_message_time:(\d+)\}', prompt)
-
+
if any([source_context_match, target_context_match, source_time_match, target_time_match]):
-
+
main = await get_main_module()
client = main.user_client
-
- # 获取源聊天和目标聊天ID
+
+ # Get source chat and target chat IDs
source_chat_id = int(rule.source_chat.telegram_chat_id)
target_chat_id = int(rule.target_chat.telegram_chat_id)
-
- # 处理源聊天的消息获取
+
+ # Process message retrieval for source chat
if source_context_match:
count = int(source_context_match.group(1))
chat_history = await _get_chat_messages(client, source_chat_id, count=count)
prompt = prompt.replace(source_context_match.group(0), chat_history)
-
+
if source_time_match:
minutes = int(source_time_match.group(1))
chat_history = await _get_chat_messages(client, source_chat_id, minutes=minutes)
prompt = prompt.replace(source_time_match.group(0), chat_history)
-
- # 处理目标聊天的消息获取
+
+ # Process message retrieval for target chat
if target_context_match:
count = int(target_context_match.group(1))
chat_history = await _get_chat_messages(client, target_chat_id, count=count)
prompt = prompt.replace(target_context_match.group(0), chat_history)
-
+
if target_time_match:
minutes = int(target_time_match.group(1))
chat_history = await _get_chat_messages(client, target_chat_id, minutes=minutes)
prompt = prompt.replace(target_time_match.group(0), chat_history)
-
- # 替换消息占位符
+
+ # Replace message placeholder
if '{Message}' in prompt:
prompt = prompt.replace('{Message}', message)
-
- logger.info(f"处理后的AI提示词: {prompt}")
-
- # 处理图片上传 - 新版本,支持内存中的图片数据
+
+ logger.info(f"Processed AI prompt: {prompt}")
+
+ # Process image upload - new version, supports in-memory image data
img_data = []
if rule.enable_ai_upload_image and image_files and len(image_files) > 0:
- # 检查图片是否已经是内存格式
+ # Check if images are already in memory format
if isinstance(image_files[0], dict) and "data" in image_files[0] and "mime_type" in image_files[0]:
- # 已经是内存格式,直接使用
+ # Already in memory format, use directly
img_data = image_files
- logger.info(f"使用内存中的图片数据,共有 {len(img_data)} 张图片")
+ logger.info(f"Using in-memory image data, total {len(img_data)} images")
else:
- # 文件路径格式,需要读取文件
+ # File path format, need to read files
for img_file in image_files:
try:
- logger.info("准备从文件读取图片")
+ logger.info("Preparing to read image from file")
with open(img_file, "rb") as f:
img_bytes = f.read()
encoded_img = base64.b64encode(img_bytes).decode('utf-8')
-
- # 获取MIME类型
- mime_type = "image/jpeg" # 默认类型
+
+ # Get MIME type
+ mime_type = "image/jpeg" # Default type
if str(img_file).lower().endswith(".png"):
mime_type = "image/png"
elif str(img_file).lower().endswith(".gif"):
mime_type = "image/gif"
elif str(img_file).lower().endswith(".webp"):
mime_type = "image/webp"
-
+
img_data.append({
"data": encoded_img,
"mime_type": mime_type
})
- # 记录图片大小而不是内容
- logger.info(f"已读取图片,类型: {mime_type},大小: {len(img_bytes) // 1024} KB")
+ # Log image size instead of content
+ logger.info(f"Read image, type: {mime_type}, size: {len(img_bytes) // 1024} KB")
except Exception as e:
- logger.error("读取图片文件时出错")
-
- logger.info(f"共有 {len(img_data)} 张图片将上传到AI")
-
+ logger.error("Error reading image file")
+
+ logger.info(f"Total {len(img_data)} images will be uploaded to AI")
+
processed_text = await provider.process_message(
message=message,
prompt=prompt,
model=rule.ai_model,
images=img_data if img_data else None
)
- logger.info(f"AI处理完成: {processed_text}")
+ logger.info(f"AI processing completed: {processed_text}")
return processed_text
-
+
except Exception as e:
- logger.error(f"AI处理消息时出错: {str(e)}")
+ logger.error(f"Error during AI message processing: {str(e)}")
return message
async def _get_chat_messages(client, chat_id, minutes=None, count=None, delay_seconds: float = 0.5) -> str:
- """获取聊天记录
-
+ """Get chat history
+
Args:
- client: Telegram客户端
- chat_id: 聊天ID
- minutes: 获取最近几分钟的消息
- count: 获取最新的几条消息
- delay_seconds: 每条消息获取之间的延迟秒数,默认0.5秒
-
+ client: Telegram client
+ chat_id: Chat ID
+ minutes: Get messages from the last N minutes
+ count: Get the latest N messages
+ delay_seconds: Delay in seconds between fetching each message, default 0.5 seconds
+
Returns:
- str: 聊天记录文本
+ str: Chat history text
"""
try:
messages = []
- limit = count if count else 500 # 设置一个合理的默认值
+ limit = count if count else 500 # Set a reasonable default value
processed_count = 0
-
+
if minutes:
- # 计算时间范围
-
+ # Calculate time range
+
end_time = datetime.now()
start_time = end_time - timedelta(minutes=minutes)
-
- # 获取指定时间范围内的消息
+
+ # Get messages within the specified time range
async for message in client.iter_messages(
chat_id,
limit=limit,
@@ -336,10 +336,10 @@ async def _get_chat_messages(client, chat_id, minutes=None, count=None, delay_se
if message.text:
messages.append(message.text)
processed_count += 1
- if processed_count % 20 == 0: # 每处理20条消息休息一次
+ if processed_count % 20 == 0: # Rest after processing every 20 messages
await asyncio.sleep(delay_seconds)
else:
- # 获取指定数量的最新消息
+ # Get the specified number of latest messages
async for message in client.iter_messages(
chat_id,
limit=count
@@ -347,11 +347,11 @@ async def _get_chat_messages(client, chat_id, minutes=None, count=None, delay_se
if message.text:
messages.append(message.text)
processed_count += 1
- if processed_count % 20 == 0: # 每处理20条消息休息一次
+ if processed_count % 20 == 0: # Rest after processing every 20 messages
await asyncio.sleep(delay_seconds)
-
+
return "\n---\n".join(messages) if messages else ""
-
+
except Exception as e:
- logger.error(f"获取聊天记录时出错: {str(e)}")
- return ""
\ No newline at end of file
+ logger.error(f"Error getting chat history: {str(e)}")
+ return ""
diff --git a/filters/base_filter.py b/filters/base_filter.py
index 2b24b26..d7e9e55 100644
--- a/filters/base_filter.py
+++ b/filters/base_filter.py
@@ -5,42 +5,42 @@
class BaseFilter(ABC):
"""
- 基础过滤器类,定义过滤器接口
+ Base filter class, defines the filter interface
"""
-
+
def __init__(self, name=None):
"""
- 初始化过滤器
-
+ Initialize filter
+
Args:
- name: 过滤器名称,如果为None则使用类名
+ name: Filter name, uses class name if None
"""
self.name = name or self.__class__.__name__
-
+
async def process(self, context):
"""
- 处理消息上下文
-
+ Process message context
+
Args:
- context: 包含消息处理所需所有信息的上下文对象
-
+ context: Context object containing all information needed for message processing
+
Returns:
- bool: 表示是否应该继续处理消息
+ bool: Indicates whether the message should continue processing
"""
- logger.debug(f"开始执行过滤器: {self.name}")
+ logger.debug(f"Starting filter execution: {self.name}")
result = await self._process(context)
- logger.debug(f"过滤器 {self.name} 处理结果: {'通过' if result else '不通过'}")
+ logger.debug(f"Filter {self.name} processing result: {'passed' if result else 'not passed'}")
return result
-
+
@abstractmethod
async def _process(self, context):
"""
- 具体的处理逻辑,子类需要实现
-
+ Actual processing logic, must be implemented by subclasses
+
Args:
- context: 包含消息处理所需所有信息的上下文对象
-
+ context: Context object containing all information needed for message processing
+
Returns:
- bool: 表示是否应该继续处理消息
+ bool: Indicates whether the message should continue processing
"""
- pass
\ No newline at end of file
+ pass
diff --git a/filters/comment_button_filter.py b/filters/comment_button_filter.py
index 83bc382..3c8f05c 100644
--- a/filters/comment_button_filter.py
+++ b/filters/comment_button_filter.py
@@ -13,255 +13,254 @@
class CommentButtonFilter(BaseFilter):
"""
- 评论区按钮过滤器,用于在消息中添加指向关联群组消息的按钮
+ Comment section button filter, used to add buttons pointing to associated group messages in messages
"""
-
+
async def _process(self, context):
"""
- 为消息添加评论区按钮
-
+ Add comment section button to messages
+
Args:
- context: 消息上下文
-
+ context: Message context
+
Returns:
- bool: 是否继续处理
+ bool: Whether to continue processing
"""
if context.rule.only_rss:
- logger.info('只转发到RSS,跳过评论区按钮过滤器')
+ logger.info('Only forwarding to RSS, skipping comment section button filter')
return True
-
- # logger.info(f"CommentButtonFilter处理消息前,context: {context.__dict__}")
+
+ # logger.info(f"Before CommentButtonFilter processing, context: {context.__dict__}")
try:
- # 如果规则不存在或未启用评论按钮功能,直接跳过
+ # If the rule doesn't exist or comment button feature is not enabled, skip directly
if not context.rule or not context.rule.enable_comment_button:
return True
-
- # 如果消息内容为空,直接跳过
+
+ # If message content is empty, skip directly
if not context.original_message_text and not context.event.message.media:
return True
-
+
try:
- # 获取用户客户端而不是Bot客户端
+ # Get user client instead of Bot client
main = await get_main_module()
client = main.user_client if (main and hasattr(main, 'user_client')) else context.client
-
+
event = context.event
-
- # 获取原始频道实体
+
+ # Get original channel entity
channel_entity = await client.get_entity(event.chat_id)
-
- # 获取频道的真实用户名
+
+ # Get channel's actual username
channel_username = None
- # logger.info(f"获取频道实体: {channel_entity}")
- # logger.info(f"频道属性内容: {channel_entity.__dict__}")
+ # logger.info(f"Got channel entity: {channel_entity}")
+ # logger.info(f"Channel attribute content: {channel_entity.__dict__}")
if hasattr(channel_entity, 'username') and channel_entity.username:
channel_username = channel_entity.username
- logger.info(f"获取到频道用户名: {channel_username}")
+ logger.info(f"Got channel username: {channel_username}")
elif hasattr(channel_entity, 'usernames') and channel_entity.usernames:
- # 获取第一个活跃的用户名
+ # Get the first active username
for username_obj in channel_entity.usernames:
if username_obj.active:
channel_username = username_obj.username
- logger.info(f"从 usernames 列表获取到频道用户名: {channel_username}")
+ logger.info(f"Got channel username from usernames list: {channel_username}")
break
-
- # 获取频道ID(去除前缀)
+
+ # Get channel ID (remove prefix)
channel_id_str = str(channel_entity.id)
if channel_id_str.startswith('-100'):
channel_id_str = channel_id_str[4:]
elif channel_id_str.startswith('100'):
channel_id_str = channel_id_str[3:]
-
- logger.info(f"处理频道ID: {channel_id_str}")
-
- # 只处理频道消息
+
+ logger.info(f"Processing channel ID: {channel_id_str}")
+
+ # Only process channel messages
if not hasattr(channel_entity, 'broadcast') or not channel_entity.broadcast:
return True
-
- # 获取关联群组ID
+
+ # Get associated group ID
try:
- # 获取频道完整信息
+ # Get full channel information
full_channel = await client(GetFullChannelRequest(channel_entity))
-
- # 检查是否有关联群组
+
+ # Check if there is an associated group
if not full_channel.full_chat.linked_chat_id:
- logger.info(f"频道 {channel_entity.id} 没有关联群组,跳过添加评论按钮")
+ logger.info(f"Channel {channel_entity.id} has no associated group, skipping comment button addition")
return True
-
+
linked_group_id = full_channel.full_chat.linked_chat_id
-
- # 获取关联群组实体
+
+ # Get associated group entity
linked_group = await client.get_entity(linked_group_id)
-
- # 检查消息是否属于媒体组
+
+ # Check if the message belongs to a media group
channel_msg_id = event.message.id
-
+
if hasattr(event.message, 'grouped_id') and event.message.grouped_id:
- logger.info(f"检测到媒体组消息,组ID: {event.message.grouped_id}")
- # 获取同一媒体组的所有消息
+ logger.info(f"Detected media group message, group ID: {event.message.grouped_id}")
+ # Get all messages in the same media group
media_group_messages = []
-
+
try:
- # 获取频道历史消息
+ # Get channel history messages
async for message in client.iter_messages(
channel_entity,
- limit=20, # 限制查询消息数量
- offset_date=event.message.date, # 从当前消息时间开始查询
- reverse=False # 从新到旧
+ limit=20, # Limit query message count
+ offset_date=event.message.date, # Start query from current message time
+ reverse=False # From newest to oldest
):
- # 检查是否属于同一媒体组
- if (hasattr(message, 'grouped_id') and
+ # Check if it belongs to the same media group
+ if (hasattr(message, 'grouped_id') and
message.grouped_id == event.message.grouped_id):
media_group_messages.append(message)
-
+
if media_group_messages:
- # 找出ID最小的消息
+ # Find the message with the smallest ID
min_id_message = min(media_group_messages, key=lambda x: x.id)
channel_msg_id = min_id_message.id
- logger.info(f"使用媒体组中ID最小的消息: {channel_msg_id}")
+ logger.info(f"Using message with smallest ID in media group: {channel_msg_id}")
except Exception as e:
- logger.error(f"获取媒体组消息失败: {e}")
- # 失败时使用原始消息ID
- logger.info(f"使用原始消息ID: {channel_msg_id}")
-
- # 添加短暂延迟,等待消息同步完成
- logger.info("等待2秒,确保消息同步完成...")
+ logger.error(f"Failed to get media group messages: {e}")
+ # Use original message ID on failure
+ logger.info(f"Using original message ID: {channel_msg_id}")
+
+ # Add brief delay to ensure message sync is complete
+ logger.info("Waiting 2 seconds to ensure message sync is complete...")
await asyncio.sleep(2)
-
- # 构建评论区链接 - 不依赖于匹配群组消息
+
+ # Build comment section link - does not depend on matching group messages
comment_link = None
if channel_username:
- # 公开频道 - 使用用户名链接
+ # Public channel - use username link
comment_link = f"https://t.me/{channel_username}/{channel_msg_id}?comment=1"
- logger.info(f"构建公开频道评论区链接: {comment_link}")
+ logger.info(f"Built public channel comment section link: {comment_link}")
else:
- # 私有频道 - 使用ID链接
+ # Private channel - use ID link
comment_link = f"https://t.me/c/{channel_id_str}/{channel_msg_id}?comment=1"
- logger.info(f"构建私有频道评论区链接: {comment_link}")
-
+ logger.info(f"Built private channel comment section link: {comment_link}")
+
-
- # 如果可以获取群组消息,尝试找到精确匹配以提供更好的体验
+ # If group messages can be obtained, try to find exact match for better experience
try:
- # 查找关联群组中对应的消息 - 使用用户客户端
- logger.info(f"尝试使用用户客户端获取群组 {linked_group_id} 的消息")
+ # Find corresponding message in associated group - use user client
+ logger.info(f"Trying to use user client to get messages from group {linked_group_id}")
group_messages = await client.get_messages(linked_group, limit=5)
- logger.info(f"成功获取关联群组 {linked_group_id} 的 {len(group_messages)} 条消息")
-
- # 尝试查找内容相同的消息
+ logger.info(f"Successfully got {len(group_messages)} messages from associated group {linked_group_id}")
+
+ # Try to find a message with matching content
matched_msg = None
-
- # 1. 先尝试完全匹配内容
+
+ # 1. First try exact content match
original_message = context.original_message_text
if original_message:
- logger.info(f"尝试查找内容完全匹配的消息,原始内容长度: {len(original_message)}")
-
+ logger.info(f"Trying to find exact content match, original content length: {len(original_message)}")
+
for msg in group_messages:
if hasattr(msg, 'message') and msg.message and msg.message == original_message:
matched_msg = msg
- logger.info(f"找到完全匹配消息: 群组消息ID {msg.id}")
+ logger.info(f"Found exact match: group message ID {msg.id}")
break
-
- # 2. 如果无法完全匹配,尝试使用SequenceMatcher进行前20字符相似度匹配
+
+ # 2. If exact match fails, try using SequenceMatcher for first 20 characters similarity match
if not matched_msg and original_message and len(original_message) > 20:
-
+
message_start = original_message[:20]
- logger.info(f"尝试对前20字符进行相似度匹配: '{message_start}'")
-
+ logger.info(f"Trying similarity match on first 20 characters: '{message_start}'")
+
for msg in group_messages:
if hasattr(msg, 'message') and msg.message and len(msg.message) > 20:
msg_start = msg.message[:20]
similarity = SequenceMatcher(None, message_start, msg_start).ratio()
if similarity > 0.75:
matched_msg = msg
- logger.info(f"找到相似度匹配消息: 群组消息ID {msg.id}, 前20字符相似度: {similarity}")
+ logger.info(f"Found similarity match: group message ID {msg.id}, first 20 characters similarity: {similarity}")
break
-
- # 3. 如果没找到匹配消息,尝试基于时间匹配
+
+ # 3. If no match found, try time-based matching
if not matched_msg and hasattr(event.message, 'date'):
message_time = event.message.date
- logger.info(f"尝试基于时间匹配,原消息时间: {message_time}")
-
- # 获取消息时间前后1分钟内的消息
- time_window = 1 # 分钟
-
+ logger.info(f"Trying time-based matching, original message time: {message_time}")
+
+ # Get messages within 1 minute before and after the message time
+ time_window = 1 # minutes
+
for msg in group_messages:
if hasattr(msg, 'date'):
time_diff = abs((msg.date - message_time).total_seconds())
if time_diff < time_window * 60:
matched_msg = msg
- logger.info(f"找到时间接近的消息: 群组消息ID {msg.id}, 时间差: {time_diff}秒")
+ logger.info(f"Found time-proximate message: group message ID {msg.id}, time difference: {time_diff} seconds")
break
-
- # 4. 如果仍未找到,使用最新消息
+
+ # 4. If still not found, use the latest message
if not matched_msg:
- logger.info("未找到匹配消息,尝试使用最新消息")
- # 使用最新消息作为默认值
+ logger.info("No matching message found, trying to use the latest message")
+ # Use the latest message as default
if group_messages:
matched_msg = group_messages[0]
- logger.info(f"使用最新消息: 群组消息ID {matched_msg.id}")
-
- # 如果找到了匹配消息,更新链接
+ logger.info(f"Using latest message: group message ID {matched_msg.id}")
+
+ # If a matching message was found, update the link
if matched_msg:
group_msg_id = matched_msg.id
if channel_username:
- # 公开频道 - 使用用户名链接
+ # Public channel - use username link
comment_link = f"https://t.me/{channel_username}/{channel_msg_id}?comment={group_msg_id}"
else:
- # 私有频道 - 使用ID链接
+ # Private channel - use ID link
comment_link = f"https://t.me/c/{channel_id_str}/{channel_msg_id}?comment={group_msg_id}"
- logger.info(f"更新为精确评论区链接: {comment_link}")
-
+ logger.info(f"Updated to precise comment section link: {comment_link}")
+
except Exception as e:
- logger.warning(f"获取群组消息失败,可能是因为未加入群组: {str(e)}")
- logger.info("将使用基本评论区链接")
- # 保持使用基本的comment=1链接
-
- # 创建群组备用链接
+ logger.warning(f"Failed to get group messages, possibly because not joined the group: {str(e)}")
+ logger.info("Will use basic comment section link")
+ # Keep using the basic comment=1 link
+
+ # Create group backup link
group_link = None
if hasattr(linked_group, 'username') and linked_group.username:
group_link = f"https://t.me/{linked_group.username}"
- logger.info(f"生成群组备用链接: {group_link}")
+ logger.info(f"Generated group backup link: {group_link}")
- # 将评论区链接保存到context中,供后续过滤器使用
+ # Save comment section link to context for subsequent filters to use
context.comment_link = comment_link
-
- # 如果是媒体组消息,跳过添加按钮(由ReplyFilter处理)
+
+ # If it's a media group message, skip adding button (handled by ReplyFilter)
if context.is_media_group:
- logger.info("媒体组消息的评论区按钮将由ReplyFilter处理")
+ logger.info("Media group message comment section button will be handled by ReplyFilter")
return True
-
- # 添加按钮
+
+ # Add buttons
buttons_added = False
-
- # 添加评论区按钮
+
+ # Add comment section button
if comment_link:
- # 创建评论区按钮
- comment_button = Button.url("💬 查看评论区", comment_link)
-
- # 将按钮添加到消息中
+ # Create comment section button
+ comment_button = Button.url("💬 View comments", comment_link)
+
+ # Add button to message
if not context.buttons:
context.buttons = [[comment_button]]
else:
- # 如果已经有按钮,添加到第一行
+ # If there are already buttons, add to the first row
context.buttons.insert(0, [comment_button])
-
- logger.info(f"为消息添加了评论区按钮,链接: {comment_link}")
+
+ logger.info(f"Added comment section button to message, link: {comment_link}")
buttons_added = True
-
-
+
+
if not buttons_added:
- logger.warning("未能添加任何按钮")
+ logger.warning("Failed to add any buttons")
except Exception as e:
- logger.error(f"获取关联群组消息时出错: {str(e)}")
+ logger.error(f"Error getting associated group messages: {str(e)}")
tb = traceback.format_exc()
- logger.debug(f"详细错误信息: {tb}")
-
+ logger.debug(f"Detailed error info: {tb}")
+
except Exception as e:
- logger.error(f"添加评论区按钮时出错: {str(e)}")
+ logger.error(f"Error adding comment section button: {str(e)}")
logger.error(traceback.format_exc())
-
- return True
+
+ return True
finally:
- # logger.info(f"CommentButtonFilter处理消息后,context: {context.__dict__}")
- pass
\ No newline at end of file
+ # logger.info(f"After CommentButtonFilter processing, context: {context.__dict__}")
+ pass
diff --git a/filters/context.py b/filters/context.py
index eedc507..701b605 100644
--- a/filters/context.py
+++ b/filters/context.py
@@ -2,68 +2,68 @@
class MessageContext:
"""
- 消息上下文类,包含处理消息所需的所有信息
+ Message context class, contains all information needed for processing messages
"""
-
+
def __init__(self, client, event, chat_id, rule):
"""
- 初始化消息上下文
-
+ Initialize message context
+
Args:
- client: 机器人客户端
- event: 消息事件
- chat_id: 聊天ID
- rule: 转发规则
+ client: Bot client
+ event: Message event
+ chat_id: Chat ID
+ rule: Forwarding rule
"""
self.client = client
self.event = event
self.chat_id = chat_id
self.rule = rule
-
- # 初始消息文本,保持不变用于引用
+
+ # Initial message text, kept unchanged for reference
self.original_message_text = event.message.text or ''
-
- # 当前处理的消息文本
+
+ # Current processed message text
self.message_text = event.message.text or ''
-
- # 用于检查的消息文本(可能包含发送者信息等)
+
+ # Message text for checking (may include sender info, etc.)
self.check_message_text = event.message.text or ''
-
- # 记录处理过程中的媒体文件
+
+ # Record media files during processing
self.media_files = []
-
- # 记录发送者信息
+
+ # Record sender information
self.sender_info = ''
-
- # 记录时间信息
+
+ # Record time information
self.time_info = ''
-
- # 原始链接
+
+ # Original link
self.original_link = ''
-
- # 按钮
+
+ # Buttons
self.buttons = event.message.buttons if hasattr(event.message, 'buttons') else None
-
- # 是否继续处理
+
+ # Whether to continue processing
self.should_forward = True
-
- # 用于记录媒体组消息
+
+ # Used to record media group messages
self.is_media_group = event.message.grouped_id is not None
self.media_group_id = event.message.grouped_id
self.media_group_messages = []
-
- # 用于跟踪被跳过的超大媒体
+
+ # Used to track skipped oversized media
self.skipped_media = []
-
- # 记录任何可能的错误
+
+ # Record any possible errors
self.errors = []
-
- # 记录已转发的消息
+
+ # Record forwarded messages
self.forwarded_messages = []
-
- # 评论区链接
+
+ # Comment section link
self.comment_link = None
-
+
def clone(self):
- """创建上下文的副本"""
- return copy.deepcopy(self)
\ No newline at end of file
+ """Create a copy of the context"""
+ return copy.deepcopy(self)
diff --git a/filters/delay_filter.py b/filters/delay_filter.py
index 08b02ef..96b4b98 100644
--- a/filters/delay_filter.py
+++ b/filters/delay_filter.py
@@ -7,89 +7,89 @@
class DelayFilter(BaseFilter):
"""
- 延迟过滤器,等待消息可能的编辑后再处理
-
- 有些频道在发送消息后会有自己的机器人对消息进行编辑,
- 添加引用、标注等内容。此过滤器会等待一段时间后,
- 重新获取消息的最新内容再进行处理。
+ Delay filter, waits for possible message edits before processing
+
+ Some channels have their own bots that edit messages after sending,
+ adding quotes, annotations, etc. This filter waits for a period of time,
+ then re-fetches the latest message content for processing.
"""
-
+
async def _process(self, context):
"""
- 根据规则配置,决定是否等待并获取最新的消息内容
-
+ Based on rule configuration, decide whether to wait and get the latest message content
+
Args:
- context: 消息上下文
-
+ context: Message context
+
Returns:
- bool: 是否继续处理
+ bool: Whether to continue processing
"""
rule = context.rule
message = context.event
-
- # 如果规则未启用延迟处理或延迟秒数为0,则直接通过
+
+ # If the rule does not have delay processing enabled or delay seconds is 0, pass directly
if not rule.enable_delay or rule.delay_seconds <= 0:
- logger.debug(f"[规则ID:{rule.id}] 延迟处理未启用或延迟秒数为0,跳过延迟处理")
+ logger.debug(f"[Rule ID:{rule.id}] Delay processing not enabled or delay seconds is 0, skipping delay processing")
return True
-
- # 如果消息不完整,则直接通过
+
+ # If the message is incomplete, pass directly
if not message or not hasattr(message, "chat_id") or not hasattr(message, "id"):
- logger.debug(f"[规则ID:{rule.id}] 消息不完整,无法应用延迟处理")
+ logger.debug(f"[Rule ID:{rule.id}] Message is incomplete, cannot apply delay processing")
return True
-
+
try:
original_id = message.id
chat_id = message.chat_id
-
- logger.info(f"[规则ID:{rule.id}] 延迟处理消息 {original_id},等待 {rule.delay_seconds} 秒...")
-
- # 等待指定的秒数
+
+ logger.info(f"[Rule ID:{rule.id}] Delaying message {original_id}, waiting {rule.delay_seconds} seconds...")
+
+ # Wait for the specified number of seconds
await asyncio.sleep(rule.delay_seconds)
- logger.info(f"[规则ID:{rule.id}] 延迟 {rule.delay_seconds} 秒结束,正在获取最新消息...")
-
- # 尝试获取用户客户端
+ logger.info(f"[Rule ID:{rule.id}] Delay of {rule.delay_seconds} seconds ended, fetching latest message...")
+
+ # Try to get user client
try:
main = await get_main_module()
client = main.user_client if (main and hasattr(main, 'user_client')) else context.client
-
- # 获取更新后的消息
- logger.info(f"[规则ID:{rule.id}] 正在获取聊天 {chat_id} 的消息 {original_id}...")
+
+ # Get the updated message
+ logger.info(f"[Rule ID:{rule.id}] Fetching message {original_id} from chat {chat_id}...")
updated_message = await client.get_messages(chat_id, ids=original_id)
-
+
if updated_message:
updated_text = getattr(updated_message, "text", "")
-
- # 不管消息内容是否有变化,都更新上下文中的所有相关字段
- logger.info(f"[规则ID:{rule.id}] 正在更新上下文中的消息数据...")
-
- # 更新上下文中的消息文本相关字段
+
+ # Regardless of whether message content has changed, update all related fields in context
+ logger.info(f"[Rule ID:{rule.id}] Updating message data in context...")
+
+ # Update message text related fields in context
context.message_text = updated_text
context.check_message_text = updated_text
-
- # 更新事件中的消息对象
+
+ # Update message object in event
context.event.message = updated_message
-
- # 更新其他相关字段
+
+ # Update other related fields
context.original_message_text = updated_text
context.buttons = updated_message.buttons if hasattr(updated_message, 'buttons') else None
-
- # 更新媒体相关信息
+
+ # Update media related information
if hasattr(updated_message, 'media') and updated_message.media:
context.is_media_group = updated_message.grouped_id is not None
context.media_group_id = updated_message.grouped_id
-
- logger.info(f"[规则ID:{rule.id}] 上下文消息数据已更新完成")
+
+ logger.info(f"[Rule ID:{rule.id}] Context message data update completed")
else:
- logger.warning(f"[规则ID:{rule.id}] 无法获取更新的消息,使用原始消息")
+ logger.warning(f"[Rule ID:{rule.id}] Unable to get updated message, using original message")
except Exception as e:
- logger.warning(f"[规则ID:{rule.id}] 获取更新消息时出错: {str(e)}")
- # 继续使用原始消息
-
- logger.info(f"[规则ID:{rule.id}] 延迟处理完成,继续后续过滤器")
+ logger.warning(f"[Rule ID:{rule.id}] Error getting updated message: {str(e)}")
+ # Continue using original message
+
+ logger.info(f"[Rule ID:{rule.id}] Delay processing completed, continuing with subsequent filters")
return True
-
+
except Exception as e:
- logger.error(f"[规则ID:{rule.id}] 延迟处理消息时出现错误: {str(e)}")
- return True
+ logger.error(f"[Rule ID:{rule.id}] Error during delay processing of message: {str(e)}")
+ return True
diff --git a/filters/delete_original_filter.py b/filters/delete_original_filter.py
index e382808..7db95e5 100644
--- a/filters/delete_original_filter.py
+++ b/filters/delete_original_filter.py
@@ -6,34 +6,34 @@
class DeleteOriginalFilter(BaseFilter):
"""
- 删除原始消息过滤器,处理转发后是否要删除原始消息
+ Delete original message filter, handles whether to delete the original message after forwarding
"""
-
+
async def _process(self, context):
"""
- 处理是否删除原始消息
-
+ Handle whether to delete the original message
+
Args:
- context: 消息上下文
-
+ context: Message context
+
Returns:
- bool: 是否继续处理
+ bool: Whether to continue processing
"""
rule = context.rule
event = context.event
-
- # 如果不需要删除原始消息,直接返回
+
+ # If deletion of original message is not needed, return directly
if not rule.is_delete_original:
return True
-
+
try:
- # 获取 main.py 中的用户客户端
+ # Get user client from main.py
main = await get_main_module()
- user_client = main.user_client # 获取用户客户端
-
- # 媒体组消息
+ user_client = main.user_client # Get user client
+
+ # Media group messages
if event.message.grouped_id:
- # 使用用户客户端获取并删除媒体组消息
+ # Use user client to get and delete media group messages
async for message in user_client.iter_messages(
event.chat_id,
min_id=event.message.id - 10,
@@ -42,15 +42,15 @@ async def _process(self, context):
):
if message.grouped_id == event.message.grouped_id:
await message.delete()
- logger.info(f'已删除媒体组消息 ID: {message.id}')
+ logger.info(f'Deleted media group message ID: {message.id}')
else:
- # 单条消息的删除逻辑
+ # Single message deletion logic
message = await user_client.get_messages(event.chat_id, ids=event.message.id)
await message.delete()
- logger.info(f'已删除原始消息 ID: {event.message.id}')
-
+ logger.info(f'Deleted original message ID: {event.message.id}')
+
return True
except Exception as e:
- logger.error(f'删除原始消息时出错: {str(e)}')
- context.errors.append(f"删除原始消息错误: {str(e)}")
- return True # 即使删除失败,也继续处理
\ No newline at end of file
+ logger.error(f'Error deleting original message: {str(e)}')
+ context.errors.append(f"Error deleting original message: {str(e)}")
+ return True # Even if deletion fails, continue processing
diff --git a/filters/edit_filter.py b/filters/edit_filter.py
index 1f2cc6d..9afd104 100644
--- a/filters/edit_filter.py
+++ b/filters/edit_filter.py
@@ -10,86 +10,86 @@
class EditFilter(BaseFilter):
"""
- 编辑过滤器,用于在编辑模式下修改原始消息
- 仅在频道消息中生效
+ Edit filter, used to modify original messages in edit mode.
+ Only effective for channel messages.
"""
-
+
async def _process(self, context):
"""
- 处理消息编辑
-
+ Process message editing
+
Args:
- context: 消息上下文
-
+ context: Message context
+
Returns:
- bool: 是否继续处理
+ bool: Whether to continue processing
"""
rule = context.rule
event = context.event
-
- logger.debug(f"开始处理编辑过滤器,消息ID: {event.message.id}, 聊天ID: {event.chat_id}")
-
- # 如果不是编辑模式,继续后续处理
+
+ logger.debug(f"Starting edit filter processing, message ID: {event.message.id}, chat ID: {event.chat_id}")
+
+ # If not in edit mode, continue with subsequent processing
if rule.handle_mode != HandleMode.EDIT:
- logger.debug(f"当前规则非编辑模式 (当前模式: {rule.handle_mode}),跳过编辑处理")
+ logger.debug(f"Current rule is not in edit mode (current mode: {rule.handle_mode}), skipping edit processing")
return True
-
- # 检查是否为频道消息
+
+ # Check if it's a channel message
chat = await event.get_chat()
- logger.debug(f"聊天类型: {type(chat).__name__}, 聊天ID: {chat.id}, 聊天标题: {getattr(chat, 'title', '未知')}")
-
+ logger.debug(f"Chat type: {type(chat).__name__}, chat ID: {chat.id}, chat title: {getattr(chat, 'title', 'unknown')}")
+
if not isinstance(chat, Channel):
- logger.info(f"不是频道消息 (聊天类型: {type(chat).__name__}),跳过编辑")
+ logger.info(f"Not a channel message (chat type: {type(chat).__name__}), skipping edit")
return False
-
+
try:
- # 获取用户客户端
- logger.debug("尝试获取用户客户端")
+ # Get user client
+ logger.debug("Trying to get user client")
main = await get_main_module()
user_client = main.user_client if (main and hasattr(main, 'user_client')) else None
-
+
if not user_client:
- logger.error("无法获取用户客户端,无法执行编辑操作")
+ logger.error("Unable to get user client, cannot perform edit operation")
return False
-
- logger.debug("成功获取用户客户端")
-
- # 根据预览模式设置 link_preview
+
+ logger.debug("Successfully got user client")
+
+ # Set link_preview based on preview mode
link_preview = {
PreviewMode.ON: True,
PreviewMode.OFF: False,
- PreviewMode.FOLLOW: event.message.media is not None # 跟随原消息
+ PreviewMode.FOLLOW: event.message.media is not None # Follow original message
}[rule.is_preview]
-
- logger.debug(f"预览模式: {rule.is_preview}, link_preview值: {link_preview}")
-
- # 组合消息文本
+
+ logger.debug(f"Preview mode: {rule.is_preview}, link_preview value: {link_preview}")
+
+ # Combine message text
message_text = context.sender_info + context.message_text + context.time_info + context.original_link
-
- logger.debug(f"原始消息文本: '{event.message.text}'")
- logger.debug(f"新消息文本: '{message_text}'")
-
- # 检查文本是否有变化
+
+ logger.debug(f"Original message text: '{event.message.text}'")
+ logger.debug(f"New message text: '{message_text}'")
+
+ # Check if text has changed
if message_text == event.message.text:
- logger.info("消息文本没有变化,跳过编辑")
+ logger.info("Message text has not changed, skipping edit")
return False
-
- # 处理媒体组消息
+
+ # Process media group messages
if context.is_media_group:
- logger.info(f"处理媒体组消息,媒体组ID: {context.media_group_id}, 消息数量: {len(context.media_group_messages) if context.media_group_messages else '未知'}")
- # 尝试编辑媒体组中的每条消息
+ logger.info(f"Processing media group message, media group ID: {context.media_group_id}, message count: {len(context.media_group_messages) if context.media_group_messages else 'unknown'}")
+ # Try to edit each message in the media group
if not context.media_group_messages:
- logger.warning("媒体组消息列表为空,无法编辑")
+ logger.warning("Media group message list is empty, cannot edit")
return False
-
+
for message in context.media_group_messages:
try:
- # 只在第一条消息上添加文本
+ # Only add text to the first message
text_to_edit = message_text if message.id == event.message.id else ""
- logger.debug(f"尝试编辑媒体组消息 {message.id}, 媒体类型: {type(message.media).__name__ if message.media else '无媒体'}")
-
+ logger.debug(f"Trying to edit media group message {message.id}, media type: {type(message.media).__name__ if message.media else 'no media'}")
+
await user_client.edit_message(
event.chat_id,
message.id,
@@ -97,21 +97,21 @@ async def _process(self, context):
parse_mode=rule.message_mode.value,
link_preview=link_preview
)
- logger.info(f"成功编辑媒体组消息 {message.id}")
+ logger.info(f"Successfully edited media group message {message.id}")
except Exception as e:
error_details = str(e)
if "was not modified" not in error_details:
- logger.error(f"编辑媒体组消息 {message.id} 失败: {error_details}")
- logger.debug(f"异常详情: {traceback.format_exc()}")
+ logger.error(f"Failed to edit media group message {message.id}: {error_details}")
+ logger.debug(f"Exception details: {traceback.format_exc()}")
else:
- logger.debug(f"媒体组消息 {message.id} 内容未修改,无需编辑")
+ logger.debug(f"Media group message {message.id} content not modified, no edit needed")
return False
- # 处理所有其他消息(包括单条媒体消息和纯文本消息)
+ # Process all other messages (including single media messages and plain text messages)
else:
try:
- logger.debug(f"尝试编辑单条消息 {event.message.id}, 消息类型: {type(event.message).__name__}, 媒体类型: {type(event.message.media).__name__ if event.message.media else '无媒体'}")
- logger.debug(f"使用解析模式: {rule.message_mode.value}")
-
+ logger.debug(f"Trying to edit single message {event.message.id}, message type: {type(event.message).__name__}, media type: {type(event.message.media).__name__ if event.message.media else 'no media'}")
+ logger.debug(f"Using parse mode: {rule.message_mode.value}")
+
await user_client.edit_message(
event.chat_id,
event.message.id,
@@ -119,21 +119,21 @@ async def _process(self, context):
parse_mode=rule.message_mode.value,
link_preview=link_preview
)
- logger.info(f"成功编辑消息 {event.message.id}")
+ logger.info(f"Successfully edited message {event.message.id}")
return False
except Exception as e:
error_details = str(e)
if "was not modified" not in error_details:
- logger.error(f"编辑消息 {event.message.id} 失败: {error_details}")
- logger.debug(f"尝试编辑的消息ID: {event.message.id}, 聊天ID: {event.chat_id}")
- logger.debug(f"消息文本长度: {len(message_text)}, 解析模式: {rule.message_mode.value}")
- logger.debug(f"异常详情: {traceback.format_exc()}")
+ logger.error(f"Failed to edit message {event.message.id}: {error_details}")
+ logger.debug(f"Attempted to edit message ID: {event.message.id}, chat ID: {event.chat_id}")
+ logger.debug(f"Message text length: {len(message_text)}, parse mode: {rule.message_mode.value}")
+ logger.debug(f"Exception details: {traceback.format_exc()}")
else:
- logger.debug(f"消息 {event.message.id} 内容未修改,无需编辑")
+ logger.debug(f"Message {event.message.id} content not modified, no edit needed")
return False
-
+
except Exception as e:
- logger.error(f"编辑过滤器处理出错: {str(e)}")
- logger.debug(f"异常详情: {traceback.format_exc()}")
- logger.debug(f"上下文信息 - 消息ID: {event.message.id}, 聊天ID: {event.chat_id}, 规则ID: {rule.id if hasattr(rule, 'id') else '未知'}")
- return False
\ No newline at end of file
+ logger.error(f"Edit filter processing error: {str(e)}")
+ logger.debug(f"Exception details: {traceback.format_exc()}")
+ logger.debug(f"Context info - message ID: {event.message.id}, chat ID: {event.chat_id}, rule ID: {rule.id if hasattr(rule, 'id') else 'unknown'}")
+ return False
diff --git a/filters/filter_chain.py b/filters/filter_chain.py
index f3aff30..c6a98b7 100644
--- a/filters/filter_chain.py
+++ b/filters/filter_chain.py
@@ -6,54 +6,54 @@
class FilterChain:
"""
- 过滤器链,用于组织和执行多个过滤器
+ Filter chain, used to organize and execute multiple filters
"""
-
+
def __init__(self):
- """初始化过滤器链"""
+ """Initialize filter chain"""
self.filters = []
-
+
def add_filter(self, filter_obj):
"""
- 添加过滤器到链中
-
+ Add a filter to the chain
+
Args:
- filter_obj: 要添加的过滤器对象,必须是BaseFilter的子类
+ filter_obj: Filter object to add, must be a subclass of BaseFilter
"""
if not isinstance(filter_obj, BaseFilter):
- raise TypeError("过滤器必须是BaseFilter的子类")
+ raise TypeError("Filter must be a subclass of BaseFilter")
self.filters.append(filter_obj)
return self
-
+
async def process(self, client, event, chat_id, rule):
"""
- 处理消息
-
+ Process message
+
Args:
- client: 机器人客户端
- event: 消息事件
- chat_id: 聊天ID
- rule: 转发规则
-
+ client: Bot client
+ event: Message event
+ chat_id: Chat ID
+ rule: Forwarding rule
+
Returns:
- bool: 表示处理是否成功
+ bool: Indicates whether processing was successful
"""
- # 创建消息上下文
+ # Create message context
context = MessageContext(client, event, chat_id, rule)
-
- logger.info(f"开始过滤器链处理,共 {len(self.filters)} 个过滤器")
-
- # 依次执行每个过滤器
+
+ logger.info(f"Starting filter chain processing, total {len(self.filters)} filters")
+
+ # Execute each filter in sequence
for filter_obj in self.filters:
try:
should_continue = await filter_obj.process(context)
if not should_continue:
- logger.info(f"过滤器 {filter_obj.name} 中断了处理链")
+ logger.info(f"Filter {filter_obj.name} interrupted the processing chain")
return False
except Exception as e:
- logger.error(f"过滤器 {filter_obj.name} 处理出错: {str(e)}")
- context.errors.append(f"过滤器 {filter_obj.name} 错误: {str(e)}")
+ logger.error(f"Filter {filter_obj.name} processing error: {str(e)}")
+ context.errors.append(f"Filter {filter_obj.name} error: {str(e)}")
return False
-
- logger.info("过滤器链处理完成")
- return True
\ No newline at end of file
+
+ logger.info("Filter chain processing completed")
+ return True
diff --git a/filters/info_filter.py b/filters/info_filter.py
index 7113524..d5ac13c 100644
--- a/filters/info_filter.py
+++ b/filters/info_filter.py
@@ -9,129 +9,129 @@
class InfoFilter(BaseFilter):
"""
- 信息过滤器,添加原始链接和发送者信息
+ Info filter, adds original link and sender information
"""
-
+
async def _process(self, context):
"""
- 添加原始链接和发送者信息
-
+ Add original link and sender information
+
Args:
- context: 消息上下文
-
+ context: Message context
+
Returns:
- bool: 是否继续处理
+ bool: Whether to continue processing
"""
rule = context.rule
event = context.event
- # logger.info(f"InfoFilter处理消息前,context: {context.__dict__}")
+ # logger.info(f"Before InfoFilter processing, context: {context.__dict__}")
try:
- # 添加原始链接
+ # Add original link
if rule.is_original_link:
- # 获取原始链接的基本信息
+ # Get basic info for original link
original_link = f"https://t.me/c/{str(event.chat_id)[4:]}/{event.message.id}"
-
- # 检查是否有原始链接模板
+
+ # Check if there is a custom link template
if hasattr(rule, 'original_link_template') and rule.original_link_template:
try:
- # 使用自定义链接模板
+ # Use custom link template
link_info = rule.original_link_template
link_info = link_info.replace("{original_link}", original_link)
-
+
context.original_link = f"\n\n{link_info}"
except Exception as le:
- logger.error(f'使用自定义链接模板出错: {str(le)},使用默认格式')
- context.original_link = f"\n\n原始消息: {original_link}"
+ logger.error(f'Error using custom link template: {str(le)}, using default format')
+ context.original_link = f"\n\nOriginal message: {original_link}"
else:
- # 使用默认格式
- context.original_link = f"\n\n原始消息: {original_link}"
-
- logger.info(f'添加原始链接: {context.original_link}')
-
- # 添加发送者信息
+ # Use default format
+ context.original_link = f"\n\nOriginal message: {original_link}"
+
+ logger.info(f'Added original link: {context.original_link}')
+
+ # Add sender information
if rule.is_original_sender:
try:
- logger.info("开始获取发送者信息")
- sender_name = "Unknown Sender" # 默认值
+ logger.info("Starting to get sender information")
+ sender_name = "Unknown Sender" # Default value
sender_id = "Unknown"
if hasattr(event.message, 'sender_chat') and event.message.sender_chat:
- # 用户以频道身份发送消息
+ # User sent message as a channel
sender = event.message.sender_chat
sender_name = sender.title if hasattr(sender, 'title') else "Unknown Channel"
sender_id = sender.id
- logger.info(f"使用频道信息: {sender_name} (ID: {sender_id})")
+ logger.info(f"Using channel info: {sender_name} (ID: {sender_id})")
elif event.sender:
- # 用户以个人身份发送消息
+ # User sent message as a personal identity
sender = event.sender
sender_name = (
sender.title if hasattr(sender, 'title')
else f"{sender.first_name or ''} {sender.last_name or ''}".strip()
)
sender_id = sender.id
- logger.info(f"使用发送者信息: {sender_name} (ID: {sender_id})")
+ logger.info(f"Using sender info: {sender_name} (ID: {sender_id})")
elif hasattr(event.message, 'peer_id') and event.message.peer_id:
- # 尝试从 peer_id 获取信息
+ # Try to get info from peer_id
peer = event.message.peer_id
if hasattr(peer, 'channel_id'):
sender_id = peer.channel_id
try:
- # 尝试获取频道信息
+ # Try to get channel info
channel = await event.client.get_entity(peer)
sender_name = channel.title if hasattr(channel, 'title') else "Unknown Channel"
except Exception as ce:
- logger.error(f'获取频道信息失败: {str(ce)}')
+ logger.error(f'Failed to get channel info: {str(ce)}')
sender_name = "Unknown Channel"
- logger.info(f"使用peer_id信息: {sender_name} (ID: {sender_id})")
-
- # 检查是否有用户自定义模板
+ logger.info(f"Using peer_id info: {sender_name} (ID: {sender_id})")
+
+ # Check if there is a custom user template
if hasattr(rule, 'userinfo_template') and rule.userinfo_template:
- # 替换模板中的变量
+ # Replace variables in the template
user_info = rule.userinfo_template
user_info = user_info.replace("{name}", sender_name)
user_info = user_info.replace("{id}", str(sender_id))
-
+
context.sender_info = f"{user_info}\n\n"
else:
- # 使用默认格式
+ # Use default format
context.sender_info = f"{sender_name}\n\n"
-
- logger.info(f'添加发送者信息: {context.sender_info}')
+
+ logger.info(f'Added sender info: {context.sender_info}')
except Exception as e:
- logger.error(f'获取发送者信息出错: {str(e)}')
-
- # 添加时间信息
+ logger.error(f'Error getting sender info: {str(e)}')
+
+ # Add time information
if rule.is_original_time:
try:
- # 创建时区对象
+ # Create timezone object
timezone = pytz.timezone(os.getenv('DEFAULT_TIMEZONE', 'Asia/Shanghai'))
local_time = event.message.date.astimezone(timezone)
-
- # 默认格式化的时间
+
+ # Default formatted time
formatted_time = local_time.strftime('%Y-%m-%d %H:%M:%S')
-
- # 检查是否有时间模板
+
+ # Check if there is a time template
if hasattr(rule, 'time_template') and rule.time_template:
try:
- # 使用自定义时间模板
+ # Use custom time template
time_info = rule.time_template.replace("{time}", formatted_time)
context.time_info = f"\n\n{time_info}"
except Exception as te:
- logger.error(f'使用自定义时间模板出错: {str(te)},使用默认格式')
+ logger.error(f'Error using custom time template: {str(te)}, using default format')
context.time_info = f"\n\n{formatted_time}"
else:
- # 使用默认格式
+ # Use default format
context.time_info = f"\n\n{formatted_time}"
-
- logger.info(f'添加时间信息: {context.time_info}')
+
+ logger.info(f'Added time info: {context.time_info}')
except Exception as e:
- logger.error(f'处理时间信息时出错: {str(e)}')
-
- return True
+ logger.error(f'Error processing time info: {str(e)}')
+
+ return True
finally:
- # logger.info(f"InfoFilter处理消息后,context: {context.__dict__}")
- pass
\ No newline at end of file
+ # logger.info(f"After InfoFilter processing, context: {context.__dict__}")
+ pass
diff --git a/filters/init_filter.py b/filters/init_filter.py
index b34f1de..7cb0cd5 100644
--- a/filters/init_filter.py
+++ b/filters/init_filter.py
@@ -11,30 +11,30 @@
class InitFilter(BaseFilter):
"""
- 初始化过滤器,为context添加基本信息
+ Initialization filter, adds basic information to context
"""
-
+
async def _process(self, context):
"""
- 添加原始链接和发送者信息
-
+ Add original link and sender information
+
Args:
- context: 消息上下文
-
+ context: Message context
+
Returns:
- bool: 是否继续处理
+ bool: Whether to continue processing
"""
rule = context.rule
event = context.event
- # logger.info(f"InitFilter处理消息前,context: {context.__dict__}")
+ # logger.info(f"Before InitFilter processing, context: {context.__dict__}")
try:
- #处理媒体组消息
+ # Process media group messages
if event.message.grouped_id:
- # 等待更长时间让所有媒体消息到达
+ # Wait longer for all media messages to arrive
# await asyncio.sleep(1)
-
- # 收集媒体组的所有消息
+
+ # Collect all messages in the media group
try:
async for message in event.client.iter_messages(
event.chat_id,
@@ -43,18 +43,18 @@ async def _process(self, context):
max_id=event.message.id + 10
):
if message.grouped_id == event.message.grouped_id:
- if message.text:
- # 保存第一条消息的文本和按钮
+ if message.text:
+ # Save the first message's text and buttons
context.message_text = message.text or ''
context.original_message_text = message.text or ''
context.check_message_text = message.text or ''
context.buttons = message.buttons if hasattr(message, 'buttons') else None
- logger.info(f'获取到媒体组文本并添加到context: {message.text}')
-
+ logger.info(f'Got media group text and added to context: {message.text}')
+
except Exception as e:
- logger.error(f'收集媒体组消息时出错: {str(e)}')
- context.errors.append(f"收集媒体组消息错误: {str(e)}")
-
+ logger.error(f'Error collecting media group messages: {str(e)}')
+ context.errors.append(f"Error collecting media group messages: {str(e)}")
+
finally:
- # logger.info(f"InitFilter处理消息后,context: {context.__dict__}")
+ # logger.info(f"After InitFilter processing, context: {context.__dict__}")
return True
diff --git a/filters/keyword_filter.py b/filters/keyword_filter.py
index 2976fc3..5e242b0 100644
--- a/filters/keyword_filter.py
+++ b/filters/keyword_filter.py
@@ -8,25 +8,24 @@
class KeywordFilter(BaseFilter):
"""
- 关键字过滤器,检查消息是否包含指定关键字
+ Keyword filter, checks if a message contains specified keywords
"""
-
+
async def _process(self, context):
"""
- 检查消息是否包含规则中的关键字
-
+ Check if the message contains keywords from the rule
+
Args:
- context: 消息上下文
-
+ context: Message context
+
Returns:
- bool: 若消息应继续处理则返回True,否则返回False
+ bool: Returns True if the message should continue processing, False otherwise
"""
rule = context.rule
message_text = context.message_text
event = context.event
-
+
should_forward = await check_keywords(rule, message_text, event)
-
+
return should_forward
-
diff --git a/filters/media_filter.py b/filters/media_filter.py
index 2dae0ba..9a65e63 100644
--- a/filters/media_filter.py
+++ b/filters/media_filter.py
@@ -15,48 +15,48 @@
class MediaFilter(BaseFilter):
"""
- 媒体过滤器,处理消息中的媒体内容
+ Media filter, processes media content in messages
"""
-
+
async def _process(self, context):
"""
- 处理媒体内容
-
+ Process media content
+
Args:
- context: 消息上下文
-
+ context: Message context
+
Returns:
- bool: 是否继续处理
+ bool: Whether to continue processing
"""
- # 确保临时目录存在
+ # Ensure temporary directory exists
os.makedirs(TEMP_DIR, exist_ok=True)
-
+
rule = context.rule
event = context.event
client = context.client
-
- # 如果是媒体组消息
+
+ # If it's a media group message
if event.message.grouped_id:
await self._process_media_group(context)
else:
await self._process_single_media(context)
-
+
return True
-
+
async def _process_media_group(self, context):
- """处理媒体组消息"""
+ """Process media group messages"""
event = context.event
rule = context.rule
client = context.client
-
- logger.info(f'处理媒体组消息 组ID: {event.message.grouped_id}')
-
- # 等待更长时间让所有媒体消息到达
+
+ logger.info(f'Processing media group message, group ID: {event.message.grouped_id}')
+
+ # Wait longer for all media messages to arrive
await asyncio.sleep(1)
-
- # 获取媒体类型设置
+
+ # Get media type settings
media_types = None
if rule.enable_media_type_filter:
session = get_session()
@@ -64,10 +64,10 @@ async def _process_media_group(self, context):
media_types = session.query(MediaTypes).filter_by(rule_id=rule.id).first()
finally:
session.close()
-
- # 收集媒体组的所有消息
- total_media_count = 0 # 总媒体数量
- blocked_media_count = 0 # 被屏蔽的媒体数量
+
+ # Collect all messages in the media group
+ total_media_count = 0 # Total media count
+ blocked_media_count = 0 # Blocked media count
try:
async for message in event.client.iter_messages(
event.chat_id,
@@ -78,29 +78,29 @@ async def _process_media_group(self, context):
if message.grouped_id == event.message.grouped_id:
if message.media:
total_media_count += 1
- # 检查媒体类型
+ # Check media type
if rule.enable_media_type_filter and media_types and message.media:
if await self._is_media_type_blocked(message.media, media_types):
- logger.info(f'媒体类型被屏蔽,跳过消息 ID={message.id}')
+ logger.info(f'Media type is blocked, skipping message ID={message.id}')
blocked_media_count += 1
continue
-
- # 检查媒体扩展名
+
+ # Check media extension
if rule.enable_extension_filter and message.media:
if not await self._is_media_extension_allowed(rule, message.media):
- logger.info(f'媒体扩展名被屏蔽,跳过消息 ID={message.id}')
+ logger.info(f'Media extension is blocked, skipping message ID={message.id}')
blocked_media_count += 1
continue
-
- # 检查媒体大小
+
+ # Check media size
if message.media:
file_size = await get_media_size(message.media)
- file_size = round(file_size/1024/1024, 2) # 转换为MB
- logger.info(f'媒体文件大小: {file_size}MB')
- logger.info(f'规则最大媒体大小: {rule.max_media_size}MB')
- logger.info(f'是否启用媒体大小过滤: {rule.enable_media_size_filter}')
- logger.info(f'是否发送媒体大小超限提醒: {rule.is_send_over_media_size_message}')
-
+ file_size = round(file_size/1024/1024, 2) # Convert to MB
+ logger.info(f'Media file size: {file_size}MB')
+ logger.info(f'Rule max media size: {rule.max_media_size}MB')
+ logger.info(f'Media size filter enabled: {rule.enable_media_size_filter}')
+ logger.info(f'Send oversized media notification: {rule.is_send_over_media_size_message}')
+
if rule.max_media_size and (file_size > rule.max_media_size) and rule.enable_media_size_filter:
file_name = ''
if hasattr(message.media, 'document') and message.media.document:
@@ -108,45 +108,45 @@ async def _process_media_group(self, context):
if hasattr(attr, 'file_name'):
file_name = attr.file_name
break
- logger.info(f'媒体文件 {file_name} 超过大小限制 ({rule.max_media_size}MB)')
+ logger.info(f'Media file {file_name} exceeds size limit ({rule.max_media_size}MB)')
context.skipped_media.append((message, file_size, file_name))
continue
-
+
context.media_group_messages.append(message)
- logger.info(f'找到媒体组消息: ID={message.id}, 类型={type(message.media).__name__ if message.media else "无媒体"}')
+ logger.info(f'Found media group message: ID={message.id}, type={type(message.media).__name__ if message.media else "no media"}')
except Exception as e:
- logger.error(f'收集媒体组消息时出错: {str(e)}')
- context.errors.append(f"收集媒体组消息错误: {str(e)}")
-
- logger.info(f'共找到 {len(context.media_group_messages)} 条媒体组消息,{len(context.skipped_media)} 条超限')
-
- # 如果所有媒体都被屏蔽,设置不转发
+ logger.error(f'Error collecting media group messages: {str(e)}')
+ context.errors.append(f"Error collecting media group messages: {str(e)}")
+
+ logger.info(f'Found {len(context.media_group_messages)} media group messages, {len(context.skipped_media)} exceeded limit')
+
+ # If all media are blocked, set not to forward
if total_media_count > 0 and total_media_count == blocked_media_count:
- logger.info('媒体组中所有媒体都被屏蔽,设置不转发')
- # 检查是否允许文本通过
+ logger.info('All media in the media group are blocked, setting not to forward')
+ # Check if text is allowed to pass through
if rule.media_allow_text:
- logger.info('媒体被屏蔽但允许文本通过')
- context.media_blocked = True # 标记媒体被屏蔽
+ logger.info('Media blocked but text allowed to pass through')
+ context.media_blocked = True # Mark media as blocked
else:
context.should_forward = False
return True
-
- # 如果所有媒体都超限且不发送超限提醒,则设置不转发
+
+ # If all media exceeded limit and oversized notification is not enabled, set not to forward
if len(context.skipped_media) > 0 and len(context.media_group_messages) == 0 and not rule.is_send_over_media_size_message:
- # 检查是否允许文本通过
+ # Check if text is allowed to pass through
if rule.media_allow_text:
- logger.info('媒体超限但允许文本通过')
- context.media_blocked = True # 标记媒体被屏蔽
+ logger.info('Media exceeded limit but text allowed to pass through')
+ context.media_blocked = True # Mark media as blocked
else:
context.should_forward = False
- logger.info('所有媒体都超限且不发送超限提醒,设置不转发')
-
+ logger.info('All media exceeded limit and oversized notification not enabled, setting not to forward')
+
async def _process_single_media(self, context):
- """处理单条媒体消息"""
+ """Process single media message"""
event = context.event
rule = context.rule
- # logger.info(f'context属性: {context.rule.__dict__}')
- # 检查是否是纯链接预览消息
+ # logger.info(f'context attributes: {context.rule.__dict__}')
+ # Check if it's a pure link preview message
is_pure_link_preview = (
event.message.media and
hasattr(event.message.media, 'webpage') and
@@ -158,8 +158,8 @@ async def _process_single_media(self, context):
getattr(event.message.media, 'voice', None)
])
)
-
- # 检查是否有实际媒体
+
+ # Check if there is actual media
has_media = (
event.message.media and
any([
@@ -171,193 +171,192 @@ async def _process_single_media(self, context):
])
)
- # 处理实际媒体
+ # Process actual media
if has_media:
- # 检查媒体类型是否被屏蔽
+ # Check if media type is blocked
if rule.enable_media_type_filter:
session = get_session()
try:
media_types = session.query(MediaTypes).filter_by(rule_id=rule.id).first()
if media_types and await self._is_media_type_blocked(event.message.media, media_types):
- logger.info(f'媒体类型被屏蔽,跳过消息 ID={event.message.id}')
- # 检查是否允许文本通过
+ logger.info(f'Media type is blocked, skipping message ID={event.message.id}')
+ # Check if text is allowed to pass through
if rule.media_allow_text:
- logger.info('媒体被屏蔽但允许文本通过')
- context.media_blocked = True # 标记媒体被屏蔽
+ logger.info('Media blocked but text allowed to pass through')
+ context.media_blocked = True # Mark media as blocked
else:
context.should_forward = False
return True
finally:
session.close()
-
- # 检查媒体扩展名
+
+ # Check media extension
if rule.enable_extension_filter and event.message.media:
if not await self._is_media_extension_allowed(rule, event.message.media):
- logger.info(f'媒体扩展名被屏蔽,跳过消息 ID={event.message.id}')
- # 检查是否允许文本通过
+ logger.info(f'Media extension is blocked, skipping message ID={event.message.id}')
+ # Check if text is allowed to pass through
if rule.media_allow_text:
- logger.info('媒体被屏蔽但允许文本通过')
- context.media_blocked = True # 标记媒体被屏蔽
+ logger.info('Media blocked but text allowed to pass through')
+ context.media_blocked = True # Mark media as blocked
else:
context.should_forward = False
return True
-
- # 检查媒体大小
+
+ # Check media size
file_size = await get_media_size(event.message.media)
file_size = round(file_size/1024/1024, 2)
logger.info(f'event.message.document: {event.message.document}')
-
- logger.info(f'媒体文件大小: {file_size}MB')
- logger.info(f'规则最大媒体大小: {rule.max_media_size}MB')
-
- logger.info(f'是否启用媒体大小过滤: {rule.enable_media_size_filter}')
+
+ logger.info(f'Media file size: {file_size}MB')
+ logger.info(f'Rule max media size: {rule.max_media_size}MB')
+
+ logger.info(f'Media size filter enabled: {rule.enable_media_size_filter}')
if rule.max_media_size and (file_size > rule.max_media_size) and rule.enable_media_size_filter:
file_name = ''
if event.message.document:
- # 正确地从文档属性中获取文件名
+ # Correctly get filename from document attributes
for attr in event.message.document.attributes:
if hasattr(attr, 'file_name'):
file_name = attr.file_name
break
-
- logger.info(f'媒体文件超过大小限制 ({rule.max_media_size}MB)')
+
+ logger.info(f'Media file exceeds size limit ({rule.max_media_size}MB)')
if rule.is_send_over_media_size_message:
- logger.info(f'是否发送媒体大小超限提醒: {rule.is_send_over_media_size_message}')
+ logger.info(f'Send oversized media notification: {rule.is_send_over_media_size_message}')
context.should_forward = True
else:
- # 检查是否允许文本通过
+ # Check if text is allowed to pass through
if rule.media_allow_text:
- logger.info('媒体超限但允许文本通过')
- context.media_blocked = True # 标记媒体被屏蔽
+ logger.info('Media exceeded limit but text allowed to pass through')
+ context.media_blocked = True # Mark media as blocked
context.skipped_media.append((event.message, file_size, file_name))
- return True # 跳过后续的媒体下载
+ return True # Skip subsequent media download
else:
context.should_forward = False
context.skipped_media.append((event.message, file_size, file_name))
- return True # 不论如何都跳过后续的媒体下载
+ return True # Skip subsequent media download regardless
else:
- # 如果只转发到RSS,则跳过下载媒体文件,交给RSS处理下载
+ # If only forwarding to RSS, skip downloading media files, let RSS handle the download
if rule.only_rss:
return True
try:
- # 下载媒体文件
+ # Download media file
file_path = await event.message.download_media(TEMP_DIR)
if file_path:
context.media_files.append(file_path)
- logger.info(f'媒体文件已下载到: {file_path}')
+ logger.info(f'Media file downloaded to: {file_path}')
except Exception as e:
- logger.error(f'下载媒体文件时出错: {str(e)}')
- context.errors.append(f"下载媒体文件错误: {str(e)}")
+ logger.error(f'Error downloading media file: {str(e)}')
+ context.errors.append(f"Error downloading media file: {str(e)}")
elif is_pure_link_preview:
- # 记录这是纯链接预览消息
+ # Record that this is a pure link preview message
context.is_pure_link_preview = True
- logger.info('这是一条纯链接预览消息')
-
+ logger.info('This is a pure link preview message')
+
async def _is_media_type_blocked(self, media, media_types):
"""
- 检查媒体类型是否被屏蔽
-
+ Check if media type is blocked
+
Args:
- media: 媒体对象
- media_types: MediaTypes对象
-
+ media: Media object
+ media_types: MediaTypes object
+
Returns:
- bool: 如果媒体类型被屏蔽返回True,否则返回False
+ bool: Returns True if media type is blocked, False otherwise
"""
- # 检查各种媒体类型
+ # Check various media types
if getattr(media, 'photo', None) and media_types.photo:
- logger.info('媒体类型为图片,已被屏蔽')
+ logger.info('Media type is photo, blocked')
return True
-
+
if getattr(media, 'document', None) and media_types.document:
- logger.info('媒体类型为文档,已被屏蔽')
+ logger.info('Media type is document, blocked')
return True
-
+
if getattr(media, 'video', None) and media_types.video:
- logger.info('媒体类型为视频,已被屏蔽')
+ logger.info('Media type is video, blocked')
return True
-
+
if getattr(media, 'audio', None) and media_types.audio:
- logger.info('媒体类型为音频,已被屏蔽')
+ logger.info('Media type is audio, blocked')
return True
-
+
if getattr(media, 'voice', None) and media_types.voice:
- logger.info('媒体类型为语音,已被屏蔽')
+ logger.info('Media type is voice, blocked')
return True
-
- return False
-
+
+ return False
+
async def _is_media_extension_allowed(self, rule, media):
"""
- 检查媒体扩展名是否被允许
-
+ Check if media extension is allowed
+
Args:
- rule: 转发规则
- media: 媒体对象
-
+ rule: Forwarding rule
+ media: Media object
+
Returns:
- bool: 如果扩展名被允许返回True,否则返回False
+ bool: Returns True if extension is allowed, False otherwise
"""
- # 如果没有启用扩展名过滤,默认允许
+ # If extension filter is not enabled, allow by default
if not rule.enable_extension_filter:
return True
-
- # 获取文件名
+
+ # Get filename
file_name = None
-
+
for attr in media.document.attributes:
if hasattr(attr, 'file_name'):
file_name = attr.file_name
break
-
- # 如果没有文件名,则无法判断扩展名,默认允许
+
+ # If filename cannot be obtained, extension cannot be determined, allow by default
if not file_name:
- logger.info("无法获取文件名,无法判断扩展名")
+ logger.info("Cannot get filename, unable to determine extension")
return True
-
- # 提取扩展名
+
+ # Extract extension
_, extension = os.path.splitext(file_name)
- extension = extension.lstrip('.').lower() # 移除点号并转为小写
-
- # 特殊处理:如果文件没有扩展名,将extension设为特殊值"无扩展名"
+ extension = extension.lstrip('.').lower() # Remove dot and convert to lowercase
+
+ # Special handling: if the file has no extension, set extension to a special value "no_extension"
if not extension:
- logger.info(f"文件 {file_name} 没有扩展名")
- extension = "无扩展名"
+ logger.info(f"File {file_name} has no extension")
+ extension = "no_extension"
else:
- logger.info(f"文件 {file_name} 的扩展名: {extension}")
-
- # 获取规则中保存的扩展名列表
+ logger.info(f"File {file_name} extension: {extension}")
+
+ # Get the extension list saved in the rule
db_ops = await get_db_ops()
session = get_session()
allowed = True
try:
- # 使用db_operations中的函数获取扩展名列表
+ # Use the function from db_operations to get the extension list
extensions = await db_ops.get_media_extensions(session, rule.id)
extension_list = [ext["extension"].lower() for ext in extensions]
-
- # 判断是否允许该扩展名
+
+ # Determine if the extension is allowed
if rule.extension_filter_mode == AddMode.BLACKLIST:
- # 黑名单模式:如果扩展名在列表中,则不允许
+ # Blacklist mode: if extension is in the list, not allowed
if extension in extension_list:
- logger.info(f"扩展名 {extension} 在黑名单中,不允许")
+ logger.info(f"Extension {extension} is in the blacklist, not allowed")
allowed = False
else:
- logger.info(f"扩展名 {extension} 不在黑名单中,允许")
+ logger.info(f"Extension {extension} is not in the blacklist, allowed")
allowed = True
else:
- # 白名单模式:如果扩展名不在列表中,则不允许
+ # Whitelist mode: if extension is not in the list, not allowed
if extension in extension_list:
- logger.info(f"扩展名 {extension} 在白名单中,允许")
+ logger.info(f"Extension {extension} is in the whitelist, allowed")
allowed = True
else:
- logger.info(f"扩展名 {extension} 不在白名单中,不允许")
+ logger.info(f"Extension {extension} is not in the whitelist, not allowed")
allowed = False
except Exception as e:
- logger.error(f"检查媒体扩展名时出错: {str(e)}")
- allowed = True # 出错时默认允许
+ logger.error(f"Error checking media extension: {str(e)}")
+ allowed = True # Allow by default on error
finally:
session.close()
-
- return allowed
+ return allowed
diff --git a/filters/process.py b/filters/process.py
index 15a749f..1875f71 100644
--- a/filters/process.py
+++ b/filters/process.py
@@ -18,65 +18,65 @@
async def process_forward_rule(client, event, chat_id, rule):
"""
- 处理转发规则
-
+ Process forwarding rule
+
Args:
- client: 机器人客户端
- event: 消息事件
- chat_id: 聊天ID
- rule: 转发规则
-
+ client: Bot client
+ event: Message event
+ chat_id: Chat ID
+ rule: Forwarding rule
+
Returns:
- bool: 处理是否成功
+ bool: Whether processing was successful
"""
- logger.info(f'使用过滤器链处理规则 ID: {rule.id}')
-
- # 创建过滤器链
+ logger.info(f'Processing rule with filter chain, ID: {rule.id}')
+
+ # Create filter chain
filter_chain = FilterChain()
- # 添加初始化过滤器
+ # Add initialization filter
filter_chain.add_filter(InitFilter())
- # 延迟处理过滤器(如果启用了延迟处理)
+ # Delay processing filter (if delay processing is enabled)
filter_chain.add_filter(DelayFilter())
-
- # 添加关键字过滤器(如果消息不匹配关键字,会中断处理链)
+
+ # Add keyword filter (if the message doesn't match keywords, it will interrupt the processing chain)
filter_chain.add_filter(KeywordFilter())
-
- # 添加替换过滤器
+
+ # Add replace filter
filter_chain.add_filter(ReplaceFilter())
- # 添加媒体过滤器(处理媒体内容)
+ # Add media filter (process media content)
filter_chain.add_filter(MediaFilter())
-
- # 添加AI处理过滤器(如果启用了AI处理后的关键字检查,可能会中断处理链)
+
+ # Add AI processing filter (if keyword check after AI processing is enabled, it may interrupt the processing chain)
filter_chain.add_filter(AIFilter())
-
- # 添加信息过滤器(处理原始链接和发送者信息)
+
+ # Add info filter (process original link and sender information)
filter_chain.add_filter(InfoFilter())
-
- # 添加评论区按钮过滤器
+
+ # Add comment section button filter
filter_chain.add_filter(CommentButtonFilter())
- # 添加RSS过滤器
+ # Add RSS filter
filter_chain.add_filter(RSSFilter())
-
- # 添加编辑过滤器(编辑原始消息)
+
+ # Add edit filter (edit original message)
filter_chain.add_filter(EditFilter())
- # 添加发送过滤器(发送消息)
+ # Add sender filter (send message)
filter_chain.add_filter(SenderFilter())
-
- # 添加回复过滤器(处理媒体组消息的评论区按钮)
+
+ # Add reply filter (handle comment section buttons for media group messages)
filter_chain.add_filter(ReplyFilter())
- # 添加推送过滤器
+ # Add push filter
filter_chain.add_filter(PushFilter())
-
- # 添加删除原始消息过滤器(最后执行)
+
+ # Add delete original message filter (executed last)
filter_chain.add_filter(DeleteOriginalFilter())
-
- # 执行过滤器链
+
+ # Execute filter chain
result = await filter_chain.process(client, event, chat_id, rule)
-
- return result
+
+ return result
diff --git a/filters/push_filter.py b/filters/push_filter.py
index 3eaae33..fdba677 100644
--- a/filters/push_filter.py
+++ b/filters/push_filter.py
@@ -14,361 +14,361 @@
class PushFilter(BaseFilter):
"""
- 推送过滤器,利用apprise库推送消息
+ Push filter, uses the apprise library to push messages
"""
-
+
async def _process(self, context):
"""
- 推送消息
-
+ Push messages
+
Args:
- context: 消息上下文
-
+ context: Message context
+
Returns:
- bool: 若消息应继续处理则返回True,否则返回False
+ bool: Returns True if the message should continue processing, False otherwise
"""
rule = context.rule
client = context.client
event = context.event
-
- # 如果规则没有启用推送,直接返回
+
+ # If the rule does not have push enabled, return directly
if not rule.enable_push:
- logger.info('推送未启用,跳过推送')
+ logger.info('Push not enabled, skipping push')
return True
-
- # 获取规则ID和所有启用的推送配置
+
+ # Get rule ID and all enabled push configurations
rule_id = rule.id
session = get_session()
-
-
- logger.info(f"推送过滤器开始处理 - 规则ID: {rule_id}")
- logger.info(f"是否是媒体组: {context.is_media_group}")
- logger.info(f"媒体组消息数量: {len(context.media_group_messages) if context.media_group_messages else 0}")
- logger.info(f"已有媒体文件数量: {len(context.media_files) if context.media_files else 0}")
- logger.info(f"是否只推送不转发: {rule.enable_only_push}")
-
- # 跟踪已处理的文件
+
+
+ logger.info(f"Push filter starting processing - Rule ID: {rule_id}")
+ logger.info(f"Is media group: {context.is_media_group}")
+ logger.info(f"Media group message count: {len(context.media_group_messages) if context.media_group_messages else 0}")
+ logger.info(f"Existing media file count: {len(context.media_files) if context.media_files else 0}")
+ logger.info(f"Push only without forwarding: {rule.enable_only_push}")
+
+ # Track processed files
processed_files = []
-
+
try:
- # 获取所有启用的推送配置
+ # Get all enabled push configurations
push_configs = session.query(PushConfig).filter(
PushConfig.rule_id == rule_id,
PushConfig.enable_push_channel == True
).all()
-
+
if not push_configs:
- logger.info(f'规则 {rule_id} 没有启用的推送配置,跳过推送')
+ logger.info(f'Rule {rule_id} has no enabled push configurations, skipping push')
return True
-
- # 对媒体组消息进行推送
+
+ # Push media group messages
if context.is_media_group or (context.media_group_messages and context.skipped_media):
processed_files = await self._push_media_group(context, push_configs)
- # 对单条媒体消息进行推送
+ # Push single media message
elif context.media_files or context.skipped_media:
processed_files = await self._push_single_media(context, push_configs)
- # 对纯文本消息进行推送
+ # Push plain text message
else:
processed_files = await self._push_text_message(context, push_configs)
-
- logger.info(f'推送已发送到 {len(push_configs)} 个配置')
+
+ logger.info(f'Push sent to {len(push_configs)} configurations')
return True
-
+
except Exception as e:
- logger.error(f'推送过滤器处理出错: {str(e)}')
+ logger.error(f'Push filter processing error: {str(e)}')
logger.error(traceback.format_exc())
- context.errors.append(f"推送错误: {str(e)}")
+ context.errors.append(f"Push error: {str(e)}")
return False
finally:
session.close()
-
- # 只清理已处理的媒体文件
+
+ # Only clean up processed media files
if processed_files:
- logger.info(f'清理已处理的媒体文件,共 {len(processed_files)} 个')
+ logger.info(f'Cleaning up processed media files, total {len(processed_files)}')
for file_path in processed_files:
try:
if os.path.exists(str(file_path)):
os.remove(file_path)
- logger.info(f'删除已处理的媒体文件: {file_path}')
+ logger.info(f'Deleted processed media file: {file_path}')
except Exception as e:
- logger.error(f'删除媒体文件失败: {str(e)}')
-
+ logger.error(f'Failed to delete media file: {str(e)}')
+
async def _push_media_group(self, context, push_configs):
- """推送媒体组消息"""
+ """Push media group messages"""
rule = context.rule
client = context.client
event = context.event
-
- # 初始化文件列表
+
+ # Initialize file list
files = []
need_cleanup = False
-
+
try:
- # 如果没有媒体组消息(都超限了),发送文本和提示
+ # If there are no media group messages (all exceeded limit), send text and prompt
if not context.media_group_messages and context.skipped_media:
- logger.info(f'所有媒体都超限,发送文本和提示')
- # 构建提示信息
+ logger.info(f'All media exceeded limit, sending text and prompt')
+ # Build prompt information
text_to_send = context.message_text or ''
-
- # 设置原始消息链接
+
+ # Set original message link
if rule.is_original_link:
- context.original_link = f"\n原始消息: https://t.me/c/{str(event.chat_id)[4:]}/{event.message.id}"
-
- # 添加每个超限文件的信息
+ context.original_link = f"\nOriginal message: https://t.me/c/{str(event.chat_id)[4:]}/{event.message.id}"
+
+ # Add information for each exceeded file
for message, size, name in context.skipped_media:
- text_to_send += f"\n\n⚠️ 媒体文件 {name if name else '未命名文件'} ({size}MB) 超过大小限制"
-
- # 组合完整文本
+ text_to_send += f"\n\n⚠️ Media file {name if name else 'unnamed file'} ({size}MB) exceeds size limit"
+
+ # Combine complete text
if rule.is_original_sender:
text_to_send = context.sender_info + text_to_send
if rule.is_original_time:
text_to_send += context.time_info
if rule.is_original_link:
text_to_send += context.original_link
-
- # 发送文本推送
+
+ # Send text push
await self._send_push_notification(push_configs, text_to_send)
return
-
- # 检查是否有媒体组消息但没有媒体文件(这是关键修复)
+
+ # Check if there are media group messages but no media files (this is the key fix)
if context.media_group_messages and not context.media_files:
- logger.info(f'检测到媒体组消息但没有媒体文件,开始下载...')
+ logger.info(f'Detected media group messages but no media files, starting download...')
need_cleanup = True
for message in context.media_group_messages:
if message.media:
file_path = await message.download_media(os.path.join(os.getcwd(), 'temp'))
if file_path:
files.append(file_path)
- logger.info(f'已下载媒体组文件: {file_path}')
- # 如果SenderFilter已经下载了文件,使用它们
+ logger.info(f'Downloaded media group file: {file_path}')
+ # If SenderFilter already downloaded files, use them
elif context.media_files:
- logger.info(f'使用SenderFilter已下载的文件: {len(context.media_files)}个')
+ logger.info(f'Using files already downloaded by SenderFilter: {len(context.media_files)}')
files = context.media_files
- # 否则,需要自己下载文件
+ # Otherwise, need to download files ourselves
elif rule.enable_only_push:
- logger.info(f'需要自己下载文件,开始下载媒体组消息...')
+ logger.info(f'Need to download files ourselves, starting media group message download...')
need_cleanup = True
for message in context.media_group_messages:
if message.media:
file_path = await message.download_media(os.path.join(os.getcwd(), 'temp'))
if file_path:
files.append(file_path)
- logger.info(f'已下载媒体文件: {file_path}')
-
- # 如果有可用的媒体文件,构建推送内容
+ logger.info(f'Downloaded media file: {file_path}')
+
+ # If there are available media files, build push content
if files:
- # 添加发送者信息和消息文本
+ # Add sender info and message text
caption_text = ""
if rule.is_original_sender and context.sender_info:
caption_text += context.sender_info
caption_text += context.message_text or ""
-
- # 如果有超限文件,添加提示信息
+
+ # If there are exceeded files, add prompt information
for message, size, name in context.skipped_media:
- caption_text += f"\n\n⚠️ 媒体文件 {name if name else '未命名文件'} ({size}MB) 超过大小限制"
-
- # 添加原始链接
+ caption_text += f"\n\n⚠️ Media file {name if name else 'unnamed file'} ({size}MB) exceeds size limit"
+
+ # Add original link
if rule.is_original_link and context.skipped_media:
- original_link = f"\n原始消息: https://t.me/c/{str(event.chat_id)[4:]}/{event.message.id}"
+ original_link = f"\nOriginal message: https://t.me/c/{str(event.chat_id)[4:]}/{event.message.id}"
caption_text += original_link
-
- # 添加时间信息
+
+ # Add time info
if rule.is_original_time and context.time_info:
caption_text += context.time_info
-
- # 设置默认描述(如果没有文本内容)
- default_caption = f"收到一组媒体文件 (共{len(files)}个)"
-
- # 按配置的媒体发送方式分别处理每个推送配置
+
+ # Set default description (if no text content)
+ default_caption = f"Received a group of media files (total {len(files)})"
+
+ # Handle each push configuration separately based on configured media send mode
processed_files = []
-
+
for config in push_configs:
- # 获取该配置的媒体发送模式
- send_mode = config.media_send_mode # "Single" 或 "Multiple"
-
- # 检查所有文件是否存在
+ # Get the media send mode for this configuration
+ send_mode = config.media_send_mode # "Single" or "Multiple"
+
+ # Check if all files exist
valid_files = [f for f in files if os.path.exists(str(f))]
if not valid_files:
continue
-
- # 根据媒体发送模式来决定发送方式
+
+ # Decide send method based on media send mode
if send_mode == "Multiple":
try:
- logger.info(f'尝试一次性发送 {len(valid_files)} 个文件到 {config.push_channel},模式: {send_mode}')
+ logger.info(f'Trying to send {len(valid_files)} files at once to {config.push_channel}, mode: {send_mode}')
await self._send_push_notification(
- [config],
- caption_text or f"收到一组媒体文件 (共{len(valid_files)}个)",
- None, # 不使用单附件参数
- valid_files # 使用多附件参数
+ [config],
+ caption_text or f"Received a group of media files (total {len(valid_files)})",
+ None, # Don't use single attachment parameter
+ valid_files # Use multiple attachments parameter
)
processed_files.extend(valid_files)
except Exception as e:
- logger.error(f'尝试一次性发送多个文件失败,错误: {str(e)}')
- # 如果一次性发送失败,则尝试逐个发送
+ logger.error(f'Failed to send multiple files at once, error: {str(e)}')
+ # If sending at once fails, try sending one by one
for i, file_path in enumerate(valid_files):
- # 第一个文件使用完整文本,后续文件使用简短描述
- file_caption = caption_text if i == 0 else f"媒体组的第 {i+1} 个文件"
+ # First file uses full text, subsequent files use short description
+ file_caption = caption_text if i == 0 else f"File {i+1} of the media group"
await self._send_push_notification([config], file_caption, file_path)
processed_files.append(file_path)
- # 逐个发送文件
+ # Send files one by one
else:
for i, file_path in enumerate(valid_files):
- # 第一个文件使用完整文本,后续文件使用简短描述
+ # First file uses full text, subsequent files use short description
if i == 0:
- file_caption = caption_text or f"收到一组媒体文件 (共{len(valid_files)}个)"
+ file_caption = caption_text or f"Received a group of media files (total {len(valid_files)})"
else:
- file_caption = f"媒体组的第 {i+1} 个文件" if len(valid_files) > 1 else ""
-
+ file_caption = f"File {i+1} of the media group" if len(valid_files) > 1 else ""
+
await self._send_push_notification([config], file_caption, file_path)
processed_files.append(file_path)
-
+
except Exception as e:
- logger.error(f'推送媒体组消息时出错: {str(e)}')
+ logger.error(f'Error pushing media group message: {str(e)}')
logger.error(traceback.format_exc())
raise
finally:
- # 如果是自己下载的文件,立即清理
+ # If files were downloaded by us, clean up immediately
if need_cleanup:
for file_path in files:
try:
if os.path.exists(str(file_path)):
os.remove(file_path)
- logger.info(f'删除临时文件: {file_path}')
- # 移除已删除的文件,避免重复删除
+ logger.info(f'Deleted temporary file: {file_path}')
+ # Remove already deleted files to avoid duplicate deletion
if file_path in processed_files:
processed_files.remove(file_path)
except Exception as e:
- logger.error(f'删除临时文件失败: {str(e)}')
-
- # 返回处理过但未删除的文件
+ logger.error(f'Failed to delete temporary file: {str(e)}')
+
+ # Return processed but not yet deleted files
return processed_files
-
+
async def _push_single_media(self, context, push_configs):
- """推送单条媒体消息"""
+ """Push single media message"""
rule = context.rule
client = context.client
event = context.event
-
- logger.info(f'推送单条媒体消息')
-
- # 初始化处理文件列表
+
+ logger.info(f'Pushing single media message')
+
+ # Initialize processed file list
processed_files = []
-
- # 检查是否所有媒体都超限
+
+ # Check if all media exceeded limit
if context.skipped_media and not context.media_files:
- # 构建提示信息
+ # Build prompt information
file_size = context.skipped_media[0][1]
file_name = context.skipped_media[0][2]
-
+
text_to_send = context.message_text or ''
- text_to_send += f"\n\n⚠️ 媒体文件 {file_name} ({file_size}MB) 超过大小限制"
-
- # 添加发送者信息
+ text_to_send += f"\n\n⚠️ Media file {file_name} ({file_size}MB) exceeds size limit"
+
+ # Add sender info
if rule.is_original_sender:
text_to_send = context.sender_info + text_to_send
-
- # 添加时间信息
+
+ # Add time info
if rule.is_original_time:
text_to_send += context.time_info
-
- # 添加原始链接
+
+ # Add original link
if rule.is_original_link:
- original_link = f"\n原始消息: https://t.me/c/{str(event.chat_id)[4:]}/{event.message.id}"
+ original_link = f"\nOriginal message: https://t.me/c/{str(event.chat_id)[4:]}/{event.message.id}"
text_to_send += original_link
-
- # 发送文本推送
+
+ # Send text push
await self._send_push_notification(push_configs, text_to_send)
return processed_files
-
- # 处理媒体文件
+
+ # Process media files
files = []
need_cleanup = False
-
+
try:
- # 如果SenderFilter已经下载了文件,使用它们
+ # If SenderFilter already downloaded files, use them
if context.media_files:
- logger.info(f'使用SenderFilter已下载的文件: {len(context.media_files)}个')
+ logger.info(f'Using files already downloaded by SenderFilter: {len(context.media_files)}')
files = context.media_files
- # 否则,需要自己下载文件
+ # Otherwise, need to download files ourselves
elif rule.enable_only_push and event.message and event.message.media:
- logger.info(f'需要自己下载文件,开始下载单个媒体消息...')
+ logger.info(f'Need to download files ourselves, starting single media message download...')
need_cleanup = True
file_path = await event.message.download_media(os.path.join(os.getcwd(), 'temp'))
if file_path:
files.append(file_path)
- logger.info(f'已下载媒体文件: {file_path}')
-
- # 发送媒体文件
+ logger.info(f'Downloaded media file: {file_path}')
+
+ # Send media files
for file_path in files:
try:
- # 构建推送内容
+ # Build push content
caption = ""
if rule.is_original_sender and context.sender_info:
caption += context.sender_info
caption += context.message_text or ""
-
- # 添加时间信息
+
+ # Add time info
if rule.is_original_time and context.time_info:
caption += context.time_info
-
- # 添加原始链接
+
+ # Add original link
if rule.is_original_link and context.original_link:
caption += context.original_link
-
- # 如果没有文本内容,添加默认描述
+
+ # If no text content, add default description
if not caption:
- # 根据文件类型设置描述
+ # Set description based on file type
caption = " "
# ext = os.path.splitext(str(file_path))[1].lower()
# if ext in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
- # caption = "收到一张图片"
+ # caption = "Received a photo"
# elif ext in ['.mp4', '.avi', '.mkv', '.mov', '.webm']:
- # caption = "收到一个视频"
+ # caption = "Received a video"
# elif ext in ['.mp3', '.wav', '.ogg', '.flac']:
- # caption = "收到一个音频文件"
+ # caption = "Received an audio file"
# else:
- # caption = f"收到一个文件 ({ext})"
-
- # 发送推送
+ # caption = f"Received a file ({ext})"
+
+ # Send push
await self._send_push_notification(push_configs, caption, file_path)
- # 添加到已处理文件列表
+ # Add to processed file list
processed_files.append(file_path)
-
+
except Exception as e:
- logger.error(f'推送单个媒体文件时出错: {str(e)}')
+ logger.error(f'Error pushing single media file: {str(e)}')
logger.error(traceback.format_exc())
raise
-
+
except Exception as e:
- logger.error(f'推送单条媒体消息时出错: {str(e)}')
+ logger.error(f'Error pushing single media message: {str(e)}')
logger.error(traceback.format_exc())
raise
finally:
- # 如果是自己下载的文件,需要清理
+ # If files were downloaded by us, need to clean up
if need_cleanup:
for file_path in files:
try:
if os.path.exists(str(file_path)):
os.remove(file_path)
- logger.info(f'删除临时文件: {file_path}')
- # 从已处理列表中移除
+ logger.info(f'Deleted temporary file: {file_path}')
+ # Remove from processed list
if file_path in processed_files:
processed_files.remove(file_path)
except Exception as e:
- logger.error(f'删除临时文件失败: {str(e)}')
-
- # 返回处理过但未删除的文件
+ logger.error(f'Failed to delete temporary file: {str(e)}')
+
+ # Return processed but not yet deleted files
return processed_files
-
+
async def _push_text_message(self, context, push_configs):
- """推送纯文本消息"""
+ """Push plain text message"""
rule = context.rule
-
+
if not context.message_text:
- logger.info('没有文本内容,不发送推送')
+ logger.info('No text content, not sending push')
return []
-
- # 组合消息文本
+
+ # Combine message text
message_text = ""
if rule.is_original_sender and context.sender_info:
message_text += context.sender_info
@@ -377,63 +377,63 @@ async def _push_text_message(self, context, push_configs):
message_text += context.time_info
if rule.is_original_link and context.original_link:
message_text += context.original_link
-
- # 发送推送
+
+ # Send push
await self._send_push_notification(push_configs, message_text)
- logger.info(f'文本消息推送已发送')
-
- # 返回空列表,表示没有处理任何文件
+ logger.info(f'Text message push sent')
+
+ # Return empty list, indicating no files were processed
return []
-
+
async def _send_push_notification(self, push_configs, body, attachment=None, all_attachments=None):
- """发送推送通知"""
+ """Send push notification"""
if not body and not attachment and not all_attachments:
- logger.warning('没有内容可推送')
+ logger.warning('No content to push')
return
-
+
for config in push_configs:
try:
- # 创建Apprise对象
+ # Create Apprise object
apobj = apprise.Apprise()
-
- # 添加推送服务
+
+ # Add push service
service_url = config.push_channel
if apobj.add(service_url):
- logger.info(f'成功添加推送服务: {service_url}')
+ logger.info(f'Successfully added push service: {service_url}')
else:
- logger.error(f'添加推送服务失败: {service_url}')
+ logger.error(f'Failed to add push service: {service_url}')
continue
-
- # 发送推送
+
+ # Send push
if all_attachments and len(all_attachments) > 0 and config.media_send_mode == "Multiple":
- # 尝试一次性发送所有附件
- logger.info(f'发送带{len(all_attachments)}个附件的推送,模式: {config.media_send_mode}')
+ # Try to send all attachments at once
+ logger.info(f'Sending push with {len(all_attachments)} attachments, mode: {config.media_send_mode}')
send_result = await asyncio.to_thread(
apobj.notify,
- body=body or f"收到{len(all_attachments)}个媒体文件",
+ body=body or f"Received {len(all_attachments)} media files",
attach=all_attachments
)
elif attachment and os.path.exists(str(attachment)):
- # 单附件推送
- logger.info(f'发送带单个附件的推送: {os.path.basename(str(attachment))}')
+ # Single attachment push
+ logger.info(f'Sending push with single attachment: {os.path.basename(str(attachment))}')
send_result = await asyncio.to_thread(
apobj.notify,
body=body or " ",
attach=attachment
)
else:
- # 纯文本推送
- logger.info('发送纯文本推送')
+ # Plain text push
+ logger.info('Sending plain text push')
send_result = await asyncio.to_thread(
apobj.notify,
body=body
)
-
+
if send_result:
- logger.info(f'推送发送成功: {service_url}')
+ logger.info(f'Push sent successfully: {service_url}')
else:
- logger.error(f'推送发送失败: {service_url}')
-
+ logger.error(f'Push sending failed: {service_url}')
+
except Exception as e:
- logger.error(f'发送推送时出错: {str(e)}')
- logger.error(traceback.format_exc())
\ No newline at end of file
+ logger.error(f'Error sending push: {str(e)}')
+ logger.error(traceback.format_exc())
diff --git a/filters/replace_filter.py b/filters/replace_filter.py
index f2567a8..6d45d02 100644
--- a/filters/replace_filter.py
+++ b/filters/replace_filter.py
@@ -6,39 +6,39 @@
class ReplaceFilter(BaseFilter):
"""
- 替换过滤器,根据规则替换消息文本
+ Replace filter, replaces message text based on rules
"""
-
+
async def _process(self, context):
"""
- 处理消息文本替换
-
+ Process message text replacement
+
Args:
- context: 消息上下文
-
+ context: Message context
+
Returns:
- bool: 是否继续处理
+ bool: Whether to continue processing
"""
rule = context.rule
message_text = context.message_text
- #打印context的所有属性
- # logger.info(f"ReplaceFilter处理消息前,context: {context.__dict__}")
- # 如果不需要替换,直接返回
+ # Print all attributes of context
+ # logger.info(f"Before ReplaceFilter processing, context: {context.__dict__}")
+ # If replacement is not needed, return directly
if not rule.is_replace or not message_text:
return True
-
+
try:
- # 应用所有替换规则
+ # Apply all replacement rules
for replace_rule in rule.replace_rules:
if replace_rule.pattern == '.*':
- # 全文替换
- logger.info(f'执行全文替换:\n原文: "{message_text}"\n替换为: "{replace_rule.content or ""}"')
+ # Full text replacement
+ logger.info(f'Performing full text replacement:\nOriginal: "{message_text}"\nReplaced with: "{replace_rule.content or ""}"')
message_text = replace_rule.content or ''
- break # 如果是全文替换,就不继续处理其他规则
+ break # If it's a full text replacement, don't continue processing other rules
else:
try:
- # 正则替换
+ # Regex replacement
old_text = message_text
matches = re.finditer(replace_rule.pattern, message_text)
message_text = re.sub(
@@ -48,19 +48,19 @@ async def _process(self, context):
)
if old_text != message_text:
matched_texts = [m.group(0) for m in matches]
- logger.info(f'执行部分替换:\n原文: "{old_text}"\n匹配内容: {matched_texts}\n替换规则: "{replace_rule.pattern}" -> "{replace_rule.content}"\n替换后: "{message_text}"')
+ logger.info(f'Performing partial replacement:\nOriginal: "{old_text}"\nMatched content: {matched_texts}\nReplacement rule: "{replace_rule.pattern}" -> "{replace_rule.content}"\nAfter replacement: "{message_text}"')
except re.error as e:
- logger.error(f'替换规则格式错误: {replace_rule.pattern}, 错误: {str(e)}')
-
- # 更新上下文中的消息文本
+ logger.error(f'Replacement rule format error: {replace_rule.pattern}, error: {str(e)}')
+
+ # Update message text in context
context.message_text = message_text
context.check_message_text = message_text
-
+
return True
except Exception as e:
- logger.error(f'应用替换规则时出错: {str(e)}')
- context.errors.append(f"替换规则错误: {str(e)}")
- return True # 即使替换出错,仍然继续处理
+ logger.error(f'Error applying replacement rules: {str(e)}')
+ context.errors.append(f"Replacement rule error: {str(e)}")
+ return True # Even if replacement fails, continue processing
finally:
- # logger.info(f"ReplaceFilter处理消息后,context: {context.__dict__}")
- pass
\ No newline at end of file
+ # logger.info(f"After ReplaceFilter processing, context: {context.__dict__}")
+ pass
diff --git a/filters/reply_filter.py b/filters/reply_filter.py
index 34270e7..15f24bd 100644
--- a/filters/reply_filter.py
+++ b/filters/reply_filter.py
@@ -8,65 +8,66 @@
class ReplyFilter(BaseFilter):
"""
- 回复过滤器,用于处理媒体组消息的评论区按钮
- 由于媒体组消息无法直接添加按钮,此过滤器会使用bot回复已转发的消息,并添加评论区按钮
+ Reply filter, used to handle comment section buttons for media group messages.
+ Since media group messages cannot have buttons added directly, this filter uses a bot to reply
+ to forwarded messages and add comment section buttons.
"""
-
+
async def _process(self, context):
"""
- 处理媒体组消息的评论区按钮
-
+ Handle comment section buttons for media group messages
+
Args:
- context: 消息上下文
-
+ context: Message context
+
Returns:
- bool: 是否继续处理
+ bool: Whether to continue processing
"""
try:
- # 如果规则不存在或未启用评论按钮功能,直接跳过
+ # If the rule doesn't exist or comment button feature is not enabled, skip directly
if not context.rule or not context.rule.enable_comment_button:
return True
-
- # 只处理媒体组消息
+
+ # Only process media group messages
if not context.is_media_group:
return True
-
- # 检查是否有评论区链接和已转发的消息
+
+ # Check if there is a comment section link and forwarded messages
if not context.comment_link or not context.forwarded_messages:
- logger.info("没有评论区链接或已转发消息,无法添加评论区按钮回复")
+ logger.info("No comment section link or forwarded messages, cannot add comment section button reply")
return True
-
- # 使用bot客户端(context.client)
+
+ # Use bot client (context.client)
client = context.client
-
- # 获取目标聊天信息
+
+ # Get target chat information
rule = context.rule
target_chat = rule.target_chat
target_chat_id = int(target_chat.telegram_chat_id)
-
- # 获取已转发的第一条消息ID
+
+ # Get the first forwarded message ID
first_forwarded_msg = context.forwarded_messages[0]
-
- # 创建评论区按钮
- comment_button = Button.url("💬 查看评论区", context.comment_link)
+
+ # Create comment section button
+ comment_button = Button.url("💬 View comments", context.comment_link)
buttons = [[comment_button]]
-
- # 回复已转发的媒体组消息
- logger.info(f"正在使用Bot给已转发的媒体组消息 {first_forwarded_msg.id} 发送评论区按钮回复")
-
- # 发送回复消息,附带评论区按钮
+
+ # Reply to the forwarded media group message
+ logger.info(f"Using Bot to send comment section button reply to forwarded media group message {first_forwarded_msg.id}")
+
+ # Send reply message with comment section button
await client.send_message(
entity=target_chat_id,
- message="💬 评论区",
+ message="💬 Comments",
buttons=buttons,
reply_to=first_forwarded_msg.id,
)
- logger.info("成功发送评论区按钮回复")
-
+ logger.info("Successfully sent comment section button reply")
+
return True
-
+
except Exception as e:
- logger.error(f"ReplyFilter处理消息时出错: {str(e)}")
+ logger.error(f"Error in ReplyFilter processing message: {str(e)}")
logger.error(traceback.format_exc())
- return True
\ No newline at end of file
+ return True
diff --git a/filters/rss_filter.py b/filters/rss_filter.py
index df23320..a8f0404 100644
--- a/filters/rss_filter.py
+++ b/filters/rss_filter.py
@@ -17,77 +17,77 @@
class RSSFilter(BaseFilter):
"""
- RSS过滤器,用于将符合条件的消息添加到RSS订阅源中
+ RSS filter, used to add messages that meet conditions to the RSS feed
"""
-
+
def __init__(self):
super().__init__()
self.rss_host = RSS_HOST
self.rss_port = RSS_PORT
self.rss_base_url = f"http://{self.rss_host}:{self.rss_port}"
-
- # 使用统一的路径常量
+
+ # Use unified path constants
self.rss_media_path = RSS_MEDIA_DIR
self.temp_dir = TEMP_DIR
-
- logger.info(f"RSS媒体文件根目录: {self.rss_media_path}")
- logger.info(f"临时文件存储路径: {self.temp_dir}")
-
- # 确保媒体文件存储根目录存在
+
+ logger.info(f"RSS media file root directory: {self.rss_media_path}")
+ logger.info(f"Temporary file storage path: {self.temp_dir}")
+
+ # Ensure the media file storage root directory exists
Path(self.rss_media_path).mkdir(parents=True, exist_ok=True)
-
+
def _get_rule_media_path(self, rule_id):
- """获取规则特定的媒体目录"""
+ """Get the rule-specific media directory"""
return get_rule_media_dir(rule_id)
-
+
async def _process(self, context):
- """处理RSS过滤器逻辑"""
-
+ """Process RSS filter logic"""
+
if not RSS_ENABLED:
- logger.info("RSS未启用,跳过RSS处理")
+ logger.info("RSS is not enabled, skipping RSS processing")
return True
-
+
if not context.should_forward:
return False
-
+
db_ops = await get_db_ops()
session = get_session()
rss_config = await db_ops.get_rss_config(session, context.rule.id)
- logger.info(f"规则ID: {context.rule.id}")
- logger.info(f"RSS配置: {rss_config}")
+ logger.info(f"Rule ID: {context.rule.id}")
+ logger.info(f"RSS config: {rss_config}")
- # 检查RSS配置是否存在
+ # Check if RSS configuration exists
if rss_config is None:
- logger.error(f"找不到规则ID为 {context.rule.id} 的RSS配置,跳过RSS处理")
+ logger.error(f"Cannot find RSS configuration for rule ID {context.rule.id}, skipping RSS processing")
session.close()
return True
-
- # 检查是否启用RSS
+
+ # Check if RSS is enabled
if not rss_config.enable_rss:
- logger.info(f"规则ID为 {context.rule.id} 的RSS未启用,跳过RSS处理")
+ logger.info(f"RSS is not enabled for rule ID {context.rule.id}, skipping RSS processing")
session.close()
return True
- # 执行RSS规则前,先确保媒体文件已经下载
- # 媒体组消息需要特殊处理
+ # Before executing RSS rule, ensure media files have been downloaded
+ # Media group messages need special handling
if context.is_media_group:
rule = context.rule
await self._process_media_group(context, rule)
else:
- # 获取消息和规则
+ # Get message and rule
message = context.event.message
client = context.client
rule = context.rule
-
+
try:
- # 准备条目数据
+ # Prepare entry data
entry_data = await self._prepare_entry_data(client, message, rule, context)
-
- # 如果准备数据失败,记录错误并尝试生成简单的数据
+
+ # If data preparation failed, log error and try to generate simple data
if entry_data is None:
- logger.warning("生成RSS条目数据失败,尝试创建简单数据")
- # 尝试从消息中提取最基本的信息
- message_text = getattr(message, 'text', '') or getattr(message, 'caption', '') or '文件消息'
+ logger.warning("Failed to generate RSS entry data, attempting to create simple data")
+ # Try to extract the most basic information from the message
+ message_text = getattr(message, 'text', '') or getattr(message, 'caption', '') or 'File message'
entry_data = {
"id": str(message.id),
"title": message_text[:20] + ('...' if len(message_text) > 20 else ''),
@@ -97,8 +97,8 @@ async def _process(self, context):
"link": "",
"media": []
}
-
- # 如果消息有媒体,尝试处理
+
+ # If the message has media, try to process it
if hasattr(message, 'media') and message.media:
media_info = await self._process_media(client, message, context)
if media_info:
@@ -106,56 +106,56 @@ async def _process(self, context):
entry_data["media"].extend(media_info)
else:
entry_data["media"].append(media_info)
-
- # 发送到RSS服务
+
+ # Send to RSS service
if entry_data:
success = await self._send_to_rss_service(rule.id, entry_data)
if success:
- logger.info(f"成功将消息添加到规则 {rule.id} 的RSS订阅源")
+ logger.info(f"Successfully added message to RSS feed for rule {rule.id}")
else:
- logger.error(f"无法将消息添加到规则 {rule.id} 的RSS订阅源")
+ logger.error(f"Failed to add message to RSS feed for rule {rule.id}")
else:
- logger.error("无法生成有效的RSS条目数据")
-
+ logger.error("Unable to generate valid RSS entry data")
+
except Exception as e:
- logger.error(f"RSS处理时出错: {str(e)}")
-
+ logger.error(f"Error during RSS processing: {str(e)}")
+
if rule.only_rss:
- logger.info('只转发到RSS,RSS过滤器已完成,结束过滤链')
+ logger.info('Only forwarding to RSS, RSS filter completed, ending filter chain')
return False
-
+
return True
-
+
async def _prepare_entry_data(self, client, message, rule, context=None):
- """准备RSS条目数据"""
+ """Prepare RSS entry data"""
try:
- # 获取标题(使用自定义方法)
+ # Get title (using custom method)
title = self._get_message_title(message)
-
- # 安全获取消息内容
+
+ # Safely get message content
content = ""
if hasattr(message, 'text') and message.text:
content = message.text
elif hasattr(message, 'caption') and message.caption:
content = message.caption
-
- # 获取发送人名称
+
+ # Get sender name
author = await self._get_sender_name(client, message)
-
- # 获取消息链接(如果有)
+
+ # Get message link (if available)
link = self._get_message_link(message)
-
- # 获取媒体(如果有)
+
+ # Get media (if available)
media_list = []
-
- # 处理媒体组消息
+
+ # Process media group messages
if context and hasattr(context, "is_media_group") and context.is_media_group:
- logger.debug("处理媒体组消息")
- # 由于媒体组已经在其他地方处理,这里不再重复处理
- # 仅记录
- logger.debug("媒体组在其他地方处理")
+ logger.debug("Processing media group messages")
+ # Since the media group has already been processed elsewhere, no need to process again here
+ # Only log
+ logger.debug("Media group processed elsewhere")
else:
- # 处理单个消息的媒体
+ # Process single message media
media_info = await self._process_media(client, message, context)
if media_info:
if isinstance(media_info, list):
@@ -163,28 +163,28 @@ async def _prepare_entry_data(self, client, message, rule, context=None):
else:
media_list.append(media_info)
elif media_list:
- logger.debug(f"_process_media返回了多个媒体: {len(media_list)}")
-
- # 检查媒体是否在skipped_media列表中
+ logger.debug(f"_process_media returned multiple media: {len(media_list)}")
+
+ # Check if media is in the skipped_media list
if context and hasattr(context, 'skipped_media') and context.skipped_media:
for skipped_msg, size, name in context.skipped_media:
if skipped_msg.id == message.id:
- logger.info(f"媒体文件 {name or ''} (大小: {size}MB) 已在skipped_media列表中,添加标记到条目数据")
- # 可以选择在content中添加标记,表明该媒体因大小超限而被跳过
- note = f"\n\n[注意:包含超过大小限制的媒体文件 {name or ''},大小: {size}MB]"
+ logger.info(f"Media file {name or ''} (size: {size}MB) is in the skipped_media list, adding marker to entry data")
+ # Optionally add a marker in content indicating the media was skipped due to size limit
+ note = f"\n\n[Note: Contains media file exceeding size limit {name or ''}, size: {size}MB]"
if hasattr(message, 'text') and message.text:
content = message.text + note
elif hasattr(message, 'caption') and message.caption:
content = message.caption + note
else:
content = note.strip()
-
- # 尝试记录媒体信息
+
+ # Try to log media information
if media_list:
for i, media in enumerate(media_list):
- logger.debug(f"媒体{i+1}: {media.get('filename', 'unknown')}, 类型: {media.get('type', 'unknown')}, 原始文件名: {media.get('original_name', 'unknown')}")
-
- # 构建条目数据
+ logger.debug(f"Media {i+1}: {media.get('filename', 'unknown')}, type: {media.get('type', 'unknown')}, original filename: {media.get('original_name', 'unknown')}")
+
+ # Build entry data
entry_data = {
"id": str(message.id),
"title": title,
@@ -196,147 +196,147 @@ async def _prepare_entry_data(self, client, message, rule, context=None):
"original_link": context.original_link,
"sender_info": context.sender_info,
}
-
+
return entry_data
-
+
except Exception as e:
- logger.error(f"准备RSS条目数据时出错: {str(e)}")
+ logger.error(f"Error preparing RSS entry data: {str(e)}")
return None
-
+
def _get_message_title(self, message):
- """获取消息标题"""
- # 使用消息的前20个字符作为标题
+ """Get message title"""
+ # Use the first 20 characters of the message as the title
text = ""
if hasattr(message, 'text') and message.text:
text = message.text
elif hasattr(message, 'caption') and message.caption:
text = message.caption
-
+
title = text.split('\n')[0][:20].strip() + "..." if text and len(text.split('\n')[0]) >= 20 else text.split('\n')[0].strip() if text else ""
-
- # 如果标题为空,使用默认标题
+
+ # If the title is empty, use a default title
if not title:
- # 检测各种媒体类型
+ # Detect various media types
has_photo = hasattr(message, 'photo') and message.photo
has_video = hasattr(message, 'video') and message.video
has_document = hasattr(message, 'document') and message.document
has_audio = hasattr(message, 'audio') and message.audio
has_voice = hasattr(message, 'voice') and message.voice
-
+
if has_photo:
- title = "图片消息"
+ title = "Photo message"
elif has_video:
- title = "视频消息"
+ title = "Video message"
elif has_document:
doc_name = ""
if hasattr(message.document, 'file_name') and message.document.file_name:
doc_name = message.document.file_name
- title = f"文件: {doc_name}" if doc_name else "文件消息"
+ title = f"File: {doc_name}" if doc_name else "File message"
elif has_audio:
audio_name = ""
if hasattr(message.audio, 'file_name') and message.audio.file_name:
audio_name = message.audio.file_name
- title = f"音频: {audio_name}" if audio_name else "音频消息"
+ title = f"Audio: {audio_name}" if audio_name else "Audio message"
elif has_voice:
- title = "语音消息"
+ title = "Voice message"
else:
- title = "新消息"
-
+ title = "New message"
+
return title
-
+
async def _get_sender_name(self, client, message):
- """获取发送者名称"""
+ """Get sender name"""
try:
- # 检查是否是频道消息
+ # Check if it's a channel message
if hasattr(message, 'sender_chat') and message.sender_chat:
return message.sender_chat.title
- # 检查是否有发送者信息
+ # Check if there is sender information
elif hasattr(message, 'from_user') and message.from_user:
return message.from_user.first_name + (f" {message.from_user.last_name}" if message.from_user.last_name else "")
- # 尝试从聊天获取名称
+ # Try to get name from chat
elif hasattr(message, 'chat') and message.chat:
if hasattr(message.chat, 'title') and message.chat.title:
return message.chat.title
elif hasattr(message.chat, 'first_name'):
return message.chat.first_name + (f" {message.chat.last_name}" if hasattr(message.chat, 'last_name') and message.chat.last_name else "")
- return "未知用户"
+ return "Unknown user"
except Exception as e:
- logger.error(f"获取发送者名称时出错: {str(e)}")
- return "未知用户"
-
+ logger.error(f"Error getting sender name: {str(e)}")
+ return "Unknown user"
+
def _get_message_link(self, message):
- """获取消息链接"""
+ """Get message link"""
try:
if hasattr(message, 'chat') and message.chat:
chat_id = getattr(message.chat, 'id', None)
username = getattr(message.chat, 'username', None)
message_id = getattr(message, 'id', None)
-
+
if message_id is None:
return ""
-
+
if username:
return f"https://t.me/{username}/{message_id}"
elif chat_id:
- # 使用chat_id创建链接
+ # Create link using chat_id
chat_id_str = str(chat_id)
- # 移除前导负号(如果有)
+ # Remove leading negative sign (if present)
if chat_id_str.startswith('-100'):
- chat_id_str = chat_id_str[4:] # 去掉'-100'
+ chat_id_str = chat_id_str[4:] # Remove '-100'
elif chat_id_str.startswith('-'):
- chat_id_str = chat_id_str[1:] # 去掉'-'
+ chat_id_str = chat_id_str[1:] # Remove '-'
return f"https://t.me/c/{chat_id_str}/{message_id}"
return ""
except Exception as e:
- logger.error(f"获取消息链接时出错: {str(e)}")
+ logger.error(f"Error getting message link: {str(e)}")
return ""
-
+
async def _process_media(self, client, message, context=None, rule_id=None):
- """处理媒体内容"""
+ """Process media content"""
media_list = []
-
+
try:
- # 检查消息是否在skipped_media列表中
+ # Check if the message is in the skipped_media list
if context and hasattr(context, 'skipped_media') and context.skipped_media:
for skipped_msg, size, name in context.skipped_media:
if skipped_msg.id == message.id:
- logger.info(f"媒体文件 {name or ''} (大小: {size}MB) 已在skipped_media列表中,RSS过滤器跳过下载")
+ logger.info(f"Media file {name or ''} (size: {size}MB) is in the skipped_media list, RSS filter skipping download")
return media_list
- # 处理文档类型
+ # Process document type
if hasattr(message, 'document') and message.document:
- # 获取原始文件名
+ # Get original filename
original_name = None
for attr in message.document.attributes:
if hasattr(attr, 'file_name'):
original_name = attr.file_name
break
-
- # 生成文件名
+
+ # Generate filename
message_id = getattr(message, 'id', 'unknown')
file_name = original_name if original_name else f"document_{message_id}"
file_name = self._sanitize_filename(file_name)
-
- # 获取规则ID,优先使用传入的rule_id
+
+ # Get rule ID, prefer the passed-in rule_id
current_rule_id = rule_id
if current_rule_id is None and context and hasattr(context, 'rule') and hasattr(context.rule, 'id'):
current_rule_id = context.rule.id
-
- # 使用规则特定的媒体目录
+
+ # Use rule-specific media directory
rule_media_path = self._get_rule_media_path(current_rule_id) if current_rule_id else self.rss_media_path
-
- # 下载文件
+
+ # Download file
local_path = os.path.join(rule_media_path, file_name)
try:
if not os.path.exists(local_path):
await message.download_media(local_path)
- logger.info(f"下载媒体文件到: {local_path}")
-
- # 获取文件大小和MIME类型
+ logger.info(f"Downloaded media file to: {local_path}")
+
+ # Get file size and MIME type
file_size = os.path.getsize(local_path)
mime_type = message.document.mime_type or mimetypes.guess_type(file_name)[0] or "application/octet-stream"
-
- # 添加到媒体列表,使用规则特定的URL
+
+ # Add to media list, using rule-specific URL
media_info = {
"url": f"/media/{current_rule_id}/{file_name}" if current_rule_id else f"/media/{file_name}",
"type": mime_type,
@@ -345,77 +345,77 @@ async def _process_media(self, client, message, context=None, rule_id=None):
"original_name": original_name or file_name
}
media_list.append(media_info)
- logger.info(f"添加文档到RSS: {file_name}, 原始文件名: {original_name or '未知'}")
+ logger.info(f"Added document to RSS: {file_name}, original filename: {original_name or 'unknown'}")
except Exception as e:
- logger.error(f"处理文档时出错: {str(e)}")
-
- # 处理图片类型
+ logger.error(f"Error processing document: {str(e)}")
+
+ # Process photo type
elif hasattr(message, 'photo') and message.photo:
message_id = getattr(message, 'id', 'unknown')
-
- # 获取规则ID,优先使用传入的rule_id
+
+ # Get rule ID, prefer the passed-in rule_id
current_rule_id = rule_id
if current_rule_id is None and context and hasattr(context, 'rule') and hasattr(context.rule, 'id'):
current_rule_id = context.rule.id
-
- # 使用规则特定的媒体目录
+
+ # Use rule-specific media directory
rule_media_path = self._get_rule_media_path(current_rule_id) if current_rule_id else self.rss_media_path
local_path = os.path.join(rule_media_path, f"photo_{message_id}.jpg")
-
+
try:
if not os.path.exists(local_path):
await message.download_media(local_path)
- logger.info(f"下载图片到: {local_path}")
-
- # 获取文件大小
+ logger.info(f"Downloaded photo to: {local_path}")
+
+ # Get file size
file_size = os.path.getsize(local_path)
-
- # 添加到媒体列表,使用规则特定的URL
+
+ # Add to media list, using rule-specific URL
media_info = {
"url": f"/media/{current_rule_id}/{f'photo_{message_id}.jpg'}" if current_rule_id else f"/media/{f'photo_{message_id}.jpg'}",
"type": "image/jpeg",
"size": file_size,
"filename": f"photo_{message_id}.jpg",
- "original_name": "photo.jpg" # 照片没有原始文件名
+ "original_name": "photo.jpg" # Photos don't have original filenames
}
media_list.append(media_info)
- logger.info(f"添加图片到RSS: {f'photo_{message_id}.jpg'}")
+ logger.info(f"Added photo to RSS: {f'photo_{message_id}.jpg'}")
except Exception as e:
- logger.error(f"处理图片时出错: {str(e)}")
-
- # 处理视频类型
+ logger.error(f"Error processing photo: {str(e)}")
+
+ # Process video type
elif hasattr(message, 'video') and message.video:
message_id = getattr(message, 'id', 'unknown')
-
- # 获取原始文件名
+
+ # Get original filename
original_name = None
for attr in message.video.attributes:
if hasattr(attr, 'file_name'):
original_name = attr.file_name
break
-
+
file_name = original_name if original_name else f"video_{message_id}.mp4"
file_name = self._sanitize_filename(file_name)
-
- # 获取规则ID,优先使用传入的rule_id
+
+ # Get rule ID, prefer the passed-in rule_id
current_rule_id = rule_id
if current_rule_id is None and context and hasattr(context, 'rule') and hasattr(context.rule, 'id'):
current_rule_id = context.rule.id
-
- # 使用规则特定的媒体目录
+
+ # Use rule-specific media directory
rule_media_path = self._get_rule_media_path(current_rule_id) if current_rule_id else self.rss_media_path
local_path = os.path.join(rule_media_path, file_name)
-
+
try:
if not os.path.exists(local_path):
await message.download_media(local_path)
- logger.info(f"下载视频到: {local_path}")
-
- # 获取文件大小和MIME类型
+ logger.info(f"Downloaded video to: {local_path}")
+
+ # Get file size and MIME type
file_size = os.path.getsize(local_path)
mime_type = message.video.mime_type or "video/mp4"
-
- # 添加到媒体列表,使用规则特定的URL
+
+ # Add to media list, using rule-specific URL
media_info = {
"url": f"/media/{current_rule_id}/{file_name}" if current_rule_id else f"/media/{file_name}",
"type": mime_type,
@@ -424,43 +424,43 @@ async def _process_media(self, client, message, context=None, rule_id=None):
"original_name": original_name or file_name
}
media_list.append(media_info)
- logger.info(f"添加视频到RSS: {file_name}")
+ logger.info(f"Added video to RSS: {file_name}")
except Exception as e:
- logger.error(f"处理视频时出错: {str(e)}")
-
- # 处理音频类型
+ logger.error(f"Error processing video: {str(e)}")
+
+ # Process audio type
elif hasattr(message, 'audio') and message.audio:
message_id = getattr(message, 'id', 'unknown')
-
- # 获取原始文件名
+
+ # Get original filename
original_name = None
for attr in message.audio.attributes:
if hasattr(attr, 'file_name'):
original_name = attr.file_name
break
-
+
file_name = original_name if original_name else f"audio_{message_id}.mp3"
file_name = self._sanitize_filename(file_name)
-
- # 获取规则ID,优先使用传入的rule_id
+
+ # Get rule ID, prefer the passed-in rule_id
current_rule_id = rule_id
if current_rule_id is None and context and hasattr(context, 'rule') and hasattr(context.rule, 'id'):
current_rule_id = context.rule.id
-
- # 使用规则特定的媒体目录
+
+ # Use rule-specific media directory
rule_media_path = self._get_rule_media_path(current_rule_id) if current_rule_id else self.rss_media_path
local_path = os.path.join(rule_media_path, file_name)
-
+
try:
if not os.path.exists(local_path):
await message.download_media(local_path)
- logger.info(f"下载音频到: {local_path}")
-
- # 获取文件大小和MIME类型
+ logger.info(f"Downloaded audio to: {local_path}")
+
+ # Get file size and MIME type
file_size = os.path.getsize(local_path)
mime_type = message.audio.mime_type or "audio/mpeg"
-
- # 添加到媒体列表,使用规则特定的URL
+
+ # Add to media list, using rule-specific URL
media_info = {
"url": f"/media/{current_rule_id}/{file_name}" if current_rule_id else f"/media/{file_name}",
"type": mime_type,
@@ -469,138 +469,138 @@ async def _process_media(self, client, message, context=None, rule_id=None):
"original_name": original_name or file_name
}
media_list.append(media_info)
- logger.info(f"添加音频到RSS: {file_name}")
+ logger.info(f"Added audio to RSS: {file_name}")
except Exception as e:
- logger.error(f"处理音频时出错: {str(e)}")
-
- # 处理语音消息
+ logger.error(f"Error processing audio: {str(e)}")
+
+ # Process voice message
elif hasattr(message, 'voice') and message.voice:
message_id = getattr(message, 'id', 'unknown')
file_name = f"voice_{message_id}.ogg"
-
- # 获取规则ID,优先使用传入的rule_id
+
+ # Get rule ID, prefer the passed-in rule_id
current_rule_id = rule_id
if current_rule_id is None and context and hasattr(context, 'rule') and hasattr(context.rule, 'id'):
current_rule_id = context.rule.id
-
- # 使用规则特定的媒体目录
+
+ # Use rule-specific media directory
rule_media_path = self._get_rule_media_path(current_rule_id) if current_rule_id else self.rss_media_path
local_path = os.path.join(rule_media_path, file_name)
-
+
try:
if not os.path.exists(local_path):
await message.download_media(local_path)
- logger.info(f"下载语音到: {local_path}")
-
- # 获取文件大小
+ logger.info(f"Downloaded voice to: {local_path}")
+
+ # Get file size
file_size = os.path.getsize(local_path)
-
- # 添加到媒体列表,使用规则特定的URL
+
+ # Add to media list, using rule-specific URL
media_info = {
"url": f"/media/{current_rule_id}/{file_name}" if current_rule_id else f"/media/{file_name}",
"type": "audio/ogg",
"size": file_size,
"filename": file_name,
- "original_name": "voice.ogg" # 语音消息没有原始文件名
+ "original_name": "voice.ogg" # Voice messages don't have original filenames
}
media_list.append(media_info)
- logger.info(f"添加语音到RSS: {file_name}")
+ logger.info(f"Added voice to RSS: {file_name}")
except Exception as e:
- logger.error(f"处理语音时出错: {str(e)}")
-
+ logger.error(f"Error processing voice: {str(e)}")
+
except Exception as e:
- logger.error(f"处理媒体内容时出错: {str(e)}")
-
+ logger.error(f"Error processing media content: {str(e)}")
+
return media_list
-
+
def _sanitize_filename(self, filename):
- """处理文件名,去除不合法字符"""
- # 替换Windows和Unix系统不支持的文件名字符
+ """Process filename, remove invalid characters"""
+ # Replace filename characters not supported by Windows and Unix systems
invalid_chars = '<>:"/\\|?*'
for char in invalid_chars:
filename = filename.replace(char, '_')
return filename
-
+
async def _send_to_rss_service(self, rule_id, entry_data):
- """发送数据到RSS服务"""
+ """Send data to RSS service"""
try:
url = f"{self.rss_base_url}/api/entries/{rule_id}/add"
-
- # 记录要发送的数据(只记录非二进制数据)
+
+ # Log the data to be sent (only log non-binary data)
debug_data = entry_data.copy()
if "media" in debug_data:
media_files = []
for media in debug_data["media"]:
if isinstance(media, dict):
- original_name = media.get("original_name", "未知")
- filename = media.get("filename", "未知")
+ original_name = media.get("original_name", "unknown")
+ filename = media.get("filename", "unknown")
media_files.append(f"{original_name}({filename})")
else:
media_files.append(str(media))
- debug_data["media"] = f"{len(debug_data['media'])} 个媒体文件: {', '.join(media_files)}"
- logger.info(f"发送到RSS服务: {url}, 数据: {debug_data}")
-
+ debug_data["media"] = f"{len(debug_data['media'])} media files: {', '.join(media_files)}"
+ logger.info(f"Sending to RSS service: {url}, data: {debug_data}")
+
async with aiohttp.ClientSession() as session:
async with session.post(url, json=entry_data) as response:
response_text = await response.text()
if response.status != 200:
- logger.error(f"发送到RSS服务失败: {response.status} - {response_text}")
+ logger.error(f"Failed to send to RSS service: {response.status} - {response_text}")
return False
-
- logger.info(f"成功发送到RSS服务, 规则ID: {rule_id}, 响应: {response_text}")
+
+ logger.info(f"Successfully sent to RSS service, rule ID: {rule_id}, response: {response_text}")
return True
-
+
except Exception as e:
- logger.error(f"发送到RSS服务时出错: {str(e)}")
+ logger.error(f"Error sending to RSS service: {str(e)}")
return False
-
+
async def _process_media_group(self, context, rule):
- """处理媒体组消息"""
+ """Process media group messages"""
try:
- # 获取规则ID
+ # Get rule ID
rule_id = rule.id
-
- # 获取规则特定的媒体目录
+
+ # Get rule-specific media directory
rule_media_path = self._get_rule_media_path(rule_id)
-
- # 获取已下载的本地媒体文件
+
+ # Get already downloaded local media files
local_media_files = []
if hasattr(context, 'media_files') and context.media_files:
local_media_files = context.media_files
-
- # 记录已下载的媒体文件数量
- logger.info(f"处理媒体组消息,已下载的媒体文件: {len(local_media_files)}")
-
- # 准备媒体列表
+
+ # Log the number of downloaded media files
+ logger.info(f"Processing media group messages, downloaded media files: {len(local_media_files)}")
+
+ # Prepare media list
media_list = []
-
- # 如果有已下载的媒体文件,使用它们
+
+ # If there are already downloaded media files, use them
if local_media_files:
- # 使用已下载的媒体文件
+ # Use already downloaded media files
for local_file in local_media_files:
try:
- # 从文件名猜测媒体类型
+ # Guess media type from filename
media_type = mimetypes.guess_type(local_file)[0] or "application/octet-stream"
filename = os.path.basename(local_file)
-
- # 复制文件到规则特定的RSS媒体目录
+
+ # Copy file to rule-specific RSS media directory
target_path = os.path.join(rule_media_path, filename)
if not os.path.exists(target_path):
shutil.copy2(local_file, target_path)
- logger.info(f"复制媒体文件到: {target_path}")
-
- # 获取文件大小
+ logger.info(f"Copied media file to: {target_path}")
+
+ # Get file size
file_size = os.path.getsize(target_path)
-
- # 尝试从原始消息中获取文件名
+
+ # Try to get filename from original message
original_name = None
for msg in context.media_group_messages:
if hasattr(msg, 'document') and msg.document:
original_name = getattr(msg.document, 'file_name', None)
if original_name:
break
-
- # 添加到媒体列表,使用规则特定的URL
+
+ # Add to media list, using rule-specific URL
media_info = {
"url": f"/media/{rule_id}/{filename}",
"type": media_type,
@@ -609,131 +609,131 @@ async def _process_media_group(self, context, rule):
"original_name": original_name or filename
}
media_list.append(media_info)
- logger.info(f"添加媒体组文件到RSS: {filename}, 原始文件名: {original_name or '未知'}")
+ logger.info(f"Added media group file to RSS: {filename}, original filename: {original_name or 'unknown'}")
except Exception as e:
- logger.error(f"处理媒体组文件时出错: {str(e)}")
+ logger.error(f"Error processing media group file: {str(e)}")
else:
- # 没有已下载的媒体文件,尝试直接从媒体组消息下载
+ # No already downloaded media files, try to download directly from media group messages
if hasattr(context, 'media_group_messages') and context.media_group_messages:
- logger.warning("媒体组没有已下载的文件,尝试从media_group_messages获取")
-
- # 直接处理媒体组消息
+ logger.warning("Media group has no downloaded files, trying to get from media_group_messages")
+
+ # Process media group messages directly
for msg in context.media_group_messages:
try:
- # 检查消息是否在skipped_media列表中
+ # Check if the message is in the skipped_media list
if hasattr(context, 'skipped_media') and context.skipped_media:
skip_msg = False
for skipped_msg, size, name in context.skipped_media:
if skipped_msg.id == msg.id:
- logger.info(f"媒体组中的媒体文件 {name or ''} (大小: {size}MB) 已在skipped_media列表中,RSS过滤器跳过下载")
+ logger.info(f"Media file {name or ''} (size: {size}MB) in media group is in the skipped_media list, RSS filter skipping download")
skip_msg = True
break
if skip_msg:
continue
- # 处理图片类型
+ # Process photo type
if hasattr(msg, 'photo') and msg.photo:
message_id = getattr(msg, 'id', 'unknown')
file_name = f"photo_{message_id}.jpg"
-
+
try:
- # 使用规则特定的媒体目录
+ # Use rule-specific media directory
local_path = os.path.join(rule_media_path, file_name)
-
- # 如果文件已存在且大小正常,跳过下载
+
+ # If the file already exists and has valid size, skip download
if os.path.exists(local_path) and os.path.getsize(local_path) > 0:
- logger.info(f"媒体文件已存在,跳过下载: {local_path}")
+ logger.info(f"Media file already exists, skipping download: {local_path}")
else:
try:
await msg.download_media(local_path)
- logger.info(f"直接下载图片到: {local_path}")
+ logger.info(f"Directly downloaded photo to: {local_path}")
except Exception as e:
if "file reference has expired" in str(e):
- logger.warning(f"文件引用已过期,尝试重新获取消息")
+ logger.warning(f"File reference has expired, trying to re-fetch message")
try:
- # 尝试重新获取消息
+ # Try to re-fetch the message
refreshed_msg = await context.client.get_messages(
msg.chat_id, ids=msg.id
)
if refreshed_msg:
await refreshed_msg.download_media(local_path)
- logger.info(f"成功重新下载图片到: {local_path}")
+ logger.info(f"Successfully re-downloaded photo to: {local_path}")
else:
- logger.error("无法重新获取消息")
+ logger.error("Unable to re-fetch message")
continue
except Exception as refresh_error:
- logger.error(f"重新获取消息时出错: {str(refresh_error)}")
+ logger.error(f"Error re-fetching message: {str(refresh_error)}")
continue
else:
- logger.error(f"下载媒体组图片时出错: {str(e)}")
+ logger.error(f"Error downloading media group photo: {str(e)}")
continue
-
- # 获取文件大小
+
+ # Get file size
if os.path.exists(local_path):
file_size = os.path.getsize(local_path)
-
- # 添加到媒体列表,使用规则特定的URL
+
+ # Add to media list, using rule-specific URL
media_info = {
"url": f"/media/{rule_id}/{file_name}",
"type": "image/jpeg",
"size": file_size,
"filename": file_name,
- "original_name": "photo.jpg" # 照片没有原始文件名
+ "original_name": "photo.jpg" # Photos don't have original filenames
}
media_list.append(media_info)
- logger.info(f"添加媒体组图片到RSS: {file_name}")
+ logger.info(f"Added media group photo to RSS: {file_name}")
except Exception as e:
- logger.error(f"处理媒体组图片时出错: {str(e)}")
+ logger.error(f"Error processing media group photo: {str(e)}")
elif hasattr(msg, 'document') and msg.document:
- # 获取消息ID,用于生成默认文件名
+ # Get message ID for generating default filename
message_id = getattr(msg, 'id', 'unknown')
original_name = None
for attr in msg.document.attributes:
if hasattr(attr, 'file_name'):
original_name = attr.file_name
break
-
+
file_name = original_name if original_name else f"document_{message_id}"
file_name = self._sanitize_filename(file_name)
-
+
try:
- # 使用规则特定的媒体目录
+ # Use rule-specific media directory
local_path = os.path.join(rule_media_path, file_name)
-
- # 如果文件已存在且大小正常,跳过下载
+
+ # If the file already exists and has valid size, skip download
if os.path.exists(local_path) and os.path.getsize(local_path) > 0:
- logger.info(f"媒体文件已存在,跳过下载: {local_path}")
+ logger.info(f"Media file already exists, skipping download: {local_path}")
else:
try:
await msg.download_media(local_path)
- logger.info(f"直接下载文档到: {local_path}")
+ logger.info(f"Directly downloaded document to: {local_path}")
except Exception as e:
if "file reference has expired" in str(e):
- logger.warning(f"文件引用已过期,尝试重新获取消息")
+ logger.warning(f"File reference has expired, trying to re-fetch message")
try:
- # 尝试重新获取消息
+ # Try to re-fetch the message
refreshed_msg = await context.client.get_messages(
msg.chat_id, ids=msg.id
)
if refreshed_msg:
await refreshed_msg.download_media(local_path)
- logger.info(f"成功重新下载文档到: {local_path}")
+ logger.info(f"Successfully re-downloaded document to: {local_path}")
else:
- logger.error("无法重新获取消息")
+ logger.error("Unable to re-fetch message")
continue
except Exception as refresh_error:
- logger.error(f"重新获取消息时出错: {str(refresh_error)}")
+ logger.error(f"Error re-fetching message: {str(refresh_error)}")
continue
else:
- logger.error(f"下载媒体组文档时出错: {str(e)}")
+ logger.error(f"Error downloading media group document: {str(e)}")
continue
-
- # 获取文件大小和MIME类型
+
+ # Get file size and MIME type
if os.path.exists(local_path):
file_size = os.path.getsize(local_path)
mime_type = msg.document.mime_type or mimetypes.guess_type(file_name)[0] or "application/octet-stream"
-
- # 添加到媒体列表,使用规则特定的URL
+
+ # Add to media list, using rule-specific URL
media_info = {
"url": f"/media/{rule_id}/{file_name}",
"type": mime_type,
@@ -742,32 +742,32 @@ async def _process_media_group(self, context, rule):
"original_name": original_name or file_name
}
media_list.append(media_info)
- logger.info(f"添加媒体组文档到RSS: {file_name}, 原始文件名: {original_name or '未知'}")
+ logger.info(f"Added media group document to RSS: {file_name}, original filename: {original_name or 'unknown'}")
except Exception as e:
- logger.error(f"处理媒体组文档时出错: {str(e)}")
-
- # 其他媒体类型处理可以类似添加
-
+ logger.error(f"Error processing media group document: {str(e)}")
+
+ # Other media types can be handled similarly
+
except Exception as e:
- logger.error(f"处理媒体组消息时出错: {str(e)}")
-
- # 准备条目数据
- # 获取消息文本内容
+ logger.error(f"Error processing media group message: {str(e)}")
+
+ # Prepare entry data
+ # Get message text content
message_text = context.message_text or ""
-
- # 构建标题:优先使用消息文本内容,没有文本内容时使用默认标题
+
+ # Build title: prefer message text content, use default title when there's no text content
if message_text.strip():
- # 使用第一行文本或前30个字符(以较短者为准)作为标题
+ # Use the first line of text or the first 30 characters (whichever is shorter) as the title
first_line = message_text.split('\n')[0].strip()
title = first_line[:30] + ('...' if len(first_line) > 30 else '')
else:
- # 没有文本内容时,使用默认标题
+ # When there's no text content, use default title
if media_list:
- title = f"媒体组消息 ({len(media_list)}个文件)"
+ title = f"Media group message ({len(media_list)} files)"
else:
- title = "媒体组消息"
-
- # 构建条目数据
+ title = "Media group message"
+
+ # Build entry data
entry_data = {
"id": str(context.event.message.id),
"title": title,
@@ -777,19 +777,19 @@ async def _process_media_group(self, context, rule):
"link": self._get_message_link(context.event.message),
"media": media_list
}
-
- # 记录媒体组条目数据
- logger.info(f"媒体组条目数据: 标题={title}, 媒体数量={len(media_list)}")
-
- # 如果有有效的媒体文件,添加到RSS订阅源
+
+ # Log media group entry data
+ logger.info(f"Media group entry data: title={title}, media count={len(media_list)}")
+
+ # If there are valid media files, add to RSS feed
if media_list:
await self._send_to_rss_service(rule.id, entry_data)
- logger.info(f"成功将媒体组消息添加到规则 {rule.id} 的RSS订阅源")
+ logger.info(f"Successfully added media group message to RSS feed for rule {rule.id}")
else:
- logger.warning("媒体组消息没有有效的媒体文件,跳过添加到RSS订阅源")
-
+ logger.warning("Media group message has no valid media files, skipping addition to RSS feed")
+
except Exception as e:
- logger.error(f"处理媒体组消息时出错: {str(e)}")
+ logger.error(f"Error processing media group message: {str(e)}")
return False
-
+
return True
diff --git a/filters/sender_filter.py b/filters/sender_filter.py
index a22864f..59d87c8 100644
--- a/filters/sender_filter.py
+++ b/filters/sender_filter.py
@@ -8,116 +8,116 @@
class SenderFilter(BaseFilter):
"""
- 消息发送过滤器,用于发送处理后的消息
+ Message sending filter, used to send processed messages
"""
-
+
async def _process(self, context):
"""
- 发送处理后的消息
-
+ Send processed messages
+
Args:
- context: 消息上下文
-
+ context: Message context
+
Returns:
- bool: 是否继续处理
+ bool: Whether to continue processing
"""
rule = context.rule
client = context.client
event = context.event
-
+
if not context.should_forward:
- logger.info('消息不满足转发条件,跳过发送')
+ logger.info('Message does not meet forwarding conditions, skipping send')
return False
-
+
if rule.enable_only_push:
- logger.info('只转发到推送配置,跳过发送')
+ logger.info('Only forwarding to push configuration, skipping send')
return True
-
- # 获取目标聊天信息
+
+ # Get target chat information
target_chat = rule.target_chat
target_chat_id = int(target_chat.telegram_chat_id)
-
- # 预先获取目标聊天实体
+
+ # Pre-fetch target chat entity
try:
entity = None
try:
- # 直接使用ID
+ # Use ID directly
entity = await client.get_entity(target_chat_id)
- logger.info(f'成功获取目标聊天实体: {target_chat.name} (ID: {target_chat_id})')
+ logger.info(f'Successfully got target chat entity: {target_chat.name} (ID: {target_chat_id})')
except Exception as e1:
try:
- # 尝试添加-100前缀
+ # Try adding -100 prefix
if not str(target_chat_id).startswith('-100'):
super_group_id = int(f'-100{abs(target_chat_id)}')
entity = await client.get_entity(super_group_id)
- target_chat_id = super_group_id # 更新使用正确的ID
- logger.info(f'使用私有群组ID格式成功获取实体: {target_chat.name} (ID: {target_chat_id})')
+ target_chat_id = super_group_id # Update to use correct ID
+ logger.info(f'Successfully got entity using private group ID format: {target_chat.name} (ID: {target_chat_id})')
except Exception as e2:
try:
- # 尝试常规群组格式
+ # Try regular group format
if not str(target_chat_id).startswith('-'):
group_id = int(f'-{abs(target_chat_id)}')
entity = await client.get_entity(group_id)
- target_chat_id = group_id # 更新使用正确的ID
- logger.info(f'使用常规群组ID格式成功获取实体: {target_chat.name} (ID: {target_chat_id})')
+ target_chat_id = group_id # Update to use correct ID
+ logger.info(f'Successfully got entity using regular group ID format: {target_chat.name} (ID: {target_chat_id})')
except Exception as e3:
- logger.warning(f'无法获取目标聊天实体,尝试继续发送: {e1}, {e2}, {e3}')
+ logger.warning(f'Unable to get target chat entity, attempting to continue sending: {e1}, {e2}, {e3}')
except Exception as e:
- logger.warning(f'获取目标聊天实体时出错: {str(e)}')
-
- # 设置消息格式
- parse_mode = rule.message_mode.value # 使用枚举的值(字符串)
- logger.info(f'使用消息格式: {parse_mode}')
-
+ logger.warning(f'Error getting target chat entity: {str(e)}')
+
+ # Set message format
+ parse_mode = rule.message_mode.value # Use enum value (string)
+ logger.info(f'Using message format: {parse_mode}')
+
try:
- # 处理媒体组消息
+ # Process media group messages
if context.is_media_group or (context.media_group_messages and context.skipped_media):
- logger.info(f'准备发送媒体组消息')
+ logger.info(f'Preparing to send media group message')
await self._send_media_group(context, target_chat_id, parse_mode)
- # 处理单条媒体消息
+ # Process single media message
elif context.media_files or context.skipped_media:
- logger.info(f'准备发送单条媒体消息')
+ logger.info(f'Preparing to send single media message')
await self._send_single_media(context, target_chat_id, parse_mode)
- # 处理纯文本消息
+ # Process plain text message
else:
- logger.info(f'准备发送纯文本消息')
+ logger.info(f'Preparing to send plain text message')
await self._send_text_message(context, target_chat_id, parse_mode)
-
- logger.info(f'消息已发送到: {target_chat.name} ({target_chat_id})')
+
+ logger.info(f'Message sent to: {target_chat.name} ({target_chat_id})')
return True
except FloodWaitError as e:
wait_time = e.seconds
- logger.error(f'发送消息频率限制,需要等待 {wait_time} 秒')
- context.errors.append(f"发送消息频率限制,需要等待 {wait_time} 秒")
+ logger.error(f'Message sending rate limited, need to wait {wait_time} seconds')
+ context.errors.append(f"Message sending rate limited, need to wait {wait_time} seconds")
return False
except Exception as e:
- logger.error(f'发送消息时出错: {str(e)}')
- context.errors.append(f"发送消息错误: {str(e)}")
+ logger.error(f'Error sending message: {str(e)}')
+ context.errors.append(f"Message sending error: {str(e)}")
return False
-
+
async def _send_media_group(self, context, target_chat_id, parse_mode):
- """发送媒体组消息"""
+ """Send media group message"""
rule = context.rule
client = context.client
event = context.event
- # 初始化转发消息列表
+ # Initialize forwarded messages list
context.forwarded_messages = []
-
+
# if not context.media_group_messages:
- # logger.info(f'所有媒体都超限,发送文本和提示')
- # # 构建提示信息
+ # logger.info(f'All media exceeded limit, sending text and prompt')
+ # # Build prompt information
# text_to_send = context.message_text or ''
- # # 设置原始消息链接
- # context.original_link = f"\n原始消息: https://t.me/c/{str(event.chat_id)[4:]}/{event.message.id}"
-
- # # 添加每个超限文件的信息
+ # # Set original message link
+ # context.original_link = f"\nOriginal message: https://t.me/c/{str(event.chat_id)[4:]}/{event.message.id}"
+
+ # # Add information for each exceeded file
# for message, size, name in context.skipped_media:
- # text_to_send += f"\n\n⚠️ 媒体文件 {name if name else '未命名文件'} ({size}MB) 超过大小限制"
-
- # # 组合完整文本
+ # text_to_send += f"\n\n⚠️ Media file {name if name else 'unnamed file'} ({size}MB) exceeds size limit"
+
+ # # Combine complete text
# text_to_send = context.sender_info + text_to_send + context.time_info + context.original_link
-
+
# await client.send_message(
# target_chat_id,
# text_to_send,
@@ -125,10 +125,10 @@ async def _send_media_group(self, context, target_chat_id, parse_mode):
# link_preview=True,
# buttons=context.buttons
# )
- # logger.info(f'媒体组所有文件超限,已发送文本和提示')
+ # logger.info(f'All media group files exceeded limit, sent text and prompt')
# return
-
- # 如果有可以发送的媒体,作为一个组发送
+
+ # If there are media that can be sent, send as a group
files = []
try:
for message in context.media_group_messages:
@@ -136,29 +136,29 @@ async def _send_media_group(self, context, target_chat_id, parse_mode):
file_path = await message.download_media(os.path.join(os.getcwd(), 'temp'))
if file_path:
files.append(file_path)
-
- # 修改:保存下载的文件路径到context.media_files
+
+ # Modification: save downloaded file paths to context.media_files
if files:
- # 初始化 media_files 如果它不存在
+ # Initialize media_files if it doesn't exist
if not hasattr(context, 'media_files') or context.media_files is None:
context.media_files = []
- # 将当前下载的文件添加到列表中
+ # Add currently downloaded files to the list
context.media_files.extend(files)
- logger.info(f'已将 {len(files)} 个下载的媒体文件路径保存到context.media_files')
-
- # 添加发送者信息和消息文本
+ logger.info(f'Saved {len(files)} downloaded media file paths to context.media_files')
+
+ # Add sender info and message text
caption_text = context.sender_info + context.message_text
-
- # 如果有超限文件,添加提示信息
+
+ # If there are exceeded files, add prompt information
for message, size, name in context.skipped_media:
- caption_text += f"\n\n⚠️ 媒体文件 {name if name else '未命名文件'} ({size}MB) 超过大小限制"
-
+ caption_text += f"\n\n⚠️ Media file {name if name else 'unnamed file'} ({size}MB) exceeds size limit"
+
if context.skipped_media:
- context.original_link = f"\n原始消息: https://t.me/c/{str(event.chat_id)[4:]}/{event.message.id}"
- # 添加时间信息和原始链接
+ context.original_link = f"\nOriginal message: https://t.me/c/{str(event.chat_id)[4:]}/{event.message.id}"
+ # Add time info and original link
caption_text += context.time_info + context.original_link
-
- # 作为一个组发送所有文件
+
+ # Send all files as a group
sent_messages = await client.send_file(
target_chat_id,
files,
@@ -171,49 +171,49 @@ async def _send_media_group(self, context, target_chat_id, parse_mode):
PreviewMode.FOLLOW: context.event.message.media is not None
}[rule.is_preview]
)
- # 保存发送的消息到上下文
+ # Save sent messages to context
if isinstance(sent_messages, list):
context.forwarded_messages = sent_messages
else:
context.forwarded_messages = [sent_messages]
-
- logger.info(f'媒体组消息已发送,保存了 {len(context.forwarded_messages)} 条已转发消息')
+
+ logger.info(f'Media group message sent, saved {len(context.forwarded_messages)} forwarded messages')
except Exception as e:
- logger.error(f'发送媒体组消息时出错: {str(e)}')
+ logger.error(f'Error sending media group message: {str(e)}')
raise
finally:
- # 删除临时文件,但如果启用了推送则保留
+ # Delete temporary files, but keep them if push is enabled
if not rule.enable_push:
for file_path in files:
try:
os.remove(file_path)
- logger.info(f'删除临时文件: {file_path}')
+ logger.info(f'Deleted temporary file: {file_path}')
except Exception as e:
- logger.error(f'删除临时文件失败: {str(e)}')
+ logger.error(f'Failed to delete temporary file: {str(e)}')
else:
- logger.info(f'推送功能已启用,保留临时文件')
-
+ logger.info(f'Push feature enabled, keeping temporary files')
+
async def _send_single_media(self, context, target_chat_id, parse_mode):
- """发送单条媒体消息"""
+ """Send single media message"""
rule = context.rule
client = context.client
event = context.event
-
- logger.info(f'发送单条媒体消息')
-
- # 检查是否所有媒体都超限
+
+ logger.info(f'Sending single media message')
+
+ # Check if all media exceeded limit
if context.skipped_media and not context.media_files:
- # 构建提示信息
+ # Build prompt information
file_size = context.skipped_media[0][1]
file_name = context.skipped_media[0][2]
- original_link = f"\n原始消息: https://t.me/c/{str(event.chat_id)[4:]}/{event.message.id}"
-
+ original_link = f"\nOriginal message: https://t.me/c/{str(event.chat_id)[4:]}/{event.message.id}"
+
text_to_send = context.message_text or ''
- text_to_send += f"\n\n⚠️ 媒体文件 {file_name} ({file_size}MB) 超过大小限制"
+ text_to_send += f"\n\n⚠️ Media file {file_name} ({file_size}MB) exceeds size limit"
text_to_send = context.sender_info + text_to_send + context.time_info
-
+
text_to_send += original_link
-
+
await client.send_message(
target_chat_id,
text_to_send,
@@ -221,23 +221,23 @@ async def _send_single_media(self, context, target_chat_id, parse_mode):
link_preview=True,
buttons=context.buttons
)
- logger.info(f'媒体文件超过大小限制,仅转发文本')
+ logger.info(f'Media file exceeds size limit, only forwarding text')
return
-
- # 确保context.media_files存在
+
+ # Ensure context.media_files exists
if not hasattr(context, 'media_files') or context.media_files is None:
context.media_files = []
-
- # 发送媒体文件
+
+ # Send media files
for file_path in context.media_files:
try:
caption = (
- context.sender_info +
- context.message_text +
- context.time_info +
+ context.sender_info +
+ context.message_text +
+ context.time_info +
context.original_link
)
-
+
await client.send_file(
target_chat_id,
file_path,
@@ -250,40 +250,40 @@ async def _send_single_media(self, context, target_chat_id, parse_mode):
PreviewMode.FOLLOW: context.event.message.media is not None
}[rule.is_preview]
)
- logger.info(f'媒体消息已发送')
+ logger.info(f'Media message sent')
except Exception as e:
- logger.error(f'发送媒体消息时出错: {str(e)}')
+ logger.error(f'Error sending media message: {str(e)}')
raise
finally:
- # 删除临时文件,但如果启用了推送则保留
+ # Delete temporary files, but keep them if push is enabled
if not rule.enable_push:
try:
os.remove(file_path)
- logger.info(f'删除临时文件: {file_path}')
+ logger.info(f'Deleted temporary file: {file_path}')
except Exception as e:
- logger.error(f'删除临时文件失败: {str(e)}')
+ logger.error(f'Failed to delete temporary file: {str(e)}')
else:
- logger.info(f'推送功能已启用,保留临时文件: {file_path}')
-
+ logger.info(f'Push feature enabled, keeping temporary file: {file_path}')
+
async def _send_text_message(self, context, target_chat_id, parse_mode):
- """发送纯文本消息"""
+ """Send plain text message"""
rule = context.rule
client = context.client
-
+
if not context.message_text:
- logger.info('没有文本内容,不发送消息')
+ logger.info('No text content, not sending message')
return
-
- # 根据预览模式设置 link_preview
+
+ # Set link_preview based on preview mode
link_preview = {
PreviewMode.ON: True,
PreviewMode.OFF: False,
- PreviewMode.FOLLOW: context.event.message.media is not None # 跟随原消息
+ PreviewMode.FOLLOW: context.event.message.media is not None # Follow original message
}[rule.is_preview]
-
- # 组合消息文本
+
+ # Combine message text
message_text = context.sender_info + context.message_text + context.time_info + context.original_link
-
+
await client.send_message(
target_chat_id,
str(message_text),
@@ -291,4 +291,4 @@ async def _send_text_message(self, context, target_chat_id, parse_mode):
link_preview=link_preview,
buttons=context.buttons
)
- logger.info(f'{"带预览的" if link_preview else "无预览的"}文本消息已发送')
\ No newline at end of file
+ logger.info(f'{"Text message with preview" if link_preview else "Text message without preview"} sent')
diff --git a/handlers/bot_handler.py b/handlers/bot_handler.py
index 7bedf0d..d31751e 100644
--- a/handlers/bot_handler.py
+++ b/handlers/bot_handler.py
@@ -11,20 +11,20 @@
logger = logging.getLogger(__name__)
-# 确保 temp 目录存在
+# Ensure temp directory exists
os.makedirs(TEMP_DIR, exist_ok=True)
load_dotenv()
async def handle_command(client, event):
- """处理机器人命令"""
+ """Handle bot commands"""
- # 检查是否是管理员
+ # Check if user is admin
if not await is_admin(event):
return
- # 处理命令逻辑
+ # Process command logic
message = event.message
if not message.text:
return
@@ -35,22 +35,22 @@ async def handle_command(client, event):
user_id = int(user_id)
- # 链接转发功能
+ # Link forwarding feature
if not message.text.startswith('/') and chat_id == user_id:
- # 检查是否是 Telegram 消息链接且是用户自己的消息
- logger.info(f'进入链接转发功能:{message.text}')
+ # Check if it is a Telegram message link and is the user's own message
+ logger.info(f'Entering link forwarding feature: {message.text}')
if 't.me/' in message.text:
await handle_message_link(client, event)
return
if not message.text.startswith('/'):
return
- logger.info(f'收到管理员命令: {event.message.text}')
- # 分割命令,处理可能带有机器人用户名的情况
+ logger.info(f'Received admin command: {event.message.text}')
+ # Split command, handle possible bot username suffix
parts = message.text.split()
- command = parts[0].split('@')[0][1:] # 移除开头的 '/' 并处理可能的 @username
+ command = parts[0].split('@')[0][1:] # Remove leading '/' and handle possible @username
- # 命令处理器字典
+ # Command handler dictionary
command_handlers = {
'bind': lambda: handle_bind_command(event, client, parts),
'b': lambda: handle_bind_command(event, client, parts),
@@ -127,36 +127,36 @@ async def handle_command(client, event):
'dru': lambda: handle_delete_rss_user_command(event, command, parts),
}
- # 执行对应的命令处理器
+ # Execute the corresponding command handler
handler = command_handlers.get(command)
if handler:
await handler()
-# 注册回调处理器
+# Register callback handler
@events.register(events.CallbackQuery)
async def callback_handler(event):
- """回调处理器入口"""
- # 检查是否是管理员的回调
+ """Callback handler entry point"""
+ # Check if callback is from admin
if not await is_admin(event):
return
await handle_callback(event)
async def send_welcome_message(client):
- """发送欢迎消息"""
+ """Send welcome message"""
main = await get_main_module()
user_id = await get_user_id()
- # 发送新消息
+ # Send new message
await client.send_message(
user_id,
WELCOME_TEXT,
parse_mode='html',
link_preview=True
)
- logger.info("已发送欢迎消息")
+ logger.info("Welcome message sent")
diff --git a/handlers/button/button_helpers.py b/handlers/button/button_helpers.py
index 2f89a25..9964b49 100644
--- a/handlers/button/button_helpers.py
+++ b/handlers/button/button_helpers.py
@@ -13,19 +13,19 @@
MEDIA_SIZE = load_max_media_size()
MEDIA_EXTENSIONS = load_media_extensions()
async def create_ai_settings_buttons(rule=None,rule_id=None):
- """创建 AI 设置按钮"""
+ """Create AI settings buttons"""
buttons = []
- # 添加 AI 设置按钮
+ # Add AI settings buttons
for field, config in AI_SETTINGS.items():
- # 非属性的项
+ # Non-attribute items
if field == 'summary_now':
display_value = config['display_name']
callback_data = f"{config['toggle_action']}:{rule.id}"
buttons.append([Button.inline(display_value, callback_data)])
continue
- # 特殊处理提示词设置
+ # Special handling for prompt settings
if field == 'ai_prompt' or field == 'summary_prompt':
display_value = config['display_name']
callback_data = f"{config['toggle_action']}:{rule.id}"
@@ -42,20 +42,20 @@ async def create_ai_settings_buttons(rule=None,rule_id=None):
callback_data = f"{config['toggle_action']}:{rule.id}"
buttons.append([Button.inline(button_text, callback_data)])
- # 添加返回按钮
+ # Add back button
buttons.append([
- Button.inline('👈 返回', f"rule_settings:{rule.id}"),
- Button.inline('❌ 关闭', "close_settings")
+ Button.inline('👈 Back', f"rule_settings:{rule.id}"),
+ Button.inline('❌ Close', "close_settings")
])
return buttons
async def create_media_settings_buttons(rule=None,rule_id=None):
- """创建媒体设置按钮"""
+ """Create media settings buttons"""
buttons = []
for field, config in MEDIA_SETTINGS.items():
- # 特殊处理selected_media_types字段,因为它已经移动到单独的表中
+ # Special handling for selected_media_types field, as it has been moved to a separate table
if field == 'selected_media_types':
display_value = f"{config['display_name']}"
callback_data = f"{config['toggle_action']}:{rule.id}"
@@ -85,16 +85,16 @@ async def create_media_settings_buttons(rule=None,rule_id=None):
callback_data = f"{config['toggle_action']}:{rule.id}"
buttons.append([Button.inline(button_text, callback_data)])
- # 添加返回按钮
+ # Add back button
buttons.append([
- Button.inline('👈 返回', f"rule_settings:{rule.id}"),
- Button.inline('❌ 关闭', "close_settings")
+ Button.inline('👈 Back', f"rule_settings:{rule.id}"),
+ Button.inline('❌ Close', "close_settings")
])
return buttons
async def create_other_settings_buttons(rule=None,rule_id=None):
- """创建其他设置按钮"""
+ """Create other settings buttons"""
buttons = []
if rule_id is None:
@@ -120,42 +120,42 @@ async def create_other_settings_buttons(rule=None,rule_id=None):
buttons.append(current_row)
current_row = []
else:
- # 其他按钮单独一行
+ # Other buttons on a separate row
display_value = f"{config['display_name']}"
callback_data = f"{config['toggle_action']}:{rule_id}"
buttons.append([Button.inline(display_value, callback_data)])
- # 添加返回按钮
+ # Add back button
buttons.append([
- Button.inline('👈 返回', f"rule_settings:{rule_id}"),
- Button.inline('❌ 关闭', "close_settings")
+ Button.inline('👈 Back', f"rule_settings:{rule_id}"),
+ Button.inline('❌ Close', "close_settings")
])
return buttons
async def create_list_buttons(total_pages, current_page, command):
- """创建分页按钮"""
+ """Create pagination buttons"""
buttons = []
row = []
- # 上一页按钮
+ # Previous page button
if current_page > 1:
row.append(Button.inline(
- '⬅️ 上一页',
+ '⬅️ Prev',
f'page:{current_page-1}:{command}'
))
- # 页码显示
+ # Page number display
row.append(Button.inline(
f'{current_page}/{total_pages}',
- 'noop:0' # 空操作
+ 'noop:0' # No operation
))
- # 下一页按钮
+ # Next page button
if current_page < total_pages:
row.append(Button.inline(
- '下一页 ➡️',
+ 'Next ➡️',
f'page:{current_page+1}:{command}'
))
@@ -165,46 +165,46 @@ async def create_list_buttons(total_pages, current_page, command):
-# 添加模型选择按钮创建函数
+# Add model selection button creation function
async def create_model_buttons(rule_id, page=0):
- """创建模型选择按钮,支持分页
+ """Create model selection buttons with pagination
Args:
- rule_id: 规则ID
- page: 当前页码(从0开始)
+ rule_id: Rule ID
+ page: Current page number (starting from 0)
"""
buttons = []
total_models = len(AI_MODELS)
total_pages = (total_models + MODELS_PER_PAGE - 1) // MODELS_PER_PAGE
- # 计算当前页的模型范围
+ # Calculate model range for current page
start_idx = page * MODELS_PER_PAGE
end_idx = min(start_idx + MODELS_PER_PAGE, total_models)
- # 添加模型按钮
+ # Add model buttons
for model in AI_MODELS[start_idx:end_idx]:
buttons.append([Button.inline(f"{model}", f"select_model:{rule_id}:{model}")])
- # 添加导航按钮
+ # Add navigation buttons
nav_buttons = []
- if page > 0: # 不是第一页,显示"上一页"
- nav_buttons.append(Button.inline("⬅️ 上一页", f"model_page:{rule_id}:{page - 1}"))
- # 添加页码显示在中间
+ if page > 0: # Not the first page, show "Prev"
+ nav_buttons.append(Button.inline("⬅️ Prev", f"model_page:{rule_id}:{page - 1}"))
+ # Add page number display in the middle
nav_buttons.append(Button.inline(f"{page + 1}/{total_pages}", f"noop:{rule_id}"))
- if page < total_pages - 1: # 不是最后一页,显示"下一页"
- nav_buttons.append(Button.inline("下一页 ➡️", f"model_page:{rule_id}:{page + 1}"))
+ if page < total_pages - 1: # Not the last page, show "Next"
+ nav_buttons.append(Button.inline("Next ➡️", f"model_page:{rule_id}:{page + 1}"))
if nav_buttons:
buttons.append(nav_buttons)
- # 添加返回按钮
- buttons.append([Button.inline("返回", f"rule_settings:{rule_id}")])
+ # Add back button
+ buttons.append([Button.inline("Back", f"rule_settings:{rule_id}")])
return buttons
async def create_summary_time_buttons(rule_id, page=0):
- """创建时间选择按钮"""
- # 从环境变量获取布局设置
+ """Create time selection buttons"""
+ # Get layout settings from environment variables
rows = SUMMARY_TIME_ROWS
cols = SUMMARY_TIME_COLS
times_per_page = rows * cols
@@ -214,11 +214,11 @@ async def create_summary_time_buttons(rule_id, page=0):
start_idx = page * times_per_page
end_idx = min(start_idx + times_per_page, total_times)
- # 检查是否是频道消息
+ # Check if it is a channel message
buttons = []
total_times = len(SUMMARY_TIMES)
- # 添加时间按钮
+ # Add time buttons
current_row = []
for i, time in enumerate(SUMMARY_TIMES[start_idx:end_idx], start=1):
current_row.append(Button.inline(
@@ -226,20 +226,20 @@ async def create_summary_time_buttons(rule_id, page=0):
f"select_time:{rule_id}:{time}"
))
- # 当达到每行的列数时,添加当前行并重置
+ # When reaching columns per row, add current row and reset
if i % cols == 0:
buttons.append(current_row)
current_row = []
- # 添加最后一个不完整的行
+ # Add the last incomplete row
if current_row:
buttons.append(current_row)
- # 添加导航按钮
+ # Add navigation buttons
nav_buttons = []
if page > 0:
nav_buttons.append(Button.inline(
- "⬅️ 上一页",
+ "⬅️ Prev",
f"time_page:{rule_id}:{page - 1}"
))
@@ -250,22 +250,22 @@ async def create_summary_time_buttons(rule_id, page=0):
if end_idx < total_times:
nav_buttons.append(Button.inline(
- "下一页 ➡️",
+ "Next ➡️",
f"time_page:{rule_id}:{page + 1}"
))
buttons.append(nav_buttons)
buttons.append([
- Button.inline('👈 返回', f"ai_settings:{rule_id}"),
- Button.inline('❌ 关闭', "close_settings")
+ Button.inline('👈 Back', f"ai_settings:{rule_id}"),
+ Button.inline('❌ Close', "close_settings")
])
return buttons
async def create_media_size_buttons(rule_id, page=0):
- """创建媒体大小选择按钮"""
- # 从环境变量获取布局设置
+ """Create media size selection buttons"""
+ # Get layout settings from environment variables
rows = MEDIA_SIZE_ROWS
cols = MEDIA_SIZE_COLS
size_select_per_page = rows * cols
@@ -275,11 +275,11 @@ async def create_media_size_buttons(rule_id, page=0):
start_idx = page * size_select_per_page
end_idx = min(start_idx + size_select_per_page, total_size)
- # 检查是否是频道消息
+ # Check if it is a channel message
buttons = []
total_size = len(MEDIA_SIZE)
- # 添加媒体大小按钮
+ # Add media size buttons
current_row = []
for i, size in enumerate(MEDIA_SIZE[start_idx:end_idx], start=1):
current_row.append(Button.inline(
@@ -287,20 +287,20 @@ async def create_media_size_buttons(rule_id, page=0):
f"select_max_media_size:{rule_id}:{size}"
))
- # 当达到每行的列数时,添加当前行并重置
+ # When reaching columns per row, add current row and reset
if i % cols == 0:
buttons.append(current_row)
current_row = []
- # 添加最后一个不完整的行
+ # Add the last incomplete row
if current_row:
buttons.append(current_row)
- # 添加导航按钮
+ # Add navigation buttons
nav_buttons = []
if page > 0:
nav_buttons.append(Button.inline(
- "⬅️ 上一页",
+ "⬅️ Prev",
f"media_size_page:{rule_id}:{page - 1}"
))
@@ -311,22 +311,22 @@ async def create_media_size_buttons(rule_id, page=0):
if end_idx < total_size:
nav_buttons.append(Button.inline(
- "下一页 ➡️",
+ "Next ➡️",
f"media_size_page:{rule_id}:{page + 1}"
))
buttons.append(nav_buttons)
buttons.append([
- Button.inline('👈 返回', f"rule_settings:{rule_id}"),
- Button.inline('❌ 关闭', "close_settings")
+ Button.inline('👈 Back', f"rule_settings:{rule_id}"),
+ Button.inline('❌ Close', "close_settings")
])
return buttons
async def create_delay_time_buttons(rule_id, page=0):
- """创建延迟时间选择按钮"""
- # 从环境变量获取布局设置
+ """Create delay time selection buttons"""
+ # Get layout settings from environment variables
rows = DELAY_TIME_ROWS
cols = DELAY_TIME_COLS
@@ -337,11 +337,11 @@ async def create_delay_time_buttons(rule_id, page=0):
start_idx = page * times_per_page
end_idx = min(start_idx + times_per_page, total_times)
- # 检查是否是频道消息
+ # Check if it is a channel message
buttons = []
total_times = len(DELAY_TIMES)
- # 添加时间按钮
+ # Add time buttons
current_row = []
for i, time in enumerate(DELAY_TIMES[start_idx:end_idx], start=1):
current_row.append(Button.inline(
@@ -349,20 +349,20 @@ async def create_delay_time_buttons(rule_id, page=0):
f"select_delay_time:{rule_id}:{time}"
))
- # 当达到每行的列数时,添加当前行并重置
+ # When reaching columns per row, add current row and reset
if i % cols == 0:
buttons.append(current_row)
current_row = []
- # 添加最后一个不完整的行
+ # Add the last incomplete row
if current_row:
buttons.append(current_row)
- # 添加导航按钮
+ # Add navigation buttons
nav_buttons = []
if page > 0:
nav_buttons.append(Button.inline(
- "⬅️ 上一页",
+ "⬅️ Prev",
f"delay_time_page:{rule_id}:{page - 1}"
))
@@ -373,51 +373,51 @@ async def create_delay_time_buttons(rule_id, page=0):
if end_idx < total_times:
nav_buttons.append(Button.inline(
- "下一页 ➡️",
+ "Next ➡️",
f"delay_time_page:{rule_id}:{page + 1}"
))
buttons.append(nav_buttons)
buttons.append([
- Button.inline('👈 返回', f"rule_settings:{rule_id}"),
- Button.inline('❌ 关闭', "close_settings")
+ Button.inline('👈 Back', f"rule_settings:{rule_id}"),
+ Button.inline('❌ Close', "close_settings")
])
return buttons
async def create_media_types_buttons(rule_id, media_types):
- """创建媒体类型选择按钮
+ """Create media type selection buttons
Args:
- rule_id: 规则ID
- media_types: MediaTypes对象
+ rule_id: Rule ID
+ media_types: MediaTypes object
Returns:
- 按钮列表
+ Button list
"""
buttons = []
- # 媒体类型按钮
+ # Media type buttons
media_type_names = {
- 'photo': '📷 图片',
- 'document': '📄 文档',
- 'video': '🎬 视频',
- 'audio': '🎵 音频',
- 'voice': '🎤 语音'
+ 'photo': '📷 Photo',
+ 'document': '📄 Document',
+ 'video': '🎬 Video',
+ 'audio': '🎵 Audio',
+ 'voice': '🎤 Voice'
}
for field, display_name in media_type_names.items():
- # 获取当前值
+ # Get current value
current_value = getattr(media_types, field, False)
- # 如果为True,添加勾选标记
+ # If True, add checkmark
button_text = f"{'✅ ' if current_value else ''}{display_name}"
callback_data = f"toggle_media_type:{rule_id}:{field}"
buttons.append([Button.inline(button_text, callback_data)])
buttons.append([
- Button.inline('👈 返回', f"media_settings:{rule_id}"),
- Button.inline('❌ 关闭', "close_settings")
+ Button.inline('👈 Back', f"media_settings:{rule_id}"),
+ Button.inline('❌ Close', "close_settings")
])
return buttons
@@ -425,16 +425,16 @@ async def create_media_types_buttons(rule_id, media_types):
async def create_media_extensions_buttons(rule_id, page=0):
- """创建媒体扩展名选择按钮
+ """Create media extension selection buttons
Args:
- rule_id: 规则ID
- page: 当前页码
+ rule_id: Rule ID
+ page: Current page number
Returns:
- 按钮列表
+ Button list
"""
- # 从环境变量获取布局设置
+ # Get layout settings from environment variables
rows = MEDIA_EXTENSIONS_ROWS
cols = MEDIA_EXTENSIONS_COLS
@@ -445,51 +445,51 @@ async def create_media_extensions_buttons(rule_id, page=0):
start_idx = page * extensions_per_page
end_idx = min(start_idx + extensions_per_page, total_extensions)
- # 获取当前规则已选择的扩展名
+ # Get currently selected extensions for the rule
db_ops = await get_db_ops()
session = get_session()
selected_extensions = []
try:
- # 使用db_ops.get_media_extensions方法获取已选择的扩展名
+ # Use db_ops.get_media_extensions method to get selected extensions
selected_extensions = await db_ops.get_media_extensions(session, rule_id)
selected_extension_list = [ext["extension"] for ext in selected_extensions]
- # 创建扩展名按钮
+ # Create extension buttons
current_row = []
for i in range(start_idx, end_idx):
ext = MEDIA_EXTENSIONS[i]
- # 检查是否已选择
+ # Check if selected
is_selected = ext in selected_extension_list
button_text = f"{'✅ ' if is_selected else ''}{ext}"
- # 在回调数据中包含页码信息
+ # Include page number info in callback data
callback_data = f"toggle_media_extension:{rule_id}:{ext}:{page}"
current_row.append(Button.inline(button_text, callback_data))
- # 每行放置cols个按钮
+ # Place cols buttons per row
if len(current_row) == cols:
buttons.append(current_row)
current_row = []
- # 添加剩余的按钮
+ # Add remaining buttons
if current_row:
buttons.append(current_row)
- # 添加分页按钮
+ # Add pagination buttons
page_buttons = []
total_pages = (total_extensions + extensions_per_page - 1) // extensions_per_page
if total_pages > 1:
- # 上一页按钮
+ # Previous page button
if page > 0:
page_buttons.append(Button.inline("⬅️", f"media_extensions_page:{rule_id}:{page-1}"))
else:
page_buttons.append(Button.inline("⬅️", f"noop"))
- # 页码指示
+ # Page indicator
page_buttons.append(Button.inline(f"{page+1}/{total_pages}", f"noop"))
- # 下一页按钮
+ # Next page button
if page < total_pages - 1:
page_buttons.append(Button.inline("➡️", f"media_extensions_page:{rule_id}:{page+1}"))
else:
@@ -500,8 +500,8 @@ async def create_media_extensions_buttons(rule_id, page=0):
buttons.append([
- Button.inline('👈 返回', f"media_settings:{rule_id}"),
- Button.inline('❌ 关闭', "close_settings")
+ Button.inline('👈 Back', f"media_settings:{rule_id}"),
+ Button.inline('❌ Close', "close_settings")
])
finally:
session.close()
@@ -510,86 +510,86 @@ async def create_media_extensions_buttons(rule_id, page=0):
async def create_sync_rule_buttons(rule_id, page=0):
- """创建同步规则选择按钮
+ """Create sync rule selection buttons
Args:
- rule_id: 当前规则ID
- page: 当前页码
+ rule_id: Current rule ID
+ page: Current page number
Returns:
- 按钮列表
+ Button list
"""
- # 设置分页参数
+ # Set pagination parameters
buttons = []
session = get_session()
try:
- # 获取当前规则
+ # Get current rule
current_rule = session.query(ForwardRule).get(rule_id)
if not current_rule:
- buttons.append([Button.inline('❌ 规则不存在', 'noop')])
- buttons.append([Button.inline('关闭', 'close_settings')])
+ buttons.append([Button.inline('❌ Rule does not exist', 'noop')])
+ buttons.append([Button.inline('Close', 'close_settings')])
return buttons
- # 获取所有规则(除了当前规则)
+ # Get all rules (except current rule)
all_rules = session.query(ForwardRule).filter(
ForwardRule.id != rule_id
).all()
- # 计算分页
+ # Calculate pagination
total_rules = len(all_rules)
total_pages = (total_rules + RULES_PER_PAGE - 1) // RULES_PER_PAGE
if total_rules == 0:
- buttons.append([Button.inline('❌ 没有可用的规则', 'noop')])
+ buttons.append([Button.inline('❌ No available rules', 'noop')])
buttons.append([
- Button.inline('👈 返回', f"rule_settings:{rule_id}"),
- Button.inline('❌ 关闭', 'close_settings')
+ Button.inline('👈 Back', f"rule_settings:{rule_id}"),
+ Button.inline('❌ Close', 'close_settings')
])
return buttons
- # 获取当前页的规则
+ # Get rules for current page
start_idx = page * RULES_PER_PAGE
end_idx = min(start_idx + RULES_PER_PAGE, total_rules)
current_page_rules = all_rules[start_idx:end_idx]
- # 获取当前规则的同步目标
+ # Get sync targets of current rule
db_ops = await get_db_ops()
sync_targets = await db_ops.get_rule_syncs(session, rule_id)
synced_rule_ids = [sync.sync_rule_id for sync in sync_targets]
- # 创建规则按钮
+ # Create rule buttons
for rule in current_page_rules:
- # 获取源聊天和目标聊天名称
+ # Get source chat and target chat names
source_chat = rule.source_chat
target_chat = rule.target_chat
- # 检查是否已同步
+ # Check if synced
is_synced = rule.id in synced_rule_ids
- # 创建按钮文本
+ # Create button text
button_text = f"{'✅ ' if is_synced else ''}{rule.id} {source_chat.name}->{target_chat.name}"
- # 创建回调数据:toggle_rule_sync:当前规则ID:目标规则ID:当前页码
+ # Create callback data: toggle_rule_sync:current_rule_id:target_rule_id:current_page
callback_data = f"toggle_rule_sync:{rule_id}:{rule.id}:{page}"
buttons.append([Button.inline(button_text, callback_data)])
- # 添加分页按钮
+ # Add pagination buttons
page_buttons = []
if total_pages > 1:
- # 上一页按钮
+ # Previous page button
if page > 0:
page_buttons.append(Button.inline("⬅️", f"sync_rule_page:{rule_id}:{page-1}"))
else:
page_buttons.append(Button.inline("⬅️", "noop"))
- # 页码指示
+ # Page indicator
page_buttons.append(Button.inline(f"{page+1}/{total_pages}", "noop"))
- # 下一页按钮
+ # Next page button
if page < total_pages - 1:
page_buttons.append(Button.inline("➡️", f"sync_rule_page:{rule_id}:{page+1}"))
else:
@@ -598,10 +598,10 @@ async def create_sync_rule_buttons(rule_id, page=0):
if page_buttons:
buttons.append(page_buttons)
- # 添加同步保存和返回按钮
+ # Add sync save and back buttons
buttons.append([
- Button.inline('👈 返回', f"rule_settings:{rule_id}"),
- Button.inline('❌ 关闭', 'close_settings')
+ Button.inline('👈 Back', f"rule_settings:{rule_id}"),
+ Button.inline('❌ Close', 'close_settings')
])
finally:
@@ -610,31 +610,31 @@ async def create_sync_rule_buttons(rule_id, page=0):
return buttons
async def create_push_settings_buttons(rule_id, page=0):
- """创建推送设置按钮菜单,支持分页
+ """Create push settings button menu with pagination
Args:
- rule_id: 规则ID
- page: 页码(从0开始)
+ rule_id: Rule ID
+ page: Page number (starting from 0)
Returns:
- 按钮列表
+ Button list
"""
buttons = []
configs_per_page = PUSH_CHANNEL_PER_PAGE
- # 从数据库获取规则对象和推送配置
+ # Get rule object and push configs from database
db_ops = await get_db_ops()
session = get_session()
try:
- # 获取规则对象
+ # Get rule object
rule = session.query(ForwardRule).get(rule_id)
if not rule:
- buttons.append([Button.inline("❌ 规则不存在", "noop")])
- buttons.append([Button.inline("关闭", "close_settings")])
+ buttons.append([Button.inline("❌ Rule does not exist", "noop")])
+ buttons.append([Button.inline("Close", "close_settings")])
return buttons
- # 添加"启用推送"按钮
+ # Add "Enable push" button
buttons.append([
Button.inline(
f"{'✅ ' if rule.enable_push else ''}{PUSH_SETTINGS['enable_push_channel']['display_name']}",
@@ -642,7 +642,7 @@ async def create_push_settings_buttons(rule_id, page=0):
)
])
- # 添加"只转发到推送配置"按钮
+ # Add "Forward to push only" button
buttons.append([
Button.inline(
f"{'✅ ' if rule.enable_only_push else ''}{PUSH_SETTINGS['enable_only_push']['display_name']}",
@@ -650,7 +650,7 @@ async def create_push_settings_buttons(rule_id, page=0):
)
])
- # 添加"添加推送配置"按钮
+ # Add "Add push configuration" button
buttons.append([
Button.inline(
PUSH_SETTINGS['add_push_channel']['display_name'],
@@ -658,39 +658,39 @@ async def create_push_settings_buttons(rule_id, page=0):
)
])
- # 获取当前规则的所有推送配置
+ # Get all push configs for current rule
push_configs = await db_ops.get_push_configs(session, rule_id)
- # 计算总页数
+ # Calculate total pages
total_configs = len(push_configs)
total_pages = (total_configs + configs_per_page - 1) // configs_per_page
- # 计算当前页的范围
+ # Calculate range for current page
start_idx = page * configs_per_page
end_idx = min(start_idx + configs_per_page, total_configs)
- # 为每个推送配置创建按钮(仅当前页)
+ # Create buttons for each push config (current page only)
for config in push_configs[start_idx:end_idx]:
- # 取前20个字符
+ # Take first 20 characters
display_name = config.push_channel[:25] + ('...' if len(config.push_channel) > 25 else '')
button_text = display_name
- # 创建按钮
+ # Create button
buttons.append([Button.inline(button_text, f"toggle_push_config:{config.id}")])
- # 添加分页按钮(如果需要)
+ # Add pagination buttons (if needed)
if total_pages > 1:
nav_buttons = []
- # 上一页按钮
+ # Previous page button
if page > 0:
nav_buttons.append(Button.inline("⬅️", f"push_page:{rule_id}:{page-1}"))
else:
nav_buttons.append(Button.inline("⬅️", "noop"))
- # 页码指示
+ # Page indicator
nav_buttons.append(Button.inline(f"{page+1}/{total_pages}", "noop"))
- # 下一页按钮
+ # Next page button
if page < total_pages - 1:
nav_buttons.append(Button.inline("➡️", f"push_page:{rule_id}:{page+1}"))
else:
@@ -701,63 +701,63 @@ async def create_push_settings_buttons(rule_id, page=0):
finally:
session.close()
- # 添加返回和关闭按钮
+ # Add back and close buttons
buttons.append([
- Button.inline('👈 返回', f"rule_settings:{rule_id}"),
- Button.inline('❌ 关闭', "close_settings")
+ Button.inline('👈 Back', f"rule_settings:{rule_id}"),
+ Button.inline('❌ Close', "close_settings")
])
return buttons
async def create_push_config_details_buttons(config_id):
- """创建推送配置详情按钮
+ """Create push config details buttons
Args:
- config_id: 推送配置ID
+ config_id: Push config ID
Returns:
- 按钮列表
+ Button list
"""
buttons = []
- # 从数据库获取推送配置
+ # Get push config from database
session = get_session()
try:
from models.models import PushConfig
- # 获取推送配置
+ # Get push config
config = session.query(PushConfig).get(config_id)
if not config:
- buttons.append([Button.inline("❌ 推送配置不存在", "noop")])
- buttons.append([Button.inline("关闭", "close_settings")])
+ buttons.append([Button.inline("❌ Push config does not exist", "noop")])
+ buttons.append([Button.inline("Close", "close_settings")])
return buttons
- # 添加启用/禁用按钮
+ # Add enable/disable button
buttons.append([
Button.inline(
- f"{'✅ ' if config.enable_push_channel else ''}启用推送",
+ f"{'✅ ' if config.enable_push_channel else ''}Enable push",
f"toggle_push_config_status:{config_id}"
)
])
- # 添加媒体发送方式切换按钮
- mode_text = "单个" if config.media_send_mode == "Single" else "全部"
+ # Add media send mode toggle button
+ mode_text = "Single" if config.media_send_mode == "Single" else "All"
buttons.append([
Button.inline(
- f"📁 媒体发送方式: {mode_text}",
+ f"📁 Media send mode: {mode_text}",
f"toggle_media_send_mode:{config_id}"
)
])
- # 添加删除按钮
+ # Add delete button
buttons.append([
- Button.inline("🗑️ 删除推送配置", f"delete_push_config:{config_id}")
+ Button.inline("🗑️ Delete push config", f"delete_push_config:{config_id}")
])
- # 添加返回按钮
+ # Add back button
buttons.append([
- Button.inline("👈 返回", f"push_settings:{config.rule_id}"),
- Button.inline("❌ 关闭", "close_settings")
+ Button.inline("👈 Back", f"push_settings:{config.rule_id}"),
+ Button.inline("❌ Close", "close_settings")
])
finally:
diff --git a/handlers/button/callback/ai_callback.py b/handlers/button/callback/ai_callback.py
index 04b73eb..d9385e2 100644
--- a/handlers/button/callback/ai_callback.py
+++ b/handlers/button/callback/ai_callback.py
@@ -17,7 +17,7 @@
async def callback_ai_settings(event, rule_id, session, message, data):
- # 显示 AI 设置页面
+ # Display AI settings page
try:
rule = session.query(ForwardRule).get(int(rule_id))
if rule:
@@ -29,23 +29,23 @@ async def callback_ai_settings(event, rule_id, session, message, data):
async def callback_set_summary_time(event, rule_id, session, message, data):
- await event.edit("请选择总结时间:", buttons=await create_summary_time_buttons(rule_id, page=0))
+ await event.edit("Please select summary time:", buttons=await create_summary_time_buttons(rule_id, page=0))
return
async def callback_set_summary_prompt(event, rule_id, session, message, data):
- """处理设置AI总结提示词的回调"""
- logger.info(f"开始处理设置AI总结提示词回调 - event: {event}, rule_id: {rule_id}")
+ """Handle callback for setting AI summary prompt"""
+ logger.info(f"Starting to handle set AI summary prompt callback - event: {event}, rule_id: {rule_id}")
rule = session.query(ForwardRule).get(rule_id)
if not rule:
- await event.answer('规则不存在')
+ await event.answer('Rule does not exist')
return
- # 检查是否频道消息
+ # Check if it is a channel message
if isinstance(event.chat, types.Channel):
- # 检查是否是管理员
+ # Check if user is admin
if not await is_admin(event):
- await event.answer('只有管理员可以修改设置')
+ await event.answer('Only admins can modify settings')
return
user_id = os.getenv('USER_ID')
else:
@@ -54,54 +54,54 @@ async def callback_set_summary_prompt(event, rule_id, session, message, data):
chat_id = abs(event.chat_id)
state = f"set_summary_prompt:{rule_id}"
- logger.info(f"准备设置状态 - user_id: {user_id}, chat_id: {chat_id}, state: {state}")
+ logger.info(f"Preparing to set state - user_id: {user_id}, chat_id: {chat_id}, state: {state}")
try:
state_manager.set_state(user_id, chat_id, state, message, state_type="ai")
- # 启动超时取消任务
+ # Start timeout cancellation task
asyncio.create_task(cancel_state_after_timeout(user_id, chat_id))
- logger.info("状态设置成功")
+ logger.info("State set successfully")
except Exception as e:
- logger.error(f"设置状态时出错: {str(e)}")
+ logger.error(f"Error setting state: {str(e)}")
logger.exception(e)
try:
- current_prompt = rule.summary_prompt or os.getenv('DEFAULT_SUMMARY_PROMPT', '未设置')
+ current_prompt = rule.summary_prompt or os.getenv('DEFAULT_SUMMARY_PROMPT', 'Not set')
await message.edit(
- f"请发送新的AI总结提示词\n"
- f"当前规则ID: `{rule_id}`\n"
- f"当前AI总结提示词:\n\n`{current_prompt}`\n\n"
- f"5分钟内未设置将自动取消",
- buttons=[[Button.inline("取消", f"cancel_set_summary:{rule_id}")]]
+ f"Please send new AI summary prompt\n"
+ f"Current rule ID: `{rule_id}`\n"
+ f"Current AI summary prompt:\n\n`{current_prompt}`\n\n"
+ f"Will be automatically cancelled if not set within 5 minutes",
+ buttons=[[Button.inline("Cancel", f"cancel_set_summary:{rule_id}")]]
)
- logger.info("消息编辑成功")
+ logger.info("Message edit successful")
except Exception as e:
- logger.error(f"编辑消息时出错: {str(e)}")
+ logger.error(f"Error editing message: {str(e)}")
logger.exception(e)
async def cancel_state_after_timeout(user_id: int, chat_id: int, timeout_minutes: int = 5):
- """在指定时间后自动取消状态"""
+ """Automatically cancel state after specified timeout"""
await asyncio.sleep(timeout_minutes * 60)
current_state, _, _ = state_manager.get_state(user_id, chat_id)
- if current_state: # 只有当状态还存在时才清除
- logger.info(f"状态超时自动取消 - user_id: {user_id}, chat_id: {chat_id}")
+ if current_state: # Only clear if state still exists
+ logger.info(f"State auto-cancelled due to timeout - user_id: {user_id}, chat_id: {chat_id}")
state_manager.clear_state(user_id, chat_id)
async def callback_set_ai_prompt(event, rule_id, session, message, data):
- """处理设置AI提示词的回调"""
- logger.info(f"开始处理设置AI提示词回调 - event: {event}, rule_id: {rule_id}")
+ """Handle callback for setting AI prompt"""
+ logger.info(f"Starting to handle set AI prompt callback - event: {event}, rule_id: {rule_id}")
rule = session.query(ForwardRule).get(rule_id)
if not rule:
- await event.answer('规则不存在')
+ await event.answer('Rule does not exist')
return
- # 检查是否频道消息
+ # Check if it is a channel message
if isinstance(event.chat, types.Channel):
- # 检查是否是管理员
+ # Check if user is admin
if not await is_admin(event):
- await event.answer('只有管理员可以修改设置')
+ await event.answer('Only admins can modify settings')
return
user_id = os.getenv('USER_ID')
else:
@@ -110,28 +110,28 @@ async def callback_set_ai_prompt(event, rule_id, session, message, data):
chat_id = abs(event.chat_id)
state = f"set_ai_prompt:{rule_id}"
- logger.info(f"准备设置状态 - user_id: {user_id}, chat_id: {chat_id}, state: {state}")
+ logger.info(f"Preparing to set state - user_id: {user_id}, chat_id: {chat_id}, state: {state}")
try:
state_manager.set_state(user_id, chat_id, state, message, state_type="ai")
- # 启动超时取消任务
+ # Start timeout cancellation task
asyncio.create_task(cancel_state_after_timeout(user_id, chat_id))
- logger.info("状态设置成功")
+ logger.info("State set successfully")
except Exception as e:
- logger.error(f"设置状态时出错: {str(e)}")
+ logger.error(f"Error setting state: {str(e)}")
logger.exception(e)
try:
- current_prompt = rule.ai_prompt or os.getenv('DEFAULT_AI_PROMPT', '未设置')
+ current_prompt = rule.ai_prompt or os.getenv('DEFAULT_AI_PROMPT', 'Not set')
await message.edit(
- f"请发送新的AI提示词\n"
- f"当前规则ID: `{rule_id}`\n"
- f"当前AI提示词:\n\n`{current_prompt}`\n\n"
- f"5分钟内未设置将自动取消",
- buttons=[[Button.inline("取消", f"cancel_set_prompt:{rule_id}")]]
+ f"Please send new AI prompt\n"
+ f"Current rule ID: `{rule_id}`\n"
+ f"Current AI prompt:\n\n`{current_prompt}`\n\n"
+ f"Will be automatically cancelled if not set within 5 minutes",
+ buttons=[[Button.inline("Cancel", f"cancel_set_prompt:{rule_id}")]]
)
- logger.info("消息编辑成功")
+ logger.info("Message edit successful")
except Exception as e:
- logger.error(f"编辑消息时出错: {str(e)}")
+ logger.error(f"Error editing message: {str(e)}")
logger.exception(e)
@@ -141,143 +141,143 @@ async def callback_set_ai_prompt(event, rule_id, session, message, data):
async def callback_time_page(event, rule_id, session, message, data):
_, rule_id, page = data.split(':')
page = int(page)
- await event.edit("请选择总结时间:", buttons=await create_summary_time_buttons(rule_id, page=page))
+ await event.edit("Please select summary time:", buttons=await create_summary_time_buttons(rule_id, page=page))
return
async def callback_select_time(event, rule_id, session, message, data):
- parts = data.split(':', 2) # 最多分割2次
+ parts = data.split(':', 2) # Split at most 2 times
if len(parts) == 3:
_, rule_id, time = parts
- logger.info(f"设置规则 {rule_id} 的总结时间为: {time}")
+ logger.info(f"Setting summary time for rule {rule_id} to: {time}")
try:
rule = session.query(ForwardRule).get(int(rule_id))
if rule:
- # 记录旧时间
+ # Record old time
old_time = rule.summary_time
- # 更新时间
+ # Update time
rule.summary_time = time
session.commit()
- logger.info(f"数据库更新成功: {old_time} -> {time}")
+ logger.info(f"Database update successful: {old_time} -> {time}")
- # 检查是否启用了同步功能
+ # Check if sync is enabled
if rule.enable_sync:
- logger.info(f"规则 {rule.id} 启用了同步功能,正在同步总结时间设置到关联规则")
- # 获取需要同步的规则列表
+ logger.info(f"Rule {rule.id} has sync enabled, syncing summary time setting to associated rules")
+ # Get list of rules to sync
sync_rules = session.query(RuleSync).filter(RuleSync.rule_id == rule.id).all()
- # 为每个同步规则应用相同的总结时间设置
+ # Apply the same summary time setting to each synced rule
for sync_rule in sync_rules:
sync_rule_id = sync_rule.sync_rule_id
- logger.info(f"正在同步总结时间到规则 {sync_rule_id}")
+ logger.info(f"Syncing summary time to rule {sync_rule_id}")
- # 获取同步目标规则
+ # Get sync target rule
target_rule = session.query(ForwardRule).get(sync_rule_id)
if not target_rule:
- logger.warning(f"同步目标规则 {sync_rule_id} 不存在,跳过")
+ logger.warning(f"Sync target rule {sync_rule_id} does not exist, skipping")
continue
- # 更新同步目标规则的总结时间设置
+ # Update sync target rule's summary time setting
try:
- # 记录旧时间
+ # Record old time
old_target_time = target_rule.summary_time
- # 设置新时间
+ # Set new time
target_rule.summary_time = time
- # 如果目标规则启用了总结功能,也更新它的调度
+ # If target rule has summary enabled, also update its schedule
if target_rule.is_summary:
- logger.info(f"目标规则 {sync_rule_id} 启用了总结功能,更新其调度任务")
+ logger.info(f"Target rule {sync_rule_id} has summary enabled, updating its schedule")
main = await get_main_module()
if hasattr(main, 'scheduler') and main.scheduler:
await main.scheduler.schedule_rule(target_rule)
- logger.info(f"目标规则调度任务更新成功,新时间: {time}")
+ logger.info(f"Target rule schedule updated successfully, new time: {time}")
else:
- logger.warning("调度器未初始化")
+ logger.warning("Scheduler not initialized")
- logger.info(f"同步规则 {sync_rule_id} 的总结时间从 {old_target_time} 到 {time}")
+ logger.info(f"Synced rule {sync_rule_id} summary time from {old_target_time} to {time}")
except Exception as e:
- logger.error(f"同步总结时间到规则 {sync_rule_id} 时出错: {str(e)}")
+ logger.error(f"Error syncing summary time to rule {sync_rule_id}: {str(e)}")
continue
- # 提交所有同步更改
+ # Commit all sync changes
session.commit()
- logger.info("所有同步总结时间更改已提交")
+ logger.info("All synced summary time changes committed")
- # 如果总结功能已开启,重新调度任务
+ # If summary feature is enabled, reschedule task
if rule.is_summary:
- logger.info("规则已启用总结功能,开始更新调度任务")
+ logger.info("Rule has summary enabled, starting to update schedule")
main = await get_main_module()
if hasattr(main, 'scheduler') and main.scheduler:
await main.scheduler.schedule_rule(rule)
- logger.info(f"调度任务更新成功,新时间: {time}")
+ logger.info(f"Schedule updated successfully, new time: {time}")
else:
- logger.warning("调度器未初始化")
+ logger.warning("Scheduler not initialized")
else:
- logger.info("规则未启用总结功能,跳过调度任务更新")
+ logger.info("Rule does not have summary enabled, skipping schedule update")
await event.edit(await get_ai_settings_text(rule), buttons=await create_ai_settings_buttons(rule))
- logger.info("界面更新完成")
+ logger.info("UI update completed")
except Exception as e:
- logger.error(f"设置总结时间时出错: {str(e)}")
- logger.error(f"错误详情: {traceback.format_exc()}")
+ logger.error(f"Error setting summary time: {str(e)}")
+ logger.error(f"Error details: {traceback.format_exc()}")
finally:
session.close()
return
async def callback_select_model(event, rule_id, session, message, data):
- # 分割数据,最多分割2次,将第三部分直接作为完整的模型名称
+ # Split data, at most 2 times, use the third part directly as complete model name
parts = data.split(':', 2)
_, rule_id_part, model = parts
try:
rule = session.query(ForwardRule).get(int(rule_id_part))
if rule:
- # 记录旧模型
+ # Record old model
old_model = rule.ai_model
- # 更新模型
+ # Update model
rule.ai_model = model
session.commit()
- logger.info(f"已更新规则 {rule_id_part} 的AI模型为: {model}")
+ logger.info(f"Updated AI model for rule {rule_id_part} to: {model}")
- # 检查是否启用了同步功能
+ # Check if sync is enabled
if rule.enable_sync:
- logger.info(f"规则 {rule.id} 启用了同步功能,正在同步AI模型设置到关联规则")
- # 获取需要同步的规则列表
+ logger.info(f"Rule {rule.id} has sync enabled, syncing AI model setting to associated rules")
+ # Get list of rules to sync
sync_rules = session.query(RuleSync).filter(RuleSync.rule_id == rule.id).all()
- # 为每个同步规则应用相同的AI模型设置
+ # Apply the same AI model setting to each synced rule
for sync_rule in sync_rules:
sync_rule_id = sync_rule.sync_rule_id
- logger.info(f"正在同步AI模型到规则 {sync_rule_id}")
+ logger.info(f"Syncing AI model to rule {sync_rule_id}")
- # 获取同步目标规则
+ # Get sync target rule
target_rule = session.query(ForwardRule).get(sync_rule_id)
if not target_rule:
- logger.warning(f"同步目标规则 {sync_rule_id} 不存在,跳过")
+ logger.warning(f"Sync target rule {sync_rule_id} does not exist, skipping")
continue
- # 更新同步目标规则的AI模型设置
+ # Update sync target rule's AI model setting
try:
- # 记录旧模型
+ # Record old model
old_target_model = target_rule.ai_model
- # 设置新模型
+ # Set new model
target_rule.ai_model = model
- logger.info(f"同步规则 {sync_rule_id} 的AI模型从 {old_target_model} 到 {model}")
+ logger.info(f"Synced rule {sync_rule_id} AI model from {old_target_model} to {model}")
except Exception as e:
- logger.error(f"同步AI模型到规则 {sync_rule_id} 时出错: {str(e)}")
+ logger.error(f"Error syncing AI model to rule {sync_rule_id}: {str(e)}")
continue
- # 提交所有同步更改
+ # Commit all sync changes
session.commit()
- logger.info("所有同步AI模型更改已提交")
+ logger.info("All synced AI model changes committed")
- # 返回到 AI 设置页面
+ # Return to AI settings page
await event.edit(await get_ai_settings_text(rule), buttons=await create_ai_settings_buttons(rule))
finally:
session.close()
@@ -286,31 +286,31 @@ async def callback_select_model(event, rule_id, session, message, data):
async def callback_model_page(event, rule_id, session, message, data):
- # 处理翻页
+ # Handle pagination
_, rule_id, page = data.split(':')
page = int(page)
- await event.edit("请选择AI模型:", buttons=await create_model_buttons(rule_id, page=page))
+ await event.edit("Please select AI model:", buttons=await create_model_buttons(rule_id, page=page))
return
async def callback_change_model(event, rule_id, session, message, data):
- await event.edit("请选择AI模型:", buttons=await create_model_buttons(rule_id, page=0))
+ await event.edit("Please select AI model:", buttons=await create_model_buttons(rule_id, page=0))
return
async def callback_cancel_set_prompt(event, rule_id, session, message, data):
- # 处理取消设置提示词
+ # Handle cancel set prompt
rule_id = data.split(':')[1]
try:
rule = session.query(ForwardRule).get(int(rule_id))
if rule:
- # 清除状态
+ # Clear state
state_manager.clear_state(event.sender_id, abs(event.chat_id))
- # 返回到 AI 设置页面
+ # Return to AI settings page
await event.edit(await get_ai_settings_text(rule), buttons=await create_ai_settings_buttons(rule))
- await event.answer("已取消设置")
+ await event.answer("Setting cancelled")
finally:
session.close()
return
@@ -319,28 +319,28 @@ async def callback_cancel_set_prompt(event, rule_id, session, message, data):
async def callback_cancel_set_summary(event, rule_id, session, message, data):
- # 处理取消设置总结
+ # Handle cancel set summary
rule_id = data.split(':')[1]
try:
rule = session.query(ForwardRule).get(int(rule_id))
if rule:
- # 清除状态
+ # Clear state
state_manager.clear_state(event.sender_id, abs(event.chat_id))
- # 返回到 AI 设置页面
+ # Return to AI settings page
await event.edit(await get_ai_settings_text(rule), buttons=await create_ai_settings_buttons(rule))
- await event.answer("已取消设置")
+ await event.answer("Setting cancelled")
finally:
session.close()
return
async def callback_summary_now(event, rule_id, session, message, data):
- # 处理立即执行总结的回调
- logger.info(f"处理立即执行总结回调 - rule_id: {rule_id}")
+ # Handle callback for executing summary now
+ logger.info(f"Handling execute summary now callback - rule_id: {rule_id}")
try:
rule = session.query(ForwardRule).get(int(rule_id))
if not rule:
- await event.answer("规则不存在")
+ await event.answer("Rule does not exist")
return
main = await get_main_module()
@@ -348,29 +348,29 @@ async def callback_summary_now(event, rule_id, session, message, data):
bot_client = main.bot_client
scheduler = SummaryScheduler(user_client, bot_client)
- await event.answer("开始执行总结,请稍候...")
+ await event.answer("Starting summary, please wait...")
await message.edit(
- f"正在为规则 {rule_id}({rule.source_chat.name} -> {rule.target_chat.name})生成总结...\n"
- f"处理需要一定时间,请耐心等待。",
- buttons=[[Button.inline("返回", f"ai_settings:{rule_id}")]]
+ f"Generating summary for rule {rule_id} ({rule.source_chat.name} -> {rule.target_chat.name})...\n"
+ f"Processing takes some time, please be patient.",
+ buttons=[[Button.inline("Back", f"ai_settings:{rule_id}")]]
)
try:
- # 执行总结任务
+ # Execute summary task
await asyncio.create_task(scheduler._execute_summary(rule.id,is_now=True))
- logger.info(f"已启动规则 {rule_id} 的立即总结任务")
+ logger.info(f"Started immediate summary task for rule {rule_id}")
except Exception as e:
- logger.error(f"执行总结任务失败: {str(e)}")
+ logger.error(f"Failed to execute summary task: {str(e)}")
logger.error(traceback.format_exc())
await message.edit(
- f"总结生成失败: {str(e)}",
- buttons=[[Button.inline("返回", f"ai_settings:{rule_id}")]]
+ f"Summary generation failed: {str(e)}",
+ buttons=[[Button.inline("Back", f"ai_settings:{rule_id}")]]
)
except Exception as e:
- logger.error(f"处理总结时出错: {str(e)}")
+ logger.error(f"Error processing summary: {str(e)}")
logger.error(traceback.format_exc())
- await event.answer(f"处理时出错: {str(e)}")
+ await event.answer(f"Error processing: {str(e)}")
finally:
session.close()
diff --git a/handlers/button/callback/callback_handlers.py b/handlers/button/callback/callback_handlers.py
index 67db1bd..b81c5a6 100644
--- a/handlers/button/callback/callback_handlers.py
+++ b/handlers/button/callback/callback_handlers.py
@@ -18,27 +18,27 @@
async def callback_switch(event, rule_id, session, message, data):
- """处理切换源聊天的回调"""
- # 获取当前聊天
+ """Handle callback for switching source chat"""
+ # Get current chat
current_chat = await event.get_chat()
current_chat_db = session.query(Chat).filter(
Chat.telegram_chat_id == str(current_chat.id)
).first()
if not current_chat_db:
- await event.answer('当前聊天不存在')
+ await event.answer('Current chat does not exist')
return
- # 如果已经选中了这个聊天,就不做任何操作
+ # If this chat is already selected, do nothing
if current_chat_db.current_add_id == rule_id:
- await event.answer('已经选中该聊天')
+ await event.answer('This chat is already selected')
return
- # 更新当前选中的源聊天
- current_chat_db.current_add_id = rule_id # 这里的 rule_id 实际上是源聊天的 telegram_chat_id
+ # Update the currently selected source chat
+ current_chat_db.current_add_id = rule_id # Here rule_id is actually the source chat's telegram_chat_id
session.commit()
- # 更新按钮显示
+ # Update button display
rules = session.query(ForwardRule).filter(
ForwardRule.target_chat_id == current_chat_db.id
).all()
@@ -47,31 +47,31 @@ async def callback_switch(event, rule_id, session, message, data):
for rule in rules:
source_chat = rule.source_chat
current = source_chat.telegram_chat_id == rule_id
- button_text = f'{"✓ " if current else ""}来自: {source_chat.name}'
+ button_text = f'{"✓ " if current else ""}From: {source_chat.name}'
callback_data = f"switch:{source_chat.telegram_chat_id}"
buttons.append([Button.inline(button_text, callback_data)])
try:
- await message.edit('请选择要管理的转发规则:', buttons=buttons)
+ await message.edit('Please select a forwarding rule to manage:', buttons=buttons)
except Exception as e:
if 'message was not modified' not in str(e).lower():
- raise # 如果是其他错误就继续抛出
+ raise # If it's a different error, re-raise it
source_chat = session.query(Chat).filter(
Chat.telegram_chat_id == rule_id
).first()
- await event.answer(f'已切换到: {source_chat.name if source_chat else "未知聊天"}')
+ await event.answer(f'Switched to: {source_chat.name if source_chat else "unknown chat"}')
async def callback_settings(event, rule_id, session, message, data):
- """处理显示设置的回调"""
- # 获取当前聊天
+ """Handle callback for displaying settings"""
+ # Get current chat
current_chat = await event.get_chat()
current_chat_db = session.query(Chat).filter(
Chat.telegram_chat_id == str(current_chat.id)
).first()
if not current_chat_db:
- await event.answer('当前聊天不存在')
+ await event.answer('Current chat does not exist')
return
rules = session.query(ForwardRule).filter(
@@ -79,10 +79,10 @@ async def callback_settings(event, rule_id, session, message, data):
).all()
if not rules:
- await event.answer('当前聊天没有任何转发规则')
+ await event.answer('No forwarding rules for current chat')
return
- # 创建规则选择按钮
+ # Create rule selection buttons
buttons = []
for rule in rules:
source_chat = rule.source_chat
@@ -90,83 +90,83 @@ async def callback_settings(event, rule_id, session, message, data):
callback_data = f"rule_settings:{rule.id}"
buttons.append([Button.inline(button_text, callback_data)])
- await message.edit('请选择要管理的转发规则:', buttons=buttons)
+ await message.edit('Please select a forwarding rule to manage:', buttons=buttons)
async def callback_delete(event, rule_id, session, message, data):
- """处理删除规则的回调"""
+ """Handle callback for deleting rule"""
rule = session.query(ForwardRule).get(rule_id)
if not rule:
- await event.answer('规则不存在')
+ await event.answer('Rule does not exist')
return
try:
- # 先保存规则对象,用于后续检查聊天关联
+ # Save rule object first for later chat association check
rule_obj = rule
- # 先删除替换规则
+ # Delete replace rules first
session.query(ReplaceRule).filter(
ReplaceRule.rule_id == rule.id
).delete()
- # 再删除关键字
+ # Then delete keywords
session.query(Keyword).filter(
Keyword.rule_id == rule.id
).delete()
- # 删除规则
+ # Delete rule
session.delete(rule)
- # 提交规则删除的更改
+ # Commit rule deletion changes
session.commit()
- # 尝试删除RSS服务中的相关数据
+ # Try to delete related data in RSS service
try:
rss_url = f"http://{RSS_HOST}:{RSS_PORT}/api/rule/{rule_id}"
async with aiohttp.ClientSession() as client_session:
async with client_session.delete(rss_url) as response:
if response.status == 200:
- logger.info(f"成功删除RSS规则数据: {rule_id}")
+ logger.info(f"Successfully deleted RSS rule data: {rule_id}")
else:
response_text = await response.text()
- logger.warning(f"删除RSS规则数据失败 {rule_id}, 状态码: {response.status}, 响应: {response_text}")
+ logger.warning(f"Failed to delete RSS rule data {rule_id}, status code: {response.status}, response: {response_text}")
except Exception as rss_err:
- logger.error(f"调用RSS删除API时出错: {str(rss_err)}")
- # 不影响主要流程,继续执行
+ logger.error(f"Error calling RSS delete API: {str(rss_err)}")
+ # Don't affect main flow, continue execution
- # 使用通用方法检查并清理不再使用的聊天记录
+ # Use common method to check and clean up unused chat records
deleted_chats = await check_and_clean_chats(session, rule_obj)
if deleted_chats > 0:
- logger.info(f"删除规则后清理了 {deleted_chats} 个未使用的聊天记录")
+ logger.info(f"Cleaned up {deleted_chats} unused chat records after deleting rule")
- # 删除机器人的消息
+ # Delete the bot's message
await message.delete()
- # 发送新的通知消息
- await respond_and_delete(event,('✅ 已删除规则'))
- await event.answer('已删除规则')
+ # Send new notification message
+ await respond_and_delete(event,('✅ Rule deleted'))
+ await event.answer('Rule deleted')
except Exception as e:
session.rollback()
- logger.error(f'删除规则时出错: {str(e)}')
+ logger.error(f'Error deleting rule: {str(e)}')
logger.exception(e)
- await event.answer('删除规则失败,请检查日志')
+ await event.answer('Failed to delete rule, please check logs')
async def callback_page(event, rule_id, session, message, data):
- """处理翻页的回调"""
- logger.info(f'翻页回调数据: action=page, rule_id={rule_id}')
+ """Handle callback for page navigation"""
+ logger.info(f'Page navigation callback data: action=page, rule_id={rule_id}')
try:
- # 解析页码和命令
+ # Parse page number and command
page_number, command = rule_id.split(':')
page = int(page_number)
- # 获取当前聊天和规则
+ # Get current chat and rule
current_chat = await event.get_chat()
current_chat_db = session.query(Chat).filter(
Chat.telegram_chat_id == str(current_chat.id)
).first()
if not current_chat_db or not current_chat_db.current_add_id:
- await event.answer('请先选择一个源聊天')
+ await event.answer('Please select a source chat first')
return
source_chat = session.query(Chat).filter(
@@ -179,7 +179,7 @@ async def callback_page(event, rule_id, session, message, data):
).first()
if command == 'keyword':
- # 获取关键字列表
+ # Get keyword list
keywords = session.query(Keyword).filter(
Keyword.rule_id == rule.id
).all()
@@ -188,13 +188,13 @@ async def callback_page(event, rule_id, session, message, data):
event,
'keyword',
keywords,
- lambda i, kw: f'{i}. {kw.keyword}{" (正则)" if kw.is_regex else ""}',
- f'关键字列表\n规则: 来自 {source_chat.name}',
+ lambda i, kw: f'{i}. {kw.keyword}{" (regex)" if kw.is_regex else ""}',
+ f'Keyword list\nRule: from {source_chat.name}',
page
)
elif command == 'replace':
- # 获取替换规则列表
+ # Get replace rule list
replace_rules = session.query(ReplaceRule).filter(
ReplaceRule.rule_id == rule.id
).all()
@@ -203,25 +203,25 @@ async def callback_page(event, rule_id, session, message, data):
event,
'replace',
replace_rules,
- lambda i, rr: f'{i}. 匹配: {rr.pattern} -> {"删除" if not rr.content else f"替换为: {rr.content}"}',
- f'替换规则列表\n规则: 来自 {source_chat.name}',
+ lambda i, rr: f'{i}. Match: {rr.pattern} -> {"Delete" if not rr.content else f"Replace with: {rr.content}"}',
+ f'Replace rule list\nRule: from {source_chat.name}',
page
)
- # 标记回调已处理
+ # Mark callback as handled
await event.answer()
except Exception as e:
- logger.error(f'处理翻页时出错: {str(e)}')
- await event.answer('处理翻页时出错,请检查日志')
+ logger.error(f'Error handling page navigation: {str(e)}')
+ await event.answer('Error handling page navigation, please check logs')
async def callback_rule_settings(event, rule_id, session, message, data):
- """处理规则设置的回调"""
+ """Handle callback for rule settings"""
rule = session.query(ForwardRule).get(rule_id)
if not rule:
- await event.answer('规则不存在')
+ await event.answer('Rule does not exist')
return
await message.edit(
@@ -230,25 +230,25 @@ async def callback_rule_settings(event, rule_id, session, message, data):
)
async def callback_toggle_current(event, rule_id, session, message, data):
- """处理切换当前规则的回调"""
+ """Handle callback for toggling current rule"""
rule = session.query(ForwardRule).get(rule_id)
if not rule:
- await event.answer('规则不存在')
+ await event.answer('Rule does not exist')
return
target_chat = rule.target_chat
source_chat = rule.source_chat
- # 检查是否已经是当前选中的规则
+ # Check if it is already the currently selected rule
if target_chat.current_add_id == source_chat.telegram_chat_id:
- await event.answer('已经是当前选中的规则')
+ await event.answer('Already the currently selected rule')
return
- # 更新当前选中的源聊天
+ # Update the currently selected source chat
target_chat.current_add_id = source_chat.telegram_chat_id
session.commit()
- # 更新按钮显示
+ # Update button display
try:
await message.edit(
await create_settings_text(rule),
@@ -258,12 +258,12 @@ async def callback_toggle_current(event, rule_id, session, message, data):
if 'message was not modified' not in str(e).lower():
raise
- await event.answer(f'已切换到: {source_chat.name}')
+ await event.answer(f'Switched to: {source_chat.name}')
async def callback_set_delay_time(event, rule_id, session, message, data):
- await event.edit("请选择延迟时间:", buttons=await create_delay_time_buttons(rule_id, page=0))
+ await event.edit("Please select delay time:", buttons=await create_delay_time_buttons(rule_id, page=0))
return
@@ -271,173 +271,173 @@ async def callback_set_delay_time(event, rule_id, session, message, data):
async def callback_delay_time_page(event, rule_id, session, message, data):
_, rule_id, page = data.split(':')
page = int(page)
- await event.edit("请选择延迟时间:", buttons=await create_delay_time_buttons(rule_id, page=page))
+ await event.edit("Please select delay time:", buttons=await create_delay_time_buttons(rule_id, page=page))
return
async def callback_select_delay_time(event, rule_id, session, message, data):
- parts = data.split(':', 2) # 最多分割2次
+ parts = data.split(':', 2) # Split at most 2 times
if len(parts) == 3:
_, rule_id, time = parts
- logger.info(f"设置规则 {rule_id} 的延迟时间为: {time}")
+ logger.info(f"Setting delay time for rule {rule_id} to: {time}")
try:
rule = session.query(ForwardRule).get(int(rule_id))
if rule:
- # 记录旧时间
+ # Record old time
old_time = rule.delay_seconds
- # 更新时间
+ # Update time
rule.delay_seconds = int(time)
session.commit()
- logger.info(f"数据库更新成功: {old_time} -> {time}")
+ logger.info(f"Database update successful: {old_time} -> {time}")
- # 获取消息对象
+ # Get message object
message = await event.get_message()
await message.edit(
await create_settings_text(rule),
buttons=await create_buttons(rule)
)
- logger.info("界面更新完成")
+ logger.info("UI update completed")
except Exception as e:
- logger.error(f"设置延迟时间时出错: {str(e)}")
- logger.error(f"错误详情: {traceback.format_exc()}")
+ logger.error(f"Error setting delay time: {str(e)}")
+ logger.error(f"Error details: {traceback.format_exc()}")
finally:
session.close()
return
async def callback_set_sync_rule(event, rule_id, session, message, data):
- """处理设置同步规则的回调"""
+ """Handle callback for setting sync rule"""
try:
rule = session.query(ForwardRule).get(int(rule_id))
if not rule:
- await event.answer('规则不存在')
+ await event.answer('Rule does not exist')
return
- await message.edit("请选择要同步到的规则:", buttons=await create_sync_rule_buttons(rule_id, page=0))
+ await message.edit("Please select a rule to sync to:", buttons=await create_sync_rule_buttons(rule_id, page=0))
except Exception as e:
- logger.error(f"设置同步规则时出错: {str(e)}")
- await event.answer('处理请求时出错,请检查日志')
+ logger.error(f"Error setting sync rule: {str(e)}")
+ await event.answer('Error processing request, please check logs')
return
async def callback_toggle_rule_sync(event, rule_id_data, session, message, data):
- """处理切换规则同步状态的回调"""
+ """Handle callback for toggling rule sync status"""
try:
- # 解析回调数据 - 格式为 source_rule_id:target_rule_id:page
+ # Parse callback data - format is source_rule_id:target_rule_id:page
parts = rule_id_data.split(":")
if len(parts) != 3:
- await event.answer('回调数据格式错误')
+ await event.answer('Invalid callback data format')
return
source_rule_id = int(parts[0])
target_rule_id = int(parts[1])
page = int(parts[2])
- # 获取数据库操作对象
+ # Get database operations object
db_ops = await get_db_ops()
- # 检查是否已存在同步关系
+ # Check if sync relationship already exists
syncs = await db_ops.get_rule_syncs(session, source_rule_id)
sync_target_ids = [sync.sync_rule_id for sync in syncs]
- # 切换同步状态
+ # Toggle sync status
if target_rule_id in sync_target_ids:
- # 如果已同步,则删除同步关系
+ # If already synced, delete sync relationship
success, message_text = await db_ops.delete_rule_sync(session, source_rule_id, target_rule_id)
if success:
- await event.answer(f'已取消同步规则 {target_rule_id}')
+ await event.answer(f'Cancelled sync for rule {target_rule_id}')
else:
- await event.answer(f'取消同步失败: {message_text}')
+ await event.answer(f'Failed to cancel sync: {message_text}')
else:
- # 如果未同步,则添加同步关系
+ # If not synced, add sync relationship
success, message_text = await db_ops.add_rule_sync(session, source_rule_id, target_rule_id)
if success:
- await event.answer(f'已设置同步到规则 {target_rule_id}')
+ await event.answer(f'Sync set to rule {target_rule_id}')
else:
- await event.answer(f'设置同步失败: {message_text}')
+ await event.answer(f'Failed to set sync: {message_text}')
- # 更新按钮显示
- await message.edit("请选择要同步到的规则:", buttons=await create_sync_rule_buttons(source_rule_id, page))
+ # Update button display
+ await message.edit("Please select a rule to sync to:", buttons=await create_sync_rule_buttons(source_rule_id, page))
except Exception as e:
- logger.error(f"切换规则同步状态时出错: {str(e)}")
- await event.answer('处理请求时出错,请检查日志')
+ logger.error(f"Error toggling rule sync status: {str(e)}")
+ await event.answer('Error processing request, please check logs')
return
async def callback_sync_rule_page(event, rule_id_data, session, message, data):
- """处理同步规则页面的翻页功能"""
+ """Handle pagination for sync rule page"""
try:
- # 解析回调数据 - 格式为 rule_id:page
+ # Parse callback data - format is rule_id:page
parts = rule_id_data.split(":")
if len(parts) != 2:
- await event.answer('回调数据格式错误')
+ await event.answer('Invalid callback data format')
return
rule_id = int(parts[0])
page = int(parts[1])
- # 检查规则是否存在
+ # Check if rule exists
rule = session.query(ForwardRule).get(rule_id)
if not rule:
- await event.answer('规则不存在')
+ await event.answer('Rule does not exist')
return
- # 更新按钮显示
- await message.edit("请选择要同步到的规则:", buttons=await create_sync_rule_buttons(rule_id, page))
+ # Update button display
+ await message.edit("Please select a rule to sync to:", buttons=await create_sync_rule_buttons(rule_id, page))
except Exception as e:
- logger.error(f"处理同步规则页面翻页时出错: {str(e)}")
- await event.answer('处理请求时出错,请检查日志')
+ logger.error(f"Error handling sync rule page navigation: {str(e)}")
+ await event.answer('Error processing request, please check logs')
return
async def callback_close_settings(event, rule_id, session, message, data):
- """处理关闭设置按钮的回调,删除当前消息"""
+ """Handle callback for close settings button, delete current message"""
try:
- logger.info("执行关闭设置操作,准备删除消息")
+ logger.info("Executing close settings, preparing to delete message")
await message.delete()
except Exception as e:
- logger.error(f"删除消息时出错: {str(e)}")
- await event.answer("关闭设置失败,请检查日志")
+ logger.error(f"Error deleting message: {str(e)}")
+ await event.answer("Failed to close settings, please check logs")
async def callback_noop(event, rule_id, session, message, data):
- # 用于页码按钮,不做任何操作
- await event.answer("当前页码")
+ # For page number button, do nothing
+ await event.answer("Current page")
return
async def callback_page_rule(event, page_str, session, message, data):
- """处理规则列表分页的回调"""
+ """Handle callback for rule list pagination"""
try:
page = int(page_str)
if page < 1:
- await event.answer('已经是第一页了')
+ await event.answer('Already on the first page')
return
per_page = 30
offset = (page - 1) * per_page
- # 获取总规则数
+ # Get total rule count
total_rules = session.query(ForwardRule).count()
if total_rules == 0:
- await event.answer('没有任何规则')
+ await event.answer('No rules found')
return
- # 计算总页数
+ # Calculate total pages
total_pages = (total_rules + per_page - 1) // per_page
if page > total_pages:
- await event.answer('已经是最后一页了')
+ await event.answer('Already on the last page')
return
- # 获取当前页的规则
+ # Get rules for current page
rules = session.query(ForwardRule).order_by(ForwardRule.id).offset(offset).limit(per_page).all()
- # 构建规则列表消息
- message_parts = [f'📋 转发规则列表 (第{page}/{total_pages}页):\n']
+ # Build rule list message
+ message_parts = [f'📋 Forwarding rule list (page {page}/{total_pages}):\n']
for rule in rules:
source_chat = rule.source_chat
@@ -445,25 +445,25 @@ async def callback_page_rule(event, page_str, session, message, data):
rule_desc = (
f'ID: {rule.id}\n'
- f'
来源: {source_chat.name} ({source_chat.telegram_chat_id})\n'
- f'目标: {target_chat.name} ({target_chat.telegram_chat_id})\n'
+ f'
'''
content += file_tag
-
- # 确保content不为空,至少包含一些默认文本
+
+ # Ensure content is not empty, at least include some default text
if not content:
- content = "
该消息没有文本内容。
"
+ content = "
This message has no text content.
"
if entry.media and len(entry.media) > 0:
- content += f"
包含 {len(entry.media)} 个媒体文件。
"
-
- # 确保content是有效的HTML
+ content += f"
Contains {len(entry.media)} media files.
"
+
+ # Ensure content is valid HTML
if not content.startswith("<"):
- # 预处理文本中的换行符,确保段落结构
+ # Preprocess line breaks in text to ensure paragraph structure
processed_content = ""
paragraphs = content.split("\n\n")
for p in paragraphs:
@@ -320,89 +320,89 @@ async def generate_feed_from_entries(rule_id: int, entries: List[Entry], base_ur
processed_content += f" {line}"
processed_content += ""
content = processed_content if processed_content else f"
{content}
"
-
- # 删除多余的HTML标签和空格,但保留有意义的段落结构
+
+ # Remove redundant HTML tags and spaces, but preserve meaningful paragraph structure
content = re.sub(r' \s* ', ' ', content)
content = re.sub(r'
\s*
', '', content)
content = re.sub(r'
', '', content)
-
- # 检查内容中是否包含硬编码的本地地址
+
+ # Check if content contains hardcoded local addresses
if "127.0.0.1" in content or "localhost" in content:
- logger.warning(f"内容中包含硬编码的本地地址,将替换为: {base_url}")
+ logger.warning(f"Content contains hardcoded local addresses, will replace with: {base_url}")
content = content.replace(f"http://127.0.0.1:{settings.PORT}", base_url)
content = content.replace(f"http://localhost:{settings.PORT}", base_url)
content = content.replace(f"http://{settings.HOST}:{settings.PORT}", base_url)
-
- # 添加媒体附件,并确保内容中包含所有媒体
+
+ # Add media attachments and ensure content includes all media
if entry.media:
for media in entry.media:
try:
- # 使用包含规则ID的媒体URL格式
+ # Use media URL format with rule ID
media_filename = os.path.basename(media.url.split('/')[-1])
full_media_url = f"{base_url}/media/{entry.rule_id}/{media_filename}"
-
- # 确保图片等内容已经添加
+
+ # Ensure images and other content have been added
if media.type.startswith('image/') and full_media_url not in content:
- # 如果内容中没有该图片,添加
+ # If the image is not in the content, add it
img_tag = f'
'
content += img_tag
- logger.info(f"添加缺失的图片标签: {media_filename}")
-
- # 记录添加的媒体附件
- logger.info(f"添加媒体附件: {full_media_url}, 类型: {media.type}, 大小: {media.size}")
-
- # 添加enclosure
+ logger.info(f"Added missing image tag: {media_filename}")
+
+ # Log added media attachment
+ logger.info(f"Added media attachment: {full_media_url}, type: {media.type}, size: {media.size}")
+
+ # Add enclosure
fe.enclosure(
url=full_media_url,
length=str(media.size) if hasattr(media, 'size') else "0",
type=media.type if hasattr(media, 'type') else "application/octet-stream"
)
except Exception as e:
- logger.error(f"添加媒体附件时出错: {str(e)}")
-
- # 设置内容字段
+ logger.error(f"Error adding media attachment: {str(e)}")
+
+ # Set content field
fe.content(content, type='html')
-
- # 设置描述字段 - 使用相同的内容
+
+ # Set description field - using the same content
fe.description(content)
-
- # 解析ISO格式时间字符串,设置发布时间
+
+ # Parse ISO format time string, set publish time
try:
published_dt = datetime.fromisoformat(entry.published)
fe.published(published_dt)
except ValueError:
- # 如果时间格式无效,使用当前时间
+ # If time format is invalid, use current time
try:
tz = pytz.timezone(DEFAULT_TIMEZONE)
fe.published(datetime.now(tz))
except Exception as tz_error:
- logger.warning(f"时区设置错误: {str(tz_error)},使用UTC时区")
+ logger.warning(f"Timezone setting error: {str(tz_error)}, using UTC timezone")
fe.published(datetime.now(pytz.UTC))
-
- # 设置作者和链接
+
+ # Set author and link
if entry.author:
fe.author(name=entry.author)
-
+
if entry.link:
fe.link(href=entry.link)
except Exception as e:
- logger.error(f"添加条目到Feed时出错: {str(e)}")
+ logger.error(f"Error adding entry to Feed: {str(e)}")
continue
-
+
return fg
-
+
@staticmethod
def _extract_chat_name(link: str) -> str:
- """从Telegram链接中提取频道/群组名称"""
+ """Extract channel/group name from Telegram link"""
if not link or 't.me/' not in link:
return ""
-
+
try:
- # 例如从 https://t.me/channel_name/1234 提取 channel_name
+ # e.g. extract channel_name from https://t.me/channel_name/1234
parts = link.split('t.me/')
if len(parts) < 2:
return ""
-
+
channel_part = parts[1].split('/')[0]
return channel_part
except Exception:
@@ -412,138 +412,138 @@ def _extract_chat_name(link: str) -> str:
@staticmethod
def convert_markdown_to_html(text):
- """将Markdown格式转换为HTML,使用标准markdown库,并保留换行结构"""
+ """Convert Markdown format to HTML using the standard markdown library, preserving line break structure"""
if not text:
return ""
-
- # 使用标准markdown库转换
+
+ # Use the standard markdown library for conversion
try:
- # 预处理文本,确保连续的换行符被正确转换成段落
- # 先将连续的多个换行替换为特殊标记
+ # Preprocess text to ensure consecutive line breaks are correctly converted to paragraphs
+ # First replace multiple consecutive line breaks with a special marker
text = re.sub(r'\n{2,}', '\n\n\n\n', text)
-
- # 转义以#开头的标签,防止被识别为标题
+
+ # Escape tags starting with # to prevent them from being recognized as headings
lines = text.split('\n')
processed_lines = []
for line in lines:
if line.startswith('#'):
line = '\\' + line
- processed_lines.append(line + ' ') # 添加两个空格确保换行
+ processed_lines.append(line + ' ') # Add two spaces to ensure line break
text = '\n'.join(processed_lines)
-
- # 使用markdown模块转换
+
+ # Use the markdown module for conversion
html = markdown.markdown(text, extensions=['extra'])
-
- # 处理特殊标记,确保段落分隔
+
+ # Process special markers to ensure paragraph separation
html = html.replace('', '
')
-
+
return html
except Exception as e:
- # 如果出现异常,退回到基本处理
- logger.error(f"Markdown转换异常: {str(e)}")
-
- # 改进的换行处理:将连续的两个或更多换行符转换为段落分隔
+ # If an exception occurs, fall back to basic processing
+ logger.error(f"Markdown conversion exception: {str(e)}")
+
+ # Improved line break handling: convert two or more consecutive line breaks to paragraph separators
text = re.sub(r'\n{2,}', '
', text)
-
- # 将单个换行符转换为
+
+ # Convert single line breaks to
text = text.replace('\n', ' ')
-
+
return f"
{text}
"
-
+
@staticmethod
def generate_test_feed(rule_id: int, base_url: str = None) -> FeedGenerator:
- """生成测试Feed,当没有真实条目数据时使用
-
+ """Generate a test Feed, used when there are no real entry data
+
Args:
- rule_id: 规则ID
- base_url: 请求的基础URL,用于生成链接
-
+ rule_id: Rule ID
+ base_url: Base URL of the request, used for generating links
+
Returns:
- FeedGenerator: 配置好的测试Feed生成器
+ FeedGenerator: Configured test Feed generator
"""
fg = FeedGenerator()
- # 设置编码
+ # Set encoding
fg.load_extension('base', atom=True)
rss_config = None
-
- # 如果没有提供base_url,使用配置中的默认值
+
+ # If no base_url is provided, use the default from configuration
if base_url is None:
base_url = f"http://{settings.HOST}:{settings.PORT}"
-
- logger.info(f"生成测试Feed - 规则ID: {rule_id}, 基础URL: {base_url}")
-
- # 从数据库获取RSS配置
+
+ logger.info(f"Generating test Feed - Rule ID: {rule_id}, Base URL: {base_url}")
+
+ # Get RSS configuration from database
session = get_session()
try:
rss_config = session.query(RSSConfig).filter(RSSConfig.rule_id == rule_id).first()
- logger.info(f"获取RSS配置: {rss_config}")
-
- # 设置Feed基本信息
+ logger.info(f"Retrieved RSS config: {rss_config}")
+
+ # Set Feed basic information
if rss_config and rss_config.enable_rss:
if rss_config.rule_title:
fg.title(rss_config.rule_title)
else:
fg.title(f'')
-
+
if rss_config.rule_description:
fg.description(rss_config.rule_description)
else:
fg.description(f' ')
-
- # 设置语言
+
+ # Set language
fg.language(rss_config.language or 'zh-CN')
finally:
- # 确保会话被关闭
+ # Ensure session is closed
session.close()
-
- # 设置Feed链接
+
+ # Set Feed link
feed_url = f'{base_url}/rss/feed/{rule_id}'
- logger.info(f"设置Feed链接: {feed_url}")
+ logger.info(f"Setting Feed link: {feed_url}")
fg.link(href=feed_url)
-
- # 处理时区
+
+ # Handle timezone
try:
tz = pytz.timezone(DEFAULT_TIMEZONE)
except Exception as tz_error:
- logger.warning(f"时区设置错误: {str(tz_error)},使用UTC时区")
+ logger.warning(f"Timezone setting error: {str(tz_error)}, using UTC timezone")
tz = pytz.UTC
-
- # # 只添加一条测试条目
+
+ # # Only add one test entry
# try:
# fe = fg.add_entry()
-
- # # 设置测试条目ID和标题
+
+ # # Set test entry ID and title
# entry_id = f"test-{rule_id}-1"
# fe.id(entry_id)
- # fe.title(f"测试条目 - 规则 {rule_id}")
-
- # # 生成内容,包括测试说明
+ # fe.title(f"Test entry - Rule {rule_id}")
+
+ # # Generate content, including test description
# current_time = datetime.now(tz)
# content = f'''
- #
这是一个测试条目,由系统自动生成,因为规则 {rule_id} 当前没有任何消息数据。
- #
当有消息被转发时,真实的条目将会在这里显示。
+ #
This is a test entry, automatically generated by the system, because rule {rule_id} currently has no message data.
+ #
When messages are forwarded, real entries will be displayed here.