nonebot-plugin-msg-rank-card 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nonebot_plugin_msg_rank_card-0.1.0/.github/workflows/pypi-publish.yml +39 -0
- nonebot_plugin_msg_rank_card-0.1.0/.gitignore +176 -0
- nonebot_plugin_msg_rank_card-0.1.0/LICENSE +21 -0
- nonebot_plugin_msg_rank_card-0.1.0/PKG-INFO +132 -0
- nonebot_plugin_msg_rank_card-0.1.0/README.md +104 -0
- nonebot_plugin_msg_rank_card-0.1.0/nonebot_plugin_msg_rank_card/__init__.py +85 -0
- nonebot_plugin_msg_rank_card-0.1.0/nonebot_plugin_msg_rank_card/card_generator.py +279 -0
- nonebot_plugin_msg_rank_card-0.1.0/nonebot_plugin_msg_rank_card/config.py +17 -0
- nonebot_plugin_msg_rank_card-0.1.0/nonebot_plugin_msg_rank_card/data_source.py +127 -0
- nonebot_plugin_msg_rank_card-0.1.0/pyproject.toml +58 -0
- nonebot_plugin_msg_rank_card-0.1.0/resources/bg/bg1.jpg +0 -0
- nonebot_plugin_msg_rank_card-0.1.0/resources/bg/bg2.jpg +0 -0
- nonebot_plugin_msg_rank_card-0.1.0/resources/bg/bg3.jpg +0 -0
- nonebot_plugin_msg_rank_card-0.1.0/resources/frame/1.png +0 -0
- nonebot_plugin_msg_rank_card-0.1.0/resources/frame/10.png +0 -0
- nonebot_plugin_msg_rank_card-0.1.0/resources/frame/2.png +0 -0
- nonebot_plugin_msg_rank_card-0.1.0/resources/frame/3.png +0 -0
- nonebot_plugin_msg_rank_card-0.1.0/resources/frame/4.png +0 -0
- nonebot_plugin_msg_rank_card-0.1.0/resources/frame/5.png +0 -0
- nonebot_plugin_msg_rank_card-0.1.0/resources/frame/6.png +0 -0
- nonebot_plugin_msg_rank_card-0.1.0/resources/frame/7.png +0 -0
- nonebot_plugin_msg_rank_card-0.1.0/resources/frame/8.png +0 -0
- nonebot_plugin_msg_rank_card-0.1.0/resources/frame/9.png +0 -0
- nonebot_plugin_msg_rank_card-0.1.0/resources/ttf//347/224/234/350/214/266/346/231/232/351/243/216/346/237/224/350/275/257/350/256/260.ttf +0 -0
- nonebot_plugin_msg_rank_card-0.1.0/tests/test_load.py +10 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- '*'
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
pypi-publish:
|
|
11
|
+
name: Upload release to PyPI
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
permissions:
|
|
14
|
+
id-token: write
|
|
15
|
+
contents: read
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- name: Set up Python
|
|
19
|
+
uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.x"
|
|
22
|
+
- name: Install pypa/build
|
|
23
|
+
run: >-
|
|
24
|
+
python -m
|
|
25
|
+
pip install
|
|
26
|
+
build
|
|
27
|
+
--user
|
|
28
|
+
- name: Build a binary wheel and a source tarball
|
|
29
|
+
run: >-
|
|
30
|
+
python -m
|
|
31
|
+
build
|
|
32
|
+
--sdist
|
|
33
|
+
--wheel
|
|
34
|
+
--outdir dist/
|
|
35
|
+
.
|
|
36
|
+
- name: Publish distribution to PyPI
|
|
37
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
38
|
+
with:
|
|
39
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# poetry
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
102
|
+
#poetry.lock
|
|
103
|
+
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
|
|
104
|
+
poetry.toml
|
|
105
|
+
|
|
106
|
+
# pdm
|
|
107
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
108
|
+
#pdm.lock
|
|
109
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
110
|
+
# in version control.
|
|
111
|
+
# https://pdm.fming.dev/#use-with-ide
|
|
112
|
+
.pdm-python
|
|
113
|
+
|
|
114
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
115
|
+
__pypackages__/
|
|
116
|
+
|
|
117
|
+
# Celery stuff
|
|
118
|
+
celerybeat-schedule
|
|
119
|
+
celerybeat.pid
|
|
120
|
+
|
|
121
|
+
# SageMath parsed files
|
|
122
|
+
*.sage.py
|
|
123
|
+
|
|
124
|
+
# Environments
|
|
125
|
+
.env
|
|
126
|
+
.venv
|
|
127
|
+
env/
|
|
128
|
+
venv/
|
|
129
|
+
ENV/
|
|
130
|
+
env.bak/
|
|
131
|
+
venv.bak/
|
|
132
|
+
|
|
133
|
+
# Spyder project settings
|
|
134
|
+
.spyderproject
|
|
135
|
+
.spyproject
|
|
136
|
+
|
|
137
|
+
# Rope project settings
|
|
138
|
+
.ropeproject
|
|
139
|
+
|
|
140
|
+
# mkdocs documentation
|
|
141
|
+
/site
|
|
142
|
+
|
|
143
|
+
# mypy
|
|
144
|
+
.mypy_cache/
|
|
145
|
+
.dmypy.json
|
|
146
|
+
dmypy.json
|
|
147
|
+
|
|
148
|
+
# Pyre type checker
|
|
149
|
+
.pyre/
|
|
150
|
+
|
|
151
|
+
# pytype static type analyzer
|
|
152
|
+
.pytype/
|
|
153
|
+
|
|
154
|
+
# Cython debug symbols
|
|
155
|
+
cython_debug/
|
|
156
|
+
|
|
157
|
+
# ruff
|
|
158
|
+
.ruff_cache/
|
|
159
|
+
|
|
160
|
+
# LSP config files
|
|
161
|
+
pyrightconfig.json
|
|
162
|
+
|
|
163
|
+
# PyCharm
|
|
164
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
165
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
166
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
167
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
168
|
+
#.idea/
|
|
169
|
+
|
|
170
|
+
# VisualStudioCode
|
|
171
|
+
.vscode/*
|
|
172
|
+
!.vscode/settings.json
|
|
173
|
+
!.vscode/tasks.json
|
|
174
|
+
!.vscode/launch.json
|
|
175
|
+
!.vscode/extensions.json
|
|
176
|
+
!.vscode/*.code-snippets
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 BigCherryBalls
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nonebot-plugin-msg-rank-card
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 每日水群排行榜图片生成插件,记录群聊消息并生成精美排行榜
|
|
5
|
+
Project-URL: Homepage, https://github.com/TonyLiangP2010405/nonebot-plugin-msg-rank-card
|
|
6
|
+
Project-URL: Repository, https://github.com/TonyLiangP2010405/nonebot-plugin-msg-rank-card
|
|
7
|
+
Author: TonyLiangP2010405
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: card,message,nonebot,nonebot2,plugin,rank
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Requires-Dist: httpx>=0.24.0
|
|
20
|
+
Requires-Dist: nonebot-adapter-onebot>=2.4.0
|
|
21
|
+
Requires-Dist: nonebot2>=2.2.0
|
|
22
|
+
Requires-Dist: pillow>=10.0.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: nonebug>=0.3; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
<div align="center">
|
|
30
|
+
<a href="https://v2.nonebot.dev/store"><img src="https://github.com/A-kirami/nonebot-plugin-template/blob/resources/nbp_logo.png" width="180" height="180" alt="NoneBotPluginLogo"></a>
|
|
31
|
+
<br>
|
|
32
|
+
<p><img src="https://github.com/A-kirami/nonebot-plugin-template/blob/resources/NoneBotPlugin.svg" width="240" alt="NoneBotPluginText"></p>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div align="center">
|
|
36
|
+
|
|
37
|
+
# nonebot-plugin-msg-rank-card
|
|
38
|
+
|
|
39
|
+
_✨ 每日水群排行榜图片生成插件 ✨_
|
|
40
|
+
|
|
41
|
+
<a href="./LICENSE">
|
|
42
|
+
<img src="https://img.shields.io/github/license/TonyLiangP2010405/nonebot-plugin-msg-rank-card.svg" alt="license">
|
|
43
|
+
</a>
|
|
44
|
+
<a href="https://pypi.python.org/pypi/nonebot-plugin-msg-rank-card">
|
|
45
|
+
<img src="https://img.shields.io/pypi/v/nonebot-plugin-msg-rank-card.svg" alt="pypi">
|
|
46
|
+
</a>
|
|
47
|
+
<img src="https://img.shields.io/badge/python-3.9+-blue.svg" alt="python">
|
|
48
|
+
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
## 📖 介绍
|
|
52
|
+
|
|
53
|
+
本插件基于 [MsgRankCard](https://github.com/TonyLiangP2010405/MsgRankCard) 项目改写,是一个 NoneBot2 插件,用于记录群聊消息并生成精美的每日水群排行榜图片。
|
|
54
|
+
|
|
55
|
+
功能特点:
|
|
56
|
+
- 📝 自动记录群聊消息数量和在线时长
|
|
57
|
+
- 🎨 生成精美的排行榜图片,支持自定义背景、边框和字体
|
|
58
|
+
- 🖼️ 自动获取用户 QQ 头像
|
|
59
|
+
- 🏆 支持查看今日水群排行榜
|
|
60
|
+
- 🧹 支持自动清理过期数据
|
|
61
|
+
|
|
62
|
+
## 💿 安装
|
|
63
|
+
|
|
64
|
+
### 使用 nb-cli 安装
|
|
65
|
+
|
|
66
|
+
在 nonebot2 项目的根目录下打开命令行,输入以下指令即可安装
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
nb plugin install nonebot-plugin-msg-rank-card
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 使用包管理器安装
|
|
73
|
+
|
|
74
|
+
在 nonebot2 项目的插件目录下,打开命令行,根据你使用的包管理器,输入相应的安装命令
|
|
75
|
+
|
|
76
|
+
<details>
|
|
77
|
+
<summary>pip</summary>
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pip install nonebot-plugin-msg-rank-card
|
|
81
|
+
```
|
|
82
|
+
</details>
|
|
83
|
+
|
|
84
|
+
<details>
|
|
85
|
+
<summary>poetry</summary>
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
poetry add nonebot-plugin-msg-rank-card
|
|
89
|
+
```
|
|
90
|
+
</details>
|
|
91
|
+
|
|
92
|
+
打开 nonebot2 项目根目录下的 `pyproject.toml` 文件,在 `[tool.nonebot]` 部分追加写入
|
|
93
|
+
|
|
94
|
+
```toml
|
|
95
|
+
plugins = ["nonebot_plugin_msg_rank_card"]
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## ⚙️ 配置
|
|
99
|
+
|
|
100
|
+
在 nonebot2 项目的 `.env` 文件中添加下表中的配置
|
|
101
|
+
|
|
102
|
+
| 配置项 | 必填 | 默认值 | 说明 |
|
|
103
|
+
|:-----:|:----:|:----:|:----:|
|
|
104
|
+
| msg_rank_data_path | 否 | data/msg_rank | 数据存储路径 |
|
|
105
|
+
| msg_rank_resource_path | 否 | 插件目录/resources | 资源文件路径(背景图、边框、字体) |
|
|
106
|
+
| msg_rank_max_count | 否 | 10 | 排行榜最大显示人数 |
|
|
107
|
+
| msg_rank_auto_clean | 否 | true | 是否自动清理过期数据 |
|
|
108
|
+
| msg_rank_title | 否 | 今日水群排行榜 | 排行榜标题 |
|
|
109
|
+
|
|
110
|
+
## 🎉 使用
|
|
111
|
+
|
|
112
|
+
### 指令表
|
|
113
|
+
|
|
114
|
+
| 指令 | 权限 | 需要@ | 范围 | 说明 |
|
|
115
|
+
|:-----:|:----:|:----:|:----:|:----:|
|
|
116
|
+
| 水群榜 / msgrank | 群员 | 否 | 群聊 | 查看今日水群排行榜 |
|
|
117
|
+
| 清理水群数据 / clean_msgrank | 群主/管理员 | 否 | 群聊 | 清理过期数据 |
|
|
118
|
+
|
|
119
|
+
### 效果图
|
|
120
|
+
|
|
121
|
+
插件会自动记录群聊消息,发送 `/水群榜` 即可生成精美排行榜图片。
|
|
122
|
+
|
|
123
|
+
### 自定义资源
|
|
124
|
+
|
|
125
|
+
你可以替换插件目录下 `resources` 文件夹中的资源文件:
|
|
126
|
+
- `bg/` - 背景图片(随机选择)
|
|
127
|
+
- `frame/` - 排名边框装饰
|
|
128
|
+
- `ttf/` - 自定义字体(第一个 ttf 文件会被使用)
|
|
129
|
+
|
|
130
|
+
## 📄 许可证
|
|
131
|
+
|
|
132
|
+
本项目基于 [MsgRankCard](https://github.com/TonyLiangP2010405/MsgRankCard) 改写,采用 MIT 许可证。
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<a href="https://v2.nonebot.dev/store"><img src="https://github.com/A-kirami/nonebot-plugin-template/blob/resources/nbp_logo.png" width="180" height="180" alt="NoneBotPluginLogo"></a>
|
|
3
|
+
<br>
|
|
4
|
+
<p><img src="https://github.com/A-kirami/nonebot-plugin-template/blob/resources/NoneBotPlugin.svg" width="240" alt="NoneBotPluginText"></p>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<div align="center">
|
|
8
|
+
|
|
9
|
+
# nonebot-plugin-msg-rank-card
|
|
10
|
+
|
|
11
|
+
_✨ 每日水群排行榜图片生成插件 ✨_
|
|
12
|
+
|
|
13
|
+
<a href="./LICENSE">
|
|
14
|
+
<img src="https://img.shields.io/github/license/TonyLiangP2010405/nonebot-plugin-msg-rank-card.svg" alt="license">
|
|
15
|
+
</a>
|
|
16
|
+
<a href="https://pypi.python.org/pypi/nonebot-plugin-msg-rank-card">
|
|
17
|
+
<img src="https://img.shields.io/pypi/v/nonebot-plugin-msg-rank-card.svg" alt="pypi">
|
|
18
|
+
</a>
|
|
19
|
+
<img src="https://img.shields.io/badge/python-3.9+-blue.svg" alt="python">
|
|
20
|
+
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
## 📖 介绍
|
|
24
|
+
|
|
25
|
+
本插件基于 [MsgRankCard](https://github.com/TonyLiangP2010405/MsgRankCard) 项目改写,是一个 NoneBot2 插件,用于记录群聊消息并生成精美的每日水群排行榜图片。
|
|
26
|
+
|
|
27
|
+
功能特点:
|
|
28
|
+
- 📝 自动记录群聊消息数量和在线时长
|
|
29
|
+
- 🎨 生成精美的排行榜图片,支持自定义背景、边框和字体
|
|
30
|
+
- 🖼️ 自动获取用户 QQ 头像
|
|
31
|
+
- 🏆 支持查看今日水群排行榜
|
|
32
|
+
- 🧹 支持自动清理过期数据
|
|
33
|
+
|
|
34
|
+
## 💿 安装
|
|
35
|
+
|
|
36
|
+
### 使用 nb-cli 安装
|
|
37
|
+
|
|
38
|
+
在 nonebot2 项目的根目录下打开命令行,输入以下指令即可安装
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
nb plugin install nonebot-plugin-msg-rank-card
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 使用包管理器安装
|
|
45
|
+
|
|
46
|
+
在 nonebot2 项目的插件目录下,打开命令行,根据你使用的包管理器,输入相应的安装命令
|
|
47
|
+
|
|
48
|
+
<details>
|
|
49
|
+
<summary>pip</summary>
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install nonebot-plugin-msg-rank-card
|
|
53
|
+
```
|
|
54
|
+
</details>
|
|
55
|
+
|
|
56
|
+
<details>
|
|
57
|
+
<summary>poetry</summary>
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
poetry add nonebot-plugin-msg-rank-card
|
|
61
|
+
```
|
|
62
|
+
</details>
|
|
63
|
+
|
|
64
|
+
打开 nonebot2 项目根目录下的 `pyproject.toml` 文件,在 `[tool.nonebot]` 部分追加写入
|
|
65
|
+
|
|
66
|
+
```toml
|
|
67
|
+
plugins = ["nonebot_plugin_msg_rank_card"]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## ⚙️ 配置
|
|
71
|
+
|
|
72
|
+
在 nonebot2 项目的 `.env` 文件中添加下表中的配置
|
|
73
|
+
|
|
74
|
+
| 配置项 | 必填 | 默认值 | 说明 |
|
|
75
|
+
|:-----:|:----:|:----:|:----:|
|
|
76
|
+
| msg_rank_data_path | 否 | data/msg_rank | 数据存储路径 |
|
|
77
|
+
| msg_rank_resource_path | 否 | 插件目录/resources | 资源文件路径(背景图、边框、字体) |
|
|
78
|
+
| msg_rank_max_count | 否 | 10 | 排行榜最大显示人数 |
|
|
79
|
+
| msg_rank_auto_clean | 否 | true | 是否自动清理过期数据 |
|
|
80
|
+
| msg_rank_title | 否 | 今日水群排行榜 | 排行榜标题 |
|
|
81
|
+
|
|
82
|
+
## 🎉 使用
|
|
83
|
+
|
|
84
|
+
### 指令表
|
|
85
|
+
|
|
86
|
+
| 指令 | 权限 | 需要@ | 范围 | 说明 |
|
|
87
|
+
|:-----:|:----:|:----:|:----:|:----:|
|
|
88
|
+
| 水群榜 / msgrank | 群员 | 否 | 群聊 | 查看今日水群排行榜 |
|
|
89
|
+
| 清理水群数据 / clean_msgrank | 群主/管理员 | 否 | 群聊 | 清理过期数据 |
|
|
90
|
+
|
|
91
|
+
### 效果图
|
|
92
|
+
|
|
93
|
+
插件会自动记录群聊消息,发送 `/水群榜` 即可生成精美排行榜图片。
|
|
94
|
+
|
|
95
|
+
### 自定义资源
|
|
96
|
+
|
|
97
|
+
你可以替换插件目录下 `resources` 文件夹中的资源文件:
|
|
98
|
+
- `bg/` - 背景图片(随机选择)
|
|
99
|
+
- `frame/` - 排名边框装饰
|
|
100
|
+
- `ttf/` - 自定义字体(第一个 ttf 文件会被使用)
|
|
101
|
+
|
|
102
|
+
## 📄 许可证
|
|
103
|
+
|
|
104
|
+
本项目基于 [MsgRankCard](https://github.com/TonyLiangP2010405/MsgRankCard) 改写,采用 MIT 许可证。
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from nonebot import on_command, on_message
|
|
2
|
+
from nonebot.adapters import Bot, Message
|
|
3
|
+
from nonebot.adapters.onebot.v11 import GroupMessageEvent, MessageSegment
|
|
4
|
+
from nonebot.params import CommandArg
|
|
5
|
+
from nonebot.plugin import PluginMetadata
|
|
6
|
+
|
|
7
|
+
from .card_generator import MemberInfo, RankCardGenerator, download_avatar
|
|
8
|
+
from .config import Config
|
|
9
|
+
from .data_source import clean_old_data, get_rank_data, record_message
|
|
10
|
+
|
|
11
|
+
__plugin_meta__ = PluginMetadata(
|
|
12
|
+
name="每日水群排行榜",
|
|
13
|
+
description="记录群聊消息,生成精美的每日水群排行榜图片",
|
|
14
|
+
usage="发送 /水群榜 或 /msgrank 查看今日排行榜",
|
|
15
|
+
type="application",
|
|
16
|
+
homepage="https://github.com/TonyLiangP2010405/nonebot-plugin-msg-rank-card",
|
|
17
|
+
config=Config,
|
|
18
|
+
supported_adapters={"~onebot.v11"},
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# 消息监听:记录群聊消息
|
|
22
|
+
msg_handler = on_message(priority=99, block=False)
|
|
23
|
+
|
|
24
|
+
# 排行榜命令
|
|
25
|
+
rank_cmd = on_command("水群榜", aliases={"msgrank", "msg_rank", "今日水群榜"}, priority=10, block=True)
|
|
26
|
+
|
|
27
|
+
# 清理命令(仅管理员可用)
|
|
28
|
+
clean_cmd = on_command("清理水群数据", aliases={"clean_msgrank"}, priority=10, block=True)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@msg_handler.handle()
|
|
32
|
+
async def handle_message(bot: Bot, event: GroupMessageEvent):
|
|
33
|
+
"""记录群聊消息"""
|
|
34
|
+
group_id = str(event.group_id)
|
|
35
|
+
user_id = str(event.user_id)
|
|
36
|
+
user_name = event.sender.nickname or "未知用户"
|
|
37
|
+
|
|
38
|
+
# 记录消息
|
|
39
|
+
record_message(group_id, user_id, user_name, msg_length=1)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@rank_cmd.handle()
|
|
43
|
+
async def handle_rank(bot: Bot, event: GroupMessageEvent, args: Message = CommandArg()):
|
|
44
|
+
"""处理排行榜命令"""
|
|
45
|
+
group_id = str(event.group_id)
|
|
46
|
+
|
|
47
|
+
# 获取排行榜数据
|
|
48
|
+
rank_data = get_rank_data(group_id)
|
|
49
|
+
|
|
50
|
+
if not rank_data:
|
|
51
|
+
await rank_cmd.finish("今天还没有人水群呢~")
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
# 构建成员信息列表
|
|
55
|
+
members = []
|
|
56
|
+
for info in rank_data:
|
|
57
|
+
# 下载头像
|
|
58
|
+
avatar = await download_avatar(info["user_id"])
|
|
59
|
+
member = MemberInfo(
|
|
60
|
+
name=info["name"],
|
|
61
|
+
msg_count=info["msg_count"],
|
|
62
|
+
time_seconds=info["time_seconds"],
|
|
63
|
+
head_pic=avatar,
|
|
64
|
+
)
|
|
65
|
+
members.append(member)
|
|
66
|
+
|
|
67
|
+
# 生成图片
|
|
68
|
+
generator = RankCardGenerator()
|
|
69
|
+
image_bytes = generator.generate_card_bytes(members)
|
|
70
|
+
|
|
71
|
+
# 发送图片
|
|
72
|
+
await rank_cmd.finish(MessageSegment.image(image_bytes))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@clean_cmd.handle()
|
|
76
|
+
async def handle_clean(bot: Bot, event: GroupMessageEvent):
|
|
77
|
+
"""清理过期数据"""
|
|
78
|
+
# 简单权限检查:只有群主和管理员可以清理
|
|
79
|
+
sender_role = event.sender.role if hasattr(event.sender, "role") else "member"
|
|
80
|
+
if sender_role not in ["owner", "admin"]:
|
|
81
|
+
await clean_cmd.finish("只有群主或管理员才能清理数据哦~")
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
clean_old_data()
|
|
85
|
+
await clean_cmd.finish("已清理过期数据~")
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import random
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from nonebot import get_driver
|
|
9
|
+
from nonebot.log import logger
|
|
10
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
11
|
+
|
|
12
|
+
from .config import Config
|
|
13
|
+
|
|
14
|
+
# 资源目录(默认值,会在首次使用时根据配置更新)
|
|
15
|
+
_resource_dir: Path = Path(__file__).parent.parent / "resources"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _get_resource_dir() -> Path:
|
|
19
|
+
"""获取资源目录(延迟加载)"""
|
|
20
|
+
global _resource_dir
|
|
21
|
+
try:
|
|
22
|
+
config = Config.parse_obj(get_driver().config.dict())
|
|
23
|
+
if config.msg_rank_resource_path:
|
|
24
|
+
_resource_dir = Path(config.msg_rank_resource_path)
|
|
25
|
+
except Exception:
|
|
26
|
+
pass
|
|
27
|
+
return _resource_dir
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _get_bg_dir() -> Path:
|
|
31
|
+
return _get_resource_dir() / "bg"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _get_frame_dir() -> Path:
|
|
35
|
+
return _get_resource_dir() / "frame"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _get_ttf_dir() -> Path:
|
|
39
|
+
return _get_resource_dir() / "ttf"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# 画布尺寸
|
|
43
|
+
BG_WIDTH = 672
|
|
44
|
+
BG_HEIGHT = 1080
|
|
45
|
+
CARD_WIDTH = 640
|
|
46
|
+
CARD_HEIGHT = 80
|
|
47
|
+
HEAD_WIDTH = 60
|
|
48
|
+
HEAD_HEIGHT = 60
|
|
49
|
+
HEAD_X = 20
|
|
50
|
+
HEAD_Y = 10
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class MemberInfo:
|
|
54
|
+
"""成员信息"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, name: str, msg_count: int, time_seconds: int, head_pic: Optional[Image.Image] = None):
|
|
57
|
+
self.name = name
|
|
58
|
+
self.msg_count = msg_count
|
|
59
|
+
self.time_seconds = time_seconds
|
|
60
|
+
self.head_pic = head_pic
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class RankCardGenerator:
|
|
64
|
+
"""排行榜图片生成器"""
|
|
65
|
+
|
|
66
|
+
_instance = None
|
|
67
|
+
|
|
68
|
+
def __new__(cls):
|
|
69
|
+
if cls._instance is None:
|
|
70
|
+
cls._instance = super().__new__(cls)
|
|
71
|
+
cls._instance._initialized = False
|
|
72
|
+
return cls._instance
|
|
73
|
+
|
|
74
|
+
def __init__(self):
|
|
75
|
+
if self._initialized:
|
|
76
|
+
return
|
|
77
|
+
self._initialized = True
|
|
78
|
+
|
|
79
|
+
self.bg_files = self._get_files(_get_bg_dir(), "bg")
|
|
80
|
+
self.frame_files = self._get_files(_get_frame_dir(), "frame")
|
|
81
|
+
self.font_file = self._get_first_ttf()
|
|
82
|
+
|
|
83
|
+
def _get_files(self, dir_path: Path, name_filter: str) -> list[Path]:
|
|
84
|
+
"""获取目录下的文件"""
|
|
85
|
+
if not dir_path.exists() or not dir_path.is_dir():
|
|
86
|
+
return []
|
|
87
|
+
return [f for f in dir_path.iterdir() if f.is_file() and name_filter in f.name]
|
|
88
|
+
|
|
89
|
+
def _get_first_ttf(self) -> Optional[Path]:
|
|
90
|
+
"""获取第一个 ttf 字体文件"""
|
|
91
|
+
ttf_dir = _get_ttf_dir()
|
|
92
|
+
if not ttf_dir.exists() or not ttf_dir.is_dir():
|
|
93
|
+
return None
|
|
94
|
+
ttfs = [f for f in ttf_dir.iterdir() if f.is_file() and f.suffix.lower() == ".ttf"]
|
|
95
|
+
return ttfs[0] if ttfs else None
|
|
96
|
+
|
|
97
|
+
def _load_font(self, size: int) -> ImageFont.FreeTypeFont:
|
|
98
|
+
"""加载字体"""
|
|
99
|
+
if self.font_file:
|
|
100
|
+
try:
|
|
101
|
+
return ImageFont.truetype(str(self.font_file), size)
|
|
102
|
+
except Exception:
|
|
103
|
+
pass
|
|
104
|
+
# 回退到默认字体
|
|
105
|
+
try:
|
|
106
|
+
return ImageFont.truetype("/System/Library/Fonts/PingFang.ttc", size)
|
|
107
|
+
except Exception:
|
|
108
|
+
try:
|
|
109
|
+
return ImageFont.truetype("/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", size)
|
|
110
|
+
except Exception:
|
|
111
|
+
return ImageFont.load_default()
|
|
112
|
+
|
|
113
|
+
def _format_time(self, seconds: int) -> str:
|
|
114
|
+
"""格式化时间"""
|
|
115
|
+
seconds = max(0, seconds)
|
|
116
|
+
hours = seconds // 3600
|
|
117
|
+
minutes = (seconds % 3600) // 60
|
|
118
|
+
remaining = seconds % 60
|
|
119
|
+
|
|
120
|
+
parts = []
|
|
121
|
+
if hours > 0:
|
|
122
|
+
parts.append(f"{hours}时")
|
|
123
|
+
if minutes > 0:
|
|
124
|
+
parts.append(f"{minutes}分")
|
|
125
|
+
if remaining > 0 or not parts:
|
|
126
|
+
parts.append(f"{remaining}秒")
|
|
127
|
+
|
|
128
|
+
return "".join(parts)
|
|
129
|
+
|
|
130
|
+
def _format_bg_image(self, image: Image.Image) -> Image.Image:
|
|
131
|
+
"""格式化背景图:缩放并裁剪到指定尺寸,添加半透明遮罩"""
|
|
132
|
+
# 计算缩放比例(覆盖模式)
|
|
133
|
+
scale = max(BG_WIDTH / image.width, BG_HEIGHT / image.height)
|
|
134
|
+
new_width = int(image.width * scale)
|
|
135
|
+
new_height = int(image.height * scale)
|
|
136
|
+
|
|
137
|
+
# 缩放
|
|
138
|
+
resized = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
|
139
|
+
|
|
140
|
+
# 居中裁剪
|
|
141
|
+
left = (new_width - BG_WIDTH) // 2
|
|
142
|
+
top = (new_height - BG_HEIGHT) // 2
|
|
143
|
+
cropped = resized.crop((left, top, left + BG_WIDTH, top + BG_HEIGHT))
|
|
144
|
+
|
|
145
|
+
# 添加白色半透明遮罩
|
|
146
|
+
overlay = Image.new("RGBA", (BG_WIDTH, BG_HEIGHT), (255, 255, 255, 128))
|
|
147
|
+
result = Image.alpha_composite(cropped.convert("RGBA"), overlay)
|
|
148
|
+
|
|
149
|
+
return result
|
|
150
|
+
|
|
151
|
+
def _create_head_mask(self) -> Image.Image:
|
|
152
|
+
"""创建圆形头像遮罩"""
|
|
153
|
+
mask = Image.new("L", (HEAD_WIDTH, HEAD_HEIGHT), 0)
|
|
154
|
+
draw = ImageDraw.Draw(mask)
|
|
155
|
+
draw.ellipse((0, 0, HEAD_WIDTH, HEAD_HEIGHT), fill=255)
|
|
156
|
+
return mask
|
|
157
|
+
|
|
158
|
+
def _create_gradient_mask(self, width: int, height: int) -> Image.Image:
|
|
159
|
+
"""创建渐变遮罩(从左到右渐变透明)"""
|
|
160
|
+
mask = Image.new("L", (width, height), 0)
|
|
161
|
+
for x in range(width):
|
|
162
|
+
alpha = int(255 * (1 - x / width))
|
|
163
|
+
for y in range(height):
|
|
164
|
+
mask.putpixel((x, y), alpha)
|
|
165
|
+
return mask
|
|
166
|
+
|
|
167
|
+
def _draw_member_card(self, member: MemberInfo, frame_path: Optional[Path], font: ImageFont.FreeTypeFont) -> Image.Image:
|
|
168
|
+
"""绘制单个成员卡片"""
|
|
169
|
+
# 创建卡片背景
|
|
170
|
+
card = Image.new("RGBA", (CARD_WIDTH, CARD_HEIGHT), (255, 255, 255, 255))
|
|
171
|
+
draw = ImageDraw.Draw(card)
|
|
172
|
+
|
|
173
|
+
# 加载边框
|
|
174
|
+
frame = None
|
|
175
|
+
if frame_path and frame_path.exists():
|
|
176
|
+
with contextlib.suppress(Exception):
|
|
177
|
+
frame = Image.open(frame_path).convert("RGBA")
|
|
178
|
+
|
|
179
|
+
# 处理头像
|
|
180
|
+
if member.head_pic:
|
|
181
|
+
head = member.head_pic.copy()
|
|
182
|
+
# 缩放头像
|
|
183
|
+
head = head.resize((HEAD_WIDTH, HEAD_HEIGHT), Image.Resampling.LANCZOS)
|
|
184
|
+
# 创建圆形遮罩
|
|
185
|
+
mask = self._create_head_mask()
|
|
186
|
+
# 应用圆形遮罩
|
|
187
|
+
head.putalpha(mask)
|
|
188
|
+
# 粘贴头像
|
|
189
|
+
card.paste(head, (HEAD_X, HEAD_Y), head)
|
|
190
|
+
|
|
191
|
+
# 绘制边框
|
|
192
|
+
if frame:
|
|
193
|
+
with contextlib.suppress(Exception):
|
|
194
|
+
frame_resized = frame.resize((CARD_WIDTH, CARD_HEIGHT), Image.Resampling.LANCZOS)
|
|
195
|
+
card = Image.alpha_composite(card, frame_resized)
|
|
196
|
+
|
|
197
|
+
# 绘制文字
|
|
198
|
+
name = member.name
|
|
199
|
+
if len(name) > 12:
|
|
200
|
+
name = name[:12] + "..."
|
|
201
|
+
|
|
202
|
+
# 使用字体
|
|
203
|
+
name_font = self._load_font(24)
|
|
204
|
+
count_font = self._load_font(16)
|
|
205
|
+
time_font = self._load_font(20)
|
|
206
|
+
|
|
207
|
+
# 绘制名字
|
|
208
|
+
draw = ImageDraw.Draw(card)
|
|
209
|
+
draw.text((100, 15), name, fill=(0, 0, 0, 255), font=name_font)
|
|
210
|
+
|
|
211
|
+
# 绘制消息数
|
|
212
|
+
draw.text((100, 50), f"消息数: {member.msg_count}条", fill=(80, 80, 80, 255), font=count_font)
|
|
213
|
+
|
|
214
|
+
# 绘制时长(右对齐)
|
|
215
|
+
time_str = self._format_time(member.time_seconds)
|
|
216
|
+
draw.text((440, 40), time_str, fill=(100, 100, 100, 255), font=time_font)
|
|
217
|
+
|
|
218
|
+
return card
|
|
219
|
+
|
|
220
|
+
def generate_card(self, members: list[MemberInfo], title: Optional[str] = None) -> Image.Image:
|
|
221
|
+
"""生成排行榜图片"""
|
|
222
|
+
try:
|
|
223
|
+
config = Config.parse_obj(get_driver().config.dict())
|
|
224
|
+
title = title or config.msg_rank_title
|
|
225
|
+
except Exception:
|
|
226
|
+
title = title or "今日水群排行榜"
|
|
227
|
+
|
|
228
|
+
# 创建背景
|
|
229
|
+
if self.bg_files:
|
|
230
|
+
try:
|
|
231
|
+
bg_path = random.choice(self.bg_files)
|
|
232
|
+
bg_image = Image.open(bg_path).convert("RGBA")
|
|
233
|
+
background = self._format_bg_image(bg_image)
|
|
234
|
+
except Exception:
|
|
235
|
+
background = Image.new("RGBA", (BG_WIDTH, BG_HEIGHT), (255, 255, 255, 255))
|
|
236
|
+
else:
|
|
237
|
+
background = Image.new("RGBA", (BG_WIDTH, BG_HEIGHT), (255, 255, 255, 255))
|
|
238
|
+
|
|
239
|
+
draw = ImageDraw.Draw(background)
|
|
240
|
+
|
|
241
|
+
# 绘制标题
|
|
242
|
+
title_font = self._load_font(60)
|
|
243
|
+
# 计算标题居中位置
|
|
244
|
+
bbox = draw.textbbox((0, 0), title, font=title_font)
|
|
245
|
+
title_width = bbox[2] - bbox[0]
|
|
246
|
+
title_x = (BG_WIDTH - title_width) // 2
|
|
247
|
+
draw.text((title_x, 30), title, fill=(255, 0, 255, 255), font=title_font)
|
|
248
|
+
|
|
249
|
+
# 绘制成员卡片
|
|
250
|
+
size = min(len(self.frame_files), len(members))
|
|
251
|
+
for idx in range(size):
|
|
252
|
+
frame_path = self.frame_files[idx] if idx < len(self.frame_files) else None
|
|
253
|
+
card = self._draw_member_card(members[idx], frame_path, title_font)
|
|
254
|
+
# 粘贴到背景上
|
|
255
|
+
card_x = (BG_WIDTH - CARD_WIDTH) // 2
|
|
256
|
+
card_y = 120 + 95 * idx
|
|
257
|
+
background.paste(card, (card_x, card_y), card)
|
|
258
|
+
|
|
259
|
+
return background
|
|
260
|
+
|
|
261
|
+
def generate_card_bytes(self, members: list[MemberInfo], title: Optional[str] = None) -> bytes:
|
|
262
|
+
"""生成排行榜图片并返回 bytes"""
|
|
263
|
+
image = self.generate_card(members, title)
|
|
264
|
+
buffer = BytesIO()
|
|
265
|
+
image.convert("RGB").save(buffer, format="PNG")
|
|
266
|
+
return buffer.getvalue()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
async def download_avatar(user_id: str) -> Optional[Image.Image]:
|
|
270
|
+
"""下载用户头像"""
|
|
271
|
+
try:
|
|
272
|
+
url = f"https://q1.qlogo.cn/g?b=qq&nk={user_id}&s=640"
|
|
273
|
+
async with httpx.AsyncClient(timeout=10) as client:
|
|
274
|
+
response = await client.get(url)
|
|
275
|
+
if response.status_code == 200:
|
|
276
|
+
return Image.open(BytesIO(response.content)).convert("RGBA")
|
|
277
|
+
except Exception as e:
|
|
278
|
+
logger.warning(f"下载头像失败 {user_id}: {e}")
|
|
279
|
+
return None
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Config(BaseModel):
|
|
6
|
+
"""插件配置"""
|
|
7
|
+
|
|
8
|
+
# 数据存储路径
|
|
9
|
+
msg_rank_data_path: str = "data/msg_rank"
|
|
10
|
+
# 资源文件路径
|
|
11
|
+
msg_rank_resource_path: str = ""
|
|
12
|
+
# 每日排行榜最大显示人数
|
|
13
|
+
msg_rank_max_count: int = 10
|
|
14
|
+
# 是否自动清理过期数据(保留最近7天)
|
|
15
|
+
msg_rank_auto_clean: bool = True
|
|
16
|
+
# 排行榜标题
|
|
17
|
+
msg_rank_title: str = "今日水群排行榜"
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from nonebot import get_driver
|
|
7
|
+
from nonebot.log import logger
|
|
8
|
+
|
|
9
|
+
from .config import Config
|
|
10
|
+
|
|
11
|
+
# 数据目录(默认值,会在首次使用时根据配置更新)
|
|
12
|
+
_data_dir: Path = Path("data/msg_rank")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _get_data_dir() -> Path:
|
|
16
|
+
"""获取数据目录(延迟加载)"""
|
|
17
|
+
global _data_dir
|
|
18
|
+
if not _data_dir.exists():
|
|
19
|
+
try:
|
|
20
|
+
config = Config.parse_obj(get_driver().config.dict())
|
|
21
|
+
_data_dir = Path(config.msg_rank_data_path)
|
|
22
|
+
except Exception:
|
|
23
|
+
_data_dir = Path("data/msg_rank")
|
|
24
|
+
_data_dir.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
return _data_dir
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_today_str() -> str:
|
|
29
|
+
"""获取今天的日期字符串"""
|
|
30
|
+
return datetime.now().strftime("%Y%m%d")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_group_data_file(group_id: str) -> Path:
|
|
34
|
+
"""获取群数据文件路径"""
|
|
35
|
+
return _get_data_dir() / f"{group_id}_{get_today_str()}.json"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_group_data(group_id: str) -> dict:
|
|
39
|
+
"""加载群数据"""
|
|
40
|
+
file_path = get_group_data_file(group_id)
|
|
41
|
+
if file_path.exists():
|
|
42
|
+
try:
|
|
43
|
+
with open(file_path, encoding="utf-8") as f:
|
|
44
|
+
return json.load(f)
|
|
45
|
+
except Exception as e:
|
|
46
|
+
logger.warning(f"加载群数据失败: {e}")
|
|
47
|
+
return {}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def save_group_data(group_id: str, data: dict):
|
|
51
|
+
"""保存群数据"""
|
|
52
|
+
file_path = get_group_data_file(group_id)
|
|
53
|
+
try:
|
|
54
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
55
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
logger.warning(f"保存群数据失败: {e}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def record_message(group_id: str, user_id: str, user_name: str, msg_length: int = 1):
|
|
61
|
+
"""记录一条消息"""
|
|
62
|
+
data = load_group_data(group_id)
|
|
63
|
+
|
|
64
|
+
if user_id not in data:
|
|
65
|
+
data[user_id] = {
|
|
66
|
+
"name": user_name,
|
|
67
|
+
"msg_count": 0,
|
|
68
|
+
"time_seconds": 0,
|
|
69
|
+
"last_msg_time": 0,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# 更新消息数
|
|
73
|
+
data[user_id]["msg_count"] += msg_length
|
|
74
|
+
|
|
75
|
+
# 计算在线时长(简单估算:如果距离上次消息在5分钟内,累加时间差)
|
|
76
|
+
now = int(time.time())
|
|
77
|
+
last_time = data[user_id].get("last_msg_time", 0)
|
|
78
|
+
if last_time > 0 and now - last_time < 300: # 5分钟内
|
|
79
|
+
data[user_id]["time_seconds"] += now - last_time
|
|
80
|
+
|
|
81
|
+
data[user_id]["last_msg_time"] = now
|
|
82
|
+
data[user_id]["name"] = user_name # 更新昵称
|
|
83
|
+
|
|
84
|
+
save_group_data(group_id, data)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_rank_data(group_id: str, max_count: int = 10) -> list[dict]:
|
|
88
|
+
"""获取排行榜数据,按消息数排序"""
|
|
89
|
+
data = load_group_data(group_id)
|
|
90
|
+
|
|
91
|
+
rank_list = []
|
|
92
|
+
for user_id, info in data.items():
|
|
93
|
+
rank_list.append({
|
|
94
|
+
"user_id": user_id,
|
|
95
|
+
"name": info.get("name", "未知用户"),
|
|
96
|
+
"msg_count": info.get("msg_count", 0),
|
|
97
|
+
"time_seconds": info.get("time_seconds", 0),
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
# 按消息数降序排序
|
|
101
|
+
rank_list.sort(key=lambda x: x["msg_count"], reverse=True)
|
|
102
|
+
|
|
103
|
+
return rank_list[:max_count]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def clean_old_data(days: int = 7):
|
|
107
|
+
"""清理过期数据"""
|
|
108
|
+
try:
|
|
109
|
+
config = Config.parse_obj(get_driver().config.dict())
|
|
110
|
+
if not config.msg_rank_auto_clean:
|
|
111
|
+
return
|
|
112
|
+
except Exception:
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
data_dir = _get_data_dir()
|
|
116
|
+
cutoff = datetime.now() - timedelta(days=days)
|
|
117
|
+
cutoff_str = cutoff.strftime("%Y%m%d")
|
|
118
|
+
|
|
119
|
+
for file_path in data_dir.glob("*_*.json"):
|
|
120
|
+
try:
|
|
121
|
+
# 文件名格式: {group_id}_{date}.json
|
|
122
|
+
date_str = file_path.stem.split("_")[-1]
|
|
123
|
+
if date_str < cutoff_str:
|
|
124
|
+
file_path.unlink()
|
|
125
|
+
logger.info(f"清理过期数据: {file_path.name}")
|
|
126
|
+
except Exception:
|
|
127
|
+
pass
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "nonebot-plugin-msg-rank-card"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "每日水群排行榜图片生成插件,记录群聊消息并生成精美排行榜"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
requires-python = ">=3.9"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "TonyLiangP2010405", email = "" },
|
|
10
|
+
]
|
|
11
|
+
keywords = ["nonebot", "nonebot2", "plugin", "rank", "card", "message"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.9",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"nonebot2>=2.2.0",
|
|
23
|
+
"nonebot-adapter-onebot>=2.4.0",
|
|
24
|
+
"pillow>=10.0.0",
|
|
25
|
+
"httpx>=0.24.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
dev = [
|
|
30
|
+
"pytest>=7.0",
|
|
31
|
+
"nonebug>=0.3",
|
|
32
|
+
"ruff>=0.1.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/TonyLiangP2010405/nonebot-plugin-msg-rank-card"
|
|
37
|
+
Repository = "https://github.com/TonyLiangP2010405/nonebot-plugin-msg-rank-card"
|
|
38
|
+
|
|
39
|
+
[tool.nonebot]
|
|
40
|
+
plugins = ["nonebot_plugin_msg_rank_card"]
|
|
41
|
+
|
|
42
|
+
[build-system]
|
|
43
|
+
requires = ["hatchling"]
|
|
44
|
+
build-backend = "hatchling.build"
|
|
45
|
+
|
|
46
|
+
[tool.hatch.build.targets.wheel]
|
|
47
|
+
packages = ["nonebot_plugin_msg_rank_card"]
|
|
48
|
+
|
|
49
|
+
[tool.ruff]
|
|
50
|
+
line-length = 120
|
|
51
|
+
target-version = "py39"
|
|
52
|
+
|
|
53
|
+
[tool.ruff.lint]
|
|
54
|
+
select = ["E", "F", "W", "I", "N", "UP", "B", "C4", "SIM"]
|
|
55
|
+
ignore = ["E501", "B008"]
|
|
56
|
+
|
|
57
|
+
[tool.pytest.ini_options]
|
|
58
|
+
testpaths = ["tests"]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import nonebot
|
|
2
|
+
from nonebot.adapters.onebot.v11 import Adapter
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_plugin_load():
|
|
6
|
+
nonebot.init()
|
|
7
|
+
driver = nonebot.get_driver()
|
|
8
|
+
driver.register_adapter(Adapter)
|
|
9
|
+
plugin = nonebot.load_plugin("nonebot_plugin_msg_rank_card")
|
|
10
|
+
assert plugin is not None
|