diff --git a/liteyuki/liteyuki_main/dev.py b/liteyuki/liteyuki_main/dev.py index ce3da55..8a18b8a 100644 --- a/liteyuki/liteyuki_main/dev.py +++ b/liteyuki/liteyuki_main/dev.py @@ -31,7 +31,7 @@ if get_config("debug", False): def on_modified(self, event): if event.src_path.endswith(src_excludes_extensions) or event.is_directory or "__pycache__" in event.src_path: return - nonebot.logger.debug(f"{event.src_path} modified, reloading bot...") + nonebot.logger.info(f"{event.src_path} modified, reloading bot...") Reloader.reload() @@ -40,7 +40,7 @@ if get_config("debug", False): Handler for resource file changes """ 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() diff --git a/liteyuki/plugins/liteyuki_statistics/stat_api.py b/liteyuki/plugins/liteyuki_statistics/stat_api.py index 72dbbe3..3a418ad 100644 --- a/liteyuki/plugins/liteyuki_statistics/stat_api.py +++ b/liteyuki/plugins/liteyuki_statistics/stat_api.py @@ -1,12 +1,16 @@ import time from typing import Any +from collections import Counter + +from nonebot import Bot + from liteyuki.utils.message.html_tool import template2image from .common import MessageEventModel, msg_db from liteyuki.utils.base.language import Language from liteyuki.utils.base.resource import get_path -from liteyuki.utils.message.npl import convert_seconds_to_time -from contextvars import ContextVar +from liteyuki.utils.message.string_tool import convert_seconds_to_time +from ...utils.external.logo import get_group_icon, get_user_icon 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) -async def get_stat_msg_image(duration: int, period: int, group_id: str = None, bot_id: str = None, user_id: str = None, - ulang: Language = Language()) -> bytes: +async def get_stat_msg_image( + duration: int, + period: int, + group_id: str = None, + bot_id: str = None, + user_id: str = None, + ulang: Language = Language() +) -> bytes: """ 获取统计消息 Args: - ctx: + user_id: ulang: bot_id: group_id: @@ -76,22 +86,87 @@ async def get_stat_msg_image(duration: int, period: int, group_id: str = None, b msg_count[index] += 1 templates = { - "data": [ - { - "name": ulang.get("stat.message") - + 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 ""), - "times": timestamps, - "counts": msg_count - } - ] + "data": [ + { + "name" : ulang.get("stat.message") + + 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 ""), + "times" : timestamps, + "counts": msg_count + } + ] } return await template2image(get_path("templates/stat_msg.html"), templates) - # if not timestamps or period_start_time != timestamps[-1]: - # timestamps.append(period_start_time) - # msg_count.append(1) - # else: - # msg_count[-1] += 1 - # + +async def get_stat_rank_image( + rank_type: str, + limit: dict[str, Any], + 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) diff --git a/liteyuki/plugins/liteyuki_statistics/stat_matchers.py b/liteyuki/plugins/liteyuki_statistics/stat_matchers.py index 74f2944..c49a251 100644 --- a/liteyuki/plugins/liteyuki_statistics/stat_matchers.py +++ b/liteyuki/plugins/liteyuki_statistics/stat_matchers.py @@ -1,5 +1,5 @@ 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 liteyuki.utils import event as event_utils from liteyuki.utils.base.language import Language @@ -7,7 +7,16 @@ from liteyuki.utils.base.ly_typing import T_MessageEvent 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( Alconna( @@ -42,6 +51,33 @@ stat_msg = on_alconna( ), alias={"msg", "m"}, 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"} @@ -51,7 +87,6 @@ stat_msg = on_alconna( @stat_msg.assign("message") async def _(result: Arparma, event: T_MessageEvent, bot: Bot): ulang = Language(event_utils.get_user_id(event)) - try: duration = convert_time_to_seconds(result.other_args.get("duration", "2d")) # 秒数 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) 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)) diff --git a/liteyuki/resources/liteyuki_statistics/lang/zh-CN.lang b/liteyuki/resources/liteyuki_statistics/lang/zh-CN.lang index 725ac8f..e84b469 100644 --- a/liteyuki/resources/liteyuki_statistics/lang/zh-CN.lang +++ b/liteyuki/resources/liteyuki_statistics/lang/zh-CN.lang @@ -1 +1,2 @@ -stat.message=统计消息 \ No newline at end of file +stat.message=统计消息 +stat.rank=发言排名 \ No newline at end of file diff --git a/liteyuki/resources/liteyuki_statistics/templates/css/stat_rank.css b/liteyuki/resources/liteyuki_statistics/templates/css/stat_rank.css new file mode 100644 index 0000000..e69de29 diff --git a/liteyuki/resources/liteyuki_statistics/templates/js/stat_rank.js b/liteyuki/resources/liteyuki_statistics/templates/js/stat_rank.js new file mode 100644 index 0000000..6404464 --- /dev/null +++ b/liteyuki/resources/liteyuki_statistics/templates/js/stat_rank.js @@ -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) +}) + diff --git a/liteyuki/resources/liteyuki_statistics/templates/stat_rank.html b/liteyuki/resources/liteyuki_statistics/templates/stat_rank.html new file mode 100644 index 0000000..d572810 --- /dev/null +++ b/liteyuki/resources/liteyuki_statistics/templates/stat_rank.html @@ -0,0 +1,54 @@ + + + + + Liteyuki Stats Message + + + + + + + + + + +
{{ data | tojson }}
+ + + + \ No newline at end of file diff --git a/liteyuki/utils/external/__init__.py b/liteyuki/utils/external/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/liteyuki/utils/external/logo.py b/liteyuki/utils/external/logo.py new file mode 100644 index 0000000..96d9192 --- /dev/null +++ b/liteyuki/utils/external/logo.py @@ -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 "" diff --git a/liteyuki/utils/message/npl.py b/liteyuki/utils/message/string_tool.py similarity index 100% rename from liteyuki/utils/message/npl.py rename to liteyuki/utils/message/string_tool.py diff --git a/liteyuki/utils/nb/__init__.py b/liteyuki/utils/nb/__init__.py new file mode 100644 index 0000000..e69de29