9 Commits

Author SHA1 Message Date
Chenric
d514d96db6 📝 改个版本号便于发版 2024-09-28 09:55:18 +08:00
Chenric
677ec56aeb 获取用户数据非常好优化 2024-09-28 09:50:32 +08:00
Chenric
4194097258 ⬆️ 升级userinfo依赖 2024-09-28 09:49:59 +08:00
Chenric
c94de918e9 ⚗️ 测试速率 2024-09-28 09:24:29 +08:00
Chenric
a765267cf1 🐛 修复计数缓存的bug 2024-09-28 09:04:01 +08:00
Chenric
ea682ef18d 缓存与重建缓存指令 2024-09-27 21:04:50 +08:00
Chenric
ed7f1167f2 🐛 修复性别显示 2024-09-23 21:37:20 +08:00
Chenric
9944ece153 👌 2024-09-21 20:22:25 +08:00
Chenric
8d905cb647 🎨 按ruff和black格式化代码 2024-09-21 20:18:40 +08:00
9 changed files with 213 additions and 99 deletions

View File

@@ -11,8 +11,10 @@
"dialectlist",
"displayname",
"htmlrender",
"httpx",
"localstore",
"parameterless",
"postprocessor",
"pyecharts",
"pygal",
"sqlalchemy",

View File

@@ -158,12 +158,6 @@ __注意__
- [x] 私聊的查询(超级用户可以任意查询群聊的信息)一半完成
- [ ] 提供多样化的渲染器配置html 渲染pillow 渲染,统计绘图软件渲染)
- [ ] 使用管理员权限直接获取 QQ 官方统计的今日消息量以优化代码运行速度
- [ ] 为 pillow 渲染方式提供插件的加载方式(什么?插件里的插件???)
- [ ] 特殊的储存方案优化消息统计
- [ ] 查询带某关键词的消息量

View File

@@ -8,21 +8,19 @@ require("nonebot_plugin_alconna")
require("nonebot_plugin_cesaa")
import re
import os
import time as t
import nonebot_plugin_saa as saa
from typing import Union, Optional, List
from typing import Union, Optional
from datetime import datetime, timedelta
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.compat import model_dump
from nonebot.params import Arg, Depends
from nonebot.adapters import Bot, Event
from nonebot.permission import SUPERUSER
from nonebot.plugin import PluginMetadata, inherit_supported_adapters
from nonebot_plugin_alconna import (
Args,
@@ -30,14 +28,10 @@ from nonebot_plugin_alconna import (
Alconna,
on_alconna,
)
from nonebot_plugin_apscheduler import scheduler
from nonebot_plugin_userinfo import get_user_info
from nonebot_plugin_chatrecorder import get_message_records
from nonebot_plugin_session import Session, SessionIdType, extract_session
# from . import migrations #抄词云的部分代码,还不知道这有什么用
# from .function import *
from .storage import get_cache,build_cache
from .config import Config, plugin_config
from .usage import __usage__
from .time import (
@@ -45,14 +39,12 @@ from .time import (
get_datetime_now_with_timezone,
parse_datetime,
)
from .model import UserRankInfo
from .utils import (
got_rank,
msg_counter,
get_rank_image,
persist_id2user_id,
get_user_infos,
# get_user_info2,
)
__plugin_meta__ = PluginMetadata(
@@ -65,7 +57,6 @@ __plugin_meta__ = PluginMetadata(
"nonebot_plugin_chatrecorder", "nonebot_plugin_saa", "nonebot_plugin_alconna"
),
config=Config,
# extra={"orm_version_location": migrations},
)
@@ -83,6 +74,13 @@ def wrapper(slot: Union[int, str], content: Optional[str], context) -> str:
return content
return "" # pragma: no cover
build_cache_cmd = on_command("build_cache", aliases={"重建缓存"}, block=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)
rank_cmd = on_alconna(
Alconna(
@@ -194,8 +192,6 @@ async def _group_message(
except ValueError:
await rank_cmd.finish("请输入正确的日期,不然我没法理解呢!")
logger.debug(f"命令解析花费时间:{t.time() - t1}")
@rank_cmd.got(
"start",
@@ -216,7 +212,7 @@ async def handle_rank(
start: datetime = Arg(),
stop: datetime = Arg(),
):
if id := state["group_id"]:
id = str(id)
logger.debug(f"group_id: {id}")
@@ -228,8 +224,11 @@ async def handle_rank(
await saa.Text("没有指定群哦").finish()
if plugin_config.counting_cache:
raise Exception("我草缓存功能还没端上来呢,你怎么就先用上了")
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,
@@ -240,14 +239,13 @@ async def handle_rank(
time_stop=stop,
exclude_id1s=plugin_config.excluded_people,
)
if not messages:
await saa.Text("明明这个时间段都没有人说话怎么会有话痨榜呢?").finish()
raw_rank = msg_counter(messages)
logger.debug(f"获取计数消息花费时间:{t.time() - t1}")
if not raw_rank:
await saa.Text("明明这个时间段都没有人说话怎么会有话痨榜呢?").finish()
rank = got_rank(raw_rank)
logger.debug(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])
@@ -283,10 +281,3 @@ async def handle_rank(
logger.debug(f"群聊消息渲染图片花费时间:{t.time() - t1}")
await msg.finish(reply=True)
# @scheduler.scheduled_job(
# "dialectlist", day="*/2", id="xxx", args=[1], kwargs={"arg2": 2}
# )
# async def __():
# pass

View File

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

View File

@@ -1,7 +1,6 @@
from datetime import datetime
from typing import Union
from pydantic import BaseModel
from sqlalchemy import JSON, TEXT, String,Integer
from sqlalchemy import Integer
from nonebot_plugin_orm import Model
from nonebot_plugin_userinfo import UserInfo
from sqlalchemy.orm import Mapped, mapped_column
@@ -15,13 +14,9 @@ class UserRankInfo(UserInfo):
user_avatar_bytes: bytes
# class MsgCountDayData(BaseModel):
# session_id: str
# session_bnum: int
class MessageCountCache(Model):
__table_args__ = {"extend_existing": True}
id: Mapped[int] = mapped_column(primary_key=True)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
time: Mapped[datetime]
session_id: Mapped[int] = mapped_column(Integer)
session_id: Mapped[int] = mapped_column(Integer, index=True)
session_bnum: Mapped[int] = mapped_column(Integer)

View File

@@ -1,2 +1,130 @@
# TODO 使用计数缓存进行数据库查询优化,避免一次性查询过多消息导致内存爆炸。
from nonebot_plugin_orm import Model
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,4 +1,3 @@
# TODO 时间处理模块,用于处理时间相关操作。
from zoneinfo import ZoneInfo
from typing import Optional, Union
from datetime import datetime, time, tzinfo

View File

@@ -1,8 +1,9 @@
import os
import httpx
import asyncio
import unicodedata
from typing import Dict, List, Optional
from typing import Dict, List
from sqlalchemy import or_, select
from sqlalchemy.sql import ColumnElement
@@ -15,6 +16,7 @@ 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
@@ -26,16 +28,6 @@ from .config import plugin_config
cache_path = get_cache_dir("nonebot_plugin_dialectlist")
# 暂时不做考虑
# def admin_permission():
# permission = SUPERUSER
# with contextlib.suppress(ImportError):
# from nonebot.adapters.onebot.v11.permission import GROUP_ADMIN, GROUP_OWNER
# permission = permission | GROUP_ADMIN | GROUP_OWNER
# return permission
async def ensure_group(matcher: Matcher, session: Session = Depends(extract_session)):
"""确保在群组中使用"""
@@ -184,7 +176,9 @@ async def get_rank_image(rank: List[UserRankInfo]) -> bytes:
},
pages={"viewport": {"width": 1000, "height": 10}},
)
def _get_user_nickname(user_info:UserInfo)->str:
def _get_user_nickname(user_info: UserInfo) -> str:
user_nickname = (
user_info.user_displayname
if user_info.user_displayname
@@ -192,56 +186,78 @@ def _get_user_nickname(user_info:UserInfo)->str:
)
return user_nickname
async def _get_user_default_avatar()->bytes:
async def _get_user_default_avatar() -> bytes:
img = open(
os.path.dirname(os.path.abspath(__file__))
+ "/template/avatar/default.jpg",
os.path.dirname(os.path.abspath(__file__)) + "/template/avatar/default.jpg",
"rb",
).read()
).read()
return img
def get_default_user_info()->UserInfo:
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} 下载失败!")
def get_default_user_info() -> UserInfo:
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 = True)-> List[UserRankInfo]:
async def get_user_infos(
bot: Bot, event: Event, rank: List, use_cache: bool = True
) -> 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_infos = await asyncio.gather(*pool)
pool = []
for i in user_infos:
if not i:
pool.append(_get_user_default_avatar())
continue
if i.user_avatar:
pool.append(i.user_avatar.get_image())
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())
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()
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_index=i + 1,
user_nickname=_get_user_nickname(user_info),
user_avatar_bytes=user_avatars[i],
)
user.user_gender = (
""
if user.user_gender == "male"
else "" if user.user_gender == "female" else ""
)
rank2.append(user)
return rank2

View File

@@ -1,6 +1,6 @@
[project]
name = "nonebot-plugin-dialectlist"
version = "2.3.0"
version = "2.4.1"
description = "看看你群群友有多能说"
authors = [
{name = "Chen_Xu233", email = "woyerpa@outlook.com"},
@@ -12,7 +12,7 @@ dependencies = [
"nonebot-plugin-apscheduler>=0.4.0",
"nonebot-plugin-alconna>=0.50.2",
"nonebot-plugin-cesaa>=0.4.0",
"nonebot-plugin-userinfo>=0.2.4",
"nonebot-plugin-userinfo>=0.2.6",
"nonebot-plugin-htmlrender>=0.3.3",
"nonebot2>=2.3.2",
"pillow>=10.4.0",
@@ -40,21 +40,10 @@ distribution = true
[tool.ruff]
line-length = 80
[tool.ruff.format]
quote-style = "single"
indent-style = "tab"
[tool.pdm.scripts]
build = 'pdm run setup.py sdist'
publish = 'pdm run python -m twine upload dist/*'
# 以下为智普 AI 生成,还不知道这玩意有啥用。
# [tool.pdm.dev-dependencies]
# black = "^23.1.0"
# isort = "^5.10.1"
# pre-commit = "^2.20.0"
# [tool.pdm.global-options]
# --no-pip-version-check = true
[tool.ruff.lint]
ignore = ["E402"]