├── .env ├── .env.linux ├── .env.prod ├── .github └── workflows │ └── sync2gitee.yml ├── .gitignore ├── Dockerfile ├── Dockerfile_pi ├── LICENSE ├── README.md ├── bot.py ├── docker-compose.yml ├── pyproject.toml ├── requirements.txt └── src └── plugins └── DicePP ├── __init__.py ├── adapter ├── __init__.py ├── client_proxy.py └── nonebot_adapter.py ├── core ├── __init__.py ├── bot │ ├── __init__.py │ ├── dicebot.py │ ├── macro.py │ └── variable.py ├── command │ ├── __init__.py │ ├── bot_cmd.py │ ├── const.py │ ├── template_cmd.py │ ├── unit_test.py │ └── user_cmd.py ├── communication │ ├── __init__.py │ ├── info.py │ ├── message.py │ ├── notice.py │ ├── port.py │ ├── process.py │ └── request.py ├── config │ ├── __init__.py │ ├── basic.py │ ├── common.py │ ├── config_item.py │ ├── declare.py │ └── manager.py ├── data │ ├── __init__.py │ ├── basic.py │ ├── data_chunk.py │ ├── json_object.py │ ├── manager.py │ └── unit_test.py ├── localization │ ├── __init__.py │ ├── common.py │ ├── localization_text.py │ └── manager.py └── statistics │ ├── __init__.py │ ├── basic_stat.py │ ├── group_stat.py │ └── user_stat.py ├── module ├── __init__.py ├── character │ ├── __init__.py │ └── dnd5e │ │ ├── __init__.py │ │ ├── ability.py │ │ ├── char_command.py │ │ ├── character.py │ │ ├── health.py │ │ ├── hp_command.py │ │ ├── money.py │ │ └── spell.py ├── common │ ├── __init__.py │ ├── activate_command.py │ ├── chat_command.py │ ├── help_command.py │ ├── macro_command.py │ ├── master_command.py │ ├── nickname_command.py │ ├── point_command.py │ ├── variable_command.py │ └── welcome_command.py ├── deck │ ├── __init__.py │ ├── deck_command.py │ ├── random_generator_command.py │ └── random_generator_data.py ├── dice_hub │ ├── __init__.py │ ├── data.py │ ├── encrypt.py │ ├── hub_command.py │ └── manager.py ├── fastapi │ ├── __init__.py │ └── api.py ├── initiative │ ├── __init__.py │ ├── initiative_command.py │ ├── initiative_entity.py │ └── initiative_list.py ├── misc │ ├── __init__.py │ ├── dnd_command.py │ ├── jrrp_command.py │ └── statistics_cmd.py ├── query │ ├── __init__.py │ └── query_command.py └── roll │ ├── __init__.py │ ├── connector.py │ ├── expression.py │ ├── modifier.py │ ├── result.py │ ├── roll_config.py │ ├── roll_dice_command.py │ ├── roll_utils.py │ └── unit_test.py └── utils ├── __init__.py ├── cq_code.py ├── data.py ├── localdata.py ├── logger.py ├── string.py └── time.py /.env: -------------------------------------------------------------------------------- 1 | COMMAND_START=[""] 2 | COMMAND_SEP=[""] -------------------------------------------------------------------------------- /.env.linux: -------------------------------------------------------------------------------- 1 | HOST=0.0.0.0 2 | PORT=8080 -------------------------------------------------------------------------------- /.env.prod: -------------------------------------------------------------------------------- 1 | HOST=127.0.0.1 2 | PORT=8080 3 | SECRET= 4 | ACCESS_TOKEN= 5 | -------------------------------------------------------------------------------- /.github/workflows/sync2gitee.yml: -------------------------------------------------------------------------------- 1 | # 通过 Github action, 在仓库的每一次 commit 后自动同步到 Gitee 上 2 | name: sync2gitee 3 | on: 4 | push: 5 | branches: 6 | - master 7 | jobs: 8 | repo-sync: 9 | env: 10 | dst_key: ${{ secrets.GITEE_PRIVATE_KEY }} 11 | dst_token: ${{ secrets.GITEE_TOKEN }} 12 | gitee_user: ${{ secrets.GITEE_USER }} 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | with: 17 | persist-credentials: false 18 | 19 | - name: sync github -> gitee 20 | uses: Yikun/hub-mirror-action@master 21 | if: env.dst_key && env.dst_token && env.gitee_user 22 | with: 23 | # 必选,需要同步的 Github 用户(源) 24 | src: 'github/${{ github.repository_owner }}' 25 | # 必选,需要同步到的 Gitee 用户(目的) 26 | dst: 'gitee/${{ secrets.GITEE_USER }}' 27 | # 必选,Gitee公钥对应的私钥,https://gitee.com/profile/sshkeys 28 | dst_key: ${{ secrets.GITEE_PRIVATE_KEY }} 29 | # 必选,Gitee对应的用于创建仓库的token,https://gitee.com/profile/personal_access_tokens 30 | dst_token: ${{ secrets.GITEE_TOKEN }} 31 | # 如果是组织,指定组织即可,默认为用户 user 32 | # account_type: org 33 | # 直接取当前项目的仓库名 34 | static_list: ${{ github.event.repository.name }} 35 | # 还有黑、白名单,静态名单机制,可以用于更新某些指定库 36 | # static_list: 'repo_name,repo_name2' 37 | # black_list: 'repo_name,repo_name2' 38 | # white_list: 'repo_name,repo_name2' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/python 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 4 | 5 | ### Python ### 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | pip-wheel-metadata/ 29 | share/python-wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | MANIFEST 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .nox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | *.py,cover 56 | .hypothesis/ 57 | .pytest_cache/ 58 | pytestdebug.log 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | local_settings.py 67 | db.sqlite3 68 | db.sqlite3-journal 69 | 70 | # Flask stuff: 71 | instance/ 72 | .webassets-cache 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | doc/_build/ 80 | 81 | # PyBuilder 82 | target/ 83 | 84 | # Jupyter Notebook 85 | .ipynb_checkpoints 86 | 87 | # IPython 88 | profile_default/ 89 | ipython_config.py 90 | 91 | # pyenv 92 | .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 102 | __pypackages__/ 103 | 104 | # Celery stuff 105 | celerybeat-schedule 106 | celerybeat.pid 107 | 108 | # SageMath parsed files 109 | *.sage.py 110 | 111 | # Environments 112 | # .env # For Nonebot env 113 | .venv 114 | env/ 115 | venv/ 116 | ENV/ 117 | env.bak/ 118 | venv.bak/ 119 | 120 | # Spyder project settings 121 | .spyderproject 122 | .spyproject 123 | 124 | # Rope project settings 125 | .ropeproject 126 | 127 | # mkdocs documentation 128 | /site 129 | 130 | # mypy 131 | .mypy_cache/ 132 | .dmypy.json 133 | dmypy.json 134 | 135 | # Pyre type checker 136 | .pyre/ 137 | 138 | # pytype static type analyzer 139 | .pytype/ 140 | 141 | # End of https://www.toptal.com/developers/gitignore/api/python 142 | 143 | # PyCharm 144 | .idea 145 | 146 | # VS Code 147 | .vscode 148 | 149 | # DicePP 150 | src/plugins/DicePP/Data/ 151 | 152 | # Poetry 153 | poetry.lock 154 | poetry.toml -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tiangolo/uvicorn-gunicorn-fastapi:python3.8 2 | 3 | RUN python3 -m pip config set global.index-url https://mirrors.aliyun.com/pypi/simple 4 | 5 | RUN python3 -m pip install poetry && poetry config virtualenvs.create false --local 6 | 7 | COPY ./pyproject.toml ./poetry.lock* /app/ 8 | 9 | RUN poetry install --no-root --no-dev -------------------------------------------------------------------------------- /Dockerfile_pi: -------------------------------------------------------------------------------- 1 | FROM python:3.8 2 | 3 | RUN pip config set global.index-url https://mirrors.aliyun.com/pypi/simple 4 | 5 | WORKDIR /DicePP 6 | 7 | COPY ./requirements.txt /DicePP/requirements.txt 8 | 9 | RUN pip install --no-cache-dir --upgrade -r /DicePP/requirements.txt 10 | 11 | COPY ./ /DicePP 12 | 13 | CMD ["python", "/DicePP/bot.py"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 pear-studio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nonebot-dicepp 2 | 基于Python的DND骰娘机器人, 可作为机器人项目Nonebot的插件使用 3 | 4 | 请加入交流群861919492获取整合包和部署指南 5 | -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import sys 5 | import nonebot 6 | from nonebot.adapters.onebot.v11 import Adapter as OneBot_V11_Adapter 7 | 8 | # Fix import path problem in server 9 | dir_path = os.path.abspath(os.path.dirname(__file__)) 10 | sys.path.insert(0, dir_path) 11 | 12 | # Custom your logger 13 | # 14 | # from nonebot.log import logger, default_format 15 | # logger.add("error.log", 16 | # rotation="00:00", 17 | # diagnose=False, 18 | # level="ERROR", 19 | # format=default_format) 20 | 21 | # You can pass some keyword args config to init function 22 | nonebot.init() 23 | nonebot.load_plugins("DicePP/plugins") 24 | app = nonebot.get_asgi() 25 | 26 | driver = nonebot.get_driver() 27 | driver.register_adapter(OneBot_V11_Adapter) 28 | 29 | nonebot.load_from_toml("pyproject.toml") 30 | # Modify some config / config depends on loaded configs 31 | # 32 | # config = driver.config 33 | # do something... 34 | 35 | 36 | if __name__ == "__main__": 37 | # nonebot.logger.warning("Always use `nb run` to start the bot instead of manually running!") 38 | nonebot.run(app="__mp_main__:app") 39 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | # 其他配置参考 https://hub.docker.com/r/tiangolo/uvicorn-gunicorn-fastapi/ 4 | nonebot: 5 | container_name: dicepp 6 | build: . 7 | volumes: 8 | - "/etc/localtime:/etc/localtime:ro" 9 | - "./:/app/" 10 | # ports: 11 | # - "8080:8080" # 映射端口到宿主机 宿主机端口:容器端口 12 | env_file: 13 | - ".env.linux" # fastapi 使用的环境变量文件 14 | environment: 15 | - ENVIRONMENT=linux # 配置 nonebot 运行环境,此项会被 .env 文件覆盖 16 | - APP_MODULE=bot:app # 配置 asgi 入口 17 | - SECRET # 通过 SECRET=xxx nb up -d 传递密钥 18 | - ACCESS_TOKEN # 通过 ACCESS_TOKEN=xxx nb up -d 传递密钥 19 | - MAX_WORKERS=1 # 如果你有多个QQ,且存在 self_id 指定,多个 worker 会导致无法找到其他 websocket 连接 20 | logging: 21 | driver: json-file 22 | options: 23 | max-size: "200k" 24 | max-file: "10" 25 | networks: 26 | default: 27 | external: 28 | name: dice-net -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "DicePP" 3 | version = "1.0.0" 4 | description = "DicePP" 5 | authors = ["Pear"] 6 | readme = "README.md" 7 | 8 | [tool.nonebot] 9 | python = "^3.8" 10 | nonebot2 = "^2.0.0b1" 11 | nb-cli = "^0.6.4" 12 | openpyxl = "^3.0.9" 13 | rsa = "^4.8" 14 | nonebot-adapter-cqhttp = "^2.0.0b1" 15 | gitpython = "3.1.26" 16 | 17 | [nonebot.plugins] 18 | plugins = [] 19 | plugin_dirs = ["src/plugins"] 20 | 21 | [build-system] 22 | requires = ["poetry>=0.12"] 23 | build-backend = "poetry.masonry.api" 24 | 25 | [[tool.poetry.source]] 26 | name = "tsinghua" 27 | default = true 28 | url = "https://pypi.tuna.tsinghua.edu.cn/simple" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi>=0.68.0 2 | pydantic>=1.8.0 3 | uvicorn>=0.15.0 4 | nb-cli==0.6.4 5 | nonebot-adapter-onebot==2.0.0b1 6 | nonebot2==2.0.0b1 7 | openpyxl==3.0.9 8 | rsa==4.8 9 | gitpython==3.1.26 -------------------------------------------------------------------------------- /src/plugins/DicePP/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | sys.path.append(os.path.abspath(os.path.dirname(__file__))) 4 | 5 | from adapter.nonebot_adapter import * 6 | -------------------------------------------------------------------------------- /src/plugins/DicePP/adapter/__init__.py: -------------------------------------------------------------------------------- 1 | from adapter.client_proxy import ClientProxy 2 | import adapter.nonebot_adapter 3 | -------------------------------------------------------------------------------- /src/plugins/DicePP/adapter/client_proxy.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from typing import List 3 | 4 | from core.command import BotCommandBase 5 | from core.communication import GroupInfo, GroupMemberInfo 6 | 7 | 8 | class ClientProxy(metaclass=abc.ABCMeta): 9 | @abc.abstractmethod 10 | async def process_bot_command(self, command: BotCommandBase): 11 | pass 12 | 13 | @abc.abstractmethod 14 | async def process_bot_command_list(self, command_list: List[BotCommandBase]): 15 | pass 16 | 17 | @abc.abstractmethod 18 | async def get_group_list(self) -> List[GroupInfo]: 19 | pass 20 | 21 | @abc.abstractmethod 22 | async def get_group_info(self, group_id: str) -> GroupInfo: 23 | pass 24 | 25 | @abc.abstractmethod 26 | async def get_group_member_list(self, group_id: str) -> List[GroupMemberInfo]: 27 | pass 28 | 29 | @abc.abstractmethod 30 | async def get_group_member_info(self, group_id: str, user_id: str) -> GroupMemberInfo: 31 | pass 32 | -------------------------------------------------------------------------------- /src/plugins/DicePP/adapter/nonebot_adapter.py: -------------------------------------------------------------------------------- 1 | """ 2 | NoneBot API https://v2.nonebot.dev/api/plugin.html 3 | """ 4 | from typing import List, Dict, Optional 5 | import asyncio 6 | from fastapi import FastAPI 7 | 8 | import nonebot 9 | from nonebot import on_message, on_notice, on_request 10 | from nonebot.rule import Rule 11 | from nonebot.adapters.onebot.v11.event import MessageEvent, PrivateMessageEvent, GroupMessageEvent 12 | from nonebot.adapters.onebot.v11.event import NoticeEvent, GroupIncreaseNoticeEvent, FriendAddNoticeEvent 13 | from nonebot.adapters.onebot.v11.event import RequestEvent, FriendRequestEvent, GroupRequestEvent 14 | from nonebot.adapters.onebot.v11.bot import Bot as NoneBot 15 | from nonebot.adapters.onebot.v11 import Message as CQMessage 16 | from nonebot.adapters.onebot.v11 import ActionFailed 17 | 18 | from core.bot import Bot as DicePPBot 19 | from core.communication import MessageMetaData, MessageSender, GroupMemberInfo, GroupInfo 20 | from core.communication import NoticeData, FriendAddNoticeData, GroupIncreaseNoticeData 21 | from core.communication import RequestData, FriendRequestData, JoinGroupRequestData, InviteGroupRequestData 22 | from core.command import BotCommandBase, BotSendMsgCommand, BotDelayCommand, BotLeaveGroupCommand 23 | from utils.logger import dice_log 24 | 25 | from adapter.client_proxy import ClientProxy 26 | 27 | from module.fastapi import dpp_api 28 | 29 | try: 30 | app: FastAPI = nonebot.get_app() 31 | app.mount("/dpp", dpp_api) 32 | except ValueError: 33 | dice_log("DPP API is not amounted because NoneBot has not been initialized") 34 | 35 | command_matcher = on_message(block=False) 36 | notice_matcher = on_notice() 37 | request_matcher = on_request() 38 | 39 | all_bots: Dict[str, DicePPBot] = {} 40 | 41 | 42 | def convert_group_info(nb_group_info: Dict) -> GroupInfo: 43 | res = GroupInfo(group_id=str(nb_group_info["group_id"])) 44 | res.group_name = nb_group_info["group_name"] 45 | res.member_count = nb_group_info["member_count"] 46 | res.max_member_count = nb_group_info["max_member_count"] 47 | return res 48 | 49 | 50 | def convert_group_member_info(nb_group_member_info: Dict) -> GroupMemberInfo: 51 | res = GroupMemberInfo(group_id=str(nb_group_member_info["group_id"]), user_id=str(nb_group_member_info["user_id"])) 52 | res.nickname = nb_group_member_info["nickname"] 53 | res.card = nb_group_member_info["card"] 54 | res.role = nb_group_member_info["role"] 55 | res.title = nb_group_member_info["title"] 56 | return res 57 | 58 | 59 | class NoneBotClientProxy(ClientProxy): 60 | def __init__(self, bot: NoneBot): 61 | self.bot = bot 62 | 63 | # noinspection PyBroadException 64 | async def process_bot_command(self, command: BotCommandBase): 65 | dice_log(f"[OneBot] [BotCommand] {command}") 66 | try: 67 | if isinstance(command, BotSendMsgCommand): 68 | for target in command.targets: 69 | if target.group_id: 70 | await self.bot.send_group_msg(group_id=int(target.group_id), message=CQMessage(command.msg)) 71 | else: 72 | await self.bot.send_private_msg(user_id=int(target.user_id), message=CQMessage(command.msg)) 73 | elif isinstance(command, BotLeaveGroupCommand): 74 | await self.bot.set_group_leave(group_id=int(command.target_group_id)) 75 | elif isinstance(command, BotDelayCommand): 76 | await asyncio.sleep(command.seconds) 77 | else: 78 | raise NotImplementedError("未定义的BotCommand类型") 79 | except ActionFailed as e: 80 | dice_log(f"[OneBot] [ActionFailed] {e}") 81 | except Exception as e: 82 | dice_log(f"[OneBot] [UnknownException] {e}") 83 | 84 | async def process_bot_command_list(self, command_list: List[BotCommandBase]): 85 | if len(command_list) > 1: 86 | log_str = "\n".join([str(command) for command in command_list]) 87 | dice_log(f"[Proxy Bot Command List]\n[{log_str}]") 88 | for command in command_list: 89 | await self.process_bot_command(command) 90 | 91 | async def get_group_list(self) -> List[GroupInfo]: 92 | group_info_list: List[Dict] = await self.bot.get_group_list() 93 | return [convert_group_info(info) for info in group_info_list] 94 | 95 | async def get_group_info(self, group_id: str) -> GroupInfo: 96 | group_info: Dict = await self.bot.get_group_info(group_id=int(group_id)) 97 | return convert_group_info(group_info) 98 | 99 | async def get_group_member_list(self, group_id: str) -> List[GroupMemberInfo]: 100 | group_member_list: List[Dict] = await self.bot.get_group_member_list(group_id=int(group_id)) 101 | return [convert_group_member_info(info) for info in group_member_list] 102 | 103 | async def get_group_member_info(self, group_id: str, user_id: str) -> GroupMemberInfo: 104 | group_member_info: Dict = await self.bot.get_group_member_info(group_id=int(group_id), user_id=int(user_id)) 105 | return convert_group_member_info(group_member_info) 106 | 107 | 108 | @command_matcher.handle() 109 | async def handle_command(bot: NoneBot, event: MessageEvent): 110 | cq_message = event.get_message() 111 | plain_msg = cq_message.extract_plain_text() 112 | raw_msg = str(cq_message) 113 | 114 | # 构建Meta信息 115 | group_id: str = "" 116 | user_id: str = str(event.get_user_id()) 117 | if isinstance(event, GroupMessageEvent): 118 | group_id = str(event.group_id) 119 | 120 | # log_str = f"[Proxy Message] Bot \033[0;37m{bot.self_id}\033[0m receive message \033[0;33m{raw_msg}\033[0m from " 121 | # if group_id: 122 | # log_str += f"\033[0;34m|Group: {group_id} User: {user_id}|\033[0m" 123 | # else: 124 | # log_str += f"\033[0;35m|Private: {user_id}|\033[0m" 125 | # dice_log(log_str) 126 | 127 | sender = MessageSender(user_id, event.sender.nickname) 128 | sender.sex, sender.age, sender.card = event.sender.sex, event.sender.age, event.sender.card 129 | sender.area, sender.level, sender.role = event.sender.area, event.sender.level, event.sender.role 130 | sender.title = event.sender.title 131 | 132 | to_me = event.to_me 133 | 134 | meta = MessageMetaData(plain_msg, raw_msg, sender, group_id, to_me) 135 | 136 | # 让机器人处理信息 137 | await all_bots[bot.self_id].process_message(plain_msg, meta) 138 | 139 | 140 | @notice_matcher.handle() 141 | async def handle_notice(bot: NoneBot, event: NoticeEvent): 142 | dice_log(f"[Proxy Notice] {event.get_event_name()}") 143 | 144 | # 构建data 145 | data: Optional[NoticeData] = None 146 | if event.notice_type == "group_increase": 147 | data = GroupIncreaseNoticeData(str(event.user_id), str(event.group_id), str(event.operator_id)) 148 | elif event.notice_type == "friend_add": 149 | data = FriendAddNoticeData(str(event.user_id)) 150 | 151 | # 处理消息提示 152 | if data: 153 | await all_bots[bot.self_id].process_notice(data) 154 | 155 | 156 | @request_matcher.handle() 157 | async def handle_request(bot: NoneBot, event: RequestEvent): 158 | dice_log(f"[Proxy Request] {event.get_event_name()}") 159 | 160 | # 构建data 161 | data: Optional[RequestData] = None 162 | if event.request_type == "friend": 163 | data = FriendRequestData(str(event.user_id), event.comment) 164 | elif event.request_type == "group": 165 | if event.sub_type == "add": 166 | data = JoinGroupRequestData(str(event.user_id), str(event.group_id), str(event.comment)) 167 | elif event.sub_type == "invite": 168 | data = InviteGroupRequestData(str(event.user_id), str(event.group_id), event.comment) 169 | 170 | # 处理请求 171 | if data: 172 | approve: Optional[bool] = all_bots[bot.self_id].process_request(data) 173 | if approve: 174 | await event.approve(bot) 175 | elif (approve is not None) and (not approve): 176 | await event.reject(bot) 177 | 178 | 179 | # 全局Driver 180 | try: 181 | driver = nonebot.get_driver() 182 | 183 | 184 | # 在Bot连接时调用 185 | 186 | @driver.on_bot_connect 187 | async def connect(bot: NoneBot) -> None: 188 | proxy = NoneBotClientProxy(bot) 189 | all_bots[bot.self_id] = DicePPBot(bot.self_id) 190 | all_bots[bot.self_id].set_client_proxy(proxy) 191 | await all_bots[bot.self_id].delay_init_command() 192 | dice_log(f"[NB Adapter] Bot {bot.self_id} Connected!") 193 | 194 | 195 | @driver.on_bot_disconnect 196 | async def disconnect(bot: NoneBot) -> None: 197 | await all_bots[bot.self_id].shutdown_async() 198 | del all_bots[bot.self_id] 199 | dice_log(f"[NB Adapter] Bot {bot.self_id} Disconnected!") 200 | except ValueError: 201 | dice_log("[NB Adapter] NoneBot has not been initialized") 202 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pear-studio/nonebot-dicepp/da8445b775bf9ccf47066f14d477c445cb55f2fc/src/plugins/DicePP/core/__init__.py -------------------------------------------------------------------------------- /src/plugins/DicePP/core/bot/__init__.py: -------------------------------------------------------------------------------- 1 | from core.bot.macro import BotMacro, MACRO_COMMAND_SPLIT, MACRO_PARSE_LIMIT 2 | from core.bot.variable import BotVariable 3 | 4 | from core.bot.dicebot import Bot 5 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/bot/macro.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Dict, List 3 | import re 4 | 5 | from core.data import JsonObject, custom_json_object 6 | 7 | MACRO_COMMAND_SPLIT = "%%" 8 | MACRO_PARSE_LIMIT = 500 # 宏展开以后的长度限制 9 | 10 | 11 | @custom_json_object 12 | class BotMacro(JsonObject): 13 | """ 14 | 用户自定义的宏, 相当于字符串替换 15 | 宏的定义方法: 16 | [关键字][参数列表, 形如(参数1,参数2,...), 可选][空格][目标字符串] 17 | 目标字符串中与参数同名的字符串将在使用宏时被替换为给定的参数 18 | 在定义时给定参数就必须在使用时给出, 否则不会被认定为宏 19 | 用{MACRO_COMMAND_SPLIT}来表示指令分隔符, {MACRO_COMMAND_SPLIT}左右的空格和换行将会被忽略 20 | 注意: 21 | 第一个空格的位置非常关键, 用来区分替换前的内容和替换后的内容 22 | 参数名字不要重名, 宏可以嵌套, 但不会处理递归(即不可重入), 先定义的宏会先处理 23 | 示例: 24 | 一颗D20 .rd20 25 | 掷骰两次(表达式,原因) .r 表达式 原因 {MACRO_COMMAND_SPLIT} .r 表达式 原因 26 | 宏的使用方法: 27 | [关键字][用:分隔给定参数] 28 | 输入: 一颗D20 这是一颗d20 -> 等同于: .rd20 这是一颗d20 29 | 输入: 掷骰两次:d20+2:某种原因 -> 等同于: 执行指令.r d20+2 某种原因 + 执行指令.r d20+2 某种原因 30 | """ 31 | def serialize(self) -> str: 32 | json_dict = {"raw": self.raw, "split": self.command_split} 33 | return json.dumps(json_dict) 34 | 35 | def deserialize(self, json_str: str) -> None: 36 | json_dict = json.loads(json_str) 37 | self.initialize(json_dict["raw"], json_dict["split"]) 38 | 39 | def __init__(self): 40 | self.raw: str = "" # 定义宏时的字符串 41 | self.key: str = "" # 宏的关键字 42 | self.args: List[str] = [] # 宏的参数, 为空则不需要参数 43 | self.target: str = "" # 将宏关键字替换为的对象 44 | self.command_split: str = "" 45 | self.pattern: re.Pattern = re.compile("") 46 | 47 | def initialize(self, raw: str, command_split: str): 48 | self.raw = raw # 定义宏时的字符串 49 | self.command_split = command_split 50 | # 解析定义字符串 51 | if self.raw.find(" ") == -1: 52 | raise ValueError("宏定义中缺少空格") 53 | key_args, target = self.raw.split(" ", 1) 54 | key_args: str = key_args.strip() 55 | target: str = target.strip() 56 | if key_args.endswith(")"): 57 | par_index = key_args.find("(") 58 | if par_index == -1: 59 | raise ValueError("参数列表缺少左括号!") 60 | self.key, self.args = key_args[:par_index], key_args[par_index+1:-1].split(",") 61 | else: 62 | self.key, self.args = key_args, [] 63 | 64 | for arg in self.args: 65 | target = target.replace(arg, "{"+arg+"}") 66 | target = target.replace(MACRO_COMMAND_SPLIT, self.command_split) 67 | re_pattern = ":".join([self.key] + ["(.*)"]*len(self.args)) 68 | self.pattern = re.compile(re_pattern) 69 | self.target = target 70 | 71 | def process(self, input_str: str): 72 | def handle_macro(match): 73 | res = self.target 74 | if self.args: 75 | kwargs: Dict[str, str] = {} 76 | for i in range(len(self.args)): 77 | kwargs[self.args[i]] = match.group(i+1) 78 | res = res.format(**kwargs) 79 | return res 80 | 81 | return self.pattern.sub(handle_macro, input_str) 82 | 83 | def __repr__(self): 84 | return f"Macro({self.key}, Args:{self.args} -> {self.target})" 85 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/bot/variable.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Union 3 | import re 4 | 5 | from core.data import JsonObject, custom_json_object 6 | 7 | VAR_DEP_PATTERN: re.Pattern = re.compile(r"%(.+)%") 8 | 9 | 10 | @custom_json_object 11 | class BotVariable(JsonObject): 12 | """ 13 | 用户自定义的变量, 代表一个数字, 可以依赖其他变量 14 | """ 15 | def serialize(self) -> str: 16 | json_dict = {"name": self.name, "val": self.val} 17 | return json.dumps(json_dict) 18 | 19 | def deserialize(self, json_str: str) -> None: 20 | json_dict = json.loads(json_str) 21 | self.initialize(json_dict["name"], json_dict["val"]) 22 | 23 | def __init__(self): 24 | self.name = "VAR" 25 | self.val: Union[int, str] = 0 26 | self.dep = [] 27 | self.is_num: bool = True 28 | 29 | def initialize(self, name: str, val: Union[int, str]): 30 | self.name = name 31 | self.val = val 32 | if isinstance(val, int): 33 | return 34 | # 是字符串, 要依赖其他变量 35 | assert isinstance(val, str) 36 | self.is_num = False 37 | 38 | def handle_dep(match): 39 | self.dep.append(match.group(1)) 40 | return match.group(0) 41 | self.val = VAR_DEP_PATTERN.sub(handle_dep, val) 42 | 43 | def __repr__(self): 44 | return f"Var({self.name} = {self.val})" 45 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/command/__init__.py: -------------------------------------------------------------------------------- 1 | from core.command.const import * 2 | from core.command.bot_cmd import BotCommandBase, BotSendMsgCommand, BotLeaveGroupCommand, BotDelayCommand 3 | from core.command.user_cmd import CommandError, UserCommandBase, custom_user_command 4 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/command/bot_cmd.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import re 3 | from typing import List 4 | 5 | from core.communication import MessagePort 6 | 7 | 8 | class BotCommandBase(metaclass=abc.ABCMeta): 9 | """ 10 | 所有机器人指令的基类, 包括发送消息, 退群等等 11 | """ 12 | def __str__(self): 13 | return str(self.__class__.__name__) 14 | 15 | 16 | class BotSendMsgCommand(BotCommandBase): 17 | """ 18 | 发送聊天消息 19 | """ 20 | 21 | def __init__(self, bot_id: str, msg: str, targets: List[MessagePort]): 22 | self.bot_id = bot_id 23 | self.msg = msg 24 | self.targets = targets 25 | 26 | def __str__(self): 27 | processed_msg = self.msg 28 | 29 | def handle_base64img(match): # 如果是base64编码就不要显示了 30 | return "[CQ:image,file=base64:...]" 31 | 32 | processed_msg = re.sub(r"\[CQ:image,file=base64:.*]", handle_base64img, processed_msg) 33 | s = f"Bot \033[0;37m{self.bot_id}\033[0m send message \033[0;33m{processed_msg}\033[0m to " 34 | s += '\n\t'.join([str(target) for target in self.targets]) 35 | return s 36 | 37 | 38 | class BotDelayCommand(BotCommandBase): 39 | """ 40 | 延迟执行后面的操作 41 | """ 42 | 43 | def __init__(self, bot_id: str, seconds: float): 44 | self.bot_id = bot_id 45 | self.seconds = seconds 46 | 47 | def __str__(self): 48 | s = f"Bot \033[0;37m{self.bot_id}\033[0m delay \033[0;33m{self.seconds}\033[0m seconds" 49 | return s 50 | 51 | 52 | class BotLeaveGroupCommand(BotCommandBase): 53 | """ 54 | 退出群 55 | """ 56 | 57 | def __init__(self, bot_id: str, target_group_id: str): 58 | self.bot_id = bot_id 59 | self.target_group_id = target_group_id 60 | 61 | def __str__(self): 62 | s = f"Bot \033[0;37m{self.bot_id}\033[0m leave group \033[0;33m{self.target_group_id}\033[0m" 63 | return s 64 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/command/const.py: -------------------------------------------------------------------------------- 1 | DPP_COMMAND_PRIORITY_DEFAULT = 1 << 10 # 默认优先级 2 | DPP_COMMAND_PRIORITY_USUAL_LOWER_BOUND = -(1 << 10) # 能被.bot off屏蔽掉的指令都要在这之后响应 3 | DPP_COMMAND_PRIORITY_MASTER = 1 << 11 # Master指令优先级 4 | DPP_COMMAND_PRIORITY_TRIVIAL = 1 << 12 # 次要指令优先级 5 | 6 | DPP_COMMAND_FLAG_DEFAULT = 0 # 命令所属的标志位 7 | DPP_COMMAND_FLAG_ROLL = 1 << 1 # 掷骰指令 8 | DPP_COMMAND_FLAG_DND = 1 << 2 # DND相关指令 9 | DPP_COMMAND_FLAG_CHAR = 1 << 3 # 角色卡相关指令 10 | DPP_COMMAND_FLAG_QUERY = 1 << 4 # 查询相关指令 11 | DPP_COMMAND_FLAG_DRAW = 1 << 5 # 抽卡相关指令 12 | DPP_COMMAND_FLAG_FUN = 1 << 6 # 娱乐相关指令 13 | DPP_COMMAND_FLAG_MANAGE = 1 << 7 # 管理相关指令 14 | DPP_COMMAND_FLAG_CHAT = 1 << 8 # 自定义聊天指令 15 | DPP_COMMAND_FLAG_HELP = 1 << 9 # 帮助指令 16 | DPP_COMMAND_FLAG_MACRO = 1 << 10 # 宏指令 17 | DPP_COMMAND_FLAG_INFO = 1 << 11 # 查看用户信息相关指令 18 | DPP_COMMAND_FLAG_HUB = 1 << 12 # Hub相关指令 19 | DPP_COMMAND_FLAG_BATTLE = 1 << 13 # 战斗相关指令 20 | 21 | DPP_COMMAND_FLAG_SET_STD = DPP_COMMAND_FLAG_ROLL | DPP_COMMAND_FLAG_DND | DPP_COMMAND_FLAG_CHAR | DPP_COMMAND_FLAG_QUERY | DPP_COMMAND_FLAG_BATTLE 22 | DPP_COMMAND_FLAG_SET_EXT_0 = DPP_COMMAND_FLAG_FUN | DPP_COMMAND_FLAG_CHAT 23 | DPP_COMMAND_FLAG_SET_HIDE_IN_STAT = DPP_COMMAND_FLAG_HUB | DPP_COMMAND_FLAG_MANAGE | DPP_COMMAND_FLAG_CHAT | DPP_COMMAND_FLAG_INFO 24 | 25 | DPP_COMMAND_FLAG_DICT = { 26 | DPP_COMMAND_FLAG_ROLL: "掷骰", 27 | DPP_COMMAND_FLAG_DND: "DND", 28 | DPP_COMMAND_FLAG_BATTLE: "战斗", 29 | DPP_COMMAND_FLAG_CHAR: "角色卡", 30 | DPP_COMMAND_FLAG_QUERY: "查询", 31 | DPP_COMMAND_FLAG_DRAW: "抽卡", 32 | DPP_COMMAND_FLAG_FUN: "娱乐", 33 | DPP_COMMAND_FLAG_MANAGE: "管理", 34 | DPP_COMMAND_FLAG_CHAT: "聊天", 35 | DPP_COMMAND_FLAG_HELP: "帮助", 36 | DPP_COMMAND_FLAG_MACRO: "宏", 37 | DPP_COMMAND_FLAG_INFO: "用户信息", 38 | DPP_COMMAND_FLAG_HUB: "Hub", 39 | } 40 | 41 | DPP_COMMAND_CLUSTER_DEFAULT = 0 # 命令所属的功能群 42 | DPP_COMMAND_CLUSTER_DICT = { 43 | DPP_COMMAND_CLUSTER_DEFAULT: "Default", 44 | } 45 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/command/template_cmd.py: -------------------------------------------------------------------------------- 1 | """ 2 | 命令模板, 复制到新创建的文件里修改 3 | """ 4 | 5 | from typing import List, Tuple, Any 6 | 7 | from core.bot import Bot 8 | # from core.data import DataChunkBase, custom_data_chunk 9 | # from core.command.const import * 10 | from core.command import UserCommandBase # , custom_user_command 11 | from core.command import BotCommandBase, BotSendMsgCommand 12 | from core.communication import MessageMetaData, PrivateMessagePort, GroupMessagePort 13 | 14 | LOC_TEMP = "template_loc" 15 | 16 | CFG_TEMP = "template_config" 17 | 18 | # 增加自定义DataChunk 19 | # DC_TEMP = "template_data" 20 | # @custom_data_chunk(identifier=DC_TEMP) 21 | # class _(DataChunkBase): 22 | # def __init__(self): 23 | # super().__init__() 24 | 25 | 26 | # 使用之前取消注释掉下面一行 27 | # @custom_user_command(readable_name="指令模板", priority=DPP_COMMAND_PRIORITY_DEFAULT) 28 | class TemplateCommand(UserCommandBase): 29 | """ 30 | 模板命令, 不要使用 31 | """ 32 | 33 | def __init__(self, bot: Bot): 34 | super().__init__(bot) 35 | bot.loc_helper.register_loc_text(LOC_TEMP, "内容", "注释") 36 | bot.cfg_helper.register_config(CFG_TEMP, "内容", "注释") 37 | 38 | def can_process_msg(self, msg_str: str, meta: MessageMetaData) -> Tuple[bool, bool, Any]: 39 | should_proc: bool = msg_str.startswith(".xxx") 40 | should_pass: bool = False 41 | return should_proc, should_pass, msg_str[4:].strip() 42 | 43 | def process_msg(self, msg_str: str, meta: MessageMetaData, hint: Any) -> List[BotCommandBase]: 44 | port = GroupMessagePort(meta.group_id) if meta.group_id else PrivateMessagePort(meta.user_id) 45 | # 解析语句 46 | arg_str = hint 47 | feedback: str = "" 48 | 49 | return [BotSendMsgCommand(self.bot.account, feedback, [port])] 50 | 51 | def get_help(self, keyword: str, meta: MessageMetaData) -> str: 52 | if keyword == "TMP": # help后的接着的内容 53 | feedback: str = "" 54 | return feedback 55 | return "" 56 | 57 | def get_description(self) -> str: 58 | return ".xxx 指令描述" # help指令中返回的内容 59 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/command/user_cmd.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from typing import List, Tuple, Dict, Type, Any 3 | 4 | from core.bot import Bot 5 | from core.communication import MessageMetaData 6 | 7 | from core.command.const import * 8 | from core.command.bot_cmd import BotCommandBase 9 | 10 | 11 | class UserCommandBase(metaclass=abc.ABCMeta): 12 | """ 13 | 所有用户指令的基类 14 | """ 15 | readable_name: str = "未命名指令" 16 | priority: int = DPP_COMMAND_PRIORITY_DEFAULT 17 | group_only: bool = False 18 | flag: int = DPP_COMMAND_FLAG_DEFAULT 19 | cluster: int = DPP_COMMAND_CLUSTER_DEFAULT 20 | 21 | def __init__(self, bot: Bot): 22 | """ 23 | Args: 24 | bot: 所属的Bot实例 25 | """ 26 | self.bot = bot 27 | self.format_loc = self.bot.loc_helper.format_loc_text # 精简代码长度 28 | 29 | def delay_init(self) -> List[str]: 30 | """在机器人完成初始化后调用, 此时可以读取本地化文本和配置, 返回提示信息""" 31 | return [] 32 | 33 | def tick(self) -> List[BotCommandBase]: 34 | """每秒调用一次的方法""" 35 | return [] 36 | 37 | def tick_daily(self) -> List[BotCommandBase]: 38 | """每天调用一次""" 39 | return [] 40 | 41 | @abc.abstractmethod 42 | def can_process_msg(self, msg_str: str, meta: MessageMetaData) -> Tuple[bool, bool, Any]: 43 | """ 44 | 确定一条信息能否被这个Command处理 45 | Args: 46 | msg_str: 预处理后的信息字符串 47 | meta: 消息的元信息, 包括原始信息字符串, 发送者id, bot id等等 48 | Returns: 49 | should_proc: 是否可以被处理 50 | should_pass: 如果可以被处理, 是否继续让该消息给其他命令处理. 若should_proc为False, 则该返回值不会被用到 51 | hint: 传给process_msg的提示 52 | """ 53 | should_proc: bool = False 54 | should_pass: bool = False 55 | return should_proc, should_pass, None 56 | 57 | @abc.abstractmethod 58 | def process_msg(self, msg_str: str, meta: MessageMetaData, hint: Any) -> List[BotCommandBase]: 59 | """ 60 | 处理信息的函数 61 | Args: 62 | msg_str: 预处理后的信息字符串 63 | meta: 消息的元信息, 包括原始信息字符串, 发送者id, bot id等等 64 | hint: 预处理时给出的提示 65 | Returns: 66 | bot_commands: 一个bot commands list, 即bot要进行的操作, 比如回复消息等等 67 | """ 68 | bot_commands = [] 69 | return bot_commands 70 | 71 | @abc.abstractmethod 72 | def get_help(self, keyword: str, meta: MessageMetaData) -> str: 73 | """ 74 | 返回命令的使用帮助 75 | Args: 76 | keyword: 查询关键词 77 | meta: 消息的元信息, 包括原始信息字符串, 发送者id, bot id等等 78 | 79 | Returns: 80 | help_str: 帮助字符串, 如果关键词不符合预期, 应该返回空字符串 81 | """ 82 | return "" 83 | 84 | @abc.abstractmethod 85 | def get_description(self) -> str: 86 | """ 87 | 返回命令的简短描述, 尽量不要超过一行 88 | """ 89 | return "" 90 | 91 | 92 | def custom_user_command(readable_name: str, 93 | priority: int = DPP_COMMAND_PRIORITY_DEFAULT, 94 | group_only: bool = False, 95 | flag: int = DPP_COMMAND_FLAG_DEFAULT, 96 | cluster: int = DPP_COMMAND_CLUSTER_DEFAULT): 97 | """ 98 | 装饰Command类, 给自定义的Command附加一些参数 99 | Args: 100 | readable_name: 可读的名称, 应当为中文 101 | priority: 优先级, 优先级高的类会先处理指令, 数字越小优先级越高 102 | group_only: 是否只能在群内使用, 如果为True且在私聊中捕获了对应消息, 则会返回提示 103 | flag: 标志位, 标志着指令的类型是DND指令, 娱乐指令等等, 主要用于profiler 104 | cluster: 所属的命令群组, 被用来开关某一组功能 105 | """ 106 | 107 | def custom_inner(cls): 108 | """ 109 | Args: 110 | cls: 要修饰的类, 必须继承自UserCommandBase 111 | """ 112 | assert issubclass(cls, UserCommandBase) 113 | cls.readable_name = readable_name 114 | cls.priority = priority 115 | cls.group_only = group_only 116 | cls.flag = flag 117 | cls.cluster = cluster 118 | USER_COMMAND_CLS_DICT[cls.__name__] = cls 119 | return cls 120 | 121 | return custom_inner 122 | 123 | 124 | class CommandError(Exception): 125 | """ 126 | 执行命令时产生的异常, 说明操作失败的原因, 应当Command内部使用, 不应该抛给Bot 127 | """ 128 | 129 | def __init__(self, info: str, to_user: bool = False, to_master: bool = True): 130 | """ 131 | 132 | Args: 133 | info: 消息内容 134 | to_user: 是否发送给用户 135 | to_master: 是否发送给管理员 136 | """ 137 | self.info = info 138 | self.to_user = to_user 139 | self.to_master = to_master 140 | 141 | def __str__(self): 142 | return f"[Command] [Error] {self.info}" 143 | 144 | 145 | USER_COMMAND_CLS_DICT: Dict[str, Type[UserCommandBase]] = {} 146 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/communication/__init__.py: -------------------------------------------------------------------------------- 1 | from core.communication.info import GroupInfo, GroupMemberInfo 2 | from core.communication.port import MessagePort, PrivateMessagePort, GroupMessagePort 3 | from core.communication.message import MessageSender, MessageMetaData 4 | from core.communication.process import preprocess_msg 5 | 6 | from core.communication.notice import NoticeData, GroupIncreaseNoticeData, FriendAddNoticeData 7 | from core.communication.request import RequestData, FriendRequestData, JoinGroupRequestData, InviteGroupRequestData 8 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/communication/info.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | 4 | class GroupInfo: 5 | def __init__(self, group_id: str): 6 | self.group_id: str = group_id 7 | self.group_name: str = "" 8 | self.member_count: int = 0 9 | self.max_member_count: int = 0 10 | 11 | 12 | class GroupMemberInfo: 13 | def __init__(self, group_id: str, user_id: str): 14 | self.group_id: str = group_id 15 | self.user_id: str = user_id 16 | self.nickname: str = "" 17 | self.card: str = "" 18 | self.role: Literal["owner", "admin", "member"] = "member" 19 | self.title: str = "" 20 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/communication/message.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | 4 | class MessageSender: 5 | def __init__(self, user_id: str, nickname: str): 6 | self.user_id: Optional[str] = user_id 7 | self.nickname: Optional[str] = nickname 8 | self.sex: Optional[str] = None 9 | self.age: Optional[int] = None 10 | self.card: Optional[str] = None 11 | self.area: Optional[str] = None 12 | self.level: Optional[str] = None 13 | self.role: Optional[str] = None 14 | self.title: Optional[str] = None 15 | 16 | 17 | class MessageMetaData: 18 | """ 19 | 包含了一条消息的元信息 20 | """ 21 | 22 | def __init__(self, plain_msg: str, raw_msg: str, sender: MessageSender, group_id: str = "", to_me: bool = False): 23 | self.plain_msg: str = plain_msg 24 | self.raw_msg: str = raw_msg 25 | self.sender: MessageSender = sender 26 | self.user_id: str = sender.user_id 27 | self.nickname: str = sender.nickname 28 | self.group_id: str = group_id 29 | self.to_me: bool = to_me 30 | 31 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/communication/notice.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | 4 | class NoticeData(metaclass=abc.ABCMeta): 5 | """通知信息""" 6 | pass 7 | 8 | 9 | class GroupIncreaseNoticeData(NoticeData): 10 | """群成员增加""" 11 | 12 | def __init__(self, user_id, group_id, operator_id): 13 | self.user_id: str = user_id 14 | self.group_id: str = group_id 15 | self.operator_id: str = operator_id 16 | 17 | def __str__(self): 18 | return f"GroupIncrease: \033[0;37m{self.user_id}\033[0m join \033[0;37m{self.group_id}\033[0m (Op:\033[0;37m{self.operator_id}\033[0m)" 19 | 20 | 21 | class FriendAddNoticeData(NoticeData): 22 | """好友添加""" 23 | 24 | def __init__(self, user_id): 25 | self.user_id: str = user_id 26 | 27 | def __str__(self): 28 | return f"FriendAdd: \033[0;37m{self.user_id}\033[0m" 29 | 30 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/communication/port.py: -------------------------------------------------------------------------------- 1 | class MessagePort: 2 | def __init__(self, group_id: str, user_id: str): 3 | """ 4 | 5 | Args: 6 | group_id: 目标群号, 为空则代表私聊 7 | user_id: 目标账号, 不为空则代表私聊, group和user同时存在则只看user 8 | """ 9 | self.group_id = group_id 10 | self.user_id = user_id 11 | 12 | def __str__(self): 13 | if not self.user_id and self.group_id: 14 | return f"\033[0;34m|Group: {self.group_id}|\033[0m" 15 | elif self.user_id: 16 | return f"\033[0;35m|Private: {self.user_id}|\033[0m" 17 | else: 18 | return f"\033[0;31m|Empty Message Target!|\033[0m" 19 | 20 | def __eq__(self, other): 21 | return self.__dict__ == other.__dict__ 22 | 23 | def __hash__(self): 24 | return hash(self.group_id+self.user_id) 25 | 26 | 27 | class PrivateMessagePort(MessagePort): 28 | def __init__(self, user_id: str): 29 | """ 30 | 31 | Args: 32 | user_id: 目标账号 33 | """ 34 | super().__init__("", user_id) 35 | 36 | 37 | class GroupMessagePort(MessagePort): 38 | def __init__(self, group_id: str): 39 | """ 40 | 41 | Args: 42 | group_id: 目标群号 43 | """ 44 | super().__init__(group_id, "") -------------------------------------------------------------------------------- /src/plugins/DicePP/core/communication/process.py: -------------------------------------------------------------------------------- 1 | import html 2 | 3 | from utils.string import to_english_str 4 | 5 | 6 | def preprocess_msg(msg_str: str) -> str: 7 | """ 8 | 预处理消息字符串 9 | """ 10 | msg_str = to_english_str(msg_str) # 转换中文标点 11 | msg_str = msg_str.lower().strip() # 转换小写, 去掉前后空格 12 | msg_str = html.unescape(msg_str) # html实体转义: $ -> $ 13 | return msg_str 14 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/communication/request.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | 4 | class RequestData(metaclass=abc.ABCMeta): 5 | """请求信息""" 6 | pass 7 | 8 | 9 | class FriendRequestData(RequestData): 10 | """加好友请求""" 11 | 12 | def __init__(self, user_id, comment): 13 | self.user_id: str = user_id 14 | self.comment: str = comment 15 | 16 | 17 | class JoinGroupRequestData(RequestData): 18 | """加群请求 - 自己是管理员""" 19 | 20 | def __init__(self, user_id, group_id, comment): 21 | self.user_id: str = user_id 22 | self.group_id: str = group_id 23 | self.comment: str = comment 24 | 25 | 26 | class InviteGroupRequestData(RequestData): 27 | """邀请加群请求""" 28 | 29 | def __init__(self, user_id, group_id, comment): 30 | self.user_id: str = user_id 31 | self.group_id: str = group_id 32 | self.comment: str = comment 33 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/config/__init__.py: -------------------------------------------------------------------------------- 1 | from core.config.basic import PROJECT_PATH, DATA_PATH, BOT_DATA_PATH, CONFIG_PATH, LOCAL_IMG_PATH 2 | from core.config.common import * 3 | from core.config.declare import BOT_VERSION, BOT_DESCRIBE, BOT_GIT_LINK 4 | 5 | from core.config.config_item import ConfigItem 6 | from core.config.manager import ConfigManager 7 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/config/basic.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from utils.logger import dice_log 4 | 5 | PROJECT_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) 6 | 7 | DATA_PATH = os.path.join(PROJECT_PATH, 'Data') 8 | 9 | BOT_DATA_PATH = os.path.join(DATA_PATH, 'Bot') 10 | CONFIG_PATH = os.path.join(DATA_PATH, 'Config') 11 | LOCAL_IMG_PATH = os.path.join(CONFIG_PATH, 'LocalImage') 12 | 13 | 14 | ALL_LOCAL_DIR_PATH = [DATA_PATH, BOT_DATA_PATH, CONFIG_PATH, LOCAL_IMG_PATH] 15 | 16 | for dirPath in ALL_LOCAL_DIR_PATH: 17 | if not os.path.exists(dirPath): 18 | os.makedirs(dirPath) 19 | dice_log("[Config] [Init] 创建文件夹: " + dirPath) -------------------------------------------------------------------------------- /src/plugins/DicePP/core/config/common.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List 2 | 3 | from core.config.declare import BOT_AGREEMENT 4 | 5 | DEFAULT_CONFIG: Dict[str, str] = {} 6 | DEFAULT_CONFIG_COMMENT: Dict[str, str] = {} 7 | 8 | # 默认配置 9 | CFG_MASTER = "master" 10 | DEFAULT_CONFIG[CFG_MASTER] = "" 11 | DEFAULT_CONFIG_COMMENT[CFG_MASTER] = "Master账号, 权限最高, 可以有多个Master" 12 | 13 | CFG_ADMIN = "admin" 14 | DEFAULT_CONFIG[CFG_ADMIN] = "" 15 | DEFAULT_CONFIG_COMMENT[CFG_ADMIN] = "管理员账号, 拥有次高权限, 可以有多个管理员" 16 | 17 | CFG_FRIEND_TOKEN = "friend_token" 18 | DEFAULT_CONFIG[CFG_FRIEND_TOKEN] = "" 19 | DEFAULT_CONFIG_COMMENT[CFG_FRIEND_TOKEN] = "用户申请好友时在验证中输入参数中的文本之一骰娘才会通过, 若字符串为空则通过所有的好友验证" 20 | 21 | CFG_GROUP_INVITE = "group_invite" 22 | DEFAULT_CONFIG[CFG_GROUP_INVITE] = "1" 23 | DEFAULT_CONFIG_COMMENT[CFG_GROUP_INVITE] = "好友邀请加群时是否同意, 0为总是拒绝, 1为总是同意" 24 | 25 | CFG_AGREEMENT = "agreement" 26 | DEFAULT_CONFIG[CFG_AGREEMENT] = BOT_AGREEMENT 27 | DEFAULT_CONFIG_COMMENT[CFG_AGREEMENT] = "使用协议" 28 | 29 | CFG_COMMAND_SPLIT = "command_split" # \\ 来分割多条指令 30 | DEFAULT_CONFIG[CFG_COMMAND_SPLIT] = "\\\\" 31 | DEFAULT_CONFIG_COMMENT[CFG_COMMAND_SPLIT] = "分割多条指令的关键字, 默认为 \\\\" 32 | 33 | CFG_DATA_EXPIRE = "data_expire" 34 | DEFAULT_CONFIG[CFG_DATA_EXPIRE] = "0" 35 | DEFAULT_CONFIG_COMMENT[CFG_DATA_EXPIRE] = "是否定期清除过期数据与退出群聊, 0为不清理, 1为清理" 36 | 37 | CFG_USER_EXPIRE_DAY = "user_expire_day" 38 | DEFAULT_CONFIG[CFG_USER_EXPIRE_DAY] = "60" 39 | DEFAULT_CONFIG_COMMENT[CFG_USER_EXPIRE_DAY] = "用户在多少天内没有使用过指令则清除相关数据" 40 | 41 | CFG_GROUP_EXPIRE_DAY = "group_expire_day" 42 | DEFAULT_CONFIG[CFG_GROUP_EXPIRE_DAY] = "14" 43 | DEFAULT_CONFIG_COMMENT[CFG_GROUP_EXPIRE_DAY] = "群聊在多少天内没有使用过指令则清除相关数据并退群" 44 | 45 | CFG_GROUP_EXPIRE_WARNING = "group_expire_warning_time" 46 | DEFAULT_CONFIG[CFG_GROUP_EXPIRE_WARNING] = "1" 47 | DEFAULT_CONFIG_COMMENT[CFG_GROUP_EXPIRE_WARNING] = f"清除相关数据并退群之前进行几次警告, 如{CFG_GROUP_EXPIRE_DAY}为14, {CFG_GROUP_EXPIRE_WARNING}为2, " \ 48 | f"则14天内群内没有人使用指令就会在第15天提示1次, 第16天提示1次然后退群. (提示词在localization中配置)" 49 | 50 | CFG_WHITE_LIST_GROUP = "white_list_group" 51 | DEFAULT_CONFIG[CFG_WHITE_LIST_GROUP] = "" 52 | DEFAULT_CONFIG_COMMENT[CFG_WHITE_LIST_GROUP] = f"可填多个单元格, 或用;在同一个单元格分隔不同的群号, 列表中的群不会被自动清除信息或退群" 53 | 54 | CFG_WHITE_LIST_USER = "white_list_user" 55 | DEFAULT_CONFIG[CFG_WHITE_LIST_USER] = "" 56 | DEFAULT_CONFIG_COMMENT[CFG_WHITE_LIST_USER] = f"可填多个单元格, 或用;在同一个单元格分隔不同的账号, 列表中的账号不会被自动清除信息" 57 | 58 | 59 | def preprocess_white_list(raw_list: List[str]) -> List[str]: 60 | result_list: List[str] = [] 61 | for raw_str in raw_list: 62 | for item in raw_str.split(";"): 63 | if item.strip(): 64 | result_list.append(item.strip()) 65 | return result_list 66 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/config/config_item.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from typing import List 3 | 4 | 5 | class ConfigItem: 6 | def __init__(self, key: str, default_content: str = "", comment: str = ""): 7 | self.key = key 8 | self.contents: list = [default_content] if default_content else [] 9 | self.comment = comment 10 | 11 | def add(self, text: str) -> None: 12 | """ 13 | 为当前配置增加一个参数, 调用get可以返回所有参数 14 | """ 15 | self.contents.append(text) 16 | 17 | def get(self) -> List[str]: 18 | """ 19 | 返回一个可选择的本地化字符串, 若没有可用的本地化字符串, 返回空字符串 20 | """ 21 | return copy.copy(self.contents) 22 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/config/declare.py: -------------------------------------------------------------------------------- 1 | BOT_VERSION = "Ver 1.4.1" 2 | 3 | BOT_DESCRIBE = "DicePP by 梨子" 4 | 5 | BOT_AGREEMENT = "1.邀请骰娘, 使用掷骰服务和在群内阅读此协议视为同意并承诺遵守此协议,否则请移除骰娘。\n" \ 6 | "2.不允许禁言骰娘或刷屏掷骰等对骰娘的不友善行为,这些行为将会提高骰娘被制裁的风险。开关骰娘响应请使用.bot on/off。\n" \ 7 | "3.邀请骰娘入群应已事先得到群内同意。因擅自邀请而使骰娘遭遇不友善行为时,邀请者因未履行预见义务而将承担连带责任。\n" \ 8 | "4.禁止将骰娘用于赌博及其他违法犯罪行为,禁止将本骰娘用作TRPG外的用途,禁止拉入非TRPG群。\n" \ 9 | "5.对于设置敏感昵称等无法预见但有可能招致言论审查的行为,骰娘可能会出于自我保护而拒绝提供服务\n" \ 10 | "6.由于技术以及资金原因,无法保证骰娘100%的时间稳定运行,可能不定时停机维护或遭遇冻结,敬请谅解。\n" \ 11 | "7.对于违反协议的行为,骰娘将视情况终止对用户和所在群提供服务。\n" \ 12 | "8.本协议内容可能改动,请注意查看最新协议。\n" \ 13 | "9.本服务最终解释权归服务提供方所有。" 14 | 15 | BOT_GIT_LINK = "DicePP说明手册: https://docs.qq.com/doc/DV3hFWUx6VG1MUnhp\n" \ 16 | "源码: https://github.com/pear-studio/nonebot-dicepp" 17 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/config/manager.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Dict, List 3 | 4 | import openpyxl 5 | from openpyxl.comments import Comment 6 | 7 | from utils.logger import dice_log 8 | from utils.localdata import read_xlsx 9 | 10 | from core.config.config_item import ConfigItem 11 | from core.config.basic import DATA_PATH 12 | from core.config.common import DEFAULT_CONFIG, DEFAULT_CONFIG_COMMENT 13 | 14 | CONFIG_FILE_PATH = "config.xlsx" 15 | 16 | 17 | class ConfigManager: 18 | def __init__(self, data_path: str, identifier: str): 19 | self.data_path = os.path.join(data_path, CONFIG_FILE_PATH) 20 | self.identifier = identifier 21 | self.all_configs: Dict[str, ConfigItem] = {} 22 | 23 | # 默认配置 24 | for key in DEFAULT_CONFIG.keys(): 25 | self.register_config(key, DEFAULT_CONFIG[key], DEFAULT_CONFIG_COMMENT[key]) 26 | 27 | def load_config(self): 28 | """用文档里的配置覆写之前的配置""" 29 | if not os.path.exists(self.data_path): 30 | dice_log(f"[BotConfig] [Load] 无法读取配置文件 {self.data_path.replace(DATA_PATH, '~')}") 31 | return 32 | workbook = read_xlsx(self.data_path) 33 | if self.identifier in workbook.sheetnames: 34 | sheet = workbook[self.identifier] 35 | elif "Default" in workbook.sheetnames: 36 | sheet = workbook["Default"] 37 | else: 38 | dice_log(f"[BotConfig] [Load] 无法读取配置文件 {self.data_path.replace(DATA_PATH, '~')} {self.identifier}") 39 | return 40 | for row in sheet.iter_rows(): 41 | key = str(row[0].value) # 第一个元素为关键字 42 | if key not in self.all_configs: 43 | continue 44 | comment: str = self.all_configs[key].comment # 沿用原来的注释, 不用文件里的 45 | self.all_configs[key] = ConfigItem(key, comment=comment) 46 | for text in [str(cell.value) for cell in row[1:] if (cell.value is not None)]: 47 | self.all_configs[key].add(text) 48 | workbook.close() 49 | dice_log(f"[BotConfig] [Load] 成功读取配置文件 {self.data_path.replace(DATA_PATH, '~')}") 50 | 51 | def save_config(self): 52 | """按现在的设置多个机器人会读写同一个配置文件, 如果并行可能存在写冲突, 现在应该是单线程异步, 应该没问题""" 53 | def save_loc_text_to_row(sheet, item: ConfigItem, row: int): 54 | header = sheet.cell(row=row, column=1, value=item.key) 55 | if item.comment: 56 | header.comment = Comment(item.comment, "DicePP") 57 | if not item.contents: 58 | sheet.cell(row=row, column=2, value="") 59 | else: 60 | for ci, text in enumerate(item.contents): 61 | sheet.cell(row=row, column=ci + 2, value=text) 62 | 63 | if os.path.exists(self.data_path): 64 | workbook = read_xlsx(self.data_path) 65 | else: 66 | workbook = openpyxl.Workbook() 67 | for name in workbook.sheetnames: 68 | del workbook[name] 69 | if self.identifier in workbook.sheetnames: 70 | cur_sheet = workbook[self.identifier] 71 | else: 72 | cur_sheet = workbook.create_sheet(self.identifier) 73 | for ri, cfg_item in enumerate(self.all_configs.values()): 74 | save_loc_text_to_row(cur_sheet, cfg_item, ri + 1) 75 | 76 | try: 77 | workbook.save(self.data_path) 78 | except PermissionError: 79 | dice_log(f"[BotConfig] [Save] 无法保存本地化文件 {self.data_path.replace(DATA_PATH, '~')}, 没有写入权限") 80 | 81 | workbook.close() 82 | 83 | dice_log(f"[BotConfig] [Save] 成功更新配置文件 {self.data_path.replace(DATA_PATH, '~')}") 84 | 85 | def register_config(self, key: str, origin_str: str, comment: str = ""): 86 | """ 87 | 将一个配置注册至Helper中 88 | Args: 89 | key: 配置关键字 90 | origin_str: 配置的默认值 91 | comment: 注释 92 | """ 93 | self.all_configs[key] = ConfigItem(key, origin_str, comment) 94 | 95 | def get_config(self, key: str) -> List[str]: 96 | """ 97 | 获取本地化语句 98 | Args: 99 | key: 本地化语句的关键字 100 | """ 101 | return self.all_configs[key].get() 102 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/data/__init__.py: -------------------------------------------------------------------------------- 1 | from core.data.basic import * 2 | 3 | from core.data.json_object import JsonObject, custom_json_object 4 | from core.data.data_chunk import DataChunkBase, custom_data_chunk 5 | from core.data.manager import DataManager, DataManagerError 6 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/data/basic.py: -------------------------------------------------------------------------------- 1 | from core.data.data_chunk import custom_data_chunk, DataChunkBase 2 | 3 | DC_META = "meta" 4 | DCK_META_STAT = "stat" 5 | 6 | DC_MACRO = "macro" 7 | DC_VARIABLE = "variable" 8 | 9 | DC_USER_DATA = "user_data" 10 | DCK_USER_STAT = "stat" 11 | 12 | DC_GROUP_DATA = "group_data" 13 | DCK_GROUP_STAT = "stat" 14 | 15 | DC_NICKNAME = "nickname" 16 | 17 | 18 | @custom_data_chunk(identifier=DC_META, include_json_object=True) 19 | class _(DataChunkBase): 20 | def __init__(self): 21 | super().__init__() 22 | self.version: int = 0 23 | 24 | def introspect(self) -> None: 25 | if self.version == 0: 26 | self.root = {} # 无效所有数据 27 | self.version = 1 28 | 29 | 30 | @custom_data_chunk(identifier=DC_MACRO, include_json_object=True) 31 | class _(DataChunkBase): 32 | def __init__(self): 33 | super().__init__() 34 | 35 | 36 | @custom_data_chunk(identifier=DC_VARIABLE, include_json_object=True) 37 | class _(DataChunkBase): 38 | def __init__(self): 39 | super().__init__() 40 | 41 | 42 | @custom_data_chunk(identifier=DC_USER_DATA, include_json_object=True) 43 | class _(DataChunkBase): 44 | def __init__(self): 45 | super().__init__() 46 | self.version: int = 0 47 | 48 | def introspect(self) -> None: 49 | if self.version == 0: 50 | self.root = {} # 无效所有数据 51 | self.version = 1 52 | 53 | 54 | @custom_data_chunk(identifier=DC_GROUP_DATA, include_json_object=True) 55 | class _(DataChunkBase): 56 | def __init__(self): 57 | super().__init__() 58 | self.version: int = 0 59 | 60 | def introspect(self) -> None: 61 | if self.version == 0: 62 | self.root = {} # 无效所有数据 63 | self.version = 1 64 | 65 | 66 | @custom_data_chunk(identifier=DC_NICKNAME) 67 | class _(DataChunkBase): 68 | def __init__(self): 69 | super().__init__() 70 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/data/data_chunk.py: -------------------------------------------------------------------------------- 1 | """ 2 | 定义所需要的数据类型, 在DataManager中使用 3 | 自定义的DataChunk应当和需要它的方法一起定义, 但在此模块内定义也是可行的(不推荐) 4 | """ 5 | import abc 6 | import copy 7 | from typing import List, Type, Dict, Any 8 | 9 | from utils.time import get_current_date_str 10 | from utils.logger import dice_log 11 | 12 | from core.data.json_object import JsonObject, JSON_OBJECT_PREFIX 13 | 14 | DC_VERSION_LATEST = "1.0" # 格式版本 15 | 16 | 17 | class DataChunkBase(metaclass=abc.ABCMeta): 18 | """ 19 | DataChunk是一次读取/更新文件的最小单位, 每个DataChunk子类都对应一个同名的持久化json文件 20 | 为了方便阅读和管理, 一个DataChunk应当包括某一类功能所需要的全部数据, 也不应包含太多或太少内容 21 | 不能拥有非基础类型, 自定义类型必须继承自DataManager.JsonObject! 否则无法序列化 22 | 例子: 保存20000条某类数据, 每条数据200字节, 大概就是4MB 23 | """ 24 | identifier = "basic_data" 25 | include_json_object = False 26 | 27 | def __init__(self): 28 | self.version_base: str = DC_VERSION_LATEST # 如果修改了相关的代码, 可以通过版本号来将旧版本的数据转换到新版本 29 | self.dirty: bool = False # 读取后是否被修改过 30 | self.strict_check: bool = False # 是否严格地检查 已有的值 与 新值/默认值 拥有相同的类型 31 | self.update_time: str = get_current_date_str() # 最后一次更新的时间 32 | self.root = {} # 树形数据结构的根节点, 所有想要持久化的数据应该存放在这里 33 | self.hash_code = hash(self) # 哈希校验码 34 | 35 | @classmethod 36 | def get_identifier(cls): 37 | """ 38 | 返回定位符, 由custom_data_chunk修饰符给出 39 | """ 40 | return cls.identifier 41 | 42 | @classmethod 43 | def from_json(cls, json_dict: dict): 44 | """ 45 | 通过json格式的字符串反序列化生成一个实例并返回 46 | Args: 47 | json_dict: 以json格式字符串生成的字典 48 | Returns: 49 | obj: 生成的实例 50 | """ 51 | 52 | # noinspection PyBroadException 53 | def deserialize_json_object_in_node(node: Any) -> None: 54 | if isinstance(node, dict): 55 | invalid_key = [] 56 | for key, value in node.items(): 57 | if isinstance(value, dict) or isinstance(value, list): 58 | deserialize_json_object_in_node(value) 59 | elif isinstance(value, str) and value.find(JSON_OBJECT_PREFIX) == 0: # 反序列化JsonObject 60 | try: 61 | node[key] = JsonObject.construct_from_json(value) 62 | except Exception as e: 63 | dice_log(f"[DataManager] [Load] 从字典中加载{key}: {value}时出现错误 {e}") 64 | invalid_key.append(key) 65 | for key in invalid_key: 66 | del node[key] 67 | elif isinstance(node, list): 68 | invalid_index = [] 69 | for index, value in enumerate(node): 70 | if isinstance(value, dict) or isinstance(value, list): 71 | deserialize_json_object_in_node(value) 72 | elif isinstance(value, str) and value.find(JSON_OBJECT_PREFIX) == 0: # 处理Json Object 73 | try: 74 | node[index] = JsonObject.construct_from_json(value) 75 | except Exception as e: 76 | dice_log(f"[DataManager] [Load] 从列表中加载{index}: {value}时出现错误 {e}") 77 | invalid_index.append(index) 78 | for index in reversed(invalid_index): # 从后往前删除 79 | del node[index] 80 | 81 | obj = cls() 82 | for k, v in json_dict.items(): 83 | obj.__setattr__(k, v) 84 | if cls.include_json_object: 85 | deserialize_json_object_in_node(obj.root) 86 | try: 87 | obj.introspect() 88 | except AssertionError: 89 | dice_log(f"[DataManager] [Introspect] Invalidate {obj.identifier}") 90 | obj = cls() 91 | 92 | return obj 93 | 94 | def to_json(self) -> Dict: 95 | """ 96 | 将自己的__dict__处理成一个字典并返回 97 | """ 98 | 99 | def serialize_json_object_in_node(node: Any) -> None: 100 | if isinstance(node, dict): 101 | for key, value in node.items(): 102 | if isinstance(value, dict) or isinstance(value, list): 103 | serialize_json_object_in_node(value) 104 | elif isinstance(value, JsonObject): # 处理Json Object 105 | node[key] = value.to_json() 106 | elif isinstance(node, list): 107 | for index, value in enumerate(node): 108 | if isinstance(value, dict) or isinstance(value, list): 109 | serialize_json_object_in_node(value) 110 | elif isinstance(value, JsonObject): # 处理Json Object 111 | node[index] = value.to_json() 112 | 113 | if self.include_json_object: 114 | json_dict = copy.deepcopy(self.__dict__) 115 | serialize_json_object_in_node(json_dict) 116 | return json_dict 117 | else: 118 | return self.__dict__ 119 | 120 | def __hash__(self): 121 | target_str = self.version_base + self.update_time + str(self.root) 122 | return hash(target_str) 123 | 124 | def introspect(self) -> None: 125 | pass 126 | 127 | 128 | DATA_CHUNK_TYPES: List[Type[DataChunkBase]] = [] # 记录所有DataChunk的子类, 不需要手动修改, 应当通过修饰器CustomDC增加 129 | 130 | 131 | def custom_data_chunk(identifier: str, 132 | include_json_object=False): 133 | """ 134 | 类修饰器, 将自定义DataChunk注册到列表中 135 | Args: 136 | identifier: 一个字符串, 作为储存该DataChunk实例的名字, 应当是一个有区分度的名字, 不能含有空格, 也不能含有文件名中的非法字符 137 | include_json_object: 是否会含有Json Object类型, 如果为否, 在序列化时不会进行检查 138 | """ 139 | 140 | def custom_inner(cls): 141 | """ 142 | 包裹函数 143 | Args: 144 | cls: 修饰的类必须继承自DataChunk 145 | Returns: 146 | cls: 返回修饰后的cls 147 | """ 148 | assert issubclass(cls, DataChunkBase) 149 | assert " " not in identifier 150 | for dc in DATA_CHUNK_TYPES: 151 | assert dc.identifier != identifier 152 | cls.identifier = identifier 153 | cls.include_json_object = include_json_object 154 | cls.__name__ = "DataChunkClass" + identifier 155 | DATA_CHUNK_TYPES.append(cls) 156 | return cls 157 | 158 | return custom_inner 159 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/data/json_object.py: -------------------------------------------------------------------------------- 1 | """ 2 | 继承JsonObject的类可以通过DataManager序列化或反序列 3 | """ 4 | 5 | import abc 6 | from typing import Type, Dict 7 | 8 | JSON_OBJECT_PREFIX = "JSON_OBJ_" 9 | 10 | 11 | class JsonObject(metaclass=abc.ABCMeta): 12 | """ 13 | 可以存在Json中的Object, 并不代表一定要通过Json反序列化和序列化 14 | 构造函数不能拥有参数! 原因见construct_from_json 15 | """ 16 | def to_json(self) -> str: 17 | return JSON_OBJECT_PREFIX + type(self).__name__ + "$" + self.serialize().strip() 18 | 19 | @classmethod 20 | def construct_from_json(cls, json_str: str) -> 'JsonObject': 21 | """ 22 | 从一个json字符串中构造一个json object并返回 23 | Args: 24 | json_str: 格式见JsonObject.to_json方法 25 | Returns: 26 | 27 | """ 28 | json_str = json_str[len(JSON_OBJECT_PREFIX):] 29 | cls_name, json_str = json_str.split("$", 1) 30 | json_cls: Type[JsonObject] = ALL_JSON_OBJ_DICT[cls_name] 31 | json_obj: JsonObject = json_cls() 32 | json_obj.deserialize(json_str) 33 | return json_obj 34 | 35 | @abc.abstractmethod 36 | def serialize(self) -> str: 37 | """ 38 | 将自身序列化为任意字符串 39 | """ 40 | raise NotImplementedError() 41 | 42 | @abc.abstractmethod 43 | def deserialize(self, json_str: str) -> None: 44 | """ 45 | 通过字符串将自身反序列化 46 | """ 47 | raise NotImplementedError() 48 | 49 | 50 | ALL_JSON_OBJ_DICT: Dict[str, Type[JsonObject]] = {} 51 | 52 | 53 | def custom_json_object(cls): 54 | """ 55 | 类修饰器, 将自定义JsonObject注册到列表中 56 | """ 57 | assert issubclass(cls, JsonObject) 58 | assert cls.__name__ not in ALL_JSON_OBJ_DICT 59 | ALL_JSON_OBJ_DICT[cls.__name__] = cls 60 | return cls 61 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/data/unit_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | 4 | from core.data.manager import DataManager, DataManagerError 5 | from core.data.data_chunk import DataChunkBase, custom_data_chunk 6 | from core.data.json_object import JsonObject, custom_json_object 7 | 8 | test_path = os.path.join(os.path.dirname(__file__), 'test_data') 9 | 10 | 11 | class MyTestCase(unittest.TestCase): 12 | test_index = -1 13 | 14 | @classmethod 15 | def setUpClass(cls) -> None: 16 | cls.test_index = 0 17 | 18 | @custom_data_chunk(identifier="Test_A") 19 | class _(DataChunkBase): 20 | def __init__(self): 21 | super().__init__() 22 | 23 | @custom_data_chunk(identifier="Test_B") 24 | class _(DataChunkBase): 25 | def __init__(self): 26 | super().__init__() 27 | self.strict_check = True 28 | 29 | @classmethod 30 | def tearDownClass(cls) -> None: 31 | DataManager._instances = {} 32 | if os.path.exists(test_path): 33 | for root, dirs, files in os.walk(test_path, topdown=False): 34 | for name in files: 35 | os.remove(os.path.join(root, name)) 36 | print(f"[Test TearDown] 清除文件{name}") 37 | for name in dirs: 38 | os.rmdir(os.path.join(root, name)) 39 | print(f"[Test TearDown] 清除文件夹{name}") 40 | os.rmdir(test_path) 41 | print(f"[Test TearDown] 清除文件夹{test_path}") 42 | else: 43 | print(f"测试路径不存在! path:{test_path}") 44 | 45 | def setUp(self) -> None: 46 | self.test_index += 1 47 | 48 | def test0_save(self): 49 | print("开始测试保存功能") 50 | 51 | @custom_data_chunk(identifier=f"Test_Add_1") 52 | class _(DataChunkBase): 53 | def __init__(self): 54 | super().__init__() 55 | self.data_manager = DataManager(test_path) 56 | self.data_manager.set_data("Test_A", ["Level-1-A", "Attr-2-A"], 0) 57 | self.data_manager.set_data("Test_B", ["Level-1-A"], "This is test B!") 58 | self.data_manager.set_data("Test_B", ["Level-1-B", "Attr-2-A"], ["ABC", 123, {}]) 59 | self.data_manager.set_data("Test_Add_1", ["Attr-1-A"], {"ABC": 1.5, "666": "666", "1": []}) 60 | self.data_manager.save_data() 61 | print("保存成功!") 62 | 63 | def test1_load(self): 64 | print("开始测试读取功能") 65 | self.data_manager = DataManager(test_path) 66 | self.assertTrue(type(self.data_manager.get_data("Test_A", [])) is dict) 67 | self.assertEqual(self.data_manager.get_data("Test_A", ["Level-1-A", "Attr-2-A"]), 0) 68 | self.assertEqual(self.data_manager.get_data("Test_B", ["Level-1-A"]), "This is test B!") 69 | self.assertEqual(self.data_manager.get_data("Test_B", ["Level-1-B", "Attr-2-A"]), ["ABC", 123, {}]) 70 | self.assertEqual(self.data_manager.get_data("Test_Add_1", ["Attr-1-A"]), {"ABC": 1.5, "666": "666", "1": []}) 71 | print("读取数据正确!") 72 | 73 | def test1_get(self): 74 | self.data_manager = DataManager(test_path) 75 | copied_data = self.data_manager.get_data("Test_B", ["Level-1-B"]) 76 | self.assertEqual(copied_data, {"Attr-2-A": ["ABC", 123, {}]}) 77 | copied_data["Attr-2-A"][0] = "DEF" 78 | self.assertEqual(self.data_manager.get_data("Test_B", ["Level-1-B", "Attr-2-A"])[0], "ABC") 79 | self.assertNotEqual(id(copied_data), id(self.data_manager.get_data("Test_B", ["Level-1-B", "Attr-2-A"]))) 80 | print("取得的数据是深拷贝!") 81 | not_exist_path = ["Level-1-A", "Attr-Not_Exist"] 82 | default_val = [0, 1, "A"] 83 | self.assertEqual(self.data_manager.get_data("Test_A", not_exist_path, default_val), default_val) 84 | self.assertEqual(self.data_manager.get_data("Test_A", not_exist_path), default_val) 85 | print("带默认值的get可以创建不存在的数据") 86 | 87 | def test2_obj(self): 88 | @custom_data_chunk(identifier=f"Test_Object", include_json_object=True) 89 | class _(DataChunkBase): 90 | def __init__(self): 91 | super().__init__() 92 | 93 | @custom_json_object 94 | class DumbJsonObject(JsonObject): 95 | def __init__(self): 96 | super().__init__() 97 | self.intField = 0 98 | self.floatField = 2.5 99 | self.strField = "ABC" 100 | self.ListField = [0, 1, "A", {"N1": -1}] 101 | self.DictField = {"A": 1, "B": 2, "C": [0, 1, "3"]} 102 | 103 | def serialize(self) -> str: 104 | json_dict = { 105 | "i": self.intField, 106 | "f": self.floatField, 107 | "s": self.strField, 108 | "l": self.ListField, 109 | "d": self.DictField, 110 | } 111 | import json 112 | res = json.dumps(json_dict) 113 | return res 114 | 115 | def deserialize(self, json_str: str) -> None: 116 | import json 117 | json_dict = json.loads(json_str) 118 | self.intField = json_dict["i"] 119 | self.floatField = json_dict["f"] 120 | self.strField = json_dict["s"] 121 | self.ListField = json_dict["l"] 122 | self.DictField = json_dict["d"] 123 | self.data_manager = DataManager(test_path) 124 | dumb_obj_1: DumbJsonObject = DumbJsonObject() 125 | dumb_obj_1.intField = -1 126 | dumb_obj_1.floatField = 6.66 127 | dumb_obj_1.strField = "CBA" 128 | dumb_obj_2: DumbJsonObject = DumbJsonObject() 129 | dumb_obj_2.strField = "" 130 | 131 | self.data_manager.set_data("Test_Object", ["Level-1", "Dumb1"], dumb_obj_1) 132 | self.data_manager.set_data("Test_Object", ["Dumb2"], [dumb_obj_2]) 133 | self.data_manager.save_data() 134 | data_manager_new = DataManager(test_path) 135 | dumb_obj_1 = data_manager_new.get_data("Test_Object", ["Level-1", "Dumb1"]) 136 | dumb_obj_2 = data_manager_new.get_data("Test_Object", ["Dumb2"])[0] 137 | self.assertEqual(dumb_obj_1.intField, -1) 138 | self.assertEqual(dumb_obj_1.floatField, 6.66) 139 | self.assertEqual(dumb_obj_1.strField, "CBA") 140 | self.assertEqual(dumb_obj_2.strField, "") 141 | 142 | def test9_exception(self): 143 | print("开始测试异常") 144 | self.data_manager = DataManager(test_path) 145 | self.assertRaisesRegex(DataManagerError, "尝试在不给出默认值的情况下访问不存在的路径", 146 | self.data_manager.get_data, "Test_A", ["Invalid-path"]) 147 | print(self.data_manager.get_data("Test_A", ["Level-1-A", "Attr-2-A"])) 148 | self.assertRaisesRegex(DataManagerError, "尝试获取的路径非终端节点不是字典类型", 149 | self.data_manager.get_data, "Test_A", ["Level-1-A", "Attr-2-A", "Invalid"], 0) 150 | 151 | self.data_manager.set_data("Test_A", ["Attr-1-A"], 0) 152 | self.data_manager.set_data("Test_A", ["Attr-1-A"], "1") 153 | self.assertEqual(self.data_manager.get_data("Test_A", ["Attr-1-A"], 0), "1") 154 | self.assertEqual(self.data_manager.get_data("Test_A", ["Attr-1-A"], []), "1") 155 | print("非严格类型检查效果正确") 156 | self.data_manager.set_data("Test_B", ["Attr-1-A"], 0) 157 | self.assertRaises(DataManagerError, self.data_manager.set_data, "Test_B", ["Attr-1-A"], "1") 158 | self.assertRaises(DataManagerError, self.data_manager.get_data, "Test_B", ["Attr-1-A"], "1") 159 | self.assertRaises(DataManagerError, self.data_manager.get_data, "Test_B", ["Attr-1-A"], []) 160 | self.assertEqual(self.data_manager.get_data("Test_B", ["Attr-1-A"]), 0) 161 | self.assertEqual(self.data_manager.get_data("Test_B", ["Attr-1-A"], 5), 0) 162 | print("严格类型检查效果正确") 163 | self.assertRaises(DataManagerError, self.data_manager.set_data, "Test_B", ["Attr-1-A", ""], 0) 164 | self.assertRaises(DataManagerError, self.data_manager.get_data, "Test_B", ["Attr-1-A", ""], 0) 165 | print("非空叶子节点检查通过") 166 | 167 | 168 | if __name__ == '__main__': 169 | unittest.main() 170 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/localization/__init__.py: -------------------------------------------------------------------------------- 1 | from core.localization.common import * 2 | from core.localization.manager import LocalizationManager 3 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/localization/common.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | COMMON_LOCAL_TEXT: Dict[str, str] = {} 4 | COMMON_LOCAL_COMMENT: Dict[str, str] = {} 5 | 6 | LOC_GROUP_ONLY_NOTICE = "group_only_notice" 7 | COMMON_LOCAL_TEXT[LOC_GROUP_ONLY_NOTICE] = "This command should only be used in group!" 8 | COMMON_LOCAL_COMMENT[LOC_GROUP_ONLY_NOTICE] = "当用户企图在私聊中执行只能在群聊中执行的命令时返回的提示" 9 | 10 | LOC_FRIEND_ADD_NOTICE = "friend_add_notice" 11 | COMMON_LOCAL_TEXT[LOC_FRIEND_ADD_NOTICE] = "Now you are my friend!" 12 | COMMON_LOCAL_COMMENT[LOC_FRIEND_ADD_NOTICE] = "用户成功添加机器人为好友时发送的语句" 13 | 14 | LOC_LOGIN_NOTICE = "login_notice" 15 | COMMON_LOCAL_TEXT[LOC_LOGIN_NOTICE] = "Hi~" 16 | COMMON_LOCAL_COMMENT[LOC_LOGIN_NOTICE] = "机器人登录时向Master发送的语句, 若为$则登录时不提示" 17 | 18 | LOC_DAILY_UPDATE = "daily_update" 19 | COMMON_LOCAL_TEXT[LOC_DAILY_UPDATE] = "Daily update complete" 20 | COMMON_LOCAL_COMMENT[LOC_DAILY_UPDATE] = "每日更新时发送的语句, 若为$则不提示每日更新" 21 | 22 | LOC_FUNC_DISABLE = "func_disable" 23 | COMMON_LOCAL_TEXT[LOC_FUNC_DISABLE] = "Function {func} is disabled by manager" 24 | COMMON_LOCAL_COMMENT[LOC_FUNC_DISABLE] = "某一功能被关闭时发送给用户的提示词, {func}为功能名" 25 | 26 | LOC_GROUP_EXPIRE_WARNING = "group_expire_warning" 27 | COMMON_LOCAL_TEXT[LOC_GROUP_EXPIRE_WARNING] = "Anyone needs me?" 28 | COMMON_LOCAL_COMMENT[LOC_GROUP_EXPIRE_WARNING] = "群聊过期前发送的提示" 29 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/localization/localization_text.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import re 3 | import random 4 | 5 | from core.config import LOCAL_IMG_PATH 6 | from utils.logger import dice_log 7 | from utils.cq_code import get_cq_image 8 | 9 | 10 | class LocalizationText: 11 | def __init__(self, key: str, default_text: str = "", comment: str = ""): 12 | self.key = key 13 | self.loc_texts: list = [default_text] if default_text else [] 14 | self.comment = comment 15 | 16 | def add(self, text: str) -> None: 17 | """ 18 | 增加一个可选择的本地化字符串, 调用get可以随机返回一个可选择的本地化字符串 19 | """ 20 | self.loc_texts.append(text) 21 | 22 | def get(self) -> str: 23 | """ 24 | 返回一个可选择的本地化字符串, 若没有可用的本地化字符串, 返回空字符串 25 | """ 26 | def replace_image_code(match): 27 | key = match.group(1) 28 | file_path = Path(LOCAL_IMG_PATH) / key 29 | if file_path.exists(): 30 | return get_cq_image(file_path.read_bytes()) 31 | else: 32 | dice_log(f"[LocalImage] 找不到图片 {file_path}") 33 | return match.group(0) 34 | 35 | loc_text = random.choice(self.loc_texts) if self.loc_texts else "" 36 | loc_text = re.sub(r"IMG\((.{1,50}?\.[A-Za-z]{1,10}?)\)", replace_image_code, loc_text) 37 | return loc_text 38 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/localization/manager.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Optional 2 | import os 3 | import re 4 | import random 5 | 6 | import openpyxl 7 | from openpyxl.comments import Comment 8 | from openpyxl.worksheet import worksheet 9 | 10 | from utils.logger import dice_log 11 | from utils.localdata import read_xlsx 12 | from core.config import DATA_PATH as ROOT_DATA_PATH 13 | from core.communication import preprocess_msg 14 | 15 | from core.localization.common import COMMON_LOCAL_TEXT, COMMON_LOCAL_COMMENT 16 | from core.localization.localization_text import LocalizationText 17 | 18 | LOCAL_FILE_PATH = "localization.xlsx" 19 | CHAT_FILE_PATH = "chat.xlsx" 20 | 21 | DEFAULT_ID = "Default" 22 | 23 | DEFAULT_CHAT_KEY = "^hi$" 24 | DEFAULT_CHAT_TEXT = ["Hello", "G'Day"] 25 | DEFAULT_CHAT_COMMENT = "可以使用正则表达式匹配, 大小写不敏感; 后面接着想要的回复, 有多个回复则会随机选择一个" 26 | 27 | 28 | class LocalizationManager: 29 | def __init__(self, data_path: str, identifier: str): 30 | self.data_path = os.path.join(data_path, LOCAL_FILE_PATH) 31 | self.chat_data_path = os.path.join(data_path, CHAT_FILE_PATH) 32 | self.identifier = identifier 33 | self.all_local_texts: Dict[str, LocalizationText] = {} 34 | self.all_chat_texts: Dict[str, LocalizationText] = {} 35 | 36 | # 通用的本地化语句 37 | for key in COMMON_LOCAL_TEXT.keys(): 38 | self.register_loc_text(key, COMMON_LOCAL_TEXT[key], COMMON_LOCAL_COMMENT[key]) 39 | 40 | def load_localization(self): 41 | """用xlsx里的配置覆写之前的本地化配置, 不存在本地文件时直接返回""" 42 | workbook, local_sheet = load_sheet_from_path(self.data_path, self.identifier) 43 | if not workbook: 44 | dice_log(f"[Local] [Load] 无法找到本地化文件 {self.data_path.replace(ROOT_DATA_PATH, '~')} {self.identifier}") 45 | return 46 | for row in local_sheet.iter_rows(): 47 | key = str(row[0].value) # 第一个元素为关键字 48 | if key not in self.all_local_texts: # 无效的关键字 49 | continue 50 | comment: str = self.all_local_texts[key].comment # 沿用原来的注释, 不用文件里的 51 | self.all_local_texts[key] = LocalizationText(key, comment=comment) 52 | for text in [str(cell.value) for cell in row[1:] if cell.value and cell.value.strip()]: 53 | self.all_local_texts[key].add(text) 54 | dice_log(f"[Local] [Load] 成功读取本地化文件 {self.data_path.replace(ROOT_DATA_PATH, '~')}") 55 | workbook.close() 56 | 57 | def save_localization(self): 58 | """注意按多个机器人会读写同一个配置文件, 如果并行可能存在写冲突, 现在单线程异步没问题""" 59 | workbook, local_sheet = get_sheet_from_path(self.data_path, self.identifier) 60 | for ri, loc_text in enumerate(self.all_local_texts.values()): 61 | save_loc_text_to_row(local_sheet, loc_text, ri + 1) 62 | 63 | try: 64 | workbook.save(self.data_path) 65 | dice_log(f"[Local] [Save] 成功更新本地化文件 {self.data_path.replace(ROOT_DATA_PATH, '~')} {local_sheet.title}") 66 | except PermissionError: 67 | dice_log(f"[Local] [Save] 无法保存本地化文件 {self.data_path.replace(ROOT_DATA_PATH, '~')}, 没有写入权限") 68 | workbook.close() 69 | 70 | def load_chat(self): 71 | """从xlsx中读取自定义对话文件""" 72 | def add_default_chat(): 73 | """增加默认自定义对话""" 74 | self.all_chat_texts[DEFAULT_CHAT_KEY] = LocalizationText(DEFAULT_CHAT_KEY, comment=DEFAULT_CHAT_COMMENT) 75 | for default_text in DEFAULT_CHAT_TEXT: 76 | self.all_chat_texts[DEFAULT_CHAT_KEY].add(default_text) 77 | 78 | workbook, chat_sheet = load_sheet_from_path(self.chat_data_path, self.identifier) 79 | if not workbook: 80 | dice_log(f"[Local] [ChatLoad] 无法找到自定义对话文件 {self.chat_data_path.replace(ROOT_DATA_PATH, '.')} {self.identifier}") 81 | add_default_chat() 82 | return 83 | 84 | for row in chat_sheet.iter_rows(): 85 | key = str(row[0].value) # 第一个元素为关键字 86 | key = preprocess_msg(key) # 对key做一下预处理, 因为匹配的目标是预处理过后的 87 | comment: str = row[0].comment # 沿用文件里的注释 88 | self.all_chat_texts[key] = LocalizationText(key, comment=comment) 89 | for text in [str(cell.value) for cell in row[1:] if cell.value and cell.value.strip()]: 90 | self.all_chat_texts[key].add(text) 91 | 92 | has_chat: bool = (len(self.all_chat_texts) != 0) 93 | if not has_chat: 94 | add_default_chat() 95 | dice_log(f"[Local] [ChatLoad] 成功读取本地化文件 {self.chat_data_path.replace(ROOT_DATA_PATH, '~')}") 96 | workbook.close() 97 | 98 | def save_chat(self): 99 | """注意多个机器人会读写同一个配置文件, 如果并行可能存在写冲突, 现在单线程异步没问题""" 100 | workbook, local_sheet = get_sheet_from_path(self.chat_data_path, self.identifier) 101 | for ri, loc_text in enumerate(self.all_chat_texts.values()): 102 | save_loc_text_to_row(local_sheet, loc_text, ri + 1) 103 | 104 | try: 105 | workbook.save(self.chat_data_path) 106 | dice_log(f"[Local] [ChatSave] 成功更新自定义对话文件 {self.chat_data_path.replace(ROOT_DATA_PATH, '~')} {local_sheet.title}") 107 | except PermissionError: 108 | dice_log(f"[Local] [ChatSave] 无法保存自定义对话文件 {self.chat_data_path.replace(ROOT_DATA_PATH, '~')}, 没有写入权限") 109 | workbook.close() 110 | 111 | def register_loc_text(self, key: str, default_text: str, comment: str = ""): 112 | """ 113 | 将一个本地化语句注册至Helper中 114 | Args: 115 | key: 本地化语句的关键字 116 | default_text: 本地化语句的默认值 117 | comment: 对本地化语句的注释 118 | """ 119 | self.all_local_texts[key] = LocalizationText(key, default_text, comment) 120 | 121 | def get_loc_text(self, key: str) -> LocalizationText: 122 | """ 123 | 获取本地化语句 124 | Args: 125 | key: 本地化语句的关键字 126 | """ 127 | return self.all_local_texts[key] 128 | 129 | def format_loc_text(self, key: str, **kwargs) -> str: 130 | """ 131 | 格式化并返回本地化语句 132 | Args: 133 | key: 本地化语句的关键字 134 | **kwargs: 本地化语句需要的参数, 可以传不会用到的参数 135 | """ 136 | loc_text: LocalizationText = self.get_loc_text(key) 137 | if kwargs: 138 | return loc_text.get().format(**kwargs) 139 | else: 140 | return loc_text.get() 141 | 142 | def process_chat(self, msg: str, **kwargs) -> str: 143 | """ 144 | Args: 145 | msg: 用户输入的语句 146 | **kwargs: 目前用不到 147 | 148 | Returns: 149 | 如果msg能与任意自定义聊天关键字匹配, 返回一个随机回复, 否则返回空字符串 150 | """ 151 | valid_loc_text_list = [] 152 | for key, loc_text in self.all_chat_texts.items(): 153 | result = re.match(key, msg) 154 | if result: 155 | valid_loc_text_list.append(loc_text) 156 | 157 | loc_text: Optional[LocalizationText] = random.choice(valid_loc_text_list) if valid_loc_text_list else None 158 | if loc_text: 159 | if kwargs: 160 | return loc_text.get().format(**kwargs) 161 | else: 162 | return loc_text.get() 163 | return "" 164 | 165 | 166 | def save_loc_text_to_row(sheet: worksheet, l_text: LocalizationText, row: int): 167 | # 先清空旧数据 168 | sheet.delete_rows(idx=row) 169 | sheet.insert_rows(idx=row) 170 | # 加入新数据 171 | header = sheet.cell(row=row, column=1, value=l_text.key) 172 | if l_text.comment: 173 | header.comment = Comment(l_text.comment, "DicePP") 174 | 175 | for ci, text in enumerate(l_text.loc_texts): 176 | sheet.cell(row=row, column=ci + 2, value=text) 177 | 178 | 179 | def load_sheet_from_path(data_path: str, identifier: str, default_id: str = DEFAULT_ID) -> (openpyxl.Workbook, worksheet): 180 | """若指定data_path无效或id无效, 返回None. 若id无效会尝试使用default_id, 一般用来得到读取的sheet""" 181 | if not os.path.exists(data_path): 182 | return None, None 183 | workbook = read_xlsx(data_path) 184 | if identifier in workbook.sheetnames: 185 | sheet = workbook[identifier] 186 | sheet.title = identifier 187 | elif default_id in workbook.sheetnames: 188 | sheet = workbook[default_id] 189 | sheet.title = default_id 190 | else: 191 | workbook.close() 192 | return None, None 193 | return workbook, sheet 194 | 195 | 196 | def get_sheet_from_path(data_path: str, identifier: str) -> (openpyxl.Workbook, worksheet): 197 | """若指定的data_path无效或id无效, 就创建新的workbook或worksheet. 一般用来得到写入的sheet""" 198 | feedback: str 199 | if os.path.exists(data_path): 200 | workbook = read_xlsx(data_path) 201 | else: 202 | workbook = openpyxl.Workbook() 203 | for name in workbook.sheetnames: 204 | del workbook[name] 205 | 206 | if identifier in workbook.sheetnames: 207 | sheet = workbook[identifier] 208 | else: 209 | sheet = workbook.create_sheet(identifier) 210 | sheet.title = identifier 211 | return workbook, sheet 212 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/statistics/__init__.py: -------------------------------------------------------------------------------- 1 | from core.statistics.basic_stat import StatElementBase, UserCommandStatInfo, RollStatInfo, D20StatInfo,\ 2 | MetaStatInfo 3 | from core.statistics.user_stat import UserMetaInfo, UserStatInfo 4 | from core.statistics.group_stat import GroupMetaInfo, GroupStatInfo 5 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/statistics/basic_stat.py: -------------------------------------------------------------------------------- 1 | import json 2 | import datetime 3 | from typing import List, Dict 4 | 5 | from core.data import JsonObject, custom_json_object 6 | 7 | from utils.time import get_current_date_int, get_current_date_raw, int_to_datetime, datetime_to_int 8 | 9 | 10 | class StatElementBase: 11 | def serialize(self) -> str: 12 | return f"{self.cur_day_val}|{self.last_day_val}|{self.total_val}|{self.update_time}" 13 | 14 | def deserialize(self, input_str: str) -> None: 15 | try: 16 | val_list = input_str.split("|") 17 | assert len(val_list) == 4 18 | val_list = [int(val) for val in val_list] 19 | self.cur_day_val, self.last_day_val, self.total_val, self.update_time = val_list 20 | except (AssertionError, ValueError): 21 | pass 22 | 23 | def __init__(self): 24 | self.cur_day_val: int = 0 25 | self.last_day_val: int = 0 26 | self.total_val: int = 0 27 | self.update_time: int = 0 28 | 29 | def __add__(self, other: "StatElementBase") -> "StatElementBase": 30 | res = StatElementBase() 31 | res.cur_day_val = self.cur_day_val + other.cur_day_val 32 | res.last_day_val = self.last_day_val + other.last_day_val 33 | res.total_val = self.total_val + other.total_val 34 | res.update_time = max(self.update_time, other.update_time) 35 | return res 36 | 37 | def inc(self, time: int = 1): 38 | self.cur_day_val += time 39 | self.total_val += time 40 | self.update_time = get_current_date_int() 41 | 42 | def clr(self): 43 | self.cur_day_val = 0 44 | self.last_day_val = 0 45 | self.total_val = 0 46 | self.update_time = get_current_date_int() 47 | 48 | def update(self, past_days: int = 1): 49 | if past_days == 1: 50 | self.last_day_val = self.cur_day_val 51 | self.cur_day_val = 0 52 | elif past_days > 1: 53 | self.last_day_val = 0 54 | self.cur_day_val = 0 55 | 56 | 57 | class UserCommandStatInfo: 58 | def serialize(self) -> str: 59 | flag_list = [] 60 | for flag, elem in self.flag_dict.items(): 61 | flag_list.append(f"{flag}|{elem.serialize()}") 62 | return "&".join(flag_list) 63 | 64 | def deserialize(self, input_str: str) -> None: 65 | if not input_str.strip(): 66 | return 67 | flag_list = input_str.split("&") 68 | for flag_info in flag_list: 69 | flag_str, elem_str = flag_info.split("|", maxsplit=1) 70 | self.flag_dict[int(flag_str)] = StatElementBase() 71 | self.flag_dict[int(flag_str)].deserialize(elem_str) 72 | 73 | def __init__(self): 74 | self.flag_dict: Dict[int, StatElementBase] = {} 75 | 76 | def __add__(self, other: "UserCommandStatInfo") -> "UserCommandStatInfo": 77 | res = UserCommandStatInfo() 78 | from core.command import DPP_COMMAND_FLAG_DICT 79 | for flag in DPP_COMMAND_FLAG_DICT: 80 | if flag not in other.flag_dict and flag not in self.flag_dict: 81 | continue 82 | if flag not in res.flag_dict: 83 | res.flag_dict[flag] = StatElementBase() 84 | if flag in self.flag_dict: 85 | res.flag_dict[flag] += self.flag_dict[flag] 86 | if flag in other.flag_dict: 87 | res.flag_dict[flag] += other.flag_dict[flag] 88 | return res 89 | 90 | def record(self, command): 91 | from core.command import UserCommandBase, DPP_COMMAND_FLAG_DICT 92 | command: UserCommandBase 93 | for flag in DPP_COMMAND_FLAG_DICT.keys(): 94 | if flag & command.flag: 95 | if command.flag not in self.flag_dict: 96 | self.flag_dict[command.flag] = StatElementBase() 97 | self.flag_dict[command.flag].inc() 98 | 99 | def update(self, past_days: int = 1): 100 | for elem in self.flag_dict.values(): 101 | elem.update(past_days) 102 | 103 | 104 | class D20StatInfo: 105 | def serialize(self) -> str: 106 | val_list = self.cur_list + self.last_list + self.total_list 107 | val_list = [str(val) for val in val_list] 108 | return "|".join(val_list) 109 | 110 | def deserialize(self, input_str: str) -> None: 111 | val_list = input_str.split("|") 112 | val_list = [int(val) for val in val_list] 113 | self.cur_list = val_list[:20] 114 | self.last_list = val_list[20:40] 115 | self.total_list = val_list[40:] 116 | 117 | def __init__(self): 118 | self.cur_list = [0] * 20 119 | self.last_list = [0] * 20 120 | self.total_list = [0] * 20 121 | 122 | def record(self, d20_val: int): 123 | if d20_val < 1 or d20_val > 20: 124 | return 125 | self.cur_list[d20_val-1] += 1 126 | self.total_list[d20_val-1] += 1 127 | 128 | def update(self): 129 | self.last_list = self.cur_list 130 | self.cur_list = [0] * 20 131 | 132 | 133 | class RollStatInfo: 134 | def serialize(self) -> str: 135 | return "&".join([self.times.serialize(), self.d20.serialize()]) 136 | 137 | def deserialize(self, input_str: str) -> None: 138 | val_list = input_str.split("&") 139 | self.times.deserialize(val_list[0]) 140 | self.d20.deserialize(val_list[1]) 141 | 142 | def __init__(self): 143 | self.times: StatElementBase = StatElementBase() 144 | self.d20: D20StatInfo = D20StatInfo() 145 | 146 | def record(self): 147 | self.times.inc() 148 | 149 | def record_d20(self, d20_val: int): 150 | self.times.inc() 151 | self.d20.record(d20_val) 152 | 153 | def update(self): 154 | self.times.update() 155 | self.d20.update() 156 | 157 | 158 | @custom_json_object 159 | class MetaStatInfo(JsonObject): 160 | def serialize(self) -> str: 161 | json_dict = {"online_period": [[datetime_to_int(start_period), datetime_to_int(end_period)] for 162 | start_period, end_period in self.online_period], 163 | "msg": self.msg.serialize(), 164 | "cmd": self.cmd.serialize(), 165 | } 166 | return json.dumps(json_dict) 167 | 168 | def deserialize(self, json_str: str) -> None: 169 | json_dict: dict = json.loads(json_str) 170 | online_time_period: List[List[int, int]] = json_dict["online_period"] 171 | self.online_period = [[int_to_datetime(time_period[0]), int_to_datetime(time_period[1])] for time_period in online_time_period] 172 | self.msg.deserialize(json_dict["msg"]) 173 | self.cmd.deserialize(json_dict["cmd"]) 174 | 175 | def __init__(self): 176 | self.online_period: List[List[datetime.datetime]] = [] 177 | self.msg: StatElementBase = StatElementBase() 178 | self.cmd: UserCommandStatInfo = UserCommandStatInfo() 179 | 180 | def stat_msg(self, time: int = 1): 181 | self.msg.inc(time) 182 | 183 | def stat_cmd(self, command): 184 | self.cmd.record(command) 185 | 186 | def update(self, is_first_time: bool = False) -> bool: 187 | current_date = get_current_date_raw() 188 | if is_first_time: 189 | self.online_period.append([current_date, current_date]) 190 | 191 | should_tick_daily: bool = False 192 | if current_date.date() != self.online_period[-1][-1].date(): # 最后在线时间和当前时间不是同一天 193 | should_tick_daily = True 194 | self.msg.update() 195 | self.cmd.update() 196 | 197 | self.online_period[-1][-1] = current_date 198 | return should_tick_daily 199 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/statistics/group_stat.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from core.data import JsonObject, custom_json_object 4 | from utils.time import get_current_date_int 5 | 6 | from core.statistics.basic_stat import StatElementBase, UserCommandStatInfo, RollStatInfo 7 | 8 | 9 | class GroupMetaInfo: 10 | def serialize(self) -> str: 11 | json_dict = {"name": self.name, 12 | "member": f"{self.member_count}|{self.max_member}", 13 | "up": self.update_time, 14 | "warn": self.warn_time, 15 | } 16 | return json.dumps(json_dict) 17 | 18 | def deserialize(self, input_str: str) -> None: 19 | json_dict: dict = json.loads(input_str) 20 | self.name = json_dict["name"] 21 | self.member_count, self.max_member = (int(val_str) for val_str in json_dict["member"].split("|")) 22 | self.update_time = json_dict["up"] 23 | self.warn_time = json_dict["warn"] 24 | 25 | def __init__(self): 26 | self.name: str = "未知" 27 | self.member_count: int = -1 28 | self.max_member: int = -1 29 | self.update_time: int = 0 30 | self.warn_time: int = 0 # 自动清理相关 31 | 32 | def update(self, name: str, member_count: int, max_member: int): 33 | self.name = name 34 | self.member_count = member_count 35 | self.max_member = max_member 36 | self.update_time = get_current_date_int() 37 | 38 | 39 | @custom_json_object 40 | class GroupStatInfo(JsonObject): 41 | def serialize(self) -> str: 42 | json_dict = {"msg": self.msg.serialize(), 43 | "cmd": self.cmd.serialize(), 44 | "roll": self.roll.serialize(), 45 | "meta": self.meta.serialize(), 46 | } 47 | return json.dumps(json_dict) 48 | 49 | def deserialize(self, json_str: str) -> None: 50 | json_dict: dict = json.loads(json_str) 51 | self.msg.deserialize(json_dict["msg"]) 52 | self.cmd.deserialize(json_dict["cmd"]) 53 | self.roll.deserialize(json_dict["roll"]) 54 | self.meta.deserialize(json_dict["meta"]) 55 | 56 | def __init__(self): 57 | self.msg: StatElementBase = StatElementBase() 58 | self.cmd: UserCommandStatInfo = UserCommandStatInfo() 59 | self.roll: RollStatInfo = RollStatInfo() 60 | self.meta: GroupMetaInfo = GroupMetaInfo() 61 | 62 | def stat_msg(self, time: int = 1): 63 | self.msg.inc(time) 64 | 65 | def stat_cmd(self, command): 66 | self.cmd.record(command) 67 | 68 | def is_valid(self): 69 | raise NotImplementedError() 70 | 71 | def daily_update(self): 72 | self.msg.update() 73 | self.cmd.update() 74 | self.roll.update() 75 | -------------------------------------------------------------------------------- /src/plugins/DicePP/core/statistics/user_stat.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from core.data import JsonObject, custom_json_object 4 | 5 | from core.statistics.basic_stat import StatElementBase, UserCommandStatInfo, RollStatInfo 6 | 7 | 8 | class UserMetaInfo: 9 | def serialize(self) -> str: 10 | return "" 11 | 12 | def deserialize(self, input_str: str) -> None: 13 | pass 14 | 15 | def __init__(self): 16 | pass 17 | 18 | 19 | @custom_json_object 20 | class UserStatInfo(JsonObject): 21 | def serialize(self) -> str: 22 | json_dict = {"msg": self.msg.serialize(), 23 | "cmd": self.cmd.serialize(), 24 | "roll": self.roll.serialize(), 25 | "meta": self.meta.serialize(), 26 | } 27 | return json.dumps(json_dict) 28 | 29 | def deserialize(self, json_str: str) -> None: 30 | json_dict: dict = json.loads(json_str) 31 | self.msg.deserialize(json_dict["msg"]) 32 | self.cmd.deserialize(json_dict["cmd"]) 33 | self.roll.deserialize(json_dict["roll"]) 34 | self.meta.deserialize(json_dict["meta"]) 35 | 36 | def __init__(self): 37 | self.msg: StatElementBase = StatElementBase() 38 | self.cmd: UserCommandStatInfo = UserCommandStatInfo() 39 | self.roll: RollStatInfo = RollStatInfo() 40 | self.meta: UserMetaInfo = UserMetaInfo() 41 | 42 | def stat_msg(self, time: int = 1): 43 | self.msg.inc(time) 44 | 45 | def stat_cmd(self, command): 46 | self.cmd.record(command) 47 | 48 | def is_valid(self): 49 | raise NotImplementedError() 50 | 51 | def daily_update(self): 52 | self.msg.update() 53 | self.cmd.update() 54 | self.roll.update() 55 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/__init__.py: -------------------------------------------------------------------------------- 1 | import module.common 2 | 3 | import module.roll 4 | import module.query 5 | import module.deck 6 | import module.dice_hub 7 | 8 | import module.character 9 | import module.initiative 10 | 11 | import module.fastapi 12 | import module.misc 13 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/character/__init__.py: -------------------------------------------------------------------------------- 1 | import module.character.dnd5e 2 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/character/dnd5e/__init__.py: -------------------------------------------------------------------------------- 1 | from .health import HPInfo, CHAR_INFO_KEY_HP, CHAR_INFO_KEY_HP_DICE 2 | from .spell import SpellInfo 3 | from .ability import AbilityInfo, CHAR_INFO_KEY_ABILITY, CHAR_INFO_KEY_LEVEL, CHAR_INFO_KEY_EXT, CHAR_INFO_KEY_PROF 4 | from .ability import ability_list, skill_list, saving_list, attack_list, check_item_list, check_item_index_dict, ext_item_list, ext_item_index_dict 5 | from .money import MoneyInfo 6 | from .character import DNDCharInfo, gen_template_char 7 | 8 | from .char_command import CharacterDNDCommand, DC_CHAR_DND 9 | from .hp_command import HPCommand, DC_CHAR_HP 10 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/character/dnd5e/money.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from core.data import JsonObject, custom_json_object 4 | 5 | 6 | @custom_json_object 7 | class MoneyInfo(JsonObject): 8 | def serialize(self) -> str: 9 | json_dict = self.__dict__ 10 | for key in json_dict.keys(): 11 | value = json_dict[key] 12 | if isinstance(value, JsonObject): 13 | json_dict[key] = value.serialize() 14 | return json.dumps(json_dict) 15 | 16 | def deserialize(self, json_str: str) -> None: 17 | json_dict: dict = json.loads(json_str) 18 | for key, value in json_dict.items(): 19 | if key in self.__dict__: 20 | value_init = self.__getattribute__(key) 21 | if isinstance(value_init, JsonObject): 22 | value_init.deserialize(value) 23 | else: 24 | self.__setattr__(key, value) 25 | 26 | def __init__(self): 27 | self.gold = 0 28 | self.silver = 0 29 | self.copper = 0 30 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/character/dnd5e/spell.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | import json 3 | 4 | from core.data import JsonObject, custom_json_object 5 | 6 | 7 | @custom_json_object 8 | class SpellInfo(JsonObject): 9 | def serialize(self) -> str: 10 | json_dict = self.__dict__ 11 | for key in json_dict.keys(): 12 | value = json_dict[key] 13 | if isinstance(value, JsonObject): 14 | json_dict[key] = value.serialize() 15 | return json.dumps(json_dict) 16 | 17 | def deserialize(self, json_str: str) -> None: 18 | json_dict: dict = json.loads(json_str) 19 | for key, value in json_dict.items(): 20 | if key in self.__dict__: 21 | value_init = self.__getattribute__(key) 22 | if isinstance(value_init, JsonObject): 23 | value_init.deserialize(value) 24 | else: 25 | self.__setattr__(key, value) 26 | 27 | def __init__(self): 28 | self.slot_num: List[int] = [0] * 9 # 当前法术位数量 29 | self.slot_max: List[int] = [0] * 9 # 最大法术位数量 30 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/common/__init__.py: -------------------------------------------------------------------------------- 1 | from .activate_command import ActivateCommand, DC_ACTIVATE 2 | from .chat_command import ChatCommand, DC_CHAT_RECORD 3 | from .help_command import HelpCommand 4 | from .nickname_command import NicknameCommand 5 | from .point_command import PointCommand, DC_POINT, try_use_point 6 | from .welcome_command import WelcomeCommand, DC_WELCOME, LOC_WELCOME_DEFAULT 7 | from .master_command import MasterCommand 8 | from .macro_command import MacroCommand 9 | from .variable_command import VariableCommand 10 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/common/activate_command.py: -------------------------------------------------------------------------------- 1 | """ 2 | bot [on/off], dismiss 3 | """ 4 | 5 | from typing import List, Tuple, Any, Literal 6 | 7 | from core.bot import Bot 8 | from core.data import custom_data_chunk, DataChunkBase, DataManagerError 9 | from core.command.const import * 10 | from core.command import UserCommandBase, custom_user_command 11 | from core.command import BotCommandBase, BotSendMsgCommand, BotLeaveGroupCommand 12 | from core.communication import MessageMetaData, PrivateMessagePort, GroupMessagePort 13 | from core.config import BOT_DESCRIBE, BOT_VERSION 14 | from utils.time import get_current_date_str 15 | 16 | LOC_BOT_SHOW = "bot_show" 17 | LOC_BOT_ON = "bot_on" 18 | LOC_BOT_OFF = "bot_off" 19 | LOC_BOT_DISMISS = "bot_dismiss" 20 | 21 | CFG_BOT_DEF_ENABLE = "bot_default_enable" 22 | 23 | DC_ACTIVATE = "activate" 24 | 25 | BOT_SHOW_APPEND = f"{BOT_DESCRIBE} {BOT_VERSION}" 26 | 27 | 28 | @custom_data_chunk(identifier=DC_ACTIVATE) 29 | class _(DataChunkBase): 30 | def __init__(self): 31 | super().__init__() 32 | 33 | 34 | def get_default_activate_data(default_enable: bool) -> List: 35 | activate_data = [default_enable, get_current_date_str()] 36 | # 是否开启, 最后更改的时间 37 | return activate_data 38 | 39 | 40 | @custom_user_command(readable_name="激活指令", 41 | priority=DPP_COMMAND_PRIORITY_USUAL_LOWER_BOUND, 42 | flag=DPP_COMMAND_FLAG_DEFAULT) # 要在能屏蔽的所有指令之前响应, 否则拦截不了信息 43 | class ActivateCommand(UserCommandBase): 44 | """ 45 | bot [on/off], dismiss指令 46 | """ 47 | 48 | def __init__(self, bot: Bot): 49 | super().__init__(bot) 50 | bot.loc_helper.register_loc_text(LOC_BOT_SHOW, "", ".bot时回应的语句") 51 | bot.loc_helper.register_loc_text(LOC_BOT_ON, "G'Day, I'm on", ".bot on时回应的语句") 52 | bot.loc_helper.register_loc_text(LOC_BOT_OFF, "See you, I'm off", ".bot off时回应的语句") 53 | bot.loc_helper.register_loc_text(LOC_BOT_DISMISS, "Good bye!", ".dismiss时回应的语句") 54 | 55 | bot.cfg_helper.register_config(CFG_BOT_DEF_ENABLE, "1", "新加入群聊时是否默认开启(.bot on)") 56 | 57 | def can_process_msg(self, msg_str: str, meta: MessageMetaData) -> Tuple[bool, bool, Any]: 58 | if meta.group_id: 59 | try: 60 | activate_data = self.bot.data_manager.get_data(DC_ACTIVATE, [meta.group_id]) 61 | except DataManagerError: 62 | try: 63 | default_enable: bool = bool(int(self.bot.cfg_helper.get_config(CFG_BOT_DEF_ENABLE)[0])) 64 | except (IndexError, ValueError): 65 | default_enable = True 66 | activate_data = self.bot.data_manager.get_data(DC_ACTIVATE, [meta.group_id], 67 | default_gen=lambda: get_default_activate_data(default_enable)) 68 | else: 69 | activate_data = None 70 | should_pass: bool = False 71 | # 下列情况允许处理: 私聊, 被at, 处于开启状态 72 | if not meta.group_id or meta.to_me or activate_data[0]: 73 | should_pass = True 74 | # 下列情况不允许处理: 群聊且在开头at其他人而不是自己 75 | if meta.group_id and meta.raw_msg.startswith("[CQ:at,qq=") and not meta.to_me: 76 | should_pass = False 77 | 78 | if msg_str.startswith(".bot"): 79 | arg_str = msg_str[4:].strip() 80 | if meta.to_me and meta.group_id and (arg_str == "on" or arg_str == "off"): 81 | return True, should_pass, arg_str 82 | if not arg_str: 83 | return True, should_pass, "show" 84 | elif meta.to_me and meta.group_id and msg_str == ".dismiss": 85 | return True, should_pass, "dismiss" 86 | 87 | return (not should_pass), should_pass, "hold" 88 | 89 | def process_msg(self, msg_str: str, meta: MessageMetaData, hint: Any) -> List[BotCommandBase]: 90 | port = GroupMessagePort(meta.group_id) if meta.group_id else PrivateMessagePort(meta.user_id) 91 | # 解析语句 92 | mode: Literal["show", "on", "off", "dismiss", "hold"] = hint 93 | feedback: str 94 | bot_commands: List[BotCommandBase] = [] 95 | 96 | if mode == "hold": 97 | return bot_commands 98 | elif mode == "show": 99 | bot_show = self.format_loc(LOC_BOT_SHOW) 100 | bot_show = bot_show + "\n" if bot_show else "" 101 | feedback = f"{bot_show}{BOT_SHOW_APPEND}" 102 | elif mode == "on": 103 | activate_data = get_default_activate_data(True) 104 | self.bot.data_manager.set_data(DC_ACTIVATE, [meta.group_id], activate_data) 105 | feedback = self.format_loc(LOC_BOT_ON) 106 | elif mode == "off": 107 | activate_data = get_default_activate_data(False) 108 | self.bot.data_manager.set_data(DC_ACTIVATE, [meta.group_id], activate_data) 109 | feedback = self.format_loc(LOC_BOT_OFF) 110 | else: # mode == "dismiss": 111 | feedback = self.format_loc(LOC_BOT_DISMISS) 112 | bot_commands.append(BotLeaveGroupCommand(self.bot.account, meta.group_id)) 113 | 114 | bot_commands.append(BotSendMsgCommand(self.bot.account, feedback, [port])) 115 | return list(reversed(bot_commands)) # 需要先发消息再退出 116 | 117 | def get_help(self, keyword: str, meta: MessageMetaData) -> str: 118 | if keyword == "bot": # help后的接着的内容 119 | feedback: str = ".bot 查看机器人信息, 即使是关闭状态也会响应" \ 120 | "\n@机器人 .bot on 在群里开启机器人, 一定要在最开始at机器人才能生效" \ 121 | "\n@机器人 .bot off 关闭机器人, 同上" 122 | return feedback 123 | elif keyword == "dismiss": 124 | return "@机器人 .dismiss 让机器人退出本群, 一定要在最开始at机器人才能生效, 私聊无效" 125 | return "" 126 | 127 | def get_description(self) -> str: 128 | return "@机器人 .bot 开关机器人 .dismiss 退出当前群聊" # help指令中返回的内容 129 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/common/chat_command.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple, Any 2 | import datetime 3 | 4 | from core.bot import Bot 5 | from core.data import custom_data_chunk, DataChunkBase, DataManagerError 6 | from core.command.const import * 7 | from core.command import UserCommandBase, custom_user_command 8 | from core.command import BotCommandBase, BotSendMsgCommand 9 | from core.communication import MessageMetaData, PrivateMessagePort, GroupMessagePort 10 | from utils.time import get_current_date_str, get_current_date_raw, str_to_datetime, datetime_to_str 11 | 12 | CFG_CHAT_INTER = "chat_interval" 13 | 14 | # 增加自定义DataChunk 15 | DC_CHAT_RECORD = "chat_record" 16 | DCK_CHAT_TIME = "time" 17 | 18 | 19 | @custom_data_chunk(identifier=DC_CHAT_RECORD) 20 | class _(DataChunkBase): 21 | def __init__(self): 22 | super().__init__() 23 | 24 | 25 | def get_default_chat_time(interval: int) -> str: 26 | cur_time = get_current_date_raw() 27 | cur_time = cur_time - datetime.timedelta(seconds=interval+1) 28 | return datetime_to_str(cur_time) 29 | 30 | 31 | @custom_user_command(readable_name="自定义对话指令", priority=DPP_COMMAND_PRIORITY_TRIVIAL, 32 | flag=DPP_COMMAND_FLAG_FUN | DPP_COMMAND_FLAG_CHAT) 33 | class ChatCommand(UserCommandBase): 34 | """ 35 | 自定义对话指令 36 | """ 37 | 38 | def __init__(self, bot: Bot): 39 | super().__init__(bot) 40 | bot.cfg_helper.register_config(CFG_CHAT_INTER, "20", "自定义聊天触发间隔, 单位:秒") 41 | self.interval: int = -1 42 | self.interval_delta: datetime.timedelta = datetime.timedelta(seconds=20) 43 | 44 | def can_process_msg(self, msg_str: str, meta: MessageMetaData) -> Tuple[bool, bool, Any]: 45 | should_proc: bool = False 46 | target: str = meta.group_id if meta.group_id else meta.user_id 47 | try: 48 | time_str = self.bot.data_manager.get_data(DC_CHAT_RECORD, [target, DCK_CHAT_TIME]) 49 | except DataManagerError: 50 | default_time = get_default_chat_time(self.get_interval()) 51 | time_str = self.bot.data_manager.get_data(DC_CHAT_RECORD, [target, DCK_CHAT_TIME], default_val=default_time) 52 | feedback = "" 53 | if get_current_date_raw() >= str_to_datetime(time_str) + self.get_interval_delta(): 54 | feedback = self.bot.loc_helper.process_chat(msg_str) 55 | if feedback: 56 | should_proc = True 57 | self.bot.data_manager.set_data(DC_CHAT_RECORD, [target, DCK_CHAT_TIME], get_current_date_str()) 58 | should_pass: bool = False 59 | return should_proc, should_pass, feedback 60 | 61 | def process_msg(self, msg_str: str, meta: MessageMetaData, hint: Any) -> List[BotCommandBase]: 62 | port = GroupMessagePort(meta.group_id) if meta.group_id else PrivateMessagePort(meta.user_id) 63 | feedback: str = hint 64 | return [BotSendMsgCommand(self.bot.account, feedback, [port])] 65 | 66 | def get_help(self, keyword: str, meta: MessageMetaData) -> str: 67 | return "" 68 | 69 | def get_description(self) -> str: 70 | return "" 71 | 72 | def get_interval(self) -> int: 73 | if self.interval >= 0: 74 | return self.interval 75 | try: 76 | self.interval = int(self.bot.cfg_helper.get_config(CFG_CHAT_INTER)[0]) 77 | except (ValueError, IndexError): 78 | self.interval = 20 79 | self.interval_delta = datetime.timedelta(seconds=self.interval) 80 | return self.interval 81 | 82 | def get_interval_delta(self) -> datetime.timedelta: 83 | if self.interval >= 0: 84 | return self.interval_delta 85 | self.get_interval() 86 | return self.interval_delta 87 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/common/help_command.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple, Any 2 | 3 | from core.bot import Bot 4 | from core.command.const import * 5 | from core.command import UserCommandBase, custom_user_command 6 | from core.command import BotCommandBase, BotSendMsgCommand 7 | from core.communication import MessageMetaData, PrivateMessagePort, GroupMessagePort 8 | from core.config import BOT_DESCRIBE, BOT_VERSION, BOT_GIT_LINK, CFG_AGREEMENT 9 | 10 | LOC_HELP_INFO = "help_info" 11 | LOC_HELP_COMMAND = "help_command" 12 | LOC_HELP_AGREEMENT = "help_agreement" 13 | LOC_HELP_NOT_FOUND = "help_command_not_found" 14 | 15 | MAX_NICKNAME_LENGTH = 30 # 昵称长度上限 16 | 17 | HELP_INFO_DEFAULT = "@骰娘 .bot on/off 开启或关闭骰娘\n" \ 18 | ".help指令 查看指令列表\n" \ 19 | ".help链接 查看源码地址\n" \ 20 | ".help协议 查看使用协议\n" \ 21 | ".help更新 查看最近更新内容\n" \ 22 | "DicePP说明手册: https://docs.qq.com/doc/DV3hFWUx6VG1MUnhp\n" \ 23 | "欢迎加入交流群:861919492 伊丽莎白粉丝群或联系开发者:821480843 梨子报告bug和提出意见~" 24 | 25 | 26 | @custom_user_command(readable_name="帮助指令", 27 | priority=0, 28 | flag=DPP_COMMAND_FLAG_HELP, 29 | cluster=DPP_COMMAND_CLUSTER_DEFAULT) 30 | class HelpCommand(UserCommandBase): 31 | """ 32 | 查询帮助的指令, 以.help开头 33 | """ 34 | 35 | def __init__(self, bot: Bot): 36 | super().__init__(bot) 37 | bot.loc_helper.register_loc_text(LOC_HELP_INFO, HELP_INFO_DEFAULT, "输入.help时回复的内容") 38 | bot.loc_helper.register_loc_text(LOC_HELP_COMMAND, "{help_text}", 39 | "{help_text}代表每个指令中定义的帮助说明, 如输入.help r时会用到这个语句") 40 | bot.loc_helper.register_loc_text(LOC_HELP_NOT_FOUND, 41 | "Cannot find help info for {keyword}, try .help", 42 | "当用户输入.help {keyword}且keyword无效时发送这条消息") 43 | 44 | def can_process_msg(self, msg_str: str, meta: MessageMetaData) -> Tuple[bool, bool, Any]: 45 | should_proc: bool = msg_str.startswith(".help") 46 | should_pass: bool = False 47 | return should_proc, should_pass, msg_str[5:].strip() 48 | 49 | def process_msg(self, msg_str: str, meta: MessageMetaData, hint: Any) -> List[BotCommandBase]: 50 | # 解析语句 51 | arg_str = hint 52 | feedback: str 53 | 54 | if not arg_str: # 显示机器人总览描述 55 | feedback = f"{BOT_DESCRIBE} {BOT_VERSION}\n{self.format_loc(LOC_HELP_INFO)}" 56 | else: # 具体指令 57 | help_text = "" 58 | for command in self.bot.command_dict.values(): 59 | help_text = command.get_help(arg_str, meta) 60 | if help_text: 61 | break 62 | if not help_text: 63 | feedback = self.format_loc(LOC_HELP_NOT_FOUND, keyword=arg_str) 64 | else: 65 | feedback = self.format_loc(LOC_HELP_COMMAND, help_text=help_text) 66 | 67 | # 回复端口 68 | if meta.group_id: 69 | port = GroupMessagePort(meta.group_id) 70 | else: 71 | port = PrivateMessagePort(meta.user_id) 72 | 73 | return [BotSendMsgCommand(self.bot.account, feedback, [port])] 74 | 75 | def get_help(self, keyword: str, meta: MessageMetaData) -> str: 76 | if keyword == "指令": 77 | feedback: str = "" 78 | for command in self.bot.command_dict.values(): 79 | description_text = command.get_description() 80 | if description_text: 81 | feedback += description_text + "\n" 82 | return feedback[:-1] if feedback else "暂无信息" 83 | elif keyword == "链接": 84 | return BOT_GIT_LINK 85 | elif keyword == "协议": 86 | return self.bot.cfg_helper.get_config(CFG_AGREEMENT)[0] 87 | elif keyword == "更新": # ToDo: 更新内容 88 | return "暂无信息" 89 | 90 | return "" 91 | 92 | def get_description(self) -> str: 93 | return ".help 查看帮助" 94 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/common/macro_command.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple, Any, Optional 2 | 3 | from core.bot import Bot, BotMacro, MACRO_COMMAND_SPLIT 4 | from core.data import DataManagerError, DC_MACRO 5 | from core.command.const import * 6 | from core.command import UserCommandBase, custom_user_command 7 | from core.command import BotCommandBase, BotSendMsgCommand 8 | from core.communication import MessageMetaData, PrivateMessagePort, GroupMessagePort 9 | from core.config import CFG_COMMAND_SPLIT 10 | 11 | LOC_DEFINE_SUCCESS = "define_success" 12 | LOC_DEFINE_FAIL = "define_fail" 13 | LOC_DEFINE_LIST = "define_list" 14 | LOC_DEFINE_INFO = "define_info" 15 | LOC_DEFINE_DEL = "define_delete" 16 | 17 | CFG_DEFINE_LEN_MAX = "define_length_max" 18 | CFG_DEFINE_NUM_MAX = "define_number_max" 19 | 20 | 21 | @custom_user_command(readable_name="宏指令", priority=DPP_COMMAND_PRIORITY_DEFAULT, 22 | flag=DPP_COMMAND_FLAG_MACRO) 23 | class MacroCommand(UserCommandBase): 24 | """ 25 | 定义和查看宏指令, 关键字为define 26 | """ 27 | 28 | def __init__(self, bot: Bot): 29 | super().__init__(bot) 30 | bot.loc_helper.register_loc_text(LOC_DEFINE_SUCCESS, "Define {macro} as {target}, args are: {args}", 31 | "成功定义宏, macro为宏, target为目标字符串, args为参数列表") 32 | bot.loc_helper.register_loc_text(LOC_DEFINE_FAIL, "Fail to define macro, reason is {error}", 33 | "未成功定义宏, error为失败原因") 34 | bot.loc_helper.register_loc_text(LOC_DEFINE_LIST, "Macro list:\n{macro_list}", 35 | f"查看当前宏列表, macro_list中的每一个元素由{LOC_DEFINE_INFO}定义") 36 | bot.loc_helper.register_loc_text(LOC_DEFINE_INFO, "Keywords: {macro} Args: {args} -> {target}", 37 | f"宏列表中每一个元素的信息, macro为宏, args为参数列表, target为目标字符串") 38 | bot.loc_helper.register_loc_text(LOC_DEFINE_DEL, "Delete macro: {macro}", 39 | f"删除宏, macro为宏关键字") 40 | 41 | bot.cfg_helper.register_config(CFG_DEFINE_LEN_MAX, "300", "每个用户可以定义的宏的上限长度") 42 | bot.cfg_helper.register_config(CFG_DEFINE_NUM_MAX, "50", "每个用户可以定义的宏上限数量") 43 | 44 | def can_process_msg(self, msg_str: str, meta: MessageMetaData) -> Tuple[bool, bool, Any]: 45 | should_proc: bool = msg_str.startswith(".define") 46 | should_pass: bool = False 47 | return should_proc, should_pass, msg_str[7:].strip() 48 | 49 | def process_msg(self, msg_str: str, meta: MessageMetaData, hint: Any) -> List[BotCommandBase]: 50 | port = GroupMessagePort(meta.group_id) if meta.group_id else PrivateMessagePort(meta.user_id) 51 | # 解析语句 52 | arg_str: str = hint 53 | feedback: str = "" 54 | 55 | macro_list: List[BotMacro] 56 | try: 57 | macro_list = self.bot.data_manager.get_data(DC_MACRO, [meta.user_id]) 58 | except DataManagerError: 59 | macro_list = [] 60 | 61 | if not arg_str: 62 | def format_macro_info(i, macro): 63 | return f"{i + 1}. " + self.format_loc(LOC_DEFINE_INFO, macro=macro.key, args=macro.args, target=macro.target) 64 | macro_list_str = "\n".join((format_macro_info(i, macro) for i, macro in enumerate(macro_list))) 65 | feedback = self.format_loc(LOC_DEFINE_LIST, macro_list=macro_list_str) 66 | elif arg_str.startswith("del"): 67 | macro_key = arg_str[3:].strip() 68 | if macro_key == "all": 69 | self.bot.data_manager.delete_data(DC_MACRO, [meta.user_id]) 70 | feedback = self.format_loc(LOC_DEFINE_DEL, macro=str([macro.key for macro in macro_list])) 71 | else: 72 | del_index = -1 73 | for i, macro in enumerate(macro_list): 74 | if macro.key == macro_key: 75 | del_index = i 76 | break 77 | if del_index != -1: 78 | del macro_list[del_index] 79 | self.bot.data_manager.set_data(DC_MACRO, [meta.user_id], macro_list) 80 | feedback = self.format_loc(LOC_DEFINE_DEL, macro=macro_key) 81 | else: 82 | feedback = self.format_loc(LOC_DEFINE_FAIL, error=f"找不到关键字为{macro_key}的宏") 83 | else: # 定义新宏 84 | macro_new: Optional[BotMacro] = None 85 | macro_len_limit = int(self.bot.cfg_helper.get_config(CFG_DEFINE_LEN_MAX)[0]) 86 | macro_num_limit = int(self.bot.cfg_helper.get_config(CFG_DEFINE_NUM_MAX)[0]) 87 | try: 88 | assert len(arg_str) <= macro_len_limit 89 | macro_new = BotMacro() 90 | macro_new.initialize(arg_str, self.bot.cfg_helper.get_config(CFG_COMMAND_SPLIT)[0]) 91 | except ValueError as e: 92 | feedback = self.format_loc(LOC_DEFINE_FAIL, error=e) 93 | except AssertionError: 94 | feedback = self.format_loc(LOC_DEFINE_FAIL, error=f"自定义宏长度超出上限: {macro_len_limit}字符") 95 | if len(macro_list) >= macro_num_limit: 96 | feedback = self.format_loc(LOC_DEFINE_FAIL, error=f"自定义宏数量超出上限: {macro_num_limit} 请先删除已有宏") 97 | macro_new = None 98 | if macro_new.key == "all": 99 | feedback = self.format_loc(LOC_DEFINE_FAIL, error=f"宏关键字为保留字: {macro_new.key}") 100 | macro_new = None 101 | 102 | if macro_new: 103 | # 先移除同名宏 104 | for macro_prev in macro_list: 105 | if macro_prev.key == macro_new.key: 106 | macro_list.remove(macro_prev) 107 | macro_list.append(macro_new) 108 | self.bot.data_manager.set_data(DC_MACRO, [meta.user_id], macro_list) 109 | feedback = self.format_loc(LOC_DEFINE_SUCCESS, macro=macro_new.key, args=macro_new.args, target=macro_new.target) 110 | 111 | return [BotSendMsgCommand(self.bot.account, feedback, [port])] 112 | 113 | def get_help(self, keyword: str, meta: MessageMetaData) -> str: 114 | if keyword == "define": # help后的接着的内容 115 | feedback: str = "宏的定义方法:" \ 116 | "\t[关键字][参数列表, 形如(参数1,参数2,...), 可选][空格][目标字符串]\n" \ 117 | "\t目标字符串中与参数同名的字符串将在使用宏时被替换为给定的参数\n" \ 118 | "\t在定义时给定参数就必须在使用时给出, 否则不会被认定为宏\n" \ 119 | f"\t用{MACRO_COMMAND_SPLIT}来表示指令分隔符, {MACRO_COMMAND_SPLIT}左右的空格和换行将会被忽略\n" \ 120 | "\t注意:\n" \ 121 | "\t\t第一个空格的位置非常关键, 用来区分替换前的内容和替换后的内容\n" \ 122 | "\t\t参数名字不要重名, 宏可以嵌套, 但不会处理递归(即不可重入), 先定义的宏会先处理\n" \ 123 | "\t示例:\n" \ 124 | "\t\t.define 一颗D20 .rd20\n" \ 125 | f"\t\t.define 掷骰两次(表达式,原因) .r 表达式 原因 {MACRO_COMMAND_SPLIT} .r 表达式 原因\n" \ 126 | "宏的使用方法:\n" \ 127 | "\t[关键字][用:分隔给定参数]\n" \ 128 | "\t输入: 一颗D20 这是一颗d20 -> 等同于: .rd20 这是一颗d20\n" \ 129 | "\t输入: 掷骰两次:d20+2:某种原因 -> 等同于: 执行指令.r d20+2 某种原因 + 执行指令.r d20+2 某种原因\n" \ 130 | "查看当前定义的宏:\n" \ 131 | "\t.define\n" \ 132 | "删除某个已经定义的宏:\n" \ 133 | "\t.define del [关键字]\n" \ 134 | "删除所有宏:\n" \ 135 | "\t.define del all" 136 | return feedback 137 | return "" 138 | 139 | def get_description(self) -> str: 140 | return ".define 定义指令宏" # help指令中返回的内容 141 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/common/master_command.py: -------------------------------------------------------------------------------- 1 | """ 2 | 命令模板, 复制到新创建的文件里修改 3 | """ 4 | 5 | from typing import List, Tuple, Any 6 | 7 | from core.bot import Bot 8 | from core.command.const import * 9 | from core.command import UserCommandBase, custom_user_command 10 | from core.command import BotCommandBase, BotSendMsgCommand 11 | from core.communication import MessageMetaData, PrivateMessagePort, GroupMessagePort 12 | from core.config import CFG_MASTER 13 | 14 | LOC_REBOOT = "master_reboot" 15 | LOC_SEND_MASTER = "master_send_to_master" 16 | LOC_SEND_TARGET = "master_send_to_target" 17 | 18 | 19 | @custom_user_command(readable_name="Master指令", priority=DPP_COMMAND_PRIORITY_MASTER, 20 | flag=DPP_COMMAND_FLAG_MANAGE) 21 | class MasterCommand(UserCommandBase): 22 | """ 23 | Master指令 24 | 包括: reboot, send 25 | """ 26 | 27 | def __init__(self, bot: Bot): 28 | super().__init__(bot) 29 | bot.loc_helper.register_loc_text(LOC_REBOOT, "Reboot Complete", "重启完成") 30 | bot.loc_helper.register_loc_text(LOC_SEND_MASTER, 31 | "Send message: {msg} to {id} (type:{type})", 32 | "用.m send指令发送消息时给Master的回复") 33 | bot.loc_helper.register_loc_text(LOC_SEND_TARGET, "From Master: {msg}", "用.m send指令发送消息时给目标的回复") 34 | 35 | def can_process_msg(self, msg_str: str, meta: MessageMetaData) -> Tuple[bool, bool, Any]: 36 | master_list = self.bot.cfg_helper.get_config(CFG_MASTER) 37 | should_proc: bool = False 38 | should_pass: bool = False 39 | if meta.user_id not in master_list: 40 | return should_proc, should_pass, None 41 | 42 | should_proc = msg_str.startswith(".m") 43 | return should_proc, should_pass, msg_str[2:].strip() 44 | 45 | def process_msg(self, msg_str: str, meta: MessageMetaData, hint: Any) -> List[BotCommandBase]: 46 | port = GroupMessagePort(meta.group_id) if meta.group_id else PrivateMessagePort(meta.user_id) 47 | # 解析语句 48 | arg_str: str = hint 49 | feedback: str 50 | command_list: List[BotCommandBase] = [] 51 | 52 | if arg_str == "reboot": 53 | # noinspection PyBroadException 54 | try: 55 | self.bot.reboot() 56 | feedback = self.format_loc(LOC_REBOOT) 57 | except Exception: 58 | return self.bot.handle_exception("重启时出现错误") 59 | elif arg_str.startswith("send"): 60 | arg_list = arg_str[4:].split(":", 2) 61 | if len(arg_list) == 3: 62 | target_type, target, msg = (arg.strip() for arg in arg_list) 63 | if target_type in ["user", "group"]: 64 | feedback = self.format_loc(LOC_SEND_MASTER, msg=msg, id=target, type=target_type) 65 | target_port = PrivateMessagePort(target) if target_type == "user" else GroupMessagePort(target) 66 | command_list.append(BotSendMsgCommand(self.bot.account, msg, [target_port])) 67 | else: 68 | feedback = "目标必须为user或group" 69 | else: 70 | feedback = f"非法输入\n使用方法: {self.get_help('m send', meta)}" 71 | elif arg_str == "update": 72 | async def async_task(): 73 | update_group_result = await self.bot.update_group_info_all() 74 | update_feedback = f"已更新{len(update_group_result)}条群信息:" 75 | update_group_result = list(sorted(update_group_result, key=lambda x: -x.member_count))[:50] 76 | for group_info in update_group_result: 77 | update_feedback += f"\n{group_info.group_name}({group_info.group_id}): 群成员{group_info.member_count}/{group_info.max_member_count}" 78 | return [BotSendMsgCommand(self.bot.account, update_feedback, [port])] 79 | 80 | self.bot.register_task(async_task, timeout=60, timeout_callback=lambda: [BotSendMsgCommand(self.bot.account, "更新超时!", [port])]) 81 | feedback = "更新开始..." 82 | elif arg_str == "clean": 83 | async def clear_expired_data(): 84 | res = await self.bot.clear_expired_data() 85 | return res 86 | 87 | self.bot.register_task(clear_expired_data, timeout=3600) 88 | feedback = "清理开始..." 89 | elif arg_str == "debug-tick": 90 | feedback = f"异步任务状态: {self.bot.tick_task.get_name()} Done:{self.bot.tick_task.done()} Cancelled:{self.bot.tick_task.cancelled()}\n" \ 91 | f"{self.bot.tick_task}" 92 | elif arg_str == "redo-tick": 93 | import asyncio 94 | self.bot.tick_task = asyncio.create_task(self.bot.tick_loop()) 95 | self.bot.todo_tasks = {} 96 | feedback = "Redo tick finish!" 97 | else: 98 | feedback = self.get_help("m", meta) 99 | 100 | command_list.append(BotSendMsgCommand(self.bot.account, feedback, [port])) 101 | return command_list 102 | 103 | def get_help(self, keyword: str, meta: MessageMetaData) -> str: 104 | if keyword == "m": # help后的接着的内容 105 | return ".m reboot 重启骰娘" \ 106 | ".m send 命令骰娘发送信息" 107 | if keyword.startswith("m"): 108 | if keyword.endswith("reboot"): 109 | return "该指令将重启DicePP进程" 110 | elif keyword.endswith("send"): 111 | return ".m send [user/group]:[账号/群号]:[消息内容]" 112 | return "" 113 | 114 | def get_description(self) -> str: 115 | return ".m Master才能使用的指令" # help指令中返回的内容 116 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/common/nickname_command.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple, Any 2 | 3 | from core.bot import Bot 4 | from core.command.const import * 5 | from core.command import UserCommandBase, custom_user_command 6 | from core.command import BotCommandBase, BotSendMsgCommand 7 | from core.communication import MessageMetaData, PrivateMessagePort, GroupMessagePort 8 | 9 | LOC_NICKNAME_SET = "nickname_set" 10 | LOC_NICKNAME_RESET = "nickname_reset" 11 | LOC_NICKNAME_RESET_FAIL = "nickname_reset_fail" 12 | LOC_NICKNAME_ILLEGAL = "nickname_illegal" 13 | 14 | MAX_NICKNAME_LENGTH = 30 # 昵称长度上限 15 | 16 | 17 | @custom_user_command(readable_name="自定义昵称指令", 18 | priority=0, 19 | group_only=False, 20 | flag=DPP_COMMAND_FLAG_MANAGE) 21 | class NicknameCommand(UserCommandBase): 22 | """ 23 | 更改用户自定义昵称的指令, 以.nn开头 24 | """ 25 | 26 | def __init__(self, bot: Bot): 27 | super().__init__(bot) 28 | bot.loc_helper.register_loc_text(LOC_NICKNAME_SET, 29 | "Set your nickname as {nickname}", 30 | ".nn {nickname}返回的语句 {nickname}:昵称") 31 | bot.loc_helper.register_loc_text(LOC_NICKNAME_RESET, 32 | "Reset your nickname from {nickname_prev} to {nickname_new}", 33 | ".nn重置昵称时返回的语句 {nickname_prev}: 之前设置的昵称; {nickname_new}: 当前默认昵称") 34 | bot.loc_helper.register_loc_text(LOC_NICKNAME_RESET_FAIL, 35 | "You have not set nickname before, your current nickname is {nickname}", 36 | ".nn重置昵称且没有设置过昵称时返回的语句 {nickname}: 当前默认昵称") 37 | bot.loc_helper.register_loc_text(LOC_NICKNAME_ILLEGAL, 38 | "Illegal nickname!", 39 | "设置不合法的昵称时返回的语句") 40 | 41 | def can_process_msg(self, msg_str: str, meta: MessageMetaData) -> Tuple[bool, bool, Any]: 42 | should_proc: bool = msg_str.startswith(".nn") 43 | should_pass: bool = False 44 | return should_proc, should_pass, None 45 | 46 | def process_msg(self, msg_str: str, meta: MessageMetaData, hint: Any) -> List[BotCommandBase]: 47 | # 解析语句 48 | arg_str = msg_str[3:].strip() 49 | feedback: str 50 | 51 | if not arg_str: # 重设昵称 52 | if not meta.group_id: 53 | group_id = "default" 54 | else: 55 | group_id = meta.group_id 56 | 57 | nickname_prev = self.bot.data_manager.delete_data("nickname", [meta.user_id, group_id]) 58 | if nickname_prev: 59 | nickname_new = self.bot.get_nickname(meta.user_id, group_id) 60 | feedback = self.format_loc(LOC_NICKNAME_RESET, nickname_prev=nickname_prev, nickname_new=nickname_new) 61 | else: # 获取不到当前昵称 62 | nickname_prev = self.bot.get_nickname(meta.user_id, group_id) 63 | feedback = self.format_loc(LOC_NICKNAME_RESET_FAIL, nickname=nickname_prev) 64 | else: # 设置昵称 65 | if not self.is_legal_nickname(arg_str): # 非法昵称 66 | feedback = self.format_loc(LOC_NICKNAME_ILLEGAL) 67 | else: 68 | self.bot.update_nickname(meta.user_id, meta.group_id, arg_str) 69 | feedback = self.format_loc(LOC_NICKNAME_SET, nickname=arg_str) 70 | 71 | # 回复端口 72 | if meta.group_id: 73 | port = GroupMessagePort(meta.group_id) 74 | else: 75 | port = PrivateMessagePort(meta.user_id) 76 | 77 | return [BotSendMsgCommand(self.bot.account, feedback, [port])] 78 | 79 | def get_help(self, keyword: str, meta: MessageMetaData) -> str: 80 | if keyword == "nn": 81 | help_str = "设置昵称:.nn [昵称]\n" \ 82 | "私聊.nn视为操作全局昵称\n" \ 83 | "昵称优先级:群昵称>私聊昵称>群名片>QQ昵称\n" \ 84 | "群聊中的nn指令会智能修改先攻列表中的名字\n" \ 85 | "示例:\n" \ 86 | ".nn //视为删除昵称\n" \ 87 | ".nn dm //将昵称设置为dm" 88 | return help_str 89 | return "" 90 | 91 | def get_description(self) -> str: 92 | return ".nn 设置昵称" 93 | 94 | @classmethod 95 | def is_legal_nickname(cls, nickname: str) -> bool: 96 | """ 97 | 检查一个昵称是否合法 98 | Args: 99 | nickname: 要检查的昵称 100 | 101 | Returns: 102 | 103 | """ 104 | if not nickname or len(nickname) > MAX_NICKNAME_LENGTH: # 昵称过长 105 | return False 106 | if nickname[0] == ".": 107 | return False 108 | 109 | return True 110 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/common/point_command.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple, Any, Optional, Literal 2 | 3 | from core.bot import Bot 4 | from core.data import DataChunkBase, custom_data_chunk, DataManagerError, DC_USER_DATA 5 | from core.command.const import * 6 | from core.command import UserCommandBase, custom_user_command 7 | from core.command import BotCommandBase, BotSendMsgCommand 8 | from core.communication import MessageMetaData, PrivateMessagePort, GroupMessagePort 9 | from core.config import CFG_MASTER 10 | 11 | 12 | LOC_POINT_SHOW = "point_show" 13 | LOC_POINT_LACK = "point_lack" 14 | LOC_POINT_CHECK = "point_check" 15 | LOC_POINT_EDIT = "point_edit" 16 | LOC_POINT_EDIT_ERROR = "point_edit_error" 17 | 18 | CFG_POINT_INIT = "point_init" 19 | CFG_POINT_ADD = "point_add" 20 | CFG_POINT_MAX = "point_max" 21 | CFG_POINT_LIMIT = "point_limit" 22 | 23 | DC_POINT = "point" 24 | DCK_POINT_CUR = "current" 25 | DCK_POINT_TODAY = "today" 26 | 27 | 28 | # 存放点数数据 29 | @custom_data_chunk(identifier=DC_POINT) 30 | class _(DataChunkBase): 31 | def __init__(self): 32 | super().__init__() 33 | 34 | 35 | @custom_user_command(readable_name="点数指令", priority=DPP_COMMAND_PRIORITY_DEFAULT, 36 | flag=DPP_COMMAND_FLAG_INFO) 37 | class PointCommand(UserCommandBase): 38 | """ 39 | .point 和.m point指令 40 | """ 41 | 42 | def __init__(self, bot: Bot): 43 | super().__init__(bot) 44 | bot.loc_helper.register_loc_text(LOC_POINT_SHOW, "Point of {name}: {point}", "用户查看点数的回复") 45 | bot.loc_helper.register_loc_text(LOC_POINT_LACK, "Cannot cost point: {reason}", "用户扣除点数失败的回复") 46 | bot.loc_helper.register_loc_text(LOC_POINT_CHECK, "Point of {id}: {result}", "Master查看某人的点数") 47 | bot.loc_helper.register_loc_text(LOC_POINT_EDIT, "Point: {result}", "Master调整某人的点数") 48 | bot.loc_helper.register_loc_text(LOC_POINT_EDIT_ERROR, "Error when editing points: {error}", "Master调整某人的点数时出现错误") 49 | 50 | bot.cfg_helper.register_config(CFG_POINT_INIT, "100", "新用户初始拥有的点数") 51 | bot.cfg_helper.register_config(CFG_POINT_ADD, "100", "每天给活跃用户增加的点数") 52 | bot.cfg_helper.register_config(CFG_POINT_MAX, "500", "用户能持有的点数上限") 53 | bot.cfg_helper.register_config(CFG_POINT_LIMIT, "300", "每天使用点数的上限") 54 | 55 | def tick_daily(self) -> List[BotCommandBase]: 56 | # 根据用户当日是否掷骰决定是否增加点数(暂定) 57 | from module.roll import DCP_USER_DATA_ROLL_A_UID, DCP_ROLL_TIME_A_ID_ROLL, DCK_ROLL_TODAY 58 | point_init = int(self.bot.cfg_helper.get_config(CFG_POINT_INIT)[0]) 59 | point_add = int(self.bot.cfg_helper.get_config(CFG_POINT_ADD)[0]) 60 | point_max = int(self.bot.cfg_helper.get_config(CFG_POINT_MAX)[0]) 61 | user_ids = self.bot.data_manager.get_keys(DC_USER_DATA, []) 62 | dcp_today_roll_total_a_uid = DCP_USER_DATA_ROLL_A_UID + DCP_ROLL_TIME_A_ID_ROLL + [DCK_ROLL_TODAY] 63 | for user_id in user_ids: 64 | try: 65 | roll_time = self.bot.data_manager.get_data(DC_USER_DATA, [user_id] + dcp_today_roll_total_a_uid) 66 | assert roll_time > 0 67 | except (DataManagerError, AssertionError): 68 | continue 69 | prev_point = self.bot.data_manager.get_data(DC_POINT, [user_id, DCK_POINT_CUR], default_val=point_init) 70 | # 若已经超过上限, 说明是Master手动调整的, 不进行修改, 否则增加point_add 71 | cur_point = prev_point if prev_point > point_max else min(point_max, prev_point + point_add) 72 | self.bot.data_manager.set_data(DC_POINT, [user_id, DCK_POINT_CUR], cur_point) 73 | self.bot.data_manager.set_data(DC_POINT, [user_id, DCK_POINT_TODAY], 0) 74 | return [] 75 | 76 | def can_process_msg(self, msg_str: str, meta: MessageMetaData) -> Tuple[bool, bool, Any]: 77 | should_proc: bool = False 78 | should_pass: bool = False 79 | if msg_str.startswith(".point"): 80 | return True, should_pass, ("show", None) 81 | elif msg_str.startswith(".m"): 82 | msg_str = msg_str[2:].lstrip() 83 | master_list = self.bot.cfg_helper.get_config(CFG_MASTER) 84 | if msg_str.startswith("point") and meta.user_id in master_list: 85 | return True, should_pass, ("mod", msg_str[5:].strip()) 86 | return should_proc, should_pass, None 87 | 88 | def process_msg(self, msg_str: str, meta: MessageMetaData, hint: Any) -> List[BotCommandBase]: 89 | port = GroupMessagePort(meta.group_id) if meta.group_id else PrivateMessagePort(meta.user_id) 90 | # 解析语句 91 | cmd_type: Literal["show", "mod"] = hint[0] 92 | msg_str: Optional[str] = hint[1] 93 | feedback: str = "" 94 | point_init = int(self.bot.cfg_helper.get_config(CFG_POINT_INIT)[0]) 95 | if cmd_type == "show": 96 | cur_point = self.bot.data_manager.get_data(DC_POINT, [meta.user_id, DCK_POINT_CUR], default_val=point_init) 97 | max_point = int(self.bot.cfg_helper.get_config(CFG_POINT_MAX)[0]) 98 | nickname = self.bot.get_nickname(meta.user_id, meta.group_id) 99 | feedback = self.format_loc(LOC_POINT_SHOW, name=nickname, point=f"{cur_point}/{max_point}") 100 | elif cmd_type == "mod": 101 | if "=" in msg_str: 102 | target_id, target_point = msg_str.split("=", 1) 103 | try: 104 | target_point = int(target_point) 105 | nickname = self.bot.get_nickname(meta.user_id, target_id) 106 | prev_point = self.bot.data_manager.get_data(DC_POINT, [target_id, DCK_POINT_CUR], default_val=point_init) 107 | self.bot.data_manager.set_data(DC_POINT, [target_id, DCK_POINT_CUR], target_point) 108 | feedback = self.format_loc(LOC_POINT_EDIT, result=f"{target_id}({nickname}) {prev_point}->{target_point}") 109 | except ValueError: 110 | feedback = self.format_loc(LOC_POINT_EDIT_ERROR, error=str(ValueError)) 111 | else: 112 | target_id = msg_str 113 | nickname = self.bot.get_nickname(meta.user_id, target_id) 114 | prev_point = self.bot.data_manager.get_data(DC_POINT, [target_id, DCK_POINT_CUR], default_val=point_init) 115 | feedback = self.format_loc(LOC_POINT_EDIT, result=f"{target_id}({nickname}): {prev_point}") 116 | 117 | return [BotSendMsgCommand(self.bot.account, feedback, [port])] 118 | 119 | def get_help(self, keyword: str, meta: MessageMetaData) -> str: 120 | if keyword == "point": # help后的接着的内容 121 | feedback: str = "输入.point查看当前点数, 点数在使用消耗较大的指令时消耗" 122 | master_list = self.bot.cfg_helper.get_config(CFG_MASTER) 123 | if meta.user_id in master_list: 124 | feedback += "\n.m point [目标账号] 查看对方点数" \ 125 | "\n.m point [目标账号]=[目标数值] 将目标账号的点数设为指定数值" 126 | return feedback 127 | return "" 128 | 129 | def get_description(self) -> str: 130 | return ".point 查看当前点数" # help指令中返回的内容 131 | 132 | 133 | def try_use_point(bot: Bot, user_id: str, point: int) -> str: 134 | """尝试为user_id扣除点数, 点数不足返回失败原因, 扣除成功返回空字符串""" 135 | point_init = int(bot.cfg_helper.get_config(CFG_POINT_INIT)[0]) 136 | point_limit = int(bot.cfg_helper.get_config(CFG_POINT_LIMIT)[0]) 137 | 138 | if point < 0: 139 | return bot.loc_helper.format_loc_text(LOC_POINT_LACK, reason=f"{point} < 0") 140 | prev_point = bot.data_manager.get_data(DC_POINT, [user_id, DCK_POINT_CUR], default_val=point_init) 141 | today_point = bot.data_manager.get_data(DC_POINT, [user_id, DCK_POINT_TODAY], 0) 142 | if prev_point < point: 143 | return bot.loc_helper.format_loc_text(LOC_POINT_LACK, reason=f"当前:{prev_point} < {point}") 144 | if today_point + point > point_limit: 145 | return bot.loc_helper.format_loc_text(LOC_POINT_LACK, reason=f"今日已使用:{prev_point} + {point} > {point_limit}") 146 | cur_point = prev_point - point 147 | bot.data_manager.set_data(DC_POINT, [user_id, DCK_POINT_CUR], cur_point) 148 | return "" 149 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/common/variable_command.py: -------------------------------------------------------------------------------- 1 | """ 2 | 变量指令, 如.set 3 | """ 4 | 5 | import re 6 | from typing import Dict, List, Tuple, Any, Literal, Optional 7 | from core.bot import Bot, BotVariable 8 | from core.data import DataManagerError, DC_VARIABLE 9 | from core.command.const import * 10 | from core.command import UserCommandBase, custom_user_command 11 | from core.command import BotCommandBase, BotSendMsgCommand 12 | from core.communication import MessageMetaData, PrivateMessagePort, GroupMessagePort 13 | 14 | 15 | from module.roll import exec_roll_exp, RollDiceError 16 | 17 | LOC_VAR_SET = "var_set" 18 | LOC_VAR_GET = "var_get" 19 | LOC_VAR_GET_ALL = "var_get_all" 20 | LOC_VAR_DEL = "var_del" 21 | LOC_VAR_ERROR = "var_error" 22 | 23 | 24 | @custom_user_command(readable_name="变量指令", priority=0, # priority要大于搜索, 否则set会被s覆盖 25 | flag=DPP_COMMAND_FLAG_MACRO, group_only=True) 26 | class VariableCommand(UserCommandBase): 27 | """ 28 | 用户自定义变量 包括.set .get .del 29 | """ 30 | 31 | def __init__(self, bot: Bot): 32 | super().__init__(bot) 33 | bot.loc_helper.register_loc_text(LOC_VAR_SET, "set variable {name} as {val}", "设置变量, name为变量名, val为数值") 34 | bot.loc_helper.register_loc_text(LOC_VAR_GET, "{name} = {val}", "获取变量, name为变量名, val为数值") 35 | bot.loc_helper.register_loc_text(LOC_VAR_GET_ALL, "All Variables:\n{info}", "获取所有变量, info为逐条变量信息") 36 | bot.loc_helper.register_loc_text(LOC_VAR_DEL, "Delete variable: {name}", "删除所有变量, name为变量名") 37 | bot.loc_helper.register_loc_text(LOC_VAR_ERROR, "Error when process var: {error}", "error为处理变量时发生的错误") 38 | 39 | def can_process_msg(self, msg_str: str, meta: MessageMetaData) -> Tuple[bool, bool, Any]: 40 | should_proc: bool = False 41 | should_pass: bool = False 42 | for cmd_type in ["set", "get", "del"]: 43 | if msg_str.startswith(f".{cmd_type}"): 44 | should_proc = True 45 | return should_proc, should_pass, (cmd_type, msg_str[4:].strip()) 46 | return should_proc, should_pass, None 47 | 48 | def process_msg(self, msg_str: str, meta: MessageMetaData, hint: Any) -> List[BotCommandBase]: 49 | port = GroupMessagePort(meta.group_id) if meta.group_id else PrivateMessagePort(meta.user_id) 50 | # 解析语句 51 | cmd_type: Literal["set", "get", "del"] 52 | arg_str: str 53 | cmd_type, arg_str = hint 54 | feedback: str 55 | 56 | try: 57 | cur_var_list = self.bot.data_manager.get_keys(DC_VARIABLE, [meta.user_id, meta.group_id]) 58 | except DataManagerError: 59 | cur_var_list = [] 60 | 61 | if cmd_type == "set": 62 | # 判断操作类型 63 | set_type_list = ["=", "+", "-"] 64 | set_type: Optional[Literal["=", "+", "-"]] 65 | # 此处any为Literal["=", "+", "-"] 66 | set_info_tmp: List[Tuple[any, int]] = [(s_type, arg_str.find(s_type)) for s_type in set_type_list if arg_str.find(s_type) != -1] 67 | if not set_info_tmp: 68 | feedback = self.format_loc(LOC_VAR_ERROR, error=f"至少包含{set_type_list}其中之一") 69 | return [BotSendMsgCommand(self.bot.account, feedback, [port])] 70 | set_info_tmp.sort(key=lambda x: x[1]) 71 | set_type = set_info_tmp[0][0] 72 | split_index = set_info_tmp[0][1] 73 | var_name, var_val_str = arg_str[:split_index].strip(), arg_str[split_index+1:].strip() 74 | # 验证变量名 75 | try: 76 | assert_valid_variable_name(var_name) 77 | except ValueError as e: 78 | feedback = self.format_loc(LOC_VAR_ERROR, error=f"{var_name}: {e.args}") 79 | return [BotSendMsgCommand(self.bot.account, feedback, [port])] 80 | # 获取修改值 81 | try: 82 | var_val: int = int(var_val_str) 83 | except ValueError: 84 | try: 85 | var_val: int = exec_roll_exp(var_val_str).get_val() 86 | except RollDiceError as e: # ToDo 支持依赖其他变量 87 | feedback = self.format_loc(LOC_VAR_ERROR, error=f"{var_val_str}: {e.info}") 88 | return [BotSendMsgCommand(self.bot.account, feedback, [port])] 89 | if set_type == "=": 90 | bot_var = BotVariable() 91 | bot_var.initialize(var_name, var_val) 92 | self.bot.data_manager.set_data(DC_VARIABLE, [meta.user_id, meta.group_id, var_name], bot_var) 93 | feedback = self.format_loc(LOC_VAR_SET, name=var_name, val=var_val) 94 | else: 95 | if var_name not in cur_var_list: 96 | feedback = self.format_loc(LOC_VAR_ERROR, error=f"{var_name}不存在, 当前可用变量: {list(cur_var_list)}") 97 | return [BotSendMsgCommand(self.bot.account, feedback, [port])] 98 | bot_var: BotVariable = self.bot.data_manager.get_data(DC_VARIABLE, [meta.user_id, meta.group_id, var_name]) 99 | if set_type == "+": 100 | feedback = self.format_loc(LOC_VAR_SET, name=var_name, val=f"{bot_var.val}+{var_val}={bot_var.val+var_val}") 101 | bot_var.val = bot_var.val + var_val 102 | else: # set_type == "-" 103 | feedback = self.format_loc(LOC_VAR_SET, name=var_name, val=f"{bot_var.val}-{var_val}={bot_var.val-var_val}") 104 | bot_var.val = bot_var.val - var_val 105 | self.bot.data_manager.set_data(DC_VARIABLE, [meta.user_id, meta.group_id, var_name], bot_var) 106 | elif cmd_type == "get": 107 | var_name = arg_str 108 | if var_name: 109 | if var_name not in cur_var_list: 110 | feedback = self.format_loc(LOC_VAR_ERROR, error=f"{var_name}不存在, 当前可用变量: {list(cur_var_list)}") 111 | return [BotSendMsgCommand(self.bot.account, feedback, [port])] 112 | bot_var: BotVariable = self.bot.data_manager.get_data(DC_VARIABLE, [meta.user_id, meta.group_id, var_name]) 113 | feedback = self.format_loc(LOC_VAR_GET, name=var_name, val=bot_var.val) 114 | else: 115 | bot_var_dict: Dict[str, BotVariable] 116 | try: 117 | bot_var_dict = self.bot.data_manager.get_data(DC_VARIABLE, [meta.user_id, meta.group_id]) 118 | except DataManagerError: 119 | bot_var_dict = {} 120 | if not bot_var_dict: 121 | info = "暂无任何变量" 122 | else: 123 | var_info = [f"{var.name}={var.val}" for var in bot_var_dict.values()] 124 | info = "; ".join(var_info) 125 | feedback = self.format_loc(LOC_VAR_GET_ALL, info=info) 126 | else: # cmd_type == "del" 127 | var_name = arg_str 128 | if not var_name: 129 | feedback = self.format_loc(LOC_VAR_ERROR, error="请指定变量名") 130 | elif var_name == "all": 131 | self.bot.data_manager.delete_data(DC_VARIABLE, [meta.user_id, meta.group_id]) 132 | feedback = self.format_loc(LOC_VAR_DEL, name="; ".join(cur_var_list)) 133 | elif var_name not in cur_var_list: 134 | feedback = self.format_loc(LOC_VAR_ERROR, error=f"{var_name}不存在, 当前可用变量: {list(cur_var_list)}") 135 | else: 136 | self.bot.data_manager.delete_data(DC_VARIABLE, [meta.user_id, meta.group_id, var_name]) 137 | feedback = self.format_loc(LOC_VAR_DEL, name=var_name) 138 | 139 | return [BotSendMsgCommand(self.bot.account, feedback, [port])] 140 | 141 | def get_help(self, keyword: str, meta: MessageMetaData) -> str: 142 | feedback = "" 143 | if keyword == "set": # help后的接着的内容 144 | feedback: str = ".set [变量名] [=/+/-] [数值或掷骰表达式]" \ 145 | "\n通过=设置变量后可以对变量进行加减操作, 为每个群独立记录变量" \ 146 | "\n变量名不能含有空格或换行" \ 147 | "\n可以在语句中通过%变量名%来引用存在的变量" 148 | elif keyword == "get": 149 | feedback: str = ".get [变量名]" \ 150 | "\n查看目标变量名, 不输入变量名则获取所有当前变量" 151 | elif keyword == "del": 152 | feedback: str = ".del [变量名]" \ 153 | "\n删除目标变量名, 若目标变量名为\"all\"则删除所有变量" 154 | return feedback 155 | 156 | def get_description(self) -> str: 157 | return ".set/get/del 记录和修改变量" # help指令中返回的内容 158 | 159 | 160 | def assert_valid_variable_name(name: str): 161 | if not name: 162 | raise ValueError("变量名不能为空") 163 | # 变量名中不能包含空格或换行 164 | if re.search(r"\s", name): 165 | raise ValueError("变量名中不能含有空格") 166 | if name in ["all"]: 167 | raise ValueError("变量名为保留字") 168 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/common/welcome_command.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple, Any 2 | 3 | from core.bot import Bot 4 | from core.data import DataChunkBase, custom_data_chunk 5 | from core.command.const import * 6 | from core.command import UserCommandBase, custom_user_command 7 | from core.command import BotCommandBase, BotSendMsgCommand 8 | from core.communication import MessageMetaData, PrivateMessagePort, GroupMessagePort 9 | 10 | LOC_WELCOME_DEFAULT = "welcome_default" 11 | LOC_WELCOME_SET = "welcome_set" 12 | LOC_WELCOME_RESET = "welcome_reset" 13 | LOC_WELCOME_ILLEGAL = "welcome_illegal" 14 | 15 | DC_WELCOME = "welcome" 16 | 17 | WELCOME_MAX_LENGTH = 100 18 | 19 | 20 | # 存放welcome数据 21 | @custom_data_chunk(identifier=DC_WELCOME) 22 | class _(DataChunkBase): 23 | def __init__(self): 24 | super().__init__() 25 | 26 | 27 | @custom_user_command(readable_name="欢迎词指令", priority=DPP_COMMAND_PRIORITY_DEFAULT, 28 | flag=DPP_COMMAND_FLAG_MANAGE, group_only=True) 29 | class WelcomeCommand(UserCommandBase): 30 | """ 31 | .welcome 欢迎词指令 32 | """ 33 | 34 | def __init__(self, bot: Bot): 35 | super().__init__(bot) 36 | bot.loc_helper.register_loc_text(LOC_WELCOME_DEFAULT, "Welcome!", "默认入群欢迎词") 37 | bot.loc_helper.register_loc_text(LOC_WELCOME_SET, "Welcoming word is \"{word}\" now", "设定入群欢迎词, word为当前设定的入群欢迎词") 38 | bot.loc_helper.register_loc_text(LOC_WELCOME_RESET, "Welcoming word has been reset", "重置入群欢迎词为空") 39 | bot.loc_helper.register_loc_text(LOC_WELCOME_ILLEGAL, "Welcoming word is illegal: {reason}", "非法的入群欢迎词, reason为原因") 40 | 41 | def can_process_msg(self, msg_str: str, meta: MessageMetaData) -> Tuple[bool, bool, Any]: 42 | should_proc: bool = msg_str.startswith(".welcome") 43 | should_pass: bool = False 44 | welcome_arg = meta.raw_msg[meta.raw_msg.find(".welcome")+8:].strip() # 处理前面有cq码的情况 45 | return should_proc, should_pass, welcome_arg 46 | 47 | def process_msg(self, msg_str: str, meta: MessageMetaData, hint: Any) -> List[BotCommandBase]: 48 | port = GroupMessagePort(meta.group_id) if meta.group_id else PrivateMessagePort(meta.user_id) 49 | # 解析语句 50 | arg_str = hint 51 | feedback: str 52 | 53 | if not arg_str: 54 | self.bot.data_manager.set_data(DC_WELCOME, [meta.group_id], "") 55 | feedback = self.format_loc(LOC_WELCOME_RESET) 56 | else: 57 | if len(arg_str) > WELCOME_MAX_LENGTH: 58 | feedback = self.format_loc(LOC_WELCOME_ILLEGAL, reason=f"欢迎词长度大于{WELCOME_MAX_LENGTH}") 59 | elif arg_str == "default": 60 | self.bot.data_manager.delete_data(DC_WELCOME, [meta.group_id]) 61 | feedback = self.format_loc(LOC_WELCOME_SET, word=self.format_loc(LOC_WELCOME_DEFAULT)) 62 | else: 63 | self.bot.data_manager.set_data(DC_WELCOME, [meta.group_id], arg_str) 64 | feedback = self.format_loc(LOC_WELCOME_SET, word=arg_str) 65 | 66 | return [BotSendMsgCommand(self.bot.account, feedback, [port])] 67 | 68 | def get_help(self, keyword: str, meta: MessageMetaData) -> str: 69 | if keyword == "welcome": # help后的接着的内容 70 | feedback: str = ".welcome [入群欢迎词]" \ 71 | "welcome后接想要设置的入群欢迎词, 不输入欢迎词则不开启入群欢迎" \ 72 | ".welcome default 使用默认入群欢迎词" 73 | return feedback 74 | return "" 75 | 76 | def get_description(self) -> str: 77 | return ".welcome 设置入群欢迎词" # help指令中返回的内容 78 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/deck/__init__.py: -------------------------------------------------------------------------------- 1 | from .deck_command import DeckCommand 2 | from .random_generator_command import RandomGeneratorCommand 3 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/dice_hub/__init__.py: -------------------------------------------------------------------------------- 1 | from .encrypt import load_rsa_public_key_from_str, load_rsa_private_key_from_str,\ 2 | save_rsa_public_key_as_str, save_rsa_private_key_as_str 3 | 4 | from .manager import HubManager 5 | 6 | from .hub_command import HubCommand 7 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/dice_hub/data.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any, List 2 | import json 3 | import datetime 4 | 5 | from core.data import custom_data_chunk, DataChunkBase, custom_json_object, JsonObject 6 | from utils.time import get_current_date_str, get_current_date_raw, datetime_to_str 7 | 8 | DC_HUB = "dicehub" 9 | DCK_HUB_FRIEND = "friend" 10 | 11 | 12 | @custom_data_chunk(identifier=DC_HUB, include_json_object=True) 13 | class _(DataChunkBase): 14 | def __init__(self): 15 | super().__init__() 16 | 17 | 18 | @custom_json_object 19 | class HubFriendInfo(JsonObject): 20 | """ 21 | 好友机器人信息 22 | """ 23 | 24 | def serialize(self) -> str: 25 | json_dict = self.__dict__ 26 | return json.dumps(json_dict) 27 | 28 | def deserialize(self, json_str: str) -> None: 29 | json_dict: dict = json.loads(json_str) 30 | for key, value in json_dict.items(): 31 | if key in self.__dict__: 32 | self.__setattr__(key, value) 33 | 34 | def __init__(self): 35 | self.id: str = "" # 账号 36 | self.name: str = "" # 昵称 37 | self.master: str = "" # master账号 38 | self.version: str = "" # 当前DicePP版本号 39 | self.distance: int = 0 # 与自身需要几次转发才能达到 40 | 41 | self.init_time: str = "" # 初始化时间 42 | self.update_time: str = "" # 最近一次更新对方信息的时间 43 | self.sync_time: str = "" # 上一次向对方同步自己信息的时间, 若distance不为0, 则该值无作用 44 | self.sync_fail_times: int = 0 45 | 46 | self.sync_info: Dict[str, Any] = {} # 同步信息 47 | 48 | def initialize(self, id_str: str, name_str: str, master_id: str, version: str, distance: int = 0): 49 | self.id = id_str 50 | self.name = name_str 51 | self.master = master_id 52 | self.version = version 53 | self.distance = distance 54 | 55 | self.init_time = get_current_date_str() 56 | self.update_time = self.init_time 57 | self.sync_time = datetime_to_str(get_current_date_raw() - datetime.timedelta(days=1)) 58 | self.sync_fail_times = 0 59 | 60 | self.sync_info = {} 61 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/dice_hub/encrypt.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Tuple 3 | import rsa 4 | import base64 5 | import zlib 6 | 7 | RSA_LEN = 1024 8 | ENCRYPT_SEG_LEN = RSA_LEN // 8 9 | CONTENT_SEG_LEN = ENCRYPT_SEG_LEN - 11 10 | 11 | HEADER_LEN = 1 12 | MAX_TEXT_LEN = 2 ** (8 * HEADER_LEN) * CONTENT_SEG_LEN - 1 13 | 14 | BYTE_TO_INT_RULE = {"byteorder": "big", "signed": False} 15 | INT_TO_BYTE_RULE = {"length": HEADER_LEN, **BYTE_TO_INT_RULE} 16 | 17 | ENCODE_STYLE = "utf-8" 18 | 19 | 20 | def encrypt_rsa(text: str, public_key: rsa.PublicKey) -> str: 21 | byte_data = text.encode(ENCODE_STYLE) 22 | byte_data = zlib.compress(byte_data) 23 | assert len(byte_data) < MAX_TEXT_LEN 24 | seg_num = len(byte_data) // CONTENT_SEG_LEN + 1 25 | header = (seg_num - 1).to_bytes(**INT_TO_BYTE_RULE) 26 | result = header 27 | for seg_index in range(seg_num): 28 | seg_data = byte_data[seg_index * CONTENT_SEG_LEN:(seg_index + 1) * CONTENT_SEG_LEN] 29 | result += rsa.encrypt(seg_data, public_key) 30 | result = base64.b64encode(result) 31 | return result.decode(ENCODE_STYLE) 32 | 33 | 34 | def decrypt_rsa(rsa_str: str, private_key: rsa.PrivateKey) -> str: 35 | decode_data = rsa_str.encode(ENCODE_STYLE) 36 | decode_data = base64.b64decode(decode_data) 37 | header, decode_data = decode_data[:HEADER_LEN], decode_data[HEADER_LEN:] 38 | seg_num = int.from_bytes(header, **BYTE_TO_INT_RULE) + 1 39 | result = b"" 40 | for seg_index in range(seg_num): 41 | seg_data = decode_data[seg_index * ENCRYPT_SEG_LEN:(seg_index + 1) * ENCRYPT_SEG_LEN] 42 | try: 43 | result += rsa.decrypt(seg_data, private_key) 44 | except rsa.pkcs1.DecryptionError: 45 | raise ValueError("RSA Fail") 46 | try: 47 | result = zlib.decompress(result) 48 | except zlib.error: 49 | raise ValueError("Z-lib Error") 50 | return result.decode(ENCODE_STYLE) 51 | 52 | 53 | def create_rsa_key(name: str, path: str) -> Tuple[rsa.PublicKey, rsa.PrivateKey]: 54 | public_key, private_key = rsa.newkeys(RSA_LEN) 55 | try: 56 | save_rsa_public_key(public_key, name, path) 57 | save_rsa_private_key(private_key, name, path) 58 | except PermissionError as e: 59 | raise e 60 | return public_key, private_key 61 | 62 | 63 | def save_rsa_public_key(public_key: rsa.PublicKey, name: str, path: str) -> str: 64 | public_path = os.path.join(path, name) + ".pub" 65 | try: 66 | with open(public_path, "w") as f: 67 | f.write(save_rsa_public_key_as_str(public_key)) 68 | except PermissionError as e: 69 | raise e 70 | return public_path 71 | 72 | 73 | def save_rsa_private_key(private_key: rsa.PrivateKey, name: str, path: str) -> str: 74 | private_path = os.path.join(path, name) 75 | try: 76 | with open(private_path, "w") as f: 77 | f.write(save_rsa_private_key_as_str(private_key)) 78 | except PermissionError as e: 79 | raise e 80 | return private_path 81 | 82 | 83 | def load_rsa_public_key(name: str, path: str) -> rsa.PublicKey: 84 | public_path = os.path.join(path, name) + ".pub" 85 | try: 86 | with open(public_path, "r") as f: 87 | key_str = f.read() 88 | except FileNotFoundError: 89 | raise ValueError() 90 | return load_rsa_public_key_from_str(key_str) 91 | 92 | 93 | def load_rsa_private_key(name: str, path: str) -> rsa.PrivateKey: 94 | private_path = os.path.join(path, name) 95 | try: 96 | with open(private_path, "r") as f: 97 | key_str = f.read() 98 | except FileNotFoundError: 99 | raise ValueError() 100 | return load_rsa_private_key_from_str(key_str) 101 | 102 | 103 | def load_rsa_public_key_from_str(key_str: str) -> rsa.PublicKey: 104 | return rsa.PublicKey.load_pkcs1(key_str.encode(ENCODE_STYLE)) 105 | 106 | 107 | def load_rsa_private_key_from_str(key_str: str) -> rsa.PrivateKey: 108 | return rsa.PrivateKey.load_pkcs1(key_str.encode(ENCODE_STYLE)) 109 | 110 | 111 | def save_rsa_public_key_as_str(public_key: rsa.PublicKey) -> str: 112 | return public_key.save_pkcs1().decode(ENCODE_STYLE) 113 | 114 | 115 | def save_rsa_private_key_as_str(private_key: rsa.PrivateKey) -> str: 116 | return private_key.save_pkcs1().decode(ENCODE_STYLE) 117 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/fastapi/__init__.py: -------------------------------------------------------------------------------- 1 | from .api import * 2 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/fastapi/api.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Path, Query 2 | 3 | dpp_api = FastAPI() 4 | 5 | 6 | @dpp_api.get("/") 7 | def test_api(): 8 | return {"Test": "This is a test api"} 9 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/initiative/__init__.py: -------------------------------------------------------------------------------- 1 | from .initiative_entity import InitEntity 2 | from .initiative_list import InitList, INIT_LIST_SIZE, DC_INIT 3 | from .initiative_command import InitiativeCommand 4 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/initiative/initiative_entity.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from core.data import JsonObject, custom_json_object 4 | 5 | 6 | @custom_json_object 7 | class InitEntity(JsonObject): 8 | def serialize(self) -> str: 9 | json_dict = self.__dict__ 10 | return json.dumps(json_dict) 11 | 12 | def deserialize(self, json_str: str) -> None: 13 | json_dict: dict = json.loads(json_str) 14 | for key, value in json_dict.items(): 15 | if key in self.__dict__: 16 | self.__setattr__(key, value) 17 | 18 | def __init__(self): 19 | self.name: str = "" # 先攻条目名称 20 | self.owner: str = "" # 拥有者id, 为空代表是npc 21 | self.init: int = 0 # 先攻数值 22 | 23 | def get_info(self) -> str: 24 | info = f"{self.name} 先攻:{self.init}" 25 | return info 26 | 27 | def __repr__(self): 28 | return f"InitEntity({self.name},{self.init},{self.owner if self.owner else None})" 29 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/initiative/initiative_list.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List 2 | import json 3 | 4 | from core.data import custom_data_chunk, DataChunkBase 5 | from core.data import JsonObject, custom_json_object 6 | from utils.time import get_current_date_str 7 | 8 | from module.initiative.initiative_entity import InitEntity 9 | 10 | DC_INIT = "initiative" 11 | 12 | INIT_LIST_SIZE = 30 # 一个先攻列表的容量 13 | 14 | 15 | @custom_data_chunk(identifier=DC_INIT, 16 | include_json_object=True) 17 | class _(DataChunkBase): 18 | def __init__(self): 19 | super().__init__() 20 | self.version: int = 0 21 | 22 | def introspect(self) -> None: 23 | if self.version == 0: 24 | self.root = {} # 无效所有数据 25 | self.version = 1 26 | 27 | 28 | @custom_json_object 29 | class InitList(JsonObject): 30 | def serialize(self) -> str: 31 | json_dict = self.__dict__ 32 | assert "entities" in json_dict.keys() 33 | for key in json_dict.keys(): 34 | value = json_dict[key] 35 | if key == "entities": 36 | json_dict[key] = [entity.serialize() for entity in self.entities] 37 | if isinstance(value, JsonObject): 38 | json_dict[key] = value.serialize() 39 | return json.dumps(json_dict) 40 | 41 | def deserialize(self, json_str: str) -> None: 42 | json_dict: dict = json.loads(json_str) 43 | assert "entities" in json_dict.keys() 44 | for key, value in json_dict.items(): 45 | if key in self.__dict__: 46 | value_init = self.__getattribute__(key) 47 | if key == "entities": 48 | self.entities = [] 49 | for entity_str in value: 50 | entity = InitEntity() 51 | entity.deserialize(entity_str) 52 | self.entities.append(entity) 53 | elif isinstance(value_init, JsonObject): 54 | value_init.deserialize(value) 55 | else: 56 | self.__setattr__(key, value) 57 | 58 | def __init__(self): 59 | self.entities: List[InitEntity] = [] 60 | self.mod_time = get_current_date_str() 61 | 62 | def __repr__(self): 63 | return f"InitList({self.entities}, {self.mod_time})" 64 | 65 | def add_entity(self, entity_name: str, owner_id: str, init: int) -> None: 66 | """ 67 | 创造一个先攻条目并加入到先攻列表中, 如果存在同名条目则抛出InitiativeError, 记录不会被添加到列表中. 68 | Args: 69 | entity_name: 条目名称 70 | owner_id: 为空代表无主的NPC, 不为空代表PC 71 | init: 生成先攻所需的完整掷骰表达式 72 | """ 73 | # 检查有没有同名条目, 有则删掉旧的 74 | replace_same_name = sum([entity.name == entity_name for entity in self.entities]) 75 | if replace_same_name: 76 | self.del_entity(entity_name) 77 | 78 | if len(self.entities) >= INIT_LIST_SIZE: 79 | raise InitiativeError(f"先攻列表大小超出限制, 至多存在{INIT_LIST_SIZE}个条目") 80 | 81 | entity: InitEntity = InitEntity() 82 | entity.name = entity_name 83 | entity.owner = owner_id 84 | entity.init = init 85 | self.entities.append(entity) 86 | self.entities = sorted(self.entities, key=lambda x: -x.init) 87 | self.mod_time = get_current_date_str() 88 | 89 | def del_entity(self, entity_name: str) -> None: 90 | """ 91 | 将一个先攻条目根据名称从列表中删除, 如果没有这样的条目或是存在多个同名条目则抛出异常 92 | Args: 93 | entity_name: 条目名称 94 | """ 95 | # 检查同名条目 96 | all_index = [index for index, entity in enumerate(self.entities) if entity.name == entity_name] 97 | if len(all_index) == 0: 98 | raise InitiativeError(f"先攻列表中不存在名称为{entity_name}的条目") 99 | if len(all_index) > 1: 100 | raise InitiativeError(f"先攻列表中存在多个名称为{entity_name}的条目") 101 | del self.entities[all_index[0]] 102 | self.mod_time = get_current_date_str() 103 | 104 | 105 | class InitiativeError(Exception): 106 | """ 107 | Initiative模块产生的异常, 说明操作失败的原因, 应当在Command内捕获 108 | """ 109 | def __init__(self, info: str): 110 | self.info = info 111 | 112 | def __str__(self): 113 | return f"[Initiative] [Error] {self.info}" 114 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/misc/__init__.py: -------------------------------------------------------------------------------- 1 | from .jrrp_command import JrrpCommand 2 | from .dnd_command import UtilsDNDCommand 3 | from .statistics_cmd import StatisticsCommand 4 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/misc/dnd_command.py: -------------------------------------------------------------------------------- 1 | """ 2 | 简单的DND相关指令 3 | """ 4 | 5 | from typing import List, Tuple, Any 6 | import random 7 | 8 | from core.bot import Bot 9 | from core.command.const import * 10 | from core.command import UserCommandBase, custom_user_command 11 | from core.command import BotCommandBase, BotSendMsgCommand 12 | from core.communication import MessageMetaData, PrivateMessagePort, GroupMessagePort 13 | from core import localization 14 | 15 | LOC_DND_RES = "dnd_result" 16 | 17 | CFG_ROLL_DND_ENABLE = "roll_dnd_enable" 18 | 19 | MAX_DND_TIMES = 10 20 | MAX_DND_RESULT_LEN = 50 21 | 22 | 23 | @custom_user_command(readable_name="DND属性指令", 24 | priority=DPP_COMMAND_PRIORITY_DEFAULT, 25 | flag=DPP_COMMAND_FLAG_FUN | DPP_COMMAND_FLAG_DND) 26 | class UtilsDNDCommand(UserCommandBase): 27 | """ 28 | .dnd指令, 相当于6#4d6k3, 可以重复投多次, 如.dnd5 29 | """ 30 | 31 | def __init__(self, bot: Bot): 32 | super().__init__(bot) 33 | bot.loc_helper.register_loc_text(LOC_DND_RES, "{name} DND Result {reason}:\n{result}", ".dnd返回的内容 name为用户昵称, reason为原因") 34 | bot.cfg_helper.register_config(CFG_ROLL_DND_ENABLE, "1", "DND指令开关") 35 | 36 | def can_process_msg(self, msg_str: str, meta: MessageMetaData) -> Tuple[bool, bool, Any]: 37 | should_proc: bool = msg_str.startswith(".dnd") 38 | should_pass: bool = False 39 | msg_str = msg_str[4:].strip() 40 | args = msg_str.split(" ", 1) 41 | reason = args[1].strip()[:MAX_DND_RESULT_LEN] if len(args) > 1 else "" 42 | try: 43 | times = int(args[0]) 44 | assert 1 <= times <= MAX_DND_TIMES 45 | except (ValueError, AssertionError): 46 | times = 1 47 | return should_proc, should_pass, (times, reason) 48 | 49 | def process_msg(self, msg_str: str, meta: MessageMetaData, hint: Any) -> List[BotCommandBase]: 50 | port = GroupMessagePort(meta.group_id) if meta.group_id else PrivateMessagePort(meta.user_id) 51 | # 判断功能开关 52 | try: 53 | assert (int(self.bot.cfg_helper.get_config(CFG_ROLL_DND_ENABLE)[0]) != 0) 54 | except AssertionError: 55 | feedback = self.bot.loc_helper.format_loc_text(localization.LOC_FUNC_DISABLE, func=self.readable_name) 56 | return [BotSendMsgCommand(self.bot.account, feedback, [port])] 57 | # 解析语句 58 | times: int 59 | reason: str 60 | times, reason = hint 61 | 62 | dnd_result = [] 63 | for _ in range(times): 64 | attr_result: List[int] = [] 65 | for _ in range(6): 66 | attr_result.append(sum(list(sorted([random.randint(1, 6) for _ in range(4)], key=lambda x: -x))[:3])) 67 | attr_result_str = str(list(sorted(attr_result, key=lambda x: -x))) 68 | dnd_result.append(f"{attr_result_str} = {sum(attr_result)}") 69 | dnd_result = "\n".join(dnd_result) 70 | 71 | user_name = self.bot.get_nickname(meta.user_id, meta.group_id) 72 | feedback: str = self.format_loc(LOC_DND_RES, name=user_name, reason=reason, result=dnd_result) 73 | 74 | return [BotSendMsgCommand(self.bot.account, feedback, [port])] 75 | 76 | def get_help(self, keyword: str, meta: MessageMetaData) -> str: 77 | if keyword == "dnd": # help后的接着的内容 78 | feedback: str = ".dnd [次数] [原因] 相当于4D6K3" 79 | return feedback 80 | return "" 81 | 82 | def get_description(self) -> str: 83 | return ".dnd DND属性生成" # help指令中返回的内容 84 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/misc/jrrp_command.py: -------------------------------------------------------------------------------- 1 | import random 2 | from typing import List, Tuple, Any 3 | 4 | from core.bot import Bot 5 | from core.command.const import * 6 | from core.command import UserCommandBase, custom_user_command 7 | from core.command import BotCommandBase, BotSendMsgCommand 8 | from core.communication import MessageMetaData, PrivateMessagePort, GroupMessagePort 9 | from utils.time import datetime_to_str_day, get_current_date_raw, datetime_to_str 10 | 11 | LOC_JRRP = "jrrp" 12 | 13 | 14 | @custom_user_command(readable_name="今日人品", priority=DPP_COMMAND_PRIORITY_DEFAULT, 15 | flag=DPP_COMMAND_FLAG_FUN) 16 | class JrrpCommand(UserCommandBase): 17 | 18 | def __init__(self, bot: Bot): 19 | super().__init__(bot) 20 | bot.loc_helper.register_loc_text(LOC_JRRP, "{name}'s daily lucky points are:{jrrp}", ".jrrp返回的内容,{name}:用户名,{jrrp}:今日人品值.") 21 | 22 | def can_process_msg(self, msg_str: str, meta: MessageMetaData) -> Tuple[bool, bool, Any]: 23 | should_proc: bool = msg_str.startswith(".jrrp") 24 | should_pass: bool = False 25 | return should_proc, should_pass, msg_str[5:].strip() 26 | 27 | def process_msg(self, msg_str: str, meta: MessageMetaData, hint: Any) -> List[BotCommandBase]: 28 | port = GroupMessagePort(meta.group_id) if meta.group_id else PrivateMessagePort(meta.user_id) 29 | # 解析语句 30 | date_str: str = datetime_to_str_day(get_current_date_raw()) 31 | seed_str: str = date_str + meta.user_id # 拼接形成一个固定的seed 32 | 33 | random.seed(seed_str) 34 | jrrp: str = str(random.randint(1, 100)) # 根据上面的seed获取确定值 35 | random.seed(datetime_to_str(get_current_date_raw())) 36 | 37 | user_name: str = self.bot.get_nickname(meta.user_id, meta.group_id) 38 | feedback: str = self.format_loc(LOC_JRRP, name=user_name, jrrp=jrrp) 39 | 40 | return [BotSendMsgCommand(self.bot.account, feedback, [port])] 41 | 42 | def get_help(self, keyword: str, meta: MessageMetaData) -> str: 43 | if keyword == "jrrp": # help后的接着的内容 44 | feedback: str = ".jrrp 获取今日人品,每日0点刷新" 45 | return feedback 46 | return "" 47 | 48 | def get_description(self) -> str: 49 | return ".jrrp 获取今日人品" # help指令中返回的内容 50 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/misc/statistics_cmd.py: -------------------------------------------------------------------------------- 1 | """ 2 | 统计指令, 返回用户或群聊的一些统计信息 3 | """ 4 | 5 | from typing import List, Tuple, Any, Dict 6 | 7 | from core.bot import Bot 8 | from core.data import DataManagerError 9 | from core.command.const import * 10 | from core.command import UserCommandBase, custom_user_command 11 | from core.command import BotCommandBase, BotSendMsgCommand 12 | from core.communication import MessageMetaData, PrivateMessagePort, GroupMessagePort 13 | 14 | from core.data import DC_META, DC_NICKNAME, DC_MACRO, DC_VARIABLE, DC_USER_DATA, DC_GROUP_DATA, DCK_USER_STAT, DCK_GROUP_STAT 15 | from core.statistics import GroupStatInfo, UserStatInfo, UserCommandStatInfo, RollStatInfo 16 | 17 | # LOC_TEMP = "template_loc" 18 | 19 | 20 | @custom_user_command(readable_name="指令模板", priority=DPP_COMMAND_PRIORITY_DEFAULT, flag=DPP_COMMAND_FLAG_INFO) 21 | class StatisticsCommand(UserCommandBase): 22 | """ 23 | 统计指令, 返回用户或群聊的一些统计信息 24 | """ 25 | 26 | def __init__(self, bot: Bot): 27 | super().__init__(bot) 28 | # bot.loc_helper.register_loc_text(LOC_TEMP, "内容", "注释") 29 | 30 | def can_process_msg(self, msg_str: str, meta: MessageMetaData) -> Tuple[bool, bool, Any]: 31 | should_proc: bool = msg_str.startswith(".统计") 32 | should_pass: bool = False 33 | return should_proc, should_pass, msg_str[3:].strip() 34 | 35 | def process_msg(self, msg_str: str, meta: MessageMetaData, hint: Any) -> List[BotCommandBase]: 36 | port = GroupMessagePort(meta.group_id) if meta.group_id else PrivateMessagePort(meta.user_id) 37 | # 解析语句 38 | arg_str = hint 39 | feedback: str = "" 40 | if not arg_str: # 统计当前用户信息 41 | # 统计处理信息情况 42 | try: 43 | user_stat: UserStatInfo = self.bot.data_manager.get_data(DC_USER_DATA, [meta.user_id, DCK_USER_STAT]) 44 | except DataManagerError: 45 | user_stat = UserStatInfo() 46 | feedback += f"今日收到信息:{user_stat.msg.cur_day_val}, 昨日:{user_stat.msg.last_day_val}, 总计:{user_stat.msg.total_val}\n" 47 | # 统计指令使用情况 48 | feedback += stat_cmd_info(user_stat.cmd) 49 | # 统计掷骰情况 50 | feedback += stat_roll_info(user_stat.roll) 51 | elif arg_str == "群聊": 52 | if not meta.group_id: 53 | feedback += f"当前不在群聊中..." 54 | # 统计处理信息情况 55 | try: 56 | group_stat: GroupStatInfo = self.bot.data_manager.get_data(DC_GROUP_DATA, [meta.group_id, DCK_GROUP_STAT]) 57 | except DataManagerError: 58 | group_stat = GroupStatInfo() 59 | feedback += f"今日收到信息:{group_stat.msg.cur_day_val}, 昨日:{group_stat.msg.last_day_val}, 总计:{group_stat.msg.total_val}\n" 60 | # 统计指令使用情况 61 | feedback += stat_cmd_info(group_stat.cmd) 62 | # 统计掷骰情况 63 | feedback += stat_roll_info(group_stat.roll) 64 | elif arg_str == "所有用户": 65 | if meta.user_id not in self.bot.get_master_ids(): 66 | feedback = "权限不足" 67 | else: 68 | merge_user_stat = UserStatInfo() 69 | for user_id in self.bot.data_manager.get_keys(DC_USER_DATA, []): 70 | try: 71 | user_stat: UserStatInfo = self.bot.data_manager.get_data(DC_USER_DATA, [user_id, DCK_USER_STAT]) 72 | except DataManagerError: 73 | continue 74 | # 统计处理信息情况 75 | merge_user_stat.msg += user_stat.msg 76 | # 统计指令使用情况 77 | merge_user_stat.cmd += user_stat.cmd 78 | feedback += f"今日收到信息:{merge_user_stat.msg.cur_day_val}," \ 79 | f" 昨日:{merge_user_stat.msg.last_day_val}," \ 80 | f" 总计:{merge_user_stat.msg.total_val}\n" 81 | feedback += stat_cmd_info(merge_user_stat.cmd) 82 | elif meta.user_id in self.bot.get_master_ids() and arg_str == "所有群聊": 83 | if meta.user_id not in self.bot.get_master_ids(): 84 | feedback = "权限不足" 85 | else: 86 | group_info_list: List[List[str, int, str]] = [] # id, sort_key, info_str 87 | for group_id in self.bot.data_manager.get_keys(DC_GROUP_DATA, []): 88 | group_info = [group_id, 0, ""] 89 | try: 90 | group_stat: GroupStatInfo = self.bot.data_manager.get_data(DC_GROUP_DATA, [group_id, DCK_GROUP_STAT]) 91 | except DataManagerError: 92 | continue 93 | 94 | group_info[1] = group_stat.meta.member_count # 最小优先级 95 | group_info[2] += f"{group_id}({group_stat.meta.name}) 成员:{group_stat.meta.member_count} " 96 | 97 | group_info[1] += (group_stat.msg.cur_day_val + group_stat.msg.total_val // 1000) << 32 # 最大优先级 98 | group_info[2] += f"信息:[{group_stat.msg.cur_day_val}, {group_stat.msg.last_day_val}, {group_stat.msg.total_val}] " 99 | # 统计指令使用情况 100 | cmd_score = stat_cmd_score(group_stat.cmd, group_stat.msg.last_day_val, group_stat.msg.total_val) 101 | group_info[1] += cmd_score << 16 # 次大优先级 102 | group_info[2] += f"评分:{cmd_score}" 103 | 104 | group_info_list.append(group_info) 105 | group_info_list = sorted(group_info_list, key=lambda x: -x[1]) 106 | feedback += f"共{len(group_info_list)}条群组信息:\n" 107 | feedback += "\n".join([group_info[2] for group_info in group_info_list[:50]]) 108 | if len(group_info_list) > 50: 109 | feedback += f"\n{len(group_info_list) - 50}条信息限于篇幅未显示完全" 110 | 111 | feedback = feedback.strip() 112 | return [BotSendMsgCommand(self.bot.account, feedback, [port])] 113 | 114 | def get_help(self, keyword: str, meta: MessageMetaData) -> str: 115 | if keyword == "统计": # help后的接着的内容 116 | feedback: str = "可以统计用户和群聊的各种信息\n" \ 117 | ".统计 显示当前用户的统计信息\n" \ 118 | ".统计群聊 显示当前群聊的统计信息\n" \ 119 | "[Master专用]\n" \ 120 | ".统计所有用户 可以显示当前所有用户对指令的使用情况\n" \ 121 | ".统计所有群聊 可以显示每一个群聊对指令的使用情况" 122 | return feedback 123 | return "" 124 | 125 | def get_description(self) -> str: 126 | return ".统计 统计用户与群聊信息" # help指令中返回的内容 127 | 128 | 129 | def stat_cmd_info(cmd_stat: UserCommandStatInfo) -> str: 130 | total_info_list, today_info_list = [], [] 131 | for flag, name in DPP_COMMAND_FLAG_DICT.items(): 132 | if flag not in cmd_stat.flag_dict: 133 | continue 134 | if flag & DPP_COMMAND_FLAG_SET_HIDE_IN_STAT: 135 | continue 136 | total_num, today_num = cmd_stat.flag_dict[flag].total_val, cmd_stat.flag_dict[flag].cur_day_val 137 | if total_num: 138 | total_info_list.append(f"{name}:{total_num}") 139 | if today_num: 140 | today_info_list.append(f"{name}:{today_num}") 141 | if total_info_list: 142 | total_info = ", ".join(total_info_list) 143 | else: 144 | total_info = "暂无记录" 145 | if today_info_list: 146 | today_info = ", ".join(today_info_list) 147 | else: 148 | today_info = "暂无记录" 149 | return f"今日指令记录: {today_info}\n总计: {total_info}\n" 150 | 151 | 152 | def stat_roll_info(roll_stat: RollStatInfo) -> str: 153 | # 今日 154 | today_info = f"今日掷骰次数:{roll_stat.times.cur_day_val} D20统计:{roll_stat.d20.cur_list}" 155 | if sum(roll_stat.d20.cur_list) == 0: 156 | d20_avg = 0 157 | else: 158 | d20_avg = sum([(i + 1) * num for i, num in enumerate(roll_stat.d20.cur_list)]) / sum(roll_stat.d20.cur_list) 159 | today_info += " 平均值: {:.3f}".format(d20_avg) 160 | # 总计 161 | total_info = f"总计掷骰次数:{roll_stat.times.total_val} D20统计:{roll_stat.d20.total_list}" 162 | if sum(roll_stat.d20.total_list) == 0: 163 | d20_avg = 0 164 | else: 165 | d20_avg = sum([(i + 1) * num for i, num in enumerate(roll_stat.d20.total_list)]) / sum(roll_stat.d20.total_list) 166 | total_info += " 平均值: {:.3f}".format(d20_avg) 167 | return f"{today_info}\n{total_info}\n" 168 | 169 | 170 | def stat_cmd_score(cmd_flag_info: UserCommandStatInfo, last_msg_num: int, total_msg_num: int) -> int: 171 | res = 0 172 | for flag in DPP_COMMAND_FLAG_DICT: 173 | if flag not in cmd_flag_info.flag_dict: 174 | continue 175 | total_num, last_num = cmd_flag_info.flag_dict[flag].total_val, cmd_flag_info.flag_dict[flag].last_day_val 176 | if not last_msg_num: 177 | last_num, last_msg_num = 0, 1 178 | if flag & DPP_COMMAND_FLAG_SET_STD: 179 | weight: float = (total_num/total_msg_num/50 + last_num/last_msg_num) * 100 180 | res += min(weight, 1) 181 | if flag & DPP_COMMAND_FLAG_SET_EXT_0: 182 | weight: float = (total_num/total_msg_num/50 + last_num/last_msg_num) * 100 183 | res -= min(weight, 1) 184 | return int(res * 100) 185 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/query/__init__.py: -------------------------------------------------------------------------------- 1 | from .query_command import QueryCommand 2 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/roll/__init__.py: -------------------------------------------------------------------------------- 1 | from .result import RollResult 2 | from .expression import RollExpression, is_roll_exp, exec_roll_exp, preprocess_roll_exp, parse_roll_exp 3 | from .roll_utils import RollDiceError 4 | 5 | from .roll_dice_command import RollDiceCommand 6 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/roll/connector.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from typing import Type, Dict 3 | 4 | from module.roll.result import RollResult 5 | 6 | 7 | class RollExpConnector(metaclass=abc.ABCMeta): 8 | """ 9 | 用于链接掷骰表达式结果 10 | """ 11 | symbol: str = None 12 | 13 | @abc.abstractmethod 14 | def connect(self, lhs: RollResult, rhs: RollResult) -> RollResult: 15 | """ 16 | 修改并返回掷骰数据, 返回的变量应该基于初始变量 17 | """ 18 | raise NotImplementedError() 19 | 20 | 21 | ROLL_CONNECTORS_DICT: Dict[str, Type[RollExpConnector]] = {} 22 | 23 | 24 | def roll_connector(symbol: str): 25 | """ 26 | 类修饰器, 将自定义掷骰表达式连接符注册到字典中 27 | Args: 28 | symbol: 一个字符串, 唯一地匹配该连接符, 若有多个符合则较早定义的优先 29 | """ 30 | def inner(cls: Type[RollExpConnector]): 31 | """ 32 | Args: 33 | cls: 修饰的类必须继承自RollExpConnector 34 | Returns: 35 | cls: 返回修饰后的cls 36 | """ 37 | assert issubclass(cls, RollExpConnector) 38 | assert " " not in symbol 39 | assert symbol not in ROLL_CONNECTORS_DICT.keys() 40 | cls.symbol = symbol 41 | ROLL_CONNECTORS_DICT[symbol] = cls 42 | return cls 43 | return inner 44 | 45 | 46 | @roll_connector("+") 47 | class REModAdd(RollExpConnector): 48 | """ 49 | 表示加法 50 | """ 51 | def connect(self, lhs: RollResult, rhs: RollResult) -> RollResult: 52 | lhs.val_list += rhs.val_list 53 | lhs.info = f"{lhs.info}+{rhs.info}" 54 | lhs.type = None 55 | lhs.exp = f"{lhs.exp}+{rhs.exp}" 56 | lhs.d20_num += rhs.d20_num 57 | lhs.d20_state = rhs.d20_state if not lhs.d20_state else lhs.d20_state 58 | return lhs 59 | 60 | 61 | @roll_connector("-") 62 | class REModSubstract(RollExpConnector): 63 | """ 64 | 表示减法 65 | """ 66 | def connect(self, lhs: RollResult, rhs: RollResult) -> RollResult: 67 | lhs.val_list += [v*-1 for v in rhs.val_list] 68 | lhs.info = f"{lhs.info}-{rhs.info}" 69 | lhs.type = None 70 | lhs.exp = f"{lhs.exp}-{rhs.exp}" 71 | lhs.d20_num += rhs.d20_num 72 | lhs.d20_state = rhs.d20_state if not lhs.d20_state else lhs.d20_state 73 | return lhs 74 | 75 | 76 | @roll_connector("*") 77 | class REModMultiply(RollExpConnector): 78 | """ 79 | 表示乘法 80 | """ 81 | def connect(self, lhs: RollResult, rhs: RollResult) -> RollResult: 82 | lhs.val_list = [sum(lhs.val_list) * sum(rhs.val_list)] 83 | lhs.info = f"{lhs.info}*{rhs.info}" 84 | lhs.type = None 85 | lhs.exp = f"{lhs.exp}*{rhs.exp}" 86 | lhs.d20_num += rhs.d20_num 87 | lhs.d20_state = rhs.d20_state if not lhs.d20_state else lhs.d20_state 88 | return lhs 89 | 90 | 91 | @roll_connector("/") 92 | class REModDivide(RollExpConnector): 93 | """ 94 | 表示除法 95 | """ 96 | def connect(self, lhs: RollResult, rhs: RollResult) -> RollResult: 97 | lhs.val_list = [int(sum(lhs.val_list) / sum(rhs.val_list))] 98 | lhs.info = f"{lhs.info}/{rhs.info}" 99 | lhs.type = None 100 | lhs.exp = f"{lhs.exp}/{rhs.exp}" 101 | lhs.d20_num += rhs.d20_num 102 | lhs.d20_state = rhs.d20_state if not lhs.d20_state else lhs.d20_state 103 | return lhs 104 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/roll/modifier.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import operator 3 | from typing import Dict, Type, Union, Iterable 4 | 5 | 6 | from .roll_config import * 7 | from .roll_utils import RollDiceError, roll_a_dice 8 | from .result import RollResult 9 | 10 | 11 | class RollExpModifier(metaclass=abc.ABCMeta): 12 | """ 13 | 用于表示掷骰表达式修饰符 14 | """ 15 | @abc.abstractmethod 16 | def __init__(self, args: str): 17 | # 确保init签名一致 18 | pass 19 | 20 | @abc.abstractmethod 21 | def modify(self, roll_res: RollResult) -> RollResult: 22 | """ 23 | 修改并返回掷骰数据, 返回的变量应该基于初始变量 24 | """ 25 | raise NotImplementedError() 26 | 27 | 28 | ROLL_MODIFIERS_DICT: Dict[str, Type[RollExpModifier]] = {} 29 | 30 | 31 | def roll_modifier(regexp: Union[str, Iterable[str]]): 32 | """ 33 | 类修饰器, 将自定义掷骰表达式修饰符注册到列表中 34 | Args: 35 | regexp: 一个或多个正则表达式, 用来唯一地匹配该修饰符, 若有多个符合则较早定义的优先 36 | """ 37 | def inner(cls: Type[RollExpModifier]): 38 | """ 39 | Args: 40 | cls: 修饰的类必须继承自RollExpModifier 41 | Returns: 42 | cls: 返回修饰后的cls 43 | """ 44 | assert issubclass(cls, RollExpModifier) 45 | if type(regexp) is str: 46 | symbols = [regexp] 47 | else: 48 | symbols = regexp 49 | for s in symbols: 50 | assert " " not in s and s.isupper() 51 | assert s not in ROLL_MODIFIERS_DICT.keys() 52 | ROLL_MODIFIERS_DICT[s] = cls 53 | return cls 54 | return inner 55 | 56 | 57 | # 注意定义的顺序也即是执行优先级, 越早定义则优先级越高 58 | # 同一种修饰符的匹配优先级为从左到右 59 | 60 | 61 | @roll_modifier("(R|X|XO)(<|>|=)?[1-9][0-9]*") 62 | class REModReroll(RollExpModifier): 63 | """ 64 | 表示某一个骰子满足条件则重骰, 爆炸, 或者爆炸一次 65 | """ 66 | def __init__(self, args: str): 67 | super().__init__(args) 68 | # 模式 69 | if args[1] == "O": 70 | mod, args = args[:2], args[2:] 71 | else: 72 | mod, args = args[0], args[1:] 73 | 74 | # 判定条件 75 | if args[0] in ("<", ">", "="): 76 | comp, rhs = args[0], args[1:] 77 | else: 78 | comp, rhs = "=", args 79 | 80 | if comp == ">": 81 | self.op = operator.gt 82 | elif comp == "<": 83 | self.op = operator.lt 84 | elif comp == "=": 85 | self.op = operator.eq 86 | 87 | self.mod: str = mod 88 | self.comp: str = comp 89 | self.rhs: int = int(rhs) 90 | if self.rhs < DICE_CONSTANT_MIN or self.rhs > DICE_CONSTANT_MAX: 91 | raise RollDiceError(f"常量大小必须在{DICE_CONSTANT_MIN}至{DICE_CONSTANT_MAX}之间") 92 | 93 | def modify(self, roll_res: RollResult) -> RollResult: 94 | """ 95 | 输入的roll_res必须由形如XDY的表达式产生, 如果是常量表达式或复合表达式将会抛出一个异常 96 | """ 97 | if roll_res.type is None: # 只处理基础表达式 98 | raise RollDiceError(f"重骰对象只能为形如XDY的表达式, 如2D20R1, 此处为{roll_res.exp}") 99 | 100 | # 如果一个条件都不满足直接原样返回 101 | if all((not self.op(val, self.rhs) for val in roll_res.val_list)): 102 | roll_res.exp += self.mod + self.comp + str(self.rhs) 103 | return roll_res 104 | # 否则重新构筑 105 | new_val_list = [] 106 | # 因为只有基础xdy表达式有+, 所以可以直接用+或/分割, 这样可以使嵌套Reroll和Explode显示正确 107 | new_info_list = [] 108 | new_connector_list = [] 109 | left = 0 110 | prev_info = roll_res.get_info() 111 | for right in range(len(prev_info)): 112 | if prev_info[right] in ["+", "|"]: 113 | new_info_list.append(prev_info[left:right]) 114 | new_connector_list.append(prev_info[right]) 115 | left = right + 1 116 | new_info_list.append(prev_info[left:]) 117 | 118 | if len(new_info_list) != len(new_connector_list) + 1 or len(new_info_list) != len(roll_res.val_list): 119 | error_info = f"{len(new_info_list)} {len(new_connector_list)} {len(roll_res.val_list)}" 120 | raise RollDiceError(f"[REModReroll] 解析表达式出现错误! Error Code: 101\nInfo: {error_info}") 121 | for index, val in enumerate(roll_res.val_list): 122 | if self.op(val, self.rhs): 123 | if self.mod == "R": 124 | new_val_list.append(roll_a_dice(roll_res.type)) 125 | new_info_list[index] = f"[{new_info_list[index]}->{new_val_list[-1]}]" 126 | elif self.mod == "XO": 127 | new_val_list.append(val) 128 | new_val_list.append(roll_a_dice(roll_res.type)) 129 | new_info_list[index] += f"|{new_val_list[-1]}" 130 | else: # "x" 131 | new_val_list.append(val) 132 | repeat_time = 0 133 | while self.op(new_val_list[-1], self.rhs) and repeat_time <= EXPLODE_LIMIT: 134 | repeat_time += 1 135 | add_dice = roll_a_dice(roll_res.type) 136 | new_val_list.append(add_dice) 137 | new_info_list[index] += f"|{add_dice}" 138 | else: 139 | new_val_list.append(val) 140 | new_info_list[index] = f"{new_val_list[-1]}" 141 | new_info = new_info_list[0] 142 | for i in range(len(new_connector_list)): 143 | new_info += new_connector_list[i] + new_info_list[i+1] 144 | roll_res.val_list = new_val_list 145 | roll_res.info = new_info 146 | roll_res.exp += self.mod + self.comp + str(self.rhs) 147 | roll_res.d20_num = 2 # 使用了这个修饰器以后无法判断d20数量, 设成2以后之后就不会用到d20_state了 148 | roll_res.d20_state = 0 149 | return roll_res 150 | 151 | 152 | @roll_modifier("KH?[1-9][0-9]?") 153 | class REModMax(RollExpModifier): 154 | """ 155 | 表示取表达式中x个最大值 156 | """ 157 | def __init__(self, args: str): 158 | super().__init__(args) 159 | if args[1] == "H": 160 | val_str = args[2:] 161 | else: 162 | val_str = args[1:] 163 | self.num = int(val_str) 164 | 165 | def modify(self, roll_res: RollResult) -> RollResult: 166 | """ 167 | 注意val的顺序会按从低到高的顺序重新排序, 但info里的内容不会 168 | """ 169 | if self.num == 1: 170 | new_info = "max" 171 | else: 172 | new_info = f"max{self.num}" 173 | 174 | new_info += "{" + str(roll_res.val_list)[1:-1] + "}" 175 | # 如果按下面这种写法就可以在嵌套时显示中间结果, 但是会影响到大成功或大失败的判断, 除非增加新的字段 176 | # if roll_res.type: 177 | # new_info += "{" + str(roll_res.val_list)[1:-1] + "}" 178 | # else: # 嵌套 179 | # new_info += "{" + roll_res.info + "}" 180 | # roll_res.type = None 181 | 182 | roll_res.val_list = sorted(roll_res.val_list)[-self.num:] 183 | roll_res.info = new_info 184 | roll_res.exp = f"{roll_res.exp}K{self.num}" 185 | if roll_res.type == 20: 186 | roll_res.d20_num = len(roll_res.val_list) 187 | if roll_res.d20_num == 1: 188 | roll_res.d20_state = roll_res.val_list[0] 189 | 190 | return roll_res 191 | 192 | 193 | @roll_modifier("KL[1-9][0-9]?") 194 | class REModMin(RollExpModifier): 195 | """ 196 | 表示取表达式中x个最小值 197 | """ 198 | 199 | def __init__(self, args: str): 200 | super().__init__(args) 201 | self.num = int(args[2:]) 202 | 203 | def modify(self, roll_res: RollResult) -> RollResult: 204 | """ 205 | 注意val的顺序会按从低到高的顺序重新排序, 但info里的内容不会 206 | """ 207 | if self.num == 1: 208 | new_info = "min" 209 | else: 210 | new_info = f"min{self.num}" 211 | new_info += "{" + str(roll_res.val_list)[1:-1] + "}" 212 | # if roll_res.type: 213 | # new_info += "{" + str(roll_res.val_list)[1:-1] + "}" 214 | # else: 215 | # new_info += "{" + roll_res.info + "}" 216 | # roll_res.type = None 217 | 218 | roll_res.val_list = sorted(roll_res.val_list)[:self.num] 219 | roll_res.info = new_info 220 | roll_res.exp = f"{roll_res.exp}KL{self.num}" 221 | if roll_res.type == 20: 222 | roll_res.d20_num = len(roll_res.val_list) 223 | if roll_res.d20_num == 1: 224 | roll_res.d20_state = roll_res.val_list[0] 225 | return roll_res 226 | 227 | 228 | @roll_modifier("CS(<|>|=)?[1-9][0-9]*") 229 | class REModCountSuccess(RollExpModifier): 230 | """ 231 | 表示计算当前结果中符合条件的骰值的数量 232 | """ 233 | 234 | def __init__(self, args: str): 235 | super().__init__(args) 236 | 237 | args = args[2:] 238 | # 判定条件 239 | if args[0] in ("<", ">", "="): 240 | comp, rhs = args[0], args[1:] 241 | else: 242 | comp, rhs = "=", args 243 | 244 | if comp == ">": 245 | self.op = operator.gt 246 | elif comp == "<": 247 | self.op = operator.lt 248 | elif comp == "=": 249 | self.op = operator.eq 250 | 251 | self.comp: str = comp 252 | self.rhs: int = int(rhs) 253 | if self.rhs < DICE_CONSTANT_MIN or self.rhs > DICE_CONSTANT_MAX: 254 | raise RollDiceError(f"常量大小必须在{DICE_CONSTANT_MIN}至{DICE_CONSTANT_MAX}之间") 255 | 256 | def modify(self, roll_res: RollResult) -> RollResult: 257 | result = sum([self.op(val, self.rhs) for val in roll_res.val_list]) 258 | 259 | roll_res.info = f"[count{self.comp}{self.rhs}" + "{" + str(roll_res.val_list)[1:-1] + "}" + f"={result}]" 260 | roll_res.val_list = [result] 261 | roll_res.exp = f"{roll_res.exp}CS{self.comp}{self.rhs}" 262 | roll_res.d20_num = 2 # 使用了这个修饰器以后无法判断d20数量, 设成2以后之后就不会用到d20_state了 263 | roll_res.d20_state = 0 264 | return roll_res 265 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/roll/result.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from .roll_utils import remove_redundant_parentheses 4 | 5 | 6 | class RollResult: 7 | """ 8 | 记录掷骰过程中的一些数据, 可以被视为一个结构体 9 | """ 10 | def __init__(self): 11 | self.val_list: List[int] = [] # 结果值列表 12 | self.info: str = "" # 代表结果的字符串 13 | self.type: int = Optional[None] # 表示骰子的面数, 仅对基础的XDY类型表达式产生的结果有效, 如果是非基础表达式, 则type为None 14 | self.exp: str = "" # 代表表达式的字符串 15 | self.d20_num: int = 0 # 表示这个结果对应的表达式包含多少个D20, 注意3D20K1只算1个D20, 而D20+D20算两个D20 16 | self.d20_state: int = 0 # 若只包含一颗d20, 表示这颗d20的骰值, 若d20_num != 1, 该属性无效 17 | 18 | def get_result(self) -> str: 19 | """ 20 | 获得形如 (1+1)*2=4的字符串, 不包括掷骰表达式 21 | """ 22 | inter_info = self.get_info() 23 | final_res = str(self.get_val()) 24 | if inter_info == final_res: 25 | return final_res 26 | else: 27 | return f"{inter_info}={final_res}" 28 | 29 | def get_info(self) -> str: 30 | """ 31 | 获得形如 (1+1)*2 的中间变量 32 | """ 33 | final_info = self.info if self.info[0] != "+" else self.info[1:] 34 | final_info = remove_redundant_parentheses(final_info) 35 | return final_info 36 | 37 | def get_exp(self) -> str: 38 | """ 39 | 获得形如D20+1的掷骰表达式 40 | """ 41 | final_exp = self.exp if self.exp[0] != "+" else self.exp[1:] 42 | final_exp = remove_redundant_parentheses(final_exp, readable=False) 43 | return final_exp 44 | 45 | def get_val(self) -> int: 46 | """ 47 | 获得掷骰结果数值 48 | """ 49 | return sum(self.val_list) 50 | 51 | def get_complete_result(self) -> str: 52 | """ 53 | 获得形如 2D20*2=(1+1)*2=4的字符串, 不包括掷骰表达式 54 | """ 55 | exp = self.get_exp() 56 | info = self.get_info() 57 | val = str(self.get_val()) 58 | res = exp 59 | if res != info: 60 | res += f"={info}" 61 | if res != val and info != val: 62 | res += f"={val}" 63 | return res 64 | 65 | def get_exp_val(self) -> str: 66 | exp = self.get_exp() 67 | val = str(self.get_val()) 68 | 69 | if exp != val: 70 | return f"{exp}={val}" 71 | else: 72 | return val 73 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/roll/roll_config.py: -------------------------------------------------------------------------------- 1 | DICE_TYPE_DEFAULT = 20 # 默认骰面大小 2 | DICE_NUM_MAX = 100 # 最大骰子数量 3 | DICE_TYPE_MAX = 1000 # 最大骰子面数 4 | DICE_CONSTANT_MIN = -1000 # 常量大小下限 5 | DICE_CONSTANT_MAX = 1000 # 常量大小上限 6 | 7 | PARSE_RECURSION_DEPTH_MAX = 100 # 解析表达式时最大递归深度 8 | EXPLODE_LIMIT = 10 # 爆炸修饰符执行次数上限 9 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/roll/roll_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | 掷骰工具 3 | """ 4 | 5 | from random import randint 6 | from typing import List, Tuple 7 | 8 | 9 | class RollDiceError(Exception): 10 | """ 11 | 因为掷骰产生的异常, 说明操作失败的原因, 应当在上一级捕获 12 | """ 13 | def __init__(self, info: str): 14 | self.info = info 15 | 16 | def __str__(self): 17 | return f"[Roll] [Error] {self.info}" 18 | 19 | 20 | def roll_a_dice(dice_type: int) -> int: 21 | """ 22 | 返回一颗dice_type面骰的结果 23 | """ 24 | return randint(1, dice_type) 25 | 26 | 27 | def match_outer_parentheses(input_str: str) -> int: 28 | """ 29 | 若输入字符串的第一个字符是(, 返回对应的)的索引. 若不存在对应的), 抛出一个ValueError. 若首字母不是(, 返回-1 30 | """ 31 | if not input_str or input_str[0] != "(": 32 | return -1 33 | level = 0 34 | for index, char in enumerate(input_str): 35 | if char == "(": 36 | level += 1 37 | elif char == ")": 38 | level -= 1 39 | if level == 0: 40 | return index 41 | raise ValueError("Input's parentheses is incomplete!") 42 | 43 | 44 | def remove_redundant_parentheses(input_str: str, readable: bool = True) -> str: 45 | """ 46 | 递归地去掉字符串中一些冗余的括号, 字符串必须不包含空格, 即连接符紧跟着括号 47 | readable为True则会尽可能少的清除括号, readable为False则会在保留数学正确性的情况下清除尽可能多的括号 48 | """ 49 | priority_dict = {"+": 1, "-": 2, "*": 3, "/": 4} 50 | max_priority = 5 51 | 52 | def remove_par(par_str: str, outer_priority_lhs: int, outer_priority_rhs: int) -> str: 53 | """ 54 | 尝试去掉字符串中冗余的括号 55 | Args: 56 | par_str: 字符串必须以(开头并以)结尾 57 | outer_priority_lhs: 外层左侧连接符的运算优先级 58 | outer_priority_rhs: 外层右侧连接符的运算优先级 59 | """ 60 | try: 61 | assert par_str and par_str[0] == "(" and par_str[-1] == ")" 62 | except AssertionError: 63 | raise RollDiceError(f"去除冗余括号算法错误! 信息: {par_str} {outer_priority_lhs} {outer_priority_rhs}") 64 | # 找到内部所有的括号, 不包括最外层的 65 | inner_par_info_list: List[Tuple[int, int]] = [] # 每一个括号表达式的左索引和右索引, 按左索引从小到大排列, 左右不重叠 66 | par_index_lhs = 1 67 | while par_index_lhs < len(par_str)-1: # 从左往右扫描 68 | if par_str[par_index_lhs] == "(": 69 | par_index_rhs = par_index_lhs + match_outer_parentheses(par_str[par_index_lhs:]) 70 | inner_par_info_list.append((par_index_lhs, par_index_rhs)) 71 | par_index_lhs = par_index_rhs + 1 72 | else: 73 | par_index_lhs += 1 74 | # 找到内部所有的运算符, 不包括内部括号内的 75 | inner_operators: List[Tuple[int, int]] = [] # 每一个运算符的索引和运算优先级, 按索引从小到大排列 76 | for operator, priority in priority_dict.items(): 77 | try: 78 | left = 1 79 | while left < len(par_str)-1: 80 | index = par_str.index(operator, left) 81 | is_valid = True 82 | for par_info in inner_par_info_list: 83 | if par_info[0] < index < par_info[1]: 84 | is_valid = False 85 | left = par_info[1]+1 86 | break 87 | if is_valid: 88 | inner_operators.append((index, priority)) 89 | left = index + 1 90 | except ValueError: 91 | pass 92 | inner_operators = sorted(inner_operators, key=lambda x: x[0]) 93 | # print(par_str, inner_par_info_list, inner_operators, outer_priority_rhs, outer_priority_lhs) 94 | # 递归剔除内部括号 95 | if len(inner_par_info_list) != 0: 96 | if len(inner_operators) == 0: # 内部没有运算符, 直接递归剔除[1:-1] 97 | assert len(inner_par_info_list) == 1, str(inner_par_info_list) 98 | output_str = f"({remove_par(par_str[1:-1], outer_priority_lhs, outer_priority_rhs)})" 99 | else: # 内部有运算符, 尝试剔除内部的括号 100 | output_list = [] 101 | priority_lhs, priority_rhs = outer_priority_lhs, inner_operators[0][1] # 当前处理的括号的左侧优先级和右侧优先级 102 | operator_index = 0 103 | left, right = 0, inner_operators[0][0] # 左右侧运算符的索引 104 | for par_info in inner_par_info_list: 105 | while right < par_info[1]: # 找到最右侧的运算符 106 | if operator_index < len(inner_operators): 107 | right = inner_operators[operator_index][0] 108 | priority_lhs, priority_rhs = priority_rhs, inner_operators[operator_index][1] 109 | operator_index += 1 110 | else: 111 | right = par_info[1]+1 112 | priority_lhs, priority_rhs = priority_rhs, outer_priority_rhs 113 | output_list.append(par_str[left:par_info[0]]) 114 | if readable: # 为了可读性, 不需要去掉所有没有数学意义的括号, 所以把记录优先级相关的去掉了而是直接用最高优先级跳过后面的检查 115 | output_list.append(remove_par(par_str[par_info[0]:par_info[1]+1], max_priority, max_priority)) 116 | else: 117 | output_list.append(remove_par(par_str[par_info[0]:par_info[1] + 1], priority_lhs, priority_rhs)) 118 | left = right 119 | 120 | output_list.append(par_str[left:]) 121 | # print("output_list", output_list) 122 | output_str = "".join(output_list) 123 | else: 124 | output_str = par_str 125 | 126 | # 判断自己最外部的括号能不能被剔除 127 | if len(inner_operators) == 0: # 内部没有运算符, 则可以去掉括号 128 | can_remove_outer = True 129 | else: 130 | can_remove_outer = (outer_priority_lhs <= inner_operators[0][1] 131 | and outer_priority_rhs <= inner_operators[-1][1]) 132 | # print(par_str, output_str, can_remove_outer) 133 | if can_remove_outer: 134 | return output_str[1:-1] 135 | else: 136 | return output_str 137 | 138 | return remove_par(f"({input_str})", -1, -1) 139 | -------------------------------------------------------------------------------- /src/plugins/DicePP/module/roll/unit_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from typing import Callable 3 | 4 | import roll_config 5 | from module.roll.expression import parse_roll_exp, exec_roll_exp, RollExpression, preprocess_roll_exp 6 | from module.roll.roll_utils import match_outer_parentheses, remove_redundant_parentheses, RollDiceError 7 | from module.roll.result import RollResult 8 | 9 | 10 | class MyTestCase(unittest.TestCase): 11 | def test_utils(self): 12 | self.assertEqual(match_outer_parentheses("()"), 1) 13 | self.assertEqual(match_outer_parentheses("(ABC)"), 4) 14 | self.assertEqual(match_outer_parentheses("(A()A)"), 5) 15 | self.assertEqual(match_outer_parentheses("(AA)))"), 3) 16 | self.assertEqual(match_outer_parentheses("()ABC"), 1) 17 | self.assertEqual(match_outer_parentheses("(1+2)+1"), 4) 18 | self.assertEqual(match_outer_parentheses("ABC"), -1) 19 | self.assertEqual(match_outer_parentheses(""), -1) 20 | self.assertEqual(match_outer_parentheses("ABC()"), -1) 21 | self.assertRaises(ValueError, match_outer_parentheses, "(((") 22 | self.assertRaises(ValueError, match_outer_parentheses, "(A(A(A))") 23 | 24 | self.assertEqual("", remove_redundant_parentheses("()")) 25 | self.assertEqual("ABC", remove_redundant_parentheses("(ABC)")) 26 | self.assertEqual("ABC", remove_redundant_parentheses("((ABC))")) 27 | self.assertEqual("1+2", remove_redundant_parentheses("(1)+(2)")) 28 | self.assertEqual("1+2", remove_redundant_parentheses("(1+2)")) 29 | self.assertEqual("(1+2)+2", remove_redundant_parentheses("(1+2)+2")) 30 | self.assertEqual("(1+2)*2", remove_redundant_parentheses("(1+2)*2")) 31 | self.assertEqual("(A+B)*C", remove_redundant_parentheses("(A+B)*C")) 32 | self.assertEqual("(A*B)+C", remove_redundant_parentheses("(A*B)+C")) 33 | self.assertEqual("C*(A+B)", remove_redundant_parentheses("C*(A+B)")) 34 | self.assertEqual("C+(A*B)", remove_redundant_parentheses("C+(A*B)")) 35 | self.assertEqual("A*((A*B)+C)", remove_redundant_parentheses("A*((A*B)+C)")) 36 | self.assertEqual("A+((A*B)+C)", remove_redundant_parentheses("A+((A*B)+C)")) 37 | self.assertEqual("A+((A*B)+C)", remove_redundant_parentheses("A+((A*B)+C)")) 38 | self.assertEqual("A+(A+B)+C", remove_redundant_parentheses("A+(A+B)+C")) 39 | self.assertEqual("A+(A*B)+C", remove_redundant_parentheses("A+(A*B)+C")) 40 | self.assertEqual("A+(A+B)*C", remove_redundant_parentheses("A+(A+B)*C")) 41 | self.assertEqual("A*(A+B)+C", remove_redundant_parentheses("A*(A+B)+C")) 42 | self.assertEqual("max{max2{+(5+14+5+12)}}", remove_redundant_parentheses("max{max2{+(5+14+5+12)}}")) 43 | 44 | def __show_exec_res(self, exp_str: str, checker: Callable[[str], bool] = None): 45 | for _ in range(100): 46 | exec_roll_exp(exp_str) 47 | res = exec_roll_exp(exp_str) 48 | self.assertIsNotNone(res) 49 | output = f"Values: {res.val_list} \tInfo: {res.get_info()} \tType: {res.type} \tExpression: {res.get_exp()}" 50 | output += f"\nFinal Output: \033[0;33m{res.get_complete_result()}" 51 | output = f"Origin Exp: \033[0;32m{exp_str} \033[0m\t{output}" 52 | 53 | if checker: 54 | self.assertTrue(checker(res.get_complete_result()), output) 55 | else: 56 | print("\t\t--- Check Result ---") 57 | print(output) 58 | 59 | def __show_exception(self, exp_str: str): 60 | self.assertRaises(RollDiceError, exec_roll_exp, exp_str) 61 | try: 62 | exec_roll_exp(exp_str) 63 | except RollDiceError as e: 64 | print("\t\t--- Check Exception ---") 65 | print(f"Origin Exp: \033[0;35m{exp_str} \t\033[0;33m{e.info}") 66 | 67 | def test_basic_roll(self): 68 | # 基础情况 69 | self.__show_exec_res("1D20", checker=lambda s: s.split("=")[0] == "1D20" and 1 <= int(s.split("=")[1]) <= 20) 70 | self.__show_exec_res("3D20") 71 | self.__show_exec_res("D", checker=lambda s: s.split("=")[0] == "1D20" and 1 <= int(s.split("=")[1]) <= 20) 72 | self.__show_exec_res("1D", checker=lambda s: s.split("=")[0] == "1D20" and 1 <= int(s.split("=")[1]) <= 20) 73 | self.__show_exec_res("D4", checker=lambda s: s.split("=")[0] == "1D4" and 1 <= int(s.split("=")[1]) <= 4) 74 | self.__show_exec_res("1", checker=lambda s: "1" == s) 75 | self.__show_exec_res("+1D20", checker=lambda s: s.split("=")[0] == "1D20" and 1 <= int(s.split("=")[1]) <= 20) 76 | self.__show_exec_res("-1D20", checker=lambda s: s.split("=")[0] == "-1D20" and -20 <= int(s.split("=")[1]) <= -1) 77 | 78 | # 基础运算 79 | self.__show_exec_res("1D20+1") 80 | self.__show_exec_res("1D20-1") 81 | self.__show_exec_res("3D20*2") 82 | self.__show_exec_res("3D20/2") 83 | self.__show_exec_res("1+1D20") 84 | self.__show_exec_res("1-1D20") 85 | self.__show_exec_res("2*3D20") 86 | self.__show_exec_res("2/3D20") 87 | 88 | # connector 89 | self.__show_exec_res("1-1-1", checker=lambda s: s.endswith("-1")) 90 | self.__show_exec_res("1+1-1", checker=lambda s: s.endswith("1")) 91 | self.__show_exec_res("1-1+1", checker=lambda s: s.endswith("1")) 92 | self.__show_exec_res("5/2+3/2", checker=lambda s: s.endswith("3")) 93 | self.__show_exec_res("1+2*2", checker=lambda s: s.endswith("5")) 94 | self.__show_exec_res("1*2+2", checker=lambda s: s.endswith("4")) 95 | self.__show_exec_res("1-1+1-1", checker=lambda s: s.endswith("0")) 96 | self.__show_exec_res("1+1-1+1") 97 | 98 | # 带空格和中文字符情况 (由于判断指令中表达式和掷骰原因的问题去掉了过滤空格的代码) 99 | self.__show_exec_res("1d20", checker=lambda s: s.split("=")[0] == "1D20" and 1 <= int(s.split("=")[1]) <= 20) 100 | self.__show_exec_res("d20+1") 101 | # self.__show_exec_res(" 1 D 2 0 ") 102 | # self.__show_exec_res("1D20 + 1") 103 | 104 | # 带修饰符情况 105 | self.__show_exec_res("2D20k1") 106 | self.__show_exec_res("1D20K2") 107 | self.__show_exec_res("4D20k2kl1") 108 | self.__show_exec_res("4D20r<10") 109 | self.__show_exec_res("4D20x<10") 110 | self.__show_exec_res("4D20xo<10") 111 | self.__show_exec_res("4D20r<10x>10") 112 | self.__show_exec_res("4D20x>10r<10") 113 | self.__show_exec_res("D20cs>5") 114 | self.__show_exec_res("10D20cs>10") 115 | self.__show_exec_res("5+10D20cs>10+5") 116 | self.__show_exec_res("10D20kl5cs>10") 117 | 118 | # 带括号 119 | self.__show_exec_res("(1+2)") 120 | self.__show_exec_res("(1+2)*2") 121 | self.__show_exec_res("(D20)*2") 122 | self.__show_exec_res("(1+D20)*2") 123 | self.__show_exec_res("((1+D20))*2") 124 | self.__show_exec_res("(D20)") 125 | self.__show_exec_res("(D20)*(D20)") 126 | 127 | # 优势与劣势 128 | self.__show_exec_res("D20优势") 129 | self.__show_exec_res("D20劣势+1") 130 | self.__show_exec_res("D20劣势+1+D优势") 131 | self.__show_exception("2D20优势") 132 | self.__show_exception("2D20优势+1") 133 | self.__show_exception("1+20优势") 134 | 135 | # 抗性与易伤 136 | self.__show_exec_res("D20+2抗性") 137 | self.__show_exec_res("5抗性") 138 | self.__show_exec_res("2D4+D20易伤") 139 | self.__show_exception("抗性") 140 | self.__show_exception("+抗性") 141 | 142 | # 非法输入 143 | self.__show_exception("") 144 | self.__show_exception("()") 145 | self.__show_exception("1D(20)") 146 | self.__show_exception("(1)D20") 147 | self.__show_exception("1(D)20") 148 | self.__show_exception("1+++1") 149 | self.__show_exception("1+1+") 150 | self.__show_exception("+") 151 | self.__show_exception("*") 152 | self.__show_exception("(D20") 153 | self.__show_exception("D20)") 154 | self.__show_exception("(D20)+(1") 155 | self.__show_exception("((D20)+1))))") 156 | self.__show_exception("(10D20+5)cs>10") 157 | 158 | # 边界条件 159 | self.__show_exception(f"1D{roll_config.DICE_TYPE_MAX + 1}") 160 | self.__show_exception(f"{roll_config.DICE_NUM_MAX + 1}D20") 161 | self.__show_exception(f"{roll_config.DICE_CONSTANT_MIN - 1}") 162 | self.__show_exception(f"{roll_config.DICE_CONSTANT_MAX + 1}") 163 | 164 | def test_d20_state(self): 165 | # 测试大成功或大失败是否可以生效 166 | def repeat_until_checked(exp_str: str) -> bool: 167 | exp: RollExpression = parse_roll_exp(preprocess_roll_exp(exp_str)) 168 | has_succ, has_fail = False, False 169 | for _ in range(2000): 170 | res: RollResult = exp.get_result() 171 | if res.d20_num == 1: 172 | if res.d20_state == 20: 173 | has_succ = True 174 | elif res.d20_state == 1: 175 | has_fail = True 176 | if has_succ and has_fail: 177 | return True 178 | return False 179 | 180 | self.assertTrue(repeat_until_checked("D20")) 181 | self.assertTrue(repeat_until_checked("2D20KL1")) 182 | self.assertTrue(repeat_until_checked("2D20K1")) 183 | self.assertTrue(repeat_until_checked("1D20K3")) 184 | self.assertTrue(repeat_until_checked("1D4+1D20+20")) 185 | 186 | self.assertTrue(not repeat_until_checked("2D20")) 187 | self.assertTrue(not repeat_until_checked("4D20K3")) 188 | self.assertTrue(not repeat_until_checked("D20+D20")) 189 | 190 | 191 | if __name__ == '__main__': 192 | unittest.main() 193 | -------------------------------------------------------------------------------- /src/plugins/DicePP/utils/__init__.py: -------------------------------------------------------------------------------- 1 | import utils.localdata 2 | import utils.time 3 | import utils.string 4 | import utils.data 5 | import utils.cq_code 6 | -------------------------------------------------------------------------------- /src/plugins/DicePP/utils/cq_code.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | from io import BytesIO 3 | from pathlib import Path 4 | from base64 import b64encode 5 | 6 | 7 | def get_cq_image(file: Union[str, bytes, BytesIO, Path]): 8 | if isinstance(file, BytesIO): 9 | file = file.getvalue() 10 | if isinstance(file, bytes): 11 | file = f"base64://{b64encode(file).decode()}" 12 | elif isinstance(file, Path): 13 | file = f"file:///{file.resolve()}" 14 | elif isinstance(file, str): 15 | file = f"file:///{file}" 16 | return f"[CQ:image,file={file}]" 17 | -------------------------------------------------------------------------------- /src/plugins/DicePP/utils/data.py: -------------------------------------------------------------------------------- 1 | def yield_deduplicate(items, key=None): 2 | seen = set() 3 | for item in items: 4 | val = item if key is None else key(item) 5 | if val not in seen: 6 | yield item 7 | seen.add(val) 8 | -------------------------------------------------------------------------------- /src/plugins/DicePP/utils/localdata.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict 2 | import os 3 | import json 4 | import openpyxl 5 | from openpyxl.comments import Comment 6 | 7 | 8 | def read_json(path: str) -> dict: 9 | """ 10 | 从path中读取一个json文件并返回对应的字典 11 | """ 12 | with open(path, "r", encoding='utf-8') as f: 13 | js = f.read() 14 | json_dict = json.loads(js) 15 | return json_dict 16 | 17 | 18 | def update_json(json_dict: dict, path: str) -> None: 19 | """ 20 | 将jsonFile保存到path路径中 21 | """ 22 | with open(path, "w", encoding='utf-8') as f: 23 | json.dump(json_dict, f, ensure_ascii=False) 24 | 25 | 26 | async def update_json_async(json_dict: dict, path: str) -> None: 27 | """ 28 | 异步地将jsonFile保存到path路径中 29 | """ 30 | with open(path, "w", encoding='utf-8') as f: 31 | json.dump(json_dict, f, ensure_ascii=False) 32 | 33 | 34 | def read_xlsx(path: str) -> openpyxl.Workbook: 35 | """ 36 | 读取xlsx, 记得之后手动关闭workbook 37 | """ 38 | wb = openpyxl.load_workbook(path) 39 | wb_title = path.rsplit("/", maxsplit=1)[-1] 40 | wb_title = wb_title.rsplit("\\", maxsplit=1)[-1] 41 | wb_title = wb_title.rsplit(".", maxsplit=1)[0] 42 | wb.properties.title = wb_title 43 | wb.properties.identifier = path 44 | return wb 45 | 46 | 47 | def update_xlsx(workbook: openpyxl.Workbook, path: str) -> None: 48 | workbook.save(path) 49 | 50 | 51 | def get_empty_col_based_workbook(keywords: List[str], keyword_comments: Dict[str, str]) -> openpyxl.Workbook: 52 | """获得一个模板工作簿""" 53 | wb = openpyxl.Workbook() 54 | for name in wb.sheetnames: 55 | del wb[name] 56 | ws_temp = wb.create_sheet("template") 57 | for i, text in enumerate(keywords): 58 | cell_field = ws_temp.cell(row=1, column=1 + i, value=text) 59 | cell_field.comment = Comment(keyword_comments[text], "DicePP") 60 | return wb 61 | 62 | 63 | def col_based_workbook_to_dict(wb: openpyxl.Workbook, keywords: List[str], error_info: List[str]) -> dict: 64 | """ 65 | 将已经读取的 column based 工作簿转换为dict, 要求第一行是关键字, 后续行是对应内容, 如果sheet中没有任何内容或关键字不完整则不会创建对应的字典 66 | Args: 67 | wb: 已经读取的xlsx 68 | keywords: 关键字列表, 如果为空将会使用表中已有的全部关键字 69 | error_info: 将检测到的错误加入到error_info中 70 | 71 | Returns: 72 | result: 字典, 第一级key为sheet名称, 第二级key为关键字, 之后是字符串列表 73 | 通过类似 result[sheet_name][key_name][row_index-2] 来访问对应的数据, 没有数据则返回空字符串 74 | """ 75 | result = {} 76 | for sheet_name in wb.sheetnames: 77 | key_index_dict: Dict[str, int] = {} 78 | ws = wb[sheet_name] 79 | # 获取当前关键字列表 80 | if not keywords: 81 | keywords_cur = [] 82 | for header_cell in ws[1]: 83 | keywords_cur.append(header_cell.value) 84 | else: 85 | keywords_cur = keywords 86 | # 获取关键字索引 87 | for header_cell in ws[1]: 88 | if header_cell.value in keywords_cur: 89 | key_index_dict[header_cell.value] = header_cell.column - 1 90 | # 检测关键字是否完整 91 | is_valid = True 92 | for keyword in keywords_cur: 93 | if keyword not in key_index_dict: 94 | error_info.append(f"不完整的表格{wb.properties.title}->{sheet_name}, 缺少{keyword}, 未加载该工作表") 95 | is_valid = False 96 | break 97 | if not is_valid: 98 | continue 99 | 100 | # 生成工作表字典 101 | result[sheet_name] = {} 102 | for keyword in keywords_cur: 103 | result[sheet_name][keyword] = [] 104 | is_valid = False 105 | # 逐行生成内容 106 | for row in ws.iter_rows(min_row=2): 107 | for keyword in keywords_cur: 108 | content = row[key_index_dict[keyword]].value 109 | content: str = str(content).strip() if content is not None else "" 110 | result[sheet_name][keyword].append(content) 111 | if not is_valid and content: 112 | is_valid = True 113 | if not is_valid: 114 | error_info.append(f"空工作表{wb.properties.title}->{sheet_name}, 未加载该工作表") 115 | del result[sheet_name] 116 | if len(result) == 0: 117 | error_info.append(f"表格中不含有任何可用工作表{wb.properties.title}") 118 | return result 119 | 120 | 121 | def format_worksheet(sheet, height: float = 30, min_width: float = 20, width_scale: float = 1): 122 | """ 123 | Args: 124 | sheet: 要修改的页面, 类型为openpyxl.worksheet.worksheet.Worksheet 125 | height: 每行高度 126 | min_width: 每列最小宽度 127 | width_scale: 列宽=列内最大字符数*width_scale 128 | """ 129 | from openpyxl.utils import get_column_letter 130 | 131 | for i in range(1, sheet.max_row + 1): 132 | sheet.row_dimensions[i].height = height 133 | for i in range(1, sheet.max_column + 1): 134 | cl = get_column_letter(i) 135 | width = max((len(cell.value) for cell in sheet[cl] if cell.value)) * width_scale 136 | width = max(width, min_width) 137 | sheet.column_dimensions[cl].width = width 138 | 139 | 140 | def create_parent_dir(path: str): 141 | """为path递归创建所有不存在的父文件夹""" 142 | path = os.path.abspath(path) 143 | assert path and path[0] != "." 144 | if "." not in path: # path是文件夹 145 | dir_name = path 146 | else: # path是文件 147 | dir_name = os.path.dirname(path) 148 | 149 | if not os.path.exists(os.path.dirname(dir_name)): 150 | create_parent_dir(os.path.dirname(dir_name)) 151 | 152 | if not os.path.exists(dir_name): 153 | os.mkdir(dir_name) 154 | -------------------------------------------------------------------------------- /src/plugins/DicePP/utils/logger.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import traceback 3 | import re 4 | from typing import List 5 | 6 | 7 | def dice_log(*args, **kwargs): 8 | """ 9 | 记录Log信息 10 | """ 11 | if kwargs: 12 | print("logger: ", *args, kwargs) 13 | else: 14 | print("logger: ", *args) 15 | 16 | 17 | def get_exception_info() -> List[str]: 18 | """返回当前简洁的堆栈信息, 越后面的字符串代表越深的堆栈, 最后一个字符串代表错误类型. 如果当前无错误堆栈, 输出空数组""" 19 | et, ev, tb = sys.exc_info() 20 | msg = traceback.format_exception(et, ev, tb) 21 | for i, m in enumerate(msg): 22 | msg[i] = re.sub(r'File ".*DicePP(.*)"', lambda match: str(match.groups()[-1])[1:], m).strip() 23 | msg[i] = re.sub(r", in.*\s{2,}", lambda match: str(match.group()).strip()+": ", msg[i]) 24 | return msg[1:] 25 | -------------------------------------------------------------------------------- /src/plugins/DicePP/utils/string.py: -------------------------------------------------------------------------------- 1 | from typing import List, Iterable 2 | 3 | 4 | def to_english_str(input_str: str) -> str: 5 | """ 6 | 将字符串中的中文符号转为英文 7 | """ 8 | if type(input_str) != str: 9 | raise ValueError(f'ChineseToEnglishSymbol: Input {input_str} must be str type') 10 | output_str = input_str 11 | output_str = output_str.replace('。', '.') 12 | output_str = output_str.replace(',', ',') 13 | output_str = output_str.replace('+', '+') 14 | output_str = output_str.replace('➕', '+') 15 | output_str = output_str.replace('-', '-') 16 | output_str = output_str.replace('➖', '-') 17 | output_str = output_str.replace('=', '=') 18 | output_str = output_str.replace('#', '#') 19 | output_str = output_str.replace(':', ':') 20 | output_str = output_str.replace(';', ';') 21 | output_str = output_str.replace('(', '(') 22 | output_str = output_str.replace(')', ')') 23 | return output_str 24 | 25 | 26 | def match_substring(substring: str, str_list: Iterable[str]) -> List[str]: 27 | """ 28 | 在一个字符串列表中找到所有包含输入字符串的字符串并返回 29 | Args: 30 | substring: 目标字符串 31 | str_list: 待匹配字符串列表 32 | 33 | Returns: 34 | res_list: 所有匹配成功的字符串 35 | """ 36 | return [s for s in str_list if s.find(substring) != -1] 37 | -------------------------------------------------------------------------------- /src/plugins/DicePP/utils/time.py: -------------------------------------------------------------------------------- 1 | import time 2 | import datetime 3 | 4 | china_tz = datetime.timezone(datetime.timedelta(hours=8), "北京时间") 5 | DATE_STR_FORMAT = "%Y_%m_%d_%H_%M_%S" 6 | DATE_STR_FORMAT_DAY = "%Y_%m_%d" 7 | 8 | 9 | def str_to_datetime(input_str: str) -> datetime: 10 | """ 11 | 将字符串表示的时间转换为datetime格式, 字符串格式由DATE_STR_FORMAT定义, 默认是%Y_%m_%d_%H_%M_%S 12 | """ 13 | result = datetime.datetime.strptime(input_str, DATE_STR_FORMAT) 14 | result = result.replace(tzinfo=china_tz) 15 | return result 16 | 17 | 18 | def datetime_to_str(input_datetime: datetime) -> str: 19 | """ 20 | 将datetime转换为字符串, 字符串格式由DATE_STR_FORMAT定义, 默认是%Y_%m_%d_%H_%M_%S 21 | """ 22 | return input_datetime.strftime(DATE_STR_FORMAT) 23 | 24 | 25 | def datetime_to_int(input_datetime: datetime) -> int: 26 | """ 27 | 将datetime转换为int, 即localtime, 时区默认为东八区, 单位为秒 28 | """ 29 | return int(time.mktime(input_datetime.timetuple())) 30 | 31 | 32 | def int_to_datetime(timestamp: int) -> datetime: 33 | """ 34 | 将int转换为datetime, 时区默认为东八区, 单位为秒 35 | """ 36 | return datetime.datetime.fromtimestamp(timestamp, tz=china_tz) 37 | 38 | 39 | def get_current_date_raw() -> datetime: 40 | """ 41 | 返回datetime格式的当前北京时间 42 | """ 43 | return datetime.datetime.now(china_tz) 44 | 45 | 46 | def get_current_date_str() -> str: 47 | """ 48 | 返回以字符串表示的当前北京时间 49 | """ 50 | return datetime_to_str(get_current_date_raw()) 51 | 52 | 53 | def get_current_date_int() -> int: 54 | """ 55 | 返回int格式的当前北京时间 56 | """ 57 | return datetime_to_int(get_current_date_raw()) 58 | 59 | 60 | def datetime_to_str_day(input_datetime: datetime) -> str: 61 | """ 62 | 将datetime转换为字符串, 字符串格式由DATE_STR_FORMAT_DAY定义, 默认是%Y_%m_%d 63 | """ 64 | return input_datetime.strftime(DATE_STR_FORMAT_DAY) 65 | 66 | 67 | def datetime_filter_day(input_datetime: datetime.datetime) -> datetime: 68 | """ 69 | 只保留datetime的date部分 70 | """ 71 | return input_datetime.replace(hour=0, minute=0, second=0, microsecond=0) 72 | --------------------------------------------------------------------------------