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)