├── .editorconfig ├── .gitignore ├── .readthedocs.yaml ├── LICENSE ├── README.md ├── cai ├── __init__.py ├── api │ ├── __init__.py │ ├── client.py │ ├── flow.py │ ├── friend.py │ ├── group.py │ └── login.py ├── client │ ├── __init__.py │ ├── client.py │ ├── command.py │ ├── config_push │ │ ├── __init__.py │ │ ├── command.py │ │ └── jce.py │ ├── event.py │ ├── friendlist │ │ ├── __init__.py │ │ ├── command.py │ │ └── jce.py │ ├── heartbeat │ │ └── __init__.py │ ├── message_service │ │ ├── __init__.py │ │ ├── command.py │ │ ├── decoders.py │ │ ├── jce.py │ │ └── models.py │ ├── models.py │ ├── online_push │ │ ├── __init__.py │ │ ├── command.py │ │ └── jce.py │ ├── packet.py │ ├── qq_service │ │ ├── __init__.py │ │ └── jce.py │ ├── sso_server │ │ ├── __init__.py │ │ └── jce.py │ ├── status_service │ │ ├── __init__.py │ │ ├── command.py │ │ └── jce.py │ └── wtlogin │ │ ├── __init__.py │ │ ├── oicq.py │ │ └── tlv.py ├── connection │ ├── __init__.py │ └── utils.py ├── exceptions.py ├── log.py ├── pb │ ├── __init__.py │ ├── im │ │ ├── __init__.py │ │ ├── msg │ │ │ ├── __init__.py │ │ │ ├── common │ │ │ │ ├── __init__.py │ │ │ │ ├── common.proto │ │ │ │ ├── common_pb2.py │ │ │ │ └── common_pb2.pyi │ │ │ ├── msg │ │ │ │ ├── __init__.py │ │ │ │ ├── msg.proto │ │ │ │ ├── msg_pb2.py │ │ │ │ └── msg_pb2.pyi │ │ │ ├── msg_body │ │ │ │ ├── __init__.py │ │ │ │ ├── msg_body.proto │ │ │ │ ├── msg_body_pb2.py │ │ │ │ └── msg_body_pb2.pyi │ │ │ ├── msg_head │ │ │ │ ├── __init__.py │ │ │ │ ├── msg_head.proto │ │ │ │ ├── msg_head_pb2.py │ │ │ │ └── msg_head_pb2.pyi │ │ │ ├── obj_msg │ │ │ │ ├── __init__.py │ │ │ │ ├── obj_msg.proto │ │ │ │ ├── obj_msg_pb2.py │ │ │ │ └── obj_msg_pb2.pyi │ │ │ ├── receipt │ │ │ │ ├── __init__.py │ │ │ │ ├── receipt.proto │ │ │ │ ├── receipt_pb2.py │ │ │ │ └── receipt_pb2.pyi │ │ │ └── service │ │ │ │ └── comm_elem │ │ │ │ ├── __init__.py │ │ │ │ ├── comm_elem.proto │ │ │ │ ├── comm_elem_pb2.py │ │ │ │ └── comm_elem_pb2.pyi │ │ └── oidb │ │ │ ├── __init__.py │ │ │ ├── cmd0x769 │ │ │ ├── __init__.py │ │ │ ├── cmd0x769.proto │ │ │ ├── cmd0x769_pb2.py │ │ │ └── cmd0x769_pb2.pyi │ │ │ └── cmd0xd50 │ │ │ ├── __init__.py │ │ │ ├── cmd0xd50.proto │ │ │ ├── cmd0xd50_pb2.py │ │ │ └── cmd0xd50_pb2.pyi │ ├── msf │ │ ├── __init__.py │ │ └── msg │ │ │ ├── __init__.py │ │ │ ├── comm │ │ │ ├── __init__.py │ │ │ ├── comm.proto │ │ │ ├── comm_pb2.py │ │ │ └── comm_pb2.pyi │ │ │ ├── ctrl │ │ │ ├── __init__.py │ │ │ ├── ctrl.proto │ │ │ ├── ctrl_pb2.py │ │ │ └── ctrl_pb2.pyi │ │ │ ├── onlinepush │ │ │ ├── __init__.py │ │ │ ├── onlinepush.proto │ │ │ ├── onlinepush_pb2.py │ │ │ └── onlinepush_pb2.pyi │ │ │ └── svc │ │ │ ├── __init__.py │ │ │ ├── svc.proto │ │ │ ├── svc_pb2.py │ │ │ └── svc_pb2.pyi │ └── wtlogin │ │ ├── __init__.py │ │ ├── data.proto │ │ ├── data_pb2.py │ │ └── data_pb2.pyi ├── py.typed ├── settings │ ├── __init__.py │ ├── device.py │ └── protocol.py ├── storage │ ├── __init__.py │ └── utils.py └── utils │ ├── __init__.py │ ├── binary.py │ ├── binary.pyi │ ├── coroutine.py │ ├── crypto.py │ ├── dataclass.py │ ├── future.py │ └── jce.py ├── docs ├── assets │ ├── logo.png │ ├── logo_text.png │ ├── logo_text_white.png │ └── logo_white.png ├── conf.py ├── examples │ └── index.rst ├── index.rst ├── py-modindex.rst └── source │ ├── cai.api.rst │ ├── cai.client.config_push.rst │ ├── cai.client.friendlist.rst │ ├── cai.client.heartbeat.rst │ ├── cai.client.message_service.rst │ ├── cai.client.online_push.rst │ ├── cai.client.qq_service.rst │ ├── cai.client.rst │ ├── cai.client.sso_server.rst │ ├── cai.client.status_service.rst │ ├── cai.client.wtlogin.rst │ ├── cai.connection.rst │ ├── cai.pb.im.msg.common.rst │ ├── cai.pb.im.msg.msg.rst │ ├── cai.pb.im.msg.msg_body.rst │ ├── cai.pb.im.msg.msg_head.rst │ ├── cai.pb.im.msg.obj_msg.rst │ ├── cai.pb.im.msg.receipt.rst │ ├── cai.pb.im.msg.rst │ ├── cai.pb.im.oidb.cmd0x769.rst │ ├── cai.pb.im.oidb.cmd0xd50.rst │ ├── cai.pb.im.oidb.rst │ ├── cai.pb.im.rst │ ├── cai.pb.msf.msg.comm.rst │ ├── cai.pb.msf.msg.ctrl.rst │ ├── cai.pb.msf.msg.onlinepush.rst │ ├── cai.pb.msf.msg.rst │ ├── cai.pb.msf.msg.svc.rst │ ├── cai.pb.msf.rst │ ├── cai.pb.rst │ ├── cai.pb.wtlogin.rst │ ├── cai.rst │ ├── cai.settings.rst │ ├── cai.storage.rst │ ├── cai.utils.rst │ └── modules.rst ├── examples ├── friend_group.py ├── login.py ├── message.py └── set_status.py ├── poetry.lock ├── pyproject.toml └── tests ├── client ├── __init__.py ├── test_login.py └── test_sso_server.py └── connection ├── __init__.py └── test_connection.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | # The JSON files contain newlines inconsistently 15 | [*.json] 16 | insert_final_newline = ignore 17 | 18 | # Minified JavaScript files shouldn't be changed 19 | [**.min.js] 20 | indent_style = ignore 21 | insert_final_newline = ignore 22 | 23 | # Makefiles always use tabs for indentation 24 | [Makefile] 25 | indent_style = tab 26 | 27 | # Batch files use tabs for indentation 28 | [*.bat] 29 | indent_style = tab 30 | 31 | [*.md] 32 | trim_trailing_whitespace = false 33 | 34 | # Matches the exact files either package.json or .travis.yml 35 | [{package.json,.travis.yml}] 36 | indent_size = 2 37 | 38 | [{*.py,*.pyi}] 39 | indent_size = 4 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 88 | __pypackages__/ 89 | 90 | # Celery stuff 91 | celerybeat-schedule 92 | celerybeat.pid 93 | 94 | # SageMath parsed files 95 | *.sage.py 96 | 97 | # Environments 98 | .env 99 | .venv 100 | env/ 101 | venv/ 102 | ENV/ 103 | env.bak/ 104 | venv.bak/ 105 | 106 | # Spyder project settings 107 | .spyderproject 108 | .spyproject 109 | 110 | # Rope project settings 111 | .ropeproject 112 | 113 | # mkdocs documentation 114 | /site 115 | 116 | # mypy 117 | .mypy_cache/ 118 | .dmypy.json 119 | dmypy.json 120 | 121 | # Pyre type checker 122 | .pyre/ 123 | 124 | # Visual Studio Code 125 | .vscode/* 126 | # !.vscode/settings.json 127 | # !.vscode/tasks.json 128 | # !.vscode/launch.json 129 | # !.vscode/extensions.json 130 | *.code-workspace 131 | 132 | # Local History for Visual Studio Code 133 | .history/ 134 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | python: 4 | version: 3.8 5 | install: 6 | - method: pip 7 | path: . 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | CAI 3 |

4 | 5 |
6 | 7 | _✨ Yet Another Bot Framework for Tencent QQ Written in Python ✨_ 8 | 9 |
10 | 11 |

12 | 13 | license 14 | 15 | python 16 | 17 | 18 | 19 | 20 | QQ Chat 21 | 22 |

23 | 24 | --- 25 | 26 | ## 声明 27 | 28 | ### 一切开发旨在学习,请勿用于非法用途 29 | 30 | - `CAI` 是完全免费且开放源代码的软件,仅供学习和娱乐用途使用 31 | - `CAI` 不会通过任何方式强制收取费用,或对使用者提出物质条件 32 | 33 | ### 许可证 34 | 35 | `CAI` 采用 [AGPLv3](LICENSE) 协议开源,不鼓励、不支持一切商业使用。 36 | 37 | --- 38 | 39 | ## 特色 40 | 41 | - 简单易用的 API,支持多账号 42 | - 极少的额外依赖 43 | - 异步编写,效率++ 44 | 45 | - 使用 [Asyncio Stream](https://docs.python.org/3/library/asyncio-stream.html) 处理网络连接 ([cai.connection.Connection](https://cai-bot.readthedocs.io/zh_CN/latest/source/cai.connection.html#cai.connection.Connection)) 46 | - 使用 [Asyncio Future](https://docs.python.org/3/library/asyncio-future.html) 处理收发包 ([cai.utils.future.FutureStore](https://cai-bot.readthedocs.io/zh_CN/latest/source/cai.utils.html#cai.utils.future.FutureStore)) 47 | 48 | - 完整的 [Type Hints](https://www.python.org/dev/peps/pep-0484/) 49 | 50 | - Packet Query 支持 [Variadic Generics](https://www.python.org/dev/peps/pep-0646/) 51 | 52 | ```python 53 | from cai.utils.binary import Packet 54 | packet = Packet(bytes.fromhex("01000233000000")) 55 | packet.start().int8().uint16().bytes(4).execute() 56 | # return type: INT8, UINT16, BYTES 57 | ``` 58 | 59 | - 便携的 JceStruct 定义 (使用方法参考 [JceStruct](https://github.com/yanyongyu/JceStruct)) 60 | 61 | ```python 62 | from typing import Optional 63 | from jce import JceStruct, JceField, types 64 | 65 | class CustomStruct(JceStruct): 66 | int32_field: types.INT32 = JceField(jce_id=0) 67 | optional_field: Optional[types.DOUBLE] = JceField(None, jce_id=1) 68 | nested_field: OtherStruct = JceField(jce_id=2) 69 | ``` 70 | 71 | ## 功能 72 | 73 | `CAI` 仅作为底层协议库使用,将协议封装为 API。 74 | 75 | > `CAI` 不会支持涉及 **金钱**、**主动邀请**、**获取凭证** 等敏感操作的协议。 76 | 77 |
78 | 已支持的协议列表: 79 | 80 | ### 登录 81 | 82 | [`cai.api.login` API Reference](https://cai-bot.readthedocs.io/zh_CN/latest/source/cai.api.html#module-cai.api.login) 83 | 84 | - [x] 账号密码登录 85 | - [x] 设备锁验证 86 | - [x] 图片验证码提交 87 | - [x] 短信验证码提交 88 | - [ ] 扫码登录 89 | 90 | ### 客户端 91 | 92 | [`cai.api.client` API Reference](https://cai-bot.readthedocs.io/zh_CN/latest/source/cai.api.html#module-cai.api.client) 93 | 94 | - [x] 设置在线状态 95 | 96 | ### 好友 97 | 98 | [`cai.api.friend` API Reference](https://cai-bot.readthedocs.io/zh_CN/latest/source/cai.api.html#module-cai.api.friend) 99 | 100 | - [x] 获取好友列表 101 | - [x] 获取好友信息 102 | - [x] 获取好友分组列表 103 | - [x] 获取好友分组信息 104 | 105 | ### 群组 106 | 107 | [`cai.api.group` API Reference](https://cai-bot.readthedocs.io/zh_CN/latest/source/cai.api.html#module-cai.api.group) 108 | 109 | - [x] 获取群列表 110 | - [x] 获取群信息 111 | - [x] 获取群成员列表 112 | 113 | ### 事件 114 | 115 | [`cai.api.flow` API Reference](https://cai-bot.readthedocs.io/zh_CN/latest/source/cai.api.html#module-cai.api.flow) 116 | 117 | 通过注册事件监听回调,在事件发生时执行指定操作。事件类型可通过 `cai.client` 模块导入。 118 | 119 | - [x] 好友消息 ([PrivateMessage](https://cai-bot.readthedocs.io/zh_CN/latest/source/cai.client.message_service.html#cai.client.message_service.models.PrivateMessage)) 120 | - [x] 群消息 ([GroupMessage](https://cai-bot.readthedocs.io/zh_CN/latest/source/cai.client.message_service.html#cai.client.message_service.models.GroupMessage)) 121 | 122 |
123 | 124 | ## 文档 125 | 126 | _In Development_ 127 | 128 | [See on Read The Docs](https://cai-bot.readthedocs.io/) 129 | -------------------------------------------------------------------------------- /cai/__init__.py: -------------------------------------------------------------------------------- 1 | """Entry Module for CAI. 2 | 3 | :Copyright: Copyright (C) 2021-2021 cscs181 4 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 5 | 6 | .. _LICENSE: 7 | https://github.com/cscs181/CAI/blob/master/LICENSE 8 | """ 9 | from .api import * 10 | from .exceptions import * 11 | -------------------------------------------------------------------------------- /cai/api/__init__.py: -------------------------------------------------------------------------------- 1 | """Application APIs. 2 | 3 | This module wraps the client methods to provide easier control (high-level api). 4 | 5 | :Copyright: Copyright (C) 2021-2021 cscs181 6 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 7 | 8 | .. _LICENSE: 9 | https://github.com/cscs181/CAI/blob/master/LICENSE 10 | """ 11 | 12 | from typing import Dict 13 | 14 | from cai.client import Client 15 | 16 | _clients: Dict[int, Client] = {} 17 | 18 | 19 | from .flow import * 20 | from .group import * 21 | from .login import * 22 | from .client import * 23 | from .friend import * 24 | -------------------------------------------------------------------------------- /cai/api/client.py: -------------------------------------------------------------------------------- 1 | """Application Client APIs. 2 | 3 | :Copyright: Copyright (C) 2021-2021 cscs181 4 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 5 | 6 | .. _LICENSE: 7 | https://github.com/cscs181/CAI/blob/master/LICENSE 8 | """ 9 | 10 | import asyncio 11 | from typing import Union, Optional 12 | 13 | from cai.client import Client, OnlineStatus 14 | from cai.exceptions import ClientNotAvailable 15 | 16 | from . import _clients 17 | 18 | 19 | def get_client(uin: Optional[int] = None) -> Client: 20 | """Get the specific client or existing client. 21 | 22 | Args: 23 | uin (Optional[int], optional): Specific account client to get. Defaults to None. 24 | 25 | Raises: 26 | ClientNotAvailable: Client not exists. 27 | ClientNotAvailable: No client available. 28 | ClientNotAvailable: Multiple clients found and not specify which one to get. 29 | 30 | Returns: 31 | Client: Current client to use. 32 | """ 33 | if not _clients: 34 | raise ClientNotAvailable(uin, f"No client available!") 35 | elif len(_clients) == 1 and not uin: 36 | return list(_clients.values())[0] 37 | else: 38 | if not uin: 39 | raise ClientNotAvailable( 40 | None, f"Multiple clients found! Specify uin to choose." 41 | ) 42 | if uin not in _clients: 43 | raise ClientNotAvailable(None, f"Client {uin} not exists!") 44 | return _clients[uin] 45 | 46 | 47 | async def close(uin: Optional[int] = None) -> None: 48 | """Close an existing client and delete it from clients. 49 | 50 | Args: 51 | uin (Optional[int], optional): Account of the client want to close. Defaults to None. 52 | """ 53 | client = get_client(uin) 54 | await client.close() 55 | del _clients[client.uin] 56 | 57 | 58 | async def close_all() -> None: 59 | """Close all existing clients and delete them.""" 60 | tasks = [close(uin) for uin in _clients.keys()] 61 | await asyncio.gather(*tasks) 62 | 63 | 64 | async def set_status( 65 | status: Union[int, OnlineStatus], 66 | battery_status: Optional[int] = None, 67 | is_power_connected: bool = False, 68 | uin: Optional[int] = None, 69 | ) -> None: 70 | """Change client status. 71 | 72 | This function wraps the :meth:`~cai.client.client.Client.register` 73 | method of the client. 74 | 75 | Args: 76 | status (OnlineStatus): Status want to change. 77 | battery_status (Optional[int], optional): Battery capacity. 78 | Defaults to None. 79 | is_power_connected (bool, optional): Is power connected to phone. 80 | Defaults to False. 81 | uin (Optional[int], optional): Account of the client want to change. 82 | Defaults to None. 83 | 84 | Raises: 85 | RuntimeError: Client already exists and is running. 86 | RuntimeError: Password not provided when login a new account. 87 | ApiResponseError: Invalid API request. 88 | RegisterException: Register Failed. 89 | """ 90 | client = get_client(uin) 91 | await client.set_status( 92 | status, 93 | battery_status, 94 | is_power_connected, 95 | ) 96 | 97 | 98 | __all__ = ["get_client", "close", "close_all", "set_status"] 99 | -------------------------------------------------------------------------------- /cai/api/flow.py: -------------------------------------------------------------------------------- 1 | """Application Flow APIs. 2 | 3 | :Copyright: Copyright (C) 2021-2021 cscs181 4 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 5 | 6 | .. _LICENSE: 7 | https://github.com/cscs181/CAI/blob/master/LICENSE 8 | """ 9 | 10 | from typing import Callable, Optional, Awaitable 11 | 12 | from cai.log import logger 13 | from cai.client import HANDLERS, Event, Client, Command, IncomingPacket 14 | 15 | from .client import get_client 16 | 17 | 18 | def add_event_listener( 19 | listener: Callable[[Client, Event], Awaitable[None]], 20 | uin: Optional[int] = None, 21 | ): 22 | """Add event listener. 23 | 24 | If uin is ``None``, listener will receive events from all clients. 25 | 26 | Args: 27 | listener (Callable[[Client, Event], Awaitable[None]]): Event listener. 28 | uin (Optional[int], optional): Account of the client want to listen. 29 | Defaults to None. 30 | """ 31 | if uin: 32 | client = get_client(uin) 33 | client.add_event_listener(listener) 34 | else: 35 | Client.LISTENERS.add(listener) 36 | 37 | 38 | def register_packet_handler( 39 | cmd: str, 40 | packet_handler: Callable[[Client, IncomingPacket], Awaitable[Command]], 41 | ) -> None: 42 | """Register custom packet handler. 43 | 44 | Note: 45 | This function is a low-level api to mock default behavior. 46 | Be aware of what you are doing! 47 | 48 | Args: 49 | cmd (str): Command name of the packet. 50 | packet_handler (Callable[[Client, IncomingPacket], Awaitable[Command]]): 51 | Asynchronous packet handler. A :obj:`~cai.client.command.Command` 52 | object should be returned. 53 | """ 54 | if cmd in HANDLERS: 55 | logger.warning( 56 | f"You are overwriting an existing handler for command {cmd}!" 57 | ) 58 | HANDLERS[cmd] = packet_handler 59 | 60 | 61 | __all__ = ["add_event_listener", "register_packet_handler"] 62 | -------------------------------------------------------------------------------- /cai/api/friend.py: -------------------------------------------------------------------------------- 1 | """Application Friend APIs. 2 | 3 | :Copyright: Copyright (C) 2021-2021 cscs181 4 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 5 | 6 | .. _LICENSE: 7 | https://github.com/cscs181/CAI/blob/master/LICENSE 8 | """ 9 | 10 | from typing import List, Optional 11 | 12 | from cai.client import Friend, FriendGroup 13 | 14 | from .client import get_client 15 | 16 | 17 | async def get_friend( 18 | friend_uin: int, cache: bool = True, uin: Optional[int] = None 19 | ) -> Optional[Friend]: 20 | """Get account friend. 21 | 22 | This function wraps the :meth:`~cai.client.client.Client.get_friend` 23 | method of the client. 24 | 25 | Args: 26 | friend_uin (int): Friend account uin. 27 | cache (bool, optional): Use cached friend list. Defaults to True. 28 | uin (Optional[int], optional): Account of the client want to use. 29 | Defaults to None. 30 | 31 | Returns: 32 | Friend: Friend object. 33 | None: Friend not exists. 34 | 35 | Raises: 36 | RuntimeError: Error response type got. This should not happen. 37 | ApiResponseError: Get friend list failed. 38 | FriendListException: Get friend list returned non-zero ret code. 39 | """ 40 | client = get_client(uin) 41 | return await client.get_friend(friend_uin, cache) 42 | 43 | 44 | async def get_friend_list( 45 | cache: bool = True, uin: Optional[int] = None 46 | ) -> List[Friend]: 47 | """Get account friend list. 48 | 49 | This function wraps the :meth:`~cai.client.client.Client.get_friend_list` 50 | method of the client. 51 | 52 | Args: 53 | cache (bool, optional): Use cached friend list. Defaults to True. 54 | uin (Optional[int], optional): Account of the client want to use. 55 | Defaults to None. 56 | 57 | Returns: 58 | List of :obj:`~cai.client.models.Friend` 59 | 60 | Raises: 61 | RuntimeError: Error response type got. This should not happen. 62 | ApiResponseError: Get friend list failed. 63 | FriendListException: Get friend list returned non-zero ret code. 64 | """ 65 | client = get_client(uin) 66 | return await client.get_friend_list(cache) 67 | 68 | 69 | async def get_friend_group( 70 | group_id: int, cache: bool = True, uin: Optional[int] = None 71 | ) -> Optional[FriendGroup]: 72 | """Get Friend Group. 73 | 74 | This function wraps the :meth:`~cai.client.client.Client.get_friend_group` 75 | method of the client. 76 | 77 | Args: 78 | group_id (int): Friend group id. 79 | cache (bool, optional): Use cached friend group list. Defaults to True. 80 | uin (Optional[int], optional): Account of the client want to use. 81 | Defaults to None. 82 | 83 | Returns: 84 | FriendGroup: Friend group object. 85 | None: Friend group not exists. 86 | 87 | Raises: 88 | RuntimeError: Error response type got. This should not happen. 89 | ApiResponseError: Get friend list failed. 90 | FriendListException: Get friend list returned non-zero ret code. 91 | """ 92 | client = get_client(uin) 93 | return await client.get_friend_group(group_id, cache) 94 | 95 | 96 | async def get_friend_group_list( 97 | cache: bool = True, uin: Optional[int] = None 98 | ) -> List[FriendGroup]: 99 | """Get account friend group list. 100 | 101 | This function wraps the :meth:`~cai.client.client.Client.get_friend_group_list` 102 | method of the client. 103 | 104 | Args: 105 | cache (bool, optional): Use cached friend group list. Defaults to True. 106 | uin (Optional[int], optional): Account of the client want to use. 107 | Defaults to None. 108 | 109 | Returns: 110 | List[FriendGroup]: Friend group list. 111 | 112 | Raises: 113 | RuntimeError: Error response type got. This should not happen. 114 | ApiResponseError: Get friend group list failed. 115 | FriendListException: Get friend group list returned non-zero ret code. 116 | """ 117 | client = get_client(uin) 118 | return await client.get_friend_group_list(cache) 119 | 120 | 121 | __all__ = [ 122 | "get_friend", 123 | "get_friend_list", 124 | "get_friend_group", 125 | "get_friend_group_list", 126 | ] 127 | -------------------------------------------------------------------------------- /cai/api/group.py: -------------------------------------------------------------------------------- 1 | """Application Group APIs. 2 | 3 | :Copyright: Copyright (C) 2021-2021 cscs181 4 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 5 | 6 | .. _LICENSE: 7 | https://github.com/cscs181/CAI/blob/master/LICENSE 8 | """ 9 | 10 | from typing import List, Union, Optional 11 | 12 | from cai.client import Group, GroupMember 13 | 14 | from .client import get_client 15 | 16 | 17 | async def get_group( 18 | group_id: int, cache: bool = True, uin: Optional[int] = None 19 | ) -> Optional[Group]: 20 | """Get Group. 21 | 22 | This function wraps the :meth:`~cai.client.client.Client.get_group` 23 | method of the client. 24 | 25 | Args: 26 | group_id (int): Group id. 27 | cache (bool, optional): Use cached friend group list. Defaults to True. 28 | uin (Optional[int], optional): Account of the client want to use. 29 | Defaults to None. 30 | 31 | Returns: 32 | Group: Group object. 33 | None: Group not exists. 34 | 35 | Raises: 36 | RuntimeError: Error response type got. This should not happen. 37 | ApiResponseError: Get friend list failed. 38 | FriendListException: Get friend list returned non-zero ret code. 39 | """ 40 | client = get_client(uin) 41 | return await client.get_group(group_id, cache) 42 | 43 | 44 | async def get_group_list( 45 | cache: bool = True, uin: Optional[int] = None 46 | ) -> List[Group]: 47 | """Get account group list. 48 | 49 | This function wraps the :meth:`~cai.client.client.Client.get_group_list` 50 | method of the client. 51 | 52 | Args: 53 | cache (bool, optional): Use cached group list. Defaults to True. 54 | uin (Optional[int], optional): Account of the client want to use. 55 | Defaults to None. 56 | 57 | Returns: 58 | List[Group]: Group list. 59 | 60 | Raises: 61 | RuntimeError: Error response type got. This should not happen. 62 | ApiResponseError: Get group list failed. 63 | GroupListException: Get group list returned non-zero ret code. 64 | """ 65 | client = get_client(uin) 66 | return await client.get_group_list(cache) 67 | 68 | 69 | async def get_group_member_list( 70 | group: Union[int, Group], cache: bool = True, uin: Optional[int] = None 71 | ) -> Optional[List[GroupMember]]: 72 | """Get account group member list. 73 | 74 | This function wraps the :meth:`~cai.client.client.Client.get_group_member_list` 75 | method of the client. 76 | 77 | Args: 78 | group (Union[int, Group]): Group id or group object want to get members. 79 | cache (bool, optional): Use cached group list. Defaults to True. 80 | uin (Optional[int], optional): Account of the client want to use. 81 | Defaults to None. 82 | 83 | Returns: 84 | List[GroupMember]: Group member list. 85 | None: Group not exists. 86 | 87 | Raises: 88 | RuntimeError: Error response type got. This should not happen. 89 | ApiResponseError: Get group list failed. 90 | GroupMemberListException: Get group member list returned non-zero ret code. 91 | """ 92 | client = get_client(uin) 93 | return await client.get_group_member_list(group, cache) 94 | 95 | 96 | __all__ = ["get_group", "get_group_list", "get_group_member_list"] 97 | -------------------------------------------------------------------------------- /cai/api/login.py: -------------------------------------------------------------------------------- 1 | """Application Login APIs. 2 | 3 | :Copyright: Copyright (C) 2021-2021 cscs181 4 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 5 | 6 | .. _LICENSE: 7 | https://github.com/cscs181/CAI/blob/master/LICENSE 8 | """ 9 | 10 | from typing import Optional 11 | 12 | from cai.client import Client 13 | from cai.exceptions import LoginException 14 | 15 | from . import _clients 16 | from .client import get_client 17 | 18 | 19 | async def login(uin: int, password_md5: Optional[bytes] = None) -> Client: 20 | """Create a new client (or use an existing one) and login. 21 | 22 | Password md5 should be provided when login a new account. 23 | 24 | This function wraps the :meth:`~cai.client.client.Client.login` method of the client. 25 | 26 | Args: 27 | uin (int): QQ account number. 28 | password_md5 (Optional[bytes], optional): md5 bytes of the password. Defaults to None. 29 | 30 | Raises: 31 | RuntimeError: Client already exists and is running. 32 | RuntimeError: Password not provided when login a new account. 33 | LoginSliderException: Need slider ticket. 34 | LoginCaptchaException: Need captcha image. 35 | """ 36 | if uin in _clients: 37 | client = _clients[uin] 38 | if client.connected: 39 | raise RuntimeError(f"Client {uin} already connected!") 40 | client._password_md5 = password_md5 or client._password_md5 41 | else: 42 | if not password_md5: 43 | raise RuntimeError(f"Password md5 needed for creating new client!") 44 | client = Client(uin, password_md5) 45 | _clients[uin] = client 46 | 47 | await client.reconnect() 48 | try: 49 | await client.login() 50 | except LoginException: 51 | raise 52 | except Exception: 53 | await client.close() 54 | raise 55 | return client 56 | 57 | 58 | async def submit_captcha( 59 | captcha: str, captcha_sign: bytes, uin: Optional[int] = None 60 | ) -> bool: 61 | """Submit captcha data to login. 62 | 63 | This function wraps the :meth:`~cai.client.client.Client.submit_captcha` 64 | method of the client. 65 | 66 | Args: 67 | captcha (str): Captcha data to submit. 68 | captcha_sign (bytes): Captcha sign received when login. 69 | uin (Optional[int], optional): Account of the client want to login. 70 | Defaults to None. 71 | 72 | Raises: 73 | RuntimeError: Client already exists and is running. 74 | RuntimeError: Password not provided when login a new account. 75 | LoginSliderException: Need slider ticket. 76 | LoginCaptchaException: Need captcha image. 77 | """ 78 | client = get_client(uin) 79 | try: 80 | await client.submit_captcha(captcha, captcha_sign) 81 | except LoginException: 82 | raise 83 | except Exception: 84 | await client.close() 85 | raise 86 | return True 87 | 88 | 89 | async def submit_slider_ticket(ticket: str, uin: Optional[int] = None) -> bool: 90 | """Submit slider ticket to login. 91 | 92 | This function wraps the :meth:`~cai.client.client.Client.submit_slider_ticket` 93 | method of the client. 94 | 95 | Args: 96 | ticket (str): Slider ticket to submit. 97 | uin (Optional[int], optional): Account of the client want to login. 98 | Defaults to None. 99 | 100 | Raises: 101 | RuntimeError: Client already exists and is running. 102 | RuntimeError: Password not provided when login a new account. 103 | LoginSliderException: Need slider ticket. 104 | LoginCaptchaException: Need captcha image. 105 | """ 106 | client = get_client(uin) 107 | try: 108 | await client.submit_slider_ticket(ticket) 109 | except LoginException: 110 | raise 111 | except Exception: 112 | await client.close() 113 | raise 114 | return True 115 | 116 | 117 | async def request_sms(uin: Optional[int] = None) -> bool: 118 | """Request sms code message to login. 119 | 120 | This function wraps the :meth:`~cai.client.client.Client.request_sms` 121 | method of the client. 122 | 123 | Args: 124 | uin (Optional[int], optional): Account of the client want to login. 125 | Defaults to None. 126 | 127 | Raises: 128 | RuntimeError: Client already exists and is running. 129 | RuntimeError: Password not provided when login a new account. 130 | LoginSMSRequestError: Too many SMS messages were sent. 131 | """ 132 | client = get_client(uin) 133 | return await client.request_sms() 134 | 135 | 136 | async def submit_sms(sms_code: str, uin: Optional[int] = None) -> bool: 137 | """Submit sms code to login. 138 | 139 | This function wraps the :meth:`~cai.client.client.Client.submit_sms` 140 | method of the client. 141 | 142 | Args: 143 | sms_code (str): SMS code to submit. 144 | uin (Optional[int], optional): Account of the client want to login. 145 | Defaults to None. 146 | 147 | Raises: 148 | RuntimeError: Client already exists and is running. 149 | RuntimeError: Password not provided when login a new account. 150 | LoginSliderException: Need slider ticket. 151 | LoginCaptchaException: Need captcha image. 152 | """ 153 | client = get_client(uin) 154 | try: 155 | await client.submit_sms(sms_code) 156 | except LoginException: 157 | raise 158 | except Exception: 159 | await client.close() 160 | raise 161 | return True 162 | 163 | 164 | __all__ = [ 165 | "login", 166 | "submit_captcha", 167 | "submit_slider_ticket", 168 | "request_sms", 169 | "submit_sms", 170 | ] 171 | -------------------------------------------------------------------------------- /cai/client/__init__.py: -------------------------------------------------------------------------------- 1 | """Application Client. 2 | 3 | This module is main entry point for the application. 4 | 5 | :Copyright: Copyright (C) 2021-2021 cscs181 6 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 7 | 8 | .. _LICENSE: 9 | https://github.com/cscs181/CAI/blob/master/LICENSE 10 | """ 11 | 12 | from .event import Event 13 | from .command import Command 14 | from .packet import IncomingPacket 15 | from .client import HANDLERS, Client 16 | from .status_service import OnlineStatus, RegPushReason 17 | from .message_service import GroupMessage, PrivateMessage 18 | from .models import Group, Friend, FriendGroup, GroupMember, GroupMemberRole 19 | -------------------------------------------------------------------------------- /cai/client/command.py: -------------------------------------------------------------------------------- 1 | """Client Base Command. 2 | 3 | This module is used to build command from packet. 4 | 5 | :Copyright: Copyright (C) 2021-2021 cscs181 6 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 7 | 8 | .. _LICENSE: 9 | https://github.com/cscs181/CAI/blob/master/LICENSE 10 | """ 11 | from typing import TYPE_CHECKING 12 | from dataclasses import dataclass 13 | 14 | from .packet import IncomingPacket 15 | 16 | if TYPE_CHECKING: 17 | from .client import Client 18 | 19 | 20 | @dataclass 21 | class Command: 22 | uin: int 23 | seq: int 24 | ret_code: int 25 | command_name: str 26 | 27 | 28 | @dataclass 29 | class UnhandledCommand(Command): 30 | data: bytes 31 | 32 | 33 | async def _packet_to_command( 34 | client: "Client", packet: IncomingPacket 35 | ) -> UnhandledCommand: 36 | return UnhandledCommand( 37 | packet.uin, 38 | packet.seq, 39 | packet.ret_code, 40 | packet.command_name, 41 | packet.data, 42 | ) 43 | -------------------------------------------------------------------------------- /cai/client/config_push/__init__.py: -------------------------------------------------------------------------------- 1 | """ConfigPushSvc Related SDK. 2 | 3 | This module is used to build and handle config push service related packet. 4 | 5 | :Copyright: Copyright (C) 2021-2021 cscs181 6 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 7 | 8 | .. _LICENSE: 9 | https://github.com/cscs181/CAI/blob/master/LICENSE 10 | """ 11 | 12 | from typing import TYPE_CHECKING 13 | 14 | from jce import types 15 | 16 | from cai.log import logger 17 | from cai.utils.binary import Packet 18 | from cai.utils.jce import RequestPacketVersion3 19 | from cai.client.packet import UniPacket, IncomingPacket 20 | 21 | from .jce import PushResp, FileServerPushList 22 | from .command import ( 23 | ConfigPushCommand, 24 | LogActionPushCommand, 25 | SsoServerPushCommand, 26 | FileServerPushCommand, 27 | _ConfigPushCommandBase, 28 | ) 29 | 30 | if TYPE_CHECKING: 31 | from cai.client import Client 32 | 33 | 34 | def encode_config_push_response( 35 | uin: int, 36 | seq: int, 37 | session_id: bytes, 38 | d2key: bytes, 39 | type: int, 40 | jcebuf: bytes, 41 | large_seq: int, 42 | ) -> Packet: 43 | """Build config push response packet. 44 | 45 | command name: ``ConfigPushSvc.PushResp`` 46 | 47 | Note: 48 | Source: com.tencent.mobileqq.msf.core.a.c.b 49 | 50 | Args: 51 | uin (int): User QQ number. 52 | seq (int): Packet sequence. 53 | session_id (bytes): Session ID. 54 | d2key (bytes): Siginfo d2 key. 55 | type (int): ConfigPushSvc request type. 56 | jcebuf (bytes): ConfigPushSvc request jcebuf. 57 | large_seq (int): ConfigPushSvc request large_seq. 58 | """ 59 | COMMAND_NAME = "ConfigPushSvc.PushResp" 60 | 61 | resp = PushResp( 62 | type=type, jcebuf=jcebuf if type == 3 else None, large_seq=large_seq 63 | ) 64 | payload = PushResp.to_bytes(0, resp) 65 | resp_packet = RequestPacketVersion3( 66 | servant_name="QQService.ConfigPushSvc.MainServant", 67 | func_name="PushResp", 68 | data=types.MAP({types.STRING("PushResp"): types.BYTES(payload)}), 69 | ).encode() 70 | packet = UniPacket.build( 71 | uin, seq, COMMAND_NAME, session_id, 1, resp_packet, d2key 72 | ) 73 | return packet 74 | 75 | 76 | # ConfigPushSvc.PushReq 77 | async def handle_config_push_request( 78 | client: "Client", packet: IncomingPacket 79 | ) -> ConfigPushCommand: 80 | command = ConfigPushCommand.decode_push_req( 81 | packet.uin, 82 | packet.seq, 83 | packet.ret_code, 84 | packet.command_name, 85 | packet.data, 86 | ) 87 | if isinstance(command, SsoServerPushCommand): 88 | logger.debug(f"ConfigPush: Got new server addresses.") 89 | elif isinstance(command, FileServerPushCommand): 90 | client._file_storage_info = command.list 91 | 92 | if isinstance(command, _ConfigPushCommandBase): 93 | resp_packet = encode_config_push_response( 94 | client.uin, 95 | command.seq, 96 | client._session_id, 97 | client._siginfo.d2key, 98 | command.type, 99 | command.jcebuf, 100 | command.large_seq, 101 | ) 102 | await client.send(command.seq, "ConfigPushSvc.PushResp", resp_packet) 103 | 104 | return command 105 | 106 | 107 | __all__ = [ 108 | "handle_config_push_request", 109 | "FileServerPushList", 110 | "ConfigPushCommand", 111 | "SsoServerPushCommand", 112 | "FileServerPushCommand", 113 | "LogActionPushCommand", 114 | ] 115 | -------------------------------------------------------------------------------- /cai/client/config_push/command.py: -------------------------------------------------------------------------------- 1 | """ConfigPushSvc Command Parser. 2 | 3 | This module is used to parse ConfigPushSvc packets into command. 4 | 5 | :Copyright: Copyright (C) 2021-2021 cscs181 6 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 7 | 8 | .. _LICENSE: 9 | https://github.com/cscs181/CAI/blob/master/LICENSE 10 | """ 11 | 12 | from dataclasses import dataclass 13 | 14 | from cai.client.command import Command 15 | from cai.utils.jce import RequestPacketVersion2 16 | 17 | from .jce import PushReq, SsoServerPushList, FileServerPushList 18 | 19 | 20 | @dataclass 21 | class ConfigPushCommand(Command): 22 | @classmethod 23 | def decode_push_req( 24 | cls, uin: int, seq: int, ret_code: int, command_name: str, data: bytes 25 | ) -> "ConfigPushCommand": 26 | """Decode ConfigPush.PushReq packet. 27 | 28 | Note: 29 | Source: 30 | 31 | com.tencent.mobileqq.msf.core.a.c.a (type 1) 32 | 33 | com.tencent.mobileqq.servlet.PushServlet.onReceive (type 2) 34 | 35 | Args: 36 | uin (int): User QQ 37 | seq (int): Sequence number of the response packet. 38 | ret_code (int): Return code of the response. 39 | command_name (str): Command name of the response. 40 | data (bytes): Payload data of the response. 41 | 42 | Returns: 43 | SsoServerPushCommand: Push Sso Server Info List. 44 | FileServerPushCommand: Push File Server Info List. 45 | """ 46 | if ret_code != 0 or not data: 47 | return cls(uin, seq, ret_code, command_name) 48 | 49 | packet = RequestPacketVersion2.decode(data) 50 | push = PushReq.decode( 51 | packet.data["PushReq"]["ConfigPush.PushReq"][1:-1] # type: ignore 52 | ) 53 | if push.type == 1: 54 | list = SsoServerPushList.decode(push.jcebuf) 55 | return SsoServerPushCommand( 56 | uin, 57 | seq, 58 | ret_code, 59 | command_name, 60 | push.type, 61 | push.jcebuf, 62 | push.large_seq, 63 | list, 64 | ) 65 | elif push.type == 2: 66 | list = FileServerPushList.decode(push.jcebuf) 67 | return FileServerPushCommand( 68 | uin, 69 | seq, 70 | ret_code, 71 | command_name, 72 | push.type, 73 | push.jcebuf, 74 | push.large_seq, 75 | list, 76 | ) 77 | elif push.type == 3: 78 | # LogAction, do nothing 79 | return LogActionPushCommand( 80 | uin, 81 | seq, 82 | ret_code, 83 | command_name, 84 | push.type, 85 | push.jcebuf, 86 | push.large_seq, 87 | ) 88 | return ConfigPushCommand(uin, seq, ret_code, command_name) 89 | 90 | 91 | @dataclass 92 | class _ConfigPushCommandBase(ConfigPushCommand): 93 | type: int 94 | jcebuf: bytes 95 | large_seq: int 96 | 97 | 98 | @dataclass 99 | class SsoServerPushCommand(_ConfigPushCommandBase): 100 | list: SsoServerPushList 101 | 102 | 103 | @dataclass 104 | class FileServerPushCommand(_ConfigPushCommandBase): 105 | list: FileServerPushList 106 | 107 | 108 | @dataclass 109 | class LogActionPushCommand(_ConfigPushCommandBase): 110 | pass 111 | -------------------------------------------------------------------------------- /cai/client/event.py: -------------------------------------------------------------------------------- 1 | """Client Base Command. 2 | 3 | This module is used to build command from packet. 4 | 5 | :Copyright: Copyright (C) 2021-2021 cscs181 6 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 7 | 8 | .. _LICENSE: 9 | https://github.com/cscs181/CAI/blob/master/LICENSE 10 | """ 11 | 12 | import abc 13 | 14 | 15 | class Event(abc.ABC): 16 | @property 17 | @abc.abstractmethod 18 | def type(self) -> str: 19 | raise NotImplementedError 20 | -------------------------------------------------------------------------------- /cai/client/heartbeat/__init__.py: -------------------------------------------------------------------------------- 1 | """Heartbeat Related SDK. 2 | 3 | This module is used to build and handle heartbeat related packet. 4 | 5 | :Copyright: Copyright (C) 2021-2021 cscs181 6 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 7 | 8 | .. _LICENSE: 9 | https://github.com/cscs181/CAI/blob/master/LICENSE 10 | """ 11 | 12 | from typing import TYPE_CHECKING 13 | from dataclasses import dataclass 14 | 15 | from cai.utils.binary import Packet 16 | from cai.client.command import Command 17 | from cai.settings.device import get_device 18 | from cai.settings.protocol import get_protocol 19 | from cai.client.packet import CSsoBodyPacket, CSsoDataPacket, IncomingPacket 20 | 21 | if TYPE_CHECKING: 22 | from cai.client import Client 23 | 24 | DEVICE = get_device() 25 | APK_INFO = get_protocol() 26 | 27 | 28 | def encode_heartbeat( 29 | seq: int, session_id: bytes, ksid: bytes, uin: int 30 | ) -> Packet: 31 | """Build heartbeat alive packet. 32 | 33 | Called in `com.tencent.mobileqq.msf.core.C26002ac.A`. 34 | 35 | command name: `Heartbeat.Alive` 36 | 37 | Note: 38 | Source: oicq.wlogin_sdk.request.n 39 | 40 | Args: 41 | seq (int): Packet sequence. 42 | session_id (bytes): Session ID. 43 | ksid (bytes): KSID of client. 44 | uin (int): User QQ number. 45 | 46 | Returns: 47 | Packet: Login packet. 48 | """ 49 | COMMAND_NAME = "Heartbeat.Alive" 50 | 51 | SUB_APP_ID = APK_INFO.sub_app_id 52 | 53 | sso_packet = CSsoBodyPacket.build( 54 | seq, SUB_APP_ID, COMMAND_NAME, DEVICE.imei, session_id, ksid, bytes() 55 | ) 56 | packet = CSsoDataPacket.build(uin, 0, sso_packet, key=None) 57 | return packet 58 | 59 | 60 | @dataclass 61 | class Heartbeat(Command): 62 | pass 63 | 64 | 65 | async def handle_heartbeat( 66 | client: "Client", packet: IncomingPacket 67 | ) -> Heartbeat: 68 | return Heartbeat( 69 | packet.uin, packet.seq, packet.ret_code, packet.command_name 70 | ) 71 | 72 | 73 | __all__ = ["encode_heartbeat", "handle_heartbeat", "Heartbeat"] 74 | -------------------------------------------------------------------------------- /cai/client/message_service/command.py: -------------------------------------------------------------------------------- 1 | """MessageSvc Command Parser. 2 | 3 | This module is used to parse MessageSvc response packets into command. 4 | 5 | :Copyright: Copyright (C) 2021-2021 cscs181 6 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 7 | 8 | .. _LICENSE: 9 | https://github.com/cscs181/CAI/blob/master/LICENSE 10 | """ 11 | 12 | from dataclasses import dataclass 13 | 14 | from cai.client.command import Command 15 | from cai.pb.msf.msg.svc import PbGetMsgResp 16 | from cai.utils.jce import RequestPacketVersion2 17 | 18 | from .jce import RequestPushNotify, RequestPushForceOffline 19 | 20 | 21 | @dataclass 22 | class GetMessageCommand(Command): 23 | @classmethod 24 | def decode_response( 25 | cls, uin: int, seq: int, ret_code: int, command_name: str, data: bytes 26 | ) -> "GetMessageCommand": 27 | """Decode MessageSvc get message response packet. 28 | 29 | Note: 30 | Source: c2c 1002 31 | 32 | com.tencent.mobileqq.app.handler.receivesuccess.MessageSvcPbGetMsg 33 | 34 | com.tencent.mobileqq.app.MessageHandler.h 35 | 36 | com.tencent.imcore.message.C2CMessageProcessor.b 37 | 38 | Args: 39 | uin (int): User QQ 40 | seq (int): Sequence number of the response packet. 41 | ret_code (int): Return code of the response. 42 | command_name (str): Command name of the response. 43 | data (bytes): Payload data of the response. 44 | """ 45 | if ret_code != 0 or not data: 46 | return GetMessageCommand(uin, seq, ret_code, command_name) 47 | 48 | try: 49 | result = PbGetMsgResp.FromString(data) 50 | return GetMessageSuccess(uin, seq, ret_code, command_name, result) 51 | except Exception as e: 52 | return GetMessageFail( 53 | uin, 54 | seq, 55 | ret_code, 56 | command_name, 57 | f"Error when decoding response! {repr(e)}", 58 | ) 59 | 60 | 61 | @dataclass 62 | class GetMessageSuccess(GetMessageCommand): 63 | response: PbGetMsgResp 64 | 65 | 66 | @dataclass 67 | class GetMessageFail(GetMessageCommand): 68 | message: str 69 | 70 | 71 | @dataclass 72 | class PushNotifyCommand(Command): 73 | @classmethod 74 | def decode_response( 75 | cls, uin: int, seq: int, ret_code: int, command_name: str, data: bytes 76 | ) -> "PushNotifyCommand": 77 | """Decode MessageSvc push notify packet. 78 | 79 | Note: 80 | Source: com.tencent.mobileqq.service.message.MessageFactoryReceiver.f 81 | 82 | Args: 83 | uin (int): User QQ 84 | seq (int): Sequence number of the response packet. 85 | ret_code (int): Return code of the response. 86 | command_name (str): Command name of the response. 87 | data (bytes): Payload data of the response. 88 | """ 89 | if ret_code != 0 or not data: 90 | return PushNotifyCommand(uin, seq, ret_code, command_name) 91 | 92 | try: 93 | # data offset 4 in source? test get 15 94 | # req_packet = RequestPacketVersion2.decode(data[4:]) 95 | req_packet = RequestPacketVersion2.decode(data[15:]) 96 | push_offline_request = RequestPushNotify.decode( 97 | req_packet.data["req_PushNotify"][ # type: ignore 98 | "PushNotifyPack.RequestPushNotify" 99 | ][1:-1] 100 | ) 101 | return PushNotify( 102 | uin, seq, ret_code, command_name, push_offline_request 103 | ) 104 | except Exception as e: 105 | return PushNotifyError( 106 | uin, 107 | seq, 108 | ret_code, 109 | command_name, 110 | f"Error when decoding response! {repr(e)}", 111 | ) 112 | 113 | 114 | @dataclass 115 | class PushNotify(PushNotifyCommand): 116 | notify: RequestPushNotify 117 | 118 | 119 | @dataclass 120 | class PushNotifyError(PushNotifyCommand): 121 | message: str 122 | 123 | 124 | @dataclass 125 | class PushForceOfflineCommand(Command): 126 | @classmethod 127 | def decode_response( 128 | cls, uin: int, seq: int, ret_code: int, command_name: str, data: bytes 129 | ) -> "PushForceOfflineCommand": 130 | """Decode MessageSvc Force Offline request. 131 | 132 | Note: 133 | Source: mqq.app.MainService 134 | 135 | Args: 136 | uin (int): User QQ 137 | seq (int): Sequence number of the response packet. 138 | ret_code (int): Return code of the response. 139 | command_name (str): Command name of the response. 140 | data (bytes): Payload data of the response. 141 | """ 142 | if ret_code != 0 or not data: 143 | return PushForceOfflineCommand(uin, seq, ret_code, command_name) 144 | 145 | try: 146 | req_packet = RequestPacketVersion2.decode(data) 147 | push_offline_request = RequestPushForceOffline.decode( 148 | req_packet.data["req_PushForceOffline"][ # type: ignore 149 | "PushNotifyPack.RequestPushForceOffline" 150 | ][1:-1] 151 | ) 152 | return PushForceOffline( 153 | uin, seq, ret_code, command_name, push_offline_request 154 | ) 155 | except Exception as e: 156 | return PushForceOfflineError( 157 | uin, 158 | seq, 159 | ret_code, 160 | command_name, 161 | f"Error when decoding response! {repr(e)}", 162 | ) 163 | 164 | 165 | @dataclass 166 | class PushForceOffline(PushForceOfflineCommand): 167 | request: RequestPushForceOffline 168 | 169 | 170 | @dataclass 171 | class PushForceOfflineError(PushForceOfflineCommand): 172 | message: str 173 | -------------------------------------------------------------------------------- /cai/client/message_service/jce.py: -------------------------------------------------------------------------------- 1 | """MessageSvc Packet Builder. 2 | 3 | This module is used to build and handle MessageSvc packets. 4 | 5 | :Copyright: Copyright (C) 2021-2021 cscs181 6 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 7 | 8 | .. _LICENSE: 9 | https://github.com/cscs181/CAI/blob/master/LICENSE 10 | """ 11 | 12 | from typing import Optional 13 | 14 | from jce import JceField, JceStruct, types 15 | 16 | from cai.client.qq_service.jce import StShareData 17 | 18 | 19 | # Push Notify 20 | class CPicInfo(JceStruct): 21 | """MessageSvc Online Push CPic Info jce packet. 22 | 23 | Note: 24 | Source: OnlinePushPack.CPicInfo 25 | """ 26 | 27 | path: types.BYTES = JceField(jce_id=0) 28 | host: types.BYTES = JceField(bytes(), jce_id=1) 29 | 30 | 31 | class TempMsgHead(JceStruct): 32 | """MessageSvc Online Push Temp Message Head jce packet. 33 | 34 | Note: 35 | Source: OnlinePushPack.TempMsgHead 36 | """ 37 | 38 | c2c_type: types.INT32 = JceField(0, jce_id=0) 39 | service_type: types.INT32 = JceField(0, jce_id=1) 40 | 41 | 42 | class MessageInfo(JceStruct): 43 | """MessageSvc Online Push Message Info jce packet. 44 | 45 | Note: 46 | Source: OnlinePushPack.MsgInfo 47 | """ 48 | 49 | from_uin: types.INT64 = JceField(jce_id=0) 50 | message_time: types.INT64 = JceField(jce_id=1) 51 | message_type: types.INT16 = JceField(jce_id=2) 52 | message_seq: types.INT16 = JceField(jce_id=3) 53 | message: types.STRING = JceField(jce_id=4) 54 | real_message_time: types.INT32 = JceField(0, jce_id=5) 55 | vec_message: types.BYTES = JceField(bytes(), jce_id=6) 56 | app_share_id: types.INT64 = JceField(0, jce_id=7) 57 | message_cookies: types.BYTES = JceField(bytes(), jce_id=8) 58 | app_share_cookie: types.BYTES = JceField(bytes(), jce_id=9) 59 | message_uid: types.INT64 = JceField(0, jce_id=10) 60 | last_change_time: types.INT64 = JceField(0, jce_id=11) 61 | cpic_info: types.LIST[CPicInfo] = JceField([], jce_id=12) 62 | share_data: Optional[StShareData] = JceField(None, jce_id=13) 63 | from_inst_id: types.INT64 = JceField(0, jce_id=14) 64 | remark_of_sender: types.BYTES = JceField(bytes(), jce_id=15) 65 | from_mobile: types.STRING = JceField("", jce_id=16) 66 | from_name: types.STRING = JceField("", jce_id=17) 67 | nickname: types.LIST[types.STRING] = JceField([], jce_id=18) 68 | c2c_temp_msg_head: Optional[TempMsgHead] = JceField(None, jce_id=19) 69 | 70 | 71 | class RequestPushNotify(JceStruct): 72 | """MessageSvc Push Notify Request jce packet. 73 | 74 | Note: 75 | Source: PushNotifyPack.RequestPushNotify 76 | """ 77 | 78 | uin: types.INT64 = JceField(jce_id=0) 79 | type: types.INT8 = JceField(jce_id=1) 80 | service: types.STRING = JceField(jce_id=2) 81 | cmd: types.STRING = JceField(jce_id=3) 82 | notify_cookie: types.BYTES = JceField(bytes(), jce_id=4) 83 | message_type: types.INT32 = JceField(0, jce_id=5) 84 | user_active: types.INT32 = JceField(0, jce_id=6) 85 | general_flag: types.INT32 = JceField(0, jce_id=7) 86 | binded_uin: types.INT64 = JceField(0, jce_id=8) 87 | message_info: Optional[MessageInfo] = JceField(None, jce_id=9) 88 | message_ctrl_buf: types.STRING = JceField("", jce_id=10) 89 | server_buf: types.BYTES = JceField(bytes(), jce_id=11) 90 | ping_flag: types.INT64 = JceField(0, jce_id=12) 91 | svrip: types.INT64 = JceField(0, jce_id=13) 92 | 93 | 94 | # Push Force Offline 95 | class RequestPushForceOffline(JceStruct): 96 | """MessageSvc Push Force Offline Request jce packet. 97 | 98 | Note: 99 | Source: PushNotifyPack.RequestPushForceOffline 100 | """ 101 | 102 | uin: types.INT64 = JceField(jce_id=0) 103 | title: types.STRING = JceField("", jce_id=1) 104 | tips: types.STRING = JceField("", jce_id=2) 105 | same_device: types.BOOL = JceField(False, jce_id=3) 106 | -------------------------------------------------------------------------------- /cai/client/message_service/models.py: -------------------------------------------------------------------------------- 1 | """MessageSvc message models. 2 | 3 | This module is used to define message models. 4 | 5 | :Copyright: Copyright (C) 2021-2021 cscs181 6 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 7 | 8 | .. _LICENSE: 9 | https://github.com/cscs181/CAI/blob/master/LICENSE 10 | """ 11 | 12 | import abc 13 | from dataclasses import dataclass 14 | from typing import List, Optional 15 | 16 | from cai.client.event import Event 17 | from cai.pb.msf.msg.comm import Msg 18 | 19 | 20 | @dataclass 21 | class PrivateMessage(Event): 22 | _msg: Msg 23 | seq: int 24 | time: int 25 | auto_reply: bool 26 | from_uin: int 27 | from_nick: str 28 | to_uin: int 29 | message: List["Element"] 30 | 31 | @property 32 | def type(self) -> str: 33 | return "private_message" 34 | 35 | 36 | @dataclass 37 | class GroupMessage(Event): 38 | _msg: Msg 39 | seq: int 40 | time: int 41 | group_id: int 42 | group_name: str 43 | group_level: int 44 | from_uin: int 45 | from_group_card: str 46 | message: List["Element"] 47 | 48 | @property 49 | def type(self) -> str: 50 | return "group_message" 51 | 52 | 53 | class Element(abc.ABC): 54 | @property 55 | @abc.abstractmethod 56 | def type(self) -> str: 57 | raise NotImplementedError 58 | 59 | 60 | @dataclass 61 | class ReplyElement(Element): 62 | seq: int 63 | time: int 64 | sender: int 65 | message: List[Element] 66 | troop_name: Optional[str] 67 | 68 | @property 69 | def type(self) -> str: 70 | return "reply" 71 | 72 | 73 | @dataclass 74 | class TextElement(Element): 75 | content: str 76 | 77 | @property 78 | def type(self) -> str: 79 | return "text" 80 | 81 | 82 | @dataclass 83 | class FaceElement(Element): 84 | id: int 85 | 86 | @property 87 | def type(self) -> str: 88 | return "face" 89 | 90 | 91 | @dataclass 92 | class SmallEmojiElement(Element): 93 | id: int 94 | text: str 95 | # byte: bytes 96 | 97 | @property 98 | def type(self) -> str: 99 | return "small_emoji" 100 | 101 | 102 | @dataclass 103 | class ImageElement(Element): 104 | filename: str 105 | size: int 106 | width: int 107 | height: int 108 | md5: bytes 109 | url: str 110 | 111 | @property 112 | def type(self) -> str: 113 | return "image" 114 | 115 | 116 | @dataclass 117 | class PokeElement(Element): 118 | id: int 119 | name: str 120 | strength: int 121 | double_hit: int 122 | 123 | @property 124 | def type(self) -> str: 125 | return "poke" 126 | -------------------------------------------------------------------------------- /cai/client/online_push/command.py: -------------------------------------------------------------------------------- 1 | """MessageSvc Command Parser. 2 | 3 | This module is used to parse MessageSvc response packets into command. 4 | 5 | :Copyright: Copyright (C) 2021-2021 cscs181 6 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 7 | 8 | .. _LICENSE: 9 | https://github.com/cscs181/CAI/blob/master/LICENSE 10 | """ 11 | 12 | from dataclasses import dataclass 13 | 14 | from cai.client.command import Command 15 | from cai.pb.msf.msg.onlinepush import PbPushMsg 16 | 17 | 18 | @dataclass 19 | class PushMsgCommand(Command): 20 | @classmethod 21 | def decode_response( 22 | cls, uin: int, seq: int, ret_code: int, command_name: str, data: bytes 23 | ) -> "PushMsgCommand": 24 | """Decode OnlinePush push message packet. 25 | 26 | Including commands: ``OnlinePush.PbPushGroupMsg``, 27 | ``OnlinePush.PbPushDisMsg``, ``OnlinePush.PbC2CMsgSync``, 28 | ``OnlinePush.PbPushC2CMsg``, ``OnlinePush.PbPushBindUinGroupMsg`` 29 | 30 | Note: 31 | Source: com.tencent.mobileqq.app.MessageHandler.b 32 | 33 | Args: 34 | uin (int): User QQ 35 | seq (int): Sequence number of the response packet. 36 | ret_code (int): Return code of the response. 37 | command_name (str): Command name of the response. 38 | data (bytes): Payload data of the response. 39 | """ 40 | if ret_code != 0 or not data: 41 | return PushMsgCommand(uin, seq, ret_code, command_name) 42 | 43 | try: 44 | push_message = PbPushMsg.FromString(data) 45 | return PushMsg(uin, seq, ret_code, command_name, push_message) 46 | except Exception as e: 47 | return PushMsgError( 48 | uin, 49 | seq, 50 | ret_code, 51 | command_name, 52 | f"Error when decoding response! {repr(e)}", 53 | ) 54 | 55 | 56 | @dataclass 57 | class PushMsg(PushMsgCommand): 58 | push: PbPushMsg 59 | 60 | 61 | @dataclass 62 | class PushMsgError(PushMsgCommand): 63 | message: str 64 | -------------------------------------------------------------------------------- /cai/client/online_push/jce.py: -------------------------------------------------------------------------------- 1 | """OnlinePush Packet Builder. 2 | 3 | This module is used to build and handle OnlinePush packets. 4 | 5 | :Copyright: Copyright (C) 2021-2021 cscs181 6 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 7 | 8 | .. _LICENSE: 9 | https://github.com/cscs181/CAI/blob/master/LICENSE 10 | """ 11 | 12 | from typing import Optional 13 | 14 | from jce import JceField, JceStruct, types 15 | 16 | 17 | class DelMsgInfo(JceStruct): 18 | """OnlinePush Delete Message Info Packet. 19 | 20 | Note: 21 | Source: OnlinePushPack.DelMsgInfo 22 | """ 23 | 24 | from_uin: types.INT64 = JceField(jce_id=0) 25 | msg_time: types.INT64 = JceField(jce_id=1) 26 | msg_seq: types.INT8 = JceField(jce_id=2) 27 | msg_cookies: types.BYTES = JceField(bytes(), jce_id=3) 28 | cmd: types.INT8 = JceField(0, jce_id=4) 29 | msg_type: types.INT64 = JceField(0, jce_id=5) 30 | app_id: types.INT64 = JceField(0, jce_id=6) 31 | send_time: types.INT64 = JceField(0, jce_id=7) 32 | sso_seq: types.INT32 = JceField(0, jce_id=8) 33 | sso_ip: types.INT32 = JceField(0, jce_id=9) 34 | client_ip: types.INT32 = JceField(0, jce_id=10) 35 | 36 | 37 | class DeviceInfo(JceStruct): 38 | """OnlinePush Device Info Packet. 39 | 40 | Note: 41 | Source: OnlinePushPack.DeviceInfo 42 | """ 43 | 44 | net_type: types.BYTE = JceField(bytes(1), jce_id=0) 45 | dev_type: types.STRING = JceField("", jce_id=1) 46 | os_ver: types.STRING = JceField("", jce_id=2) 47 | vendor_name: types.STRING = JceField("", jce_id=3) 48 | vendor_os_name: types.STRING = JceField("", jce_id=4) 49 | ios_idfa: types.STRING = JceField("", jce_id=5) 50 | 51 | 52 | class SvcRespPushMsg(JceStruct): 53 | """OnlinePush Service Push Response Packet. 54 | 55 | Note: 56 | Source: OnlinePushPack.SvcRespPushMsg 57 | """ 58 | 59 | uin: types.INT64 = JceField(jce_id=0) 60 | del_infos: types.LIST[DelMsgInfo] = JceField(jce_id=1) 61 | svrip: types.INT32 = JceField(jce_id=2) 62 | push_token: Optional[types.BYTES] = JceField(None, jce_id=3) 63 | service_type: types.INT32 = JceField(0, jce_id=4) 64 | device_info: Optional[DeviceInfo] = JceField(None, jce_id=5) 65 | -------------------------------------------------------------------------------- /cai/client/qq_service/__init__.py: -------------------------------------------------------------------------------- 1 | """QQService Related SDK. 2 | 3 | This module is used to build and handle qq service related packet. 4 | 5 | :Copyright: Copyright (C) 2021-2021 cscs181 6 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 7 | 8 | .. _LICENSE: 9 | https://github.com/cscs181/CAI/blob/master/LICENSE 10 | """ 11 | -------------------------------------------------------------------------------- /cai/client/qq_service/jce.py: -------------------------------------------------------------------------------- 1 | """QQ Service Packet Builder. 2 | 3 | This module is used to build and handle qq service packets. 4 | 5 | :Copyright: Copyright (C) 2021-2021 cscs181 6 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 7 | 8 | .. _LICENSE: 9 | https://github.com/cscs181/CAI/blob/master/LICENSE 10 | """ 11 | 12 | from jce import JceField, JceStruct, types 13 | 14 | 15 | class StShareData(JceStruct): 16 | """Share Data Jce Packet. 17 | 18 | Note: 19 | Source: QQService.ShareData 20 | """ 21 | 22 | pkg_name: types.STRING = JceField(jce_id=0) 23 | msgtail: types.STRING = JceField(jce_id=1) 24 | pic_url: types.STRING = JceField(jce_id=2) 25 | url: types.STRING = JceField(jce_id=3) 26 | 27 | 28 | class VipOpenInfo(JceStruct): 29 | """Vip Open Info Jce Packet. 30 | 31 | Note: 32 | Source: QQService.VipOpenInfo 33 | """ 34 | 35 | open: types.BOOL = JceField(jce_id=0) 36 | vip_type: types.INT32 = JceField(jce_id=1) 37 | vip_level: types.INT32 = JceField(jce_id=2) 38 | vip_flag: types.INT32 = JceField(0, jce_id=3) 39 | nameplate_id: types.INT64 = JceField(0, jce_id=4) 40 | 41 | 42 | class VipBaseInfo(JceStruct): 43 | """Vip Base Info Jce Packet. 44 | 45 | Note: 46 | Source: QQService.VipBaseInfo 47 | """ 48 | 49 | open_info: types.MAP[types.INT, VipOpenInfo] = JceField(jce_id=0) 50 | nameplate_vip_type: types.INT32 = JceField(0, jce_id=1) 51 | gray_nameplate_flag: types.INT32 = JceField(0, jce_id=2) 52 | extend_nameplate_id: types.STRING = JceField("", jce_id=3) 53 | -------------------------------------------------------------------------------- /cai/client/sso_server/__init__.py: -------------------------------------------------------------------------------- 1 | """SSO Server SDK. 2 | 3 | This module is used to get server list and choose the best one. 4 | 5 | :Copyright: Copyright (C) 2021-2021 cscs181:License: AGPL-3.0 or later. See `LICENSE`_ for detail. 6 | 7 | .. _LICENSE: 8 | https://github.com/cscs181/CAI/blob/master/LICENSE 9 | """ 10 | import asyncio 11 | import http.client 12 | from io import BytesIO 13 | from typing import List, Tuple, Union, Iterable, Optional, Container 14 | 15 | from jce import types 16 | from rtea import qqtea_decrypt, qqtea_encrypt 17 | 18 | from cai.connection import connect 19 | from cai.settings.device import get_device 20 | from cai.exceptions import SsoServerException 21 | from cai.settings.protocol import get_protocol 22 | from cai.utils.jce import RequestPacketVersion3 23 | from cai.connection.utils import tcp_latency_test 24 | 25 | from .jce import SsoServer, SsoServerRequest, SsoServerResponse 26 | 27 | _cached_server: Optional["SsoServer"] = None 28 | _cached_servers: List["SsoServer"] = [] 29 | 30 | 31 | class _FakeSocket: 32 | """Fake socket object. 33 | 34 | This class is used to wrap raw response bytes 35 | and pass them to :class:`http.client.HTTPResponse`. 36 | 37 | Args: 38 | response (:obj:`bytes`): HTTP raw response from server. 39 | """ 40 | 41 | def __init__(self, response: bytes): 42 | self._file = BytesIO(response) 43 | 44 | def makefile(self, *args, **kwargs): 45 | return self._file 46 | 47 | 48 | async def get_sso_list() -> SsoServerResponse: 49 | """Do sso server list request and return the response. 50 | 51 | Note: 52 | Source: com.tencent.mobileqq.msf.core.a.f.run 53 | 54 | Returns: 55 | SsoServerResponse: Sso server list response 56 | 57 | Raises: 58 | SsoServerException: Get sso server list failed. 59 | """ 60 | device = get_device() 61 | protocol = get_protocol() 62 | key = bytes( 63 | [ 64 | 0xF0, 65 | 0x44, 66 | 0x1F, 67 | 0x5F, 68 | 0xF4, 69 | 0x2D, 70 | 0xA5, 71 | 0x8F, 72 | 0xDC, 73 | 0xF7, 74 | 0x94, 75 | 0x9A, 76 | 0xBA, 77 | 0x62, 78 | 0xD4, 79 | 0x11, 80 | ] 81 | ) 82 | payload = SsoServerRequest.to_bytes( 83 | 0, SsoServerRequest(app_id=protocol.app_id, imei=device.imei) 84 | ) 85 | req_packet = RequestPacketVersion3( 86 | servant_name="HttpServerListReq", 87 | func_name="HttpServerListReq", 88 | data=types.MAP( 89 | {types.STRING("HttpServerListReq"): types.BYTES(payload)} 90 | ), 91 | ).encode(with_length=True) 92 | buffer: bytes = qqtea_encrypt(req_packet, key) 93 | async with connect("configsvr.msf.3g.qq.com", 443, ssl=True) as conn: 94 | query = ( 95 | b"POST /configsvr/serverlist.jsp HTTP/1.1\r\n" 96 | b"Host: configsvr.msf.3g.qq.com\r\n" 97 | b"User-Agent: QQ/8.4.1.2703 CFNetwork/1126\r\n" 98 | b"Net-Type: Wifi\r\n" 99 | b"Accept: */*\r\n" 100 | b"Connection: close\r\n" 101 | b"Content-Type: application/octet-stream\r\n" 102 | b"Content-Length: " + str(len(buffer)).encode() + b"\r\n" 103 | b"\r\n" + buffer 104 | ) 105 | conn.write(query) 106 | conn.write_eof() 107 | resp_bytes = await conn.read_all() 108 | response = http.client.HTTPResponse( 109 | _FakeSocket(resp_bytes) # type: ignore 110 | ) 111 | response.begin() 112 | 113 | if response.status != 200: 114 | raise SsoServerException( 115 | f"Get sso server list failed with response code {response.status}" 116 | ) 117 | data: bytes = qqtea_decrypt(response.read(), key) 118 | resp_packet = RequestPacketVersion3.decode(data[4:]) 119 | server_info = SsoServerResponse.decode( 120 | resp_packet.data["HttpServerListRes"][1:-1] # type: ignore 121 | ) 122 | return server_info 123 | 124 | 125 | async def quality_test( 126 | servers: Iterable[SsoServer], threshold: float = 500.0 127 | ) -> List[Tuple[SsoServer, float]]: 128 | """Test given servers' quality by tcp latency. 129 | 130 | Args: 131 | servers (Iterable[:obj:`.SsoServer`]): Server list to test. 132 | threshold (float, optional): Latency more than threshold will not be returned. 133 | Defaults to 500.0. 134 | 135 | Returns: 136 | List[Tuple[:obj:`.SsoServer`, float]]: List of server and latency in tuple. 137 | """ 138 | servers_ = list(servers) 139 | tasks = [tcp_latency_test(server.host, server.port) for server in servers_] 140 | result: List[Union[float, Exception]] = await asyncio.gather( 141 | *tasks, return_exceptions=True 142 | ) 143 | success_servers = [ 144 | (server, latency) 145 | for server, latency in zip(servers_, result) 146 | if isinstance(latency, float) and latency < threshold 147 | ] 148 | return success_servers 149 | 150 | 151 | async def get_sso_server( 152 | cache: bool = True, 153 | cache_server_list: bool = True, 154 | exclude: Optional[Container[str]] = None, 155 | ) -> SsoServer: 156 | """Get the best sso server 157 | 158 | Args: 159 | cache (bool, optional): Using cache server or not. Defaults to True. 160 | cache_server_list (bool, optional): Using cache server list or not. Defaults to True. 161 | exclude (List[str], optional): List of servers' ip want to be excluded 162 | 163 | Returns: 164 | :obj:`.SsoServer`: The best server with smallest latency. 165 | """ 166 | global _cached_server 167 | if cache and _cached_server: 168 | return _cached_server 169 | 170 | if cache_server_list and _cached_servers: 171 | servers = _cached_servers 172 | else: 173 | sso_list = await get_sso_list() 174 | servers = [*sso_list.socket_v4_mobile, *sso_list.socket_v4_wifi] 175 | _cached_servers.clear() 176 | _cached_servers.extend(servers) 177 | 178 | exclude_server = exclude or [] 179 | success_servers = await quality_test( 180 | server for server in servers if server.host not in exclude_server 181 | ) 182 | success_servers.sort(key=lambda x: x[1]) 183 | _cached_server = success_servers[0][0] 184 | return _cached_server 185 | -------------------------------------------------------------------------------- /cai/client/sso_server/jce.py: -------------------------------------------------------------------------------- 1 | """SSO Server Packet Builder. 2 | 3 | This module is used to build sso server packets. 4 | 5 | :Copyright: Copyright (C) 2021-2021 cscs181:License: AGPL-3.0 or later. See `LICENSE`_ for detail. 6 | 7 | .. _LICENSE: 8 | https://github.com/cscs181/CAI/blob/master/LICENSE 9 | """ 10 | 11 | from jce import JceField, JceStruct, types 12 | 13 | 14 | class SsoServerRequest(JceStruct): 15 | """Sso Server List Request. 16 | 17 | Note: 18 | Source: com.tencent.msf.service.protocol.serverconfig.d 19 | """ 20 | 21 | uin: types.INT64 = JceField(0, jce_id=1) # uin or 0 22 | """:obj:`~jce.types.INT64`: User QQ or 0, renamed from a.""" 23 | timeout: types.INT64 = JceField(0, jce_id=2) 24 | """:obj:`~jce.types.INT64`: may be timeout (s), default is 60, renamed from b.""" 25 | c: types.BYTE = JceField(bytes([1]), jce_id=3) 26 | """:obj:`~jce.types.BYTE`: always 1.""" 27 | imsi: types.STRING = JceField("46000", jce_id=4) 28 | """:obj:`~jce.types.STRING`: imsi string, renamed from d. 29 | 30 | ``null``->``""`` or ``imsi.substring(0, 5)``. 31 | """ 32 | is_wifi: types.INT32 = JceField(100, jce_id=5) 33 | """:obj:`~jce.types.INT32`: is_wifi, renamed from e. 34 | 35 | NetConnInfoCenter.isWifiConn: true->100, false->1. 36 | """ 37 | app_id: types.INT64 = JceField(jce_id=6) 38 | """:obj:`~jce.types.INT64`: app_id, renamed from f. 39 | 40 | same to :attr:`cai.settings.protocol.ApkInfo.app_id`. 41 | """ 42 | imei: types.STRING = JceField(jce_id=7) 43 | """:obj:`~jce.types.STRING`: imei, renamed from g. 44 | 45 | same to :attr:`cai.settings.device.DeviceInfo.imei`. 46 | """ 47 | cell_id: types.INT64 = JceField(0, jce_id=8) 48 | """:obj:`~jce.types.INT64`: cell_id, renamed from h. 49 | 50 | cell location get from ``CdmaCellLocation.getBaseStationId``. 51 | """ 52 | i: types.INT64 = JceField(0, jce_id=9) 53 | """:obj:`~jce.types.INT64`: unknown.""" 54 | j: types.INT64 = JceField(0, jce_id=10) 55 | """:obj:`~jce.types.INT64`: unknown.""" 56 | k: types.BYTE = JceField(bytes(1), jce_id=11) 57 | """:obj:`~jce.types.BYTE`: unknown boolean, true->1, false->0.""" 58 | l: types.BYTE = JceField(bytes(1), jce_id=12) 59 | """:obj:`~jce.types.BYTE`: active net ip family. 60 | 61 | get from ``NetConnInfoCenter.getActiveNetIpFamily``. 62 | """ 63 | m: types.INT64 = JceField(0, jce_id=13) 64 | """:obj:`~jce.types.INT64`: unknown.""" 65 | 66 | 67 | class SsoServer(JceStruct): 68 | """Sso Server Info. 69 | 70 | Note: 71 | Source: com.tencent.msf.service.protocol.serverconfig.i 72 | """ 73 | 74 | host: types.STRING = JceField(jce_id=1) 75 | """:obj:`~jce.types.STRING`: server host, renamed from a.""" 76 | port: types.INT32 = JceField(jce_id=2) 77 | """:obj:`~jce.types.INT32`: server port, renamed from b.""" 78 | # c: types.BYTE = JceField(jce_id=3) 79 | # d: types.BYTE = JceField(jce_id=4) 80 | protocol: types.BYTE = JceField(jce_id=5) 81 | """:obj:`~jce.types.BYTE`: protocol, renamed from e. 82 | 83 | 0, 1: socket; 2, 3: http. 84 | """ 85 | # f: types.INT32 = JceField(jce_id=6) 86 | # g: types.BYTE = JceField(jce_id=7) 87 | city: types.STRING = JceField(jce_id=8) 88 | """:obj:`~jce.types.STRING`: city, renamed from h.""" 89 | country: types.STRING = JceField(jce_id=9) 90 | """:obj:`~jce.types.STRING`: country, renamed from i.""" 91 | 92 | 93 | class SsoServerResponse(JceStruct): 94 | """Sso Server List Response. 95 | 96 | Note: 97 | Source: com.tencent.msf.service.protocol.serverconfig.e 98 | """ 99 | 100 | # a: types.INT32 = JceField(0, jce_id=1) 101 | socket_v4_mobile: types.LIST[SsoServer] = JceField(jce_id=2) 102 | """:obj:`~jce.types.LIST` of :obj:`.SsoServer`: 103 | socket ipv4 mobile server, renamed from b. 104 | """ 105 | socket_v4_wifi: types.LIST[SsoServer] = JceField(jce_id=3) 106 | """:obj:`~jce.types.LIST` of :obj:`.SsoServer`: 107 | socket ipv4 wifi server, renamed from c. 108 | """ 109 | # d: types.INT32 = JceField(0, jce_id=4) 110 | # e: types.INT32 = JceField(86400, jce_id=5) 111 | # f: types.BYTE = JceField(bytes(1), jce_id=6) 112 | # g: types.BYTE = JceField(bytes(1), jce_id=7) 113 | # h: types.INT32 = JceField(1, jce_id=8) 114 | # i: types.INT32 = JceField(5, jce_id=9) 115 | # j: types.INT64 = JceField(0, jce_id=10) 116 | # k: types.INT32 = JceField(0, jce_id=11) 117 | http_v4_mobile: types.LIST[SsoServer] = JceField(jce_id=12) 118 | """:obj:`~jce.types.LIST` of :obj:`.SsoServer`: 119 | http ipv4 mobile server, renamed from l. 120 | """ 121 | http_v4_wifi: types.LIST[SsoServer] = JceField(jce_id=13) 122 | """:obj:`~jce.types.LIST` of :obj:`.SsoServer`: 123 | http ipv4 wifi server, renamed from m. 124 | """ 125 | speed_info: types.BYTES = JceField(jce_id=14) 126 | """:obj:`~jce.types.BYTES`: vCesuInfo, renamed from n. 127 | 128 | bytes from :class:`~cai.utils.jce.RequestPacketVersion3` (QualityTest) 129 | """ 130 | socket_v6: types.LIST[SsoServer] = JceField(jce_id=15) 131 | """:obj:`~jce.types.LIST` of :obj:`.SsoServer`: 132 | socket ipv6 server, renamed from o. 133 | 134 | used when (wifi and :attr:`~.SsoServerResponse.nettype` & 1 == 1) 135 | or (mobile and :attr:`~.SsoServerResponse.nettype` & 2 == 2) 136 | """ 137 | http_v6: types.LIST[SsoServer] = JceField(jce_id=16) 138 | """:obj:`~jce.types.LIST` of :obj:`.SsoServer`: 139 | http ipv6 server, renamed from p. 140 | 141 | used when (wifi and :attr:`~.SsoServerResponse.nettype` & 1 == 1) 142 | or (mobile and :attr:`~.SsoServerResponse.nettype` & 2 == 2) 143 | """ 144 | udp_v6: types.LIST[SsoServer] = JceField(jce_id=17) 145 | """:obj:`~jce.types.LIST` of :obj:`.SsoServer`: 146 | quic ipv6 server, renamed from q. 147 | 148 | used when (wifi and :attr:`~.SsoServerResponse.nettype` & 1 == 1) 149 | or (mobile and :attr:`~.SsoServerResponse.nettype` & 2 == 2) 150 | """ 151 | nettype: types.BYTE = JceField(bytes(1), jce_id=18) 152 | """:obj:`~jce.types.BYTE`: nettype, renamed from r.""" 153 | delay_threshold: types.INT32 = JceField(0, jce_id=19) 154 | """:obj:`~jce.types.INT32`: delay threshold, renamed from s.""" 155 | policy_id: types.STRING = JceField("", jce_id=20) 156 | """:obj:`~jce.types.STRING`: policy id, renamed from t.""" 157 | -------------------------------------------------------------------------------- /cai/client/status_service/command.py: -------------------------------------------------------------------------------- 1 | """StatSvc Command Parser. 2 | 3 | This module is used to parse StatSvc response packets into command. 4 | 5 | :Copyright: Copyright (C) 2021-2021 cscs181 6 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 7 | 8 | .. _LICENSE: 9 | https://github.com/cscs181/CAI/blob/master/LICENSE 10 | """ 11 | 12 | from typing import Optional 13 | from dataclasses import dataclass 14 | 15 | from cai.client.command import Command 16 | from cai.utils.jce import RequestPacketVersion2, RequestPacketVersion3 17 | 18 | from .jce import SvcRespRegister, RequestMSFForceOffline 19 | 20 | 21 | @dataclass 22 | class SvcRegisterResponse(Command): 23 | @classmethod 24 | def decode_response( 25 | cls, uin: int, seq: int, ret_code: int, command_name: str, data: bytes 26 | ) -> "SvcRegisterResponse": 27 | """Decode StatSvc register response. 28 | 29 | Note: 30 | Source: com.tencent.mobileqq.servlet.PushServlet.onReceive 31 | 32 | Args: 33 | uin (int): User QQ 34 | seq (int): Sequence number of the response packet. 35 | ret_code (int): Return code of the response. 36 | command_name (str): Command name of the response. 37 | data (bytes): Payload data of the response. 38 | 39 | Returns: 40 | RegisterSuccess: register success. 41 | RegisterFail: register failed. 42 | """ 43 | if ret_code != 0 or not data: 44 | return SvcRegisterResponse(uin, seq, ret_code, command_name) 45 | 46 | try: 47 | resp_packet = RequestPacketVersion2.decode(data) 48 | svc_register_response = SvcRespRegister.decode( 49 | resp_packet.data["SvcRespRegister"][ # type: ignore 50 | "QQService.SvcRespRegister" 51 | ][1:-1] 52 | ) 53 | return RegisterSuccess( 54 | uin, seq, ret_code, command_name, svc_register_response 55 | ) 56 | except Exception as e: 57 | return RegisterFail( 58 | uin, 59 | seq, 60 | ret_code, 61 | command_name, 62 | f"Error when decoding response! {repr(e)}", 63 | ) 64 | 65 | 66 | @dataclass 67 | class RegisterFail(SvcRegisterResponse): 68 | message: Optional[str] = None 69 | 70 | 71 | @dataclass 72 | class RegisterSuccess(SvcRegisterResponse): 73 | response: SvcRespRegister 74 | 75 | 76 | @dataclass 77 | class MSFForceOfflineCommand(Command): 78 | @classmethod 79 | def decode_response( 80 | cls, uin: int, seq: int, ret_code: int, command_name: str, data: bytes 81 | ) -> "MSFForceOfflineCommand": 82 | """Decode StatSvc MSF Offline request. 83 | 84 | Note: 85 | Source: com.tencent.mobileqq.msf.core.af.a 86 | 87 | Args: 88 | uin (int): User QQ 89 | seq (int): Sequence number of the response packet. 90 | ret_code (int): Return code of the response. 91 | command_name (str): Command name of the response. 92 | data (bytes): Payload data of the response. 93 | """ 94 | if ret_code != 0 or not data: 95 | return MSFForceOfflineCommand(uin, seq, ret_code, command_name) 96 | 97 | try: 98 | req_packet = RequestPacketVersion3.decode(data) 99 | msf_offline_request = RequestMSFForceOffline.decode( 100 | req_packet.data["RequestMSFForceOffline"][1:-1] # type: ignore 101 | ) 102 | return MSFForceOffline( 103 | uin, seq, ret_code, command_name, msf_offline_request 104 | ) 105 | except Exception as e: 106 | return MSFForceOfflineError( 107 | uin, 108 | seq, 109 | ret_code, 110 | command_name, 111 | f"Error when decoding response! {repr(e)}", 112 | ) 113 | 114 | 115 | @dataclass 116 | class MSFForceOffline(MSFForceOfflineCommand): 117 | request: RequestMSFForceOffline 118 | 119 | 120 | @dataclass 121 | class MSFForceOfflineError(MSFForceOfflineCommand): 122 | message: str 123 | -------------------------------------------------------------------------------- /cai/connection/__init__.py: -------------------------------------------------------------------------------- 1 | """Low Level Async TCP Connection 2 | 3 | This module is used to build a async TCP connection to target. 4 | 5 | :Copyright: Copyright (C) 2021-2021 cscs181 6 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 7 | 8 | .. _LICENSE: 9 | https://github.com/cscs181/CAI/blob/master/LICENSE 10 | """ 11 | import asyncio 12 | from types import TracebackType 13 | from typing import Any, Type, Union, Optional 14 | 15 | from cai.utils.binary import Packet 16 | from cai.utils.coroutine import ContextManager 17 | 18 | 19 | class Connection: 20 | def __init__( 21 | self, 22 | host: str, 23 | port: int, 24 | ssl: bool = False, 25 | timeout: Optional[float] = None, 26 | ) -> None: 27 | self._host = host 28 | self._port = port 29 | self._ssl = ssl 30 | self.timeout = timeout 31 | 32 | self._reader: Optional[asyncio.StreamReader] = None 33 | self._writer: Optional[asyncio.StreamWriter] = None 34 | 35 | @property 36 | def host(self) -> str: 37 | return self._host 38 | 39 | @property 40 | def port(self) -> int: 41 | return self._port 42 | 43 | @property 44 | def ssl(self) -> bool: 45 | return self._ssl 46 | 47 | @property 48 | def writer(self) -> asyncio.StreamWriter: 49 | if not self._writer: 50 | raise RuntimeError("Connection closed!") 51 | return self._writer 52 | 53 | @property 54 | def reader(self) -> asyncio.StreamReader: 55 | if not self._reader: 56 | raise RuntimeError("Connection closed!") 57 | return self._reader 58 | 59 | @property 60 | def closed(self) -> bool: 61 | return self._writer is None 62 | 63 | async def __aenter__(self): 64 | await self._connect() 65 | return self 66 | 67 | async def __aexit__( 68 | self, 69 | exc_type: Optional[Type[BaseException]], 70 | exc_value: Optional[BaseException], 71 | traceback: Optional[TracebackType], 72 | ) -> None: 73 | await self.close() 74 | return 75 | 76 | async def _connect(self): 77 | try: 78 | self._reader, self._writer = await asyncio.wait_for( 79 | asyncio.open_connection(self._host, self._port, ssl=self._ssl), 80 | self.timeout, 81 | ) 82 | except Exception as e: 83 | if self._writer: 84 | self._writer.transport.close() 85 | self._reader = None 86 | self._writer = None 87 | raise ConnectionError( 88 | f"Open connection to {self._host}:{self._port} failed" 89 | ) from e 90 | 91 | async def close(self): 92 | if self._writer: 93 | self._writer.close() 94 | await self._writer.wait_closed() 95 | self._writer = None 96 | self._reader = None 97 | 98 | async def reconnect(self) -> None: 99 | await self.close() 100 | await self._connect() 101 | 102 | async def read_bytes(self, num_bytes: int): 103 | try: 104 | data = await self.reader.readexactly(num_bytes) 105 | except (asyncio.IncompleteReadError, IOError, OSError) as e: 106 | await self.close() 107 | raise ConnectionAbortedError( 108 | f"Lost connection to {self._host}:{self._port}" 109 | ) from e 110 | return data 111 | 112 | async def read_line(self): 113 | try: 114 | data = await self.reader.readline() 115 | except (asyncio.IncompleteReadError, IOError, OSError) as e: 116 | await self.close() 117 | raise ConnectionAbortedError( 118 | f"Lost connection to {self._host}:{self._port}" 119 | ) from e 120 | return data 121 | 122 | async def read_all(self): 123 | try: 124 | data = await self.reader.read(-1) 125 | except (asyncio.IncompleteReadError, IOError, OSError) as e: 126 | await self.close() 127 | raise ConnectionAbortedError( 128 | f"Lost connection to {self._host}:{self._port}" 129 | ) from e 130 | return data 131 | 132 | def write(self, data: Union[bytes, Packet]): 133 | self.writer.write(data) # type: ignore 134 | 135 | def write_eof(self): 136 | if self.writer.can_write_eof(): 137 | self.writer.write_eof() 138 | 139 | async def awrite(self, data: Union[bytes, Packet]): 140 | self.writer.write(data) # type: ignore 141 | await self.writer.drain() 142 | 143 | 144 | def connect( 145 | host: str, port: int, ssl: bool = False, timeout: Optional[float] = None 146 | ) -> ContextManager[Any, Any, Connection]: 147 | coro = _connect(host, port, ssl=ssl, timeout=timeout) 148 | return ContextManager(coro) 149 | 150 | 151 | async def _connect(*args, **kwargs): 152 | conn = Connection(*args, **kwargs) 153 | await conn._connect() 154 | return conn 155 | -------------------------------------------------------------------------------- /cai/connection/utils.py: -------------------------------------------------------------------------------- 1 | """Connection Related tools 2 | 3 | This module is used to build tcp latency test and other convenient tools. 4 | 5 | :Copyright: Copyright (C) 2021-2021 cscs181 6 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 7 | 8 | .. _LICENSE: 9 | https://github.com/cscs181/CAI/blob/master/LICENSE 10 | """ 11 | import math 12 | import time 13 | 14 | from . import connect 15 | 16 | 17 | async def tcp_latency_test(host: str, port: int, timeout: float = 5.0) -> float: 18 | start = time.time() 19 | 20 | try: 21 | async with connect(host, port, timeout=timeout): 22 | pass 23 | except ConnectionError: 24 | return math.inf 25 | 26 | end = time.time() 27 | return (end - start) * 1000 28 | -------------------------------------------------------------------------------- /cai/exceptions.py: -------------------------------------------------------------------------------- 1 | """Application Exceptions 2 | 3 | This module is used to collect all application exceptions. 4 | 5 | :Copyright: Copyright (C) 2021-2021 cscs181 6 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 7 | 8 | .. _LICENSE: 9 | https://github.com/cscs181/CAI/blob/master/LICENSE 10 | """ 11 | from typing import Optional 12 | 13 | 14 | class CaiException(Exception): 15 | """Application Base Exception""" 16 | 17 | def __str__(self): 18 | return self.__repr__() 19 | 20 | 21 | # sso server 22 | class SsoServerException(CaiException): 23 | """Server Related Exception""" 24 | 25 | 26 | # api 27 | class ApiException(CaiException): 28 | """Base Exception for API""" 29 | 30 | def __init__(self, uin: Optional[int]): 31 | self.uin = uin 32 | 33 | def __repr__(self) -> str: 34 | return f"ApiException(uin={self.uin})" 35 | 36 | 37 | class ClientNotAvailable(ApiException): 38 | """Cannot get client""" 39 | 40 | def __init__(self, uin: Optional[int], reason: str): 41 | super().__init__(uin) 42 | self.reason = reason 43 | 44 | def __repr__(self) -> str: 45 | return f"ClientNotAvailable(uin={self.uin}, reason={self.reason})" 46 | 47 | 48 | class ApiResponseError(ApiException): 49 | """Receive a response with non zero return code.""" 50 | 51 | def __init__(self, uin: int, seq: int, ret_code: int, command_name: str): 52 | super().__init__(uin) 53 | self.seq = seq 54 | self.ret_code = ret_code 55 | self.command_name = command_name 56 | 57 | def __repr__(self) -> str: 58 | return ( 59 | f"ApiResponseError(uin={self.uin}, seq={self.seq}, " 60 | f"ret_code={self.ret_code}, command_name={self.command_name})" 61 | ) 62 | 63 | 64 | # login api 65 | class LoginException(ApiException): 66 | """Base Exception for Login""" 67 | 68 | def __init__(self, uin: int, status: int, message: str = ""): 69 | self.uin = uin 70 | self.status = status 71 | self.message = message 72 | 73 | def __repr__(self) -> str: 74 | return f"LoginException(uin={self.uin}, status={self.status}, message={self.message})" 75 | 76 | 77 | class LoginSliderNeeded(LoginException): 78 | """Need Slider Ticket when Login""" 79 | 80 | def __init__(self, uin: int, verify_url: str): 81 | self.uin = uin 82 | self.verify_url = verify_url 83 | 84 | def __repr__(self) -> str: 85 | return f"LoginSliderException(uin={self.uin}, verify_url={self.verify_url})" 86 | 87 | 88 | class LoginCaptchaNeeded(LoginException): 89 | """Need Captcha Image when Login""" 90 | 91 | def __init__(self, uin: int, captcha_image: bytes, captcha_sign: bytes): 92 | self.uin = uin 93 | self.captcha_image = captcha_image 94 | self.captcha_sign = captcha_sign 95 | 96 | def __repr__(self) -> str: 97 | return ( 98 | f"LoginCaptchaException(uin={self.uin}, captcha_image=)" 99 | ) 100 | 101 | 102 | class LoginAccountFrozen(LoginException): 103 | """Account is already frozen""" 104 | 105 | def __init__(self, uin: int): 106 | self.uin = uin 107 | 108 | def __repr__(self) -> str: 109 | return f"LoginAccountFrozen(uin={self.uin})" 110 | 111 | 112 | class LoginDeviceLocked(LoginException): 113 | """Device lock checking is needed""" 114 | 115 | def __init__( 116 | self, 117 | uin: int, 118 | sms_phone: Optional[str], 119 | verify_url: Optional[str], 120 | message: Optional[str], 121 | ): 122 | self.uin = uin 123 | self.sms_phone = sms_phone 124 | self.verify_url = verify_url 125 | self.message = message 126 | 127 | def __repr__(self) -> str: 128 | return ( 129 | f"LoginDeviceLocked(uin={self.uin}, message={self.message}, " 130 | f"sms_phone={self.sms_phone}, verify_url={self.verify_url})" 131 | ) 132 | 133 | 134 | class LoginSMSRequestError(LoginException): 135 | """Too many sms verify request""" 136 | 137 | def __init__(self, uin: int): 138 | self.uin = uin 139 | 140 | def __repr__(self) -> str: 141 | return f"LoginSMSRequestError(uin={self.uin})" 142 | 143 | 144 | # register 145 | class RegisterException(ApiException): 146 | """Exception for Register""" 147 | 148 | def __init__(self, uin: int, status: int, message: str = ""): 149 | self.uin = uin 150 | self.status = status 151 | self.message = message 152 | 153 | def __repr__(self) -> str: 154 | return f"RegisterException(uin={self.uin}, status={self.status}, message={self.message})" 155 | 156 | 157 | # friendlist 158 | class BaseFriendListException(ApiException): 159 | """Base Exception for Friend List""" 160 | 161 | 162 | class FriendListException(BaseFriendListException): 163 | """Exception for Friend List""" 164 | 165 | def __init__(self, uin: int, status: int, message: str = ""): 166 | self.uin = uin 167 | self.status = status 168 | self.message = message 169 | 170 | def __repr__(self) -> str: 171 | return f"FriendListException(uin={self.uin}, status={self.status}, message={self.message})" 172 | 173 | 174 | class GroupListException(BaseFriendListException): 175 | """Exception for Group List""" 176 | 177 | def __init__(self, uin: int, status: int, message: str = ""): 178 | self.uin = uin 179 | self.status = status 180 | self.message = message 181 | 182 | def __repr__(self) -> str: 183 | return f"GroupListException(uin={self.uin}, status={self.status}, message={self.message})" 184 | 185 | 186 | class GroupMemberListException(BaseFriendListException): 187 | """Exception for Group Member List""" 188 | 189 | def __init__(self, uin: int, status: int, message: str = ""): 190 | self.uin = uin 191 | self.status = status 192 | self.message = message 193 | 194 | def __repr__(self) -> str: 195 | return ( 196 | f"GroupMemberListException(uin={self.uin}, " 197 | f"status={self.status}, message={self.message})" 198 | ) 199 | -------------------------------------------------------------------------------- /cai/log.py: -------------------------------------------------------------------------------- 1 | """Application Logger 2 | 3 | This module is used to build application logger. 4 | 5 | :Copyright: Copyright (C) 2021-2021 cscs181 6 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 7 | 8 | .. _LICENSE: 9 | https://github.com/cscs181/CAI/blob/master/LICENSE 10 | """ 11 | import sys 12 | import logging 13 | 14 | logger = logging.getLogger("cai") 15 | logger.setLevel(logging.DEBUG) 16 | logger.addHandler(logging.StreamHandler(sys.stdout)) 17 | -------------------------------------------------------------------------------- /cai/pb/__init__.py: -------------------------------------------------------------------------------- 1 | """Protocol Buffer Model Category. 2 | 3 | This module is used to store all protobuf files. 4 | 5 | Generate all protobuf file using: 6 | 7 | .. code-block:: bash 8 | 9 | protoc cai/pb/**/*.proto --python_out=. --mypy_out=readable_stubs:. 10 | 11 | :Copyright: Copyright (C) 2021-2021 cscs181 12 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 13 | 14 | .. _LICENSE: 15 | https://github.com/cscs181/CAI/blob/master/LICENSE 16 | """ 17 | -------------------------------------------------------------------------------- /cai/pb/im/__init__.py: -------------------------------------------------------------------------------- 1 | """IM Protocol Buffer Model Category. 2 | 3 | This module is used to store all im protobuf files. 4 | 5 | Generate all protobuf file using: 6 | 7 | .. code-block:: bash 8 | 9 | protoc cai/pb/**/*.proto --python_out=. --mypy_out=readable_stubs:. 10 | 11 | :Copyright: Copyright (C) 2021-2021 cscs181 12 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 13 | 14 | .. _LICENSE: 15 | https://github.com/cscs181/CAI/blob/master/LICENSE 16 | """ 17 | -------------------------------------------------------------------------------- /cai/pb/im/msg/__init__.py: -------------------------------------------------------------------------------- 1 | """IM Message Protocol Buffer Model Category. 2 | 3 | This module is used to store all im message protobuf files. 4 | 5 | Generate all protobuf file using: 6 | 7 | .. code-block:: bash 8 | 9 | protoc cai/pb/**/*.proto --python_out=. --mypy_out=readable_stubs:. 10 | 11 | :Copyright: Copyright (C) 2021-2021 cscs181 12 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 13 | 14 | .. _LICENSE: 15 | https://github.com/cscs181/CAI/blob/master/LICENSE 16 | """ 17 | -------------------------------------------------------------------------------- /cai/pb/im/msg/common/__init__.py: -------------------------------------------------------------------------------- 1 | """IM Message Protocol Buffer Model Category. 2 | 3 | This module is used to store all im message protobuf files. 4 | 5 | Generate all protobuf file using: 6 | 7 | .. code-block:: bash 8 | 9 | protoc cai/pb/**/*.proto --python_out=. --mypy_out=readable_stubs:. 10 | 11 | :Copyright: Copyright (C) 2021-2021 cscs181 12 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 13 | 14 | .. _LICENSE: 15 | https://github.com/cscs181/CAI/blob/master/LICENSE 16 | """ 17 | 18 | from .common_pb2 import * 19 | -------------------------------------------------------------------------------- /cai/pb/im/msg/common/common.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | package im.msg.common; 3 | 4 | // tencent/im/msg/im_common.java 5 | 6 | message GroupInfo { 7 | optional uint64 group_id = 1; 8 | optional uint32 group_type = 2; 9 | } 10 | 11 | message Signature { 12 | optional uint32 key_type = 1; 13 | optional uint32 session_app_id = 2; 14 | optional bytes session_key = 3; 15 | } 16 | 17 | message Token { 18 | optional bytes buf = 1; 19 | optional uint32 c2c_type = 2; 20 | optional uint32 service_type = 3; 21 | } 22 | 23 | message User { 24 | optional uint64 uin = 1; 25 | optional uint32 app_id = 2; 26 | optional uint32 instance_id = 3; 27 | optional uint32 app_type = 4; 28 | optional fixed32 client_ip = 5; 29 | optional uint32 version = 6; 30 | optional string phone_number = 7; 31 | optional uint32 platform_id = 8; 32 | optional uint32 language = 9; 33 | optional bytes equip_key = 10; 34 | } 35 | -------------------------------------------------------------------------------- /cai/pb/im/msg/common/common_pb2.pyi: -------------------------------------------------------------------------------- 1 | """ 2 | @generated by mypy-protobuf. Do not edit manually! 3 | isort:skip_file 4 | """ 5 | from builtins import ( 6 | bool, 7 | bytes, 8 | int, 9 | ) 10 | 11 | from google.protobuf.descriptor import ( 12 | Descriptor, 13 | FileDescriptor, 14 | ) 15 | 16 | from google.protobuf.message import ( 17 | Message, 18 | ) 19 | 20 | from typing import ( 21 | Optional, 22 | Text, 23 | ) 24 | 25 | from typing_extensions import ( 26 | Literal, 27 | ) 28 | 29 | 30 | DESCRIPTOR: FileDescriptor = ... 31 | 32 | class GroupInfo(Message): 33 | DESCRIPTOR: Descriptor = ... 34 | GROUP_ID_FIELD_NUMBER: int 35 | GROUP_TYPE_FIELD_NUMBER: int 36 | group_id: int = ... 37 | group_type: int = ... 38 | 39 | def __init__(self, 40 | *, 41 | group_id : Optional[int] = ..., 42 | group_type : Optional[int] = ..., 43 | ) -> None: ... 44 | def HasField(self, field_name: Literal[u"group_id",b"group_id",u"group_type",b"group_type"]) -> bool: ... 45 | def ClearField(self, field_name: Literal[u"group_id",b"group_id",u"group_type",b"group_type"]) -> None: ... 46 | 47 | class Signature(Message): 48 | DESCRIPTOR: Descriptor = ... 49 | KEY_TYPE_FIELD_NUMBER: int 50 | SESSION_APP_ID_FIELD_NUMBER: int 51 | SESSION_KEY_FIELD_NUMBER: int 52 | key_type: int = ... 53 | session_app_id: int = ... 54 | session_key: bytes = ... 55 | 56 | def __init__(self, 57 | *, 58 | key_type : Optional[int] = ..., 59 | session_app_id : Optional[int] = ..., 60 | session_key : Optional[bytes] = ..., 61 | ) -> None: ... 62 | def HasField(self, field_name: Literal[u"key_type",b"key_type",u"session_app_id",b"session_app_id",u"session_key",b"session_key"]) -> bool: ... 63 | def ClearField(self, field_name: Literal[u"key_type",b"key_type",u"session_app_id",b"session_app_id",u"session_key",b"session_key"]) -> None: ... 64 | 65 | class Token(Message): 66 | DESCRIPTOR: Descriptor = ... 67 | BUF_FIELD_NUMBER: int 68 | C2C_TYPE_FIELD_NUMBER: int 69 | SERVICE_TYPE_FIELD_NUMBER: int 70 | buf: bytes = ... 71 | c2c_type: int = ... 72 | service_type: int = ... 73 | 74 | def __init__(self, 75 | *, 76 | buf : Optional[bytes] = ..., 77 | c2c_type : Optional[int] = ..., 78 | service_type : Optional[int] = ..., 79 | ) -> None: ... 80 | def HasField(self, field_name: Literal[u"buf",b"buf",u"c2c_type",b"c2c_type",u"service_type",b"service_type"]) -> bool: ... 81 | def ClearField(self, field_name: Literal[u"buf",b"buf",u"c2c_type",b"c2c_type",u"service_type",b"service_type"]) -> None: ... 82 | 83 | class User(Message): 84 | DESCRIPTOR: Descriptor = ... 85 | UIN_FIELD_NUMBER: int 86 | APP_ID_FIELD_NUMBER: int 87 | INSTANCE_ID_FIELD_NUMBER: int 88 | APP_TYPE_FIELD_NUMBER: int 89 | CLIENT_IP_FIELD_NUMBER: int 90 | VERSION_FIELD_NUMBER: int 91 | PHONE_NUMBER_FIELD_NUMBER: int 92 | PLATFORM_ID_FIELD_NUMBER: int 93 | LANGUAGE_FIELD_NUMBER: int 94 | EQUIP_KEY_FIELD_NUMBER: int 95 | uin: int = ... 96 | app_id: int = ... 97 | instance_id: int = ... 98 | app_type: int = ... 99 | client_ip: int = ... 100 | version: int = ... 101 | phone_number: Text = ... 102 | platform_id: int = ... 103 | language: int = ... 104 | equip_key: bytes = ... 105 | 106 | def __init__(self, 107 | *, 108 | uin : Optional[int] = ..., 109 | app_id : Optional[int] = ..., 110 | instance_id : Optional[int] = ..., 111 | app_type : Optional[int] = ..., 112 | client_ip : Optional[int] = ..., 113 | version : Optional[int] = ..., 114 | phone_number : Optional[Text] = ..., 115 | platform_id : Optional[int] = ..., 116 | language : Optional[int] = ..., 117 | equip_key : Optional[bytes] = ..., 118 | ) -> None: ... 119 | def HasField(self, field_name: Literal[u"app_id",b"app_id",u"app_type",b"app_type",u"client_ip",b"client_ip",u"equip_key",b"equip_key",u"instance_id",b"instance_id",u"language",b"language",u"phone_number",b"phone_number",u"platform_id",b"platform_id",u"uin",b"uin",u"version",b"version"]) -> bool: ... 120 | def ClearField(self, field_name: Literal[u"app_id",b"app_id",u"app_type",b"app_type",u"client_ip",b"client_ip",u"equip_key",b"equip_key",u"instance_id",b"instance_id",u"language",b"language",u"phone_number",b"phone_number",u"platform_id",b"platform_id",u"uin",b"uin",u"version",b"version"]) -> None: ... 121 | -------------------------------------------------------------------------------- /cai/pb/im/msg/msg/__init__.py: -------------------------------------------------------------------------------- 1 | """IM Message Protocol Buffer Model Category. 2 | 3 | This module is used to store all im message protobuf files. 4 | 5 | Generate all protobuf file using: 6 | 7 | .. code-block:: bash 8 | 9 | protoc cai/pb/**/*.proto --python_out=. --mypy_out=readable_stubs:. 10 | 11 | :Copyright: Copyright (C) 2021-2021 cscs181 12 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 13 | 14 | .. _LICENSE: 15 | https://github.com/cscs181/CAI/blob/master/LICENSE 16 | """ 17 | 18 | from .msg_pb2 import * 19 | -------------------------------------------------------------------------------- /cai/pb/im/msg/msg/msg.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | package im.msg.msg; 3 | 4 | // tencent/im/msg/im_msg.java 5 | 6 | import "cai/pb/im/msg/common/common.proto"; 7 | import "cai/pb/im/msg/msg_body/msg_body.proto"; 8 | 9 | message C2C { 10 | optional common.User sender = 1; 11 | optional common.User receiver = 2; 12 | optional C2CRelation c2c_relation = 3; 13 | } 14 | 15 | message C2CRelation { 16 | optional uint32 c2c_type = 1; 17 | optional common.GroupInfo group_info = 2; 18 | optional common.Token token = 3; 19 | } 20 | 21 | message ContentHead { 22 | optional uint32 pkg_num = 1; 23 | optional uint32 pkg_index = 2; 24 | optional uint32 seq = 3; 25 | optional uint32 date_time = 4; 26 | optional uint32 type = 5; 27 | optional uint32 div_seq = 6; 28 | optional uint64 msgdb_uin = 7; 29 | optional uint32 msgdb_seq = 8; 30 | optional uint32 word_msg_seq = 9; 31 | optional uint32 rand = 10; 32 | } 33 | 34 | message Group { 35 | optional common.User sender = 1; 36 | optional common.User receiver = 2; 37 | optional common.GroupInfo group_info = 3; 38 | } 39 | 40 | message Msg { 41 | optional MsgHead head = 1; 42 | optional msg_body.MsgBody body = 2; 43 | } 44 | 45 | message MsgHead { 46 | optional RoutingHead routing_head = 1; 47 | optional ContentHead content_head = 2; 48 | optional bytes gbk_tmp_msg_body = 3; 49 | } 50 | 51 | message MsgSendReq { 52 | optional Msg msg = 1; 53 | optional bytes bu_msg = 2; 54 | optional uint32 tail_id = 3; 55 | optional uint32 conn_msg_flag = 4; 56 | optional bytes cookie = 5; 57 | } 58 | 59 | message MsgSendResp { 60 | } 61 | 62 | message RoutingHead { 63 | optional C2C c2_c = 1; 64 | optional Group group = 2; 65 | } 66 | -------------------------------------------------------------------------------- /cai/pb/im/msg/msg_body/__init__.py: -------------------------------------------------------------------------------- 1 | """IM Message Protocol Buffer Model Category. 2 | 3 | This module is used to store all im message protobuf files. 4 | 5 | Generate all protobuf file using: 6 | 7 | .. code-block:: bash 8 | 9 | protoc cai/pb/**/*.proto --python_out=. --mypy_out=readable_stubs:. 10 | 11 | :Copyright: Copyright (C) 2021-2021 cscs181 12 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 13 | 14 | .. _LICENSE: 15 | https://github.com/cscs181/CAI/blob/master/LICENSE 16 | """ 17 | 18 | from .msg_body_pb2 import * 19 | -------------------------------------------------------------------------------- /cai/pb/im/msg/msg_head/__init__.py: -------------------------------------------------------------------------------- 1 | """IM Message Protocol Buffer Model Category. 2 | 3 | This module is used to store all im message protobuf files. 4 | 5 | Generate all protobuf file using: 6 | 7 | .. code-block:: bash 8 | 9 | protoc cai/pb/**/*.proto --python_out=. --mypy_out=readable_stubs:. 10 | 11 | :Copyright: Copyright (C) 2021-2021 cscs181 12 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 13 | 14 | .. _LICENSE: 15 | https://github.com/cscs181/CAI/blob/master/LICENSE 16 | """ 17 | 18 | from .msg_head_pb2 import * 19 | -------------------------------------------------------------------------------- /cai/pb/im/msg/msg_head/msg_head.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | package im.msg.msg_head; 3 | 4 | // tencent/im/msg/im_msg_head.java 5 | 6 | message C2CHead { 7 | optional uint64 to_uin = 1; 8 | optional uint64 from_uin = 2; 9 | optional uint32 cc_type = 3; 10 | optional uint32 cc_cmd = 4; 11 | optional bytes auth_pic_sig = 5; 12 | optional bytes auth_sig = 6; 13 | optional bytes auth_buf = 7; 14 | optional uint32 server_time = 8; 15 | optional uint32 client_time = 9; 16 | optional uint32 rand = 10; 17 | optional string phone_number = 11; 18 | } 19 | 20 | message CSHead { 21 | optional uint64 uin = 1; 22 | optional uint32 command = 2; 23 | optional uint32 seq = 3; 24 | optional uint32 version = 4; 25 | optional uint32 retry_times = 5; 26 | optional uint32 client_type = 6; 27 | optional uint32 pubno = 7; 28 | optional uint32 localid = 8; 29 | optional uint32 timezone = 9; 30 | optional fixed32 client_ip = 10; 31 | optional uint32 client_port = 11; 32 | optional fixed32 conn_ip = 12; 33 | optional uint32 conn_port = 13; 34 | optional fixed32 interface_ip = 14; 35 | optional uint32 interface_port = 15; 36 | optional fixed32 actual_ip = 16; 37 | optional uint32 flag = 17; 38 | optional fixed32 timestamp = 18; 39 | optional uint32 subcmd = 19; 40 | optional uint32 result = 20; 41 | optional uint32 app_id = 21; 42 | optional uint32 instance_id = 22; 43 | optional uint64 session_id = 23; 44 | optional uint32 idc_id = 24; 45 | } 46 | 47 | message DeltaHead { 48 | optional uint64 total_len = 1; 49 | optional uint64 offset = 2; 50 | optional uint64 ack_offset = 3; 51 | optional bytes cookie = 4; 52 | optional bytes ack_cookie = 5; 53 | optional uint32 result = 6; 54 | optional uint32 flags = 7; 55 | } 56 | 57 | message Head { 58 | optional uint32 head_type = 1; 59 | optional CSHead cs_head = 2; 60 | optional S2CHead s2c_head = 3; 61 | optional HttpConnHead httpconn_head = 4; 62 | optional uint32 paint_flag = 5; 63 | optional LoginSig login_sig = 6; 64 | optional DeltaHead delta_head = 7; 65 | optional C2CHead c2c_head = 8; 66 | optional SConnHead sconn_head = 9; 67 | optional InstCtrl inst_ctrl = 10; 68 | } 69 | 70 | message HttpConnHead { 71 | optional uint64 uin = 1; 72 | optional uint32 command = 2; 73 | optional uint32 sub_command = 3; 74 | optional uint32 seq = 4; 75 | optional uint32 version = 5; 76 | optional uint32 retry_times = 6; 77 | optional uint32 client_type = 7; 78 | optional uint32 pub_no = 8; 79 | optional uint32 local_id = 9; 80 | optional uint32 time_zone = 10; 81 | optional fixed32 client_ip = 11; 82 | optional uint32 client_port = 12; 83 | optional fixed32 qzhttp_ip = 13; 84 | optional uint32 qzhttp_port = 14; 85 | optional fixed32 spp_ip = 15; 86 | optional uint32 spp_port = 16; 87 | optional uint32 flag = 17; 88 | optional bytes key = 18; 89 | optional uint32 compress_type = 19; 90 | optional uint32 origin_size = 20; 91 | optional uint32 error_code = 21; 92 | optional RedirectMsg redirect = 22; 93 | optional uint32 command_id = 23; 94 | optional uint32 service_cmdid = 24; 95 | optional TransOidbHead oidbhead = 25; 96 | } 97 | 98 | message InstCtrl { 99 | repeated InstInfo send_to_inst = 1; 100 | repeated InstInfo exclude_inst = 2; 101 | optional InstInfo from_inst = 3; 102 | } 103 | 104 | message InstInfo { 105 | optional uint32 apppid = 1; 106 | optional uint32 instid = 2; 107 | optional uint32 platform = 3; 108 | optional uint32 enum_device_type = 10; 109 | } 110 | 111 | message LoginSig { 112 | optional uint32 type = 1; 113 | optional bytes sig = 2; 114 | } 115 | 116 | message RedirectMsg { 117 | optional fixed32 last_redirect_ip = 1; 118 | optional uint32 last_redirect_port = 2; 119 | optional fixed32 redirect_ip = 3; 120 | optional uint32 redirect_port = 4; 121 | optional uint32 redirect_count = 5; 122 | } 123 | 124 | message S2CHead { 125 | optional uint32 sub_msgtype = 1; 126 | optional uint32 msg_type = 2; 127 | optional uint64 from_uin = 3; 128 | optional uint32 msg_id = 4; 129 | optional fixed32 relay_ip = 5; 130 | optional uint32 relay_port = 6; 131 | optional uint64 to_uin = 7; 132 | } 133 | 134 | message SConnHead { 135 | } 136 | 137 | message TransOidbHead { 138 | optional uint32 command = 1; 139 | optional uint32 service_type = 2; 140 | optional uint32 result = 3; 141 | optional string error_msg = 4; 142 | } 143 | -------------------------------------------------------------------------------- /cai/pb/im/msg/obj_msg/__init__.py: -------------------------------------------------------------------------------- 1 | """IM Message Protocol Buffer Model Category. 2 | 3 | This module is used to store all im message protobuf files. 4 | 5 | Generate all protobuf file using: 6 | 7 | .. code-block:: bash 8 | 9 | protoc cai/pb/**/*.proto --python_out=. --mypy_out=readable_stubs:. 10 | 11 | :Copyright: Copyright (C) 2021-2021 cscs181 12 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 13 | 14 | .. _LICENSE: 15 | https://github.com/cscs181/CAI/blob/master/LICENSE 16 | """ 17 | 18 | from .obj_msg_pb2 import * 19 | -------------------------------------------------------------------------------- /cai/pb/im/msg/obj_msg/obj_msg.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | package im.msg.obj_msg; 3 | 4 | // tencent/im/msg/obj_msg.java 5 | 6 | message MsgContentInfo { 7 | optional bytes content_info_id = 1; 8 | optional MsgFile file = 2; 9 | } 10 | 11 | message MsgFile { 12 | optional uint32 bus_id = 1; 13 | optional bytes file_path = 2; 14 | optional uint64 file_size = 3; 15 | optional string file_name = 4; 16 | optional int64 dead_time = 5; 17 | optional bytes file_sha1 = 6; 18 | optional bytes ext = 7; 19 | optional bytes file_md5 = 8; 20 | } 21 | 22 | message MsgPic { 23 | optional bytes small_pic_url = 1; 24 | optional bytes original_pic_url = 2; 25 | optional uint32 local_pic_id = 3; 26 | } 27 | 28 | message ObjMsg { 29 | optional uint32 msg_type = 1; 30 | optional bytes title = 2; 31 | repeated bytes abstact = 3; 32 | optional bytes title_ext = 5; 33 | repeated MsgPic pic = 6; 34 | repeated MsgContentInfo content_info = 7; 35 | optional uint32 report_id_show = 8; 36 | } 37 | -------------------------------------------------------------------------------- /cai/pb/im/msg/obj_msg/obj_msg_pb2.pyi: -------------------------------------------------------------------------------- 1 | """ 2 | @generated by mypy-protobuf. Do not edit manually! 3 | isort:skip_file 4 | """ 5 | from builtins import ( 6 | bool, 7 | bytes, 8 | int, 9 | ) 10 | 11 | from google.protobuf.descriptor import ( 12 | Descriptor, 13 | FileDescriptor, 14 | ) 15 | 16 | from google.protobuf.internal.containers import ( 17 | RepeatedCompositeFieldContainer, 18 | RepeatedScalarFieldContainer, 19 | ) 20 | 21 | from google.protobuf.message import ( 22 | Message, 23 | ) 24 | 25 | from typing import ( 26 | Iterable, 27 | Optional, 28 | Text, 29 | ) 30 | 31 | from typing_extensions import ( 32 | Literal, 33 | ) 34 | 35 | 36 | DESCRIPTOR: FileDescriptor = ... 37 | 38 | class MsgContentInfo(Message): 39 | DESCRIPTOR: Descriptor = ... 40 | CONTENT_INFO_ID_FIELD_NUMBER: int 41 | FILE_FIELD_NUMBER: int 42 | content_info_id: bytes = ... 43 | 44 | @property 45 | def file(self) -> MsgFile: ... 46 | 47 | def __init__(self, 48 | *, 49 | content_info_id : Optional[bytes] = ..., 50 | file : Optional[MsgFile] = ..., 51 | ) -> None: ... 52 | def HasField(self, field_name: Literal[u"content_info_id",b"content_info_id",u"file",b"file"]) -> bool: ... 53 | def ClearField(self, field_name: Literal[u"content_info_id",b"content_info_id",u"file",b"file"]) -> None: ... 54 | 55 | class MsgFile(Message): 56 | DESCRIPTOR: Descriptor = ... 57 | BUS_ID_FIELD_NUMBER: int 58 | FILE_PATH_FIELD_NUMBER: int 59 | FILE_SIZE_FIELD_NUMBER: int 60 | FILE_NAME_FIELD_NUMBER: int 61 | DEAD_TIME_FIELD_NUMBER: int 62 | FILE_SHA1_FIELD_NUMBER: int 63 | EXT_FIELD_NUMBER: int 64 | FILE_MD5_FIELD_NUMBER: int 65 | bus_id: int = ... 66 | file_path: bytes = ... 67 | file_size: int = ... 68 | file_name: Text = ... 69 | dead_time: int = ... 70 | file_sha1: bytes = ... 71 | ext: bytes = ... 72 | file_md5: bytes = ... 73 | 74 | def __init__(self, 75 | *, 76 | bus_id : Optional[int] = ..., 77 | file_path : Optional[bytes] = ..., 78 | file_size : Optional[int] = ..., 79 | file_name : Optional[Text] = ..., 80 | dead_time : Optional[int] = ..., 81 | file_sha1 : Optional[bytes] = ..., 82 | ext : Optional[bytes] = ..., 83 | file_md5 : Optional[bytes] = ..., 84 | ) -> None: ... 85 | def HasField(self, field_name: Literal[u"bus_id",b"bus_id",u"dead_time",b"dead_time",u"ext",b"ext",u"file_md5",b"file_md5",u"file_name",b"file_name",u"file_path",b"file_path",u"file_sha1",b"file_sha1",u"file_size",b"file_size"]) -> bool: ... 86 | def ClearField(self, field_name: Literal[u"bus_id",b"bus_id",u"dead_time",b"dead_time",u"ext",b"ext",u"file_md5",b"file_md5",u"file_name",b"file_name",u"file_path",b"file_path",u"file_sha1",b"file_sha1",u"file_size",b"file_size"]) -> None: ... 87 | 88 | class MsgPic(Message): 89 | DESCRIPTOR: Descriptor = ... 90 | SMALL_PIC_URL_FIELD_NUMBER: int 91 | ORIGINAL_PIC_URL_FIELD_NUMBER: int 92 | LOCAL_PIC_ID_FIELD_NUMBER: int 93 | small_pic_url: bytes = ... 94 | original_pic_url: bytes = ... 95 | local_pic_id: int = ... 96 | 97 | def __init__(self, 98 | *, 99 | small_pic_url : Optional[bytes] = ..., 100 | original_pic_url : Optional[bytes] = ..., 101 | local_pic_id : Optional[int] = ..., 102 | ) -> None: ... 103 | def HasField(self, field_name: Literal[u"local_pic_id",b"local_pic_id",u"original_pic_url",b"original_pic_url",u"small_pic_url",b"small_pic_url"]) -> bool: ... 104 | def ClearField(self, field_name: Literal[u"local_pic_id",b"local_pic_id",u"original_pic_url",b"original_pic_url",u"small_pic_url",b"small_pic_url"]) -> None: ... 105 | 106 | class ObjMsg(Message): 107 | DESCRIPTOR: Descriptor = ... 108 | MSG_TYPE_FIELD_NUMBER: int 109 | TITLE_FIELD_NUMBER: int 110 | ABSTACT_FIELD_NUMBER: int 111 | TITLE_EXT_FIELD_NUMBER: int 112 | PIC_FIELD_NUMBER: int 113 | CONTENT_INFO_FIELD_NUMBER: int 114 | REPORT_ID_SHOW_FIELD_NUMBER: int 115 | msg_type: int = ... 116 | title: bytes = ... 117 | abstact: RepeatedScalarFieldContainer[bytes] = ... 118 | title_ext: bytes = ... 119 | report_id_show: int = ... 120 | 121 | @property 122 | def pic(self) -> RepeatedCompositeFieldContainer[MsgPic]: ... 123 | 124 | @property 125 | def content_info(self) -> RepeatedCompositeFieldContainer[MsgContentInfo]: ... 126 | 127 | def __init__(self, 128 | *, 129 | msg_type : Optional[int] = ..., 130 | title : Optional[bytes] = ..., 131 | abstact : Optional[Iterable[bytes]] = ..., 132 | title_ext : Optional[bytes] = ..., 133 | pic : Optional[Iterable[MsgPic]] = ..., 134 | content_info : Optional[Iterable[MsgContentInfo]] = ..., 135 | report_id_show : Optional[int] = ..., 136 | ) -> None: ... 137 | def HasField(self, field_name: Literal[u"msg_type",b"msg_type",u"report_id_show",b"report_id_show",u"title",b"title",u"title_ext",b"title_ext"]) -> bool: ... 138 | def ClearField(self, field_name: Literal[u"abstact",b"abstact",u"content_info",b"content_info",u"msg_type",b"msg_type",u"pic",b"pic",u"report_id_show",b"report_id_show",u"title",b"title",u"title_ext",b"title_ext"]) -> None: ... 139 | -------------------------------------------------------------------------------- /cai/pb/im/msg/receipt/__init__.py: -------------------------------------------------------------------------------- 1 | """IM Message Protocol Buffer Model Category. 2 | 3 | This module is used to store all im message protobuf files. 4 | 5 | Generate all protobuf file using: 6 | 7 | .. code-block:: bash 8 | 9 | protoc cai/pb/**/*.proto --python_out=. --mypy_out=readable_stubs:. 10 | 11 | :Copyright: Copyright (C) 2021-2021 cscs181 12 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 13 | 14 | .. _LICENSE: 15 | https://github.com/cscs181/CAI/blob/master/LICENSE 16 | """ 17 | 18 | from .receipt_pb2 import * 19 | -------------------------------------------------------------------------------- /cai/pb/im/msg/receipt/receipt.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | package im.msg.receipt; 3 | 4 | message MsgInfo { 5 | optional uint64 from_uin = 1; 6 | optional uint64 to_uin = 2; 7 | optional uint32 msg_seq = 3; 8 | optional uint32 msg_random = 4; 9 | } 10 | 11 | message ReceiptInfo { 12 | optional uint64 read_time = 1; 13 | } 14 | 15 | message ReceiptReq { 16 | optional uint32 command = 1; 17 | optional MsgInfo info = 2; 18 | } 19 | 20 | message ReceiptResp { 21 | optional uint32 command = 1; 22 | optional ReceiptInfo receipt_info = 2; 23 | } 24 | -------------------------------------------------------------------------------- /cai/pb/im/msg/receipt/receipt_pb2.pyi: -------------------------------------------------------------------------------- 1 | """ 2 | @generated by mypy-protobuf. Do not edit manually! 3 | isort:skip_file 4 | """ 5 | from builtins import ( 6 | bool, 7 | int, 8 | ) 9 | 10 | from google.protobuf.descriptor import ( 11 | Descriptor, 12 | FileDescriptor, 13 | ) 14 | 15 | from google.protobuf.message import ( 16 | Message, 17 | ) 18 | 19 | from typing import ( 20 | Optional, 21 | ) 22 | 23 | from typing_extensions import ( 24 | Literal, 25 | ) 26 | 27 | 28 | DESCRIPTOR: FileDescriptor = ... 29 | 30 | class MsgInfo(Message): 31 | DESCRIPTOR: Descriptor = ... 32 | FROM_UIN_FIELD_NUMBER: int 33 | TO_UIN_FIELD_NUMBER: int 34 | MSG_SEQ_FIELD_NUMBER: int 35 | MSG_RANDOM_FIELD_NUMBER: int 36 | from_uin: int = ... 37 | to_uin: int = ... 38 | msg_seq: int = ... 39 | msg_random: int = ... 40 | 41 | def __init__(self, 42 | *, 43 | from_uin : Optional[int] = ..., 44 | to_uin : Optional[int] = ..., 45 | msg_seq : Optional[int] = ..., 46 | msg_random : Optional[int] = ..., 47 | ) -> None: ... 48 | def HasField(self, field_name: Literal[u"from_uin",b"from_uin",u"msg_random",b"msg_random",u"msg_seq",b"msg_seq",u"to_uin",b"to_uin"]) -> bool: ... 49 | def ClearField(self, field_name: Literal[u"from_uin",b"from_uin",u"msg_random",b"msg_random",u"msg_seq",b"msg_seq",u"to_uin",b"to_uin"]) -> None: ... 50 | 51 | class ReceiptInfo(Message): 52 | DESCRIPTOR: Descriptor = ... 53 | READ_TIME_FIELD_NUMBER: int 54 | read_time: int = ... 55 | 56 | def __init__(self, 57 | *, 58 | read_time : Optional[int] = ..., 59 | ) -> None: ... 60 | def HasField(self, field_name: Literal[u"read_time",b"read_time"]) -> bool: ... 61 | def ClearField(self, field_name: Literal[u"read_time",b"read_time"]) -> None: ... 62 | 63 | class ReceiptReq(Message): 64 | DESCRIPTOR: Descriptor = ... 65 | COMMAND_FIELD_NUMBER: int 66 | INFO_FIELD_NUMBER: int 67 | command: int = ... 68 | 69 | @property 70 | def info(self) -> MsgInfo: ... 71 | 72 | def __init__(self, 73 | *, 74 | command : Optional[int] = ..., 75 | info : Optional[MsgInfo] = ..., 76 | ) -> None: ... 77 | def HasField(self, field_name: Literal[u"command",b"command",u"info",b"info"]) -> bool: ... 78 | def ClearField(self, field_name: Literal[u"command",b"command",u"info",b"info"]) -> None: ... 79 | 80 | class ReceiptResp(Message): 81 | DESCRIPTOR: Descriptor = ... 82 | COMMAND_FIELD_NUMBER: int 83 | RECEIPT_INFO_FIELD_NUMBER: int 84 | command: int = ... 85 | 86 | @property 87 | def receipt_info(self) -> ReceiptInfo: ... 88 | 89 | def __init__(self, 90 | *, 91 | command : Optional[int] = ..., 92 | receipt_info : Optional[ReceiptInfo] = ..., 93 | ) -> None: ... 94 | def HasField(self, field_name: Literal[u"command",b"command",u"receipt_info",b"receipt_info"]) -> bool: ... 95 | def ClearField(self, field_name: Literal[u"command",b"command",u"receipt_info",b"receipt_info"]) -> None: ... 96 | -------------------------------------------------------------------------------- /cai/pb/im/msg/service/comm_elem/__init__.py: -------------------------------------------------------------------------------- 1 | """IM Message Protocol Buffer Model Category. 2 | 3 | This module is used to store all im message protobuf files. 4 | 5 | Generate all protobuf file using: 6 | 7 | .. code-block:: bash 8 | 9 | protoc cai/pb/**/*.proto --python_out=. --mypy_out=readable_stubs:. 10 | 11 | :Copyright: Copyright (C) 2021-2021 cscs181 12 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 13 | 14 | .. _LICENSE: 15 | https://github.com/cscs181/CAI/blob/master/LICENSE 16 | """ 17 | 18 | from .comm_elem_pb2 import * 19 | -------------------------------------------------------------------------------- /cai/pb/im/oidb/__init__.py: -------------------------------------------------------------------------------- 1 | """IM OIDB Protocol Buffer Model Category. 2 | 3 | This module is used to store all oidb protobuf files. 4 | 5 | Generate all protobuf file using: 6 | 7 | .. code-block:: bash 8 | 9 | protoc cai/pb/**/*.proto --python_out=. --mypy_out=readable_stubs:. 10 | 11 | :Copyright: Copyright (C) 2021-2021 cscs181 12 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 13 | 14 | .. _LICENSE: 15 | https://github.com/cscs181/CAI/blob/master/LICENSE 16 | """ 17 | -------------------------------------------------------------------------------- /cai/pb/im/oidb/cmd0x769/__init__.py: -------------------------------------------------------------------------------- 1 | from .cmd0x769_pb2 import * 2 | -------------------------------------------------------------------------------- /cai/pb/im/oidb/cmd0x769/cmd0x769.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | package im.oidb.cmd0x769; 3 | 4 | // tencent/im/oidb/cmd0xd50/Oidb_0x769.java 5 | 6 | message CPU { 7 | optional string model = 1; 8 | optional uint32 cores = 2; 9 | optional uint32 frequency = 3; 10 | } 11 | 12 | message Camera { 13 | optional uint64 primary = 1; 14 | optional uint64 secondary = 2; 15 | optional bool flash = 3; 16 | } 17 | 18 | message Config { 19 | optional uint32 type = 1; 20 | optional uint32 version = 2; 21 | repeated string content_list = 3; 22 | optional string debug_msg = 4; 23 | repeated Content msg_content_list = 5; 24 | } 25 | 26 | message ConfigSeq { 27 | optional uint32 type = 1; 28 | optional uint32 version = 2; 29 | } 30 | 31 | message Content { 32 | optional uint32 task_id = 1; 33 | optional uint32 compress = 2; 34 | optional bytes content = 10; 35 | } 36 | 37 | message DeviceInfo { 38 | optional string brand = 1; 39 | optional string model = 2; 40 | optional OS os = 3; 41 | optional CPU cpu = 4; 42 | optional Memory memory = 5; 43 | optional Storage storage = 6; 44 | optional Screen screen = 7; 45 | optional Camera camera = 8; 46 | } 47 | 48 | message Memory { 49 | optional uint64 total = 1; 50 | optional uint64 process = 2; 51 | } 52 | 53 | message OS { 54 | optional uint32 type = 1; 55 | optional string version = 2; 56 | optional string sdk = 3; 57 | optional string kernel = 4; 58 | optional string rom = 5; 59 | } 60 | 61 | message QueryUinPackageUsageReq { 62 | optional uint32 type = 1; 63 | optional uint64 uin_file_size = 2; 64 | } 65 | 66 | message QueryUinPackageUsageRsp { 67 | optional uint32 status = 1; 68 | optional uint64 left_uin_num = 2; 69 | optional uint64 max_uin_num = 3; 70 | optional uint32 proportion = 4; 71 | repeated UinPackageUsedInfo uin_package_used_list = 10; 72 | } 73 | 74 | message ReqBody { 75 | repeated ConfigSeq config_list = 1; 76 | optional DeviceInfo device_info = 2; 77 | optional string info = 3; 78 | optional string province = 4; 79 | optional string city = 5; 80 | optional int32 req_debug_msg = 6; 81 | optional QueryUinPackageUsageReq query_uin_package_usage_req = 101; 82 | } 83 | 84 | message RspBody { 85 | optional uint32 result = 1; 86 | repeated Config config_list = 2; 87 | optional QueryUinPackageUsageRsp query_uin_package_usage_rsp = 101; 88 | } 89 | 90 | message Screen { 91 | optional string model = 1; 92 | optional uint32 width = 2; 93 | optional uint32 height = 3; 94 | optional uint32 dpi = 4; 95 | optional bool multi_touch = 5; 96 | } 97 | 98 | message Storage { 99 | optional uint64 builtin = 1; 100 | optional uint64 external = 2; 101 | } 102 | 103 | message UinPackageUsedInfo { 104 | optional uint32 rule_id = 1; 105 | optional string author = 2; 106 | optional string url = 3; 107 | optional uint64 uin_num = 4; 108 | } 109 | -------------------------------------------------------------------------------- /cai/pb/im/oidb/cmd0xd50/__init__.py: -------------------------------------------------------------------------------- 1 | from .cmd0xd50_pb2 import * 2 | -------------------------------------------------------------------------------- /cai/pb/im/oidb/cmd0xd50/cmd0xd50.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | package im.oidb.cmd0xd50; 3 | 4 | // tencent/im/oidb/cmd0xd50/Oidb_0xd50.java 5 | 6 | message ExtSnsFrdData { 7 | optional uint64 frd_uin = 1; 8 | optional bytes lovers = 11002; 9 | optional bytes confidante = 21002; 10 | optional bytes buddy = 31002; 11 | optional bytes frd_tree = 41001; 12 | optional bytes chat = 51001; 13 | optional bytes praise = 61001; 14 | optional bytes qzone_love = 71001; 15 | optional bytes qzone_house = 81001; 16 | optional bytes music_switch = 91001; 17 | optional bytes mutualmark_alienation = 101001; 18 | optional bytes unread_message = 111001; 19 | optional bytes boat = 121001; 20 | optional bytes close_frd = 131001; 21 | optional bytes mutualmark_score = 141001; 22 | optional bytes ksing_switch = 151001; 23 | optional bytes lbs_share = 181001; 24 | optional bytes dont_forget_me = 211001; 25 | optional bytes my_online_status_visible_to_frd = 221001; 26 | optional bytes frd_online_status_visible_to_me = 221002; 27 | optional bytes visitor_record = 231001; 28 | optional bytes frd_steal_record = 231002; 29 | optional bytes my_steal_record = 231003; 30 | optional bytes avgame = 241001; 31 | optional bytes aio_quick_app = 251001; 32 | } 33 | 34 | message KSingRelationInfo { 35 | optional uint32 flag = 1; 36 | } 37 | 38 | message ReqBody { 39 | optional uint64 appid = 1; 40 | optional uint32 max_pkg_size = 2; 41 | optional uint32 start_time = 3; 42 | optional uint32 start_index = 4; 43 | optional uint32 req_num = 5; 44 | repeated uint64 uin_list = 6; 45 | optional uint32 req_lovers = 11002; 46 | optional uint32 req_confidante = 21002; 47 | optional uint32 req_buddy = 31002; 48 | optional uint32 req_frd_tree = 41001; 49 | optional uint32 req_chat = 51001; 50 | optional uint32 req_praise = 61001; 51 | optional uint32 req_qzone_love = 71001; 52 | optional uint32 req_qzone_house = 81001; 53 | optional uint32 req_music_switch = 91001; 54 | optional uint32 req_mutualmark_alienation = 101001; 55 | optional uint32 req_unread_message = 111001; 56 | optional uint32 req_boat = 121001; 57 | optional uint32 req_close_frd = 131001; 58 | optional uint32 req_mutualmark_score = 141001; 59 | optional uint32 req_ksing_switch = 151001; 60 | optional uint32 req_mutualmark_lbsshare = 181001; 61 | optional uint32 req_dont_forget_me = 211001; 62 | optional uint32 req_my_online_status_visible_to_frd = 221001; 63 | optional uint32 req_frd_online_status_visible_to_me = 221002; 64 | optional uint32 req_visitor_record = 231001; 65 | optional uint32 req_frd_steal_record = 231002; 66 | optional uint32 req_my_steal_record = 231003; 67 | optional uint32 req_avgame = 241001; 68 | optional uint32 req_aio_quick_app = 251001; 69 | } 70 | 71 | message RspBody { 72 | repeated ExtSnsFrdData update_data = 1; 73 | optional uint32 over = 11; 74 | optional uint32 next_start = 12; 75 | repeated uint64 unfinished_uins = 13; 76 | } 77 | -------------------------------------------------------------------------------- /cai/pb/msf/__init__.py: -------------------------------------------------------------------------------- 1 | """MSF Protocol Buffer Model Category. 2 | 3 | This module is used to store all msf protobuf files. 4 | 5 | Generate all protobuf file using: 6 | 7 | .. code-block:: bash 8 | 9 | protoc cai/pb/**/*.proto --python_out=. --mypy_out=readable_stubs:. 10 | 11 | :Copyright: Copyright (C) 2021-2021 cscs181 12 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 13 | 14 | .. _LICENSE: 15 | https://github.com/cscs181/CAI/blob/master/LICENSE 16 | """ 17 | -------------------------------------------------------------------------------- /cai/pb/msf/msg/__init__.py: -------------------------------------------------------------------------------- 1 | """Message Service Protocol Buffer Model Category. 2 | 3 | This module is used to store all message service protobuf files. 4 | 5 | Generate all protobuf file using: 6 | 7 | .. code-block:: bash 8 | 9 | protoc cai/pb/**/*.proto --python_out=. --mypy_out=readable_stubs:. 10 | 11 | :Copyright: Copyright (C) 2021-2021 cscs181 12 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 13 | 14 | .. _LICENSE: 15 | https://github.com/cscs181/CAI/blob/master/LICENSE 16 | """ 17 | -------------------------------------------------------------------------------- /cai/pb/msf/msg/comm/__init__.py: -------------------------------------------------------------------------------- 1 | """Message Service Protocol Buffer Model Category. 2 | 3 | This module is used to store all message service protobuf files. 4 | 5 | Generate all protobuf file using: 6 | 7 | .. code-block:: bash 8 | 9 | protoc cai/pb/**/*.proto --python_out=. --mypy_out=readable_stubs:. 10 | 11 | :Copyright: Copyright (C) 2021-2021 cscs181 12 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 13 | 14 | .. _LICENSE: 15 | https://github.com/cscs181/CAI/blob/master/LICENSE 16 | """ 17 | 18 | from .comm_pb2 import * 19 | -------------------------------------------------------------------------------- /cai/pb/msf/msg/comm/comm.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | package msf.msg.comm; 3 | 4 | // msf/msgcomm/msg_comm.java 5 | 6 | import "cai/pb/im/msg/msg_body/msg_body.proto"; 7 | import "cai/pb/im/msg/msg_head/msg_head.proto"; 8 | 9 | message AppShareInfo { 10 | optional uint32 appshare_id = 1; 11 | optional bytes appshare_cookie = 2; 12 | optional PluginInfo appshare_resource = 3; 13 | } 14 | 15 | message C2CTmpMsgHead { 16 | optional uint32 c2c_type = 1; 17 | optional uint32 service_type = 2; 18 | optional uint64 group_uin = 3; 19 | optional uint64 group_code = 4; 20 | optional bytes sig = 5; 21 | optional uint32 sig_type = 6; 22 | optional string from_phone = 7; 23 | optional string to_phone = 8; 24 | optional uint32 lock_display = 9; 25 | optional uint32 direction_flag = 10; 26 | optional bytes reserved = 11; 27 | optional bytes business_name = 14; 28 | optional bytes business_sub_content = 15; 29 | } 30 | 31 | message ContentHead { 32 | optional uint32 pkg_num = 1; 33 | optional uint32 pkg_index = 2; 34 | optional uint32 div_seq = 3; 35 | optional uint32 auto_reply = 4; 36 | } 37 | 38 | message DiscussInfo { 39 | optional uint64 discuss_uin = 1; 40 | optional uint32 discuss_type = 2; 41 | optional uint64 discuss_info_seq = 3; 42 | optional bytes discuss_remark = 4; 43 | optional bytes discuss_name = 5; 44 | } 45 | 46 | message ExtGroupKeyInfo { 47 | optional uint32 cur_max_seq = 1; 48 | optional uint64 cur_time = 2; 49 | optional uint32 operate_by_parents = 3; 50 | optional bytes ext_group_info = 4; 51 | } 52 | 53 | message GroupInfo { 54 | optional uint64 group_code = 1; 55 | optional uint32 group_type = 2; 56 | optional uint64 group_info_seq = 3; 57 | optional bytes group_card = 4; 58 | optional bytes group_rank = 5; 59 | optional uint32 group_level = 6; 60 | optional uint32 group_card_type = 7; 61 | optional bytes group_name = 8; 62 | } 63 | 64 | message Msg { 65 | optional MsgHead head = 1; 66 | optional ContentHead content_head = 2; 67 | optional im.msg.msg_body.MsgBody body = 3; 68 | optional AppShareInfo appshare_info = 4; 69 | } 70 | 71 | message MsgHead { 72 | optional uint64 from_uin = 1; 73 | optional uint64 to_uin = 2; 74 | optional uint32 type = 3; 75 | optional uint32 c2c_cmd = 4; 76 | optional uint32 seq = 5; 77 | optional uint32 time = 6; 78 | optional uint64 uid = 7; 79 | optional C2CTmpMsgHead c2c_tmp_msg_head = 8; 80 | optional GroupInfo group_info = 9; 81 | optional uint32 from_appid = 10; 82 | optional uint32 from_instid = 11; 83 | optional uint32 user_active = 12; 84 | optional DiscussInfo discuss_info = 13; 85 | optional string from_nick = 14; 86 | optional uint64 auth_uin = 15; 87 | optional string auth_nick = 16; 88 | optional uint32 flag = 17; 89 | optional string auth_remark = 18; 90 | optional string group_name = 19; 91 | optional MutilTransHead mutiltrans_head = 20; 92 | optional im.msg.msg_head.InstCtrl inst_ctrl = 21; 93 | optional uint32 public_account_group_send_flag = 22; 94 | optional uint32 wseq_in_c2c_msghead = 23; 95 | optional uint64 cpid = 24; 96 | optional ExtGroupKeyInfo ext_group_key_info = 25; 97 | optional string multi_compatible_text = 26; 98 | optional uint32 auth_sex = 27; 99 | optional bool is_src_msg = 28; 100 | } 101 | 102 | message MsgType0x210 { 103 | optional uint32 sub_msg_type = 1; 104 | optional bytes content = 2; 105 | } 106 | 107 | message MutilTransHead { 108 | optional uint32 status = 1; 109 | optional uint32 msg_id = 2; 110 | } 111 | 112 | message PluginInfo { 113 | optional uint32 res_id = 1; 114 | optional string pkg_name = 2; 115 | optional uint32 new_ver = 3; 116 | optional uint32 res_type = 4; 117 | optional uint32 lan_type = 5; 118 | optional uint32 priority = 6; 119 | optional string res_name = 7; 120 | optional string res_desc = 8; 121 | optional string res_url_big = 9; 122 | optional string res_url_small = 10; 123 | optional string res_conf = 11; 124 | } 125 | 126 | message Uin2Nick { 127 | optional uint64 uin = 1; 128 | optional string nick = 2; 129 | } 130 | 131 | message UinPairMsg { 132 | optional uint32 last_read_time = 1; 133 | optional uint64 peer_uin = 2; 134 | optional uint32 completed = 3; 135 | repeated Msg msg = 4; 136 | optional uint32 unread_msg_num = 5; 137 | optional uint32 c2c_type = 8; 138 | optional uint32 service_type = 9; 139 | optional bytes pb_reserve = 10; 140 | optional uint64 to_tiny_id = 11; 141 | } 142 | -------------------------------------------------------------------------------- /cai/pb/msf/msg/ctrl/__init__.py: -------------------------------------------------------------------------------- 1 | """Message Service Protocol Buffer Model Category. 2 | 3 | This module is used to store all message service protobuf files. 4 | 5 | Generate all protobuf file using: 6 | 7 | .. code-block:: bash 8 | 9 | protoc cai/pb/**/*.proto --python_out=. --mypy_out=readable_stubs:. 10 | 11 | :Copyright: Copyright (C) 2021-2021 cscs181 12 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 13 | 14 | .. _LICENSE: 15 | https://github.com/cscs181/CAI/blob/master/LICENSE 16 | """ 17 | 18 | from .ctrl_pb2 import * 19 | -------------------------------------------------------------------------------- /cai/pb/msf/msg/ctrl/ctrl.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | package msf.msg.ctrl; 3 | 4 | // msf/msgsvc/msg_ctrl.java 5 | 6 | message MsgCtrl { 7 | optional uint32 flag = 1; 8 | optional ResvResvInfo resv_resv_info = 2; 9 | } 10 | 11 | message ResvResvInfo { 12 | optional uint32 flag = 1; 13 | optional bytes reserv1 = 2; 14 | optional uint64 reserv2 = 3; 15 | optional uint64 reserv3 = 4; 16 | optional uint32 create_time = 5; 17 | optional uint32 pic_height = 6; 18 | optional uint32 pic_width = 7; 19 | optional uint32 resv_flag = 8; 20 | } 21 | -------------------------------------------------------------------------------- /cai/pb/msf/msg/ctrl/ctrl_pb2.pyi: -------------------------------------------------------------------------------- 1 | """ 2 | @generated by mypy-protobuf. Do not edit manually! 3 | isort:skip_file 4 | """ 5 | from builtins import ( 6 | bool, 7 | bytes, 8 | int, 9 | ) 10 | 11 | from google.protobuf.descriptor import ( 12 | Descriptor, 13 | FileDescriptor, 14 | ) 15 | 16 | from google.protobuf.message import ( 17 | Message, 18 | ) 19 | 20 | from typing import ( 21 | Optional, 22 | ) 23 | 24 | from typing_extensions import ( 25 | Literal, 26 | ) 27 | 28 | 29 | DESCRIPTOR: FileDescriptor = ... 30 | 31 | class MsgCtrl(Message): 32 | DESCRIPTOR: Descriptor = ... 33 | FLAG_FIELD_NUMBER: int 34 | RESV_RESV_INFO_FIELD_NUMBER: int 35 | flag: int = ... 36 | 37 | @property 38 | def resv_resv_info(self) -> ResvResvInfo: ... 39 | 40 | def __init__(self, 41 | *, 42 | flag : Optional[int] = ..., 43 | resv_resv_info : Optional[ResvResvInfo] = ..., 44 | ) -> None: ... 45 | def HasField(self, field_name: Literal[u"flag",b"flag",u"resv_resv_info",b"resv_resv_info"]) -> bool: ... 46 | def ClearField(self, field_name: Literal[u"flag",b"flag",u"resv_resv_info",b"resv_resv_info"]) -> None: ... 47 | 48 | class ResvResvInfo(Message): 49 | DESCRIPTOR: Descriptor = ... 50 | FLAG_FIELD_NUMBER: int 51 | RESERV1_FIELD_NUMBER: int 52 | RESERV2_FIELD_NUMBER: int 53 | RESERV3_FIELD_NUMBER: int 54 | CREATE_TIME_FIELD_NUMBER: int 55 | PIC_HEIGHT_FIELD_NUMBER: int 56 | PIC_WIDTH_FIELD_NUMBER: int 57 | RESV_FLAG_FIELD_NUMBER: int 58 | flag: int = ... 59 | reserv1: bytes = ... 60 | reserv2: int = ... 61 | reserv3: int = ... 62 | create_time: int = ... 63 | pic_height: int = ... 64 | pic_width: int = ... 65 | resv_flag: int = ... 66 | 67 | def __init__(self, 68 | *, 69 | flag : Optional[int] = ..., 70 | reserv1 : Optional[bytes] = ..., 71 | reserv2 : Optional[int] = ..., 72 | reserv3 : Optional[int] = ..., 73 | create_time : Optional[int] = ..., 74 | pic_height : Optional[int] = ..., 75 | pic_width : Optional[int] = ..., 76 | resv_flag : Optional[int] = ..., 77 | ) -> None: ... 78 | def HasField(self, field_name: Literal[u"create_time",b"create_time",u"flag",b"flag",u"pic_height",b"pic_height",u"pic_width",b"pic_width",u"reserv1",b"reserv1",u"reserv2",b"reserv2",u"reserv3",b"reserv3",u"resv_flag",b"resv_flag"]) -> bool: ... 79 | def ClearField(self, field_name: Literal[u"create_time",b"create_time",u"flag",b"flag",u"pic_height",b"pic_height",u"pic_width",b"pic_width",u"reserv1",b"reserv1",u"reserv2",b"reserv2",u"reserv3",b"reserv3",u"resv_flag",b"resv_flag"]) -> None: ... 80 | -------------------------------------------------------------------------------- /cai/pb/msf/msg/onlinepush/__init__.py: -------------------------------------------------------------------------------- 1 | """Message Service Protocol Buffer Model Category. 2 | 3 | This module is used to store all message service protobuf files. 4 | 5 | Generate all protobuf file using: 6 | 7 | .. code-block:: bash 8 | 9 | protoc cai/pb/**/*.proto --python_out=. --mypy_out=readable_stubs:. 10 | 11 | :Copyright: Copyright (C) 2021-2021 cscs181 12 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 13 | 14 | .. _LICENSE: 15 | https://github.com/cscs181/CAI/blob/master/LICENSE 16 | """ 17 | 18 | from .onlinepush_pb2 import * 19 | -------------------------------------------------------------------------------- /cai/pb/msf/msg/onlinepush/onlinepush.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | package msf.msg.onlinepush; 3 | 4 | // msf/msgsvc/msg_onlinepush.java 5 | 6 | import "cai/pb/msf/msg/comm/comm.proto"; 7 | 8 | message PbPushMsg { 9 | optional comm.Msg msg = 1; 10 | optional int32 svrip = 2; 11 | optional bytes push_token = 3; 12 | optional uint32 ping_flag = 4; 13 | optional uint32 general_flag = 9; 14 | optional uint64 bind_uin = 10; 15 | } 16 | -------------------------------------------------------------------------------- /cai/pb/msf/msg/onlinepush/onlinepush_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: cai/pb/msf/msg/onlinepush/onlinepush.proto 4 | """Generated protocol buffer code.""" 5 | from google.protobuf import descriptor as _descriptor 6 | from google.protobuf import message as _message 7 | from google.protobuf import reflection as _reflection 8 | from google.protobuf import symbol_database as _symbol_database 9 | # @@protoc_insertion_point(imports) 10 | 11 | _sym_db = _symbol_database.Default() 12 | 13 | 14 | from cai.pb.msf.msg.comm import comm_pb2 as cai_dot_pb_dot_msf_dot_msg_dot_comm_dot_comm__pb2 15 | 16 | 17 | DESCRIPTOR = _descriptor.FileDescriptor( 18 | name='cai/pb/msf/msg/onlinepush/onlinepush.proto', 19 | package='msf.msg.onlinepush', 20 | syntax='proto2', 21 | serialized_options=None, 22 | create_key=_descriptor._internal_create_key, 23 | serialized_pb=b'\n*cai/pb/msf/msg/onlinepush/onlinepush.proto\x12\x12msf.msg.onlinepush\x1a\x1e\x63\x61i/pb/msf/msg/comm/comm.proto\"\x89\x01\n\tPbPushMsg\x12\x1e\n\x03msg\x18\x01 \x01(\x0b\x32\x11.msf.msg.comm.Msg\x12\r\n\x05svrip\x18\x02 \x01(\x05\x12\x12\n\npush_token\x18\x03 \x01(\x0c\x12\x11\n\tping_flag\x18\x04 \x01(\r\x12\x14\n\x0cgeneral_flag\x18\t \x01(\r\x12\x10\n\x08\x62ind_uin\x18\n \x01(\x04' 24 | , 25 | dependencies=[cai_dot_pb_dot_msf_dot_msg_dot_comm_dot_comm__pb2.DESCRIPTOR,]) 26 | 27 | 28 | 29 | 30 | _PBPUSHMSG = _descriptor.Descriptor( 31 | name='PbPushMsg', 32 | full_name='msf.msg.onlinepush.PbPushMsg', 33 | filename=None, 34 | file=DESCRIPTOR, 35 | containing_type=None, 36 | create_key=_descriptor._internal_create_key, 37 | fields=[ 38 | _descriptor.FieldDescriptor( 39 | name='msg', full_name='msf.msg.onlinepush.PbPushMsg.msg', index=0, 40 | number=1, type=11, cpp_type=10, label=1, 41 | has_default_value=False, default_value=None, 42 | message_type=None, enum_type=None, containing_type=None, 43 | is_extension=False, extension_scope=None, 44 | serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), 45 | _descriptor.FieldDescriptor( 46 | name='svrip', full_name='msf.msg.onlinepush.PbPushMsg.svrip', index=1, 47 | number=2, type=5, cpp_type=1, label=1, 48 | has_default_value=False, default_value=0, 49 | message_type=None, enum_type=None, containing_type=None, 50 | is_extension=False, extension_scope=None, 51 | serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), 52 | _descriptor.FieldDescriptor( 53 | name='push_token', full_name='msf.msg.onlinepush.PbPushMsg.push_token', index=2, 54 | number=3, type=12, cpp_type=9, label=1, 55 | has_default_value=False, default_value=b"", 56 | message_type=None, enum_type=None, containing_type=None, 57 | is_extension=False, extension_scope=None, 58 | serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), 59 | _descriptor.FieldDescriptor( 60 | name='ping_flag', full_name='msf.msg.onlinepush.PbPushMsg.ping_flag', index=3, 61 | number=4, type=13, cpp_type=3, label=1, 62 | has_default_value=False, default_value=0, 63 | message_type=None, enum_type=None, containing_type=None, 64 | is_extension=False, extension_scope=None, 65 | serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), 66 | _descriptor.FieldDescriptor( 67 | name='general_flag', full_name='msf.msg.onlinepush.PbPushMsg.general_flag', index=4, 68 | number=9, type=13, cpp_type=3, label=1, 69 | has_default_value=False, default_value=0, 70 | message_type=None, enum_type=None, containing_type=None, 71 | is_extension=False, extension_scope=None, 72 | serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), 73 | _descriptor.FieldDescriptor( 74 | name='bind_uin', full_name='msf.msg.onlinepush.PbPushMsg.bind_uin', index=5, 75 | number=10, type=4, cpp_type=4, label=1, 76 | has_default_value=False, default_value=0, 77 | message_type=None, enum_type=None, containing_type=None, 78 | is_extension=False, extension_scope=None, 79 | serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), 80 | ], 81 | extensions=[ 82 | ], 83 | nested_types=[], 84 | enum_types=[ 85 | ], 86 | serialized_options=None, 87 | is_extendable=False, 88 | syntax='proto2', 89 | extension_ranges=[], 90 | oneofs=[ 91 | ], 92 | serialized_start=99, 93 | serialized_end=236, 94 | ) 95 | 96 | _PBPUSHMSG.fields_by_name['msg'].message_type = cai_dot_pb_dot_msf_dot_msg_dot_comm_dot_comm__pb2._MSG 97 | DESCRIPTOR.message_types_by_name['PbPushMsg'] = _PBPUSHMSG 98 | _sym_db.RegisterFileDescriptor(DESCRIPTOR) 99 | 100 | PbPushMsg = _reflection.GeneratedProtocolMessageType('PbPushMsg', (_message.Message,), { 101 | 'DESCRIPTOR' : _PBPUSHMSG, 102 | '__module__' : 'cai.pb.msf.msg.onlinepush.onlinepush_pb2' 103 | # @@protoc_insertion_point(class_scope:msf.msg.onlinepush.PbPushMsg) 104 | }) 105 | _sym_db.RegisterMessage(PbPushMsg) 106 | 107 | 108 | # @@protoc_insertion_point(module_scope) 109 | -------------------------------------------------------------------------------- /cai/pb/msf/msg/onlinepush/onlinepush_pb2.pyi: -------------------------------------------------------------------------------- 1 | """ 2 | @generated by mypy-protobuf. Do not edit manually! 3 | isort:skip_file 4 | """ 5 | from builtins import ( 6 | bool, 7 | bytes, 8 | int, 9 | ) 10 | 11 | from cai.pb.msf.msg.comm.comm_pb2 import ( 12 | Msg, 13 | ) 14 | 15 | from google.protobuf.descriptor import ( 16 | Descriptor, 17 | FileDescriptor, 18 | ) 19 | 20 | from google.protobuf.message import ( 21 | Message, 22 | ) 23 | 24 | from typing import ( 25 | Optional, 26 | ) 27 | 28 | from typing_extensions import ( 29 | Literal, 30 | ) 31 | 32 | 33 | DESCRIPTOR: FileDescriptor = ... 34 | 35 | class PbPushMsg(Message): 36 | DESCRIPTOR: Descriptor = ... 37 | MSG_FIELD_NUMBER: int 38 | SVRIP_FIELD_NUMBER: int 39 | PUSH_TOKEN_FIELD_NUMBER: int 40 | PING_FLAG_FIELD_NUMBER: int 41 | GENERAL_FLAG_FIELD_NUMBER: int 42 | BIND_UIN_FIELD_NUMBER: int 43 | svrip: int = ... 44 | push_token: bytes = ... 45 | ping_flag: int = ... 46 | general_flag: int = ... 47 | bind_uin: int = ... 48 | 49 | @property 50 | def msg(self) -> Msg: ... 51 | 52 | def __init__(self, 53 | *, 54 | msg : Optional[Msg] = ..., 55 | svrip : Optional[int] = ..., 56 | push_token : Optional[bytes] = ..., 57 | ping_flag : Optional[int] = ..., 58 | general_flag : Optional[int] = ..., 59 | bind_uin : Optional[int] = ..., 60 | ) -> None: ... 61 | def HasField(self, field_name: Literal[u"bind_uin",b"bind_uin",u"general_flag",b"general_flag",u"msg",b"msg",u"ping_flag",b"ping_flag",u"push_token",b"push_token",u"svrip",b"svrip"]) -> bool: ... 62 | def ClearField(self, field_name: Literal[u"bind_uin",b"bind_uin",u"general_flag",b"general_flag",u"msg",b"msg",u"ping_flag",b"ping_flag",u"push_token",b"push_token",u"svrip",b"svrip"]) -> None: ... 63 | -------------------------------------------------------------------------------- /cai/pb/msf/msg/svc/__init__.py: -------------------------------------------------------------------------------- 1 | """Message Service Protocol Buffer Model Category. 2 | 3 | This module is used to store all message service protobuf files. 4 | 5 | Generate all protobuf file using: 6 | 7 | .. code-block:: bash 8 | 9 | protoc cai/pb/**/*.proto --python_out=. --mypy_out=readable_stubs:. 10 | 11 | :Copyright: Copyright (C) 2021-2021 cscs181 12 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 13 | 14 | .. _LICENSE: 15 | https://github.com/cscs181/CAI/blob/master/LICENSE 16 | """ 17 | 18 | from .svc_pb2 import * 19 | -------------------------------------------------------------------------------- /cai/pb/wtlogin/__init__.py: -------------------------------------------------------------------------------- 1 | """Wtlogin Protocol Buffer Model Category. 2 | 3 | This module is used to store all wtlogin protobuf files. 4 | 5 | Generate all protobuf file using: 6 | 7 | .. code-block:: bash 8 | 9 | protoc cai/pb/**/*.proto --python_out=. --mypy_out=readable_stubs:. 10 | 11 | :Copyright: Copyright (C) 2021-2021 cscs181 12 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 13 | 14 | .. _LICENSE: 15 | https://github.com/cscs181/CAI/blob/master/LICENSE 16 | """ 17 | 18 | from .data_pb2 import * 19 | -------------------------------------------------------------------------------- /cai/pb/wtlogin/data.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | package wtlogin; 3 | 4 | // oicq/wlogin_sdk/pb/device_report.java 5 | message DeviceReport { 6 | optional string bootloader = 1; 7 | optional string proc_version = 2; 8 | optional string codename = 3; 9 | optional string incremental = 4; 10 | optional string fingerprint = 5; 11 | optional string boot_id = 6; 12 | optional string android_id = 7; 13 | optional string base_band = 8; 14 | optional string inner_version = 9; 15 | } 16 | 17 | // oicq/wlogin_sdk/pb/sec_trans.java 18 | message SecTransInfo { 19 | optional string phone_brand = 1; 20 | optional string model_type = 2; 21 | optional string wifi_mac = 3; 22 | optional string bssid = 4; 23 | optional string os_language = 5; 24 | optional uint32 qq_language = 6; 25 | optional string gps_location = 7; 26 | } 27 | -------------------------------------------------------------------------------- /cai/pb/wtlogin/data_pb2.pyi: -------------------------------------------------------------------------------- 1 | """ 2 | @generated by mypy-protobuf. Do not edit manually! 3 | isort:skip_file 4 | """ 5 | from builtins import ( 6 | bool, 7 | int, 8 | ) 9 | 10 | from google.protobuf.descriptor import ( 11 | Descriptor, 12 | FileDescriptor, 13 | ) 14 | 15 | from google.protobuf.message import ( 16 | Message, 17 | ) 18 | 19 | from typing import ( 20 | Optional, 21 | Text, 22 | ) 23 | 24 | from typing_extensions import ( 25 | Literal, 26 | ) 27 | 28 | 29 | DESCRIPTOR: FileDescriptor = ... 30 | 31 | class DeviceReport(Message): 32 | DESCRIPTOR: Descriptor = ... 33 | BOOTLOADER_FIELD_NUMBER: int 34 | PROC_VERSION_FIELD_NUMBER: int 35 | CODENAME_FIELD_NUMBER: int 36 | INCREMENTAL_FIELD_NUMBER: int 37 | FINGERPRINT_FIELD_NUMBER: int 38 | BOOT_ID_FIELD_NUMBER: int 39 | ANDROID_ID_FIELD_NUMBER: int 40 | BASE_BAND_FIELD_NUMBER: int 41 | INNER_VERSION_FIELD_NUMBER: int 42 | bootloader: Text = ... 43 | proc_version: Text = ... 44 | codename: Text = ... 45 | incremental: Text = ... 46 | fingerprint: Text = ... 47 | boot_id: Text = ... 48 | android_id: Text = ... 49 | base_band: Text = ... 50 | inner_version: Text = ... 51 | 52 | def __init__(self, 53 | *, 54 | bootloader : Optional[Text] = ..., 55 | proc_version : Optional[Text] = ..., 56 | codename : Optional[Text] = ..., 57 | incremental : Optional[Text] = ..., 58 | fingerprint : Optional[Text] = ..., 59 | boot_id : Optional[Text] = ..., 60 | android_id : Optional[Text] = ..., 61 | base_band : Optional[Text] = ..., 62 | inner_version : Optional[Text] = ..., 63 | ) -> None: ... 64 | def HasField(self, field_name: Literal[u"android_id",b"android_id",u"base_band",b"base_band",u"boot_id",b"boot_id",u"bootloader",b"bootloader",u"codename",b"codename",u"fingerprint",b"fingerprint",u"incremental",b"incremental",u"inner_version",b"inner_version",u"proc_version",b"proc_version"]) -> bool: ... 65 | def ClearField(self, field_name: Literal[u"android_id",b"android_id",u"base_band",b"base_band",u"boot_id",b"boot_id",u"bootloader",b"bootloader",u"codename",b"codename",u"fingerprint",b"fingerprint",u"incremental",b"incremental",u"inner_version",b"inner_version",u"proc_version",b"proc_version"]) -> None: ... 66 | 67 | class SecTransInfo(Message): 68 | DESCRIPTOR: Descriptor = ... 69 | PHONE_BRAND_FIELD_NUMBER: int 70 | MODEL_TYPE_FIELD_NUMBER: int 71 | WIFI_MAC_FIELD_NUMBER: int 72 | BSSID_FIELD_NUMBER: int 73 | OS_LANGUAGE_FIELD_NUMBER: int 74 | QQ_LANGUAGE_FIELD_NUMBER: int 75 | GPS_LOCATION_FIELD_NUMBER: int 76 | phone_brand: Text = ... 77 | model_type: Text = ... 78 | wifi_mac: Text = ... 79 | bssid: Text = ... 80 | os_language: Text = ... 81 | qq_language: int = ... 82 | gps_location: Text = ... 83 | 84 | def __init__(self, 85 | *, 86 | phone_brand : Optional[Text] = ..., 87 | model_type : Optional[Text] = ..., 88 | wifi_mac : Optional[Text] = ..., 89 | bssid : Optional[Text] = ..., 90 | os_language : Optional[Text] = ..., 91 | qq_language : Optional[int] = ..., 92 | gps_location : Optional[Text] = ..., 93 | ) -> None: ... 94 | def HasField(self, field_name: Literal[u"bssid",b"bssid",u"gps_location",b"gps_location",u"model_type",b"model_type",u"os_language",b"os_language",u"phone_brand",b"phone_brand",u"qq_language",b"qq_language",u"wifi_mac",b"wifi_mac"]) -> bool: ... 95 | def ClearField(self, field_name: Literal[u"bssid",b"bssid",u"gps_location",b"gps_location",u"model_type",b"model_type",u"os_language",b"os_language",u"phone_brand",b"phone_brand",u"qq_language",b"qq_language",u"wifi_mac",b"wifi_mac"]) -> None: ... 96 | -------------------------------------------------------------------------------- /cai/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cscs181/CAI/89eb91ce0d9eb609f30ecaa4007a0e84f094d86d/cai/py.typed -------------------------------------------------------------------------------- /cai/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cscs181/CAI/89eb91ce0d9eb609f30ecaa4007a0e84f094d86d/cai/settings/__init__.py -------------------------------------------------------------------------------- /cai/settings/protocol.py: -------------------------------------------------------------------------------- 1 | """Application Protocol setting 2 | 3 | This module is used to get or new the application protocol setting. 4 | Protocol settings will be stored in APP_DIR provided by storage manager. 5 | Once the protocol setting is loaded, it will be cached until application shut down. 6 | 7 | :Copyright: Copyright (C) 2021-2021 cscs181 8 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 9 | 10 | .. _LICENSE: 11 | https://github.com/cscs181/CAI/blob/master/LICENSE 12 | """ 13 | import os 14 | from typing import Optional, NamedTuple 15 | 16 | from cai.storage import Storage 17 | 18 | MISSING = "MISSING" 19 | 20 | _protocol: Optional["ApkInfo"] = None 21 | 22 | 23 | class ApkInfo(NamedTuple): 24 | apk_id: str 25 | app_id: int 26 | sub_app_id: int # com.tencent.common.config.AppSetting.f 27 | version: str 28 | apk_sign: bytes 29 | build_time: int # oicq.wlogin_sdk.tools.util.BUILD_TIME 30 | sdk_version: str # oicq.wlogin_sdk.tools.util.SDK_VERSION 31 | sso_version: int # oicq.wlogin_sdk.tools.util.SSO_VERSION 32 | bitmap: int # oicq.wlogin_sdk.request.WtloginHelper.mMiscBitmap | 0x2000000 33 | main_sigmap: int # com.tencent.mobileqq.msf.core.auth.n.f 34 | sub_sigmap: int # oicq.wlogin_sdk.request.WtloginHelper.mSubSigMap 35 | 36 | 37 | ANDROID_PHONE = ApkInfo( 38 | apk_id="com.tencent.mobileqq", 39 | app_id=16, 40 | sub_app_id=537066738, 41 | version="8.5.0", 42 | build_time=1607689988, 43 | apk_sign=bytes( 44 | [ 45 | 0xA6, 46 | 0xB7, 47 | 0x45, 48 | 0xBF, 49 | 0x24, 50 | 0xA2, 51 | 0xC2, 52 | 0x77, 53 | 0x52, 54 | 0x77, 55 | 0x16, 56 | 0xF6, 57 | 0xF3, 58 | 0x6E, 59 | 0xB6, 60 | 0x8D, 61 | ] 62 | ), 63 | sdk_version="6.0.0.2454", 64 | sso_version=15, 65 | bitmap=184024956, 66 | main_sigmap=34869472, 67 | sub_sigmap=0x10400, 68 | ) 69 | ANDROID_WATCH = ApkInfo( 70 | apk_id="com.tencent.mobileqq", 71 | app_id=16, 72 | sub_app_id=537061176, 73 | version="8.2.7", 74 | build_time=1571193922, 75 | apk_sign=bytes( 76 | [ 77 | 0xA6, 78 | 0xB7, 79 | 0x45, 80 | 0xBF, 81 | 0x24, 82 | 0xA2, 83 | 0xC2, 84 | 0x77, 85 | 0x52, 86 | 0x77, 87 | 0x16, 88 | 0xF6, 89 | 0xF3, 90 | 0x6E, 91 | 0xB6, 92 | 0x8D, 93 | ] 94 | ), 95 | sdk_version="6.0.0.2413", 96 | sso_version=5, 97 | bitmap=184024956, 98 | main_sigmap=34869472, 99 | sub_sigmap=0x10400, 100 | ) 101 | IPAD = ApkInfo( 102 | apk_id="com.tencent.minihd.qq", 103 | app_id=16, 104 | sub_app_id=537065739, 105 | version="5.8.9", 106 | build_time=1595836208, 107 | apk_sign=bytes( 108 | [ 109 | 170, 110 | 57, 111 | 120, 112 | 244, 113 | 31, 114 | 217, 115 | 111, 116 | 249, 117 | 145, 118 | 74, 119 | 102, 120 | 158, 121 | 24, 122 | 100, 123 | 116, 124 | 199, 125 | ] 126 | ), 127 | sdk_version="6.0.0.2433", 128 | sso_version=12, 129 | bitmap=150470524, 130 | main_sigmap=1970400, 131 | sub_sigmap=66560, 132 | ) 133 | MACOS = ApkInfo( 134 | apk_id="com.tencent.minihd.qq", 135 | app_id=16, 136 | sub_app_id=537064315, 137 | version="5.8.9", 138 | build_time=1595836208, 139 | apk_sign=bytes( 140 | [ 141 | 170, 142 | 57, 143 | 120, 144 | 244, 145 | 31, 146 | 217, 147 | 111, 148 | 249, 149 | 145, 150 | 74, 151 | 102, 152 | 158, 153 | 24, 154 | 100, 155 | 116, 156 | 199, 157 | ] 158 | ), 159 | sdk_version="6.0.0.2433", 160 | sso_version=12, 161 | bitmap=150470524, 162 | main_sigmap=1970400, 163 | sub_sigmap=66560, 164 | ) 165 | 166 | 167 | def get_apk_info(type_: str = "0") -> ApkInfo: 168 | info = {"0": IPAD, "1": ANDROID_PHONE, "2": ANDROID_WATCH, "3": MACOS} 169 | if type_ not in info: 170 | raise ValueError(f"Invalid Protocol Type: {type_}") 171 | return info[type_] 172 | 173 | 174 | def get_protocol(cache: bool = True) -> ApkInfo: 175 | global _protocol 176 | if cache and _protocol: 177 | return _protocol 178 | 179 | type_ = os.getenv(Storage.protocol_env_name, MISSING) 180 | if type_ is MISSING and os.path.exists(Storage.protocol_file): 181 | with open(Storage.protocol_file, "r") as f: 182 | type_ = f.read() 183 | elif type_ is MISSING: 184 | type_ = "0" 185 | with open(Storage.protocol_file, "w") as f: 186 | f.write("0") 187 | _protocol = get_apk_info(type_) 188 | return _protocol 189 | -------------------------------------------------------------------------------- /cai/storage/__init__.py: -------------------------------------------------------------------------------- 1 | """Application Storage Manager 2 | 3 | This module is used to manage application storage. 4 | 5 | :Copyright: Copyright (C) 2021-2021 cscs181 6 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 7 | 8 | .. _LICENSE: 9 | https://github.com/cscs181/CAI/blob/master/LICENSE 10 | """ 11 | import os 12 | import shutil 13 | 14 | from .utils import user_cache_dir, user_config_dir 15 | 16 | 17 | class Storage: 18 | app_name: str = "CAI" 19 | 20 | # application config dir 21 | default_app_dir: str = user_config_dir(app_name, roaming=True) 22 | app_dir: str = os.getenv(f"{app_name}_APP_DIR", default_app_dir) 23 | if not os.path.exists(app_dir): 24 | os.makedirs(app_dir) 25 | if not os.path.isdir(app_dir): 26 | raise RuntimeError( 27 | f"Application directory {app_dir} is not a directory!" 28 | ) 29 | 30 | # application cache dir 31 | default_cache_dir: str = user_cache_dir(app_name) 32 | cache_dir: str = os.getenv(f"{app_name}_CACHE_DIR", default_cache_dir) 33 | if not os.path.exists(cache_dir): 34 | os.makedirs(cache_dir) 35 | if not os.path.isdir(cache_dir): 36 | raise RuntimeError( 37 | f"Application Cache directory {cache_dir} is not a directory!" 38 | ) 39 | 40 | # cai.settings.device 41 | device_file: str = os.path.join(app_dir, "device.json") 42 | 43 | # cai.settings.protocol 44 | protocol_env_name: str = f"{app_name}_PROTOCOL" 45 | protocol_file: str = os.path.join(app_dir, "protocol") 46 | 47 | @classmethod 48 | def clear_cache(cls): 49 | # FIXME: delete used dir only 50 | for path in os.listdir(cls.cache_dir): 51 | if os.path.isdir(path): 52 | shutil.rmtree(path) 53 | else: 54 | os.remove(path) 55 | -------------------------------------------------------------------------------- /cai/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cscs181/CAI/89eb91ce0d9eb609f30ecaa4007a0e84f094d86d/cai/utils/__init__.py -------------------------------------------------------------------------------- /cai/utils/binary.pyi: -------------------------------------------------------------------------------- 1 | # type: ignore 2 | """Binary Tools Stub File. 3 | 4 | Type hints in this file needs python>=3.10 or using ``pyright`` or ``pylance``... 5 | 6 | :Copyright: Copyright (C) 2021-2021 cscs181 7 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 8 | 9 | .. _LICENSE: 10 | https://github.com/cscs181/CAI/blob/master/LICENSE 11 | """ 12 | 13 | from typing_extensions import Unpack, TypeVarTuple 14 | from typing import ( 15 | Any, 16 | List, 17 | Type, 18 | Tuple, 19 | Union, 20 | Generic, 21 | NewType, 22 | TypeVar, 23 | Callable, 24 | Optional, 25 | ) 26 | 27 | P = TypeVar("P", bound="BasePacket") 28 | Ts = TypeVarTuple("Ts") 29 | 30 | BOOL = NewType("BOOL", bool) 31 | INT8 = NewType("INT8", int) 32 | UINT8 = NewType("UINT8", int) 33 | INT16 = NewType("INT16", int) 34 | UINT16 = NewType("UINT16", int) 35 | INT32 = NewType("INT32", int) 36 | UINT32 = NewType("UINT32", int) 37 | INT64 = NewType("INT64", int) 38 | UINT64 = NewType("UINT64", int) 39 | FLOAT = NewType("FLOAT", float) 40 | DOUBLE = NewType("DOUBLE", float) 41 | BYTE = NewType("BYTE", bytes) 42 | BYTES = NewType("BYTES", bytes) 43 | STRING = NewType("STRING", str) 44 | 45 | class BasePacket(bytearray): 46 | @classmethod 47 | def build(cls: Type[P], *data: Union[bytes, "BasePacket"]) -> P: ... 48 | def write(self: P, *data: Union[bytes, "BasePacket"]) -> P: ... 49 | def write_with_length( 50 | self: P, *data: Union[bytes, "BasePacket"], offset: int = ... 51 | ) -> P: ... 52 | def unpack(self, format: Union[bytes, str]) -> Tuple[Any, ...]: ... 53 | def unpack_from( 54 | self, format: Union[bytes, str], offset: int = ... 55 | ) -> Tuple[Any, ...]: ... 56 | def read_int8(self, offset: int = ...) -> INT8: ... 57 | def read_uint8(self, offset: int = ...) -> UINT8: ... 58 | def read_int16(self, offset: int = ...) -> INT16: ... 59 | def read_uint16(self, offset: int = ...) -> UINT16: ... 60 | def read_int32(self, offset: int = ...) -> INT32: ... 61 | def read_uint32(self, offset: int = ...) -> UINT32: ... 62 | def read_int64(self, offset: int = ...) -> INT64: ... 63 | def read_uint64(self, offset: int = ...) -> UINT64: ... 64 | def read_byte(self, offset: int = ...) -> BYTE: ... 65 | def read_bytes(self, n: int, offset: int = ...) -> BYTES: ... 66 | def read_string(self, offset: int = ...) -> STRING: ... 67 | 68 | _bool = bool 69 | 70 | class Packet(BasePacket, Generic[Unpack[Ts]]): 71 | _query: str = ... 72 | _offset: int = ... 73 | _executed: _bool = ... 74 | _filters: List[Callable[[Any], Any]] = ... 75 | def __init__( 76 | self, *args, cache: Optional[Tuple[Any, ...]] = None, **kwargs 77 | ) -> Packet[()]: ... 78 | def __new__(cls, *args, **kwargs) -> Packet[()]: ... 79 | def start(self: Packet[Unpack[Ts]], offset: int = ...) -> Packet[()]: ... 80 | def bool(self: Packet[Unpack[Ts]]) -> Packet[Unpack[Ts], BOOL]: ... 81 | def int8(self: Packet[Unpack[Ts]]) -> Packet[Unpack[Ts], INT8]: ... 82 | def uint8(self: Packet[Unpack[Ts]]) -> Packet[Unpack[Ts], UINT8]: ... 83 | def int16(self: Packet[Unpack[Ts]]) -> Packet[Unpack[Ts], INT16]: ... 84 | def uint16(self: Packet[Unpack[Ts]]) -> Packet[Unpack[Ts], UINT16]: ... 85 | def int32(self: Packet[Unpack[Ts]]) -> Packet[Unpack[Ts], INT32]: ... 86 | def uint32(self: Packet[Unpack[Ts]]) -> Packet[Unpack[Ts], UINT32]: ... 87 | def int64(self: Packet[Unpack[Ts]]) -> Packet[Unpack[Ts], INT64]: ... 88 | def uint64(self: Packet[Unpack[Ts]]) -> Packet[Unpack[Ts], UINT64]: ... 89 | def float(self: Packet[Unpack[Ts]]) -> Packet[Unpack[Ts], FLOAT]: ... 90 | def double(self: Packet[Unpack[Ts]]) -> Packet[Unpack[Ts], DOUBLE]: ... 91 | def byte(self: Packet[Unpack[Ts]]) -> Packet[Unpack[Ts], BYTE]: ... 92 | def bytes( 93 | self: Packet[Unpack[Ts]], length: int 94 | ) -> Packet[Unpack[Ts], BYTES]: ... 95 | def bytes_with_length( 96 | self: Packet[Unpack[Ts]], head_bytes: int, offset: int = ... 97 | ) -> Packet[Unpack[Ts], BYTES]: ... 98 | def string( 99 | self: Packet[Unpack[Ts]], 100 | head_bytes: int, 101 | offset: int = ..., 102 | encoding: str = ..., 103 | ) -> Packet[Unpack[Ts], STRING]: ... 104 | def offset(self: Packet[Unpack[Ts]], offset: int) -> Packet[Unpack[Ts]]: ... 105 | def remain( 106 | self: Packet[Unpack[Ts]], 107 | ) -> Packet[Unpack[Ts], Packet[()]]: ... 108 | def _exec_cache(self: Packet[Unpack[Ts]]) -> Packet[Unpack[Ts]]: ... 109 | def execute(self: Packet[Unpack[Ts]]) -> Tuple[Unpack[Ts]]: ... 110 | -------------------------------------------------------------------------------- /cai/utils/coroutine.py: -------------------------------------------------------------------------------- 1 | """Coroutine Related Tools 2 | 3 | This module is used to build coroutine related tools. 4 | 5 | :Copyright: Copyright (C) 2021-2021 cscs181 6 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 7 | 8 | .. _LICENSE: 9 | https://github.com/cscs181/CAI/blob/master/LICENSE 10 | """ 11 | from types import TracebackType 12 | from collections.abc import Coroutine 13 | from typing import AsyncContextManager 14 | from typing import Coroutine as CoroutineGeneric 15 | from typing import Type, Generic, TypeVar, Optional 16 | 17 | TY = TypeVar("TY") 18 | TS = TypeVar("TS") 19 | TR = TypeVar("TR", bound=AsyncContextManager) 20 | 21 | 22 | class ContextManager(Coroutine, Generic[TY, TS, TR]): 23 | 24 | __slots__ = ("_coro", "_obj") 25 | 26 | def __init__(self, coro: CoroutineGeneric[TY, TS, TR]): 27 | self._coro = coro 28 | self._obj: Optional[TR] = None 29 | 30 | def send(self, value: TS) -> TY: 31 | return self._coro.send(value) 32 | 33 | def throw(self, typ, val=None, tb=None): 34 | if val is None: 35 | return self._coro.throw(typ) 36 | elif tb is None: 37 | return self._coro.throw(typ, val) 38 | else: 39 | return self._coro.throw(typ, val, tb) 40 | 41 | def close(self): 42 | return self._coro.close() 43 | 44 | def __next__(self): 45 | return self.send(None) 46 | 47 | def __iter__(self): 48 | return self._coro.__await__() 49 | 50 | def __await__(self): 51 | return self._coro.__await__() 52 | 53 | async def __aenter__(self) -> TR: 54 | self._obj = await self._coro 55 | return self._obj 56 | 57 | async def __aexit__( 58 | self, 59 | exc_type: Optional[Type[BaseException]], 60 | exc_value: Optional[BaseException], 61 | traceback: Optional[TracebackType], 62 | ): 63 | await self._obj.__aexit__(exc_type, exc_value, traceback) # type: ignore 64 | self._obj = None 65 | -------------------------------------------------------------------------------- /cai/utils/crypto.py: -------------------------------------------------------------------------------- 1 | """Crypto Tool. 2 | 3 | This module is used to encrypt data using ECDH or Session ticket encryption. 4 | 5 | :Copyright: Copyright (C) 2021-2021 cscs181 6 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 7 | 8 | .. _LICENSE: 9 | https://github.com/cscs181/CAI/blob/master/LICENSE 10 | """ 11 | import struct 12 | from hashlib import md5 13 | from typing import Union 14 | 15 | from rtea import qqtea_encrypt 16 | from cryptography.hazmat.primitives.asymmetric import ec 17 | from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat 18 | 19 | from cai.utils.binary import Packet 20 | 21 | 22 | class ECDH: 23 | id = 0x87 24 | _p256 = ec.SECP256R1() 25 | 26 | svr_public_key = ec.EllipticCurvePublicKey.from_encoded_point( 27 | _p256, 28 | bytes.fromhex( 29 | "04" 30 | "EBCA94D733E399B2DB96EACDD3F69A8BB0F74224E2B44E3357812211D2E62EFB" 31 | "C91BB553098E25E33A799ADC7F76FEB208DA7C6522CDB0719A305180CC54A82E" 32 | ), 33 | ) 34 | 35 | client_private_key = ec.generate_private_key(_p256) 36 | client_public_key = client_private_key.public_key().public_bytes( 37 | Encoding.X962, PublicFormat.UncompressedPoint 38 | ) 39 | 40 | share_key = md5( 41 | client_private_key.exchange(ec.ECDH(), svr_public_key)[:16] 42 | ).digest() 43 | 44 | @classmethod 45 | def encrypt( 46 | cls, data: Union[bytes, Packet], key: Union[bytes, Packet] 47 | ) -> Packet: 48 | return Packet.build( 49 | struct.pack(">BB", 2, 1), 50 | key, 51 | struct.pack( 52 | ">HHH", 53 | 305, 54 | 1, # oicq.wlogin_sdk.tools.EcdhCrypt.sKeyVersion 55 | len(cls.client_public_key), 56 | ), 57 | cls.client_public_key, 58 | qqtea_encrypt(bytes(data), cls.share_key), 59 | ) 60 | 61 | 62 | class EncryptSession: 63 | id = 0x45 64 | 65 | def __init__(self, ticket: bytes): 66 | self.ticket = ticket 67 | 68 | def encrypt( 69 | self, data: Union[bytes, Packet], key: Union[bytes, Packet] 70 | ) -> Packet: 71 | return Packet.build( 72 | struct.pack(">H", len(self.ticket)), 73 | self.ticket, 74 | qqtea_encrypt(bytes(data), bytes(key)), 75 | ) 76 | -------------------------------------------------------------------------------- /cai/utils/future.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Any, Dict, Generic, TypeVar, Callable, Optional 3 | 4 | KT = TypeVar("KT") 5 | VT = TypeVar("VT") 6 | 7 | 8 | class FutureStore(Generic[KT, VT]): 9 | def __init__(self): 10 | # Generic Future is supported since py3.9 11 | self._futures: Dict[KT, "asyncio.Future[VT]"] = {} 12 | 13 | def __contains__(self, seq: KT) -> bool: 14 | return seq in self._futures 15 | 16 | def store_seq(self, seq: KT) -> "asyncio.Future[VT]": 17 | if seq in self._futures: 18 | raise KeyError(f"Sequence {seq} already exists!") 19 | 20 | future = asyncio.get_event_loop().create_future() 21 | self._futures[seq] = future 22 | return future 23 | 24 | def store_result(self, seq: KT, result: VT): 25 | future = self._futures.get(seq) 26 | if future and not future.cancelled(): 27 | future.set_result(result) 28 | 29 | def pop_seq(self, seq: KT) -> "asyncio.Future[VT]": 30 | return self._futures.pop(seq) 31 | 32 | def add_callback( 33 | self, seq: KT, callback: Callable[["asyncio.Future[VT]"], Any] 34 | ): 35 | future = self._futures[seq] 36 | future.add_done_callback(callback) 37 | 38 | def remove_callback( 39 | self, seq: KT, callback: Callable[["asyncio.Future[VT]"], Any] 40 | ) -> int: 41 | future = self._futures[seq] 42 | return future.remove_done_callback(callback) 43 | 44 | def done(self, seq: KT) -> bool: 45 | return self._futures[seq].done() 46 | 47 | def result(self, seq: KT) -> VT: 48 | return self._futures[seq].result() 49 | 50 | def cancel(self, seq: KT) -> bool: 51 | return self._futures[seq].cancel() 52 | 53 | def cancel_all(self): 54 | for future in self._futures.values(): 55 | future.cancel() 56 | 57 | def exception(self, seq: KT) -> Optional[BaseException]: 58 | return self._futures[seq].exception() 59 | 60 | async def fetch(self, seq: KT, timeout: Optional[float] = None) -> VT: 61 | future = ( 62 | self.store_seq(seq) 63 | if seq not in self._futures 64 | else self._futures[seq] 65 | ) 66 | try: 67 | return await asyncio.wait_for(future, timeout) 68 | finally: 69 | del self._futures[seq] 70 | -------------------------------------------------------------------------------- /cai/utils/jce.py: -------------------------------------------------------------------------------- 1 | """JCE Related Tools 2 | 3 | This module is used to build JCE related tools including packaging and serialization. 4 | 5 | :Copyright: Copyright (C) 2021-2021 cscs181 6 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 7 | 8 | .. _LICENSE: 9 | https://github.com/cscs181/CAI/blob/master/LICENSE 10 | """ 11 | import struct 12 | from typing import Optional 13 | 14 | from jce import JceField, JceStruct, JceDecoder, types 15 | 16 | 17 | class RequestPacket(JceStruct): 18 | """ 19 | Note: 20 | Source: com.qq.taf.RequestPacket 21 | """ 22 | 23 | version: types.INT16 = JceField(0, jce_id=1) 24 | pkg_type: types.BYTE = JceField(bytes(1), jce_id=2) 25 | msg_type: types.INT32 = JceField(0, jce_id=3) 26 | req_id: types.INT32 = JceField(0, jce_id=4) 27 | servant_name: types.STRING = JceField(jce_id=5) 28 | func_name: types.STRING = JceField(jce_id=6) 29 | buffer: types.BYTES = JceField(bytes(0), jce_id=7) 30 | timeout: types.INT32 = JceField(0, jce_id=8) 31 | context: types.MAP = JceField({}, jce_id=9) 32 | status: types.MAP = JceField({}, jce_id=10) 33 | 34 | 35 | class RequestPacketVersion2(RequestPacket): 36 | """ 37 | Note: 38 | Source: com.qq.jce.wup.OldUniAttribute 39 | """ 40 | 41 | version: types.INT16 = JceField(2, jce_id=1) 42 | # raw data for buffer field 43 | data: Optional[ 44 | types.MAP[types.STRING, types.MAP[types.STRING, types.JceType]] 45 | ] = None 46 | 47 | def _prepare_buffer(self): 48 | if not self.data: 49 | raise RuntimeError("No data available") 50 | self.buffer = types.BYTES.validate(types.MAP.to_bytes(0, self.data)) 51 | 52 | @classmethod 53 | def _prepare_data( 54 | cls, buffer: types.BYTES 55 | ) -> types.MAP[types.STRING, types.MAP[types.STRING, types.JceType]]: 56 | _, data, _ = JceDecoder.decode_single(buffer) 57 | return data # type: ignore 58 | 59 | def encode(self, with_length: bool = False) -> bytes: 60 | self._prepare_buffer() 61 | buffer = super().encode() 62 | if with_length: 63 | return struct.pack(">I", len(buffer) + 4) + buffer 64 | return buffer 65 | 66 | @classmethod 67 | def decode(cls, data: bytes, **extra) -> "RequestPacketVersion2": 68 | packet: RequestPacketVersion2 = super().decode( 69 | data, **extra 70 | ) # type: ignore 71 | data_ = cls._prepare_data(packet.buffer) 72 | packet.data = data_ 73 | return packet 74 | 75 | 76 | class RequestPacketVersion3(RequestPacket): 77 | """ 78 | Note: 79 | Source: com.qq.jce.wup.UniAttribute 80 | """ 81 | 82 | version: types.INT16 = JceField(3, jce_id=1) 83 | # raw data for buffer field 84 | data: Optional[types.MAP[types.STRING, types.JceType]] = None 85 | 86 | def _prepare_buffer(self): 87 | if not self.data: 88 | raise RuntimeError("No data available") 89 | self.buffer = types.BYTES.validate(types.MAP.to_bytes(0, self.data)) 90 | 91 | @classmethod 92 | def _prepare_data( 93 | cls, buffer: types.BYTES 94 | ) -> types.MAP[types.STRING, types.JceType]: 95 | _, data, _ = JceDecoder.decode_single(buffer) 96 | return data # type: ignore 97 | 98 | def encode(self, with_length: bool = False) -> bytes: 99 | self._prepare_buffer() 100 | buffer = super().encode() 101 | if with_length: 102 | return struct.pack(">I", len(buffer) + 4) + buffer 103 | return buffer 104 | 105 | @classmethod 106 | def decode(cls, data: bytes, **extra) -> "RequestPacketVersion3": 107 | packet: RequestPacketVersion3 = super().decode( 108 | data, **extra 109 | ) # type: ignore 110 | data_ = cls._prepare_data(packet.buffer) 111 | packet.data = data_ 112 | return packet 113 | -------------------------------------------------------------------------------- /docs/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cscs181/CAI/89eb91ce0d9eb609f30ecaa4007a0e84f094d86d/docs/assets/logo.png -------------------------------------------------------------------------------- /docs/assets/logo_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cscs181/CAI/89eb91ce0d9eb609f30ecaa4007a0e84f094d86d/docs/assets/logo_text.png -------------------------------------------------------------------------------- /docs/assets/logo_text_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cscs181/CAI/89eb91ce0d9eb609f30ecaa4007a0e84f094d86d/docs/assets/logo_text_white.png -------------------------------------------------------------------------------- /docs/assets/logo_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cscs181/CAI/89eb91ce0d9eb609f30ecaa4007a0e84f094d86d/docs/assets/logo_white.png -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | 16 | sys.path.insert(0, os.path.abspath("..")) 17 | 18 | if os.getenv("READTHEDOCS"): 19 | import subprocess 20 | 21 | requirements_path = os.path.join( 22 | os.path.dirname(os.path.abspath(__file__)), "requirements.txt" 23 | ) 24 | prog = subprocess.run( 25 | f"{sys.executable} -m pip install poetry &&" 26 | f"{sys.executable} -m poetry export -o {requirements_path} --dev --without-hashes &&" 27 | f"{sys.executable} -m pip install -r {requirements_path}", 28 | shell=True, 29 | ) 30 | assert prog.returncode == 0 31 | 32 | # -- Project information ----------------------------------------------------- 33 | 34 | project = "cai" 35 | copyright = "2021, cscs181" 36 | author = "cscs181" 37 | 38 | # The full version, including alpha/beta/rc tags 39 | release = "0.1.0" 40 | language = "zh_CN" 41 | 42 | # -- General configuration --------------------------------------------------- 43 | 44 | # Add any Sphinx extension module names here, as strings. They can be 45 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 46 | # ones. 47 | import sphinx_rtd_theme 48 | 49 | extensions = [ 50 | "sphinxcontrib.napoleon", 51 | "sphinx_rtd_theme", 52 | "sphinx_copybutton", 53 | ] 54 | 55 | # Add any paths that contain templates here, relative to this directory. 56 | templates_path = ["_templates"] 57 | 58 | # List of patterns, relative to source directory, that match files and 59 | # directories to ignore when looking for source files. 60 | # This pattern also affects html_static_path and html_extra_path. 61 | exclude_patterns = [] 62 | 63 | # -- Options for HTML output ------------------------------------------------- 64 | 65 | # The theme to use for HTML and HTML Help pages. See the documentation for 66 | # a list of builtin themes. 67 | # 68 | html_theme = "sphinx_rtd_theme" 69 | html_theme_options = {"logo_only": True, "collapse_navigation": False} 70 | html_logo = "assets/logo_text_white.png" 71 | 72 | # Add any paths that contain custom static files (such as style sheets) here, 73 | # relative to this directory. They are copied after the builtin static files, 74 | # so a file named "default.css" will overwrite the builtin "default.css". 75 | html_static_path = ["_static"] 76 | 77 | # -- Options for autodoc extension ---------------------------------------------- 78 | autodoc_default_options = { 79 | "member-order": "bysource", 80 | "ignore-module-all": True, 81 | } 82 | -------------------------------------------------------------------------------- /docs/examples/index.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | 4 | 5 | Login 6 | ----- 7 | 8 | .. literalinclude:: ../../examples/login.py 9 | :language: python 10 | :linenos: 11 | 12 | 13 | Set Client Status 14 | ----------------- 15 | 16 | .. literalinclude:: ../../examples/set_status.py 17 | :language: python 18 | :linenos: 19 | 20 | 21 | Friend and Group 22 | ---------------- 23 | 24 | .. literalinclude:: ../../examples/friend_group.py 25 | :language: python 26 | :linenos: 27 | 28 | 29 | Message 30 | ------- 31 | 32 | .. literalinclude:: ../../examples/message.py 33 | :language: python 34 | :linenos: 35 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. cai documentation master file, created by 2 | sphinx-quickstart on Fri Feb 5 17:41:41 2021. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to cai's documentation! 7 | =============================== 8 | 9 | Examples 10 | -------- 11 | 12 | :doc:`Example Code ` 13 | 14 | Api Reference 15 | ------------- 16 | 17 | :doc:`Api Reference ` 18 | 19 | .. toctree:: 20 | :maxdepth: 2 21 | :caption: Examples 22 | 23 | examples/index 24 | 25 | .. toctree:: 26 | :maxdepth: 1 27 | :caption: Api Reference 28 | 29 | py-modindex 30 | -------------------------------------------------------------------------------- /docs/py-modindex.rst: -------------------------------------------------------------------------------- 1 | Api Reference 2 | ============= 3 | -------------------------------------------------------------------------------- /docs/source/cai.api.rst: -------------------------------------------------------------------------------- 1 | cai.api package 2 | =============== 3 | 4 | .. automodule:: cai.api 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | cai.api.client module 13 | --------------------- 14 | 15 | .. automodule:: cai.api.client 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | cai.api.flow module 21 | ------------------- 22 | 23 | .. automodule:: cai.api.flow 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | cai.api.friend module 29 | --------------------- 30 | 31 | .. automodule:: cai.api.friend 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | cai.api.group module 37 | -------------------- 38 | 39 | .. automodule:: cai.api.group 40 | :members: 41 | :undoc-members: 42 | :show-inheritance: 43 | 44 | cai.api.login module 45 | -------------------- 46 | 47 | .. automodule:: cai.api.login 48 | :members: 49 | :undoc-members: 50 | :show-inheritance: 51 | -------------------------------------------------------------------------------- /docs/source/cai.client.config_push.rst: -------------------------------------------------------------------------------- 1 | cai.client.config\_push package 2 | =============================== 3 | 4 | .. automodule:: cai.client.config_push 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | cai.client.config\_push.command module 13 | -------------------------------------- 14 | 15 | .. automodule:: cai.client.config_push.command 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | cai.client.config\_push.jce module 21 | ---------------------------------- 22 | 23 | .. automodule:: cai.client.config_push.jce 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | -------------------------------------------------------------------------------- /docs/source/cai.client.friendlist.rst: -------------------------------------------------------------------------------- 1 | cai.client.friendlist package 2 | ============================= 3 | 4 | .. automodule:: cai.client.friendlist 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | cai.client.friendlist.command module 13 | ------------------------------------ 14 | 15 | .. automodule:: cai.client.friendlist.command 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | cai.client.friendlist.jce module 21 | -------------------------------- 22 | 23 | .. automodule:: cai.client.friendlist.jce 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | -------------------------------------------------------------------------------- /docs/source/cai.client.heartbeat.rst: -------------------------------------------------------------------------------- 1 | cai.client.heartbeat package 2 | ============================ 3 | 4 | .. automodule:: cai.client.heartbeat 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/cai.client.message_service.rst: -------------------------------------------------------------------------------- 1 | cai.client.message\_service package 2 | =================================== 3 | 4 | .. automodule:: cai.client.message_service 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | cai.client.message\_service.command module 13 | ------------------------------------------ 14 | 15 | .. automodule:: cai.client.message_service.command 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | cai.client.message\_service.decoders module 21 | ------------------------------------------- 22 | 23 | .. automodule:: cai.client.message_service.decoders 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | cai.client.message\_service.jce module 29 | -------------------------------------- 30 | 31 | .. automodule:: cai.client.message_service.jce 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | cai.client.message\_service.models module 37 | ----------------------------------------- 38 | 39 | .. automodule:: cai.client.message_service.models 40 | :members: 41 | :undoc-members: 42 | :show-inheritance: 43 | -------------------------------------------------------------------------------- /docs/source/cai.client.online_push.rst: -------------------------------------------------------------------------------- 1 | cai.client.online\_push package 2 | =============================== 3 | 4 | .. automodule:: cai.client.online_push 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | cai.client.online\_push.command module 13 | -------------------------------------- 14 | 15 | .. automodule:: cai.client.online_push.command 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | cai.client.online\_push.jce module 21 | ---------------------------------- 22 | 23 | .. automodule:: cai.client.online_push.jce 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | -------------------------------------------------------------------------------- /docs/source/cai.client.qq_service.rst: -------------------------------------------------------------------------------- 1 | cai.client.qq\_service package 2 | ============================== 3 | 4 | .. automodule:: cai.client.qq_service 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | cai.client.qq\_service.jce module 13 | --------------------------------- 14 | 15 | .. automodule:: cai.client.qq_service.jce 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/source/cai.client.rst: -------------------------------------------------------------------------------- 1 | cai.client package 2 | ================== 3 | 4 | .. automodule:: cai.client 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Subpackages 10 | ----------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | cai.client.config_push 16 | cai.client.friendlist 17 | cai.client.heartbeat 18 | cai.client.message_service 19 | cai.client.online_push 20 | cai.client.qq_service 21 | cai.client.sso_server 22 | cai.client.status_service 23 | cai.client.wtlogin 24 | 25 | Submodules 26 | ---------- 27 | 28 | cai.client.client module 29 | ------------------------ 30 | 31 | .. automodule:: cai.client.client 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | cai.client.command module 37 | ------------------------- 38 | 39 | .. automodule:: cai.client.command 40 | :members: 41 | :undoc-members: 42 | :show-inheritance: 43 | 44 | cai.client.event module 45 | ----------------------- 46 | 47 | .. automodule:: cai.client.event 48 | :members: 49 | :undoc-members: 50 | :show-inheritance: 51 | 52 | cai.client.models module 53 | ------------------------ 54 | 55 | .. automodule:: cai.client.models 56 | :members: 57 | :undoc-members: 58 | :show-inheritance: 59 | 60 | cai.client.packet module 61 | ------------------------ 62 | 63 | .. automodule:: cai.client.packet 64 | :members: 65 | :undoc-members: 66 | :show-inheritance: 67 | -------------------------------------------------------------------------------- /docs/source/cai.client.sso_server.rst: -------------------------------------------------------------------------------- 1 | cai.client.sso\_server package 2 | ============================== 3 | 4 | .. automodule:: cai.client.sso_server 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | cai.client.sso\_server.jce module 13 | --------------------------------- 14 | 15 | .. automodule:: cai.client.sso_server.jce 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/source/cai.client.status_service.rst: -------------------------------------------------------------------------------- 1 | cai.client.status\_service package 2 | ================================== 3 | 4 | .. automodule:: cai.client.status_service 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | cai.client.status\_service.command module 13 | ----------------------------------------- 14 | 15 | .. automodule:: cai.client.status_service.command 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | cai.client.status\_service.jce module 21 | ------------------------------------- 22 | 23 | .. automodule:: cai.client.status_service.jce 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | -------------------------------------------------------------------------------- /docs/source/cai.client.wtlogin.rst: -------------------------------------------------------------------------------- 1 | cai.client.wtlogin package 2 | ========================== 3 | 4 | .. automodule:: cai.client.wtlogin 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | cai.client.wtlogin.oicq module 13 | ------------------------------ 14 | 15 | .. automodule:: cai.client.wtlogin.oicq 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | cai.client.wtlogin.tlv module 21 | ----------------------------- 22 | 23 | .. automodule:: cai.client.wtlogin.tlv 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | -------------------------------------------------------------------------------- /docs/source/cai.connection.rst: -------------------------------------------------------------------------------- 1 | cai.connection package 2 | ====================== 3 | 4 | .. automodule:: cai.connection 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | cai.connection.utils module 13 | --------------------------- 14 | 15 | .. automodule:: cai.connection.utils 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/source/cai.pb.im.msg.common.rst: -------------------------------------------------------------------------------- 1 | cai.pb.im.msg.common package 2 | ============================ 3 | 4 | .. automodule:: cai.pb.im.msg.common 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | cai.pb.im.msg.common.common\_pb2 module 13 | --------------------------------------- 14 | 15 | .. automodule:: cai.pb.im.msg.common.common_pb2 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/source/cai.pb.im.msg.msg.rst: -------------------------------------------------------------------------------- 1 | cai.pb.im.msg.msg package 2 | ========================= 3 | 4 | .. automodule:: cai.pb.im.msg.msg 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | cai.pb.im.msg.msg.msg\_pb2 module 13 | --------------------------------- 14 | 15 | .. automodule:: cai.pb.im.msg.msg.msg_pb2 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/source/cai.pb.im.msg.msg_body.rst: -------------------------------------------------------------------------------- 1 | cai.pb.im.msg.msg\_body package 2 | =============================== 3 | 4 | .. automodule:: cai.pb.im.msg.msg_body 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | cai.pb.im.msg.msg\_body.msg\_body\_pb2 module 13 | --------------------------------------------- 14 | 15 | .. automodule:: cai.pb.im.msg.msg_body.msg_body_pb2 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/source/cai.pb.im.msg.msg_head.rst: -------------------------------------------------------------------------------- 1 | cai.pb.im.msg.msg\_head package 2 | =============================== 3 | 4 | .. automodule:: cai.pb.im.msg.msg_head 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | cai.pb.im.msg.msg\_head.msg\_head\_pb2 module 13 | --------------------------------------------- 14 | 15 | .. automodule:: cai.pb.im.msg.msg_head.msg_head_pb2 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/source/cai.pb.im.msg.obj_msg.rst: -------------------------------------------------------------------------------- 1 | cai.pb.im.msg.obj\_msg package 2 | ============================== 3 | 4 | .. automodule:: cai.pb.im.msg.obj_msg 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | cai.pb.im.msg.obj\_msg.obj\_msg\_pb2 module 13 | ------------------------------------------- 14 | 15 | .. automodule:: cai.pb.im.msg.obj_msg.obj_msg_pb2 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/source/cai.pb.im.msg.receipt.rst: -------------------------------------------------------------------------------- 1 | cai.pb.im.msg.receipt package 2 | ============================= 3 | 4 | .. automodule:: cai.pb.im.msg.receipt 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | cai.pb.im.msg.receipt.receipt\_pb2 module 13 | ----------------------------------------- 14 | 15 | .. automodule:: cai.pb.im.msg.receipt.receipt_pb2 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/source/cai.pb.im.msg.rst: -------------------------------------------------------------------------------- 1 | cai.pb.im.msg package 2 | ===================== 3 | 4 | .. automodule:: cai.pb.im.msg 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Subpackages 10 | ----------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | cai.pb.im.msg.common 16 | cai.pb.im.msg.msg 17 | cai.pb.im.msg.msg_body 18 | cai.pb.im.msg.msg_head 19 | cai.pb.im.msg.obj_msg 20 | cai.pb.im.msg.receipt 21 | -------------------------------------------------------------------------------- /docs/source/cai.pb.im.oidb.cmd0x769.rst: -------------------------------------------------------------------------------- 1 | cai.pb.im.oidb.cmd0x769 package 2 | =============================== 3 | 4 | .. automodule:: cai.pb.im.oidb.cmd0x769 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | cai.pb.im.oidb.cmd0x769.cmd0x769\_pb2 module 13 | -------------------------------------------- 14 | 15 | .. automodule:: cai.pb.im.oidb.cmd0x769.cmd0x769_pb2 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/source/cai.pb.im.oidb.cmd0xd50.rst: -------------------------------------------------------------------------------- 1 | cai.pb.im.oidb.cmd0xd50 package 2 | =============================== 3 | 4 | .. automodule:: cai.pb.im.oidb.cmd0xd50 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | cai.pb.im.oidb.cmd0xd50.cmd0xd50\_pb2 module 13 | -------------------------------------------- 14 | 15 | .. automodule:: cai.pb.im.oidb.cmd0xd50.cmd0xd50_pb2 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/source/cai.pb.im.oidb.rst: -------------------------------------------------------------------------------- 1 | cai.pb.im.oidb package 2 | ====================== 3 | 4 | .. automodule:: cai.pb.im.oidb 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Subpackages 10 | ----------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | cai.pb.im.oidb.cmd0x769 16 | cai.pb.im.oidb.cmd0xd50 17 | -------------------------------------------------------------------------------- /docs/source/cai.pb.im.rst: -------------------------------------------------------------------------------- 1 | cai.pb.im package 2 | ================= 3 | 4 | .. automodule:: cai.pb.im 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Subpackages 10 | ----------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | cai.pb.im.msg 16 | cai.pb.im.oidb 17 | -------------------------------------------------------------------------------- /docs/source/cai.pb.msf.msg.comm.rst: -------------------------------------------------------------------------------- 1 | cai.pb.msf.msg.comm package 2 | =========================== 3 | 4 | .. automodule:: cai.pb.msf.msg.comm 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | cai.pb.msf.msg.comm.comm\_pb2 module 13 | ------------------------------------ 14 | 15 | .. automodule:: cai.pb.msf.msg.comm.comm_pb2 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/source/cai.pb.msf.msg.ctrl.rst: -------------------------------------------------------------------------------- 1 | cai.pb.msf.msg.ctrl package 2 | =========================== 3 | 4 | .. automodule:: cai.pb.msf.msg.ctrl 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | cai.pb.msf.msg.ctrl.ctrl\_pb2 module 13 | ------------------------------------ 14 | 15 | .. automodule:: cai.pb.msf.msg.ctrl.ctrl_pb2 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/source/cai.pb.msf.msg.onlinepush.rst: -------------------------------------------------------------------------------- 1 | cai.pb.msf.msg.onlinepush package 2 | ================================= 3 | 4 | .. automodule:: cai.pb.msf.msg.onlinepush 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | cai.pb.msf.msg.onlinepush.onlinepush\_pb2 module 13 | ------------------------------------------------ 14 | 15 | .. automodule:: cai.pb.msf.msg.onlinepush.onlinepush_pb2 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/source/cai.pb.msf.msg.rst: -------------------------------------------------------------------------------- 1 | cai.pb.msf.msg package 2 | ====================== 3 | 4 | .. automodule:: cai.pb.msf.msg 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Subpackages 10 | ----------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | cai.pb.msf.msg.comm 16 | cai.pb.msf.msg.ctrl 17 | cai.pb.msf.msg.onlinepush 18 | cai.pb.msf.msg.svc 19 | -------------------------------------------------------------------------------- /docs/source/cai.pb.msf.msg.svc.rst: -------------------------------------------------------------------------------- 1 | cai.pb.msf.msg.svc package 2 | ========================== 3 | 4 | .. automodule:: cai.pb.msf.msg.svc 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | cai.pb.msf.msg.svc.svc\_pb2 module 13 | ---------------------------------- 14 | 15 | .. automodule:: cai.pb.msf.msg.svc.svc_pb2 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/source/cai.pb.msf.rst: -------------------------------------------------------------------------------- 1 | cai.pb.msf package 2 | ================== 3 | 4 | .. automodule:: cai.pb.msf 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Subpackages 10 | ----------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | cai.pb.msf.msg 16 | -------------------------------------------------------------------------------- /docs/source/cai.pb.rst: -------------------------------------------------------------------------------- 1 | cai.pb package 2 | ============== 3 | 4 | .. automodule:: cai.pb 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Subpackages 10 | ----------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | cai.pb.im 16 | cai.pb.msf 17 | cai.pb.wtlogin 18 | -------------------------------------------------------------------------------- /docs/source/cai.pb.wtlogin.rst: -------------------------------------------------------------------------------- 1 | cai.pb.wtlogin package 2 | ====================== 3 | 4 | .. automodule:: cai.pb.wtlogin 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | cai.pb.wtlogin.data\_pb2 module 13 | ------------------------------- 14 | 15 | .. automodule:: cai.pb.wtlogin.data_pb2 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/source/cai.rst: -------------------------------------------------------------------------------- 1 | cai package 2 | =========== 3 | 4 | .. automodule:: cai 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Subpackages 10 | ----------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | cai.api 16 | cai.client 17 | cai.connection 18 | cai.pb 19 | cai.settings 20 | cai.storage 21 | cai.utils 22 | 23 | Submodules 24 | ---------- 25 | 26 | cai.exceptions module 27 | --------------------- 28 | 29 | .. automodule:: cai.exceptions 30 | :members: 31 | :undoc-members: 32 | :show-inheritance: 33 | 34 | cai.log module 35 | -------------- 36 | 37 | .. automodule:: cai.log 38 | :members: 39 | :undoc-members: 40 | :show-inheritance: 41 | -------------------------------------------------------------------------------- /docs/source/cai.settings.rst: -------------------------------------------------------------------------------- 1 | cai.settings package 2 | ==================== 3 | 4 | .. automodule:: cai.settings 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | cai.settings.device module 13 | -------------------------- 14 | 15 | .. automodule:: cai.settings.device 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | cai.settings.protocol module 21 | ---------------------------- 22 | 23 | .. automodule:: cai.settings.protocol 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | -------------------------------------------------------------------------------- /docs/source/cai.storage.rst: -------------------------------------------------------------------------------- 1 | cai.storage package 2 | =================== 3 | 4 | .. automodule:: cai.storage 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | cai.storage.utils module 13 | ------------------------ 14 | 15 | .. automodule:: cai.storage.utils 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /docs/source/cai.utils.rst: -------------------------------------------------------------------------------- 1 | cai.utils package 2 | ================= 3 | 4 | .. automodule:: cai.utils 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | cai.utils.binary module 13 | ----------------------- 14 | 15 | .. automodule:: cai.utils.binary 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | cai.utils.coroutine module 21 | -------------------------- 22 | 23 | .. automodule:: cai.utils.coroutine 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | cai.utils.crypto module 29 | ----------------------- 30 | 31 | .. automodule:: cai.utils.crypto 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | cai.utils.dataclass module 37 | -------------------------- 38 | 39 | .. automodule:: cai.utils.dataclass 40 | :members: 41 | :undoc-members: 42 | :show-inheritance: 43 | 44 | cai.utils.future module 45 | ----------------------- 46 | 47 | .. automodule:: cai.utils.future 48 | :members: 49 | :undoc-members: 50 | :show-inheritance: 51 | 52 | cai.utils.jce module 53 | -------------------- 54 | 55 | .. automodule:: cai.utils.jce 56 | :members: 57 | :undoc-members: 58 | :show-inheritance: 59 | -------------------------------------------------------------------------------- /docs/source/modules.rst: -------------------------------------------------------------------------------- 1 | cai 2 | === 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | cai 8 | -------------------------------------------------------------------------------- /examples/friend_group.py: -------------------------------------------------------------------------------- 1 | """Example Code for Friend/Group. 2 | 3 | :Copyright: Copyright (C) 2021-2021 cscs181 4 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 5 | 6 | .. _LICENSE: 7 | https://github.com/cscs181/CAI/blob/master/LICENSE 8 | """ 9 | import os 10 | import signal 11 | import asyncio 12 | from hashlib import md5 13 | 14 | import cai 15 | 16 | 17 | async def run(): 18 | account = os.getenv("ACCOUNT", "") 19 | password = os.getenv("PASSWORD") 20 | try: 21 | account = int(account) 22 | assert password 23 | except Exception: 24 | print( 25 | f"Error: account '{account}', password '{password}'" # type: ignore 26 | ) 27 | return 28 | 29 | client = await cai.login(account, md5(password.encode()).digest()) 30 | 31 | # friend 32 | friend_list = await cai.get_friend_list() 33 | friend_group_list = await cai.get_friend_group_list() 34 | print("========== friends ==========", *friend_list, sep="\n") 35 | print("========== friend groups ==========", *friend_group_list, sep="\n") 36 | example_friend = friend_list[0] 37 | # friend = await cai.get_friend(friend_uin) 38 | print("========== example friend ==========") 39 | print("uin: ", example_friend.uin) 40 | print("nick: ", example_friend.nick) 41 | print("remark: ", example_friend.remark) 42 | print("group: ", await example_friend.get_group()) 43 | 44 | group_list = await cai.get_group_list() 45 | print("\n========== group list ==========", *group_list, sep="\n") 46 | example_group = group_list[0] 47 | # group = await cai.get_group(group_id) 48 | print("========== example group ==========") 49 | print("group id: ", example_group.group_id) 50 | print("group name: ", example_group.group_name) 51 | print("group owner: ", example_group.group_owner_uin) 52 | example_group_member_list = await example_group.get_members() 53 | print( 54 | "========== example group members ==========", 55 | *example_group_member_list, 56 | sep="\n", 57 | ) 58 | 59 | 60 | if __name__ == "__main__": 61 | close = asyncio.Event() 62 | 63 | async def wait_cleanup(): 64 | await close.wait() 65 | await cai.close_all() 66 | 67 | loop = asyncio.get_event_loop() 68 | loop.add_signal_handler(signal.SIGINT, close.set) 69 | loop.add_signal_handler(signal.SIGTERM, close.set) 70 | loop.create_task(run()) 71 | loop.run_until_complete(wait_cleanup()) 72 | -------------------------------------------------------------------------------- /examples/login.py: -------------------------------------------------------------------------------- 1 | """Example Code for Login. 2 | 3 | :Copyright: Copyright (C) 2021-2021 cscs181 4 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 5 | 6 | .. _LICENSE: 7 | https://github.com/cscs181/CAI/blob/master/LICENSE 8 | """ 9 | import os 10 | import signal 11 | import asyncio 12 | import traceback 13 | from io import BytesIO 14 | from hashlib import md5 15 | 16 | from PIL import Image 17 | 18 | import cai 19 | from cai.exceptions import ( 20 | LoginException, 21 | ApiResponseError, 22 | LoginDeviceLocked, 23 | LoginSliderNeeded, 24 | LoginAccountFrozen, 25 | LoginCaptchaNeeded, 26 | ) 27 | 28 | 29 | async def run(): 30 | account = os.getenv("ACCOUNT", "") 31 | password = os.getenv("PASSWORD") 32 | try: 33 | account = int(account) 34 | assert password 35 | except Exception: 36 | print( 37 | f"Error: account '{account}', password '{password}'" # type: ignore 38 | ) 39 | return 40 | 41 | try: 42 | client = await cai.login(account, md5(password.encode()).digest()) 43 | print(f"Login Success! Client status: {client.status!r}") 44 | except Exception as e: 45 | await handle_failure(e) 46 | 47 | 48 | async def handle_failure(exception: Exception): 49 | if isinstance(exception, LoginSliderNeeded): 50 | print("Verify url:", exception.verify_url) 51 | ticket = input("Please enter the ticket: ").strip() 52 | try: 53 | await cai.submit_slider_ticket(ticket) 54 | print("Login Success!") 55 | await asyncio.sleep(3) 56 | except Exception as e: 57 | await handle_failure(e) 58 | elif isinstance(exception, LoginCaptchaNeeded): 59 | print("Captcha:") 60 | image = Image.open(BytesIO(exception.captcha_image)) 61 | image.show() 62 | captcha = input("Please enter the captcha: ").strip() 63 | try: 64 | await cai.submit_captcha(captcha, exception.captcha_sign) 65 | print("Login Success!") 66 | await asyncio.sleep(3) 67 | except Exception as e: 68 | await handle_failure(e) 69 | elif isinstance(exception, LoginAccountFrozen): 70 | print("Account is frozen!") 71 | elif isinstance(exception, LoginDeviceLocked): 72 | print("Device lock detected!") 73 | way = ( 74 | "sms" 75 | if exception.sms_phone 76 | else "url" 77 | if exception.verify_url 78 | else "" 79 | ) 80 | if exception.sms_phone and exception.verify_url: 81 | while True: 82 | choice = input( 83 | f"1. Send sms message to {exception.sms_phone}.\n" 84 | f"2. Verify device by scanning.\nChoose: " 85 | ) 86 | if "1" in choice: 87 | way = "sms" 88 | break 89 | elif "2" in choice: 90 | way = "url" 91 | break 92 | print(f"'{choice}' is not valid!") 93 | if not way: 94 | print("No way to verify device...") 95 | elif way == "sms": 96 | await cai.request_sms() 97 | print(f"SMS was sent to {exception.sms_phone}!") 98 | sms_code = input("Please enter the sms_code: ").strip() 99 | try: 100 | await cai.submit_sms(sms_code) 101 | except Exception as e: 102 | await handle_failure(e) 103 | elif way == "url": 104 | await cai.close() 105 | print(f"Go to {exception.verify_url} to verify device!") 106 | input("Press ENTER after verification to continue login...") 107 | try: 108 | await cai.login(exception.uin) 109 | except Exception as e: 110 | await handle_failure(e) 111 | elif isinstance(exception, LoginException): 112 | print("Login Error:", repr(exception)) 113 | elif isinstance(exception, ApiResponseError): 114 | print("Response Error:", repr(exception)) 115 | else: 116 | print("Unknown Error:", repr(exception)) 117 | traceback.print_exc() 118 | 119 | 120 | if __name__ == "__main__": 121 | close = asyncio.Event() 122 | 123 | async def wait_cleanup(): 124 | await close.wait() 125 | await cai.close_all() 126 | 127 | loop = asyncio.get_event_loop() 128 | loop.add_signal_handler(signal.SIGINT, close.set) 129 | loop.add_signal_handler(signal.SIGTERM, close.set) 130 | loop.create_task(run()) 131 | loop.run_until_complete(wait_cleanup()) 132 | -------------------------------------------------------------------------------- /examples/message.py: -------------------------------------------------------------------------------- 1 | """Example Code for Message. 2 | 3 | :Copyright: Copyright (C) 2021-2021 cscs181 4 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 5 | 6 | .. _LICENSE: 7 | https://github.com/cscs181/CAI/blob/master/LICENSE 8 | """ 9 | import os 10 | import signal 11 | import asyncio 12 | from hashlib import md5 13 | 14 | import cai 15 | from cai.client import Event, Client, GroupMessage, PrivateMessage 16 | 17 | 18 | async def run(): 19 | account = os.getenv("ACCOUNT", "") 20 | password = os.getenv("PASSWORD") 21 | try: 22 | account = int(account) 23 | assert password 24 | except Exception: 25 | print( 26 | f"Error: account '{account}', password '{password}'" # type: ignore 27 | ) 28 | return 29 | 30 | client = await cai.login(account, md5(password.encode()).digest()) 31 | 32 | cai.add_event_listener(listen_message) 33 | # cai.add_event_listener(listen_message, uin=account) 34 | # client.add_event_listener(listen_message) 35 | 36 | 37 | async def listen_message(client: Client, event: Event): 38 | if isinstance(event, PrivateMessage): 39 | print("Private Message received from", event.from_uin) 40 | print("Private Message elements:", event.message) 41 | elif isinstance(event, GroupMessage): 42 | print( 43 | f"Group Message received from {event.group_name}({event.group_id})" 44 | ) 45 | print("Group Message elements:", event.message) 46 | 47 | 48 | if __name__ == "__main__": 49 | close = asyncio.Event() 50 | 51 | async def wait_cleanup(): 52 | await close.wait() 53 | await cai.close_all() 54 | 55 | loop = asyncio.get_event_loop() 56 | loop.add_signal_handler(signal.SIGINT, close.set) 57 | loop.add_signal_handler(signal.SIGTERM, close.set) 58 | loop.create_task(run()) 59 | loop.run_until_complete(wait_cleanup()) 60 | -------------------------------------------------------------------------------- /examples/set_status.py: -------------------------------------------------------------------------------- 1 | """Example Code for Set Client Status. 2 | 3 | :Copyright: Copyright (C) 2021-2021 cscs181 4 | :License: AGPL-3.0 or later. See `LICENSE`_ for detail. 5 | 6 | .. _LICENSE: 7 | https://github.com/cscs181/CAI/blob/master/LICENSE 8 | """ 9 | import os 10 | import signal 11 | import asyncio 12 | from hashlib import md5 13 | 14 | import cai 15 | from cai.client import OnlineStatus 16 | 17 | 18 | async def run(): 19 | account = os.getenv("ACCOUNT", "") 20 | password = os.getenv("PASSWORD") 21 | try: 22 | account = int(account) 23 | assert password 24 | except Exception: 25 | print( 26 | f"Error: account '{account}', password '{password}'" # type: ignore 27 | ) 28 | return 29 | 30 | client = await cai.login(account, md5(password.encode()).digest()) 31 | 32 | await asyncio.sleep(10) 33 | await cai.set_status(OnlineStatus.Qme) 34 | print("Current client status: ", client.status) 35 | 36 | 37 | if __name__ == "__main__": 38 | close = asyncio.Event() 39 | 40 | async def wait_cleanup(): 41 | await close.wait() 42 | await cai.close_all() 43 | 44 | loop = asyncio.get_event_loop() 45 | loop.add_signal_handler(signal.SIGINT, close.set) 46 | loop.add_signal_handler(signal.SIGTERM, close.set) 47 | loop.create_task(run()) 48 | loop.run_until_complete(wait_cleanup()) 49 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "CAI" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["yanyongyu "] 6 | license = "AGPL-3.0-or-later" 7 | readme = "README.md" 8 | homepage = "https://github.com/cscs181/CAI" 9 | repository = "https://github.com/cscs181/CAI" 10 | documentation = "https://github.com/cscs181/CAI#readme" 11 | keywords = ["qq", "mirai", "cqhttp"] 12 | classifiers = ["Framework :: Robot Framework", "Programming Language :: Python :: 3"] 13 | include = ["cai/py.typed"] 14 | 15 | [tool.poetry.dependencies] 16 | python = "^3.7" 17 | rtea = "^0.3.0" 18 | jcestruct = "^0.1.0" 19 | protobuf = "^3.14.0" 20 | cachetools = "^4.2.2" 21 | cryptography = "^3.4.1" 22 | typing-extensions = ">=3.10.0,<5.0.0" 23 | 24 | [tool.poetry.dev-dependencies] 25 | isort = "^5.9.3" 26 | black = "^21.7b0" 27 | pillow = "^8.1.0" 28 | sphinx = "^4.1.0" 29 | mypy-protobuf = "^2.4" 30 | sphinx-rtd-theme = "^0.5.1" 31 | sphinx-copybutton = "^0.4.0" 32 | sphinxcontrib-napoleon = "^0.7" 33 | 34 | # [[tool.poetry.source]] 35 | # name = "aliyun" 36 | # url = "https://mirrors.aliyun.com/pypi/simple/" 37 | # default = true 38 | 39 | [tool.black] 40 | line-length = 80 41 | extend-exclude = ''' 42 | ^/cai/pb/ 43 | ''' 44 | 45 | [tool.isort] 46 | profile = "black" 47 | line_length = 80 48 | length_sort = true 49 | extend_skip = "cai/pb" 50 | skip_gitignore = true 51 | force_sort_within_sections = true 52 | extra_standard_library = "typing_extensions" 53 | 54 | [tool.pyright] 55 | ignore = [".vscode/*.py", "**/site-packages/**/*.py", "cai/pb/**/*_pb2.py"] 56 | 57 | [build-system] 58 | requires = ["poetry-core>=1.0.0"] 59 | build-backend = "poetry.core.masonry.api" 60 | -------------------------------------------------------------------------------- /tests/client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cscs181/CAI/89eb91ce0d9eb609f30ecaa4007a0e84f094d86d/tests/client/__init__.py -------------------------------------------------------------------------------- /tests/client/test_login.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import logging 3 | import unittest 4 | import unittest.mock 5 | from hashlib import md5 6 | 7 | from cai.log import logger 8 | import cai.settings.device as device 9 | import cai.settings.protocol as protocol 10 | 11 | # clear cache 12 | for module in list(sys.modules.keys()): 13 | if module.startswith("cai") and not module in [ 14 | "cai.settings.device", 15 | "cai.settings.protocol", 16 | "cai.log", 17 | ]: 18 | del sys.modules[module] 19 | 20 | mock_device = unittest.mock.patch.object( 21 | device, 22 | "get_device", 23 | return_value=device.DeviceInfo( 24 | product="missi", 25 | device="venus", 26 | board="venus", 27 | brand="Xiaomi", 28 | model="MI 11", 29 | vendor_name="MIUI", 30 | vendor_os_name="MIUI", 31 | bootloader="unknown", 32 | boot_id="dc109fd7-f17f-4f43-a266-b68469c19a1f", 33 | proc_version="Linux version 4.19.71-ab0b8e88 (android-build@github.com)", 34 | baseband="", 35 | mac_address="89:C2:A9:C5:FA:E9", 36 | ip_address="10.0.46.76", 37 | wifi_ssid="", 38 | imei="862542082770767", 39 | android_id="BRAND.141613.779", 40 | version=device.Version( 41 | incremental="V12.0.19.0.RKBCNXM", 42 | release="11", 43 | codename="REL", 44 | sdk=30, 45 | ), 46 | sim="T-Mobile", 47 | os_type="android", 48 | apn="wifi", 49 | _imsi_md5="0f63d5c351fd1d75a29d88cae86d315d", 50 | _tgtgt_md5="c4e512e6924e4872e552e861a914d49a", 51 | _guid_md5=None, 52 | ), 53 | ) 54 | mock_protocol = unittest.mock.patch.object( 55 | protocol, 56 | "get_protocol", 57 | return_value=protocol.ApkInfo( 58 | apk_id="com.tencent.minihd.qq", 59 | app_id=16, 60 | sub_app_id=537065739, 61 | version="5.8.9", 62 | build_time=1595836208, 63 | apk_sign=bytes( 64 | [ 65 | 170, 66 | 57, 67 | 120, 68 | 244, 69 | 31, 70 | 217, 71 | 111, 72 | 249, 73 | 145, 74 | 74, 75 | 102, 76 | 158, 77 | 24, 78 | 100, 79 | 116, 80 | 199, 81 | ] 82 | ), 83 | sdk_version="6.0.0.2433", 84 | sso_version=12, 85 | bitmap=150470524, 86 | main_sigmap=1970400, 87 | sub_sigmap=66560, 88 | ), 89 | ) 90 | 91 | with mock_device: 92 | with mock_protocol: 93 | from cai.settings.device import get_device 94 | from cai.client.wtlogin import encode_login_request9 95 | 96 | 97 | class TestEncodeLoginRequest(unittest.IsolatedAsyncioTestCase): 98 | def log(self, level: int, message: str, *args, exc_info=False, **kwargs): 99 | message = "| TestEncodeLoginRequest | " + message 100 | return logger.log(level, message, *args, exc_info=exc_info, **kwargs) 101 | 102 | def setUp(self): 103 | self.log(logging.INFO, "Start Testing Encode Login Request...") 104 | 105 | def tearDown(self): 106 | self.log(logging.INFO, "End Testing Encode Login Request!") 107 | 108 | def test_encode_login_request(self): 109 | self.log(logging.INFO, "test encode login request") 110 | # ensure encode has no error 111 | packet = encode_login_request9( 112 | 10, 113 | bytes(16), 114 | bytes([0x02, 0xB0, 0x5B, 0x8B]), 115 | f"|{get_device().imei}|A8.2.7.27f6ea96".encode(), 116 | 123456, 117 | md5("123456".encode()).digest(), 118 | ) 119 | # print(packet.hex()) 120 | 121 | 122 | if __name__ == "__main__": 123 | unittest.main() 124 | -------------------------------------------------------------------------------- /tests/client/test_sso_server.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import unittest 3 | import ipaddress 4 | 5 | from cai.log import logger 6 | from cai.client.sso_server import ( 7 | SsoServer, 8 | SsoServerResponse, 9 | get_sso_list, 10 | get_sso_server, 11 | ) 12 | 13 | 14 | class TestSsoServer(unittest.IsolatedAsyncioTestCase): 15 | def log(self, level: int, message: str, *args, exc_info=False, **kwargs): 16 | message = "| TestSsoServer | " + message 17 | return logger.log(level, message, *args, exc_info=exc_info, **kwargs) 18 | 19 | def setUp(self): 20 | self.log(logging.INFO, "Start Testing SsoServer...") 21 | 22 | def tearDown(self): 23 | self.log(logging.INFO, "End Testing SsoServer!") 24 | 25 | async def test_get_list(self): 26 | self.log(logging.INFO, "test get sso server response") 27 | sso_list = await get_sso_list() 28 | self.assertIsInstance(sso_list, SsoServerResponse) 29 | self.assertGreater( 30 | len(sso_list.socket_v4_mobile) + len(sso_list.socket_v4_wifi), 0 31 | ) 32 | 33 | async def test_get_sso_server(self): 34 | self.log(logging.INFO, "test get sso server") 35 | sso_server = await get_sso_server(cache=False) 36 | self.assertIsInstance(sso_server, SsoServer) 37 | self.assertIsInstance( 38 | ipaddress.ip_address(sso_server.host), ipaddress.IPv4Address 39 | ) 40 | 41 | self.log(logging.INFO, "test get sso server again with cache") 42 | sso_server_cached = await get_sso_server(cache=True) 43 | self.assertIs(sso_server_cached, sso_server) 44 | 45 | 46 | if __name__ == "__main__": 47 | unittest.main() 48 | -------------------------------------------------------------------------------- /tests/connection/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cscs181/CAI/89eb91ce0d9eb609f30ecaa4007a0e84f094d86d/tests/connection/__init__.py -------------------------------------------------------------------------------- /tests/connection/test_connection.py: -------------------------------------------------------------------------------- 1 | import time 2 | import asyncio 3 | import logging 4 | import unittest 5 | from types import TracebackType 6 | from typing import Any, Type, Tuple, Union, Optional 7 | 8 | from cai.log import logger 9 | from cai.connection import Connection, connect 10 | from cai.connection.utils import tcp_latency_test 11 | 12 | _SysExcInfoType = Union[ 13 | Tuple[Type[BaseException], BaseException, Optional[TracebackType]], 14 | Tuple[None, None, None], 15 | ] 16 | 17 | 18 | class TestConnection(unittest.IsolatedAsyncioTestCase): 19 | def log( 20 | self, 21 | level: int, 22 | message: str, 23 | *args: Any, 24 | exc_info: Union[None, bool, Exception, _SysExcInfoType] = False, 25 | **kwargs: Any, 26 | ): 27 | message = "| TestConnection | " + message 28 | return logger.log(level, message, *args, exc_info=exc_info, **kwargs) 29 | 30 | def setUp(self): 31 | self.log(logging.INFO, "Start Testing Connection...") 32 | 33 | def tearDown(self): 34 | self.log(logging.INFO, "End Testing Connection!") 35 | 36 | async def test_tcp_echo(self): 37 | MSG: bytes = b"Hello" 38 | 39 | self.log(logging.INFO, "test tcp echo") 40 | 41 | start = time.time() 42 | conn = await connect("tcpbin.com", 4242, timeout=10.0) 43 | end = time.time() 44 | self.assertIsInstance(conn, Connection) 45 | self.log(logging.INFO, f"connected in {end - start} seconds") 46 | 47 | conn.write(MSG) 48 | conn.write_eof() 49 | resp = await conn.read_all() 50 | self.log(logging.INFO, f"received in {time.time() - end} seconds") 51 | await conn.close() 52 | self.assertEqual(resp, MSG) 53 | 54 | await asyncio.sleep(3) 55 | 56 | self.log(logging.INFO, "test tcp echo with context manager") 57 | 58 | start = time.time() 59 | async with connect("tcpbin.com", 4242, timeout=10.0) as conn: 60 | end = time.time() 61 | self.assertIsInstance(conn, Connection) 62 | self.log(logging.INFO, f"connected in {end - start} seconds") 63 | 64 | conn.write(MSG) 65 | conn.write_eof() 66 | resp = await conn.read_all() 67 | self.log(logging.INFO, f"received in {time.time() - end} seconds") 68 | self.assertEqual(resp, MSG) 69 | 70 | async def test_tcp_latency(self): 71 | self.log(logging.INFO, "test TCP latency") 72 | 73 | delay = await tcp_latency_test("tcpbin.com", 4242, timeout=10.0) 74 | self.assertIsInstance(delay, float) 75 | self.assertGreater(delay, 0.0) 76 | 77 | 78 | if __name__ == "__main__": 79 | unittest.main() 80 | --------------------------------------------------------------------------------