33 Commits

Author SHA1 Message Date
XuChenXu
b36e32fe19 🔖 Version 3.0.1 2025-09-22 21:45:54 +08:00
XuChenXu
bd195ac87a 🔖 Version 3.0.0 2025-09-22 21:40:56 +08:00
XuChenXu
152128391c 超级性能提升 2025-09-22 21:26:19 +08:00
XuChenXu
eee9ac2ab3 🔖 Version 2.7.1 2025-07-11 00:50:48 +08:00
XuChenXu
4970a47b83 🚑 暂时停止缓存支持 2025-07-11 00:48:09 +08:00
沐浴晨煦
383a3f602a 📝 wakatime show 2025-05-17 17:20:19 +08:00
XuChenXu
b0e84c778b 🔖 Version 2.7.0 2025-04-20 11:06:31 +08:00
XuChenXu
1cb2be770d 💄 修复显示 2025-04-20 11:05:49 +08:00
XuChenXu
d19d65f75e 🐛 修复响应问题 2025-04-20 11:03:58 +08:00
XuChenXu
a7bdd6033e Merge branch 'master' of https://github.com/ChenXu233/nonebot_plugin_dialectlist 2025-04-20 08:53:33 +08:00
XuChenXu
0d4f69356a 👽 适配chatrecorder 0.7.0 2025-04-20 08:53:28 +08:00
ProgramRipper
0348a5d66c 🐛 fix: add migrations (#50) 2025-03-30 11:36:32 +08:00
XuChenXu
aff02f2666 🔖 2.6.2 2025-02-11 21:10:40 +08:00
XuChenXu
2f725e0808 📌 限制chatrecord到0.7.0以下 2025-02-11 21:07:36 +08:00
XuChenXu
3434a5352f 🔖 Version 2.6.1 2024-12-18 21:06:03 +08:00
XuChenXu
0954eb4f3e ⬆️ 解除python依赖限制 2024-12-18 21:05:18 +08:00
XuChenXu
5b94bc0fff 📝 更新自述文件 2024-11-25 21:42:02 +08:00
XuChenXu
e1a20922c4 🔖 Version 2.6.0 2024-11-25 21:19:14 +08:00
XuChenXu
5e00605dfb 🎨 结构化代码 2024-11-25 21:18:09 +08:00
XuChenXu
833cdb4a75 💄 还是人性化 2024-11-25 21:16:37 +08:00
XuChenXu
29b4598e5f 💄 人性化提示 2024-11-25 21:02:04 +08:00
XuChenXu
10cc5cf278 关键词支持正则 2024-11-25 21:00:00 +08:00
XuChenXu
ed167a56b5 🔖 Version 2.5.2 2024-11-25 20:51:32 +08:00
XuChenXu
b40c7be1f9 🐛 修复转发消息 2024-11-25 20:50:50 +08:00
XuChenXu
07f85be527 🔖 Version 2.5.1 2024-11-24 20:15:43 +08:00
XuChenXu
f2cdfe7f8a 📝 usage 2024-11-24 20:12:32 +08:00
XuChenXu
e0ca7907c0 kkb! 2024-11-24 20:07:42 +08:00
XuChenXu
53b2a33923 合并转发开关! 2024-11-24 19:39:07 +08:00
XuChenXu
4e339bcdb1 🔖 Version 2.4.6 2024-11-20 23:29:39 +08:00
XuChenXu
0b85f6fb34 remove requests 2024-11-20 23:29:23 +08:00
XuChenXu
83d9cda2eb 🥚 Update README.md 2024-11-11 12:56:38 +08:00
XuChenXu
dfde832dba 📝 fix readme 2024-11-10 21:11:58 +08:00
enKl03B
987b34e282 🔀 📝 Update README.md (#44) 2024-11-10 21:03:01 +08:00
12 changed files with 1324 additions and 2056 deletions

View File

@@ -19,6 +19,7 @@
"pygal",
"sqlalchemy",
"timecost",
"uninfo",
"userinfo",
"whereclause",
"xaxis",

207
README.md
View File

@@ -6,7 +6,8 @@
<div align="center">
# 📃话痨排行榜
# 📃 话痨排行榜
nonebot-plugin-dialectlist
<p align="center">
@@ -21,20 +22,27 @@ nonebot-plugin-dialectlist
</a>
</p>
[![wakatime](https://wakatime.com/badge/user/114aee90-a5f9-49aa-b441-9eca7eebfda3/project/68aaa2a1-fb8a-4b00-bb66-ee67e8a3f3b0.svg)](https://wakatime.com/badge/user/114aee90-a5f9-49aa-b441-9eca7eebfda3/project/68aaa2a1-fb8a-4b00-bb66-ee67e8a3f3b0)
\>💬**看看群友们这些天在群里水了多少话**💬<
</div>
## 💿 安装
通过`pip``nb`安装
通过`pip``nb`安装
>**通过 pip **安装
- 通过 pip 安装
`pip install nonebot-plugin-dialectlist`
```bash
pip install nonebot-plugin-dialectlist
```
>**通过 nb **安装
- 通过 nb-cli 安装
`nb plugin install nonebot-plugin-dialectlist`
```bash
nb plugin install nonebot-plugin-dialectlist
```
### ✅ 插件依赖于
@@ -45,93 +53,122 @@ nonebot-plugin-dialectlist
5. [nonebot-plugin-chatrecorder](https://github.com/noneplugin/nonebot-plugin-chatrecorder) ————实现消息储存
6. [nonebot-plugin-cesaa](https://github.com/MountainDash/nonebot-plugin-send-anything-anywhere) ————实现多平台
**⚠注意** 若先前没有安装过```nonebot-plugin-chatrecorder```或者```nonebot-plugin-orm```,则会在启动时报错,请按报错的提示安装数据库!
** 注意** 若先前没有安装过`nonebot-plugin-chatrecorder`或者`nonebot-plugin-orm`,则会在启动时报错,请按报错的提示安装数据库!
## ⚙ 配置
需要**提前配置**本插件所**依赖的插件**
在 .env 中,可以添加以下配置项
```python
dialectlist__string_format = "第{index}名:\n{nickname},{chatdatanum}条消息、\n" #消息格式
dialectlist__get_num = 10 #获取人数数量
dialectlist__visualization = True #是否可视化
# dialectlist__visualization_type = "圆环图" #可视化方案 (不再支持)
dialectlist__font = "SimHei" #字体格式
dialectlist__excluded_people = [] #排除的人的 QQ 号(或频道号?(未经测试))
dialectlist__excluded_self = True #是否排除机器人自己 QQ
dialectlist__suffix: bool = False # 是否显示后缀
dialectlist__string_suffix: str = "统计花费时间{timecost}" # 消息格式后缀
```
💭也可以不进行配置,这将会使插件按照默认配置运行
### ⚠ 注意!!
```python
dialectlist__get_num: int = 5 # 获取人数数量
dialectlist__font: str = "SimHei" # 字体格式
dialectlist__suffix: bool = True # 是否显示后缀
dialectlist__excluded_self: bool = True # 是否排除自己
dialectlist__visualization: bool = True # 是否可视化
dialectlist__show_text_rank: bool = True # 是否显示文本排名
dialectlist__excluded_people: List[str] = [] # 排除的人的 QQ 号
dialectlist__use_user_info_cache: bool = False # 是否使用用户信息缓存
dialectlist__aggregate_transmission: bool = False # 是否聚合转发消息
dialectlist__timezone: Optional[str] = "Asia/Shanghai" # 时区,影响统计时间
dialectlist__string_suffix: str = "统计花费时间:{timecost}" # 消息格式后缀
dialectlist__template_path: str = "./template/rank_template.j2" # 模板路径
dialectlist__string_format: str = "{index}名:\n{nickname},{chatdatanum}条消息、n" # 消息格式
```
💭 也可以不进行配置,这将会使插件按照默认配置运行
### ⚠ 注意!!
> 在旧版插件2.0.0 以下dialectlist 与后面的配置项只隔了一个下划线,若更新到新版本以后需要俩个下划线。
## 🗨命令
__注意__
> 由于插件依赖 chatrecord 多次引入爆炸性修改,建议在遇到问题后优先尝试降级 chatrecord 插件
## 🗨 命令
**!!注意!!**
新版本指令调用方式改变,改为更易理解也更好打的 B 话榜。
同时也可以用类似 `/今日废话榜` 的方式(只要改前面的就好了)(算是给 [盘古之白](https://github.com/vinta/pangu.js) 风格爱好者的福利吧?)
### 🎨一般用法
### 🎨 一般用法
-`/B话榜` ————看看有史以来(机器人存在以来)群友们发了多少消息! (好像没写)
#### B 话榜
-`/今日B话榜` ————看看今天的群友发了多少消息!
-`/B 话榜` ————看看有史以来(机器人存在以来)群友发了多少消息! (好像没写)
-`/昨日B话榜` ————看看天的群友发了多少消息!
-`/今日 B 话榜` ————看看天的群友发了多少消息!
-`/前日B话榜` ————看看天的群友发了多少消息!
-`/昨日 B 话榜` ————看看天的群友发了多少消息!
-`/本周B话榜` ————看看本周的群友发了多少消息!
-`/前日 B 话榜` ————看看前天的群友发了多少消息!
-`/上周B话榜` ————看看周的群友发了多少消息!
-`/本周 B 话榜` ————看看周的群友发了多少消息!
-`/本月B话榜` ————看看这个月的群友发了多少消息!
-`/上周 B 话榜` ————看看上周的群友发了多少消息!
-`/年度B话榜` ————看看今年的群友发了多少消息!
-`/本月 B 话榜` ————看看这个月的群友发了多少消息!
-`/历史B话榜` ————看看历史上(机器人存在以来)的群友发了多少消息!
-`/年度 B 话榜` ————看看今年的群友发了多少消息!
### 🚀进阶用法
-`/历史 B 话榜` ————看看历史上(机器人存在以来)的群友发了多少消息!
`/{时间类型(今日|年度)?}{B话榜|废话榜} {时间类型?} {ISO8601 格式时间 ?} {群号} {关键词}`
#### 看看 B 话kkb
如:`/B话榜 历史 2024-01-01~2024-01-02 12345678 女装`
-`/看看 B 话 [@某人|QQ 号]` ————看看这个 b 人在这个 b 群发了多少 b 话!
也可以 `/{时间类型(今日|年度)?}{B话榜|废话榜} {时间类型?} {ISO8601 格式时间 ?} -g {群号} -k {关键词}`
### 🚀 进阶用法
#### B 话榜
`/{时间类型(今日|年度)?}{B 话榜|废话榜} {时间类型?} {ISO8601 格式时间 ?} {群号} {关键词}`
如:`/B 话榜 历史 2024-01-01~2024-01-02 12345678 女装`
也可以 `/{时间类型(今日|年度)?}{B 话榜|废话榜} {时间类型?} {ISO8601 格式时间 ?} -g {群号} -k {关键词}`
以下调用方法均合法:
`/今日B话榜 -g 12345678 -k 女装`
`/昨日B话榜 -k 女装`
`/本周B话榜 -g 12345678`
`/今日 B 话榜 -g 12345678 -k 女装`
`/昨日 B 话榜 -k 女装`
`/本周 B 话榜 -g 12345678`
#### 看看 B 话
`/看看 B 话 {@|QQ 号} {群号?} {关键词?}`
以下调用方法均合法:
`/kkb 114514 1919810 ♂`
`/kkb @man -k ♂`
> [!IMPORTANT]
> 关键词支持正则表达式!
## 💪 目前支持的平台
| 平台 | 是否经过测试 | 是否能够正常工作 | 测试环境 |
|:-----:|:----:|:----:| :----: |
| Onebot | ✅ | ✅ | NapCat + Window11|
| 飞书 | ❌ | ❓ | 🤔 |
| Red | ❌ | ❓ | 🤔 |
| DoDo | ❌ | ❓ | 🤔 |
| Mirai | ❌ | ❓ | 🤔 |
| 开黑啦 | ❌ | ❓ | 🤔 |
| Kritor | ❌ | ❓ | 🤔 |
| Ntchat | ❌ | ❓ | 🤔 |
| Satori | ❌ | ❓ | 🤔 |
| Telegram | ❌ | ❓ | 🤔 |
| Discord | ❌ | ❓ | 🤔 |
| Tailchat | ❌ | ❓ | 🤔 |
| QQ 官方接口 | ❌ | ❓ | 🤔 |
| Rocket.Chat | ❌ | ❓ | 🤔 |
| 平台 | 是否经过测试 | 是否能够正常工作 | 测试环境 |
| :---------: | :----------: | :--------------: | :---------------: |
| Onebot | ✅ | ✅ | NapCat + Window11 |
| 飞书 | ❌ | ❓ | 🤔 |
| Red | ❌ | ❓ | 🤔 |
| DoDo | ❌ | ❓ | 🤔 |
| Mirai | ❌ | ❓ | 🤔 |
| 开黑啦 | ❌ | ❓ | 🤔 |
| Kritor | ❌ | ❓ | 🤔 |
| Ntchat | ❌ | ❓ | 🤔 |
| Satori | ❌ | ❓ | 🤔 |
| Telegram | ❌ | ❓ | 🤔 |
| Discord | ❌ | ❓ | 🤔 |
| Tailchat | ❌ | ❓ | 🤔 |
| QQ 官方接口 | ❌ | ❓ | 🤔 |
| Rocket.Chat | ❌ | ❓ | 🤔 |
- 如果你测试过能够使用,请在 Issue 中指出
## 📦另外
## 📦 另外
### 😳加入作者的 BUG 反馈群 ~~(🥵女装粉丝群)~~
### 😳 加入作者的 BUG 反馈群 ~~(🥵 女装粉丝群)~~
[群连接](https://qm.qq.com/q/Yty2yc9Bee)
@@ -142,9 +179,11 @@ __注意__
</details>
### 💕感谢
![9c149e99ca747c13892be3f9fbeedf31](https://github.com/user-attachments/assets/d02d108c-1ab3-4ad1-9123-c1a98862e627)
本插件的__init__.py 中的处理函数参考了词云中的方法 ~~(其实大部分都是 Ctrl+C Ctr+V~~
### 💕 感谢
本插件的**init**.py 中的处理函数参考了词云中的方法 ~~(其实大部分都是 Ctrl+C Ctr+V~~
[nonebot-plugin-wordcloud](https://github.com/he0119/nonebot-plugin-wordcloud)
@@ -161,7 +200,6 @@ __注意__
- [x] 更好看的图片渲染
- [x] 添加一些全新的可配置项
- [x] 尝试利用 jinja2 模板引擎制作可视化图片
- [x] 私聊的查询(超级用户可以任意查询群聊的信息)一半完成
@@ -170,53 +208,10 @@ __注意__
- [x] 查询带某关键词的消息量
- [ ] 合并转发
- [x] 合并转发
待补充。.....
待补充。.....
## 📖版本日志
### 👾 题外话
<details>
<summary>点我展开</summary>
### V1.0
- 看看群里群友能有多话痨
### V1.1
- 支持频道咯!(*^_^*)
### V1.2
- 排行榜可视化
### V1.3
- 添加了一些可配置项
### V1.4
- 适配新版本的 chatrecorder, 暂时停止频道支持
### V2.0
- 理论支持全平台!暂停图片支持。
### V2.1
- 恢复图片支持。
### V2.2
- 优化代码,添加一些新的可配置项。
### V2.4
- 添加一些新的可配置项。
</details>
### 👾题外话
~~整个项目快被我写成屎山了~~

View File

@@ -1,304 +1,352 @@
from nonebot import require
require("nonebot_plugin_chatrecorder")
require("nonebot_plugin_apscheduler")
require("nonebot_plugin_htmlrender")
require("nonebot_plugin_userinfo")
require("nonebot_plugin_alconna")
require("nonebot_plugin_cesaa")
require('nonebot_plugin_chatrecorder')
require('nonebot_plugin_apscheduler')
require('nonebot_plugin_htmlrender')
require('nonebot_plugin_userinfo')
require('nonebot_plugin_alconna')
require('nonebot_plugin_uninfo')
require('nonebot_plugin_cesaa')
import re
import time as t
import nonebot_plugin_saa as saa
from typing import Union, Optional
from datetime import datetime, timedelta
from typing import Optional, Union
import nonebot_plugin_saa as saa
from arclet.alconna import ArparmaBehavior
from arclet.alconna.arparma import Arparma
from nonebot import on_command
from nonebot.log import logger
from nonebot.typing import T_State
from nonebot.params import Arg, Depends
from nonebot.adapters import Bot, Event
from nonebot.log import logger
from nonebot.params import Arg, Depends
from nonebot.plugin import PluginMetadata, inherit_supported_adapters
from nonebot.typing import T_State
from nonebot_plugin_alconna import (
Args,
Option,
Alconna,
on_alconna,
Alconna,
Args,
At,
Field,
Match,
Option,
on_alconna,
)
from nonebot_plugin_chatrecorder import get_message_records
from nonebot_plugin_session import Session, SessionIdType, extract_session
from nonebot_plugin_uninfo import Session, Uninfo, get_session
from .storage import get_cache, build_cache
from .config import Config, plugin_config
from .usage import __usage__
# from .storage import build_cache, get_cache
from .time import (
get_datetime_fromisoformat_with_timezone,
get_datetime_now_with_timezone,
parse_datetime,
get_datetime_fromisoformat_with_timezone,
get_datetime_now_with_timezone,
parse_datetime,
)
from .usage import __usage__
from .utils import (
got_rank,
msg_counter,
get_rank_image,
persist_id2user_id,
get_user_infos,
get_rank_image,
get_user_infos,
got_rank,
persist_id2user_id,
get_user_message_counts,
)
__plugin_meta__ = PluginMetadata(
name="B话排行榜",
description="调查群U的B话数量以一定的顺序排序后排序出来。",
usage=__usage__,
homepage="https://github.com/ChenXu233/nonebot_plugin_dialectlist",
type="application",
supported_adapters=inherit_supported_adapters(
"nonebot_plugin_chatrecorder", "nonebot_plugin_saa", "nonebot_plugin_alconna"
),
config=Config,
name='B话排行榜',
description='调查群U的B话数量以一定的顺序排序后排序出来。',
usage=__usage__,
homepage='https://github.com/ChenXu233/nonebot_plugin_dialectlist',
type='application',
supported_adapters=inherit_supported_adapters(
'nonebot_plugin_chatrecorder',
'nonebot_plugin_saa',
'nonebot_plugin_alconna',
),
config=Config,
)
# 抄的词云不过真的很适合B话榜。
class SameTime(ArparmaBehavior):
def operate(self, interface: Arparma):
type = interface.query("type")
time = interface.query("time")
if type is None and time:
interface.behave_fail()
def operate(self, interface: Arparma):
type = interface.query('type')
time = interface.query('time')
if type is None and time:
interface.behave_fail()
def wrapper(slot: Union[int, str], content: Optional[str], context) -> str:
if slot == "type" and content:
return content
return "" # pragma: no cover
if slot == 'type' and content:
return content
return '' # pragma: no cover
build_cache_cmd = on_command("build_cache", aliases={"重建缓存"}, block=True)
b_cmd = on_alconna(
Alconna(
'看看B话',
Args['at', [str, At], Field(completion=lambda: '请想要查询的人的QQ号')],
Option('-g|--group_id', Args['group_id?', str]),
Option('-k|--keyword', Args['keyword?', str]),
),
aliases={'kkb'},
use_cmd_start=True,
)
@build_cache_cmd.handle()
async def _build_cache(bot: Bot, event: Event):
await saa.Text("正在重建缓存,请稍等。").send(reply=True)
await build_cache()
await saa.Text("重建缓存完成。").send(reply=True)
@b_cmd.handle()
async def handle_b_cmd(
at: Match[str | At],
group_id: Match[str],
keyword: Match[str],
uninfo: Uninfo,
session: Session = Depends(get_session),
):
id = at.result
if isinstance(id, At):
id = id.target
if group_id.available:
gid = group_id.result
else:
gid = session.scene.id
if not gid:
await b_cmd.finish('请指定群号。')
_keyword = keyword.result
d = await get_user_message_counts(
keyword=_keyword,
scene_ids=[gid],
user_ids=[id],
types=['message'], # 排除机器人自己发的消息
exclude_user_ids=plugin_config.excluded_people,
)
rank = got_rank(d)
if not rank:
await b_cmd.finish(
f'该用户在群“{uninfo.scene.name}”关于“{keyword.result}”的B话数量为0。'
)
await saa.Text(
f'该用户在群“{uninfo.scene.name}”关于“{keyword.result}”的B话数量为{rank[0][1]}'
).send(reply=True)
rank_cmd = on_alconna(
Alconna(
"B话榜",
Args["type?", ["今日", "昨日", "本周", "上周", "本月", "上月", "年度", "历史"]][
"time?",
str,
],
Option("-g|--group_id", Args["group_id?", str]),
Option("-k|--keyword", Args["keyword?", str]),
behaviors=[SameTime()],
),
aliases={"废话榜"},
use_cmd_start=True,
block=True,
Alconna(
'B话榜',
Args[
'type?',
['今日', '昨日', '本周', '上周', '本月', '上月', '年度', '历史'],
][
'time?',
str,
],
Option('-g|--group_id', Args['group_id?', str]),
Option('-k|--keyword', Args['keyword?', str]),
behaviors=[SameTime()],
),
aliases={'废话榜'},
use_cmd_start=True,
block=True,
)
rank_cmd.shortcut(
r"(?P<type>今日|昨日|本周|上周|本月|上月|年度|历史)B话榜",
{
"prefix": True,
"command": "B话榜",
"wrapper": wrapper,
"args": ["{type}"],
},
r'(?P<type>今日|昨日|本周|上周|本月|上月|年度|历史)B话榜',
{
'prefix': True,
'command': 'B话榜',
'wrapper': wrapper,
'args': ['{type}'],
},
)
rank_cmd.shortcut(
r"(?P<type>今日|昨日|本周|上周|本月|上月|年度|历史)废话榜",
{
"prefix": True,
"command": "废话榜",
"wrapper": wrapper,
"args": ["{type}"],
},
r'(?P<type>今日|昨日|本周|上周|本月|上月|年度|历史)废话榜',
{
'prefix': True,
'command': '废话榜',
'wrapper': wrapper,
'args': ['{type}'],
},
)
# 这段函数完全抄的词云
@rank_cmd.handle()
async def _group_message(
state: T_State,
session: Session = Depends(extract_session),
type: Optional[str] = None,
time: Optional[str] = None,
group_id: Optional[str] = None,
keyword: Optional[str] = None,
state: T_State,
session: Session = Depends(get_session),
type: Optional[str] = None,
time: Optional[str] = None,
group_id: Optional[str] = None,
keyword: Optional[str] = None,
):
t1 = t.time()
state["t1"] = t1
dt = get_datetime_now_with_timezone()
t1 = t.time()
state['t1'] = t1
dt = get_datetime_now_with_timezone()
if not group_id:
group_id = session.id2
logger.debug(f"session id2: {group_id}")
if group_id:
state["group_id"] = group_id
if not group_id:
group_id = session.scene.id
logger.debug(f'session id2: {group_id}')
if group_id:
state['group_id'] = group_id
state["keyword"] = keyword
state['keyword'] = keyword
if not type:
await rank_cmd.finish(__plugin_meta__.usage)
if not type:
await rank_cmd.finish(__plugin_meta__.usage)
dt = get_datetime_now_with_timezone()
dt = get_datetime_now_with_timezone()
if type == "今日":
state["start"] = dt.replace(hour=0, minute=0, second=0, microsecond=0)
state["stop"] = dt
elif type == "昨日":
state["stop"] = dt.replace(hour=0, minute=0, second=0, microsecond=0)
state["start"] = state["stop"] - timedelta(days=1)
elif type == "本周":
state["start"] = dt.replace(
hour=0, minute=0, second=0, microsecond=0
) - timedelta(days=dt.weekday())
state["stop"] = dt
elif type == "上周":
state["stop"] = dt.replace(
hour=0, minute=0, second=0, microsecond=0
) - timedelta(days=dt.weekday())
state["start"] = state["stop"] - timedelta(days=7)
elif type == "本月":
state["start"] = dt.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
state["stop"] = dt
elif type == "上月":
state["stop"] = dt.replace(
day=1, hour=0, minute=0, second=0, microsecond=0
) - timedelta(microseconds=1)
state["start"] = state["stop"].replace(
day=1, hour=0, minute=0, second=0, microsecond=0
)
elif type == "年度":
state["start"] = dt.replace(
month=1, day=1, hour=0, minute=0, second=0, microsecond=0
)
state["stop"] = dt
elif type == "历史":
if time:
plaintext = time
if match := re.match(r"^(.+?)(?:~(.+))?$", plaintext):
start = match[1]
stop = match[2]
try:
state["start"] = get_datetime_fromisoformat_with_timezone(start)
if stop:
state["stop"] = get_datetime_fromisoformat_with_timezone(stop)
else:
# 如果没有指定结束日期,则认为是所给日期的当天的词云
state["start"] = state["start"].replace(
hour=0, minute=0, second=0, microsecond=0
)
state["stop"] = state["start"] + timedelta(days=1)
except ValueError:
await rank_cmd.finish("请输入正确的日期,不然我没法理解呢!")
if type == '今日':
state['start'] = dt.replace(hour=0, minute=0, second=0, microsecond=0)
state['stop'] = dt
elif type == '昨日':
state['stop'] = dt.replace(hour=0, minute=0, second=0, microsecond=0)
state['start'] = state['stop'] - timedelta(days=1)
elif type == '本周':
state['start'] = dt.replace(
hour=0, minute=0, second=0, microsecond=0
) - timedelta(days=dt.weekday())
state['stop'] = dt
elif type == '上周':
state['stop'] = dt.replace(
hour=0, minute=0, second=0, microsecond=0
) - timedelta(days=dt.weekday())
state['start'] = state['stop'] - timedelta(days=7)
elif type == '本月':
state['start'] = dt.replace(
day=1, hour=0, minute=0, second=0, microsecond=0
)
state['stop'] = dt
elif type == '上月':
state['stop'] = dt.replace(
day=1, hour=0, minute=0, second=0, microsecond=0
) - timedelta(microseconds=1)
state['start'] = state['stop'].replace(
day=1, hour=0, minute=0, second=0, microsecond=0
)
elif type == '年度':
state['start'] = dt.replace(
month=1, day=1, hour=0, minute=0, second=0, microsecond=0
)
state['stop'] = dt
elif type == '历史':
if time:
plaintext = time
if match := re.match(r'^(.+?)(?:~(.+))?$', plaintext):
start = match[1]
stop = match[2]
try:
state['start'] = get_datetime_fromisoformat_with_timezone(
start
)
if stop:
state['stop'] = (
get_datetime_fromisoformat_with_timezone(stop)
)
else:
# 如果没有指定结束日期,则认为是所给日期的当天的词云
state['start'] = state['start'].replace(
hour=0, minute=0, second=0, microsecond=0
)
state['stop'] = state['start'] + timedelta(days=1)
except ValueError:
await rank_cmd.finish(
'请输入正确的日期,不然我没法理解呢!'
)
@rank_cmd.got(
"start",
prompt="请输入你要查询的起始日期(如 2022-01-01",
parameterless=[Depends(parse_datetime("start"))],
'start',
prompt='请输入你要查询的起始日期(如 2022-01-01',
parameterless=[Depends(parse_datetime('start'))],
)
@rank_cmd.got(
"stop",
prompt="请输入你要查询的结束日期(如 2022-02-22",
parameterless=[Depends(parse_datetime("stop"))],
'stop',
prompt='请输入你要查询的结束日期(如 2022-02-22',
parameterless=[Depends(parse_datetime('stop'))],
)
@rank_cmd.got("group_id", prompt="请输入你要查询的群号。")
@rank_cmd.got('group_id', prompt='请输入你要查询的群号。')
async def handle_rank(
state: T_State,
bot: Bot,
event: Event,
session: Session = Depends(extract_session),
start: datetime = Arg(),
stop: datetime = Arg(),
state: T_State,
bot: Bot,
event: Event,
session: Session = Depends(get_session),
start: datetime = Arg(),
stop: datetime = Arg(),
):
if id := state['group_id']:
id = str(id)
logger.debug(f'group_id: {id}')
else:
id = session.scene.id
logger.debug(f'group_id: {id}')
if id := state["group_id"]:
id = str(id)
logger.debug(f"group_id: {id}")
else:
id = session.id2
logger.debug(f"group_id: {id}")
if not id:
await saa.Text('没有指定群哦').finish()
if not id:
await saa.Text("没有指定群哦").finish()
keyword = state['keyword']
keyword = state["keyword"]
t1 = t.time()
raw_rank = await get_user_message_counts(
keyword=keyword,
scene_ids=[id],
types=['message'], # 排除机器人自己发的消息
time_start=start,
time_stop=stop,
exclude_user_ids=plugin_config.excluded_people,
)
logger.debug(f'获取计数消息花费时间:{t.time() - t1}')
if plugin_config.counting_cache:
if keyword:
await saa.Text("已开启缓存~缓存不支持关键词查询哦").finish()
t1 = t.time()
raw_rank = await get_cache(start, stop, id)
logger.debug(f"获取计数消息花费时间:{t.time() - t1}")
else:
t1 = t.time()
messages = await get_message_records(
id2s=[id],
id_type=SessionIdType.GROUP,
include_bot_id=False,
include_bot_type=False,
types=["message"], # 排除机器人自己发的消息
time_start=start,
time_stop=stop,
exclude_id1s=plugin_config.excluded_people,
)
raw_rank = msg_counter(messages, keyword)
logger.debug(f"获取计数消息花费时间:{t.time() - t1}")
if not raw_rank:
await saa.Text(
'没有获取到排行榜数据哦,请确认时间范围和群号是否正确或者关键词是否存在~'
).finish()
if not raw_rank:
await saa.Text(
"没有获取到排行榜数据哦,请确认时间范围和群号是否正确或者关键词是否存在~"
).finish()
rank = got_rank(raw_rank)
ids = await persist_id2user_id([int(i[0]) for i in rank])
for i in range(len(rank)):
rank[i][0] = str(ids[i])
logger.debug(rank[i])
rank = got_rank(raw_rank)
ids = await persist_id2user_id([int(i[0]) for i in rank])
for i in range(len(rank)):
rank[i][0] = str(ids[i])
logger.debug(rank[i])
t1 = t.time()
rank2 = await get_user_infos(bot, event, rank)
logger.debug(f'获取用户信息花费时间:{t.time() - t1}')
t1 = t.time()
rank2 = await get_user_infos(bot, event, rank)
logger.debug(f"获取用户信息花费时间:{t.time() - t1}")
string: str = ''
if plugin_config.show_text_rank:
if keyword:
string += f'关于{keyword}的话痨榜结果:\n'
else:
string += '话痨榜:\n'
string: str = ""
if plugin_config.show_text_rank:
for i in rank2:
logger.debug(i.user_name)
for i in range(len(rank2)):
str_example = plugin_config.string_format.format(
index=rank2[i].user_index,
nickname=rank2[i].user_nickname,
chatdatanum=rank2[i].user_bnum,
)
string += str_example
if keyword:
string += f"关于{keyword}的话痨榜结果:\n"
else:
string += "话痨榜:\n"
msg = saa.Text(string)
for i in rank2:
logger.debug(i.user_name)
for i in range(len(rank2)):
str_example = plugin_config.string_format.format(
index=rank2[i].user_index,
nickname=rank2[i].user_nickname,
chatdatanum=rank2[i].user_bnum,
)
string += str_example
if plugin_config.visualization:
t1 = t.time()
image = await get_rank_image(rank2)
msg += saa.Image(image)
logger.debug(f'群聊消息渲染图片花费时间:{t.time() - t1}')
msg = saa.Text(string)
if plugin_config.suffix:
timecost = t.time() - state['t1']
suffix = saa.Text(plugin_config.string_suffix.format(timecost=timecost))
msg += suffix
if not msg:
await saa.Text('你把可视化都关了哪来的排行榜?').finish()
if plugin_config.visualization:
t1 = t.time()
image = await get_rank_image(rank2)
msg += saa.Image(image)
logger.debug(f"群聊消息渲染图片花费时间:{t.time() - t1}")
if plugin_config.suffix:
timecost = t.time() - state["t1"]
suffix = saa.Text(plugin_config.string_suffix.format(timecost=timecost))
msg += suffix
if not msg:
await saa.Text("你把可视化都关了哪来的排行榜?").finish()
await msg.finish(reply=True)
if plugin_config.aggregate_transmission:
await saa.AggregatedMessageFactory([msg]).finish()
else:
await msg.finish(reply=True)

View File

@@ -1,26 +1,29 @@
from pydantic import BaseModel
from typing import Optional, List
from typing import List, Optional
from nonebot import get_driver, get_plugin_config
from pydantic import BaseModel
class ScopedConfig(BaseModel):
get_num: int = 5 # 获取人数数量
font: str = "SimHei" # 字体格式
suffix: bool = True # 是否显示后缀
excluded_self: bool = True # 是否排除自己
visualization: bool = True # 是否可视化
show_text_rank:bool = True # 是否显示文本排名
counting_cache: bool = False # 计数缓存(能够提高回复速度)
excluded_people: List[str] = [] # 排除的人的QQ号
use_user_info_cache: bool = False # 是否使用用户信息缓存
timezone: Optional[str] = "Asia/Shanghai" # 时区,影响统计时间
string_suffix: str = "统计花费时间:{timecost}" # 消息格式后缀
template_path: str = "./template/rank_template.j2" # 模板路径
string_format: str = "{index}名:\n{nickname},{chatdatanum}条消息\n" # 消息格式
get_num: int = 5 # 获取人数数量
font: str = 'SimHei' # 字体格式
suffix: bool = True # 是否显示后缀
excluded_self: bool = True # 是否排除自己
visualization: bool = True # 是否可视化
show_text_rank: bool = True # 是否显示文本排名
excluded_people: List[str] = [] # 排除的人的QQ号
use_user_info_cache: bool = False # 是否使用用户信息缓存
aggregate_transmission: bool = False # 是否聚合转发消息
timezone: Optional[str] = 'Asia/Shanghai' # 时区,影响统计时间
string_suffix: str = '统计花费时间:{timecost}' # 消息格式后缀
template_path: str = './template/rank_template.j2' # 模板路径
string_format: str = (
'{index}名:\n{nickname},{chatdatanum}条消息\n' # 消息格式
)
class Config(BaseModel):
dialectlist: ScopedConfig = ScopedConfig()
dialectlist: ScopedConfig = ScopedConfig()
global_config = get_driver().config

View File

@@ -1,22 +0,0 @@
from datetime import datetime
from typing import Union
from sqlalchemy import Integer
from nonebot_plugin_orm import Model
from nonebot_plugin_userinfo import UserInfo
from sqlalchemy.orm import Mapped, mapped_column
class UserRankInfo(UserInfo):
user_bnum: int
user_proportion: float
user_nickname: str
user_index: Union[int, str]
user_avatar_bytes: bytes
class MessageCountCache(Model):
__table_args__ = {"extend_existing": True}
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
time: Mapped[datetime]
session_id: Mapped[int] = mapped_column(Integer, index=True)
session_bnum: Mapped[int] = mapped_column(Integer)

View File

@@ -0,0 +1,11 @@
from typing import Union
from nonebot_plugin_userinfo import UserInfo
class UserRankInfo(UserInfo):
user_bnum: int
user_proportion: float
user_nickname: str
user_index: Union[int, str]
user_avatar_bytes: bytes

View File

@@ -1,134 +0,0 @@
import os
import json
from datetime import datetime
from sqlalchemy import delete, or_, select
from nonebot import get_driver
from nonebot.log import logger
from nonebot.params import Depends
from nonebot.adapters import Event, Bot
from nonebot.message import event_postprocessor
from .model import MessageCountCache
from .config import plugin_config
from nonebot_plugin_localstore import get_data_file
from nonebot_plugin_chatrecorder import get_message_records
from nonebot_plugin_chatrecorder.utils import remove_timezone
from nonebot_plugin_session import extract_session, Session
from nonebot_plugin_session_orm import SessionModel, get_session_persist_id
from nonebot_plugin_orm import get_session
async def get_cache(time_start: datetime, time_stop: datetime, group_id: str):
async with get_session() as db_session:
where = [or_(SessionModel.id2 == group_id)]
statement = select(SessionModel).where(*where)
sessions = (await db_session.scalars(statement)).all()
where = [
or_(*[MessageCountCache.session_id == session.id for session in sessions])
]
statement = select(MessageCountCache).where(*where)
where.append(or_(MessageCountCache.time >= remove_timezone(time_start)))
where.append(or_(MessageCountCache.time <= remove_timezone(time_stop)))
statement = select(MessageCountCache).where(*where)
user_caches = (await db_session.scalars(statement)).all()
raw_rank = {}
for i in user_caches:
raw_rank[i.session_id] = raw_rank.get(i.session_id, 0) + i.session_bnum
return raw_rank
async def build_cache():
async with get_session() as db_session:
await db_session.execute(delete(MessageCountCache))
await db_session.commit()
logger.info("先前可能存在的缓存已清空")
messages = await get_message_records(types=["message"])
async with get_session() as db_session:
for msg in messages:
msg_session_id = msg.session_persist_id
where = [or_(MessageCountCache.session_id == msg_session_id)]
where.append(
or_(
MessageCountCache.time
== remove_timezone(
msg.time.replace(hour=1, minute=0, second=0, microsecond=0)
)
)
)
statement = select(MessageCountCache).where(*where)
user_cache = (await db_session.scalars(statement)).all()
if user_cache:
user_cache[0].session_bnum += 1
else:
user_cache = MessageCountCache(
session_id=msg.session_persist_id,
time=remove_timezone(
msg.time.replace(hour=1, minute=0, second=0, microsecond=0)
),
session_bnum=1,
)
db_session.add(user_cache)
await db_session.commit()
logger.info("缓存构建完成")
driver = get_driver()
@driver.on_startup
async def _():
if not plugin_config.counting_cache:
return
f_name = get_data_file("nonebot-plugin-dialectlist", "is-pre-cached.json")
if not os.path.exists(f_name):
with open(f_name, "w", encoding="utf-8") as f:
s = json.dumps({"is-pre-cached": False}, ensure_ascii=False, indent=4)
f.write(s)
with open(f_name, "r", encoding="utf-8") as f:
if json.load(f)["is-pre-cached"]:
return
logger.info("未检查到缓存,开始构建缓存")
with open(f_name, "w", encoding="utf-8") as f:
await build_cache()
json.dump({"is-pre-cached": True}, f, ensure_ascii=False, indent=4)
@event_postprocessor
async def _(bot: Bot, event: Event, session: Session = Depends(extract_session)):
if not plugin_config.counting_cache:
return
if not session.id2:
return
if event.get_type() != "message":
return
now = datetime.now()
now = now.replace(hour=1, minute=0, second=0, microsecond=0)
async with get_session() as db_session:
session_id = await get_session_persist_id(session)
logger.debug("session_id:" + str(session_id))
where = [or_(MessageCountCache.session_id == session_id)]
where.append(or_(MessageCountCache.time == remove_timezone(now)))
statement = select(MessageCountCache).where(*where)
user_cache = (await db_session.scalars(statement)).first()
if user_cache:
user_cache.session_bnum += 1
else:
user_cache = MessageCountCache(
session_id=session_id,
time=remove_timezone(now),
session_bnum=1,
)
db_session.add(user_cache)
await db_session.commit()
logger.debug("已计入缓存")

View File

@@ -1,13 +1,12 @@
from zoneinfo import ZoneInfo
from typing import Optional, Union
from datetime import datetime, time, tzinfo
from typing import Optional, Union
from zoneinfo import ZoneInfo
from nonebot.adapters import Message
from nonebot.params import Arg
from nonebot.typing import T_State
from nonebot.adapters import Message
from nonebot_plugin_apscheduler import scheduler
from nonebot_plugin_alconna import AlconnaMatcher
from nonebot_plugin_apscheduler import scheduler
from .config import plugin_config

View File

@@ -1,12 +1,14 @@
from inspect import cleandoc
__usage__ = cleandoc(
"""
"""
快速调用:
/今日B话榜 ————看看今天群友发了多少消息。
### 🎨一般用法
#### B话榜
-`/B话榜` ————看看有史以来(机器人存在以来)群友们发了多少消息! (好像没写)
-`/今日B话榜` ————看看今天的群友发了多少消息!
@@ -25,8 +27,14 @@ __usage__ = cleandoc(
-`/历史B话榜` ————看看历史上(机器人存在以来)的群友发了多少消息!
#### 看看B话kkb
-`/看看B话 [@某人|QQ号]` ————看看这个b人在这个b群发了多少b话
### 🚀进阶用法
#### B话榜
`/{时间类型(今日|年度)?}{B话榜|废话榜} {时间类型?} {ISO8601 格式时间 ?} {群号} {关键词}`
如:`/B话榜 历史 2024-01-01~2024-01-02 12345678 女装`
@@ -39,5 +47,14 @@ __usage__ = cleandoc(
`/昨日B话榜 -k 女装`
`/本周B话榜 -g 12345678`
#### 看看B话
`/看看B话 {@|QQ号} {群号?} {关键词?}`
以下调用方法均合法:
`/kkb 114514 1919810 ♂`
`/kkb @man -k ♂`
"""
)

View File

@@ -1,277 +1,269 @@
import os
import httpx
import asyncio
import os
import re
import unicodedata
from typing import Dict, List, Optional, Any
from typing import Dict, List, Optional
from sqlalchemy import or_, select
from sqlalchemy.sql import ColumnElement
import httpx
from sqlalchemy import select, func
from nonebot.log import logger
from nonebot.params import Depends
from nonebot.compat import model_dump
from nonebot.matcher import Matcher
from nonebot.adapters import Bot, Event
from nonebot_plugin_orm import get_session
from nonebot_plugin_session import Session, SessionLevel, extract_session
from nonebot_plugin_userinfo import get_user_info, UserInfo
from nonebot_plugin_userinfo.exception import NetworkError
from nonebot_plugin_localstore import get_cache_dir
from nonebot_plugin_htmlrender import template_to_pic
from nonebot_plugin_session_orm import SessionModel
from nonebot.compat import model_dump
from nonebot.log import logger
from nonebot.matcher import Matcher
from nonebot.params import Depends
from nonebot_plugin_chatrecorder import MessageRecord
from nonebot_plugin_htmlrender import template_to_pic
from nonebot_plugin_localstore import get_cache_dir
from nonebot_plugin_orm import get_session
from nonebot_plugin_userinfo import UserInfo, get_user_info
from nonebot_plugin_uninfo import Session
from nonebot_plugin_uninfo.model import SceneType
from nonebot_plugin_uninfo.orm import SessionModel, UserModel
from nonebot_plugin_uninfo import get_session as extract_session
from nonebot_plugin_userinfo.exception import NetworkError
from nonebot_plugin_chatrecorder.record import filter_statement
from .model import UserRankInfo
from .config import plugin_config
from .schema import UserRankInfo
cache_path = get_cache_dir("nonebot_plugin_dialectlist")
cache_path = get_cache_dir('nonebot_plugin_dialectlist')
async def ensure_group(matcher: Matcher, session: Session = Depends(extract_session)):
"""确保在群组中使用"""
if session.level not in [SessionLevel.LEVEL2, SessionLevel.LEVEL3]:
await matcher.finish("在群组中使用")
async def ensure_group(
matcher: Matcher, session: Session = Depends(extract_session)
):
"""确保在群组中使用"""
if session.scene.type not in [SceneType.GROUP, SceneType.GUILD]:
await matcher.finish('请在群组中使用!')
async def persist_id2user_id(ids: List) -> List[str]:
user_ids = []
async with get_session() as db_session:
for i in ids:
user_id = (await db_session.scalar(select(SessionModel).where(or_(*[SessionModel.id == i])))).id1 # type: ignore
user_ids.append(user_id)
return user_ids
user_ids = []
if not ids:
return user_ids
async with get_session() as db_session:
statement = (
select(UserModel.user_id)
.join(SessionModel, UserModel.id == SessionModel.user_persist_id)
.where(SessionModel.id.in_(ids))
)
result = await db_session.scalars(statement)
user_ids = result.all()
return list(user_ids)
async def user_id2persist_id(ids: List[str]) -> List[int]:
whereclause: List[ColumnElement[bool]] = []
whereclause.append(or_(*[SessionModel.id1 == id for id in ids]))
statement = (
select(SessionModel).where(*whereclause)
# .join(SessionModel, SessionModel.id == MessageRecord.session_persist_id)
)
async with get_session() as db_session:
records = (await db_session.scalars(statement)).all()
return [i.id for i in records]
def got_rank(msg_dict: Dict[int, int]) -> List[List[Any]]:
"""### 获得排行榜
Args:
msg_dict (Dict[int,int]): 要处理的字典
async def group_id2persist_id(ids: List[str]) -> List[int]:
persist_ids = []
async with get_session() as db_session:
for i in ids:
persist_id = (await db_session.scalar(select(SessionModel).where(or_(*[SessionModel.id2 == i])))).id # type: ignore
persist_ids.append(persist_id)
return persist_ids
Returns:
List[Tuple[int,int]]: 排行榜列表(已排序)
"""
rank = []
while len(rank) < plugin_config.get_num:
try:
max_key = max(msg_dict.items(), key=lambda x: x[1])
rank.append(list(max_key))
msg_dict.pop(max_key[0])
except ValueError:
logger.error(
'群内拥有聊天记录的人数不足,无法获取到长度为{}的排行榜,已将长度变化为:{}'.format(
plugin_config.get_num, len(rank)
)
)
break
async def persist_id2group_id(ids: List[str]) -> List[str]:
whereclause: List[ColumnElement[bool]] = []
whereclause.append(or_(*[SessionModel.id == id for id in ids]))
statement = (
select(SessionModel).where(*whereclause)
# .join(SessionModel, SessionModel.id == MessageRecord.session_persist_id)
)
async with get_session() as db_session:
records = (await db_session.scalars(statement)).all()
return [i.id2 for i in records]
def msg_counter(
msg_list: List[MessageRecord], keyword: Optional[str]
) -> Dict[str, int]:
"""### 计算每个人的消息量
Args:
msg_list (list[MessageRecord]): 需要处理的消息列表
Returns:
(dict[str,int]): 处理后的消息数量字典,键为用户,值为消息数量
"""
lst: Dict[str, int] = {}
msg_len = len(msg_list)
logger.info("wow , there are {} msgs to count !!!".format(msg_len))
for i in msg_list:
logger.debug(f"processing msg {i.plain_text}")
if keyword:
if keyword not in i.plain_text:
continue
try:
lst[str(i.session_persist_id)] += 1
except KeyError:
lst[str(i.session_persist_id)] = 1
logger.debug(f"finish counting, result is {lst}")
return lst
def got_rank(msg_dict: Dict[str, int]) -> List:
"""### 获得排行榜
Args:
msg_dict (Dict[str,int]): 要处理的字典
Returns:
List[Tuple[str,int]]: 排行榜列表(已排序)
"""
rank = []
while len(rank) < plugin_config.get_num:
try:
max_key = max(msg_dict.items(), key=lambda x: x[1])
rank.append(list(max_key))
msg_dict.pop(max_key[0])
except ValueError:
logger.error(
"群内拥有聊天记录的人数不足,无法获取到长度为{}的排行榜,已将长度变化为:{}".format(
plugin_config.get_num, len(rank)
)
)
break
return rank
return rank
def remove_control_characters(string: str) -> str:
"""### 将字符串中的控制符去除
"""### 将字符串中的控制符去除
Args:
string (str): 需要去除的字符串
Args:
string (str): 需要去除的字符串
Returns:
(str): 经过处理的字符串
"""
return "".join(ch for ch in string if unicodedata.category(ch)[0] != "C")
Returns:
(str): 经过处理的字符串
"""
return ''.join(ch for ch in string if unicodedata.category(ch)[0] != 'C')
async def get_rank_image(rank: List[UserRankInfo]) -> bytes:
for i in rank:
if i.user_avatar:
try:
user_avatar = i.user_avatar_bytes
except NotImplementedError:
user_avatar = open(
os.path.dirname(os.path.abspath(__file__))
+ "/template/avatar/default.jpg",
"rb",
).read()
# if not os.path.exists(cache_path / str(i.user_id)):
with open(cache_path / (str(i.user_id) + ".jpg"), "wb") as f:
f.write(user_avatar)
for i in rank:
if i.user_avatar:
try:
user_avatar = i.user_avatar_bytes
except NotImplementedError:
user_avatar = open(
os.path.dirname(os.path.abspath(__file__))
+ '/template/avatar/default.jpg',
'rb',
).read()
# if not os.path.exists(cache_path / str(i.user_id)):
with open(cache_path / (str(i.user_id) + '.jpg'), 'wb') as f:
f.write(user_avatar)
if plugin_config.template_path[:2] == "./":
path = (
os.path.dirname(os.path.abspath(__file__)) + plugin_config.template_path[1:]
)
else:
path = plugin_config.template_path
if plugin_config.template_path[:2] == './':
path = (
os.path.dirname(os.path.abspath(__file__))
+ plugin_config.template_path[1:]
)
else:
path = plugin_config.template_path
path_dir, filename = os.path.split(path)
logger.debug(
os.path.dirname(os.path.abspath(__file__)) + plugin_config.template_path[1:]
)
return await template_to_pic(
path_dir,
filename,
{
"users": rank,
"cache_path": cache_path,
"file_path": os.path.dirname(os.path.abspath(__file__)),
},
pages={"viewport": {"width": 1000, "height": 10}},
)
path_dir, filename = os.path.split(path)
logger.debug(
os.path.dirname(os.path.abspath(__file__))
+ plugin_config.template_path[1:]
)
return await template_to_pic(
path_dir,
filename,
{
'users': rank,
'cache_path': cache_path,
'file_path': os.path.dirname(os.path.abspath(__file__)),
},
pages={'viewport': {'width': 1000, 'height': 10}},
)
def _get_user_nickname(user_info: UserInfo) -> str:
user_nickname = (
user_info.user_displayname
if user_info.user_displayname
else user_info.user_name if user_info.user_name else user_info.user_id
)
return user_nickname
user_nickname = (
user_info.user_displayname
if user_info.user_displayname
else user_info.user_name
if user_info.user_name
else user_info.user_id
)
return user_nickname
async def _get_user_default_avatar() -> bytes:
img = open(
os.path.dirname(os.path.abspath(__file__)) + "/template/avatar/default.jpg",
"rb",
).read()
return img
img = open(
os.path.dirname(os.path.abspath(__file__))
+ '/template/avatar/default.jpg',
'rb',
).read()
return img
async def _get_user_avatar(user: UserInfo, client: httpx.AsyncClient) -> bytes:
if not user.user_avatar:
return await _get_user_default_avatar()
url = user.user_avatar.get_url()
for i in range(3):
try:
resp = await client.get(url, timeout=10)
resp.raise_for_status()
return resp.content
except Exception as e:
logger.warning(f"Error downloading {url}, retry {i}/3: {e}")
await asyncio.sleep(3)
raise NetworkError(f"{url} 下载失败!")
if not user.user_avatar:
return await _get_user_default_avatar()
url = user.user_avatar.get_url()
for i in range(3):
try:
resp = await client.get(url, timeout=10)
resp.raise_for_status()
return resp.content
except Exception as e:
logger.warning(f'Error downloading {url}, retry {i}/3: {e}')
await asyncio.sleep(3)
raise NetworkError(f'{url} 下载失败!')
def get_default_user_info() -> UserInfo:
user_info = UserInfo(
user_id="114514",
user_name="鬼知道这谁bot获取不了",
)
return user_info
user_info = UserInfo(
user_id='114514',
user_name='鬼知道这谁bot获取不了',
)
return user_info
async def get_user_infos(
bot: Bot,
event: Event,
rank: List,
use_cache: bool = plugin_config.use_user_info_cache,
bot: Bot,
event: Event,
rank: List,
use_cache: bool = plugin_config.use_user_info_cache,
) -> List[UserRankInfo]:
user_ids = [i[0] for i in rank]
pool = [get_user_info(bot, event, id, use_cache) for id in user_ids]
user_infos = await asyncio.gather(*pool)
user_ids = [i[0] for i in rank]
pool = [get_user_info(bot, event, id, use_cache) for id in user_ids]
user_infos = await asyncio.gather(*pool)
async with httpx.AsyncClient() as client:
pool = []
for i in user_infos:
if not i:
pool.append(_get_user_default_avatar())
continue
if i.user_avatar:
pool.append(_get_user_avatar(i, client))
user_avatars = await asyncio.gather(*pool)
async with httpx.AsyncClient() as client:
pool = []
for i in user_infos:
if not i:
pool.append(_get_user_default_avatar())
continue
if i.user_avatar:
pool.append(_get_user_avatar(i, client))
user_avatars = await asyncio.gather(*pool)
for i in user_avatars:
if not i:
user_avatars[
user_avatars.index(i)
] = await _get_user_default_avatar()
for i in user_avatars:
if not i:
user_avatars[user_avatars.index(i)] = await _get_user_default_avatar()
total = sum([i[1] for i in rank])
rank2 = []
for i in range(len(rank)):
user_info = user_infos[i]
if not user_info:
user_info = get_default_user_info()
total = sum([i[1] for i in rank])
rank2 = []
for i in range(len(rank)):
user = UserRankInfo(
**model_dump(user_info),
user_bnum=rank[i][1],
user_proportion=round(rank[i][1] / total * 100, 2),
user_index=i + 1,
user_nickname=_get_user_nickname(user_info),
user_avatar_bytes=user_avatars[i],
)
print(user.user_gender)
if user.user_gender == 'male':
user.user_gender = ''
elif user.user_gender == 'female':
user.user_gender = ''
else:
user.user_gender = '🤔'
rank2.append(user)
user_info = user_infos[i]
if not user_info:
user_info = get_default_user_info()
return rank2
user = UserRankInfo(
**model_dump(user_info),
user_bnum=rank[i][1],
user_proportion=round(rank[i][1] / total * 100, 2),
user_index=i + 1,
user_nickname=_get_user_nickname(user_info),
user_avatar_bytes=user_avatars[i],
)
print(user.user_gender)
if user.user_gender == "male":
user.user_gender = ""
elif user.user_gender == "female":
user.user_gender = ""
else:
user.user_gender = "🤔"
rank2.append(user)
return rank2
async def get_user_message_counts(
keyword: Optional[str] = None, **kwargs
) -> Dict[int, int]:
"""获取每个用户的消息数量(直接在数据库层面统计)
参数:
* ``keyword``: 可选,关键词,只统计包含该关键词的消息
* ``**kwargs``: 筛选参数,具体查看 `filter_statement` 中的定义
返回值:
* ``Dict[str, int]``: 键为user_persist_id值为该用户的消息数量
"""
whereclause = filter_statement(**kwargs)
# 如果提供了关键词,添加关键词过滤条件
if keyword:
# 构造LIKE条件类似于msg_counter函数中的正则匹配
# 根据数据库类型不同可能需要调整LIKE的语法
keyword_condition = MessageRecord.plain_text.ilike(f'%{keyword}%')
whereclause.append(keyword_condition)
# 使用SQL的GROUP BY和COUNT进行分组统计
statement = (
select(
SessionModel.user_persist_id,
func.count(MessageRecord.id).label('message_count'),
)
.select_from(MessageRecord)
.join(SessionModel, SessionModel.id == MessageRecord.session_persist_id)
.where(*whereclause)
.group_by(SessionModel.user_persist_id)
)
async with get_session() as db_session:
result = await db_session.execute(statement)
# 转换为字典格式返回
return {user_id: count for user_id, count in result.all()}

1959
pdm.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,10 @@
[project]
name = "nonebot-plugin-dialectlist"
version = "2.4.5"
version = "3.0.1"
description = "看看你群群友有多能说"
authors = [
{name = "Chen_Xu233", email = "woyerpa@outlook.com"},
]
authors = [{ name = "Chen_Xu233", email = "woyerpa@outlook.com" }]
dependencies = [
"nonebot-plugin-chatrecorder>=0.6.0",
"requests>=2.32.3",
"nonebot-plugin-chatrecorder>=0.7.0",
"nonebot-plugin-orm[default]",
"nonebot-plugin-apscheduler>=0.4.0",
"nonebot-plugin-alconna>=0.50.2",
@@ -15,25 +12,23 @@ dependencies = [
"nonebot-plugin-userinfo>=0.2.6",
"nonebot-plugin-htmlrender>=0.3.3",
"nonebot2>=2.3.2",
"pillow>=10.4.0",
"pillow>=11.3.0",
"nonebot-plugin-uninfo>=0.1.1",
]
requires-python = ">=3.9,<3.13"
requires-python = ">=3.9,<4.0"
readme = "README.md"
license = {text = "MIT"}
license = { text = "MIT" }
[project.optional-dependencies]
dev = [
"ruff>=0.5.5",
"setuptools>=71.1.0",
"twine>=5.1.0",
"nb-cli>=0.7.6",
"setuptools>=80.9.0",
"twine>=6.2.0",
"nb-cli>=1.4.2",
"py-spy>=0.3.14",
]
Test = [
"nonebot-adapter-onebot>=2.4.4",
]
Test = ["nonebot-adapter-onebot>=2.4.4"]
[tool.pdm]
distribution = true