diff --git a/LittlePaimon/config/models/__init__.py b/LittlePaimon/config/models/__init__.py deleted file mode 100644 index 48aad58..0000000 --- a/LittlePaimon/config/models/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .plugin import * diff --git a/LittlePaimon/config/models/plugin.py b/LittlePaimon/config/models/plugin.py deleted file mode 100644 index 1958dd4..0000000 --- a/LittlePaimon/config/models/plugin.py +++ /dev/null @@ -1,37 +0,0 @@ -from typing import Literal, DefaultDict -from collections import defaultdict -from pydantic import BaseModel - - -class Statistics(BaseModel): - """ - 插件调用统计 - """ - month: DefaultDict[int, int] = defaultdict(lambda: 0) - """月统计""" - week: DefaultDict[int, int] = defaultdict(lambda: 0) - """周统计""" - day: DefaultDict[int, int] = defaultdict(lambda: 0) - """日统计""" - - def add(self, user_id: int): - """ - 增加统计数据 - :param user_id: 用户id - """ - self.day[user_id] += 1 - self.week[user_id] += 1 - self.month[user_id] += 1 - - def clear(self, type: Literal['month', 'week', 'day']): - """ - 清除统计数据 - :param type: 统计类型 - """ - if type == 'month': - self.month.clear() - elif type == 'week': - self.week.clear() - elif type == 'day': - self.day.clear() - diff --git a/LittlePaimon/database/models/manager.py b/LittlePaimon/database/models/manager.py index 48ebfcc..ac6c19b 100644 --- a/LittlePaimon/database/models/manager.py +++ b/LittlePaimon/database/models/manager.py @@ -1,10 +1,9 @@ +import datetime from typing import List from tortoise import fields from tortoise.models import Model -from LittlePaimon.config.models import Statistics - class PluginPermission(Model): id = fields.IntField(pk=True, generated=True, auto_increment=True) @@ -18,9 +17,29 @@ class PluginPermission(Model): """插件总开关""" ban: List[int] = fields.JSONField(default=[]) """插件屏蔽列表""" - statistics: Statistics = fields.JSONField(encoder=Statistics.json, decoder=Statistics.parse_raw, default=Statistics()) - """插件调用统计""" + statistics: dict = fields.JSONField(default=dict) + """插件调用统计,废弃选项,不再使用""" class Meta: table = 'plugin_permission' + +class PluginStatistics(Model): + id = fields.IntField(pk=True, generated=True, auto_increment=True) + plugin_name: str = fields.CharField(max_length=255) + """插件名称""" + matcher_name: str = fields.CharField(max_length=255) + """命令名称""" + matcher_usage: str = fields.CharField(max_length=255, null=True) + """命令用法""" + group_id: int = fields.IntField(null=True) + """群id""" + user_id: int = fields.IntField() + """用户id""" + message_type: str = fields.CharField(max_length=10) + """消息类型,group/user""" + time: datetime.datetime = fields.DatetimeField() + """调用时间""" + + class Meta: + table = 'plugin_statistics' diff --git a/LittlePaimon/manager/database_manager/__init__.py b/LittlePaimon/manager/database_manager/__init__.py new file mode 100644 index 0000000..17cb1a0 --- /dev/null +++ b/LittlePaimon/manager/database_manager/__init__.py @@ -0,0 +1,22 @@ +import datetime +from LittlePaimon.utils import scheduler, logger +from LittlePaimon.database.models import GuessVoiceRank, PluginStatistics, DailyNoteSub, CookieCache, PublicCookie + + +@scheduler.scheduled_job('cron', hour=0, minute=0, misfire_grace_time=10) +async def _(): + now = datetime.datetime.now() + + logger.info('原神实时便签', '重置每日提醒次数限制') + await DailyNoteSub.all().update(today_remind_num=0) + + logger.info('原神Cookie', '清空每日Cookie缓存和限制') + await CookieCache.all().delete() + await PublicCookie.filter(status=2).update(status=1) + + logger.info('功能调用统计', '清除超过一个月的统计数据') + await PluginStatistics.filter(time__lt=now - datetime.timedelta(days=30)).delete() + + if now.weekday() == 0: + logger.info('原神猜语音', '清空每周排行榜') + await GuessVoiceRank.all().delete() diff --git a/LittlePaimon/manager/plugin_manager/__init__.py b/LittlePaimon/manager/plugin_manager/__init__.py index 9cf4583..d96f155 100644 --- a/LittlePaimon/manager/plugin_manager/__init__.py +++ b/LittlePaimon/manager/plugin_manager/__init__.py @@ -1,4 +1,5 @@ import asyncio +import datetime from nonebot import on_regex, on_command from nonebot.matcher import Matcher @@ -12,8 +13,9 @@ from nonebot.typing import T_State from LittlePaimon import SUPERUSERS, DRIVER from LittlePaimon.utils import logger from LittlePaimon.utils.message import CommandObjectID -from LittlePaimon.database.models import PluginPermission +from LittlePaimon.database.models import PluginPermission, PluginStatistics from .manager import PluginManager, hidden_plugins +from .model import MatcherInfo from .draw_help import draw_help plugin_manager = PluginManager() @@ -126,11 +128,26 @@ async def _(event: MessageEvent, matcher: Matcher): session_type = 'group' else: return - perm = await PluginPermission.get_or_none(name=matcher.plugin_name, session_id=session_id, session_type=session_type) + # 权限检查 + perm = await PluginPermission.get_or_none(name=matcher.plugin_name, session_id=session_id, session_type=session_type) if not perm: return if not perm.status: raise IgnoredException('插件使用权限已禁用') if isinstance(event, GroupMessageEvent) and event.user_id in perm.ban: raise IgnoredException('用户被禁止使用该插件') + + # 命令调用统计 + if matcher.plugin_name in plugin_manager.data and 'pm_name' in matcher.state: + if matcher_info := filter(lambda x: x.pm_name == matcher.state['pm_name'], plugin_manager.data[matcher.plugin_name].matchers): + matcher_info = list(matcher_info)[0] + await PluginStatistics.create(plugin_name=matcher.plugin_name, + matcher_name=matcher_info.pm_name, + matcher_usage=matcher_info.pm_usage, + group_id=event.group_id if isinstance(event, GroupMessageEvent) else None, + user_id=event.user_id, + message_type=session_type, + time=datetime.datetime.now()) + + diff --git a/LittlePaimon/manager/plugin_manager/manager.py b/LittlePaimon/manager/plugin_manager/manager.py index 660176c..35b2c00 100644 --- a/LittlePaimon/manager/plugin_manager/manager.py +++ b/LittlePaimon/manager/plugin_manager/manager.py @@ -11,10 +11,13 @@ from .model import MatcherInfo, PluginInfo, Config hidden_plugins = [ 'LittlePaimon', + 'config', 'nonebot_plugin_apscheduler', 'nonebot_plugin_gocqhttp', 'nonebot_plugin_htmlrender', + 'nonebot_plugin_imageutils', 'plugin_manager', + 'database_manager', 'admin' ] diff --git a/LittlePaimon/manager/plugin_manager/model.py b/LittlePaimon/manager/plugin_manager/model.py index 0d02e6c..749a3f2 100644 --- a/LittlePaimon/manager/plugin_manager/model.py +++ b/LittlePaimon/manager/plugin_manager/model.py @@ -57,3 +57,5 @@ class Config(BaseModel): ssbq_begin: int = Field(0, alias='实时便签停止检查开始时间') ssbq_end: int = Field(6, alias='实时便签停止检查结束时间') ssbq_check: int = Field(16, alias='实时便签检查间隔') + + AI_voice_cooldown: int = Field(10, alias='原神AI语音合成冷却时间(秒)') diff --git a/LittlePaimon/plugins/Genshin_AIVoice/__init__.py b/LittlePaimon/plugins/Genshin_AIVoice/__init__.py index 39cb026..54ab900 100644 --- a/LittlePaimon/plugins/Genshin_AIVoice/__init__.py +++ b/LittlePaimon/plugins/Genshin_AIVoice/__init__.py @@ -3,7 +3,9 @@ from nonebot import on_regex from nonebot.plugin import PluginMetadata from nonebot.params import RegexDict from nonebot.adapters.onebot.v11 import GroupMessageEvent, PrivateMessageEvent, MessageSegment -from nonebot.adapters.onebot.v11.helpers import Cooldown, CooldownIsolateLevel +from LittlePaimon.utils.tool import freq_limiter +from LittlePaimon.utils.filter import filter_msg +from LittlePaimon.manager.plugin_manager import plugin_manager as pm __plugin_meta__ = PluginMetadata( name='原神语音合成', @@ -16,26 +18,30 @@ __plugin_meta__ = PluginMetadata( } ) -voice_cmd = on_regex(r'(?P\w*)说(?P[\w,。!?、:;“”‘’〔()〕——!\?,\.`\'"\(\)\[\]{}~\s]+)', priority=20, block=True, state={ - 'pm_name': '原神语音合成', - 'pm_description': 'AI语音合成,让原神角色说任何话!', - 'pm_usage': '<角色名>说<话>', - 'pm_priority': 10 -}) +SUPPORTS_CHARA = ['派蒙', '凯亚', '安柏', '丽莎', '琴', '香菱', '枫原万叶', '迪卢克', '温迪', '可莉', '早柚', '托马', '芭芭拉', + '优菈', '云堇', '钟离', '魈', '凝光', '雷电将军', '北斗', '甘雨', '七七', '刻晴', '神里绫华', '戴因斯雷布', '雷泽', + '神里绫人', '罗莎莉亚', '阿贝多', '八重神子', '宵宫', '荒泷一斗', '九条裟罗', '夜兰', '珊瑚宫心海', '五郎', '散兵', + '女士', '达达利亚', '莫娜', '班尼特', '申鹤', '行秋', '烟绯', '久岐忍', '辛焱', '砂糖', '胡桃', '重云', '菲谢尔', + '诺艾尔', '迪奥娜', '鹿野院平藏'] + +CHARA_RE = '|'.join(SUPPORTS_CHARA) + +voice_cmd = on_regex(rf'(?P({CHARA_RE})?)说(?P[\w,。!?、:;“”‘’〔()〕——!\?,\.`\'"\(\)\[\]{{}}~\s]+)', priority=90, block=True, + state={ + 'pm_name': '原神语音合成', + 'pm_description': 'AI语音合成,让原神角色说任何话!', + 'pm_usage': '<角色名>说<话>', + 'pm_priority': 10 + }) -@voice_cmd.handle(parameterless=[Cooldown(cooldown=6, isolate_level=CooldownIsolateLevel.GROUP, prompt='冷却中...')]) +@voice_cmd.handle() async def _(event: Union[GroupMessageEvent, PrivateMessageEvent], regex_dict: dict = RegexDict()): - regex_dict['text'] = regex_dict['text'].replace('\r', '').replace('\n', '') if not regex_dict['chara']: regex_dict['chara'] = '派蒙' - elif regex_dict['chara'] not in ['派蒙', '凯亚', '安柏', '丽莎', '琴', '香菱', '枫原万叶', '迪卢克', '温迪', '可莉', '早柚', '托马', '芭芭拉', - '优菈', '云堇', '钟离', '魈', '凝光', '雷电将军', '北斗', '甘雨', '七七', '刻晴', '神里绫华', '戴因斯雷布', '雷泽', - '神里绫人', '罗莎莉亚', '阿贝多', '八重神子', '宵宫', '荒泷一斗', '九条裟罗', '夜兰', '珊瑚宫心海', '五郎', '散兵', - '女士', '达达利亚', '莫娜', '班尼特', '申鹤', '行秋', '烟绯', '久岐忍', '辛焱', '砂糖', '胡桃', '重云', '菲谢尔', - '诺艾尔', '迪奥娜', '鹿野院平藏']: - return - elif len(regex_dict['text']) > 20: - return + regex_dict['text'] = filter_msg(regex_dict['text'].replace('\r', '').replace('\n', '')) + if not freq_limiter.check(f'genshin_ai_voice_{event.group_id if isinstance(event, GroupMessageEvent) else event.user_id}'): + await voice_cmd.finish(f'原神语音合成冷却中...剩余{freq_limiter.left(f"genshin_ai_voice_{event.group_id if isinstance(event, GroupMessageEvent) else event.user_id}")}秒') + freq_limiter.start(f'genshin_ai_voice_{event.group_id if isinstance(event, GroupMessageEvent) else event.user_id}', pm.config.AI_voice_cooldown) await voice_cmd.finish(MessageSegment.record( f'http://233366.proxy.nscc-gz.cn:8888/?text={regex_dict["text"]}&speaker={regex_dict["chara"]}')) diff --git a/LittlePaimon/plugins/Genshin_Voice/handler.py b/LittlePaimon/plugins/Genshin_Voice/handler.py index a63f9d2..53f7e3b 100644 --- a/LittlePaimon/plugins/Genshin_Voice/handler.py +++ b/LittlePaimon/plugins/Genshin_Voice/handler.py @@ -63,14 +63,14 @@ async def get_rank(group_id: int): records = await GuessVoiceRank.filter(group_id=group_id, guess_time__gte=datetime.datetime.now() - datetime.timedelta(days=7)) if not records: - return '暂无排行榜数据' + return '本群本周暂无排行榜数据哦!' rank = {} for record in records: if record.user_id in rank: rank[record.user_id] += 1 else: rank[record.user_id] = 1 - msg = '猜语音排行榜\n' + msg = '本周猜语音排行榜\n' for i, (user_id, count) in enumerate(sorted(rank.items(), key=lambda x: x[1], reverse=True), start=1): msg += f'{i}.{user_id}: {count}次\n' return msg @@ -116,7 +116,4 @@ async def get_character_voice(character: str, language: str = '中'): async def get_voice_list(character: str, language: str = '中'): voice_list = await GenshinVoice.filter(character=character, language=language).all() - if not voice_list: - return MessageSegment.text(f'暂无{character}的{language}语音资源,让超级用户[更新原神语音资源]吧!') - else: - return await draw_voice_list(voice_list) + return await draw_voice_list(voice_list) if voice_list else MessageSegment.text(f'暂无{character}的{language}语音资源,让超级用户[更新原神语音资源]吧!') diff --git a/LittlePaimon/plugins/Paimon_DailyNote/handler.py b/LittlePaimon/plugins/Paimon_DailyNote/handler.py index 836703a..3133f62 100644 --- a/LittlePaimon/plugins/Paimon_DailyNote/handler.py +++ b/LittlePaimon/plugins/Paimon_DailyNote/handler.py @@ -133,9 +133,3 @@ async def check_note(): # 等待一会再检查下一个,防止检查过快 await asyncio.sleep(random.randint(4, 8)) logger.info('原神实时便签', f'树脂检查完成,共花费{round((time.time() - t) / 60, 2)}分钟') - - -@scheduler.scheduled_job('cron', hour=0, minute=0, misfire_grace_time=10) -async def _(): - logger.info('原神实时便签', '清空每日提醒次数限制') - await DailyNoteSub.all().update(today_remind_num=0) diff --git a/LittlePaimon/utils/genshin.py b/LittlePaimon/utils/genshin.py index 3f1077c..4b143ba 100644 --- a/LittlePaimon/utils/genshin.py +++ b/LittlePaimon/utils/genshin.py @@ -5,10 +5,9 @@ from typing import Optional, List, Union, Tuple import pytz from LittlePaimon.config import JSON_DATA -from LittlePaimon.database.models import PlayerInfo, Character, LastQuery, PrivateCookie, PublicCookie, CookieCache, \ - AbyssInfo +from LittlePaimon.database.models import PlayerInfo, Character, LastQuery, PrivateCookie, AbyssInfo from LittlePaimon.database.models import Artifact, CharacterProperty, Artifacts, Talents, Talent -from LittlePaimon.utils import logger, scheduler +from LittlePaimon.utils import logger from LittlePaimon.utils.files import load_json from LittlePaimon.utils.api import get_enka_data, get_mihoyo_public_data, get_mihoyo_private_data from LittlePaimon.utils.typing import DataSourceType @@ -394,10 +393,3 @@ class GenshinTools: if '防御力' in effective and '防御力' in prop_name: return True return prop_name in effective - - -@scheduler.scheduled_job('cron', hour=0, minute=0, misfire_grace_time=10) -async def _(): - logger.info('原神Cookie', '清空每日Cookie缓存和限制') - await CookieCache.all().delete() - await PublicCookie.filter(status=2).update(status=1) diff --git a/LittlePaimon/utils/message.py b/LittlePaimon/utils/message.py index fe7f6b2..5b6d14c 100644 --- a/LittlePaimon/utils/message.py +++ b/LittlePaimon/utils/message.py @@ -8,7 +8,6 @@ from typing import Union, Optional, Tuple, List from PIL import Image from nonebot import get_bot from nonebot.adapters.onebot.v11 import MessageEvent, Message, MessageSegment, GroupMessageEvent -from nonebot.internal.params import Arg from nonebot.matcher import Matcher from nonebot.params import CommandArg, Depends from nonebot.typing import T_State @@ -18,7 +17,8 @@ from LittlePaimon.utils import aiorequests, load_image from LittlePaimon.utils.alias import get_match_alias from LittlePaimon.utils.image import PMImage from LittlePaimon.utils.filter import filter_msg -from LittlePaimon.utils.typing import CHARACTERS, MALE_CHARACTERS, FEMALE_CHARACTERS, GIRL_CHARACTERS, BOY_CHARACTERS, LOLI_CHARACTERS +from LittlePaimon.utils.typing import CHARACTERS, MALE_CHARACTERS, FEMALE_CHARACTERS, GIRL_CHARACTERS, BOY_CHARACTERS, \ + LOLI_CHARACTERS class MessageBuild: @@ -60,48 +60,6 @@ class MessageBuild: img.save(bio, format='JPEG' if img.mode == 'RGB' else 'PNG', quality=quality) return MessageSegment.image(bio) - @classmethod - async def StaticImage(cls, - url: str, - size: Optional[Tuple[int, int]] = None, - crop: Optional[Tuple[int, int, int, int]] = None, - quality: Optional[int] = 100, - mode: Optional[str] = None, - tips: Optional[str] = None, - is_check_time: Optional[bool] = True, - check_time_day: Optional[int] = 3 - ): - """ - 从url下载图片,并预处理并构造成MessageSegment,如果url的图片已存在本地,则直接读取本地图片 - :param url: 图片url - :param size: 预处理尺寸 - :param crop: 预处理裁剪大小 - :param quality: 预处理图片质量 - :param mode: 预处理图像模式 - :param tips: url中不存在该图片时的提示语 - :param is_check_time: 是否检查本地图片最后修改时间 - :param check_time_day: 检查本地图片最后修改时间的天数,超过该天数则重新下载图片 - :return: MessageSegment.image - """ - path = Path() / 'resources' / url - if path.exists() and ( - not is_check_time or (is_check_time and not check_time(path.stat().st_mtime, check_time_day))): - img = Image.open(path) - else: - path.parent.mkdir(parents=True, exist_ok=True) - img = await aiorequests.get_img(url='https://static.cherishmoon.fun/' + url, save_path=path) - if img == 'No Such File': - return MessageBuild.Text(tips or '缺少该静态资源') - if size: - img = img.resize(size) - if crop: - img = img.crop(crop) - if mode: - img = img.convert(mode) - bio = BytesIO() - img.save(bio, format='JPEG' if img.mode == 'RGB' else 'PNG', quality=quality) - return MessageSegment.image(bio) - @classmethod def Text(cls, text: str) -> MessageSegment: """ @@ -134,21 +92,6 @@ class MessageBuild: def Video(cls, path: str) -> MessageSegment: return MessageSegment.video(path) - @classmethod - async def StaticVideo(cls, url: str) -> MessageSegment: - """ - 从url中下载视频文件,并构造成MessageSegment,如果本地已有该视频文件,则直接读取本地文件 - :param url: 视频url - :return: MessageSegment.video - """ - path = Path() / 'data' / url - if not path.exists(): - path.parent.mkdir(parents=True, exist_ok=True) - resp = await aiorequests.get(url=f'https://static.cherishmoon.fun/{url}') - content = resp.content - path.write_bytes(content) - return MessageSegment.video(file=path) - def CommandPlayer(limit: int = 3, only_cn: bool = True) -> List[Player]: """ @@ -385,18 +328,6 @@ def replace_all(raw_text: str, text_list: Union[str, list]) -> str: return raw_text -# def transform_uid(msg: Union[Message, str]) -> Union[List[str], str, None]: -# if isinstance(msg, Message): -# msg = msg.extract_plain_text().strip() -# check_uid = msg.split(' ') -# uid_list = [] -# for check in check_uid: -# uid = re.search(r'(?P(1|2|5)\d{8})', check) -# if uid: -# uid_list.append(uid['uid']) -# return uid_list if len(uid_list) > 1 else uid_list[0] if uid_list else None - - def check_time(time_stamp: float, days: int = 1): """ 检查时间戳是否在指定天数内 diff --git a/matcher_patch.py b/matcher_patch.py index d81e1e8..35a03aa 100644 --- a/matcher_patch.py +++ b/matcher_patch.py @@ -1,6 +1,6 @@ from typing import Union, Tuple, Set -from nonebot import on_command, on_regex, on_endswith, on_keyword +from nonebot import on_command, on_regex, on_endswith, on_keyword, on_startswith import nonebot """ @@ -24,6 +24,14 @@ def on_endswith_(msg: Union[str, Tuple[str, ...]], state: dict = None, *args, ** return on_endswith(msg=msg, state=state, _depth=1, *args, **kwargs) +def on_startswith_(msg: Union[str, Tuple[str, ...]], state: dict = None, *args, **kwargs): + if state is None: + state = {} + if 'pm_name' not in state: + state['pm_name'] = msg if isinstance(msg, str) else msg[0] + return on_startswith(msg=msg, state=state, _depth=1, *args, **kwargs) + + def on_regex_(pattern: str, state: dict = None, *args, **kwargs): if state is None: state = {} @@ -40,7 +48,9 @@ def on_keyword_(keywords: Set[str], state: dict = None, *args, **kwargs): return on_keyword(keywords=keywords, state=state, _depth=1, *args, **kwargs) + nonebot.on_command = on_command_ nonebot.on_regex = on_regex_ +nonebot.on_startswith = on_startswith_ nonebot.on_endswith = on_endswith_ nonebot.on_keyword = on_keyword_