diff --git a/README.md b/README.md index 4387bb5..f89a391 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ -# WeChatter +# WeChatter: Make WeChat Better
[![CI/CD](https://github.com/Cassius0924/WeChatter/actions/workflows/test.yml/badge.svg)](https://github.com/Cassius0924/WeChatter/actions/workflows/test.yml) [![GitHub Release](https://img.shields.io/github/v/release/Cassius0924/WeChatter)](https://github.com/Cassius0924/WeChatter/releases) [![GitHub License](https://img.shields.io/github/license/Cassius0924/WeChatter)](https://github.com/Cassius0924/WeChatter/blob/master/LICENSE) +![Welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)
@@ -12,6 +13,8 @@ 基于 [wechatbot-webhook](https://github.com/danni-cool/wechatbot-webhook) 的微信机器人💬,支持 GPT 问答、热搜、天气预报、消息转发、Webhook提醒等功能。 +[![wechatter show](docs/images/wechatter_show.png)](docs/command_show.md) + ## 快速开始 ### 运行 wechatbot-webhook @@ -57,6 +60,8 @@ cd WeChatter 2. 安装依赖项 ```bash +# 如果需要,则创建虚拟环境... + pip install -r requirements.txt ``` @@ -80,37 +85,34 @@ python3 main.py ## 支持的命令 - [x] GPT 问答,基于 [Copilot-GPT4-Server](https://github.com/aaamoon/copilot-gpt4-service) -- [x] 获取 Bilibili 热搜 -- [x] 获取知乎热搜 -- [x] 获取微博热搜 -- [x] 获取抖音热搜 -- [x] 获取 GitHub 趋势 -- [x] 单词/词语翻译 -- [x] 获取少数派早报 -- [x] 获取历史上的今天 -- [x] 二维码生成器 +- [x] Bilibili 热搜 +- [x] 知乎热搜 +- [x] 微博热搜 +- [x] 抖音热搜 +- [x] GitHub 趋势 +- [x] 单词词语翻译 +- [x] 少数派早报 +- [x] 历史上的今天 +- [x] 二维码生成 - [x] 待办清单(TODO) -- [x] 获取人民日报PDF -- [x] 获取天气预报 -- [x] 获取食物热量/卡路里 -- [x] 随机获取冷知识 -- [x] 获取中石化92号汽油指导价 +- [x] 人民日报PDF +- [x] 天气预报 +- [x] 食物热量 +- [x] 冷知识 +- [x] 中石化92号汽油指导价 > [!TIP] -> 更多命令使用 `/help` 命令查看。 +> 命令帮助请使用 `/help` 命令查询或查看[命令功能展示](docs/command_show.md)。 ## 支持的功能 -- [x] 消息转发,需[配置](#%EF%B8%8F-message-forwarding-配置) -- [x] 天气预报定时推送,需[配置](#%EF%B8%8F-weather-cron-配置) -- [x] 中石化92号汽油指导价定时推送,需[配置](#%EF%B8%8F-gasoline-price-cron-配置) +- [x] 消息转发,需[配置](#%EF%B8%8F-message-forwarding-配置)。 +- [x] 消息可引用回复,用户通过引用并回复可以进一步获取消息内容。带“(可引用:***)”的机器人消息均为可进一步互动的可引用消息。 +- [x] 天气预报定时推送,需[配置](#%EF%B8%8F-weather-cron-配置)。 ## 支持的 Webhook -- [x] GitHub 仓库 Webhook,需[配置](#%EF%B8%8F-github-webhook-配置) - -> [!NOTE] -> 需要在 GitHub 仓库 Settings 中添加 Webhook +- [x] GitHub 仓库 Webhook,需在 GitHub 仓库 Settings 中添加 Webhook 并[配置](#%EF%B8%8F-github-webhook-配置) ## 配置文件 @@ -127,43 +129,41 @@ python3 main.py ### ⚙️ WxBotWebhook 配置 | 配置项 | 解释 | 备注 | -| --- | --- | --- | -| `wx_webhook_host` | 发送消息的地址 | 默认为 `localhost`,需和 `wxBotWebhook` 的 Docker IP 地址一致 | -| `wx_webhook_port` | 发送消息的端口 | 默认为 `3001`,需和 `wxBotWebhook` 的 Docker 端口一致 | -| `wx_webhook_recv_api_path` | 接收消息的接口路径 | 默认为 `/receive_msg`,此路径为 `RECV_MSG_API` 的路径 | +| --- | --- | --- | +| `wx_webhook_base_api` | 发送消息的 BaseAPI | 默认为 `localhost:3001`,即 `wxBotWebhook` Docker 的地址 | +| `wx_webhook_recv_api_path` | 接收消息的接口路径 | 默认为 `/receive_msg`,此路径为 Docker 参数 `RECVD_MSG_API` 的路径 | ### ⚙️ Admin 配置 | 配置项 | 解释 | 备注 | -| --- | --- | --- | +| --- | --- | --- | | `admin_list` | 设置管理员,用于接收机器人状态变化通知 | 填入管理员微信名(不是备注) | | `admin_group_list` | 与 `admin_list` 同理,接收机器人状态变化通知 | 填入群名称(不是群备注) | ### ⚙️ Bot 配置 | 配置项 | 解释 | 备注 | -| --- | --- | --- | +| --- | --- | --- | | `bot_name` | 微信机器人的名字 | 微信名称,非微信号 | ### ⚙️ Chat 配置 | 配置项 | 解释 | 备注 | -| --- | --- | --- | +| --- | --- | --- | | `command_prefix` | 机器人命令前缀 | 默认为 `/` ,可以设置为`>>`、`!` 等 | | `need_mentioned` | 群聊中的命令是否需要@机器人 | 默认为 `True` | ### ⚙️ Copilot GPT4 配置 | 配置项 | 解释 | 备注 | -| --- | --- | --- | -| `cp_gpt4_api_host` | CopilotGPT4 服务的API地址 | 默认为 `http://localhost` | -| `cp_gpt4_port` | CopilotGPT4 服务的端口 | 默认为 `8080` | -| `cp_token` | Copilot 的 Token | 以 `ghu_` 开头的字符串 | +| --- | --- | --- | +| `cp_gpt4_base_api` | CopilotGPT4 服务的 BaseAPI | 默认为 `http://localhost:8080` | +| `cp_token` | GitHub Copilot 的 Token | 以 `ghu_` 开头的字符串 | ### ⚙️ GitHub Webhook 配置 | 配置项 | 解释 | 备注 | -| --- | --- | --- | +| --- | --- | --- | | `github_webhook_enabled` | 功能开关,是否接收 GitHub Webhook | 默认为 `False` | | `github_webhook_api_path` | 接收 GitHub Webhook 的接口路径 | 默认为 `/webhook/github` | | `github_webhook_receiver_list` | 接收 GitHub Webhook 的微信用户 | | @@ -175,14 +175,14 @@ python3 main.py | --- | --- | --- | --- | | `message_forwarding_enabled` | | 功能开关,是否开启消息转发 | 默认为 `False` | | `message_forwarding_rule_list` | | 消息规则列表,每个规则包含三个字段:`froms`, `to_persons` 和 `to_groups` | 规则是由字典组成的JSON列表,最后的 `]` 不能单独一行 | -| ➤➤➤ | `froms` | 消息转发来源列表,即消息发送者 | 可以填多个用户名称或群名称 | -| ➤➤➤ | `to_persons` | 消息转发目标用户列表,即消息接收用户 | 可以填多个用户名称或为空列表 | -| ➤➤➤ | `to_groups` | 消息转发目标群列表,即消息接收群 | 可以填多个群名称或为空列表 | +| | `froms` | 消息转发来源列表,即消息发送者 | 可以填多个用户名称或群名称 | +| | `to_persons` | 消息转发目标用户列表,即消息接收用户 | 可以填多个用户名称或为空列表 | +| | `to_groups` | 消息转发目标群列表,即消息接收群 | 可以填多个群名称或为空列表 | ### ⚙️ Weather Cron 配置 | 配置项 | 解释 | 备注 | -| --- | --- | --- | +| --- | --- | --- | | `weather_cron_enabled` | 功能开关,是否开启定时天气推送 | 默认为 `False` | | `weather_cron_rule_list` | 推送规则列表,每个规则包含两个字段:`cron` 和 `tasks` | | @@ -191,19 +191,16 @@ python3 main.py ### ⚙️ Custom Command Key 配置 | 配置项 | 解释 | 备注 | -| --- | --- | --- | +| --- | --- | --- | | `custom_command_key_dict` | 自定义命令关键词字典,格式为 `command: [key1, key2, ...]`, 其中 `command` 为命令名称,`key1` 和 `key2` 为自定义命令关键词 | | 关于命令名称可选值详见[自定义命令关键词配置详细](docs/custom_command_key_config_detail.md) -### ⚙️ Gasoline Price Cron 配置 +# 贡献者 -| 配置项 | 解释 | 备注 | -| --- | --- | --- | -| `gasoline_price_cron_enabled` | 功能开关,是否开启定时推送92号汽油指导价 | 默认为 `False` | -| `gasoline_price_cron_rule_list` | 推送规则列表,每个规则包含两个字段:`cron` 和 `tasks` | | +Thanks to the following people who have contributed to this project: -关于 `cron` 和 `tasks` 的配置见[中石化92号汽油指导价定时任务配置详细](docs/gasoline_price_cron_config_detail.md) +[![Contributors](https://contrib.rocks/image?repo=Cassius0924/WeChatter)](https://github.com/Cassius0924/WeChatter/graphs/contributors) ## 插件化 diff --git a/config.ini.example b/config.ini.example index b63c009..b92e62e 100644 --- a/config.ini.example +++ b/config.ini.example @@ -7,10 +7,8 @@ wechatter_port = 4000 [wx-bot-webhook] -# 发送消息的 API 地址,必须包含http(s)://(<>) -wx_webhook_host = http://localhost -# 发送消息的端口,wxBotWebhook Docker 的端口(<<3001>>) -wx_webhook_port = 3001 +# 发送消息的 API 地址,必须包含"http(s)://"(<>) +wx_webhook_base_api = http://localhost:3001 # 接收消息的接口路径,RECV_MSG_API的路径(<>) wx_webhook_recv_api_path = /receive_msg @@ -36,17 +34,15 @@ need_mentioned = True [copilot-gpt4] -# Copilot GPT4 服务的 API 地址,必须包含http(s)://(<>) -cp_gpt4_api_host = http://localhost -# Copilot GPT4 服务的端口(<<999>>) -cp_gpt4_port = 999 +# Copilot GPT4 服务的 API 地址,必须包含http(s)://(<>) +cp_gpt4_base_api = http://localhost:999 # Copilot 的 Token cp_token = ghu_your_token [github-webhook] # 是否接收 GitHub Webhook (True/<>) -github_webhook_enabled = True +github_webhook_enabled = False # 接收 GitHub Webhook 的接口路径(<>) github_webhook_api_path = /webhook/github # 接收 GitHub Webhook 的微信用户 diff --git a/docs/command_show.md b/docs/command_show.md new file mode 100644 index 0000000..6166346 --- /dev/null +++ b/docs/command_show.md @@ -0,0 +1,127 @@ +# 命令功能展示 + +本文档展示了 WeChatter 支持的命令功能。 + +## 目录 +- [天气预报](#天气预报) +- [待办清单](#待办清单) + - [添加待办](#添加待办) + - [删除待办](#删除待办) + - [查看待办](#查看待办) +- [Bilibili 热搜](#Bilibili-热搜) +- [知乎热搜](#知乎热搜) +- [微博热搜](#微博热搜) +- [抖音热搜](#抖音热搜) +- [GitHub 趋势](#GitHub-趋势) +- [GPT 问答](#GPT-问答) + - [持续对话](#持续对话) + - [新建对话](#新建对话) + - [所有对话](#所有对话) + - [继续对话](#继续对话) + - [对话记录](#对话记录) +- [单词词语翻译](#单词词语翻译) +- [少数派早报](#少数派早报) +- [人民日报](#人民日报) +- [二维码生成](#二维码生成) +- [食物热量](#食物热量) +- [中石化92号汽油指导价](#中石化92号汽油指导价) +- [冷知识](#冷知识) +- [历史上的今天](#历史上的今天) + +## 天气预报 + +![获取天气预报](./images/cmd_weather.png) + +## 待办清单 + +### 添加待办 + +![待办清单](./images/cmd_todo_1.png) + +### 删除待办 + +![待办清单](./images/cmd_todo_2.png) + +### 查看待办 + +![待办清单](./images/cmd_todo_3.png) + +## Bilibili 热搜 + +![获取 Bilibili 热搜](./images/cmd_bili_hot.png) + +## 知乎热搜 + +![获取知乎热搜](./images/cmd_zhihu_hot.png) + +## 微博热搜 + +![获取微博热搜](./images/cmd_weibo_hot.png) + +## 抖音热搜 + +![获取抖音热搜](./images/cmd_douyin_hot.png) + +## GitHub 趋势 + +![获取 GitHub 趋势](./images/cmd_github_trending.png) + +## GPT 问答 + +### 持续对话 + +![GPT 问答](./images/cmd_gpt4_1.png) + +### 新建对话 + +![GPT 问答](./images/cmd_gpt4_2.png) + +### 所有对话 + +![GPT 问答](./images/cmd_gpt4_3.png) + +### 继续对话 + +![GPT 问答](./images/cmd_gpt4_4.png) + +### 对话记录 + +![GPT 问答](./images/cmd_gpt4_5.png) + +## 单词词语翻译 + +![单词词语翻译](./images/cmd_word.png) + +## 少数派早报 + +![少数派早报](./images/cmd_pai_post.png) + +## 人民日报 + +### PDF 文件 + +![人民日报](./images/cmd_people_daily_1.png) + +### 日报链接 + +![人民日报](./images/cmd_people_daily_2.png) + +## 二维码生成 + +![二维码生成](./images/cmd_qrcode.png) + +## 食物热量 + +![食物热量](./images/cmd_food_calories.png) + +## 中石化92号汽油指导价 + +![中石化92号汽油指导价](./images/cmd_gasoline_price.png) + +## 冷知识 + +![冷知识](./images/cmd_trivia.png) + +## 历史上的今天 + +![历史上的今天](./images/cmd_today_in_history.png) diff --git a/docs/images/cmd_bili_hot.png b/docs/images/cmd_bili_hot.png new file mode 100644 index 0000000..87fa242 Binary files /dev/null and b/docs/images/cmd_bili_hot.png differ diff --git a/docs/images/cmd_douyin_hot.png b/docs/images/cmd_douyin_hot.png new file mode 100644 index 0000000..bd768e4 Binary files /dev/null and b/docs/images/cmd_douyin_hot.png differ diff --git a/docs/images/cmd_food_calories.png b/docs/images/cmd_food_calories.png new file mode 100644 index 0000000..b6de435 Binary files /dev/null and b/docs/images/cmd_food_calories.png differ diff --git a/docs/images/cmd_gasoline_price.png b/docs/images/cmd_gasoline_price.png new file mode 100644 index 0000000..d21868a Binary files /dev/null and b/docs/images/cmd_gasoline_price.png differ diff --git a/docs/images/cmd_github_trending.png b/docs/images/cmd_github_trending.png new file mode 100644 index 0000000..03c24b1 Binary files /dev/null and b/docs/images/cmd_github_trending.png differ diff --git a/docs/images/cmd_gpt4_1.png b/docs/images/cmd_gpt4_1.png new file mode 100644 index 0000000..0c28cd4 Binary files /dev/null and b/docs/images/cmd_gpt4_1.png differ diff --git a/docs/images/cmd_gpt4_2.png b/docs/images/cmd_gpt4_2.png new file mode 100644 index 0000000..0c603c8 Binary files /dev/null and b/docs/images/cmd_gpt4_2.png differ diff --git a/docs/images/cmd_gpt4_3.png b/docs/images/cmd_gpt4_3.png new file mode 100644 index 0000000..5343dd0 Binary files /dev/null and b/docs/images/cmd_gpt4_3.png differ diff --git a/docs/images/cmd_gpt4_4.png b/docs/images/cmd_gpt4_4.png new file mode 100644 index 0000000..108508f Binary files /dev/null and b/docs/images/cmd_gpt4_4.png differ diff --git a/docs/images/cmd_gpt4_5.png b/docs/images/cmd_gpt4_5.png new file mode 100644 index 0000000..ebfb49d Binary files /dev/null and b/docs/images/cmd_gpt4_5.png differ diff --git a/docs/images/cmd_pai_post.png b/docs/images/cmd_pai_post.png new file mode 100644 index 0000000..5b99d4a Binary files /dev/null and b/docs/images/cmd_pai_post.png differ diff --git a/docs/images/cmd_people_daily_1.png b/docs/images/cmd_people_daily_1.png new file mode 100644 index 0000000..fd9c98e Binary files /dev/null and b/docs/images/cmd_people_daily_1.png differ diff --git a/docs/images/cmd_people_daily_2.png b/docs/images/cmd_people_daily_2.png new file mode 100644 index 0000000..caa4383 Binary files /dev/null and b/docs/images/cmd_people_daily_2.png differ diff --git a/docs/images/cmd_qrcode.png b/docs/images/cmd_qrcode.png new file mode 100644 index 0000000..37b423d Binary files /dev/null and b/docs/images/cmd_qrcode.png differ diff --git a/docs/images/cmd_today_in_history.png b/docs/images/cmd_today_in_history.png new file mode 100644 index 0000000..da70041 Binary files /dev/null and b/docs/images/cmd_today_in_history.png differ diff --git a/docs/images/cmd_todo_1.png b/docs/images/cmd_todo_1.png new file mode 100644 index 0000000..8c3a708 Binary files /dev/null and b/docs/images/cmd_todo_1.png differ diff --git a/docs/images/cmd_todo_2.png b/docs/images/cmd_todo_2.png new file mode 100644 index 0000000..8982fa7 Binary files /dev/null and b/docs/images/cmd_todo_2.png differ diff --git a/docs/images/cmd_todo_3.png b/docs/images/cmd_todo_3.png new file mode 100644 index 0000000..b3cc040 Binary files /dev/null and b/docs/images/cmd_todo_3.png differ diff --git a/docs/images/cmd_trivia.png b/docs/images/cmd_trivia.png new file mode 100644 index 0000000..c539ccc Binary files /dev/null and b/docs/images/cmd_trivia.png differ diff --git a/docs/images/cmd_weather.png b/docs/images/cmd_weather.png new file mode 100644 index 0000000..571f339 Binary files /dev/null and b/docs/images/cmd_weather.png differ diff --git a/docs/images/cmd_weibo_hot.png b/docs/images/cmd_weibo_hot.png new file mode 100644 index 0000000..edc8d6a Binary files /dev/null and b/docs/images/cmd_weibo_hot.png differ diff --git a/docs/images/cmd_word.png b/docs/images/cmd_word.png new file mode 100644 index 0000000..8e8aa97 Binary files /dev/null and b/docs/images/cmd_word.png differ diff --git a/docs/images/cmd_zhihu_hot.png b/docs/images/cmd_zhihu_hot.png new file mode 100644 index 0000000..ebe1eac Binary files /dev/null and b/docs/images/cmd_zhihu_hot.png differ diff --git a/docs/images/wechatter_show.png b/docs/images/wechatter_show.png new file mode 100644 index 0000000..3027845 Binary files /dev/null and b/docs/images/wechatter_show.png differ diff --git a/docs/images/wechatter_show2.png b/docs/images/wechatter_show2.png new file mode 100644 index 0000000..e228395 Binary files /dev/null and b/docs/images/wechatter_show2.png differ diff --git a/main.py b/main.py index f736a30..8ed7d39 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,13 @@ # WeChatter 启动文件 +# +# __ __ ______ ______ __ __ ______ ______ ______ ______ ______ +# /\ \ _ \ \ /\ ___\ /\ ___\ /\ \_\ \ /\ __ \ /\__ _\/\__ _\/\ ___\ /\ == \ +# \ \ \/ ".\ \\ \ __\ \ \ \____\ \ __ \\ \ __ \\/_/\ \/\/_/\ \/\ \ __\ \ \ __< +# \ \__/".~\_\\ \_____\\ \_____\\ \_\ \_\\ \_\ \_\ \ \_\ \ \_\ \ \_____\\ \_\ \_\ +# \/_/ \/_/ \/_____/ \/_____/ \/_/\/_/ \/_/\/_/ \/_/ \/_/ \/_____/ \/_/ /_/ +# + import uvicorn -from loguru import logger import wechatter.database as db import wechatter.utils.file_manager as fm @@ -28,7 +35,6 @@ def main(): db.create_tables() - logger.info("WeChatter 启动成功!") # 启动uvicorn uvicorn.run(app, host="0.0.0.0", port=config.wechatter_port) diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..d829cc9 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +from wechatter.init_logger import init_logger + +init_logger("CRITICAL") diff --git a/tests/commands/test_bili_hot/test_bili_hot.py b/tests/commands/test_bili_hot/test_bili_hot.py index 2dd135a..0cf596e 100644 --- a/tests/commands/test_bili_hot/test_bili_hot.py +++ b/tests/commands/test_bili_hot/test_bili_hot.py @@ -26,3 +26,10 @@ def test_generate_bili_hot_message_success(self): def test_generate_bili_hot_message_empty_list(self): result = bili_hot._generate_bili_hot_message([]) self.assertEqual(result, "暂无Bilibili热搜") + + def test_generate_bili_hot_quoted_response_success(self): + result = bili_hot._generate_bili_hot_quoted_response( + self.r_json["data"]["list"] + ) + true_result = '{"1": "https://search.bilibili.com/all?keyword=%E5%A4%8D%E6%97%A6%E6%95%99%E5%B8%88%E6%9D%80%E5%AE%B3%E5%AD%A6%E9%99%A2%E4%B9%A6%E8%AE%B0%E8%A2%AB%E5%88%A4%E6%AD%BB%E7%BC%93", "2": "https://search.bilibili.com/all?keyword=%E5%85%8D%E8%B4%B9%E9%A2%86%E5%8F%96%E5%BF%83%E9%AD%94", "3": "https://search.bilibili.com/all?keyword=%E6%89%8E%E5%85%8B%E4%BC%AF%E6%A0%BC%E9%81%93%E6%AD%89", "4": "https://search.bilibili.com/all?keyword=%E6%98%93%E7%83%8A%E5%8D%83%E7%8E%BA%E5%BD%93%E9%80%89%E4%B8%AD%E5%9B%BD%E5%BD%B1%E5%8D%8F%E7%90%86%E4%BA%8B", "5": "https://search.bilibili.com/all?keyword=kei%E5%92%8Cmarin%E5%88%86%E6%89%8B", "6": "https://search.bilibili.com/all?keyword=%E4%B8%8A%E6%B5%B7%E7%A6%81%E6%AD%A2%E7%BD%91%E7%BA%A6%E8%BD%A6%E5%9C%A8%E6%B5%A6%E4%B8%9C%E6%9C%BA%E5%9C%BA%E8%BF%90%E8%90%A5", "7": "https://search.bilibili.com/all?keyword=%E4%B8%AD%E5%9B%BD%E8%B1%86%E6%B5%86%E6%9C%BA%E5%9C%A8%E9%9F%A9%E5%9B%BD%E7%83%AD%E9%94%80", "8": "https://search.bilibili.com/all?keyword=%E4%BD%95%E5%90%8C%E5%AD%A6%E5%B7%A5%E4%BD%9C%E5%AE%A4%E9%A6%96%E6%AC%A1%E5%85%AC%E5%BC%80", "9": "https://search.bilibili.com/all?keyword=%E5%AE%98%E6%96%B9%E7%A7%B0%E7%A1%AE%E5%AE%9A%E5%9B%BD%E8%8A%B1%E6%97%B6%E6%9C%BA%E6%9C%AA%E6%88%90%E7%86%9F", "10": "https://search.bilibili.com/all?keyword=Aimer%E5%8F%82%E6%BC%94%E5%8E%9F%E7%A5%9E%E6%96%B0%E6%98%A5%E4%BC%9A", "11": "https://search.bilibili.com/all?keyword=%E6%80%BB%E5%8F%B0%E9%BE%99%E5%B9%B4%E6%98%A5%E6%99%9A%E5%8A%A8%E7%94%BB%E5%AE%A3%E4%BC%A0%E7%89%87", "12": "https://search.bilibili.com/all?keyword=%E5%8F%AA%E8%A7%A3%E5%86%BB%E5%88%98%E5%BE%B7%E5%8D%8E%E5%A4%AA%E4%BF%9D%E5%AE%88%E4%BA%86", "13": "https://search.bilibili.com/all?keyword=%E7%83%9F%E8%8A%B1%E5%BC%95%E5%8F%91%E4%BD%8F%E5%AE%85%E8%B5%B7%E7%81%AB%E8%87%B4%E8%80%81%E4%BA%BA%E5%8E%BB%E4%B8%96", "14": "https://search.bilibili.com/all?keyword=750%E4%B8%87%E4%BA%BA%E5%9C%A8%E7%AD%89%E5%B0%8A%E4%B8%A5%E6%AD%BB", "15": "https://search.bilibili.com/all?keyword=%E5%9D%A0%E4%BA%A1%E5%A7%90%E5%BC%9F%E7%94%9F%E6%AF%8D%E8%A2%AB%E9%80%81%E5%8C%BB%E9%99%A2", "16": "https://search.bilibili.com/all?keyword=%E4%B8%8D%E7%BB%B5%E4%B9%8B%E5%A4%9C", "17": "https://search.bilibili.com/all?keyword=%E6%B8%85%E5%8D%8E%E5%AE%A3%E5%B8%83%E8%84%91%E6%9C%BA%E6%8E%A5%E5%8F%A3%E9%87%8D%E5%A4%A7%E7%AA%81%E7%A0%B4", "18": "https://search.bilibili.com/all?keyword=Uzi%20%E4%BD%BF%E7%94%A8%E6%9B%BF%E8%BA%AB%E6%94%BB%E5%87%BB", "19": "https://search.bilibili.com/all?keyword=%E6%8D%A1%E9%9B%AA%E5%90%83%E6%82%A3%E7%97%85%E8%BF%9E%E7%83%A7%E5%85%AB%E5%A4%A9", "20": "https://search.bilibili.com/all?keyword=%E5%B9%BB%E5%A1%94EVA%E8%81%94%E5%8A%A8%E6%98%8E%E6%97%A5%E9%A6%99%E7%99%BB%E5%9C%BA"}' + self.assertEqual(result, true_result) diff --git a/tests/commands/test_douyin_hot/test_douyin_hot.py b/tests/commands/test_douyin_hot/test_douyin_hot.py index b44ffcb..da5af64 100644 --- a/tests/commands/test_douyin_hot/test_douyin_hot.py +++ b/tests/commands/test_douyin_hot/test_douyin_hot.py @@ -26,3 +26,10 @@ def test_generate_douyin_hot_message_success(self): def test_generate_douyin_hot_message_empty_list(self): result = douyin_hot._generate_douyin_hot_message([]) self.assertEqual(result, "暂无抖音热搜") + + def test_generate_douyin_hot_quoted_response_success(self): + result = douyin_hot._generate_douyin_hot_quoted_response( + self.r_json["word_list"] + ) + true_result = '{"1": "https://www.douyin.com/search/%E8%91%A3%E5%AE%87%E8%BE%89%E5%AF%B9%E5%88%98%E5%BE%B7%E5%8D%8E%E8%AF%B4%E6%83%B3%E6%BC%94%E5%85%B5%E9%A9%AC%E4%BF%91", "2": "https://www.douyin.com/search/%E4%BB%8A%E5%B9%B4%E6%98%A5%E8%81%94%E6%98%AF%E8%87%AA%E5%B7%B1%E5%86%99%E7%9A%84", "3": "https://www.douyin.com/search/%E4%B8%AD%E5%9B%BD%E5%90%8C23%E5%9B%BD%E5%85%A8%E9%9D%A2%E4%BA%92%E5%85%8D%E7%AD%BE%E8%AF%81", "4": "https://www.douyin.com/search/%E6%9A%97%E5%A4%9C%E5%8F%98%E8%A3%85%E6%8C%91%E6%88%98", "5": "https://www.douyin.com/search/%E6%99%92%E5%87%BA%E4%BD%A0%E7%9A%84%E6%96%B0%E6%98%A5%E7%BA%A2", "6": "https://www.douyin.com/search/%E7%9B%B8%E4%BA%B2%E7%9B%B8%E7%88%B1%E6%8E%A5%E5%8A%9B%E6%8C%91%E6%88%98", "7": "https://www.douyin.com/search/%E8%AF%80%E5%88%AB%E4%B9%A6%E7%9A%84%E6%AD%A3%E7%A1%AE%E6%89%93%E5%BC%80%E6%96%B9%E5%BC%8F", "8": "https://www.douyin.com/search/2%E6%9C%88%E7%AC%AC%E4%B8%80%E5%A4%A9", "9": "https://www.douyin.com/search/%E5%8C%97%E4%BA%AC%E4%BA%A7%E6%9D%83%E4%BA%A4%E6%98%93%E6%89%80%E6%BE%84%E6%B8%85%E5%A3%B0%E6%98%8E", "10": "https://www.douyin.com/search/%E8%B4%B5%E5%B7%9E%E6%9C%89%E5%A4%9A%E9%92%9F%E7%88%B1%E5%8A%9E%E9%85%92%E5%B8%AD", "11": "https://www.douyin.com/search/%E5%90%84%E5%9C%B0%E4%BA%BA%E8%BF%99%E4%B9%88%E5%81%9A%E4%B8%80%E5%AE%9A%E6%9C%89%E5%8E%9F%E5%9B%A0", "12": "https://www.douyin.com/search/%E9%9F%A9%E5%9B%BD%E7%91%9C%E5%BD%93%E9%80%89%E5%8F%B0%E7%AB%8B%E6%B3%95%E6%9C%BA%E6%9E%84%E8%B4%9F%E8%B4%A3%E4%BA%BA", "13": "https://www.douyin.com/search/%E4%B8%80%E8%B5%B7%E8%B7%B3%E7%94%9C%E5%A6%B9%E6%89%8B%E5%8A%BF%E8%88%9E", "14": "https://www.douyin.com/search/%E4%B8%8A%E6%B5%B7%E6%A5%BC%E6%88%BF%E5%87%8C%E6%99%A8%E5%9D%8D%E5%A1%8C%20%E5%A4%9A%E6%96%B9%E5%9B%9E%E5%BA%94", "15": "https://www.douyin.com/search/%E5%88%98%E5%BE%B7%E5%8D%8E%E5%AE%81%E6%B5%A9%E7%BA%A2%E6%AF%AF%E5%85%88%E7%94%9F%E4%BB%8A%E6%99%9A%E7%9B%B4%E6%92%AD", "16": "https://www.douyin.com/search/%E6%B2%A1%E6%9C%89%E5%90%8C%E6%A1%8C%E6%88%91%E5%8F%AF%E6%80%8E%E4%B9%88%E5%8A%9E%E5%95%8A", "17": "https://www.douyin.com/search/%E6%B2%B3%E5%8C%97%E4%B8%80%E4%BF%9D%E5%AE%89%E9%98%BB%E6%AD%A2%E5%A5%94%E9%A9%B0%E5%8A%A0%E5%A1%9E%E8%A2%AB%E9%A1%B6%E6%92%9E", "18": "https://www.douyin.com/search/%E7%BD%91%E5%8F%8B%E8%BF%87%E5%B9%B4%E7%88%B1%E4%B8%8A%E7%BB%84%E5%85%BB%E7%94%9F%E5%B1%80%E4%BA%86", "19": "https://www.douyin.com/search/%E5%8F%AC%E9%9B%86%E5%85%A8%E6%8A%96%E9%9F%B3%E6%99%9A8%E6%89%BE%E4%B9%90%E5%AD%90", "20": "https://www.douyin.com/search/%E6%98%A5%E8%BF%90%E6%9C%9F%E9%97%B4%E5%A4%A9%E6%B0%94%E9%A2%84%E6%B5%8B"}' + self.assertEqual(result, true_result) diff --git a/tests/commands/test_github_trending/test_github_trending.py b/tests/commands/test_github_trending/test_github_trending.py index e82ce15..2cb8667 100644 --- a/tests/commands/test_github_trending/test_github_trending.py +++ b/tests/commands/test_github_trending/test_github_trending.py @@ -28,9 +28,14 @@ def test_parse_github_trending_response_failure(self): def test_generate_github_trending_message_success(self): result = gt._generate_github_trending_message(self.gt_list) - true_result = "1. 🏎️ danielmiessler / fabric\n⭐ 2,538 total (⭐1,139 today)\n🔤 Python\n📖 fabric is an open-source framework for augmenting humans using AI.\n2. 🏎️ InkboxSoftware / excelCPU\n⭐ 2,507 total (⭐337 today)\n🔤 Python\n📖 16-bit CPU for Excel, and related files\n3. 🏎️ f / awesome-chatgpt-prompts\n⭐ 98,933 total (⭐115 today)\n🔤 HTML\n📖 This repo includes ChatGPT prompt curation to use ChatGPT better.\n4. 🏎️ all-in-aigc / aicover\n⭐ 973 total (⭐205 today)\n🔤 TypeScript\n📖 ai cover generator\n5. 🏎️ facebookresearch / codellama\n⭐ 12,972 total (⭐197 today)\n🔤 Python\n📖 Inference code for CodeLlama models\n6. 🏎️ webprodigies / plura-production\n⭐ 315 total (⭐43 today)\n🔤 TypeScript\n📖 No description.\n7. 🏎️ ExOK / Celeste64\n⭐ 895 total (⭐170 today)\n🔤 C#\n📖 A game made by the Celeste developers in a week(ish, closer to 2)\n8. 🏎️ haotian-liu / LLaVA\n⭐ 13,169 total (⭐155 today)\n🔤 Python\n📖 [NeurIPS'23 Oral] Visual Instruction Tuning (LLaVA) built towards GPT-4V level capabilities and beyond.\n9. 🏎️ mlflow / mlflow\n⭐ 16,583 total (⭐127 today)\n🔤 Python\n📖 Open source platform for the machine learning lifecycle\n10. 🏎️ PKU-YuanGroup / MoE-LLaVA\n⭐ 689 total (⭐219 today)\n🔤 Python\n📖 Mixture-of-Experts for Large Vision-Language Models" - self.assertIn(true_result, result) + true_result = "✨=====GitHub Trending=====✨\n1.📦 danielmiessler / fabric\n ⭐ 2,538 total (⭐1,139 today)\n 🔤 Python\n 📖 fabric is an open-source framework for augmenting humans using AI.\n2.📦 InkboxSoftware / excelCPU\n ⭐ 2,507 total (⭐337 today)\n 🔤 Python\n 📖 16-bit CPU for Excel, and related files\n3.📦 f / awesome-chatgpt-prompts\n ⭐ 98,933 total (⭐115 today)\n 🔤 HTML\n 📖 This repo includes ChatGPT prompt curation to use ChatGPT better.\n4.📦 all-in-aigc / aicover\n ⭐ 973 total (⭐205 today)\n 🔤 TypeScript\n 📖 ai cover generator\n5.📦 facebookresearch / codellama\n ⭐ 12,972 total (⭐197 today)\n 🔤 Python\n 📖 Inference code for CodeLlama models\n6.📦 webprodigies / plura-production\n ⭐ 315 total (⭐43 today)\n 🔤 TypeScript\n 📖 No description.\n7.📦 ExOK / Celeste64\n ⭐ 895 total (⭐170 today)\n 🔤 C#\n 📖 A game made by the Celeste developers in a week(ish, closer to 2)\n8.📦 haotian-liu / LLaVA\n ⭐ 13,169 total (⭐155 today)\n 🔤 Python\n 📖 [NeurIPS'23 Oral] Visual Instruction Tuning (LLaVA) built towards GPT-4V level capabilities and beyond.\n9.📦 mlflow / mlflow\n ⭐ 16,583 total (⭐127 today)\n 🔤 Python\n 📖 Open source platform for the machine learning lifecycle\n10.📦 PKU-YuanGroup / MoE-LLaVA\n ⭐ 689 total (⭐219 today)\n 🔤 Python\n 📖 Mixture-of-Experts for Large Vision-Language Models\n" + self.assertEqual(result, true_result) def test_generate_zhihu_hot_message_empty_list(self): result = gt._generate_github_trending_message([]) self.assertEqual(result, "暂无 GitHub 趋势") + + def test_generate_github_trending_quoted_response_success(self): + result = gt._generate_github_trending_quoted_response(self.gt_list) + true_result = '{"1": "https://github.com/danielmiessler/fabric", "2": "https://github.com/InkboxSoftware/excelCPU", "3": "https://github.com/f/awesome-chatgpt-prompts", "4": "https://github.com/all-in-aigc/aicover", "5": "https://github.com/facebookresearch/codellama", "6": "https://github.com/webprodigies/plura-production", "7": "https://github.com/ExOK/Celeste64", "8": "https://github.com/haotian-liu/LLaVA", "9": "https://github.com/mlflow/mlflow", "10": "https://github.com/PKU-YuanGroup/MoE-LLaVA"}' + self.assertEqual(result, true_result) diff --git a/tests/commands/test_pai_post/pai_post_data.json b/tests/commands/test_pai_post/pai_post_data.json index d87c7f4..bec91a7 100644 --- a/tests/commands/test_pai_post/pai_post_data.json +++ b/tests/commands/test_pai_post/pai_post_data.json @@ -1,35 +1,46 @@ [ { + "href": "/post/86250", "title": "HMD Global 社交账户改名" }, { + "href": "/post/86250", "title": "第三方社区「Linux 中国」停止运营" }, { + "href": "/post/86250", "title": "微软为 Apple Vision Pro 推出 Microsoft 365 套件" }, { + "href": "/post/86250", "title": "Hulu 开始打击账户密码共享" }, { + "href": "/post/86250", "title": "Apple Vision Pro 新闻 N 则" }, { + "href": "/post/86222", "title": "Apple 延长与高通的基带合作协议" }, { + "href": "/post/86222", "title": "Adobe 将停止更新 Adobe XD" }, { + "href": "/post/86222", "title": "环球音乐集团计划将从 TikTok 收回歌曲版权" }, { + "href": "/post/86222", "title": "全球 2023 第四季度智能手机出货量有所回升" }, { + "href": "/post/86222", "title": "ICANN 将正式推出内网域名 .internal" }, { + "href": "/post/86222", "title": "索尼召开 State of Play 新作发布会" } -] +] \ No newline at end of file diff --git a/tests/commands/test_pai_post/test_pai_post.py b/tests/commands/test_pai_post/test_pai_post.py index 3f97f40..09c5420 100644 --- a/tests/commands/test_pai_post/test_pai_post.py +++ b/tests/commands/test_pai_post/test_pai_post.py @@ -32,3 +32,8 @@ def test_generate_pai_post_message_success(self): def test_generate_zhihu_hot_message_empty_list(self): result = pai_post._generate_pai_post_message([]) self.assertEqual(result, "暂无少数派早报") + + def test_generate_pai_post_quoted_response_success(self): + result = pai_post._generate_pai_post_quoted_response(self.pai_post_list) + true_result = '{"1": "https://sspai.com/post/86250", "2": "https://sspai.com/post/86250", "3": "https://sspai.com/post/86250", "4": "https://sspai.com/post/86250", "5": "https://sspai.com/post/86250", "6": "https://sspai.com/post/86222", "7": "https://sspai.com/post/86222", "8": "https://sspai.com/post/86222", "9": "https://sspai.com/post/86222", "10": "https://sspai.com/post/86222", "11": "https://sspai.com/post/86222"}' + self.assertEqual(result, true_result) diff --git a/tests/commands/test_weibo_hot/test_weibo_hot.py b/tests/commands/test_weibo_hot/test_weibo_hot.py index 0063cb7..52669bc 100644 --- a/tests/commands/test_weibo_hot/test_weibo_hot.py +++ b/tests/commands/test_weibo_hot/test_weibo_hot.py @@ -26,3 +26,8 @@ def test_generate_weibo_hot_message_success(self): def test_generate_weibo_hot_message_empty_list(self): result = weibo_hot._generate_weibo_hot_message([]) self.assertEqual(result, "微博热搜列表为空") + + def test_generate_weibo_hot_quoted_response_success(self): + result = weibo_hot._generate_weibo_hot_quoted_response(self.weibo_hot_list) + true_result = '{"1": "https://s.weibo.com/weibo?q=%E5%B0%86%E4%B8%AD%E6%B3%95%E5%85%B3%E7%B3%BB%E6%89%93%E9%80%A0%E5%BE%97%E6%9B%B4%E5%8A%A0%E7%89%A2%E5%9B%BA%E5%92%8C%E5%AF%8C%E6%9C%89%E6%B4%BB%E5%8A%9B", "2": "https://s.weibo.com/weibo?q=%E9%9F%A9%E5%BA%9A%E9%9F%A9%E5%9B%BD%20%E6%8A%91%E9%83%81", "3": "https://s.weibo.com/weibo?q=6%E5%A4%A9%E7%8F%AD", "4": "https://s.weibo.com/weibo?q=%E4%BD%95%E4%BB%A5%E4%B8%AD%E5%9B%BD%E5%90%91%E6%B5%B7%E6%B3%89%E5%B7%9E", "5": "https://s.weibo.com/weibo?q=%E7%A5%9D%E4%BD%A0KFC%E7%A5%9D%E4%BD%A0%E5%BF%AB%E5%8F%91%E8%B4%A2", "6": "https://s.weibo.com/weibo?q=%E4%B8%A4%E4%B8%AA14%E5%B2%81%E7%9A%84%E4%B8%8A%E6%B5%B7%E5%A5%B3%E5%AD%A6%E7%94%9F%E7%9A%84%E9%87%87%E8%AE%BF", "7": "https://s.weibo.com/weibo?q=%E6%88%91%E5%9B%BD%E5%A5%B3%E6%80%A7HPV%E6%84%9F%E6%9F%93%E7%8E%87%E5%91%88%E5%8F%8C%E5%B3%B0%E5%88%86%E5%B8%83", "8": "https://s.weibo.com/weibo?q=%E6%94%AF%E4%BB%98%E5%AE%9D%E4%BA%94%E7%A6%8F", "9": "https://s.weibo.com/weibo?q=%E7%BD%91%E6%B0%91%E5%8F%91%E5%B8%83%E4%B8%AD%E5%AD%A6%E5%8F%91%E7%94%9F%E6%80%A7%E4%BE%B5%E8%99%9A%E5%81%87%E4%BF%A1%E6%81%AF%E8%A2%AB%E6%8B%98", "10": "https://s.weibo.com/weibo?q=%E7%94%B7%E5%AD%90%E4%B8%BE%E6%8A%A5%E7%BA%AA%E5%A7%94%E5%B9%B2%E9%83%A840%E5%88%86%E9%92%9F%E5%90%8E%E5%B0%B1%E8%A2%AB%E6%8A%93", "11": "https://s.weibo.com/weibo?q=9%E9%83%A8%E7%94%B5%E5%BD%B1%E5%AE%98%E5%AE%A32024%E6%98%A5%E8%8A%82%E6%A1%A3", "12": "https://s.weibo.com/weibo?q=%E8%82%89%E9%A9%AC%E5%B0%94", "13": "https://s.weibo.com/weibo?q=%E5%94%AF%E4%B8%80%E7%9A%84%E5%A7%90%E4%B8%81%E6%B3%BD%E4%BB%81", "14": "https://s.weibo.com/weibo?q=%E7%BD%91%E5%8F%8B%E7%A7%B0%E9%99%88%E7%89%A7%E9%A9%B0%E6%89%94%E4%BA%86%E7%B2%89%E4%B8%9D%E9%80%81%E7%9A%84%E7%A4%BC%E7%89%A9", "15": "https://s.weibo.com/weibo?q=%E5%AD%99%E7%BA%A2%E9%9B%B7%E5%9B%9E%E5%BA%94%E9%BB%84%E6%B8%A4%E5%BC%A0%E8%89%BA%E5%85%B4%E4%B8%8D%E5%B8%A6%E8%87%AA%E5%B7%B1%E7%8E%A9", "16": "https://s.weibo.com/weibo?q=%E9%95%BF%E5%A4%A7%E4%BA%86%E7%9C%8B%E5%B0%8F%E5%AD%A9%E7%BB%93%E5%A9%9A", "17": "https://s.weibo.com/weibo?q=%E5%A4%A9%E5%AE%98%E8%B5%90%E7%A6%8F%E7%BA%A2%E5%8C%85", "18": "https://s.weibo.com/weibo?q=%E6%9B%9D%E5%91%A8%E8%BF%85%E8%B5%B5%E4%B8%BD%E9%A2%96%E6%9D%A8%E7%B4%AB%E4%BA%89%E6%BC%94%E5%BC%A0%E8%89%BA%E8%B0%8B%E6%96%B0%E5%89%A7", "19": "https://s.weibo.com/weibo?q=%E5%AC%9B%E5%AC%9B%20%E6%9C%95%E5%87%BA%E4%B8%93%E8%BE%91%E5%95%A6", "20": "https://s.weibo.com/weibo?q=%E5%A4%AB%E5%A6%BB%E5%88%86%E5%B1%85%E5%9E%8B%E6%98%A5%E8%8A%82"}' + self.assertEqual(result, true_result) diff --git a/tests/commands/test_zhihu_hot/test_zhihu_hot.py b/tests/commands/test_zhihu_hot/test_zhihu_hot.py index 7116130..1717d36 100644 --- a/tests/commands/test_zhihu_hot/test_zhihu_hot.py +++ b/tests/commands/test_zhihu_hot/test_zhihu_hot.py @@ -26,3 +26,8 @@ def test_generate_zhihu_hot_message_success(self): def test_generate_zhihu_hot_message_empty_list(self): result = zhihu_hot._generate_zhihu_hot_message([]) self.assertEqual(result, "暂无知乎热搜") + + def test_generate_zhihu_hot_quoted_response_success(self): + result = zhihu_hot._generate_zhihu_hot_quoted_response(self.zhihu_hot_list) + true_result = '{"1": "https://www.zhihu.com/question/642169181", "2": "https://www.zhihu.com/question/642297518", "3": "https://www.zhihu.com/question/642108938", "4": "https://www.zhihu.com/question/641995117", "5": "https://www.zhihu.com/question/642299758", "6": "https://www.zhihu.com/question/642287890", "7": "https://www.zhihu.com/question/642154023", "8": "https://www.zhihu.com/question/642290406", "9": "https://www.zhihu.com/question/641648705", "10": "https://www.zhihu.com/question/640060689", "11": "https://www.zhihu.com/question/642299465", "12": "https://www.zhihu.com/question/642125769", "13": "https://www.zhihu.com/question/642330312", "14": "https://www.zhihu.com/question/642307679", "15": "https://www.zhihu.com/question/642184584", "16": "https://www.zhihu.com/question/521484226", "17": "https://www.zhihu.com/question/642314136", "18": "https://www.zhihu.com/question/642267038", "19": "https://www.zhihu.com/question/641865103", "20": "https://www.zhihu.com/question/640626886"}' + self.assertEqual(result, true_result) diff --git a/wechatter/sqlite/__init__.py b/wechatter/__init__.py similarity index 100% rename from wechatter/sqlite/__init__.py rename to wechatter/__init__.py diff --git a/wechatter/app/app.py b/wechatter/app/app.py index b9bbbb5..d81d6d7 100644 --- a/wechatter/app/app.py +++ b/wechatter/app/app.py @@ -1,7 +1,9 @@ from fastapi import FastAPI +from loguru import logger import wechatter.app.routers as routers import wechatter.config as config +from wechatter.art_text import print_wechatter_art_text from wechatter.config.parsers import ( parse_gasoline_price_cron_rule_list, parse_weather_cron_rule_list, @@ -33,6 +35,8 @@ @app.on_event("startup") async def startup_event(): scheduler.startup() + print_wechatter_art_text() + logger.info("WeChatter 启动成功!") @app.on_event("shutdown") async def shutdown_event(): diff --git a/wechatter/app/routers/wechat.py b/wechatter/app/routers/wechat.py index d526eee..2670070 100644 --- a/wechatter/app/routers/wechat.py +++ b/wechatter/app/routers/wechat.py @@ -6,7 +6,7 @@ import wechatter.config as config from wechatter.bot.bot_info import BotInfo -from wechatter.commands import commands +from wechatter.commands import commands, quoted_handlers from wechatter.database import ( Group as DbGroup, Message as DbMessage, @@ -72,7 +72,7 @@ async def recv_wechat_msg( MessageForwarder(config.message_forwarding_rule_list).forward_message(message) # 传入命令字典,构造消息处理器 - message_handler = MessageHandler(commands) + message_handler = MessageHandler(commands=commands, quoted_handlers=quoted_handlers) # 用户发来的消息均送给消息解析器处理 message_handler.handle_message(message) diff --git a/wechatter/art_text.py b/wechatter/art_text.py new file mode 100644 index 0000000..415bea1 --- /dev/null +++ b/wechatter/art_text.py @@ -0,0 +1,35 @@ +# WECHATTER_ART_TEXT0 = r'+-------------------------------------------------------------------------------------+' +# WECHATTER_ART_TEXT1 = r'| __ __ ______ ______ __ __ ______ ______ ______ ______ ______ |' +# WECHATTER_ART_TEXT2 = r'| /\ \ _ \ \ /\ ___\ /\ ___\ /\ \_\ \ /\ __ \ /\__ _\/\__ _\/\ ___\ /\ == \ |' +# WECHATTER_ART_TEXT3 = r'| \ \ \/ ".\ \\ \ __\ \ \ \____\ \ __ \\ \ __ \\/_/\ \/\/_/\ \/\ \ __\ \ \ __< |' +# WECHATTER_ART_TEXT4 = r'| \ \__/".~\_\\ \_____\\ \_____\\ \_\ \_\\ \_\ \_\ \ \_\ \ \_\ \ \_____\\ \_\ \_\ |' +# WECHATTER_ART_TEXT5 = r'| \/_/ \/_/ \/_____/ \/_____/ \/_/\/_/ \/_/\/_/ \/_/ \/_/ \/_____/ \/_/ /_/ |' +# WECHATTER_ART_TEXT6 = r'+-------------------------------------------------------------------------------------+' + +WECHATTER_ART_TEXT = r""" + __ __ ______ ______ __ __ ______ ______ ______ ______ ______ + /\ \ _ \ \ /\ ___\ /\ ___\ /\ \_\ \ /\ __ \ /\__ _\/\__ _\/\ ___\ /\ == \ + \ \ \/ ".\ \\ \ __\ \ \ \____\ \ __ \\ \ __ \\/_/\ \/\/_/\ \/\ \ __\ \ \ __< + \ \__/".~\_\\ \_____\\ \_____\\ \_\ \_\\ \_\ \_\ \ \_\ \ \_\ \ \_____\\ \_\ \_\ + \/_/ \/_/ \/_____/ \/_____/ \/_/\/_/ \/_/\/_/ \/_/ \/_/ \/_____/ \/_/ /_/ +""" + + +def print_wechatter_art_text(): + print(WECHATTER_ART_TEXT) + + +# print(WECHATTER_ART_TEXT0) +# print(WECHATTER_ART_TEXT1) +# print(WECHATTER_ART_TEXT2) +# print(WECHATTER_ART_TEXT3) +# print(WECHATTER_ART_TEXT4) +# print(WECHATTER_ART_TEXT5) +# print(WECHATTER_ART_TEXT6) +# print("\033[1;36m%s\033[0m" % WECHATTER_ART_TEXT0) +# print("\033[1;36m%s\033[0m" % WECHATTER_ART_TEXT1) +# print("\033[1;36m%s\033[0m" % WECHATTER_ART_TEXT2) +# print("\033[1;31m%s\033[0m" % WECHATTER_ART_TEXT3) +# print("\033[1;34m%s\033[0m" % WECHATTER_ART_TEXT4) +# print("\033[1;34m%s\033[0m" % WECHATTER_ART_TEXT5) +# print("\033[1;34m%s\033[0m" % WECHATTER_ART_TEXT6) diff --git a/wechatter/commands/__init__.py b/wechatter/commands/__init__.py index a757963..9b94e83 100644 --- a/wechatter/commands/__init__.py +++ b/wechatter/commands/__init__.py @@ -1,6 +1,6 @@ # isort: skip_file -from .handlers import commands +from .handlers import commands, quoted_handlers from ._commands import * # noqa -__all__ = ["commands"] +__all__ = ["commands", "quoted_handlers"] diff --git a/wechatter/commands/_commands/bili_hot.py b/wechatter/commands/_commands/bili_hot.py index d1f96ea..f6f9ffb 100644 --- a/wechatter/commands/_commands/bili_hot.py +++ b/wechatter/commands/_commands/bili_hot.py @@ -1,35 +1,66 @@ -from typing import Dict, List +import json +from typing import Dict, List, Tuple from loguru import logger from wechatter.commands.handlers import command -from wechatter.models.wechat import SendTo +from wechatter.models.wechat import QuotedResponse, SendTo from wechatter.sender import sender -from wechatter.utils import get_request_json +from wechatter.utils import get_request_json, url_encode + +COMMAND_NAME = "bili-hot" @command( - command="bili-hot", + command=COMMAND_NAME, keys=["b站热搜", "bili-hot"], desc="获取b站热搜。", ) -def bili_hot_command_handler(to: SendTo, message: str = "") -> None: +def bili_hot_command_handler(to: SendTo, message: str = ""): try: - result = get_bili_hot_str() + result, q_response = get_bili_hot_str() except Exception as e: error_message = f"获取Bilibili热搜失败,错误信息: {str(e)}" logger.error(error_message) sender.send_msg(to, error_message) else: - sender.send_msg(to, result) + sender.send_msg( + to, + result, + quoted_response=QuotedResponse( + command=COMMAND_NAME, + response=q_response, + ), + ) + + +@bili_hot_command_handler.quoted_handler +def bili_hot_quoted_handler(to: SendTo, message: str = "", q_response: str = ""): + if not message.isdigit(): + logger.error("输入的热搜编号不是数字") + sender.send_msg(to, "请输入热搜编号") + return + + hot_url_dict = json.loads(q_response) + try: + hot_url = hot_url_dict[message] + except Exception: + logger.error("输入的热搜编号错误") + sender.send_msg(to, "输入的热搜编号错误") + return + else: + sender.send_msg(to, hot_url) -def get_bili_hot_str() -> str: +def get_bili_hot_str() -> Tuple[str, str]: response = get_request_json( url="https://app.bilibili.com/x/v2/search/trending/ranking" ) hot_list = _extract_bili_hot_data(response) - return _generate_bili_hot_message(hot_list) + return ( + _generate_bili_hot_message(hot_list), + _generate_bili_hot_quoted_response(hot_list), + ) def _extract_bili_hot_data(r_json: Dict) -> List: @@ -50,3 +81,13 @@ def _generate_bili_hot_message(hot_list: List) -> str: hot_str += f"{i + 1}. {hot_search.get('keyword')}\n" return hot_str + + +def _generate_bili_hot_quoted_response(hot_list: List) -> str: + search_api = "https://search.bilibili.com/all?keyword=%s" + result = {} + for i, hot_search in enumerate(hot_list): + keyword = hot_search.get("keyword", None) + if keyword: + result[str(i + 1)] = url_encode(search_api % keyword) + return json.dumps(result) diff --git a/wechatter/commands/_commands/copilot_gpt4.py b/wechatter/commands/_commands/copilot_gpt4.py index 2f722e4..8249697 100644 --- a/wechatter/commands/_commands/copilot_gpt4.py +++ b/wechatter/commands/_commands/copilot_gpt4.py @@ -191,7 +191,7 @@ def _gptx_continue(model: str, to: SendTo, message: str = "") -> None: class CopilotGPT4: - api = f"{config.cp_gpt4_api_host}:{config.cp_gpt4_port}/v1/chat/completions" + api = f"{config.cp_gpt4_base_api}/v1/chat/completions" bearer_token = "Bearer " + config.cp_token save_path = pm.get_abs_path("data/copilot_gpt4/chats/") diff --git a/wechatter/commands/_commands/douyin_hot.py b/wechatter/commands/_commands/douyin_hot.py index 9e07958..ee28d81 100644 --- a/wechatter/commands/_commands/douyin_hot.py +++ b/wechatter/commands/_commands/douyin_hot.py @@ -1,35 +1,68 @@ -from typing import Dict, List +import json +from typing import Dict, List, Tuple from loguru import logger from wechatter.commands.handlers import command -from wechatter.models.wechat import SendTo +from wechatter.models.wechat import QuotedResponse, SendTo from wechatter.sender import sender -from wechatter.utils import get_request_json +from wechatter.utils import get_request_json, url_encode + +COMMAND_NAME = "douyin-hot" @command( - command="douyin-hot", + command=COMMAND_NAME, keys=["抖音热搜", "douyin-hot"], desc="获取抖音热搜。", ) def douyin_hot_command_handler(to: SendTo, message: str = "") -> None: try: - result = get_douyin_hot_str() + result, q_response = get_douyin_hot_str() except Exception as e: error_message = f"获取抖音热搜失败,错误信息: {str(e)}" logger.error(error_message) sender.send_msg(to, error_message) else: - sender.send_msg(to, result) + sender.send_msg( + to, + result, + quoted_response=QuotedResponse( + command=COMMAND_NAME, + response=q_response, + ), + ) + + +@douyin_hot_command_handler.quoted_handler +def douyin_hot_quoted_handler( + to: SendTo, message: str = "", q_response: str = "" +) -> None: + if not message.isdigit(): + logger.error("输入的热搜编号不是数字") + sender.send_msg(to, "请输入热搜编号") + return + + hot_url_dict = json.loads(q_response) + try: + hot_url = hot_url_dict[message] + except Exception: + logger.error("输入的热搜编号错误") + sender.send_msg(to, "输入的热搜编号错误") + return + else: + sender.send_msg(to, hot_url) -def get_douyin_hot_str() -> str: +def get_douyin_hot_str() -> Tuple[str, str]: r_json = get_request_json( url="https://www.iesdouyin.com/web/api/v2/hotsearch/billboard/word/" ) hot_list = _extract_douyin_hot_data(r_json) - return _generate_douyin_hot_message(hot_list) + return ( + _generate_douyin_hot_message(hot_list), + _generate_douyin_hot_quoted_response(hot_list), + ) def _extract_douyin_hot_data(r_json: Dict) -> List: @@ -50,3 +83,13 @@ def _generate_douyin_hot_message(hot_list: List) -> str: hot_str += f"{i + 1}. {hot_search.get('word')}\n" return hot_str + + +def _generate_douyin_hot_quoted_response(hot_list: List) -> str: + search_api = "https://www.douyin.com/search/%s" + hot_url_dict = {} + for i, hot_search in enumerate(hot_list[:20]): + keyword = hot_search.get("word", None) + if keyword: + hot_url_dict[str(i + 1)] = url_encode(search_api % keyword) + return json.dumps(hot_url_dict) diff --git a/wechatter/commands/_commands/github_trending.py b/wechatter/commands/_commands/github_trending.py index 8360e17..7d1fb1b 100644 --- a/wechatter/commands/_commands/github_trending.py +++ b/wechatter/commands/_commands/github_trending.py @@ -1,4 +1,5 @@ -from typing import List +import json +from typing import List, Tuple import requests from bs4 import BeautifulSoup @@ -6,31 +7,63 @@ from wechatter.commands.handlers import command from wechatter.exceptions import Bs4ParsingError -from wechatter.models.wechat import SendTo +from wechatter.models.wechat import QuotedResponse, SendTo from wechatter.sender import sender -from wechatter.utils import get_request +from wechatter.utils import get_request, url_encode + +COMMAND_NAME = "github-trending" @command( - command="github-trending", + command=COMMAND_NAME, keys=["github趋势", "github-trending"], desc="获取 GitHub 趋势。", ) def github_trending_command_handler(to: SendTo, message: str = "") -> None: try: - result = get_github_trending_str() + result, q_response = get_github_trending_str() except Exception as e: error_message = f"获取GitHub趋势失败,错误信息: {str(e)}" logger.error(error_message) sender.send_msg(to, error_message) else: - sender.send_msg(to, result) + sender.send_msg( + to, + result, + quoted_response=QuotedResponse( + command=COMMAND_NAME, + response=q_response, + ), + ) + +@github_trending_command_handler.quoted_handler +def github_trending_quoted_handler( + to: SendTo, message: str = "", q_response: str = "" +) -> None: + if not message.isdigit(): + logger.error("输入的趋势编号不是数字") + sender.send_msg(to, "请输入趋势编号") + return + + trending_url_dict = json.loads(q_response) + try: + trending_url = trending_url_dict[message] + except Exception: + logger.error("输入的趋势编号错误") + sender.send_msg(to, "输入的趋势编号错误") + return + else: + sender.send_msg(to, trending_url) -def get_github_trending_str() -> str: + +def get_github_trending_str() -> Tuple[str, str]: response = get_request(url="https://github.com/trending", timeout=10) gt_list = _parse_github_trending_response(response) - return _generate_github_trending_message(gt_list) + return ( + _generate_github_trending_message(gt_list), + _generate_github_trending_quoted_response(gt_list), + ) def _parse_github_trending_response(response: requests.Response) -> List: @@ -96,10 +129,20 @@ def _generate_github_trending_message(gt_list: List) -> str: gt_str = "✨=====GitHub Trending=====✨\n" for i, trending in enumerate(gt_list[:10]): # 只获取前10个趋势 gt_str += ( - f"{i + 1}. 🏎️ {trending['author']} / {trending['repo']}\n" - f"⭐ {trending['star_total']} total (⭐{trending['star_today']})\n" - f"🔤 {trending['programmingLanguage']}\n" - f"📖 {trending['comment']}\n" + f"{i + 1}.📦 {trending['author']} / {trending['repo']}\n" + f" ⭐ {trending['star_total']} total (⭐{trending['star_today']})\n" + f" 🔤 {trending['programmingLanguage']}\n" + f" 📖 {trending['comment']}\n" ) return gt_str + + +def _generate_github_trending_quoted_response(gt_list: List) -> str: + search_api = "https://github.com/%s/%s" + result = {} + for i, trending in enumerate(gt_list[:10]): # 只获取前10个趋势 + result[str(i + 1)] = url_encode( + search_api % (trending["author"], trending["repo"]) + ) + return json.dumps(result) diff --git a/wechatter/commands/_commands/pai_post.py b/wechatter/commands/_commands/pai_post.py index 36021e9..ee98dc9 100644 --- a/wechatter/commands/_commands/pai_post.py +++ b/wechatter/commands/_commands/pai_post.py @@ -1,4 +1,5 @@ -from typing import List +import json +from typing import List, Tuple import requests from bs4 import BeautifulSoup @@ -6,31 +7,63 @@ from wechatter.commands.handlers import command from wechatter.exceptions import Bs4ParsingError -from wechatter.models.wechat import SendTo +from wechatter.models.wechat import QuotedResponse, SendTo from wechatter.sender import sender -from wechatter.utils import get_request +from wechatter.utils import get_request, url_encode + +COMMAND_NAME = "pai-post" @command( - command="pai-post", + command=COMMAND_NAME, keys=["派早报", "pai-post"], desc="获取少数派早报。", ) def pai_post_command_handler(to: SendTo, message: str = "") -> None: try: - result = get_pai_post_str() + result, q_response = get_pai_post_str() except Exception as e: error_message = f"获取少数派早报失败,错误信息:{str(e)}" logger.error(error_message) sender.send_msg(to, error_message) else: - sender.send_msg(to, result) + sender.send_msg( + to, + result, + quoted_response=QuotedResponse( + command=COMMAND_NAME, + response=q_response, + ), + ) + + +@pai_post_command_handler.quoted_handler +def pai_post_quoted_handler( + to: SendTo, message: str = "", q_response: str = "" +) -> None: + if not message.isdigit(): + logger.error("输入的早报编号不是数字") + sender.send_msg(to, "请输入早报编号") + return + + post_url_dict = json.loads(q_response) + try: + hot_url = post_url_dict[message] + except Exception: + logger.error("输入的早报编号错误") + sender.send_msg(to, "输入的早报编号错误") + return + else: + sender.send_msg(to, hot_url) -def get_pai_post_str() -> str: +def get_pai_post_str() -> Tuple[str, str]: response = get_request(url="https://sspai.com/") pai_post_list = _parse_pai_post_response(response) - return _generate_pai_post_message(pai_post_list) + return ( + _generate_pai_post_message(pai_post_list), + _generate_pai_post_quoted_response(pai_post_list), + ) def _parse_pai_post_response(response: requests.Response) -> List: @@ -44,6 +77,10 @@ def _parse_pai_post_response(response: requests.Response) -> List: for article in articles: pai_post_item = {} + href = article.select_one("a") + if href: + pai_post_item["href"] = href.get("href") + title = article.select_one("a div") if title: pai_post_item["title"] = title.text.strip() @@ -67,3 +104,13 @@ def _generate_pai_post_message(pai_post_list: List) -> str: pai_post_str += f"{i + 1}. {pai_post.get('title')}\n" return pai_post_str + + +def _generate_pai_post_quoted_response(pai_post_list: List) -> str: + result = {} + base_url = "https://sspai.com" + for i, pai_post in enumerate(pai_post_list): + href = pai_post.get("href", None) + if href: + result[str(i + 1)] = url_encode(base_url + href) + return json.dumps(result) diff --git a/wechatter/commands/_commands/weather.py b/wechatter/commands/_commands/weather.py index d863bfe..ba8a906 100644 --- a/wechatter/commands/_commands/weather.py +++ b/wechatter/commands/_commands/weather.py @@ -30,6 +30,8 @@ def weather_command_handler(to: SendTo, message: str = "") -> None: sender.send_msg(to, result) +# TODO: Quoted Handler,获取更具体的天气,关键词例如:明天,逐日,日出日落,空气质量,紫外线指数等 + # class WeatherTip: # def __init__(self, priority, condition, tip): # self.priority = priority diff --git a/wechatter/commands/_commands/weibo_hot.py b/wechatter/commands/_commands/weibo_hot.py index ce5e318..fc5c6b0 100644 --- a/wechatter/commands/_commands/weibo_hot.py +++ b/wechatter/commands/_commands/weibo_hot.py @@ -1,35 +1,68 @@ -from typing import Dict, List +import json +from typing import Dict, List, Tuple from loguru import logger from wechatter.commands.handlers import command -from wechatter.models.wechat import SendTo +from wechatter.models.wechat import QuotedResponse, SendTo from wechatter.sender import sender -from wechatter.utils import get_request_json +from wechatter.utils import get_request_json, url_encode + +COMMAND_NAME = "weibo-hot" @command( - command="weibo-hot", + command=COMMAND_NAME, keys=["微博热搜", "weibo-hot"], desc="获取微博热搜。", ) def weibo_hot_command_handler(to: SendTo, message: str = "") -> None: try: - result = get_weibo_hot_str() + result, q_response = get_weibo_hot_str() except Exception as e: error_message = f"获取微博热搜失败,错误信息: {str(e)}" logger.error(error_message) sender.send_msg(to, error_message) else: - sender.send_msg(to, result) + sender.send_msg( + to, + result, + quoted_response=QuotedResponse( + command=COMMAND_NAME, + response=q_response, + ), + ) + + +@weibo_hot_command_handler.quoted_handler +def weibo_hot_quoted_handler( + to: SendTo, message: str = "", q_response: str = "" +) -> None: + if not message.isdigit(): + logger.error("输入的热搜编号不是数字") + sender.send_msg(to, "请输入热搜编号") + return + hot_url_dict = json.loads(q_response) + try: + hot_url = hot_url_dict[message] + except Exception: + logger.error("输入的热搜编号错误") + sender.send_msg(to, "输入的热搜编号错误") + return + else: + sender.send_msg(to, hot_url) -def get_weibo_hot_str() -> str: + +def get_weibo_hot_str() -> Tuple[str, str]: r_json = get_request_json( url="https://m.weibo.cn/api/container/getIndex?containerid=106003%26filter_type%3Drealtimehot" ) hot_list = _extract_weibo_hot_data(r_json) - return _generate_weibo_hot_message(hot_list) + return ( + _generate_weibo_hot_message(hot_list), + _generate_weibo_hot_quoted_response(hot_list), + ) def _extract_weibo_hot_data(r_json: Dict) -> List: @@ -50,3 +83,13 @@ def _generate_weibo_hot_message(hot_list: List) -> str: hot_search_str += f"{i + 1}. {hot_search.get('desc')}\n" return hot_search_str + + +def _generate_weibo_hot_quoted_response(hot_list: List) -> str: + result = {} + search_url = "https://s.weibo.com/weibo?q=%s" + for i, hot_search in enumerate(hot_list): + keyword = hot_search.get("desc", None) + if keyword: + result[str(i + 1)] = search_url % url_encode(keyword) + return json.dumps(result) diff --git a/wechatter/commands/_commands/zhihu_hot.py b/wechatter/commands/_commands/zhihu_hot.py index 90435af..6aa6111 100644 --- a/wechatter/commands/_commands/zhihu_hot.py +++ b/wechatter/commands/_commands/zhihu_hot.py @@ -1,33 +1,66 @@ -from typing import Dict, List +import json +from typing import Dict, List, Tuple from loguru import logger from wechatter.commands.handlers import command -from wechatter.models.wechat import SendTo +from wechatter.models.wechat import QuotedResponse, SendTo from wechatter.sender import sender from wechatter.utils import get_request_json +COMMAND_NAME = "zhihu-hot" + @command( - command="zhihu-hot", + command=COMMAND_NAME, keys=["知乎热搜", "zhihu-hot"], desc="获取知乎热搜。", ) def zhihu_hot_command_handler(to: SendTo, message: str = "") -> None: try: - result = get_zhihu_hot_str() + result, q_response = get_zhihu_hot_str() except Exception as e: error_message = f"获取知乎热搜失败,错误信息: {str(e)}" logger.error(error_message) sender.send_msg(to, error_message) else: - sender.send_msg(to, result) + sender.send_msg( + to, + result, + quoted_response=QuotedResponse( + command=COMMAND_NAME, + response=q_response, + ), + ) + + +@zhihu_hot_command_handler.quoted_handler +def zhihu_hot_quoted_handler( + to: SendTo, message: str = "", q_response: str = "" +) -> None: + if not message.isdigit(): + logger.error("输入的热搜编号不是数字") + sender.send_msg(to, "请输入热搜编号") + return + + hot_url_dict = json.loads(q_response) + try: + hot_url = hot_url_dict[message] + except Exception: + logger.error("输入的热搜编号错误") + sender.send_msg(to, "输入的热搜编号错误") + return + else: + sender.send_msg(to, hot_url) -def get_zhihu_hot_str() -> str: +def get_zhihu_hot_str() -> Tuple[str, str]: response = get_request_json(url="https://api.zhihu.com/topstory/hot-list?limit=10") hot_list = _extract_zhihu_hot_data(response) - return _generate_zhihu_hot_message(hot_list) + return ( + _generate_zhihu_hot_message(hot_list), + _generate_zhihu_hot_quoted_response(hot_list), + ) def _extract_zhihu_hot_data(r_json: Dict) -> List: @@ -48,3 +81,14 @@ def _generate_zhihu_hot_message(hot_list: List) -> str: hot_search_str += f"{i + 1}. {hot_search.get('target', {}).get('title', '')}\n" return hot_search_str + + +def _generate_zhihu_hot_quoted_response(hot_list: List) -> str: + hot_url_dict = {} + for i, hot_search in enumerate(hot_list[:20]): + url = hot_search.get("target", {}).get("url", "") + hot_url_dict[str(i + 1)] = url.replace("api", "www", 1).replace( + "questions", "question", 1 + ) + + return json.dumps(hot_url_dict) diff --git a/wechatter/commands/handlers.py b/wechatter/commands/handlers.py index bfd5226..080fc5d 100644 --- a/wechatter/commands/handlers.py +++ b/wechatter/commands/handlers.py @@ -7,20 +7,28 @@ commands = {} """ -存储所有命令消息和其信息的及其处理函数的字典。 +存储所有命令的信息及其处理函数的字典 +""" +quoted_handlers = {} +""" +存储所有可引用的命令消息的处理函数的字典 """ -def command(command: str, keys: List[str], desc: str): - """ - 注册命令 - :param command: 命令 - :param keys: 命令关键词列表 - :param desc: 命令描述 - :return: 装饰器 - """ +# 改为类装饰器 +class command: + def __init__(self, command: str, keys: List[str], desc: str): + """ + :param command: 命令 + :param keys: 命令关键词列表 + :param desc: 命令描述 + """ + # TODO: 检测command是否重复 + self.command = command + self.keys = keys + self.desc = desc - def decorator(func): + def __call__(self, func): sig = inspect.signature(func) params = sig.parameters if len(params) < 2: @@ -50,16 +58,19 @@ def decorator(func): logger.error(error_message) raise ValueError(error_message) - commands[command] = {} + commands[self.command] = {} # 自定义命令关键词 - if config.custom_command_key_dict.get(command, None): - keys.extend(config.custom_command_key_dict[command]) + if config.custom_command_key_dict.get(self.command, None): + self.keys.extend(config.custom_command_key_dict[self.command]) - commands[command]["keys"] = keys - commands[command]["desc"] = desc - commands[command]["handler"] = func - commands[command]["param_count"] = len(params) + commands[self.command]["keys"] = self.keys + commands[self.command]["desc"] = self.desc + commands[self.command]["handler"] = func + commands[self.command]["param_count"] = len(params) - return func + return self - return decorator + def quoted_handler(self, func): + # TODO: 判断参数是否合理 + quoted_handlers[self.command] = func + return func diff --git a/wechatter/config/config.py b/wechatter/config/config.py index ba14f45..72e0fbf 100644 --- a/wechatter/config/config.py +++ b/wechatter/config/config.py @@ -9,8 +9,7 @@ wechatter_port = config_reader.getint("wechatter", "wechatter_port") # wx-bot-webhook 配置 -wx_webhook_host = config_reader.getstr("wx-bot-webhook", "wx_webhook_host") -wx_webhook_port = config_reader.getint("wx-bot-webhook", "wx_webhook_port") +wx_webhook_base_api = config_reader.getstr("wx-bot-webhook", "wx_webhook_base_api") wx_webhook_recv_api_path = config_reader.getstr( "wx-bot-webhook", "wx_webhook_recv_api_path" ) @@ -27,8 +26,7 @@ need_mentioned = config_reader.getbool("chat", "need_mentioned") # copilot-gpt4 配置 -cp_gpt4_api_host = config_reader.getstr("copilot-gpt4", "cp_gpt4_api_host") -cp_gpt4_port = config_reader.getint("copilot-gpt4", "cp_gpt4_port") +cp_gpt4_base_api = config_reader.getstr("copilot-gpt4", "cp_gpt4_base_api") cp_token = config_reader.getstr("copilot-gpt4", "cp_token") # github-webhook 配置 @@ -66,7 +64,9 @@ gasoline_price_cron_enable = config_reader.getbool( "gasoline-price-cron", "gasoline_price_cron_enabled" ) -gasoline_price_cron_rule_list = config_reader.getlist("gasoline-price-cron", "gasoline_price_cron_rule_list") +gasoline_price_cron_rule_list = config_reader.getlist( + "gasoline-price-cron", "gasoline_price_cron_rule_list" +) logger.info(config_reader.config_dict) diff --git a/wechatter/database/__init__.py b/wechatter/database/__init__.py index 57f6e28..3a8a862 100644 --- a/wechatter/database/__init__.py +++ b/wechatter/database/__init__.py @@ -5,6 +5,7 @@ from .tables.group import Group from .tables.message import Message from .tables.person import Person +from .tables.quoted_response import QuotedResponse __all__ = [ "make_db_session", @@ -14,4 +15,5 @@ "Message", "Group", "Person", + "QuotedResponse", ] diff --git a/wechatter/database/tables/quoted_response.py b/wechatter/database/tables/quoted_response.py new file mode 100644 index 0000000..a6c61f4 --- /dev/null +++ b/wechatter/database/tables/quoted_response.py @@ -0,0 +1,35 @@ +from sqlalchemy import Integer, String +from sqlalchemy.orm import Mapped, mapped_column + +from wechatter.database.tables import Base +from wechatter.models.wechat import QuotedResponse as QuotedResponseModel + + +class QuotedResponse(Base): + """ + 引用回复表 + """ + + __tablename__ = "quoted_response" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + quotable_id: Mapped[str] = mapped_column(String(10)) + command: Mapped[str] + response: Mapped[str] + + @classmethod + def from_model(cls, model: QuotedResponseModel): + return cls( + id=model.id, + quotable_id=model.quotable_id, + command=model.command, + response=model.response, + ) + + def to_model(self) -> QuotedResponseModel: + return QuotedResponseModel( + id=self.id, + quotable_id=self.quotable_id, + command=self.command, + response=self.response, + ) diff --git a/wechatter/init_logger.py b/wechatter/init_logger.py index 997776a..71cb121 100644 --- a/wechatter/init_logger.py +++ b/wechatter/init_logger.py @@ -40,7 +40,12 @@ def emit(self, record): # TODO: 解决DEBUG级别下multipart.multipart:callback太多问题 -def init_logger(): +def init_logger(log_level: str = ""): + if log_level: + global LOG_LEVEL_NAME, LOG_LEVEL + LOG_LEVEL_NAME = log_level + LOG_LEVEL = logging.getLevelName(LOG_LEVEL_NAME) + # 拦截根日志记录器的所有内容 logging.root.handlers = [InterceptHandler()] logging.root.setLevel(LOG_LEVEL) diff --git a/wechatter/message/__init__.py b/wechatter/message/__init__.py index 7ecea2a..d57cd4b 100644 --- a/wechatter/message/__init__.py +++ b/wechatter/message/__init__.py @@ -1,4 +1,4 @@ -from .message_parser import MessageHandler +from .message_handler import MessageHandler __all__ = [ "MessageHandler", diff --git a/wechatter/message/message_parser.py b/wechatter/message/message_handler.py similarity index 53% rename from wechatter/message/message_parser.py rename to wechatter/message/message_handler.py index 963ccf6..be0f342 100644 --- a/wechatter/message/message_parser.py +++ b/wechatter/message/message_handler.py @@ -1,10 +1,12 @@ # 消息解析器 import re +from typing import Dict from loguru import logger import wechatter.config as config from wechatter.bot.bot_info import BotInfo +from wechatter.database import QuotedResponse, make_db_session from wechatter.models.wechat import Message, SendTo @@ -13,36 +15,64 @@ class MessageHandler: 消息处理器,用于处理用户发来的消息 """ - def __init__(self, commands: dict): - self.__commands = commands + def __init__(self, commands: Dict, quoted_handlers: Dict): + """ + :param commands: 命令处理函数字典 + :param quoted_handlers: 可引用的命令消息处理函数字典 + """ + self.commands = commands + self.quoted_handlers = quoted_handlers - def handle_message(self, message: Message) -> None: + def handle_message(self, message_obj: Message): """ 处理消息 + :param message_obj: 消息对象 """ + to = SendTo(person=message_obj.person, group=message_obj.group) # 解析命令 - content = message.content # 消息内容 - # 消息内容格式: / - cmd_dict = self.parse_command(content, message.is_mentioned, message.is_group) - + content = message_obj.content + cmd_dict = self.__parse_command( + content, message_obj.is_mentioned, message_obj.is_group + ) logger.info(cmd_dict["desc"]) + # 如果是,处理可引用的命令消息 + if message_obj.quotable_id: + with make_db_session() as session: + _quoted_response = ( + session.query(QuotedResponse) + .filter_by(quotable_id=message_obj.quotable_id) + .order_by(QuotedResponse.id.desc()) + .first() + ) + quoted_response = _quoted_response.to_model() + quoted_handler = self.quoted_handlers.get(quoted_response.command, None) + if quoted_handler: + quoted_handler( + to=to, + message=message_obj.pure_content, + q_response=quoted_response.response, + ) + else: + logger.warning( + f"未找到可引用的命令消息处理函数: {quoted_response.command}" + ) + return + # 非命令消息 if cmd_dict["command"] == "None": logger.info("该消息不是命令类型") return - # TODO: 判断是否引用消息,接着判断引用是否为“可引用的命令回复”消息 - # if message.is_quote: - # pass - # TODO: 可以为不同的群设置是否need_mentioned - if config.need_mentioned and message.is_group and not message.is_mentioned: + if ( + config.need_mentioned + and message_obj.is_group + and not message_obj.is_mentioned + ): logger.debug("该消息为群消息,但未@机器人,不处理") return - to = SendTo(person=message.person, group=message.group) - # 是命令消息 # 开始处理命令 cmd_handler = cmd_dict["handler"] @@ -56,15 +86,18 @@ def handle_message(self, message: Message) -> None: cmd_handler( to=to, message=cmd_dict["arg"], - message_obj=message, + message_obj=message_obj, ) else: logger.error("该命令未实现") return - def parse_command(self, content: str, is_mentioned: bool, is_group: bool) -> dict: + def __parse_command(self, content: str, is_mentioned: bool, is_group: bool) -> Dict: """ 解析命令 + :param content: 消息内容 + :param is_mentioned: 是否@机器人 + :param is_group: 是否群消息 """ cmd_dict = { "command": "None", @@ -77,7 +110,7 @@ def parse_command(self, content: str, is_mentioned: bool, is_group: bool) -> dic if is_mentioned and is_group: # 去掉"@机器人名"的前缀 content = content.replace(f"@{BotInfo.name} ", "") - for command, info in self.__commands.items(): + for command, info in self.commands.items(): # 第一个空格或回车前的内容即为指令 cont_list = re.split(r"\s|\n", content, 1) if not cont_list[0].startswith(config.command_prefix): diff --git a/wechatter/models/wechat/__init__.py b/wechatter/models/wechat/__init__.py index 571e673..5c1ec4e 100644 --- a/wechatter/models/wechat/__init__.py +++ b/wechatter/models/wechat/__init__.py @@ -1,6 +1,7 @@ from .group import Group, GroupMember from .message import Message, MessageType from .person import Gender, Person +from .quoted_response import QUOTABLE_FORMAT, QuotedResponse from .send_to import SendTo __all__ = [ @@ -11,4 +12,6 @@ "GroupMember", "Person", "Gender", + "QuotedResponse", + "QUOTABLE_FORMAT", ] diff --git a/wechatter/models/wechat/message.py b/wechatter/models/wechat/message.py index d9e771c..fad1c70 100644 --- a/wechatter/models/wechat/message.py +++ b/wechatter/models/wechat/message.py @@ -1,4 +1,3 @@ -# 消息类 import enum import json import re @@ -11,6 +10,7 @@ import wechatter.config as config from wechatter.models.wechat.group import Group from wechatter.models.wechat.person import Person +from wechatter.models.wechat.quoted_response import QUOTABLE_FORMAT class MessageType(enum.Enum): @@ -141,6 +141,32 @@ def sender_name(self) -> str: """ return self.group.name if self.is_group else self.person.name + @computed_field + @cached_property + def quotable_id(self) -> Optional[str]: + """ + 获取引用消息的id + """ + if self.is_quoted: + pattern = rf'「.+{QUOTABLE_FORMAT % "(.{3})"}' + try: + return re.search(pattern, self.content).group(1) + except AttributeError: + return None + return None + + @computed_field + @cached_property + def pure_content(self) -> str: + """ + 获取不带引用的消息内容,即用户真实发送的消息 + """ + if self.is_quoted: + pattern = "「[\s\S]+」\n- - - - - - - - - - - - - - -\n([\s\S]*)" + return re.search(pattern, self.content).group(1) + else: + return self.content + def __str__(self) -> str: source = self.person if self.is_group: diff --git a/wechatter/models/wechat/quoted_response.py b/wechatter/models/wechat/quoted_response.py new file mode 100644 index 0000000..1c27c9d --- /dev/null +++ b/wechatter/models/wechat/quoted_response.py @@ -0,0 +1,16 @@ +from typing import Optional + +from pydantic import BaseModel + +QUOTABLE_FORMAT = "(可引用:%s)\n" + + +class QuotedResponse(BaseModel): + """ + 引用回复类 + """ + + command: str + response: str + id: Optional[int] = None + quotable_id: Optional[str] = None diff --git a/wechatter/sender/notifier.py b/wechatter/sender/notifier.py index d9c4e56..fa89097 100644 --- a/wechatter/sender/notifier.py +++ b/wechatter/sender/notifier.py @@ -1,23 +1,33 @@ # 消息通知器 -from wechatter.models.wechat import SendTo +from typing import TYPE_CHECKING + from wechatter.sender import sender +if TYPE_CHECKING: + from wechatter.models.wechat import SendTo + -def notify_received(to: SendTo) -> None: - """通知收到命令请求""" +def notify_received(to: "SendTo") -> None: + """ + 通知收到命令请求 + """ msg = "收到命令请求" sender.send_msg(to, msg) # 机器人登录登出通知,若是登录(登出)则发送登录(登出)消息给所有管理员 def notify_logged_in() -> None: - """通知登录成功""" + """ + 通知登录成功 + """ msg = "微信机器人启动成功" sender.mass_send_msg_to_admins(msg) # FIXME: 登出消息发送不出去,因为发消息时候,机器人已经退出登录了 def notify_logged_out() -> None: - """通知已退出登录""" + """ + 通知已退出登录 + """ msg = "微信机器人已退出" sender.mass_send_msg_to_admins(msg) diff --git a/wechatter/sender/quotable.py b/wechatter/sender/quotable.py new file mode 100644 index 0000000..d2e7e75 --- /dev/null +++ b/wechatter/sender/quotable.py @@ -0,0 +1,60 @@ +import string + +from wechatter.database import QuotedResponse as DbQuotedResponse, make_db_session +from wechatter.models.wechat import QUOTABLE_FORMAT, QuotedResponse + +# QUOTABLE_FORMAT = "(可引用:%s)\n" +CHARS = string.digits + string.ascii_letters + + +# 将消息可引用化 +def make_quotable(message: str, quoted_response: QuotedResponse) -> str: + """ + 将消息可引用化 + :param message: 消息内容 + :param quoted_response: 可引用的消息内容 + :return: 可引用的消息内容 + """ + # 获取可引用消息的ID(可引用标识符) + quotable_id = _get_quotable_id() + quoted_response.quotable_id = quotable_id + with make_db_session() as session: + # 将可引用标识符和回复消息存入数据库 + q_message = DbQuotedResponse.from_model(quoted_response) + session.add(q_message) + session.commit() + + # 将消息内容和可引用消息的ID拼接 + return (QUOTABLE_FORMAT % quotable_id) + message + + +def _get_quotable_id() -> str: + """ + 获取可引用消息的ID + :return: 可引用消息的ID + """ + # 获取最后一个可引用消息的ID + with make_db_session() as session: + quotable_id = ( + session.query(DbQuotedResponse).order_by(DbQuotedResponse.id.desc()).first() + ) + # quotable_id 是由52个大小写字母加10个数字组成的三位字符串,每一位都有可能是0-9、a-z、A-Z中的任意一个字符 + if quotable_id: + # id加1 + quotable_id = _increase_id(quotable_id.quotable_id) + else: + quotable_id = "000" + return quotable_id + + +def _increase_id(quotable_id: str) -> str: + """ + 将ID加1,如果ID超过 ZZZ,则从 000 开始 + """ + if quotable_id == "Z": + return "0" + + if quotable_id[-1] == CHARS[-1]: + return _increase_id(quotable_id[:-1]) + "0" + else: + return quotable_id[:-1] + CHARS[CHARS.find(quotable_id[-1]) + 1] diff --git a/wechatter/sender/quotation.py b/wechatter/sender/quotation.py deleted file mode 100644 index 6e9be8b..0000000 --- a/wechatter/sender/quotation.py +++ /dev/null @@ -1,20 +0,0 @@ -QUOTABLE_FORMAT = "(可引用:%s)\n" - - -# 将消息可引用化 -def make_quotable(message: str) -> str: - """ - 将消息可引用化 - :param message: 消息内容 - :return: 可引用的消息内容 - """ - # 获取可引用消息的ID(可引用标识符) - quotable_id = _get_random_quotable_id() - return QUOTABLE_FORMAT % quotable_id + message - - -def _get_random_quotable_id() -> str: - """ - 获取可引用消息的ID - :return: 可引用消息的ID - """ diff --git a/wechatter/sender/sender.py b/wechatter/sender/sender.py index dbcde6c..75a7e32 100644 --- a/wechatter/sender/sender.py +++ b/wechatter/sender/sender.py @@ -8,7 +8,8 @@ import wechatter.config as config import wechatter.utils.http_request as http_request -from wechatter.models.wechat import SendTo +from wechatter.models.wechat import QuotedResponse, SendTo +from wechatter.sender.quotable import make_quotable # 对retry装饰器重新包装,增加日志输出 @@ -16,7 +17,7 @@ def _retry( stop=tenacity.stop_after_attempt(3), retry_error_log_level="ERROR", ): - def wrapper(func): + def retry_wrapper(func): @tenacity.retry(stop=stop) def wrapped_func(*args, **kwargs): try: @@ -30,9 +31,53 @@ def wrapped_func(*args, **kwargs): return wrapped_func - return wrapper + return retry_wrapper + + +# TODO: 改成装饰器 +def _logging(func): + def logging_wrapper(*args, **kwargs): + response = func(*args, **kwargs) + r_json = response.json() + # https://github.com/danni-cool/wechatbot-webhook?tab=readme-ov-file#%E8%BF%94%E5%9B%9E%E5%80%BC-response-%E7%BB%93%E6%9E%84 + if r_json["message"].startswith("Message"): + pass + elif r_json["message"].startswith("Some"): + logger.error("发送消息失败,参数校验不通过") + elif r_json["message"].startswith("All"): + logger.error("发送消息失败,所有消息均发送失败") + return + elif r_json["message"].startswith("Part"): + logger.warning("发送消息失败,部分消息发送成功") + return + + if "task" not in r_json: + return + + try: + data = json.loads(response.request.body.decode("utf-8")) + except UnicodeDecodeError: + # 本地文件发送无法解码 + # logger.info("发送图片成功") + return + except json.JSONDecodeError as e: + logger.error(f"发送消息失败,错误信息:{str(e)}") + return + if isinstance(data, list): + for item in data: + logger.info( + f"发送消息成功,发送给:{item['to']},发送的内容:{item['data']}" + ) + elif isinstance(data, dict): + logger.info( + f"发送消息成功,发送给:{data['to']},发送的内容:{data['data']}" + ) + return logging_wrapper + + +@_logging @_retry() def _post_request( url, data=None, json=None, files=None, headers={}, timeout=5 @@ -42,9 +87,11 @@ def _post_request( ) -# TODO: 改成装饰器 def _log(response: requests.Response) -> bool: - """检查发送状态""" + """ + 检查发送状态 + """ + r_json = response.json() # https://github.com/danni-cool/wechatbot-webhook?tab=readme-ov-file#%E8%BF%94%E5%9B%9E%E5%80%BC-response-%E7%BB%93%E6%9E%84 if r_json["message"].startswith("Message"): @@ -81,8 +128,8 @@ def _log(response: requests.Response) -> bool: return True -URL = f"{config.wx_webhook_host}:{config.wx_webhook_port}/webhook/msg/v2" -V1_URL = f"{config.wx_webhook_host}:{config.wx_webhook_port}/webhook/msg" +URL = f"{config.wx_webhook_base_api}/webhook/msg/v2" +V1_URL = f"{config.wx_webhook_base_api}/webhook/msg" def _validate(fn): @@ -109,7 +156,7 @@ def send_msg( message: str, is_group: bool = False, type: str = "text", - quotable: bool = False, + quoted_response: QuotedResponse = None, ): """ 发送消息 @@ -117,14 +164,14 @@ def send_msg( 当传入的第一个参数是字符串时,is_group 默认为 False。 当传入的第一个参数是 SendTo 对象时,is_group 默认为 True。 - 当 quotable 为 Ture 时,该消息为可引用消息。表示该消息被 + 当 quoted_response 不为 None 时,该消息为可引用消息。表示该消息被 引用回复后,会触发进一步的消息互动。 :param to: 接收对象的名字或SendTo对象 :param message: 消息内容 :param is_group: 是否为群组(默认值根据 to 的类型而定) :param type: 消息类型,可选 text、fileUrl(默认值为 text) - :param quotable: 是否可引用(默认值为 False) + :param quoted_response: 被引用后的回复消息(默认值为 None) """ pass @@ -136,51 +183,81 @@ def _send_msg1( message: str, is_group: bool = False, type: str = "text", - quotable: bool = False, -) -> None: + quoted_response: QuotedResponse = None, +): """ 发送消息 :param name: 接收者 :param message: 消息内容 :param is_group: 是否为群组(默认为个人,False) :param type: 消息类型(text、fileUrl) - :param quotable: 是否可引用(默认为不可引用,False) + :param quoted_response: 被引用后的回复消息(默认值为 None) """ - # if quotable: - # message = f"@{name} {message}" + if quoted_response: + message = make_quotable(message=message, quoted_response=quoted_response) data = { "to": name, "isRoom": is_group, "data": {"type": type, "content": message}, } - _log(_post_request(URL, json=data)) + _post_request(URL, json=data) @send_msg.register(SendTo) -def _send_msg2(to: SendTo, message: str, is_group: bool = True, type: str = "text"): +def _send_msg2( + to: SendTo, + message: str, + is_group: bool = True, + type: str = "text", + quoted_response: QuotedResponse = None, +): """ 发送消息 :param to: SendTo 对象 :param message: 消息内容 :param is_group: 是否为群组(默认为群组,True) :param type: 消息类型(text、fileUrl) + :param quoted_response: 被引用后的回复消息(默认值为 None) """ if not is_group: - return _send_msg1(to.p_name, message, is_group=False, type=type) + return _send_msg1( + to.p_name, + message, + is_group=False, + type=type, + quoted_response=quoted_response, + ) if to.group: - return _send_msg1(to.g_name, message, is_group=True, type=type) + return _send_msg1( + to.g_name, + message, + is_group=True, + type=type, + quoted_response=quoted_response, + ) elif to.person: - return _send_msg1(to.p_name, message, is_group=False, type=type) + return _send_msg1( + to.p_name, + message, + is_group=False, + type=type, + quoted_response=quoted_response, + ) else: logger.error("发送消息失败,接收者为空") @singledispatch -def send_msg_list(): +def send_msg_list( + to: Union[str, SendTo], + message_list: List[str], + is_group: bool = False, + type: str = "text", +): """ 发送多条消息,消息类型相同 - :param name: 接收者 + :param to: 接收者 :param message_list: 消息内容列表 :param is_group: 是否为群组 :param type: 消息类型(text、fileUrl) @@ -191,7 +268,10 @@ def send_msg_list(): @send_msg_list.register(str) @_validate def _send_msg_list1( - name: str, message_list: List[str], is_group: bool = False, type: str = "text" + name: str, + message_list: List[str], + is_group: bool = False, + type: str = "text", ): """ 发送多条消息,消息类型相同 @@ -203,7 +283,7 @@ def _send_msg_list1( data = {"to": name, "isRoom": is_group, "data": []} for message in message_list: data["data"].append({"type": type, "content": message}) - _log(_post_request(URL, json=data)) + _post_request(URL, json=data) @send_msg_list.register(SendTo) @@ -220,9 +300,9 @@ def _send_msg_list2( if not is_group: return _send_msg_list1(to.p_name, message_list, is_group=False, type=type) - if to.g_name != "": + if to.group: return _send_msg_list1(to.g_name, message_list, is_group=True, type=type) - elif to.p_name != "": + elif to.person: return _send_msg_list1(to.p_name, message_list, is_group=False, type=type) else: logger.error("发送消息失败,接收者为空") @@ -230,7 +310,11 @@ def _send_msg_list2( @_validate def mass_send_msg( - name_list: List[str], message: str, is_group: bool = False, type: str = "text" + name_list: List[str], + message: str, + is_group: bool = False, + type: str = "text", + quoted_response: QuotedResponse = None, ): """ 群发消息,给多个人发送一条消息 @@ -238,7 +322,10 @@ def mass_send_msg( :param message: 消息内容 :param is_group: 是否为群组 :param type: 消息类型(text、fileUrl) + :param quoted_response: 被引用后的回复消息(默认值为 None) """ + if quoted_response: + message = make_quotable(message=message, quoted_response=quoted_response) data = [] for name in name_list: data.append( @@ -248,7 +335,7 @@ def mass_send_msg( "data": {"type": type, "content": message}, } ) - _log(_post_request(URL, json=data)) + _post_request(URL, json=data) @singledispatch @@ -273,7 +360,7 @@ def _send_localfile_msg1(name: str, file_path: str, is_group: bool = False): """ data = {"to": name, "isRoom": int(is_group)} files = {"content": open(file_path, "rb")} - _log(_post_request(V1_URL, data=data, files=files)) + _post_request(V1_URL, data=data, files=files) @send_localfile_msg.register(SendTo) @@ -287,19 +374,25 @@ def _send_localfile_msg2(to: SendTo, file_path: str, is_group: bool = True): if not is_group: return _send_localfile_msg1(to.p_name, file_path, is_group=False) - if to.g_name != "": + if to.group: return _send_localfile_msg1(to.g_name, file_path, is_group=True) - elif to.p_name != "": + elif to.person: return _send_localfile_msg1(to.p_name, file_path, is_group=False) else: logger.error("发送消息失败,接收者为空") -def mass_send_msg_to_admins(message: str, type: str = "text"): +def mass_send_msg_to_admins( + message: str, type: str = "text", quoted_response: QuotedResponse = None +): """ 群发消息给所有管理员 :param message: 消息内容 + :param type: 消息类型(text、fileUrl) + :param quoted_response: 被引用后的回复消息(默认值为 None) """ + if quoted_response: + message = make_quotable(message=message, quoted_response=quoted_response) if len(config.admin_list) == 0: logger.warning("管理员列表为空") else: @@ -310,15 +403,26 @@ def mass_send_msg_to_admins(message: str, type: str = "text"): mass_send_msg(config.admin_group_list, message, is_group=True, type=type) -def mass_send_msg_to_github_webhook_receivers(message: str): +def mass_send_msg_to_github_webhook_receivers( + message: str, type: str = "text", quoted_response: QuotedResponse = None +): """ 群发消息给所有 GitHub Webhook 接收者 :param message: 消息内容 + :param type: 消息类型(text、fileUrl) + :param quoted_response: 被引用后的回复消息(默认值为 None) """ + if quoted_response: + message = make_quotable(message=message, quoted_response=quoted_response) if len(config.github_webhook_receiver_list) == 0: logger.warning("GitHub Webhook 接收者列表为空") else: - mass_send_msg(config.github_webhook_receiver_list, message, type="text") + mass_send_msg( + config.github_webhook_receiver_list, + message, + is_group=False, + type=type, + ) if len(config.github_webhook_receive_group_list) == 0: logger.warning("GitHub Webhook 接收群列表为空") else: @@ -326,5 +430,5 @@ def mass_send_msg_to_github_webhook_receivers(message: str): config.github_webhook_receive_group_list, message, is_group=True, - type="text", + type=type, ) diff --git a/wechatter/sqlite/sqlite_manager.py b/wechatter/sqlite/sqlite_manager.py deleted file mode 100644 index d3f09a1..0000000 --- a/wechatter/sqlite/sqlite_manager.py +++ /dev/null @@ -1,129 +0,0 @@ -# SQLite 管理类 -import sqlite3 -from typing import List, Tuple - -import wechatter.utils.file_manager as fm -import wechatter.utils.path_manager as pm - - -# 单例模式 -class Singleton(object): - def __init__(self, cls): - self._cls = cls - self._instance = {} - - def __call__(self, *args, **kwargs): - if self._cls not in self._instance: - self._instance[self._cls] = self._cls(*args, **kwargs) - return self._instance[self._cls] - - -@Singleton -class SqliteManager: - """SQLite 管理类""" - - def __init__(self, db_file_path: str) -> None: - """初始化""" - self.db_file_path = pm.get_abs_path(db_file_path) - self.conn = sqlite3.connect(self.db_file_path) - self.cursor = self.conn.cursor() - - def __del__(self) -> None: - """析构""" - self.cursor.close() - self.conn.close() - - def execute(self, sql: str, params: Tuple = ()) -> None: - """执行 SQL 语句 - :param sql: SQL 语句 - :param params: 可选参数(元组) - """ - self.cursor.execute(sql, params) - self.conn.commit() - - def fetch_all(self, sql: str, params: Tuple = ()) -> List: - """查询所有 - :param sql: SQL 语句 - :param params: 可选参数(元组) - """ - self.cursor.execute(sql, params) - return self.cursor.fetchall() - - def fetch_one(self, sql: str, params: Tuple = ()) -> Tuple: - """查询一条 - :param sql: SQL 语句 - :param params: 可选参数(元组) - """ - self.cursor.execute(sql, params) - return self.cursor.fetchone() - - def insert(self, table: str, data: dict) -> None: - """插入数据 - :param table: 表名 - :param data: 插入的数据,字典类型,key 为字段名,value 为字段值 - """ - keys = ", ".join(data.keys()) - values = ", ".join(["?"] * len(data)) - sql = f"INSERT INTO {table}({keys}) VALUES({values})" - self.cursor.execute(sql, tuple(data.values())) - self.conn.commit() - - def update(self, table: str, data: dict, condition: str) -> None: - """更新数据 - :param table: 表名 - :param data: 更新的数据,字典类型,key 为字段名,value 为字段值 - :param condition: 更新条件 - """ - keys = ", ".join([f"{key} = ?" for key in data.keys()]) - sql = f"UPDATE {table} SET {keys} WHERE {condition}" - self.cursor.execute(sql, tuple(data.values())) - self.conn.commit() - - def delete(self, table: str, condition: str) -> None: - """删除数据 - :param table: 表名 - :param condition: 删除条件 - """ - sql = f"DELETE FROM {table} WHERE {condition}" - self.cursor.execute(sql) - self.conn.commit() - - def create_table(self, table: str, columns: List) -> None: - """创建表 - :param table: 表名 - :param columns: 字段列表,列表元素为字段名和字段类型,例如:["id INTEGER PRIMARY KEY", "name TEXT"] - """ - columns_str = ", ".join(columns) - sql = f"CREATE TABLE {table}({columns_str})" - self.cursor.execute(sql) - self.conn.commit() - - def check_and_create_table(self, table: str, columns: List) -> None: - """检查并创建表 - :param table: 表名 - :param columns: 字段列表,列表元素为字段名和字段类型,例如:["id INTEGER PRIMARY KEY", "name TEXT"] - """ - sql = f"SELECT name FROM sqlite_master WHERE type='table' AND name='{table}'" - result = self.fetch_one(sql) - if result is None: - self.create_table(table, columns) - - def excute_file(self, file_path: str) -> None: - """运行.SQL文件 - :param file_path: SQL文件路径 - """ - sql_script = "" - with open(file_path, "r", encoding="utf-8") as f: - sql_script = f.read() - self.cursor.executescript(sql_script) - self.conn.commit() - - def excute_folder(self, folder_path: str) -> None: - """运行文件夹下所有.SQL文件 - :param folder_path: SQL文件夹路径 - """ - sql_files = fm.list_files(folder_path, suffixs=[".sql"]) - for sql_file in sql_files: - path = pm.join_path(folder_path, sql_file) - abs_path = pm.get_abs_path(path) - self.excute_file(abs_path) diff --git a/wechatter/utils/__init__.py b/wechatter/utils/__init__.py index 3066859..1919f88 100644 --- a/wechatter/utils/__init__.py +++ b/wechatter/utils/__init__.py @@ -1,5 +1,6 @@ from .http_request import get_request, get_request_json, post_request, post_request_json from .json_manager import load_json, save_json +from .url_codec import url_decode, url_encode __all__ = [ "load_json", @@ -8,4 +9,6 @@ "get_request_json", "post_request", "post_request_json", + "url_encode", + "url_decode", ] diff --git a/wechatter/utils/url_codec.py b/wechatter/utils/url_codec.py new file mode 100644 index 0000000..85ae73b --- /dev/null +++ b/wechatter/utils/url_codec.py @@ -0,0 +1,28 @@ +from urllib.parse import quote, quote_plus, unquote, unquote_plus + + +def url_encode(s: str, safe=":/?=&", plus: bool = False) -> str: + """ + 对字符串进行url编码 + :param s: 待编码字符串 + :param safe: 保留字符 + :param plus: 是否用"+"替换空格 + :return: 编码后的字符串 + """ + if plus: + return quote_plus(s, safe=safe) + else: + return quote(s, safe=safe) + + +def url_decode(s: str, plus: bool = False) -> str: + """ + 对字符串进行url解码 + :param s: 待解码字符串 + :param plus: 是否用"+"替换空格 + :return: 解码后的字符串 + """ + if plus: + return unquote_plus(s) + else: + return unquote(s)