forked from bot/app
✨ 新增 stat rank 功能
This commit is contained in:
parent
4162ea33ff
commit
35823be13e
@ -31,7 +31,7 @@ if get_config("debug", False):
|
|||||||
def on_modified(self, event):
|
def on_modified(self, event):
|
||||||
if event.src_path.endswith(src_excludes_extensions) or event.is_directory or "__pycache__" in event.src_path:
|
if event.src_path.endswith(src_excludes_extensions) or event.is_directory or "__pycache__" in event.src_path:
|
||||||
return
|
return
|
||||||
nonebot.logger.debug(f"{event.src_path} modified, reloading bot...")
|
nonebot.logger.info(f"{event.src_path} modified, reloading bot...")
|
||||||
Reloader.reload()
|
Reloader.reload()
|
||||||
|
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ if get_config("debug", False):
|
|||||||
Handler for resource file changes
|
Handler for resource file changes
|
||||||
"""
|
"""
|
||||||
def on_modified(self, event):
|
def on_modified(self, event):
|
||||||
nonebot.logger.debug(f"{event.src_path} modified, reloading resource...")
|
nonebot.logger.info(f"{event.src_path} modified, reloading resource...")
|
||||||
load_resources()
|
load_resources()
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
import time
|
import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
from nonebot import Bot
|
||||||
|
|
||||||
from liteyuki.utils.message.html_tool import template2image
|
from liteyuki.utils.message.html_tool import template2image
|
||||||
from .common import MessageEventModel, msg_db
|
from .common import MessageEventModel, msg_db
|
||||||
from liteyuki.utils.base.language import Language
|
from liteyuki.utils.base.language import Language
|
||||||
from liteyuki.utils.base.resource import get_path
|
from liteyuki.utils.base.resource import get_path
|
||||||
from liteyuki.utils.message.npl import convert_seconds_to_time
|
from liteyuki.utils.message.string_tool import convert_seconds_to_time
|
||||||
from contextvars import ContextVar
|
from ...utils.external.logo import get_group_icon, get_user_icon
|
||||||
|
|
||||||
|
|
||||||
async def count_msg_by_bot_id(bot_id: str) -> int:
|
async def count_msg_by_bot_id(bot_id: str) -> int:
|
||||||
@ -22,12 +26,18 @@ async def count_msg_by_bot_id(bot_id: str) -> int:
|
|||||||
return len(msg_rows)
|
return len(msg_rows)
|
||||||
|
|
||||||
|
|
||||||
async def get_stat_msg_image(duration: int, period: int, group_id: str = None, bot_id: str = None, user_id: str = None,
|
async def get_stat_msg_image(
|
||||||
ulang: Language = Language()) -> bytes:
|
duration: int,
|
||||||
|
period: int,
|
||||||
|
group_id: str = None,
|
||||||
|
bot_id: str = None,
|
||||||
|
user_id: str = None,
|
||||||
|
ulang: Language = Language()
|
||||||
|
) -> bytes:
|
||||||
"""
|
"""
|
||||||
获取统计消息
|
获取统计消息
|
||||||
Args:
|
Args:
|
||||||
ctx:
|
user_id:
|
||||||
ulang:
|
ulang:
|
||||||
bot_id:
|
bot_id:
|
||||||
group_id:
|
group_id:
|
||||||
@ -78,10 +88,11 @@ async def get_stat_msg_image(duration: int, period: int, group_id: str = None, b
|
|||||||
templates = {
|
templates = {
|
||||||
"data": [
|
"data": [
|
||||||
{
|
{
|
||||||
"name": ulang.get("stat.message")
|
"name" : ulang.get("stat.message")
|
||||||
+ f" Period {convert_seconds_to_time(period)}" + f" Duration {convert_seconds_to_time(duration)}"
|
+ f" Period {convert_seconds_to_time(period)}" + f" Duration {convert_seconds_to_time(duration)}"
|
||||||
+ (f" Group {group_id}" if group_id else "") + (f" Bot {bot_id}" if bot_id else "") + (f" User {user_id}" if user_id else ""),
|
+ (f" Group {group_id}" if group_id else "") + (f" Bot {bot_id}" if bot_id else "") + (
|
||||||
"times": timestamps,
|
f" User {user_id}" if user_id else ""),
|
||||||
|
"times" : timestamps,
|
||||||
"counts": msg_count
|
"counts": msg_count
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -89,9 +100,73 @@ async def get_stat_msg_image(duration: int, period: int, group_id: str = None, b
|
|||||||
|
|
||||||
return await template2image(get_path("templates/stat_msg.html"), templates)
|
return await template2image(get_path("templates/stat_msg.html"), templates)
|
||||||
|
|
||||||
# if not timestamps or period_start_time != timestamps[-1]:
|
|
||||||
# timestamps.append(period_start_time)
|
async def get_stat_rank_image(
|
||||||
# msg_count.append(1)
|
rank_type: str,
|
||||||
# else:
|
limit: dict[str, Any],
|
||||||
# msg_count[-1] += 1
|
ulang: Language = Language(),
|
||||||
#
|
bot: Bot = None,
|
||||||
|
) -> bytes:
|
||||||
|
if rank_type == "user":
|
||||||
|
condition = "user_id != ''"
|
||||||
|
condition_args = []
|
||||||
|
else:
|
||||||
|
condition = "group_id != ''"
|
||||||
|
condition_args = []
|
||||||
|
|
||||||
|
for k, v in limit.items():
|
||||||
|
match k:
|
||||||
|
case "user_id":
|
||||||
|
condition += " AND user_id = ?"
|
||||||
|
condition_args.append(v)
|
||||||
|
case "group_id":
|
||||||
|
condition += " AND group_id = ?"
|
||||||
|
condition_args.append(v)
|
||||||
|
case "bot_id":
|
||||||
|
condition += " AND bot_id = ?"
|
||||||
|
condition_args.append(v)
|
||||||
|
case "duration":
|
||||||
|
condition += " AND time > ?"
|
||||||
|
condition_args.append(v)
|
||||||
|
|
||||||
|
msg_rows = msg_db.where_all(
|
||||||
|
MessageEventModel(),
|
||||||
|
condition,
|
||||||
|
*condition_args
|
||||||
|
)
|
||||||
|
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
name: string, # user name or group name
|
||||||
|
count: int, # message count
|
||||||
|
icon: string # icon url
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
if rank_type == "user":
|
||||||
|
ranking_counter = Counter([msg.user_id for msg in msg_rows])
|
||||||
|
else:
|
||||||
|
ranking_counter = Counter([msg.group_id for msg in msg_rows])
|
||||||
|
sorted_data = sorted(ranking_counter.items(), key=lambda x: x[1], reverse=True)
|
||||||
|
|
||||||
|
ranking: list[dict[str, Any]] = [
|
||||||
|
{
|
||||||
|
"name" : _[0],
|
||||||
|
"count": _[1],
|
||||||
|
"icon" : await (get_group_icon(platform="qq", group_id=_[0]) if rank_type == "group" else get_user_icon(
|
||||||
|
platform="qq", user_id=_[0]
|
||||||
|
))
|
||||||
|
}
|
||||||
|
for _ in sorted_data[0:min(len(sorted_data), limit["rank"])]
|
||||||
|
]
|
||||||
|
|
||||||
|
templates = {
|
||||||
|
"data":
|
||||||
|
{
|
||||||
|
"name" : ulang.get("stat.rank") + f" Type {rank_type}" + f" Limit {limit}",
|
||||||
|
"ranking": ranking
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return await template2image(get_path("templates/stat_rank.html"), templates, debug=True)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from nonebot import Bot, require
|
from nonebot import Bot, require
|
||||||
from liteyuki.utils.message.npl import convert_duration, convert_time_to_seconds
|
from liteyuki.utils.message.string_tool import convert_duration, convert_time_to_seconds
|
||||||
from .stat_api import *
|
from .stat_api import *
|
||||||
from liteyuki.utils import event as event_utils
|
from liteyuki.utils import event as event_utils
|
||||||
from liteyuki.utils.base.language import Language
|
from liteyuki.utils.base.language import Language
|
||||||
@ -7,7 +7,16 @@ from liteyuki.utils.base.ly_typing import T_MessageEvent
|
|||||||
|
|
||||||
require("nonebot_plugin_alconna")
|
require("nonebot_plugin_alconna")
|
||||||
|
|
||||||
from nonebot_plugin_alconna import UniMessage, on_alconna, Alconna, Args, Subcommand, Arparma, Option
|
from nonebot_plugin_alconna import (
|
||||||
|
UniMessage,
|
||||||
|
on_alconna,
|
||||||
|
Alconna,
|
||||||
|
Args,
|
||||||
|
Subcommand,
|
||||||
|
Arparma,
|
||||||
|
Option,
|
||||||
|
MultiVar
|
||||||
|
)
|
||||||
|
|
||||||
stat_msg = on_alconna(
|
stat_msg = on_alconna(
|
||||||
Alconna(
|
Alconna(
|
||||||
@ -42,6 +51,33 @@ stat_msg = on_alconna(
|
|||||||
),
|
),
|
||||||
alias={"msg", "m"},
|
alias={"msg", "m"},
|
||||||
help_text="查看统计次数内的消息"
|
help_text="查看统计次数内的消息"
|
||||||
|
),
|
||||||
|
Subcommand(
|
||||||
|
"rank",
|
||||||
|
Option(
|
||||||
|
"-u|--user",
|
||||||
|
help_text="以用户为指标",
|
||||||
|
),
|
||||||
|
Option(
|
||||||
|
"-g|--group",
|
||||||
|
help_text="以群组为指标",
|
||||||
|
),
|
||||||
|
Option(
|
||||||
|
"-l|--limit",
|
||||||
|
Args["limit", MultiVar(str)],
|
||||||
|
help_text="限制参数,使用key=val格式",
|
||||||
|
),
|
||||||
|
Option(
|
||||||
|
"-d|--duration",
|
||||||
|
Args["duration", str, "1d"],
|
||||||
|
help_text="统计时间",
|
||||||
|
),
|
||||||
|
Option(
|
||||||
|
"-r|--rank",
|
||||||
|
Args["rank", int, 20],
|
||||||
|
help_text="指定排名",
|
||||||
|
),
|
||||||
|
alias={"r"},
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
aliases={"stat"}
|
aliases={"stat"}
|
||||||
@ -51,7 +87,6 @@ stat_msg = on_alconna(
|
|||||||
@stat_msg.assign("message")
|
@stat_msg.assign("message")
|
||||||
async def _(result: Arparma, event: T_MessageEvent, bot: Bot):
|
async def _(result: Arparma, event: T_MessageEvent, bot: Bot):
|
||||||
ulang = Language(event_utils.get_user_id(event))
|
ulang = Language(event_utils.get_user_id(event))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
duration = convert_time_to_seconds(result.other_args.get("duration", "2d")) # 秒数
|
duration = convert_time_to_seconds(result.other_args.get("duration", "2d")) # 秒数
|
||||||
period = convert_time_to_seconds(result.other_args.get("period", "1m"))
|
period = convert_time_to_seconds(result.other_args.get("period", "1m"))
|
||||||
@ -77,3 +112,24 @@ async def _(result: Arparma, event: T_MessageEvent, bot: Bot):
|
|||||||
|
|
||||||
img = await get_stat_msg_image(duration=duration, period=period, group_id=group_id, bot_id=bot_id, user_id=user_id, ulang=ulang)
|
img = await get_stat_msg_image(duration=duration, period=period, group_id=group_id, bot_id=bot_id, user_id=user_id, ulang=ulang)
|
||||||
await stat_msg.send(UniMessage.image(raw=img))
|
await stat_msg.send(UniMessage.image(raw=img))
|
||||||
|
|
||||||
|
|
||||||
|
@stat_msg.assign("rank")
|
||||||
|
async def _(result: Arparma, event: T_MessageEvent, bot: Bot):
|
||||||
|
ulang = Language(event_utils.get_user_id(event))
|
||||||
|
rank_type = "user"
|
||||||
|
duration = convert_time_to_seconds(result.other_args.get("duration", "1d"))
|
||||||
|
print(result)
|
||||||
|
if result.subcommands.get("rank").options.get("user"):
|
||||||
|
rank_type = "user"
|
||||||
|
elif result.subcommands.get("rank").options.get("group"):
|
||||||
|
rank_type = "group"
|
||||||
|
|
||||||
|
limit = result.other_args.get("limit", {})
|
||||||
|
if limit:
|
||||||
|
limit = dict([i.split("=") for i in limit])
|
||||||
|
limit["duration"] = time.time() - duration # 起始时间戳
|
||||||
|
limit["rank"] = result.other_args.get("rank", 20)
|
||||||
|
|
||||||
|
img = await get_stat_rank_image(rank_type=rank_type, limit=limit, ulang=ulang)
|
||||||
|
await stat_msg.send(UniMessage.image(raw=img))
|
||||||
|
@ -1 +1,2 @@
|
|||||||
stat.message=统计消息
|
stat.message=统计消息
|
||||||
|
stat.rank=发言排名
|
@ -0,0 +1,25 @@
|
|||||||
|
let data = JSON.parse(document.getElementById("data").innerText) // object
|
||||||
|
|
||||||
|
const rowDiv = document.importNode(document.getElementById("row-template").content, true)
|
||||||
|
|
||||||
|
function randomHideChar(str) {
|
||||||
|
// 随机隐藏6位以上字符串的中间连续四位字符,用*代替
|
||||||
|
if (str.length <= 6) {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
let start = Math.floor(str.length / 2) - 2
|
||||||
|
return str.slice(0, start) + "****" + str.slice(start + 4)
|
||||||
|
}
|
||||||
|
data["ranking"].forEach((item) => {
|
||||||
|
let row = rowDiv.cloneNode(true)
|
||||||
|
let rowID = item["name"]
|
||||||
|
let rowIconSrc = item["icon"]
|
||||||
|
let rowCount = item["count"]
|
||||||
|
|
||||||
|
row.querySelector(".row-name").innerText = randomHideChar(rowID)
|
||||||
|
row.querySelector(".row-icon").src = rowIconSrc
|
||||||
|
row.querySelector(".row-count").innerText = rowCount
|
||||||
|
|
||||||
|
document.body.appendChild(row)
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,54 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh" xmlns="http://www.w3.org/1999/html">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Liteyuki Stats Message</title>
|
||||||
|
<link rel="stylesheet" href="./css/card.css">
|
||||||
|
<link rel="stylesheet" href="./css/fonts.css">
|
||||||
|
<link rel="stylesheet" href="./css/stat_rank.css">
|
||||||
|
<style>
|
||||||
|
.row {
|
||||||
|
height: 100px;
|
||||||
|
display: flex;
|
||||||
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
|
border-radius: 100px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-name {
|
||||||
|
font-size: 40px;
|
||||||
|
align-content: center;
|
||||||
|
width: 100px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-icon {
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-count {
|
||||||
|
align-content: center;
|
||||||
|
font-size: 40px;
|
||||||
|
/* 靠右*/
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<template id="row-template">
|
||||||
|
<div class="row">
|
||||||
|
<img src="./img/arrow-up.svg" alt="up" class="row-icon">
|
||||||
|
<div class="row-name"></div>
|
||||||
|
<div class="row-count"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="data-storage" id="data">{{ data | tojson }}</div>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/5.5.0/echarts.min.js"></script>
|
||||||
|
<script src="./js/stat_rank.js"></script>
|
||||||
|
<script src="./js/card.js"></script>
|
||||||
|
</body>
|
0
liteyuki/utils/external/__init__.py
vendored
Normal file
0
liteyuki/utils/external/__init__.py
vendored
Normal file
40
liteyuki/utils/external/logo.py
vendored
Normal file
40
liteyuki/utils/external/logo.py
vendored
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
async def get_user_icon(platform: str, user_id: str) -> str:
|
||||||
|
"""
|
||||||
|
获取用户头像
|
||||||
|
Args:
|
||||||
|
platform: qq, telegram, discord...
|
||||||
|
user_id: 1234567890
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 头像链接
|
||||||
|
"""
|
||||||
|
match platform:
|
||||||
|
case "qq":
|
||||||
|
return f"http://q1.qlogo.cn/g?b=qq&nk={user_id}&s=640"
|
||||||
|
case "telegram":
|
||||||
|
return f"https://t.me/i/userpic/320/{user_id}.jpg"
|
||||||
|
case "discord":
|
||||||
|
return f"https://cdn.discordapp.com/avatars/{user_id}/"
|
||||||
|
case _:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
async def get_group_icon(platform: str, group_id: str) -> str:
|
||||||
|
"""
|
||||||
|
获取群组头像
|
||||||
|
Args:
|
||||||
|
platform: qq, telegram, discord...
|
||||||
|
group_id: 1234567890
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 头像链接
|
||||||
|
"""
|
||||||
|
match platform:
|
||||||
|
case "qq":
|
||||||
|
return f"http://p.qlogo.cn/gh/{group_id}/{group_id}/640"
|
||||||
|
case "telegram":
|
||||||
|
return f"https://t.me/c/{group_id}/"
|
||||||
|
case "discord":
|
||||||
|
return f"https://cdn.discordapp.com/icons/{group_id}/"
|
||||||
|
case _:
|
||||||
|
return ""
|
0
liteyuki/utils/nb/__init__.py
Normal file
0
liteyuki/utils/nb/__init__.py
Normal file
Loading…
Reference in New Issue
Block a user