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 @@ ![img](images/logo/png/logo-title.png) -

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: -![示例图片](./images/user_spy.png) +![Example image](./images/user_spy.png) --- -#### 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): ![img](./images/flow_chart.png) -### ⚙️ 设置说明 -| 主设置界面 | AI设置界面 | 媒体设置界面 | +### ⚙️ Settings Description +| Main Settings Interface | AI Settings Interface | Media Settings Interface | |---------|------|------| | ![img](./images/settings_main.png) | ![img](./images/settings_ai.png) | ![img](./images/settings_media.png) | -#### 主设置说明 -以下对设置选项进行说明 -| 设置选项 | 说明 | +#### 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 | |---------|------| | ![img](./images/settings_push.png) | ![img](./images/settings_push_sub1.png) | -#### 设置说明 +#### 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 | |---------|------|------| | ![img](./images/rss_login.png) | ![img](./images/rss_dashboard.png) | ![img](./images/rss_create_config.png) | -### 新建/编辑配置界面说明 -| 设置选项 | 说明 | +### 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: [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](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'
Source: {source_chat.name} ({source_chat.telegram_chat_id})\n' + f'Target: {target_chat.name} ({target_chat.telegram_chat_id})\n' '
' ) message_parts.append(rule_desc) - # 创建分页按钮 + # Create pagination buttons buttons = [] nav_row = [] if page > 1: - nav_row.append(Button.inline('⬅️ 上一页', f'page_rule:{page-1}')) + nav_row.append(Button.inline('⬅️ Prev', f'page_rule:{page-1}')) else: nav_row.append(Button.inline('⬅️', 'noop')) nav_row.append(Button.inline(f'{page}/{total_pages}', 'noop')) if page < total_pages: - nav_row.append(Button.inline('下一页 ➡️', f'page_rule:{page+1}')) + nav_row.append(Button.inline('Next ➡️', f'page_rule:{page+1}')) else: nav_row.append(Button.inline('➡️', 'noop')) @@ -473,26 +473,26 @@ async def callback_page_rule(event, page_str, session, message, data): await event.answer() except Exception as e: - logger.error(f'处理规则列表分页时出错: {str(e)}') - await event.answer('处理分页请求时出错,请检查日志') + logger.error(f'Error handling rule list pagination: {str(e)}') + await event.answer('Error handling pagination request, please check logs') async def update_rule_setting(event, rule_id, session, message, field_name, config, setting_type): - """通用的规则设置更新函数 + """Generic rule setting update function Args: - event: 回调事件 - rule_id: 规则ID - session: 数据库会话 - message: 消息对象 - field_name: 字段名 - config: 设置配置 - setting_type: 设置类型 ('rule', 'media', 'ai') + event: Callback event + rule_id: Rule ID + session: Database session + message: Message object + field_name: Field name + config: Setting configuration + setting_type: Setting type ('rule', 'media', 'ai') """ - logger.info(f'找到匹配的设置项: {field_name}') + logger.info(f'Found matching setting: {field_name}') rule = session.query(ForwardRule).get(int(rule_id)) if not rule: - logger.warning(f'规则不存在: {rule_id}') - await event.answer('规则不存在') + logger.warning(f'Rule does not exist: {rule_id}') + await event.answer('Rule does not exist') return False current_value = getattr(rule, field_name) @@ -500,116 +500,116 @@ async def update_rule_setting(event, rule_id, session, message, field_name, conf setattr(rule, field_name, new_value) try: - # 首先更新当前规则 + # First update current rule session.commit() - logger.info(f'更新规则 {rule.id} 的 {field_name} 从 {current_value} 到 {new_value}') + logger.info(f'Updated rule {rule.id} {field_name} from {current_value} to {new_value}') - # 检查是否启用了同步功能,且不是"是否启用规则"字段和"启用同步"字段 + # Check if sync is enabled, and it's not the "enable_rule" or "enable_sync" field if rule.enable_sync and field_name != 'enable_rule' and field_name != 'enable_sync': - logger.info(f"规则 {rule.id} 启用了同步功能,正在同步设置更改到关联规则") - # 获取需要同步的规则列表 + logger.info(f"Rule {rule.id} has sync enabled, syncing setting changes to associated rules") + # Get list of rules to sync sync_rules = session.query(RuleSync).filter(RuleSync.rule_id == rule.id).all() - # 为每个同步规则应用相同的设置 + # Apply the same settings to each synced rule for sync_rule in sync_rules: sync_rule_id = sync_rule.sync_rule_id - logger.info(f"正在同步设置 {field_name} 到规则 {sync_rule_id}") + logger.info(f"Syncing setting {field_name} 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 settings try: - # 记录旧值 + # Record old value old_value = getattr(target_rule, field_name) - # 设置新值 + # Set new value setattr(target_rule, field_name, new_value) session.flush() - logger.info(f"同步规则 {sync_rule_id} 的 {field_name} 从 {old_value} 到 {new_value}") + logger.info(f"Synced rule {sync_rule_id} {field_name} from {old_value} to {new_value}") except Exception as e: - logger.error(f"同步设置到规则 {sync_rule_id} 时出错: {str(e)}") + logger.error(f"Error syncing settings to rule {sync_rule_id}: {str(e)}") continue - # 提交所有同步更改 + # Commit all sync changes session.commit() - logger.info("所有同步更改已提交") + logger.info("All sync changes committed") - # 根据设置类型更新UI + # Update UI based on setting type if setting_type == 'rule': await message.edit( await create_settings_text(rule), buttons=await create_buttons(rule) ) elif setting_type == 'media': - await event.edit("媒体设置:", buttons=await create_media_settings_buttons(rule)) + await event.edit("Media settings:", buttons=await create_media_settings_buttons(rule)) elif setting_type == 'ai': await message.edit( await get_ai_settings_text(rule), buttons=await create_ai_settings_buttons(rule) ) elif setting_type == 'other': - await event.edit("其他设置:", buttons=await create_other_settings_buttons(rule)) + await event.edit("Other settings:", buttons=await create_other_settings_buttons(rule)) elif setting_type == 'push': await event.edit(PUSH_SETTINGS_TEXT, buttons=await create_push_settings_buttons(rule), link_preview=False) display_name = config.get('display_name', field_name) if field_name == 'use_bot': - await event.answer(f'已切换到{"机器人" if new_value else "用户账号"}模式') + await event.answer(f'Switched to {"bot" if new_value else "user account"} mode') else: - await event.answer(f'已更新{display_name}') + await event.answer(f'Updated {display_name}') return True except Exception as e: session.rollback() - logger.error(f'更新规则设置时出错: {str(e)}') - await event.answer('更新设置失败,请检查日志') + logger.error(f'Error updating rule setting: {str(e)}') + await event.answer('Failed to update setting, please check logs') return False async def handle_callback(event): - """处理按钮回调""" + """Handle button callback""" try: data = event.data.decode() - logger.info(f'收到回调数据: {data}') + logger.info(f'Received callback data: {data}') - # 解析回调数据 + # Parse callback data parts = data.split(':') action = parts[0] rule_id = ':'.join(parts[1:]) if len(parts) > 1 else None - logger.info(f'解析回调数据: action={action}, rule_id={rule_id}') + logger.info(f'Parsed callback data: action={action}, rule_id={rule_id}') - # 获取消息对象 + # Get message object message = await event.get_message() - # 使用会话 + # Use session session = get_session() try: - # 获取对应的处理器 + # Get corresponding handler handler = CALLBACK_HANDLERS.get(action) if handler: - logger.info(f'找到对应的处理器: {handler}') + logger.info(f'Found corresponding handler: {handler}') await handler(event, rule_id, session, message, data) else: - logger.info(f'未找到对应的处理器,尝试处理规则设置切换: {action}') + logger.info(f'No corresponding handler found, trying rule setting toggle: {action}') - # 尝试在RULE_SETTINGS中查找 + # Try to find in RULE_SETTINGS for field_name, config in RULE_SETTINGS.items(): if action == config['toggle_action']: success = await update_rule_setting(event, rule_id, session, message, field_name, config, 'rule') if success: return - # 尝试在MEDIA_SETTINGS中查找 + # Try to find in MEDIA_SETTINGS for field_name, config in MEDIA_SETTINGS.items(): if action == config['toggle_action']: success = await update_rule_setting(event, rule_id, session, message, field_name, config, 'media') if success: return - # 尝试在AI_SETTINGS中查找 + # Try to find in AI_SETTINGS for field_name, config in AI_SETTINGS.items(): if action == config['toggle_action']: success = await update_rule_setting(event, rule_id, session, message, field_name, config, 'ai') @@ -619,13 +619,13 @@ async def handle_callback(event): session.close() except Exception as e: - logger.error(f'处理按钮回调时出错: {str(e)}') - logger.error(f'错误堆栈: {traceback.format_exc()}') - await event.answer('处理请求时出错,请检查日志') + logger.error(f'Error handling button callback: {str(e)}') + logger.error(f'Error stack trace: {traceback.format_exc()}') + await event.answer('Error processing request, please check logs') -# 回调处理器字典 +# Callback handler dictionary CALLBACK_HANDLERS = { 'toggle_current': callback_toggle_current, 'switch': callback_switch, @@ -642,7 +642,7 @@ async def handle_callback(event): 'set_sync_rule': callback_set_sync_rule, 'toggle_rule_sync': callback_toggle_rule_sync, 'sync_rule_page': callback_sync_rule_page, - # AI设置 + # AI settings 'set_summary_prompt': callback_set_summary_prompt, 'set_ai_prompt': callback_set_ai_prompt, 'ai_settings': callback_ai_settings, @@ -654,7 +654,7 @@ async def handle_callback(event): 'cancel_set_prompt': callback_cancel_set_prompt, 'cancel_set_summary': callback_cancel_set_summary, 'summary_now':callback_summary_now, - # 媒体设置 + # Media settings 'select_max_media_size': callback_select_max_media_size, 'set_max_media_size': callback_set_max_media_size, 'media_settings': callback_media_settings, @@ -665,7 +665,7 @@ async def handle_callback(event): 'toggle_media_extension': callback_toggle_media_extension, 'toggle_media_allow_text': callback_toggle_media_allow_text, 'noop': callback_noop, - # 其他设置 + # Other settings 'other_settings': callback_other_settings, 'copy_rule': callback_copy_rule, 'copy_keyword': callback_copy_keyword, @@ -687,7 +687,7 @@ async def handle_callback(event): 'cancel_set_original_link': callback_cancel_set_original_link, 'toggle_reverse_blacklist': callback_toggle_reverse_blacklist, 'toggle_reverse_whitelist': callback_toggle_reverse_whitelist, - # 推送设置 + # Push settings 'push_settings': callback_push_settings, 'toggle_enable_push': callback_toggle_enable_push, 'toggle_enable_only_push': callback_toggle_enable_only_push, diff --git a/handlers/button/callback/media_callback.py b/handlers/button/callback/media_callback.py index e3f638d..4936600 100644 --- a/handlers/button/callback/media_callback.py +++ b/handlers/button/callback/media_callback.py @@ -13,7 +13,7 @@ async def callback_media_settings(event, rule_id, session, message, data): - # 显示媒体设置页面 + # Display media settings page try: rule = session.query(ForwardRule).get(int(rule_id)) if rule: @@ -27,70 +27,70 @@ async def callback_media_settings(event, rule_id, session, message, data): async def callback_set_max_media_size(event, rule_id, session, message, data): - await event.edit("请选择最大媒体大小(MB):", buttons=await create_media_size_buttons(rule_id, page=0)) + await event.edit("Please select max media size (MB):", buttons=await create_media_size_buttons(rule_id, page=0)) return async def callback_select_max_media_size(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, size = parts - logger.info(f"设置规则 {rule_id} 的最大媒体大小为: {size}") + logger.info(f"Setting max media size for rule {rule_id} to: {size}") try: rule = session.query(ForwardRule).get(int(rule_id)) if rule: - # 记录旧大小 + # Record old size old_size = rule.max_media_size - # 更新最大媒体大小 + # Update max media size rule.max_media_size = int(size) session.commit() - logger.info(f"数据库更新成功: {old_size} -> {size}") + logger.info(f"Database update successful: {old_size} -> {size}") - # 检查是否启用了同步功能 + # Check if sync is enabled if rule.enable_sync: - logger.info(f"规则 {rule.id} 启用了同步功能,正在同步媒体大小设置到关联规则") - # 获取需要同步的规则列表 + logger.info(f"Rule {rule.id} has sync enabled, syncing media size 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 media size 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 media size 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 media size setting try: - # 记录旧大小 + # Record old size old_target_size = target_rule.max_media_size - # 设置新大小 + # Set new size target_rule.max_media_size = int(size) - logger.info(f"同步规则 {sync_rule_id} 的媒体大小从 {old_target_size} 到 {size}") + logger.info(f"Synced rule {sync_rule_id} media size from {old_target_size} to {size}") except Exception as e: - logger.error(f"同步媒体大小到规则 {sync_rule_id} 时出错: {str(e)}") + logger.error(f"Error syncing media size to rule {sync_rule_id}: {str(e)}") continue - # 提交所有同步更改 + # Commit all sync changes session.commit() - logger.info("所有同步媒体大小更改已提交") + logger.info("All synced media size changes committed") - # 获取消息对象 + # Get message object message = await event.get_message() - await event.edit("媒体设置:",buttons=await create_media_settings_buttons(rule)) - await event.answer(f"已设置最大媒体大小为: {size}MB") - logger.info("界面更新完成") + await event.edit("Media settings:",buttons=await create_media_settings_buttons(rule)) + await event.answer(f"Max media size set to: {size}MB") + 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 max media size: {str(e)}") + logger.error(f"Error details: {traceback.format_exc()}") finally: session.close() return @@ -101,156 +101,156 @@ async def callback_select_max_media_size(event, rule_id, session, message, data) async def callback_set_media_types(event, rule_id, session, message, data): - """处理查看并设置媒体类型的回调""" + """Handle callback for viewing and setting media types""" try: rule = session.query(ForwardRule).get(int(rule_id)) if not rule: - await event.answer("规则不存在") + await event.answer("Rule does not exist") return - # 获取或创建媒体类型设置 + # Get or create media type settings db_ops = await get_db_ops() success, msg, media_types = await db_ops.get_media_types(session, rule.id) if not success: - await event.answer(f"获取媒体类型设置失败: {msg}") + await event.answer(f"Failed to get media type settings: {msg}") return - # 显示媒体类型选择界面 - await event.edit("请选择要屏蔽的媒体类型", buttons=await create_media_types_buttons(rule.id, media_types)) + # Display media type selection interface + await event.edit("Please select media types to block", buttons=await create_media_types_buttons(rule.id, media_types)) except Exception as e: - logger.error(f"设置媒体类型时出错: {str(e)}") - logger.error(f"错误详情: {traceback.format_exc()}") - await event.answer(f"设置媒体类型时出错: {str(e)}") + logger.error(f"Error setting media types: {str(e)}") + logger.error(f"Error details: {traceback.format_exc()}") + await event.answer(f"Error setting media types: {str(e)}") finally: session.close() return async def callback_toggle_media_type(event, rule_id, session, message, data): - """处理切换媒体类型的回调""" + """Handle callback for toggling media type""" try: - # 正确解析数据获取rule_id和媒体类型 + # Correctly parse data to get rule_id and media type parts = data.split(':') if len(parts) < 3: - await event.answer("数据格式错误") + await event.answer("Invalid data format") return # toggle_media_type:31:voice action = parts[0] rule_id = parts[1] media_type = parts[2] - # 检查媒体类型是否有效 + # Check if media type is valid if media_type not in ['photo', 'document', 'video', 'audio', 'voice']: - await event.answer(f"无效的媒体类型: {media_type}") + await event.answer(f"Invalid media type: {media_type}") return - # 获取规则 + # Get rule rule = session.query(ForwardRule).get(int(rule_id)) if not rule: - await event.answer("规则不存在") + await event.answer("Rule does not exist") return - # 切换媒体类型状态 + # Toggle media type status db_ops = await get_db_ops() success, msg = await db_ops.toggle_media_type(session, rule.id, media_type) if not success: - await event.answer(f"切换媒体类型失败: {msg}") + await event.answer(f"Failed to toggle media type: {msg}") return - # 检查是否启用了同步功能 + # Check if sync is enabled if rule.enable_sync: - logger.info(f"规则 {rule.id} 启用了同步功能,正在同步媒体类型设置到关联规则") + logger.info(f"Rule {rule.id} has sync enabled, syncing media type settings to associated rules") - # 获取该规则的当前媒体类型状态 + # Get current media type status for this rule success, _, current_media_types = await db_ops.get_media_types(session, rule.id) if not success: - logger.warning(f"获取媒体类型设置失败,无法同步") + logger.warning(f"Failed to get media type settings, cannot sync") else: - # 获取需要同步的规则列表 + # Get list of rules to sync sync_rules = session.query(RuleSync).filter(RuleSync.rule_id == rule.id).all() - # 为每个同步规则应用相同的媒体类型设置 + # Apply the same media type settings to each synced rule for sync_rule in sync_rules: sync_rule_id = sync_rule.sync_rule_id - logger.info(f"正在同步媒体类型 {media_type} 到规则 {sync_rule_id}") + logger.info(f"Syncing media type {media_type} 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 media type settings try: - # 获取目标规则当前媒体类型设置 + # Get target rule's current media type settings target_success, _, target_media_types = await db_ops.get_media_types(session, sync_rule_id) if not target_success: - logger.warning(f"获取目标规则 {sync_rule_id} 的媒体类型设置失败,跳过") + logger.warning(f"Failed to get media type settings for target rule {sync_rule_id}, skipping") continue - # 获取当前类型的新状态 + # Get new status of current type current_type_status = getattr(current_media_types, media_type) - # 如果目标媒体类型状态与主规则不同,则进行更新 + # If target media type status differs from main rule, update it if getattr(target_media_types, media_type) != current_type_status: - # 强制设置为与主规则相同的状态 + # Force set to same status as main rule if current_type_status: - # 当前主规则是开启状态,确保目标规则也开启 + # Main rule is enabled, ensure target rule is also enabled if not getattr(target_media_types, media_type): await db_ops.toggle_media_type(session, sync_rule_id, media_type) - logger.info(f"同步规则 {sync_rule_id} 的媒体类型 {media_type} 已开启") + logger.info(f"Synced rule {sync_rule_id} media type {media_type} enabled") else: - # 当前主规则是关闭状态,确保目标规则也关闭 + # Main rule is disabled, ensure target rule is also disabled if getattr(target_media_types, media_type): await db_ops.toggle_media_type(session, sync_rule_id, media_type) - logger.info(f"同步规则 {sync_rule_id} 的媒体类型 {media_type} 已关闭") + logger.info(f"Synced rule {sync_rule_id} media type {media_type} disabled") else: - logger.info(f"目标规则 {sync_rule_id} 的媒体类型 {media_type} 状态已经是 {current_type_status},无需更改") + logger.info(f"Target rule {sync_rule_id} media type {media_type} status is already {current_type_status}, no change needed") except Exception as e: - logger.error(f"同步媒体类型到规则 {sync_rule_id} 时出错: {str(e)}") + logger.error(f"Error syncing media type to rule {sync_rule_id}: {str(e)}") continue - # 重新获取媒体类型设置 + # Re-fetch media type settings success, _, media_types = await db_ops.get_media_types(session, rule.id) if not success: - await event.answer("获取媒体类型设置失败") + await event.answer("Failed to get media type settings") return - # 更新界面 - await event.edit("请选择要屏蔽的媒体类型", buttons=await create_media_types_buttons(rule.id, media_types)) + # Update interface + await event.edit("Please select media types to block", buttons=await create_media_types_buttons(rule.id, media_types)) await event.answer(msg) except Exception as e: - logger.error(f"切换媒体类型时出错: {str(e)}") - logger.error(f"错误详情: {traceback.format_exc()}") - await event.answer(f"切换媒体类型时出错: {str(e)}") + logger.error(f"Error toggling media type: {str(e)}") + logger.error(f"Error details: {traceback.format_exc()}") + await event.answer(f"Error toggling media type: {str(e)}") finally: session.close() return async def callback_set_media_extensions(event, rule_id, session, message, data): - await event.edit("请选择要过滤的媒体扩展名:", buttons=await create_media_extensions_buttons(rule_id, page=0)) + await event.edit("Please select media extensions to filter:", buttons=await create_media_extensions_buttons(rule_id, page=0)) return async def callback_media_extensions_page(event, rule_id, session, message, data): _, rule_id, page = data.split(':') page = int(page) - await event.edit("请选择要过滤的媒体扩展名:", buttons=await create_media_extensions_buttons(rule_id, page=page)) + await event.edit("Please select media extensions to filter:", buttons=await create_media_extensions_buttons(rule_id, page=page)) return async def callback_toggle_media_extension(event, rule_id, session, message, data): - """处理切换媒体扩展名的回调""" + """Handle callback for toggling media extension""" try: - # 解析数据获取rule_id和扩展名 + # Parse data to get rule_id and extension parts = data.split(':') if len(parts) < 3: - await event.answer("数据格式错误") + await event.answer("Invalid data format") return # toggle_media_extension:31:jpg:0 @@ -258,176 +258,176 @@ async def callback_toggle_media_extension(event, rule_id, session, message, data rule_id = parts[1] extension = parts[2] - # 获取当前页码,如果提供了页码 + # Get current page number, if provided current_page = 0 if len(parts) > 3 and parts[3].isdigit(): current_page = int(parts[3]) - # 获取规则 + # Get rule rule = session.query(ForwardRule).get(int(rule_id)) if not rule: - await event.answer("规则不存在") + await event.answer("Rule does not exist") return - # 获取当前规则已选择的扩展名 + # Get currently selected extensions for the rule db_ops = await get_db_ops() selected_extensions = await db_ops.get_media_extensions(session, rule.id) selected_extension_list = [ext["extension"] for ext in selected_extensions] - # 切换扩展名状态 + # Toggle extension status was_selected = extension in selected_extension_list if was_selected: - # 如果已存在,则删除 + # If it exists, delete it extension_id = next((ext["id"] for ext in selected_extensions if ext["extension"] == extension), None) if extension_id: success, msg = await db_ops.delete_media_extensions(session, rule.id, [extension_id]) if success: - await event.answer(f"已移除扩展名: {extension}") + await event.answer(f"Extension removed: {extension}") - # 检查是否启用了同步功能 + # Check if sync is enabled if rule.enable_sync: - logger.info(f"规则 {rule.id} 启用了同步功能,正在同步媒体扩展名移除到关联规则") + logger.info(f"Rule {rule.id} has sync enabled, syncing media extension removal to associated rules") - # 获取需要同步的规则列表 + # Get list of rules to sync sync_rules = session.query(RuleSync).filter(RuleSync.rule_id == rule.id).all() - # 为每个同步规则应用相同的媒体扩展名设置 + # Apply the same media extension settings to each synced rule for sync_rule in sync_rules: sync_rule_id = sync_rule.sync_rule_id - logger.info(f"正在同步移除媒体扩展名 {extension} 到规则 {sync_rule_id}") + logger.info(f"Syncing removal of media extension {extension} 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 media extension settings try: - # 获取目标规则当前扩展名设置 + # Get target rule's current extension settings target_extensions = await db_ops.get_media_extensions(session, sync_rule_id) target_extension_list = [ext["extension"] for ext in target_extensions] - # 如果目标规则中存在该扩展名,则删除 + # If the extension exists in target rule, delete it if extension in target_extension_list: target_extension_id = next((ext["id"] for ext in target_extensions if ext["extension"] == extension), None) if target_extension_id: await db_ops.delete_media_extensions(session, sync_rule_id, [target_extension_id]) - logger.info(f"同步规则 {sync_rule_id} 的媒体扩展名 {extension} 已移除") + logger.info(f"Media extension {extension} removed from synced rule {sync_rule_id}") else: - logger.warning(f"目标规则 {sync_rule_id} 中找不到扩展名 {extension} 的ID") + logger.warning(f"Cannot find ID for extension {extension} in target rule {sync_rule_id}") else: - logger.info(f"目标规则 {sync_rule_id} 中不存在扩展名 {extension},无需删除") + logger.info(f"Extension {extension} does not exist in target rule {sync_rule_id}, no need to delete") except Exception as e: - logger.error(f"同步移除媒体扩展名到规则 {sync_rule_id} 时出错: {str(e)}") + logger.error(f"Error syncing media extension removal to rule {sync_rule_id}: {str(e)}") continue else: - await event.answer(f"移除扩展名失败: {msg}") + await event.answer(f"Failed to remove extension: {msg}") else: - # 如果不存在,则添加 + # If it doesn't exist, add it success, msg = await db_ops.add_media_extensions(session, rule.id, [extension]) if success: - await event.answer(f"已添加扩展名: {extension}") + await event.answer(f"Extension added: {extension}") - # 检查是否启用了同步功能 + # Check if sync is enabled if rule.enable_sync: - logger.info(f"规则 {rule.id} 启用了同步功能,正在同步媒体扩展名添加到关联规则") + logger.info(f"Rule {rule.id} has sync enabled, syncing media extension addition to associated rules") - # 获取需要同步的规则列表 + # Get list of rules to sync sync_rules = session.query(RuleSync).filter(RuleSync.rule_id == rule.id).all() - # 为每个同步规则应用相同的媒体扩展名设置 + # Apply the same media extension settings to each synced rule for sync_rule in sync_rules: sync_rule_id = sync_rule.sync_rule_id - logger.info(f"正在同步添加媒体扩展名 {extension} 到规则 {sync_rule_id}") + logger.info(f"Syncing addition of media extension {extension} 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 media extension settings try: - # 获取目标规则当前扩展名设置 + # Get target rule's current extension settings target_extensions = await db_ops.get_media_extensions(session, sync_rule_id) target_extension_list = [ext["extension"] for ext in target_extensions] - # 如果目标规则中不存在该扩展名,则添加 + # If the extension doesn't exist in target rule, add it if extension not in target_extension_list: await db_ops.add_media_extensions(session, sync_rule_id, [extension]) - logger.info(f"同步规则 {sync_rule_id} 的媒体扩展名 {extension} 已添加") + logger.info(f"Media extension {extension} added to synced rule {sync_rule_id}") else: - logger.info(f"目标规则 {sync_rule_id} 中已存在扩展名 {extension},无需添加") + logger.info(f"Extension {extension} already exists in target rule {sync_rule_id}, no need to add") except Exception as e: - logger.error(f"同步添加媒体扩展名到规则 {sync_rule_id} 时出错: {str(e)}") + logger.error(f"Error syncing media extension addition to rule {sync_rule_id}: {str(e)}") continue else: - await event.answer(f"添加扩展名失败: {msg}") + await event.answer(f"Failed to add extension: {msg}") - # 更新界面,使用之前获取的页码 - await event.edit("请选择要过滤的媒体扩展名:", buttons=await create_media_extensions_buttons(rule_id, page=current_page)) + # Update interface, using previously obtained page number + await event.edit("Please select media extensions to filter:", buttons=await create_media_extensions_buttons(rule_id, page=current_page)) except Exception as e: - logger.error(f"切换媒体扩展名时出错: {str(e)}") - logger.error(f"错误详情: {traceback.format_exc()}") - await event.answer(f"切换媒体扩展名时出错: {str(e)}") + logger.error(f"Error toggling media extension: {str(e)}") + logger.error(f"Error details: {traceback.format_exc()}") + await event.answer(f"Error toggling media extension: {str(e)}") finally: session.close() return async def callback_toggle_media_allow_text(event, rule_id, session, message, data): - """处理切换放行文本的回调""" + """Handle callback for toggling allow text""" try: rule = session.query(ForwardRule).get(int(rule_id)) if not rule: - await event.answer("规则不存在") + await event.answer("Rule does not exist") return - # 切换状态 + # Toggle status rule.media_allow_text = not rule.media_allow_text - # 检查是否启用了同步功能 + # Check if sync is enabled if rule.enable_sync: - logger.info(f"规则 {rule.id} 启用了同步功能,正在同步'放行文本'设置到关联规则") + logger.info(f"Rule {rule.id} has sync enabled, syncing 'allow text' 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 settings 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 'allow text' setting 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 settings try: target_rule.media_allow_text = rule.media_allow_text - logger.info(f"同步规则 {sync_rule_id} 的'放行文本'设置已更新为 {rule.media_allow_text}") + logger.info(f"'Allow text' setting of synced rule {sync_rule_id} updated to {rule.media_allow_text}") except Exception as e: - logger.error(f"同步'放行文本'设置到规则 {sync_rule_id} 时出错: {str(e)}") + logger.error(f"Error syncing 'allow text' setting to rule {sync_rule_id}: {str(e)}") continue - # 提交更改 + # Commit changes session.commit() - # 更新界面 + # Update interface await event.edit(await get_media_settings_text(), buttons=await create_media_settings_buttons(rule)) - # 向用户显示结果 - status = "开启" if rule.media_allow_text else "关闭" - await event.answer(f"已{status}放行文本") + # Show result to user + status = "enabled" if rule.media_allow_text else "disabled" + await event.answer(f"Allow text {status}") except Exception as e: session.rollback() - logger.error(f"切换放行文本设置时出错: {str(e)}") - logger.error(f"错误详情: {traceback.format_exc()}") - await event.answer(f"切换放行文本设置时出错: {str(e)}") + logger.error(f"Error toggling allow text setting: {str(e)}") + logger.error(f"Error details: {traceback.format_exc()}") + await event.answer(f"Error toggling allow text setting: {str(e)}") finally: session.close() return diff --git a/handlers/button/callback/other_callback.py b/handlers/button/callback/other_callback.py index 334ecdd..db3de5d 100644 --- a/handlers/button/callback/other_callback.py +++ b/handlers/button/callback/other_callback.py @@ -24,53 +24,53 @@ async def callback_other_settings(event, rule_id, session, message, data): - await event.edit("其他设置:", buttons=await create_other_settings_buttons(rule_id=rule_id)) + await event.edit("Other Settings:", buttons=await create_other_settings_buttons(rule_id=rule_id)) return async def callback_copy_rule(event, rule_id, session, message, data): - """显示复制规则选择界面 + """Show copy rule selection interface - 选择后将当前规则的设置复制到目标规则。 + After selection, current rule settings will be copied to the target rule. """ try: - # 检查是否包含page参数 + # Check if page parameter is included parts = data.split(':') page = 0 if len(parts) > 2: page = int(parts[2]) - # 从rule_id中提取源规则ID + # Extract source rule ID from rule_id source_rule_id = rule_id if ':' in str(rule_id): source_rule_id = str(rule_id).split(':')[0] - # 创建规则选择按钮 + # Create rule selection buttons buttons = await create_copy_rule_buttons(source_rule_id, page) - await event.edit("请选择要将当前规则复制到的目标规则:", buttons=buttons) + await event.edit("Please select the target rule to copy the current rule to:", buttons=buttons) except Exception as e: - logger.error(f"显示复制规则选择界面时出错: {str(e)}") - logger.error(f"错误详情: {traceback.format_exc()}") - await event.answer("显示复制规则界面失败") + logger.error(f"Error showing copy rule selection interface: {str(e)}") + logger.error(f"Error details: {traceback.format_exc()}") + await event.answer("Failed to show copy rule interface") return async def create_copy_rule_buttons(rule_id, page=0): - """创建复制规则按钮列表 + """Create copy rule button list 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 if ':' in str(rule_id): parts = str(rule_id).split(':') source_rule_id = int(parts[0]) @@ -79,59 +79,59 @@ async def create_copy_rule_buttons(rule_id, page=0): current_rule = session.query(ForwardRule).get(source_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 != source_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('👈 返回', f"other_settings:{source_rule_id}"), - Button.inline('❌ 关闭', 'close_settings') + Button.inline('👈 Back', f"other_settings:{source_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] - # 创建规则按钮 + # 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 - # 创建按钮文本 + # Create button text button_text = f"{rule.id} {source_chat.name}->{target_chat.name}" - # 创建回调数据:perform_copy_rule:源规则ID:目标规则ID + # Create callback data: perform_copy_rule:source_rule_id:target_rule_id callback_data = f"perform_copy_rule:{source_rule_id}:{rule.id}" 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"copy_rule:{source_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"copy_rule:{source_rule_id}:{page+1}")) else: @@ -141,8 +141,8 @@ async def create_copy_rule_buttons(rule_id, page=0): buttons.append(page_buttons) buttons.append([ - Button.inline('👈 返回', f"other_settings:{source_rule_id}"), - Button.inline('❌ 关闭', 'close_settings') + Button.inline('👈 Back', f"other_settings:{source_rule_id}"), + Button.inline('❌ Close', 'close_settings') ]) finally: @@ -151,34 +151,34 @@ async def create_copy_rule_buttons(rule_id, page=0): return buttons async def callback_perform_copy_rule(event, rule_id_data, session, message, data): - """执行复制规则操作 + """Execute copy rule operation Args: - rule_id_data: 格式为 "源规则ID:目标规则ID" + rule_id_data: Format is "source_rule_id:target_rule_id" """ try: - # 解析规则ID + # Parse rule IDs parts = rule_id_data.split(':') if len(parts) != 2: - await event.answer("数据格式错误") + await event.answer("Data format error") return source_rule_id = int(parts[0]) target_rule_id = int(parts[1]) - # 获取源规则和目标规则 + # Get source rule and target rule source_rule = session.query(ForwardRule).get(source_rule_id) target_rule = session.query(ForwardRule).get(target_rule_id) if not source_rule or not target_rule: - await event.answer("源规则或目标规则不存在") + await event.answer("Source rule or target rule does not exist") return if source_rule.id == target_rule.id: - await event.answer('不能复制规则到自身') + await event.answer('Cannot copy rule to itself') return - # 记录复制的各个部分成功数量 + # Record success count for each copied part keywords_normal_success = 0 keywords_normal_skip = 0 keywords_regex_success = 0 @@ -190,10 +190,10 @@ async def callback_perform_copy_rule(event, rule_id_data, session, message, data rule_syncs_success = 0 rule_syncs_skip = 0 - # 复制普通关键字 + # Copy plain keywords for keyword in source_rule.keywords: - if not keyword.is_regex: # 普通关键字 - # 检查是否已存在 + if not keyword.is_regex: # Plain keywords + # Check if already exists exists = any(not k.is_regex and k.keyword == keyword.keyword and k.is_blacklist == keyword.is_blacklist for k in target_rule.keywords) if not exists: @@ -208,10 +208,10 @@ async def callback_perform_copy_rule(event, rule_id_data, session, message, data else: keywords_normal_skip += 1 - # 复制正则关键字 + # Copy regex keywords for keyword in source_rule.keywords: - if keyword.is_regex: # 正则关键字 - # 检查是否已存在 + if keyword.is_regex: # Regex keywords + # Check if already exists exists = any(k.is_regex and k.keyword == keyword.keyword and k.is_blacklist == keyword.is_blacklist for k in target_rule.keywords) if not exists: @@ -226,9 +226,9 @@ async def callback_perform_copy_rule(event, rule_id_data, session, message, data else: keywords_regex_skip += 1 - # 复制替换规则 + # Copy replace rules for replace_rule in source_rule.replace_rules: - # 检查是否已存在 + # Check if already exists exists = any(r.pattern == replace_rule.pattern and r.content == replace_rule.content for r in target_rule.replace_rules) if not exists: @@ -242,10 +242,10 @@ async def callback_perform_copy_rule(event, rule_id_data, session, message, data else: replace_rules_skip += 1 - # 复制媒体扩展名设置 + # Copy media extension settings if hasattr(source_rule, 'media_extensions') and source_rule.media_extensions: for extension in source_rule.media_extensions: - # 检查是否已存在 + # Check if already exists exists = any(e.extension == extension.extension for e in target_rule.media_extensions) if not exists: new_extension = MediaExtensions( @@ -257,15 +257,15 @@ async def callback_perform_copy_rule(event, rule_id_data, session, message, data else: media_extensions_skip += 1 - # 复制媒体类型设置 + # Copy media type settings if hasattr(source_rule, 'media_types') and source_rule.media_types: target_media_types = session.query(MediaTypes).filter_by(rule_id=target_rule.id).first() if not target_media_types: - # 如果目标规则没有媒体类型设置,创建新的 + # If target rule has no media type settings, create new one target_media_types = MediaTypes(rule_id=target_rule.id) - # 使用inspect自动复制所有字段(除了id和rule_id) + # Use inspect to automatically copy all fields (except id and rule_id) media_inspector = inspect(MediaTypes) for column in media_inspector.columns: column_name = column.key @@ -274,22 +274,22 @@ async def callback_perform_copy_rule(event, rule_id_data, session, message, data session.add(target_media_types) else: - # 如果已有设置,更新现有设置 - # 使用inspect自动复制所有字段(除了id和rule_id) + # If settings already exist, update existing settings + # Use inspect to automatically copy all fields (except id and rule_id) media_inspector = inspect(MediaTypes) for column in media_inspector.columns: column_name = column.key if column_name not in ['id', 'rule_id']: setattr(target_media_types, column_name, getattr(source_rule.media_types, column_name)) - # 复制规则同步表数据 - # 检查源规则是否有同步关系 + # Copy rule sync table data + # Check if source rule has sync relationships if hasattr(source_rule, 'rule_syncs') and source_rule.rule_syncs: for sync in source_rule.rule_syncs: - # 检查是否已存在 + # Check if already exists exists = any(s.sync_rule_id == sync.sync_rule_id for s in target_rule.rule_syncs) if not exists: - # 确保不会创建自引用的同步关系 + # Ensure self-referencing sync relationships are not created if sync.sync_rule_id != target_rule.id: new_sync = RuleSync( rule_id=target_rule.id, @@ -298,55 +298,55 @@ async def callback_perform_copy_rule(event, rule_id_data, session, message, data session.add(new_sync) rule_syncs_success += 1 - # 启用目标规则的同步功能 + # Enable sync feature for target rule if rule_syncs_success > 0: target_rule.enable_sync = True else: rule_syncs_skip += 1 - # 复制规则设置 - # 保存目标规则的原始关联 + # Copy rule settings + # Save target rule's original associations original_source_chat_id = target_rule.source_chat_id original_target_chat_id = target_rule.target_chat_id - # 获取ForwardRule模型的所有字段 + # Get all fields of ForwardRule model inspector = inspect(ForwardRule) for column in inspector.columns: column_name = column.key if column_name not in ['id', 'source_chat_id', 'target_chat_id', 'source_chat', 'target_chat', 'keywords', 'replace_rules', 'media_types']: - # 获取源规则的值并设置到目标规则 + # Get value from source rule and set to target rule value = getattr(source_rule, column_name) setattr(target_rule, column_name, value) - # 恢复目标规则的原始关联 + # Restore target rule's original associations target_rule.source_chat_id = original_source_chat_id target_rule.target_chat_id = original_target_chat_id - # 保存更改 + # Save changes session.commit() - # 构建消息内容 + # Build message content result_message = ( - f"✅ 已从规则 `{source_rule_id}` 复制到规则 `{target_rule.id}`\n\n" - f"普通关键字: 成功复制 {keywords_normal_success} 个, 跳过重复 {keywords_normal_skip} 个\n" - f"正则关键字: 成功复制 {keywords_regex_success} 个, 跳过重复 {keywords_regex_skip} 个\n" - f"替换规则: 成功复制 {replace_rules_success} 个, 跳过重复 {replace_rules_skip} 个\n" - f"媒体扩展名: 成功复制 {media_extensions_success} 个, 跳过重复 {media_extensions_skip} 个\n" - f"同步规则: 成功复制 {rule_syncs_success} 个, 跳过重复 {rule_syncs_skip} 个\n" - f"媒体类型设置和其他规则设置已复制\n" + f"✅ Copied from rule `{source_rule_id}` to rule `{target_rule.id}`\n\n" + f"Plain keywords: successfully copied {keywords_normal_success}, skipped duplicates {keywords_normal_skip}\n" + f"Regex keywords: successfully copied {keywords_regex_success}, skipped duplicates {keywords_regex_skip}\n" + f"Replace rules: successfully copied {replace_rules_success}, skipped duplicates {replace_rules_skip}\n" + f"Media extensions: successfully copied {media_extensions_success}, skipped duplicates {media_extensions_skip}\n" + f"Sync rules: successfully copied {rule_syncs_success}, skipped duplicates {rule_syncs_skip}\n" + f"Media type settings and other rule settings have been copied\n" ) - # 创建返回设置按钮 + # Create back to settings button buttons = [[ - Button.inline('👈 返回设置', f"other_settings:{source_rule.id}"), - Button.inline('❌ 关闭', 'close_settings') + Button.inline('👈 Back to Settings', f"other_settings:{source_rule.id}"), + Button.inline('❌ Close', 'close_settings') ]] - # 删除原消息 + # Delete original message await message.delete() - # 发送新消息 + # Send new message await send_message_and_delete( event.client, event.chat_id, @@ -355,75 +355,75 @@ async def callback_perform_copy_rule(event, rule_id_data, session, message, data parse_mode='markdown' ) - await event.answer(f"已从规则 {source_rule_id} 复制所有设置到规则 {target_rule_id}") + await event.answer(f"Copied all settings from rule {source_rule_id} to rule {target_rule_id}") except Exception as e: - logger.error(f"复制规则时出错: {str(e)}") - logger.error(f"错误详情: {traceback.format_exc()}") - await event.answer(f"复制规则失败: {str(e)}") + logger.error(f"Error copying rule: {str(e)}") + logger.error(f"Error details: {traceback.format_exc()}") + await event.answer(f"Failed to copy rule: {str(e)}") return async def callback_copy_keyword(event, rule_id, session, message, data): - """复制关键字 + """Copy keywords - 显示可选择的规则列表,供用户选择要复制关键字到的目标规则。 - 选择后将当前规则的关键字复制到目标规则。 + Shows a list of selectable rules for the user to choose the target rule to copy keywords to. + After selection, keywords from the current rule will be copied to the target rule. """ try: - # 调用通用的规则选择函数 + # Call the generic rule selection function await show_rule_selection( - event, rule_id, data, "请选择要将当前规则的关键字复制到的目标规则:", "perform_copy_keyword" + event, rule_id, data, "Please select the target rule to copy keywords from the current rule to:", "perform_copy_keyword" ) except Exception as e: - logger.error(f"显示复制关键字选择界面时出错: {str(e)}") - logger.error(f"错误详情: {traceback.format_exc()}") - await event.answer("显示复制关键字界面失败") + logger.error(f"Error showing copy keyword selection interface: {str(e)}") + logger.error(f"Error details: {traceback.format_exc()}") + await event.answer("Failed to show copy keyword interface") return async def callback_copy_replace(event, rule_id, session, message, data): - """复制替换规则 + """Copy replace rules - 显示可选择的规则列表,供用户选择要复制替换规则到的目标规则。 - 选择后将当前规则的替换规则复制到目标规则。 + Shows a list of selectable rules for the user to choose the target rule to copy replace rules to. + After selection, replace rules from the current rule will be copied to the target rule. """ try: - # 调用通用的规则选择函数 + # Call the generic rule selection function await show_rule_selection( - event, rule_id, data, "请选择要将当前规则的替换规则复制到的目标规则:", "perform_copy_replace" + event, rule_id, data, "Please select the target rule to copy replace rules from the current rule to:", "perform_copy_replace" ) except Exception as e: - logger.error(f"显示复制替换规则选择界面时出错: {str(e)}") - logger.error(f"错误详情: {traceback.format_exc()}") - await event.answer("显示复制替换规则界面失败") + logger.error(f"Error showing copy replace rule selection interface: {str(e)}") + logger.error(f"Error details: {traceback.format_exc()}") + await event.answer("Failed to show copy replace rule interface") return async def callback_perform_copy_keyword(event, rule_id_data, session, message, data): - """执行复制关键字操作 + """Execute copy keyword operation Args: - rule_id_data: 格式为 "源规则ID:目标规则ID" + rule_id_data: Format is "source_rule_id:target_rule_id" """ try: - # 解析规则ID + # Parse rule IDs source_rule_id, target_rule_id = await parse_rule_ids(event, rule_id_data) if source_rule_id is None or target_rule_id is None: return - # 获取源规则和目标规则 + # Get source rule and target rule source_rule, target_rule = await get_rules(event, session, source_rule_id, target_rule_id) if not source_rule or not target_rule: return - # 记录复制的各个部分成功数量 + # Record success count for each copied part keywords_normal_success = 0 keywords_normal_skip = 0 keywords_regex_success = 0 keywords_regex_skip = 0 - # 复制普通关键字 + # Copy plain keywords for keyword in source_rule.keywords: - if not keyword.is_regex: # 普通关键字 - # 检查是否已存在 + if not keyword.is_regex: # Plain keywords + # Check if already exists exists = any(not k.is_regex and k.keyword == keyword.keyword and k.is_blacklist == keyword.is_blacklist for k in target_rule.keywords) if not exists: @@ -438,10 +438,10 @@ async def callback_perform_copy_keyword(event, rule_id_data, session, message, d else: keywords_normal_skip += 1 - # 复制正则关键字 + # Copy regex keywords for keyword in source_rule.keywords: - if keyword.is_regex: # 正则关键字 - # 检查是否已存在 + if keyword.is_regex: # Regex keywords + # Check if already exists exists = any(k.is_regex and k.keyword == keyword.keyword and k.is_blacklist == keyword.is_blacklist for k in target_rule.keywords) if not exists: @@ -456,51 +456,51 @@ async def callback_perform_copy_keyword(event, rule_id_data, session, message, d else: keywords_regex_skip += 1 - # 保存更改 + # Save changes session.commit() - # 构建消息内容 + # Build message content result_message = ( - f"✅ 已从规则 `{source_rule_id}` 复制关键字到规则 `{target_rule.id}`\n\n" - f"普通关键字: 成功复制 {keywords_normal_success} 个, 跳过重复 {keywords_normal_skip} 个\n" - f"正则关键字: 成功复制 {keywords_regex_success} 个, 跳过重复 {keywords_regex_skip} 个\n" + f"✅ Copied keywords from rule `{source_rule_id}` to rule `{target_rule.id}`\n\n" + f"Plain keywords: successfully copied {keywords_normal_success}, skipped duplicates {keywords_normal_skip}\n" + f"Regex keywords: successfully copied {keywords_regex_success}, skipped duplicates {keywords_regex_skip}\n" ) - # 发送结果消息 + # Send result message await send_result_message(event, message, result_message, source_rule.id) - await event.answer(f"已从规则 {source_rule_id} 复制关键字到规则 {target_rule_id}") + await event.answer(f"Copied keywords from rule {source_rule_id} to rule {target_rule_id}") except Exception as e: - logger.error(f"复制关键字时出错: {str(e)}") - logger.error(f"错误详情: {traceback.format_exc()}") - await event.answer(f"复制关键字失败: {str(e)}") + logger.error(f"Error copying keywords: {str(e)}") + logger.error(f"Error details: {traceback.format_exc()}") + await event.answer(f"Failed to copy keywords: {str(e)}") return async def callback_perform_copy_replace(event, rule_id_data, session, message, data): - """执行复制替换规则操作 + """Execute copy replace rules operation Args: - rule_id_data: 格式为 "源规则ID:目标规则ID" + rule_id_data: Format is "source_rule_id:target_rule_id" """ try: - # 解析规则ID + # Parse rule IDs source_rule_id, target_rule_id = await parse_rule_ids(event, rule_id_data) if source_rule_id is None or target_rule_id is None: return - # 获取源规则和目标规则 + # Get source and target rules source_rule, target_rule = await get_rules(event, session, source_rule_id, target_rule_id) if not source_rule or not target_rule: return - # 记录复制的成功数量 + # Record copy success count replace_rules_success = 0 replace_rules_skip = 0 - # 复制替换规则 + # Copy replace rules for replace_rule in source_rule.replace_rules: - # 检查是否已存在 + # Check if already exists exists = any(r.pattern == replace_rule.pattern and r.content == replace_rule.content for r in target_rule.replace_rules) if not exists: @@ -514,70 +514,70 @@ async def callback_perform_copy_replace(event, rule_id_data, session, message, d else: replace_rules_skip += 1 - # 保存更改 + # Save changes session.commit() - # 构建消息内容 + # Build message content result_message = ( - f"✅ 已从规则 `{source_rule_id}` 复制替换规则到规则 `{target_rule.id}`\n\n" - f"替换规则: 成功复制 {replace_rules_success} 个, 跳过重复 {replace_rules_skip} 个\n" + f"✅ Copied replace rules from rule `{source_rule_id}` to rule `{target_rule.id}`\n\n" + f"Replace rules: successfully copied {replace_rules_success}, skipped duplicates {replace_rules_skip}\n" ) - # 发送结果消息 + # Send result message await send_result_message(event, message, result_message, source_rule.id) - await event.answer(f"已从规则 {source_rule_id} 复制替换规则到规则 {target_rule_id}") + await event.answer(f"Copied replace rules from rule {source_rule_id} to rule {target_rule_id}") except Exception as e: - logger.error(f"复制替换规则时出错: {str(e)}") - logger.error(f"错误详情: {traceback.format_exc()}") - await event.answer(f"复制替换规则失败: {str(e)}") + logger.error(f"Error copying replace rules: {str(e)}") + logger.error(f"Error details: {traceback.format_exc()}") + await event.answer(f"Failed to copy replace rules: {str(e)}") return -# 通用辅助函数 +# Generic helper functions async def show_rule_selection(event, rule_id, data, title, callback_action): - """显示规则选择界面的通用函数 + """Generic function to show rule selection interface Args: - event: 事件对象 - rule_id: 当前规则ID - data: 回调数据 - title: 显示标题 - callback_action: 选择后要执行的回调动作 + event: Event object + rule_id: Current rule ID + data: Callback data + title: Display title + callback_action: Callback action to execute after selection """ - # 检查是否包含page参数 + # Check if page parameter is included parts = data.split(':') page = 0 if len(parts) > 2: page = int(parts[2]) - # 从rule_id中提取源规则ID + # Extract source rule ID from rule_id source_rule_id = rule_id if ':' in str(rule_id): source_rule_id = str(rule_id).split(':')[0] - # 创建规则选择按钮 + # Create rule selection buttons buttons = await create_rule_selection_buttons(source_rule_id, page, callback_action) await event.edit(title, buttons=buttons) async def create_rule_selection_buttons(rule_id, page=0, callback_action="perform_copy_rule"): - """创建规则选择按钮的通用函数 + """Generic function to create rule selection buttons Args: - rule_id: 当前规则ID - page: 当前页码 - callback_action: 按钮点击后的回调动作 + rule_id: Current rule ID + page: Current page number + callback_action: Callback action after button click Returns: - 按钮列表 + Button list """ - # 设置分页参数 + # Set pagination parameters buttons = [] session = get_session() try: - # 获取当前规则 + # Get current rule if ':' in str(rule_id): parts = str(rule_id).split(':') source_rule_id = int(parts[0]) @@ -586,61 +586,61 @@ async def create_rule_selection_buttons(rule_id, page=0, callback_action="perfor current_rule = session.query(ForwardRule).get(source_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 != source_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"other_settings:{source_rule_id}"), - Button.inline('❌ 关闭', 'close_settings') + Button.inline('👈 Back', f"other_settings:{source_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] - # 创建规则按钮 + # 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 - # 创建按钮文本 + # Create button text button_text = f"{rule.id} {source_chat.name}->{target_chat.name}" - # 创建回调数据:callback_action:源规则ID:目标规则ID + # Create callback data: callback_action:source_rule_id:target_rule_id callback_data = f"{callback_action}:{source_rule_id}:{rule.id}" buttons.append([Button.inline(button_text, callback_data)]) - # 添加分页按钮 + # Add pagination buttons page_buttons = [] action_name = callback_action.replace("perform_", "") if total_pages > 1: - # 上一页按钮 + # Previous page button if page > 0: page_buttons.append(Button.inline("⬅️", f"{action_name}:{source_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"{action_name}:{source_rule_id}:{page+1}")) else: @@ -650,8 +650,8 @@ async def create_rule_selection_buttons(rule_id, page=0, callback_action="perfor buttons.append(page_buttons) buttons.append([ - Button.inline('👈 返回', f"other_settings:{source_rule_id}"), - Button.inline('❌ 关闭', 'close_settings') + Button.inline('👈 Back', f"other_settings:{source_rule_id}"), + Button.inline('❌ Close', 'close_settings') ]) finally: @@ -660,69 +660,69 @@ async def create_rule_selection_buttons(rule_id, page=0, callback_action="perfor return buttons async def parse_rule_ids(event, rule_id_data): - """解析规则ID + """Parse rule IDs Args: - event: 事件对象 - rule_id_data: 格式为 "源规则ID:目标规则ID" + event: Event object + rule_id_data: Format is "source_rule_id:target_rule_id" Returns: - (source_rule_id, target_rule_id) 或 (None, None) + (source_rule_id, target_rule_id) or (None, None) """ parts = rule_id_data.split(':') if len(parts) != 2: - await event.answer("数据格式错误") + await event.answer("Data format error") return None, None source_rule_id = int(parts[0]) target_rule_id = int(parts[1]) if source_rule_id == target_rule_id: - await event.answer('不能复制到自身') + await event.answer('Cannot copy to itself') return None, None return source_rule_id, target_rule_id async def get_rules(event, session, source_rule_id, target_rule_id): - """获取源规则和目标规则 + """Get source rule and target rule Args: - event: 事件对象 - session: 数据库会话 - source_rule_id: 源规则ID - target_rule_id: 目标规则ID + event: Event object + session: Database session + source_rule_id: Source rule ID + target_rule_id: Target rule ID Returns: - (source_rule, target_rule) 或 (None, None) + (source_rule, target_rule) or (None, None) """ source_rule = session.query(ForwardRule).get(source_rule_id) target_rule = session.query(ForwardRule).get(target_rule_id) if not source_rule or not target_rule: - await event.answer("源规则或目标规则不存在") + await event.answer("Source rule or target rule does not exist") return None, None return source_rule, target_rule async def send_result_message(event, message, result_message, target_rule_id): - """发送结果消息 + """Send result message Args: - event: 事件对象 - message: 原消息对象 - result_message: 结果消息内容 - target_rule_id: 目标规则ID + event: Event object + message: Original message object + result_message: Result message content + target_rule_id: Target rule ID """ - # 创建返回设置按钮 + # Create back to settings button buttons = [[ - Button.inline('👈 返回设置', f"other_settings:{target_rule_id}"), - Button.inline('❌ 关闭', 'close_settings') + Button.inline('👈 Back to Settings', f"other_settings:{target_rule_id}"), + Button.inline('❌ Close', 'close_settings') ]] - # 删除原消息 + # Delete original message await message.delete() - # 发送新消息 + # Send new message await send_message_and_delete( event.client, event.chat_id, @@ -732,115 +732,115 @@ async def send_result_message(event, message, result_message, target_rule_id): ) async def callback_clear_keyword(event, rule_id, session, message, data): - """显示清空关键字规则选择界面""" + """Show clear keywords rule selection interface""" try: - # 检查是否包含page参数 + # Check if page parameter is included parts = data.split(':') page = 0 if len(parts) > 2: page = int(parts[2]) - # 获取规则信息 + # Get rule info current_rule = session.query(ForwardRule).get(int(rule_id)) if not current_rule: - await event.answer("规则不存在") + await event.answer("Rule does not exist") return - # 创建按钮列表,首先添加当前规则 + # Create button list, add current rule first buttons = [] source_chat = current_rule.source_chat target_chat = current_rule.target_chat - # 当前规则按钮 - current_button_text = f"🗑️ 清空当前规则" + # Current rule button + current_button_text = f"🗑️ Clear current rule" current_callback_data = f"perform_clear_keyword:{current_rule.id}" buttons.append([Button.inline(current_button_text, current_callback_data)]) - # 检查是否有其他规则 + # Check if there are other rules other_rules = session.query(ForwardRule).filter( ForwardRule.id != current_rule.id ).count() if other_rules > 0: - # 分隔符 + # Separator buttons.append([Button.inline("---------", "noop")]) - # 添加其他规则按钮 + # Add other rule buttons other_buttons = await create_rule_selection_buttons(rule_id, page, "perform_clear_keyword") - # 将所有其他规则按钮添加到buttons中 + # Add all other rule buttons to buttons buttons.extend(other_buttons) else: - # 添加返回和关闭按钮 + # Add back and close buttons buttons.append([ - Button.inline('👈 返回', f"other_settings:{current_rule.id}"), - Button.inline('❌ 关闭', 'close_settings') + Button.inline('👈 Back', f"other_settings:{current_rule.id}"), + Button.inline('❌ Close', 'close_settings') ]) - await event.edit("请选择要清空关键字的规则:", buttons=buttons) + await event.edit("Please select the rule to clear keywords for:", buttons=buttons) except Exception as e: - logger.error(f"显示清空关键字选择界面时出错: {str(e)}") - logger.error(f"错误详情: {traceback.format_exc()}") - await event.answer("显示清空关键字界面失败") + logger.error(f"Error showing clear keyword selection interface: {str(e)}") + logger.error(f"Error details: {traceback.format_exc()}") + await event.answer("Failed to show clear keyword interface") return async def callback_clear_replace(event, rule_id, session, message, data): - """显示清空替换规则选择界面""" + """Show clear replace rules selection interface""" try: - # 检查是否包含page参数 + # Check if page parameter is included parts = data.split(':') page = 0 if len(parts) > 2: page = int(parts[2]) - # 获取规则信息 + # Get rule info current_rule = session.query(ForwardRule).get(int(rule_id)) if not current_rule: - await event.answer("规则不存在") + await event.answer("Rule does not exist") return - # 创建按钮列表,首先添加当前规则 + # Create button list, add current rule first buttons = [] source_chat = current_rule.source_chat target_chat = current_rule.target_chat - # 当前规则按钮 - current_button_text = f"🗑️ 清空当前规则" + # Current rule button + current_button_text = f"🗑️ Clear current rule" current_callback_data = f"perform_clear_replace:{current_rule.id}" buttons.append([Button.inline(current_button_text, current_callback_data)]) - # 检查是否有其他规则 + # Check if there are other rules other_rules = session.query(ForwardRule).filter( ForwardRule.id != current_rule.id ).count() if other_rules > 0: - # 分隔符 + # Separator buttons.append([Button.inline("---------", "noop")]) - # 添加其他规则按钮 + # Add other rule buttons other_buttons = await create_rule_selection_buttons(rule_id, page, "perform_clear_replace") - # 将所有其他规则按钮添加到buttons中 + # Add all other rule buttons to buttons buttons.extend(other_buttons) else: - # 添加返回和关闭按钮 + # Add back and close buttons buttons.append([ - Button.inline('👈 返回', f"other_settings:{current_rule.id}"), - Button.inline('❌ 关闭', 'close_settings') + Button.inline('👈 Back', f"other_settings:{current_rule.id}"), + Button.inline('❌ Close', 'close_settings') ]) - await event.edit("请选择要清空替换规则的规则:", buttons=buttons) + await event.edit("Please select the rule to clear replace rules for:", buttons=buttons) except Exception as e: - logger.error(f"显示清空替换规则选择界面时出错: {str(e)}") - logger.error(f"错误详情: {traceback.format_exc()}") - await event.answer("显示清空替换规则界面失败") + logger.error(f"Error showing clear replace rule selection interface: {str(e)}") + logger.error(f"Error details: {traceback.format_exc()}") + await event.answer("Failed to show clear replace rule interface") return async def callback_delete_rule(event, rule_id, session, message, data): - """显示删除规则选择界面""" + """Show delete rule selection interface""" try: - # 检查是否包含page参数 + # Check if page parameter is included parts = data.split(':') page = 0 if len(parts) > 2: @@ -850,97 +850,97 @@ async def callback_delete_rule(event, rule_id, session, message, data): if ':' in str(rule_id): source_rule_id = str(rule_id).split(':')[0] - # 获取规则信息 + # Get rule info current_rule = session.query(ForwardRule).get(int(source_rule_id)) if not current_rule: - await event.answer("规则不存在") + await event.answer("Rule does not exist") return - # 创建按钮列表,首先添加当前规则 + # Create button list, add current rule first buttons = [] source_chat = current_rule.source_chat target_chat = current_rule.target_chat - # 当前规则按钮 - current_button_text = f"❌ 删除当前规则" + # Current rule button + current_button_text = f"❌ Delete current rule" current_callback_data = f"perform_delete_rule:{current_rule.id}" buttons.append([Button.inline(current_button_text, current_callback_data)]) - # 检查是否有其他规则 + # Check if there are other rules other_rules = session.query(ForwardRule).filter( ForwardRule.id != current_rule.id ).count() if other_rules > 0: - # 分隔符 + # Separator buttons.append([Button.inline("---------", "noop")]) - # 添加其他规则按钮 + # Add other rule buttons other_buttons = await create_rule_selection_buttons(rule_id, page, "perform_delete_rule") - # 将所有其他规则按钮添加到buttons中 + # Add all other rule buttons to buttons buttons.extend(other_buttons) else: - # 添加返回和关闭按钮 + # Add back and close buttons buttons.append([ - Button.inline('👈 返回', f"other_settings:{current_rule.id}"), - Button.inline('❌ 关闭', 'close_settings') + Button.inline('👈 Back', f"other_settings:{current_rule.id}"), + Button.inline('❌ Close', 'close_settings') ]) - await event.edit("请选择要删除的规则:", buttons=buttons) + await event.edit("Please select the rule to delete:", buttons=buttons) except Exception as e: - logger.error(f"显示删除规则选择界面时出错: {str(e)}") - logger.error(f"错误详情: {traceback.format_exc()}") - await event.answer("显示删除规则界面失败") + logger.error(f"Error showing delete rule selection interface: {str(e)}") + logger.error(f"Error details: {traceback.format_exc()}") + await event.answer("Failed to show delete rule interface") return -# 执行清空关键字的回调 +# Execute clear keywords callback async def callback_perform_clear_keyword(event, rule_id_data, session, message, data): - """执行清空关键字操作""" + """Execute clear keywords operation""" try: - # 检查是否包含多个规则ID(格式为source_id:target_id) + # Check if contains multiple rule IDs (format: source_id:target_id) if ':' in rule_id_data: - # 解析规则ID + # Parse rule IDs source_rule_id, target_rule_id = await parse_rule_ids(event, rule_id_data) if source_rule_id is None or target_rule_id is None: return - # 使用目标规则ID + # Use target rule ID rule_id = target_rule_id else: - # 单个规则ID的情况(当前规则) + # Single rule ID case (current rule) rule_id = int(rule_id_data) - # 获取规则 + # Get rule rule = session.query(ForwardRule).get(rule_id) if not rule: - await event.answer("规则不存在") + await event.answer("Rule does not exist") return - # 获取并删除所有关键字 + # Get and delete all keywords keyword_count = len(rule.keywords) - # 删除所有关键字 + # Delete all keywords session.query(Keyword).filter(Keyword.rule_id == rule.id).delete() session.commit() - # 构建消息内容 - result_message = f"✅ 已清空规则 `{rule.id}` 的所有关键字,共删除 {keyword_count} 个关键字" + # Build message content + result_message = f"✅ Cleared all keywords for rule `{rule.id}`, deleted {keyword_count} keywords in total" - # 返回按钮指向源规则的设置页面(如果有的话) + # Back button points to source rule's settings page (if applicable) source_id = int(rule_id_data.split(':')[0]) if ':' in rule_id_data else rule.id - # 发送结果消息 - # 创建返回设置按钮 + # Send result message + # Create back to settings button buttons = [[ - Button.inline('👈 返回设置', f"other_settings:{source_id}"), - Button.inline('❌ 关闭', 'close_settings') + Button.inline('👈 Back to Settings', f"other_settings:{source_id}"), + Button.inline('❌ Close', 'close_settings') ]] - # 删除原消息 + # Delete original message await message.delete() - # 发送新消息 + # Send new message await send_message_and_delete( event.client, event.chat_id, @@ -949,61 +949,61 @@ async def callback_perform_clear_keyword(event, rule_id_data, session, message, parse_mode='markdown' ) - await event.answer(f"已清空规则 {rule.id} 的所有关键字") + await event.answer(f"Cleared all keywords for rule {rule.id}") except Exception as e: - logger.error(f"清空关键字时出错: {str(e)}") - logger.error(f"错误详情: {traceback.format_exc()}") - await event.answer(f"清空关键字失败: {str(e)}") + logger.error(f"Error clearing keywords: {str(e)}") + logger.error(f"Error details: {traceback.format_exc()}") + await event.answer(f"Failed to clear keywords: {str(e)}") return -# 执行清空替换规则的回调 +# Execute clear replace rules callback async def callback_perform_clear_replace(event, rule_id_data, session, message, data): - """执行清空替换规则操作""" + """Execute clear replace rules operation""" try: - # 检查是否包含多个规则ID(格式为source_id:target_id) + # Check if contains multiple rule IDs (format: source_id:target_id) if ':' in rule_id_data: - # 解析规则ID + # Parse rule IDs source_rule_id, target_rule_id = await parse_rule_ids(event, rule_id_data) if source_rule_id is None or target_rule_id is None: return - # 使用目标规则ID + # Use target rule ID rule_id = target_rule_id else: - # 单个规则ID的情况(当前规则) + # Single rule ID case (current rule) rule_id = int(rule_id_data) - # 获取规则 + # Get rule rule = session.query(ForwardRule).get(rule_id) if not rule: - await event.answer("规则不存在") + await event.answer("Rule does not exist") return - # 获取并删除所有替换规则 + # Get and delete all replace rules replace_count = len(rule.replace_rules) - # 删除所有替换规则 + # Delete all replace rules session.query(ReplaceRule).filter(ReplaceRule.rule_id == rule.id).delete() session.commit() - # 构建消息内容 - result_message = f"✅ 已清空规则 `{rule.id}` 的所有替换规则,共删除 {replace_count} 个替换规则" + # Build message content + result_message = f"✅ Cleared all replace rules for rule `{rule.id}`, deleted {replace_count} replace rules in total" - # 返回按钮指向源规则的设置页面(如果有的话) + # Back button points to source rule's settings page (if applicable) source_id = int(rule_id_data.split(':')[0]) if ':' in rule_id_data else rule.id - # 发送结果消息 - # 创建返回设置按钮 + # Send result message + # Create back to settings button buttons = [[ - Button.inline('👈 返回设置', f"other_settings:{source_id}"), - Button.inline('❌ 关闭', 'close_settings') + Button.inline('👈 Back to Settings', f"other_settings:{source_id}"), + Button.inline('❌ Close', 'close_settings') ]] - # 删除原消息 + # Delete original message await message.delete() - # 发送新消息 + # Send new message await send_message_and_delete( event.client, event.chat_id, @@ -1012,113 +1012,113 @@ async def callback_perform_clear_replace(event, rule_id_data, session, message, parse_mode='markdown' ) - await event.answer(f"已清空规则 {rule.id} 的所有替换规则") + await event.answer(f"Cleared all replace rules for rule {rule.id}") except Exception as e: - logger.error(f"清空替换规则时出错: {str(e)}") - logger.error(f"错误详情: {traceback.format_exc()}") - await event.answer(f"清空替换规则失败: {str(e)}") + logger.error(f"Error clearing replace rules: {str(e)}") + logger.error(f"Error details: {traceback.format_exc()}") + await event.answer(f"Failed to clear replace rules: {str(e)}") return -# 执行删除规则的回调 +# Execute delete rule callback async def callback_perform_delete_rule(event, rule_id_data, session, message, data): - """执行删除规则操作""" + """Execute delete rule operation""" try: - # 检查是否包含多个规则ID(格式为source_id:target_id) + # Check if contains multiple rule IDs (format: source_id:target_id) if ':' in rule_id_data: - # 尝试使用parse_rule_ids函数解析 + # Try parsing with parse_rule_ids function parts = rule_id_data.split(':') if len(parts) == 2 and parts[0].isdigit() and parts[1].isdigit(): source_rule_id = int(parts[0]) target_rule_id = int(parts[1]) - # 使用目标规则ID + # Use target rule ID rule_id = target_rule_id else: - # 如果格式不是source_id:target_id,可能是rule_id:page格式 - # 只取第一部分作为规则ID + # If format is not source_id:target_id, it may be rule_id:page format + # Take only the first part as rule ID rule_id = int(parts[0]) else: - # 单个规则ID的情况(当前规则) + # Single rule ID case (current rule) rule_id = int(rule_id_data) - # 获取规则 + # Get rule rule = session.query(ForwardRule).get(rule_id) if not rule: - await event.answer("规则不存在") + await event.answer("Rule does not exist") return - # 先保存规则对象,用于后续检查聊天关联 + # 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 media extensions if hasattr(rule, 'media_extensions'): session.query(MediaExtensions).filter(MediaExtensions.rule_id == rule.id).delete() - # 删除媒体类型 + # Delete media types if hasattr(rule, 'media_types'): session.query(MediaTypes).filter(MediaTypes.rule_id == rule.id).delete() - # 删除规则同步关系 + # Delete rule sync relationships if hasattr(rule, 'rule_syncs'): session.query(RuleSync).filter(RuleSync.rule_id == rule.id).delete() session.query(RuleSync).filter(RuleSync.sync_rule_id == rule.id).delete() - # 删除规则 + # Delete rule session.delete(rule) - # 提交规则删除的更改 + # Commit rule deletion changes session.commit() - # 尝试删除RSS服务中的相关数据 + # Try to delete related data from 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)}") + # Does not affect main flow, continue execution - # 使用通用方法检查并清理不再使用的聊天记录 + # Use generic 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 rules") - # 构建消息内容 - result_message = f"✅ 已删除规则 `{rule.id}`" + # Build message content + result_message = f"✅ Rule `{rule.id}` deleted" - # 删除原消息 + # Delete original message await message.delete() - # 获取源规则ID(如果有的话) + # Get source rule ID (if applicable) source_id = int(rule_id_data.split(':')[0]) if ':' in rule_id_data else None - # 准备按钮 + # Prepare buttons if source_id and source_id != rule.id: - # 如果是从另一个规则删除的,提供返回原规则的按钮 + # If deleted from another rule, provide button to return to original rule buttons = [[ - Button.inline('👈 返回设置', f"other_settings:{source_id}"), - Button.inline('❌ 关闭', 'close_settings') + Button.inline('👈 Back to Settings', f"other_settings:{source_id}"), + Button.inline('❌ Close', 'close_settings') ]] else: - # 如果是删除的当前规则,只提供关闭按钮 - buttons = [[Button.inline('❌ 关闭', 'close_settings')]] + # If current rule was deleted, only provide close button + buttons = [[Button.inline('❌ Close', 'close_settings')]] - # 发送结果消息 + # Send result message await send_message_and_delete( event.client, event.chat_id, @@ -1127,29 +1127,29 @@ async def callback_perform_delete_rule(event, rule_id_data, session, message, da parse_mode='markdown' ) - await event.answer("规则已成功删除") + await event.answer("Rule deleted successfully") except Exception as e: session.rollback() - logger.error(f"删除规则时出错: {str(e)}") - logger.error(f"错误详情: {traceback.format_exc()}") - await event.answer(f"删除规则失败: {str(e)}") + logger.error(f"Error deleting rule: {str(e)}") + logger.error(f"Error details: {traceback.format_exc()}") + await event.answer(f"Failed to delete rule: {str(e)}") return async def callback_set_userinfo_template(event, rule_id, session, message, data): - """设置用户信息模板""" - logger.info(f"开始处理设置用户信息模板回调 - event: {event}, rule_id: {rule_id}") + """Set user info template""" + logger.info(f"Starting to process set user info template 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: @@ -1158,54 +1158,54 @@ async def callback_set_userinfo_template(event, rule_id, session, message, data) chat_id = abs(event.chat_id) state = f"set_userinfo_template:{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="userinfo") - # 启动超时取消任务 + # 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_template = rule.userinfo_template if hasattr(rule, 'userinfo_template') and rule.userinfo_template else '未设置' + current_template = rule.userinfo_template if hasattr(rule, 'userinfo_template') and rule.userinfo_template else 'Not set' help_text = ( - "用户信息模板用于在转发消息中添加用户信息。\n" - "可用变量:\n" - "{name} - 用户名\n" - "{id} - 用户ID\n" + "User info template is used to add user info in forwarded messages.\n" + "Available variables:\n" + "{name} - Username\n" + "{id} - User ID\n" ) await message.edit( - f"请发送新的用户信息模板\n" - f"当前规则ID: `{rule_id}`\n" - f"当前用户信息模板:\n\n`{current_template}`\n\n" + f"Please send the new user info template\n" + f"Current rule ID: `{rule_id}`\n" + f"Current user info template:\n\n`{current_template}`\n\n" f"{help_text}\n" - f"5分钟内未设置将自动取消", - buttons=[[Button.inline("取消", f"cancel_set_userinfo:{rule_id}")]] + f"Will be automatically cancelled if not set within 5 minutes", + buttons=[[Button.inline("Cancel", f"cancel_set_userinfo:{rule_id}")]] ) - logger.info("消息编辑成功") + logger.info("Message edited successfully") except Exception as e: - logger.error(f"编辑消息时出错: {str(e)}") + logger.error(f"Error editing message: {str(e)}") logger.exception(e) return async def callback_set_time_template(event, rule_id, session, message, data): - """设置时间模板""" - logger.info(f"开始处理设置时间模板回调 - event: {event}, rule_id: {rule_id}") + """Set time template""" + logger.info(f"Starting to process set time template 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 channel message if isinstance(event.chat, types.Channel): - # 检查是否是管理员 + # Check if 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: @@ -1214,91 +1214,91 @@ async def callback_set_time_template(event, rule_id, session, message, data): chat_id = abs(event.chat_id) state = f"set_time_template:{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="time") - # 启动超时取消任务 + # 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_template = rule.time_template if hasattr(rule, 'time_template') and rule.time_template else '未设置' + current_template = rule.time_template if hasattr(rule, 'time_template') and rule.time_template else 'Not set' help_text = ( - "时间模板用于在转发消息中添加时间信息。\n" - "可用变量:\n" - "{time} - 当前时间\n" + "Time template is used to add time info in forwarded messages.\n" + "Available variables:\n" + "{time} - Current time\n" ) await message.edit( - f"请发送新的时间模板\n" - f"当前规则ID: `{rule_id}`\n" - f"当前时间模板:\n\n`{current_template}`\n\n" + f"Please send the new time template\n" + f"Current rule ID: `{rule_id}`\n" + f"Current time template:\n\n`{current_template}`\n\n" f"{help_text}\n" - f"5分钟内未设置将自动取消", - buttons=[[Button.inline("取消", f"cancel_set_time:{rule_id}")]] + f"Will be automatically cancelled if not set within 5 minutes", + buttons=[[Button.inline("Cancel", f"cancel_set_time:{rule_id}")]] ) - logger.info("消息编辑成功") + logger.info("Message edited successfully") except Exception as e: - logger.error(f"编辑消息时出错: {str(e)}") + logger.error(f"Error editing message: {str(e)}") logger.exception(e) return async def cancel_state_after_timeout(user_id: int, chat_id: int, timeout_minutes: int = 5): - """在指定时间后自动取消状态""" + """Automatically cancel state after specified time""" 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_cancel_set_userinfo(event, rule_id, session, message, data): - """取消设置用户信息模板""" + """Cancel set user info template""" 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)) - # 返回到其他设置页面 - await event.edit("其他设置:", buttons=await create_other_settings_buttons(rule_id=rule_id)) - await event.answer("已取消设置") + # Return to other settings page + await event.edit("Other Settings:", buttons=await create_other_settings_buttons(rule_id=rule_id)) + await event.answer("Setting cancelled") finally: session.close() return async def callback_cancel_set_time(event, rule_id, session, message, data): - """取消设置时间模板""" + """Cancel set time template""" 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)) - # 返回到其他设置页面 - await event.edit("其他设置:", buttons=await create_other_settings_buttons(rule_id=rule_id)) - await event.answer("已取消设置") + # Return to other settings page + await event.edit("Other Settings:", buttons=await create_other_settings_buttons(rule_id=rule_id)) + await event.answer("Setting cancelled") finally: session.close() return async def callback_set_original_link_template(event, rule_id, session, message, data): - """设置原始链接模板""" - logger.info(f"开始处理设置原始链接模板回调 - event: {event}, rule_id: {rule_id}") + """Set original link template""" + logger.info(f"Starting to process set original link template 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 channel message if isinstance(event.chat, types.Channel): - # 检查是否是管理员 + # Check if 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: @@ -1307,84 +1307,84 @@ async def callback_set_original_link_template(event, rule_id, session, message, chat_id = abs(event.chat_id) state = f"set_original_link_template:{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="link") - # 启动超时取消任务 + # 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_template = rule.original_link_template if hasattr(rule, 'original_link_template') and rule.original_link_template else '未设置' + current_template = rule.original_link_template if hasattr(rule, 'original_link_template') and rule.original_link_template else 'Not set' help_text = ( - "原始链接模板用于在转发消息中添加原始链接。\n" - "可用变量:\n" - "{original_link} - 完整的原始链接\n" + "Original link template is used to add original links in forwarded messages.\n" + "Available variables:\n" + "{original_link} - Full original link\n" ) await message.edit( - f"请发送新的原始链接模板\n" - f"当前规则ID: `{rule_id}`\n" - f"当前原始链接模板:\n\n`{current_template}`\n\n" + f"Please send the new original link template\n" + f"Current rule ID: `{rule_id}`\n" + f"Current original link template:\n\n`{current_template}`\n\n" f"{help_text}\n" - f"5分钟内未设置将自动取消", - buttons=[[Button.inline("取消", f"cancel_set_link:{rule_id}")]] + f"Will be automatically cancelled if not set within 5 minutes", + buttons=[[Button.inline("Cancel", f"cancel_set_link:{rule_id}")]] ) - logger.info("消息编辑成功") + logger.info("Message edited successfully") except Exception as e: - logger.error(f"编辑消息时出错: {str(e)}") + logger.error(f"Error editing message: {str(e)}") logger.exception(e) return async def callback_cancel_set_original_link(event, rule_id, session, message, data): - """取消设置原始链接模板""" + """Cancel set original link template""" 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)) - # 返回到其他设置页面 - await event.edit("其他设置:", buttons=await create_other_settings_buttons(rule_id=rule_id)) - await event.answer("已取消设置") + # Return to other settings page + await event.edit("Other Settings:", buttons=await create_other_settings_buttons(rule_id=rule_id)) + await event.answer("Setting cancelled") finally: session.close() return async def callback_toggle_reverse_blacklist(event, rule_id, session, message, data): - """切换反转黑名单设置""" + """Toggle reverse blacklist setting""" try: rule = session.query(ForwardRule).get(int(rule_id)) if rule: rule.enable_reverse_blacklist = not rule.enable_reverse_blacklist session.commit() - await event.answer("设置已更新") + await event.answer("Setting updated") await event.edit( buttons=await create_other_settings_buttons(rule_id=rule_id) ) except Exception as e: - logger.error(f"切换反转黑名单设置时出错: {str(e)}") - await event.answer("更新设置失败") + logger.error(f"Error toggling reverse blacklist setting: {str(e)}") + await event.answer("Failed to update setting") return async def callback_toggle_reverse_whitelist(event, rule_id, session, message, data): - """切换反转白名单设置""" + """Toggle reverse whitelist setting""" try: rule = session.query(ForwardRule).get(int(rule_id)) if rule: rule.enable_reverse_whitelist = not rule.enable_reverse_whitelist session.commit() - await event.answer("设置已更新") + await event.answer("Setting updated") await event.edit( buttons=await create_other_settings_buttons(rule_id=rule_id) ) except Exception as e: - logger.error(f"切换反转白名单设置时出错: {str(e)}") - await event.answer("更新设置失败") + logger.error(f"Error toggling reverse whitelist setting: {str(e)}") + await event.answer("Failed to update setting") return diff --git a/handlers/button/callback/push_callback.py b/handlers/button/callback/push_callback.py index 25996d0..77f2b17 100644 --- a/handlers/button/callback/push_callback.py +++ b/handlers/button/callback/push_callback.py @@ -28,104 +28,104 @@ async def callback_push_settings(event, rule_id, session, message, data): return async def callback_toggle_enable_push(event, rule_id, session, message, data): - """处理切换推送启用状态的回调""" + """Handle callback for toggling push enable status""" try: - # 获取规则 + # Get rule rule = session.query(ForwardRule).get(int(rule_id)) rule.enable_push = not rule.enable_push - # 检查是否启用了同步功能 + # Check if sync is enabled if rule.enable_sync: - logger.info(f"规则 {rule.id} 启用了同步功能,正在同步推送状态到关联规则") - # 获取需要同步的规则列表 + logger.info(f"Rule {rule.id} has sync enabled, syncing push status to associated rules") + # Get list of rules to sync sync_rules = session.query(RuleSync).filter(RuleSync.rule_id == rule.id).all() - # 为每个同步规则应用相同的设置 + # Apply the same settings 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 push status 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 try: - # 更新同步目标规则的推送状态 + # Update push status of sync target rule target_rule.enable_push = rule.enable_push - logger.info(f"同步规则 {sync_rule_id} 的推送状态已更新为 {rule.enable_push}") + logger.info(f"Push status of synced rule {sync_rule_id} updated to {rule.enable_push}") except Exception as e: - logger.error(f"同步推送状态到规则 {sync_rule_id} 时出错: {str(e)}") + logger.error(f"Error syncing push status to rule {sync_rule_id}: {str(e)}") continue session.commit() await event.edit(PUSH_SETTINGS_TEXT, buttons=await create_push_settings_buttons(rule_id), link_preview=False) - status = "启用" if rule.enable_push else "禁用" - await event.answer(f'已{status}推送功能') + status = "enabled" if rule.enable_push else "disabled" + await event.answer(f'Push function {status}') except Exception as e: session.rollback() - logger.error(f"切换推送状态时出错: {str(e)}") + logger.error(f"Error toggling push status: {str(e)}") logger.error(traceback.format_exc()) - await event.answer('处理请求时出错,请检查日志') + await event.answer('Error processing request, please check logs') async def callback_add_push_channel(event, rule_id, session, message, data): - """处理添加推送配置的回调""" + """Handle callback for adding push configuration""" try: - # 获取规则 + # Get rule rule = session.query(ForwardRule).get(int(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: user_id = event.sender_id - # 设置用户状态 + # Set user state chat_id = abs(event.chat_id) state = f"add_push_channel:{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}") state_manager.set_state(user_id, chat_id, state, message, state_type="push") - # 启动超时取消任务 + # Start timeout cancellation task asyncio.create_task(cancel_state_after_timeout(user_id, chat_id)) await message.edit( - f"请发送推送配置\n" - f"5分钟内未设置将自动取消", - buttons=[[Button.inline("取消", f"cancel_add_push_channel:{rule_id}")]] + f"Please send push configuration\n" + f"Will be automatically cancelled if not set within 5 minutes", + buttons=[[Button.inline("Cancel", f"cancel_add_push_channel:{rule_id}")]] ) except Exception as e: session.rollback() - logger.error(f"添加推送配置时出错: {str(e)}") + logger.error(f"Error adding push configuration: {str(e)}") logger.error(traceback.format_exc()) - await event.answer('处理请求时出错,请检查日志') + await event.answer('Error processing request, please check logs') async def callback_cancel_add_push_channel(event, rule_id, session, message, data): - """取消添加推送配置""" + """Cancel adding push configuration""" try: rule_id = data.split(':')[1] rule = session.query(ForwardRule).get(int(rule_id)) if not rule: - await event.answer('规则不存在') + await event.answer('Rule does not exist') return - # 清除状态 + # Clear state if isinstance(event.chat, types.Channel): user_id = os.getenv('USER_ID') else: @@ -135,46 +135,46 @@ async def callback_cancel_add_push_channel(event, rule_id, session, message, dat state_manager.clear_state(user_id, chat_id) await event.edit(PUSH_SETTINGS_TEXT, buttons=await create_push_settings_buttons(rule_id), link_preview=False) - await event.answer("已取消添加推送配置") + await event.answer("Push configuration addition cancelled") except Exception as e: - logger.error(f"取消添加推送配置时出错: {str(e)}") + logger.error(f"Error cancelling push configuration addition: {str(e)}") logger.error(traceback.format_exc()) - await event.answer('处理请求时出错,请检查日志') + await event.answer('Error processing request, please check logs') 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_toggle_push_config(event, config_id, session, message, data): - """处理点击推送配置的回调""" + """Handle callback for clicking push configuration""" try: config = session.query(PushConfig).get(int(config_id)) if not config: - await event.answer("推送配置不存在") + await event.answer("Push configuration does not exist") return await event.edit( - f"推送配置: `{config.push_channel}`\n", + f"Push config: `{config.push_channel}`\n", buttons=await create_push_config_details_buttons(config.id) ) except Exception as e: - logger.error(f"显示推送配置详情时出错: {str(e)}") + logger.error(f"Error displaying push configuration details: {str(e)}") logger.error(traceback.format_exc()) - await event.answer("处理请求时出错,请检查日志") + await event.answer("Error processing request, please check logs") async def callback_toggle_push_config_status(event, config_id, session, message, data): - """处理切换推送配置状态的回调""" + """Handle callback for toggling push configuration status""" try: config = session.query(PushConfig).get(int(config_id)) if not config: - await event.answer("推送配置不存在") + await event.answer("Push configuration does not exist") return rule_id = config.rule_id @@ -182,238 +182,238 @@ async def callback_toggle_push_config_status(event, config_id, session, message, config.enable_push_channel = not config.enable_push_channel - # 获取规则对象 + # Get rule object rule = session.query(ForwardRule).get(int(rule_id)) - # 检查是否启用了同步功能 + # Check if sync is enabled if rule and rule.enable_sync: - logger.info(f"规则 {rule.id} 启用了同步功能,正在同步推送配置状态到关联规则") + logger.info(f"Rule {rule.id} has sync enabled, syncing push configuration status to associated rules") - # 获取需要同步的规则列表 + # Get list of rules to sync sync_rules = session.query(RuleSync).filter(RuleSync.rule_id == rule.id).all() - # 为每个同步规则更新相同推送频道的状态 + # Update the same push channel status for each synced rule for sync_rule in sync_rules: sync_rule_id = sync_rule.sync_rule_id - logger.info(f"正在同步规则 {sync_rule_id} 的推送频道 {push_channel} 状态") + logger.info(f"Syncing push channel {push_channel} status for rule {sync_rule_id}") - # 查找目标规则的相同推送频道配置 + # Find the same push channel config for target rule target_config = session.query(PushConfig).filter_by( rule_id=sync_rule_id, push_channel=push_channel ).first() if not target_config: - logger.warning(f"同步目标规则 {sync_rule_id} 不存在推送频道 {push_channel},跳过") + logger.warning(f"Sync target rule {sync_rule_id} does not have push channel {push_channel}, skipping") continue try: - # 更新目标规则推送配置的状态 + # Update the push config status for target rule target_config.enable_push_channel = config.enable_push_channel - logger.info(f"已更新规则 {sync_rule_id} 的推送频道 {push_channel} 状态为 {config.enable_push_channel}") + logger.info(f"Updated push channel {push_channel} status for rule {sync_rule_id} to {config.enable_push_channel}") except Exception as e: - logger.error(f"更新规则 {sync_rule_id} 的推送配置状态时出错: {str(e)}") + logger.error(f"Error updating push config status for rule {sync_rule_id}: {str(e)}") continue session.commit() await event.edit( - f"推送配置: `{config.push_channel}`\n", + f"Push config: `{config.push_channel}`\n", buttons=await create_push_config_details_buttons(config.id) ) - status = "启用" if config.enable_push_channel else "禁用" - await event.answer(f"已{status}推送配置") + status = "enabled" if config.enable_push_channel else "disabled" + await event.answer(f"Push configuration {status}") except Exception as e: session.rollback() - logger.error(f"切换推送配置状态时出错: {str(e)}") + logger.error(f"Error toggling push configuration status: {str(e)}") logger.error(traceback.format_exc()) - await event.answer("处理请求时出错,请检查日志") + await event.answer("Error processing request, please check logs") async def callback_delete_push_config(event, config_id, session, message, data): - """处理删除推送配置的回调""" + """Handle callback for deleting push configuration""" try: config = session.query(PushConfig).get(int(config_id)) if not config: - await event.answer("推送配置不存在") + await event.answer("Push configuration does not exist") return rule_id = config.rule_id push_channel = config.push_channel - # 获取规则对象 + # Get rule object rule = session.query(ForwardRule).get(int(rule_id)) - # 检查是否启用了同步功能 + # Check if sync is enabled if rule and rule.enable_sync: - logger.info(f"规则 {rule.id} 启用了同步功能,正在同步删除推送配置到关联规则") + logger.info(f"Rule {rule.id} has sync enabled, syncing push config deletion to associated rules") - # 获取需要同步的规则列表 + # Get list of rules to sync sync_rules = session.query(RuleSync).filter(RuleSync.rule_id == rule.id).all() - # 为每个同步规则删除相同的推送配置 + # Delete the same push config for each synced rule for sync_rule in sync_rules: sync_rule_id = sync_rule.sync_rule_id - logger.info(f"正在同步删除规则 {sync_rule_id} 的推送频道 {push_channel}") + logger.info(f"Syncing deletion of push channel {push_channel} for rule {sync_rule_id}") - # 查找目标规则的相同推送频道配置 + # Find the same push channel config for target rule target_config = session.query(PushConfig).filter_by( rule_id=sync_rule_id, push_channel=push_channel ).first() if not target_config: - logger.warning(f"同步目标规则 {sync_rule_id} 不存在推送频道 {push_channel},跳过") + logger.warning(f"Sync target rule {sync_rule_id} does not have push channel {push_channel}, skipping") continue try: - # 删除目标规则的推送配置 + # Delete target rule's push config session.delete(target_config) - logger.info(f"已删除规则 {sync_rule_id} 的推送频道 {push_channel}") + logger.info(f"Deleted push channel {push_channel} for rule {sync_rule_id}") except Exception as e: - logger.error(f"删除规则 {sync_rule_id} 的推送配置时出错: {str(e)}") + logger.error(f"Error deleting push config for rule {sync_rule_id}: {str(e)}") continue - # 删除配置 + # Delete config session.delete(config) session.commit() await event.edit(PUSH_SETTINGS_TEXT, buttons=await create_push_settings_buttons(rule_id), link_preview=False) - await event.answer("已删除推送配置") + await event.answer("Push configuration deleted") except Exception as e: session.rollback() - logger.error(f"删除推送配置时出错: {str(e)}") + logger.error(f"Error deleting push configuration: {str(e)}") logger.error(traceback.format_exc()) - await event.answer("处理请求时出错,请检查日志") + await event.answer("Error processing request, please check logs") async def callback_push_page(event, rule_id_data, session, message, data): - """处理推送设置页面翻页的回调""" + """Handle callback for push settings page navigation""" try: - # 解析数据 + # Parse data parts = rule_id_data.split(":") if len(parts) != 2: - await event.answer("数据格式错误") + await event.answer("Invalid data format") return rule_id = int(parts[0]) page = int(parts[1]) await event.edit(PUSH_SETTINGS_TEXT, buttons=await create_push_settings_buttons(rule_id, page), link_preview=False) - await event.answer(f"第 {page+1} 页") + await event.answer(f"Page {page+1}") except Exception as e: - logger.error(f"处理推送设置翻页时出错: {str(e)}") + logger.error(f"Error handling push settings pagination: {str(e)}") logger.error(traceback.format_exc()) - await event.answer("处理请求时出错,请检查日志") + await event.answer("Error processing request, please check logs") async def callback_toggle_enable_only_push(event, rule_id, session, message, data): - """处理切换只转发到推送配置的回调""" + """Handle callback for toggling forward-to-push-only mode""" try: rule = session.query(ForwardRule).get(int(rule_id)) rule.enable_only_push = not rule.enable_only_push - # 检查是否启用了同步功能 + # Check if sync is enabled if rule.enable_sync: - logger.info(f"规则 {rule.id} 启用了同步功能,正在同步'只转发到推送配置'设置到关联规则") - # 获取需要同步的规则列表 + logger.info(f"Rule {rule.id} has sync enabled, syncing 'forward-to-push-only' 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 settings 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 'forward-to-push-only' setting 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 try: - # 更新同步目标规则的设置 + # Update sync target rule's setting target_rule.enable_only_push = rule.enable_only_push - logger.info(f"同步规则 {sync_rule_id} 的'只转发到推送配置'设置已更新为 {rule.enable_only_push}") + logger.info(f"'Forward-to-push-only' setting of synced rule {sync_rule_id} updated to {rule.enable_only_push}") except Exception as e: - logger.error(f"同步'只转发到推送配置'设置到规则 {sync_rule_id} 时出错: {str(e)}") + logger.error(f"Error syncing 'forward-to-push-only' setting to rule {sync_rule_id}: {str(e)}") continue session.commit() await event.edit(PUSH_SETTINGS_TEXT, buttons=await create_push_settings_buttons(rule_id), link_preview=False) - status = "启用" if rule.enable_only_push else "禁用" - await event.answer(f'已{status}只转发到推送配置') + status = "enabled" if rule.enable_only_push else "disabled" + await event.answer(f'Forward-to-push-only mode {status}') except Exception as e: session.rollback() - logger.error(f"切换只转发到推送配置状态时出错: {str(e)}") + logger.error(f"Error toggling forward-to-push-only status: {str(e)}") logger.error(traceback.format_exc()) - await event.answer('处理请求时出错,请检查日志') + await event.answer('Error processing request, please check logs') async def callback_toggle_media_send_mode(event, config_id, session, message, data): - """处理切换媒体发送方式的回调""" + """Handle callback for toggling media send mode""" try: config = session.query(PushConfig).get(int(config_id)) if not config: - await event.answer("推送配置不存在") + await event.answer("Push configuration does not exist") return rule_id = config.rule_id - # 切换媒体发送模式 + # Toggle media send mode if config.media_send_mode == "Single": config.media_send_mode = "Multiple" - new_mode = "全部" + new_mode = "All" else: config.media_send_mode = "Single" - new_mode = "单个" + new_mode = "Single" session.commit() - # 检查是否启用了同步功能 + # Check if sync is enabled rule = session.query(ForwardRule).get(int(rule_id)) if rule and rule.enable_sync: - logger.info(f"规则 {rule.id} 启用了同步功能,正在同步媒体发送方式到关联规则的推送配置") - # 获取需要同步的规则列表 + logger.info(f"Rule {rule.id} has sync enabled, syncing media send mode to push configs of associated rules") + # Get list of rules to sync sync_rules = session.query(RuleSync).filter(RuleSync.rule_id == rule.id).all() - # 获取当前推送配置的推送频道 + # Get the push channel of current push config push_channel = config.push_channel - # 为每个同步规则查找相同推送频道的配置并应用相同设置 + # Find the same push channel config for each synced rule and apply the same settings for sync_rule in sync_rules: sync_rule_id = sync_rule.sync_rule_id - logger.info(f"正在同步媒体发送方式到规则 {sync_rule_id} 的相同推送频道") + logger.info(f"Syncing media send mode to the same push channel of rule {sync_rule_id}") - # 查找目标规则下的相同推送频道配置 + # Find the same push channel config under target rule target_config = session.query(PushConfig).filter_by(rule_id=sync_rule_id, push_channel=push_channel).first() if not target_config: - logger.warning(f"同步目标规则 {sync_rule_id} 不存在相同推送频道 {push_channel},跳过") + logger.warning(f"Sync target rule {sync_rule_id} does not have push channel {push_channel}, skipping") continue try: - # 更新同步目标配置的媒体发送方式 + # Update media send mode of sync target config target_config.media_send_mode = config.media_send_mode - logger.info(f"同步规则 {sync_rule_id} 的推送频道 {push_channel} 的媒体发送方式已更新为 {config.media_send_mode}") + logger.info(f"Media send mode of push channel {push_channel} for synced rule {sync_rule_id} updated to {config.media_send_mode}") except Exception as e: - logger.error(f"同步媒体发送方式到规则 {sync_rule_id} 时出错: {str(e)}") + logger.error(f"Error syncing media send mode to rule {sync_rule_id}: {str(e)}") continue session.commit() - # 更新界面 + # Update interface await event.edit( - f"推送配置: `{config.push_channel}`\n", + f"Push config: `{config.push_channel}`\n", buttons=await create_push_config_details_buttons(config.id) ) - await event.answer(f"已设置媒体发送方式为: {new_mode}") + await event.answer(f"Media send mode set to: {new_mode}") except Exception as e: session.rollback() - logger.error(f"切换媒体发送方式时出错: {str(e)}") + logger.error(f"Error toggling media send mode: {str(e)}") logger.error(traceback.format_exc()) - await event.answer("处理请求时出错,请检查日志") + await event.answer("Error processing request, please check logs") diff --git a/handlers/button/settings_manager.py b/handlers/button/settings_manager.py index 8bf56e4..8383b82 100644 --- a/handlers/button/settings_manager.py +++ b/handlers/button/settings_manager.py @@ -7,42 +7,42 @@ AI_MODELS = load_ai_models() -# 规则配置字段定义 +# Rule configuration field definitions RULE_SETTINGS = { 'enable_rule': { - 'display_name': '是否启用规则', + 'display_name': 'Enable rule', 'values': { - True: '是', - False: '否' + True: 'Yes', + False: 'No' }, 'toggle_action': 'toggle_enable_rule', 'toggle_func': lambda current: not current }, 'add_mode': { - 'display_name': '当前关键字添加模式', + 'display_name': 'Current keyword add mode', 'values': { - AddMode.WHITELIST: '白名单', - AddMode.BLACKLIST: '黑名单' + AddMode.WHITELIST: 'Whitelist', + AddMode.BLACKLIST: 'Blacklist' }, 'toggle_action': 'toggle_add_mode', 'toggle_func': lambda current: AddMode.BLACKLIST if current == AddMode.WHITELIST else AddMode.WHITELIST }, 'is_filter_user_info': { - 'display_name': '过滤关键字时是否附带发送者名称和ID', + 'display_name': 'Include sender name and ID when filtering keywords', 'values': { - True: '是', - False: '否' + True: 'Yes', + False: 'No' }, 'toggle_action': 'toggle_filter_user_info', 'toggle_func': lambda current: not current }, 'forward_mode': { - 'display_name': '转发模式', + 'display_name': 'Forward mode', 'values': { - ForwardMode.BLACKLIST: '仅黑名单', - ForwardMode.WHITELIST: '仅白名单', - ForwardMode.BLACKLIST_THEN_WHITELIST: '先黑名单后白名单', - ForwardMode.WHITELIST_THEN_BLACKLIST: '先白名单后黑名单' + ForwardMode.BLACKLIST: 'Blacklist only', + ForwardMode.WHITELIST: 'Whitelist only', + ForwardMode.BLACKLIST_THEN_WHITELIST: 'Blacklist then whitelist', + ForwardMode.WHITELIST_THEN_BLACKLIST: 'Whitelist then blacklist' }, 'toggle_action': 'toggle_forward_mode', 'toggle_func': lambda current: { @@ -53,25 +53,25 @@ }[current] }, 'use_bot': { - 'display_name': '转发方式', + 'display_name': 'Forward method', 'values': { - True: '使用机器人', - False: '使用用户账号' + True: 'Use bot', + False: 'Use user account' }, 'toggle_action': 'toggle_bot', 'toggle_func': lambda current: not current }, 'is_replace': { - 'display_name': '替换模式', + 'display_name': 'Replace mode', 'values': { - True: '开启', - False: '关闭' + True: 'On', + False: 'Off' }, 'toggle_action': 'toggle_replace', 'toggle_func': lambda current: not current }, 'message_mode': { - 'display_name': '消息模式', + 'display_name': 'Message mode', 'values': { MessageMode.MARKDOWN: 'Markdown', MessageMode.HTML: 'HTML' @@ -80,11 +80,11 @@ 'toggle_func': lambda current: MessageMode.HTML if current == MessageMode.MARKDOWN else MessageMode.MARKDOWN }, 'is_preview': { - 'display_name': '预览模式', + 'display_name': 'Preview mode', 'values': { - PreviewMode.ON: '开启', - PreviewMode.OFF: '关闭', - PreviewMode.FOLLOW: '跟随原消息' + PreviewMode.ON: 'On', + PreviewMode.OFF: 'Off', + PreviewMode.FOLLOW: 'Follow original message' }, 'toggle_action': 'toggle_preview', 'toggle_func': lambda current: { @@ -94,56 +94,56 @@ }[current] }, 'is_original_link': { - 'display_name': '原始链接', + 'display_name': 'Original link', 'values': { - True: '附带', - False: '不附带' + True: 'Include', + False: 'Exclude' }, 'toggle_action': 'toggle_original_link', 'toggle_func': lambda current: not current }, 'is_delete_original': { - 'display_name': '删除原始消息', + 'display_name': 'Delete original message', 'values': { - True: '开启', - False: '关闭' + True: 'On', + False: 'Off' }, 'toggle_action': 'toggle_delete_original', 'toggle_func': lambda current: not current }, 'is_ufb': { - 'display_name': 'UFB同步', + 'display_name': 'UFB sync', 'values': { - True: '开启', - False: '关闭' + True: 'On', + False: 'Off' }, 'toggle_action': 'toggle_ufb', 'toggle_func': lambda current: not current }, 'is_original_sender': { - 'display_name': '原始发送者', + 'display_name': 'Original sender', 'values': { - True: '显示', - False: '隐藏' + True: 'Show', + False: 'Hide' }, 'toggle_action': 'toggle_original_sender', 'toggle_func': lambda current: not current }, 'is_original_time': { - 'display_name': '发送时间', + 'display_name': 'Send time', 'values': { - True: '显示', - False: '隐藏' + True: 'Show', + False: 'Hide' }, 'toggle_action': 'toggle_original_time', 'toggle_func': lambda current: not current }, - # 添加延迟过滤器设置 + # Add delay filter settings 'enable_delay': { - 'display_name': '延迟处理', + 'display_name': 'Delay processing', 'values': { - True: '开启', - False: '关闭' + True: 'On', + False: 'Off' }, 'toggle_action': 'toggle_enable_delay', 'toggle_func': lambda current: not current @@ -157,42 +157,42 @@ 'toggle_func': None }, 'handle_mode': { - 'display_name': '处理模式', + 'display_name': 'Handle mode', 'values': { - HandleMode.FORWARD: '转发模式', - HandleMode.EDIT: '编辑模式' + HandleMode.FORWARD: 'Forward mode', + HandleMode.EDIT: 'Edit mode' }, 'toggle_action': 'toggle_handle_mode', 'toggle_func': lambda current: HandleMode.EDIT if current == HandleMode.FORWARD else HandleMode.FORWARD }, 'enable_comment_button': { - 'display_name': '查看评论区', + 'display_name': 'View comments', 'values': { - True: '开启', - False: '关闭' + True: 'On', + False: 'Off' }, 'toggle_action': 'toggle_enable_comment_button', 'toggle_func': lambda current: not current }, 'only_rss': { - 'display_name': '只转发到RSS', + 'display_name': 'Forward to RSS only', 'values': { - True: '开启', - False: '关闭' + True: 'On', + False: 'Off' }, 'toggle_action': 'toggle_only_rss', 'toggle_func': lambda current: not current }, 'close_settings': { - 'display_name': '关闭', + 'display_name': 'Close', 'toggle_action': 'close_settings', 'toggle_func': None }, 'enable_sync': { - 'display_name': '启用同步', + 'display_name': 'Enable sync', 'values': { - True: '开启', - False: '关闭' + True: 'On', + False: 'Off' }, 'toggle_action': 'toggle_enable_sync', 'toggle_func': lambda current: not current @@ -200,61 +200,61 @@ } -# 添加 AI 设置 +# Add AI settings AI_SETTINGS = { 'is_ai': { - 'display_name': 'AI处理', + 'display_name': 'AI processing', 'values': { - True: '开启', - False: '关闭' + True: 'On', + False: 'Off' }, 'toggle_action': 'toggle_ai', 'toggle_func': lambda current: not current }, 'ai_model': { - 'display_name': 'AI模型', + 'display_name': 'AI model', 'values': { - None: '默认', - '': '默认', + None: 'Default', + '': 'Default', **{model: model for model in AI_MODELS} }, 'toggle_action': 'change_model', 'toggle_func': None }, 'ai_prompt': { - 'display_name': '设置AI处理提示词', + 'display_name': 'Set AI processing prompt', 'toggle_action': 'set_ai_prompt', 'toggle_func': None }, 'enable_ai_upload_image': { - 'display_name': '上传图片', + 'display_name': 'Upload image', 'values': { - True: '开启', - False: '关闭' + True: 'On', + False: 'Off' }, 'toggle_action': 'toggle_ai_upload_image', 'toggle_func': lambda current: not current }, 'is_keyword_after_ai': { - 'display_name': 'AI处理后再次执行关键字过滤', + 'display_name': 'Run keyword filter again after AI processing', 'values': { - True: '开启', - False: '关闭' + True: 'On', + False: 'Off' }, 'toggle_action': 'toggle_keyword_after_ai', 'toggle_func': lambda current: not current }, 'is_summary': { - 'display_name': 'AI总结', + 'display_name': 'AI summary', 'values': { - True: '开启', - False: '关闭' + True: 'On', + False: 'Off' }, 'toggle_action': 'toggle_summary', 'toggle_func': lambda current: not current }, 'summary_time': { - 'display_name': '总结时间', + 'display_name': 'Summary time', 'values': { None: '00:00', '': '00:00' @@ -263,21 +263,21 @@ 'toggle_func': None }, 'summary_prompt': { - 'display_name': '设置AI总结提示词', + 'display_name': 'Set AI summary prompt', 'toggle_action': 'set_summary_prompt', 'toggle_func': None }, 'is_top_summary': { - 'display_name': '顶置总结消息', + 'display_name': 'Pin summary message', 'values': { - True: '是', - False: '否' + True: 'Yes', + False: 'No' }, 'toggle_action': 'toggle_top_summary', 'toggle_func': lambda current: not current }, 'summary_now': { - 'display_name': '立即执行总结', + 'display_name': 'Execute summary now', 'toggle_action': 'summary_now', 'toggle_func': None } @@ -286,30 +286,30 @@ MEDIA_SETTINGS = { 'enable_media_type_filter': { - 'display_name': '媒体类型过滤', + 'display_name': 'Media type filter', 'values': { - True: '开启', - False: '关闭' + True: 'On', + False: 'Off' }, 'toggle_action': 'toggle_enable_media_type_filter', 'toggle_func': lambda current: not current }, 'selected_media_types': { - 'display_name': '选择的媒体类型', + 'display_name': 'Selected media types', 'toggle_action': 'set_media_types', 'toggle_func': None }, 'enable_media_size_filter': { - 'display_name': '媒体大小过滤', + 'display_name': 'Media size filter', 'values': { - True: '开启', - False: '关闭' + True: 'On', + False: 'Off' }, 'toggle_action': 'toggle_enable_media_size_filter', 'toggle_func': lambda current: not current }, 'max_media_size': { - 'display_name': '媒体大小限制', + 'display_name': 'Media size limit', 'values': { None: '5MB', '': '5MB' @@ -318,43 +318,43 @@ 'toggle_func': None }, 'is_send_over_media_size_message': { - 'display_name': '媒体大小超限时发送提醒', + 'display_name': 'Send alert when media size exceeds limit', 'values': { - True: '开启', - False: '关闭' + True: 'On', + False: 'Off' }, 'toggle_action': 'toggle_send_over_media_size_message', 'toggle_func': lambda current: not current }, 'enable_extension_filter': { - 'display_name': '媒体扩展名过滤', + 'display_name': 'Media extension filter', 'values': { - True: '开启', - False: '关闭' + True: 'On', + False: 'Off' }, 'toggle_action': 'toggle_enable_media_extension_filter', 'toggle_func': lambda current: not current }, 'extension_filter_mode': { - 'display_name': '媒体扩展名过滤模式', + 'display_name': 'Media extension filter mode', 'values': { - AddMode.BLACKLIST: '黑名单', - AddMode.WHITELIST: '白名单' + AddMode.BLACKLIST: 'Blacklist', + AddMode.WHITELIST: 'Whitelist' }, 'toggle_action': 'toggle_media_extension_filter_mode', 'toggle_func': lambda current: AddMode.WHITELIST if current == AddMode.BLACKLIST else AddMode.BLACKLIST }, 'media_extensions': { - 'display_name': '设置媒体扩展名', + 'display_name': 'Set media extensions', 'toggle_action': 'set_media_extensions', 'toggle_func': None, 'values': {} }, 'media_allow_text': { - 'display_name': '放行文本', + 'display_name': 'Allow text', 'values': { - True: '开启', - False: '关闭' + True: 'On', + False: 'Off' }, 'toggle_action': 'toggle_media_allow_text', 'toggle_func': lambda current: not current @@ -364,32 +364,32 @@ OTHER_SETTINGS = { 'copy_rule': { - 'display_name': '复制规则', + 'display_name': 'Copy rule', 'toggle_action': 'copy_rule', 'toggle_func': None }, 'copy_keyword': { - 'display_name': '复制关键字', + 'display_name': 'Copy keywords', 'toggle_action': 'copy_keyword', 'toggle_func': None }, 'copy_replace': { - 'display_name': '复制替换', + 'display_name': 'Copy replace rules', 'toggle_action': 'copy_replace', 'toggle_func': None }, 'clear_keyword': { - 'display_name': '清空所有关键字', + 'display_name': 'Clear all keywords', 'toggle_action': 'clear_keyword', 'toggle_func': None }, 'clear_replace': { - 'display_name': '清空所有替换规则', + 'display_name': 'Clear all replace rules', 'toggle_action': 'clear_replace', 'toggle_func': None }, 'delete_rule': { - 'display_name': '删除规则', + 'display_name': 'Delete rule', 'toggle_action': 'delete_rule', 'toggle_func': None }, @@ -399,27 +399,27 @@ 'toggle_func': None }, 'set_userinfo_template': { - 'display_name': '设置用户信息模板', + 'display_name': 'Set user info template', 'toggle_action': 'set_userinfo_template', 'toggle_func': None }, 'set_time_template': { - 'display_name': '设置时间模板', + 'display_name': 'Set time template', 'toggle_action': 'set_time_template', 'toggle_func': None }, 'set_original_link_template': { - 'display_name': '设置原始链接模板', + 'display_name': 'Set original link template', 'toggle_action': 'set_original_link_template', 'toggle_func': None }, 'reverse_blacklist': { - 'display_name': '反转黑名单', + 'display_name': 'Reverse blacklist', 'toggle_action': 'toggle_reverse_blacklist', 'toggle_func': None }, 'reverse_whitelist': { - 'display_name': '反转白名单', + 'display_name': 'Reverse whitelist', 'toggle_action': 'toggle_reverse_whitelist', 'toggle_func': None } @@ -427,91 +427,91 @@ PUSH_SETTINGS = { 'enable_push_channel': { - 'display_name': '启用推送', + 'display_name': 'Enable push', 'toggle_action': 'toggle_enable_push', 'toggle_func': None }, 'add_push_channel': { - 'display_name': '➕ 添加推送配置', + 'display_name': '➕ Add push configuration', 'toggle_action': 'add_push_channel', 'toggle_func': None }, 'enable_only_push': { - 'display_name': '只转发到推送配置', + 'display_name': 'Forward to push only', 'toggle_action': 'toggle_enable_only_push', 'toggle_func': None } } async def create_settings_text(rule): - """创建设置信息文本""" + """Create settings info text""" text = ( - "📋 管理转发规则\n\n" - f"规则ID: `{rule.id}`\n" + "📋 Manage forwarding rules\n\n" + f"Rule ID: `{rule.id}`\n" f"{rule.source_chat.name} --> {rule.target_chat.name}" ) return text async def create_buttons(rule): - """创建规则设置按钮""" + """Create rule settings buttons""" buttons = [] - # 获取当前聊天的当前选中规则 + # Get the currently selected rule for the current chat session = get_session() try: target_chat = rule.target_chat current_add_id = target_chat.current_add_id source_chat = rule.source_chat - # 添加规则切换按钮 + # Add rule toggle button is_current = current_add_id == source_chat.telegram_chat_id buttons.append([ Button.inline( - f"{'✅ ' if is_current else ''}应用当前规则", + f"{'✅ ' if is_current else ''}Apply current rule", f"toggle_current:{rule.id}" ) ]) buttons.append([ Button.inline( - f"是否启用规则: {RULE_SETTINGS['enable_rule']['values'][rule.enable_rule]}", + f"Enable rule: {RULE_SETTINGS['enable_rule']['values'][rule.enable_rule]}", f"toggle_enable_rule:{rule.id}" ) ]) - # 当前关键字添加模式 + # Current keyword add mode buttons.append([ Button.inline( - f"当前关键字添加模式: {RULE_SETTINGS['add_mode']['values'][rule.add_mode]}", + f"Keyword add mode: {RULE_SETTINGS['add_mode']['values'][rule.add_mode]}", f"toggle_add_mode:{rule.id}" ) ]) - # 是否过滤用户信息 + # Whether to filter user info buttons.append([ Button.inline( - f"过滤关键字时是否附带发送者名称和ID: {RULE_SETTINGS['is_filter_user_info']['values'][rule.is_filter_user_info]}", + f"Include sender name and ID when filtering: {RULE_SETTINGS['is_filter_user_info']['values'][rule.is_filter_user_info]}", f"toggle_filter_user_info:{rule.id}" ) ]) if RSS_ENABLED == 'false': - # 处理模式 + # Handle mode buttons.append([ Button.inline( - f"⚙️ 处理模式: {RULE_SETTINGS['handle_mode']['values'][rule.handle_mode]}", + f"⚙️ Handle mode: {RULE_SETTINGS['handle_mode']['values'][rule.handle_mode]}", f"toggle_handle_mode:{rule.id}" ) ]) else: - # 处理模式 + # Handle mode buttons.append([ Button.inline( - f"⚙️ 处理模式: {RULE_SETTINGS['handle_mode']['values'][rule.handle_mode]}", + f"⚙️ Handle mode: {RULE_SETTINGS['handle_mode']['values'][rule.handle_mode]}", f"toggle_handle_mode:{rule.id}" ), Button.inline( - f"⚠️ 只转发到RSS: {RULE_SETTINGS['only_rss']['values'][rule.only_rss]}", + f"⚠️ RSS only: {RULE_SETTINGS['only_rss']['values'][rule.only_rss]}", f"toggle_only_rss:{rule.id}" ) ]) @@ -519,84 +519,84 @@ async def create_buttons(rule): buttons.append([ Button.inline( - f"📥 过滤模式: {RULE_SETTINGS['forward_mode']['values'][rule.forward_mode]}", + f"📥 Filter mode: {RULE_SETTINGS['forward_mode']['values'][rule.forward_mode]}", f"toggle_forward_mode:{rule.id}" ), Button.inline( - f"🤖 转发方式: {RULE_SETTINGS['use_bot']['values'][rule.use_bot]}", + f"🤖 Forward method: {RULE_SETTINGS['use_bot']['values'][rule.use_bot]}", f"toggle_bot:{rule.id}" ) ]) - if rule.use_bot: # 只在使用机器人时显示这些设置 + if rule.use_bot: # Only show these settings when using bot buttons.append([ Button.inline( - f"🔄 替换模式: {RULE_SETTINGS['is_replace']['values'][rule.is_replace]}", + f"🔄 Replace mode: {RULE_SETTINGS['is_replace']['values'][rule.is_replace]}", f"toggle_replace:{rule.id}" ), Button.inline( - f"📝 消息格式: {RULE_SETTINGS['message_mode']['values'][rule.message_mode]}", + f"📝 Message format: {RULE_SETTINGS['message_mode']['values'][rule.message_mode]}", f"toggle_message_mode:{rule.id}" ) ]) buttons.append([ Button.inline( - f"👁 预览模式: {RULE_SETTINGS['is_preview']['values'][rule.is_preview]}", + f"👁 Preview mode: {RULE_SETTINGS['is_preview']['values'][rule.is_preview]}", f"toggle_preview:{rule.id}" ), Button.inline( - f"🔗 原始链接: {RULE_SETTINGS['is_original_link']['values'][rule.is_original_link]}", + f"🔗 Original link: {RULE_SETTINGS['is_original_link']['values'][rule.is_original_link]}", f"toggle_original_link:{rule.id}" ) ]) buttons.append([ Button.inline( - f"👤 原始发送者: {RULE_SETTINGS['is_original_sender']['values'][rule.is_original_sender]}", + f"👤 Original sender: {RULE_SETTINGS['is_original_sender']['values'][rule.is_original_sender]}", f"toggle_original_sender:{rule.id}" ), Button.inline( - f"⏰ 发送时间: {RULE_SETTINGS['is_original_time']['values'][rule.is_original_time]}", + f"⏰ Send time: {RULE_SETTINGS['is_original_time']['values'][rule.is_original_time]}", f"toggle_original_time:{rule.id}" ) ]) buttons.append([ Button.inline( - f"🗑 删除原消息: {RULE_SETTINGS['is_delete_original']['values'][rule.is_delete_original]}", + f"🗑 Delete original: {RULE_SETTINGS['is_delete_original']['values'][rule.is_delete_original]}", f"toggle_delete_original:{rule.id}" ), Button.inline( - f"💬 评论区按钮: {RULE_SETTINGS['enable_comment_button']['values'][rule.enable_comment_button]}", + f"💬 Comment button: {RULE_SETTINGS['enable_comment_button']['values'][rule.enable_comment_button]}", f"toggle_enable_comment_button:{rule.id}" ) ]) - # 添加延迟过滤器按钮 + # Add delay filter buttons buttons.append([ Button.inline( - f"⏱️ 延迟处理: {RULE_SETTINGS['enable_delay']['values'][rule.enable_delay]}", + f"⏱️ Delay processing: {RULE_SETTINGS['enable_delay']['values'][rule.enable_delay]}", f"toggle_enable_delay:{rule.id}" ), Button.inline( - f"⌛ 延迟秒数: {rule.delay_seconds or 5}秒", + f"⌛ Delay: {rule.delay_seconds or 5}s", f"set_delay_time:{rule.id}" ) ]) - # 添加同步规则相关按钮 + # Add sync rule related buttons buttons.append([ Button.inline( - f"🔄 同步规则: {RULE_SETTINGS['enable_sync']['values'][rule.enable_sync]}", + f"🔄 Sync rules: {RULE_SETTINGS['enable_sync']['values'][rule.enable_sync]}", f"toggle_enable_sync:{rule.id}" ), Button.inline( - f"📡 同步设置", + f"📡 Sync settings", f"set_sync_rule:{rule.id}" ) ]) @@ -604,7 +604,7 @@ async def create_buttons(rule): if UFB_ENABLED == 'true': buttons.append([ Button.inline( - f"☁️ UFB同步: {RULE_SETTINGS['is_ufb']['values'][rule.is_ufb]}", + f"☁️ UFB sync: {RULE_SETTINGS['is_ufb']['values'][rule.is_ufb]}", f"toggle_ufb:{rule.id}" ) ]) @@ -614,15 +614,15 @@ async def create_buttons(rule): buttons.append([ Button.inline( - "🤖 AI设置", + "🤖 AI settings", f"ai_settings:{rule.id}" ), Button.inline( - "🎬 媒体设置", + "🎬 Media settings", f"media_settings:{rule.id}" ), Button.inline( - "➕ 其他设置", + "➕ Other settings", f"other_settings:{rule.id}" ) ]) @@ -630,18 +630,18 @@ async def create_buttons(rule): buttons.append([ Button.inline( - "🔔 推送设置", + "🔔 Push settings", f"push_settings:{rule.id}" ) ]) buttons.append([ Button.inline( - "👈 返回", + "👈 Back", "settings" ), Button.inline( - "❌ 关闭", + "❌ Close", "close_settings" ) ]) diff --git a/handlers/command_handlers.py b/handlers/command_handlers.py index 0d68115..4b78c24 100644 --- a/handlers/command_handlers.py +++ b/handlers/command_handlers.py @@ -23,94 +23,94 @@ logger = logging.getLogger(__name__) async def handle_bind_command(event, client, parts): - """处理 bind 命令""" - # 使用shlex解析命令 + """Handle bind command""" + # Use shlex to parse command message_text = event.message.text try: - # 去掉命令前缀,获取原始参数字符串 + # Remove command prefix, get raw argument string if ' ' in message_text: command, args_str = message_text.split(' ', 1) args = shlex.split(args_str) if len(args) >= 1: source_target = args[0] - # 检查是否有第二个参数(目标聊天) + # Check if there is a second argument (target chat) target_chat_input = args[1] if len(args) >= 2 else None else: - raise ValueError("参数不足") + raise ValueError("Insufficient arguments") else: - raise ValueError("参数不足") + raise ValueError("Insufficient arguments") except ValueError: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'用法: /bind <源聊天链接或名称> [目标聊天链接或名称]\n例如:\n/bind https://t.me/channel_name\n/bind "频道 名称"\n/bind https://t.me/source_channel https://t.me/target_channel\n/bind "源频道名称" "目标频道名称"') + await reply_and_delete(event,'Usage: /bind [target chat link or name]\nExamples:\n/bind https://t.me/channel_name\n/bind "channel name"\n/bind https://t.me/source_channel https://t.me/target_channel\n/bind "source channel name" "target channel name"') return - # 检查是否是链接 + # Check if it is a link is_source_link = source_target.startswith(('https://', 't.me/')) - # 默认使用当前聊天作为目标聊天 + # Default to using current chat as target chat current_chat = await event.get_chat() try: - # 获取 main 模块中的用户客户端 + # Get user client from main module main = await get_main_module() user_client = main.user_client - # 使用用户客户端获取源聊天的实体信息 + # Use user client to get source chat entity info try: if is_source_link: - # 如果是链接,直接获取实体 + # If it is a link, get entity directly source_chat_entity = await user_client.get_entity(source_target) else: - # 如果是名称,获取对话列表并查找匹配的第一个 + # If it is a name, get dialog list and find the first match async for dialog in user_client.iter_dialogs(): if dialog.name and source_target.lower() in dialog.name.lower(): source_chat_entity = dialog.entity break else: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'未找到匹配的源群组/频道,请确保名称正确且账号已加入该群组/频道') + await reply_and_delete(event,'No matching source group/channel found, please ensure the name is correct and the account has joined the group/channel') return - # 获取目标聊天实体 + # Get target chat entity if target_chat_input: is_target_link = target_chat_input.startswith(('https://', 't.me/')) if is_target_link: - # 如果是链接,直接获取实体 + # If it is a link, get entity directly target_chat_entity = await user_client.get_entity(target_chat_input) else: - # 如果是名称,获取对话列表并查找匹配的第一个 + # If it is a name, get dialog list and find the first match async for dialog in user_client.iter_dialogs(): if dialog.name and target_chat_input.lower() in dialog.name.lower(): target_chat_entity = dialog.entity break else: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'未找到匹配的目标群组/频道,请确保名称正确且账号已加入该群组/频道') + await reply_and_delete(event,'No matching target group/channel found, please ensure the name is correct and the account has joined the group/channel') return else: - # 使用当前聊天作为目标 + # Use current chat as target target_chat_entity = current_chat - # # 检查是否在绑定自己 + # # Check if binding to self # if str(source_chat_entity.id) == str(target_chat_entity.id): # await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - # await reply_and_delete(event,'⚠️ 不能将频道/群组绑定到自己') + # await reply_and_delete(event,'⚠️ Cannot bind channel/group to itself') # return except ValueError: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'无法获取聊天信息,请确保链接/名称正确且账号已加入该群组/频道') + await reply_and_delete(event,'Unable to get chat info, please ensure the link/name is correct and the account has joined the group/channel') return except Exception as e: - logger.error(f'获取聊天信息时出错: {str(e)}') + logger.error(f'Error getting chat info: {str(e)}') await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'获取聊天信息时出错,请检查日志') + await reply_and_delete(event,'Error getting chat info, please check logs') return - # 保存到数据库 + # Save to database session = get_session() try: - # 保存源聊天 + # Save source chat source_chat_db = session.query(Chat).filter( Chat.telegram_chat_id == str(source_chat_entity.id) ).first() @@ -123,7 +123,7 @@ async def handle_bind_command(event, client, parts): session.add(source_chat_db) session.flush() - # 保存目标聊天 + # Save target chat target_chat_db = session.query(Chat).filter( Chat.telegram_chat_id == str(target_chat_entity.id) ).first() @@ -136,17 +136,17 @@ async def handle_bind_command(event, client, parts): session.add(target_chat_db) session.flush() - # 如果当前没有选中的源聊天,就设置为新绑定的聊天 + # If no source chat is currently selected, set it to the newly bound chat if not target_chat_db.current_add_id: target_chat_db.current_add_id = str(source_chat_entity.id) - # 创建转发规则 + # Create forwarding rule rule = ForwardRule( source_chat_id=source_chat_db.id, target_chat_id=target_chat_db.id ) - # 如果是绑定自己,则默认使用白名单模式 + # If binding to self, default to whitelist mode if str(source_chat_entity.id) == str(target_chat_entity.id): rule.forward_mode = ForwardMode.WHITELIST rule.add_mode = AddMode.WHITELIST @@ -156,75 +156,75 @@ async def handle_bind_command(event, client, parts): await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) await reply_and_delete(event, - f'已设置转发规则:\n' - f'源聊天: {source_chat_db.name} ({source_chat_db.telegram_chat_id})\n' - f'目标聊天: {target_chat_db.name} ({target_chat_db.telegram_chat_id})\n' - f'请使用 /add 或 /add_regex 添加关键字', - buttons=[Button.inline("⚙️ 打开设置", f"rule_settings:{rule.id}")] + f'Forwarding rule set:\n' + f'Source chat: {source_chat_db.name} ({source_chat_db.telegram_chat_id})\n' + f'Target chat: {target_chat_db.name} ({target_chat_db.telegram_chat_id})\n' + f'Please use /add or /add_regex to add keywords', + buttons=[Button.inline("⚙️ Open Settings", f"rule_settings:{rule.id}")] ) except IntegrityError: session.rollback() await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) await reply_and_delete(event, - f'已存在相同的转发规则:\n' - f'源聊天: {source_chat_db.name}\n' - f'目标聊天: {target_chat_db.name}\n' - f'如需修改请使用 /settings 命令' + f'Identical forwarding rule already exists:\n' + f'Source chat: {source_chat_db.name}\n' + f'Target chat: {target_chat_db.name}\n' + f'Use /settings command to modify' ) return finally: session.close() except Exception as e: - logger.error(f'设置转发规则时出错: {str(e)}\n{traceback.format_exc()}') + logger.error(f'Error setting forwarding rule: {str(e)}\n{traceback.format_exc()}') await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'设置转发规则时出错,请检查日志') + await reply_and_delete(event,'Error setting forwarding rule, please check logs') return async def handle_settings_command(event, command, parts): - """处理 settings 命令""" - # 添加日志 - logger.info(f'处理 settings 命令 - parts: {parts}') + """Handle settings command""" + # Add logs + logger.info(f'Processing settings command - parts: {parts}') - # 获取参数 + # Get arguments args = parts[1:] if len(parts) > 1 else [] - # 检查是否提供了规则ID + # Check if rule ID is provided if len(args) >= 1 and args[0].isdigit(): rule_id = int(args[0]) - # 直接打开指定规则的设置界面 + # Open the specified rule settings page directly session = get_session() try: rule = session.query(ForwardRule).get(rule_id) if not rule: - await reply_and_delete(event, f'找不到ID为 {rule_id} 的规则') + await reply_and_delete(event, f'Cannot find rule with ID {rule_id}') return - # 与callback_rule_settings函数相同的处理方式 + # Same processing as callback_rule_settings function settings_message = await event.respond( await create_settings_text(rule), buttons=await create_buttons(rule) ) except Exception as e: - logger.error(f'打开规则设置时出错: {str(e)}') - await reply_and_delete(event, '打开规则设置时出错,请检查日志') + logger.error(f'Error opening rule settings: {str(e)}') + await reply_and_delete(event, 'Error opening rule settings, please check logs') finally: session.close() return current_chat = await event.get_chat() current_chat_id = str(current_chat.id) - # 添加日志 - logger.info(f'正在查找聊天ID: {current_chat_id} 的转发规则') + # Add logs + logger.info(f'Looking for forwarding rules for chat ID: {current_chat_id} ') session = get_session() try: - # 添加日志,显示数据库中的所有聊天 + # Add logs, show all chats in database all_chats = session.query(Chat).all() - logger.info('数据库中的所有聊天:') + logger.info('All chats in database:') for chat in all_chats: logger.info(f'ID: {chat.id}, telegram_chat_id: {chat.telegram_chat_id}, name: {chat.name}') @@ -233,54 +233,54 @@ async def handle_settings_command(event, command, parts): ).first() if not current_chat_db: - logger.info(f'在数据库中找不到聊天ID: {current_chat_id}') + logger.info(f'Cannot find chat ID in database: {current_chat_id}') await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'当前聊天没有任何转发规则') + await reply_and_delete(event,'Current chat has no forwarding rules') return - # 添加日志 - logger.info(f'找到聊天: {current_chat_db.name} (ID: {current_chat_db.id})') + # Add logs + logger.info(f'Found chat: {current_chat_db.name} (ID: {current_chat_db.id})') - # 查找以当前聊天为目标的规则 + # Find rules with current chat as target rules = session.query(ForwardRule).filter( - ForwardRule.target_chat_id == current_chat_db.id # 改为 target_chat_id + ForwardRule.target_chat_id == current_chat_db.id # Changed to target_chat_id ).all() - # 添加日志 - logger.info(f'找到 {len(rules)} 条转发规则') + # Add logs + logger.info(f'Found {len(rules)} forwarding rules') for rule in rules: - logger.info(f'规则ID: {rule.id}, 源聊天: {rule.source_chat.name}, 目标聊天: {rule.target_chat.name}') + logger.info(f'Rule ID: {rule.id}, source chat: {rule.source_chat.name}, target chat: {rule.target_chat.name}') if not rules: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'当前聊天没有任何转发规则') + await reply_and_delete(event,'Current chat has no forwarding rules') return - # 创建规则选择按钮 + # Create rule selection buttons buttons = [] for rule in rules: - source_chat = rule.source_chat # 显示源聊天 + source_chat = rule.source_chat # Display source chat button_text = f'{source_chat.name}' callback_data = f"rule_settings:{rule.id}" buttons.append([Button.inline(button_text, callback_data)]) - # 删除用户消息 + # Delete user message client = await get_bot_client() await async_delete_user_message(client, event.message.chat_id, event.message.id, 0) await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'请选择要管理的转发规则:', buttons=buttons) + await reply_and_delete(event,'Please select a forwarding rule to manage:', buttons=buttons) except Exception as e: - logger.info(f'获取转发规则时出错: {str(e)}') + logger.info(f'Error getting forwarding rules: {str(e)}') await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'获取转发规则时出错,请检查日志') + await reply_and_delete(event,'Error getting forwarding rules, please check logs') finally: session.close() async def handle_switch_command(event): - """处理 switch 命令""" - # 显示可切换的规则列表 + """Handle switch command""" + # Show list of rules that can be switched current_chat = await event.get_chat() current_chat_id = str(current_chat.id) @@ -292,7 +292,7 @@ async def handle_switch_command(event): if not current_chat_db: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'当前聊天没有任何转发规则') + await reply_and_delete(event,'Current chat has no forwarding rules') return rules = session.query(ForwardRule).filter( @@ -301,59 +301,59 @@ async def handle_switch_command(event): if not rules: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'当前聊天没有任何转发规则') + await reply_and_delete(event,'Current chat has no forwarding rules') return - # 创建规则选择按钮 + # Create rule selection buttons buttons = [] for rule in rules: source_chat = rule.source_chat - # 标记当前选中的规则 + # Mark the currently selected rule current = current_chat_db.current_add_id == source_chat.telegram_chat_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)]) await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'请选择要管理的转发规则:', buttons=buttons) + await reply_and_delete(event,'Please select a forwarding rule to manage:', buttons=buttons) finally: session.close() async def handle_add_command(event, command, parts): - """处理 add 和 add_regex 命令""" + """Handle add and add_regex commands""" message_text = event.message.text - logger.info(f"收到原始消息: {message_text}") + logger.info(f"Received raw message: {message_text}") if len(message_text.split(None, 1)) < 2: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f'用法: /{command} <关键字1> [关键字2] ...\n例如:\n/{command} keyword1 "key word 2" \'key word 3\'') + await reply_and_delete(event,f'Usage: /{command} [keyword2] ...\nExamples:\n/{command} keyword1 "key word 2" \'key word 3\'') return - # 分离命令和参数部分 + # Separate command and argument parts _, args_text = message_text.split(None, 1) - logger.info(f"分离出的参数部分: {args_text}") + logger.info(f"Separated argument part: {args_text}") keywords = [] if command in ['add', 'a']: try: - # 使用 shlex 来正确处理带引号的参数 - logger.info("开始使用 shlex 解析参数") + # Use shlex to properly handle quoted arguments + logger.info("Starting to parse arguments with shlex") keywords = shlex.split(args_text) - logger.info(f"shlex 解析结果: {keywords}") + logger.info(f"shlex parse result: {keywords}") except ValueError as e: - logger.error(f"shlex 解析出错: {str(e)}") - # 处理未闭合的引号等错误 + logger.error(f"shlex parse error: {str(e)}") + # Handle errors like unclosed quotes await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'参数格式错误:请确保引号正确配对') + await reply_and_delete(event,'Argument format error: please ensure quotes are properly paired') return else: - # add_regex 命令保持原样 + # add_regex command keeps as is keywords = parts[1:] - logger.info(f"add_regex 命令,使用原始参数: {keywords}") + logger.info(f"add_regex command, using raw arguments: {keywords}") if not keywords: - logger.warning("没有提供任何关键字") + logger.warning("No keywords provided") await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'请提供至少一个关键字') + await reply_and_delete(event,'Please provide at least one keyword') return session = get_session() @@ -363,11 +363,11 @@ async def handle_add_command(event, command, parts): return rule, source_chat = rule_info - logger.info(f"当前规则ID: {rule.id}, 源聊天: {source_chat.name}") + logger.info(f"Current rule ID: {rule.id}, source chat: {source_chat.name}") - # 使用 db_operations 添加关键字 + # Use db_operations to add keywords db_ops = await get_db_ops() - logger.info(f"准备添加关键字: {keywords}, is_regex={command == 'add_regex'}, is_blacklist={rule.add_mode == AddMode.BLACKLIST}") + logger.info(f"Preparing to add keywords: {keywords}, is_regex={command == 'add_regex'}, is_blacklist={rule.add_mode == AddMode.BLACKLIST}") success_count, duplicate_count = await db_ops.add_keywords( session, rule.id, @@ -375,66 +375,66 @@ async def handle_add_command(event, command, parts): is_regex=(command == 'add_regex'), is_blacklist=(rule.add_mode == AddMode.BLACKLIST) ) - logger.info(f"添加结果: 成功={success_count}, 重复={duplicate_count}") + logger.info(f"Add result: success={success_count}, duplicates={duplicate_count}") session.commit() - # 构建回复消息 - keyword_type = "正则" if command == "add_regex" else "关键字" + # Build reply message + keyword_type = "regex" if command == "add_regex" else "keyword" keywords_text = '\n'.join(f'- {k}' for k in keywords) - result_text = f'已添加 {success_count} 个{keyword_type}' + result_text = f'Added {success_count} {keyword_type}(s)' if duplicate_count > 0: - result_text += f'\n跳过重复: {duplicate_count} 个' - result_text += f'\n关键字列表:\n{keywords_text}\n' - result_text += f'当前规则: 来自 {source_chat.name}\n' - mode_text = '白名单' if rule.add_mode == AddMode.WHITELIST else '黑名单' - result_text += f'当前关键字添加模式: {mode_text}' + result_text += f'\nSkipped duplicates: {duplicate_count}' + result_text += f'\nKeyword list:\n{keywords_text}\n' + result_text += f'Current rule: from {source_chat.name}\n' + mode_text = 'whitelist' if rule.add_mode == AddMode.WHITELIST else 'blacklist' + result_text += f'Current keyword add mode: {mode_text}' - logger.info(f"发送回复消息: {result_text}") + logger.info(f"Sending reply message: {result_text}") await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) await reply_and_delete(event,result_text) except Exception as e: session.rollback() - logger.error(f'添加关键字时出错: {str(e)}\n{traceback.format_exc()}') + logger.error(f'Error adding keywords: {str(e)}\n{traceback.format_exc()}') await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'添加关键字时出错,请检查日志') + await reply_and_delete(event,'Error adding keywords, please check logs') finally: session.close() async def handle_replace_command(event, parts): - """处理 replace 命令""" + """Handle replace command""" message_text = event.message.text if len(message_text.split(None, 1)) < 2: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'用法: /replace <匹配规则> [替换内容]\n例如:\n/replace 广告 # 删除匹配内容\n/replace 广告 [已替换]\n/replace "广告 文本" [已替换]\n/replace \'广告 文本\' [已替换]') + await reply_and_delete(event,'Usage: /replace [replacement content]\nExamples:\n/replace ad # Delete matched content\n/replace ad [replaced]\n/replace "ad text" [replaced]\n/replace \'ad text\' [replaced]') return - # 直接分割参数,保持正则表达式的原始形式 + # Split arguments directly, keeping regex in original form try: - # 去掉命令前缀,获取原始参数字符串 + # Remove command prefix, get raw argument string _, args_text = message_text.split(None, 1) - # 按第一个空格分割,保持后续内容不变 + # Split by first space, keeping remaining content unchanged parts = args_text.split(None, 1) if not parts: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'请提供有效的匹配规则') + await reply_and_delete(event,'Please provide a valid match pattern') return pattern = parts[0] content = parts[1] if len(parts) > 1 else '' - logger.info(f"解析替换命令参数: pattern='{pattern}', content='{content}'") + logger.info(f"Parsed replace command arguments: pattern='{pattern}', content='{content}'") except ValueError as e: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f'参数解析错误: {str(e)}\n请确保引号成对出现') + await reply_and_delete(event,f'Argument parsing error: {str(e)}\nPlease ensure quotes are properly paired') return if not pattern: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'请提供有效的匹配规则') + await reply_and_delete(event,'Please provide a valid match pattern') return session = get_session() @@ -445,49 +445,49 @@ async def handle_replace_command(event, parts): rule, source_chat = rule_info - # 使用 add_replace_rules 添加替换规则 + # Use add_replace_rules to add replace rule db_ops = await get_db_ops() - # 分别传递 patterns 和 contents 参数 + # Pass patterns and contents arguments separately success_count, duplicate_count = await db_ops.add_replace_rules( session, rule.id, - [pattern], # patterns 参数 - [content] # contents 参数 + [pattern], # patterns argument + [content] # contents argument ) - # 确保启用替换模式 + # Ensure replace mode is enabled if success_count > 0 and not rule.is_replace: rule.is_replace = True session.commit() - # 检查是否是全文替换 - rule_type = "全文替换" if pattern == ".*" else "正则替换" - action_type = "删除" if not content else "替换" + # Check if it is full text replacement + rule_type = "full text replacement" if pattern == ".*" else "regex replacement" + action_type = "delete" if not content else "replace" - # 构建回复消息 - result_text = f'已添加{rule_type}规则:\n' + # Build reply message + result_text = f'Added {rule_type} rule:\n' if success_count > 0: - result_text += f'匹配: {pattern}\n' - result_text += f'动作: {action_type}\n' - result_text += f'{"替换为: " + content if content else "删除匹配内容"}\n' + result_text += f'Match: {pattern}\n' + result_text += f'Action: {action_type}\n' + result_text += f'{"Replace with: " + content if content else "Delete matched content"}\n' if duplicate_count > 0: - result_text += f'跳过重复规则: {duplicate_count} 个\n' - result_text += f'当前规则: 来自 {source_chat.name}' + result_text += f'Skipped duplicate rules: {duplicate_count}\n' + result_text += f'Current rule: from {source_chat.name}' await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) await reply_and_delete(event,result_text) except Exception as e: session.rollback() - logger.error(f'添加替换规则时出错: {str(e)}') + logger.error(f'Error adding replace rule: {str(e)}') await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'添加替换规则时出错,请检查日志') + await reply_and_delete(event,'Error adding replace rule, please check logs') finally: session.close() async def handle_list_keyword_command(event): - """处理 list_keyword 命令""" + """Handle list_keyword command""" session = get_session() try: rule_info = await get_current_rule(session, event) @@ -496,7 +496,7 @@ async def handle_list_keyword_command(event): rule, source_chat = rule_info - # 使用 get_keywords 获取所有关键字 + # Use get_keywords to get all keywords db_ops = await get_db_ops() rule_mode = "blacklist" if rule.add_mode == AddMode.BLACKLIST else "whitelist" keywords = await db_ops.get_keywords(session, rule.id, rule_mode) @@ -505,15 +505,15 @@ async def handle_list_keyword_command(event): event, 'keyword', keywords, - lambda i, kw: f'{i}. {kw.keyword}{" (正则)" if kw.is_regex else ""}', - f'关键字列表\n当前模式: {"黑名单" if rule.add_mode == AddMode.BLACKLIST else "白名单"}\n规则: 来自 {source_chat.name}' + lambda i, kw: f'{i}. {kw.keyword}{" (regex)" if kw.is_regex else ""}', + f'Keyword List\nCurrent mode: {"blacklist" if rule.add_mode == AddMode.BLACKLIST else "whitelist"}\nRule: from {source_chat.name}' ) finally: session.close() async def handle_list_replace_command(event): - """处理 list_replace 命令""" + """Handle list_replace command""" session = get_session() try: rule_info = await get_current_rule(session, event) @@ -522,7 +522,7 @@ async def handle_list_replace_command(event): rule, source_chat = rule_info - # 使用 get_replace_rules 获取所有替换规则 + # Use get_replace_rules to get all replace rules db_ops = await get_db_ops() replace_rules = await db_ops.get_replace_rules(session, rule.id) @@ -530,72 +530,72 @@ async def handle_list_replace_command(event): 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}' ) finally: session.close() async def handle_remove_command(event, command, parts): - """处理 remove_keyword 和 remove_replace 命令""" + """Handle remove_keyword and remove_replace commands""" message_text = event.message.text - logger.info(f"收到原始消息: {message_text}") + logger.info(f"Received raw message: {message_text}") - # 如果是替换规则,保持原来的 ID 删除方式 + # If it is a replace rule, keep the original ID deletion method if command == 'remove_replace': if len(parts) < 2: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f'用法: /{command} [ID2] [ID3] ...\n例如: /{command} 1 2 3') + await reply_and_delete(event,f'Usage: /{command} [ID2] [ID3] ...\nExample: /{command} 1 2 3') return try: ids_to_remove = [int(x) for x in parts[1:]] except ValueError: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'ID必须是数字') + await reply_and_delete(event,'ID must be a number') return - elif command in ['remove_keyword_by_id', 'rkbi']: # 添加按ID删除关键字的处理 + elif command in ['remove_keyword_by_id', 'rkbi']: # Add handling for deleting keywords by ID if len(parts) < 2: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f'用法: /{command} [ID2] [ID3] ...\n例如: /{command} 1 2 3') + await reply_and_delete(event,f'Usage: /{command} [ID2] [ID3] ...\nExample: /{command} 1 2 3') return try: ids_to_remove = [int(x) for x in parts[1:]] - logger.info(f"准备按ID删除关键字: {ids_to_remove}") + logger.info(f"Preparing to delete keywords by ID: {ids_to_remove}") except ValueError: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'ID必须是数字') + await reply_and_delete(event,'ID must be a number') return else: # remove_keyword if len(message_text.split(None, 1)) < 2: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f'用法: /{command} <关键字1> [关键字2] ...\n例如:\n/{command} keyword1 "key word 2" \'key word 3\'') + await reply_and_delete(event,f'Usage: /{command} [keyword2] ...\nExamples:\n/{command} keyword1 "key word 2" \'key word 3\'') return - # 分离命令和参数部分 + # Separate command and argument parts _, args_text = message_text.split(None, 1) - logger.info(f"分离出的参数部分: {args_text}") + logger.info(f"Separated argument part: {args_text}") try: - # 使用 shlex 来正确处理带引号的参数 - logger.info("开始使用 shlex 解析参数") + # Use shlex to properly handle quoted arguments + logger.info("Starting to parse arguments with shlex") keywords_to_remove = shlex.split(args_text) - logger.info(f"shlex 解析结果: {keywords_to_remove}") + logger.info(f"shlex parse result: {keywords_to_remove}") except ValueError as e: - logger.error(f"shlex 解析出错: {str(e)}") + logger.error(f"shlex parse error: {str(e)}") await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'参数格式错误:请确保引号正确配对') + await reply_and_delete(event,'Argument format error: please ensure quotes are properly paired') return if not keywords_to_remove: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'请提供至少一个关键字') + await reply_and_delete(event,'Please provide at least one keyword') return - # 在 try 块外定义 item_type - item_type = '关键字' if command in ['remove_keyword', 'remove_keyword_by_id', 'rkbi'] else '替换规则' + # Define item_type outside try block + item_type = 'keyword' if command in ['remove_keyword', 'remove_keyword_by_id', 'rkbi'] else 'replace rule' session = get_session() try: @@ -605,66 +605,66 @@ async def handle_remove_command(event, command, parts): rule, source_chat = rule_info rule_mode = "blacklist" if rule.add_mode == AddMode.BLACKLIST else "whitelist" - mode_name = "黑名单" if rule.add_mode == AddMode.BLACKLIST else "白名单" + mode_name = "blacklist" if rule.add_mode == AddMode.BLACKLIST else "whitelist" db_ops = await get_db_ops() if command == 'remove_keyword': - # 获取当前模式下的关键字 + # Get keywords under current mode items = await db_ops.get_keywords(session, rule.id, rule_mode) if not items: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f'当前规则在{mode_name}模式下没有任何关键字') + await reply_and_delete(event,f'Current rule has no keywords in {mode_name} mode') return - # 修改:删除匹配的关键字 + # Modified: delete matching keywords removed_count = 0 - removed_indices = [] # 存储要删除的关键字索引 + removed_indices = [] # Store keyword indices to delete for keyword in keywords_to_remove: - logger.info(f"尝试删除关键字: {keyword}") + logger.info(f"Attempting to delete keyword: {keyword}") for i, item in enumerate(items): if item.keyword == keyword: - logger.info(f"找到匹配的关键字: {item.keyword}") - removed_indices.append(i + 1) # 转为1-based索引 + logger.info(f"Found matching keyword: {item.keyword}") + removed_indices.append(i + 1) # Convert to 1-based index removed_count += 1 break if removed_indices: - # 使用db_ops删除关键字(支持同步功能) + # Use db_ops to delete keywords (supports sync feature) await db_ops.delete_keywords(session, rule.id, removed_indices) session.commit() - logger.info(f"成功删除 {removed_count} 个关键字") + logger.info(f"Successfully deleted {removed_count} keywords") - # 重新获取更新后的列表 + # Re-fetch the updated list remaining_items = await db_ops.get_keywords(session, rule.id, rule_mode) - # 显示删除结果 + # Show deletion results if removed_count > 0: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f"已从{mode_name}中删除 {removed_count} 个关键字") + await reply_and_delete(event,f"Deleted {removed_count} keywords from {mode_name}") else: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f"在{mode_name}中未找到匹配的关键字") + await reply_and_delete(event,f"No matching keywords found in {mode_name}") elif command in ['remove_keyword_by_id', 'rkbi']: - # 获取当前模式下的关键字 + # Get keywords under current mode items = await db_ops.get_keywords(session, rule.id, rule_mode) if not items: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f'当前规则在{mode_name}模式下没有任何关键字') + await reply_and_delete(event,f'Current rule has no keywords in {mode_name} mode') return - # 检查ID是否有效 + # Check if IDs are valid max_id = len(items) invalid_ids = [id for id in ids_to_remove if id < 1 or id > max_id] if invalid_ids: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f'无效的ID: {", ".join(map(str, invalid_ids))}') + await reply_and_delete(event,f'Invalid ID(s): {", ".join(map(str, invalid_ids))}') return - # 修改:记录要删除的关键字 + # Modified: record keywords to delete removed_count = 0 removed_keywords = [] valid_ids = [id for id in ids_to_remove if 1 <= id <= max_id] @@ -672,36 +672,36 @@ async def handle_remove_command(event, command, parts): for id in valid_ids: removed_keywords.append(items[id - 1].keyword) - # 使用db_ops删除关键字(支持同步功能) + # Use db_ops to delete keywords (supports sync feature) removed_count, _ = await db_ops.delete_keywords(session, rule.id, valid_ids) session.commit() - logger.info(f"成功删除 {removed_count} 个关键字") + logger.info(f"Successfully deleted {removed_count} keywords") - # 构建回复消息 + # Build reply message if removed_count > 0: keywords_text = '\n'.join(f'- {k}' for k in removed_keywords) await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) await reply_and_delete(event, - f"已从{mode_name}中删除 {removed_count} 个关键字:\n" + f"Deleted {removed_count} keywords from {mode_name}:\n" f"{keywords_text}" ) else: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f"在{mode_name}中未找到匹配的关键字") + await reply_and_delete(event,f"No matching keywords found in {mode_name}") else: # remove_replace - # 处理替换规则的删除(保持原有逻辑) + # Handle replace rule deletion (keep original logic) items = await db_ops.get_replace_rules(session, rule.id) if not items: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f'当前规则没有任何{item_type}') + await reply_and_delete(event,f'Current rule has no {item_type}') return max_id = len(items) invalid_ids = [id for id in ids_to_remove if id < 1 or id > max_id] if invalid_ids: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f'无效的ID: {", ".join(map(str, invalid_ids))}') + await reply_and_delete(event,f'Invalid ID(s): {", ".join(map(str, invalid_ids))}') return await db_ops.delete_replace_rules(session, rule.id, ids_to_remove) @@ -709,133 +709,133 @@ async def handle_remove_command(event, command, parts): remaining_items = await db_ops.get_replace_rules(session, rule.id) await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f'已删除 {len(ids_to_remove)} 个替换规则') + await reply_and_delete(event,f'Deleted {len(ids_to_remove)} replace rules') except Exception as e: session.rollback() - logger.error(f'删除{item_type}时出错: {str(e)}\n{traceback.format_exc()}') + logger.error(f'Error deleting {item_type}: {str(e)}\n{traceback.format_exc()}') await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f'删除{item_type}时出错,请检查日志') + await reply_and_delete(event,f'Error deleting {item_type}, please check logs') finally: session.close() async def handle_clear_all_command(event): - """处理 clear_all 命令""" + """Handle clear_all command""" session = get_session() try: - # 删除所有替换规则 + # Delete all replace rules replace_count = session.query(ReplaceRule).delete(synchronize_session=False) - # 删除所有关键字 + # Delete all keywords keyword_count = session.query(Keyword).delete(synchronize_session=False) - # 删除所有转发规则 + # Delete all forwarding rules rule_count = session.query(ForwardRule).delete(synchronize_session=False) - # 删除所有聊天 + # Delete all chats chat_count = session.query(Chat).delete(synchronize_session=False) session.commit() await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) await reply_and_delete(event, - '已清空所有数据:\n' - f'- {chat_count} 个聊天\n' - f'- {rule_count} 条转发规则\n' - f'- {keyword_count} 个关键字\n' - f'- {replace_count} 条替换规则' + 'All data cleared:\n' + f'- {chat_count} chats\n' + f'- {rule_count} forwarding rules\n' + f'- {keyword_count} keywords\n' + f'- {replace_count} replace rules' ) except Exception as e: session.rollback() - logger.error(f'清空数据时出错: {str(e)}') + logger.error(f'Error clearing data: {str(e)}') await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'清空数据时出错,请检查日志') + await reply_and_delete(event,'Error clearing data, please check logs') finally: session.close() async def handle_changelog_command(event): - """处理 changelog 命令""" + """Handle changelog command""" await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) await reply_and_delete(event,UPDATE_INFO, parse_mode='html') async def handle_start_command(event): - """处理 start 命令""" + """Handle start command""" welcome_text = f""" - 👋 欢迎使用 Telegram 消息转发机器人! + 👋 Welcome to the Telegram Message Forwarding Bot! - 📱 当前版本:v{VERSION} + 📱 Current version: v{VERSION} - 📖 查看完整命令列表请使用 /help + 📖 Use /help to view the full command list """ await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) await reply_and_delete(event,welcome_text) async def handle_help_command(event, command): - """处理帮助命令""" + """Handle help command""" help_text = ( - f"🤖 **Telegram 消息转发机器人 v{VERSION}**\n\n" - - "**基础命令**\n" - "/start - 开始使用\n" - "/help(/h) - 显示此帮助信息\n\n" - - "**绑定和设置**\n" - "/bind(/b) <源聊天链接或名称> [目标聊天链接或名称] - 绑定源聊天\n" - "/settings(/s) [规则ID] - 管理转发规则\n" - "/changelog(/cl) - 查看更新日志\n\n" - - "**转发规则管理**\n" - "/copy_rule(/cr) <源规则ID> [目标规则ID] - 复制指定规则的所有设置到当前规则或目标规则ID\n" - "/list_rule(/lr) - 列出所有转发规则\n" - "/delete_rule(/dr) <规则ID> [规则ID] [规则ID] ... - 删除指定规则\n\n" - - "**关键字管理**\n" - "/add(/a) <关键字> [关键字] [\"关 键 字\"] [\'关 键 字\'] ... - 添加普通关键字\n" - "/add_regex(/ar) <正则表达式> [正则表达式] [正则表达式] ... - 添加正则表达式\n" - "/add_all(/aa) <关键字> [关键字] [关键字] ... - 添加普通关键字到当前频道绑定的所有规则\n" - "/add_regex_all(/ara) <正则表达式> [正则表达式] [正则表达式] ... - 添加正则表达式到所有规则\n" - "/list_keyword(/lk) - 列出所有关键字\n" - "/remove_keyword(/rk) <关键词1> [\"关 键 字\"] [\'关 键 字\'] ... - 删除关键字\n" - "/remove_keyword_by_id(/rkbi) [ID] [ID] ... - 按ID删除关键字\n" - "/remove_all_keyword(/rak) [关键字] [\"关 键 字\"] [\'关 键 字\'] ... - 删除当前频道绑定的所有规则的指定关键字\n" - "/clear_all_keywords(/cak) - 清除当前规则的所有关键字\n" - "/clear_all_keywords_regex(/cakr) - 清除当前规则的所有正则关键字\n" - "/copy_keywords(/ck) <规则ID> - 复制指定规则的关键字到当前规则\n" - "/copy_keywords_regex(/ckr) <规则ID> - 复制指定规则的正则关键字到当前规则\n\n" - - "**替换规则管理**\n" - "/replace(/r) <正则表达式> [替换内容] - 添加替换规则\n" - "/replace_all(/ra) <正则表达式> [替换内容] - 添加替换规则到所有规则\n" - "/list_replace(/lrp) - 列出所有替换规则\n" - "/remove_replace(/rr) <序号> - 删除替换规则\n" - "/clear_all_replace(/car) - 清除当前规则的所有替换规则\n" - "/copy_replace(/crp) <规则ID> - 复制指定规则的替换规则到当前规则\n\n" - - "**导入导出**\n" - "/export_keyword(/ek) - 导出当前规则的关键字\n" - "/export_replace(/er) - 导出当前规则的替换规则\n" - "/import_keyword(/ik) <同时发送文件> - 导入普通关键字\n" - "/import_regex_keyword(/irk) <同时发送文件> - 导入正则关键字\n" - "/import_replace(/ir) <同时发送文件> - 导入替换规则\n\n" - - "**RSS相关**\n" - "/delete_rss_user(/dru) [用户名] - 删除RSS用户\n" - - "**UFB相关**\n" - "/ufb_bind(/ub) <域名> - 绑定UFB域名\n" - "/ufb_unbind(/uu) - 解绑UFB域名\n" - "/ufb_item_change(/uic) - 切换UFB同步配置类型\n\n" - - "💡 **提示**\n" - "• 括号内为命令的简写形式\n" - "• 尖括号 <> 表示必填参数\n" - "• 方括号 [] 表示可选参数\n" - "• 导入命令需要同时发送文件" + f"🤖 **Telegram Message Forwarding Bot v{VERSION}**\n\n" + + "**Basic Commands**\n" + "/start - Get started\n" + "/help(/h) - Show this help message\n\n" + + "**Binding and Settings**\n" + "/bind(/b) [target chat link or name] - Bind source chat\n" + "/settings(/s) [rule ID] - Manage forwarding rules\n" + "/changelog(/cl) - View changelog\n\n" + + "**Forwarding Rule Management**\n" + "/copy_rule(/cr) [target rule ID] - Copy all settings from specified rule to current rule or target rule ID\n" + "/list_rule(/lr) - List all forwarding rules\n" + "/delete_rule(/dr) [rule ID] [rule ID] ... - Delete specified rules\n\n" + + "**Keyword Management**\n" + "/add(/a) [keyword] [\"key word\"] [\'key word\'] ... - Add plain keywords\n" + "/add_regex(/ar) [regex] [regex] ... - Add regular expressions\n" + "/add_all(/aa) [keyword] [keyword] ... - Add plain keywords to all rules bound to current channel\n" + "/add_regex_all(/ara) [regex] [regex] ... - Add regular expressions to all rules\n" + "/list_keyword(/lk) - List all keywords\n" + "/remove_keyword(/rk) [\"key word\"] [\'key word\'] ... - Remove keywords\n" + "/remove_keyword_by_id(/rkbi) [ID] [ID] ... - Remove keywords by ID\n" + "/remove_all_keyword(/rak) [keyword] [\"key word\"] [\'key word\'] ... - Remove specified keywords from all rules bound to current channel\n" + "/clear_all_keywords(/cak) - Clear all keywords of current rule\n" + "/clear_all_keywords_regex(/cakr) - Clear all regex keywords of current rule\n" + "/copy_keywords(/ck) - Copy keywords from specified rule to current rule\n" + "/copy_keywords_regex(/ckr) - Copy regex keywords from specified rule to current rule\n\n" + + "**Replace Rule Management**\n" + "/replace(/r) [replacement content] - Add replace rule\n" + "/replace_all(/ra) [replacement content] - Add replace rule to all rules\n" + "/list_replace(/lrp) - List all replace rules\n" + "/remove_replace(/rr) - Remove replace rule\n" + "/clear_all_replace(/car) - Clear all replace rules of current rule\n" + "/copy_replace(/crp) - Copy replace rules from specified rule to current rule\n\n" + + "**Import/Export**\n" + "/export_keyword(/ek) - Export keywords of current rule\n" + "/export_replace(/er) - Export replace rules of current rule\n" + "/import_keyword(/ik) - Import plain keywords\n" + "/import_regex_keyword(/irk) - Import regex keywords\n" + "/import_replace(/ir) - Import replace rules\n\n" + + "**RSS Related**\n" + "/delete_rss_user(/dru) [username] - Delete RSS user\n" + + "**UFB Related**\n" + "/ufb_bind(/ub) - Bind UFB domain\n" + "/ufb_unbind(/uu) - Unbind UFB domain\n" + "/ufb_item_change(/uic) - Switch UFB sync configuration type\n\n" + + "💡 **Tips**\n" + "• Content in parentheses is the shorthand form of the command\n" + "• Angle brackets <> indicate required parameters\n" + "• Square brackets [] indicate optional parameters\n" + "• Import commands require sending the file simultaneously" ) await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) @@ -844,7 +844,7 @@ async def handle_help_command(event, command): await reply_and_delete(event,help_text, parse_mode='markdown') async def handle_export_keyword_command(event, command): - """处理 export_keyword 命令""" + """Handle export_keyword command""" session = get_session() try: rule_info = await get_current_rule(session, event) @@ -853,37 +853,37 @@ async def handle_export_keyword_command(event, command): rule, source_chat = rule_info - # 获取所有关键字 + # Get all keywords normal_keywords = [] regex_keywords = [] - # 直接从规则对象获取关键字 + # Get keywords directly from rule object for keyword in rule.keywords: if keyword.is_regex: regex_keywords.append(f"{keyword.keyword} {1 if keyword.is_blacklist else 0}") else: normal_keywords.append(f"{keyword.keyword} {1 if keyword.is_blacklist else 0}") - # 创建临时文件 + # Create temporary files normal_file = os.path.join(TEMP_DIR, 'keywords.txt') regex_file = os.path.join(TEMP_DIR, 'regex_keywords.txt') - # 写入普通关键字,确保每行一个 + # Write plain keywords, one per line with open(normal_file, 'w', encoding='utf-8') as f: f.write('\n'.join(normal_keywords)) - # 写入正则关键字,确保每行一个 + # Write regex keywords, one per line with open(regex_file, 'w', encoding='utf-8') as f: f.write('\n'.join(regex_keywords)) - # 如果两个文件都是空的 + # If both files are empty if not normal_keywords and not regex_keywords: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event, "当前规则没有任何关键字") + await reply_and_delete(event, "Current rule has no keywords") return try: - # 先发送文件 + # Send files first files = [] if normal_keywords: files.append(normal_file) @@ -895,33 +895,33 @@ async def handle_export_keyword_command(event, command): files ) - # 然后单独发送说明文字 - await respond_and_delete(event,(f"规则: {source_chat.name}")) + # Then send description text separately + await respond_and_delete(event,(f"Rule: {source_chat.name}")) finally: - # 删除临时文件 + # Delete temporary files if os.path.exists(normal_file): os.remove(normal_file) if os.path.exists(regex_file): os.remove(regex_file) except Exception as e: - logger.error(f'导出关键字时出错: {str(e)}') + logger.error(f'Error exporting keywords: {str(e)}') await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'导出关键字时出错,请检查日志') + await reply_and_delete(event,'Error exporting keywords, please check logs') finally: session.close() async def handle_import_command(event, command): - """处理导入命令""" + """Handle import command""" try: - # 检查是否有附件 + # Check if there is an attachment if not event.message.file: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f'请将文件和 /{command} 命令一起发送') + await reply_and_delete(event,f'Please send the file together with the /{command} command') return - # 获取当前规则 + # Get current rule session = get_session() try: rule_info = await get_current_rule(session, event) @@ -930,28 +930,28 @@ async def handle_import_command(event, command): rule, source_chat = rule_info - # 下载文件 + # Download file file_path = await event.message.download_media(TEMP_DIR) try: - # 读取文件内容 + # Read file content with open(file_path, 'r', encoding='utf-8') as f: lines = [line.strip() for line in f if line.strip()] - # 根据命令类型处理 + # Process according to command type if command == 'import_replace': success_count = 0 - logger.info(f'开始导入替换规则,共 {len(lines)} 行') + logger.info(f'Starting to import replace rules, {len(lines)} lines total') for i, line in enumerate(lines, 1): try: - # 按第一个制表符分割 + # Split by first tab parts = line.split('\t', 1) pattern = parts[0].strip() content = parts[1].strip() if len(parts) > 1 else '' - logger.info(f'处理第 {i} 行: pattern="{pattern}", content="{content}"') + logger.info(f'Processing line {i}: pattern="{pattern}", content="{content}"') - # 创建替换规则 + # Create replace rule replace_rule = ReplaceRule( rule_id=rule.id, pattern=pattern, @@ -959,42 +959,42 @@ async def handle_import_command(event, command): ) session.add(replace_rule) success_count += 1 - logger.info(f'成功添加替换规则: pattern="{pattern}", content="{content}"') + logger.info(f'Successfully added replace rule: pattern="{pattern}", content="{content}"') - # 确保启用替换模式 + # Ensure replace mode is enabled if not rule.is_replace: rule.is_replace = True - logger.info('已启用替换模式') + logger.info('Replace mode enabled') except Exception as e: - logger.error(f'处理第 {i} 行替换规则时出错: {str(e)}\n{traceback.format_exc()}') + logger.error(f'Error processing replace rule on line {i}: {str(e)}\n{traceback.format_exc()}') continue session.commit() - logger.info(f'导入完成,成功导入 {success_count} 条替换规则') + logger.info(f'Import complete, successfully imported {success_count} replace rules') await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f'成功导入 {success_count} 条替换规则\n规则: 来自 {source_chat.name}') + await reply_and_delete(event,f'Successfully imported {success_count} replace rules\nRule: from {source_chat.name}') else: - # 处理关键字导入 + # Process keyword import success_count = 0 duplicate_count = 0 is_regex = (command == 'import_regex_keyword') for i, line in enumerate(lines, 1): try: - # 按空格分割,提取关键字和标志 + # Split by space, extract keyword and flag parts = line.split() if len(parts) < 2: - raise ValueError("行格式无效,至少需要关键字和标志") - flag_str = parts[-1] # 最后一个部分为标志 + raise ValueError("Invalid line format, at least keyword and flag required") + flag_str = parts[-1] # Last part is the flag if flag_str not in ('0', '1'): - raise ValueError("标志值必须为 0 或 1") - is_blacklist = (flag_str == '1') # 转换为布尔值 - keyword = ' '.join(parts[:-1]) # 前面的部分组合为关键字 + raise ValueError("Flag value must be 0 or 1") + is_blacklist = (flag_str == '1') # Convert to boolean + keyword = ' '.join(parts[:-1]) # Combine preceding parts as keyword if not keyword: - raise ValueError("关键字为空") - # 检查是否已存在相同的关键字 + raise ValueError("Keyword is empty") + # Check if the same keyword already exists existing = session.query(Keyword).filter_by( rule_id=rule.id, keyword=keyword, @@ -1005,7 +1005,7 @@ async def handle_import_command(event, command): duplicate_count += 1 continue - # 创建新的 Keyword 对象 + # Create new Keyword object new_keyword = Keyword( rule_id=rule.id, keyword=keyword, @@ -1016,19 +1016,19 @@ async def handle_import_command(event, command): success_count += 1 except Exception as e: - logger.error(f'处理第 {i} 行时出错: {line}\n{str(e)}') + logger.error(f'Error processing line {i}: {line}\n{str(e)}') continue session.commit() - keyword_type = "正则表达式" if is_regex else "关键字" - result_text = f'成功导入 {success_count} 个{keyword_type}' + keyword_type = "regex" if is_regex else "keyword" + result_text = f'Successfully imported {success_count} {keyword_type}(s)' if duplicate_count > 0: - result_text += f'\n跳过重复: {duplicate_count} 个' - result_text += f'\n规则: 来自 {source_chat.name}' + result_text += f'\nSkipped duplicates: {duplicate_count}' + result_text += f'\nRule: from {source_chat.name}' await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) await reply_and_delete(event,result_text) finally: - # 删除临时文件 + # Delete temporary file if os.path.exists(file_path): os.remove(file_path) @@ -1036,12 +1036,12 @@ async def handle_import_command(event, command): session.close() except Exception as e: - logger.error(f'导入过程出错: {str(e)}') + logger.error(f'Error during import: {str(e)}') await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'导入过程出错,请检查日志') + await reply_and_delete(event,'Error during import, please check logs') async def handle_ufb_item_change_command(event, command): - """处理 ufb_item_change 命令""" + """Handle ufb_item_change command""" session = get_session() try: @@ -1051,32 +1051,32 @@ async def handle_ufb_item_change_command(event, command): rule, source_chat = rule_info - # 创建4个按钮 + # Create 4 buttons buttons = [ [ - Button.inline("主页关键字", "ufb_item:main"), - Button.inline("内容页关键字", "ufb_item:content") + Button.inline("Homepage Keywords", "ufb_item:main"), + Button.inline("Content Page Keywords", "ufb_item:content") ], [ - Button.inline("主页用户名", "ufb_item:main_username"), - Button.inline("内容页用户名", "ufb_item:content_username") + Button.inline("Homepage Username", "ufb_item:main_username"), + Button.inline("Content Page Username", "ufb_item:content_username") ] ] - # 发送带按钮的消息 + # Send message with buttons await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event, "请选择要切换的UFB同步配置类型:", buttons=buttons) + await reply_and_delete(event, "Please select the UFB sync configuration type to switch:", buttons=buttons) except Exception as e: session.rollback() - logger.error(f'切换UFB配置类型时出错: {str(e)}') + logger.error(f'Error switching UFB configuration type: {str(e)}') await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'切换UFB配置类型时出错,请检查日志') + await reply_and_delete(event,'Error switching UFB configuration type, please check logs') finally: session.close() async def handle_ufb_bind_command(event, command): - """处理 ufb_bind 命令""" + """Handle ufb_bind command""" session = get_session() try: rule_info = await get_current_rule(session, event) @@ -1085,41 +1085,41 @@ async def handle_ufb_bind_command(event, command): rule, source_chat = rule_info - # 从消息中获取域名和类型 + # Get domain and type from message parts = event.message.text.split() if len(parts) < 2 or len(parts) > 3: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'用法: /ufb_bind <域名> [类型]\n类型可选: main, content, main_username, content_username\n例如: /ufb_bind example.com main') + await reply_and_delete(event,'Usage: /ufb_bind [type]\nType options: main, content, main_username, content_username\nExample: /ufb_bind example.com main') return domain = parts[1].strip().lower() - item = 'main' # 默认值 + item = 'main' # Default value if len(parts) == 3: item = parts[2].strip().lower() if item not in ['main', 'content', 'main_username', 'content_username']: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'类型必须是以下之一: main, content, main_username, content_username') + await reply_and_delete(event,'Type must be one of: main, content, main_username, content_username') return - # 更新规则的 ufb_domain 和 ufb_item + # Update rule ufb_domain and ufb_item rule.ufb_domain = domain rule.ufb_item = item session.commit() await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f'已绑定 UFB 域名: {domain}\n类型: {item}\n规则: 来自 {source_chat.name}') + await reply_and_delete(event,f'UFB domain bound: {domain}\nType: {item}\nRule: from {source_chat.name}') except Exception as e: session.rollback() - logger.error(f'绑定 UFB 域名时出错: {str(e)}') + logger.error(f'Error binding UFB domain: {str(e)}') await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'绑定 UFB 域名时出错,请检查日志') + await reply_and_delete(event,'Error binding UFB domain, please check logs') finally: session.close() async def handle_ufb_unbind_command(event, command): - """处理 ufb_unbind 命令""" + """Handle ufb_unbind command""" session = get_session() try: rule_info = await get_current_rule(session, event) @@ -1128,24 +1128,24 @@ async def handle_ufb_unbind_command(event, command): rule, source_chat = rule_info - # 清除规则的 ufb_domain + # Clear rule ufb_domain old_domain = rule.ufb_domain rule.ufb_domain = None session.commit() await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f'已解绑 UFB 域名: {old_domain or "无"}\n规则: 来自 {source_chat.name}') + await reply_and_delete(event,f'UFB domain unbound: {old_domain or "none"}\nRule: from {source_chat.name}') except Exception as e: session.rollback() - logger.error(f'解绑 UFB 域名时出错: {str(e)}') + logger.error(f'Error unbinding UFB domain: {str(e)}') await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'解绑 UFB 域名时出错,请检查日志') + await reply_and_delete(event,'Error unbinding UFB domain, please check logs') finally: session.close() async def handle_clear_all_keywords_command(event, command): - """处理清除所有关键字命令""" + """Handle clear all keywords command""" session = get_session() try: rule_info = await get_current_rule(session, event) @@ -1154,39 +1154,39 @@ async def handle_clear_all_keywords_command(event, command): rule, source_chat = rule_info - # 获取当前规则的关键字数量 + # Get keyword count for current rule keyword_count = len(rule.keywords) if keyword_count == 0: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event, "当前规则没有任何关键字") + await reply_and_delete(event, "Current rule has no keywords") return - # 删除所有关键字 + # Delete all keywords for keyword in rule.keywords: session.delete(keyword) session.commit() - # 发送成功消息 + # Send success message await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) await reply_and_delete(event, - f"✅ 已清除规则 `{rule.id}` 的所有关键字\n" - f"源聊天: {source_chat.name}\n" - f"共删除: {keyword_count} 个关键字", + f"✅ Cleared all keywords for rule `{rule.id}`\n" + f"Source chat: {source_chat.name}\n" + f"Total deleted: {keyword_count} keywords", parse_mode='markdown' ) except Exception as e: session.rollback() - logger.error(f'清除关键字时出错: {str(e)}') + logger.error(f'Error clearing keywords: {str(e)}') await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'清除关键字时出错,请检查日志') + await reply_and_delete(event,'Error clearing keywords, please check logs') finally: session.close() async def handle_clear_all_keywords_regex_command(event, command): - """处理清除所有正则关键字命令""" + """Handle clear all regex keywords command""" session = get_session() try: rule_info = await get_current_rule(session, event) @@ -1195,40 +1195,40 @@ async def handle_clear_all_keywords_regex_command(event, command): rule, source_chat = rule_info - # 获取当前规则的正则关键字数量 + # Get regex keyword count for current rule regex_keywords = [kw for kw in rule.keywords if kw.is_regex] keyword_count = len(regex_keywords) if keyword_count == 0: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event, "当前规则没有任何正则关键字") + await reply_and_delete(event, "Current rule has no regex keywords") return - # 删除所有正则关键字 + # Delete all regex keywords for keyword in regex_keywords: session.delete(keyword) session.commit() - # 发送成功消息 + # Send success message await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) await reply_and_delete(event, - f"✅ 已清除规则 `{rule.id}` 的所有正则关键字\n" - f"源聊天: {source_chat.name}\n" - f"共删除: {keyword_count} 个正则关键字", + f"✅ Cleared all regex keywords for rule `{rule.id}`\n" + f"Source chat: {source_chat.name}\n" + f"Total deleted: {keyword_count} regex keywords", parse_mode='markdown' ) except Exception as e: session.rollback() - logger.error(f'清除正则关键字时出错: {str(e)}') + logger.error(f'Error clearing regex keywords: {str(e)}') await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'清除正则关键字时出错,请检查日志') + await reply_and_delete(event,'Error clearing regex keywords, please check logs') finally: session.close() async def handle_clear_all_replace_command(event, command): - """处理清除所有替换规则命令""" + """Handle clear all replace rules command""" session = get_session() try: rule_info = await get_current_rule(session, event) @@ -1237,78 +1237,78 @@ async def handle_clear_all_replace_command(event, command): rule, source_chat = rule_info - # 获取当前规则的替换规则数量 + # Get replace rule count for current rule replace_count = len(rule.replace_rules) if replace_count == 0: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event, "当前规则没有任何替换规则") + await reply_and_delete(event, "Current rule has no replace rules") return - # 删除所有替换规则 + # Delete all replace rules for replace_rule in rule.replace_rules: session.delete(replace_rule) - # 如果没有替换规则了,关闭替换模式 + # If there are no more replace rules, disable replace mode rule.is_replace = False session.commit() - # 发送成功消息 + # Send success message await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) await reply_and_delete(event, - f"✅ 已清除规则 `{rule.id}` 的所有替换规则\n" - f"源聊天: {source_chat.name}\n" - f"共删除: {replace_count} 个替换规则\n" - "已自动关闭替换模式", + f"✅ Cleared all replace rules for rule `{rule.id}`\n" + f"Source chat: {source_chat.name}\n" + f"Total deleted: {replace_count} replace rules\n" + "Replace mode has been automatically disabled", parse_mode='markdown' ) except Exception as e: session.rollback() - logger.error(f'清除替换规则时出错: {str(e)}') + logger.error(f'Error clearing replace rules: {str(e)}') await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'清除替换规则时出错,请检查日志') + await reply_and_delete(event,'Error clearing replace rules, please check logs') finally: session.close() async def handle_copy_keywords_command(event, command): - """处理复制关键字命令""" + """Handle copy keywords command""" parts = event.message.text.split() if len(parts) != 2: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'用法: /copy_keywords <规则ID>') + await reply_and_delete(event,'Usage: /copy_keywords ') return try: source_rule_id = int(parts[1]) except ValueError: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'规则ID必须是数字') + await reply_and_delete(event,'Rule ID must be a number') return session = get_session() try: - # 获取当前规则 + # Get current rule rule_info = await get_current_rule(session, event) if not rule_info: return target_rule, source_chat = rule_info - # 获取源规则 + # Get source rule source_rule = session.query(ForwardRule).get(source_rule_id) if not source_rule: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f'找不到规则ID: {source_rule_id}') + await reply_and_delete(event,f'Cannot find rule ID: {source_rule_id}') return - # 复制关键字 + # Copy keywords success_count = 0 skip_count = 0 for keyword in source_rule.keywords: - if not keyword.is_regex: # 只复制普通关键字 - # 检查是否已存在 + if not keyword.is_regex: # Only copy plain keywords + # Check if already exists exists = any(k.keyword == keyword.keyword and not k.is_regex for k in target_rule.keywords) if not exists: @@ -1325,60 +1325,60 @@ async def handle_copy_keywords_command(event, command): session.commit() - # 发送结果消息 + # Send result message await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) await reply_and_delete(event, - f"✅ 已从规则 `{source_rule_id}` 复制关键字到规则 `{target_rule.id}`\n" - f"成功复制: {success_count} 个\n" - f"跳过重复: {skip_count} 个", + f"✅ Copied keywords from rule `{source_rule_id}` to rule `{target_rule.id}`\n" + f"Successfully copied: {success_count}\n" + f"Skipped duplicates: {skip_count}", parse_mode='markdown' ) except Exception as e: session.rollback() - logger.error(f'复制关键字时出错: {str(e)}') + logger.error(f'Error copying keywords: {str(e)}') await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'复制关键字时出错,请检查日志') + await reply_and_delete(event,'Error copying keywords, please check logs') finally: session.close() async def handle_copy_keywords_regex_command(event, command): - """处理复制正则关键字命令""" + """Handle copy regex keywords command""" parts = event.message.text.split() if len(parts) != 2: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'用法: /copy_keywords_regex <规则ID>') + await reply_and_delete(event,'Usage: /copy_keywords_regex ') return try: source_rule_id = int(parts[1]) except ValueError: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'规则ID必须是数字') + await reply_and_delete(event,'Rule ID must be a number') return session = get_session() try: - # 获取当前规则 + # Get current rule rule_info = await get_current_rule(session, event) if not rule_info: return target_rule, source_chat = rule_info - # 获取源规则 + # Get source rule source_rule = session.query(ForwardRule).get(source_rule_id) if not source_rule: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f'找不到规则ID: {source_rule_id}') + await reply_and_delete(event,f'Cannot find rule ID: {source_rule_id}') return - # 复制正则关键字 + # Copy regex keywords success_count = 0 skip_count = 0 for keyword in source_rule.keywords: - if keyword.is_regex: # 只复制正则关键字 - # 检查是否已存在 + if keyword.is_regex: # Only copy regex keywords + # Check if already exists exists = any(k.keyword == keyword.keyword and k.is_regex for k in target_rule.keywords) if not exists: @@ -1395,59 +1395,59 @@ async def handle_copy_keywords_regex_command(event, command): session.commit() - # 发送结果消息 + # Send result message await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) await reply_and_delete(event, - f"✅ 已从规则 `{source_rule_id}` 复制正则关键字到规则 `{target_rule.id}`\n" - f"成功复制: {success_count} 个\n" - f"跳过重复: {skip_count} 个", + f"✅ Copied regex keywords from rule `{source_rule_id}` to rule `{target_rule.id}`\n" + f"Successfully copied: {success_count}\n" + f"Skipped duplicates: {skip_count}", parse_mode='markdown' ) except Exception as e: session.rollback() - logger.error(f'复制正则关键字时出错: {str(e)}') + logger.error(f'Error copying regex keywords: {str(e)}') await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'复制正则关键字时出错,请检查日志') + await reply_and_delete(event,'Error copying regex keywords, please check logs') finally: session.close() async def handle_copy_replace_command(event, command): - """处理复制替换规则命令""" + """Handle copy replace rules command""" parts = event.message.text.split() if len(parts) != 2: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'用法: /copy_replace <规则ID>') + await reply_and_delete(event,'Usage: /copy_replace ') return try: source_rule_id = int(parts[1]) except ValueError: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'规则ID必须是数字') + await reply_and_delete(event,'Rule ID must be a number') return session = get_session() try: - # 获取当前规则 + # Get current rule rule_info = await get_current_rule(session, event) if not rule_info: return target_rule, source_chat = rule_info - # 获取源规则 + # Get source rule source_rule = session.query(ForwardRule).get(source_rule_id) if not source_rule: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f'找不到规则ID: {source_rule_id}') + await reply_and_delete(event,f'Cannot find rule ID: {source_rule_id}') return - # 复制替换规则 + # Copy replace rules success_count = 0 skip_count = 0 for replace_rule in source_rule.replace_rules: - # 检查是否已存在 + # Check if already exists exists = any(r.pattern == replace_rule.pattern for r in target_rule.replace_rules) if not exists: @@ -1463,80 +1463,80 @@ async def handle_copy_replace_command(event, command): session.commit() - # 发送结果消息 + # Send result message await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) await reply_and_delete(event, - f"✅ 已从规则 `{source_rule_id}` 复制替换规则到规则 `{target_rule.id}`\n" - f"成功复制: {success_count} 个\n" - f"跳过重复: {skip_count} 个\n", + f"✅ Copied replace rules from rule `{source_rule_id}` to rule `{target_rule.id}`\n" + f"Successfully copied: {success_count}\n" + f"Skipped duplicates: {skip_count}\n", parse_mode='markdown' ) except Exception as e: session.rollback() - logger.error(f'复制替换规则时出错: {str(e)}') + logger.error(f'Error copying replace rules: {str(e)}') await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'复制替换规则时出错,请检查日志') + await reply_and_delete(event,'Error copying replace rules, please check logs') finally: session.close() async def handle_copy_rule_command(event, command): - """处理复制规则命令 - 复制一个规则的所有设置到当前规则或指定规则""" + """Handle copy rule command - Copy all settings from one rule to current rule or specified rule""" parts = event.message.text.split() - # 检查参数数量 + # Check argument count if len(parts) not in [2, 3]: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'用法: /copy_rule <源规则ID> [目标规则ID]') + await reply_and_delete(event,'Usage: /copy_rule [target rule ID]') return try: source_rule_id = int(parts[1]) - # 确定目标规则ID + # Determine target rule ID if len(parts) == 3: - # 如果提供了两个参数,使用第二个参数作为目标规则ID + # If two arguments provided, use the second as target rule ID target_rule_id = int(parts[2]) use_current_rule = False else: - # 如果只提供了一个参数,使用当前规则作为目标 + # If only one argument provided, use current rule as target target_rule_id = None use_current_rule = True except ValueError: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'规则ID必须是数字') + await reply_and_delete(event,'Rule ID must be a number') return session = get_session() try: - # 获取源规则 + # Get source rule source_rule = session.query(ForwardRule).get(source_rule_id) if not source_rule: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f'找不到源规则ID: {source_rule_id}') + await reply_and_delete(event,f'Cannot find source rule ID: {source_rule_id}') return - # 获取目标规则 + # Get target rule if use_current_rule: - # 获取当前规则 + # Get current rule rule_info = await get_current_rule(session, event) if not rule_info: return target_rule, source_chat = rule_info else: - # 使用指定的目标规则ID + # Use specified target rule ID target_rule = session.query(ForwardRule).get(target_rule_id) if not target_rule: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f'找不到目标规则ID: {target_rule_id}') + await reply_and_delete(event,f'Cannot find target rule ID: {target_rule_id}') return if source_rule.id == target_rule.id: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'不能复制规则到自身') + await reply_and_delete(event,'Cannot copy rule to itself') return - # 记录复制的各个部分成功数量 + # Record success count for each copied part keywords_normal_success = 0 keywords_normal_skip = 0 keywords_regex_success = 0 @@ -1547,10 +1547,10 @@ async def handle_copy_rule_command(event, command): media_extensions_skip = 0 - # 复制普通关键字 + # Copy plain keywords for keyword in source_rule.keywords: if not keyword.is_regex: - # 检查是否已存在 + # Check if already exists exists = any(k.keyword == keyword.keyword and not k.is_regex and k.is_blacklist == keyword.is_blacklist for k in target_rule.keywords) if not exists: @@ -1565,10 +1565,10 @@ async def handle_copy_rule_command(event, command): else: keywords_normal_skip += 1 - # 复制正则关键字 + # Copy regex keywords for keyword in source_rule.keywords: if keyword.is_regex: - # 检查是否已存在 + # Check if already exists exists = any(k.keyword == keyword.keyword and k.is_regex and k.is_blacklist == keyword.is_blacklist for k in target_rule.keywords) if not exists: @@ -1583,9 +1583,9 @@ async def handle_copy_rule_command(event, command): else: keywords_regex_skip += 1 - # 复制替换规则 + # Copy replace rules for replace_rule in source_rule.replace_rules: - # 检查是否已存在 + # Check if already exists exists = any(r.pattern == replace_rule.pattern and r.content == replace_rule.content for r in target_rule.replace_rules) if not exists: @@ -1599,10 +1599,10 @@ async def handle_copy_rule_command(event, command): else: replace_rules_skip += 1 - # 复制媒体扩展名设置 + # Copy media extension settings if hasattr(source_rule, 'media_extensions') and source_rule.media_extensions: for extension in source_rule.media_extensions: - # 检查是否已存在 + # Check if already exists exists = any(e.extension == extension.extension for e in target_rule.media_extensions) if not exists: new_extension = MediaExtensions( @@ -1614,15 +1614,15 @@ async def handle_copy_rule_command(event, command): else: media_extensions_skip += 1 - # 复制媒体类型设置 + # Copy media type settings if hasattr(source_rule, 'media_types') and source_rule.media_types: target_media_types = session.query(MediaTypes).filter_by(rule_id=target_rule.id).first() if not target_media_types: - # 如果目标规则没有媒体类型设置,创建新的 + # If target rule has no media type settings, create new one target_media_types = MediaTypes(rule_id=target_rule.id) - # 使用inspect自动复制所有字段(除了id和rule_id) + # Use inspect to automatically copy all fields (except id and rule_id) media_inspector = inspect(MediaTypes) for column in media_inspector.columns: column_name = column.key @@ -1631,25 +1631,25 @@ async def handle_copy_rule_command(event, command): session.add(target_media_types) else: - # 如果已有设置,更新现有设置 - # 使用inspect自动复制所有字段(除了id和rule_id) + # If settings already exist, update existing settings + # Use inspect to automatically copy all fields (except id and rule_id) media_inspector = inspect(MediaTypes) for column in media_inspector.columns: column_name = column.key if column_name not in ['id', 'rule_id']: setattr(target_media_types, column_name, getattr(source_rule.media_types, column_name)) - # 复制规则同步表数据 + # Copy rule sync table data rule_syncs_success = 0 rule_syncs_skip = 0 - # 检查源规则是否有同步关系 + # Check if source rule has sync relationships if hasattr(source_rule, 'rule_syncs') and source_rule.rule_syncs: for sync in source_rule.rule_syncs: - # 检查是否已存在 + # Check if already exists exists = any(s.sync_rule_id == sync.sync_rule_id for s in target_rule.rule_syncs) if not exists: - # 确保不会创建自引用的同步关系 + # Ensure self-referencing sync relationships are not created if sync.sync_rule_id != target_rule.id: new_sync = RuleSync( rule_id=target_rule.id, @@ -1658,49 +1658,49 @@ async def handle_copy_rule_command(event, command): session.add(new_sync) rule_syncs_success += 1 - # 启用目标规则的同步功能 + # Enable sync feature for target rule if rule_syncs_success > 0: target_rule.enable_sync = True else: rule_syncs_skip += 1 - # 复制规则设置 - # 获取ForwardRule模型的所有字段 + # Copy rule settings + # Get all fields of ForwardRule model inspector = inspect(ForwardRule) for column in inspector.columns: column_name = column.key if column_name not in ['id', 'source_chat_id', 'target_chat_id', 'source_chat', 'target_chat', 'keywords', 'replace_rules', 'media_types']: - # 获取源规则的值并设置到目标规则 + # Get value from source rule and set to target rule value = getattr(source_rule, column_name) setattr(target_rule, column_name, value) session.commit() - # 发送结果消息 + # Send result message await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) await reply_and_delete(event, - f"✅ 已从规则 `{source_rule_id}` 复制到规则 `{target_rule.id}`\n\n" - f"普通关键字: 成功复制 {keywords_normal_success} 个, 跳过重复 {keywords_normal_skip} 个\n" - f"正则关键字: 成功复制 {keywords_regex_success} 个, 跳过重复 {keywords_regex_skip} 个\n" - f"替换规则: 成功复制 {replace_rules_success} 个, 跳过重复 {replace_rules_skip} 个\n" - f"媒体扩展名: 成功复制 {media_extensions_success} 个, 跳过重复 {media_extensions_skip} 个\n" - f"同步规则: 成功复制 {rule_syncs_success} 个, 跳过重复 {rule_syncs_skip} 个\n" - f"媒体类型设置和其他规则设置已复制\n", + f"✅ Copied from rule `{source_rule_id}` to rule `{target_rule.id}`\n\n" + f"Plain keywords: successfully copied {keywords_normal_success}, skipped duplicates {keywords_normal_skip}\n" + f"Regex keywords: successfully copied {keywords_regex_success}, skipped duplicates {keywords_regex_skip}\n" + f"Replace rules: successfully copied {replace_rules_success}, skipped duplicates {replace_rules_skip}\n" + f"Media extensions: successfully copied {media_extensions_success}, skipped duplicates {media_extensions_skip}\n" + f"Sync rules: successfully copied {rule_syncs_success}, skipped duplicates {rule_syncs_skip}\n" + f"Media type settings and other rule settings have been copied\n", parse_mode='markdown' ) except Exception as e: session.rollback() - logger.error(f'复制规则时出错: {str(e)}') + logger.error(f'Error copying rule: {str(e)}') await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'复制规则时出错,请检查日志') + await reply_and_delete(event,'Error copying rule, please check logs') finally: session.close() async def handle_export_replace_command(event, client): - """处理 export_replace 命令""" + """Handle export_replace command""" session = get_session() try: rule_info = await get_current_rule(session, event) @@ -1709,91 +1709,91 @@ async def handle_export_replace_command(event, client): rule, source_chat = rule_info - # 获取所有替换规则 + # Get all replace rules replace_rules = [] for rule in rule.replace_rules: replace_rules.append((rule.pattern, rule.content)) - # 如果没有替换规则 + # If there are no replace rules if not replace_rules: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event, "当前规则没有任何替换规则") + await reply_and_delete(event, "Current rule has no replace rules") return - # 创建并写入文件 + # Create and write to file replace_file = os.path.join(TEMP_DIR, 'replace_rules.txt') - # 写入替换规则,每行一个规则,用制表符分隔 + # Write replace rules, one rule per line, tab separated with open(replace_file, 'w', encoding='utf-8') as f: for pattern, content in replace_rules: line = f"{pattern}\t{content if content else ''}" f.write(line + '\n') try: - # 先发送文件 + # Send file first await event.client.send_file( event.chat_id, replace_file ) - # 然后单独发送说明文字 - await respond_and_delete(event,(f"规则: {source_chat.name}")) + # Then send description text separately + await respond_and_delete(event,(f"Rule: {source_chat.name}")) finally: - # 删除临时文件 + # Delete temporary file if os.path.exists(replace_file): os.remove(replace_file) except Exception as e: - logger.error(f'导出替换规则时出错: {str(e)}') + logger.error(f'Error exporting replace rules: {str(e)}') await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'导出替换规则时出错,请检查日志') + await reply_and_delete(event,'Error exporting replace rules, please check logs') finally: session.close() async def handle_remove_all_keyword_command(event, command, parts): - """处理 remove_all_keyword 命令""" + """Handle remove_all_keyword command""" message_text = event.message.text - logger.info(f"收到原始消息: {message_text}") + logger.info(f"Received raw message: {message_text}") if len(message_text.split(None, 1)) < 2: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f'用法: /{command} <关键字1> [关键字2] ...\n例如:\n/{command} keyword1 "key word 2" \'key word 3\'') + await reply_and_delete(event,f'Usage: /{command} [keyword2] ...\nExamples:\n/{command} keyword1 "key word 2" \'key word 3\'') return - # 分离命令和参数部分 + # Separate command and argument parts _, args_text = message_text.split(None, 1) - logger.info(f"分离出的参数部分: {args_text}") + logger.info(f"Separated argument part: {args_text}") try: - # 使用 shlex 来正确处理带引号的参数 - logger.info("开始使用 shlex 解析参数") + # Use shlex to properly handle quoted arguments + logger.info("Starting to parse arguments with shlex") keywords_to_remove = shlex.split(args_text) - logger.info(f"shlex 解析结果: {keywords_to_remove}") + logger.info(f"shlex parse result: {keywords_to_remove}") except ValueError as e: - logger.error(f"shlex 解析出错: {str(e)}") - # 处理未闭合的引号等错误 + logger.error(f"shlex parse error: {str(e)}") + # Handle errors like unclosed quotes await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'参数格式错误:请确保引号正确配对') + await reply_and_delete(event,'Argument format error: please ensure quotes are properly paired') return if not keywords_to_remove: - logger.warning("没有提供任何关键字") + logger.warning("No keywords provided") await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'请提供至少一个关键字') + await reply_and_delete(event,'Please provide at least one keyword') return session = get_session() try: - # 获取当前规则以确定黑白名单模式 + # Get current rule to determine blacklist/whitelist mode rule_info = await get_current_rule(session, event) if not rule_info: return current_rule, source_chat = rule_info - mode_name = "黑名单" if current_rule.add_mode == AddMode.BLACKLIST else "白名单" + mode_name = "blacklist" if current_rule.add_mode == AddMode.BLACKLIST else "whitelist" - # 获取所有相关规则 + # Get all related rules rules = await get_all_rules(session, event) if not rules: return @@ -1801,11 +1801,11 @@ async def handle_remove_all_keyword_command(event, command, parts): db_ops = await get_db_ops() total_removed = 0 total_not_found = 0 - removed_details = {} # 用于记录每个规则删除的关键字 + removed_details = {} # Used to record deleted keywords for each rule - # 从每个规则中删除关键字 + # Delete keywords from each rule for rule in rules: - # 获取当前规则的关键字 + # Get keywords for current rule rule_mode = "blacklist" if rule.add_mode == AddMode.BLACKLIST else "whitelist" keywords = await db_ops.get_keywords(session, rule.id, rule_mode) @@ -1815,10 +1815,10 @@ async def handle_remove_all_keyword_command(event, command, parts): rule_removed = 0 rule_removed_keywords = [] - # 删除匹配的关键字 + # Delete matching keywords for keyword in keywords: if keyword.keyword in keywords_to_remove: - logger.info(f"在规则 {rule.id} 中删除关键字: {keyword.keyword}") + logger.info(f"Deleting keyword in rule {rule.id}: {keyword.keyword}") session.delete(keyword) rule_removed += 1 rule_removed_keywords.append(keyword.keyword) @@ -1831,73 +1831,73 @@ async def handle_remove_all_keyword_command(event, command, parts): session.commit() - # 构建回复消息 + # Build reply message if total_removed > 0: - result_text = f"已从{mode_name}中删除关键字:\n\n" + result_text = f"Deleted keywords from {mode_name}:\n\n" for rule_id, keywords in removed_details.items(): rule = next((r for r in rules if r.id == rule_id), None) if rule: - result_text += f"规则 {rule_id} (来自: {rule.source_chat.name}):\n" + result_text += f"Rule {rule_id} (from: {rule.source_chat.name}):\n" result_text += "\n".join(f"- {k}" for k in keywords) result_text += "\n\n" - result_text += f"总计删除: {total_removed} 个关键字" + result_text += f"Total deleted: {total_removed} keywords" - logger.info(f"发送回复消息: {result_text}") + logger.info(f"Sending reply message: {result_text}") await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) await reply_and_delete(event,result_text) else: - msg = f"在{mode_name}中未找到匹配的关键字" + msg = f"No matching keywords found in {mode_name}" logger.info(msg) await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) await reply_and_delete(event,msg) except Exception as e: session.rollback() - logger.error(f'批量删除关键字时出错: {str(e)}\n{traceback.format_exc()}') + logger.error(f'Error batch deleting keywords: {str(e)}\n{traceback.format_exc()}') await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'删除关键字时出错,请检查日志') + await reply_and_delete(event,'Error deleting keywords, please check logs') finally: session.close() async def handle_add_all_command(event, command, parts): - """处理 add_all 和 add_regex_all 命令""" + """Handle add_all and add_regex_all commands""" message_text = event.message.text - logger.info(f"收到原始消息: {message_text}") + logger.info(f"Received raw message: {message_text}") if len(message_text.split(None, 1)) < 2: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f'用法: /{command} <关键字1> [关键字2] ...\n例如:\n/{command} keyword1 "key word 2" \'key word 3\'') + await reply_and_delete(event,f'Usage: /{command} [keyword2] ...\nExamples:\n/{command} keyword1 "key word 2" \'key word 3\'') return - # 分离命令和参数部分 + # Separate command and argument parts _, args_text = message_text.split(None, 1) - logger.info(f"分离出的参数部分: {args_text}") + logger.info(f"Separated argument part: {args_text}") keywords = [] if command == 'add_all': try: - # 使用 shlex 来正确处理带引号的参数 - logger.info("开始使用 shlex 解析参数") + # Use shlex to properly handle quoted arguments + logger.info("Starting to parse arguments with shlex") keywords = shlex.split(args_text) - logger.info(f"shlex 解析结果: {keywords}") + logger.info(f"shlex parse result: {keywords}") except ValueError as e: - logger.error(f"shlex 解析出错: {str(e)}") - # 处理未闭合的引号等错误 + logger.error(f"shlex parse error: {str(e)}") + # Handle errors like unclosed quotes await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'参数格式错误:请确保引号正确配对') + await reply_and_delete(event,'Argument format error: please ensure quotes are properly paired') return else: - # add_regex_all 命令使用简单分割,保持正则表达式的原始形式 + # add_regex_all command uses simple split, keeping regex in original form if len(args_text.split()) > 0: keywords = args_text.split() else: keywords = [args_text] - logger.info(f"add_regex_all 命令,使用原始参数: {keywords}") + logger.info(f"add_regex_all command, using raw arguments: {keywords}") if not keywords: - logger.warning("没有提供任何关键字") + logger.warning("No keywords provided") await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'请提供至少一个关键字') + await reply_and_delete(event,'Please provide at least one keyword') return session = get_session() @@ -1913,11 +1913,11 @@ async def handle_add_all_command(event, command, parts): current_rule, source_chat = rule_info db_ops = await get_db_ops() - # 为每个规则添加关键字 + # Add keywords for each rule success_count = 0 duplicate_count = 0 for rule in rules: - # 使用 add_keywords 添加关键字 + # Use add_keywords to add keywords s_count, d_count = await db_ops.add_keywords( session, rule.id, @@ -1930,44 +1930,44 @@ async def handle_add_all_command(event, command, parts): session.commit() - # 构建回复消息 - keyword_type = "正则表达式" if command == "add_regex_all" else "关键字" + # Build reply message + keyword_type = "regex" if command == "add_regex_all" else "keyword" keywords_text = '\n'.join(f'- {k}' for k in keywords) - result_text = f'已添加 {success_count} 个{keyword_type}\n' + result_text = f'Added {success_count} {keyword_type}(s)\n' if duplicate_count > 0: - result_text += f'跳过重复: {duplicate_count} 个\n' - result_text += f'关键字列表:\n{keywords_text}' + result_text += f'Skipped duplicates: {duplicate_count}\n' + result_text += f'Keyword list:\n{keywords_text}' - logger.info(f"发送回复消息: {result_text}") + logger.info(f"Sending reply message: {result_text}") await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) await reply_and_delete(event,result_text) except Exception as e: session.rollback() - logger.error(f'批量添加关键字时出错: {str(e)}\n{traceback.format_exc()}') + logger.error(f'Error batch adding keywords: {str(e)}\n{traceback.format_exc()}') await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'添加关键字时出错,请检查日志') + await reply_and_delete(event,'Error adding keywords, please check logs') finally: session.close() async def handle_replace_all_command(event, parts): - """处理 replace_all 命令""" + """Handle replace_all command""" message_text = event.message.text if len(message_text.split(None, 1)) < 2: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'用法: /replace_all <匹配规则> [替换内容]\n例如:\n/replace_all 广告 # 删除匹配内容\n/replace_all 广告 [已替换]') + await reply_and_delete(event,'Usage: /replace_all [replacement content]\nExamples:\n/replace_all ad # Delete matched content\n/replace_all ad [replaced]') return - # 直接分割参数,保持正则表达式的原始形式 + # Split arguments directly, keeping regex in original form _, args_text = message_text.split(None, 1) - # 按第一个空格分割,保持后续内容不变 + # Split by first space, keeping remaining content unchanged parts = args_text.split(None, 1) pattern = parts[0] content = parts[1] if len(parts) > 1 else '' - logger.info(f"解析替换命令参数: pattern='{pattern}', content='{content}'") + logger.info(f"Parsed replace command arguments: pattern='{pattern}', content='{content}'") session = get_session() try: @@ -1976,150 +1976,150 @@ async def handle_replace_all_command(event, parts): return db_ops = await get_db_ops() - # 为每个规则添加替换规则 + # Add replace rule for each rule total_success = 0 total_duplicate = 0 for rule in rules: - # 使用 add_replace_rules 添加替换规则 + # Use add_replace_rules to add replace rule success_count, duplicate_count = await db_ops.add_replace_rules( session, rule.id, - [(pattern, content)] # 传入一个元组列表,每个元组包含 pattern 和 content + [(pattern, content)] # Pass a list of tuples, each containing pattern and content ) - # 累计成功和重复的数量 + # Accumulate success and duplicate counts total_success += success_count total_duplicate += duplicate_count - # 确保启用替换模式 + # Ensure replace mode is enabled if success_count > 0 and not rule.is_replace: rule.is_replace = True session.commit() - # 构建回复消息 - action_type = "删除" if not content else "替换" - result_text = f'已为 {len(rules)} 个规则添加替换规则:\n' + # Build reply message + action_type = "delete" if not content else "replace" + result_text = f'Added replace rules for {len(rules)} rules:\n' if total_success > 0: - result_text += f'成功添加: {total_success} 个\n' - result_text += f'匹配模式: {pattern}\n' - result_text += f'动作: {action_type}\n' + result_text += f'Successfully added: {total_success}\n' + result_text += f'Match pattern: {pattern}\n' + result_text += f'Action: {action_type}\n' if content: - result_text += f'替换为: {content}\n' + result_text += f'Replace with: {content}\n' if total_duplicate > 0: - result_text += f'跳过重复规则: {total_duplicate} 个' + result_text += f'Skipped duplicate rules: {total_duplicate}' await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) await reply_and_delete(event,result_text) except Exception as e: session.rollback() - logger.error(f'批量添加替换规则时出错: {str(e)}') + logger.error(f'Error batch adding replace rules: {str(e)}') await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'添加替换规则时出错,请检查日志') + await reply_and_delete(event,'Error adding replace rules, please check logs') finally: session.close() async def handle_list_rule_command(event, command, parts): - """处理 list_rule 命令""" + """Handle list_rule command""" session = get_session() try: - # 获取页码参数,默认为第1页 + # Get page number argument, default to page 1 try: page = int(parts[1]) if len(parts) > 1 else 1 if page < 1: page = 1 except ValueError: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'页码必须是数字') + await reply_and_delete(event,'Page number must be a number') return - # 设置每页显示的数量 + # Set items per page per_page = 30 offset = (page - 1) * per_page - # 获取总规则数 + # Get total rule count total_rules = session.query(ForwardRule).count() if total_rules == 0: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'当前没有任何转发规则') + await reply_and_delete(event,'There are no forwarding rules') return - # 计算总页数 + # Calculate total pages total_pages = (total_rules + per_page - 1) // per_page - # 如果请求的页码超出范围,使用最后一页 + # If requested page exceeds range, use last page if page > total_pages: page = total_pages offset = (page - 1) * per_page - # 获取当前页的规则 + # 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 Rules List (Page {page}/{total_pages}):\n'] for rule in rules: - # 获取源聊天和目标聊天的名称 + # Get source chat and target chat names source_chat = rule.source_chat target_chat = rule.target_chat - # 构建规则描述 + # Build rule description 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'
Source: {source_chat.name} ({source_chat.telegram_chat_id})\n' + f'Target: {target_chat.name} ({target_chat.telegram_chat_id})\n' '
' ) message_parts.append(rule_desc) - # 创建分页按钮 + # Create pagination buttons buttons = [] nav_row = [] - # 添加上一页按钮 + # Add previous page button if page > 1: - nav_row.append(Button.inline('⬅️ 上一页', f'page_rule:{page-1}')) + nav_row.append(Button.inline('⬅️ Prev', f'page_rule:{page-1}')) else: - nav_row.append(Button.inline('⬅️', 'noop')) # 禁用状态的按钮 + nav_row.append(Button.inline('⬅️', 'noop')) # Disabled state button - # 添加页码按钮 + # Add page number button nav_row.append(Button.inline(f'{page}/{total_pages}', 'noop')) - # 添加下一页按钮 + # Add next page button if page < total_pages: - nav_row.append(Button.inline('下一页 ➡️', f'page_rule:{page+1}')) + nav_row.append(Button.inline('Next ➡️', f'page_rule:{page+1}')) else: - nav_row.append(Button.inline('➡️', 'noop')) # 禁用状态的按钮 + nav_row.append(Button.inline('➡️', 'noop')) # Disabled state button buttons.append(nav_row) - # 发送消息 + # Send message await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) await reply_and_delete(event,'\n'.join(message_parts), buttons=buttons, parse_mode='html') except Exception as e: - logger.error(f'列出规则时出错: {str(e)}') + logger.error(f'Error listing rules: {str(e)}') logger.exception(e) await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'获取规则列表时发生错误,请检查日志') + await reply_and_delete(event,'Error getting rule list, please check logs') finally: session.close() async def handle_delete_rule_command(event, command, parts): - """处理 delete_rule 命令""" + """Handle delete_rule command""" if len(parts) < 2: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f'用法: /{command} [ID2] [ID3] ...\n例如: /{command} 1 2 3') + await reply_and_delete(event,f'Usage: /{command} [ID2] [ID3] ...\nExample: /{command} 1 2 3') return try: ids_to_remove = [int(x) for x in parts[1:]] except ValueError: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'ID必须是数字') + await reply_and_delete(event,'ID must be a number') return session = get_session() @@ -2135,116 +2135,116 @@ async def handle_delete_rule_command(event, command, parts): continue try: - # 删除规则(关联的替换规则、关键字和媒体类型会自动删除) + # Delete rule (associated replace rules, keywords and media types will be automatically deleted) session.delete(rule) - # 尝试从RSS服务删除规则数据 + # Try to delete rule data from 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)}") + # Does not affect main flow, continue execution success_ids.append(rule_id) except Exception as e: - logger.error(f'删除规则 {rule_id} 时出错: {str(e)}') + logger.error(f'Error deleting rule {rule_id}: {str(e)}') failed_ids.append(rule_id) - # 提交事务 + # Commit transaction session.commit() - # 清理不再使用的聊天记录 - # 这里直接对整个数据库进行一次清理,不需要单独处理每个规则 - # 因为所有规则都已经从数据库中删除 + # Clean up unused chat records + # Clean the entire database at once here, no need to process each rule individually + # Because all rules have already been deleted from the database deleted_chats = await check_and_clean_chats(session) if deleted_chats > 0: - logger.info(f"删除规则后清理了 {deleted_chats} 个未使用的聊天记录") + logger.info(f"Cleaned up {deleted_chats} unused chat records after deleting rules") - # 构建响应消息 + # Build response message response_parts = [] if success_ids: - response_parts.append(f'✅ 成功删除规则: {", ".join(map(str, success_ids))}') + response_parts.append(f'✅ Successfully deleted rules: {", ".join(map(str, success_ids))}') if not_found_ids: - response_parts.append(f'❓ 未找到规则: {", ".join(map(str, not_found_ids))}') + response_parts.append(f'❓ Rules not found: {", ".join(map(str, not_found_ids))}') if failed_ids: - response_parts.append(f'❌ 删除失败的规则: {", ".join(map(str, failed_ids))}') + response_parts.append(f'❌ Failed to delete rules: {", ".join(map(str, failed_ids))}') if deleted_chats > 0: - response_parts.append(f'🧹 清理了 {deleted_chats} 个未使用的聊天记录') + response_parts.append(f'🧹 Cleaned up {deleted_chats} unused chat records') await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'\n'.join(response_parts) or '没有规则被删除') + await reply_and_delete(event,'\n'.join(response_parts) or 'No rules were deleted') except Exception as e: session.rollback() - logger.error(f'删除规则时出错: {str(e)}') + logger.error(f'Error deleting rules: {str(e)}') logger.exception(e) await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,'删除规则时发生错误,请检查日志') + await reply_and_delete(event,'Error deleting rules, please check logs') finally: session.close() async def handle_delete_rss_user_command(event, command, parts): - """处理 delete_rss_user 命令""" + """Handle delete_rss_user command""" db_ops = await get_db_ops() session = get_session() try: - # 检查是否指定了用户名 + # Check if username is specified specified_username = None if len(parts) > 1: specified_username = parts[1].strip() - # 查询所有用户 + # Query all users users = session.query(models.User).all() if not users: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event, "RSS系统中没有用户账户") + await reply_and_delete(event, "No user accounts in RSS system") return - # 占位,不排除以后有多用户功能,如果指定了用户名,尝试删除该用户 + # Placeholder, may have multi-user feature in the future. If username specified, try to delete the user if specified_username: user = session.query(models.User).filter(models.User.username == specified_username).first() if user: session.delete(user) session.commit() await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f"已删除RSS用户: {specified_username}") + await reply_and_delete(event,f"Deleted RSS user: {specified_username}") return else: await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f"未找到用户名为 '{specified_username}' 的RSS用户") + await reply_and_delete(event,f"RSS user with username '{specified_username}' not found") return - # 如果没有指定用户名 - # 默认只有一个用户,直接删除 + # If no username specified + # Default only one user, delete directly if len(users) == 1: user = users[0] username = user.username session.delete(user) session.commit() await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f"已删除RSS用户: {username}") + await reply_and_delete(event,f"Deleted RSS user: {username}") return - # 占位,不排除以后有多用户功能,如果有多个用户,则列出所有用户并提示指定用户名 + # Placeholder, may have multi-user feature in the future. If multiple users exist, list all and prompt for username usernames = [user.username for user in users] user_list = "\n".join([f"{i+1}. {username}" for i, username in enumerate(usernames)]) await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) - await reply_and_delete(event,f"RSS系统中有多个用户,请使用 `/delete_rss_user <用户名>` 指定要删除的用户:\n\n{user_list}") + await reply_and_delete(event,f"Multiple users in RSS system, please use `/delete_rss_user ` to specify the user to delete:\n\n{user_list}") except Exception as e: session.rollback() - error_message = f"删除RSS用户时出错: {str(e)}" + error_message = f"Error deleting RSS user: {str(e)}" logger.error(error_message) logger.error(traceback.format_exc()) await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0) diff --git a/handlers/link_handlers.py b/handlers/link_handlers.py index c33fb2d..9da44d9 100644 --- a/handlers/link_handlers.py +++ b/handlers/link_handlers.py @@ -7,11 +7,11 @@ logger = logging.getLogger(__name__) async def handle_message_link(client, event): - """处理 Telegram 消息链接""" + """Handle Telegram message links""" if not event.message.text: return - # 解析消息链接 + # Parse message link match = re.match(r'https?://t\.me/(?:c/(\d+)|([^/]+))/(\d+)', event.message.text) if not match: return @@ -20,29 +20,29 @@ async def handle_message_link(client, event): chat_id = None message_id = int(match.group(3)) - if match.group(1): # 私有频道格式 + if match.group(1): # Private channel format chat_id = int('-100' + match.group(1)) - else: # 公开频道格式 + else: # Public channel format chat_name = match.group(2) try: entity = await client.get_entity(chat_name) chat_id = entity.id except Exception as e: - logger.error(f'获取频道信息失败: {str(e)}') - await reply_and_delete(event,'⚠️ 无法访问该频道,请确保已关注该频道。') + logger.error(f'Failed to get channel info: {str(e)}') + await reply_and_delete(event,'⚠️ Unable to access this channel, please make sure you have followed it.') return - # 获取用户客户端 + # Get user client main = await get_main_module() user_client = main.user_client - # 获取原始消息 + # Get original message message = await user_client.get_messages(chat_id, ids=message_id) if not message: - await reply_and_delete(event,'⚠️ 无法获取该消息,可能是消息已被删除或无权限访问。') + await reply_and_delete(event,'⚠️ Unable to get this message, it may have been deleted or access is not permitted.') return - # 检查是否是媒体组消息 + # Check if it is a media group message if message.grouped_id: await handle_media_group(client, user_client, chat_id, message, event) else: @@ -50,19 +50,19 @@ async def handle_message_link(client, event): except Exception as e: - logger.error(f'处理消息链接时出错: {str(e)}') - await reply_and_delete(event,'⚠️ 处理消息时出错,请确保链接正确且有权限访问该消息。') + logger.error(f'Error processing message link: {str(e)}') + await reply_and_delete(event,'⚠️ Error processing message, please ensure the link is correct and you have permission to access it.') async def handle_media_group(client, user_client, chat_id, message, event): - """处理媒体组消息""" - files = [] # 将 files 移到外层作用域 + """Handle media group messages""" + files = [] # Move files to outer scope try: - # 收集媒体组的所有消息 + # Collect all messages in the media group media_group_messages = [] caption = None buttons = None - # 在消息ID前后范围内搜索同组消息 + # Search for messages in the same group within range of message ID async for grouped_message in user_client.iter_messages( chat_id, limit=20, @@ -71,25 +71,25 @@ async def handle_media_group(client, user_client, chat_id, message, event): ): if grouped_message.grouped_id == message.grouped_id: media_group_messages.append(grouped_message) - # 保存第一条消息的文本和按钮 + # Save text and buttons from the first message if not caption: caption = grouped_message.text buttons = grouped_message.buttons if hasattr(grouped_message, 'buttons') else None if media_group_messages: - # 下载所有媒体文件 + # Download all media files for msg in media_group_messages: if msg.media: try: file_path = await msg.download_media(TEMP_DIR) if file_path: files.append(file_path) - logger.info(f'已下载媒体文件: {file_path}') + logger.info(f'Downloaded media file: {file_path}') except Exception as e: - logger.error(f'下载媒体文件失败: {str(e)}') + logger.error(f'Failed to download media file: {str(e)}') if files: - # 发送媒体组 + # Send media group await client.send_file( event.chat_id, files, @@ -97,33 +97,33 @@ async def handle_media_group(client, user_client, chat_id, message, event): parse_mode='Markdown', buttons=buttons ) - logger.info(f'已转发媒体组消息,共 {len(files)} 个文件') + logger.info(f'Forwarded media group message, {len(files)} files in total') except Exception as e: - logger.error(f'处理媒体组消息时出错: {str(e)}') + logger.error(f'Error processing media group message: {str(e)}') raise finally: - # 确保清理所有临时文件 + # Ensure all temporary files are cleaned up for file_path in files: try: if os.path.exists(file_path): os.remove(file_path) - logger.info(f'已删除临时文件: {file_path}') + logger.info(f'Deleted temporary file: {file_path}') except Exception as e: - logger.error(f'删除临时文件失败 {file_path}: {str(e)}') + logger.error(f'Failed to delete temporary file {file_path}: {str(e)}') async def handle_single_message(client, message, event): - """处理单条消息""" + """Handle single message""" parse_mode = 'Markdown' buttons = message.buttons if hasattr(message, 'buttons') else None file_path = None try: if message.media: - # 处理媒体消息 + # Process media message file_path = await message.download_media(TEMP_DIR) if file_path: - logger.info(f'已下载媒体文件: {file_path}') + logger.info(f'Downloaded media file: {file_path}') caption = message.text if message.text else '' await client.send_file( event.chat_id, @@ -132,9 +132,9 @@ async def handle_single_message(client, message, event): parse_mode=parse_mode, buttons=buttons ) - logger.info('已转发单条媒体消息') + logger.info('Forwarded single media message') else: - # 处理纯文本消息 + # Process plain text message await client.send_message( event.chat_id, message.text, @@ -142,16 +142,16 @@ async def handle_single_message(client, message, event): link_preview=True, buttons=buttons ) - logger.info('已转发文本消息') + logger.info('Forwarded text message') except Exception as e: - logger.error(f'处理单条消息时出错: {str(e)}') + logger.error(f'Error processing single message: {str(e)}') raise finally: - # 确保清理临时文件 + # Ensure temporary files are cleaned up if file_path and os.path.exists(file_path): 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'删除临时文件失败 {file_path}: {str(e)}') + logger.error(f'Failed to delete temporary file {file_path}: {str(e)}') diff --git a/handlers/list_handlers.py b/handlers/list_handlers.py index fae843c..d7b87c7 100644 --- a/handlers/list_handlers.py +++ b/handlers/list_handlers.py @@ -2,7 +2,7 @@ from utils.auto_delete import reply_and_delete async def show_list(event, command, items, formatter, title, page=1): - """显示分页列表""" + """Display paginated list""" # KEYWORDS_PER_PAGE PAGE_SIZE = KEYWORDS_PER_PAGE @@ -11,40 +11,40 @@ async def show_list(event, command, items, formatter, title, page=1): if not items: try: - return await event.edit(f'没有找到任何{title}') + return await event.edit(f'No {title} found') except: - return await reply_and_delete(event,f'没有找到任何{title}') + return await reply_and_delete(event,f'No {title} found') - # 获取当前页的项目 + # Get items for current page start = (page - 1) * PAGE_SIZE end = min(start + PAGE_SIZE, total_items) current_items = items[start:end] - # 格式化列表项 + # Format list items item_list = [] for i, item in enumerate(current_items): formatted_item = formatter(i + start + 1, item) - # 如果是关键字列表,给关键字添加反引号 + # If it is a keyword list, add backticks to keywords if command == 'keyword': - # 分割序号和关键字内容 + # Split index and keyword content parts = formatted_item.split('. ', 1) if len(parts) == 2: number = parts[0] content = parts[1] - # 如果是正则表达式,在关键字部分添加反引号 - if ' (正则)' in content: - keyword, regex_mark = content.split(' (正则)') - formatted_item = f'{number}. `{keyword}` (正则)' + # If it is a regex, add backticks to the keyword part + if ' (regex)' in content: + keyword, regex_mark = content.split(' (regex)') + formatted_item = f'{number}. `{keyword}` (regex)' else: formatted_item = f'{number}. `{content}`' item_list.append(formatted_item) - # 创建分页按钮 + # Create pagination buttons buttons = await create_list_buttons(total_pages, page, command) - # 构建消息文本 + # Build message text text = f'{title}\n{chr(10).join(item_list)}' - if len(text) > 4096: # Telegram消息长度限制 + if len(text) > 4096: # Telegram message length limit text = text[:4093] + '...' try: diff --git a/handlers/prompt_handlers.py b/handlers/prompt_handlers.py index 8fc8139..88c69d4 100644 --- a/handlers/prompt_handlers.py +++ b/handlers/prompt_handlers.py @@ -13,11 +13,11 @@ logger = logging.getLogger(__name__) async def handle_prompt_setting(event, client, sender_id, chat_id, current_state, message): - """处理设置提示词的逻辑""" - logger.info(f"开始处理提示词设置,用户ID:{sender_id},聊天ID:{chat_id},当前状态:{current_state}") + """Handle the logic for setting prompts""" + logger.info(f"Starting to process prompt setting, user ID: {sender_id}, chat ID: {chat_id}, current state: {current_state}") if not current_state: - logger.info("当前无状态,返回False") + logger.info("No current state, returning False") return False rule_id = None @@ -28,91 +28,91 @@ async def handle_prompt_setting(event, client, sender_id, chat_id, current_state if current_state.startswith("set_summary_prompt:"): rule_id = current_state.split(":")[1] field_name = "summary_prompt" - prompt_type = "AI总结" + prompt_type = "AI Summary" template_type = "ai" - logger.info(f"检测到设置总结提示词,规则ID:{rule_id}") + logger.info(f"Detected setting summary prompt, rule ID: {rule_id}") elif current_state.startswith("set_ai_prompt:"): rule_id = current_state.split(":")[1] field_name = "ai_prompt" prompt_type = "AI" template_type = "ai" - logger.info(f"检测到设置AI提示词,规则ID:{rule_id}") + logger.info(f"Detected setting AI prompt, rule ID: {rule_id}") elif current_state.startswith("set_userinfo_template:"): rule_id = current_state.split(":")[1] field_name = "userinfo_template" - prompt_type = "用户信息" + prompt_type = "User Info" template_type = "userinfo" - logger.info(f"检测到设置用户信息模板,规则ID:{rule_id}") + logger.info(f"Detected setting user info template, rule ID: {rule_id}") elif current_state.startswith("set_time_template:"): rule_id = current_state.split(":")[1] field_name = "time_template" - prompt_type = "时间" + prompt_type = "Time" template_type = "time" - logger.info(f"检测到设置时间模板,规则ID:{rule_id}") + logger.info(f"Detected setting time template, rule ID: {rule_id}") elif current_state.startswith("set_original_link_template:"): rule_id = current_state.split(":")[1] field_name = "original_link_template" - prompt_type = "原始链接" + prompt_type = "Original Link" template_type = "link" - logger.info(f"检测到设置原始链接模板,规则ID:{rule_id}") + logger.info(f"Detected setting original link template, rule ID: {rule_id}") elif current_state.startswith("add_push_channel:"): - # 处理添加推送频道 + # Handle adding push channel rule_id = current_state.split(":")[1] - logger.info(f"检测到添加推送频道,规则ID:{rule_id}") + logger.info(f"Detected adding push channel, rule ID: {rule_id}") return await handle_add_push_channel(event, client, sender_id, chat_id, rule_id, message) else: - logger.info(f"未知的状态类型:{current_state}") + logger.info(f"Unknown state type: {current_state}") return False - logger.info(f"处理设置{prompt_type}提示词/模板,规则ID:{rule_id},字段名:{field_name}") + logger.info(f"Processing setting {prompt_type} prompt/template, rule ID: {rule_id}, field name: {field_name}") session = get_session() try: - logger.info(f"查询规则ID:{rule_id}") + logger.info(f"Querying rule ID: {rule_id}") rule = session.query(ForwardRule).get(int(rule_id)) if rule: old_prompt = getattr(rule, field_name) if hasattr(rule, field_name) else None new_prompt = event.message.text - logger.info(f"找到规则,原提示词/模板:{old_prompt}") - logger.info(f"准备更新为新提示词/模板:{new_prompt}") + logger.info(f"Rule found, original prompt/template: {old_prompt}") + logger.info(f"Preparing to update to new prompt/template: {new_prompt}") setattr(rule, field_name, new_prompt) session.commit() - logger.info(f"已更新规则{rule_id}的{prompt_type}提示词/模板") + logger.info(f"Updated rule {rule_id}'s {prompt_type} prompt/template") - # 检查是否启用了同步功能 + # Check if sync feature is enabled if rule.enable_sync: - logger.info(f"规则 {rule.id} 启用了同步功能,正在同步提示词/模板设置到关联规则") - # 获取需要同步的规则列表 + logger.info(f"Rule {rule.id} has sync enabled, syncing prompt/template settings to associated rules") + # Get list of rules that need to be synced sync_rules = session.query(RuleSync).filter(RuleSync.rule_id == rule.id).all() - # 为每个同步规则应用相同的提示词设置 + # Apply the same prompt settings for each sync rule for sync_rule in sync_rules: sync_rule_id = sync_rule.sync_rule_id - logger.info(f"正在同步{prompt_type}提示词/模板到规则 {sync_rule_id}") - - # 获取同步目标规则 + logger.info(f"Syncing {prompt_type} prompt/template 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 prompt settings try: - # 记录旧提示词 + # Record old prompt old_target_prompt = getattr(target_rule, field_name) if hasattr(target_rule, field_name) else None - # 设置新提示词 + # Set new prompt setattr(target_rule, field_name, new_prompt) - logger.info(f"同步规则 {sync_rule_id} 的{prompt_type}提示词/模板从 '{old_target_prompt}' 到 '{new_prompt}'") + logger.info(f"Synced rule {sync_rule_id}'s {prompt_type} prompt/template from '{old_target_prompt}' to '{new_prompt}'") except Exception as e: - logger.error(f"同步{prompt_type}提示词/模板到规则 {sync_rule_id} 时出错: {str(e)}") + logger.error(f"Error syncing {prompt_type} prompt/template to rule {sync_rule_id}: {str(e)}") continue session.commit() - logger.info("所有同步提示词/模板更改已提交") + logger.info("All synced prompt/template changes have been committed") - logger.info(f"清除用户状态,用户ID:{sender_id},聊天ID:{chat_id}") + logger.info(f"Clearing user state, user ID: {sender_id}, chat ID: {chat_id}") state_manager.clear_state(sender_id, chat_id) @@ -123,58 +123,58 @@ async def handle_prompt_setting(event, client, sender_id, chat_id, current_state try: await async_delete_user_message(bot_client, message_chat_id, event.message.id, 0) except Exception as e: - logger.error(f"删除用户消息失败: {str(e)}") + logger.error(f"Failed to delete user message: {str(e)}") await message.delete() - logger.info("准备发送更新后的设置消息") - - # 根据模板类型选择不同的显示页面 + logger.info("Preparing to send updated settings message") + + # Choose different display page based on template type if template_type == "ai": - # AI设置页面 + # AI settings page await client.send_message( chat_id, await get_ai_settings_text(rule), buttons=await bot_handler.create_ai_settings_buttons(rule) ) elif template_type in ["userinfo", "time", "link"]: - # 其他设置页面 + # Other settings page await client.send_message( chat_id, - f"已更新规则 {rule_id} 的{prompt_type}模板", + f"Updated rule {rule_id}'s {prompt_type} template", buttons=await bot_handler.create_other_settings_buttons(rule_id=rule_id) ) - # 删除用户消息 - logger.info("设置消息发送成功") + # Delete user message + logger.info("Settings message sent successfully") return True else: - logger.warning(f"未找到规则ID:{rule_id}") + logger.warning(f"Rule ID not found: {rule_id}") except Exception as e: - logger.error(f"处理提示词/模板设置时发生错误:{str(e)}") + logger.error(f"Error occurred while processing prompt/template setting: {str(e)}") raise finally: session.close() - logger.info("数据库会话已关闭") + logger.info("Database session closed") return True async def handle_add_push_channel(event, client, sender_id, chat_id, rule_id, message): - """处理添加推送频道的逻辑""" - logger.info(f"开始处理添加推送频道,规则ID:{rule_id}") + """Handle the logic for adding push channel""" + logger.info(f"Starting to process adding push channel, rule ID: {rule_id}") session = get_session() try: - # 获取规则 + # Get rule rule = session.query(ForwardRule).get(int(rule_id)) if not rule: - logger.warning(f"未找到规则ID:{rule_id}") + logger.warning(f"Rule ID not found: {rule_id}") return False - # 获取用户输入的推送频道信息 + # Get push channel info entered by user push_channel = event.message.text.strip() - logger.info(f"用户输入的推送频道: {push_channel}") + logger.info(f"Push channel entered by user: {push_channel}") try: - # 创建新的推送配置 + # Create new push configuration is_email = push_channel.startswith(('mailto://', 'mailtos://', 'email://')) push_config = PushConfig( rule_id=int(rule_id), @@ -184,38 +184,38 @@ async def handle_add_push_channel(event, client, sender_id, chat_id, rule_id, me ) session.add(push_config) - # 启用规则的推送功能 + # Enable push feature for the rule rule.enable_push = True - # 检查是否启用了同步功能 + # Check if sync feature is enabled if rule.enable_sync: - logger.info(f"规则 {rule.id} 启用了同步功能,正在同步推送配置到关联规则") - - # 获取需要同步的规则列表 + logger.info(f"Rule {rule.id} has sync enabled, syncing push configuration to associated rules") + + # Get list of rules that need to be synced sync_rules = session.query(RuleSync).filter(RuleSync.rule_id == rule.id).all() - # 为每个同步规则创建相同的推送配置 + # Create the same push configuration for each sync 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 push configuration 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 - - # 检查目标规则是否已存在相同推送频道 + + # Check if target rule already has the same push channel existing_config = session.query(PushConfig).filter_by( rule_id=sync_rule_id, push_channel=push_channel ).first() if existing_config: - logger.info(f"目标规则 {sync_rule_id} 已存在推送频道 {push_channel},跳过") + logger.info(f"Target rule {sync_rule_id} already has push channel {push_channel}, skipping") continue - # 创建新的推送配置 + # Create new push configuration try: sync_push_config = PushConfig( rule_id=sync_rule_id, @@ -225,61 +225,61 @@ async def handle_add_push_channel(event, client, sender_id, chat_id, rule_id, me ) session.add(sync_push_config) - # 启用目标规则的推送功能 + # Enable push feature for target rule target_rule.enable_push = True - logger.info(f"已为规则 {sync_rule_id} 添加推送频道 {push_channel}") + logger.info(f"Added push channel {push_channel} for rule {sync_rule_id}") except Exception as e: - logger.error(f"为规则 {sync_rule_id} 添加推送配置时出错: {str(e)}") + logger.error(f"Error adding push configuration for rule {sync_rule_id}: {str(e)}") continue - # 提交更改 + # Commit changes session.commit() success = True - message_text = "成功添加推送配置" + message_text = "Successfully added push configuration" except Exception as db_error: session.rollback() success = False - message_text = f"添加推送配置失败: {str(db_error)}" - logger.error(f"添加推送配置到数据库时出错: {str(db_error)}") + message_text = f"Failed to add push configuration: {str(db_error)}" + logger.error(f"Error adding push configuration to database: {str(db_error)}") - # 清除状态 + # Clear state state_manager.clear_state(sender_id, chat_id) - # 删除用户消息 + # Delete user message message_chat_id = event.message.chat_id bot_client = await get_bot_client() try: await async_delete_user_message(bot_client, message_chat_id, event.message.id, 0) except Exception as e: - logger.error(f"删除用户消息失败: {str(e)}") - - # 删除原始消息并显示结果 + logger.error(f"Failed to delete user message: {str(e)}") + + # Delete original message and display result await message.delete() - # 获取主界面 + # Get main module main_module = await get_main_module() bot_client = main_module.bot_client - # 发送结果通知 + # Send result notification if success: await send_message_and_delete( bot_client, chat_id, - f"已成功添加推送频道: {push_channel}", + f"Successfully added push channel: {push_channel}", buttons=await bot_handler.create_push_settings_buttons(rule_id) ) else: await send_message_and_delete( bot_client, chat_id, - f"添加推送频道失败: {message_text}", + f"Failed to add push channel: {message_text}", buttons=await bot_handler.create_push_settings_buttons(rule_id) ) return True except Exception as e: - logger.error(f"处理添加推送频道时出错: {str(e)}") + logger.error(f"Error processing adding push channel: {str(e)}") logger.error(traceback.format_exc()) return False finally: diff --git a/handlers/user_handler.py b/handlers/user_handler.py index 5de5e2f..d90a408 100644 --- a/handlers/user_handler.py +++ b/handlers/user_handler.py @@ -8,32 +8,32 @@ logger = logging.getLogger(__name__) async def process_forward_rule(client, event, chat_id, rule): - """处理转发规则(用户模式)""" + """Process forwarding rule (user mode)""" if not rule.enable_rule: - logger.info(f'规则 ID: {rule.id} 已禁用,跳过处理') + logger.info(f'Rule ID: {rule.id} is disabled, skipping processing') return message_text = event.message.text or '' check_message_text = message_text - # 添加日志 - logger.info(f'处理规则 ID: {rule.id}') - logger.info(f'消息内容: {message_text}') - logger.info(f'规则模式: {rule.forward_mode.value}') + # Add logs + logger.info(f'Processing rule ID: {rule.id}') + logger.info(f'Message content: {message_text}') + logger.info(f'Rule mode: {rule.forward_mode.value}') if rule.is_filter_user_info: - sender_info = await get_sender_info(event, rule.id) # 调用新的函数获取 sender_info + sender_info = await get_sender_info(event, rule.id) # Call the new function to get sender_info if sender_info: check_message_text = f"{sender_info}:\n{message_text}" - logger.info(f'附带用户信息后的消息: {message_text}') + logger.info(f'Message with user info attached: {message_text}') else: - logger.warning(f"规则 ID: {rule.id} - 无法获取发送者信息") + logger.warning(f"Rule ID: {rule.id} - Unable to get sender info") should_forward = await check_keywords(rule,check_message_text) - logger.info(f'最终决定: {"转发" if should_forward else "不转发"}') + logger.info(f'Final decision: {"forward" if should_forward else "do not forward"}') if should_forward: target_chat = rule.target_chat @@ -43,42 +43,42 @@ async def process_forward_rule(client, event, chat_id, rule): if event.message.grouped_id: - # 等待一段时间以确保收到所有媒体组消息 + # Wait a moment to ensure all media group messages are received await asyncio.sleep(1) - # 收集媒体组的所有消息 + # Collect all messages in the media group messages = [] async for message in client.iter_messages( event.chat_id, - limit=20, # 限制搜索范围 + limit=20, # Limit search range min_id=event.message.id - 10, max_id=event.message.id + 10 ): if message.grouped_id == event.message.grouped_id: messages.append(message.id) - logger.info(f'找到媒体组消息: ID={message.id}') + logger.info(f'Found media group message: ID={message.id}') - # 按照ID排序,确保转发顺序正确 + # Sort by ID to ensure correct forwarding order messages.sort() - # 一次性转发所有消息 + # Forward all messages at once await client.forward_messages( target_chat_id, messages, event.chat_id ) - logger.info(f'[用户] 已转发 {len(messages)} 条媒体组消息到: {target_chat.name} ({target_chat_id})') + logger.info(f'[User] Forwarded {len(messages)} media group messages to: {target_chat.name} ({target_chat_id})') else: - # 处理单条消息 + # Process single message await client.forward_messages( target_chat_id, event.message.id, event.chat_id ) - logger.info(f'[用户] 消息已转发到: {target_chat.name} ({target_chat_id})') + logger.info(f'[User] Message forwarded to: {target_chat.name} ({target_chat_id})') except Exception as e: - logger.error(f'转发消息时出错: {str(e)}') + logger.error(f'Error forwarding message: {str(e)}') logger.exception(e) \ No newline at end of file diff --git a/main.py b/main.py index b3bf10f..806f240 100644 --- a/main.py +++ b/main.py @@ -16,25 +16,25 @@ from rss.main import app as rss_app from utils.log_config import setup_logging -# 设置Docker日志的默认配置,如果docker-compose.yml中没有配置日志选项将使用这些值 +# Set default Docker log configuration; these values will be used if not configured in docker-compose.yml os.environ.setdefault('DOCKER_LOG_MAX_SIZE', '10m') os.environ.setdefault('DOCKER_LOG_MAX_FILE', '3') -# 设置日志配置 +# Set up logging configuration setup_logging() logger = logging.getLogger(__name__) -# 加载环境变量 +# Load environment variables load_dotenv() -# 从环境变量获取配置 +# Get configuration from environment variables api_id = os.getenv('API_ID') api_hash = os.getenv('API_HASH') bot_token = os.getenv('BOT_TOKEN') phone_number = os.getenv('PHONE_NUMBER') -# 创建 DBOperations 实例 +# Create DBOperations instance db_ops = None scheduler = None @@ -42,34 +42,34 @@ async def init_db_ops(): - """初始化 DBOperations 实例""" + """Initialize DBOperations instance""" global db_ops if db_ops is None: db_ops = await DBOperations.create() return db_ops -# 创建文件夹 +# Create directories os.makedirs('./sessions', exist_ok=True) os.makedirs('./temp', exist_ok=True) -# 清空./temp文件夹 +# Clear ./temp directory def clear_temp_dir(): for file in os.listdir('./temp'): os.remove(os.path.join('./temp', file)) -# 创建客户端 +# Create clients user_client = TelegramClient('./sessions/user', api_id, api_hash) bot_client = TelegramClient('./sessions/bot', api_id, api_hash) -# 初始化数据库 +# Initialize database engine = init_db() def run_rss_server(host: str, port: int): - """在新进程中运行 RSS 服务器""" + """Run RSS server in a new process""" uvicorn.run( rss_app, host=host, @@ -78,271 +78,271 @@ def run_rss_server(host: str, port: int): async def start_clients(): - # 初始化 DBOperations + # Initialize DBOperations global db_ops, scheduler, chat_updater db_ops = await DBOperations.create() try: - # 启动用户客户端 + # Start user client await user_client.start(phone=phone_number) me_user = await user_client.get_me() - print(f'用户客户端已启动: {me_user.first_name} (@{me_user.username})') + print(f'User client started: {me_user.first_name} (@{me_user.username})') - # 启动机器人客户端 + # Start bot client await bot_client.start(bot_token=bot_token) me_bot = await bot_client.get_me() - print(f'机器人客户端已启动: {me_bot.first_name} (@{me_bot.username})') + print(f'Bot client started: {me_bot.first_name} (@{me_bot.username})') - # 设置消息监听器 + # Set up message listeners await setup_listeners(user_client, bot_client) - # 注册命令 + # Register commands await register_bot_commands(bot_client) - # 创建并启动调度器 + # Create and start scheduler scheduler = SummaryScheduler(user_client, bot_client) await scheduler.start() - - # 创建并启动聊天信息更新器 + + # Create and start chat info updater chat_updater = ChatUpdater(user_client) await chat_updater.start() - # 如果启用了 RSS 服务 + # If RSS service is enabled if os.getenv('RSS_ENABLED', '').lower() == 'true': try: rss_host = os.getenv('RSS_HOST', '0.0.0.0') rss_port = int(os.getenv('RSS_PORT', '8000')) - logger.info(f"正在启动 RSS 服务 (host={rss_host}, port={rss_port})") - - # 在新进程中启动 RSS 服务 + logger.info(f"Starting RSS service (host={rss_host}, port={rss_port})") + + # Start RSS service in a new process rss_process = multiprocessing.Process( target=run_rss_server, args=(rss_host, rss_port) ) rss_process.start() - logger.info("RSS 服务启动成功") + logger.info("RSS service started successfully") except Exception as e: - logger.error(f"启动 RSS 服务失败: {str(e)}") + logger.error(f"Failed to start RSS service: {str(e)}") logger.exception(e) else: - logger.info("RSS 服务未启用") + logger.info("RSS service is not enabled") - # 发送欢迎消息 + # Send welcome message await send_welcome_message(bot_client) - # 等待两个客户端都断开连接 + # Wait for both clients to disconnect await asyncio.gather( user_client.run_until_disconnected(), bot_client.run_until_disconnected() ) finally: - # 关闭 DBOperations + # Close DBOperations if db_ops and hasattr(db_ops, 'close'): await db_ops.close() - # 停止调度器 + # Stop scheduler if scheduler: scheduler.stop() - # 停止聊天信息更新器 + # Stop chat info updater if chat_updater: chat_updater.stop() - # 如果 RSS 服务在运行,停止它 + # If RSS service is running, stop it if 'rss_process' in locals() and rss_process.is_alive(): rss_process.terminate() rss_process.join() async def register_bot_commands(bot): - """注册机器人命令""" - # # 先清空现有命令 + """Register bot commands""" + # # Clear existing commands first # try: # await bot(SetBotCommandsRequest( # scope=types.BotCommandScopeDefault(), # lang_code='', - # commands=[] # 空列表清空所有命令 + # commands=[] # Empty list to clear all commands # )) - # logger.info('已清空现有机器人命令') + # logger.info('Existing bot commands cleared') # except Exception as e: - # logger.error(f'清空机器人命令时出错: {str(e)}') + # logger.error(f'Error clearing bot commands: {str(e)}') commands = [ - # 基础命令 + # Basic commands BotCommand( command='start', - description='开始使用' + description='Get started' ), BotCommand( command='help', - description='查看帮助' + description='View help' ), - # 绑定和设置 + # Binding and settings BotCommand( command='bind', - description='绑定源聊天' + description='Bind source chat' ), BotCommand( command='settings', - description='管理转发规则' + description='Manage forwarding rules' ), BotCommand( command='switch', - description='切换当前需要设置的聊天规则' + description='Switch the chat rule currently being configured' ), - # 关键字管理 + # Keyword management BotCommand( command='add', - description='添加关键字' + description='Add keyword' ), BotCommand( command='add_regex', - description='添加正则关键字' + description='Add regex keyword' ), BotCommand( command='add_all', - description='添加普通关键字到所有规则' + description='Add plain keyword to all rules' ), BotCommand( command='add_regex_all', - description='添加正则表达式到所有规则' + description='Add regex to all rules' ), BotCommand( command='list_keyword', - description='列出所有关键字' + description='List all keywords' ), BotCommand( command='remove_keyword', - description='删除关键字' + description='Remove keyword' ), BotCommand( command='remove_keyword_by_id', - description='按ID删除关键字' + description='Remove keyword by ID' ), BotCommand( command='remove_all_keyword', - description='删除当前频道绑定的所有规则的指定关键字' + description='Remove specified keyword from all rules bound to current channel' ), - # 替换规则管理 + # Replace rule management BotCommand( command='replace', - description='添加替换规则' + description='Add replace rule' ), BotCommand( command='replace_all', - description='添加替换规则到所有规则' + description='Add replace rule to all rules' ), BotCommand( command='list_replace', - description='列出所有替换规则' + description='List all replace rules' ), BotCommand( command='remove_replace', - description='删除替换规则' + description='Remove replace rule' ), - # 导入导出功能 + # Import/export functionality BotCommand( command='export_keyword', - description='导出当前规则的关键字' + description='Export keywords of current rule' ), BotCommand( command='export_replace', - description='导出当前规则的替换规则' + description='Export replace rules of current rule' ), BotCommand( command='import_keyword', - description='导入普通关键字' + description='Import plain keywords' ), BotCommand( command='import_regex_keyword', - description='导入正则表达式关键字' + description='Import regex keywords' ), BotCommand( command='import_replace', - description='导入替换规则' + description='Import replace rules' ), - # UFB相关功能 + # UFB related features BotCommand( command='ufb_bind', - description='绑定ufb域名' + description='Bind UFB domain' ), BotCommand( command='ufb_unbind', - description='解绑ufb域名' + description='Unbind UFB domain' ), BotCommand( command='ufb_item_change', - description='切换ufb同步配置类型' + description='Switch UFB sync configuration type' ), BotCommand( command='clear_all_keywords', - description='清除当前规则的所有关键字' + description='Clear all keywords of current rule' ), BotCommand( command='clear_all_keywords_regex', - description='清除当前规则的所有正则关键字' + description='Clear all regex keywords of current rule' ), BotCommand( command='clear_all_replace', - description='清除当前规则的所有替换规则' + description='Clear all replace rules of current rule' ), BotCommand( command='copy_keywords', - description='复制参数规则的关键字到当前规则' + description='Copy keywords from specified rule to current rule' ), BotCommand( command='copy_keywords_regex', - description='复制参数规则的正则关键字到当前规则' + description='Copy regex keywords from specified rule to current rule' ), BotCommand( command='copy_replace', - description='复制参数规则的替换规则到当前规则' + description='Copy replace rules from specified rule to current rule' ), BotCommand( command='copy_rule', - description='复制参数规则到当前规则' + description='Copy specified rule to current rule' ), BotCommand( command='changelog', - description='查看更新日志' + description='View changelog' ), BotCommand( command='list_rule', - description='列出所有转发规则' + description='List all forwarding rules' ), BotCommand( command='delete_rule', - description='删除转发规则' + description='Delete forwarding rule' ), BotCommand( command='delete_rss_user', - description='删除RSS用户' + description='Delete RSS user' ), # BotCommand( # command='clear_all', - # description='慎用!清空所有数据' + # description='Use with caution! Clear all data' # ), ] try: result = await bot(SetBotCommandsRequest( scope=types.BotCommandScopeDefault(), - lang_code='', # 空字符串表示默认语言 + lang_code='', # Empty string means default language commands=commands )) if result: - logger.info('已成功注册机器人命令') + logger.info('Bot commands registered successfully') else: - logger.error('注册机器人命令失败') + logger.error('Failed to register bot commands') except Exception as e: - logger.error(f'注册机器人命令时出错: {str(e)}') + logger.error(f'Error registering bot commands: {str(e)}') if __name__ == '__main__': - # 运行事件循环 + # Run event loop loop = asyncio.get_event_loop() try: loop.run_until_complete(start_clients()) except KeyboardInterrupt: - print("正在关闭客户端...") + print("Shutting down clients...") finally: - loop.close() \ No newline at end of file + loop.close() diff --git a/managers/state_manager.py b/managers/state_manager.py index 64db6d3..fbd83cf 100644 --- a/managers/state_manager.py +++ b/managers/state_manager.py @@ -7,42 +7,42 @@ class StateManager: def __init__(self): self._states: Dict[Tuple[int, int], Tuple[str, Optional[Message], Optional[str]]] = {} - logger.info("StateManager 初始化") - + logger.info("StateManager initialized") + def set_state(self, user_id: int, chat_id: int, state: str, message: Optional[Message] = None, state_type: Optional[str] = None) -> None: - """设置用户状态""" + """Set user state""" key = (user_id, chat_id) self._states[key] = (state, message, state_type) - logger.info(f"设置状态 - key: {key}, state: {state}, type: {state_type}") - logger.debug(f"当前所有状态: {self._states}") # 改为 debug 级别 - + logger.info(f"Set state - key: {key}, state: {state}, type: {state_type}") + logger.debug(f"All current states: {self._states}") # Changed to debug level + def get_state(self, user_id: int, chat_id: int) -> Union[Tuple[str, Optional[Message], Optional[str]], Tuple[None, None, None]]: - """获取用户状态""" + """Get user state""" key = (user_id, chat_id) state_data = self._states.get(key) - if state_data: # 只在状态存在时记录日志 - if len(state_data) == 3: # 兼容新格式 + if state_data: # Only log when state exists + if len(state_data) == 3: # Compatible with new format state, message, state_type = state_data - logger.info(f"获取状态 - key: {key}, state: {state}, type: {state_type}") - else: # 兼容旧格式 + logger.info(f"Get state - key: {key}, state: {state}, type: {state_type}") + else: # Compatible with old format state, message = state_data state_type = None - logger.info(f"获取状态 - key: {key}, state: {state}, type: None (旧格式)") + logger.info(f"Get state - key: {key}, state: {state}, type: None (old format)") return state, message, state_type return None, None, None - + def clear_state(self, user_id: int, chat_id: int) -> None: - """清除用户状态""" + """Clear user state""" key = (user_id, chat_id) if key in self._states: del self._states[key] - logger.info(f"清除状态 - key: {key}") - logger.debug(f"当前所有状态: {self._states}") # 改为 debug 级别 - + logger.info(f"Clear state - key: {key}") + logger.debug(f"All current states: {self._states}") # Changed to debug level + def check_state(self) -> bool: - """检查是否存在状态""" + """Check if any state exists""" return bool(self._states) -# 创建全局实例 +# Create global instance state_manager = StateManager() -logger.info("StateManager 全局实例已创建") \ No newline at end of file +logger.info("StateManager global instance created") \ No newline at end of file diff --git a/message_listener.py b/message_listener.py index 5115a1f..c7f834b 100644 --- a/message_listener.py +++ b/message_listener.py @@ -10,201 +10,200 @@ from managers.state_manager import state_manager from telethon.tl import types from filters.process import process_forward_rule -# 加载环境变量 +# Load environment variables load_dotenv() -# 获取logger +# Get logger logger = logging.getLogger(__name__) -# 添加一个缓存来存储已处理的媒体组 +# Add a cache to store processed media groups PROCESSED_GROUPS = set() BOT_ID = None async def setup_listeners(user_client, bot_client): """ - 设置消息监听器 - + Set up message listeners + Args: - user_client: 用户客户端(用于监听消息和转发) - bot_client: 机器人客户端(用于处理命令和转发) + user_client: User client (for listening to messages and forwarding) + bot_client: Bot client (for handling commands and forwarding) """ global BOT_ID - - # 直接获取机器人ID + + # Directly get bot ID try: me = await bot_client.get_me() BOT_ID = me.id - logger.info(f"获取到机器人ID: {BOT_ID} (类型: {type(BOT_ID)})") + logger.info(f"Got bot ID: {BOT_ID} (type: {type(BOT_ID)})") except Exception as e: - logger.error(f"获取机器人ID时出错: {str(e)}") - - # 过滤器,排除机器人自己的消息 + logger.error(f"Error getting bot ID: {str(e)}") + + # Filter to exclude bot's own messages async def not_from_bot(event): if BOT_ID is None: - return True # 如果未获取到机器人ID,不进行过滤 - + return True # If bot ID not obtained, do not filter + sender = event.sender_id try: sender_id = int(sender) if sender is not None else None is_not_bot = sender_id != BOT_ID if not is_not_bot: - logger.info(f"过滤器识别到机器人消息,忽略处理: {sender_id}") + logger.info(f"Filter detected bot message, skipping processing: {sender_id}") return is_not_bot except (ValueError, TypeError): - return True # 转换失败时不过滤 - - # 用户客户端监听器 - 使用过滤器,避免处理机器人消息 + return True # Do not filter on conversion failure + + # User client listener - use filter to avoid processing bot messages @user_client.on(events.NewMessage(func=not_from_bot)) async def user_message_handler(event): await handle_user_message(event, user_client, bot_client) - - # 机器人客户端监听器 - 使用过滤器 + + # Bot client listener - use filter @bot_client.on(events.NewMessage(func=not_from_bot)) async def bot_message_handler(event): - # logger.info(f"机器人收到非自身消息, 发送者ID: {event.sender_id}") + # logger.info(f"Bot received non-self message, sender ID: {event.sender_id}") await handle_bot_message(event, bot_client) - - # 注册机器人回调处理器 + + # Register bot callback handler bot_client.add_event_handler(bot_handler.callback_handler) async def handle_user_message(event, user_client, bot_client): - """处理用户客户端收到的消息""" - # logger.info("handle_user_message:开始处理用户消息") - + """Handle messages received by user client""" + # logger.info("handle_user_message: Starting to process user message") + chat = await event.get_chat() chat_id = abs(chat.id) - # logger.info(f"handle_user_message:获取到聊天ID: {chat_id}") + # logger.info(f"handle_user_message: Got chat ID: {chat_id}") - # 检查是否频道消息 + # Check if it's a channel message if isinstance(event.chat, types.Channel) and state_manager.check_state(): - # logger.info("handle_user_message:检测到频道消息且存在状态") + # logger.info("handle_user_message: Detected channel message with existing state") sender_id = os.getenv('USER_ID') - # 频道ID需要加上100前缀 + # Channel ID needs 100 prefix chat_id = int(f"100{chat_id}") - # logger.info(f"handle_user_message:频道消息处理: sender_id={sender_id}, chat_id={chat_id}") + # logger.info(f"handle_user_message: Channel message processing: sender_id={sender_id}, chat_id={chat_id}") else: sender_id = event.sender_id - # logger.info(f"handle_user_message:非频道消息处理: sender_id={sender_id}") + # logger.info(f"handle_user_message: Non-channel message processing: sender_id={sender_id}") - # 检查用户状态 + # Check user state current_state, message, state_type = state_manager.get_state(sender_id, chat_id) - # logger.info(f'handle_user_message:当前是否有状态: {state_manager.check_state()}') - # logger.info(f"handle_user_message:当前用户ID和聊天ID: {sender_id}, {chat_id}") - # logger.info(f"handle_user_message:获取当前聊天窗口的用户状态: {current_state}") - + # logger.info(f'handle_user_message: Current state exists: {state_manager.check_state()}') + # logger.info(f"handle_user_message: Current user ID and chat ID: {sender_id}, {chat_id}") + # logger.info(f"handle_user_message: Got current chat window user state: {current_state}") + if current_state: - # logger.info(f"检测到用户状态: {current_state}") - # 处理提示词设置 - # logger.info("准备处理提示词设置") + # logger.info(f"Detected user state: {current_state}") + # Handle prompt setting + # logger.info("Preparing to handle prompt setting") if await handle_prompt_setting(event, bot_client, sender_id, chat_id, current_state, message): - # logger.info("提示词设置处理完成,返回") + # logger.info("Prompt setting processing completed, returning") return - # logger.info("提示词设置处理未完成,继续执行") + # logger.info("Prompt setting processing not completed, continuing execution") - # 检查是否是媒体组消息 + # Check if it's a media group message if event.message.grouped_id: - # 如果这个媒体组已经处理过,就跳过 + # If this media group has already been processed, skip it group_key = f"{chat_id}:{event.message.grouped_id}" if group_key in PROCESSED_GROUPS: return - # 标记这个媒体组为已处理 + # Mark this media group as processed PROCESSED_GROUPS.add(group_key) asyncio.create_task(clear_group_cache(group_key)) - - # 首先检查数据库中是否有该聊天的转发规则 + + # First check if there are forwarding rules for this chat in the database session = get_session() try: - # 查询源聊天 + # Query source chat source_chat = session.query(Chat).filter( Chat.telegram_chat_id == str(chat_id) ).first() - + if not source_chat: return - - # 添加日志:查询转发规则 - logger.info(f'找到源聊天: {source_chat.name} (ID: {source_chat.id})') - - # 查找以当前聊天为源的规则 + + # Add log: query forwarding rules + logger.info(f'Found source chat: {source_chat.name} (ID: {source_chat.id})') + + # Find rules where current chat is the source rules = session.query(ForwardRule).filter( ForwardRule.source_chat_id == source_chat.id ).all() - + if not rules: - logger.info(f'聊天 {source_chat.name} 没有转发规则') + logger.info(f'Chat {source_chat.name} has no forwarding rules') return - - # 有转发规则时,才记录消息信息 + + # Only log message info when there are forwarding rules if event.message.grouped_id: - logger.info(f'[用户] 收到媒体组消息 来自聊天: {source_chat.name} ({chat_id}) 组ID: {event.message.grouped_id}') + logger.info(f'[User] Received media group message from chat: {source_chat.name} ({chat_id}) Group ID: {event.message.grouped_id}') else: - logger.info(f'[用户] 收到新消息 来自聊天: {source_chat.name} ({chat_id}) 内容: {event.message.text}') - - # 添加日志:处理规则 - logger.info(f'找到 {len(rules)} 条转发规则') - - # 处理每条转发规则 + logger.info(f'[User] Received new message from chat: {source_chat.name} ({chat_id}) Content: {event.message.text}') + + # Add log: process rules + logger.info(f'Found {len(rules)} forwarding rules') + + # Process each forwarding rule for rule in rules: target_chat = rule.target_chat if not rule.enable_rule: - logger.info(f'规则 {rule.id} 未启用') + logger.info(f'Rule {rule.id} is not enabled') continue - logger.info(f'处理转发规则 ID: {rule.id} (从 {source_chat.name} 转发到: {target_chat.name})') + logger.info(f'Processing forwarding rule ID: {rule.id} (from {source_chat.name} to: {target_chat.name})') if rule.use_bot: - # 直接使用过滤器模块中的process_forward_rule函数 + # Directly use process_forward_rule function from filter module await process_forward_rule(bot_client, event, str(chat_id), rule) else: await user_handler.process_forward_rule(user_client, event, str(chat_id), rule) - + except Exception as e: - logger.error(f'处理用户消息时发生错误: {str(e)}') - logger.exception(e) # 添加详细的错误堆栈 + logger.error(f'Error processing user message: {str(e)}') + logger.exception(e) # Add detailed error stack trace finally: session.close() async def handle_bot_message(event, bot_client): - """处理机器人客户端收到的消息(命令)""" + """Handle messages (commands) received by bot client""" try: - - # logger.info("handle_bot_message:开始处理机器人消息") - + + # logger.info("handle_bot_message: Starting to process bot message") + chat = await event.get_chat() chat_id = abs(chat.id) - # logger.info(f"handle_bot_message:获取到聊天ID: {chat_id}") + # logger.info(f"handle_bot_message: Got chat ID: {chat_id}") - # 检查是否频道消息 + # Check if it's a channel message if isinstance(event.chat, types.Channel) and state_manager.check_state(): - # logger.info("handle_bot_message:检测到频道消息且存在状态") + # logger.info("handle_bot_message: Detected channel message with existing state") sender_id = os.getenv('USER_ID') - # 频道ID需要加上100前缀 + # Channel ID needs 100 prefix chat_id = int(f"100{chat_id}") - # logger.info(f"handle_bot_message:频道消息处理: sender_id={sender_id}, chat_id={chat_id}") + # logger.info(f"handle_bot_message: Channel message processing: sender_id={sender_id}, chat_id={chat_id}") else: sender_id = event.sender_id - # logger.info(f"handle_bot_message:非频道消息处理: sender_id={sender_id}") + # logger.info(f"handle_bot_message: Non-channel message processing: sender_id={sender_id}") - # 检查用户状态 + # Check user state current_state, message, state_type = state_manager.get_state(sender_id, chat_id) - # logger.info(f'handle_bot_message:当前是否有状态: {state_manager.check_state()}') - # logger.info(f"handle_bot_message:当前用户ID和聊天ID: {sender_id}, {chat_id}") - # logger.info(f"handle_bot_message:获取当前聊天窗口的用户状态: {current_state}") + # logger.info(f'handle_bot_message: Current state exists: {state_manager.check_state()}') + # logger.info(f"handle_bot_message: Current user ID and chat ID: {sender_id}, {chat_id}") + # logger.info(f"handle_bot_message: Got current chat window user state: {current_state}") + - - - # 处理提示词设置 + + # Handle prompt setting if current_state: await handle_prompt_setting(event, bot_client, sender_id, chat_id, current_state, message) return - # 如果没有特殊状态,则处理常规命令 + # If no special state, handle regular commands await bot_handler.handle_command(bot_client, event) except Exception as e: - logger.error(f'处理机器人命令时发生错误: {str(e)}') + logger.error(f'Error processing bot command: {str(e)}') logger.exception(e) -async def clear_group_cache(group_key, delay=300): # 5分钟后清除缓存 - """清除已处理的媒体组记录""" +async def clear_group_cache(group_key, delay=300): # Clear cache after 5 minutes + """Clear processed media group records""" await asyncio.sleep(delay) - PROCESSED_GROUPS.discard(group_key) - + PROCESSED_GROUPS.discard(group_key) diff --git a/models/db_operations.py b/models/db_operations.py index 5ed62da..e35778b 100644 --- a/models/db_operations.py +++ b/models/db_operations.py @@ -24,24 +24,24 @@ def __init__(self): @classmethod async def create(cls): - """创建DBOperations实例""" + """Create DBOperations instance""" instance = cls() await instance.init_ufb() return instance async def init_ufb(self): - """初始化UFB客户端""" + """Initialize UFB client""" try: - # 从环境变量获取UFB配置 - logger.info("初始化UFB客户端") + # Get UFB configuration from environment variables + logger.info("Initializing UFB client") is_ufb = os.getenv('UFB_ENABLED', 'false').lower() == 'true' if is_ufb: server_url = os.getenv('UFB_SERVER_URL', '') token = os.getenv('UFB_TOKEN') - logger.info(f"UFB配置: server_url={server_url}, token={token and '***'}") - + logger.info(f"UFB configuration: server_url={server_url}, token={token and '***'}") + if server_url and token: - # 处理URL + # Process URL if not server_url.startswith(('ws://', 'wss://')): if server_url.startswith('http://'): server_url = f"ws://{server_url[7:]}" @@ -49,57 +49,57 @@ async def init_ufb(self): server_url = f"wss://{server_url[8:]}" else: server_url = f"wss://{server_url}" - - logger.info(f"处理后的URL: {server_url}") + + logger.info(f"Processed URL: {server_url}") self.ufb_client = UFBClient() - logger.info("UFB客户端已创建") - + logger.info("UFB client created") + try: await self.ufb_client.start(server_url=server_url, token=token) - logger.info("UFB客户端已启动") + logger.info("UFB client started") except Exception as e: - logger.error(f"UFB客户端启动失败: {str(e)}") + logger.error(f"UFB client failed to start: {str(e)}") self.ufb_client = None else: - logger.warning("UFB配置不完整,未启用UFB功能") + logger.warning("UFB configuration is incomplete, UFB feature not enabled") self.ufb_client = None else: - logger.info("UFB未启用") + logger.info("UFB is not enabled") except Exception as e: - logger.error(f"初始化UFB时出错: {str(e)}") + logger.error(f"Error initializing UFB: {str(e)}") self.ufb_client = None - - + + async def sync_to_server(self,session,rule_id): - """同步UFB配置""" + """Sync UFB configuration""" if self.ufb_client and os.getenv('UFB_ENABLED').lower() == 'true': - # 通过rule_id获取规则ufb是否开启 + # Check if UFB is enabled for this rule via rule_id rule = session.query(ForwardRule).filter(ForwardRule.id == rule_id).first() ufb_domain = rule.ufb_domain if rule.is_ufb and ufb_domain: item = rule.ufb_item - # 获取规则的所有非正则表达关键字 + # Get all non-regex keywords for the rule normal_keywords = session.query(Keyword).filter( Keyword.rule_id == rule_id, Keyword.is_regex == False ).all() - - # 获取规则的所有正则表达关键字 + + # Get all regex keywords for the rule regex_keywords = session.query(Keyword).filter( Keyword.rule_id == rule_id, Keyword.is_regex == True ).all() - # 获取../ufb/config/config.json文件 + # Get ../ufb/config/config.json file config_file = Path(__file__).parent.parent / 'ufb' / 'config' / 'config.json' - # 读取文件 + # Read file with open(config_file, 'r', encoding='utf-8') as file: config = json.load(file) - # 在userConfig中找到对应domain的配置 + # Find the configuration for the corresponding domain in userConfig for user_config in config.get('userConfig', []): if user_config.get('domain') == ufb_domain: - # 根据item类型更新关键字 + # Update keywords based on item type if item == 'main': keywords_config = user_config.get('mainAndSubPageKeywords', {}) elif item == 'content': @@ -109,12 +109,12 @@ async def sync_to_server(self,session,rule_id): elif item == 'content_username': keywords_config = user_config.get('contentPageUserKeywords', {}) - # 更新关键字列表 + # Update keyword lists keywords_config['keywords'] = [k.keyword for k in normal_keywords] keywords_config['regexPatterns'] = [k.keyword for k in regex_keywords] - # 保存回对应的位置 - if item == 'main': + # Save back to corresponding location + if item == 'main': user_config['mainAndSubPageKeywords'] = keywords_config elif item == 'content': user_config['contentPageKeywords'] = keywords_config @@ -123,66 +123,66 @@ async def sync_to_server(self,session,rule_id): elif item == 'content_username': user_config['contentPageUserKeywords'] = keywords_config else: - logger.error(f"未设置UFB_ITEM环境变量") + logger.error(f"UFB_ITEM environment variable is not set") return break - - # 更新时间戳 + + # Update timestamp config['globalConfig']['SYNC_CONFIG']['lastSyncTime'] = int(time.time() * 1000) - # 保存到本地文件 + # Save to local file with open(config_file, 'w', encoding='utf-8') as file: json.dump(config, file, ensure_ascii=False, indent=2) - # 更新配置到服务器 + # Update configuration to server if self.ufb_client.is_connected: await self.ufb_client.websocket.send(json.dumps({ "additional_info": "to_server", "type": "update", **config })) - logger.info("UFB配置已同步") + logger.info("UFB configuration synced") else: - logger.warning("UFB客户端未连接,无法同步配置") + logger.warning("UFB client is not connected, unable to sync configuration") else: - logger.warning("UFB未开启,无法同步配置") + logger.warning("UFB is not enabled, unable to sync configuration") else: - logger.warning("UFB客户端未初始化,无法同步配置") + logger.warning("UFB client is not initialized, unable to sync configuration") async def sync_from_json(self, config): - """从收到的JSON配置同步关键字到数据库 - + """Sync keywords from received JSON configuration to database + Args: - config: 收到的配置数据 + config: Received configuration data """ - logger.info(f"从JSON同步关键字到数据库") + logger.info(f"Syncing keywords from JSON to database") session = get_session() try: - # 获取所有启用了UFB的规则 + # Get all rules with UFB enabled ufb_rules = session.query(ForwardRule).filter( ForwardRule.is_ufb == True, ForwardRule.ufb_domain != None ).all() - + if not ufb_rules: - logger.info("没有找到启用UFB的规则") + logger.info("No rules with UFB enabled found") return logger.info(f"ufb_rules: {ufb_rules}") - - # 遍历所有启用UFB的规则 + + # Iterate through all UFB-enabled rules for rule in ufb_rules: - # 获取item类型 + # Get item type item = rule.ufb_item logger.info(f"item: {item}") if not item: - logger.error("未设置UFB_ITEM环境变量") - continue # 跳过没有设置 item 的规则 - - # 在收到的配置中查找对应domain的配置 + logger.error("UFB_ITEM environment variable is not set") + continue # Skip rules without item set + + # Find the configuration for the corresponding domain in received config for user_config in config.get('userConfig', []): if user_config.get('domain') == rule.ufb_domain: - logger.info(f"找到匹配的domain配置: {rule.ufb_domain}") - - # 根据item类型获取关键字配置 + logger.info(f"Found matching domain configuration: {rule.ufb_domain}") + + # Get keyword configuration based on item type if item == 'main': keywords_config = user_config.get('mainAndSubPageKeywords', {}) elif item == 'content': @@ -192,15 +192,15 @@ async def sync_from_json(self, config): elif item == 'content_username': keywords_config = user_config.get('contentPageUserKeywords', {}) else: - logger.error(f"未设置UFB_ITEM环境变量") + logger.error(f"UFB_ITEM environment variable is not set") continue - - # 清空现有关键字 + + # Clear existing keywords session.query(Keyword).filter( Keyword.rule_id == rule.id ).delete() - - # 添加普通关键字 + + # Add normal keywords for keyword in keywords_config.get('keywords', []): new_keyword = Keyword( rule_id=rule.id, @@ -208,8 +208,8 @@ async def sync_from_json(self, config): is_regex=False ) session.add(new_keyword) - - # 添加正则关键字 + + # Add regex keywords for pattern in keywords_config.get('regexPatterns', []): new_keyword = Keyword( rule_id=rule.id, @@ -217,39 +217,39 @@ async def sync_from_json(self, config): is_regex=True ) session.add(new_keyword) - + session.commit() - logger.info(f"已从JSON同步关键字到规则 {rule.id} (domain: {rule.ufb_domain})") - break # 找到匹配的domain后跳出内层循环 + logger.info(f"Synced keywords from JSON to rule {rule.id} (domain: {rule.ufb_domain})") + break # Break inner loop after finding matching domain finally: session.close() async def add_keywords(self, session, rule_id, keywords, is_regex=False, is_blacklist=False): - """添加关键字到规则 + """Add keywords to rule Args: - session: 数据库会话 - rule_id: 规则ID - keywords: 关键字列表 - is_regex: 是否是正则表达式 - is_blacklist: 是否为黑名单关键字 + session: Database session + rule_id: Rule ID + keywords: Keyword list + is_regex: Whether it is a regular expression + is_blacklist: Whether it is a blacklist keyword Returns: - tuple: (成功数量, 重复数量) + tuple: (success count, duplicate count) """ success_count = 0 duplicate_count = 0 - # 获取当前规则 + # Get current rule rule = session.query(ForwardRule).get(rule_id) if not rule: - logger.error(f"规则ID {rule_id} 不存在") + logger.error(f"Rule ID {rule_id} does not exist") return 0, 0 - # 处理单个规则的关键字添加 + # Process keyword addition for single rule for keyword in keywords: try: - # 检查是否存在相同的关键字(考虑黑白名单) + # Check if the same keyword exists (considering blacklist/whitelist) existing_keyword = session.query(Keyword).filter( Keyword.rule_id == rule_id, Keyword.keyword == keyword, @@ -270,45 +270,45 @@ async def add_keywords(self, session, rule_id, keywords, is_regex=False, is_blac session.flush() success_count += 1 except Exception as e: - logger.error(f"添加关键字时出错: {str(e)}") + logger.error(f"Error adding keyword: {str(e)}") session.rollback() duplicate_count += 1 continue - # 检查是否启用了同步功能 + # Check if sync feature is enabled if rule.enable_sync: - logger.info(f"规则 {rule_id} 启用了同步功能,正在同步关键字到关联规则") - # 获取需要同步的规则列表 + logger.info(f"Rule {rule_id} has sync enabled, syncing keywords to associated rules") + # Get list of rules to sync sync_rules = session.query(RuleSync).filter(RuleSync.rule_id == rule_id).all() - - # 为每个同步规则添加相同的关键字 + + # Add same keywords to each sync 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 keywords 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 - - # 为同步目标规则添加关键字 + + # Add keywords to sync target rule sync_success = 0 sync_duplicate = 0 for keyword in keywords: try: - # 检查同步规则是否已有此关键字 + # Check if sync rule already has this keyword existing_keyword = session.query(Keyword).filter( Keyword.rule_id == sync_rule_id, Keyword.keyword == keyword, Keyword.is_blacklist == is_blacklist ).first() - + if existing_keyword: sync_duplicate += 1 continue - - # 添加新关键字到同步规则 + + # Add new keyword to sync rule new_keyword = Keyword( rule_id=sync_rule_id, keyword=keyword, @@ -319,23 +319,23 @@ async def add_keywords(self, session, rule_id, keywords, is_regex=False, is_blac session.flush() sync_success += 1 except Exception as e: - logger.error(f"同步关键字到规则 {sync_rule_id} 时出错: {str(e)}") + logger.error(f"Error syncing keyword to rule {sync_rule_id}: {str(e)}") continue - - logger.info(f"同步规则 {sync_rule_id} 的结果: 成功={sync_success}, 重复={sync_duplicate}") + + logger.info(f"Sync rule {sync_rule_id} result: success={sync_success}, duplicate={sync_duplicate}") await self.sync_to_server(session, rule_id) return success_count, duplicate_count async def get_keywords(self, session, rule_id, add_mode): - """获取规则的所有关键字 - + """Get all keywords for a rule + Args: - session: 数据库会话 - rule_id: 规则ID - + session: Database session + rule_id: Rule ID + Returns: - list: 关键字列表 + list: Keyword list """ return session.query(Keyword).filter( Keyword.rule_id == rule_id, @@ -343,31 +343,31 @@ async def get_keywords(self, session, rule_id, add_mode): ).all() async def delete_keywords(self, session, rule_id, indices): - """删除指定索引的关键字 - + """Delete keywords at specified indices + Args: - session: 数据库会话 - rule_id: 规则ID - indices: 要删除的索引列表(1-based) - + session: Database session + rule_id: Rule ID + indices: List of indices to delete (1-based) + Returns: - tuple: (删除数量, 剩余关键字列表) + tuple: (deleted count, remaining keyword list) """ - # 获取当前规则 + # Get current rule rule = session.query(ForwardRule).get(rule_id) if not rule: - logger.error(f"规则ID {rule_id} 不存在") + logger.error(f"Rule ID {rule_id} does not exist") return 0, [] - - # 获取当前规则的关键字 + + # Get keywords for current rule keywords = await self.get_keywords(session, rule_id, 'blacklist' if rule.add_mode == AddMode.BLACKLIST else 'whitelist') if not keywords: return 0, [] - + deleted_count = 0 max_id = len(keywords) - - # 保存要删除的关键字信息,用于后续同步 + + # Save keyword info to delete for subsequent sync keywords_to_delete = [] for idx in indices: if 1 <= idx <= max_id: @@ -379,88 +379,88 @@ async def delete_keywords(self, session, rule_id, indices): }) session.delete(keyword) deleted_count += 1 - - # 检查是否启用了同步功能 + + # Check if sync feature is enabled if rule.enable_sync and keywords_to_delete: - logger.info(f"规则 {rule_id} 启用了同步功能,正在同步删除关联规则的关键字") - # 获取需要同步的规则列表 + logger.info(f"Rule {rule_id} has sync enabled, syncing keyword deletion to associated rules") + # Get list of rules to sync sync_rules = session.query(RuleSync).filter(RuleSync.rule_id == rule_id).all() - - # 为每个同步规则删除相同的关键字 + + # Delete same keywords from each sync 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 keyword deletion 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 - - # 在同步目标规则中删除相同的关键字 + + # Delete same keywords from sync target rule sync_deleted = 0 for kw_info in keywords_to_delete: try: - # 查找目标规则中匹配的关键字 + # Find matching keywords in target rule target_keywords = session.query(Keyword).filter( Keyword.rule_id == sync_rule_id, Keyword.keyword == kw_info['keyword'], Keyword.is_regex == kw_info['is_regex'], Keyword.is_blacklist == kw_info['is_blacklist'] ).all() - - # 删除匹配的关键字 + + # Delete matching keywords for target_kw in target_keywords: session.delete(target_kw) sync_deleted += 1 except Exception as e: - logger.error(f"同步删除规则 {sync_rule_id} 的关键字时出错: {str(e)}") + logger.error(f"Error syncing keyword deletion to rule {sync_rule_id}: {str(e)}") continue - - logger.info(f"同步删除规则 {sync_rule_id} 的关键字: 删除了 {sync_deleted} 个") + + logger.info(f"Synced keyword deletion to rule {sync_rule_id}: deleted {sync_deleted} items") await self.sync_to_server(session, rule_id) return deleted_count, await self.get_keywords(session, rule_id, 'blacklist' if rule.add_mode == AddMode.BLACKLIST else 'whitelist') async def add_replace_rules(self, session, rule_id, patterns, contents=None): - """添加替换规则 - + """Add replace rules + Args: - session: 数据库会话 - rule_id: 规则ID - patterns: 匹配模式列表 - contents: 替换内容列表(可选) - + session: Database session + rule_id: Rule ID + patterns: Match pattern list + contents: Replacement content list (optional) + Returns: - tuple: (成功数量, 重复数量) + tuple: (success count, duplicate count) """ - # 获取当前规则 + # Get current rule rule = session.query(ForwardRule).get(rule_id) if not rule: - logger.error(f"规则ID {rule_id} 不存在") + logger.error(f"Rule ID {rule_id} does not exist") return 0, 0 - + success_count = 0 duplicate_count = 0 - + if contents is None: contents = [''] * len(patterns) - - # 添加替换规则到主规则 - added_rules = [] # 存储成功添加的规则,用于后续同步 + + # Add replace rules to main rule + added_rules = [] # Store successfully added rules for subsequent sync for pattern, content in zip(patterns, contents): try: - # 检查是否已存在相同的替换规则 + # Check if the same replace rule already exists existing_rule = session.query(ReplaceRule).filter( ReplaceRule.rule_id == rule_id, ReplaceRule.pattern == pattern, ReplaceRule.content == content ).first() - + if existing_rule: duplicate_count += 1 continue - + new_rule = ReplaceRule( rule_id=rule_id, pattern=pattern, @@ -474,41 +474,41 @@ async def add_replace_rules(self, session, rule_id, patterns, contents=None): session.rollback() duplicate_count += 1 continue - - # 检查是否启用了同步功能 + + # Check if sync feature is enabled if rule.enable_sync and added_rules: - logger.info(f"规则 {rule_id} 启用了同步功能,正在同步添加替换规则到关联规则") - # 获取需要同步的规则列表 + logger.info(f"Rule {rule_id} has sync enabled, syncing replace rules to associated rules") + # Get list of rules to sync sync_rules = session.query(RuleSync).filter(RuleSync.rule_id == rule_id).all() - - # 为每个同步规则添加相同的替换规则 + + # Add same replace rules to each sync 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 replace rules 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 - - # 为同步目标规则添加替换规则 + + # Add replace rules to sync target rule sync_success = 0 sync_duplicate = 0 for rule_info in added_rules: try: - # 检查同步规则是否已有此替换规则 + # Check if sync rule already has this replace rule existing_rule = session.query(ReplaceRule).filter( ReplaceRule.rule_id == sync_rule_id, ReplaceRule.pattern == rule_info['pattern'], ReplaceRule.content == rule_info['content'] ).first() - + if existing_rule: sync_duplicate += 1 continue - - # 添加新替换规则到同步规则 + + # Add new replace rule to sync rule new_rule = ReplaceRule( rule_id=sync_rule_id, pattern=rule_info['pattern'], @@ -518,52 +518,52 @@ async def add_replace_rules(self, session, rule_id, patterns, contents=None): session.flush() sync_success += 1 except Exception as e: - logger.error(f"同步添加替换规则到规则 {sync_rule_id} 时出错: {str(e)}") + logger.error(f"Error syncing replace rule to rule {sync_rule_id}: {str(e)}") continue - - logger.info(f"同步规则 {sync_rule_id} 的替换规则添加结果: 成功={sync_success}, 重复={sync_duplicate}") - + + logger.info(f"Sync rule {sync_rule_id} replace rule addition result: success={sync_success}, duplicate={sync_duplicate}") + return success_count, duplicate_count async def get_replace_rules(self, session, rule_id): - """获取规则的所有替换规则 - + """Get all replace rules for a rule + Args: - session: 数据库会话 - rule_id: 规则ID - + session: Database session + rule_id: Rule ID + Returns: - list: 替换规则列表 + list: Replace rule list """ return session.query(ReplaceRule).filter( ReplaceRule.rule_id == rule_id ).all() async def delete_replace_rules(self, session, rule_id, indices): - """删除指定索引的替换规则 - + """Delete replace rules at specified indices + Args: - session: 数据库会话 - rule_id: 规则ID - indices: 要删除的索引列表(1-based) - + session: Database session + rule_id: Rule ID + indices: List of indices to delete (1-based) + Returns: - tuple: (删除数量, 剩余替换规则列表) + tuple: (deleted count, remaining replace rule list) """ - # 获取当前规则 + # Get current rule rule = session.query(ForwardRule).get(rule_id) if not rule: - logger.error(f"规则ID {rule_id} 不存在") + logger.error(f"Rule ID {rule_id} does not exist") return 0, [] - + rules = await self.get_replace_rules(session, rule_id) if not rules: return 0, [] - + deleted_count = 0 max_id = len(rules) - - # 保存要删除的替换规则信息,用于后续同步 + + # Save replace rule info to delete for subsequent sync rules_to_delete = [] for idx in indices: if 1 <= idx <= max_id: @@ -574,57 +574,57 @@ async def delete_replace_rules(self, session, rule_id, indices): }) session.delete(replace_rule) deleted_count += 1 - - # 检查是否启用了同步功能 + + # Check if sync feature is enabled if rule.enable_sync and rules_to_delete: - logger.info(f"规则 {rule_id} 启用了同步功能,正在同步删除关联规则的替换规则") - # 获取需要同步的规则列表 + logger.info(f"Rule {rule_id} has sync enabled, syncing replace rule deletion to associated rules") + # Get list of rules to sync sync_rules = session.query(RuleSync).filter(RuleSync.rule_id == rule_id).all() - - # 为每个同步规则删除相同的替换规则 + + # Delete same replace rules from each sync 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 replace rule deletion 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 - - # 在同步目标规则中删除相同的替换规则 + + # Delete same replace rules from sync target rule sync_deleted = 0 for rule_info in rules_to_delete: try: - # 查找目标规则中匹配的替换规则 + # Find matching replace rules in target rule target_rules = session.query(ReplaceRule).filter( ReplaceRule.rule_id == sync_rule_id, ReplaceRule.pattern == rule_info['pattern'], ReplaceRule.content == rule_info['content'] ).all() - - # 删除匹配的替换规则 + + # Delete matching replace rules for target_rule in target_rules: session.delete(target_rule) sync_deleted += 1 except Exception as e: - logger.error(f"同步删除规则 {sync_rule_id} 的替换规则时出错: {str(e)}") + logger.error(f"Error syncing replace rule deletion to rule {sync_rule_id}: {str(e)}") continue - - logger.info(f"同步删除规则 {sync_rule_id} 的替换规则: 删除了 {sync_deleted} 个") - + + logger.info(f"Synced replace rule deletion to rule {sync_rule_id}: deleted {sync_deleted} items") + return deleted_count, await self.get_replace_rules(session, rule_id) async def get_media_types(self, session, rule_id): - """获取媒体类型设置""" + """Get media type settings""" try: rule = session.query(ForwardRule).get(rule_id) if not rule: - return False, "规则不存在", None - + return False, "Rule does not exist", None + media_types = session.query(MediaTypes).filter_by(rule_id=rule_id).first() if not media_types: - # 如果不存在则创建默认设置 + # Create default settings if not exist media_types = MediaTypes( rule_id=rule_id, photo=False, @@ -635,181 +635,181 @@ async def get_media_types(self, session, rule_id): ) session.add(media_types) session.commit() - - return True, "获取媒体类型设置成功", media_types + + return True, "Successfully retrieved media type settings", media_types except Exception as e: - logger.error(f"获取媒体类型设置时出错: {str(e)}") + logger.error(f"Error getting media type settings: {str(e)}") session.rollback() - return False, f"获取媒体类型设置时出错: {str(e)}", None + return False, f"Error getting media type settings: {str(e)}", None async def update_media_types(self, session, rule_id, media_types_dict): - """更新媒体类型设置""" + """Update media type settings""" try: rule = session.query(ForwardRule).get(rule_id) if not rule: - return False, "规则不存在" - + return False, "Rule does not exist" + media_types = session.query(MediaTypes).filter_by(rule_id=rule_id).first() if not media_types: media_types = MediaTypes(rule_id=rule_id) session.add(media_types) - - # 更新媒体类型设置 + + # Update media type settings for field in ['photo', 'document', 'video', 'audio', 'voice']: if field in media_types_dict: setattr(media_types, field, media_types_dict[field]) - + session.commit() - return True, "更新媒体类型设置成功" + return True, "Successfully updated media type settings" except Exception as e: - logger.error(f"更新媒体类型设置时出错: {str(e)}") + logger.error(f"Error updating media type settings: {str(e)}") session.rollback() - return False, f"更新媒体类型设置时出错: {str(e)}" + return False, f"Error updating media type settings: {str(e)}" async def toggle_media_type(self, session, rule_id, media_type): - """切换特定媒体类型的启用状态""" + """Toggle the enabled state of a specific media type""" try: if media_type not in ['photo', 'document', 'video', 'audio', 'voice']: - return False, f"无效的媒体类型: {media_type}" - + return False, f"Invalid media type: {media_type}" + success, msg, media_types = await self.get_media_types(session, rule_id) if not success: return False, msg - - # 切换状态 + + # Toggle state current_value = getattr(media_types, media_type) setattr(media_types, media_type, not current_value) - + session.commit() - return True, f"媒体类型 {media_type} 切换为 {not current_value}" + return True, f"Media type {media_type} toggled to {not current_value}" except Exception as e: - logger.error(f"切换媒体类型时出错: {str(e)}") + logger.error(f"Error toggling media type: {str(e)}") session.rollback() - return False, f"切换媒体类型时出错: {str(e)}" + return False, f"Error toggling media type: {str(e)}" async def add_media_extensions(self, session, rule_id, extensions): - """添加媒体扩展名 - + """Add media extensions + Args: - session: 数据库会话 - rule_id: 规则ID - extensions: 扩展名列表,比如 ['jpg', 'png', 'pdf'] - + session: Database session + rule_id: Rule ID + extensions: Extension list, e.g. ['jpg', 'png', 'pdf'] + Returns: - (bool, str): 成功状态和消息 + (bool, str): Success status and message """ try: added_count = 0 for ext in extensions: - # 确保扩展名不带点,去除可能存在的点 + # Ensure extension has no dot, strip any existing dot ext = ext.lstrip('.') - - # 检查是否已存在相同的扩展名 + + # Check if the same extension already exists existing = session.execute( text("SELECT id FROM media_extensions WHERE rule_id = :rule_id AND extension = :extension"), {"rule_id": rule_id, "extension": ext} ) - + if existing.first() is None: - # 添加新的扩展名 + # Add new extension new_extension = MediaExtensions(rule_id=rule_id, extension=ext) session.add(new_extension) added_count += 1 - + if added_count > 0: session.commit() - return True, f"成功添加 {added_count} 个媒体扩展名" + return True, f"Successfully added {added_count} media extensions" else: - return False, "所有扩展名已存在,未添加任何新扩展名" - + return False, "All extensions already exist, no new extensions added" + except Exception as e: session.rollback() - logger.error(f"添加媒体扩展名失败: {str(e)}") - return False, f"添加媒体扩展名失败: {str(e)}" + logger.error(f"Failed to add media extensions: {str(e)}") + return False, f"Failed to add media extensions: {str(e)}" async def get_media_extensions(self, session, rule_id): - """获取规则的媒体扩展名列表 - + """Get media extension list for a rule + Args: - session: 数据库会话 - rule_id: 规则ID - + session: Database session + rule_id: Rule ID + Returns: - list: 媒体扩展名对象列表 + list: Media extension object list """ try: - # 使用SQLAlchemy文本SQL查询,不需要await + # Use SQLAlchemy text SQL query, no need for await result = session.execute( text("SELECT id, extension FROM media_extensions WHERE rule_id = :rule_id ORDER BY id"), {"rule_id": rule_id} ) - - # 构建返回结果 + + # Build return result extensions = [] for row in result: extensions.append({ "id": row[0], "extension": row[1] }) - - # 返回扩展名列表 + + # Return extension list return extensions - + except Exception as e: - # 记录错误并返回空列表 - logger.error(f"获取媒体扩展名失败: {str(e)}") + # Log error and return empty list + logger.error(f"Failed to get media extensions: {str(e)}") return [] async def delete_media_extensions(self, session, rule_id, indices): - """删除媒体扩展名 - + """Delete media extensions + Args: - session: 数据库会话 - rule_id: 规则ID - indices: 要删除的扩展名ID列表 - + session: Database session + rule_id: Rule ID + indices: List of extension IDs to delete + Returns: - (bool, str): 成功状态和消息 + (bool, str): Success status and message """ try: if not indices: - return False, "未指定要删除的扩展名" - + return False, "No extensions specified for deletion" + for index in indices: - # 查找并删除扩展名 + # Find and delete extension result = session.execute( text("SELECT id FROM media_extensions WHERE id = :id AND rule_id = :rule_id"), {"id": index, "rule_id": rule_id} ) - + extension = result.first() if extension: session.execute( text("DELETE FROM media_extensions WHERE id = :id"), {"id": extension[0]} ) - + session.commit() - return True, f"成功删除 {len(indices)} 个媒体扩展名" + return True, f"Successfully deleted {len(indices)} media extensions" except Exception as e: session.rollback() - logger.error(f"删除媒体扩展名失败: {str(e)}") - return False, f"删除媒体扩展名失败: {str(e)}" + logger.error(f"Failed to delete media extensions: {str(e)}") + return False, f"Failed to delete media extensions: {str(e)}" - # RSS配置相关操作 + # RSS configuration related operations async def get_rss_config(self, session, rule_id): - """获取指定规则的RSS配置""" + """Get RSS configuration for specified rule""" return session.query(RSSConfig).filter(RSSConfig.rule_id == rule_id).first() async def create_rss_config(self, session, rule_id, **kwargs): - """创建RSS配置""" + """Create RSS configuration""" rss_config = RSSConfig(rule_id=rule_id, **kwargs) session.add(rss_config) session.commit() return rss_config async def update_rss_config(self, session, rule_id, **kwargs): - """更新RSS配置""" + """Update RSS configuration""" rss_config = await self.get_rss_config(session, rule_id) if rss_config: for key, value in kwargs.items(): @@ -818,7 +818,7 @@ async def update_rss_config(self, session, rule_id, **kwargs): return rss_config async def delete_rss_config(self, session, rule_id): - """删除RSS配置""" + """Delete RSS configuration""" rss_config = await self.get_rss_config(session, rule_id) if rss_config: session.delete(rss_config) @@ -826,18 +826,18 @@ async def delete_rss_config(self, session, rule_id): return True return False - # RSS模式相关操作 + # RSS pattern related operations async def get_rss_patterns(self, session, rss_config_id): - """获取指定RSS配置的所有模式""" + """Get all patterns for specified RSS configuration""" return session.query(RSSPattern).filter(RSSPattern.rss_config_id == rss_config_id).order_by(RSSPattern.priority).all() async def get_rss_pattern(self, session, pattern_id): - """获取指定的RSS模式""" + """Get specified RSS pattern""" return session.query(RSSPattern).filter(RSSPattern.id == pattern_id).first() async def create_rss_pattern(self, session, rss_config_id, pattern, pattern_type, priority=0): - """创建RSS模式""" - logger.info(f"创建RSS模式:config_id={rss_config_id}, pattern={pattern}, type={pattern_type}, priority={priority}") + """Create RSS pattern""" + logger.info(f"Creating RSS pattern: config_id={rss_config_id}, pattern={pattern}, type={pattern_type}, priority={priority}") try: pattern_obj = RSSPattern( rss_config_id=rss_config_id, @@ -847,35 +847,35 @@ async def create_rss_pattern(self, session, rss_config_id, pattern, pattern_type ) session.add(pattern_obj) session.commit() - logger.info(f"RSS模式创建成功:{pattern_obj.id}") + logger.info(f"RSS pattern created successfully: {pattern_obj.id}") return pattern_obj except Exception as e: - logger.error(f"创建RSS模式失败:{str(e)}") + logger.error(f"Failed to create RSS pattern: {str(e)}") session.rollback() raise async def update_rss_pattern(self, session, pattern_id, **kwargs): - """更新RSS模式""" - logger.info(f"更新RSS模式:pattern_id={pattern_id}, kwargs={kwargs}") + """Update RSS pattern""" + logger.info(f"Updating RSS pattern: pattern_id={pattern_id}, kwargs={kwargs}") try: pattern = session.query(RSSPattern).filter(RSSPattern.id == pattern_id).first() if not pattern: - logger.error(f"RSS模式不存在:pattern_id={pattern_id}") - raise ValueError("RSS模式不存在") - + logger.error(f"RSS pattern does not exist: pattern_id={pattern_id}") + raise ValueError("RSS pattern does not exist") + for key, value in kwargs.items(): setattr(pattern, key, value) - + session.commit() - logger.info(f"RSS模式更新成功:{pattern.id}") + logger.info(f"RSS pattern updated successfully: {pattern.id}") return pattern except Exception as e: - logger.error(f"更新RSS模式失败:{str(e)}") + logger.error(f"Failed to update RSS pattern: {str(e)}") session.rollback() raise async def delete_rss_pattern(self, session, pattern_id): - """删除RSS模式""" + """Delete RSS pattern""" rss_pattern = await self.get_rss_pattern(session, pattern_id) if rss_pattern: session.delete(rss_pattern) @@ -884,27 +884,27 @@ async def delete_rss_pattern(self, session, pattern_id): return False async def reorder_rss_patterns(self, session, rss_config_id, pattern_ids): - """重新排序RSS模式""" + """Reorder RSS patterns""" patterns = await self.get_rss_patterns(session, rss_config_id) pattern_dict = {p.id: p for p in patterns} - + for index, pattern_id in enumerate(pattern_ids): if pattern_id in pattern_dict: pattern_dict[pattern_id].priority = index - + session.commit() - # 用户相关操作 + # User related operations async def get_user(self, session, username): - """通过用户名获取用户""" + """Get user by username""" return session.query(User).filter(User.username == username).first() async def get_user_by_id(self, session, user_id): - """通过ID获取用户""" + """Get user by ID""" return session.query(User).filter(User.id == user_id).first() async def create_user(self, session, username, password): - """创建用户""" + """Create user""" user = User( username=username, @@ -915,7 +915,7 @@ async def create_user(self, session, username, password): return user async def update_user_password(self, session, username, new_password): - """更新用户密码""" + """Update user password""" user = await self.get_user(session, username) if user: @@ -924,259 +924,259 @@ async def update_user_password(self, session, username, new_password): return user async def verify_user(self, session, username, password): - """验证用户密码""" - + """Verify user password""" + user = await self.get_user(session, username) if user and check_password_hash(user.password, password): return user return None - # 批量操作 + # Batch operations async def get_all_enabled_rss_configs(self, session): - """获取所有启用的RSS配置""" + """Get all enabled RSS configurations""" return session.query(RSSConfig).filter(RSSConfig.enable_rss == True).all() async def get_rss_config_with_patterns(self, session, rule_id): - """获取RSS配置及其所有模式""" + """Get RSS configuration with all its patterns""" return session.query(RSSConfig).options( joinedload(RSSConfig.patterns) - ).filter(RSSConfig.rule_id == rule_id).first() + ).filter(RSSConfig.rule_id == rule_id).first() - # 规则同步相关操作 + # Rule sync related operations async def add_rule_sync(self, session, rule_id, sync_rule_id): - """添加规则同步关系 - + """Add rule sync relationship + Args: - session: 数据库会话 - rule_id: 源规则ID - sync_rule_id: 目标规则ID(要同步到的规则) - + session: Database session + rule_id: Source rule ID + sync_rule_id: Target rule ID (rule to sync to) + Returns: - tuple: (bool, str) - (成功状态, 消息) + tuple: (bool, str) - (success status, message) """ try: - # 检查源规则是否存在 + # Check if source rule exists source_rule = session.query(ForwardRule).get(rule_id) if not source_rule: - return False, f"源规则ID {rule_id} 不存在" - - # 检查目标规则是否存在 + return False, f"Source rule ID {rule_id} does not exist" + + # Check if target rule exists target_rule = session.query(ForwardRule).get(sync_rule_id) if not target_rule: - return False, f"目标规则ID {sync_rule_id} 不存在" - - # 检查是否已存在相同的同步关系 + return False, f"Target rule ID {sync_rule_id} does not exist" + + # Check if the same sync relationship already exists existing_sync = session.query(RuleSync).filter( RuleSync.rule_id == rule_id, RuleSync.sync_rule_id == sync_rule_id ).first() - + if existing_sync: - return False, f"同步关系已存在" - - # 创建新的同步关系 + return False, f"Sync relationship already exists" + + # Create new sync relationship new_sync = RuleSync( rule_id=rule_id, sync_rule_id=sync_rule_id ) - - # 启用规则的同步功能 + + # Enable sync feature for the rule source_rule.enable_sync = True - + session.add(new_sync) session.commit() - - logger.info(f"已添加规则同步: 从规则 {rule_id} 到规则 {sync_rule_id}") - return True, f"成功添加同步关系" - + + logger.info(f"Added rule sync: from rule {rule_id} to rule {sync_rule_id}") + return True, f"Successfully added sync relationship" + except Exception as e: session.rollback() - logger.error(f"添加规则同步关系时出错: {str(e)}") - return False, f"添加同步关系失败: {str(e)}" - + logger.error(f"Error adding rule sync relationship: {str(e)}") + return False, f"Failed to add sync relationship: {str(e)}" + async def get_rule_syncs(self, session, rule_id): - """获取指定规则的同步关系列表 - + """Get sync relationship list for specified rule + Args: - session: 数据库会话 - rule_id: 规则ID - + session: Database session + rule_id: Rule ID + Returns: - list: 同步关系列表 + list: Sync relationship list """ try: - # 获取该规则的所有同步目标 + # Get all sync targets for this rule syncs = session.query(RuleSync).filter( RuleSync.rule_id == rule_id ).all() - + return syncs - + except Exception as e: - logger.error(f"获取规则同步关系时出错: {str(e)}") + logger.error(f"Error getting rule sync relationships: {str(e)}") return [] - + async def delete_rule_sync(self, session, rule_id, sync_rule_id): - """删除规则同步关系 - + """Delete rule sync relationship + Args: - session: 数据库会话 - rule_id: 源规则ID - sync_rule_id: 目标规则ID - + session: Database session + rule_id: Source rule ID + sync_rule_id: Target rule ID + Returns: - tuple: (bool, str) - (成功状态, 消息) + tuple: (bool, str) - (success status, message) """ try: - # 查找同步关系 + # Find sync relationship sync = session.query(RuleSync).filter( RuleSync.rule_id == rule_id, RuleSync.sync_rule_id == sync_rule_id ).first() - + if not sync: - return False, "指定的同步关系不存在" - - # 删除同步关系 + return False, "Specified sync relationship does not exist" + + # Delete sync relationship session.delete(sync) - - # 检查规则是否还有其他同步关系 + + # Check if the rule has other sync relationships remaining_syncs = session.query(RuleSync).filter( RuleSync.rule_id == rule_id ).count() - - # 如果没有其他同步关系,关闭规则的同步功能 + + # If no other sync relationships, disable sync feature for the rule if remaining_syncs == 0: rule = session.query(ForwardRule).get(rule_id) if rule: rule.enable_sync = False - + session.commit() - - logger.info(f"已删除规则同步: 从规则 {rule_id} 到规则 {sync_rule_id}") - return True, "成功删除同步关系" - + + logger.info(f"Deleted rule sync: from rule {rule_id} to rule {sync_rule_id}") + return True, "Successfully deleted sync relationship" + except Exception as e: session.rollback() - logger.error(f"删除规则同步关系时出错: {str(e)}") - return False, f"删除同步关系失败: {str(e)}" + logger.error(f"Error deleting rule sync relationship: {str(e)}") + return False, f"Failed to delete sync relationship: {str(e)}" async def get_push_configs(self, session, rule_id): - """获取指定规则的所有推送配置 - + """Get all push configurations for specified rule + Args: - session: 数据库会话 - rule_id: 规则ID - + session: Database session + rule_id: Rule ID + Returns: - list: 推送配置列表 + list: Push configuration list """ try: return session.query(PushConfig).filter( PushConfig.rule_id == rule_id ).all() except Exception as e: - logger.error(f"获取推送配置时出错: {str(e)}") + logger.error(f"Error getting push configurations: {str(e)}") return [] - + async def add_push_config(self, session, rule_id, push_channel, enable_push_channel=True): - """添加推送配置 - + """Add push configuration + Args: - session: 数据库会话 - rule_id: 规则ID - push_channel: 推送频道 - enable_push_channel: 是否启用推送频道 - + session: Database session + rule_id: Rule ID + push_channel: Push channel + enable_push_channel: Whether to enable push channel + Returns: - tuple: (bool, str, obj) - (成功状态, 消息, 创建的对象) + tuple: (bool, str, obj) - (success status, message, created object) """ try: - # 检查规则是否存在 + # Check if rule exists rule = session.query(ForwardRule).get(rule_id) if not rule: - return False, f"规则ID {rule_id} 不存在", None - - # 创建新的推送配置 + return False, f"Rule ID {rule_id} does not exist", None + + # Create new push configuration push_config = PushConfig( rule_id=rule_id, push_channel=push_channel, enable_push_channel=enable_push_channel ) - + session.add(push_config) session.commit() - - # 启用规则的推送功能 + + # Enable push feature for the rule rule.enable_push = True session.commit() - - return True, "成功添加推送配置", push_config + + return True, "Successfully added push configuration", push_config except Exception as e: session.rollback() - logger.error(f"添加推送配置时出错: {str(e)}") - return False, f"添加推送配置失败: {str(e)}", None - + logger.error(f"Error adding push configuration: {str(e)}") + return False, f"Failed to add push configuration: {str(e)}", None + async def toggle_push_config(self, session, config_id): - """切换推送配置的启用状态 - + """Toggle the enabled state of a push configuration + Args: - session: 数据库会话 - config_id: 配置ID - + session: Database session + config_id: Configuration ID + Returns: - tuple: (bool, str) - (成功状态, 消息) + tuple: (bool, str) - (success status, message) """ try: push_config = session.query(PushConfig).get(config_id) if not push_config: - return False, "推送配置不存在" - - # 切换启用状态 + return False, "Push configuration does not exist" + + # Toggle enabled state push_config.enable_push_channel = not push_config.enable_push_channel session.commit() - - return True, f"推送配置已{'启用' if push_config.enable_push_channel else '禁用'}" + + return True, f"Push configuration has been {'enabled' if push_config.enable_push_channel else 'disabled'}" except Exception as e: session.rollback() - logger.error(f"切换推送配置状态时出错: {str(e)}") - return False, f"切换推送配置状态失败: {str(e)}" - + logger.error(f"Error toggling push configuration state: {str(e)}") + return False, f"Failed to toggle push configuration state: {str(e)}" + async def delete_push_config(self, session, config_id): - """删除推送配置 - + """Delete push configuration + Args: - session: 数据库会话 - config_id: 配置ID - + session: Database session + config_id: Configuration ID + Returns: - tuple: (bool, str) - (成功状态, 消息) + tuple: (bool, str) - (success status, message) """ try: push_config = session.query(PushConfig).get(config_id) if not push_config: - return False, "推送配置不存在" - + return False, "Push configuration does not exist" + rule_id = push_config.rule_id - - # 删除配置 + + # Delete configuration session.delete(push_config) - - # 检查是否还有其他推送配置 + + # Check if there are other push configurations remaining_configs = session.query(PushConfig).filter( PushConfig.rule_id == rule_id ).count() - - # 如果没有其他推送配置,关闭规则的推送功能 + + # If no other push configurations, disable push feature for the rule if remaining_configs == 0: rule = session.query(ForwardRule).get(rule_id) if rule: rule.enable_push = False - + session.commit() - - return True, "成功删除推送配置" + + return True, "Successfully deleted push configuration" except Exception as e: session.rollback() - logger.error(f"删除推送配置时出错: {str(e)}") - return False, f"删除推送配置失败: {str(e)}" + logger.error(f"Error deleting push configuration: {str(e)}") + return False, f"Failed to delete push configuration: {str(e)}" diff --git a/models/models.py b/models/models.py index eeb1260..b53825a 100644 --- a/models/models.py +++ b/models/models.py @@ -17,7 +17,7 @@ class Chat(Base): name = Column(String, nullable=True) current_add_id = Column(String, nullable=True) - # 关系 + # Relationships source_rules = relationship('ForwardRule', foreign_keys='ForwardRule.source_chat_id', back_populates='source_chat') target_rules = relationship('ForwardRule', foreign_keys='ForwardRule.target_chat_id', back_populates='target_chat') @@ -31,58 +31,58 @@ class ForwardRule(Base): use_bot = Column(Boolean, default=True) message_mode = Column(Enum(MessageMode), nullable=False, default=MessageMode.MARKDOWN) is_replace = Column(Boolean, default=False) - is_preview = Column(Enum(PreviewMode), nullable=False, default=PreviewMode.FOLLOW) # 三个值,开,关,按照原消息 - is_original_link = Column(Boolean, default=False) # 是否附带原消息链接 + is_preview = Column(Enum(PreviewMode), nullable=False, default=PreviewMode.FOLLOW) # Three values: on, off, follow original message + is_original_link = Column(Boolean, default=False) # Whether to include original message link is_ufb = Column(Boolean, default=False) ufb_domain = Column(String, nullable=True) ufb_item = Column(String, nullable=True,default='main') - is_delete_original = Column(Boolean, default=False) # 是否删除原始消息 - is_original_sender = Column(Boolean, default=False) # 是否附带原始消息发送人名称 - userinfo_template = Column(String, default='**{name}**', nullable=True) # 用户信息模板 - time_template = Column(String, default='{time}', nullable=True) # 时间模板 - original_link_template = Column(String, default='原始连接:{original_link}', nullable=True) # 原始链接模板 - is_original_time = Column(Boolean, default=False) # 是否附带原始消息发送时间 - add_mode = Column(Enum(AddMode), nullable=False, default=AddMode.BLACKLIST) # 添加模式,默认黑名单 - enable_rule = Column(Boolean, default=True) # 是否启用规则 - is_filter_user_info = Column(Boolean, default=False) # 是否过滤用户信息 - handle_mode = Column(Enum(HandleMode), nullable=False, default=HandleMode.FORWARD) # 处理模式,编辑模式和转发模式,默认转发 - enable_comment_button = Column(Boolean, default=False) # 是否添加对应消息的评论区直达按钮 - enable_media_type_filter = Column(Boolean, default=False) # 是否启用媒体类型过滤 - enable_media_size_filter = Column(Boolean, default=False) # 是否启用媒体大小过滤 - max_media_size = Column(Integer, default=os.getenv('DEFAULT_MAX_MEDIA_SIZE', 10)) # 媒体大小限制,单位MB - is_send_over_media_size_message = Column(Boolean, default=True) # 超过限制的媒体是否发送提示消息 - enable_extension_filter = Column(Boolean, default=False) # 是否启用媒体扩展名过滤 - extension_filter_mode = Column(Enum(AddMode), nullable=False, default=AddMode.BLACKLIST) # 媒体扩展名过滤模式,默认黑名单 - enable_reverse_blacklist = Column(Boolean, default=False) # 是否反转黑名单 - enable_reverse_whitelist = Column(Boolean, default=False) # 是否反转白名单 - media_allow_text = Column(Boolean, default=False) # 是否放行文本 - # 推送相关字段 - enable_push = Column(Boolean, default=False) # 是否启用推送 - enable_only_push = Column(Boolean, default=False) # 是否只转发到推送配置 - - # AI相关字段 - is_ai = Column(Boolean, default=False) # 是否启用AI处理 - ai_model = Column(String, nullable=True) # 使用的AI模型 - ai_prompt = Column(String, nullable=True) # AI处理的prompt - enable_ai_upload_image = Column(Boolean, default=False) # 是否启用AI图片上传功能 - is_summary = Column(Boolean, default=False) # 是否启用AI总结 + is_delete_original = Column(Boolean, default=False) # Whether to delete original message + is_original_sender = Column(Boolean, default=False) # Whether to include original message sender name + userinfo_template = Column(String, default='**{name}**', nullable=True) # User info template + time_template = Column(String, default='{time}', nullable=True) # Time template + original_link_template = Column(String, default='Original link: {original_link}', nullable=True) # Original link template + is_original_time = Column(Boolean, default=False) # Whether to include original message send time + add_mode = Column(Enum(AddMode), nullable=False, default=AddMode.BLACKLIST) # Add mode, default blacklist + enable_rule = Column(Boolean, default=True) # Whether to enable rule + is_filter_user_info = Column(Boolean, default=False) # Whether to filter user info + handle_mode = Column(Enum(HandleMode), nullable=False, default=HandleMode.FORWARD) # Handle mode, edit mode and forward mode, default forward + enable_comment_button = Column(Boolean, default=False) # Whether to add comment section shortcut button for corresponding message + enable_media_type_filter = Column(Boolean, default=False) # Whether to enable media type filter + enable_media_size_filter = Column(Boolean, default=False) # Whether to enable media size filter + max_media_size = Column(Integer, default=os.getenv('DEFAULT_MAX_MEDIA_SIZE', 10)) # Media size limit, in MB + is_send_over_media_size_message = Column(Boolean, default=True) # Whether to send notification for media exceeding size limit + enable_extension_filter = Column(Boolean, default=False) # Whether to enable media extension filter + extension_filter_mode = Column(Enum(AddMode), nullable=False, default=AddMode.BLACKLIST) # Media extension filter mode, default blacklist + enable_reverse_blacklist = Column(Boolean, default=False) # Whether to reverse blacklist + enable_reverse_whitelist = Column(Boolean, default=False) # Whether to reverse whitelist + media_allow_text = Column(Boolean, default=False) # Whether to allow text through + # Push related fields + enable_push = Column(Boolean, default=False) # Whether to enable push + enable_only_push = Column(Boolean, default=False) # Whether to only forward to push configuration + + # AI related fields + is_ai = Column(Boolean, default=False) # Whether to enable AI processing + ai_model = Column(String, nullable=True) # AI model in use + ai_prompt = Column(String, nullable=True) # AI processing prompt + enable_ai_upload_image = Column(Boolean, default=False) # Whether to enable AI image upload feature + is_summary = Column(Boolean, default=False) # Whether to enable AI summary summary_time = Column(String(5), default=os.getenv('DEFAULT_SUMMARY_TIME', '07:00')) - summary_prompt = Column(String, nullable=True) # AI总结的prompt - is_keyword_after_ai = Column(Boolean, default=False) # AI处理后是否再次执行关键字过滤 - is_top_summary = Column(Boolean, default=True) # 是否顶置总结消息 - enable_delay = Column(Boolean, default=False) # 是否启用延迟处理 - delay_seconds = Column(Integer, default=5) # 延迟处理秒数 - # RSS相关字段 - only_rss = Column(Boolean, default=False) # 是否只转发RSS - # 同步功能相关 - enable_sync = Column(Boolean, default=False) # 是否启用规则同步功能 - - # 添加唯一约束 + summary_prompt = Column(String, nullable=True) # AI summary prompt + is_keyword_after_ai = Column(Boolean, default=False) # Whether to execute keyword filtering again after AI processing + is_top_summary = Column(Boolean, default=True) # Whether to pin summary message + enable_delay = Column(Boolean, default=False) # Whether to enable delayed processing + delay_seconds = Column(Integer, default=5) # Delay processing seconds + # RSS related fields + only_rss = Column(Boolean, default=False) # Whether to only forward RSS + # Sync feature related + enable_sync = Column(Boolean, default=False) # Whether to enable rule sync feature + + # Add unique constraint __table_args__ = ( UniqueConstraint('source_chat_id', 'target_chat_id', name='unique_source_target'), ) - # 关系 + # Relationships source_chat = relationship('Chat', foreign_keys=[source_chat_id], back_populates='source_rules') target_chat = relationship('Chat', foreign_keys=[target_chat_id], back_populates='target_rules') keywords = relationship('Keyword', back_populates='rule') @@ -102,10 +102,10 @@ class Keyword(Base): is_regex = Column(Boolean, default=False) is_blacklist = Column(Boolean, default=True) - # 关系 + # Relationships rule = relationship('ForwardRule', back_populates='keywords') - # 添加唯一约束 + # Add unique constraint __table_args__ = ( UniqueConstraint('rule_id', 'keyword','is_regex','is_blacklist', name='unique_rule_keyword_is_regex_is_blacklist'), ) @@ -115,13 +115,13 @@ class ReplaceRule(Base): id = Column(Integer, primary_key=True) rule_id = Column(Integer, ForeignKey('forward_rules.id'), nullable=False) - pattern = Column(String, nullable=False) # 替换模式 - content = Column(String, nullable=True) # 替换内容 + pattern = Column(String, nullable=False) # Replace pattern + content = Column(String, nullable=True) # Replace content - # 关系 + # Relationships rule = relationship('ForwardRule', back_populates='replace_rules') - # 添加唯一约束 + # Add unique constraint __table_args__ = ( UniqueConstraint('rule_id', 'pattern', 'content', name='unique_rule_pattern_content'), ) @@ -137,7 +137,7 @@ class MediaTypes(Base): audio = Column(Boolean, default=False) voice = Column(Boolean, default=False) - # 关系 + # Relationships rule = relationship('ForwardRule', back_populates='media_types') @@ -146,12 +146,12 @@ class MediaExtensions(Base): id = Column(Integer, primary_key=True) rule_id = Column(Integer, ForeignKey('forward_rules.id'), nullable=False) - extension = Column(String, nullable=False) # 存储不带点的扩展名,如 "jpg", "pdf" + extension = Column(String, nullable=False) # Store extension without dot, e.g. "jpg", "pdf" - # 关系 + # Relationships rule = relationship('ForwardRule', back_populates='media_extensions') - # 添加唯一约束 + # Add unique constraint __table_args__ = ( UniqueConstraint('rule_id', 'extension', name='unique_rule_extension'), ) @@ -163,7 +163,7 @@ class RuleSync(Base): rule_id = Column(Integer, ForeignKey('forward_rules.id'), nullable=False) sync_rule_id = Column(Integer, nullable=False) - # 关系 + # Relationships rule = relationship('ForwardRule', back_populates='rule_syncs') class PushConfig(Base): @@ -173,10 +173,10 @@ class PushConfig(Base): rule_id = Column(Integer, ForeignKey('forward_rules.id'), nullable=False) enable_push_channel = Column(Boolean, default=False) push_channel = Column(String, nullable=False) - #媒体发送方式,一次一张Single还是多张Multiple + # Media send mode, one at a time Single or multiple Multiple media_send_mode = Column(String, nullable=False, default='Single') - # 关系 + # Relationships rule = relationship('ForwardRule', back_populates='push_config') class RSSConfig(Base): @@ -184,24 +184,24 @@ class RSSConfig(Base): id = Column(Integer, primary_key=True) rule_id = Column(Integer, ForeignKey('forward_rules.id'), nullable=False, unique=True) - enable_rss = Column(Boolean, default=False) # 是否启用RSS - rule_title = Column(String, nullable=True) # RSS feed 标题 - rule_description = Column(String, nullable=True) # RSS feed 描述 - language = Column(String, default='zh-CN') # RSS feed 语言 - max_items = Column(Integer, default=50) # RSS feed 最大条目数 - # 是否启用自动提取标题和内容 + enable_rss = Column(Boolean, default=False) # Whether to enable RSS + rule_title = Column(String, nullable=True) # RSS feed title + rule_description = Column(String, nullable=True) # RSS feed description + language = Column(String, default='zh-CN') # RSS feed language + max_items = Column(Integer, default=50) # RSS feed maximum items + # Whether to enable auto title and content extraction is_auto_title = Column(Boolean, default=False) is_auto_content = Column(Boolean, default=False) - # 是否启用ai提取标题和内容 + # Whether to enable AI title and content extraction is_ai_extract = Column(Boolean, default=False) - # ai提取标题和内容的prompt + # AI title and content extraction prompt ai_extract_prompt = Column(String, nullable=True) is_auto_markdown_to_html = Column(Boolean, default=False) - # 是否启用自定义提取标题和内容的正则表达式 + # Whether to enable custom regex for title and content extraction enable_custom_title_pattern = Column(Boolean, default=False) enable_custom_content_pattern = Column(Boolean, default=False) - # 关系 + # Relationships rule = relationship('ForwardRule', back_populates='rss_config') patterns = relationship('RSSPattern', back_populates='rss_config', cascade="all, delete-orphan") @@ -212,15 +212,15 @@ class RSSPattern(Base): id = Column(Integer, primary_key=True) rss_config_id = Column(Integer, ForeignKey('rss_configs.id'), nullable=False) - pattern = Column(String, nullable=False) # 正则表达式模式 - pattern_type = Column(String, nullable=False) # 模式类型: 'title' 或 'content' - priority = Column(Integer, default=0) # 执行优先级,数字越小优先级越高 + pattern = Column(String, nullable=False) # Regex pattern + pattern_type = Column(String, nullable=False) # Pattern type: 'title' or 'content' + priority = Column(Integer, default=0) # Execution priority, lower number means higher priority - # 关系 + # Relationships rss_config = relationship('RSSConfig', back_populates='patterns') - # 添加联合唯一约束 + # Add composite unique constraint __table_args__ = ( UniqueConstraint('rss_config_id', 'pattern', 'pattern_type', name='unique_rss_pattern'), ) @@ -229,66 +229,66 @@ class User(Base): __tablename__ = 'users' id = Column(Integer, primary_key=True) - username = Column(String, nullable=False) - password = Column(String, nullable=False) + username = Column(String, nullable=False) + password = Column(String, nullable=False) def migrate_db(engine): - """数据库迁移函数,确保新字段的添加""" + """Database migration function, ensures new fields are added""" inspector = inspect(engine) - - # 获取当前数据库中所有表 + + # Get all tables in current database existing_tables = inspector.get_table_names() - - # 连接数据库 + + # Connect to database connection = engine.connect() - + try: with engine.connect() as connection: - # 如果rule_syncs表不存在,创建表 + # If rule_syncs table does not exist, create it if 'rule_syncs' not in existing_tables: - logging.info("创建rule_syncs表...") + logging.info("Creating rule_syncs table...") RuleSync.__table__.create(engine) - # 如果users表不存在,创建表 + # If users table does not exist, create it if 'users' not in existing_tables: - logging.info("创建users表...") + logging.info("Creating users table...") User.__table__.create(engine) - # 如果rss_configs表不存在,创建表 + # If rss_configs table does not exist, create it if 'rss_configs' not in existing_tables: - logging.info("创建rss_configs表...") + logging.info("Creating rss_configs table...") RSSConfig.__table__.create(engine) - - # 如果rss_patterns表不存在,创建表 + + # If rss_patterns table does not exist, create it if 'rss_patterns' not in existing_tables: - logging.info("创建rss_patterns表...") + logging.info("Creating rss_patterns table...") RSSPattern.__table__.create(engine) - # 如果push_configs表不存在,创建表 + # If push_configs table does not exist, create it if 'push_configs' not in existing_tables: - logging.info("创建push_configs表...") + logging.info("Creating push_configs table...") PushConfig.__table__.create(engine) - - - # 如果media_types表不存在,创建表 + + + # If media_types table does not exist, create it if 'media_types' not in existing_tables: - logging.info("创建media_types表...") + logging.info("Creating media_types table...") MediaTypes.__table__.create(engine) - - # 如果forward_rules表中有selected_media_types列,迁移数据到新表 + + # If forward_rules table has selected_media_types column, migrate data to new table if 'selected_media_types' in forward_rules_columns: - logging.info("迁移媒体类型数据到新表...") - # 查询所有规则 + logging.info("Migrating media type data to new table...") + # Query all rules rules = connection.execute(text("SELECT id, selected_media_types FROM forward_rules WHERE selected_media_types IS NOT NULL")) - + for rule in rules: rule_id = rule[0] selected_types = rule[1] if selected_types: - # 创建媒体类型记录 + # Create media type record media_types_data = { 'photo': 'photo' in selected_types, 'document': 'document' in selected_types, @@ -296,8 +296,8 @@ def migrate_db(engine): 'audio': 'audio' in selected_types, 'voice': 'voice' in selected_types } - - # 插入数据 + + # Insert data connection.execute( text(""" INSERT INTO media_types (rule_id, photo, document, video, audio, voice) @@ -313,22 +313,21 @@ def migrate_db(engine): } ) if 'media_extensions' not in existing_tables: - logging.info("创建media_extensions表...") + logging.info("Creating media_extensions table...") MediaExtensions.__table__.create(engine) - + except Exception as e: - logging.error(f'迁移媒体类型数据时出错: {str(e)}') - - + logging.error(f'Error migrating media type data: {str(e)}') - # 检查forward_rules表的现有列 + + # Check existing columns of forward_rules table forward_rules_columns = {column['name'] for column in inspector.get_columns('forward_rules')} - # 检查Keyword表的现有列 + # Check existing columns of Keyword table keyword_columns = {column['name'] for column in inspector.get_columns('keywords')} - # 需要添加的新列及其默认值 + # New columns to add and their default values forward_rules_new_columns = { 'is_ai': 'ALTER TABLE forward_rules ADD COLUMN is_ai BOOLEAN DEFAULT FALSE', 'ai_model': 'ALTER TABLE forward_rules ADD COLUMN ai_model VARCHAR DEFAULT NULL', @@ -360,7 +359,7 @@ def migrate_db(engine): 'enable_sync': 'ALTER TABLE forward_rules ADD COLUMN enable_sync BOOLEAN DEFAULT FALSE', 'userinfo_template': 'ALTER TABLE forward_rules ADD COLUMN userinfo_template VARCHAR DEFAULT "**{name}**"', 'time_template': 'ALTER TABLE forward_rules ADD COLUMN time_template VARCHAR DEFAULT "{time}"', - 'original_link_template': 'ALTER TABLE forward_rules ADD COLUMN original_link_template VARCHAR DEFAULT "原始连接:{original_link}"', + 'original_link_template': 'ALTER TABLE forward_rules ADD COLUMN original_link_template VARCHAR DEFAULT "Original link: {original_link}"', 'enable_push': 'ALTER TABLE forward_rules ADD COLUMN enable_push BOOLEAN DEFAULT FALSE', 'enable_only_push': 'ALTER TABLE forward_rules ADD COLUMN enable_only_push BOOLEAN DEFAULT FALSE', 'media_allow_text': 'ALTER TABLE forward_rules ADD COLUMN media_allow_text BOOLEAN DEFAULT FALSE', @@ -371,48 +370,48 @@ def migrate_db(engine): 'is_blacklist': 'ALTER TABLE keywords ADD COLUMN is_blacklist BOOLEAN DEFAULT TRUE', } - # 添加缺失的列 + # Add missing columns with engine.connect() as connection: - # 添加forward_rules表的列 + # Add columns to forward_rules table for column, sql in forward_rules_new_columns.items(): if column not in forward_rules_columns: try: connection.execute(text(sql)) - logging.info(f'已添加列: {column}') + logging.info(f'Column added: {column}') except Exception as e: - logging.error(f'添加列 {column} 时出错: {str(e)}') - + logging.error(f'Error adding column {column}: {str(e)}') + - # 添加keywords表的列 + # Add columns to keywords table for column, sql in keywords_new_columns.items(): if column not in keyword_columns: try: connection.execute(text(sql)) - logging.info(f'已添加列: {column}') + logging.info(f'Column added: {column}') except Exception as e: - logging.error(f'添加列 {column} 时出错: {str(e)}') + logging.error(f'Error adding column {column}: {str(e)}') - #先检查forward_rules表的列的forward_mode是否存在 + # First check if forward_rules table has forward_mode column if 'forward_mode' not in forward_rules_columns: - # 修改forward_rules表的列mode为forward_mode + # Rename mode column to forward_mode in forward_rules table connection.execute(text("ALTER TABLE forward_rules RENAME COLUMN mode TO forward_mode")) - logging.info('修改forward_rules表的列mode为forward_mode成功') + logging.info('Successfully renamed mode column to forward_mode in forward_rules table') - # 修改keywords表的唯一约束 + # Update unique constraint for keywords table try: with engine.connect() as connection: - # 检查索引是否存在 + # Check if index exists result = connection.execute(text(""" - SELECT name FROM sqlite_master + SELECT name FROM sqlite_master WHERE type='index' AND name='unique_rule_keyword_is_regex_is_blacklist' """)) index_exists = result.fetchone() is not None if not index_exists: - logging.info('开始更新 keywords 表的唯一约束...') + logging.info('Starting to update keywords table unique constraint...') try: - + with engine.begin() as connection: - # 创建临时表 + # Create temporary table connection.execute(text(""" CREATE TABLE keywords_temp ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -420,59 +419,59 @@ def migrate_db(engine): keyword TEXT, is_regex BOOLEAN, is_blacklist BOOLEAN - -- 如果 keywords 表还有其他字段,请在这里一并定义 + -- If keywords table has other fields, define them here as well ) """)) - logging.info('创建 keywords_temp 表结构成功') + logging.info('Successfully created keywords_temp table structure') - # 将原表数据复制到临时表,让数据库自动生成 id + # Copy original table data to temporary table, let database auto-generate id result = connection.execute(text(""" INSERT INTO keywords_temp (rule_id, keyword, is_regex, is_blacklist) SELECT rule_id, keyword, is_regex, is_blacklist FROM keywords """)) - logging.info(f'复制数据到 keywords_temp 成功,影响行数: {result.rowcount}') + logging.info(f'Successfully copied data to keywords_temp, rows affected: {result.rowcount}') - # 删除原表 keywords + # Delete original keywords table connection.execute(text("DROP TABLE keywords")) - logging.info('删除原表 keywords 成功') + logging.info('Successfully deleted original keywords table') - # 4将临时表重命名为 keywords + # Rename temporary table to keywords connection.execute(text("ALTER TABLE keywords_temp RENAME TO keywords")) - logging.info('重命名 keywords_temp 为 keywords 成功') + logging.info('Successfully renamed keywords_temp to keywords') - # 添加唯一约束 + # Add unique constraint connection.execute(text(""" - CREATE UNIQUE INDEX unique_rule_keyword_is_regex_is_blacklist + CREATE UNIQUE INDEX unique_rule_keyword_is_regex_is_blacklist ON keywords (rule_id, keyword, is_regex, is_blacklist) """)) - logging.info('添加唯一约束 unique_rule_keyword_is_regex_is_blacklist 成功') + logging.info('Successfully added unique constraint unique_rule_keyword_is_regex_is_blacklist') - logging.info('成功更新 keywords 表结构和唯一约束') + logging.info('Successfully updated keywords table structure and unique constraint') except Exception as e: - logging.error(f'更新 keywords 表结构时出错: {str(e)}') + logging.error(f'Error updating keywords table structure: {str(e)}') else: - logging.info('唯一约束已存在,跳过创建') + logging.info('Unique constraint already exists, skipping creation') except Exception as e: - logging.error(f'更新唯一约束时出错: {str(e)}') + logging.error(f'Error updating unique constraint: {str(e)}') def init_db(): - """初始化数据库""" - # 创建数据库文件夹 + """Initialize database""" + # Create database folder os.makedirs('./db', exist_ok=True) engine = create_engine('sqlite:///./db/forward.db') - # 首先创建所有表 + # First create all tables Base.metadata.create_all(engine) - # 然后进行必要的迁移 + # Then perform necessary migrations migrate_db(engine) return engine def get_session(): - """创建会话工厂""" + """Create session factory""" engine = create_engine('sqlite:///./db/forward.db') Session = sessionmaker(bind=engine) return Session() @@ -481,4 +480,4 @@ def get_session(): logging.basicConfig(level=logging.INFO) engine = init_db() session = get_session() - logging.info("数据库初始化和迁移完成。") \ No newline at end of file + logging.info("Database initialization and migration completed.") \ No newline at end of file diff --git a/rss/app/__init__.py b/rss/app/__init__.py index 16ea87c..434345a 100644 --- a/rss/app/__init__.py +++ b/rss/app/__init__.py @@ -9,8 +9,8 @@ app = FastAPI(title="TG Forwarder RSS") -# 注册路由 +# Register routes app.include_router(auth_router) -# 模板配置 -templates = Jinja2Templates(directory="rss/app/templates") \ No newline at end of file +# Template configuration +templates = Jinja2Templates(directory="rss/app/templates") diff --git a/rss/app/api/endpoints/feed.py b/rss/app/api/endpoints/feed.py index 91ad075..7db2651 100644 --- a/rss/app/api/endpoints/feed.py +++ b/rss/app/api/endpoints/feed.py @@ -27,39 +27,39 @@ logger = logging.getLogger(__name__) router = APIRouter() -# 添加本地访问验证依赖 +# Add local access verification dependency async def verify_local_access(request: Request): - """验证请求是否来自本地或Docker内部网络""" + """Verify that the request comes from local or Docker internal network""" client_host = request.client.host if request.client else None - - # 允许的本地IP地址列表 + + # List of allowed local IP addresses local_addresses = ['127.0.0.1', '::1', 'localhost', '0.0.0.0'] - - # 如果设置了 HOST 环境变量,也将其添加到允许列表中 + + # If the HOST environment variable is set, add it to the allowed list if hasattr(settings, 'HOST') and settings.HOST: local_addresses.append(settings.HOST) - - # 检查是否是Docker内部网络IP (常见的私有网络范围) + + # Check if it's a Docker internal network IP (common private network ranges) docker_ip = False if client_host: docker_prefixes = ['172.', '192.168.', '10.'] docker_ip = any(client_host.startswith(prefix) for prefix in docker_prefixes) - - # 如果是本地地址或Docker内部网络IP,允许访问 + + # If it's a local address or Docker internal network IP, allow access if client_host in local_addresses or docker_ip: - logger.debug(f"已验证访问权限: {client_host}") + logger.debug(f"Access verified: {client_host}") return True - - # 拒绝来自外部网络的访问 - logger.warning(f"拒绝来自外部网络的访问: {client_host}") + + # Deny access from external networks + logger.warning(f"Denied access from external network: {client_host}") raise HTTPException( - status_code=403, - detail="此API端点仅允许本地或内部网络访问" + status_code=403, + detail="This API endpoint only allows local or internal network access" ) @router.get("/") async def root(): - """服务状态检查""" + """Service status check""" return { "status": "ok", "service": "TG Forwarder RSS" @@ -67,184 +67,184 @@ async def root(): @router.get("/rss/feed/{rule_id}") async def get_feed(rule_id: int, request: Request): - """返回规则对应的RSS Feed""" + """Return the RSS Feed for a rule""" session = None try: - # 创建数据库会话 + # Create database session session = get_session() - - # 查询规则配置 + + # Query rule configuration rss_config = session.query(RSSConfig).filter(RSSConfig.rule_id == rule_id).first() if not rss_config or not rss_config.enable_rss: - logger.warning(f"规则 {rule_id} 的RSS未启用或不存在") - raise HTTPException(status_code=404, detail="RSS feed 未启用或不存在") - - # 获取请求URL的基础部分 + logger.warning(f"RSS for rule {rule_id} is not enabled or does not exist") + raise HTTPException(status_code=404, detail="RSS feed is not enabled or does not exist") + + # Get the base part of the request URL base_url = str(request.base_url).rstrip('/') - logger.info(f"请求基础URL: {base_url}") - logger.info(f"请求头: {request.headers}") - logger.info(f"请求客户端: {request.client}") - - # 检查是否有环境变量中配置的基础URL + logger.info(f"Request base URL: {base_url}") + logger.info(f"Request headers: {request.headers}") + logger.info(f"Request client: {request.client}") + + # Check if there is a base URL configured in environment variables if RSS_MEDIA_BASE_URL: - logger.info(f"使用环境变量中配置的媒体基础URL: {RSS_MEDIA_BASE_URL}") + logger.info(f"Using media base URL from environment variable: {RSS_MEDIA_BASE_URL}") base_url = RSS_MEDIA_BASE_URL.rstrip('/') else: - # 检查是否有X-Forwarded-Host或Host头 + # Check if there is an X-Forwarded-Host or Host header forwarded_host = request.headers.get("X-Forwarded-Host") host_header = request.headers.get("Host") if forwarded_host: - logger.info(f"检测到X-Forwarded-Host: {forwarded_host}") - # 构建基于forwarded_host的URL + logger.info(f"Detected X-Forwarded-Host: {forwarded_host}") + # Build URL based on forwarded_host scheme = request.headers.get("X-Forwarded-Proto", "http") base_url = f"{scheme}://{forwarded_host}" - logger.info(f"基于X-Forwarded-Host的媒体基础URL: {base_url}") + logger.info(f"Media base URL based on X-Forwarded-Host: {base_url}") elif host_header and host_header != f"{settings.HOST}:{settings.PORT}": - logger.info(f"检测到自定义Host: {host_header}") - # 构建基于Host的URL + logger.info(f"Detected custom Host: {host_header}") + # Build URL based on Host scheme = request.url.scheme base_url = f"{scheme}://{host_header}" - logger.info(f"基于Host的媒体基础URL: {base_url}") - - logger.info(f"最终使用的媒体基础URL: {base_url}") - - # 获取规则对应的条目 + logger.info(f"Media base URL based on Host: {base_url}") + + logger.info(f"Final media base URL: {base_url}") + + # Get entries for the rule entries = await get_entries(rule_id) - logger.info(f"获取到 {len(entries)} 个条目") - - # 如果没有条目,返回测试数据 + logger.info(f"Retrieved {len(entries)} entries") + + # If no entries, return test data if not entries: - logger.warning(f"规则 {rule_id} 没有条目数据,返回测试数据") + logger.warning(f"Rule {rule_id} has no entry data, returning test data") try: fg = FeedService.generate_test_feed(rule_id, base_url) - - # 生成 RSS XML + + # Generate RSS XML rss_xml = fg.rss_str(pretty=True) - - # 确保rss_xml是字符串类型 + + # Ensure rss_xml is string type if isinstance(rss_xml, bytes): - logger.info("将RSS XML从字节转换为字符串") + logger.info("Converting RSS XML from bytes to string") rss_xml = rss_xml.decode('utf-8') - - # 记录XML内容的一部分 + + # Log a portion of the XML content xml_sample = rss_xml[:500] + "..." if len(rss_xml) > 500 else rss_xml - logger.info(f"生成的测试RSS XML (前500字符): {xml_sample}") - - # 检查XML中是否还有硬编码的localhost或127.0.0.1地址 + logger.info(f"Generated test RSS XML (first 500 chars): {xml_sample}") + + # Check if XML still contains hardcoded localhost or 127.0.0.1 addresses if "127.0.0.1" in rss_xml or "localhost" in rss_xml: - logger.warning(f"RSS XML中仍包含硬编码的本地地址") - - # 替换硬编码的地址 + logger.warning(f"RSS XML still contains hardcoded local addresses") + + # Replace hardcoded addresses rss_xml = rss_xml.replace(f"http://127.0.0.1:{settings.PORT}", base_url) rss_xml = rss_xml.replace(f"http://localhost:{settings.PORT}", base_url) rss_xml = rss_xml.replace(f"http://{settings.HOST}:{settings.PORT}", base_url) - - logger.info(f"已替换硬编码的本地地址为: {base_url}") - - # 确保返回的是字节类型 + + logger.info(f"Replaced hardcoded local addresses with: {base_url}") + + # Ensure return type is bytes if isinstance(rss_xml, str): rss_xml = rss_xml.encode('utf-8') - + return Response( content=rss_xml, media_type="application/xml; charset=utf-8" ) except Exception as e: - logger.error(f"生成测试Feed时出错: {str(e)}", exc_info=True) - raise HTTPException(status_code=500, detail=f"生成测试Feed失败: {str(e)}") + logger.error(f"Error generating test Feed: {str(e)}", exc_info=True) + raise HTTPException(status_code=500, detail=f"Failed to generate test Feed: {str(e)}") else: - # 根据真实数据生成 Feed,传入基础URL + # Generate Feed from real data, pass in base URL try: fg = await FeedService.generate_feed_from_entries(rule_id, entries, base_url) - - # 生成 RSS XML + + # Generate RSS XML rss_xml = fg.rss_str(pretty=True) - - # 确保rss_xml是字符串类型 + + # Ensure rss_xml is string type if isinstance(rss_xml, bytes): - logger.info("将RSS XML从字节转换为字符串") + logger.info("Converting RSS XML from bytes to string") rss_xml = rss_xml.decode('utf-8') - - # 记录XML内容的一部分 + + # Log a portion of the XML content xml_sample = rss_xml[:500] + "..." if len(rss_xml) > 500 else rss_xml - logger.info(f"生成的RSS XML (前500字符): {xml_sample}") - - # 检查XML中是否还有硬编码的localhost或127.0.0.1地址 + logger.info(f"Generated RSS XML (first 500 chars): {xml_sample}") + + # Check if XML still contains hardcoded localhost or 127.0.0.1 addresses if "127.0.0.1" in rss_xml or "localhost" in rss_xml: - logger.warning(f"RSS XML中仍包含硬编码的本地地址") - - # 替换硬编码的地址 + logger.warning(f"RSS XML still contains hardcoded local addresses") + + # Replace hardcoded addresses rss_xml = rss_xml.replace(f"http://127.0.0.1:{settings.PORT}", base_url) rss_xml = rss_xml.replace(f"http://localhost:{settings.PORT}", base_url) rss_xml = rss_xml.replace(f"http://{settings.HOST}:{settings.PORT}", base_url) - - logger.info(f"已替换硬编码的本地地址为: {base_url}") - - # 确保返回的是字节类型 + + logger.info(f"Replaced hardcoded local addresses with: {base_url}") + + # Ensure return type is bytes if isinstance(rss_xml, str): rss_xml = rss_xml.encode('utf-8') - + return Response( content=rss_xml, media_type="application/xml; charset=utf-8" ) except Exception as e: - logger.error(f"生成真实条目Feed时出错: {str(e)}", exc_info=True) - raise HTTPException(status_code=500, detail=f"生成Feed失败: {str(e)}") + logger.error(f"Error generating Feed from real entries: {str(e)}", exc_info=True) + raise HTTPException(status_code=500, detail=f"Failed to generate Feed: {str(e)}") except HTTPException: raise except Exception as e: - logger.error(f"生成RSS feed时出错: {str(e)}", exc_info=True) + logger.error(f"Error generating RSS feed: {str(e)}", exc_info=True) raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}") finally: - # 确保会话被关闭 + # Ensure session is closed if session: session.close() @router.get("/media/{rule_id}/{filename}") async def get_media(rule_id: int, filename: str, request: Request): - """返回媒体文件""" - # 记录请求信息 - logger.info(f"媒体请求 - 规则ID: {rule_id}, 文件名: {filename}") - logger.info(f"请求URL: {request.url}") - logger.info(f"请求头: {request.headers}") - - # 获取基础URL,用于日志记录 + """Return a media file""" + # Log request information + logger.info(f"Media request - Rule ID: {rule_id}, Filename: {filename}") + logger.info(f"Request URL: {request.url}") + logger.info(f"Request headers: {request.headers}") + + # Get base URL for logging base_url = str(request.base_url).rstrip('/') if RSS_MEDIA_BASE_URL: - logger.info(f"环境变量中配置的媒体基础URL: {RSS_MEDIA_BASE_URL}") + logger.info(f"Media base URL from environment variable: {RSS_MEDIA_BASE_URL}") base_url = RSS_MEDIA_BASE_URL.rstrip('/') else: - # 检查是否有X-Forwarded-Host或Host头 + # Check if there is an X-Forwarded-Host or Host header forwarded_host = request.headers.get("X-Forwarded-Host") host_header = request.headers.get("Host") if forwarded_host: - logger.info(f"检测到X-Forwarded-Host: {forwarded_host}") + logger.info(f"Detected X-Forwarded-Host: {forwarded_host}") scheme = request.headers.get("X-Forwarded-Proto", "http") base_url = f"{scheme}://{forwarded_host}" elif host_header and host_header != f"{settings.HOST}:{settings.PORT}": - logger.info(f"检测到自定义Host: {host_header}") + logger.info(f"Detected custom Host: {host_header}") scheme = request.url.scheme base_url = f"{scheme}://{host_header}" - - logger.info(f"最终使用的媒体基础URL: {base_url}") - - # 构建规则特定的媒体文件路径 + + logger.info(f"Final media base URL: {base_url}") + + # Build rule-specific media file path media_path = Path(settings.get_rule_media_path(rule_id)) / filename - - # 记录尝试访问的路径 - logger.info(f"尝试访问媒体文件: {media_path}") - - # 检查文件是否存在 + + # Log the attempted access path + logger.info(f"Attempting to access media file: {media_path}") + + # Check if file exists if not media_path.exists(): - # 文件不存在,返回404 - logger.error(f"媒体文件未找到: {filename}") - raise HTTPException(status_code=404, detail=f"媒体文件未找到: {filename}") - - # 确定正确的MIME类型 + # File does not exist, return 404 + logger.error(f"Media file not found: {filename}") + raise HTTPException(status_code=404, detail=f"Media file not found: {filename}") + + # Determine the correct MIME type mime_type = mimetypes.guess_type(str(media_path))[0] if not mime_type: - # 如果无法确定MIME类型,根据文件扩展名猜测 + # If MIME type cannot be determined, guess based on file extension ext = filename.split('.')[-1].lower() if '.' in filename else '' if ext in ['mp4', 'mov', 'avi', 'webm']: mime_type = f"video/{ext}" @@ -254,10 +254,10 @@ async def get_media(rule_id: int, filename: str, request: Request): mime_type = f"image/{ext}" else: mime_type = "application/octet-stream" - - logger.info(f"发送媒体文件: {filename}, MIME类型: {mime_type}, 大小: {os.path.getsize(media_path)} 字节") - - # 返回文件,并设置正确的Content-Type + + logger.info(f"Sending media file: {filename}, MIME type: {mime_type}, Size: {os.path.getsize(media_path)} bytes") + + # Return file with correct Content-Type return FileResponse( path=media_path, media_type=mime_type, @@ -266,14 +266,14 @@ async def get_media(rule_id: int, filename: str, request: Request): @router.post("/api/entries/{rule_id}/add", dependencies=[Depends(verify_local_access)]) async def add_entry(rule_id: int, entry_data: Dict[str, Any] = Body(...)): - """添加新的条目 (仅限本地访问)""" + """Add a new entry (local access only)""" try: - # 记录接收到的数据摘要 + # Log summary of received data media_count = len(entry_data.get("media", [])) has_context = "context" in entry_data and entry_data["context"] is not None - logger.info(f"接收到新条目数据: 规则ID={rule_id}, 标题='{entry_data.get('title', '无标题')}', 媒体数量={media_count}, 包含上下文={has_context}") - - # 获取 RSS 配置信息,确定最大条目数量 + logger.info(f"Received new entry data: Rule ID={rule_id}, Title='{entry_data.get('title', 'No title')}', Media count={media_count}, Has context={has_context}") + + # Get RSS configuration to determine maximum entry count session = get_session() max_items = None try: @@ -281,59 +281,59 @@ async def add_entry(rule_id: int, entry_data: Dict[str, Any] = Body(...)): max_items = rss_config.max_items finally: session.close() - - # 验证媒体数据 + + # Validate media data if media_count > 0: media_filenames = [] for m in entry_data.get("media", []): if isinstance(m, dict): - media_filenames.append(m.get('filename', '未知')) + media_filenames.append(m.get('filename', 'unknown')) else: - media_filenames.append(getattr(m, 'filename', '未知')) - logger.info(f"媒体文件列表: {media_filenames}") - - # 确保媒体文件存在 + media_filenames.append(getattr(m, 'filename', 'unknown')) + logger.info(f"Media file list: {media_filenames}") + + # Ensure media files exist for media in entry_data.get("media", []): if isinstance(media, dict): filename = media.get("filename", "") else: filename = getattr(media, "filename", "") - + media_path = os.path.join(settings.MEDIA_PATH, filename) if not os.path.exists(media_path): - logger.warning(f"媒体文件不存在: {media_path}") - - # 记录上下文信息 + logger.warning(f"Media file does not exist: {media_path}") + + # Log context information if has_context: - logger.info(f"条目包含原始上下文对象,属性: {', '.join(entry_data['context'].keys()) if hasattr(entry_data['context'], 'keys') else '无法获取属性'}") - - # 确保必要的字段存在 + logger.info(f"Entry contains original context object, attributes: {', '.join(entry_data['context'].keys()) if hasattr(entry_data['context'], 'keys') else 'unable to get attributes'}") + + # Ensure required fields exist entry_data["rule_id"] = rule_id if not entry_data.get("message_id"): entry_data["message_id"] = entry_data.get("id", "") - - # 检查当前条目数量,如果接近限制则删除最旧的条目 + + # Check current entry count, delete oldest entries if approaching the limit current_entries = await get_entries(rule_id) if len(current_entries) >= max_items - 1: - # 计算需要删除的条目数量,确保添加新条目后总数不超过最大限制 + # Calculate the number of entries to delete, ensuring total does not exceed maximum after adding new entry to_delete_count = len(current_entries) - (max_items - 1) if to_delete_count > 0: - logger.info(f"当前条目数量({len(current_entries)})将超过限制({max_items}),需要删除 {to_delete_count} 个最早的条目") - - # 对条目按发布时间排序(从早到晚) + logger.info(f"Current entry count ({len(current_entries)}) will exceed limit ({max_items}), need to delete {to_delete_count} oldest entries") + + # Sort entries by publish time (oldest first) sorted_entries = sorted(current_entries, key=lambda e: datetime.fromisoformat(e.published) if hasattr(e, 'published') else datetime.now()) - - # 获取要删除的条目 + + # Get entries to delete entries_to_delete = sorted_entries[:to_delete_count] - - # 删除多余条目 + + # Delete excess entries for entry in entries_to_delete: try: - # 删除条目前先处理其媒体文件 + # Process media files before deleting the entry if hasattr(entry, 'media') and entry.media: - logger.info(f"条目 {entry.id} 包含 {len(entry.media)} 个媒体文件,将一并删除") - - # 删除媒体文件 + logger.info(f"Entry {entry.id} contains {len(entry.media)} media files, will be deleted together") + + # Delete media files media_dir = Path(settings.get_rule_media_path(rule_id)) for media in entry.media: if hasattr(media, 'filename'): @@ -341,24 +341,24 @@ async def add_entry(rule_id: int, entry_data: Dict[str, Any] = Body(...)): if media_path.exists(): try: os.remove(media_path) - logger.info(f"已删除媒体文件: {media_path}") + logger.info(f"Deleted media file: {media_path}") except Exception as e: - logger.error(f"删除媒体文件失败: {media_path}, 错误: {str(e)}") - - # 删除条目 + logger.error(f"Failed to delete media file: {media_path}, Error: {str(e)}") + + # Delete entry success = await delete_entry(rule_id, entry.id) if success: - logger.info(f"已删除条目: {entry.id}") + logger.info(f"Deleted entry: {entry.id}") else: - logger.warning(f"删除条目失败: {entry.id}") + logger.warning(f"Failed to delete entry: {entry.id}") except Exception as e: - logger.error(f"处理过期条目时出错: {str(e)}") - - # 转换为Entry对象 + logger.error(f"Error processing expired entry: {str(e)}") + + # Convert to Entry object entry = Entry( rule_id=rule_id, message_id=entry_data.get("message_id", entry_data.get("id", "")), - title=entry_data.get("title", "新消息"), + title=entry_data.get("title", "New message"), content=entry_data.get("content", ""), published=entry_data.get("published"), author=entry_data.get("author", ""), @@ -368,9 +368,9 @@ async def add_entry(rule_id: int, entry_data: Dict[str, Any] = Body(...)): sender_info=entry_data.get("sender_info") ) - - # 使用AI提取内容 + + # Use AI to extract content if rss_config.is_ai_extract: try: rule = session.query(ForwardRule).filter(ForwardRule.id == rule_id).first() @@ -380,302 +380,301 @@ async def add_entry(rule_id: int, entry_data: Dict[str, Any] = Body(...)): prompt=rss_config.ai_extract_prompt, model=rule.ai_model ) - logger.info(f"AI提取内容: {json_text}") - - # 去除代码块标记,如果有的话 + logger.info(f"AI extracted content: {json_text}") + + # Remove code block markers if present if "```" in json_text: - # 移除所有代码块标记,包括语言标识和结束标记 - json_text = re.sub(r'```(\w+)?\n', '', json_text) # 开始标记(带可选的语言标识) - json_text = re.sub(r'\n```', '', json_text) # 结束标记 + # Remove all code block markers, including language identifiers and closing markers + json_text = re.sub(r'```(\w+)?\n', '', json_text) # Opening marker (with optional language identifier) + json_text = re.sub(r'\n```', '', json_text) # Closing marker json_text = json_text.strip() - logger.info(f"去除代码块标记后的内容: {json_text}") - - # 解析JSON数据 + logger.info(f"Content after removing code block markers: {json_text}") + + # Parse JSON data try: json_data = json.loads(json_text) - logger.info(f"解析后的JSON数据: {json_data}") - - # 提取标题和内容 + logger.info(f"Parsed JSON data: {json_data}") + + # Extract title and content title = json_data.get("title", "") content = json_data.get("content", "") entry.title = title entry.content = content except json.JSONDecodeError as e: - logger.error(f"JSON解析错误: {str(e)}, 原始文本: {json_text}") - # 尝试其他清理方式 + logger.error(f"JSON parse error: {str(e)}, Original text: {json_text}") + # Try alternative cleaning approach try: - # 匹配大括号之间的JSON内容 + # Match JSON content between curly braces json_match = re.search(r'\{.*\}', json_text, re.DOTALL) if json_match: clean_json = json_match.group(0) - logger.info(f"尝试提取JSON: {clean_json}") + logger.info(f"Attempting to extract JSON: {clean_json}") json_data = json.loads(clean_json) - - # 提取标题和内容 + + # Extract title and content title = json_data.get("title", "") content = json_data.get("content", "") entry.title = title entry.content = content - logger.info(f"成功从文本中提取JSON数据") + logger.info(f"Successfully extracted JSON data from text") else: - logger.error("无法从AI响应中提取有效JSON") + logger.error("Unable to extract valid JSON from AI response") except Exception as inner_e: - logger.error(f"尝试二次解析JSON时出错: {str(inner_e)}") + logger.error(f"Error during secondary JSON parsing attempt: {str(inner_e)}") except Exception as e: - logger.error(f"处理JSON数据时出错: {str(e)}") + logger.error(f"Error processing JSON data: {str(e)}") except Exception as e: - logger.error(f"AI提取内容时出错: {str(e)}") + logger.error(f"Error during AI content extraction: {str(e)}") finally: if session: session.close() - - logger.info(f"启用自定义标题模式: {rss_config.enable_custom_title_pattern}, 启用自定义内容模式: {rss_config.enable_custom_content_pattern}") + + logger.info(f"Custom title pattern enabled: {rss_config.enable_custom_title_pattern}, Custom content pattern enabled: {rss_config.enable_custom_content_pattern}") if rss_config.enable_custom_title_pattern or rss_config.enable_custom_content_pattern: try: - # 获取原始内容 + # Get original content original_content = entry.content or "" original_title = entry.title - - # 如果启用了标题正则表达式提取 + + # If title regex extraction is enabled if rss_config.enable_custom_title_pattern: - # 直接使用会话查询标题模式并按优先级排序 + # Query title patterns directly using session and sort by priority title_patterns = session.query(RSSPattern).filter_by( - rss_config_id=rss_config.id, + rss_config_id=rss_config.id, pattern_type='title' ).order_by(RSSPattern.priority).all() - - logger.info(f"找到 {len(title_patterns)} 个标题模式") - - # 设置初始处理文本 + + logger.info(f"Found {len(title_patterns)} title patterns") + + # Set initial processing text processing_content = original_content - logger.info(f"标题提取初始文本: {processing_content[:100]}..." if len(processing_content) > 100 else processing_content) - - # 依次应用每个模式,每次处理后的结果作为下一个模式的输入 + logger.info(f"Title extraction initial text: {processing_content[:100]}..." if len(processing_content) > 100 else processing_content) + + # Apply each pattern sequentially, using the result of each as input for the next for pattern in title_patterns: - logger.info(f"开始尝试标题模式: {pattern.pattern}") + logger.info(f"Trying title pattern: {pattern.pattern}") try: - logger.info(f"对内容应用正则表达式: {pattern.pattern}") + logger.info(f"Applying regex to content: {pattern.pattern}") match = re.search(pattern.pattern, processing_content) if match: - logger.info(f"找到匹配: {match.groups()}") + logger.info(f"Match found: {match.groups()}") if match.groups(): entry.title = match.group(1) - logger.info(f"使用标题模式 '{pattern.pattern}' 提取到标题: {entry.title}") + logger.info(f"Extracted title using pattern '{pattern.pattern}': {entry.title}") else: - logger.warning(f"模式 '{pattern.pattern}' 匹配成功但没有捕获组") + logger.warning(f"Pattern '{pattern.pattern}' matched but has no capture groups") else: - logger.info(f"模式 '{pattern.pattern}' 未找到匹配") + logger.info(f"Pattern '{pattern.pattern}' found no match") except Exception as e: - logger.error(f"应用标题正则表达式 '{pattern.pattern}' 时出错: {str(e)}") - logger.exception("详细错误信息:") - - # 如果启用了内容正则表达式提取 + logger.error(f"Error applying title regex '{pattern.pattern}': {str(e)}") + logger.exception("Detailed error info:") + + # If content regex extraction is enabled if rss_config.enable_custom_content_pattern: - # 直接使用会话查询内容模式并按优先级排序 + # Query content patterns directly using session and sort by priority content_patterns = session.query(RSSPattern).filter_by( - rss_config_id=rss_config.id, + rss_config_id=rss_config.id, pattern_type='content' ).order_by(RSSPattern.priority).all() - - logger.info(f"找到 {len(content_patterns)} 个内容模式") - - # 设置初始处理文本 + + logger.info(f"Found {len(content_patterns)} content patterns") + + # Set initial processing text processing_content = original_content - logger.info(f"内容提取初始文本: {processing_content[:100]}..." if len(processing_content) > 100 else processing_content) - - # 依次应用每个模式,每次处理后的结果作为下一个模式的输入 + logger.info(f"Content extraction initial text: {processing_content[:100]}..." if len(processing_content) > 100 else processing_content) + + # Apply each pattern sequentially, using the result of each as input for the next for i, pattern in enumerate(content_patterns): try: - logger.info(f"[步骤 {i+1}/{len(content_patterns)}] 对内容应用正则表达式: {pattern.pattern}") - logger.info(f"处理前的内容长度: {len(processing_content)}, 预览: {processing_content[:150]}..." if len(processing_content) > 150 else processing_content) - + logger.info(f"[Step {i+1}/{len(content_patterns)}] Applying regex to content: {pattern.pattern}") + logger.info(f"Content length before processing: {len(processing_content)}, Preview: {processing_content[:150]}..." if len(processing_content) > 150 else processing_content) + match = re.search(pattern.pattern, processing_content) if match and match.groups(): extracted_content = match.group(1) - processing_content = extracted_content # 更新处理内容为提取结果 + processing_content = extracted_content # Update processing content with extracted result entry.content = extracted_content - - logger.info(f"使用内容模式 '{pattern.pattern}' 提取到内容,长度: {len(extracted_content)}") - logger.info(f"处理后的内容长度: {len(processing_content)}, 预览: {processing_content[:150]}..." if len(processing_content) > 150 else processing_content) + + logger.info(f"Extracted content using pattern '{pattern.pattern}', length: {len(extracted_content)}") + logger.info(f"Content length after processing: {len(processing_content)}, Preview: {processing_content[:150]}..." if len(processing_content) > 150 else processing_content) else: - logger.info(f"模式 '{pattern.pattern}' 未找到匹配或没有捕获组,内容保持不变") + logger.info(f"Pattern '{pattern.pattern}' found no match or has no capture groups, content remains unchanged") except Exception as e: - logger.error(f"应用内容正则表达式 '{pattern.pattern}' 时出错: {str(e)}") - - - # 如果执行到这里但没有提取到标题,则恢复原标题 + logger.error(f"Error applying content regex '{pattern.pattern}': {str(e)}") + + + # If no title was extracted by this point, restore the original title if not entry.title and original_title: entry.title = original_title - logger.info(f"恢复原标题: {entry.title}") - + logger.info(f"Restored original title: {entry.title}") + except Exception as e: - logger.error(f"使用正则表达式提取标题和内容时出错: {str(e)}") + logger.error(f"Error extracting title and content using regex: {str(e)}") if entry.sender_info: - # 清楚空格和换行 + # Strip spaces and line breaks entry.sender_info = entry.sender_info.strip() entry.content = entry.sender_info +":" +"\n\n" + entry.content - # 添加原始链接 + # Add original link if entry.original_link: - # 清理链接中的前缀、换行符和多余空格 - clean_link = entry.original_link.replace("原始消息:", "").strip() - # 删除链接中的所有换行符 + # Clean prefix, line breaks, and extra spaces from the link + clean_link = entry.original_link.replace("Original message:", "").strip() + # Remove all line breaks from the link clean_link = clean_link.replace("\n", "").replace("\r", "") - # 处理链接中的多余空格 + # Handle extra spaces in the link clean_link = re.sub(r'\s+', ' ', clean_link).strip() - - # 确保链接是URL格式 + + # Ensure the link is in URL format if clean_link.startswith("http"): if entry.author: - # 使用Markdown格式的链接 - entry.content += f'\n\n[来源: {entry.author}]({clean_link})' + # Use Markdown format link + entry.content += f'\n\n[Source: {entry.author}]({clean_link})' else: - # 使用Markdown格式的链接 - entry.content += f'\n\n[来源]({clean_link})' - logger.info(f"已添加清理后的链接(Markdown格式): {clean_link}") + # Use Markdown format link + entry.content += f'\n\n[Source]({clean_link})' + logger.info(f"Added cleaned link (Markdown format): {clean_link}") else: - logger.warning(f"链接格式不正确,跳过添加: {clean_link}") - - # 处理后的消息 - logger.info(f"处理后的消息: {entry.content}") + logger.warning(f"Link format is incorrect, skipping: {clean_link}") + + # Processed message + logger.info(f"Processed message: {entry.content}") + - - - # 添加条目 + # Add entry success = await create_entry(entry) if success: - return {"status": "success", "message": f"条目已添加,媒体文件数量: {media_count}"} + return {"status": "success", "message": f"Entry added, media file count: {media_count}"} else: - logger.error("添加条目失败") - raise HTTPException(status_code=500, detail="添加条目失败") - + logger.error("Failed to add entry") + raise HTTPException(status_code=500, detail="Failed to add entry") + except ValidationError as e: - logger.error(f"验证错误: {str(e)}") + logger.error(f"Validation error: {str(e)}") raise HTTPException(status_code=422, detail=str(e)) except Exception as e: - logger.error(f"添加条目时出错: {str(e)}") + logger.error(f"Error adding entry: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) @router.delete("/api/entries/{rule_id}/{entry_id}", dependencies=[Depends(verify_local_access)]) async def delete_entry_api(rule_id: int, entry_id: str): - """删除条目 (仅限本地访问)""" + """Delete an entry (local access only)""" try: - # 删除条目 + # Delete entry success = await delete_entry(rule_id, entry_id) if not success: - raise HTTPException(status_code=404, detail="条目未找到") - - return {"status": "success", "message": "条目已删除"} + raise HTTPException(status_code=404, detail="Entry not found") + + return {"status": "success", "message": "Entry deleted"} except Exception as e: - logger.error(f"删除条目时出错: {str(e)}") + logger.error(f"Error deleting entry: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/api/entries/{rule_id}") async def list_entries(rule_id: int, limit: int = 20, offset: int = 0): - """列出规则对应的所有条目""" + """List all entries for a rule""" try: entries = await get_entries(rule_id, limit, offset) return {"entries": entries, "total": len(entries), "limit": limit, "offset": offset} except Exception as e: - logger.error(f"获取条目列表时出错: {str(e)}") + logger.error(f"Error getting entry list: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) @router.delete("/api/rule/{rule_id}", dependencies=[Depends(verify_local_access)]) async def delete_rule_data(rule_id: int): - """删除规则相关的所有数据和媒体文件 (仅限本地访问)""" + """Delete all data and media files for a rule (local access only)""" try: - - - # 获取规则的数据目录和媒体目录 + + + # Get the data directory and media directory for the rule data_path = Path(settings.get_rule_data_path(rule_id)) media_path = Path(settings.get_rule_media_path(rule_id)) - + deleted_files = 0 deleted_dirs = 0 failed_paths = [] - - # 辅助函数:强制删除目录 + + # Helper function: force delete directory def force_delete_directory(dir_path): if not dir_path.exists(): - return True, "目录不存在" - - # 方法1: 使用 shutil.rmtree + return True, "Directory does not exist" + + # Method 1: Use shutil.rmtree try: shutil.rmtree(dir_path, ignore_errors=True) if not dir_path.exists(): - return True, "使用 shutil.rmtree 成功删除" + return True, "Successfully deleted using shutil.rmtree" except Exception as e: pass - - # 方法2: 使用系统命令 + + # Method 2: Use system commands try: system = platform.system() if system == "Windows": - # Windows: 使用 rd /s /q - subprocess.run(["rd", "/s", "/q", str(dir_path)], - shell=True, - stderr=subprocess.PIPE, + # Windows: Use rd /s /q + subprocess.run(["rd", "/s", "/q", str(dir_path)], + shell=True, + stderr=subprocess.PIPE, stdout=subprocess.PIPE) else: - # Linux/Mac: 使用 rm -rf - subprocess.run(["rm", "-rf", str(dir_path)], - stderr=subprocess.PIPE, + # Linux/Mac: Use rm -rf + subprocess.run(["rm", "-rf", str(dir_path)], + stderr=subprocess.PIPE, stdout=subprocess.PIPE) - + if not dir_path.exists(): - return True, "使用系统命令成功删除" + return True, "Successfully deleted using system command" except Exception as e: pass - - # 方法3: 重命名后删除 + + # Method 3: Rename then delete try: temp_path = dir_path.parent / f"temp_delete_{time.time()}" os.rename(dir_path, temp_path) shutil.rmtree(temp_path, ignore_errors=True) if not dir_path.exists() and not temp_path.exists(): - return True, "使用重命名后删除成功" + return True, "Successfully deleted after renaming" except Exception as e: pass - - return False, "所有删除方法都失败" - - # 删除媒体目录 + + return False, "All deletion methods failed" + + # Delete media directory if media_path.exists(): - logger.info(f"开始删除媒体目录: {media_path}") + logger.info(f"Starting to delete media directory: {media_path}") success, method = force_delete_directory(media_path) if success: deleted_dirs += 1 - logger.info(f"已删除媒体目录: {media_path} - {method}") + logger.info(f"Deleted media directory: {media_path} - {method}") else: - logger.error(f"无法删除媒体目录: {media_path} - {method}") + logger.error(f"Unable to delete media directory: {media_path} - {method}") failed_paths.append(str(media_path)) - - # 删除数据目录 + + # Delete data directory if data_path.exists(): - logger.info(f"开始删除数据目录: {data_path}") + logger.info(f"Starting to delete data directory: {data_path}") success, method = force_delete_directory(data_path) if success: deleted_dirs += 1 - logger.info(f"已删除数据目录: {data_path} - {method}") + logger.info(f"Deleted data directory: {data_path} - {method}") else: - logger.error(f"无法删除数据目录: {data_path} - {method}") + logger.error(f"Unable to delete data directory: {data_path} - {method}") failed_paths.append(str(data_path)) - - # 验证删除结果 + + # Verify deletion results remaining_paths = [] if media_path.exists(): remaining_paths.append(str(media_path)) if data_path.exists(): remaining_paths.append(str(data_path)) - - # 返回删除结果 + + # Return deletion results status = "success" if not remaining_paths else "failed" return { "status": status, - "message": f"处理规则 {rule_id} 的数据{'失败,目录仍然存在' if remaining_paths else '成功,目录已删除'}", + "message": f"Processing rule {rule_id} data {'failed, directories still exist' if remaining_paths else 'succeeded, directories deleted'}", "details": { "data_path": str(data_path), "media_path": str(media_path), @@ -688,5 +687,5 @@ def force_delete_directory(dir_path): } } except Exception as e: - logger.error(f"删除规则数据时出错: {str(e)}") - raise HTTPException(status_code=500, detail=f"删除规则数据时出错: {str(e)}") \ No newline at end of file + logger.error(f"Error deleting rule data: {str(e)}") + raise HTTPException(status_code=500, detail=f"Error deleting rule data: {str(e)}") diff --git a/rss/app/configs/title_template.json b/rss/app/configs/title_template.json index 4536c21..6e58c41 100644 --- a/rss/app/configs/title_template.json +++ b/rss/app/configs/title_template.json @@ -2,39 +2,39 @@ "patterns": [ { "pattern": "^(?:#\\S+\\s*)+\\n\\s*\\n([^\\n]+)", - "description": "第一行全是标签后的标题:#标签1 #标签2\\n\\n标题内容" + "description": "Title after first line of all tags: #tag1 #tag2\\n\\nTitle content" }, { "pattern": "^#[^\\s]+\\s+\\*\\*([^\\*]+?)\\*\\*", - "description": "带标签的粗体标题:#标签 **标题**" + "description": "Bold title with tag: #tag **Title**" }, { "pattern": "^#[^\\s]+\\s+(.+?)(?=\\n|$)", - "description": "带标签的标题:#标签 标题内容" + "description": "Title with tag: #tag Title content" }, { "pattern": "^\\[\\*\\*([^\\*]+?)\\*\\*\\]\\([^\\)]+?\\)", - "description": "带链接的粗体标题:[**标题**](链接)" + "description": "Bold title with link: [**Title**](link)" }, { "pattern": "^\\[([^\\]]+?)\\]\\([^\\)]+?\\)", - "description": "带链接的标题:[标题](链接)" + "description": "Title with link: [Title](link)" }, { "pattern": "^\\*\\*([^\\*]+?)\\*\\*", - "description": "粗体标题:**标题**" + "description": "Bold title: **Title**" }, { "pattern": "^【([^】]+?)】", - "description": "中文方括号标题:【标题】" + "description": "Chinese bracket title: 【Title】" }, { "pattern": "^\\[([^\\]]+?)\\]", - "description": "中括号标题:[标题]" + "description": "Square bracket title: [Title]" }, { "pattern": "^(.+?)\\n", - "description": "第一行作为标题" + "description": "First line as title" } ] - } \ No newline at end of file + } diff --git a/rss/app/core/config.py b/rss/app/core/config.py index 5da8e87..0ac0c19 100644 --- a/rss/app/core/config.py +++ b/rss/app/core/config.py @@ -4,13 +4,13 @@ import logging import sys from utils.constants import RSS_HOST, RSS_PORT,DEFAULT_TIMEZONE,PROJECT_NAME -# 添加项目根目录到系统路径 +# Add project root directory to system path sys.path.append(str(Path(__file__).resolve().parent.parent.parent.parent)) -# 导入统一的常量 +# Import unified constants from utils.constants import RSS_MEDIA_DIR, RSS_MEDIA_PATH, RSS_DATA_DIR, get_rule_media_dir, get_rule_data_dir -# 加载环境变量 +# Load environment variables load_dotenv() class Settings: @@ -18,32 +18,32 @@ class Settings: HOST: str = RSS_HOST PORT: int = RSS_PORT TIMEZONE: str = DEFAULT_TIMEZONE - # 数据存储路径 + # Data storage path BASE_DIR = Path(__file__).resolve().parent.parent.parent.parent DATA_PATH = RSS_DATA_DIR - - # 使用统一的媒体路径常量 + + # Use unified media path constants RSS_MEDIA_PATH = RSS_MEDIA_PATH MEDIA_PATH = RSS_MEDIA_DIR - - - # 获取规则特定路径的方法 + + + # Methods to get rule-specific paths @classmethod def get_rule_media_path(cls, rule_id): - """获取指定规则的媒体目录""" + """Get the media directory for a specified rule""" return get_rule_media_dir(rule_id) - + @classmethod def get_rule_data_path(cls, rule_id): - """获取指定规则的数据目录""" + """Get the data directory for a specified rule""" return get_rule_data_dir(rule_id) - - # 确保目录存在 + + # Ensure directories exist def __init__(self): os.makedirs(self.DATA_PATH, exist_ok=True) os.makedirs(self.MEDIA_PATH, exist_ok=True) logger = logging.getLogger(__name__) - logger.info(f"RSS数据路径: {self.DATA_PATH}") - logger.info(f"RSS媒体路径: {self.MEDIA_PATH}") + logger.info(f"RSS data path: {self.DATA_PATH}") + logger.info(f"RSS media path: {self.MEDIA_PATH}") -settings = Settings() \ No newline at end of file +settings = Settings() diff --git a/rss/app/crud/entry.py b/rss/app/crud/entry.py index e419d15..a565014 100644 --- a/rss/app/crud/entry.py +++ b/rss/app/crud/entry.py @@ -9,70 +9,70 @@ logger = logging.getLogger(__name__) -# 确保数据存储目录存在 +# Ensure data storage directory exists def ensure_storage_exists(): - """确保数据存储目录存在""" + """Ensure data storage directory exists""" entries_dir = Path(settings.DATA_PATH) entries_dir.mkdir(parents=True, exist_ok=True) -# 获取规则对应的条目存储文件路径 +# Get the entry storage file path for a rule def get_rule_entries_path(rule_id: int) -> Path: - """获取规则对应的条目存储文件路径""" - # 使用规则特定的数据目录 + """Get the entry storage file path for a rule""" + # Use rule-specific data directory rule_data_path = settings.get_rule_data_path(rule_id) return Path(rule_data_path) / "entries.json" async def get_entries(rule_id: int, limit: int = 100, offset: int = 0) -> List[Entry]: - """获取规则对应的条目""" + """Get entries for a rule""" try: file_path = get_rule_entries_path(rule_id) - - # 如果文件不存在,返回空列表 + + # If file does not exist, return empty list if not file_path.exists(): return [] - - # 读取文件内容 + + # Read file content with open(file_path, 'r', encoding='utf-8') as file: data = json.load(file) - - # 将数据转换为Entry对象 + + # Convert data to Entry objects entries = [Entry(**entry) for entry in data] - - # 按发布时间排序(新的在前) + + # Sort by publish time (newest first) entries.sort(key=lambda x: x.published, reverse=True) - - # 应用分页 + + # Apply pagination return entries[offset:offset + limit] except Exception as e: - logger.error(f"获取条目时出错: {str(e)}") + logger.error(f"Error getting entries: {str(e)}") return [] async def create_entry(entry: Entry) -> bool: - """创建新条目""" + """Create a new entry""" try: - # 设置条目ID和创建时间 + # Set entry ID and creation time if not entry.id: entry.id = str(uuid.uuid4()) - + entry.created_at = datetime.now().isoformat() - - # 获取规则对应的条目 + + # Get entries for the rule file_path = get_rule_entries_path(entry.rule_id) - + entries = [] - # 如果文件已存在,读取现有条目 + # If file already exists, read existing entries if file_path.exists(): with open(file_path, 'r', encoding='utf-8') as file: try: entries = json.load(file) except json.JSONDecodeError: - logger.warning(f"解析条目文件时出错,将创建新文件: {file_path}") + logger.warning(f"Error parsing entry file, will create new file: {file_path}") entries = [] - - # 转换Entry对象为字典并添加到列表 + + # Convert Entry object to dictionary and add to list entries.append(entry.dict()) - - # 获取规则的RSS配置,获取最大条目数量 + + # Get RSS configuration for the rule to get maximum entry count try: from models.models import get_session, RSSConfig session = get_session() @@ -80,82 +80,82 @@ async def create_entry(entry: Entry) -> bool: max_items = rss_config.max_items if rss_config and hasattr(rss_config, 'max_items') else 50 session.close() except Exception as e: - logger.warning(f"获取RSS配置失败,使用默认最大条目数量(50): {str(e)}") + logger.warning(f"Failed to get RSS configuration, using default maximum entry count (50): {str(e)}") max_items = 50 - - # 限制条目数量,保留最新的N条 + + # Limit entry count, keep the most recent N entries if len(entries) > max_items: - # 按发布时间排序(新的在前) + # Sort by publish time (newest first) entries.sort(key=lambda x: x.get('published', ''), reverse=True) entries = entries[:max_items] - - # 保存到文件 + + # Save to file with open(file_path, 'w', encoding='utf-8') as file: json.dump(entries, file, ensure_ascii=False, indent=2) - + return True except Exception as e: - logger.error(f"创建条目时出错: {str(e)}") + logger.error(f"Error creating entry: {str(e)}") return False async def update_entry(rule_id: int, entry_id: str, updated_data: Dict[str, Any]) -> bool: - """更新条目""" + """Update an entry""" try: file_path = get_rule_entries_path(rule_id) - - # 如果文件不存在,返回False + + # If file does not exist, return False if not file_path.exists(): return False - - # 读取文件内容 + + # Read file content with open(file_path, 'r', encoding='utf-8') as file: entries = json.load(file) - - # 查找并更新条目 + + # Find and update the entry found = False for i, entry in enumerate(entries): if entry.get('id') == entry_id: entries[i].update(updated_data) found = True break - + if not found: return False - - # 保存到文件 + + # Save to file with open(file_path, 'w', encoding='utf-8') as file: json.dump(entries, file, ensure_ascii=False, indent=2) - + return True except Exception as e: - logger.error(f"更新条目时出错: {str(e)}") + logger.error(f"Error updating entry: {str(e)}") return False async def delete_entry(rule_id: int, entry_id: str) -> bool: - """删除条目""" + """Delete an entry""" try: file_path = get_rule_entries_path(rule_id) - - # 如果文件不存在,返回False + + # If file does not exist, return False if not file_path.exists(): return False - - # 读取文件内容 + + # Read file content with open(file_path, 'r', encoding='utf-8') as file: entries = json.load(file) - - # 查找并删除条目 + + # Find and delete the entry original_length = len(entries) entries = [entry for entry in entries if entry.get('id') != entry_id] - + if len(entries) == original_length: - return False # 没有找到对应ID的条目 - - # 保存到文件 + return False # No entry with the corresponding ID found + + # Save to file with open(file_path, 'w', encoding='utf-8') as file: json.dump(entries, file, ensure_ascii=False, indent=2) - + return True except Exception as e: - logger.error(f"删除条目时出错: {str(e)}") - return False \ No newline at end of file + logger.error(f"Error deleting entry: {str(e)}") + return False diff --git a/rss/app/models/entry.py b/rss/app/models/entry.py index bac0ef4..4d1e562 100644 --- a/rss/app/models/entry.py +++ b/rss/app/models/entry.py @@ -3,7 +3,7 @@ from datetime import datetime class Media(BaseModel): - """媒体文件信息""" + """Media file information""" url: str type: str size: int = 0 @@ -11,27 +11,27 @@ class Media(BaseModel): original_name: Optional[str] = None def get(self, key: str, default: Any = None) -> Any: - """获取属性值,如果不存在返回默认值""" + """Get attribute value, return default if it does not exist""" return getattr(self, key, default) class Entry(BaseModel): - """RSS条目数据模型""" + """RSS entry data model""" id: Optional[str] = None rule_id: int message_id: str title: str content: str - published: str # ISO格式的日期时间字符串 + published: str # ISO format datetime string author: str = "" link: str = "" media: List[Media] = [] - created_at: Optional[str] = None # 添加到系统的时间 + created_at: Optional[str] = None # Time added to the system original_link: Optional[str] = None sender_info: Optional[str] = None - + def __init__(self, **data): - # 处理媒体数据,确保它是Media对象列表 + # Process media data, ensure it is a list of Media objects if "media" in data and isinstance(data["media"], list): media_list = [] for item in data["media"]: @@ -39,19 +39,19 @@ def __init__(self, **data): if isinstance(item, dict): media_list.append(Media(**item)) elif not isinstance(item, Media): - # 尝试转换为字典 + # Try to convert to dictionary if hasattr(item, '__dict__'): media_list.append(Media(**item.__dict__)) else: media_list.append(item) except Exception as e: - # 忽略无法转换的媒体项 + # Ignore media items that cannot be converted pass data["media"] = media_list - - # 确保必要字段有默认值 + + # Ensure required fields have default values if "message_id" not in data and "id" in data: data["message_id"] = data["id"] - - # 调用父类初始化 - super().__init__(**data) \ No newline at end of file + + # Call parent class initialization + super().__init__(**data) diff --git a/rss/app/routes/auth.py b/rss/app/routes/auth.py index a8140b2..8cbdd11 100644 --- a/rss/app/routes/auth.py +++ b/rss/app/routes/auth.py @@ -18,10 +18,10 @@ templates = Jinja2Templates(directory="rss/app/templates") db_ops = None -# JWT 配置 +# JWT configuration SECRET_KEY = secrets.token_hex(32) ALGORITHM = "HS256" -ACCESS_TOKEN_EXPIRE_MINUTES = 1440 # 24小时 +ACCESS_TOKEN_EXPIRE_MINUTES = 1440 # 24 hours def init_db_ops(): global db_ops @@ -50,7 +50,7 @@ async def get_current_user(request: Request): return None except jwt.PyJWTError: return None - + db_session = get_session() try: init_db_ops() @@ -63,10 +63,10 @@ async def get_current_user(request: Request): async def login_page(request: Request, user = Depends(get_current_user)): if user: return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND) - + db_session = get_session() try: - # 检查是否有任何用户存在 + # Check if any users exist users = db_session.query(User).all() if not users: return RedirectResponse(url="/register", status_code=status.HTTP_302_FOUND) @@ -87,15 +87,15 @@ async def login( if not user: return templates.TemplateResponse( "login.html", - {"request": request, "error": "用户名或密码错误"}, + {"request": request, "error": "Incorrect username or password"}, status_code=status.HTTP_401_UNAUTHORIZED ) - + access_token = create_access_token( data={"sub": user.username}, expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) ) - + response = RedirectResponse(url="/", status_code=status.HTTP_302_FOUND) response.set_cookie( key="access_token", @@ -111,7 +111,7 @@ async def login( async def register_page(request: Request): db_session = get_session() try: - # 检查是否已有用户 + # Check if users already exist users = db_session.query(User).all() if users: return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND) @@ -125,14 +125,14 @@ async def register(request: Request): username = form_data.get("username") password = form_data.get("password") confirm_password = form_data.get("confirm_password") - + if password != confirm_password: return templates.TemplateResponse( "register.html", - {"request": request, "error": "两次输入的密码不一致"}, + {"request": request, "error": "Passwords do not match"}, status_code=status.HTTP_400_BAD_REQUEST ) - + db_session = get_session() try: init_db_ops() @@ -140,15 +140,15 @@ async def register(request: Request): if not user: return templates.TemplateResponse( "register.html", - {"request": request, "error": "创建用户失败"}, + {"request": request, "error": "Failed to create user"}, status_code=status.HTTP_400_BAD_REQUEST ) - + access_token = create_access_token( data={"sub": user.username}, expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) ) - + response = RedirectResponse(url="/", status_code=status.HTTP_302_FOUND) response.set_cookie( key="access_token", @@ -170,8 +170,8 @@ async def logout(): async def index(request: Request, user = Depends(get_current_user)): if not user: return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND) - - # 直接重定向到 RSS 仪表盘 + + # Redirect directly to the RSS dashboard return RedirectResponse(url="/rss/dashboard", status_code=status.HTTP_302_FOUND) @router.post("/rss/change_password") @@ -179,47 +179,47 @@ async def change_password( request: Request, user = Depends(get_current_user), ): - """修改用户密码""" + """Change user password""" if not user: return JSONResponse( - {"success": False, "message": "未登录或会话已过期"}, + {"success": False, "message": "Not logged in or session expired"}, status_code=status.HTTP_401_UNAUTHORIZED ) - + try: form_data = await request.form() current_password = form_data.get("current_password") new_password = form_data.get("new_password") confirm_password = form_data.get("confirm_password") - - # 验证表单数据 + + # Validate form data if not current_password: - return JSONResponse({"success": False, "message": "请输入当前密码"}) - + return JSONResponse({"success": False, "message": "Please enter your current password"}) + if not new_password: - return JSONResponse({"success": False, "message": "请输入新密码"}) - + return JSONResponse({"success": False, "message": "Please enter a new password"}) + if len(new_password) < 8: - return JSONResponse({"success": False, "message": "新密码长度必须至少为8个字符"}) - + return JSONResponse({"success": False, "message": "New password must be at least 8 characters long"}) + if new_password != confirm_password: - return JSONResponse({"success": False, "message": "新密码和确认密码不一致"}) - - # 验证当前密码 + return JSONResponse({"success": False, "message": "New password and confirmation password do not match"}) + + # Verify current password db_session = get_session() try: init_db_ops() is_valid = await db_ops.verify_user(db_session, user.username, current_password) if not is_valid: - return JSONResponse({"success": False, "message": "当前密码不正确"}) - - # 更新密码 + return JSONResponse({"success": False, "message": "Current password is incorrect"}) + + # Update password success = await db_ops.update_user_password(db_session, user.username, new_password) if not success: - return JSONResponse({"success": False, "message": "修改密码失败,请重试"}) - - return JSONResponse({"success": True, "message": "密码修改成功"}) + return JSONResponse({"success": False, "message": "Failed to change password, please try again"}) + + return JSONResponse({"success": True, "message": "Password changed successfully"}) finally: db_session.close() except Exception as e: - return JSONResponse({"success": False, "message": f"修改密码出错: {str(e)}"}) \ No newline at end of file + return JSONResponse({"success": False, "message": f"Error changing password: {str(e)}"}) diff --git a/rss/app/routes/rss.py b/rss/app/routes/rss.py index 3d5287d..de9c331 100644 --- a/rss/app/routes/rss.py +++ b/rss/app/routes/rss.py @@ -16,7 +16,7 @@ import aiohttp from utils.constants import RSS_HOST, RSS_PORT, RSS_BASE_URL -# 配置日志 +# Configure logging logger = logging.getLogger(__name__) router = APIRouter(prefix="/rss") @@ -33,29 +33,29 @@ async def init_db_ops(): async def rss_dashboard(request: Request, user = Depends(get_current_user)): if not user: return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND) - + db_session = get_session() try: - # 初始化数据库操作对象 + # Initialize database operations object await init_db_ops() - - # 获取所有RSS配置 + + # Get all RSS configurations rss_configs = db_session.query(RSSConfig).options( joinedload(RSSConfig.rule) ).all() - - # 将 RSSConfig 对象转换为字典列表 + + # Convert RSSConfig objects to a list of dictionaries configs_list = [] for config in rss_configs: - # 处理AI提取提示词,使用Base64编码避免JSON解析问题 + # Process AI extraction prompt, use Base64 encoding to avoid JSON parsing issues ai_prompt = config.ai_extract_prompt ai_prompt_encoded = None if ai_prompt: - # 使用Base64编码处理提示词 + # Use Base64 encoding for the prompt ai_prompt_encoded = base64.b64encode(ai_prompt.encode('utf-8')).decode('utf-8') - # 添加标记,表示这是Base64编码的内容 + # Add marker indicating this is Base64 encoded content ai_prompt_encoded = "BASE64:" + ai_prompt_encoded - + configs_list.append({ "id": config.id, "rule_id": config.rule_id, @@ -72,14 +72,14 @@ async def rss_dashboard(request: Request, user = Depends(get_current_user)): "enable_custom_title_pattern": config.enable_custom_title_pattern, "enable_custom_content_pattern": config.enable_custom_content_pattern }) - - # 获取所有转发规则(用于创建新的RSS配置) + + # Get all forward rules (for creating new RSS configurations) rules = db_session.query(ForwardRule).options( joinedload(ForwardRule.source_chat), joinedload(ForwardRule.target_chat) ).all() - - # 将 ForwardRule 对象转换为字典列表 + + # Convert ForwardRule objects to a list of dictionaries rules_list = [] for rule in rules: rules_list.append({ @@ -93,9 +93,9 @@ async def rss_dashboard(request: Request, user = Depends(get_current_user)): "name": rule.target_chat.name } if rule.target_chat else None }) - + return templates.TemplateResponse( - "rss_dashboard.html", + "rss_dashboard.html", { "request": request, "user": user, @@ -127,26 +127,26 @@ async def rss_config_save( enable_custom_content_pattern: bool = Form(False) ): if not user: - return JSONResponse(content={"success": False, "message": "未登录"}) - - # 记录接收到的AI提取提示词内容,帮助调试 - logger.info(f"接收到的AI提取提示词字符数: {len(ai_extract_prompt)}") - - # 初始化数据库操作 + return JSONResponse(content={"success": False, "message": "Not logged in"}) + + # Log the received AI extraction prompt content for debugging + logger.info(f"Received AI extraction prompt character count: {len(ai_extract_prompt)}") + + # Initialize database operations await init_db_ops() - + db_session = get_session() try: - # 创建或更新RSS配置 - # 如果有config_id,表示更新 + # Create or update RSS configuration + # If config_id exists, it means update if config_id and config_id.strip(): config_id = int(config_id) - # 检查配置是否存在 + # Check if configuration exists rss_config = db_session.query(RSSConfig).filter(RSSConfig.id == config_id).first() if not rss_config: - return JSONResponse(content={"success": False, "message": "配置不存在"}) - - # 更新配置 + return JSONResponse(content={"success": False, "message": "Configuration does not exist"}) + + # Update configuration rss_config.rule_id = rule_id rss_config.enable_rss = enable_rss rss_config.rule_title = rule_title @@ -161,12 +161,12 @@ async def rss_config_save( rss_config.enable_custom_title_pattern = enable_custom_title_pattern rss_config.enable_custom_content_pattern = enable_custom_content_pattern else: - # 检查是否已经存在该规则的配置 + # Check if a configuration already exists for this rule existing_config = db_session.query(RSSConfig).filter(RSSConfig.rule_id == rule_id).first() if existing_config: - return JSONResponse(content={"success": False, "message": "该规则已经存在RSS配置"}) - - # 创建新配置 + return JSONResponse(content={"success": False, "message": "An RSS configuration already exists for this rule"}) + + # Create new configuration rss_config = RSSConfig( rule_id=rule_id, enable_rss=enable_rss, @@ -182,19 +182,19 @@ async def rss_config_save( enable_custom_title_pattern=enable_custom_title_pattern, enable_custom_content_pattern=enable_custom_content_pattern ) - - # 保存配置 + + # Save configuration db_session.add(rss_config) db_session.commit() - + return JSONResponse({ - "success": True, - "message": "RSS 配置已保存", + "success": True, + "message": "RSS configuration saved", "config_id": rss_config.id, "rule_id": rss_config.rule_id }) except Exception as e: - return JSONResponse({"success": False, "message": f"保存配置失败: {str(e)}"}) + return JSONResponse({"success": False, "message": f"Failed to save configuration: {str(e)}"}) finally: db_session.close() @@ -202,29 +202,29 @@ async def rss_config_save( async def toggle_rss(rule_id: int, user = Depends(get_current_user)): if not user: return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND) - + db_session = get_session() try: - # 初始化数据库操作对象 + # Initialize database operations object db_ops_instance = await init_db_ops() - - # 获取配置 + + # Get configuration config = await db_ops_instance.get_rss_config(db_session, rule_id) if not config: return RedirectResponse( - url="/rss/dashboard?error=配置不存在", + url="/rss/dashboard?error=Configuration does not exist", status_code=status.HTTP_302_FOUND ) - - # 切换启用/禁用状态 + + # Toggle enable/disable state await db_ops_instance.update_rss_config( db_session, rule_id, enable_rss=not config.enable_rss ) - + return RedirectResponse( - url="/rss/dashboard?success=RSS状态已切换", + url="/rss/dashboard?success=RSS status toggled", status_code=status.HTTP_302_FOUND ) finally: @@ -234,36 +234,36 @@ async def toggle_rss(rule_id: int, user = Depends(get_current_user)): async def delete_rss(rule_id: int, user = Depends(get_current_user)): if not user: return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND) - + db_session = get_session() try: - # 初始化数据库操作对象 + # Initialize database operations object db_ops_instance = await init_db_ops() - - # 删除配置 + + # Delete configuration config_deleted = await db_ops_instance.delete_rss_config(db_session, rule_id) - + if config_deleted: - # 删除关联的媒体和数据文件 + # Delete associated media and data files try: - logger.info(f"开始删除规则 {rule_id} 的媒体和数据文件") - # 构建删除API的URL + logger.info(f"Starting to delete media and data files for rule {rule_id}") + # Build the delete API URL rss_url = f"http://{RSS_HOST}:{RSS_PORT}/api/rule/{rule_id}" - - # 调用删除API + + # Call the delete API async with aiohttp.ClientSession() as client_session: async with client_session.delete(rss_url) as response: if response.status == 200: - logger.info(f"成功删除规则 {rule_id} 的媒体和数据文件") + logger.info(f"Successfully deleted media and data files for rule {rule_id}") else: response_text = await response.text() - logger.warning(f"删除规则 {rule_id} 的媒体和数据文件失败, 状态码: {response.status}, 响应: {response_text}") + logger.warning(f"Failed to delete media and data files for rule {rule_id}, status code: {response.status}, response: {response_text}") except Exception as e: - logger.error(f"调用删除媒体文件API时出错: {str(e)}") - # 不影响主流程,继续执行 - + logger.error(f"Error calling delete media files API: {str(e)}") + # Does not affect the main flow, continue execution + return RedirectResponse( - url="/rss/dashboard?success=RSS配置已删除", + url="/rss/dashboard?success=RSS configuration deleted", status_code=status.HTTP_302_FOUND ) finally: @@ -271,21 +271,21 @@ async def delete_rss(rule_id: int, user = Depends(get_current_user)): @router.get("/patterns/{config_id}") async def get_patterns(config_id: int, user = Depends(get_current_user)): - """获取指定RSS配置的所有模式""" + """Get all patterns for a specified RSS configuration""" if not user: - return JSONResponse({"success": False, "message": "未登录"}, status_code=status.HTTP_401_UNAUTHORIZED) - + return JSONResponse({"success": False, "message": "Not logged in"}, status_code=status.HTTP_401_UNAUTHORIZED) + db_session = get_session() try: - # 初始化数据库操作对象 + # Initialize database operations object db_ops_instance = await init_db_ops() - - # 获取所有正则表达式数据 + + # Get all regex data config = await db_ops_instance.get_rss_config_with_patterns(db_session, config_id) if not config: - return JSONResponse({"success": False, "message": "配置不存在"}, status_code=status.HTTP_404_NOT_FOUND) - - # 将模式转换为JSON格式 + return JSONResponse({"success": False, "message": "Configuration does not exist"}, status_code=status.HTTP_404_NOT_FOUND) + + # Convert patterns to JSON format patterns = [] for pattern in config.patterns: patterns.append({ @@ -294,7 +294,7 @@ async def get_patterns(config_id: int, user = Depends(get_current_user)): "pattern_type": pattern.pattern_type, "priority": pattern.priority }) - + return JSONResponse({"success": True, "patterns": patterns}) finally: db_session.close() @@ -309,29 +309,29 @@ async def save_pattern( pattern_type: str = Form(...), priority: int = Form(0) ): - """保存模式""" - logger.info(f"开始保存模式,参数:config_id={rss_config_id}, pattern={pattern}, type={pattern_type}, priority={priority}") - + """Save pattern""" + logger.info(f"Starting to save pattern, parameters: config_id={rss_config_id}, pattern={pattern}, type={pattern_type}, priority={priority}") + if not user: - logger.warning("未登录的访问尝试") - return JSONResponse({"success": False, "message": "未登录"}, status_code=status.HTTP_401_UNAUTHORIZED) - + logger.warning("Unauthenticated access attempt") + return JSONResponse({"success": False, "message": "Not logged in"}, status_code=status.HTTP_401_UNAUTHORIZED) + db_session = get_session() try: - # 初始化数据库操作对象 + # Initialize database operations object db_ops_instance = await init_db_ops() - - # 检查RSS配置是否存在 + + # Check if RSS configuration exists config = await db_ops_instance.get_rss_config(db_session, rss_config_id) if not config: - logger.error(f"RSS配置不存在:config_id={rss_config_id}") - return JSONResponse({"success": False, "message": "RSS配置不存在"}) - - logger.debug(f"找到RSS配置:{config}") - - - logger.info("创建新模式") - # 创建新模式 + logger.error(f"RSS configuration does not exist: config_id={rss_config_id}") + return JSONResponse({"success": False, "message": "RSS configuration does not exist"}) + + logger.debug(f"Found RSS configuration: {config}") + + + logger.info("Creating new pattern") + # Create new pattern try: pattern_obj = await db_ops_instance.create_rss_pattern( db_session, @@ -340,125 +340,125 @@ async def save_pattern( pattern_type=pattern_type, priority=priority ) - logger.info(f"新模式创建成功:{pattern_obj}") - return JSONResponse({"success": True, "message": "模式已创建", "pattern_id": pattern_obj.id}) + logger.info(f"New pattern created successfully: {pattern_obj}") + return JSONResponse({"success": True, "message": "Pattern created", "pattern_id": pattern_obj.id}) except Exception as e: - logger.error(f"创建模式失败:{str(e)}") + logger.error(f"Failed to create pattern: {str(e)}") raise except Exception as e: - logger.error(f"保存模式时发生错误:{str(e)}", exc_info=True) - return JSONResponse({"success": False, "message": f"保存模式失败: {str(e)}"}) + logger.error(f"Error occurred while saving pattern: {str(e)}", exc_info=True) + return JSONResponse({"success": False, "message": f"Failed to save pattern: {str(e)}"}) finally: db_session.close() @router.delete("/pattern/{pattern_id}") async def delete_pattern(pattern_id: int, user = Depends(get_current_user)): - """删除模式""" + """Delete pattern""" if not user: - return JSONResponse({"success": False, "message": "未登录"}, status_code=status.HTTP_401_UNAUTHORIZED) - + return JSONResponse({"success": False, "message": "Not logged in"}, status_code=status.HTTP_401_UNAUTHORIZED) + db_session = get_session() try: - # 初始化数据库操作对象 + # Initialize database operations object await init_db_ops() - - # 查询模式 + + # Query pattern pattern = db_session.query(RSSPattern).filter(RSSPattern.id == pattern_id).first() if not pattern: - return JSONResponse({"success": False, "message": "找不到该模式"}) - - # 删除模式 + return JSONResponse({"success": False, "message": "Pattern not found"}) + + # Delete pattern db_session.delete(pattern) db_session.commit() - - return JSONResponse({"success": True, "message": "模式删除成功"}) + + return JSONResponse({"success": True, "message": "Pattern deleted successfully"}) except Exception as e: db_session.rollback() - logger.error(f"删除模式时出错: {str(e)}") - return JSONResponse({"success": False, "message": f"删除模式失败: {str(e)}"}) + logger.error(f"Error deleting pattern: {str(e)}") + return JSONResponse({"success": False, "message": f"Failed to delete pattern: {str(e)}"}) finally: db_session.close() @router.delete("/patterns/{config_id}") async def delete_all_patterns(config_id: int, user = Depends(get_current_user)): - """删除配置的所有模式,通常在更新前调用以便重建模式列表""" + """Delete all patterns for a configuration, typically called before updating to rebuild the pattern list""" if not user: - return JSONResponse({"success": False, "message": "未登录"}, status_code=status.HTTP_401_UNAUTHORIZED) - + return JSONResponse({"success": False, "message": "Not logged in"}, status_code=status.HTTP_401_UNAUTHORIZED) + db_session = get_session() try: - # 初始化数据库操作对象 + # Initialize database operations object await init_db_ops() - - # 查询并删除指定配置的所有模式 + + # Query and delete all patterns for the specified configuration patterns = db_session.query(RSSPattern).filter(RSSPattern.rss_config_id == config_id).all() count = len(patterns) for pattern in patterns: db_session.delete(pattern) - + db_session.commit() - logger.info(f"已删除配置 {config_id} 的所有模式,共 {count} 个") - - return JSONResponse({"success": True, "message": f"已删除 {count} 个模式"}) + logger.info(f"Deleted all patterns for configuration {config_id}, total: {count}") + + return JSONResponse({"success": True, "message": f"Deleted {count} patterns"}) except Exception as e: db_session.rollback() - logger.error(f"删除配置 {config_id} 的所有模式时出错: {str(e)}") - return JSONResponse({"success": False, "message": f"删除所有模式失败: {str(e)}"}) + logger.error(f"Error deleting all patterns for configuration {config_id}: {str(e)}") + return JSONResponse({"success": False, "message": f"Failed to delete all patterns: {str(e)}"}) finally: db_session.close() @router.post("/test-regex") -async def test_regex(user = Depends(get_current_user), - pattern: str = Form(...), - test_text: str = Form(...), +async def test_regex(user = Depends(get_current_user), + pattern: str = Form(...), + test_text: str = Form(...), pattern_type: str = Form(...)): - """测试正则表达式匹配结果""" + """Test regex matching results""" if not user: - return JSONResponse({"success": False, "message": "未登录"}, status_code=status.HTTP_401_UNAUTHORIZED) - + return JSONResponse({"success": False, "message": "Not logged in"}, status_code=status.HTTP_401_UNAUTHORIZED) + try: - - - # 记录测试信息 - logger.info(f"测试正则表达式: {pattern}") - logger.info(f"测试类型: {pattern_type}") - logger.info(f"测试文本长度: {len(test_text)} 字符") - - # 执行正则匹配 + + + # Log test information + logger.info(f"Testing regex: {pattern}") + logger.info(f"Test type: {pattern_type}") + logger.info(f"Test text length: {len(test_text)} characters") + + # Execute regex matching match = re.search(pattern, test_text) - - # 检查是否有匹配 + + # Check if there is a match if not match: return JSONResponse({ "success": True, "matched": False, - "message": "未找到匹配" + "message": "No match found" }) - - # 检查捕获组 + + # Check capture groups if not match.groups(): return JSONResponse({ "success": True, "matched": True, "has_groups": False, - "message": "匹配成功,但没有捕获组。请使用括号 () 来创建捕获组。" + "message": "Match successful, but no capture groups. Please use parentheses () to create capture groups." }) - - # 成功匹配且有捕获组 + + # Successfully matched with capture groups extracted_content = match.group(1) - - # 返回匹配结果 + + # Return match results return JSONResponse({ "success": True, "matched": True, "has_groups": True, "extracted": extracted_content, - "message": "匹配成功!" + "message": "Match successful!" }) - + except Exception as e: - logger.error(f"测试正则表达式时出错: {str(e)}") + logger.error(f"Error testing regex: {str(e)}") return JSONResponse({ "success": False, - "message": f"测试失败: {str(e)}" - }) \ No newline at end of file + "message": f"Test failed: {str(e)}" + }) diff --git a/rss/app/services/feed_generator.py b/rss/app/services/feed_generator.py index c5e3e17..6081664 100644 --- a/rss/app/services/feed_generator.py +++ b/rss/app/services/feed_generator.py @@ -11,171 +11,171 @@ import json from models.models import get_session, RSSConfig from utils.constants import DEFAULT_TIMEZONE -import pytz +import pytz logger = logging.getLogger(__name__) class FeedService: - - + + @staticmethod def extract_telegram_title_and_content(content: str) -> tuple[str, str]: - """从Telegram消息中提取标题和内容 - + """Extract title and content from a Telegram message + Args: - content: 原始消息内容 - + content: Original message content + Returns: - tuple: (标题, 剩余内容) + tuple: (title, remaining content) """ if not content: - logger.info("输入内容为空,返回空标题和内容") + logger.info("Input content is empty, returning empty title and content") return "", "" - + try: - # 读取标题模板配置 + # Read title template configuration config_path = Path(__file__).parent.parent / 'configs' / 'title_template.json' - logger.info(f"正在读取标题模板配置文件: {config_path}") + logger.info(f"Reading title template configuration file: {config_path}") with open(config_path, 'r', encoding='utf-8') as f: title_config = json.load(f) - - # 遍历每个模式 + + # Iterate through each pattern for pattern_info in title_config['patterns']: pattern_str = pattern_info['pattern'] pattern_desc = pattern_info['description'] - logger.debug(f"尝试匹配模式: {pattern_desc} ({pattern_str})") - - # 编译正则表达式 + logger.debug(f"Trying to match pattern: {pattern_desc} ({pattern_str})") + + # Compile regex pattern = re.compile(pattern_str, re.MULTILINE) - - # 尝试匹配 + + # Try to match match = pattern.match(content) if match: title = FeedService.clean_title(match.group(1)) - # 获取匹配部分的起始和结束位置 + # Get the start and end positions of the matched portion start, end = match.span(0) - # 提取剩余内容,去除开头的空白字符 + # Extract remaining content, strip leading whitespace remaining_content = content[end:].lstrip() - logger.info(f"成功匹配到标题模式: {pattern_desc}") - logger.info(f"原始内容: {content[:100]}...") # 只显示前100个字符 - logger.info(f"匹配模式: {pattern_str}") - logger.info(f"提取的标题: {title}") - logger.info(f"剩余内容长度: {len(remaining_content)} 字符") + logger.info(f"Successfully matched title pattern: {pattern_desc}") + logger.info(f"Original content: {content[:100]}...") # Only show first 100 characters + logger.info(f"Matched pattern: {pattern_str}") + logger.info(f"Extracted title: {title}") + logger.info(f"Remaining content length: {len(remaining_content)} characters") return title, remaining_content - - # 如果没有匹配到任何模式,使用前20个字符作为标题 - logger.info("未匹配到任何标题模式,使用前20个字符作为标题") - # 去除内容中的换行符,并限制标题长度为20个字符 + + # If no pattern matched, use the first 20 characters as the title + logger.info("No title pattern matched, using first 20 characters as title") + # Remove line breaks from content and limit title length to 20 characters clean_content = FeedService.clean_content(content) clean_content = clean_content.replace('\n', ' ').strip() title = clean_content[:20] if len(clean_content) > 20: title += "..." - logger.debug(f"生成的默认标题: {title}") + logger.debug(f"Generated default title: {title}") return title, content - + except Exception as e: - logger.error(f"提取标题和内容时出错: {str(e)}") + logger.error(f"Error extracting title and content: {str(e)}") return "", content - + @staticmethod def clean_title(title: str) -> str: - """清理标题中的特殊字符和格式标记 - + """Clean special characters and formatting marks from the title + Args: - title: 原始标题文本 - + title: Original title text + Returns: - str: 清理后的标题 + str: Cleaned title """ if not title: return "" - - # 移除所有 * 号 + + # Remove all * characters title = title.replace('*', '') - - # 处理链接格式 [text](url),保留text部分 + + # Handle link format [text](url), keep the text part title = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', title) - - # 移除换行和首尾空白 + + # Remove line breaks and leading/trailing whitespace title = title.replace('\n', ' ').strip() - + return title - + @staticmethod def clean_content(content: str) -> str: - """清理内容中的特殊字符和格式标记 - + """Clean special characters and formatting marks from the content + Args: - content: 原始内容文本 - + content: Original content text + Returns: - str: 清理后的内容 + str: Cleaned content """ if not content: return "" - - # 去除开头可能的1-2个星号 + + # Remove possible 1-2 asterisks at the beginning content = re.sub(r'^\*{1,2}\s*', '', content) - - # 去除开头的空行 + + # Remove empty lines at the beginning content = re.sub(r'^\s*\n+', '', content) - + return content - + @staticmethod async def generate_feed_from_entries(rule_id: int, entries: List[Entry], base_url: str = None) -> FeedGenerator: - """根据真实条目生成Feed""" + """Generate Feed from real entries""" 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}, 条目数量: {len(entries)}, 基础URL: {base_url}") - + + logger.info(f"Generating Feed - Rule ID: {rule_id}, Entry count: {len(entries)}, Base URL: {base_url}") + session = get_session() try: rss_config = session.query(RSSConfig).filter(RSSConfig.rule_id == rule_id).first() - logger.info(f"获取RSS配置: {rss_config.__dict__}") - # 获取 Feed 标题和描述 + logger.info(f"Retrieved RSS config: {rss_config.__dict__}") + # Get Feed title and description if rss_config and rss_config.enable_rss: if rss_config.rule_title: fg.title(rss_config.rule_title) else: fg.title(f'TG Forwarder RSS - Rule {rule_id}') - + if rss_config.rule_description: fg.description(rss_config.rule_description) else: - fg.description(f'TG Forwarder RSS - 规则 {rule_id} 的消息') - - # 设置语言 + fg.description(f'TG Forwarder RSS - Messages for rule {rule_id}') + + # Set language fg.language(rss_config.language or 'zh-CN') else: - # 默认标题和描述 + # Default title and description fg.title(f'TG Forwarder RSS - Rule {rule_id}') - fg.description(f'TG Forwarder RSS - 规则 {rule_id} 的消息') + fg.description(f'TG Forwarder RSS - Messages for rule {rule_id}') fg.language('zh-CN') finally: - # 确保会话被关闭 + # Ensure session is closed session.close() - - # 设置Feed链接 + + # Set Feed link fg.link(href=f'{base_url}/rss/feed/{rule_id}') - - # 添加条目 + + # Add entries for entry in entries: try: fe = fg.add_entry() fe.id(entry.id or entry.message_id) - # 初始化content变量 + # Initialize content variable content = None fe.title(entry.title) @@ -187,7 +187,7 @@ async def generate_feed_from_entries(rule_id: int, entries: List[Entry], base_ur fe.title(entry.title) if rss_config.enable_custom_content_pattern: content = entry.content - # 自动提取标题和内容 + # Auto-extract title and content if rss_config.is_auto_title or rss_config.is_auto_content: extracted_title, extracted_content = FeedService.extract_telegram_title_and_content(entry.content or "") if rss_config.is_auto_title: @@ -195,120 +195,120 @@ async def generate_feed_from_entries(rule_id: int, entries: List[Entry], base_ur if rss_config.is_auto_content: content = FeedService.convert_markdown_to_html(extracted_content) else: - # 如果不自动提取内容,使用原始内容 + # If not auto-extracting content, use original content content = FeedService.convert_markdown_to_html(entry.content or "") else: - # 如果不是自动提取,直接使用原始内容 + # If not auto-extracting, use original content directly content = FeedService.convert_markdown_to_html(entry.content or "") - # 添加图片 - 针对各种RSS阅读器的优化处理 - all_media_urls = [] # 存储所有媒体URL用于后续检查 - + # Add images - optimized handling for various RSS readers + all_media_urls = [] # Store all media URLs for subsequent checks + if entry.media: - logger.info(f"处理条目 {entry.id} 的媒体文件,数量: {len(entry.media)}") - # 处理每个媒体文件 + logger.info(f"Processing media files for entry {entry.id}, count: {len(entry.media)}") + # Process each media file for idx, media in enumerate(entry.media): - # 记录原始媒体URL - original_url = media.url if hasattr(media, 'url') else "未知" - logger.info(f"媒体 {idx+1}/{len(entry.media)} - 原始URL: {original_url}") - - # 构建规范化的媒体URL - 恢复为包含规则ID的格式 + # Log original media URL + original_url = media.url if hasattr(media, 'url') else "unknown" + logger.info(f"Media {idx+1}/{len(entry.media)} - Original URL: {original_url}") + + # Build normalized media URL - restored to format including rule ID media_filename = os.path.basename(media.url.split('/')[-1]) media_url = f"/media/{entry.rule_id}/{media_filename}" full_media_url = f"{base_url}{media_url}" all_media_urls.append(full_media_url) - - logger.info(f"媒体 {idx+1}/{len(entry.media)} - 新URL: {full_media_url}") - - # 处理图片类型 + + logger.info(f"Media {idx+1}/{len(entry.media)} - New URL: {full_media_url}") + + # Handle image types if media.type.startswith('image/'): try: - # 构建媒体文件路径 + # Build media file path rule_media_path = settings.get_rule_media_path(entry.rule_id) media_path = os.path.join(rule_media_path, media_filename) - - # 添加图片标签到内容中 - 使用包含规则ID的URL格式 + + # Add image tag to content - using URL format with rule ID img_tag = f'

{media.filename}

' content += img_tag - - logger.info(f"已添加图片标签到内容中: {media_filename}") + + logger.info(f"Added image tag to content: {media_filename}") except Exception as e: - logger.error(f"添加图片标签时出错: {str(e)}") + logger.error(f"Error adding image tag: {str(e)}") elif media.type.startswith('video/'): - # 为视频添加特殊处理 + # Special handling for videos display_name = "" if hasattr(media, "original_name") and media.original_name: display_name = media.original_name else: display_name = media.filename - - # 添加HTML5视频播放器 - 使用内联样式 + + # Add HTML5 video player - using inline styles video_player = f'''

- 下载视频: {display_name} + Download video: {display_name}

''' content += video_player - - logger.info(f"添加视频播放器到内容中: {display_name}") + + logger.info(f"Added video player to content: {display_name}") elif media.type.startswith('audio/'): - # 为音频添加特殊处理 + # Special handling for audio display_name = "" if hasattr(media, "original_name") and media.original_name: display_name = media.original_name else: display_name = media.filename - - # 添加HTML5音频播放器 - 使用内联样式 + + # Add HTML5 audio player - using inline styles audio_player = f'''

- 下载音频: {display_name} + Download audio: {display_name}

''' content += audio_player - - logger.info(f"添加音频播放器到内容中: {display_name}") + + logger.info(f"Added audio player to content: {display_name}") else: - # 其他类型文件添加下载链接 + # Add download link for other file types display_name = "" if hasattr(media, "original_name") and media.original_name: display_name = media.original_name else: display_name = media.filename - - # 添加美观的下载链接 + + # Add styled download link file_tag = 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'

{media.filename}

' 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.

#
- #

此测试条目生成于: {current_time.strftime('%Y-%m-%d %H:%M:%S %Z')}

+ #

This test entry was generated at: {current_time.strftime('%Y-%m-%d %H:%M:%S %Z')}

# ''' - - # # 设置内容和描述 + + # # Set content and description # fe.content(content, type='html') # fe.description(content) - - # # 设置测试条目的发布时间 + + # # Set test entry publish time # fe.published(datetime.now(tz)) - - # # 设置测试条目的作者和链接 + + # # Set test entry author and link # fe.author(name="TG Forwarder System") - - # # 使用正确的URL格式 + + # # Use correct URL format # entry_url = f"{base_url}/rss/feed/{rule_id}?entry={entry_id}" - # logger.info(f"添加测试条目链接: {entry_url}") + # logger.info(f"Added test entry link: {entry_url}") # fe.link(href=entry_url) - - # logger.info(f"成功添加测试条目") + + # logger.info(f"Successfully added test entry") # except Exception as e: - # logger.error(f"添加测试条目时出错: {str(e)}") - - # logger.info(f"测试Feed生成完成,包含1个测试条目") - return fg \ No newline at end of file + # logger.error(f"Error adding test entry: {str(e)}") + + # logger.info(f"Test Feed generation completed, containing 1 test entry") + return fg diff --git a/rss/app/templates/login.html b/rss/app/templates/login.html index 24bbc4d..659c8f0 100644 --- a/rss/app/templates/login.html +++ b/rss/app/templates/login.html @@ -1,7 +1,7 @@ - 登录 - TG Forwarder RSS + Login - TG Forwarder RSS @@ -129,18 +129,18 @@

TG Forwarder RSS

{% endif %}
- - + +
- - + +
diff --git a/rss/app/templates/register.html b/rss/app/templates/register.html index 1b7c395..6342a7b 100644 --- a/rss/app/templates/register.html +++ b/rss/app/templates/register.html @@ -1,7 +1,7 @@ - 创建用户 - TG Forwarder RSS + Create User - TG Forwarder RSS @@ -14,7 +14,7 @@ align-items: center; } .card { - opacity: 0; /* 初始设置为不可见 */ + opacity: 0; /* Initially set to invisible */ border: none; border-radius: 15px; box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23); @@ -63,7 +63,7 @@ padding: 20px; } .brand-logo { - opacity: 0; /* 初始设置为不可见 */ + opacity: 0; /* Initially set to invisible */ text-align: center; margin-bottom: 30px; } @@ -116,7 +116,7 @@ } form { - opacity: 0; /* 初始设置为不可见 */ + opacity: 0; /* Initially set to invisible */ } @@ -136,24 +136,24 @@

TG Forwarder RSS

{% endif %}
- - + +
- - + +
- - + +
@@ -171,14 +171,14 @@

TG Forwarder RSS

- +
-
正则表达式配置
- - +
Regex Configuration
+ +
- +
- +
- - + +
- - + +
- +
- +
-
优先级说明:数字越低优先级越高,越先执行
- - +
Priority note: lower numbers have higher priority and are executed first
+ +
- +
-
正则表达式测试
-

在这里输入示例文本并测试您的正则表达式是否能正确匹配并提取内容

- +
Regex Test
+

Enter sample text here and test whether your regex can correctly match and extract content

+
- - + +
- +
- +
- +
-
记得使用括号 () 创建捕获组来提取内容
+
Remember to use parentheses () to create capture groups for extracting content
- + - +
- - + +
- +
- - - + + + - +
模式优先级操作PatternPriorityActions
-

暂无标题模式配置

+

No title patterns configured yet

- - + +
- - - + + + - +
模式优先级操作PatternPriorityActions
-

暂无内容模式配置

+

No content patterns configured yet

@@ -983,135 +983,135 @@
正则表达式测试
- + - +
- 已复制 RSS 链接到剪贴板 + RSS link copied to clipboard
- \ No newline at end of file + diff --git a/rss/main.py b/rss/main.py index f0291fd..89e5138 100644 --- a/rss/main.py +++ b/rss/main.py @@ -17,25 +17,25 @@ sys.path.append(str(root_dir)) -# 获取日志记录器 +# Get logger logger = logging.getLogger(__name__) app = FastAPI(title="TG Forwarder RSS") -# 注册路由 +# Register routes app.include_router(auth_router) app.include_router(rss_router) app.include_router(feed.router) -# 模板配置 +# Template configuration templates = Jinja2Templates(directory="rss/app/templates") def run_server(host: str = "0.0.0.0", port: int = 8000): - """运行 RSS 服务器""" + """Run the RSS server""" uvicorn.run(app, host=host, port=port) -# 添加直接运行支持 +# Add direct run support if __name__ == "__main__": - # 只有在直接运行时才设置日志(而不是被导入时) + # Only set up logging when running directly (not when imported) setup_logging() - run_server() \ No newline at end of file + run_server() diff --git a/scheduler/chat_updater.py b/scheduler/chat_updater.py index ba93b6b..77b5a46 100644 --- a/scheduler/chat_updater.py +++ b/scheduler/chat_updater.py @@ -15,138 +15,138 @@ def __init__(self, user_client: TelegramClient): self.user_client = user_client self.timezone = pytz.timezone(DEFAULT_TIMEZONE) self.task = None - # 从环境变量获取更新时间,默认凌晨3点 + # Get update time from environment variable, default is 3:00 AM self.update_time = os.getenv('CHAT_UPDATE_TIME', "03:00") - + async def start(self): - """启动定时更新任务""" - logger.info("开始启动聊天信息更新器...") + """Start the scheduled update task""" + logger.info("Starting chat info updater...") try: - # 计算下一次执行时间 + # Calculate the next execution time now = datetime.now(self.timezone) next_time = self._get_next_run_time(now, self.update_time) wait_seconds = (next_time - now).total_seconds() - - logger.info(f"下一次聊天信息更新时间: {next_time.strftime('%Y-%m-%d %H:%M:%S')}") - logger.info(f"等待时间: {wait_seconds:.2f} 秒") - - # 创建定时任务 + + logger.info(f"Next chat info update time: {next_time.strftime('%Y-%m-%d %H:%M:%S')}") + logger.info(f"Wait time: {wait_seconds:.2f} seconds") + + # Create scheduled task self.task = asyncio.create_task(self._run_update_task()) - logger.info("聊天信息更新器启动完成") + logger.info("Chat info updater startup completed") except Exception as e: - logger.error(f"启动聊天信息更新器时出错: {str(e)}") - logger.error(f"错误详情: {traceback.format_exc()}") - + logger.error(f"Error starting chat info updater: {str(e)}") + logger.error(f"Error details: {traceback.format_exc()}") + def _get_next_run_time(self, now, target_time): - """计算下一次运行时间""" + """Calculate the next run time""" hour, minute = map(int, target_time.split(':')) next_time = now.replace(hour=hour, minute=minute, second=0, microsecond=0) - + if next_time <= now: next_time += timedelta(days=1) - + return next_time - + async def _run_update_task(self): - """运行更新任务""" + """Run the update task""" while True: try: - # 计算下一次执行时间 + # Calculate the next execution time now = datetime.now(self.timezone) target_time = self._get_next_run_time(now, self.update_time) - - # 等待到执行时间 + + # Wait until execution time wait_seconds = (target_time - now).total_seconds() await asyncio.sleep(wait_seconds) - - # 执行更新任务 + + # Execute the update task await self._update_all_chats() - + except asyncio.CancelledError: - logger.info("聊天信息更新任务已取消") + logger.info("Chat info update task has been cancelled") break except Exception as e: - logger.error(f"聊天信息更新任务出错: {str(e)}") - logger.error(f"错误详情: {traceback.format_exc()}") - await asyncio.sleep(60) # 出错后等待一分钟再重试 - + logger.error(f"Chat info update task encountered an error: {str(e)}") + logger.error(f"Error details: {traceback.format_exc()}") + await asyncio.sleep(60) # Wait one minute before retrying after an error + async def _update_all_chats(self): - """更新所有聊天信息""" - logger.info("开始更新所有聊天信息...") + """Update all chat info""" + logger.info("Starting to update all chat info...") session = get_session() try: - # 获取所有聊天 + # Get all chats chats = session.query(Chat).all() total_chats = len(chats) - logger.info(f"找到 {total_chats} 个聊天需要更新信息") - + logger.info(f"Found {total_chats} chats that need info updates") + updated_count = 0 skipped_count = 0 error_count = 0 - - # 处理每个聊天 + + # Process each chat for i, chat in enumerate(chats, 1): try: - # 每10个聊天报告一次进度 + # Report progress every 10 chats if i % 10 == 0 or i == total_chats: - logger.info(f"进度: {i}/{total_chats} ({i/total_chats*100:.1f}%)") - + logger.info(f"Progress: {i}/{total_chats} ({i/total_chats*100:.1f}%)") + chat_id = chat.telegram_chat_id - # 尝试获取聊天实体 + # Try to get chat entity try: - # 尝试转换聊天ID为整数 + # Try to convert chat ID to integer try: chat_id_int = int(chat_id) except ValueError: - logger.warning(f"聊天ID '{chat_id}' 不是有效的数字格式") + logger.warning(f"Chat ID '{chat_id}' is not a valid numeric format") skipped_count += 1 continue - + entity = await self.user_client.get_entity(chat_id_int) - # 更新聊天名称 + # Update chat name new_name = entity.title if hasattr(entity, 'title') else ( - f"{entity.first_name} {entity.last_name}" if hasattr(entity, 'last_name') and entity.last_name - else entity.first_name if hasattr(entity, 'first_name') - else "私聊" + f"{entity.first_name} {entity.last_name}" if hasattr(entity, 'last_name') and entity.last_name + else entity.first_name if hasattr(entity, 'first_name') + else "Private Chat" ) - - # 只有当名称有变化时才更新 + + # Only update when the name has changed if chat.name != new_name: - old_name = chat.name or "未命名" + old_name = chat.name or "Unnamed" chat.name = new_name session.commit() - logger.info(f"已更新聊天 {chat_id}: {old_name} -> {new_name}") + logger.info(f"Updated chat {chat_id}: {old_name} -> {new_name}") updated_count += 1 else: skipped_count += 1 - + except ValueError as e: - logger.warning(f"无法获取聊天 {chat_id} 的信息: 无效的ID格式 - {str(e)}") + logger.warning(f"Unable to get info for chat {chat_id}: Invalid ID format - {str(e)}") skipped_count += 1 continue except Exception as e: - logger.warning(f"无法获取聊天 {chat_id} 的信息: {str(e)}") + logger.warning(f"Unable to get info for chat {chat_id}: {str(e)}") skipped_count += 1 continue - + except Exception as e: - logger.error(f"处理聊天 {chat.telegram_chat_id} 时出错: {str(e)}") + logger.error(f"Error processing chat {chat.telegram_chat_id}: {str(e)}") error_count += 1 continue - - # 每个聊天处理后暂停一会,避免请求过于频繁 + + # Pause briefly after processing each chat to avoid excessive request frequency await asyncio.sleep(1) - - logger.info(f"聊天信息更新完成。总计: {total_chats}, 更新: {updated_count}, 跳过: {skipped_count}, 错误: {error_count}") - + + logger.info(f"Chat info update completed. Total: {total_chats}, Updated: {updated_count}, Skipped: {skipped_count}, Errors: {error_count}") + except Exception as e: - logger.error(f"更新聊天信息时出错: {str(e)}") - logger.error(f"错误详情: {traceback.format_exc()}") + logger.error(f"Error updating chat info: {str(e)}") + logger.error(f"Error details: {traceback.format_exc()}") finally: session.close() - + def stop(self): - """停止定时任务""" + """Stop the scheduled task""" if self.task: self.task.cancel() - logger.info("聊天信息更新任务已停止") \ No newline at end of file + logger.info("Chat info update task has been stopped") diff --git a/scheduler/summary_scheduler.py b/scheduler/summary_scheduler.py index 4b10edf..0d1fbbf 100644 --- a/scheduler/summary_scheduler.py +++ b/scheduler/summary_scheduler.py @@ -21,67 +21,67 @@ class SummaryScheduler: def __init__(self, user_client: TelegramClient, bot_client: TelegramClient): - self.tasks = {} # 存储所有定时任务 {rule_id: task} + self.tasks = {} # Store all scheduled tasks {rule_id: task} self.timezone = pytz.timezone(DEFAULT_TIMEZONE) self.user_client = user_client self.bot_client = bot_client - # 添加信号量来限制并发请求 - self.request_semaphore = asyncio.Semaphore(2) # 最多同时执行2个请求 - # 从环境变量获取配置 + # Add semaphore to limit concurrent requests + self.request_semaphore = asyncio.Semaphore(2) # Execute at most 2 requests simultaneously + # Get configuration from environment variables self.batch_size = int(os.getenv('SUMMARY_BATCH_SIZE', 20)) self.batch_delay = int(os.getenv('SUMMARY_BATCH_DELAY', 2)) async def schedule_rule(self, rule): - """为规则创建或更新定时任务""" + """Create or update a scheduled task for a rule""" try: - # 如果规则已有任务,先取消 + # If the rule already has a task, cancel it first if rule.id in self.tasks: old_task = self.tasks[rule.id] old_task.cancel() - logger.info(f"已取消规则 {rule.id} 的旧任务") + logger.info(f"Cancelled old task for rule {rule.id}") del self.tasks[rule.id] - # 如果启用了AI总结,创建新任务 + # If AI summary is enabled, create a new task if rule.is_summary: - # 计算下一次执行时间 + # Calculate the next execution time now = datetime.now(self.timezone) next_time = self._get_next_run_time(now, rule.summary_time) wait_seconds = (next_time - now).total_seconds() - logger.info(f"规则 {rule.id} 的下一次执行时间: {next_time.strftime('%Y-%m-%d %H:%M:%S')}") - logger.info(f"等待时间: {wait_seconds:.2f} 秒") + logger.info(f"Next execution time for rule {rule.id}: {next_time.strftime('%Y-%m-%d %H:%M:%S')}") + logger.info(f"Wait time: {wait_seconds:.2f} seconds") task = asyncio.create_task(self._run_summary_task(rule)) self.tasks[rule.id] = task - logger.info(f"已为规则 {rule.id} 创建新的总结任务,时间: {rule.summary_time}") + logger.info(f"Created new summary task for rule {rule.id}, time: {rule.summary_time}") else: - logger.info(f"规则 {rule.id} 的总结功能已关闭,不创建新任务") + logger.info(f"Summary feature for rule {rule.id} is disabled, not creating a new task") except Exception as e: - logger.error(f"调度规则 {rule.id} 时出错: {str(e)}") - logger.error(f"错误详情: {traceback.format_exc()}") + logger.error(f"Error scheduling rule {rule.id}: {str(e)}") + logger.error(f"Error details: {traceback.format_exc()}") async def _run_summary_task(self, rule): - """运行单个规则的总结任务""" + """Run the summary task for a single rule""" while True: try: - # 计算下一次执行时间 + # Calculate the next execution time now = datetime.now(self.timezone) target_time = self._get_next_run_time(now, rule.summary_time) - # 等待到执行时间 + # Wait until execution time wait_seconds = (target_time - now).total_seconds() await asyncio.sleep(wait_seconds) - # 执行总结任务 + # Execute the summary task await self._execute_summary(rule.id) except asyncio.CancelledError: - logger.info(f"规则 {rule.id} 的旧任务已取消") + logger.info(f"Old task for rule {rule.id} has been cancelled") break except Exception as e: - logger.error(f"规则 {rule.id} 的总结任务出错: {str(e)}") - await asyncio.sleep(60) # 出错后等待一分钟再重试 + logger.error(f"Summary task for rule {rule.id} encountered an error: {str(e)}") + await asyncio.sleep(60) # Wait one minute before retrying after an error def _split_message(self, text: str, max_length: int = MAX_MESSAGE_PART_LENGTH): if not text: @@ -114,7 +114,7 @@ def _split_message(self, text: str, max_length: int = MAX_MESSAGE_PART_LENGTH): return parts def _get_next_run_time(self, now, target_time): - """计算下一次运行时间""" + """Calculate the next run time""" hour, minute = map(int, target_time.split(':')) next_time = now.replace(hour=hour, minute=minute, second=0, microsecond=0) @@ -124,7 +124,7 @@ def _get_next_run_time(self, now, target_time): return next_time async def _execute_summary(self, rule_id, is_now=False): - """执行单个规则的总结任务""" + """Execute the summary task for a single rule""" session = get_session() try: rule = session.query(ForwardRule).get(rule_id) @@ -138,14 +138,14 @@ async def _execute_summary(self, rule_id, is_now=False): messages = [] - # 计算时间范围 + # Calculate time range now = datetime.now(self.timezone) summary_hour, summary_minute = map(int, rule.summary_time.split(':')) - # 设置结束时间为当前时间 + # Set end time to current time end_time = now - # 设置开始时间为前一天的总结时间 + # Set start time to the summary time of the previous day start_time = now.replace( hour=summary_hour, minute=summary_minute, @@ -153,14 +153,14 @@ async def _execute_summary(self, rule_id, is_now=False): microsecond=0 ) - timedelta(days=1) - logger.info(f'规则 {rule_id} 获取消息时间范围: {start_time} 到 {end_time}') + logger.info(f'Rule {rule_id} message retrieval time range: {start_time} to {end_time}') async with self.request_semaphore: messages = [] current_offset = 0 while True: - batch = [] # 移到循环外部 + batch = [] # Moved outside the loop messages_batch = await self.user_client.get_messages( source_chat_id, limit=self.batch_size, @@ -170,60 +170,60 @@ async def _execute_summary(self, rule_id, is_now=False): ) if not messages_batch: - logger.info(f'规则 {rule_id} 没有获取到新消息,退出循环') + logger.info(f'Rule {rule_id} no new messages retrieved, exiting loop') break - logger.info(f'规则 {rule_id} 获取到批次消息数量: {len(messages_batch)}') + logger.info(f'Rule {rule_id} batch message count retrieved: {len(messages_batch)}') should_break = False for message in messages_batch: msg_time = message.date.astimezone(self.timezone) preview = message.text[:20] + '...' if message.text else 'None' - logger.info(f'规则 {rule_id} 处理消息 - 时间: {msg_time}, 预览: {preview}, 长度: {len(message.text) if message.text else 0}') + logger.info(f'Rule {rule_id} processing message - time: {msg_time}, preview: {preview}, length: {len(message.text) if message.text else 0}') - # 跳过未来时间的消息 + # Skip messages with future timestamps if msg_time > end_time: continue - # 如果消息在有效时间范围内,添加到批次 + # If the message is within the valid time range, add it to the batch if start_time <= msg_time <= end_time and message.text: batch.append(message.text) - # 如果遇到早于开始时间的消息,标记退出 + # If a message earlier than the start time is encountered, mark for exit if msg_time < start_time: - logger.info(f'规则 {rule_id} 消息时间 {msg_time} 早于开始时间 {start_time},停止获取') + logger.info(f'Rule {rule_id} message time {msg_time} is earlier than start time {start_time}, stopping retrieval') should_break = True break - # 如果当前批次有消息,添加到总消息列表 + # If the current batch has messages, add them to the total message list if batch: messages.extend(batch) - logger.info(f'规则 {rule_id} 当前批次添加了 {len(batch)} 条消息,总消息数: {len(messages)}') + logger.info(f'Rule {rule_id} current batch added {len(batch)} messages, total messages: {len(messages)}') - # 更新offset为最后一条消息的ID + # Update offset to the ID of the last message current_offset = messages_batch[-1].id - # 如果需要退出循环 + # If the loop needs to exit if should_break: break - # 在批次之间等待 + # Wait between batches await asyncio.sleep(self.batch_delay) if not messages: - logger.info(f'规则 {rule_id} 没有需要总结的消息') + logger.info(f'Rule {rule_id} no messages to summarize') return all_messages = '\n'.join(messages) - # 检查AI模型设置,如未设置则使用默认模型 + # Check AI model setting, use default model if not set 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 for summary: {rule.ai_model}") else: - logger.info(f"使用规则配置的AI模型进行总结: {rule.ai_model}") + logger.info(f"Using rule-configured AI model for summary: {rule.ai_model}") - # 获取AI提供者并处理总结 + # Get AI provider and process the summary provider = await get_ai_provider(rule.ai_model) summary = await provider.process_message( all_messages, @@ -234,9 +234,9 @@ async def _execute_summary(self, rule_id, is_now=False): if summary: duration_hours = round((end_time - start_time).total_seconds() / 3600) - header = f"📋 {rule.source_chat.name} - {duration_hours}小时消息总结\n" - header += f"🕐 时间范围: {start_time.strftime('%Y-%m-%d %H:%M')} - {end_time.strftime('%Y-%m-%d %H:%M')}\n" - header += f"📊 消息数量: {len(messages)} 条\n\n" + header = f"📋 {rule.source_chat.name} - {duration_hours}-hour message summary\n" + header += f"🕐 Time range: {start_time.strftime('%Y-%m-%d %H:%M')} - {end_time.strftime('%Y-%m-%d %H:%M')}\n" + header += f"📊 Message count: {len(messages)} messages\n\n" summary_parts = self._split_message(summary, MAX_MESSAGE_PART_LENGTH) @@ -245,9 +245,9 @@ async def _execute_summary(self, rule_id, is_now=False): if i == 0: message_to_send = header + part else: - message_to_send = f"📋 {rule.source_chat.name} - 总结报告 (续 {i+1}/{len(summary_parts)})\n\n" + part + message_to_send = f"📋 {rule.source_chat.name} - Summary report (continued {i+1}/{len(summary_parts)})\n\n" + part - # 发送消息,支持重试机制 + # Send message with retry mechanism current_message = None use_markdown = True attempt = 0 @@ -271,31 +271,31 @@ async def _execute_summary(self, rule_id, is_now=False): except errors.MarkupInvalidError as e: if use_markdown: - logger.warning(f"Markdown解析失败: {e}. 降级为纯文本后重试。") + logger.warning(f"Markdown parsing failed: {e}. Falling back to plain text and retrying.") use_markdown = False - continue # 立即重试,使用纯文本格式 + continue # Retry immediately using plain text format else: # This should not happen, but if it does, it's a bug. - logger.error(f"纯文本发送时出现意外的 MarkupInvalidError : {e}") + logger.error(f"Unexpected MarkupInvalidError when sending plain text: {e}") raise # Fail fast except errors.FloodWaitError as fwe: if attempt < MAX_SEND_ATTEMPTS - 1: - logger.warning(f"触发Telegram发送频率限制,等待 {fwe.seconds} 秒后重试...") + logger.warning(f"Telegram rate limit triggered, waiting {fwe.seconds} seconds before retrying...") await asyncio.sleep(fwe.seconds) attempt += 1 else: - logger.error("重试次数已达上限,发送失败。") + logger.error("Maximum retry attempts reached, sending failed.") raise except Exception as send_error: - logger.error(f"发送总结第 {i+1} 部分时出错: {str(send_error)}") + logger.error(f"Error sending summary part {i+1}: {str(send_error)}") if attempt >= MAX_SEND_ATTEMPTS - 1: raise # Re-raise on last attempt await asyncio.sleep(1) # Wait a bit before retrying on other errors attempt += 1 - # 统一处理第一条消息的赋值 + # Assign the first message uniformly if i == 0: summary_message = current_message @@ -303,67 +303,67 @@ async def _execute_summary(self, rule_id, is_now=False): try: await self.bot_client.pin_message(target_chat_id, summary_message) except Exception as pin_error: - logger.warning(f"置顶总结消息失败: {str(pin_error)}") + logger.warning(f"Failed to pin summary message: {str(pin_error)}") - logger.info(f'规则 {rule_id} 总结完成,共处理 {len(messages)} 条消息,分为 {len(summary_parts)} 部分发送') + logger.info(f'Rule {rule_id} summary completed, processed {len(messages)} messages, sent in {len(summary_parts)} parts') except Exception as e: - logger.error(f'执行规则 {rule_id} 的总结任务时出错: {str(e)}') - logger.error(f'错误详情: {traceback.format_exc()}') + logger.error(f'Error executing summary task for rule {rule_id}: {str(e)}') + logger.error(f'Error details: {traceback.format_exc()}') finally: session.close() async def start(self): - """启动调度器""" - logger.info("开始启动调度器...") + """Start the scheduler""" + logger.info("Starting scheduler...") session = get_session() try: - # 获取所有启用了总结功能的规则 + # Get all rules with summary feature enabled rules = session.query(ForwardRule).filter_by(is_summary=True).all() - logger.info(f"找到 {len(rules)} 个启用了总结功能的规则") + logger.info(f"Found {len(rules)} rules with summary feature enabled") for rule in rules: - logger.info(f"正在为规则 {rule.id} ({rule.source_chat.name} -> {rule.target_chat.name}) 创建调度任务") - logger.info(f"总结时间: {rule.summary_time}") + logger.info(f"Creating scheduled task for rule {rule.id} ({rule.source_chat.name} -> {rule.target_chat.name})") + logger.info(f"Summary time: {rule.summary_time}") - # 计算下一次执行时间 + # Calculate the next execution time now = datetime.now(self.timezone) next_time = self._get_next_run_time(now, rule.summary_time) wait_seconds = (next_time - now).total_seconds() - logger.info(f"下一次执行时间: {next_time.strftime('%Y-%m-%d %H:%M:%S')}") - logger.info(f"等待时间: {wait_seconds:.2f} 秒") + logger.info(f"Next execution time: {next_time.strftime('%Y-%m-%d %H:%M:%S')}") + logger.info(f"Wait time: {wait_seconds:.2f} seconds") await self.schedule_rule(rule) if not rules: - logger.info("没有找到启用了总结功能的规则") + logger.info("No rules with summary feature enabled found") - logger.info("调度器启动完成") + logger.info("Scheduler startup completed") except Exception as e: - logger.error(f"启动调度器时出错: {str(e)}") - logger.error(f"错误详情: {traceback.format_exc()}") + logger.error(f"Error starting scheduler: {str(e)}") + logger.error(f"Error details: {traceback.format_exc()}") finally: session.close() def stop(self): - """停止所有任务""" + """Stop all tasks""" for task in self.tasks.values(): task.cancel() self.tasks.clear() async def execute_all_summaries(self): - """立即执行所有启用了总结功能的规则""" + """Immediately execute all rules with summary feature enabled""" session = get_session() try: rules = session.query(ForwardRule).filter_by(is_summary=True).all() - # 使用 gather 但限制并发数 + # Use gather but limit concurrency tasks = [self._execute_summary(rule.id, is_now=True) for rule in rules] - for i in range(0, len(tasks), 2): # 每次执行2个任务 + for i in range(0, len(tasks), 2): # Execute 2 tasks at a time batch = tasks[i:i+2] await asyncio.gather(*batch) - await asyncio.sleep(1) # 每批次之间稍微暂停 + await asyncio.sleep(1) # Brief pause between batches finally: session.close() diff --git a/ufb/ufb_client.py b/ufb/ufb_client.py index 5869a8e..d05d1ea 100644 --- a/ufb/ufb_client.py +++ b/ufb/ufb_client.py @@ -12,11 +12,11 @@ logger = logging.getLogger(__name__) async def get_main_module(): - """获取 main 模块""" + """Get the main module""" try: return sys.modules['__main__'] except KeyError: - # 如果找不到 main 模块,尝试手动导入 + # If main module is not found, try to import it manually spec = importlib.util.spec_from_file_location( "main", os.path.join(os.path.dirname(os.path.dirname(__file__)), "main.py") @@ -26,7 +26,7 @@ async def get_main_module(): return main async def get_db_ops(): - """获取 main.py 中的 db_ops 实例""" + """Get the db_ops instance from main.py""" main = await get_main_module() if main.db_ops is None: main.db_ops = await main.init_db_ops() @@ -34,132 +34,132 @@ async def get_db_ops(): class UFBClient: def __init__(self, config_dir: str = "./ufb/config"): - # 获取当前文件所在目录(ufb目录) + # Get the directory where the current file is located (ufb directory) current_file_dir = Path(__file__).parent - # 获取项目根目录(当前文件的上级目录) + # Get the project root directory (parent of the current file's directory) project_root = current_file_dir.parent - + self.server_url: Optional[str] = None self.token: Optional[str] = None - - # 使用项目根目录作为基准 + + # Use the project root directory as the base self.config_dir = (project_root / config_dir).resolve() - # logger.info(f"配置目录: {self.config_dir}") - + # logger.info(f"Config directory: {self.config_dir}") + self.config_path = self.config_dir / "config.json" self.websocket: Optional[websockets.WebSocketClientProtocol] = None self.is_connected = False self.on_config_update_callbacks: list[Callable[[Dict[str, Any]], None]] = [] - self.reconnect_task = None # 用于存储重连任务 - - # 确保配置目录存在 + self.reconnect_task = None # Used to store the reconnect task + + # Ensure the config directory exists self.config_dir.mkdir(parents=True, exist_ok=True) async def ensure_config_dir(self): - """确保配置目录存在""" + """Ensure the config directory exists""" self.config_dir.mkdir(parents=True, exist_ok=True) def load_config(self) -> Dict[str, Any]: - """加载本地配置""" + """Load local config""" if self.config_path.exists(): try: return json.loads(self.config_path.read_text(encoding='utf-8')) except json.JSONDecodeError: - logger.error("配置文件损坏") + logger.error("Config file is corrupted") return {} return {} async def save_config(self, config: Dict[str, Any], to_client: bool = False): - """保存配置到本地""" - logger.info(f"保存配置到本地: {self.config_path.absolute()}") + """Save config locally""" + logger.info(f"Saving config locally: {self.config_path.absolute()}") self.config_path.write_text(json.dumps(config, ensure_ascii=False, indent=2), encoding='utf-8') if to_client: db_ops = await get_db_ops() await db_ops.sync_from_json(config) - + def merge_configs(self, local_config: Dict[str, Any], cloud_config: Dict[str, Any]) -> Dict[str, Any]: - """递归合并本地和云端配置 - 策略: - 1. 如果本地配置为空,使用云端配置 - 2. 如果是字典类型,递归合并 - 3. 如果是列表类型,合并列表(去重) - 4. 如果是其他类型,使用云端的值覆盖本地值 + """Recursively merge local and cloud configs + Strategy: + 1. If local config is empty, use cloud config + 2. If it's a dict type, merge recursively + 3. If it's a list type, merge lists (deduplicate) + 4. If it's another type, use cloud value to override local value """ - # 如果本地配置为空,直接使用云端配置 + # If local config is empty, use cloud config directly if not local_config: return cloud_config.copy() - - # 如果云端配置为空,使用本地配置 + + # If cloud config is empty, use local config if not cloud_config: return local_config.copy() - # 开始递归合并 + # Begin recursive merge merged = local_config.copy() - + for key, cloud_value in cloud_config.items(): - # 如果是字典类型,递归合并 + # If it's a dict type, merge recursively if isinstance(cloud_value, dict): if key not in merged: merged[key] = {} if isinstance(merged[key], dict): merged[key] = self.merge_configs(merged[key], cloud_value) else: - # 如果本地值不是字典类型,但云端是字典类型,使用云端的值 + # If local value is not a dict type but cloud is, use cloud value merged[key] = cloud_value.copy() - # 如果是列表类型,合并列表 + # If it's a list type, merge lists elif isinstance(cloud_value, list): if key not in merged or not isinstance(merged[key], list): merged[key] = cloud_value.copy() else: - # 合并列表,去重 + # Merge lists, deduplicate merged_list = merged[key].copy() for item in cloud_value: if item not in merged_list: merged_list.append(item) merged[key] = merged_list else: - # 非字典和列表类型,使用云端的值 + # Non-dict and non-list types, use cloud value merged[key] = cloud_value return merged def on_config_update(self, callback: Callable[[Dict[str, Any]], None]): - """注册配置更新回调""" + """Register a config update callback""" self.on_config_update_callbacks.append(callback) def notify_config_update(self, config: Dict[str, Any]): - """通知所有监听器配置已更新""" + """Notify all listeners that config has been updated""" for callback in self.on_config_update_callbacks: try: callback(config) except Exception as e: - logger.error(f"配置更新回调执行失败: {e}") + logger.error(f"Config update callback execution failed: {e}") async def handle_config_conflict(self, conflict_data: Dict[str, Any], local_config: Dict[str, Any]) -> Dict[str, Any]: - """处理配置冲突 - 返回最终使用的配置 + """Handle config conflict + Returns the final config to use """ - logger.info(f"配置冲突: \n云端时间: {conflict_data['cloudTime']}\n本地时间: {conflict_data['localTime']}") - - # 总是选择使用云端配置 + logger.info(f"Config conflict: \nCloud time: {conflict_data['cloudTime']}\nLocal time: {conflict_data['localTime']}") + + # Always choose to use cloud config await self.websocket.send(json.dumps({ "type": "resolveConflict", "choice": "useCloud" })) - - # 等待服务器响应 + + # Wait for server response cloud_config = json.loads(await self.websocket.recv()) - logger.info(f"收到云端配置: {json.dumps(cloud_config, ensure_ascii=False, indent=2)}") - - # 合并云端和本地配置 + logger.info(f"Received cloud config: {json.dumps(cloud_config, ensure_ascii=False, indent=2)}") + + # Merge cloud and local configs merged_config = self.merge_configs(local_config, cloud_config) - logger.info(f"合并后的配置: {json.dumps(merged_config, ensure_ascii=False, indent=2)}") - + logger.info(f"Merged config: {json.dumps(merged_config, ensure_ascii=False, indent=2)}") + return merged_config async def connect(self, server_url: str, token: str): - """建立WebSocket连接""" + """Establish WebSocket connection""" if self.is_connected: await self.close() @@ -169,68 +169,68 @@ async def connect(self, server_url: str, token: str): try: self.websocket = await websockets.connect(f"{server_url}/ws/config/{token}") self.is_connected = True - logger.info("WebSocket连接已建立") - - # 连接成功后取消重连任务 + logger.info("WebSocket connection established") + + # Cancel reconnect task after successful connection if self.reconnect_task: self.reconnect_task.cancel() self.reconnect_task = None - + except Exception as e: - logger.error(f"WebSocket连接失败: {e}") - # 启动重连 + logger.error(f"WebSocket connection failed: {e}") + # Start reconnect await self.start_reconnect() raise async def reconnect(self): - """重连逻辑""" + """Reconnect logic""" while True: try: if not self.is_connected and self.server_url and self.token: - logger.info("尝试重新连接...") + logger.info("Attempting to reconnect...") self.websocket = await websockets.connect(f"{self.server_url}/ws/config/{self.token}") self.is_connected = True - logger.info("重连成功") - - # 重新启动消息处理 + logger.info("Reconnection successful") + + # Restart message handling asyncio.create_task(self._handle_messages()) - - # 重新发送配置更新 + + # Resend config update local_config = self.load_config() await self.websocket.send(json.dumps({ "type": "update", **local_config })) - - # 重连成功后退出循环 + + # Exit loop after successful reconnection break except Exception as e: - logger.error(f"重连失败: {e}") - await asyncio.sleep(10) # 等待10秒后重试 + logger.error(f"Reconnection failed: {e}") + await asyncio.sleep(10) # Wait 10 seconds before retrying async def start_reconnect(self): - """启动重连任务""" + """Start reconnect task""" if not self.reconnect_task or self.reconnect_task.done(): self.reconnect_task = asyncio.create_task(self.reconnect()) async def start(self, server_url: Optional[str] = None, token: Optional[str] = None): - """启动客户端""" - logger.info("启动客户端") + """Start the client""" + logger.info("Starting client") await self.ensure_config_dir() - + if server_url and token: await self.connect(server_url, token) elif self.server_url and self.token: await self.connect(self.server_url, self.token) else: - logger.info("等待连接参数...") + logger.info("Waiting for connection parameters...") return - # 检查本地配置 + # Check local config local_config = self.load_config() current_timestamp = int(time.time() * 1000) - # 确保配置结构完整 + # Ensure config structure is complete if not local_config: local_config = { "globalConfig": { @@ -247,35 +247,35 @@ async def start(self, server_url: Optional[str] = None, token: Optional[str] = N if "lastSyncTime" not in local_config["globalConfig"]["SYNC_CONFIG"]: local_config["globalConfig"]["SYNC_CONFIG"]["lastSyncTime"] = current_timestamp - # 检查是否为首次同步(配置文件不存在或为空) + # Check if this is a first-time sync (config file doesn't exist or is empty) if not self.config_path.exists() or not local_config: - # 发送首次同步请求 + # Send first sync request await self.websocket.send(json.dumps({ "type": "firstSync", **local_config })) else: - # 非首次同步,直接检查配置是否需要更新 + # Not first sync, directly check if config needs updating await self.websocket.send(json.dumps({ "type": "update", **local_config })) - # 创建后台任务处理消息 + # Create background task to handle messages asyncio.create_task(self._handle_messages()) async def _handle_messages(self): - """在后台处理WebSocket消息""" + """Handle WebSocket messages in the background""" try: async for message in self.websocket: try: data = json.loads(message) - logger.info(f"收到服务器消息") + logger.info(f"Received server message") msg_type = data.get("type") if msg_type == "firstSync": if data.get("message") == "firstSync_success": - logger.info("首次同步成功") + logger.info("First sync successful") await self.save_config(data) self.notify_config_update(data) @@ -286,63 +286,63 @@ async def _handle_messages(self): else: await self.save_config(data) self.notify_config_update(data) - + if data.get("message") == "config_updated": - logger.info("配置已更新") + logger.info("Config has been updated") elif msg_type == "configConflict": - # 获取时间戳 + # Get timestamps cloud_time = data.get("cloudTime") local_time = data.get("localTime") newer_config = data.get("newerConfig") - - logger.info(f"配置冲突:\n云端时间: {cloud_time}\n本地时间: {local_time}\n较新配置: {newer_config}") - - # 加载本地配置 + + logger.info(f"Config conflict:\nCloud time: {cloud_time}\nLocal time: {local_time}\nNewer config: {newer_config}") + + # Load local config local_config = self.load_config() - - # 总是使用云端配置 + + # Always use cloud config await self.websocket.send(json.dumps({ "type": "resolveConflict", "choice": "useCloud" })) - - # 等待服务器响应 + + # Wait for server response response = json.loads(await self.websocket.recv()) - # 合并配置 + # Merge configs merged_config = self.merge_configs(local_config, response) await self.save_config(merged_config) self.notify_config_update(merged_config) elif msg_type == "delete": if data.get("success"): - logger.info("配置删除成功") + logger.info("Config deletion successful") else: - logger.error(f"配置删除失败: {data.get('message', '')}") + logger.error(f"Config deletion failed: {data.get('message', '')}") except json.JSONDecodeError: - logger.error("收到无效的JSON消息") + logger.error("Received invalid JSON message") except Exception as e: - logger.error(f"处理消息时出错: {e}") + logger.error(f"Error processing message: {e}") except websockets.ConnectionClosed: - logger.info("WebSocket连接已关闭") + logger.info("WebSocket connection closed") self.is_connected = False - # 启动重连 + # Start reconnect await self.start_reconnect() except Exception as e: - logger.error(f"WebSocket错误: {e}") + logger.error(f"WebSocket error: {e}") self.is_connected = False - # 启动重连 + # Start reconnect await self.start_reconnect() async def close(self): - """关闭客户端""" + """Close the client""" if self.websocket: await self.websocket.close() self.is_connected = False - logger.info("WebSocket连接已关闭") - # 取消重连任务 + logger.info("WebSocket connection closed") + # Cancel reconnect task if self.reconnect_task: self.reconnect_task.cancel() self.reconnect_task = None diff --git a/utils/auto_delete.py b/utils/auto_delete.py index 5a8afd4..8dce6e3 100644 --- a/utils/auto_delete.py +++ b/utils/auto_delete.py @@ -5,120 +5,119 @@ from utils.constants import BOT_MESSAGE_DELETE_TIMEOUT, USER_MESSAGE_DELETE_ENABLE logger = logging.getLogger(__name__) -# 从环境变量获取默认超时时间 +# Get default timeout from environment variable async def delete_after(message, seconds): - """等待指定秒数后删除消息 - - 参数: - message: 要删除的消息 - seconds: 等待多少秒后删除, 0表示立即删除, -1表示不删除 + """Wait for the specified number of seconds then delete the message + + Args: + message: The message to delete + seconds: Number of seconds to wait before deleting, 0 means delete immediately, -1 means do not delete """ - if seconds == -1: # -1 表示不删除 + if seconds == -1: # -1 means do not delete return - - if seconds > 0: # 正数表示等待指定秒数再删除 + + if seconds > 0: # Positive number means wait the specified seconds before deleting await asyncio.sleep(seconds) - + try: await message.delete() except Exception as e: - logger.error(f"删除消息失败: {e}") + logger.error(f"Failed to delete message: {e}") async def reply_and_delete(event, text, delete_after_seconds=None, **kwargs): - """回复消息并安排自动删除 - - 参数: - event: Telethon事件对象 - text: 要发送的文本 - delete_after_seconds: 多少秒后删除消息,None使用默认值,0表示立即删除,-1表示不删除 - **kwargs: 传递给reply方法的其他参数 + """Reply to a message and schedule auto-deletion + + Args: + event: Telethon event object + text: Text to send + delete_after_seconds: Number of seconds before deleting the message, None uses default, 0 means delete immediately, -1 means do not delete + **kwargs: Other parameters passed to the reply method """ - # 如果没有指定删除时间,使用环境变量中的默认值 + # If no deletion time is specified, use the default from environment variable if delete_after_seconds is None: deletion_timeout = BOT_MESSAGE_DELETE_TIMEOUT else: deletion_timeout = delete_after_seconds - - # 发送回复 + + # Send reply message = await event.reply(text, **kwargs) - - # 安排删除任务,只有当deletion_timeout不等于-1时才删除 + + # Schedule deletion task, only delete when deletion_timeout is not -1 if deletion_timeout != -1: asyncio.create_task(delete_after(message, deletion_timeout)) - + return message async def respond_and_delete(event, text, delete_after_seconds=None, **kwargs): - """使用respond回复消息并安排自动删除 - - 参数: - event: Telethon事件对象 - text: 要发送的文本 - delete_after_seconds: 多少秒后删除消息,None使用默认值,0表示立即删除,-1表示不删除 - **kwargs: 传递给respond方法的其他参数 + """Use respond to reply to a message and schedule auto-deletion + + Args: + event: Telethon event object + text: Text to send + delete_after_seconds: Number of seconds before deleting the message, None uses default, 0 means delete immediately, -1 means do not delete + **kwargs: Other parameters passed to the respond method """ - # 如果没有指定删除时间,使用环境变量中的默认值 + # If no deletion time is specified, use the default from environment variable if delete_after_seconds is None: deletion_timeout = BOT_MESSAGE_DELETE_TIMEOUT else: deletion_timeout = delete_after_seconds - - # 发送回复 + + # Send reply message = await event.respond(text, **kwargs) - - # 安排删除任务,只有当deletion_timeout不等于-1时才删除 + + # Schedule deletion task, only delete when deletion_timeout is not -1 if deletion_timeout != -1: asyncio.create_task(delete_after(message, deletion_timeout)) - + return message async def send_message_and_delete(client, entity, text, delete_after_seconds=None, **kwargs): - """发送消息并安排自动删除 - - 参数: - client: Telethon客户端对象 - entity: 聊天对象或ID - text: 要发送的文本 - delete_after_seconds: 多少秒后删除消息,None使用默认值,0表示立即删除,-1表示不删除 - **kwargs: 传递给send_message方法的其他参数 + """Send a message and schedule auto-deletion + + Args: + client: Telethon client object + entity: Chat object or ID + text: Text to send + delete_after_seconds: Number of seconds before deleting the message, None uses default, 0 means delete immediately, -1 means do not delete + **kwargs: Other parameters passed to the send_message method """ - # 如果没有指定删除时间,使用环境变量中的默认值 + # If no deletion time is specified, use the default from environment variable if delete_after_seconds is None: deletion_timeout = BOT_MESSAGE_DELETE_TIMEOUT else: deletion_timeout = delete_after_seconds - - # 发送消息 + + # Send message message = await client.send_message(entity, text, **kwargs) - - # 安排删除任务,只有当deletion_timeout不等于-1时才删除 + + # Schedule deletion task, only delete when deletion_timeout is not -1 if deletion_timeout != -1: asyncio.create_task(delete_after(message, deletion_timeout)) - + return message -# 删除用户消息 +# Delete user message async def async_delete_user_message(client, chat_id, message_id, seconds): - """删除用户消息 - - 参数: - client: bot客户端 - chat_id: 聊天ID - message_id: 消息ID - seconds: 等待多少秒后删除, 0表示立即删除, -1表示不删除 + """Delete a user message + + Args: + client: Bot client + chat_id: Chat ID + message_id: Message ID + seconds: Number of seconds to wait before deleting, 0 means delete immediately, -1 means do not delete """ if USER_MESSAGE_DELETE_ENABLE == "false": return - - if seconds == -1: # -1 表示不删除 + + if seconds == -1: # -1 means do not delete return - - if seconds > 0: # 正数表示等待指定秒数再删除 + + if seconds > 0: # Positive number means wait the specified seconds before deleting await asyncio.sleep(seconds) - + try: await client.delete_messages(chat_id, message_id) except Exception as e: - logger.error(f"删除用户消息失败: {e}") - + logger.error(f"Failed to delete user message: {e}") diff --git a/utils/common.py b/utils/common.py index 71eb2f5..5a66b0e 100644 --- a/utils/common.py +++ b/utils/common.py @@ -16,11 +16,11 @@ logger = logging.getLogger(__name__) async def get_main_module(): - """获取 main 模块""" + """Get the main module""" try: return sys.modules['__main__'] except KeyError: - # 如果找不到 main 模块,尝试手动导入 + # If the main module is not found, try to import it manually spec = importlib.util.spec_from_file_location( "main", os.path.join(os.path.dirname(os.path.dirname(__file__)), "main.py") @@ -30,58 +30,58 @@ async def get_main_module(): return main async def get_user_client(): - """获取用户客户端""" + """Get the user client""" main = await get_main_module() return main.user_client async def get_bot_client(): - """获取机器人客户端""" + """Get the bot client""" main = await get_main_module() return main.bot_client async def get_db_ops(): - """获取 main.py 中的 db_ops 实例""" + """Get the db_ops instance from main.py""" main = await get_main_module() if main.db_ops is None: main.db_ops = await main.init_db_ops() return main.db_ops async def get_user_id(): - """获取用户ID,确保环境变量已加载""" + """Get the user ID, ensuring environment variables are loaded""" user_id_str = os.getenv('USER_ID') if not user_id_str: - logger.error('未设置 USER_ID 环境变量') - raise ValueError('必须在 .env 文件中设置 USER_ID') + logger.error('USER_ID environment variable is not set') + raise ValueError('USER_ID must be set in the .env file') return int(user_id_str) async def get_current_rule(session, event): - """获取当前选中的规则""" + """Get the currently selected rule""" try: - # 获取当前聊天 + # Get the current chat current_chat = await event.get_chat() - logger.info(f'获取当前聊天: {current_chat.id}') + logger.info(f'Getting current chat: {current_chat.id}') 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: - logger.info('未找到当前聊天或未选择源聊天') - await reply_and_delete(event,'请先使用 /switch 选择一个源聊天') + logger.info('Current chat not found or no source chat selected') + await reply_and_delete(event,'Please use /switch to select a source chat first') return None - logger.info(f'当前选中的源聊天ID: {current_chat_db.current_add_id}') + logger.info(f'Currently selected source chat ID: {current_chat_db.current_add_id}') - # 查找对应的规则 + # Find the corresponding rule source_chat = session.query(Chat).filter( Chat.telegram_chat_id == current_chat_db.current_add_id ).first() if source_chat: - logger.info(f'找到源聊天: {source_chat.name}') + logger.info(f'Found source chat: {source_chat.name}') else: - logger.error('未找到源聊天') + logger.error('Source chat not found') return None rule = session.query(ForwardRule).filter( @@ -90,152 +90,152 @@ async def get_current_rule(session, event): ).first() if not rule: - logger.info('未找到对应的转发规则') - await reply_and_delete(event,'转发规则不存在') + logger.info('Corresponding forward rule not found') + await reply_and_delete(event,'Forward rule does not exist') return None - logger.info(f'找到转发规则 ID: {rule.id}') + logger.info(f'Found forward rule ID: {rule.id}') return rule, source_chat except Exception as e: - logger.error(f'获取当前规则时出错: {str(e)}') + logger.error(f'Error getting current rule: {str(e)}') logger.exception(e) - await reply_and_delete(event,'获取当前规则时出错,请检查日志') + await reply_and_delete(event,'Error getting current rule, please check the logs') return None async def get_all_rules(session, event): - """获取当前聊天的所有规则""" + """Get all rules for the current chat""" try: - # 获取当前聊天 + # Get the current chat current_chat = await event.get_chat() - logger.info(f'获取当前聊天: {current_chat.id}') + logger.info(f'Getting current chat: {current_chat.id}') current_chat_db = session.query(Chat).filter( Chat.telegram_chat_id == str(current_chat.id) ).first() if not current_chat_db: - logger.info('未找到当前聊天') - await reply_and_delete(event,'当前聊天没有任何转发规则') + logger.info('Current chat not found') + await reply_and_delete(event,'No forward rules exist for the current chat') return None - logger.info(f'找到当前聊天数据库记录 ID: {current_chat_db.id}') + logger.info(f'Found current chat database record ID: {current_chat_db.id}') - # 查找所有以当前聊天为目标的规则 + # Find all rules targeting the current chat rules = session.query(ForwardRule).filter( ForwardRule.target_chat_id == current_chat_db.id ).all() if not rules: - logger.info('未找到任何转发规则') - await reply_and_delete(event,'当前聊天没有任何转发规则') + logger.info('No forward rules found') + await reply_and_delete(event,'No forward rules exist for the current chat') return None - logger.info(f'找到 {len(rules)} 条转发规则') + logger.info(f'Found {len(rules)} forward rule(s)') return rules except Exception as e: - logger.error(f'获取所有规则时出错: {str(e)}') + logger.error(f'Error getting all rules: {str(e)}') logger.exception(e) - await reply_and_delete(event,'获取规则时出错,请检查日志') + await reply_and_delete(event,'Error getting rules, please check the logs') return None -# 添加缓存字典 +# Add cache dictionary _admin_cache = {} -_CACHE_DURATION = timedelta(minutes=30) # 缓存30分钟 +_CACHE_DURATION = timedelta(minutes=30) # Cache for 30 minutes async def get_channel_admins(client, chat_id): - """获取频道管理员列表,带缓存机制""" + """Get channel admin list with caching mechanism""" current_time = datetime.now() - - # 检查缓存是否存在且未过期 + + # Check if cache exists and is not expired if chat_id in _admin_cache: cache_data = _admin_cache[chat_id] if current_time - cache_data['timestamp'] < _CACHE_DURATION: return cache_data['admin_ids'] - - # 缓存不存在或已过期,重新获取管理员列表 + + # Cache does not exist or has expired, re-fetch admin list try: admins = await client.get_participants(chat_id, filter=ChannelParticipantsAdmins) admin_ids = [admin.id for admin in admins] - - # 更新缓存 + + # Update cache _admin_cache[chat_id] = { 'admin_ids': admin_ids, 'timestamp': current_time } return admin_ids except Exception as e: - logger.error(f'获取频道管理员列表失败: {str(e)}') + logger.error(f'Failed to get channel admin list: {str(e)}') return None async def is_admin(event): - """检查用户是否为频道/群组管理员 - + """Check if the user is a channel/group admin + Args: - event: 事件对象 + event: Event object Returns: - bool: 是否是管理员 + bool: Whether the user is an admin """ try: - # 获取所有机器人管理员列表 + # Get all bot admin list bot_admins = get_admin_list() - # 检查是否有message属性 + # Check if the message attribute exists if not hasattr(event, 'message'): - # 没有message属性,是回调处理 + # No message attribute, this is a callback handler if event.sender_id in bot_admins: return True else: - logger.info(f'用户 {event.sender_id} 非管理员,操作已被忽略') + logger.info(f'User {event.sender_id} is not an admin, operation ignored') return False - + message = event.message main = await get_main_module() client = main.user_client - - - + + + if message.is_channel and not message.is_group: - # 获取频道管理员列表(使用缓存) + # Get channel admin list (using cache) channel_admins = await get_channel_admins(client, event.chat_id) if channel_admins is None: return False - - - - # 检查机器人管理员是否在频道管理员列表中 + + + + # Check if bot admin is in the channel admin list admin_in_channel = any(admin_id in channel_admins for admin_id in bot_admins) if not admin_in_channel: - logger.info(f'机器人管理员不在频道管理员列表中,已忽略') + logger.info(f'Bot admin is not in the channel admin list, ignored') return False return True else: - # 检查发送者ID - user_id = event.sender_id # 使用 sender_id 作为主要ID来源 - logger.info(f'发送者ID:{user_id}') - + # Check sender ID + user_id = event.sender_id # Use sender_id as the primary ID source + logger.info(f'Sender ID: {user_id}') + bot_admins = get_admin_list() - # 检查是否是机器人管理员 + # Check if the user is a bot admin if user_id not in bot_admins: - logger.info(f'非管理员的消息,已忽略') + logger.info(f'Non-admin message, ignored') return False return True except Exception as e: - logger.error(f"检查管理员权限时出错: {str(e)}") + logger.error(f"Error checking admin permissions: {str(e)}") return False async def get_media_settings_text(): - """生成媒体设置页面的文本""" + """Generate text for the media settings page""" return MEDIA_SETTINGS_TEXT async def get_ai_settings_text(rule): - """生成AI设置页面的文本""" - ai_prompt = rule.ai_prompt or os.getenv('DEFAULT_AI_PROMPT', '未设置') - summary_prompt = rule.summary_prompt or os.getenv('DEFAULT_SUMMARY_PROMPT', '未设置') + """Generate text for the AI settings page""" + ai_prompt = rule.ai_prompt or os.getenv('DEFAULT_AI_PROMPT', 'Not set') + summary_prompt = rule.summary_prompt or os.getenv('DEFAULT_SUMMARY_PROMPT', 'Not set') return AI_SETTINGS_TEXT.format( ai_prompt=ai_prompt, @@ -244,137 +244,137 @@ async def get_ai_settings_text(rule): async def get_sender_info(event, rule_id): """ - 获取发送者信息 - + Get sender information + Args: - event: 消息事件 - rule_id: 规则ID - + event: Message event + rule_id: Rule ID + Returns: - str: 发送者信息 + str: Sender information """ try: - logger.info("开始获取发送者信息") + logger.info("Starting to get sender information") sender_name = None if hasattr(event.message, 'sender_chat') and event.message.sender_chat: - # 用户以频道身份发送消息 + # User sent the message as a channel sender = event.message.sender_chat sender_name = sender.title if hasattr(sender, 'title') else None - logger.info(f"使用频道信息: {sender_name}") + logger.info(f"Using channel info: {sender_name}") elif event.sender: - # 用户以个人身份发送消息 + # User sent the message as an individual sender = event.sender sender_name = ( sender.title if hasattr(sender, 'title') else f"{sender.first_name or ''} {sender.last_name or ''}".strip() ) - logger.info(f"使用发送者信息: {sender_name}") + logger.info(f"Using sender info: {sender_name}") 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'): try: - # 尝试获取频道信息 + # Try to get channel info channel = await event.client.get_entity(peer) sender_name = channel.title if hasattr(channel, 'title') else None - logger.info(f"使用peer_id信息: {sender_name}") + logger.info(f"Using peer_id info: {sender_name}") except Exception as ce: - logger.error(f'获取频道信息失败: {str(ce)}') + logger.error(f'Failed to get channel info: {str(ce)}') if sender_name: return sender_name else: - logger.warning(f"规则 ID: {rule_id} - 无法获取发送者信息") + logger.warning(f"Rule ID: {rule_id} - Unable to get sender information") return None except Exception as e: - logger.error(f'获取发送者信息出错: {str(e)}') + logger.error(f'Error getting sender information: {str(e)}') return None async def check_and_clean_chats(session, rule=None): """ - 检查并清理不再与任何规则关联的聊天记录 - + Check and clean chat records that are no longer associated with any rules + Args: - session: 数据库会话 - rule: 被删除的规则对象(可选),如果提供则从中获取聊天ID - + session: Database session + rule: Deleted rule object (optional), if provided, chat IDs are extracted from it + Returns: - int: 删除的聊天记录数量 + int: Number of deleted chat records """ deleted_count = 0 - + try: - # 获取所有聊天ID + # Get all chat IDs chat_ids_to_check = set() - - # 如果提供了规则,先检查这些受影响的聊天 + + # If a rule is provided, first check these affected chats if rule: if rule.source_chat_id: chat_ids_to_check.add(rule.source_chat_id) if rule.target_chat_id: chat_ids_to_check.add(rule.target_chat_id) else: - # 如果没有提供规则,则获取所有聊天 + # If no rule is provided, get all chats all_chats = session.query(Chat.id).all() chat_ids_to_check = set(chat[0] for chat in all_chats) - - # 对每个聊天ID进行检查 + + # Check each chat ID for chat_id in chat_ids_to_check: - # 检查此聊天是否还被任何规则引用 + # Check if this chat is still referenced by any rule as_source = session.query(ForwardRule).filter( ForwardRule.source_chat_id == chat_id ).count() - + as_target = session.query(ForwardRule).filter( ForwardRule.target_chat_id == chat_id ).count() - - # 如果聊天不再被任何规则引用 + + # If the chat is no longer referenced by any rule if as_source == 0 and as_target == 0: chat = session.query(Chat).get(chat_id) if chat: - # 获取telegram_chat_id以便日志记录 + # Get telegram_chat_id for logging purposes telegram_chat_id = chat.telegram_chat_id - name = chat.name or "未命名聊天" - - # 清理所有引用此聊天作为current_add_id的记录 + name = chat.name or "Unnamed chat" + + # Clean up all records referencing this chat as current_add_id chats_using_this = session.query(Chat).filter( Chat.current_add_id == telegram_chat_id ).all() - + for other_chat in chats_using_this: other_chat.current_add_id = None - logger.info(f'清除聊天 {other_chat.name} 的current_add_id设置') - - # 删除聊天记录 + logger.info(f'Cleared current_add_id setting for chat {other_chat.name}') + + # Delete the chat record session.delete(chat) - logger.info(f'删除未使用的聊天: {name} (ID: {telegram_chat_id})') + logger.info(f'Deleted unused chat: {name} (ID: {telegram_chat_id})') deleted_count += 1 - - # 如果有删除操作,提交更改 + + # If there were deletions, commit the changes if deleted_count > 0: session.commit() - logger.info(f'共清理了 {deleted_count} 个未使用的聊天记录') - + logger.info(f'Cleaned up {deleted_count} unused chat record(s) in total') + return deleted_count - + except Exception as e: - logger.error(f'检查和清理聊天记录时出错: {str(e)}') + logger.error(f'Error checking and cleaning chat records: {str(e)}') session.rollback() return 0 def get_admin_list(): - """获取管理员ID列表,如果ADMINS为空则使用USER_ID""" + """Get admin ID list, use USER_ID if ADMINS is empty""" admin_str = os.getenv('ADMINS', '') if not admin_str: user_id = os.getenv('USER_ID') if not user_id: - logger.error('未设置 USER_ID 环境变量') - raise ValueError('必须在 .env 文件中设置 USER_ID') + logger.error('USER_ID environment variable is not set') + raise ValueError('USER_ID must be set in the .env file') return [int(user_id)] return [int(admin.strip()) for admin in admin_str.split(',') if admin.strip()] @@ -383,132 +383,132 @@ def get_admin_list(): async def check_keywords(rule, message_text, event = None): """ - 检查消息是否匹配关键字规则 + Check if a message matches keyword rules Args: - rule: 转发规则对象,包含 forward_mode 和 keywords 属性 - message_text: 要检查的消息文本 - event: 可选的消息事件对象 + rule: Forward rule object, containing forward_mode and keywords attributes + message_text: Message text to check + event: Optional message event object Returns: - bool: 是否应该转发消息 + bool: Whether the message should be forwarded """ reverse_blacklist = rule.enable_reverse_blacklist reverse_whitelist = rule.enable_reverse_whitelist - logger.info(f"反转黑名单: {reverse_blacklist}, 反转白名单: {reverse_whitelist}") + logger.info(f"Reverse blacklist: {reverse_blacklist}, Reverse whitelist: {reverse_whitelist}") - # 处理用户信息过滤 + # Process user info filtering if rule.is_filter_user_info and event: message_text = await process_user_info(event, rule.id, message_text) - logger.info("开始检查关键字规则") - logger.info(f"当前转发模式: {rule.forward_mode}") + logger.info("Starting keyword rule check") + logger.info(f"Current forward mode: {rule.forward_mode}") forward_mode = rule.forward_mode - # 仅白名单模式 + # Whitelist only mode if forward_mode == ForwardMode.WHITELIST: return await process_whitelist_mode(rule, message_text, reverse_blacklist) - # 仅黑名单模式 + # Blacklist only mode elif forward_mode == ForwardMode.BLACKLIST: return await process_blacklist_mode(rule, message_text, reverse_whitelist) - # 先白后黑模式 + # Whitelist then blacklist mode elif forward_mode == ForwardMode.WHITELIST_THEN_BLACKLIST: return await process_whitelist_then_blacklist_mode(rule, message_text, reverse_blacklist) - # 先黑后白模式 + # Blacklist then whitelist mode elif forward_mode == ForwardMode.BLACKLIST_THEN_WHITELIST: return await process_blacklist_then_whitelist_mode(rule, message_text, reverse_whitelist) - logger.error(f"未知的转发模式: {forward_mode}") + logger.error(f"Unknown forward mode: {forward_mode}") return False async def process_whitelist_mode(rule, message_text, reverse_blacklist): - """处理仅白名单模式""" - logger.info("进入仅白名单模式") + """Process whitelist only mode""" + logger.info("Entering whitelist only mode") should_forward = False - # 检查普通白名单关键词 + # Check regular whitelist keywords whitelist_keywords = [k for k in rule.keywords if not k.is_blacklist] - logger.info(f"普通白名单关键词: {[k.keyword for k in whitelist_keywords]}") - + logger.info(f"Regular whitelist keywords: {[k.keyword for k in whitelist_keywords]}") + for keyword in whitelist_keywords: if await check_keyword_match(keyword, message_text): should_forward = True break - + if not should_forward: - logger.info("未匹配到普通白名单关键词,不转发") + logger.info("No regular whitelist keywords matched, not forwarding") return False - # 如果启用了黑名单反转,还需要匹配反转后的黑名单(作为第二重白名单) + # If blacklist reversal is enabled, also need to match reversed blacklist (as a second whitelist) if reverse_blacklist: - logger.info("检查反转后的黑名单关键词(作为白名单)") + logger.info("Checking reversed blacklist keywords (as whitelist)") reversed_blacklist = [k for k in rule.keywords if k.is_blacklist] - logger.info(f"反转后的黑名单关键词: {[k.keyword for k in reversed_blacklist]}") - + logger.info(f"Reversed blacklist keywords: {[k.keyword for k in reversed_blacklist]}") + reversed_match = False for keyword in reversed_blacklist: if await check_keyword_match(keyword, message_text): reversed_match = True break - + if not reversed_match: - logger.info("未匹配到反转后的黑名单关键词,不转发") + logger.info("No reversed blacklist keywords matched, not forwarding") return False - logger.info("所有白名单条件都满足,允许转发") + logger.info("All whitelist conditions are met, allowing forward") return True async def process_blacklist_mode(rule, message_text, reverse_whitelist): - """处理仅黑名单模式""" - logger.info("进入仅黑名单模式") + """Process blacklist only mode""" + logger.info("Entering blacklist only mode") - # 检查普通黑名单关键词 + # Check regular blacklist keywords blacklist_keywords = [k for k in rule.keywords if k.is_blacklist] - logger.info(f"普通黑名单关键词: {[k.keyword for k in blacklist_keywords]}") - + logger.info(f"Regular blacklist keywords: {[k.keyword for k in blacklist_keywords]}") + for keyword in blacklist_keywords: if await check_keyword_match(keyword, message_text): - logger.info(f"匹配到黑名单关键词 '{keyword.keyword}',不转发") + logger.info(f"Matched blacklist keyword '{keyword.keyword}', not forwarding") return False - # 如果启用了白名单反转,检查反转后的白名单(作为黑名单) + # If whitelist reversal is enabled, check reversed whitelist (as blacklist) if reverse_whitelist: - logger.info("检查反转后的白名单关键词(作为黑名单)") + logger.info("Checking reversed whitelist keywords (as blacklist)") reversed_whitelist = [k for k in rule.keywords if not k.is_blacklist] - logger.info(f"反转后的白名单关键词: {[k.keyword for k in reversed_whitelist]}") - + logger.info(f"Reversed whitelist keywords: {[k.keyword for k in reversed_whitelist]}") + for keyword in reversed_whitelist: if await check_keyword_match(keyword, message_text): - logger.info(f"匹配到反转后的白名单关键词 '{keyword.keyword}',不转发") + logger.info(f"Matched reversed whitelist keyword '{keyword.keyword}', not forwarding") return False - logger.info("未匹配到任何黑名单关键词,允许转发") + logger.info("No blacklist keywords matched, allowing forward") return True async def check_keyword_match(keyword, message_text): - """检查单个关键词是否匹配""" - logger.info(f"检查关键字: {keyword.keyword} (正则: {keyword.is_regex})") + """Check if a single keyword matches""" + logger.info(f"Checking keyword: {keyword.keyword} (regex: {keyword.is_regex})") if keyword.is_regex: try: if re.search(keyword.keyword, message_text): - logger.info(f"正则匹配成功: {keyword.keyword}") + logger.info(f"Regex match successful: {keyword.keyword}") return True except re.error: - logger.error(f"正则表达式错误: {keyword.keyword}") + logger.error(f"Regex expression error: {keyword.keyword}") else: if keyword.keyword.lower() in message_text.lower(): - logger.info(f"关键字匹配成功: {keyword.keyword}") + logger.info(f"Keyword match successful: {keyword.keyword}") return True return False async def process_user_info(event, rule_id, message_text): - """处理用户信息过滤""" + """Process user info filtering""" username = await get_sender_info(event, rule_id) name = None - + if hasattr(event.message, 'sender_chat') and event.message.sender_chat: sender = event.message.sender_chat name = sender.title if hasattr(sender, 'title') else None @@ -518,112 +518,112 @@ async def process_user_info(event, rule_id, message_text): sender.title if hasattr(sender, 'title') else f"{sender.first_name or ''} {sender.last_name or ''}".strip() ) - + if username and name: - logger.info(f"成功获取用户信息: {username} {name}") + logger.info(f"Successfully retrieved user info: {username} {name}") return f"{username} {name}:\n{message_text}" elif username: - logger.info(f"成功获取用户信息: {username}") + logger.info(f"Successfully retrieved user info: {username}") return f"{username}:\n{message_text}" elif name: - logger.info(f"成功获取用户信息: {name}") + logger.info(f"Successfully retrieved user info: {name}") return f"{name}:\n{message_text}" else: - logger.warning(f"规则 ID: {rule_id} - 无法获取发送者信息") + logger.warning(f"Rule ID: {rule_id} - Unable to get sender information") return message_text async def process_whitelist_then_blacklist_mode(rule, message_text, reverse_blacklist): - """处理先白后黑模式 - - 先检查白名单(必须匹配),然后检查黑名单(不能匹配) - 如果启用黑名单反转,则黑名单变成第二重白名单(必须匹配) + """Process whitelist then blacklist mode + + First check whitelist (must match), then check blacklist (must not match) + If blacklist reversal is enabled, blacklist becomes a second whitelist (must match) """ - logger.info("进入先白后黑模式") + logger.info("Entering whitelist then blacklist mode") - # 检查普通白名单(必须匹配) + # Check regular whitelist (must match) whitelist_match = False whitelist_keywords = [k for k in rule.keywords if not k.is_blacklist] - logger.info(f"检查普通白名单关键词: {[k.keyword for k in whitelist_keywords]}") - + logger.info(f"Checking regular whitelist keywords: {[k.keyword for k in whitelist_keywords]}") + for keyword in whitelist_keywords: if await check_keyword_match(keyword, message_text): whitelist_match = True break - + if not whitelist_match: - logger.info("未匹配到白名单关键词,不转发") + logger.info("No whitelist keywords matched, not forwarding") return False - # 根据反转设置处理黑名单 + # Process blacklist based on reversal setting blacklist_keywords = [k for k in rule.keywords if k.is_blacklist] - + if reverse_blacklist: - # 黑名单反转为白名单,必须匹配才转发 - logger.info("黑名单已反转,作为第二重白名单检查") - logger.info(f"反转后的黑名单关键词: {[k.keyword for k in blacklist_keywords]}") - + # Blacklist reversed to whitelist, must match to forward + logger.info("Blacklist reversed, checking as second whitelist") + logger.info(f"Reversed blacklist keywords: {[k.keyword for k in blacklist_keywords]}") + blacklist_match = False for keyword in blacklist_keywords: if await check_keyword_match(keyword, message_text): blacklist_match = True break - + if not blacklist_match: - logger.info("未匹配到反转后的黑名单关键词,不转发") + logger.info("No reversed blacklist keywords matched, not forwarding") return False else: - # 正常黑名单,匹配则不转发 - logger.info(f"检查普通黑名单关键词: {[k.keyword for k in blacklist_keywords]}") + # Normal blacklist, match means do not forward + logger.info(f"Checking regular blacklist keywords: {[k.keyword for k in blacklist_keywords]}") for keyword in blacklist_keywords: if await check_keyword_match(keyword, message_text): - logger.info(f"匹配到黑名单关键词 '{keyword.keyword}',不转发") + logger.info(f"Matched blacklist keyword '{keyword.keyword}', not forwarding") return False - logger.info("所有条件都满足,允许转发") + logger.info("All conditions are met, allowing forward") return True async def process_blacklist_then_whitelist_mode(rule, message_text, reverse_whitelist): - """处理先黑后白模式 - - 先检查黑名单(不能匹配),然后检查白名单(必须匹配) - 如果启用白名单反转,则白名单变成第二重黑名单(不能匹配) + """Process blacklist then whitelist mode + + First check blacklist (must not match), then check whitelist (must match) + If whitelist reversal is enabled, whitelist becomes a second blacklist (must not match) """ - logger.info("进入先黑后白模式") + logger.info("Entering blacklist then whitelist mode") - # 检查普通黑名单(匹配则拒绝) + # Check regular blacklist (match means reject) blacklist_keywords = [k for k in rule.keywords if k.is_blacklist] - logger.info(f"检查普通黑名单关键词: {[k.keyword for k in blacklist_keywords]}") - + logger.info(f"Checking regular blacklist keywords: {[k.keyword for k in blacklist_keywords]}") + for keyword in blacklist_keywords: if await check_keyword_match(keyword, message_text): - logger.info(f"匹配到黑名单关键词 '{keyword.keyword}',不转发") + logger.info(f"Matched blacklist keyword '{keyword.keyword}', not forwarding") return False - # 处理白名单 + # Process whitelist whitelist_keywords = [k for k in rule.keywords if not k.is_blacklist] - + if reverse_whitelist: - # 白名单反转为黑名单,匹配则不转发 - logger.info("白名单已反转,作为第二重黑名单检查") - logger.info(f"反转后的白名单关键词: {[k.keyword for k in whitelist_keywords]}") - + # Whitelist reversed to blacklist, match means do not forward + logger.info("Whitelist reversed, checking as second blacklist") + logger.info(f"Reversed whitelist keywords: {[k.keyword for k in whitelist_keywords]}") + for keyword in whitelist_keywords: if await check_keyword_match(keyword, message_text): - logger.info(f"匹配到反转后的白名单关键词 '{keyword.keyword}',不转发") + logger.info(f"Matched reversed whitelist keyword '{keyword.keyword}', not forwarding") return False else: - # 正常白名单,必须匹配才转发 - logger.info(f"检查普通白名单关键词: {[k.keyword for k in whitelist_keywords]}") + # Normal whitelist, must match to forward + logger.info(f"Checking regular whitelist keywords: {[k.keyword for k in whitelist_keywords]}") whitelist_match = False for keyword in whitelist_keywords: if await check_keyword_match(keyword, message_text): whitelist_match = True break - + if not whitelist_match: - logger.info("未匹配到白名单关键词,不转发") + logger.info("No whitelist keywords matched, not forwarding") return False - logger.info("所有条件都满足,允许转发") + logger.info("All conditions are met, allowing forward") return True diff --git a/utils/constants.py b/utils/constants.py index dd588a5..86d43ce 100644 --- a/utils/constants.py +++ b/utils/constants.py @@ -2,20 +2,20 @@ from pathlib import Path from dotenv import load_dotenv -# 加载环境变量 +# Load environment variables load_dotenv() -# 目录配置 +# Directory configuration BASE_DIR = Path(__file__).parent.parent TEMP_DIR = os.path.join(BASE_DIR, 'temp') RSS_HOST = os.getenv('RSS_HOST', '127.0.0.1') RSS_PORT = os.getenv('RSS_PORT', '8000') -# RSS基础URL,如果未设置,则使用请求的URL +# RSS base URL, if not set, use the request URL RSS_BASE_URL = os.environ.get('RSS_BASE_URL', None) -# RSS媒体文件的基础URL,用于生成媒体链接,如果未设置,则使用请求的URL +# RSS media file base URL, used for generating media links, if not set, use the request URL RSS_MEDIA_BASE_URL = os.getenv('RSS_MEDIA_BASE_URL', '') RSS_ENABLED = os.getenv('RSS_ENABLED', 'false') @@ -26,32 +26,32 @@ DEFAULT_TIMEZONE = os.getenv('DEFAULT_TIMEZONE', 'Asia/Shanghai') PROJECT_NAME = os.getenv('PROJECT_NAME', 'TG Forwarder RSS') -# RSS相关路径配置 +# RSS related path configuration RSS_MEDIA_PATH = os.getenv('RSS_MEDIA_PATH', './rss/media') -# 转换为绝对路径 -RSS_MEDIA_DIR = os.path.abspath(os.path.join(BASE_DIR, RSS_MEDIA_PATH) - if not os.path.isabs(RSS_MEDIA_PATH) +# Convert to absolute path +RSS_MEDIA_DIR = os.path.abspath(os.path.join(BASE_DIR, RSS_MEDIA_PATH) + if not os.path.isabs(RSS_MEDIA_PATH) else RSS_MEDIA_PATH) -# RSS数据路径 +# RSS data path RSS_DATA_PATH = os.getenv('RSS_DATA_PATH', './rss/data') RSS_DATA_DIR = os.path.abspath(os.path.join(BASE_DIR, RSS_DATA_PATH) if not os.path.isabs(RSS_DATA_PATH) else RSS_DATA_PATH) -# 默认AI模型 +# Default AI model DEFAULT_AI_MODEL = os.getenv('DEFAULT_AI_MODEL', 'gpt-4o') -# 默认AI总结提示词 -DEFAULT_SUMMARY_PROMPT = os.getenv('DEFAULT_SUMMARY_PROMPT', '请总结以下频道/群组24小时内的消息。') -# 默认AI提示词 -DEFAULT_AI_PROMPT = os.getenv('DEFAULT_AI_PROMPT', '请尊重原意,保持原有格式不变,用简体中文重写下面的内容:') +# Default AI summary prompt +DEFAULT_SUMMARY_PROMPT = os.getenv('DEFAULT_SUMMARY_PROMPT', 'Please summarize the following channel/group messages from the last 24 hours.') +# Default AI prompt +DEFAULT_AI_PROMPT = os.getenv('DEFAULT_AI_PROMPT', 'Please respect the original meaning, keep the original format unchanged, and rewrite the following content in Simplified Chinese:') -# 分页配置 +# Pagination configuration MODELS_PER_PAGE = int(os.getenv('AI_MODELS_PER_PAGE', 10)) KEYWORDS_PER_PAGE = int(os.getenv('KEYWORDS_PER_PAGE', 50)) -# 按钮布局配置 +# Button layout configuration SUMMARY_TIME_ROWS = int(os.getenv('SUMMARY_TIME_ROWS', 10)) SUMMARY_TIME_COLS = int(os.getenv('SUMMARY_TIME_COLS', 6)) @@ -67,48 +67,48 @@ LOG_MAX_SIZE_MB = 10 LOG_BACKUP_COUNT = 3 -# 默认消息删除时间 (秒) +# Default message deletion time (seconds) BOT_MESSAGE_DELETE_TIMEOUT = int(os.getenv("BOT_MESSAGE_DELETE_TIMEOUT", 300)) -# 自动删除用户发送的指令消息 +# Auto-delete user-sent command messages USER_MESSAGE_DELETE_ENABLE = os.getenv("USER_MESSAGE_DELETE_ENABLE", "false") -# 是否启用UFB +# Whether to enable UFB UFB_ENABLED = os.getenv("UFB_ENABLED", "false") -# 菜单标题 +# Menu title AI_SETTINGS_TEXT = """ -当前AI提示词: +Current AI prompt: `{ai_prompt}` -当前总结提示词: +Current summary prompt: `{summary_prompt}` """ -# 媒体设置文本 +# Media settings text MEDIA_SETTINGS_TEXT = """ -媒体设置: +Media settings: """ PUSH_SETTINGS_TEXT = """ -推送设置: -请前往 https://github.com/caronc/apprise/wiki 查看添加推送配置格式说明 -如 `ntfy://ntfy.sh/你的主题名` +Push settings: +Please visit https://github.com/caronc/apprise/wiki to see the push configuration format instructions +e.g. `ntfy://ntfy.sh/your_topic_name` """ -# 为每个规则生成特定的路径 +# Generate specific paths for each rule def get_rule_media_dir(rule_id): - """获取指定规则的媒体目录""" + """Get the media directory for a specified rule""" rule_path = os.path.join(RSS_MEDIA_DIR, str(rule_id)) - # 确保目录存在 + # Ensure the directory exists os.makedirs(rule_path, exist_ok=True) return rule_path def get_rule_data_dir(rule_id): - """获取指定规则的数据目录""" + """Get the data directory for a specified rule""" rule_path = os.path.join(RSS_DATA_DIR, str(rule_id)) - # 确保目录存在 + # Ensure the directory exists os.makedirs(rule_path, exist_ok=True) - return rule_path \ No newline at end of file + return rule_path diff --git a/utils/file_creator.py b/utils/file_creator.py index f797e7f..4940960 100644 --- a/utils/file_creator.py +++ b/utils/file_creator.py @@ -4,7 +4,7 @@ logger = logging.getLogger(__name__) -# 默认AI模型配置(JSON格式) +# Default AI model configuration (JSON format) AI_MODELS_CONFIG = { "openai": [ "gpt-4o", @@ -73,7 +73,7 @@ ] } -# 汇总时间列表 +# Summary time list SUMMARY_TIMES_CONTENT = """00:00 00:30 01:00 @@ -124,7 +124,7 @@ 23:30 23:50""" -# 延迟时间列表 +# Delay time list DELAY_TIMES_CONTENT = """1 2 3 @@ -136,7 +136,7 @@ 9 10""" -# 最大媒体大小列表 +# Maximum media size list MAX_MEDIA_SIZE_CONTENT = """1 2 3 @@ -186,7 +186,7 @@ 2048 """ -MEDIA_EXTENSIONS_CONTENT = """无扩展名 +MEDIA_EXTENSIONS_CONTENT = """No extension jpg jpeg png @@ -250,11 +250,11 @@ def create_default_configs(): - """创建默认配置文件""" + """Create default configuration files""" config_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config') os.makedirs(config_dir, exist_ok=True) - # 定义默认配置内容 + # Define default configuration content default_configs = { 'summary_times.txt': SUMMARY_TIMES_CONTENT, 'delay_times.txt': DELAY_TIMES_CONTENT, @@ -262,15 +262,15 @@ def create_default_configs(): 'media_extensions.txt': MEDIA_EXTENSIONS_CONTENT, } - # 检查并创建每个配置文件 + # Check and create each configuration file for filename, content in default_configs.items(): file_path = os.path.join(config_dir, filename) if not os.path.exists(file_path): with open(file_path, 'w', encoding='utf-8') as f: f.write(content.strip()) logger.info(f"Created {filename}") - - # 创建JSON格式的AI模型配置文件 + + # Create JSON format AI model configuration file json_config_path = os.path.join(config_dir, 'ai_models.json') if not os.path.exists(json_config_path): try: @@ -278,4 +278,4 @@ def create_default_configs(): json.dump(AI_MODELS_CONFIG, f, ensure_ascii=False, indent=4) logger.info("Created ai_models.json") except Exception as e: - logger.error(f"创建 ai_models.json 失败: {e}") \ No newline at end of file + logger.error(f"Failed to create ai_models.json: {e}") diff --git a/utils/log_config.py b/utils/log_config.py index 97bb3d0..7013094 100644 --- a/utils/log_config.py +++ b/utils/log_config.py @@ -5,29 +5,29 @@ def setup_logging(): """ - 配置日志系统,将所有日志输出到标准输出, - 由Docker收集并管理日志 + Configure the logging system, output all logs to stdout, + collected and managed by Docker """ - # 加载环境变量 + # Load environment variables load_dotenv() - - # 创建根日志记录器 + + # Create root logger root_logger = logging.getLogger() - - # 设置日志级别 - 默认使用INFO级别 + + # Set log level - default to INFO level root_logger.setLevel(logging.INFO) - - # 创建一个处理器,用于将日志输出到控制台 + + # Create a handler to output logs to the console console_handler = logging.StreamHandler() - - # 创建格式化器 + + # Create formatter formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - - # 将格式化器添加到处理器 + + # Add formatter to handler console_handler.setFormatter(formatter) - - # 将处理器添加到根日志记录器 + + # Add handler to root logger root_logger.addHandler(console_handler) - - # 返回配置的日志记录器 - return root_logger \ No newline at end of file + + # Return the configured logger + return root_logger diff --git a/utils/media.py b/utils/media.py index 7f46b39..730691e 100644 --- a/utils/media.py +++ b/utils/media.py @@ -4,34 +4,34 @@ logger = logging.getLogger(__name__) async def get_media_size(media): - """获取媒体文件大小""" + """Get media file size""" if not media: return 0 try: - # 对于所有类型的媒体,先尝试获取 document + # For all types of media, first try to get the document if hasattr(media, 'document') and media.document: return media.document.size - # 对于照片,获取最大尺寸 + # For photos, get the largest size if hasattr(media, 'photo') and media.photo: - # 获取最大尺寸的照片 + # Get the photo with the largest size largest_photo = max(media.photo.sizes, key=lambda x: x.size if hasattr(x, 'size') else 0) return largest_photo.size if hasattr(largest_photo, 'size') else 0 - # 如果是其他类型,尝试直接获取 size 属性 + # If it's another type, try to directly get the size attribute if hasattr(media, 'size'): return media.size except Exception as e: - logger.error(f'获取媒体大小时出错: {str(e)}') + logger.error(f'Error getting media size: {str(e)}') return 0 async def get_max_media_size(): - """获取媒体文件大小上限""" + """Get media file size upper limit""" max_media_size_str = os.getenv('MAX_MEDIA_SIZE') if not max_media_size_str: - logger.error('未设置 MAX_MEDIA_SIZE 环境变量') - raise ValueError('必须在 .env 文件中设置 MAX_MEDIA_SIZE') - return float(max_media_size_str) * 1024 * 1024 # 转换为字节,支持小数 \ No newline at end of file + logger.error('MAX_MEDIA_SIZE environment variable is not set') + raise ValueError('MAX_MEDIA_SIZE must be set in the .env file') + return float(max_media_size_str) * 1024 * 1024 # Convert to bytes, supports decimals diff --git a/utils/settings.py b/utils/settings.py index 1fa171e..7de6508 100644 --- a/utils/settings.py +++ b/utils/settings.py @@ -8,109 +8,109 @@ def load_ai_models(type="list"): """ - 加载AI模型配置 - - 参数: - type (str): 返回类型 - - "list": 返回所有模型的平铺列表 [model1, model2, ...] - - "dict"/"json": 返回原始配置格式 {provider: [model1, model2, ...]} - - 返回值: - 根据type参数返回不同格式的模型配置 + Load AI model configuration + + Args: + type (str): Return type + - "list": Return a flat list of all models [model1, model2, ...] + - "dict"/"json": Return the original configuration format {provider: [model1, model2, ...]} + + Returns: + Model configuration in different formats depending on the type parameter """ try: models_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config', 'ai_models.json') - - # 如果配置文件不存在,创建默认配置 + + # If the configuration file does not exist, create default configuration if not os.path.exists(models_path): create_default_configs() - - # 读取JSON配置文件 + + # Read JSON configuration file with open(models_path, 'r', encoding='utf-8') as f: models_config = json.load(f) - - # 根据type参数返回不同格式 + + # Return different formats based on the type parameter if type.lower() in ["dict", "json"]: return models_config - - # 默认返回模型列表 + + # Default: return model list all_models = [] for provider, models in models_config.items(): all_models.extend(models) - - # 确保列表不为空 + + # Ensure the list is not empty if all_models: return all_models - + except (FileNotFoundError, IOError, json.JSONDecodeError) as e: - logger.error(f"加载AI模型配置失败: {e}") - - # 如果出现任何问题,根据type返回默认值 + logger.error(f"Failed to load AI model configuration: {e}") + + # If any issue occurs, return default value based on type if type.lower() in ["dict", "json"]: return AI_MODELS_CONFIG - - # 默认返回模型列表 + + # Default: return model list return ["gpt-3.5-turbo", "gemini-1.5-flash", "claude-3-sonnet"] def load_summary_times(): - """加载总结时间列表""" + """Load summary time list""" try: times_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config', 'summary_times.txt') if not os.path.exists(times_path): create_default_configs() - + with open(times_path, 'r', encoding='utf-8') as f: times = [line.strip() for line in f if line.strip()] if times: return times except (FileNotFoundError, IOError) as e: - logger.warning(f"summary_times.txt 加载失败: {e},使用默认时间列表") + logger.warning(f"Failed to load summary_times.txt: {e}, using default time list") return ['00:00', '06:00', '12:00', '18:00'] def load_delay_times(): - """加载延迟时间列表""" + """Load delay time list""" try: times_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config', 'delay_times.txt') if not os.path.exists(times_path): create_default_configs() - + with open(times_path, 'r', encoding='utf-8') as f: times = [line.strip() for line in f if line.strip()] if times: return times except (FileNotFoundError, IOError) as e: - logger.warning(f"delay_times.txt 加载失败: {e},使用默认时间列表") + logger.warning(f"Failed to load delay_times.txt: {e}, using default time list") return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] def load_max_media_size(): - """加载媒体大小限制""" + """Load media size limit""" try: size_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config', 'max_media_size.txt') if not os.path.exists(size_path): create_default_configs() - + with open(size_path, 'r', encoding='utf-8') as f: size = [line.strip() for line in f if line.strip()] if size: return size - + except (FileNotFoundError, IOError) as e: - logger.warning(f"max_media_size.txt 加载失败: {e},使用默认大小限制") + logger.warning(f"Failed to load max_media_size.txt: {e}, using default size limit") return [5,10,15,20,50,100,200,300,500,1024,2048] def load_media_extensions(): - """加载媒体扩展名""" + """Load media extensions""" try: size_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config', 'media_extensions.txt') if not os.path.exists(size_path): create_default_configs() - + with open(size_path, 'r', encoding='utf-8') as f: size = [line.strip() for line in f if line.strip()] if size: return size - + except (FileNotFoundError, IOError) as e: - logger.warning(f"media_extensions.txt 加载失败: {e},使用默认扩展名") - return ['无扩展名','txt','jpg','png','gif','mp4','mp3','wav','ogg','flac','aac','wma','m4a','m4v','mov','avi','mkv','webm','mpg','mpeg','mpe','mp3','mp2','m4a','m4p','m4b','m4r','m4v','mpg','mpeg','mp2','mp3','mp4','mpc','oga','ogg','wav','wma','3gp','3g2','3gpp','3gpp2','amr','awb','caf','flac','m4a','m4b','m4p','oga','ogg','opus','spx','vorbis','wav','wma','webm','aac','ac3','dts','dtshd','flac','mp3','mp4','m4a','m4b','m4p','oga','ogg','wav','wma','webm','aac','ac3','dts','dtshd','flac','mp3','mp4','m4a','m4b','m4p','oga','ogg','wav','wma','webm'] \ No newline at end of file + logger.warning(f"Failed to load media_extensions.txt: {e}, using default extensions") + return ['No extension','txt','jpg','png','gif','mp4','mp3','wav','ogg','flac','aac','wma','m4a','m4v','mov','avi','mkv','webm','mpg','mpeg','mpe','mp3','mp2','m4a','m4p','m4b','m4r','m4v','mpg','mpeg','mp2','mp3','mp4','mpc','oga','ogg','wav','wma','3gp','3g2','3gpp','3gpp2','amr','awb','caf','flac','m4a','m4b','m4p','oga','ogg','opus','spx','vorbis','wav','wma','webm','aac','ac3','dts','dtshd','flac','mp3','mp4','m4a','m4b','m4p','oga','ogg','wav','wma','webm','aac','ac3','dts','dtshd','flac','mp3','mp4','m4a','m4b','m4p','oga','ogg','wav','wma','webm'] diff --git a/version.py b/version.py index eed3ea9..caf0f0c 100644 --- a/version.py +++ b/version.py @@ -1,32 +1,32 @@ VERSION = "1.7.2" -# 版本号说明 +# Version number description VERSION_INFO = { - "major": 1, # 主版本号:重大更新,可能不兼容旧版本 - "feature": 7, # 功能版本号:添加重要新功能 - "minor": 2, # 次要版本号:添加小功能或优化 - "patch": 0, # 补丁版本号:Bug修复和小改动 -} + "major": 1, # Major version: significant updates, may be incompatible with older versions + "feature": 7, # Feature version: addition of important new features + "minor": 2, # Minor version: addition of small features or optimizations + "patch": 0, # Patch version: bug fixes and minor changes +} -UPDATE_INFO = """
✨ 更新日志 v1.7.2 +UPDATE_INFO = """
✨ Changelog v1.7.2 -- 提高AI总结的健壮性 @iCross https://github.com/Heavrnl/TelegramForwarder/pull/47 +- Improved robustness of AI summary @iCross https://github.com/Heavrnl/TelegramForwarder/pull/47
""" WELCOME_TEXT = """ -🎉 欢迎使用 TelegramForwarder ! - -如果您觉得这个项目对您有帮助,欢迎通过以下方式支持我: +🎉 Welcome to TelegramForwarder! -
给项目点个小小的 Star: TelegramForwarder -☕ 请我喝杯咖啡: Ko-fi
+If you find this project helpful, feel free to support me through the following: -当前版本: v1.7.2 -更新日志: /changelog +
Give the project a Star: TelegramForwarder +☕ Buy me a coffee: Ko-fi
-感谢您的支持! +Current version: v1.7.2 +Changelog: /changelog + +Thank you for your support! """ \ No newline at end of file