nonebot-plugin-group-historian 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.
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: nonebot-plugin-group-historian
3
+ Version: 0.1.0
4
+ Summary: 统计每日群聊发言字数 生成话痨榜图片的NoneBot2插件
5
+ Author-email: Wojusensei <3442006415@qq.com>
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: nonebot2>=2.3.0
8
+ Requires-Dist: nonebot-adapter-onebot>=2.0.0
9
+ Requires-Dist: nonebot-plugin-orm[default]>=0.7.0
10
+ Requires-Dist: nonebot-plugin-apscheduler>=0.5.0
11
+ Requires-Dist: nonebot-plugin-localstore>=0.5.0
12
+ Requires-Dist: Pillow>=10.0.0
13
+ Requires-Dist: aiohttp>=3.9.0
@@ -0,0 +1,168 @@
1
+ import asyncio
2
+ from datetime import datetime, timedelta
3
+ from nonebot import on_command, on_message, on_notice, require, get_plugin_config, get_driver
4
+ from nonebot.adapters.onebot.v11 import (
5
+ Bot,
6
+ GroupMessageEvent,
7
+ GroupRecallNoticeEvent,
8
+ MessageSegment,
9
+ )
10
+ from nonebot.plugin import PluginMetadata
11
+ from nonebot.log import logger
12
+
13
+
14
+ # ————————————————————————————
15
+ # 依赖声明
16
+ # ————————————————————————————
17
+
18
+ require("nonebot_plugin_orm")
19
+ require("nonebot_plugin_apscheduler")
20
+ require("nonebot_plugin_localstore")
21
+
22
+
23
+ # ————————————————————————————
24
+ # 元数据 插件身份证
25
+ # ————————————————————————————
26
+
27
+ __plugin_meta__ = PluginMetadata(
28
+ name="群聊史官",
29
+ description="统计每日群聊发言字数 生成话痨榜图片",
30
+ usage="在群里发送 话痨榜 查看昨日排行\n发送 今日话痨榜 查看今日实时排行\n可加页码 例如 话痨榜 2",
31
+ type="application",
32
+ homepage="https://github.com/Wojusensei/nonebot-plugin-group-historian",
33
+ config=None,
34
+ supported_adapters={"~onebot.v11"},
35
+ )
36
+
37
+
38
+ # ————————————————————————————
39
+ # 导入插件内部模块 放在元数据之后
40
+ # ————————————————————————————
41
+
42
+ from .config import Config
43
+ from .data import add_message, delete_last_message, get_daily_ranking, clean_old_data
44
+ from .image import create_ranking_image
45
+
46
+
47
+ # ————————————————————————————
48
+ # 加载配置 使用 get_plugin_config
49
+ # ————————————————————————————
50
+
51
+ config = get_plugin_config(Config)
52
+
53
+
54
+ # ————————————————————————————
55
+ # 消息缓存 用于撤回时扣除字数
56
+ # key 是消息ID value 是 (群号, QQ号, 字数)
57
+ # ————————————————————————————
58
+
59
+ message_cache = {}
60
+
61
+
62
+ # ————————————————————————————
63
+ # 监听群消息 记录字数
64
+ # ————————————————————————————
65
+
66
+ msg_handler = on_message(block=False)
67
+
68
+
69
+ @msg_handler.handle()
70
+ async def handle_message(event: GroupMessageEvent):
71
+ text = event.get_plaintext()
72
+
73
+ # 过滤纯图片表情等无文字消息
74
+ if not text or not text.strip():
75
+ return
76
+
77
+ length = len(text.replace(" ", ""))
78
+ group_id = str(event.group_id)
79
+ user_id = str(event.user_id)
80
+ nickname = event.sender.card or event.sender.nickname or user_id
81
+
82
+ await add_message(group_id, user_id, nickname, length)
83
+
84
+ # 存入缓存 供撤回时扣除
85
+ message_cache[str(event.message_id)] = (group_id, user_id, length)
86
+
87
+
88
+ # ————————————————————————————
89
+ # 监听群撤回 扣除字数
90
+ # ————————————————————————————
91
+
92
+ recall_handler = on_notice(block=False)
93
+
94
+
95
+ @recall_handler.handle()
96
+ async def handle_recall(event: GroupRecallNoticeEvent):
97
+ msg_id = str(event.message_id)
98
+ if msg_id in message_cache:
99
+ group_id, user_id, length = message_cache[msg_id]
100
+ await delete_last_message(group_id, user_id, length)
101
+ del message_cache[msg_id]
102
+
103
+
104
+ # ————————————————————————————
105
+ # 话痨榜命令
106
+ # 默认查昨日 加今日查今天
107
+ # ————————————————————————————
108
+
109
+ rank_cmd = on_command("话痨榜", aliases={"今日话痨榜"}, block=True)
110
+
111
+
112
+ @rank_cmd.handle()
113
+ async def handle_rank(event: GroupMessageEvent, bot: Bot):
114
+ raw = event.get_plaintext().strip()
115
+ parts = raw.split()
116
+
117
+ # 判断查今天还是昨天
118
+ is_today = "今日" in raw
119
+
120
+ # 解析页码
121
+ page = 1
122
+ for p in parts:
123
+ if p.isdigit():
124
+ page = int(p)
125
+ if page < 1:
126
+ page = 1
127
+ break
128
+
129
+ # 确定日期
130
+ date = datetime.now().date() if is_today else (datetime.now().date() - timedelta(days=1))
131
+
132
+ # 获取排行榜
133
+ ranking = await get_daily_ranking(str(event.group_id), date)
134
+
135
+ if not ranking:
136
+ day_text = "今天" if is_today else "昨天"
137
+ await rank_cmd.finish(f"{day_text}还没有人说话呢", at_sender=True)
138
+ return
139
+
140
+ # 生成图片 放入线程池避免阻塞事件循环
141
+ rank_count = config.historian_rank_count
142
+ img_bytes = await asyncio.to_thread(
143
+ create_ranking_image,
144
+ ranking,
145
+ page=page,
146
+ rank_count=rank_count,
147
+ )
148
+
149
+ await rank_cmd.send(MessageSegment.image(img_bytes))
150
+
151
+
152
+ # ————————————————————————————
153
+ # 定时任务 每日凌晨清理旧数据
154
+ # ————————————————————————————
155
+
156
+ from nonebot_plugin_apscheduler import scheduler
157
+
158
+
159
+ @scheduler.scheduled_job("cron", hour=0, minute=0, id="clean_old_historian_data")
160
+ async def scheduled_clean():
161
+ await clean_old_data(config.historian_data_retention_days)
162
+
163
+
164
+ # ————————————————————————————
165
+ # 启动日志
166
+ # ————————————————————————————
167
+
168
+ logger.info("群聊史官 插件已加载")
@@ -0,0 +1,14 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+
4
+ class Config(BaseModel):
5
+ """插件配置,用户可在 .env 文件中设置"""
6
+
7
+ historian_rank_count: int = Field(
8
+ default=10,
9
+ description="排行榜每页显示的人数"
10
+ )
11
+ historian_data_retention_days: int = Field(
12
+ default=30,
13
+ description="数据保留天数,超过自动清理"
14
+ )
@@ -0,0 +1,113 @@
1
+ from datetime import datetime, timedelta
2
+ from sqlalchemy import Column, Integer, String, Date, func
3
+ from nonebot_plugin_orm import Model
4
+ from nonebot import require
5
+
6
+ require("nonebot_plugin_orm")
7
+
8
+
9
+ # ————————————————————————————
10
+ # 每日发言记录表
11
+ # ————————————————————————————
12
+
13
+ class DailyMessage(Model):
14
+ __tablename__ = "daily_messages"
15
+
16
+ id = Column(Integer, primary_key=True, autoincrement=True)
17
+ group_id = Column(String, nullable=False)
18
+ user_id = Column(String, nullable=False)
19
+ nickname = Column(String)
20
+ message_length = Column(Integer, default=0)
21
+ timestamp = Column(Date, nullable=False)
22
+
23
+
24
+ # ————————————————————————————
25
+ # 记录
26
+ # ————————————————————————————
27
+
28
+ async def add_message(group_id: str, user_id: str, nickname: str, length: int):
29
+ from nonebot_plugin_orm import get_scoped_session
30
+
31
+ async with get_scoped_session() as session:
32
+ record = DailyMessage(
33
+ group_id=group_id,
34
+ user_id=user_id,
35
+ nickname=nickname,
36
+ message_length=length,
37
+ timestamp=datetime.now().date(),
38
+ )
39
+ session.add(record)
40
+ await session.commit()
41
+
42
+
43
+ # ————————————————————————————
44
+ # 撤回消息时扣除最近一条匹配的字数
45
+ # ————————————————————————————
46
+
47
+ async def delete_last_message(group_id: str, user_id: str, length: int):
48
+ from nonebot_plugin_orm import get_scoped_session
49
+
50
+ today = datetime.now().date()
51
+ async with get_scoped_session() as session:
52
+ record = await session.execute(
53
+ DailyMessage.__table__.select()
54
+ .where(
55
+ DailyMessage.group_id == group_id,
56
+ DailyMessage.user_id == user_id,
57
+ DailyMessage.message_length == length,
58
+ DailyMessage.timestamp == today,
59
+ )
60
+ .order_by(DailyMessage.id.desc())
61
+ .limit(1)
62
+ )
63
+ row = record.fetchone()
64
+ if row:
65
+ await session.execute(
66
+ DailyMessage.__table__.delete().where(DailyMessage.id == row.id)
67
+ )
68
+ await session.commit()
69
+
70
+
71
+ # ————————————————————————————
72
+ # 获取指定日期的字数排行榜
73
+ # 返回 [(user_id, nickname, total), ...]
74
+ # ————————————————————————————
75
+
76
+ async def get_daily_ranking(group_id: str, date=None) -> list:
77
+ from nonebot_plugin_orm import get_scoped_session
78
+
79
+ if date is None:
80
+ date = datetime.now().date()
81
+
82
+ async with get_scoped_session() as session:
83
+ result = await session.execute(
84
+ DailyMessage.__table__.select()
85
+ .with_only_columns(
86
+ DailyMessage.user_id,
87
+ func.max(DailyMessage.nickname).label("nickname"),
88
+ func.sum(DailyMessage.message_length).label("total"),
89
+ )
90
+ .where(
91
+ DailyMessage.group_id == group_id,
92
+ DailyMessage.timestamp == date,
93
+ )
94
+ .group_by(DailyMessage.user_id)
95
+ .order_by(func.sum(DailyMessage.message_length).desc())
96
+ )
97
+ rows = result.fetchall()
98
+ return [(row.user_id, row.nickname, row.total) for row in rows]
99
+
100
+
101
+ # ————————————————————————————
102
+ # 删除超过保留天数的旧数据
103
+ # ————————————————————————————
104
+
105
+ async def clean_old_data(retention_days: int):
106
+ from nonebot_plugin_orm import get_scoped_session
107
+
108
+ cutoff = datetime.now().date() - timedelta(days=retention_days)
109
+ async with get_scoped_session() as session:
110
+ await session.execute(
111
+ DailyMessage.__table__.delete().where(DailyMessage.timestamp < cutoff)
112
+ )
113
+ await session.commit()
@@ -0,0 +1,140 @@
1
+ import io
2
+ from PIL import Image, ImageDraw, ImageFont
3
+ from nonebot.log import logger
4
+ from nonebot_plugin_localstore import get_plugin_data_dir
5
+ from nonebot import require
6
+
7
+ require("nonebot_plugin_localstore")
8
+
9
+
10
+ # ————————————————————————————
11
+ # 资源路径
12
+ # 字体和背景图放在插件数据目录下
13
+ # ————————————————————————————
14
+
15
+ DATA_DIR = get_plugin_data_dir()
16
+ FONT_PATH = DATA_DIR / "font.ttf"
17
+ BG_PATH = DATA_DIR / "background.png"
18
+
19
+
20
+ # ————————————————————————————
21
+ # 荣誉图标
22
+ # ————————————————————————————
23
+
24
+ ICONS = {
25
+ 1: "👑",
26
+ 2: "💎",
27
+ 3: "⭐",
28
+ }
29
+
30
+
31
+ # ————————————————————————————
32
+ # 生成排行榜图片
33
+ # ranking: [(user_id, nickname, total), ...]
34
+ # page: 页码1开始
35
+ # rank_count: 每页人数
36
+ # 返回图片的 bytes
37
+ # ————————————————————————————
38
+
39
+ def create_ranking_image(ranking, page=1, rank_count=10):
40
+ # ———————— 计算分页 ————————
41
+ start = (page - 1) * rank_count
42
+ end = start + rank_count
43
+ page_data = ranking[start:end]
44
+
45
+ # ———————— 画布尺寸 ————————
46
+ width = 800
47
+ header_height = 120
48
+ row_height = 90
49
+ footer_height = 80
50
+ height = header_height + row_height * rank_count + footer_height
51
+
52
+ # ———————— 背景图 ————————
53
+ if BG_PATH.exists():
54
+ bg = Image.open(BG_PATH).convert("RGBA")
55
+ bg = bg.resize((width, height), Image.LANCZOS)
56
+ # 蒙灰:降低饱和度并加半透明遮罩
57
+ bg = bg.convert("L").convert("RGBA")
58
+ overlay = Image.new("RGBA", (width, height), (0, 0, 0, 140))
59
+ bg = Image.alpha_composite(bg, overlay)
60
+ else:
61
+ bg = Image.new("RGBA", (width, height), (30, 30, 50, 255))
62
+
63
+ # ———————— 字体 ————————
64
+ if FONT_PATH.exists():
65
+ font_title = ImageFont.truetype(str(FONT_PATH), 36)
66
+ font_name = ImageFont.truetype(str(FONT_PATH), 24)
67
+ font_small = ImageFont.truetype(str(FONT_PATH), 18)
68
+ else:
69
+ font_title = ImageFont.load_default()
70
+ font_name = ImageFont.load_default()
71
+ font_small = ImageFont.load_default()
72
+
73
+ # ———————— 创建画布 ————————
74
+ img = Image.new("RGBA", (width, height), (0, 0, 0, 0))
75
+ img.paste(bg, (0, 0))
76
+ draw = ImageDraw.Draw(img)
77
+
78
+ # ———————— 标题 ————————
79
+ title = f"话痨榜 第{page}页"
80
+ draw.text(
81
+ (width // 2, 40),
82
+ title,
83
+ fill=(255, 215, 0),
84
+ font=font_title,
85
+ anchor="ma",
86
+ )
87
+
88
+ # ———————— 分割线 ————————
89
+ draw.line(
90
+ (40, header_height - 10, width - 40, header_height - 10),
91
+ fill=(255, 215, 0, 200),
92
+ width=2,
93
+ )
94
+
95
+ # ———————— 排行榜条目 ————————
96
+ for i, (user_id, nickname, total) in enumerate(page_data):
97
+ rank = start + i + 1
98
+ y = header_height + i * row_height
99
+
100
+ # 排名
101
+ icon = ICONS.get(rank, "")
102
+ rank_text = f"{icon} {rank}" if icon else str(rank)
103
+ draw.text((60, y + row_height // 2), rank_text, fill=(255, 255, 255), font=font_name, anchor="lm")
104
+
105
+ # 昵称
106
+ draw.text((140, y + 25), nickname, fill=(255, 255, 255), font=font_name)
107
+
108
+ # QQ号
109
+ draw.text((140, y + 55), user_id, fill=(180, 180, 180), font=font_small)
110
+
111
+ # 字数
112
+ count_text = f"{total} 字"
113
+ bbox = draw.textbbox((0, 0), count_text, font=font_name)
114
+ text_w = bbox[2] - bbox[0]
115
+ draw.text(
116
+ (width - 60 - text_w, y + row_height // 2),
117
+ count_text,
118
+ fill=(255, 215, 0),
119
+ font=font_name,
120
+ anchor="lm",
121
+ )
122
+
123
+ # ———————— 页脚 ————————
124
+ draw.text(
125
+ (width // 2, height - 50),
126
+ "排行由群聊史官自动生成 | 记录昨日话痨数据",
127
+ fill=(150, 150, 150),
128
+ font=font_small,
129
+ anchor="ma",
130
+ )
131
+
132
+ # ———————— 输出 ————————
133
+ buf = io.BytesIO()
134
+ img.save(buf, format="PNG")
135
+ buf.seek(0)
136
+ return buf.read()
137
+
138
+ logger.info("群聊史官 图片生成模块已就绪")
139
+
140
+ #累死了
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: nonebot-plugin-group-historian
3
+ Version: 0.1.0
4
+ Summary: 统计每日群聊发言字数 生成话痨榜图片的NoneBot2插件
5
+ Author-email: Wojusensei <3442006415@qq.com>
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: nonebot2>=2.3.0
8
+ Requires-Dist: nonebot-adapter-onebot>=2.0.0
9
+ Requires-Dist: nonebot-plugin-orm[default]>=0.7.0
10
+ Requires-Dist: nonebot-plugin-apscheduler>=0.5.0
11
+ Requires-Dist: nonebot-plugin-localstore>=0.5.0
12
+ Requires-Dist: Pillow>=10.0.0
13
+ Requires-Dist: aiohttp>=3.9.0
@@ -0,0 +1,10 @@
1
+ pyproject.toml
2
+ nonebot_plugin_group_historian/__init__.py
3
+ nonebot_plugin_group_historian/config.py
4
+ nonebot_plugin_group_historian/data.py
5
+ nonebot_plugin_group_historian/image.py
6
+ nonebot_plugin_group_historian.egg-info/PKG-INFO
7
+ nonebot_plugin_group_historian.egg-info/SOURCES.txt
8
+ nonebot_plugin_group_historian.egg-info/dependency_links.txt
9
+ nonebot_plugin_group_historian.egg-info/requires.txt
10
+ nonebot_plugin_group_historian.egg-info/top_level.txt
@@ -0,0 +1,7 @@
1
+ nonebot2>=2.3.0
2
+ nonebot-adapter-onebot>=2.0.0
3
+ nonebot-plugin-orm[default]>=0.7.0
4
+ nonebot-plugin-apscheduler>=0.5.0
5
+ nonebot-plugin-localstore>=0.5.0
6
+ Pillow>=10.0.0
7
+ aiohttp>=3.9.0
@@ -0,0 +1,22 @@
1
+ [project]
2
+ name = "nonebot-plugin-group-historian"
3
+ version = "0.1.0"
4
+ description = "统计每日群聊发言字数 生成话痨榜图片的NoneBot2插件"
5
+ authors = [{name = "Wojusensei", email = "3442006415@qq.com"}]
6
+ requires-python = ">=3.10"
7
+ dependencies = [
8
+ "nonebot2>=2.3.0",
9
+ "nonebot-adapter-onebot>=2.0.0",
10
+ "nonebot-plugin-orm[default]>=0.7.0",
11
+ "nonebot-plugin-apscheduler>=0.5.0",
12
+ "nonebot-plugin-localstore>=0.5.0",
13
+ "Pillow>=10.0.0",
14
+ "aiohttp>=3.9.0",
15
+ ]
16
+
17
+ [tool.setuptools]
18
+ packages = ["nonebot_plugin_group_historian"]
19
+
20
+ [build-system]
21
+ requires = ["setuptools>=64.0", "wheel"]
22
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+