├── .gitignore ├── 00TT.TTF ├── DockerFile ├── README.md ├── benchmarker.py ├── bot.py ├── minesweeper.py ├── mirai ├── __init__.py ├── application.py ├── depend.py ├── entities │ ├── __init__.py │ ├── builtins.py │ ├── friend.py │ └── group.py ├── event │ ├── __init__.py │ ├── builtins.py │ ├── enums.py │ ├── external │ │ ├── __init__.py │ │ └── enums.py │ └── message │ │ ├── __init__.py │ │ ├── base.py │ │ ├── chain.py │ │ ├── components.py │ │ └── models.py ├── exceptions.py ├── face.py ├── image.py ├── logger.py ├── misc.py ├── network.py ├── protocol.py └── utilles │ ├── __init__.py │ └── dependencies.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | config.py 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | -------------------------------------------------------------------------------- /00TT.TTF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzdluo123/MineSweeper/f0ef81b9369e5332d3ef1ec370bd6ae1a6256a22/00TT.TTF -------------------------------------------------------------------------------- /DockerFile: -------------------------------------------------------------------------------- 1 | FROM python:3.7.7 2 | COPY requirements.txt /tmp/requirements.txt 3 | VOLUME [ "/data" ] 4 | WORKDIR /data 5 | 6 | RUN cd /tmp &&\ 7 | pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt 8 | CMD cd /data && python bot.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MineSweeper 2 | Mirai群内的扫雷小游戏 3 | 4 | 在任意群内或私聊,临时消息发送`扫雷`可查看菜单 5 | 6 | # 部署 7 | 8 | clone项目,新建config.py,填入以下内容 9 | ```python 10 | qq = qq号 11 | authKey = "你得key" 12 | mirai_api_http_locate = "地址:端口/" 13 | ``` 14 | 输入以下命令启动 15 | ``` 16 | docker build . --rm -t minesweeper 17 | docker run --rm -it -v 当前路径:/data minesweeper 18 | ``` 19 | 如需后台运行请使用 20 | ``` 21 | docker run --rm -it -d -v 当前路径:/data minesweeper 22 | ``` 23 | 24 | 25 | 还有一个没啥用的cython优化版,请切换到cython分支。比master分支稍快一些 26 | -------------------------------------------------------------------------------- /benchmarker.py: -------------------------------------------------------------------------------- 1 | import time 2 | from minesweeper import MineSweeper 3 | 4 | if __name__ == '__main__': 5 | start = time.time() 6 | for i in range(0, 300): 7 | mine = MineSweeper(25, 25, 25) 8 | mine.mine(5, 10) 9 | print(time.time() - start) 10 | -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | from mirai import Mirai, Plain, At, Group, Member, Image, Friend, FriendMessage, GroupMessage, TempMessage 2 | from mirai.event.message.models import MessageItemType 3 | from config import mirai_api_http_locate, authKey, qq 4 | from minesweeper import MineSweeper, GameState 5 | from typing import Dict 6 | from io import BytesIO 7 | from time import time, sleep 8 | from threading import Thread 9 | import signal 10 | 11 | app = Mirai(f"mirai://{mirai_api_http_locate}?authKey={authKey}&qq={qq}", websocket=True) 12 | running = True 13 | in_gaming_list: Dict[int, MineSweeper] = {} 14 | 15 | HELP = """ 16 | 欢迎游玩扫雷小游戏 17 | 输入 【m 开始】 即可开始游戏 18 | 输入 【m 中级或高级】 即可开始不同难度游戏 19 | 输入 【m 自定义 长 宽 雷数】 即可开始自定义游戏 20 | 使用 【m d 位置1 位置2】 来挖开多个方快 21 | 使用 【m t 位置1 位置2】 来标记多个方块 22 | 使用 【m show】 来重新查看游戏盘 23 | 使用 【m help】 来查看帮助 24 | 使用 【m exit】 退出游戏 25 | 项目地址 https://github.com/mzdluo123/MineSweeper 26 | """ 27 | 28 | 29 | def clean_thread(): 30 | while running: 31 | for k, v in in_gaming_list.items(): 32 | if time() - v.start_time > 15 * 60: 33 | del in_gaming_list[k] 34 | sleep(2) 35 | 36 | 37 | async def send_msg(target, msg: list, user, msg_type): 38 | if msg_type is MessageItemType.GroupMessage: 39 | msg.insert(0, At(user.id)) 40 | await app.sendGroupMessage(target, msg) 41 | return 42 | if msg_type is MessageItemType.FriendMessage: 43 | await app.sendFriendMessage(target, msg) 44 | return 45 | if msg_type is MessageItemType.TempMessage: 46 | await app.sendTempMessage(target, user, msg) 47 | 48 | 49 | async def send_panel(app: Mirai, group: Group, member: Member, msg_type): 50 | byte_io = BytesIO() 51 | in_gaming_list[member.id].draw_panel().save(byte_io, format="jpeg") 52 | byte_io.flush() 53 | await send_msg(group, [Image.fromIO(byte_io)], member, msg_type) 54 | byte_io.close() 55 | 56 | 57 | async def send_game_over(app: Mirai, group: Group, member: Member, msg_type): 58 | minesweeper = in_gaming_list[member.id] 59 | if minesweeper.state == GameState.WIN: 60 | await send_msg(group, [Plain( 61 | f"恭喜你赢了,再来一次吧!耗时{time() - minesweeper.start_time}秒 操作了{minesweeper.actions}次")], member, msg_type) 62 | if minesweeper.state == GameState.FAIL: 63 | await send_msg(group, [Plain( 64 | f"太可惜了,就差一点点,再来一次吧!耗时{time() - minesweeper.start_time}秒 操作了{minesweeper.actions}次")], member, msg_type) 65 | del in_gaming_list[member.id] 66 | 67 | 68 | @app.receiver("FriendMessage") 69 | async def friend_handel(app: Mirai, friend: Friend, message: FriendMessage): 70 | plain: Plain = message.messageChain.getFirstComponent(Plain) 71 | await msg_handel(friend, plain, friend, MessageItemType.FriendMessage) 72 | 73 | 74 | @app.receiver("TempMessage") 75 | async def tm_handel(app: Mirai, group: Group, member: Member, message: TempMessage): 76 | plain: Plain = message.messageChain.getFirstComponent(Plain) 77 | await msg_handel(group, plain, member, MessageItemType.TempMessage) 78 | 79 | 80 | @app.receiver("GroupMessage") 81 | async def gm_handel(app: Mirai, group: Group, member: Member, message: GroupMessage): 82 | plain: Plain = message.messageChain.getFirstComponent(Plain) 83 | await msg_handel(group, plain, member, MessageItemType.GroupMessage) 84 | 85 | 86 | async def new_game(source, user, msg_type, row: int, column: int, mines: int): 87 | if user.id in in_gaming_list: 88 | await send_msg(source, [Plain("你已经在游戏中了")], user, msg_type) 89 | return 90 | in_gaming_list[user.id] = MineSweeper(row, column, mines) 91 | await send_panel(app, source, user, msg_type) 92 | 93 | 94 | async def msg_handel(source, plain, user, msg_type): 95 | if plain is None: 96 | return 97 | if plain.text == "扫雷": 98 | await send_msg(source, [Plain(HELP)], user, msg_type) 99 | if len(plain.text) > 2 and plain.text[:1] == "m": 100 | commands = plain.text.split(" ") 101 | if commands[1] == "开始": 102 | await new_game(source, user, msg_type, 10, 10, 10) 103 | if commands[1] == "中级": 104 | await new_game(source, user, msg_type, 16, 16, 40) 105 | if commands[1] == "高级": 106 | await new_game(source, user, msg_type, 20, 20, 90) 107 | 108 | if commands[1] == "自定义" and len(commands) == 5: 109 | try: 110 | await new_game(source, user, msg_type, int(commands[2]), int(commands[3]), int(commands[4])) 111 | except ValueError as e: 112 | await send_msg(source, [Plain(f"错误 {e}")], user, msg_type) 113 | if commands[1] == "help": 114 | await send_msg(source, [Plain(HELP)], user, msg_type) 115 | 116 | # 以下命令只有在游戏中才可以使用 117 | if user.id not in in_gaming_list: 118 | return 119 | if commands[1] == "show": 120 | await send_panel(app, source, user, msg_type) 121 | 122 | if commands[1] == "exit": 123 | if user.id in in_gaming_list: 124 | await send_msg(source, [Plain("退出成功")], user, msg_type) 125 | del in_gaming_list[user.id] 126 | else: 127 | await send_msg(source, [Plain("请输入 m 开始 开始游戏")], user, msg_type) 128 | # 命令长度大于3才可以使用 129 | if len(commands) < 3: 130 | return 131 | if commands[1] == "d": 132 | try: 133 | for i in range(2, len(commands)): 134 | location = MineSweeper.parse_input(commands[i]) 135 | in_gaming_list[user.id].mine(location[0], location[1]) 136 | if in_gaming_list[user.id].state != GameState.GAMING: 137 | break 138 | except ValueError as e: 139 | await send_msg(source, [Plain(f"错误: {e}")], user, msg_type) 140 | await send_panel(app, source, user, msg_type) 141 | if in_gaming_list[user.id].state != GameState.GAMING: 142 | await send_game_over(app, source, user, msg_type) 143 | 144 | if commands[1] == "t": 145 | try: 146 | for i in range(2, len(commands)): 147 | location = MineSweeper.parse_input(commands[i]) 148 | in_gaming_list[user.id].tag(location[0], location[1]) 149 | await send_panel(app, source, user, msg_type) 150 | except ValueError as e: 151 | await send_msg(source, [Plain(f"错误: {e}")], user, msg_type) 152 | 153 | 154 | def my_exit(): 155 | global running 156 | running = False 157 | exit() 158 | 159 | 160 | if __name__ == "__main__": 161 | Thread(target=clean_thread).start() 162 | signal.signal(signal.SIGINT, my_exit) 163 | signal.signal(signal.SIGTERM, my_exit) 164 | app.run() 165 | -------------------------------------------------------------------------------- /minesweeper.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageDraw, ImageColor, ImageFont 2 | from enum import Enum 3 | import random 4 | from typing import Tuple 5 | from time import time 6 | 7 | COLUMN_NAME = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 8 | 9 | 10 | class GameState(Enum): 11 | PREPARE = 1 12 | GAMING = 2 13 | WIN = 3 14 | FAIL = 4 15 | 16 | 17 | class Cell: 18 | def __init__(self, is_mine: bool, row: int = 0, column: int = 0, is_mined: bool = False, is_marked: bool = False): 19 | self.is_mine = is_mine 20 | self.is_mined = is_mined 21 | self.is_marked = is_marked 22 | self.row = row 23 | self.column = column 24 | self.is_checked = False 25 | 26 | def __str__(self): 27 | return f"[Cell] is_mine:{self.is_mine} is_marked:{self.is_marked} is_mined:{self.is_mined}" 28 | 29 | 30 | class MineSweeper: 31 | def __init__(self, row: int, column: int, mines: int): 32 | if row > 26 or column > 26: 33 | raise ValueError("暂不支持这么大的游戏盘") 34 | if mines >= row * column or mines == 0: 35 | raise ValueError("非法操作") 36 | if mines < column - 1 or mines < row - 1: 37 | raise ValueError("就不能来点难的吗") 38 | self.row = row 39 | self.column = column 40 | self.mines = mines 41 | self.start_time = time() 42 | self.actions = 0 43 | self.font = ImageFont.truetype("00TT.TTF", 40) 44 | self.panel = [[Cell(False, row=r, column=c) for c in range(column)] for r in range(row)] 45 | self.state = GameState.PREPARE 46 | 47 | def __str__(self): 48 | return f"[MineSweeper] {self.mines} in {self.row}*{self.column}" 49 | 50 | 51 | def draw_panel(self) -> Image.Image: 52 | start = time() 53 | img = Image.new("RGB", (80 * self.column, 80 * self.row), (255, 255, 255)) 54 | self.__draw_split_line(img) 55 | self.__draw_cell_cover(img) 56 | self.__draw_cell(img) 57 | print(f"draw spend {time()-start}ms at {str(self)}") 58 | return img 59 | 60 | def __draw_split_line(self, img: Image.Image): 61 | draw = ImageDraw.Draw(img) 62 | for i in range(0, self.row): 63 | draw.line((0, i * 80, img.size[0], i * 80), fill=ImageColor.getrgb("black")) 64 | for i in range(0, self.column): 65 | draw.line((i * 80, 0, i * 80, img.size[1]), fill=ImageColor.getrgb("black")) 66 | 67 | def __draw_cell_cover(self, img: Image.Image): 68 | draw = ImageDraw.Draw(img) 69 | for i in range(0, self.row): 70 | for j in range(0, self.column): 71 | cell = self.panel[i][j] 72 | if self.state == GameState.FAIL and cell.is_mine: 73 | draw.rectangle((j * 80 + 1, i * 80 + 1, (j + 1) * 80 - 1, (i + 1) * 80 - 1), 74 | fill=ImageColor.getrgb("red")) 75 | continue 76 | if cell.is_marked: 77 | draw.rectangle((j * 80 + 1, i * 80 + 1, (j + 1) * 80 - 1, (i + 1) * 80 - 1), 78 | fill=ImageColor.getrgb("blue")) 79 | continue 80 | if not cell.is_mined: 81 | draw.rectangle((j * 80 + 1, i * 80 + 1, (j + 1) * 80 - 1, (i + 1) * 80 - 1), 82 | fill=ImageColor.getrgb("gray")) 83 | 84 | def __draw_cell(self, img: Image.Image): 85 | draw = ImageDraw.Draw(img) 86 | for i in range(0, self.row): 87 | for j in range(0, self.column): 88 | cell = self.panel[i][j] 89 | if not cell.is_mined: 90 | font_size = self.font.getsize("AA") 91 | index = f"{COLUMN_NAME[i]}{COLUMN_NAME[j]}" 92 | center = (80 * (j + 1) - (font_size[0] / 2) - 40, 80 * (i + 1) - 40 - (font_size[1] / 2)) 93 | draw.text(center, index, fill=ImageColor.getrgb("black"), font=self.font) 94 | else: 95 | count = self.count_around(i, j) 96 | if count == 0: 97 | continue 98 | font_size = self.font.getsize(str(count)) 99 | center = (80 * (j + 1) - (font_size[0] / 2) - 40, 80 * (i + 1) - 40 - (font_size[1] / 2)) 100 | draw.text(center, str(count), fill=self.__get_count_text_color(count), font=self.font) 101 | 102 | @staticmethod 103 | def __get_count_text_color(count): 104 | if count == 1: 105 | return ImageColor.getrgb("green") 106 | if count == 2: 107 | return ImageColor.getrgb("orange") 108 | if count == 3: 109 | return ImageColor.getrgb("red") 110 | if count == 4: 111 | return ImageColor.getrgb("darkred") 112 | return ImageColor.getrgb("black") 113 | 114 | def mine(self, row: int, column: int): 115 | if not self.__is_valid_location(row, column): 116 | raise ValueError("非法操作") 117 | start = time() 118 | cell = self.panel[row][column] 119 | if cell.is_mined: 120 | raise ValueError("你已经挖过这里了") 121 | cell.is_mined = True 122 | if self.state == GameState.PREPARE: 123 | self.__gen_mine() 124 | if self.state != GameState.GAMING: 125 | raise ValueError("游戏已结束") 126 | self.actions += 1 127 | if cell.is_mine: 128 | self.state = GameState.FAIL 129 | return 130 | self.__reset_check() 131 | self.__spread_not_mine(row, column) 132 | self.__win_check() 133 | print(f"mine spend {time()-start}ms at {str(self)}") 134 | 135 | def tag(self, row: int, column: int): 136 | cell = self.panel[row][column] 137 | start = time() 138 | if cell.is_mined: 139 | raise ValueError("你不能标记一个你挖开的地方") 140 | if self.state != GameState.GAMING and self.state != GameState.PREPARE: 141 | raise ValueError("游戏已结束") 142 | self.actions += 1 143 | if cell.is_marked: 144 | cell.is_marked = False 145 | else: 146 | cell.is_marked = True 147 | print(f"tag spend {time()-start}ms at {str(self)}") 148 | 149 | def __gen_mine(self): 150 | count = 0 151 | while count < self.mines: 152 | row = random.randint(0, self.row - 1) 153 | column = random.randint(0, self.column - 1) 154 | if self.panel[row][column].is_mine or self.panel[row][column].is_mined: 155 | continue 156 | self.panel[row][column].is_mine = True 157 | count += 1 158 | self.state = GameState.GAMING 159 | 160 | def __spread_not_mine(self, row: int, column): 161 | if not self.__is_valid_location(row, column): 162 | return 163 | cell = self.panel[row][column] 164 | if cell.is_checked: 165 | return 166 | if cell.is_mine: 167 | return 168 | cell.is_mined = True 169 | cell.is_checked = True 170 | count = self.count_around(row, column) 171 | if count > 0: 172 | return 173 | self.__spread_not_mine(row + 1, column) 174 | self.__spread_not_mine(row - 1, column) 175 | self.__spread_not_mine(row, column + 1) 176 | self.__spread_not_mine(row, column - 1) 177 | if count == 0: 178 | self.__spread_not_mine(row + 1, column + 1) 179 | self.__spread_not_mine(row - 1, column - 1) 180 | self.__spread_not_mine(row + 1, column - 1) 181 | self.__spread_not_mine(row - 1, column + 1) 182 | 183 | def __reset_check(self): 184 | for i in range(0, self.row): 185 | for j in range(0, self.column): 186 | self.panel[i][j].is_checked = False 187 | 188 | def __win_check(self): 189 | mined = 0 190 | for i in range(0, self.row): 191 | for j in range(0, self.column): 192 | if self.panel[i][j].is_mined: 193 | mined += 1 194 | if mined == (self.column * self.row) - self.mines: 195 | self.state = GameState.WIN 196 | 197 | def count_around(self, row: int, column: int) -> int: 198 | count = 0 199 | for r in range(row - 1, row + 2): 200 | for c in range(column - 1, column + 2): 201 | if not self.__is_valid_location(r, c): 202 | continue 203 | if self.panel[r][c].is_mine: 204 | count += 1 205 | if self.panel[row][column].is_mine: 206 | count -= 1 207 | return count 208 | 209 | @staticmethod 210 | def parse_input(input_text: str) -> Tuple[int, int]: 211 | if len(input_text) != 2: 212 | raise ValueError("非法位置") 213 | return COLUMN_NAME.index(input_text[0].upper()), COLUMN_NAME.index(input_text[1].upper()) 214 | 215 | def __is_valid_location(self, row: int, column: int) -> bool: 216 | if row > self.row - 1 or column > self.column - 1 or row < 0 or column < 0: 217 | return False 218 | return True 219 | 220 | 221 | if __name__ == '__main__': 222 | mine = MineSweeper(25, 25, 25) 223 | mine.draw_panel().show() 224 | while True: 225 | try: 226 | location = MineSweeper.parse_input(input()) 227 | mine.mine(location[0], location[1]) 228 | mine.draw_panel().show() 229 | print(mine.state) 230 | except Exception as e: 231 | print(e) 232 | -------------------------------------------------------------------------------- /mirai/__init__.py: -------------------------------------------------------------------------------- 1 | import mirai.logger 2 | from mirai.misc import ( 3 | ImageType 4 | ) 5 | from mirai.face import QQFaces 6 | from mirai.exceptions import NetworkError, Cancelled 7 | from mirai.depend import Depend 8 | 9 | import mirai.event.message.base 10 | from mirai.event.message.components import ( 11 | At, 12 | Plain, 13 | Source, 14 | AtAll, 15 | Face, 16 | Quote, 17 | Json as JsonMessage, 18 | Xml as XmlMessage, 19 | App as LightApp, 20 | Image, 21 | FlashImage 22 | ) 23 | from mirai.event.message.chain import ( 24 | MessageChain 25 | ) 26 | from mirai.event.message.models import ( 27 | GroupMessage, 28 | FriendMessage, 29 | BotMessage, 30 | TempMessage 31 | ) 32 | 33 | from mirai.event import ( 34 | InternalEvent, 35 | ExternalEvent 36 | ) 37 | 38 | from mirai.event.external import ( 39 | BotOnlineEvent, 40 | BotOfflineEventActive, 41 | BotOfflineEventForce, 42 | BotOfflineEventDropped, 43 | BotReloginEvent, 44 | BotGroupPermissionChangeEvent, 45 | BotMuteEvent, 46 | BotUnmuteEvent, 47 | BotJoinGroupEvent, 48 | 49 | GroupRecallEvent, 50 | FriendRecallEvent, 51 | 52 | GroupNameChangeEvent, 53 | GroupEntranceAnnouncementChangeEvent, 54 | GroupMuteAllEvent, 55 | 56 | # 群设置被修改事件 57 | GroupAllowAnonymousChatEvent, 58 | GroupAllowConfessTalkEvent, 59 | GroupAllowMemberInviteEvent, 60 | 61 | # 群事件(被 Bot 监听到的, 为"被动事件", 其中 Bot 身份为第三方.) 62 | MemberJoinEvent, 63 | MemberLeaveEventKick, 64 | MemberLeaveEventQuit, 65 | MemberCardChangeEvent, 66 | MemberSpecialTitleChangeEvent, 67 | MemberPermissionChangeEvent, 68 | MemberMuteEvent, 69 | MemberUnmuteEvent, 70 | 71 | NewFriendRequestEvent, 72 | MemberJoinRequestEvent 73 | ) 74 | from mirai.event.enums import ( 75 | NewFriendRequestResponseOperate as NewFriendRequestResp, 76 | MemberJoinRequestResponseOperate as MemberJoinRequestResp 77 | ) 78 | 79 | from mirai.entities.friend import ( 80 | Friend 81 | ) 82 | from mirai.entities.group import ( 83 | Group, 84 | Member, 85 | MemberChangeableSetting, 86 | Permission, 87 | GroupSetting 88 | ) 89 | 90 | import mirai.network 91 | import mirai.protocol 92 | 93 | from mirai.application import Mirai 94 | from mirai.event.builtins import ( 95 | UnexpectedException 96 | ) 97 | from mirai.event.external.enums import ExternalEvents 98 | -------------------------------------------------------------------------------- /mirai/application.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import copy 3 | import inspect 4 | import traceback 5 | from functools import partial, lru_cache 6 | from async_lru import alru_cache 7 | from typing import ( 8 | Any, Awaitable, Callable, Dict, List, NamedTuple, Optional, Union) 9 | from urllib import parse 10 | from contextlib import AsyncExitStack, ExitStack 11 | 12 | import pydantic 13 | import aiohttp 14 | import sys 15 | 16 | from mirai.depend import Depend 17 | from mirai.entities.friend import Friend 18 | from mirai.entities.group import Group, Member 19 | from mirai.event import ExternalEvent, ExternalEventTypes, InternalEvent 20 | from mirai.event.message import MessageChain, components 21 | from mirai.event.message.models import ( 22 | FriendMessage, GroupMessage, TempMessage, 23 | MessageItemType, MessageTypes 24 | ) 25 | from mirai.logger import ( 26 | Event as EventLogger, 27 | Session as SessionLogger, 28 | Network as NetworkLogger 29 | ) 30 | from mirai.misc import argument_signature, raiser, TRACEBACKED, printer 31 | from mirai.network import fetch 32 | from mirai.protocol import MiraiProtocol 33 | from mirai.entities.builtins import ExecutorProtocol 34 | from functools import lru_cache 35 | from mirai import exceptions 36 | 37 | class Mirai(MiraiProtocol): 38 | event: Dict[ 39 | str, List[Callable[[Any], Awaitable]] 40 | ] = {} 41 | subroutines: List[Callable] = [] 42 | lifecycle: Dict[str, List[Callable]] = { 43 | "start": [], 44 | "end": [], 45 | "around": [] 46 | } 47 | useWebsocket = False 48 | listening_exceptions: List[Exception] = [] 49 | 50 | extensite_config: Dict 51 | global_dependencies: List[Depend] 52 | global_middlewares: List 53 | 54 | def __init__(self, 55 | url: Optional[str] = None, 56 | 57 | host: Optional[str] = None, 58 | port: Optional[int] = None, 59 | authKey: Optional[str] = None, 60 | qq: Optional[int] = None, 61 | 62 | websocket: bool = False, 63 | extensite_config: dict = None, 64 | global_dependencies: List[Depend] = None, 65 | global_middlewares: List = None 66 | ): 67 | self.extensite_config = extensite_config or {} 68 | self.global_dependencies = global_dependencies or [] 69 | self.global_middlewares = global_middlewares or [] 70 | self.useWebsocket = websocket 71 | 72 | if url: 73 | urlinfo = parse.urlparse(url) 74 | if urlinfo: 75 | query_info = parse.parse_qs(urlinfo.query) 76 | if all([ 77 | urlinfo.scheme == "mirai", 78 | urlinfo.path in ["/", "/ws"], 79 | 80 | "authKey" in query_info and query_info["authKey"], 81 | "qq" in query_info and query_info["qq"] 82 | ]): 83 | if urlinfo.path == "/ws": 84 | self.useWebsocket = True 85 | else: 86 | self.useWebsocket = websocket 87 | 88 | authKey = query_info["authKey"][0] 89 | 90 | self.baseurl = f"http://{urlinfo.netloc}" 91 | self.auth_key = authKey 92 | self.qq = int(query_info["qq"][0]) 93 | else: 94 | raise ValueError("invaild url: wrong format") 95 | else: 96 | raise ValueError("invaild url") 97 | else: 98 | if all([host, port, authKey, qq]): 99 | self.baseurl = f"http://{host}:{port}" 100 | self.auth_key = authKey 101 | self.qq = int(qq) 102 | else: 103 | raise ValueError("invaild arguments") 104 | 105 | async def enable_session(self): 106 | auth_response = await self.auth() 107 | if all([ 108 | "code" in auth_response and auth_response['code'] == 0, 109 | "session" in auth_response and auth_response['session'] 110 | ]): 111 | if "msg" in auth_response and auth_response['msg']: 112 | self.session_key = auth_response['msg'] 113 | else: 114 | self.session_key = auth_response['session'] 115 | 116 | await self.verify() 117 | else: 118 | if "code" in auth_response and auth_response['code'] == 1: 119 | raise ValueError("invaild authKey") 120 | else: 121 | raise ValueError('invaild args: unknown response') 122 | 123 | self.enabled = True 124 | return self 125 | 126 | def receiver(self, 127 | event_name, 128 | dependencies: List[Depend] = None, 129 | use_middlewares: List[Callable] = None 130 | ): 131 | event_name = self.getEventCurrentName(event_name) 132 | def receiver_warpper(func: Callable): 133 | if not inspect.iscoroutinefunction(func): 134 | raise TypeError("event body must be a coroutine function.") 135 | 136 | self.event.setdefault(event_name, []) 137 | self.event[event_name].append(ExecutorProtocol( 138 | callable=func, 139 | dependencies=(dependencies or []) + self.global_dependencies, 140 | middlewares=(use_middlewares or []) + self.global_middlewares 141 | )) 142 | return func 143 | return receiver_warpper 144 | 145 | async def message_polling(self, count=10): 146 | while True: 147 | await asyncio.sleep(0.5) 148 | 149 | try: 150 | result = \ 151 | await super().fetchMessage(count) 152 | except pydantic.ValidationError: 153 | continue 154 | last_length = len(result) 155 | latest_result = [] 156 | while True: 157 | if last_length == count: 158 | latest_result = await super().fetchMessage(count) 159 | last_length = len(latest_result) 160 | result += latest_result 161 | continue 162 | break 163 | 164 | for message_index in range(len(result)): 165 | item = result[message_index] 166 | await self.queue.put( 167 | InternalEvent( 168 | name=self.getEventCurrentName(type(item)), 169 | body=item 170 | ) 171 | ) 172 | 173 | async def ws_message(self): 174 | async with aiohttp.ClientSession() as session: 175 | async with session.ws_connect( 176 | f"{self.baseurl}/message?sessionKey={self.session_key}" 177 | ) as ws_connection: 178 | while True: 179 | try: 180 | received_data = await ws_connection.receive_json() 181 | except TypeError: 182 | continue 183 | if received_data: 184 | NetworkLogger.debug("received", received_data) 185 | try: 186 | received_data['messageChain'] = MessageChain.parse_obj(received_data['messageChain']) 187 | received_data = MessageTypes[received_data['type']].parse_obj(received_data) 188 | except pydantic.ValidationError: 189 | SessionLogger.error(f"parse failed: {received_data}") 190 | traceback.print_exc() 191 | else: 192 | await self.queue.put(InternalEvent( 193 | name=self.getEventCurrentName(type(received_data)), 194 | body=received_data 195 | )) 196 | 197 | async def ws_event(self): 198 | from mirai.event.external.enums import ExternalEvents 199 | async with aiohttp.ClientSession() as session: 200 | async with session.ws_connect( 201 | f"{self.baseurl}/event?sessionKey={self.session_key}" 202 | ) as ws_connection: 203 | while True: 204 | try: 205 | received_data = await ws_connection.receive_json() 206 | except TypeError: 207 | continue 208 | if received_data: 209 | try: 210 | if hasattr(ExternalEvents, received_data['type']): 211 | received_data = \ 212 | ExternalEvents[received_data['type']]\ 213 | .value\ 214 | .parse_obj(received_data) 215 | else: 216 | raise exceptions.UnknownEvent(f"a unknown event has been received, it's '{received_data['type']}'") 217 | except pydantic.ValidationError: 218 | SessionLogger.error(f"parse failed: {received_data}") 219 | traceback.print_exc() 220 | else: 221 | await self.queue.put(InternalEvent( 222 | name=self.getEventCurrentName(type(received_data)), 223 | body=received_data 224 | )) 225 | 226 | async def event_runner(self): 227 | while True: 228 | try: 229 | event_context: NamedTuple[InternalEvent] = await asyncio.wait_for(self.queue.get(), 3) 230 | except asyncio.TimeoutError: 231 | continue 232 | 233 | if event_context.name in self.registeredEventNames: 234 | EventLogger.info(f"handling a event: {event_context.name}") 235 | for event_body in list(self.event.values())\ 236 | [self.registeredEventNames.index(event_context.name)]: 237 | if event_body: 238 | running_loop = asyncio.get_running_loop() 239 | running_loop.create_task(self.executor(event_body, event_context)) 240 | 241 | @staticmethod 242 | def sort_middlewares(iterator): 243 | return { 244 | "async": [ 245 | i for i in iterator if all([ 246 | hasattr(i, "__aenter__"), 247 | hasattr(i, "__aexit__") 248 | ]) 249 | ], 250 | "normal": [ 251 | i for i in iterator if all([ 252 | hasattr(i, "__enter__"), 253 | hasattr(i, "__exit__") 254 | ]) 255 | ] 256 | } 257 | 258 | async def put_exception(self, event_context, exception): 259 | from mirai.event.builtins import UnexpectedException 260 | if event_context.name != "UnexpectedException": 261 | if exception.__class__ in self.listening_exceptions: 262 | EventLogger.error(f"threw a exception by {event_context.name}, Exception: {exception.__class__.__name__}, and it has been catched.") 263 | else: 264 | EventLogger.error(f"threw a exception by {event_context.name}, Exception: {exception.__class__.__name__}, and it hasn't been catched!") 265 | traceback.print_exc() 266 | await self.queue.put(InternalEvent( 267 | name="UnexpectedException", 268 | body=UnexpectedException( 269 | error=exception, 270 | event=event_context, 271 | application=self 272 | ) 273 | )) 274 | else: 275 | EventLogger.critical(f"threw a exception in a exception handler by {event_context.name}, Exception: {exception.__class__.__name__}.") 276 | 277 | async def executor_with_middlewares(self, 278 | callable, raw_middlewares, 279 | event_context, 280 | lru_cache_sets=None 281 | ): 282 | middlewares = self.sort_middlewares(raw_middlewares) 283 | try: 284 | async with AsyncExitStack() as stack: 285 | for async_middleware in middlewares['async']: 286 | await stack.enter_async_context(async_middleware) 287 | for normal_middleware in middlewares['normal']: 288 | stack.enter_context(normal_middleware) 289 | 290 | result = await self.executor( 291 | ExecutorProtocol( 292 | callable=callable, 293 | dependencies=self.global_dependencies, 294 | middlewares=[] 295 | ), 296 | event_context, 297 | lru_cache_sets=lru_cache_sets 298 | ) 299 | if result is TRACEBACKED: 300 | return TRACEBACKED 301 | except exceptions.Cancelled: 302 | return TRACEBACKED 303 | except (NameError, TypeError) as e: 304 | EventLogger.error(f"threw a exception by {event_context.name}, it's about Annotations Checker, please report to developer.") 305 | traceback.print_exc() 306 | except Exception as exception: 307 | if type(exception) not in self.listening_exceptions: 308 | EventLogger.error(f"threw a exception by {event_context.name} in a depend, and it's {exception}, body has been cancelled.") 309 | raise 310 | else: 311 | await self.put_exception( 312 | event_context, 313 | exception 314 | ) 315 | return TRACEBACKED 316 | 317 | async def executor(self, 318 | executor_protocol: ExecutorProtocol, 319 | event_context, 320 | extra_parameter={}, 321 | lru_cache_sets=None 322 | ): 323 | lru_cache_sets = lru_cache_sets or {} 324 | executor_protocol: ExecutorProtocol 325 | for depend in executor_protocol.dependencies: 326 | if not inspect.isclass(depend.func): 327 | depend_func = depend.func 328 | elif hasattr(depend.func, "__call__"): 329 | depend_func = depend.func.__call__ 330 | else: 331 | raise TypeError("must be callable.") 332 | 333 | if depend_func in lru_cache_sets and depend.cache: 334 | depend_func = lru_cache_sets[depend_func] 335 | else: 336 | if depend.cache: 337 | original = depend_func 338 | if inspect.iscoroutinefunction(depend_func): 339 | depend_func = alru_cache(depend_func) 340 | else: 341 | depend_func = lru_cache(depend_func) 342 | lru_cache_sets[original] = depend_func 343 | 344 | result = await self.executor_with_middlewares( 345 | depend_func, depend.middlewares, event_context, lru_cache_sets 346 | ) 347 | if result is TRACEBACKED: 348 | return TRACEBACKED 349 | 350 | ParamSignatures = argument_signature(executor_protocol.callable) 351 | PlaceAnnotation = self.get_annotations_mapping() 352 | CallParams = {} 353 | for name, annotation, default in ParamSignatures: 354 | if default: 355 | if isinstance(default, Depend): 356 | if not inspect.isclass(default.func): 357 | depend_func = default.func 358 | elif hasattr(default.func, "__call__"): 359 | depend_func = default.func.__call__ 360 | else: 361 | raise TypeError("must be callable.") 362 | 363 | if depend_func in lru_cache_sets and default.cache: 364 | depend_func = lru_cache_sets[depend_func] 365 | else: 366 | if default.cache: 367 | original = depend_func 368 | if inspect.iscoroutinefunction(depend_func): 369 | depend_func = alru_cache(depend_func) 370 | else: 371 | depend_func = lru_cache(depend_func) 372 | lru_cache_sets[original] = depend_func 373 | 374 | CallParams[name] = await self.executor_with_middlewares( 375 | depend_func, default.middlewares, event_context, lru_cache_sets 376 | ) 377 | continue 378 | else: 379 | raise RuntimeError("checked a unexpected default value.") 380 | else: 381 | if annotation in PlaceAnnotation: 382 | CallParams[name] = PlaceAnnotation[annotation](event_context) 383 | continue 384 | else: 385 | if name not in extra_parameter: 386 | raise RuntimeError(f"checked a unexpected annotation: {annotation}") 387 | 388 | try: 389 | async with AsyncExitStack() as stack: 390 | sorted_middlewares = self.sort_middlewares(executor_protocol.middlewares) 391 | for async_middleware in sorted_middlewares['async']: 392 | await stack.enter_async_context(async_middleware) 393 | for normal_middleware in sorted_middlewares['normal']: 394 | stack.enter_context(normal_middleware) 395 | 396 | return await self.run_func(executor_protocol.callable, **CallParams, **extra_parameter) 397 | except exceptions.Cancelled: 398 | return TRACEBACKED 399 | except Exception as e: 400 | await self.put_exception(event_context, e) 401 | return TRACEBACKED 402 | 403 | def getRestraintMapping(self): 404 | from mirai.event.external.enums import ExternalEvents 405 | return { 406 | Mirai: lambda k: True, 407 | GroupMessage: lambda k: k.__class__.__name__ == "GroupMessage", 408 | FriendMessage: lambda k: k.__class__.__name__ == "FriendMessage", 409 | TempMessage: lambda k: k.__class__.__name__ == "TempMessage", 410 | MessageChain: lambda k: k.__class__.__name__ in MessageTypes, 411 | components.Source: lambda k: k.__class__.__name__ in MessageTypes, 412 | Group: lambda k: k.__class__.__name__ in ["GroupMessage", "TempMessage"], 413 | Friend: lambda k: k.__class__.__name__ =="FriendMessage", 414 | Member: lambda k: k.__class__.__name__ in ["GroupMessage", "TempMessage"], 415 | "Sender": lambda k: k.__class__.__name__ in MessageTypes, 416 | "Type": lambda k: k.__class__.__name__, 417 | **({ 418 | event_class.value: partial( 419 | (lambda a, b: a == b.__class__.__name__), 420 | copy.copy(event_name) 421 | ) 422 | for event_name, event_class in \ 423 | ExternalEvents.__members__.items() 424 | }) 425 | } 426 | 427 | def checkEventBodyAnnotations(self): 428 | event_bodys: Dict[Callable, List[str]] = {} 429 | for event_name in self.event: 430 | event_body_list = self.event[event_name] 431 | for i in event_body_list: 432 | event_bodys.setdefault(i.callable, []) 433 | event_bodys[i.callable].append(event_name) 434 | 435 | restraint_mapping = self.getRestraintMapping() 436 | for func in event_bodys: 437 | self.checkFuncAnnotations(func) 438 | 439 | def getFuncRegisteredEvents(self, callable_target: Callable): 440 | result = [] 441 | for event_name in self.event: 442 | if callable_target in [i.callable for i in self.event[event_name]]: 443 | result.append(event_name) 444 | return result 445 | 446 | def checkFuncAnnotations(self, callable_target: Callable): 447 | restraint_mapping = self.getRestraintMapping() 448 | registered_events = self.getFuncRegisteredEvents(callable_target) 449 | for name, annotation, default in argument_signature(callable_target): 450 | if not default: 451 | if not registered_events: 452 | raise ValueError(f"error in annotations checker: {callable_target} is invaild.") 453 | for event_name in registered_events: 454 | try: 455 | if not restraint_mapping[annotation](type(event_name, (object,), {})()): 456 | raise ValueError(f"error in annotations checker: {callable_target}.[{name}:{annotation}]: {event_name}") 457 | except KeyError: 458 | raise ValueError(f"error in annotations checker: {callable_target}.[{name}:{annotation}] is invaild.") 459 | except ValueError: 460 | raise 461 | 462 | def checkDependencies(self, depend_target: Depend): 463 | self.checkEventBodyAnnotations() 464 | for name, annotation, default in argument_signature(depend_target.func): 465 | if type(default) == Depend: 466 | self.checkDependencies(default) 467 | 468 | def checkEventDependencies(self): 469 | for event_name, event_bodys in self.event.items(): 470 | for i in event_bodys: 471 | for depend in i.dependencies: 472 | if type(depend) != Depend: 473 | raise TypeError(f"error in dependencies checker: {i['func']}: {event_name}") 474 | else: 475 | self.checkDependencies(depend) 476 | 477 | def exception_handler(self, exception_class=None): 478 | from .event.builtins import UnexpectedException 479 | from mirai.event.external.enums import ExternalEvents 480 | def receiver_warpper(func: Callable): 481 | event_name = "UnexpectedException" 482 | 483 | if not inspect.iscoroutinefunction(func): 484 | raise TypeError("event body must be a coroutine function.") 485 | 486 | async def func_warpper_inout(context: UnexpectedException, *args, **kwargs): 487 | if type(context.error) == exception_class: 488 | return await func(context, *args, **kwargs) 489 | 490 | func_warpper_inout.__annotations__.update(func.__annotations__) 491 | 492 | self.event.setdefault(event_name, []) 493 | self.event[event_name].append(ExecutorProtocol( 494 | callable=func_warpper_inout, 495 | dependencies=self.global_dependencies, 496 | middlewares=self.global_middlewares 497 | )) 498 | 499 | if exception_class: 500 | if exception_class not in self.listening_exceptions: 501 | self.listening_exceptions.append(exception_class) 502 | return func 503 | return receiver_warpper 504 | 505 | def gen_event_anno(self): 506 | from mirai.event.external.enums import ExternalEvents 507 | 508 | def warpper(name, event_context): 509 | if name != event_context.name: 510 | raise ValueError("cannot look up a non-listened event.") 511 | return event_context.body 512 | return { 513 | event_class.value: partial(warpper, copy.copy(event_name))\ 514 | for event_name, event_class in ExternalEvents.__members__.items() 515 | } 516 | 517 | def get_annotations_mapping(self): 518 | return { 519 | Mirai: lambda k: self, 520 | GroupMessage: lambda k: k.body \ 521 | if self.getEventCurrentName(k.body) == "GroupMessage" else\ 522 | raiser(ValueError("you cannot setting a unbind argument.")), 523 | FriendMessage: lambda k: k.body \ 524 | if self.getEventCurrentName(k.body) == "FriendMessage" else\ 525 | raiser(ValueError("you cannot setting a unbind argument.")), 526 | TempMessage: lambda k: k.body \ 527 | if self.getEventCurrentName(k.body) == "TempMessage" else\ 528 | raiser(ValueError("you cannot setting a unbind argument.")), 529 | MessageChain: lambda k: k.body.messageChain\ 530 | if self.getEventCurrentName(k.body) in MessageTypes else\ 531 | raiser(ValueError("MessageChain is not enable in this type of event.")), 532 | components.Source: lambda k: k.body.messageChain.getSource()\ 533 | if self.getEventCurrentName(k.body) in MessageTypes else\ 534 | raiser(TypeError("Source is not enable in this type of event.")), 535 | Group: lambda k: k.body.sender.group\ 536 | if self.getEventCurrentName(k.body) in ["GroupMessage", "TempMessage"] else\ 537 | raiser(ValueError("Group is not enable in this type of event.")), 538 | Friend: lambda k: k.body.sender\ 539 | if self.getEventCurrentName(k.body) == "FriendMessage" else\ 540 | raiser(ValueError("Friend is not enable in this type of event.")), 541 | Member: lambda k: k.body.sender\ 542 | if self.getEventCurrentName(k.body) in ["GroupMessage", "TempMessage"] else\ 543 | raiser(ValueError("Group is not enable in this type of event.")), 544 | "Sender": lambda k: k.body.sender\ 545 | if self.getEventCurrentName(k.body) in MessageTypes else\ 546 | raiser(ValueError("Sender is not enable in this type of event.")), 547 | "Type": lambda k: self.getEventCurrentName(k.body), 548 | **self.gen_event_anno() 549 | } 550 | 551 | def getEventCurrentName(self, event_value): 552 | from .event.builtins import UnexpectedException 553 | from mirai.event.external.enums import ExternalEvents 554 | if inspect.isclass(event_value) and issubclass(event_value, ExternalEvent): # subclass 555 | return event_value.__name__ 556 | elif isinstance(event_value, ( # normal class 557 | UnexpectedException, 558 | GroupMessage, 559 | FriendMessage, 560 | TempMessage 561 | )): 562 | return event_value.__class__.__name__ 563 | elif event_value in [ # message 564 | GroupMessage, 565 | FriendMessage, 566 | TempMessage 567 | ]: 568 | return event_value.__name__ 569 | elif isinstance(event_value, ( # enum 570 | MessageItemType, 571 | ExternalEvents 572 | )): 573 | return event_value.name 574 | else: 575 | return event_value 576 | 577 | @property 578 | def registeredEventNames(self): 579 | return [self.getEventCurrentName(i) for i in self.event.keys()] 580 | 581 | def subroutine(self, func: Callable[["Mirai"], Any]): 582 | from .event.builtins import UnexpectedException 583 | async def warpper(app: "Mirai"): 584 | try: 585 | return await func(app) 586 | except Exception as e: 587 | await self.queue.put(InternalEvent( 588 | name="UnexpectedException", 589 | body=UnexpectedException( 590 | error=e, 591 | event=None, 592 | application=self 593 | ) 594 | )) 595 | self.subroutines.append(warpper) 596 | return func 597 | 598 | async def checkWebsocket(self, force=False): 599 | return (await self.getConfig())["enableWebsocket"] 600 | 601 | @staticmethod 602 | async def run_func(func, *args, **kwargs): 603 | if inspect.iscoroutinefunction(func): 604 | await func(*args, **kwargs) 605 | else: 606 | func(*args, **kwargs) 607 | 608 | def onStage(self, stage_name): 609 | def warpper(func): 610 | self.lifecycle.setdefault(stage_name, []) 611 | self.lifecycle[stage_name].append(func) 612 | return func 613 | return warpper 614 | 615 | def include_others(self, *args: List["Mirai"]): 616 | for other in args: 617 | for event_name, items in other.event.items(): 618 | if event_name in self.event: 619 | self.event[event_name] += items 620 | else: 621 | self.event[event_name] = items.copy() 622 | self.subroutines = other.subroutines 623 | for life_name, items in other.lifecycle: 624 | self.lifecycle.setdefault(life_name, []) 625 | self.lifecycle[life_name] += items 626 | self.listening_exceptions += other.listening_exceptions 627 | 628 | def run(self, loop=None, no_polling=False, no_forever=False): 629 | self.checkEventBodyAnnotations() 630 | self.checkEventDependencies() 631 | 632 | loop = loop or asyncio.get_event_loop() 633 | self.queue = asyncio.Queue(loop=loop) 634 | exit_signal = False 635 | loop.run_until_complete(self.enable_session()) 636 | if not no_polling: 637 | # check ws status 638 | if self.useWebsocket: 639 | SessionLogger.info("event receive method: websocket") 640 | else: 641 | SessionLogger.info("event receive method: http polling") 642 | 643 | result = loop.run_until_complete(self.checkWebsocket()) 644 | if not result: # we can use http, not ws. 645 | # should use http, but we can change it. 646 | if self.useWebsocket: 647 | SessionLogger.warning("catched wrong config: enableWebsocket=false, we will modify it.") 648 | loop.run_until_complete(self.setConfig(enableWebsocket=True)) 649 | loop.create_task(self.ws_event()) 650 | loop.create_task(self.ws_message()) 651 | else: 652 | loop.create_task(self.message_polling()) 653 | else: # we can use websocket, it's fine 654 | if self.useWebsocket: 655 | loop.create_task(self.ws_event()) 656 | loop.create_task(self.ws_message()) 657 | else: 658 | SessionLogger.warning("catched wrong config: enableWebsocket=true, we will modify it.") 659 | loop.run_until_complete(self.setConfig(enableWebsocket=False)) 660 | loop.create_task(self.message_polling()) 661 | loop.create_task(self.event_runner()) 662 | 663 | if not no_forever: 664 | for i in self.subroutines: 665 | loop.create_task(i(self)) 666 | 667 | try: 668 | for start_callable in self.lifecycle['start']: 669 | loop.run_until_complete(self.run_func(start_callable, self)) 670 | 671 | for around_callable in self.lifecycle['around']: 672 | loop.run_until_complete(self.run_func(around_callable, self)) 673 | 674 | loop.run_forever() 675 | except KeyboardInterrupt: 676 | SessionLogger.info("catched Ctrl-C, exiting..") 677 | except Exception as e: 678 | traceback.print_exc() 679 | finally: 680 | for around_callable in self.lifecycle['around']: 681 | loop.run_until_complete(self.run_func(around_callable, self)) 682 | 683 | for end_callable in self.lifecycle['end']: 684 | loop.run_until_complete(self.run_func(end_callable, self)) 685 | 686 | loop.run_until_complete(self.release()) 687 | -------------------------------------------------------------------------------- /mirai/depend.py: -------------------------------------------------------------------------------- 1 | class Depend: 2 | def __init__(self, func, middlewares=[], cache=True): 3 | self.func = func 4 | self.middlewares = middlewares 5 | self.cache = cache -------------------------------------------------------------------------------- /mirai/entities/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzdluo123/MineSweeper/f0ef81b9369e5332d3ef1ec370bd6ae1a6256a22/mirai/entities/__init__.py -------------------------------------------------------------------------------- /mirai/entities/builtins.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | import typing as T 3 | from mirai.depend import Depend 4 | 5 | class ExecutorProtocol(BaseModel): 6 | callable: T.Callable 7 | dependencies: T.List[Depend] 8 | middlewares: T.List 9 | 10 | class Config: 11 | arbitrary_types_allowed = True -------------------------------------------------------------------------------- /mirai/entities/friend.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID 2 | from pydantic import BaseModel 3 | from typing import Optional 4 | 5 | class Friend(BaseModel): 6 | id: int 7 | nickname: Optional[str] 8 | remark: Optional[str] 9 | 10 | def __repr__(self): 11 | return f"" 12 | 13 | def getAvatarUrl(self) -> str: 14 | return f'http://q4.qlogo.cn/g?b=qq&nk={self.id}&s=140' 15 | 16 | -------------------------------------------------------------------------------- /mirai/entities/group.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from dataclasses import dataclass 3 | from pydantic import BaseModel 4 | 5 | class Permission(Enum): 6 | Member = "MEMBER" 7 | Administrator = "ADMINISTRATOR" 8 | Owner = "OWNER" 9 | 10 | class Group(BaseModel): 11 | id: int 12 | name: str 13 | permission: Permission 14 | 15 | def __repr__(self): 16 | return f"" 17 | 18 | def getAvatarUrl(self) -> str: 19 | return f'https://p.qlogo.cn/gh/{self.id}/{self.id}/' 20 | 21 | class Member(BaseModel): 22 | id: int 23 | memberName: str 24 | permission: Permission 25 | group: Group 26 | 27 | def __repr__(self): 28 | return f"" 29 | 30 | def getAvatarUrl(self) -> str: 31 | return f'http://q4.qlogo.cn/g?b=qq&nk={self.id}&s=140' 32 | 33 | class MemberChangeableSetting(BaseModel): 34 | name: str 35 | specialTitle: str 36 | 37 | def modify(self, **kwargs): 38 | for i in ("name", "kwargs"): 39 | if i in kwargs: 40 | setattr(self, i, kwargs[i]) 41 | return self 42 | 43 | class GroupSetting(BaseModel): 44 | name: str 45 | announcement: str 46 | confessTalk: bool 47 | allowMemberInvite: bool 48 | autoApprove: bool 49 | anonymousChat: bool 50 | 51 | def modify(self, **kwargs): 52 | for i in ("name", 53 | "announcement", 54 | "confessTalk", 55 | "allowMemberInvite", 56 | "autoApprove", 57 | "anonymousChat" 58 | ): 59 | if i in kwargs: 60 | setattr(self, i, kwargs[i]) 61 | return self 62 | -------------------------------------------------------------------------------- /mirai/event/__init__.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | from typing import Any 3 | from enum import Enum 4 | from pydantic import BaseModel 5 | 6 | # 内部事件实现. 7 | InternalEvent = namedtuple("Event", ("name", "body")) 8 | 9 | from .enums import ExternalEventTypes 10 | class ExternalEvent(BaseModel): 11 | type: ExternalEventTypes -------------------------------------------------------------------------------- /mirai/event/builtins.py: -------------------------------------------------------------------------------- 1 | from . import InternalEvent 2 | from pydantic import BaseModel 3 | from mirai import Mirai 4 | 5 | class UnexpectedException(BaseModel): 6 | error: Exception 7 | event: InternalEvent 8 | application: Mirai 9 | 10 | class Config: 11 | arbitrary_types_allowed = True -------------------------------------------------------------------------------- /mirai/event/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class ExternalEventTypes(Enum): 4 | BotOnlineEvent = "BotOnlineEvent" 5 | BotOfflineEventActive = "BotOfflineEventActive" 6 | BotOfflineEventForce = "BotOfflineEventForce" 7 | BotOfflineEventDropped = "BotOfflineEventDropped" 8 | BotReloginEvent = "BotReloginEvent" 9 | BotGroupPermissionChangeEvent = "BotGroupPermissionChangeEvent" 10 | BotMuteEvent = "BotMuteEvent" 11 | BotUnmuteEvent = "BotUnmuteEvent" 12 | BotJoinGroupEvent = "BotJoinGroupEvent" 13 | BotLeaveEventActive = "BotLeaveEventActive" 14 | BotLeaveEventKick = "BotLeaveEventKick" 15 | 16 | GroupRecallEvent = "GroupRecallEvent" 17 | FriendRecallEvent = "FriendRecallEvent" 18 | 19 | GroupNameChangeEvent = "GroupNameChangeEvent" 20 | GroupEntranceAnnouncementChangeEvent = "GroupEntranceAnnouncementChangeEvent" 21 | GroupMuteAllEvent = "GroupMuteAllEvent" 22 | 23 | # 群设置被修改事件 24 | GroupAllowAnonymousChatEvent = "GroupAllowAnonymousChatEvent" # 群设置 是否允许匿名聊天 被修改 25 | GroupAllowConfessTalkEvent = "GroupAllowConfessTalkEvent" # 坦白说 26 | GroupAllowMemberInviteEvent = "GroupAllowMemberInviteEvent" # 邀请进群 27 | 28 | # 群事件(被 Bot 监听到的, 为"被动事件", 其中 Bot 身份为第三方.) 29 | MemberJoinEvent = "MemberJoinEvent" 30 | MemberLeaveEventKick = "MemberLeaveEventKick" 31 | MemberLeaveEventQuit = "MemberLeaveEventQuit" 32 | MemberCardChangeEvent = "MemberCardChangeEvent" 33 | MemberSpecialTitleChangeEvent = "MemberSpecialTitleChangeEvent" 34 | MemberPermissionChangeEvent = "MemberPermissionChangeEvent" 35 | MemberMuteEvent = "MemberMuteEvent" 36 | MemberUnmuteEvent = "MemberUnmuteEvent" 37 | 38 | NewFriendRequestEvent = "NewFriendRequestEvent" 39 | MemberJoinRequestEvent = "MemberJoinRequestEvent" 40 | 41 | # python-mirai 自己提供的事件 42 | UnexceptedException = "UnexceptedException" 43 | 44 | class NewFriendRequestResponseOperate(Enum): 45 | accept = 0 46 | refuse = 1 47 | refuse_and_blacklist = 2 48 | 49 | class MemberJoinRequestResponseOperate(Enum): 50 | accept = 0 51 | refuse = 1 52 | ignore = 2 53 | refuse_and_blacklist = 3 54 | ignore_and_blacklist = 4 55 | -------------------------------------------------------------------------------- /mirai/event/external/__init__.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from mirai.event import ExternalEvent 3 | from mirai.event.enums import ExternalEventTypes as EventType 4 | from mirai.entities.group import Permission, Group, Member 5 | from mirai.entities.friend import Friend 6 | import typing as T 7 | from datetime import datetime 8 | 9 | class BotOnlineEvent(ExternalEvent): 10 | type: EventType = EventType.BotOnlineEvent 11 | qq: int 12 | 13 | class BotOfflineEventActive(ExternalEvent): 14 | type: EventType = EventType.BotOfflineEventActive 15 | qq: int 16 | 17 | class BotOfflineEventForce(ExternalEvent): 18 | type: EventType = EventType.BotOfflineEventForce 19 | qq: int 20 | 21 | class BotOfflineEventDropped(ExternalEvent): 22 | type: EventType = EventType.BotOfflineEventDropped 23 | qq: int 24 | 25 | class BotReloginEvent(ExternalEvent): 26 | type: EventType = EventType.BotReloginEvent 27 | qq: int 28 | 29 | class BotGroupPermissionChangeEvent(ExternalEvent): 30 | type: EventType = EventType.BotGroupPermissionChangeEvent 31 | origin: Permission 32 | current: Permission 33 | group: Group 34 | 35 | class BotMuteEvent(ExternalEvent): 36 | type: EventType = EventType.BotMuteEvent 37 | durationSeconds: int 38 | operator: T.Optional[Member] 39 | 40 | class BotUnmuteEvent(ExternalEvent): 41 | type: EventType = EventType.BotUnmuteEvent 42 | operator: T.Optional[Member] 43 | 44 | class BotJoinGroupEvent(ExternalEvent): 45 | type: EventType = EventType.BotJoinGroupEvent 46 | group: Group 47 | 48 | class GroupRecallEvent(ExternalEvent): 49 | type: EventType = EventType.GroupRecallEvent 50 | authorId: int 51 | messageId: int 52 | time: datetime 53 | group: Group 54 | operator: T.Optional[Member] 55 | 56 | class FriendRecallEvent(ExternalEvent): 57 | type: EventType = EventType.FriendRecallEvent 58 | authorId: int 59 | messageId: int 60 | time: int 61 | operator: int 62 | 63 | class GroupNameChangeEvent(ExternalEvent): 64 | type: EventType = EventType.GroupNameChangeEvent 65 | origin: str 66 | current: str 67 | group: Group 68 | operator: T.Optional[Member] 69 | 70 | class GroupEntranceAnnouncementChangeEvent(ExternalEvent): 71 | type: EventType = EventType.GroupEntranceAnnouncementChangeEvent 72 | origin: str 73 | current: str 74 | group: Group 75 | operator: T.Optional[Member] 76 | 77 | class GroupMuteAllEvent(ExternalEvent): 78 | type: EventType = EventType.GroupMuteAllEvent 79 | origin: bool 80 | current: bool 81 | group: Group 82 | operator: T.Optional[Member] 83 | 84 | class GroupAllowAnonymousChatEvent(ExternalEvent): 85 | type: EventType = EventType.GroupAllowAnonymousChatEvent 86 | origin: bool 87 | current: bool 88 | group: Group 89 | operator: T.Optional[Member] 90 | 91 | class GroupAllowConfessTalkEvent(ExternalEvent): 92 | type: EventType = EventType.GroupAllowAnonymousChatEvent 93 | origin: bool 94 | current: bool 95 | group: Group 96 | isByBot: bool 97 | 98 | class GroupAllowMemberInviteEvent(ExternalEvent): 99 | type: EventType = EventType.GroupAllowMemberInviteEvent 100 | origin: bool 101 | current: bool 102 | group: Group 103 | operator: T.Optional[Member] 104 | 105 | class MemberJoinEvent(ExternalEvent): 106 | type: EventType = EventType.MemberJoinEvent 107 | member: Member 108 | 109 | class MemberLeaveEventKick(ExternalEvent): 110 | type: EventType = EventType.MemberLeaveEventKick 111 | member: Member 112 | operator: T.Optional[Member] 113 | 114 | class MemberLeaveEventQuit(ExternalEvent): 115 | type: EventType = EventType.MemberLeaveEventQuit 116 | member: Member 117 | 118 | class MemberCardChangeEvent(ExternalEvent): 119 | type: EventType = EventType.MemberCardChangeEvent 120 | origin: str 121 | current: str 122 | member: Member 123 | operator: T.Optional[Member] 124 | 125 | class MemberSpecialTitleChangeEvent(ExternalEvent): 126 | type: EventType = EventType.MemberSpecialTitleChangeEvent 127 | origin: str 128 | current: str 129 | member: Member 130 | 131 | class MemberPermissionChangeEvent(ExternalEvent): 132 | type: EventType = EventType.MemberPermissionChangeEvent 133 | origin: str 134 | current: str 135 | member: Member 136 | 137 | class MemberMuteEvent(ExternalEvent): 138 | type: EventType = EventType.MemberMuteEvent 139 | durationSeconds: int 140 | member: Member 141 | operator: T.Optional[Member] 142 | 143 | class MemberUnmuteEvent(ExternalEvent): 144 | type: EventType = EventType.MemberUnmuteEvent 145 | member: Member 146 | operator: T.Optional[Member] 147 | 148 | class BotLeaveEventActive(ExternalEvent): 149 | type: EventType = EventType.BotLeaveEventActive 150 | group: Group 151 | 152 | class BotLeaveEventKick(ExternalEvent): 153 | type: EventType = EventType.BotLeaveEventKick 154 | group: Group 155 | 156 | class NewFriendRequestEvent(ExternalEvent): 157 | type: EventType = EventType.NewFriendRequestEvent 158 | requestId: int = Field(..., alias="eventId") 159 | supplicant: int = Field(..., alias="fromId") # 即请求方 QQ 160 | sourceGroup: T.Optional[int] = Field(..., alias="groupId") 161 | nickname: str = Field(..., alias="nick") 162 | 163 | class MemberJoinRequestEvent(ExternalEvent): 164 | type: EventType = EventType.MemberJoinRequestEvent 165 | requestId: int = Field(..., alias="eventId") 166 | supplicant: int = Field(..., alias="fromId") # 即请求方 QQ 167 | groupId: T.Optional[int] = Field(..., alias="groupId") 168 | groupName: str = Field(..., alias="groupName") 169 | nickname: str = Field(..., alias="nick") 170 | -------------------------------------------------------------------------------- /mirai/event/external/enums.py: -------------------------------------------------------------------------------- 1 | from . import * 2 | from ..builtins import UnexpectedException 3 | from enum import Enum 4 | 5 | class ExternalEvents(Enum): 6 | BotOnlineEvent = BotOnlineEvent 7 | BotOfflineEventActive = BotOfflineEventActive 8 | BotOfflineEventForce = BotOfflineEventForce 9 | BotOfflineEventDropped = BotOfflineEventDropped 10 | BotReloginEvent = BotReloginEvent 11 | BotGroupPermissionChangeEvent = BotGroupPermissionChangeEvent 12 | BotMuteEvent = BotMuteEvent 13 | BotUnmuteEvent = BotUnmuteEvent 14 | BotJoinGroupEvent = BotJoinGroupEvent 15 | BotLeaveEventActive = BotLeaveEventActive 16 | BotLeaveEventKick = BotLeaveEventKick 17 | 18 | GroupRecallEvent = GroupRecallEvent 19 | FriendRecallEvent = FriendRecallEvent 20 | 21 | GroupNameChangeEvent = GroupNameChangeEvent 22 | GroupEntranceAnnouncementChangeEvent = GroupEntranceAnnouncementChangeEvent 23 | GroupMuteAllEvent = GroupMuteAllEvent 24 | 25 | # 群设置被修改事件 26 | GroupAllowAnonymousChatEvent = GroupAllowAnonymousChatEvent # 群设置 是否允许匿名聊天 被修改 27 | GroupAllowConfessTalkEvent = GroupAllowConfessTalkEvent # 坦白说 28 | GroupAllowMemberInviteEvent = GroupAllowMemberInviteEvent # 邀请进群 29 | 30 | # 群事件(被 Bot 监听到的, 为被动事件, 其中 Bot 身份为第三方.) 31 | MemberJoinEvent = MemberJoinEvent 32 | MemberLeaveEventKick = MemberLeaveEventKick 33 | MemberLeaveEventQuit = MemberLeaveEventQuit 34 | MemberCardChangeEvent = MemberCardChangeEvent 35 | MemberSpecialTitleChangeEvent = MemberSpecialTitleChangeEvent 36 | MemberPermissionChangeEvent = MemberPermissionChangeEvent 37 | MemberMuteEvent = MemberMuteEvent 38 | MemberUnmuteEvent = MemberUnmuteEvent 39 | 40 | NewFriendRequestEvent = NewFriendRequestEvent 41 | MemberJoinEvent 42 | 43 | UnexpectedException = UnexpectedException 44 | -------------------------------------------------------------------------------- /mirai/event/message/__init__.py: -------------------------------------------------------------------------------- 1 | from .components import * 2 | from .chain import MessageChain 3 | from .models import MessageTypes -------------------------------------------------------------------------------- /mirai/event/message/base.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from pydantic import BaseModel 3 | 4 | __all__ = [ 5 | "MessageComponentTypes", 6 | "BaseMessageComponent" 7 | ] 8 | 9 | class MessageComponentTypes(Enum): 10 | Source = "Source" 11 | Plain = "Plain" 12 | Face = "Face" 13 | At = "At" 14 | AtAll = "AtAll" 15 | Image = "Image" 16 | Quote = "Quote" 17 | Xml = "Xml" 18 | Json = "Json" 19 | App = "App" 20 | Poke = "Poke" 21 | FlashImage = "FlashImage" 22 | Unknown = "Unknown" 23 | 24 | class BaseMessageComponent(BaseModel): 25 | type: MessageComponentTypes 26 | 27 | def toString(self): 28 | return self.__repr__() -------------------------------------------------------------------------------- /mirai/event/message/chain.py: -------------------------------------------------------------------------------- 1 | import typing as T 2 | from pydantic import BaseModel 3 | 4 | from .base import BaseMessageComponent 5 | from mirai.misc import raiser, printer, if_error_print_arg 6 | from .components import Source 7 | from mirai.logger import Protocol 8 | 9 | class MessageChain(BaseModel): 10 | __root__: T.List[BaseMessageComponent] = [] 11 | 12 | def __add__(self, value): 13 | if isinstance(value, BaseMessageComponent): 14 | self.__root__.append(value) 15 | return self 16 | elif isinstance(value, MessageChain): 17 | self.__root__ += value.__root__ 18 | return self 19 | 20 | def toString(self) -> str: 21 | return "".join([i.toString() for i in self.__root__]) 22 | 23 | @classmethod 24 | def parse_obj(cls, obj): 25 | from .components import MessageComponents 26 | result = [] 27 | for i in obj: 28 | if not isinstance(i, dict): 29 | raise TypeError("invaild value") 30 | try: 31 | result.append(MessageComponents[i['type']].parse_obj(i)) 32 | except: 33 | Protocol.error(f"error throwed by message serialization: {i['type']}, it's {i}") 34 | raise 35 | return cls(__root__=result) 36 | 37 | def __iter__(self): 38 | yield from self.__root__ 39 | 40 | def __getitem__(self, index): 41 | return self.__root__[index] 42 | 43 | def hasComponent(self, component_class) -> bool: 44 | for i in self: 45 | if type(i) == component_class: 46 | return True 47 | else: 48 | return False 49 | 50 | def __len__(self) -> int: 51 | return len(self.__root__) 52 | 53 | def getFirstComponent(self, component_class) -> T.Optional[BaseMessageComponent]: 54 | for i in self: 55 | if type(i) == component_class: 56 | return i 57 | 58 | def getAllofComponent(self, component_class) -> T.List[BaseMessageComponent]: 59 | return [i for i in self if type(i) == component_class] 60 | 61 | def getSource(self) -> Source: 62 | return self.getFirstComponent(Source) 63 | 64 | __contains__ = hasComponent 65 | __getitem__ = getAllofComponent -------------------------------------------------------------------------------- /mirai/event/message/components.py: -------------------------------------------------------------------------------- 1 | import typing as T 2 | from mirai.misc import findKey, printer, ImageRegex, getMatchedString 3 | from mirai.event.message.base import BaseMessageComponent, MessageComponentTypes 4 | from pydantic import Field, validator, HttpUrl 5 | from io import BytesIO 6 | from pathlib import Path 7 | from mirai.image import ( 8 | LocalImage, 9 | IOImage, 10 | Base64Image, BytesImage, 11 | ) 12 | from mirai.logger import Protocol as ProtocolLogger 13 | from aiohttp import ClientSession 14 | import datetime 15 | 16 | __all__ = [ 17 | "Plain", 18 | "Source", 19 | "At", 20 | "AtAll", 21 | "Face", 22 | "Image", 23 | "Unknown", 24 | "Quote", 25 | "FlashImage" 26 | ] 27 | 28 | class Plain(BaseMessageComponent): 29 | type: MessageComponentTypes = "Plain" 30 | text: str 31 | 32 | def __init__(self, text, **_): 33 | if len(text) > 128: 34 | ProtocolLogger.warn(f"mirai does not support for long string: now its length is {len(text)}") 35 | super().__init__(text=text, type="Plain") 36 | 37 | def toString(self): 38 | return self.text 39 | 40 | class Source(BaseMessageComponent): 41 | type: MessageComponentTypes = "Source" 42 | id: int 43 | time: datetime.datetime 44 | 45 | def toString(self): 46 | return "" 47 | 48 | from .chain import MessageChain 49 | 50 | class Quote(BaseMessageComponent): 51 | type: MessageComponentTypes = "Quote" 52 | id: T.Optional[int] 53 | groupId: T.Optional[int] 54 | senderId: T.Optional[int] 55 | targetId: T.Optional[int] 56 | origin: MessageChain 57 | 58 | @validator("origin", always=True, pre=True) 59 | @classmethod 60 | def origin_formater(cls, v): 61 | return MessageChain.parse_obj(v) 62 | 63 | def __init__(self, id: int, groupId: int, senderId: int, origin: int, **_): 64 | super().__init__( 65 | id=id, 66 | groupId=groupId, 67 | senderId=senderId, 68 | origin=origin 69 | ) 70 | 71 | def toString(self): 72 | return "" 73 | 74 | class At(BaseMessageComponent): 75 | type: MessageComponentTypes = "At" 76 | target: int 77 | display: T.Optional[str] = None 78 | 79 | def __init__(self, target, display=None, **_): 80 | super().__init__(target=target, display=display) 81 | 82 | def toString(self): 83 | return f"[At::target={self.target}]" 84 | 85 | class AtAll(BaseMessageComponent): 86 | type: MessageComponentTypes = "AtAll" 87 | 88 | def __init__(self, **_): 89 | super().__init__() 90 | 91 | def toString(self): 92 | return f"[AtAll]" 93 | 94 | class Face(BaseMessageComponent): 95 | type: MessageComponentTypes = "Face" 96 | faceId: int 97 | name: T.Optional[str] 98 | 99 | def __init__(self, faceId, name=None, **_): 100 | super().__init__(faceId=faceId, name=name) 101 | 102 | def toString(self): 103 | return f"[Face::name={self.name}]" 104 | 105 | class Image(BaseMessageComponent): 106 | type: MessageComponentTypes = "Image" 107 | imageId: T.Optional[str] 108 | url: T.Optional[HttpUrl] = None 109 | 110 | @validator("imageId", always=True, pre=True) 111 | @classmethod 112 | def imageId_formater(cls, v): 113 | length = len(v) 114 | if length == 44: 115 | # group 116 | return v[1:-7] 117 | elif length == 37: 118 | return v[1:] 119 | else: 120 | return v 121 | 122 | def __init__(self, imageId, url=None, **_): 123 | super().__init__(imageId=imageId, url=url) 124 | 125 | def toString(self): 126 | return f"[Image::{self.imageId}]" 127 | 128 | def asGroupImage(self) -> str: 129 | return f"{{{self.imageId}}}.mirai" 130 | 131 | def asFriendImage(self) -> str: 132 | return self.imageId.upper() 133 | 134 | def asFlashImage(self) -> "FlashImage": 135 | return FlashImage(self.imageId, self.url) 136 | 137 | @staticmethod 138 | async def fromRemote(url, **extra) -> BytesImage: 139 | async with ClientSession() as session: 140 | async with session.get(url, **extra) as response: 141 | return BytesImage(await response.read()) 142 | 143 | @staticmethod 144 | def fromFileSystem(path: T.Union[Path, str]) -> LocalImage: 145 | return LocalImage(path) 146 | 147 | async def toBytes(self, chunk_size=256) -> BytesIO: 148 | async with ClientSession() as session: 149 | async with session.get(self.url) as response: 150 | result = BytesIO() 151 | while True: 152 | chunk = await response.content.read(chunk_size) 153 | if not chunk: 154 | break 155 | result.write(chunk) 156 | return result 157 | 158 | @staticmethod 159 | def fromBytes(data) -> BytesImage: 160 | return BytesImage(data) 161 | 162 | @staticmethod 163 | def fromBase64(base64_str) -> Base64Image: 164 | return Base64Image(base64_str) 165 | 166 | @staticmethod 167 | def fromIO(IO) -> IOImage: 168 | return IOImage(IO) 169 | 170 | class Xml(BaseMessageComponent): 171 | type: MessageComponentTypes = "Xml" 172 | XML: str 173 | 174 | def __init__(self, xml, type="Xml"): 175 | super().__init__(XML=xml) 176 | 177 | class Json(BaseMessageComponent): 178 | type: MessageComponentTypes = "Json" 179 | Json: dict = Field(..., alias="json") 180 | 181 | def __init__(self, json: dict, **_): 182 | super().__init__(Json=json) 183 | 184 | class App(BaseMessageComponent): 185 | type: MessageComponentTypes = "App" 186 | content: str 187 | 188 | def __init__(self, content: str, **_): 189 | super().__init__(content=content) 190 | 191 | class Poke(BaseMessageComponent): 192 | type: MessageComponentTypes = "Poke" 193 | name: str 194 | 195 | def __init__(self, name: str, **_): 196 | super().__init__(name=name) 197 | 198 | class Unknown(BaseMessageComponent): 199 | type: MessageComponentTypes = "Unknown" 200 | text: str 201 | 202 | def toString(self): 203 | return "" 204 | 205 | class FlashImage(BaseMessageComponent): 206 | type: MessageComponentTypes = "FlashImage" 207 | imageId: T.Optional[str] 208 | url: T.Optional[HttpUrl] = None 209 | 210 | @validator("imageId", always=True, pre=True) 211 | @classmethod 212 | def imageId_formater(cls, v): 213 | length = len(v) 214 | if length == 42: 215 | # group 216 | return v[1:-5] 217 | elif length == 37: 218 | return v[1:] 219 | else: 220 | return v 221 | 222 | def __init__(self, imageId, url=None, **_): 223 | super().__init__(imageId=imageId, url=url) 224 | 225 | def toString(self): 226 | return f"[FlashImage::{self.imageId}]" 227 | 228 | def asGroupImage(self) -> str: 229 | return f"{{{self.imageId.upper()}}}.mirai" 230 | 231 | def asFriendImage(self) -> str: 232 | return self.imageId.upper() 233 | 234 | def asNormal(self) -> Image: 235 | return Image(self.imageId, self.url) 236 | 237 | @staticmethod 238 | def fromFileSystem(path: T.Union[Path, str]) -> LocalImage: 239 | return LocalImage(path, flash=True) 240 | 241 | async def toBytes(self, chunk_size=256) -> BytesIO: 242 | async with ClientSession() as session: 243 | async with session.get(self.url) as response: 244 | result = BytesIO() 245 | while True: 246 | chunk = await response.content.read(chunk_size) 247 | if not chunk: 248 | break 249 | result.write(chunk) 250 | return result 251 | 252 | @staticmethod 253 | def fromBytes(data) -> BytesImage: 254 | return BytesImage(data, flash=True) 255 | 256 | @staticmethod 257 | def fromBase64(base64_str) -> Base64Image: 258 | return Base64Image(base64_str, flash=True) 259 | 260 | @staticmethod 261 | def fromIO(IO) -> IOImage: 262 | return IOImage(IO, flash=True) 263 | 264 | MessageComponents = { 265 | "At": At, 266 | "AtAll": AtAll, 267 | "Face": Face, 268 | "Plain": Plain, 269 | "Image": Image, 270 | "Source": Source, 271 | "Quote": Quote, 272 | "Xml": Xml, 273 | "Json": Json, 274 | "App": App, 275 | "Poke": Poke, 276 | "FlashImage": FlashImage, 277 | "Unknown": Unknown 278 | } 279 | -------------------------------------------------------------------------------- /mirai/event/message/models.py: -------------------------------------------------------------------------------- 1 | import typing as T 2 | from enum import Enum 3 | from .base import MessageComponentTypes 4 | from mirai.entities.friend import Friend 5 | from mirai.entities.group import Group, Member 6 | from pydantic import BaseModel 7 | from .chain import MessageChain 8 | 9 | class MessageItemType(Enum): 10 | FriendMessage = "FriendMessage" 11 | GroupMessage = "GroupMessage" 12 | TempMessage = "TempMessage" 13 | BotMessage = "BotMessage" 14 | 15 | class FriendMessage(BaseModel): 16 | type: MessageItemType = "FriendMessage" 17 | messageChain: T.Optional[MessageChain] 18 | sender: Friend 19 | 20 | def toString(self): 21 | if self.messageChain: 22 | return self.messageChain.toString() 23 | 24 | class GroupMessage(BaseModel): 25 | type: MessageItemType = "GroupMessage" 26 | messageChain: T.Optional[MessageChain] 27 | sender: Member 28 | 29 | def toString(self): 30 | if self.messageChain: 31 | return self.messageChain.toString() 32 | 33 | class TempMessage(BaseModel): 34 | type: MessageItemType = "TempMessage" 35 | messageChain: T.Optional[MessageChain] 36 | sender: Member 37 | 38 | def toString(self): 39 | if self.messageChain: 40 | return self.messageChain.toString() 41 | 42 | class BotMessage(BaseModel): 43 | type: MessageItemType = 'BotMessage' 44 | messageId: int 45 | 46 | MessageTypes = { 47 | "GroupMessage": GroupMessage, 48 | "FriendMessage": FriendMessage, 49 | "TempMessage": TempMessage 50 | } -------------------------------------------------------------------------------- /mirai/exceptions.py: -------------------------------------------------------------------------------- 1 | class NetworkError(Exception): 2 | pass 3 | 4 | class Cancelled(Exception): 5 | pass 6 | 7 | class UnknownTarget(Exception): 8 | pass 9 | 10 | class UnknownEvent(Exception): 11 | pass 12 | 13 | class LoginException(Exception): 14 | "你忘记在mirai-console登录就这种错误." 15 | pass 16 | 17 | class AuthenticateError(Exception): 18 | pass 19 | 20 | class InvaildSession(Exception): 21 | pass 22 | 23 | class ValidatedSession(Exception): 24 | "一般来讲 这种情况出现时需要刷新session" 25 | 26 | class UnknownReceiverTarget(Exception): 27 | pass 28 | 29 | class CallDevelopers(Exception): 30 | '还愣着干啥?开ISSUE啊!' 31 | pass 32 | 33 | class NonEnabledError(Exception): 34 | pass 35 | 36 | class BotMutedError(Exception): 37 | pass 38 | 39 | class TooLargeMessageError(Exception): 40 | pass -------------------------------------------------------------------------------- /mirai/face.py: -------------------------------------------------------------------------------- 1 | QQFaces = { 2 | "unknown": 0xff, 3 | "jingya": 0, 4 | "piezui": 1, 5 | "se": 2, 6 | "fadai": 3, 7 | "deyi": 4, 8 | "liulei": 5, 9 | "haixiu": 6, 10 | "bizui": 7, 11 | "shui": 8, 12 | "daku": 9, 13 | "ganga": 10, 14 | "fanu": 11, 15 | "tiaopi": 12, 16 | "ciya": 13, 17 | "weixiao": 14, 18 | "nanguo": 15, 19 | "ku": 16, 20 | "zhuakuang": 18, 21 | "tu": 19, 22 | "touxiao": 20, 23 | "keai": 21, 24 | "baiyan": 22, 25 | "aoman": 23, 26 | "ji_e": 24, 27 | "kun": 25, 28 | "jingkong": 26, 29 | "liuhan": 27, 30 | "hanxiao": 28, 31 | "dabing": 29, 32 | "fendou": 30, 33 | "zhouma": 31, 34 | "yiwen": 32, 35 | "yun": 34, 36 | "zhemo": 35, 37 | "shuai": 36, 38 | "kulou": 37, 39 | "qiaoda": 38, 40 | "zaijian": 39, 41 | "fadou": 41, 42 | "aiqing": 42, 43 | "tiaotiao": 43, 44 | "zhutou": 46, 45 | "yongbao": 49, 46 | "dan_gao": 53, 47 | "shandian": 54, 48 | "zhadan": 55, 49 | "dao": 56, 50 | "zuqiu": 57, 51 | "bianbian": 59, 52 | "kafei": 60, 53 | "fan": 61, 54 | "meigui": 63, 55 | "diaoxie": 64, 56 | "aixin": 66, 57 | "xinsui": 67, 58 | "liwu": 69, 59 | "taiyang": 74, 60 | "yueliang": 75, 61 | "qiang": 76, 62 | "ruo": 77, 63 | "woshou": 78, 64 | "shengli": 79, 65 | "feiwen": 85, 66 | "naohuo": 86, 67 | "xigua": 89, 68 | "lenghan": 96, 69 | "cahan": 97, 70 | "koubi": 98, 71 | "guzhang": 99, 72 | "qiudale": 100, 73 | "huaixiao": 101, 74 | "zuohengheng": 102, 75 | "youhengheng": 103, 76 | "haqian": 104, 77 | "bishi": 105, 78 | "weiqu": 106, 79 | "kuaikule": 107, 80 | "yinxian": 108, 81 | "qinqin": 109, 82 | "xia": 110, 83 | "kelian": 111, 84 | "caidao": 112, 85 | "pijiu": 113, 86 | "lanqiu": 114, 87 | "pingpang": 115, 88 | "shiai": 116, 89 | "piaochong": 117, 90 | "baoquan": 118, 91 | "gouyin": 119, 92 | "quantou": 120, 93 | "chajin": 121, 94 | "aini": 122, 95 | "bu": 123, 96 | "hao": 124, 97 | "zhuanquan": 125, 98 | "ketou": 126, 99 | "huitou": 127, 100 | "tiaosheng": 128, 101 | "huishou": 129, 102 | "jidong": 130, 103 | "jiewu": 131, 104 | "xianwen": 132, 105 | "zuotaiji": 133, 106 | "youtaiji": 134, 107 | "shuangxi": 136, 108 | "bianpao": 137, 109 | "denglong": 138, 110 | "facai": 139, 111 | "K_ge": 140, 112 | "gouwu": 141, 113 | "youjian": 142, 114 | "shuai_qi": 143, 115 | "hecai": 144, 116 | "qidao": 145, 117 | "baojin": 146, 118 | "bangbangtang": 147, 119 | "he_nai": 148, 120 | "xiamian": 149, 121 | "xiangjiao": 150, 122 | "feiji": 151, 123 | "kaiche": 152, 124 | "gaotiezuochetou": 153, 125 | "chexiang": 154, 126 | "gaotieyouchetou": 155, 127 | "duoyun": 156, 128 | "xiayu": 157, 129 | "chaopiao": 158, 130 | "xiongmao": 159, 131 | "dengpao": 160, 132 | "fengche": 161, 133 | "naozhong": 162, 134 | "dasan": 163, 135 | "caiqiu": 164, 136 | "zuanjie": 165, 137 | "shafa": 166, 138 | "zhijin": 167, 139 | "yao": 168, 140 | "shouqiang": 169, 141 | "qingwa": 170, 142 | "cha": 171, 143 | "zhayan": 172, 144 | "leibeng": 173, 145 | "wunai": 174, 146 | "maimeng": 175, 147 | "xiaojiujie": 176, 148 | "penxue": 177, 149 | "xieyanxiao": 178, 150 | "dog": 179, 151 | "jinxi": 180, 152 | "saorao": 181, 153 | "xiaoku": 182, 154 | "wozuimei": 183, 155 | "hexie": 184, 156 | "yangtuo": 185, 157 | "banli": 186, 158 | "youling": 187, 159 | "dan": 188, 160 | "mofang": 189, 161 | "juhua": 190, 162 | "feizao": 191, 163 | "hongbao": 192, 164 | "daxiao": 193, 165 | "bukaixin": 194, 166 | "zhenjing": 195, 167 | "ganga": 196, 168 | "lenmo": 197, 169 | "ye": 198, 170 | "haobang": 199, 171 | "baituo": 200, 172 | "dianzan": 201, 173 | "wuliao": 202, 174 | "tuolian": 203, 175 | "chi": 204, 176 | "songhua": 205, 177 | "haipa": 206, 178 | "huachi": 207, 179 | "xiaoyang": 208, 180 | "unknown2": 209,#暂时不知道 181 | "biaolei": 210, 182 | "wobukan": 211, 183 | "tuosai": 212, 184 | "unknown3": 213,#暂时不知道 185 | #214-247表情在电脑版qq9.2.3无法显示 186 | "bobo": 214, 187 | "hulian": 215, 188 | "paitou": 216, 189 | "cheyiche": 217, 190 | "tianyitian": 218, 191 | "cengyiceng": 219, 192 | "zhaozhatian": 220, 193 | "dingguagua": 221, 194 | "baobao": 222, 195 | "baoji": 223, 196 | "kaiqiang": 224, 197 | "liaoyiliao": 225, 198 | "paizhuo": 226, 199 | "paishou": 227, 200 | "gongxi": 228, 201 | "ganbei": 229, 202 | "chaofeng": 230, 203 | "hen": 231, 204 | "foxi": 232, 205 | "jingdai": 234, 206 | "chandou": 235, 207 | "jiaotou": 236, 208 | "toukan": 237, 209 | "shanlian": 238, 210 | "yuanliang": 239, 211 | "penlian": 240, 212 | "shengrikuaile": 241, 213 | "touzhuangji": 242, 214 | "shuaitou": 243, 215 | "renggou": 244, 216 | "jiayoubisheng": 245, 217 | "jiayoubaobao": 246, 218 | "kouzhaohuti": 247, 219 | #248-255未定义 220 | "jinya": 256, 221 | "piezei": 257, 222 | "se": 258, 223 | "fadai": 259, 224 | "deyi": 260, 225 | "liulei": 261, 226 | "haixiu": 262, 227 | "bizui": 263, 228 | "shui": 264, 229 | "daku": 265, 230 | "ganga": 266, 231 | "falu": 267, 232 | "tiaopi": 268, 233 | "ziya": 269, 234 | "weixiao": 270, 235 | "nanguo": 271, 236 | "ku": 272, 237 | "unknown4": 273,#暂时不知道,qq安卓版本8.2.8.4440不显示 238 | "zhuakuang": 274, 239 | "tu": 275, 240 | "touxiao": 276, 241 | "keai": 277, 242 | "baiyan": 278, 243 | "aoman": 279, 244 | "jie": 280, 245 | "kun": 281, 246 | "jingkong": 282, 247 | "liuhan": 283, 248 | "hanxiao": 284, 249 | "dabing": 285, 250 | "fendou": 286, 251 | "zhouma": 287, 252 | "yiwen": 288, 253 | "xu": 289, 254 | "yun": 290, 255 | "zhemo": 291, 256 | "shuai": 292, 257 | "kulou": 293, 258 | "qiaoda": 294, 259 | "zaijian": 295, 260 | "unknown5": 296,#安卓版本无显示 261 | "dadou": 297, 262 | "aiqing": 298, 263 | "tiaotiao": 299, 264 | "unknown6": 300,#暂时不知道 265 | "unknown7": 301,#暂时不知道 266 | "zhutou": 302, 267 | "mao": 303, 268 | "unknown8": 304,#暂时不知道 269 | "baobao": 305, 270 | "meiyuanfuhao": 306, 271 | "dengpao": 307,#安卓版本不显示 272 | "gaijiaobei": 308,#安卓版本不显示 273 | "dangao": 309, 274 | "shandian": 310, 275 | "zhadan": 311, 276 | "shiai": 321, 277 | "aixin": 322, 278 | "xinsui": 323, 279 | "zhuozi": 324,#安卓qq不显示 280 | "liwu": 325, 281 | } -------------------------------------------------------------------------------- /mirai/image.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from abc import ABCMeta, abstractmethod 3 | import base64 4 | 5 | class InternalImage(metaclass=ABCMeta): 6 | @abstractmethod 7 | def __init__(self): 8 | super().__init__() 9 | 10 | @abstractmethod 11 | def render(self) -> bytes: 12 | pass 13 | 14 | class LocalImage(InternalImage): 15 | path: Path 16 | flash: bool = False 17 | 18 | def __init__(self, path, flash: bool = False): 19 | if isinstance(path, str): 20 | self.path = Path(path) 21 | elif isinstance(path, Path): 22 | self.path = path 23 | self.flash = flash 24 | 25 | def render(self) -> bytes: 26 | return self.path.read_bytes() 27 | 28 | class IOImage(InternalImage): 29 | def __init__(self, IO, flash: bool = False): 30 | """make a object with 'read' method a image. 31 | 32 | IO - a object, must has a `read` method to return bytes. 33 | """ 34 | self.IO = IO 35 | self.flash = flash 36 | 37 | def render(self) -> bytes: 38 | return self.IO.getvalue() 39 | 40 | class BytesImage(InternalImage): 41 | def __init__(self, data: bytes, flash: bool = False): 42 | self.data = data 43 | self.flash = flash 44 | 45 | def render(self) -> bytes: 46 | return self.data 47 | 48 | class Base64Image(InternalImage): 49 | def __init__(self, base64_str, flash: bool = False): 50 | self.base64_str = base64_str 51 | self.flash = flash 52 | 53 | def render(self) -> bytes: 54 | return base64.b64decode(self.base64_str) 55 | -------------------------------------------------------------------------------- /mirai/logger.py: -------------------------------------------------------------------------------- 1 | from logbook import Logger, StreamHandler 2 | from logbook import ( 3 | INFO, 4 | DEBUG 5 | ) 6 | import os 7 | import sys 8 | 9 | stream_handler = StreamHandler(sys.stdout, level=INFO if not os.environ.get("MIRAI_DEBUG") else DEBUG) 10 | stream_handler.format_string = '[{record.time:%Y-%m-%d %H:%M:%S}][Mirai] {record.level_name}: {record.channel}: {record.message}' 11 | stream_handler.push_application() 12 | 13 | Event = Logger('Event', level=INFO) 14 | Network = Logger("Network", level=DEBUG) 15 | Session = Logger("Session", level=INFO) 16 | Protocol = Logger("Protocol", level=INFO) -------------------------------------------------------------------------------- /mirai/misc.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import inspect 3 | import os 4 | import random 5 | import re 6 | import traceback 7 | import typing as T 8 | from collections import namedtuple 9 | from enum import Enum 10 | 11 | import aiohttp 12 | 13 | from . import exceptions 14 | from .logger import Protocol 15 | 16 | 17 | def assertOperatorSuccess(result, raise_exception=False, return_as_is=False): 18 | if not result: 19 | if raise_exception: 20 | raise exceptions.InvaildSession("this method returned None, as sessionkey invaild...") 21 | else: 22 | return None 23 | if "code" in result: 24 | if not raise_exception: 25 | return result['code'] == 0 26 | else: 27 | if result['code'] != 0: 28 | print(result) 29 | raise { 30 | 1: exceptions.AuthenticateError, # 这种情况需要检查Authkey, 可能还是连错了. 31 | 2: exceptions.LoginException, # 嗯...你是不是忘记在mirai-console登录了?...算了 自动重连. 32 | 3: exceptions.InvaildSession, # 这种情况会自动重连. 33 | 4: exceptions.ValidatedSession, # 啊 smjb错误... 也会自动重连 34 | 5: exceptions.UnknownReceiverTarget, # 业务代码错误. 35 | 10: PermissionError, # 一般业务代码错误, 自行亦会 36 | 20: exceptions.BotMutedError, # 机器人被禁言 37 | 30: exceptions.TooLargeMessageError, 38 | 400: exceptions.CallDevelopers # 发生这个错误...你就给我提个ISSUE 39 | }[result['code']](f"""invaild stdin: { { 40 | 1: "wrong auth key", 41 | 2: "unknown qq account", 42 | 3: "invaild session key", 43 | 4: "disabled session key", 44 | 5: "unknown receiver target", 45 | 10: "permission denied", 46 | 20: "bot account has been muted", 47 | 30: "mirai backend cannot deal with so large message", 48 | 400: "wrong arguments" 49 | }[result['code']] }""") 50 | else: 51 | if return_as_is: 52 | return result 53 | else: 54 | return True 55 | if return_as_is: 56 | return result 57 | return False 58 | 59 | class ImageType(Enum): 60 | Friend = "friend" 61 | Group = "group" 62 | 63 | Parameter = namedtuple("Parameter", ["name", "annotation", "default"]) 64 | 65 | TRACEBACKED = os.urandom(32) 66 | 67 | ImageRegex = { 68 | "group": r"({(?<=\{)([0-9A-Z]{8})\-([0-9A-Z]{4})-([0-9A-Z]{4})-([0-9A-Z]{4})-([0-9A-Z]{12})(?=\})}\..{5}", 69 | "friend": r"(?<=/)([0-9a-z]{8})\-([0-9a-z]{4})-([0-9a-z]{4})-([0-9a-z]{4})-([0-9a-z]{12})" 70 | } 71 | 72 | _windows_device_files = ( 73 | "CON", 74 | "AUX", 75 | "COM1", 76 | "COM2", 77 | "COM3", 78 | "COM4", 79 | "LPT1", 80 | "LPT2", 81 | "LPT3", 82 | "PRN", 83 | "NUL", 84 | ) 85 | _filename_ascii_strip_re = re.compile(r"[^A-Za-z0-9_.-]") 86 | 87 | def getMatchedString(regex_result): 88 | if regex_result: 89 | return regex_result.string[slice(*regex_result.span())] 90 | 91 | def findKey(mapping, value): 92 | try: 93 | index = list(mapping.values()).index(value) 94 | except ValueError: 95 | return "Unknown" 96 | return list(mapping.keys())[index] 97 | 98 | def raiser(error): 99 | raise error 100 | 101 | def printer(val): 102 | print(val) 103 | return val 104 | 105 | def justdo(call, val): 106 | print(call()) 107 | return val 108 | 109 | def randomNumberString(): 110 | return str(random.choice(range(100000000, 9999999999))) 111 | 112 | def randomRangedNumberString(length_range=(9,)): 113 | length = random.choice(length_range) 114 | return random.choice(range(10**(length - 1), int("9"*(length)))) 115 | 116 | def protocol_log(func): 117 | async def wrapper(*args, **kwargs): 118 | try: 119 | result = await func(*args, **kwargs) 120 | Protocol.info(f"protocol method {func.__name__} was called") 121 | return result 122 | except Exception as e: 123 | Protocol.error(f"protocol method {func.__name__} raised a error: {e.__class__.__name__}") 124 | raise e 125 | return wrapper 126 | 127 | def secure_filename(filename): 128 | if isinstance(filename, str): 129 | from unicodedata import normalize 130 | 131 | filename = normalize("NFKD", filename).encode("ascii", "ignore") 132 | filename = filename.decode("ascii") 133 | 134 | for sep in os.path.sep, os.path.altsep: 135 | if sep: 136 | filename = filename.replace(sep, " ") 137 | 138 | filename = \ 139 | str(_filename_ascii_strip_re.sub("", "_".join(filename.split()))).strip("._") 140 | 141 | if ( 142 | os.name == "nt" and filename and \ 143 | filename.split(".")[0].upper() in _windows_device_files 144 | ): 145 | filename = "_" + filename 146 | 147 | return filename 148 | 149 | def edge_case_handler(func): 150 | async def wrapper(self, *args, **kwargs): 151 | retry_times = 0 152 | while retry_times <= 5: 153 | retry_times += 1 154 | try: 155 | return await func(self, *args, **kwargs) 156 | except exceptions.AuthenticateError: 157 | Protocol.error("invaild authkey, please check your input.") 158 | exit(1) 159 | except exceptions.LoginException: 160 | Protocol.error("there is not such qq in headless client, we will try again after 5 seconds.") 161 | await asyncio.sleep(5) 162 | if func.__name__ != "verify": 163 | await self.verify() 164 | continue 165 | except (exceptions.InvaildSession, exceptions.ValidatedSession): 166 | Protocol.error("a unexpected session error, we will deal with it.") 167 | await self.enable_session() 168 | except aiohttp.client_exceptions.ClientError: 169 | Protocol.error(f"cannot connect to the headless client, will retry after 5 seconds.") 170 | await asyncio.sleep(5) 171 | continue 172 | except exceptions.CallDevelopers: 173 | Protocol.error("emmm, please contect me at github.") 174 | exit(-1) 175 | except: 176 | raise 177 | else: 178 | Protocol.error("we retried many times, but it doesn't send a success message to us...") 179 | wrapper.__name__ = func.__name__ 180 | return wrapper 181 | 182 | def throw_error_if_not_enable(func): 183 | def wrapper(self, *args, **kwargs): 184 | if not self.enabled: 185 | raise exceptions.NonEnabledError( 186 | f"you mustn't use any methods in MiraiProtocol...,\ 187 | if you want to access '{func.__name__}' before `app.run()`\ 188 | , use 'Subroutine'." 189 | ) 190 | return func(self, *args, **kwargs) 191 | wrapper.__name__ = func.__name__ 192 | wrapper.__annotations__ = func.__annotations__ 193 | return wrapper 194 | 195 | def if_error_print_arg(func): 196 | def wrapper(*args, **kwargs): 197 | try: 198 | return func(*args, **kwargs) 199 | except: 200 | print(args, kwargs) 201 | traceback.print_exc() 202 | return wrapper 203 | 204 | def argument_signature(callable_target) -> T.List[Parameter]: 205 | return [ 206 | Parameter( 207 | name=name, 208 | annotation=param.annotation if param.annotation != inspect._empty else None, 209 | default=param.default if param.default != inspect._empty else None 210 | ) 211 | for name, param in dict(inspect.signature(callable_target).parameters).items() 212 | ] 213 | -------------------------------------------------------------------------------- /mirai/network.py: -------------------------------------------------------------------------------- 1 | import json 2 | import mimetypes 3 | import typing as T 4 | from pathlib import Path 5 | from .logger import Network 6 | 7 | import aiohttp 8 | 9 | from mirai.exceptions import NetworkError 10 | 11 | class fetch: 12 | @staticmethod 13 | async def http_post(url, data_map): 14 | async with aiohttp.ClientSession() as session: 15 | async with session.post(url, json=data_map) as response: 16 | data = await response.text(encoding="utf-8") 17 | Network.debug(f"requested url={url}, by data_map={data_map}, and status={response.status}, data={data}") 18 | response.raise_for_status() 19 | try: 20 | return json.loads(data) 21 | except json.decoder.JSONDecodeError: 22 | Network.error(f"requested {url} with {data_map}, responsed {data}, decode failed...") 23 | 24 | @staticmethod 25 | async def http_get(url, params=None): 26 | async with aiohttp.ClientSession() as session: 27 | async with session.get(url, params=params) as response: 28 | response.raise_for_status() 29 | data = await response.text(encoding="utf-8") 30 | Network.debug(f"requested url={url}, by params={params}, and status={response.status}, data={data}") 31 | try: 32 | return json.loads(data) 33 | except json.decoder.JSONDecodeError: 34 | Network.error(f"requested {url} with {params}, responsed {data}, decode failed...") 35 | 36 | @staticmethod 37 | async def upload(url, filedata: bytes, addon_dict: dict): 38 | upload_data = aiohttp.FormData() 39 | upload_data.add_field("img", filedata) 40 | for item in addon_dict.items(): 41 | upload_data.add_fields(item) 42 | 43 | async with aiohttp.ClientSession() as session: 44 | async with session.post(url, data=upload_data) as response: 45 | response.raise_for_status() 46 | Network.debug(f"requested url={url}, and status={response.status}, addon_dict={addon_dict}") 47 | return await response.text("utf-8") -------------------------------------------------------------------------------- /mirai/protocol.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | import threading 4 | import traceback 5 | import typing as T 6 | from datetime import timedelta 7 | from pathlib import Path 8 | from uuid import UUID 9 | 10 | import pydantic 11 | 12 | from mirai.entities.friend import Friend 13 | from mirai.entities.group import (Group, GroupSetting, Member, 14 | MemberChangeableSetting) 15 | from mirai.event import ExternalEvent 16 | from mirai.event import external as eem 17 | from mirai.event.enums import ( 18 | NewFriendRequestResponseOperate, 19 | MemberJoinRequestResponseOperate 20 | ) 21 | from mirai.event.message import components 22 | from mirai.event.message.base import BaseMessageComponent 23 | from mirai.event.message.chain import MessageChain 24 | from mirai.event.message.models import (BotMessage, FriendMessage, 25 | GroupMessage, MessageTypes) 26 | from mirai.image import InternalImage 27 | from mirai.logger import Protocol as ProtocolLogger 28 | from mirai.misc import (ImageRegex, ImageType, assertOperatorSuccess, 29 | edge_case_handler, getMatchedString, printer, 30 | protocol_log, raiser, throw_error_if_not_enable) 31 | from mirai.network import fetch 32 | 33 | # 与 mirai 的 Command 部分将由 mirai.command 模块进行魔法支持, 34 | # 并尽量的兼容 mirai-console 的内部机制. 35 | 36 | class MiraiProtocol: 37 | qq: int 38 | baseurl: str 39 | session_key: str 40 | auth_key: str 41 | 42 | @protocol_log 43 | @edge_case_handler 44 | async def auth(self): 45 | return assertOperatorSuccess( 46 | await fetch.http_post(f"{self.baseurl}/auth", { 47 | "authKey": self.auth_key 48 | } 49 | ), raise_exception=True, return_as_is=True) 50 | 51 | @protocol_log 52 | @edge_case_handler 53 | async def verify(self): 54 | return assertOperatorSuccess( 55 | await fetch.http_post(f"{self.baseurl}/verify", { 56 | "sessionKey": self.session_key, 57 | "qq": self.qq 58 | } 59 | ), raise_exception=True, return_as_is=True) 60 | 61 | @throw_error_if_not_enable 62 | @protocol_log 63 | @edge_case_handler 64 | async def release(self): 65 | return assertOperatorSuccess( 66 | await fetch.http_post(f"{self.baseurl}/release", { 67 | "sessionKey": self.session_key, 68 | "qq": self.qq 69 | } 70 | ), raise_exception=True) 71 | 72 | @throw_error_if_not_enable 73 | @edge_case_handler 74 | async def getConfig(self) -> dict: 75 | return assertOperatorSuccess( 76 | await fetch.http_get(f"{self.baseurl}/config", { 77 | "sessionKey": self.session_key 78 | } 79 | ), raise_exception=True, return_as_is=True) 80 | 81 | @throw_error_if_not_enable 82 | @edge_case_handler 83 | async def setConfig(self, 84 | cacheSize=None, 85 | enableWebsocket=None 86 | ): 87 | return assertOperatorSuccess( 88 | await fetch.http_post(f"{self.baseurl}/config", { 89 | "sessionKey": self.session_key, 90 | **({ 91 | "cacheSize": cacheSize 92 | } if cacheSize else {}), 93 | **({ 94 | "enableWebsocket": enableWebsocket 95 | } if enableWebsocket else {}) 96 | } 97 | ), raise_exception=True, return_as_is=True) 98 | 99 | @throw_error_if_not_enable 100 | @protocol_log 101 | @edge_case_handler 102 | async def sendFriendMessage(self, 103 | friend: T.Union[Friend, int], 104 | message: T.Union[ 105 | MessageChain, 106 | BaseMessageComponent, 107 | T.List[T.Union[BaseMessageComponent, InternalImage]], 108 | str 109 | ] 110 | ) -> BotMessage: 111 | return BotMessage.parse_obj(assertOperatorSuccess( 112 | await fetch.http_post(f"{self.baseurl}/sendFriendMessage", { 113 | "sessionKey": self.session_key, 114 | "target": self.handleTargetAsFriend(friend), 115 | "messageChain": await self.handleMessageAsFriend(message) 116 | } 117 | ), raise_exception=True, return_as_is=True)) 118 | 119 | @throw_error_if_not_enable 120 | @protocol_log 121 | @edge_case_handler 122 | async def sendGroupMessage(self, 123 | group: T.Union[Group, int], 124 | message: T.Union[ 125 | MessageChain, 126 | BaseMessageComponent, 127 | T.List[T.Union[BaseMessageComponent, InternalImage]], 128 | str 129 | ], 130 | quoteSource: T.Union[int, components.Source]=None 131 | ) -> BotMessage: 132 | return BotMessage.parse_obj(assertOperatorSuccess( 133 | await fetch.http_post(f"{self.baseurl}/sendGroupMessage", { 134 | "sessionKey": self.session_key, 135 | "target": self.handleTargetAsGroup(group), 136 | "messageChain": await self.handleMessageAsGroup(message), 137 | **({"quote": quoteSource.id \ 138 | if isinstance(quoteSource, components.Source) else quoteSource}\ 139 | if quoteSource else {}) 140 | } 141 | ), raise_exception=True, return_as_is=True)) 142 | 143 | @throw_error_if_not_enable 144 | @protocol_log 145 | @edge_case_handler 146 | async def sendTempMessage(self, 147 | group: T.Union[Group, int], 148 | member: T.Union[Member, int], 149 | message: T.Union[ 150 | MessageChain, 151 | BaseMessageComponent, 152 | T.List[T.Union[BaseMessageComponent, InternalImage]], 153 | str 154 | ], 155 | quoteSource: T.Union[int, components.Source]=None 156 | ) -> BotMessage: 157 | return BotMessage.parse_obj(assertOperatorSuccess( 158 | await fetch.http_post(f"{self.baseurl}/sendTempMessage", { 159 | "sessionKey": self.session_key, 160 | "qq": (member.id if isinstance(member, Member) else member), 161 | "group": (group.id if isinstance(group, Group) else group), 162 | "messageChain": await self.handleMessageForTempMessage(message), 163 | **({"quote": quoteSource.id \ 164 | if isinstance(quoteSource, components.Source) else quoteSource}\ 165 | if quoteSource else {}) 166 | } 167 | ), raise_exception=True, return_as_is=True)) 168 | 169 | @throw_error_if_not_enable 170 | @protocol_log 171 | @edge_case_handler 172 | async def revokeMessage(self, source: T.Union[components.Source, BotMessage, int]): 173 | return assertOperatorSuccess(await fetch.http_post(f"{self.baseurl}/recall", { 174 | "sessionKey": self.session_key, 175 | "target": source if isinstance(source, int) else source.id \ 176 | if isinstance(source, components.Source) else source.messageId\ 177 | if isinstance(source, BotMessage) else\ 178 | raiser(TypeError("invaild message source")) 179 | }), raise_exception=True) 180 | 181 | @throw_error_if_not_enable 182 | @protocol_log 183 | @edge_case_handler 184 | async def groupList(self) -> T.List[Group]: 185 | return [Group.parse_obj(group_info) \ 186 | for group_info in await fetch.http_get(f"{self.baseurl}/groupList", { 187 | "sessionKey": self.session_key 188 | }) 189 | ] 190 | 191 | @throw_error_if_not_enable 192 | @protocol_log 193 | @edge_case_handler 194 | async def friendList(self) -> T.List[Friend]: 195 | return [Friend.parse_obj(friend_info) \ 196 | for friend_info in await fetch.http_get(f"{self.baseurl}/friendList", { 197 | "sessionKey": self.session_key 198 | }) 199 | ] 200 | 201 | @throw_error_if_not_enable 202 | @protocol_log 203 | @edge_case_handler 204 | async def memberList(self, target: int) -> T.List[Member]: 205 | return [Member.parse_obj(member_info) \ 206 | for member_info in await fetch.http_get(f"{self.baseurl}/memberList", { 207 | "sessionKey": self.session_key, 208 | "target": target 209 | }) 210 | ] 211 | 212 | @throw_error_if_not_enable 213 | @protocol_log 214 | @edge_case_handler 215 | async def groupMemberNumber(self, target: int) -> int: 216 | return len(await self.memberList(target)) + 1 217 | 218 | @throw_error_if_not_enable 219 | @protocol_log 220 | @edge_case_handler 221 | async def uploadImage(self, type: T.Union[str, ImageType], image: InternalImage): 222 | post_result = json.loads(await fetch.upload(f"{self.baseurl}/uploadImage", image.render(), { 223 | "sessionKey": self.session_key, 224 | "type": type if isinstance(type, str) else type.value 225 | })) 226 | return components.Image(**post_result) 227 | 228 | @protocol_log 229 | @edge_case_handler 230 | async def sendCommand(self, command, *args): 231 | return assertOperatorSuccess(await fetch.http_post(f"{self.baseurl}/command/send", { 232 | "authKey": self.auth_key, 233 | "name": command, 234 | "args": args 235 | }), raise_exception=True, return_as_is=True) 236 | 237 | @throw_error_if_not_enable 238 | @edge_case_handler 239 | async def fetchMessage(self, count: int) -> T.List[T.Union[FriendMessage, GroupMessage, ExternalEvent]]: 240 | from mirai.event.external.enums import ExternalEvents 241 | result = assertOperatorSuccess( 242 | await fetch.http_get(f"{self.baseurl}/fetchMessage", { 243 | "sessionKey": self.session_key, 244 | "count": count 245 | } 246 | ), raise_exception=True, return_as_is=True)['data'] 247 | # 因为重新生成一个开销太大, 所以就直接在原数据内进行遍历替换 248 | try: 249 | for index in range(len(result)): 250 | # 判断当前项是否为 Message 251 | if result[index]['type'] in MessageTypes: 252 | if 'messageChain' in result[index]: 253 | result[index]['messageChain'] = MessageChain.parse_obj(result[index]['messageChain']) 254 | 255 | result[index] = \ 256 | MessageTypes[result[index]['type']].parse_obj(result[index]) 257 | 258 | elif hasattr(ExternalEvents, result[index]['type']): 259 | # 判断当前项为 Event 260 | result[index] = \ 261 | ExternalEvents[result[index]['type']].value.parse_obj(result[index]) 262 | except pydantic.ValidationError: 263 | ProtocolLogger.error(f"parse failed: {result}") 264 | traceback.print_exc() 265 | raise 266 | return result 267 | 268 | @protocol_log 269 | @edge_case_handler 270 | async def getManagers(self): 271 | return assertOperatorSuccess(await fetch.http_get(f"{self.baseurl}/managers")) 272 | 273 | @throw_error_if_not_enable 274 | @protocol_log 275 | @edge_case_handler 276 | async def messageFromId(self, sourceId: T.Union[components.Source, components.Quote, int]): 277 | if isinstance(sourceId, (components.Source, components.Quote)): 278 | sourceId = sourceId.id 279 | 280 | result = assertOperatorSuccess(await fetch.http_get(f"{self.baseurl}/messageFromId", { 281 | "sessionKey": self.session_key, 282 | "id": sourceId 283 | }), raise_exception=True, return_as_is=True) 284 | 285 | if result['type'] in MessageTypes: 286 | if "messageChain" in result: 287 | result['messageChain'] = MessageChain.custom_parse(result['messageChain']) 288 | 289 | return MessageTypes[result['type']].parse_obj(result) 290 | else: 291 | raise TypeError(f"unknown message, not found type.") 292 | 293 | @throw_error_if_not_enable 294 | @protocol_log 295 | @edge_case_handler 296 | async def muteAll(self, group: T.Union[Group, int]) -> bool: 297 | return assertOperatorSuccess( 298 | await fetch.http_post(f"{self.baseurl}/muteAll", { 299 | "sessionKey": self.session_key, 300 | "target": self.handleTargetAsGroup(group) 301 | } 302 | ), raise_exception=True) 303 | 304 | @throw_error_if_not_enable 305 | @protocol_log 306 | @edge_case_handler 307 | async def unmuteAll(self, group: T.Union[Group, int]) -> bool: 308 | return assertOperatorSuccess( 309 | await fetch.http_post(f"{self.baseurl}/unmuteAll", { 310 | "sessionKey": self.session_key, 311 | "target": self.handleTargetAsGroup(group) 312 | } 313 | ), raise_exception=True) 314 | 315 | @throw_error_if_not_enable 316 | @protocol_log 317 | @edge_case_handler 318 | async def memberInfo(self, 319 | group: T.Union[Group, int], 320 | member: T.Union[Member, int] 321 | ): 322 | return MemberChangeableSetting.parse_obj(assertOperatorSuccess( 323 | await fetch.http_get(f"{self.baseurl}/memberInfo", { 324 | "sessionKey": self.session_key, 325 | "target": self.handleTargetAsGroup(group), 326 | "memberId": self.handleTargetAsMember(member) 327 | } 328 | ), raise_exception=True, return_as_is=True)) 329 | 330 | @throw_error_if_not_enable 331 | @protocol_log 332 | @edge_case_handler 333 | async def botMemberInfo(self, 334 | group: T.Union[Group, int] 335 | ): 336 | return await self.memberInfo(group, self.qq) 337 | 338 | @throw_error_if_not_enable 339 | @protocol_log 340 | @edge_case_handler 341 | async def changeMemberInfo(self, 342 | group: T.Union[Group, int], 343 | member: T.Union[Member, int], 344 | setting: MemberChangeableSetting 345 | ) -> bool: 346 | return assertOperatorSuccess( 347 | await fetch.http_post(f"{self.baseurl}/memberInfo", { 348 | "sessionKey": self.session_key, 349 | "target": self.handleTargetAsGroup(group), 350 | "memberId": self.handleTargetAsMember(member), 351 | "info": json.loads(setting.json()) 352 | } 353 | ), raise_exception=True) 354 | 355 | @throw_error_if_not_enable 356 | @protocol_log 357 | @edge_case_handler 358 | async def groupConfig(self, group: T.Union[Group, int]) -> GroupSetting: 359 | return GroupSetting.parse_obj( 360 | await fetch.http_get(f"{self.baseurl}/groupConfig", { 361 | "sessionKey": self.session_key, 362 | "target": self.handleTargetAsGroup(group) 363 | }) 364 | ) 365 | 366 | @throw_error_if_not_enable 367 | @protocol_log 368 | @edge_case_handler 369 | async def changeGroupConfig(self, 370 | group: T.Union[Group, int], 371 | config: GroupSetting 372 | ) -> bool: 373 | return assertOperatorSuccess( 374 | await fetch.http_post(f"{self.baseurl}/groupConfig", { 375 | "sessionKey": self.session_key, 376 | "target": self.handleTargetAsGroup(group), 377 | "config": json.loads(config.json()) 378 | } 379 | ), raise_exception=True) 380 | 381 | @throw_error_if_not_enable 382 | @protocol_log 383 | @edge_case_handler 384 | async def mute(self, 385 | group: T.Union[Group, int], 386 | member: T.Union[Member, int], 387 | time: T.Union[timedelta, int] 388 | ): 389 | if isinstance(time, timedelta): 390 | time = int(time.total_seconds()) 391 | time = min(86400 * 30, max(60, time)) 392 | return assertOperatorSuccess( 393 | await fetch.http_post(f"{self.baseurl}/mute", { 394 | "sessionKey": self.session_key, 395 | "target": self.handleTargetAsGroup(group), 396 | "memberId": self.handleTargetAsMember(member), 397 | "time": time 398 | } 399 | ), raise_exception=True) 400 | 401 | @throw_error_if_not_enable 402 | @protocol_log 403 | @edge_case_handler 404 | async def unmute(self, 405 | group: T.Union[Group, int], 406 | member: T.Union[Member, int] 407 | ): 408 | return assertOperatorSuccess( 409 | await fetch.http_post(f"{self.baseurl}/unmute", { 410 | "sessionKey": self.session_key, 411 | "target": self.handleTargetAsGroup(group), 412 | "memberId": self.handleTargetAsMember(member), 413 | } 414 | ), raise_exception=True) 415 | 416 | @throw_error_if_not_enable 417 | @protocol_log 418 | @edge_case_handler 419 | async def kick(self, 420 | group: T.Union[Group, int], 421 | member: T.Union[Member, int], 422 | kickMessage: T.Optional[str] = None 423 | ): 424 | return assertOperatorSuccess( 425 | await fetch.http_post(f"{self.baseurl}/kick", { 426 | "sessionKey": self.session_key, 427 | "target": self.handleTargetAsGroup(group), 428 | "memberId": self.handleTargetAsMember(member), 429 | **({ 430 | "msg": kickMessage 431 | } if kickMessage else {}) 432 | } 433 | ), raise_exception=True) 434 | 435 | @throw_error_if_not_enable 436 | @protocol_log 437 | @edge_case_handler 438 | async def quitGroup(self, 439 | group: T.Union[Group, int] 440 | ): 441 | return assertOperatorSuccess( 442 | await fetch.http_post(f"{self.baseurl}/quit", { 443 | "sessionKey": self.session_key, 444 | "target": self.handleTargetAsGroup(group) 445 | } 446 | ), raise_exception=True) 447 | 448 | @throw_error_if_not_enable 449 | @protocol_log 450 | @edge_case_handler 451 | async def respondRequest(self, 452 | request: T.Union[ 453 | eem.NewFriendRequestEvent, 454 | eem.MemberJoinRequestEvent 455 | ], 456 | operate: T.Union[ 457 | NewFriendRequestResponseOperate, 458 | MemberJoinRequestResponseOperate, 459 | int 460 | ], 461 | message: T.Optional[str] = "" 462 | ): 463 | """回应请求, 请求指 `添加好友请求` 或 `申请加群请求`.""" 464 | if isinstance(request, eem.NewFriendRequestEvent): 465 | if not isinstance(operate, (NewFriendRequestResponseOperate, int)): 466 | raise TypeError(f"unknown operate: {operate}") 467 | operate = (operate.value if isinstance(operate, NewFriendRequestResponseOperate) else operate) 468 | return assertOperatorSuccess(await fetch.http_post(f"{self.baseurl}/resp/newFriendRequestEvent", { 469 | "sessionKey": self.session_key, 470 | "eventId": request.requestId, 471 | "fromId": request.supplicant, 472 | "groupId": request.sourceGroup, 473 | "operate": operate, 474 | "message": message 475 | }), raise_exception=True) 476 | elif isinstance(request, eem.MemberJoinRequestEvent): 477 | if not isinstance(operate, (MemberJoinRequestResponseOperate, int)): 478 | raise TypeError(f"unknown operate: {operate}") 479 | operate = (operate.value if isinstance(operate, MemberJoinRequestResponseOperate) else operate) 480 | return assertOperatorSuccess(await fetch.http_post(f"{self.baseurl}/resp/memberJoinRequestEvent", { 481 | "sessionKey": self.session_key, 482 | "eventId": request.requestId, 483 | "fromId": request.supplicant, 484 | "groupId": request.sourceGroup, 485 | "operate": operate, 486 | "message": message 487 | }), raise_exception=True) 488 | else: 489 | raise TypeError(f"unknown request: {request}") 490 | 491 | async def handleMessageAsGroup( 492 | self, 493 | message: T.Union[ 494 | MessageChain, 495 | BaseMessageComponent, 496 | T.List[T.Union[BaseMessageComponent, InternalImage]], 497 | str 498 | ]): 499 | if isinstance(message, MessageChain): 500 | return json.loads(message.json()) 501 | elif isinstance(message, BaseMessageComponent): 502 | return [json.loads(message.json())] 503 | elif isinstance(message, (tuple, list)): 504 | result = [] 505 | for i in message: 506 | if isinstance(i, InternalImage): 507 | result.append({ 508 | "type": "Image" if not i.flash else "FlashImage", 509 | "imageId": (await self.handleInternalImageAsGroup(i)).asGroupImage() 510 | }) 511 | elif isinstance(i, components.Image): 512 | result.append({ 513 | "type": "Image", 514 | "imageId": i.asGroupImage() 515 | }) 516 | elif isinstance(i, components.FlashImage): 517 | result.append({ 518 | "type": "FlashImage", 519 | "imageId": i.asGroupImage() 520 | }) 521 | else: 522 | result.append(json.loads(i.json())) 523 | return result 524 | elif isinstance(message, str): 525 | return [json.loads(components.Plain(text=message).json())] 526 | else: 527 | raise raiser(ValueError("invaild message.")) 528 | 529 | async def handleMessageAsFriend( 530 | self, 531 | message: T.Union[ 532 | MessageChain, 533 | BaseMessageComponent, 534 | T.List[BaseMessageComponent], 535 | str 536 | ]): 537 | if isinstance(message, MessageChain): 538 | return json.loads(message.json()) 539 | elif isinstance(message, BaseMessageComponent): 540 | return [json.loads(message.json())] 541 | elif isinstance(message, (tuple, list)): 542 | result = [] 543 | for i in message: 544 | if isinstance(i, InternalImage): 545 | result.append({ 546 | "type": "Image" if not i.flash else "FlashImage", 547 | "imageId": (await self.handleInternalImageAsFriend(i)).asFriendImage() 548 | }) 549 | elif isinstance(i, components.Image): 550 | result.append({ 551 | "type": "Image" if not i.flash else "FlashImage", 552 | "imageId": i.asFriendImage() 553 | }) 554 | else: 555 | result.append(json.loads(i.json())) 556 | return result 557 | elif isinstance(message, str): 558 | return [json.loads(components.Plain(text=message).json())] 559 | else: 560 | raise raiser(ValueError("invaild message.")) 561 | 562 | async def handleMessageForTempMessage( 563 | self, 564 | message: T.Union[ 565 | MessageChain, 566 | BaseMessageComponent, 567 | T.List[BaseMessageComponent], 568 | str 569 | ]): 570 | if isinstance(message, MessageChain): 571 | return json.loads(message.json()) 572 | elif isinstance(message, BaseMessageComponent): 573 | return [json.loads(message.json())] 574 | elif isinstance(message, (tuple, list)): 575 | result = [] 576 | for i in message: 577 | if isinstance(i, InternalImage): 578 | result.append({ 579 | "type": "Image" if not i.flash else "FlashImage", 580 | "imageId": (await self.handleInternalImageForTempMessage(i)).asFriendImage() 581 | }) 582 | elif isinstance(i, components.Image): 583 | result.append({ 584 | "type": "Image" if not i.flash else "FlashImage", 585 | "imageId": i.asFriendImage() 586 | }) 587 | else: 588 | result.append(json.loads(i.json())) 589 | return result 590 | elif isinstance(message, str): 591 | return [json.loads(components.Plain(text=message).json())] 592 | else: 593 | raise raiser(ValueError("invaild message.")) 594 | 595 | def handleTargetAsGroup(self, target: T.Union[Group, int]): 596 | return target if isinstance(target, int) else \ 597 | target.id if isinstance(target, Group) else \ 598 | raiser(ValueError("invaild target as group.")) 599 | 600 | def handleTargetAsFriend(self, target: T.Union[Friend, int]): 601 | return target if isinstance(target, int) else \ 602 | target.id if isinstance(target, Friend) else \ 603 | raiser(ValueError("invaild target as a friend obj.")) 604 | 605 | def handleTargetAsMember(self, target: T.Union[Member, int]): 606 | return target if isinstance(target, int) else \ 607 | target.id if isinstance(target, Member) else \ 608 | raiser(ValueError("invaild target as a member obj.")) 609 | 610 | async def handleInternalImageAsGroup(self, image: InternalImage): 611 | return await self.uploadImage("group", image) 612 | 613 | async def handleInternalImageAsFriend(self, image: InternalImage): 614 | return await self.uploadImage("friend", image) 615 | 616 | async def handleInternalImageForTempMessage(self, image: InternalImage): 617 | return await self.uploadImage("temp", image) 618 | -------------------------------------------------------------------------------- /mirai/utilles/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzdluo123/MineSweeper/f0ef81b9369e5332d3ef1ec370bd6ae1a6256a22/mirai/utilles/__init__.py -------------------------------------------------------------------------------- /mirai/utilles/dependencies.py: -------------------------------------------------------------------------------- 1 | """ python-mirai 自带的一些小型依赖注入设施. 2 | 3 | 各个函数皆返回 mirai.Depend 实例, 不需要进一步的包装. 4 | 5 | """ 6 | 7 | from mirai.depend import Depend 8 | from mirai import MessageChain, Cancelled, Image, Mirai, At, Group 9 | import re 10 | from typing import List, Union 11 | 12 | def RegexMatch(pattern): 13 | async def regex_depend_wrapper(message: MessageChain): 14 | if not re.match(pattern, message.toString()): 15 | raise Cancelled 16 | return Depend(regex_depend_wrapper) 17 | 18 | def StartsWith(string): 19 | async def startswith_wrapper(message: MessageChain): 20 | if not message.toString().startswith(string): 21 | raise Cancelled 22 | return Depend(startswith_wrapper) 23 | 24 | def WithPhoto(num=1): 25 | "断言消息中图片的数量" 26 | async def photo_wrapper(message: MessageChain): 27 | if len(message.getAllofComponent(Image)) < num: 28 | raise Cancelled 29 | return Depend(photo_wrapper) 30 | 31 | def AssertAt(qq=None): 32 | "断言是否at了某人, 如果没有给出则断言是否at了机器人" 33 | async def at_wrapper(app: Mirai, message: MessageChain): 34 | at_set: List[At] = message.getAllofComponent(At) 35 | qq = qq or app.qq 36 | if at_set: 37 | for at in at_set: 38 | if at.target == qq: 39 | return 40 | else: 41 | raise Cancelled 42 | return Depend(at_wrapper) 43 | 44 | def GroupsRestraint(*groups: List[Union[Group, int]]): 45 | "断言事件是否发生在某个群内" 46 | async def gr_wrapper(app: Mirai, group: Group): 47 | groups = [group if isinstance(group, int) else group.id for group in groups] 48 | if group.id not in groups: 49 | raise Cancelled 50 | return Depend(gr_wrapper) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pillow 2 | aiohttp 3 | pydantic 4 | logbook 5 | async_lru --------------------------------------------------------------------------------