├── nonebot └── adapters │ └── red │ ├── api │ ├── __init__.py │ ├── handle.py │ └── model.py │ ├── utils.py │ ├── __init__.py │ ├── compat.py │ ├── permission.py │ ├── config.py │ ├── adapter.py │ ├── event.py │ ├── bot.py │ └── message.py ├── .github ├── workflows │ ├── ruff.yml │ └── release.yml ├── ISSUE_TEMPLATE │ ├── feature.md │ ├── bug.md │ ├── feature_request.yml │ └── bug_report.yml └── actions │ └── setup-python │ └── action.yml ├── .pre-commit-config.yaml ├── LICENSE ├── pyproject.toml ├── api.md ├── README.md ├── .gitignore ├── migrate.md └── pdm.lock /nonebot/adapters/red/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nonebot/adapters/red/utils.py: -------------------------------------------------------------------------------- 1 | from nonebot.utils import logger_wrapper 2 | 3 | log = logger_wrapper("RedProtocol") 4 | -------------------------------------------------------------------------------- /.github/workflows/ruff.yml: -------------------------------------------------------------------------------- 1 | name: Ruff Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | ruff: 11 | name: Ruff Lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Run Ruff Lint 17 | uses: chartboost/ruff-action@v1 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 功能建议 (Classic) 3 | about: 为适配器加份菜 4 | title: "[Feature] " 5 | labels: enhancement, triage 6 | assignees: "" 7 | --- 8 | 9 | ## 请确认: 10 | 11 | * [ ] 新特性的目的明确 12 | * [ ] 我已经使用过该项目并且了解其功能 13 | 14 | 15 | ## Feature 16 | ### 概要 17 | 18 | 19 | 20 | ### 是否已有相关实现 21 | 22 | 暂无 23 | 24 | 25 | ### 其他内容 26 | 27 | 暂无 28 | -------------------------------------------------------------------------------- /nonebot/adapters/red/__init__.py: -------------------------------------------------------------------------------- 1 | from .permission import * 2 | from .bot import Bot as Bot 3 | from .adapter import Adapter as Adapter 4 | from .message import Message as Message 5 | from .event import MessageEvent as MessageEvent 6 | from .message import MessageSegment as MessageSegment 7 | from .event import GroupMessageEvent as GroupMessageEvent 8 | from .event import PrivateMessageEvent as PrivateMessageEvent 9 | 10 | __version__ = "0.9.0" 11 | -------------------------------------------------------------------------------- /.github/actions/setup-python/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup Python 2 | description: Setup Python 3 | 4 | inputs: 5 | python-version: 6 | description: Python version 7 | required: false 8 | default: "3.10" 9 | 10 | runs: 11 | using: "composite" 12 | steps: 13 | - uses: pdm-project/setup-pdm@v3 14 | name: Setup PDM 15 | with: 16 | python-version: ${{ inputs.python-version }} 17 | architecture: "x64" 18 | cache: true 19 | 20 | - run: pdm sync -G:all 21 | shell: bash 22 | -------------------------------------------------------------------------------- /nonebot/adapters/red/compat.py: -------------------------------------------------------------------------------- 1 | from typing import Literal, overload 2 | 3 | from nonebot.compat import PYDANTIC_V2 4 | 5 | __all__ = ("model_validator",) 6 | 7 | 8 | if PYDANTIC_V2: 9 | from pydantic import model_validator as model_validator 10 | else: 11 | from pydantic import root_validator 12 | 13 | @overload 14 | def model_validator(*, mode: Literal["before"]): 15 | ... 16 | 17 | @overload 18 | def model_validator(*, mode: Literal["after"]): 19 | ... 20 | 21 | def model_validator(*, mode: Literal["before", "after"]): 22 | return root_validator(pre=mode == "before", allow_reuse=True) 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug 反馈 (Classic) 3 | about: 有关 bug 的报告 4 | title: "[Bug]" 5 | labels: bug, triage 6 | assignees: "" 7 | --- 8 | 9 | ## 请确认: 10 | 11 | * [ ] 问题的标题明确 12 | * [ ] 我翻阅过其他的 issue 并且找不到类似的问题 13 | * [ ] 我已经阅读了[相关文档](https://chronocat.vercel.app) 并仍然认为这是一个Bug 14 | 15 | # Bug 16 | 17 | ## 问题 18 | 19 | 20 | ## 如何复现 21 | 22 | 23 | ## 预期行为 24 | 25 | 26 | ## 使用环境: 27 | - 操作系统 (Windows/Linux/Mac): 28 | - Python 版本: 29 | - Nonebot2 版本: 30 | - Chronocat 版本: 31 | - Red 适配器版本: 32 | 33 | ## 配置文件 34 | 35 | ```dotenv 36 | ``` 37 | 38 | ## 日志/截图 39 | 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 功能建议 2 | title: "[Feature]: " 3 | description: 提出关于项目新功能的想法 4 | labels: ["enhancement"] 5 | body: 6 | - type: checkboxes 7 | id: ensure 8 | attributes: 9 | label: 确认项 10 | description: 请确认以下选项 11 | options: 12 | - label: 新特性的目的明确 13 | required: true 14 | - label: 我已经使用过该项目并且了解其功能 15 | required: true 16 | - type: textarea 17 | id: problem 18 | attributes: 19 | label: 希望能解决的问题 20 | description: 在使用中遇到什么问题而需要新的功能? 21 | validations: 22 | required: true 23 | 24 | - type: textarea 25 | id: feature 26 | attributes: 27 | label: 描述所需要的功能 28 | description: 请说明需要的功能或解决方法 29 | validations: 30 | required: true 31 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_install_hook_types: [pre-commit, prepare-commit-msg] 2 | ci: 3 | autofix_commit_msg: ":rotating_light: auto fix by pre-commit hooks" 4 | autofix_prs: true 5 | autoupdate_branch: master 6 | autoupdate_schedule: monthly 7 | autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks" 8 | repos: 9 | - repo: https://github.com/astral-sh/ruff-pre-commit 10 | rev: v0.0.276 11 | hooks: 12 | - id: ruff 13 | args: [--fix, --exit-non-zero-on-fix] 14 | stages: [commit] 15 | 16 | - repo: https://github.com/pycqa/isort 17 | rev: 5.12.0 18 | hooks: 19 | - id: isort 20 | stages: [commit] 21 | 22 | - repo: https://github.com/psf/black 23 | rev: 23.3.0 24 | hooks: 25 | - id: black 26 | stages: [commit] 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | id-token: write 13 | contents: write 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - name: Setup Python environment 18 | uses: ./.github/actions/setup-python 19 | 20 | - name: Get Version 21 | id: version 22 | run: | 23 | echo "VERSION=$(pdm show --version)" >> $GITHUB_OUTPUT 24 | echo "TAG_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT 25 | echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT 26 | 27 | - name: Check Version 28 | if: steps.version.outputs.VERSION != steps.version.outputs.TAG_VERSION 29 | run: exit 1 30 | 31 | - name: Publish Package 32 | run: | 33 | pdm publish 34 | gh release upload --clobber ${{ steps.version.outputs.TAG_NAME }} dist/*.tar.gz dist/*.whl 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 nonebot 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /nonebot/adapters/red/permission.py: -------------------------------------------------------------------------------- 1 | from nonebot.permission import Permission 2 | 3 | from .event import MessageEvent 4 | 5 | 6 | async def _private(event: MessageEvent) -> bool: 7 | return event.is_private 8 | 9 | 10 | async def _private_friend(event: MessageEvent) -> bool: 11 | return event.is_private and event.roleType == 0 12 | 13 | 14 | async def _private_group(event: MessageEvent) -> bool: 15 | return event.is_private and event.roleType == 1 16 | 17 | 18 | PRIVATE = Permission(_private) 19 | """ 匹配任意私聊消息类型事件""" 20 | PRIVATE_FRIEND: Permission = Permission(_private_friend) 21 | """匹配任意好友私聊消息类型事件""" 22 | PRIVATE_GROUP: Permission = Permission(_private_group) 23 | """匹配任意群临时私聊消息类型事件""" 24 | 25 | 26 | async def _group(event: MessageEvent) -> bool: 27 | return event.is_group 28 | 29 | 30 | async def _group_member(event: MessageEvent) -> bool: 31 | return event.is_group and event.roleType == 2 32 | 33 | 34 | async def _group_admin(event: MessageEvent) -> bool: 35 | return event.is_group and event.roleType == 3 36 | 37 | 38 | async def _group_owner(event: MessageEvent) -> bool: 39 | return event.is_group and event.roleType == 4 40 | 41 | 42 | GROUP: Permission = Permission(_group) 43 | """匹配任意群聊消息类型事件""" 44 | GROUP_MEMBER: Permission = Permission(_group_member) 45 | """匹配任意群员群聊消息类型事件""" 46 | GROUP_ADMIN: Permission = Permission(_group_admin) 47 | """匹配任意群管理员群聊消息类型事件""" 48 | GROUP_OWNER: Permission = Permission(_group_owner) 49 | """匹配任意群主群聊消息类型事件""" 50 | 51 | __all__ = [ 52 | "PRIVATE", 53 | "PRIVATE_FRIEND", 54 | "PRIVATE_GROUP", 55 | "GROUP", 56 | "GROUP_MEMBER", 57 | "GROUP_ADMIN", 58 | "GROUP_OWNER", 59 | ] 60 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "nonebot-adapter-red" 3 | description = "Red Protocol Adapter for Nonebot2" 4 | authors = [ 5 | {name = "zhaomaoniu"}, 6 | {name = "RF-Tar-Railt", email = "rf_tar_railt@qq.com"}, 7 | ] 8 | dependencies = [ 9 | "nonebot2>=2.2.0", 10 | "packaging>=23.1", 11 | ] 12 | requires-python = ">=3.8" 13 | readme = "README.md" 14 | license = {text = "MIT"} 15 | dynamic = ["version"] 16 | 17 | [project.urls] 18 | homepage = "https://github.com/nonebot/adapter-red" 19 | repository = "https://github.com/nonebot/adapter-red" 20 | 21 | [project.optional-dependencies] 22 | auto_detect = ["PyYAML"] 23 | 24 | [build-system] 25 | requires = ["pdm-backend"] 26 | build-backend = "pdm.backend" 27 | 28 | [tool.pdm.dev-dependencies] 29 | dev = [ 30 | "isort>=5.12.0", 31 | "black>=23.7.0", 32 | "ruff>=0.0.280", 33 | "pre-commit>=3.3.3", 34 | "nonebot2[httpx,websockets]>=2.2.0", 35 | "PyYAML>=6.0.1", 36 | ] 37 | [tool.pdm.build] 38 | includes = ["nonebot"] 39 | 40 | [tool.pdm.version] 41 | source = "file" 42 | path = "nonebot/adapters/red/__init__.py" 43 | 44 | [tool.black] 45 | line-length = 88 46 | target-version = ["py38", "py39", "py310", "py311"] 47 | include = '\.pyi?$' 48 | extend-exclude = ''' 49 | ''' 50 | 51 | [tool.isort] 52 | profile = "black" 53 | line_length = 88 54 | length_sort = true 55 | skip_gitignore = true 56 | force_sort_within_sections = true 57 | extra_standard_library = ["typing_extensions"] 58 | 59 | [tool.ruff] 60 | select = ["E", "W", "F", "UP", "C", "T", "PYI", "PT", "Q"] 61 | ignore = ["C901", "T201", "E731"] 62 | 63 | line-length = 88 64 | target-version = "py38" 65 | 66 | [tool.ruff.per-file-ignores] 67 | "nonebot/adapters/red/__init__.py" = ["F403"] 68 | -------------------------------------------------------------------------------- /nonebot/adapters/red/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | from typing import Dict, List 4 | 5 | from yarl import URL 6 | from pydantic import Field, BaseModel 7 | from nonebot.compat import type_validate_python 8 | 9 | 10 | class BotInfo(BaseModel): 11 | host: str = "localhost" 12 | port: int 13 | token: str 14 | 15 | @property 16 | def api_base(self): 17 | return URL(f"http://{self.host}:{self.port}") / "api" 18 | 19 | 20 | class Server(BaseModel): 21 | type: str 22 | token: str 23 | port: int = 16530 24 | enable: bool = True 25 | host: str = Field(default="localhost", alias="listen") 26 | 27 | 28 | class Servers(BaseModel): 29 | servers: List[Server] = Field(default_factory=list) 30 | enable: bool = True 31 | 32 | 33 | class ChronocatConfig(Servers): 34 | overrides: Dict[str, Servers] = Field(default_factory=dict) 35 | 36 | 37 | class Config(BaseModel): 38 | red_bots: List[BotInfo] = Field(default_factory=list) 39 | """bot 配置""" 40 | 41 | red_auto_detect: bool = False 42 | """是否自动检测 chronocat 配置,默认为 False""" 43 | 44 | 45 | # get `home` path 46 | home = Path(os.path.expanduser("~")) 47 | # get `config` path 48 | config = home / ".chronocat" / "config" / "chronocat.yml" 49 | 50 | 51 | def get_config() -> List[BotInfo]: 52 | import yaml 53 | 54 | if not config.exists(): 55 | return [] 56 | with open(config, encoding="utf-8") as f: 57 | chrono_config = type_validate_python(ChronocatConfig, yaml.safe_load(f)) 58 | base_config = next( 59 | (s for s in chrono_config.servers if s.type == "red" and s.enable), None 60 | ) 61 | if ( 62 | not chrono_config.overrides 63 | or len(chrono_config.overrides) == 1 64 | and "10000" in chrono_config.overrides 65 | ): 66 | return [ 67 | BotInfo( 68 | port=base_config.port, token=base_config.token, host=base_config.host 69 | ) 70 | ] 71 | return [ 72 | BotInfo(port=server.port, token=server.token, host=server.host) 73 | for servers in chrono_config.overrides.values() 74 | for server in servers.servers 75 | if servers.enable and server.type == "red" and server.enable 76 | ] 77 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug 反馈 2 | title: "[Bug]: " 3 | description: 提交 Bug 反馈以帮助我们改进代码 4 | labels: ["bug"] 5 | body: 6 | - type: checkboxes 7 | id: ensure 8 | attributes: 9 | label: 确认项 10 | description: 请确认以下选项 11 | options: 12 | - label: 问题的标题明确 13 | required: true 14 | - label: 我翻阅过其他的 issue 并且找不到类似的问题 15 | required: true 16 | - label: 我已经阅读了[相关文档](https://chronocat.vercel.app) 并仍然认为这是一个 Bug 17 | required: true 18 | - label: 我已经尝试过在最新的代码中修复这个问题 19 | required: false 20 | - type: dropdown 21 | id: env-os 22 | attributes: 23 | label: 操作系统 24 | description: 选择运行 NoneBot 的系统 25 | options: 26 | - Windows 27 | - MacOS 28 | - Linux 29 | - Other 30 | validations: 31 | required: true 32 | 33 | - type: input 34 | id: env-python-ver 35 | attributes: 36 | label: Python 版本 37 | description: 填写运行 NoneBot 的 Python 版本 38 | placeholder: e.g. 3.11.0 39 | validations: 40 | required: true 41 | 42 | - type: input 43 | id: env-nb-ver 44 | attributes: 45 | label: NoneBot 版本 46 | description: 填写 NoneBot 版本 47 | placeholder: e.g. 2.0.0 48 | validations: 49 | required: true 50 | 51 | - type: input 52 | id: env-adapter 53 | attributes: 54 | label: 适配器 55 | description: 填写使用的Red适配器版本 56 | placeholder: e.g. 0.5.1 57 | validations: 58 | required: true 59 | 60 | - type: input 61 | id: env-protocol 62 | attributes: 63 | label: 协议端 64 | description: 填写 Chronocat 版本 65 | placeholder: e.g. 0.0.52 66 | validations: 67 | required: true 68 | 69 | - type: textarea 70 | id: describe 71 | attributes: 72 | label: 描述问题 73 | description: 清晰简洁地说明问题是什么 74 | validations: 75 | required: true 76 | 77 | - type: textarea 78 | id: reproduction 79 | attributes: 80 | label: 复现步骤 81 | description: 提供能复现此问题的详细操作步骤 82 | placeholder: | 83 | 1. 首先…… 84 | 2. 然后…… 85 | 3. 发生…… 86 | validations: 87 | required: true 88 | 89 | - type: textarea 90 | id: expected 91 | attributes: 92 | label: 期望的结果 93 | description: 清晰简洁地描述你期望发生的事情 94 | 95 | - type: textarea 96 | id: logs 97 | attributes: 98 | label: 截图或日志 99 | description: 提供有助于诊断问题的任何日志和截图 100 | 101 | - type: textarea 102 | id: config 103 | attributes: 104 | label: Nonebot 配置项 105 | description: Nonebot 配置项 (如果你的配置文件中包含敏感信息,请自行删除) 106 | render: dotenv 107 | placeholder: | 108 | # e.g. 109 | # KEY=VALUE 110 | # KEY2=VALUE2 111 | -------------------------------------------------------------------------------- /api.md: -------------------------------------------------------------------------------- 1 | # API 列表 2 | 3 | ## send_message 4 | 5 | 依据聊天类型与目标 id 发送消息 6 | 7 | 参数: 8 | 9 | - chat_type: 聊天类型,分为好友与群组 10 | - target: 目标 id 11 | - message: 发送的消息 12 | 13 | ## send_friend_message 14 | 15 | 发送好友消息 16 | 17 | 参数: 18 | 19 | - target: 好友 id 20 | - message: 发送的消息 21 | 22 | ## send_group_message 23 | 24 | 发送群组消息 25 | 26 | 参数: 27 | 28 | - target: 群组 id 29 | - message: 发送的消息 30 | 31 | ## send 32 | 33 | 依据收到的事件发送消息 34 | 35 | 参数: 36 | 37 | - event: 收到的事件 38 | - message: 发送的消息 39 | 40 | ## get_self_profile 41 | 42 | 获取登录账号自己的资料 43 | 44 | ## get_friends 45 | 46 | 获取登录账号所有好友的资料 47 | 48 | ## get_groups 49 | 50 | 获取登录账号所有群组的资料 51 | 52 | ## mute_member 53 | 54 | 禁言群成员 55 | 56 | 禁言时间会自动限制在 60s 至 30天内 57 | 58 | 参数: 59 | 60 | - group: 群号 61 | - *members: 禁言目标的 id 62 | - duration: 禁言时间 63 | 64 | ## unmute_member 65 | 66 | 解除群成员禁言 67 | 68 | 参数: 69 | 70 | - group: 群号 71 | - *members: 禁言目标的 id 72 | 73 | ## mute_everyone 74 | 75 | 开启全体禁言 76 | 77 | 参数: 78 | 79 | - group: 群号 80 | 81 | ## unmute_everyone 82 | 83 | 关闭全体禁言 84 | 85 | 参数: 86 | 87 | - group: 群号 88 | 89 | ## kick 90 | 91 | 移除群成员 92 | 93 | 参数: 94 | 95 | - group: 群号 96 | - *members: 要移除的群成员账号 97 | - refuse_forever: 是否不再接受群成员的入群申请 98 | - reason: 移除理由 99 | 100 | ## get_announcements 101 | 102 | 拉取群公告 103 | 104 | 参数: 105 | 106 | - group: 群号 107 | 108 | ## get_members 109 | 110 | 获取指定群组内的成员资料 111 | 112 | 参数: 113 | 114 | - group: 群号 115 | - size: 拉取多少个成员资料 116 | 117 | ## fetch 118 | 119 | 获取媒体消息段的二进制数据 120 | 121 | 参数: 122 | 123 | - ms: 消息段 124 | 125 | ## fetch_media 126 | 127 | 获取媒体消息的二进制数据 128 | 129 | **注意:此接口不推荐直接使用** 130 | 131 | 若需要获取媒体数据,你可以使用 `bot.fetch(MessageSegment)` 接口,或 `ms.download(Bot)` 接口 132 | 133 | 参数: 134 | 135 | - msg_id: 媒体消息的消息 id 136 | - chat_type: 媒体消息的聊天类型 137 | - target: 媒体消息的聊天对象 id 138 | - element_id: 媒体消息中媒体元素的 id 139 | 140 | ## upload 141 | 142 | 上传资源 143 | 144 | **注意:此接口不推荐直接使用** 145 | 146 | 参数: 147 | 148 | - file: 上传的资源数据 149 | 150 | ## recall_message 151 | 152 | 撤回消息 153 | 154 | 参数: 155 | 156 | - chat_type: 聊天类型,分为好友与群组 157 | - target: 目标 id 158 | - *ids: 要撤回的消息 id 159 | 160 | ## recall_group_message 161 | 162 | 撤回群组消息 163 | 164 | 参数: 165 | 166 | - target: 群组 id 167 | - *ids: 要撤回的消息 id 168 | 169 | ## recall_friend_message 170 | 171 | 撤回好友消息 172 | 173 | 参数: 174 | 175 | - target: 好友 id 176 | - *ids: 要撤回的消息 id 177 | 178 | ## get_history_messages 179 | 180 | 拉取历史消息 181 | 182 | 参数: 183 | 184 | - chat_type: 聊天类型,分为好友与群组 185 | - target: 目标 id 186 | - offset_msg_id: 从哪一条消息开始拉取,使用event.msgId 187 | - count: 一次拉取多少消息 188 | 189 | ## send_fake_forward 190 | 191 | 发送伪造合并转发消息 192 | 193 | 参数: 194 | 195 | - nodes: 合并转发节点 196 | - chat_type: 聊天类型,分为好友与群组 197 | - target: 目标 id 198 | - source_chat_type: 伪造的消息来源聊天类型,分为好友与群组 199 | - source_target: 伪造的消息来源聊天对象 id 200 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # NoneBot-Adapter-Red 4 | 5 | _✨ NoneBot2 Red Protocol适配器 / Red Protocol Adapter for NoneBot2 ✨_ 6 | 7 |
8 | 9 | ## 安装 10 | 11 | ### Chronocat 12 | 13 | 请按照 [Chronocat](https://chronocat.vercel.app) 的指引安装。 14 | 15 | **目前推荐版本为 `v0.0.51`** 16 | 17 | ## 迁移指南 18 | 19 | 如果你原先为 `go-cqhttp` 用户,可以参考 [迁移指南](./migrate.md) 来修改你的插件。 20 | 21 | **首次使用者同样可以参考该指南。** 22 | 23 | ## 配置 24 | 25 | 修改 NoneBot 配置文件 `.env` 或者 `.env.*`。 26 | 27 | ### Driver 28 | 29 | 参考 [driver](https://nonebot.dev/docs/appendices/config#driver) 配置项,添加 `ForwardDriver` 支持。 30 | 31 | 如: 32 | 33 | ```dotenv 34 | DRIVER=~httpx+~websockets 35 | DRIVER=~aiohttp 36 | ``` 37 | 38 | 关于 `ForwardDriver` ,参考 [Driver](https://nonebot.dev/docs/advanced/driver)。 39 | 40 | ### RED_AUTO_DETECT 41 | 42 | 是否自动检测 Chronocat 的配置文件 `~/.chronocat/config/chronocat.yml` 并读取内容,默认为 `False`。 43 | 44 | 配置文件详细内容请参考 [Chronocat/config](https://chronocat.vercel.app/config/)。 45 | 46 | 该配置项需要在 `Chronocat` 版本 `v0.0.46` 以上才可用。 47 | 48 | 使用该配置项时,你需要通过 `pip install nonebot-adapter-red[auto_detect]` 安装 `nonebot-adapter-red`。 49 | 50 | **如果你已经配置了 `RED_BOTS`,则该配置项不会生效。** 51 | 52 | ### RED_BOTS 53 | 54 | 配置机器人帐号,如: 55 | 56 | ```dotenv 57 | RED_BOTS=' 58 | [ 59 | { 60 | "port": "xxx", 61 | "token": "xxx", 62 | "host": "xxx" 63 | } 64 | ] 65 | ' 66 | ``` 67 | 68 | 你需要从 Chronocat 的配置文件 `~/.chronocat/config/chronocat.yml` 中获取 `port`、`token`、`host`。 69 | 70 | 在单账号下, 71 | - `port` 与配置文件下的 `servers[X].port` 一致 72 | - `token` 与配置文件下的 `servers[X].token` 一致 73 | - `host` 与配置文件下的 `servers[X].listen` 一致 74 | 75 | ```yaml 76 | # ~/.chronocat/config/chronocat.yml 77 | servers: 78 | - type: red 79 | # Chronocat 已经自动生成了随机 token。要妥善保存哦! 80 | # 客户端使用服务时需要提供这个 token! 81 | token: DEFINE_CHRONO_TOKEN # token 82 | # Chronocat 开启 red 服务的端口,默认为 16530。 83 | port: 16530 # port 84 | # 服务器监听的地址。 如果你不知道这是什么,那么不填此项即可! 85 | listen: localhost # host 86 | ``` 87 | 88 | 而多账号下, 89 | - `port` 与配置文件下下的 `overrides[QQ].servers[X].port` 一致,并且一个 `QQ` 只能对应一个 `port` 90 | - `token` 与配置文件下下的 `overrides[QQ].servers[X].token` 一致 91 | - `host` 与配置文件下下的 `overrides[QQ].servers[X].listen` 一致 92 | 93 | ```yaml 94 | # ~/.chronocat/config/chronocat.yml 95 | overrides: 96 | 1234567890: 97 | servers: 98 | - type: red 99 | # Chronocat 已经自动生成了随机 token。要妥善保存哦! 100 | # 客户端使用服务时需要提供这个 token! 101 | token: DEFINE_CHRONO_TOKEN # token 102 | # Chronocat 开启 red 服务的端口,默认为 16530。 103 | port: 16531 # port 104 | # 服务器监听的地址。 如果你不知道这是什么,那么不填此项即可! 105 | listen: localhost 106 | ``` 107 | 108 | #### 旧版 Chronocat 109 | 110 | 对于旧版的 Chronocat, 111 | - `port` 是默认的 `16530` 112 | - `token` 被默认存储在 `%AppData%/BetterUniverse/QQNT/RED_PROTOCOL_TOKEN` 或 `~/BetterUniverse/QQNT/RED_PROTOCOL_TOKEN` 中,并保持不变。 113 | - `host` 默认为 `localhost`。 114 | 115 | 116 | ## 功能 117 | 118 | 支持的事件: 119 | - 群聊消息、好友消息 (能够接收到来着不同设备的自己的消息) 120 | - 群名称改动事件 121 | - 群成员禁言/解除禁言事件 122 | - 群成员加入事件 (包括旧版受邀请入群) 123 | 124 | 支持的 api: 125 | - 发送消息 (文字,at,图片,文件,表情,引用回复) 126 | - 发送伪造合并转发 (文字,at,图片) 127 | - 获取自身资料 128 | - 获取好友、群组、群组内群员资料 129 | - 获取群公告 130 | - 禁言/解禁群员 131 | - 全体禁言 132 | - 获取历史消息 133 | - 获取媒体消息的原始数据 134 | 135 | 完整的 api 文档请参考 [API 文档](api.md) 或 [QQNTRedProtocol](https://chrononeko.github.io/QQNTRedProtocol/http/) 136 | 137 | ## 示例 138 | 139 | ```python 140 | from pathlib import Path 141 | 142 | from nonebot import on_command 143 | from nonebot.adapters.red import Bot 144 | from nonebot.adapters.red.event import MessageEvent 145 | from nonebot.adapters.red.message import MessageSegment 146 | 147 | 148 | matcher = on_command("test") 149 | 150 | @matcher.handle() 151 | async def handle_receive(bot: Bot, event: MessageEvent): 152 | if event.is_group: 153 | await bot.send_group_message(event.scene, MessageSegment.image(Path("path/to/img.jpg"))) 154 | ``` 155 | -------------------------------------------------------------------------------- /.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 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 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 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | .idea/ 163 | -------------------------------------------------------------------------------- /nonebot/adapters/red/api/handle.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Tuple, Callable 2 | 3 | 4 | def _send_message(data: Dict[str, Any]) -> Tuple[str, str, dict]: 5 | return ( 6 | "message/send", 7 | "POST", 8 | { 9 | "peer": { 10 | "chatType": data["chat_type"], 11 | "peerUin": data["target"], 12 | "guildId": None, 13 | }, 14 | "elements": data["elements"], 15 | }, 16 | ) 17 | 18 | 19 | def _get_self_profile(data: Dict[str, Any]) -> Tuple[str, str, dict]: 20 | return "getSelfProfile", "GET", {} 21 | 22 | 23 | def _get_friends(data: Dict[str, Any]) -> Tuple[str, str, dict]: 24 | return "bot/friends", "GET", {} 25 | 26 | 27 | def _get_groups(data: Dict[str, Any]) -> Tuple[str, str, dict]: 28 | return "bot/groups", "GET", {} 29 | 30 | 31 | def _mute_member(data: Dict[str, Any]) -> Tuple[str, str, dict]: 32 | return ( 33 | "group/muteMember", 34 | "POST", 35 | { 36 | "group": data["group"], 37 | "memList": [ 38 | {"uin": i, "timeStamp": data["duration"]} for i in data["members"] 39 | ], 40 | }, 41 | ) 42 | 43 | 44 | def _unmute_member(data: Dict[str, Any]) -> Tuple[str, str, dict]: 45 | return ( 46 | "group/muteMember", 47 | "POST", 48 | { 49 | "group": data["group"], 50 | "memList": [{"uin": i, "timeStamp": 0} for i in data["members"]], 51 | }, 52 | ) 53 | 54 | 55 | def _mute_everyone(data: Dict[str, Any]) -> Tuple[str, str, dict]: 56 | return ( 57 | "group/muteEveryone", 58 | "POST", 59 | {"group": data["group"], "enable": True}, 60 | ) 61 | 62 | 63 | def _unmute_everyone(data: Dict[str, Any]) -> Tuple[str, str, dict]: 64 | return ( 65 | "group/muteEveryone", 66 | "POST", 67 | {"group": data["group"], "enable": False}, 68 | ) 69 | 70 | 71 | def _kick(data: Dict[str, Any]) -> Tuple[str, str, dict]: 72 | return ( 73 | "group/kick", 74 | "POST", 75 | { 76 | "uidList": data["members"], 77 | "group": data["group"], 78 | "refuseForever": data["refuse_forever"], 79 | "reason": data["reason"], 80 | }, 81 | ) 82 | 83 | 84 | def _get_announcements(data: Dict[str, Any]) -> Tuple[str, str, dict]: 85 | return "group/getAnnouncements", "POST", {"group": data["group"]} 86 | 87 | 88 | def _get_members(data: Dict[str, Any]) -> Tuple[str, str, dict]: 89 | return ( 90 | "group/getMemberList", 91 | "POST", 92 | {"group": data["group"], "size": data["size"]}, 93 | ) 94 | 95 | 96 | def _fetch_media(data: Dict[str, Any]) -> Tuple[str, str, dict]: 97 | return ( 98 | "message/fetchRichMedia", 99 | "POST", 100 | { 101 | "msgId": data["msg_id"], 102 | "chatType": data["chat_type"], 103 | "peerUid": data["target"], 104 | "elementId": data["element_id"], 105 | "thumbSize": data["thumb_size"], 106 | "downloadType": data["download_type"], 107 | }, 108 | ) 109 | 110 | 111 | def _upload(data: Dict[str, Any]) -> Tuple[str, str, dict]: 112 | return "upload", "POST", data["file"] 113 | 114 | 115 | def _recall_message(data: Dict[str, Any]) -> Tuple[str, str, dict]: 116 | return ( 117 | "message/recall", 118 | "POST", 119 | { 120 | "msgIds": data["msg_ids"], 121 | "peer": { 122 | "chatType": data["chat_type"], 123 | "peerUin": data["target"], 124 | "guildId": None, 125 | }, 126 | }, 127 | ) 128 | 129 | 130 | def _get_history_messages(data: Dict[str, Any]) -> Tuple[str, str, dict]: 131 | return ( 132 | "message/getHistory", 133 | "POST", 134 | { 135 | "peer": { 136 | "chatType": data["chat_type"], 137 | "peerUin": data["target"], 138 | "guildId": None, 139 | }, 140 | "offsetMsgId": data["offset_msg_id"], 141 | "count": data["count"], 142 | }, 143 | ) 144 | 145 | 146 | def _send_fake_forward(data: Dict[str, Any]) -> Tuple[str, str, dict]: 147 | return ( 148 | "message/unsafeSendForward", 149 | "POST", 150 | { 151 | "dstContact": { 152 | "chatType": data["chat_type"], 153 | "peerUin": data["target"], 154 | "guildId": None, 155 | }, 156 | "srcContact": { 157 | "chatType": data["source_chat_type"], 158 | "peerUin": data["source_target"], 159 | "guildId": None, 160 | }, 161 | "msgElements": data["elements"], 162 | }, 163 | ) 164 | 165 | 166 | HANDLERS: Dict[str, Callable[[Dict[str, Any]], Tuple[str, str, dict]]] = { 167 | "send_message": _send_message, 168 | "get_self_profile": _get_self_profile, 169 | "get_friends": _get_friends, 170 | "get_groups": _get_groups, 171 | "mute_member": _mute_member, 172 | "unmute_member": _unmute_member, 173 | "mute_everyone": _mute_everyone, 174 | "unmute_everyone": _unmute_everyone, 175 | "kick": _kick, 176 | "get_announcements": _get_announcements, 177 | "get_members": _get_members, 178 | "fetch_media": _fetch_media, 179 | "upload": _upload, 180 | "recall_message": _recall_message, 181 | "get_history_messages": _get_history_messages, 182 | "send_fake_forward": _send_fake_forward, 183 | } 184 | -------------------------------------------------------------------------------- /migrate.md: -------------------------------------------------------------------------------- 1 | # 迁移指南 2 | 3 | ## 前言 4 | 5 | `Red Protocol` 实现的事件非常少,对于原先使用 `onebot` 协议的机器人,需要进行大量的修改。 6 | 7 | ### 为什么不推荐使用 `Chronocat` 8 | 9 | `Chronocat` 属于 hook 框架,意味着你需要先运行一个完整的 `NTQQ` 客户端。哪怕 `Chronocat` 提供了无头模式,大幅降低了资源占用,但是其无法阻止 `NTQQ` 客户端 10 | 向存储设备写入大量的缓存数据,这对于一些资源有限的设备来说是致命的。 11 | 12 | 而由于 `Chronocat` 所依靠运行的 `LiteLoaderQQNT` 等框架的不稳定性 (`llqqnt` 受最新版 ntqq 的检测影响,至今仍未给出解决方案), 13 | 以及 `Chronocat` 的维护者的个人原因(如学业问题),`Chronocat` 也不是一个长期可靠的解决方案。 14 | 15 | 因此,我们不推荐也不反对使用 `Chronocat` 16 | 17 | ### 官方接口 18 | 19 | 再者,若你的机器人只需要接入 `QQ`,QQ 的官方接口也即将开放,我们也推荐有能力的开发者使用官方接口进行开发。`Nonebot` 已经发布了 QQ 官方接口的适配器:[`nonebot-adapter-qq`](https://github.com/nonebot/adapter-qq)。 20 | 21 | ### 其他的 qqnt 框架 22 | 23 | 如果你不想迁移插件,仍然想使用 `onebot` 适配器,那么你可以尝试 [Shamrock](https://github.com/linxinrao/Shamrock) 24 | 「试试 Shamrock,更新积极,模拟器可用,支持 onebot,方便迁移」 25 | 26 | 如果你对需要定期清理客户端缓存感到烦恼,那么你可以尝试 NTQQ 的协议实现,如 [Lagrange](https://github.com/Linwenxuan05/Lagrange.Core) 27 | 「试试 Lagrange,NTPC 协议,新时代协议实现」 28 | 29 | 30 | ## 跨平台方案 31 | 32 | 基于不稳定因素,我们更希望开发者考虑为自己的插件使用跨平台组件,如 `nonebot-adapter-satori`,`nonebot-plugin-alconna`, 33 | `nonebot-plugin-send-anything-anywhere` 等。 34 | 35 | ### Satori 36 | 37 | 随着 [`Satori` 适配器](https://github.com/nonebot/adapter-satori)的发布,接入 `Chronocat` 现在更推荐使用 `Satori` 适配器了。 38 | 39 | 关于 `Satori`:[介绍](https://satori.js.org/zh-CN/introduction.html) 40 | 41 | `Satori` 与 `onebot12` 定位相同,都属于跨平台协议,并且你可以通过 satori 接入 `Koishi` (https://github.com/koishijs/koishi) 等框架。 42 | 43 | 你只需要把 `Chronocat` 的配置文件做如下修改: 44 | 45 | ```yaml 46 | # ~/.chronocat/config/chronocat.yml 47 | servers: 48 | - type: satori # <------------------------------------ 修改这里 49 | # Chronocat 已经自动生成了随机 token。要妥善保存哦! 50 | # 客户端使用服务时需要提供这个 token! 51 | token: DEFINE_CHRONO_TOKEN # token 52 | # Chronocat 开启 red 服务的端口,默认为 5500。 53 | port: 5500 # port 54 | ``` 55 | 56 | 便能让你的 `Chronocat` 以 `Satori` 服务的形式运行。 57 | 58 | ### Plugin-Alconna 59 | 60 | [`Plugin-Alconna`](https://github.com/nonebot/plugin-alconna) 作为官方插件之一,是一个强大的 Nonebot2 命令匹配拓展,支持富文本/多媒体解析,跨平台消息收发。 61 | 62 | 使用文档:https://nonebot.dev/docs/next/best-practice/alconna/alconna 63 | 64 | 其支持的复杂命令结构、富文本解析,足以帮你丢弃以往的多媒体元素判断方法,而是直接通过 `on_alconna` 完成解析处理。 65 | 66 | 其实现的跨平台消息收发,也能让你丢弃对以往各平台的消息格式判断,而是直接通过 `send` 方法发送消息。 67 | 68 | `Plugin-Alconna` 支持现在 `Nonebot2` 的所有适配器,包括 `Satori`。 69 | 70 | 示例: 71 | ```python 72 | from nonebot_plugin_alconna import Image, Alconna, AlconnaMatcher, Args, Match, UniMessage, on_alconna 73 | 74 | test = on_alconna(Alconna("test", Args["img?", Image])) 75 | 76 | @test.handle() 77 | async def handle_test(matcher: AlconnaMatcher, img: Match[Image]): 78 | if img.available: 79 | matcher.set_path_arg("img", img.result) 80 | 81 | 82 | @test.got_path("img", prompt=UniMessage.template("{:At(user, $event.get_user_id())}\n请输入图片")) 83 | async def handle_foo(img: Image): 84 | await save_image(img) 85 | await test.send("图片已收到") 86 | ``` 87 | 88 | ### Send-Anything-Anywhere 89 | 90 | [`Send-Anything-Anywhere`](https://github.com/MountainDash/nonebot-plugin-send-anything-anywhere) 是一个帮助处理不同 adapter 消息的适配和发送的插件。 91 | 92 | 使用文档:https://send-anything-anywhere.felinae98.cn/ 93 | 94 | saa 通过以下方式帮助你处理不同 adapter 的消息: 95 | - 为常见的消息类型提供抽象类,自适应转换成对应 adapter 的消息 96 | - 提供一套统一的,符合直觉的发送接口 97 | - 为复杂的消息提供易用的生成接口(规划中) 98 | - 通过传入 bot 的类型来自适应生成对应 bot adapter 所使用的 Message 99 | 100 | saa 目前支持的适配器有 onebot v11/v12, QQ 官方接口/频道接口,Kook,Telegram,Feishu,以及本适配器。 101 | 102 | 示例: 103 | ```python 104 | from nonebot.adapters.onebot.v11.event import MessageEvent as V11MessageEvent 105 | from nonebot.adapters.onebot.v12.event import MessageEvent as V12MessageEvent 106 | from nonebot.internal.adapter.bot import Bot 107 | from nonebot_plugin_saa import Image, Text, MessageFactory 108 | 109 | pic_matcher = nonebot.on_command('发送图片') 110 | 111 | pic_matcher.handle() 112 | async def _handle_v12(bot: Bot, event: Union[V12MessageEvent, V11MessageEvent]): 113 | pic_content = ... 114 | msg_builder = MessageFactory([ 115 | Image(pic_content), Text("这是你要的图片") 116 | ]) 117 | # or msg_builder = Image(pic_content) + Text("这是你要的图片") 118 | await msg_builder.send() 119 | await pic_matcher.finish() 120 | ``` 121 | 122 | 123 | ## 消息事件 124 | 125 | `Red` 下的消息事件只有两种:`PrivateMessageEvent` 和 `GroupMessageEvent`。 126 | 127 | 因为 `Red` 下事件结构比较贴合原始协议,以下字段你可能会感到陌生: 128 | - `senderUin` / `senderUId`:发送者 QQ 号 129 | - `peerUin` / `peerUId`:消息所在群组的群号或私聊对象的 QQ 号 130 | - `sendNickName`: 发送者昵称 131 | - `sendMemberName`: 发送者群名片 (假如是群消息) 132 | - `peerName`: 群名 (假如是群消息) 133 | 134 | 当然,我们为 `MessageEvent` 提供了一些属性来帮助你更方便地获取消息内容: 135 | 136 | ```python 137 | @property 138 | def time(self): 139 | """消息发送时间""" 140 | return datetime.fromtimestamp(int(self.msgTime)) 141 | 142 | @property 143 | def scene(self) -> str: 144 | """群组或好友的id""" 145 | return self.peerUin or self.peerUid 146 | 147 | @property 148 | def is_group(self) -> bool: 149 | """是否为群组消息""" 150 | return self.chatType == ChatType.GROUP 151 | 152 | @property 153 | def is_private(self) -> bool: 154 | """是否为私聊消息""" 155 | return self.chatType == ChatType.FRIEND 156 | 157 | @property 158 | def user_id(self) -> str: 159 | """好友的id""" 160 | return self.peerUin or self.peerUid 161 | 162 | @property 163 | def group_id(self) -> str: 164 | """群组的id""" 165 | return self.peerUin or self.peerUid 166 | ``` 167 | 168 | ### 消息内容 169 | 170 | `Red` 下消息支持的内容类型有: 171 | 172 | - `Text`:文本消息 173 | - `Image`:图片消息 174 | - `Voice`:语音消息 175 | - `Video`:视频消息 176 | - `File`:文件消息 177 | - `At`:@ 消息 178 | - `AtAll`:@ 全体成员消息 179 | - `Reply`:回复消息 180 | - `Face`:表情消息 181 | - `MarketFace`: 商店表情消息 182 | - `Forward`:合并转发消息 183 | 184 | 其中 `market_face` 仅能接收,不能发送。 185 | 186 | `forward` 需要通过特殊的 api 发送,不支持直接发送。 187 | 188 | **注意:`Red` 不是 gocqhttp,不支持 `cq码`。如果你仍然在使用 `cq码`,请务必迁移使用 `MessageSegment`。** 189 | 190 | 关于消息与消息段的使用:https://nonebot.dev/docs/next/tutorial/message 191 | 192 | ## 提醒事件 193 | 194 | `Red` 下的提醒事件有: 195 | - `GroupNameUpdateEvent`:群名变更事件 196 | - `MemberAddEvent`:群成员加入事件 197 | - `MemberMuteEvent`:群成员禁言事件 198 | - `MemberMutedEvent`:群成员被禁言事件 199 | - `MemberUnmutedEvent`:群成员被解除禁言事件 200 | 201 | ### 群名变更事件 202 | 203 | 其有以下属性: 204 | - `currentName`:当前群名 205 | - `operatorUid`:操作者 QQ 号 206 | - `operatorName`:操作者昵称 207 | 208 | ### 群成员加入事件 209 | 210 | 其有以下属性: 211 | - `memberUid`:加入者 QQ 号 212 | - `memberName`:加入者昵称 213 | - `operatorUid`:操作者 QQ 号 214 | 215 | ### 群成员禁言事件 216 | 217 | 其有以下属性: 218 | - `start`:禁言开始时间 219 | - `duration`:禁言时长 220 | - `operator`: 操作者 221 | - `member`:被禁言者 222 | 223 | ## API 224 | 225 | 参照 [API 列表](./api.md) 226 | -------------------------------------------------------------------------------- /nonebot/adapters/red/adapter.py: -------------------------------------------------------------------------------- 1 | import json 2 | import asyncio 3 | from typing_extensions import override 4 | from typing import Any, List, Type, Union, Optional 5 | 6 | from nonebot.utils import escape_tag 7 | from pydantic import ValidationError 8 | from nonebot.compat import type_validate_python 9 | from nonebot.drivers import Driver, Request, WebSocket, ForwardDriver 10 | from nonebot.exception import ActionFailed, NetworkError, WebSocketClosed 11 | 12 | from nonebot import get_plugin_config 13 | from nonebot.adapters import Adapter as BaseAdapter 14 | 15 | from .bot import Bot 16 | from .utils import log 17 | from .api.model import MsgType 18 | from .api.handle import HANDLERS 19 | from .api.model import Message as MessageModel 20 | from .config import Config, BotInfo, get_config 21 | from .event import ( 22 | Event, 23 | MemberAddEvent, 24 | MemberMuteEvent, 25 | GroupMessageEvent, 26 | PrivateMessageEvent, 27 | GroupNameUpdateEvent, 28 | ) 29 | 30 | 31 | class Adapter(BaseAdapter): 32 | @override 33 | def __init__(self, driver: Driver, **kwargs: Any): 34 | super().__init__(driver, **kwargs) 35 | # 读取适配器所需的配置项 36 | self.red_config: Config = get_plugin_config(Config) 37 | self._bots = self.red_config.red_bots 38 | if self.red_config.red_auto_detect and not self._bots: 39 | try: 40 | log("INFO", "Auto detect chronocat config...") 41 | self._bots = get_config() 42 | log("SUCCESS", f"Auto detect {len(self._bots)} bots.") 43 | except ImportError: 44 | log("ERROR", "Please install `PyYAML` to enable auto detect!") 45 | self.tasks: List[asyncio.Task] = [] # 存储 ws 任务 46 | self.setup() 47 | 48 | @classmethod 49 | @override 50 | def get_name(cls) -> str: 51 | """适配器名称""" 52 | return "RedProtocol" 53 | 54 | def setup(self) -> None: 55 | if not isinstance(self.driver, ForwardDriver): 56 | # 判断用户配置的Driver类型是否符合适配器要求,不符合时应抛出异常 57 | raise RuntimeError( 58 | f"Current driver {self.config.driver} " 59 | f"doesn't support forward connections!" 60 | f"{self.get_name()} Adapter need a ForwardDriver to work." 61 | ) 62 | # 在 NoneBot 启动和关闭时进行相关操作 63 | self.driver.on_startup(self.startup) 64 | self.driver.on_shutdown(self.shutdown) 65 | 66 | async def startup(self) -> None: 67 | """定义启动时的操作,例如和平台建立连接""" 68 | if not self._bots: 69 | log( 70 | "WARNING", 71 | "No bots found in config! \n" 72 | "Please check your config file and make sure it's correct.", 73 | ) 74 | for bot in self._bots: 75 | self.tasks.append(asyncio.create_task(self._forward_ws(bot))) 76 | 77 | async def shutdown(self) -> None: 78 | for task in self.tasks: 79 | if not task.done(): 80 | task.cancel() 81 | 82 | async def _forward_ws(self, bot_info: BotInfo) -> None: 83 | bot: Optional[Bot] = None 84 | ws_url = f"ws://{bot_info.host}:{bot_info.port}/" 85 | req = Request("GET", ws_url, timeout=60.0) 86 | while True: 87 | try: 88 | async with self.websocket(req) as ws: 89 | log( 90 | "DEBUG", 91 | f"WebSocket Connection to " 92 | f"{escape_tag(str(ws_url))} established", 93 | ) 94 | connect_packet = { 95 | "type": "meta::connect", 96 | "payload": {"token": bot_info.token}, 97 | } 98 | try: 99 | await ws.send(json.dumps(connect_packet)) 100 | connect_data = json.loads(await ws.receive()) 101 | 102 | self_id = connect_data["payload"]["authData"]["uin"] 103 | bot = Bot(self, self_id, bot_info) 104 | self.bot_connect(bot) 105 | log( 106 | "INFO", 107 | f"Bot {escape_tag(self_id)} connected, " 108 | f"Chronocat Version: " 109 | f"{connect_data['payload']['version']}", 110 | ) 111 | await self._loop(bot, ws) 112 | except WebSocketClosed as e: 113 | log( 114 | "ERROR", 115 | "WebSocket Closed", 116 | e, 117 | ) 118 | except Exception as e: 119 | log( 120 | "ERROR", 121 | "Error while process data from websocket " 122 | f"{escape_tag(str(ws_url))}. " 123 | f"Trying to reconnect...", 124 | e, 125 | ) 126 | finally: 127 | if bot: 128 | self.bot_disconnect(bot) 129 | except Exception as e: 130 | # 尝试重连 131 | log( 132 | "ERROR", 133 | "" 134 | "Error while setup websocket to " 135 | f"{escape_tag(str(ws_url))}. Trying to reconnect..." 136 | f"", 137 | e, 138 | ) 139 | await asyncio.sleep(3) # 重连间隔 140 | 141 | async def _loop(self, bot: Bot, ws: WebSocket): 142 | while True: 143 | data = await ws.receive() 144 | json_data = json.loads(data) 145 | _event_type = json_data["type"] 146 | if not json_data["payload"]: 147 | log("WARNING", f"received empty event {_event_type}") 148 | continue 149 | 150 | def _handle_event(event_data: Any, target: Type[Event]): 151 | try: 152 | event = target.convert(event_data) 153 | except Exception as e: 154 | log( 155 | "WARNING", 156 | f"Failed to parse event data: {event_data}", 157 | e, 158 | ) 159 | else: 160 | asyncio.create_task(bot.handle_event(event)) 161 | 162 | def _handle_message(message: dict): 163 | try: 164 | _data = type_validate_python(MessageModel, message) 165 | except ValidationError as e: 166 | log( 167 | "WARNING", 168 | f"Failed to parse message data: {message}", 169 | e, 170 | ) 171 | return 172 | if _data.msgType == MsgType.system and _data.sendType == 3: 173 | if ( 174 | _data.subMsgType == 8 175 | and _data.elements[0].elementType == 8 176 | and _data.elements[0].grayTipElement 177 | and _data.elements[0].grayTipElement.subElementType == 4 178 | and _data.elements[0].grayTipElement.groupElement 179 | and _data.elements[0].grayTipElement.groupElement.type == 1 180 | ): 181 | _handle_event(_data, MemberAddEvent) 182 | elif ( 183 | _data.subMsgType == 8 184 | and _data.elements[0].elementType == 8 185 | and _data.elements[0].grayTipElement 186 | and _data.elements[0].grayTipElement.subElementType == 4 187 | and _data.elements[0].grayTipElement.groupElement 188 | and _data.elements[0].grayTipElement.groupElement.type == 8 189 | ): 190 | _handle_event(_data, MemberMuteEvent) 191 | elif ( 192 | _data.subMsgType == 8 193 | and _data.elements[0].elementType == 8 194 | and _data.elements[0].grayTipElement 195 | and _data.elements[0].grayTipElement.subElementType == 4 196 | and _data.elements[0].grayTipElement.groupElement 197 | and _data.elements[0].grayTipElement.groupElement.type == 5 198 | ): 199 | _handle_event(_data, GroupNameUpdateEvent) 200 | elif ( 201 | _data.subMsgType == 12 202 | and _data.elements[0].elementType == 8 203 | and _data.elements[0].grayTipElement 204 | and _data.elements[0].grayTipElement.subElementType == 12 205 | and _data.elements[0].grayTipElement.xmlElement 206 | and _data.elements[0].grayTipElement.xmlElement.busiType == "1" 207 | and _data.elements[0].grayTipElement.xmlElement.busiId 208 | == "10145" 209 | ): 210 | _handle_event(_data, MemberAddEvent) 211 | else: 212 | log("WARNING", f"received unsupported event: {message}") 213 | return 214 | else: 215 | if _data.chatType == 1: 216 | _handle_event(_data, PrivateMessageEvent) 217 | elif _data.chatType == 2: 218 | _handle_event(_data, GroupMessageEvent) 219 | else: 220 | log("WARNING", f"received unsupported event: {message}") 221 | return 222 | 223 | if _event_type == "message::recv": 224 | for msg in json_data["payload"]: 225 | _handle_message(msg) 226 | else: 227 | _handle_event(json_data["payload"], Event) 228 | 229 | @override 230 | async def _call_api(self, bot: Bot, api: str, **data: Any) -> Union[dict, bytes]: 231 | log("DEBUG", f"Calling API {api}") # 给予日志提示 232 | if not (handler := HANDLERS.get(api)): 233 | raise NotImplementedError(f"API {api} not implemented") 234 | api, method, platform_data = handler(data) 235 | # 采用 HTTP 请求的方式,需要构造一个 Request 对象 236 | request = Request( 237 | method=method, # 请求方法 238 | url=bot.info.api_base / api, # 接口地址 239 | headers={"Authorization": f"Bearer {bot.info.token}"}, 240 | content=json.dumps(platform_data), 241 | data=platform_data, 242 | ) 243 | if api == "message/fetchRichMedia": 244 | return (await self.request(request)).content # type: ignore 245 | # 发送请求,返回结果 246 | return json.loads((await self.request(request)).content) # type: ignore 247 | 248 | @override 249 | async def request(self, setup: Request): 250 | try: 251 | resp = await super().request(setup) 252 | except Exception as e: 253 | raise NetworkError(f"Failed to request {setup.url}") from e 254 | if resp.status_code != 200: 255 | raise ActionFailed( 256 | self.get_name(), 257 | f"HTTP status code {resp.status_code} " 258 | f"response body: {resp.content}", 259 | ) 260 | return resp 261 | -------------------------------------------------------------------------------- /nonebot/adapters/red/event.py: -------------------------------------------------------------------------------- 1 | import re 2 | from copy import deepcopy 3 | from typing import Any, Dict, Optional 4 | from typing_extensions import override 5 | from datetime import datetime, timedelta 6 | 7 | from nonebot.utils import escape_tag 8 | from nonebot.compat import model_dump, type_validate_python 9 | 10 | from nonebot.adapters import Event as BaseEvent 11 | 12 | from .message import Message 13 | from .compat import model_validator 14 | from .api.model import Message as MessageModel 15 | from .api.model import MsgType, ChatType, ReplyElement, ShutUpTarget 16 | 17 | 18 | class Event(BaseEvent): 19 | @override 20 | def get_type(self) -> str: 21 | # 现阶段Red协议只有message事件 22 | return "event" 23 | 24 | @override 25 | def get_event_name(self) -> str: 26 | # 返回事件的名称,用于日志打印 27 | return "event" 28 | 29 | @override 30 | def get_event_description(self) -> str: 31 | return escape_tag(str(model_dump(self))) 32 | 33 | @override 34 | def get_message(self): 35 | raise ValueError("Event has no message!") 36 | 37 | @override 38 | def get_user_id(self) -> str: 39 | raise ValueError("Event has no context!") 40 | 41 | @override 42 | def get_session_id(self) -> str: 43 | raise ValueError("Event has no context!") 44 | 45 | @override 46 | def is_tome(self) -> bool: 47 | raise ValueError("Event has no context!") 48 | 49 | @classmethod 50 | def convert(cls, obj: Any): 51 | """将 Red API 返回的数据转换为对应的 Model 类 52 | 53 | 子类可根据需要重写此方法 54 | """ 55 | return type_validate_python(cls, obj) 56 | 57 | 58 | class MessageEvent(Event, MessageModel): 59 | """消息事件""" 60 | 61 | to_me: bool = False 62 | """ 63 | :说明: 消息是否与机器人有关 64 | 65 | :类型: ``bool`` 66 | """ 67 | reply: Optional[ReplyElement] = None 68 | """ 69 | :说明: 消息中提取的回复消息,内容为 ``get_msg`` API 返回结果 70 | 71 | :类型: ``Optional[ReplyElement]`` 72 | """ 73 | message: Message 74 | original_message: Message 75 | 76 | @override 77 | def get_type(self) -> str: 78 | return "message" 79 | 80 | @override 81 | def get_event_name(self) -> str: 82 | # 返回事件的名称,用于日志打印 83 | return "message" 84 | 85 | @override 86 | def get_message(self) -> Message: 87 | return self.message 88 | 89 | @model_validator(mode="before") 90 | def check_message(cls, values: Dict[str, Any]) -> Dict[str, Any]: 91 | if "elements" in values: 92 | values["message"] = Message.from_red_message( 93 | values["elements"], 94 | values["msgId"], 95 | values["chatType"], 96 | values["peerUin"] or values["peerUid"], 97 | ) 98 | values["original_message"] = deepcopy(values["message"]) 99 | return values 100 | 101 | @override 102 | def get_user_id(self) -> str: 103 | # 获取用户 ID 的方法,根据事件具体实现,如果事件没有用户 ID,则抛出异常 104 | if self.senderUin is None: 105 | raise ValueError("user_id doesn't exist.") 106 | return self.senderUin 107 | 108 | @override 109 | def get_session_id(self) -> str: 110 | # 获取事件会话 ID 的方法,根据事件具体实现,如果事件没有相关 ID,则抛出异常 111 | return f"{self.peerUin or self.peerUid}_{self.senderUin or self.senderUid}" 112 | 113 | @override 114 | def is_tome(self) -> bool: 115 | return self.to_me 116 | 117 | @property 118 | def scene(self) -> str: 119 | """群组或好友的id""" 120 | return self.peerUin or self.peerUid 121 | 122 | @property 123 | def is_group(self) -> bool: 124 | """是否为群组消息""" 125 | return self.chatType == ChatType.GROUP 126 | 127 | @property 128 | def is_private(self) -> bool: 129 | """是否为私聊消息""" 130 | return self.chatType == ChatType.FRIEND 131 | 132 | 133 | class PrivateMessageEvent(MessageEvent): 134 | """好友消息事件""" 135 | 136 | @override 137 | def get_event_name(self) -> str: 138 | return "message.private" 139 | 140 | @override 141 | def get_event_description(self) -> str: 142 | text = ( 143 | f"Message from {self.sendNickName or self.senderUin or self.senderUid}: " 144 | f"{self.get_message()}" 145 | ) 146 | return escape_tag(text) 147 | 148 | @property 149 | def user_id(self) -> str: 150 | """好友的id""" 151 | return self.peerUin or self.peerUid 152 | 153 | 154 | class GroupMessageEvent(MessageEvent): 155 | @override 156 | def get_event_name(self) -> str: 157 | return "message.group" 158 | 159 | @override 160 | def get_event_description(self) -> str: 161 | text = ( 162 | f"Message from {self.sendMemberName or self.senderUin or self.senderUid} " 163 | f"in {self.peerName or self.peerUin or self.peerUid}: " 164 | f"{self.get_message()}" 165 | ) 166 | return escape_tag(text) 167 | 168 | @property 169 | def group_id(self) -> str: 170 | """群组的id""" 171 | return self.peerUin or self.peerUid 172 | 173 | 174 | class NoticeEvent(Event): 175 | msgId: str 176 | msgRandom: str 177 | msgSeq: str 178 | cntSeq: str 179 | chatType: ChatType 180 | msgType: MsgType 181 | subMsgType: int 182 | peerUid: str 183 | peerUin: Optional[str] = None 184 | 185 | @override 186 | def get_type(self) -> str: 187 | return "notice" 188 | 189 | @override 190 | def get_event_name(self) -> str: 191 | return "notice" 192 | 193 | @property 194 | def scene(self) -> str: 195 | """群组或好友的id""" 196 | return self.peerUin or self.peerUid 197 | 198 | class Config: 199 | extra = "ignore" 200 | 201 | 202 | class GroupNameUpdateEvent(NoticeEvent): 203 | """群名变更事件""" 204 | 205 | currentName: str 206 | operatorUid: str 207 | operatorName: str 208 | 209 | @override 210 | def get_event_name(self) -> str: 211 | return "notice.group_name_update" 212 | 213 | @override 214 | def get_event_description(self) -> str: 215 | text = ( 216 | f"Group {self.peerUin or self.peerUid} name updated to {self.currentName}" 217 | ) 218 | return escape_tag(text) 219 | 220 | @override 221 | def get_user_id(self) -> str: 222 | # 获取用户 ID 的方法,根据事件具体实现,如果事件没有用户 ID,则抛出异常 223 | if self.operatorUid is None: 224 | raise ValueError("user_id doesn't exist.") 225 | return self.operatorUid 226 | 227 | @override 228 | def get_session_id(self) -> str: 229 | # 获取事件会话 ID 的方法,根据事件具体实现,如果事件没有相关 ID,则抛出异常 230 | return f"{self.peerUin or self.peerUid}_{self.operatorUid}" 231 | 232 | @classmethod 233 | @override 234 | def convert(cls, obj: Any): 235 | assert isinstance(obj, MessageModel) 236 | return cls( 237 | msgId=obj.msgId, 238 | msgRandom=obj.msgRandom, 239 | msgSeq=obj.msgSeq, 240 | cntSeq=obj.cntSeq, 241 | chatType=obj.chatType, 242 | msgType=obj.msgType, 243 | subMsgType=obj.subMsgType, 244 | peerUid=obj.peerUid, 245 | peerUin=obj.peerUin, 246 | currentName=obj.elements[0].grayTipElement.groupElement.groupName, # type: ignore # noqa: E501 247 | operatorUid=obj.elements[0].grayTipElement.groupElement.memberUin, # type: ignore # noqa: E501 248 | operatorName=obj.elements[0].grayTipElement.groupElement.memberNick, # type: ignore # noqa: E501 249 | ) 250 | 251 | 252 | legacy_invite_message = re.compile( 253 | r'jp="(\d+)".*jp="(\d+)"', re.DOTALL | re.MULTILINE | re.IGNORECASE 254 | ) 255 | 256 | 257 | class MemberAddEvent(NoticeEvent): 258 | """群成员增加事件""" 259 | 260 | memberUid: str 261 | operatorUid: str 262 | memberName: Optional[str] = None 263 | 264 | @override 265 | def get_event_name(self) -> str: 266 | return "notice.member_add" 267 | 268 | @override 269 | def get_event_description(self) -> str: 270 | text = ( 271 | f"Member {f'{self.memberName}({self.memberUid})' if self.memberName else self.memberUid} added to " # noqa: E501 272 | f"{self.peerUin or self.peerUid}" 273 | ) 274 | return escape_tag(text) 275 | 276 | @override 277 | def get_user_id(self) -> str: 278 | return self.memberUid 279 | 280 | @override 281 | def get_session_id(self) -> str: 282 | # 获取事件会话 ID 的方法,根据事件具体实现,如果事件没有相关 ID,则抛出异常 283 | return f"{self.peerUin or self.peerUid}_{self.memberUid}" 284 | 285 | @classmethod 286 | @override 287 | def convert(cls, obj: Any): 288 | assert isinstance(obj, MessageModel) 289 | params = { 290 | "msgId": obj.msgId, 291 | "msgRandom": obj.msgRandom, 292 | "msgSeq": obj.msgSeq, 293 | "cntSeq": obj.cntSeq, 294 | "chatType": obj.chatType, 295 | "msgType": obj.msgType, 296 | "subMsgType": obj.subMsgType, 297 | "peerUid": obj.peerUid, 298 | "peerUin": obj.peerUin, 299 | } 300 | if ( 301 | obj.elements[0].grayTipElement 302 | and obj.elements[0].grayTipElement.xmlElement 303 | and obj.elements[0].grayTipElement.xmlElement.content 304 | ): # type: ignore # noqa: E501 305 | # fmt: off 306 | if not (mat := legacy_invite_message.search(obj.elements[0].grayTipElement.xmlElement.content)): # type: ignore # noqa: E501 307 | raise ValueError("Invalid legacy invite message.") 308 | # fmt: on 309 | params["operatorUid"] = mat[1] 310 | params["memberUid"] = mat[2] 311 | else: 312 | params["memberUid"] = obj.elements[0].grayTipElement.groupElement.memberUin # type: ignore # noqa: E501 313 | params["operatorUid"] = obj.elements[0].grayTipElement.groupElement.adminUin # type: ignore # noqa: E501 314 | params["memberName"] = obj.elements[ 315 | 0 316 | ].grayTipElement.groupElement.memberNick # type: ignore # noqa: E501 317 | return cls(**params) 318 | 319 | 320 | class MemberMuteEvent(NoticeEvent): 321 | """群成员禁言相关事件""" 322 | 323 | start: datetime 324 | duration: timedelta 325 | operator: ShutUpTarget 326 | member: ShutUpTarget 327 | 328 | @override 329 | def get_user_id(self) -> str: 330 | return self.member.uin or self.member.uid 331 | 332 | @override 333 | def get_session_id(self) -> str: 334 | # 获取事件会话 ID 的方法,根据事件具体实现,如果事件没有相关 ID,则抛出异常 335 | return f"{self.peerUin or self.peerUid}_{self.member.uin or self.member.uid}" 336 | 337 | @classmethod 338 | @override 339 | def convert(cls, obj: Any): 340 | assert isinstance(obj, MessageModel) 341 | # fmt: off 342 | params = { 343 | "msgId": obj.msgId, 344 | "msgRandom": obj.msgRandom, 345 | "msgSeq": obj.msgSeq, 346 | "cntSeq": obj.cntSeq, 347 | "chatType": obj.chatType, 348 | "msgType": obj.msgType, 349 | "subMsgType": obj.subMsgType, 350 | "peerUid": obj.peerUid, 351 | "peerUin": obj.peerUin, 352 | "start": datetime.fromtimestamp( 353 | obj.elements[0].grayTipElement.groupElement.shutUp.curTime # type: ignore # noqa: E501 354 | ), 355 | "duration": timedelta( 356 | seconds=obj.elements[0].grayTipElement.groupElement.shutUp.duration # type: ignore # noqa: E501 357 | ), 358 | "operator": obj.elements[0].grayTipElement.groupElement.shutUp.admin, # type: ignore # noqa: E501 359 | "member": obj.elements[0].grayTipElement.groupElement.shutUp.member, # type: ignore # noqa: E501 360 | } 361 | # fmt: on 362 | if params["duration"].total_seconds() < 1: 363 | return MemberUnmuteEvent(**params) 364 | return MemberMutedEvent(**params) 365 | 366 | 367 | class MemberMutedEvent(MemberMuteEvent): 368 | """群成员被禁言事件""" 369 | 370 | @override 371 | def get_event_name(self) -> str: 372 | return "notice.member_muted" 373 | 374 | @override 375 | def get_event_description(self) -> str: 376 | text = ( 377 | f"Member {self.member.uin or self.member.uid} muted in " 378 | f"{self.peerUin or self.peerUid} for {self.duration}" 379 | ) 380 | return escape_tag(text) 381 | 382 | 383 | class MemberUnmuteEvent(MemberMuteEvent): 384 | """群成员被解除禁言事件""" 385 | 386 | @override 387 | def get_event_name(self) -> str: 388 | return "notice.member_unmute" 389 | 390 | @override 391 | def get_event_description(self) -> str: 392 | text = ( 393 | f"Member {self.member.uin or self.member.uid} unmute in " 394 | f"{self.peerUin or self.peerUid}" 395 | ) 396 | return escape_tag(text) 397 | -------------------------------------------------------------------------------- /nonebot/adapters/red/api/model.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | from datetime import datetime 3 | from typing import Any, List, Optional 4 | 5 | from pydantic import BaseModel 6 | 7 | 8 | class ChatType(IntEnum): 9 | FRIEND = 1 10 | GROUP = 2 11 | 12 | 13 | class RoleInfo(BaseModel): 14 | roleId: str 15 | name: str 16 | color: int 17 | 18 | 19 | class EmojiAd(BaseModel): 20 | url: str 21 | desc: str 22 | 23 | 24 | class EmojiMall(BaseModel): 25 | packageId: int 26 | emojiId: int 27 | 28 | 29 | class EmojiZplan(BaseModel): 30 | actionId: int 31 | actionName: str 32 | actionType: int 33 | playerNumber: int 34 | peerUid: str 35 | bytesReserveInfo: str 36 | 37 | 38 | class OtherAdd(BaseModel): 39 | uid: Optional[str] = None 40 | name: Optional[str] = None 41 | uin: Optional[str] = None 42 | 43 | 44 | class MemberAdd(BaseModel): 45 | showType: int 46 | otherAdd: Optional[OtherAdd] = None 47 | otherAddByOtherQRCode: Optional[Any] = None 48 | otherAddByYourQRCode: Optional[Any] = None 49 | youAddByOtherQRCode: Optional[Any] = None 50 | otherInviteOther: Optional[Any] = None 51 | otherInviteYou: Optional[Any] = None 52 | youInviteOther: Optional[Any] = None 53 | 54 | 55 | class ShutUpTarget(BaseModel): 56 | uid: str = "undefined" 57 | card: str 58 | name: str 59 | role: int 60 | uin: str 61 | 62 | 63 | class ShutUp(BaseModel): 64 | curTime: int 65 | duration: int 66 | admin: ShutUpTarget 67 | member: ShutUpTarget 68 | 69 | 70 | class GroupElement(BaseModel): 71 | type: int 72 | role: int 73 | groupName: Optional[str] = None 74 | memberUid: Optional[str] = None 75 | memberNick: Optional[str] = None 76 | memberRemark: Optional[str] = None 77 | adminUid: Optional[str] = None 78 | adminNick: Optional[str] = None 79 | adminRemark: Optional[str] = None 80 | createGroup: Optional[Any] = None 81 | memberAdd: Optional[MemberAdd] = None 82 | shutUp: Optional[ShutUp] = None 83 | memberUin: Optional[str] = None 84 | adminUin: Optional[str] = None 85 | 86 | 87 | class XmlElement(BaseModel): 88 | busiType: Optional[str] = None 89 | busiId: Optional[str] = None 90 | c2cType: int 91 | serviceType: int 92 | ctrlFlag: int 93 | content: Optional[str] = None 94 | templId: Optional[str] = None 95 | seqId: Optional[str] = None 96 | templParam: Optional[Any] = None 97 | pbReserv: Optional[str] = None 98 | members: Optional[Any] = None 99 | 100 | 101 | class TextElement(BaseModel): 102 | content: str 103 | atType: Optional[int] = None 104 | atUid: Optional[str] = None 105 | atTinyId: Optional[str] = None 106 | atNtUid: Optional[str] = None 107 | atNtUin: Optional[str] = None 108 | subElementType: Optional[int] = None 109 | atChannelId: Optional[str] = None 110 | atRoleId: Optional[str] = None 111 | atRoleColor: Optional[str] = None 112 | atRoleName: Optional[str] = None 113 | needNotify: Optional[str] = None 114 | 115 | 116 | class PicElement(BaseModel): 117 | picSubType: Optional[int] = None 118 | fileName: str 119 | fileSize: str 120 | picWidth: Optional[int] = None 121 | picHeight: Optional[int] = None 122 | original: Optional[bool] = None 123 | md5HexStr: str 124 | sourcePath: str 125 | thumbPath: Optional[Any] = None 126 | transferStatus: Optional[int] = None 127 | progress: Optional[int] = None 128 | picType: Optional[int] = None 129 | invalidState: Optional[int] = None 130 | fileUuid: Optional[str] = None 131 | fileSubId: Optional[str] = None 132 | thumbFileSize: Optional[int] = None 133 | summary: Optional[str] = None 134 | emojiAd: Optional[EmojiAd] = None 135 | emojiMall: Optional[EmojiMall] = None 136 | emojiZplan: Optional[EmojiZplan] = None 137 | 138 | 139 | class FaceElement(BaseModel): 140 | faceIndex: int 141 | faceText: Optional[str] = None 142 | """{None: normal, '/xxx': sticker, '': poke}""" 143 | faceType: int 144 | """{1: normal, 2: normal-extended, 3: sticker, 5: poke}""" 145 | packId: Optional[Any] = None 146 | stickerId: Optional[Any] = None 147 | sourceType: Optional[Any] = None 148 | stickerType: Optional[Any] = None 149 | resultId: Optional[Any] = None 150 | surpriseId: Optional[Any] = None 151 | randomType: Optional[Any] = None 152 | imageType: Optional[Any] = None 153 | pokeType: Optional[Any] = None 154 | spokeSummary: Optional[Any] = None 155 | doubleHit: Optional[Any] = None 156 | vaspokeId: Optional[Any] = None 157 | vaspokeName: Optional[Any] = None 158 | vaspokeMinver: Optional[Any] = None 159 | pokeStrength: Optional[Any] = None 160 | msgType: Optional[Any] = None 161 | faceBubbleCount: Optional[Any] = None 162 | pokeFlag: Optional[Any] = None 163 | 164 | 165 | class FileElement(BaseModel): 166 | fileMd5: str 167 | fileName: str 168 | filePath: str 169 | fileSize: str 170 | picHeight: Optional[int] = None 171 | picWidth: Optional[int] = None 172 | picThumbPath: Optional[Any] = None 173 | expireTime: Optional[str] = None 174 | file10MMd5: Optional[str] = None 175 | fileSha: Optional[str] = None 176 | fileSha3: Optional[str] = None 177 | videoDuration: Optional[int] = None 178 | transferStatus: Optional[int] = None 179 | progress: Optional[int] = None 180 | invalidState: Optional[int] = None 181 | fileUuid: Optional[str] = None 182 | fileSubId: Optional[str] = None 183 | thumbFileSize: Optional[int] = None 184 | fileBizId: Optional[Any] = None 185 | thumbMd5: Optional[Any] = None 186 | folderId: Optional[Any] = None 187 | fileGroupIndex: Optional[int] = None 188 | fileTransType: Optional[Any] = None 189 | 190 | 191 | class PttElement(BaseModel): 192 | fileName: str 193 | filePath: str 194 | md5HexStr: str 195 | fileSize: str 196 | duration: int 197 | formatType: int 198 | voiceType: int 199 | voiceChangeType: int 200 | canConvert2Text: bool 201 | fileId: int 202 | fileUuid: str 203 | text: Optional[str] = None 204 | translateStatus: Optional[int] = None 205 | transferStatus: Optional[int] = None 206 | progress: Optional[int] = None 207 | playState: Optional[int] = None 208 | waveAmplitudes: Optional[List[int]] = None 209 | invalidState: Optional[int] = None 210 | fileSubId: Optional[str] = None 211 | fileBizId: Optional[Any] = None 212 | 213 | 214 | class VideoElement(BaseModel): 215 | filePath: str 216 | fileName: str 217 | videoMd5: str 218 | thumbMd5: str 219 | fileTime: int 220 | thumbSize: int 221 | fileFormat: int 222 | fileSize: str 223 | thumbWidth: int 224 | thumbHeight: int 225 | busiType: int 226 | subBusiType: int 227 | thumbPath: Optional[Any] = None 228 | transferStatus: Optional[int] = None 229 | progress: Optional[int] = None 230 | invalidState: Optional[int] = None 231 | fileUuid: Optional[str] = None 232 | fileSubId: Optional[str] = None 233 | fileBizId: Optional[Any] = None 234 | 235 | 236 | class ReplyElement(BaseModel): 237 | replayMsgId: Optional[str] = None 238 | replayMsgSeq: str 239 | replyMsgTime: Optional[str] = None 240 | sourceMsgIdInRecords: str 241 | sourceMsgTextElems: Optional[Any] = None 242 | senderUid: Optional[str] = None 243 | senderUidStr: Optional[str] = None 244 | senderUin: Optional[str] = None 245 | 246 | 247 | class ArkElement(BaseModel): 248 | bytesData: str 249 | """application/json""" 250 | 251 | 252 | class MarketFaceElement(BaseModel): 253 | itemType: int 254 | faceInfo: int 255 | emojiPackageId: str 256 | subType: int 257 | faceName: str 258 | emojiId: str 259 | key: str 260 | staticFacePath: str 261 | dynamicFacePath: str 262 | 263 | 264 | class MultiForwardMsgElement(BaseModel): 265 | xmlContent: str 266 | resId: str 267 | fileName: str 268 | 269 | 270 | class GrayTipElement(BaseModel): 271 | subElementType: Optional[int] = None 272 | revokeElement: Optional[dict] = None 273 | proclamationElement: Optional[dict] = None 274 | emojiReplyElement: Optional[dict] = None 275 | groupElement: Optional[GroupElement] = None 276 | buddyElement: Optional[dict] = None 277 | feedMsgElement: Optional[dict] = None 278 | essenceElement: Optional[dict] = None 279 | groupNotifyElement: Optional[dict] = None 280 | buddyNotifyElement: Optional[dict] = None 281 | xmlElement: Optional[XmlElement] = None 282 | fileReceiptElement: Optional[dict] = None 283 | localGrayTipElement: Optional[dict] = None 284 | blockGrayTipElement: Optional[dict] = None 285 | aioOpGrayTipElement: Optional[dict] = None 286 | jsonGrayTipElement: Optional[dict] = None 287 | 288 | 289 | class Element(BaseModel): 290 | elementType: int 291 | elementId: Optional[str] = None 292 | extBufForUI: Optional[str] = None 293 | picElement: Optional[PicElement] = None 294 | textElement: Optional[TextElement] = None 295 | arkElement: Optional[ArkElement] = None 296 | avRecordElement: Optional[dict] = None 297 | calendarElement: Optional[dict] = None 298 | faceElement: Optional[FaceElement] = None 299 | fileElement: Optional[FileElement] = None 300 | giphyElement: Optional[dict] = None 301 | grayTipElement: Optional[GrayTipElement] = None 302 | inlineKeyboardElement: Optional[dict] = None 303 | liveGiftElement: Optional[dict] = None 304 | markdownElement: Optional[dict] = None 305 | marketFaceElement: Optional[MarketFaceElement] = None 306 | multiForwardMsgElement: Optional[MultiForwardMsgElement] = None 307 | pttElement: Optional[PttElement] = None 308 | replyElement: Optional[ReplyElement] = None 309 | structLongMsgElement: Optional[dict] = None 310 | textGiftElement: Optional[dict] = None 311 | videoElement: Optional[VideoElement] = None 312 | walletElement: Optional[dict] = None 313 | yoloGameResultElement: Optional[dict] = None 314 | 315 | 316 | class MsgType(IntEnum): 317 | normal = 2 318 | may_file = 3 319 | system = 5 320 | voice = 6 321 | video = 7 322 | value8 = 8 323 | reply = 9 324 | wallet = 10 325 | ark = 11 326 | may_market = 17 327 | 328 | 329 | class Message(BaseModel): 330 | msgId: str 331 | msgRandom: str 332 | msgSeq: str 333 | cntSeq: str 334 | chatType: ChatType 335 | msgType: MsgType 336 | subMsgType: int 337 | sendType: int 338 | senderUid: str = "undefined" 339 | senderUin: str = "-1" 340 | peerUid: str = "undefined" 341 | peerUin: str = "-1" 342 | channelId: str 343 | guildId: str 344 | guildCode: str 345 | fromUid: str 346 | fromAppid: str 347 | msgTime: str 348 | msgMeta: str 349 | sendStatus: int 350 | sendMemberName: str 351 | sendNickName: str 352 | guildName: str 353 | channelName: str 354 | elements: List[Element] 355 | records: List["Message"] 356 | emojiLikesList: List[Any] 357 | commentCnt: str 358 | directMsgFlag: int 359 | directMsgMembers: List[Any] 360 | peerName: str 361 | editable: bool 362 | avatarMeta: str 363 | avatarPendant: Optional[str] = None 364 | feedId: Optional[str] = None 365 | roleId: str 366 | timeStamp: str 367 | isImportMsg: bool 368 | atType: int 369 | roleType: Optional[int] = None 370 | fromChannelRoleInfo: RoleInfo 371 | fromGuildRoleInfo: RoleInfo 372 | levelRoleInfo: RoleInfo 373 | recallTime: str 374 | isOnlineMsg: bool 375 | generalFlags: str 376 | clientSeq: str 377 | nameType: Optional[int] = None 378 | avatarFlag: Optional[int] = None 379 | 380 | @property 381 | def time(self): 382 | return datetime.fromtimestamp(int(self.msgTime)) 383 | 384 | 385 | class Profile(BaseModel): 386 | uid: str 387 | qid: str 388 | uin: str 389 | nick: str 390 | remark: str 391 | longNick: str 392 | avatarUrl: str 393 | birthday_year: int 394 | birthday_month: int 395 | birthday_day: int 396 | sex: int 397 | topTime: str 398 | isBlock: bool 399 | isMsgDisturb: bool 400 | isSpecialCareOpen: bool 401 | isSpecialCareZone: bool 402 | ringId: str 403 | status: int 404 | extStatus: Optional[int] = None 405 | categoryId: int 406 | onlyChat: bool 407 | qzoneNotWatch: bool 408 | qzoneNotWatched: bool 409 | vipFlag: Optional[bool] = None 410 | yearVipFlag: Optional[bool] = None 411 | svipFlag: Optional[bool] = None 412 | vipLevel: Optional[int] = None 413 | 414 | 415 | class Member(BaseModel): 416 | uid: str 417 | qid: str 418 | uin: str 419 | nick: str 420 | remark: str 421 | cardType: int 422 | cardName: str 423 | role: int 424 | avatarPath: str 425 | shutUpTime: int 426 | isDelete: bool 427 | 428 | 429 | class Group(BaseModel): 430 | groupCode: str 431 | maxMember: int 432 | memberCount: int 433 | groupName: str 434 | groupStatus: int 435 | memberRole: int 436 | isTop: bool 437 | toppedTimestamp: str 438 | privilegeFlag: int 439 | isConf: bool 440 | hasModifyConfGroupFace: bool 441 | hasModifyConfGroupName: bool 442 | remarkName: str 443 | avatarUrl: str 444 | hasMemo: bool 445 | groupShutupExpireTime: str 446 | personShutupExpireTime: str 447 | discussToGroupUin: str 448 | discussToGroupMaxMsgSeq: int 449 | discussToGroupTime: int 450 | 451 | 452 | class ImageInfo(BaseModel): 453 | width: int 454 | height: int 455 | type: Optional[str] = None 456 | mime: Optional[str] = None 457 | wUnits: Optional[str] = None 458 | hUnits: Optional[str] = None 459 | 460 | 461 | class UploadResponse(BaseModel): 462 | md5: str 463 | imageInfo: Optional[ImageInfo] = None 464 | fileSize: int 465 | filePath: str 466 | ntFilePath: str 467 | -------------------------------------------------------------------------------- /nonebot/adapters/red/bot.py: -------------------------------------------------------------------------------- 1 | import re 2 | import random 3 | from datetime import timedelta 4 | from typing_extensions import override 5 | from typing import Any, List, Tuple, Union, Optional 6 | 7 | from nonebot.message import handle_event 8 | from nonebot.compat import type_validate_python 9 | 10 | from nonebot.adapters import Bot as BaseBot 11 | from nonebot.adapters import Adapter as BaseAdapter 12 | from nonebot.adapters import MessageSegment as BaseMessageSegment 13 | 14 | from .utils import log 15 | from .config import BotInfo 16 | from .api.model import Group, Member 17 | from .api.model import Message as MessageModel 18 | from .event import Event, NoticeEvent, MessageEvent 19 | from .api.model import Profile, ChatType, UploadResponse 20 | from .message import Message, ForwardNode, MessageSegment, MediaMessageSegment 21 | 22 | 23 | def _check_reply(bot: "Bot", event: MessageEvent) -> None: 24 | """检查消息中存在的回复,去除并赋值 `event.reply`, `event.to_me`。 25 | 26 | 参数: 27 | bot: Bot 对象 28 | event: MessageEvent 对象 29 | """ 30 | try: 31 | index = event.message.index("reply") 32 | except ValueError: 33 | return 34 | 35 | msg_seg = event.message[index] 36 | 37 | event.reply = msg_seg.data["_origin"] # type: ignore 38 | 39 | # ensure string comparation 40 | if str(event.reply.senderUin) == str(bot.self_id) or str( 41 | event.reply.senderUid 42 | ) == str(bot.self_id): 43 | event.to_me = True 44 | 45 | del event.message[index] 46 | if len(event.message) > index and event.message[index].type == "at": 47 | del event.message[index] 48 | if len(event.message) > index and event.message[index].type == "text": 49 | event.message[index].data["text"] = event.message[index].data["text"].lstrip() 50 | if not event.message[index].data["text"]: 51 | del event.message[index] 52 | if not event.message: 53 | event.message.append(MessageSegment.text("")) 54 | 55 | 56 | def _check_to_me(bot: "Bot", event: MessageEvent) -> None: 57 | """检查消息开头或结尾是否存在 @机器人,去除并赋值 `event.to_me`。 58 | 59 | 参数: 60 | bot: Bot 对象 61 | event: MessageEvent 对象 62 | """ 63 | if not isinstance(event, MessageEvent): 64 | return 65 | 66 | # ensure message not empty 67 | if not event.message: 68 | event.message.append(MessageSegment.text("")) 69 | 70 | if event.chatType == ChatType.FRIEND: 71 | event.to_me = True 72 | else: 73 | 74 | def _is_at_me_seg(segment: MessageSegment) -> bool: 75 | return segment.type == "at" and str(segment.data.get("user_id", "")) == str( 76 | bot.self_id 77 | ) 78 | 79 | # check the first segment 80 | if _is_at_me_seg(event.message[0]): 81 | event.to_me = True 82 | event.message.pop(0) 83 | if event.message and event.message[0].type == "text": 84 | event.message[0].data["text"] = event.message[0].data["text"].lstrip() 85 | if not event.message[0].data["text"]: 86 | del event.message[0] 87 | 88 | if not event.to_me: 89 | # check the last segment 90 | i = -1 91 | last_msg_seg = event.message[i] 92 | if ( 93 | last_msg_seg.type == "text" 94 | and not last_msg_seg.data["text"].strip() 95 | and len(event.message) >= 2 96 | ): 97 | i -= 1 98 | last_msg_seg = event.message[i] 99 | 100 | if _is_at_me_seg(last_msg_seg): 101 | event.to_me = True 102 | del event.message[i:] 103 | 104 | if not event.message: 105 | event.message.append(MessageSegment.text("")) 106 | 107 | 108 | def _check_nickname(bot: "Bot", event: MessageEvent) -> None: 109 | """检查消息开头是否存在昵称,去除并赋值 `event.to_me`。 110 | 111 | 参数: 112 | bot: Bot 对象 113 | event: MessageEvent 对象 114 | """ 115 | first_msg_seg = event.message[0] 116 | if first_msg_seg.type != "text": 117 | return 118 | 119 | nicknames = {re.escape(n) for n in bot.config.nickname} 120 | if not nicknames: 121 | return 122 | 123 | # check if the user is calling me with my nickname 124 | nickname_regex = "|".join(nicknames) 125 | first_text = first_msg_seg.data["text"] 126 | if m := re.search(rf"^({nickname_regex})([\s,,]*|$)", first_text, re.IGNORECASE): 127 | log("DEBUG", f"User is calling me {m[1]}") 128 | event.to_me = True 129 | first_msg_seg.data["text"] = first_text[m.end() :] 130 | 131 | 132 | def get_peer_data(event: Event, **kwargs: Any) -> Tuple[int, str]: 133 | if isinstance(event, (MessageEvent, NoticeEvent)): 134 | return event.chatType, event.peerUin or event.peerUid 135 | return kwargs["chatType"], kwargs["peerUin"] 136 | 137 | 138 | class Bot(BaseBot): 139 | """ 140 | Red 协议 Bot 适配。 141 | """ 142 | 143 | @override 144 | def __init__( 145 | self, adapter: BaseAdapter, self_id: str, info: BotInfo, **kwargs: Any 146 | ): 147 | super().__init__(adapter, self_id) 148 | self.adapter: BaseAdapter = adapter 149 | self.info: BotInfo = info 150 | # 一些有关 Bot 的信息也可以在此定义和存储 151 | 152 | async def handle_event(self, event: Event): 153 | # TODO: 检查事件是否有回复消息,调用平台 API 获取原始消息的消息内容 154 | if isinstance(event, MessageEvent): 155 | _check_reply(self, event) 156 | _check_to_me(self, event) 157 | _check_nickname(self, event) 158 | 159 | await handle_event(self, event) 160 | 161 | async def send_message( 162 | self, 163 | chat_type: ChatType, 164 | target: Union[int, str], 165 | message: Union[str, Message, MessageSegment], 166 | ) -> MessageModel: 167 | """依据聊天类型与目标 id 发送消息 168 | 169 | 参数: 170 | chat_type: 聊天类型,分为好友与群组 171 | target: 目标 id 172 | message: 发送的消息 173 | """ 174 | message = Message(message) 175 | if message.has("forward"): 176 | forward = message["forward", 0] 177 | return await self.send_fake_forward( 178 | forward.data["nodes"], 179 | chat_type, 180 | target, 181 | ) 182 | element_data = await message.export(self) 183 | resp = await self.call_api( 184 | "send_message", 185 | chat_type=chat_type, 186 | target=str(target), 187 | elements=element_data, 188 | ) 189 | return type_validate_python(MessageModel, resp) 190 | 191 | async def send_friend_message( 192 | self, 193 | target: Union[int, str], 194 | message: Union[str, Message, MessageSegment], 195 | ) -> MessageModel: 196 | """发送好友消息 197 | 198 | 参数: 199 | target: 好友 id 200 | message: 发送的消息 201 | """ 202 | return await self.send_message(ChatType.FRIEND, target, message) 203 | 204 | async def send_group_message( 205 | self, 206 | target: Union[int, str], 207 | message: Union[str, Message, MessageSegment], 208 | ) -> MessageModel: 209 | """发送群组消息 210 | 211 | 参数: 212 | target: 群组 id 213 | message: 发送的消息 214 | """ 215 | return await self.send_message(ChatType.GROUP, target, message) 216 | 217 | @override 218 | async def send( 219 | self, 220 | event: Event, 221 | message: Union[str, Message, MessageSegment], 222 | **kwargs: Any, 223 | ) -> MessageModel: 224 | """依据收到的事件发送消息 225 | 226 | 参数: 227 | event: 收到的事件 228 | message: 发送的消息 229 | """ 230 | chatType, peerUin = get_peer_data(event, **kwargs) 231 | message = Message(message) 232 | if message.has("forward"): 233 | forward = message["forward", 0] 234 | return await self.send_fake_forward( 235 | forward.data["nodes"], 236 | ChatType(chatType), 237 | peerUin, 238 | ) 239 | element_data = await message.export(self) 240 | resp = await self.call_api( 241 | "send_message", 242 | chat_type=chatType, 243 | target=peerUin, 244 | elements=element_data, 245 | ) 246 | return type_validate_python(MessageModel, resp) 247 | 248 | async def get_self_profile(self) -> Profile: 249 | """获取登录账号自己的资料""" 250 | resp = await self.call_api("get_self_profile") 251 | return type_validate_python(Profile, resp) 252 | 253 | async def get_friends(self) -> List[Profile]: 254 | """获取登录账号所有好友的资料""" 255 | resp = await self.call_api("get_friends") 256 | return [type_validate_python(Profile, data) for data in resp] 257 | 258 | async def get_groups(self) -> List[Group]: 259 | """获取登录账号所有群组的资料""" 260 | resp = await self.call_api("get_groups") 261 | return [type_validate_python(Group, data) for data in resp] 262 | 263 | async def mute_member( 264 | self, group: int, *members: int, duration: Union[int, timedelta] = 60 265 | ): 266 | """禁言群成员 267 | 268 | 禁言时间会自动限制在 60s 至 30天内 269 | 270 | 参数: 271 | group: 群号 272 | *members: 禁言目标的 id 273 | duration: 禁言时间 274 | """ 275 | if isinstance(duration, timedelta): 276 | duration = int(duration.total_seconds()) 277 | duration = max(60, min(2592000, duration)) 278 | await self.call_api( 279 | "mute_member", group=group, members=list(members), duration=duration 280 | ) 281 | 282 | async def unmute_member(self, group: int, *members: int): 283 | """解除群成员禁言 284 | 285 | 参数: 286 | group: 群号 287 | *members: 禁言目标的 id 288 | """ 289 | await self.call_api("unmute_member", group=group, members=list(members)) 290 | 291 | async def mute_everyone(self, group: int): 292 | """开启全体禁言 293 | 294 | 参数: 295 | group: 群号 296 | """ 297 | await self.call_api("mute_everyone", group=group) 298 | 299 | async def unmute_everyone(self, group: int): 300 | """关闭全体禁言 301 | 302 | 参数: 303 | group: 群号 304 | """ 305 | await self.call_api("unmute_everyone", group=group) 306 | 307 | async def kick( 308 | self, 309 | group: int, 310 | *members: int, 311 | refuse_forever: bool = False, 312 | reason: Optional[str] = None, 313 | ): 314 | """移除群成员 315 | 316 | 参数: 317 | group: 群号 318 | *members: 要移除的群成员账号 319 | refuse_forever: 是否不再接受群成员的入群申请 320 | reason: 移除理由 321 | """ 322 | await self.call_api( 323 | "kick", 324 | group=group, 325 | members=list(members), 326 | refuse_forever=refuse_forever, 327 | reason=reason, 328 | ) 329 | 330 | async def get_announcements(self, group: int) -> List[dict]: 331 | """拉取群公告 332 | 333 | 参数: 334 | group: 群号 335 | """ 336 | return await self.call_api("get_announcements", group=group) 337 | 338 | async def get_members(self, group: int, size: int = 20) -> List[Member]: 339 | """获取指定群组内的成员资料 340 | 341 | 参数: 342 | group: 群号 343 | size: 拉取多少个成员资料 344 | """ 345 | resp = await self.call_api("get_members", group=group, size=size) 346 | return [type_validate_python(Member, data["detail"]) for data in resp] 347 | 348 | async def fetch(self, ms: BaseMessageSegment): 349 | """获取媒体消息段的二进制数据 350 | 351 | 参数: 352 | ms: 消息段 353 | """ 354 | if not isinstance(ms, MediaMessageSegment): 355 | raise ValueError(f"{ms} do not support to fetch data") 356 | return await ms.download(self) 357 | 358 | async def fetch_media( 359 | self, 360 | msg_id: str, 361 | chat_type: ChatType, 362 | target: Union[int, str], 363 | element_id: str, 364 | thumb_size: int = 0, 365 | download_type: int = 2, 366 | ) -> bytes: 367 | """获取媒体消息的二进制数据 368 | 369 | 注意:此接口不推荐直接使用 370 | 371 | 若需要获取媒体数据,你可以使用 `bot.fetch(MessageSegment)` 接口, 372 | 或 `ms.download(Bot)` 接口 373 | 374 | 参数: 375 | msg_id: 媒体消息的消息 id 376 | chat_type: 媒体消息的聊天类型 377 | target: 媒体消息的聊天对象 id 378 | element_id: 媒体消息中媒体元素的 id 379 | """ 380 | log("WARING", "This API is not suggest for user usage") 381 | peer = str(target) 382 | return await self.call_api( 383 | "fetch_media", 384 | msg_id=msg_id, 385 | chat_type=chat_type, 386 | target=peer, 387 | element_id=element_id, 388 | thumb_size=thumb_size, 389 | download_type=download_type, 390 | ) 391 | 392 | async def upload(self, file: bytes) -> UploadResponse: 393 | """上传资源 394 | 395 | 注意:此接口不推荐直接使用 396 | 397 | 参数: 398 | file: 上传的资源数据 399 | """ 400 | log("WARING", "This API is not suggest for user usage") 401 | return type_validate_python( 402 | UploadResponse, await self.call_api("upload", file=file) 403 | ) 404 | 405 | async def recall_message( 406 | self, 407 | chat_type: ChatType, 408 | target: Union[int, str], 409 | *ids: str, 410 | ): 411 | """撤回消息 412 | 413 | 参数: 414 | chat_type: 聊天类型,分为好友与群组 415 | target: 目标 id 416 | *ids: 要撤回的消息 id 417 | """ 418 | peer = str(target) 419 | await self.call_api( 420 | "recall_message", 421 | chat_type=chat_type, 422 | target=peer, 423 | msg_ids=list(ids), 424 | ) 425 | 426 | async def recall_group_message(self, group: int, *ids: str): 427 | """撤回群组消息 428 | 429 | 参数: 430 | target: 群组 id 431 | *ids: 要撤回的消息 id 432 | """ 433 | await self.recall_message(ChatType.GROUP, group, *ids) 434 | 435 | async def recall_friend_message(self, friend: int, *ids: str): 436 | """撤回好友消息 437 | 438 | 参数: 439 | target: 好友 id 440 | *ids: 要撤回的消息 id 441 | """ 442 | await self.recall_message(ChatType.FRIEND, friend, *ids) 443 | 444 | async def get_history_messages( 445 | self, 446 | chat_type: ChatType, 447 | target: Union[int, str], 448 | offset_msg_id: Optional[str] = None, 449 | count: int = 100, 450 | ): 451 | """拉取历史消息 452 | 453 | 参数: 454 | chat_type: 聊天类型,分为好友与群组 455 | target: 目标 id 456 | offset_msg_id: 从哪一条消息开始拉取,使用event.msgId 457 | count: 一次拉取多少消息 458 | """ 459 | peer = str(target) 460 | return await self.call_api( 461 | "get_history_messages", 462 | chat_type=chat_type, 463 | target=peer, 464 | offset_msg_id=offset_msg_id, 465 | count=count, 466 | ) 467 | 468 | async def send_fake_forward( 469 | self, 470 | nodes: List[ForwardNode], 471 | chat_type: ChatType, 472 | target: Union[int, str], 473 | source_chat_type: Optional[ChatType] = None, 474 | source_target: Optional[Union[int, str]] = None, 475 | ): 476 | """发送伪造合并转发消息 477 | 478 | 参数: 479 | nodes: 合并转发节点 480 | chat_type: 聊天类型,分为好友与群组 481 | target: 目标 id 482 | source_chat_type: 伪造的消息来源聊天类型,分为好友与群组 483 | source_target: 伪造的消息来源聊天对象 id 484 | """ 485 | if not nodes: 486 | raise ValueError("nodes cannot be empty") 487 | peer = str(target) 488 | src_peer = str(source_target or target) 489 | base_seq = random.randint(0, 65535) 490 | elems = [] 491 | for node in nodes: 492 | elems.append(await node.export(base_seq, self, int(src_peer))) 493 | base_seq += 1 494 | return await self.call_api( 495 | "send_fake_forward", 496 | chat_type=chat_type, 497 | target=peer, 498 | source_chat_type=source_chat_type or chat_type, 499 | source_target=src_peer, 500 | elements=elems, 501 | ) 502 | 503 | async def send_group_forward( 504 | self, 505 | nodes: List[ForwardNode], 506 | group: Union[int, str], 507 | source_group: Optional[Union[int, str]] = None, 508 | ): 509 | """发送群组合并转发消息 510 | 511 | 参数: 512 | nodes: 合并转发节点 513 | group: 群组 id 514 | source_group: 伪造的消息来源群组 id 515 | """ 516 | return await self.send_fake_forward( 517 | nodes, 518 | ChatType.GROUP, 519 | group, 520 | source_chat_type=ChatType.GROUP, 521 | source_target=source_group, 522 | ) 523 | -------------------------------------------------------------------------------- /nonebot/adapters/red/message.py: -------------------------------------------------------------------------------- 1 | import random 2 | from io import BytesIO 3 | from pathlib import Path 4 | from datetime import datetime 5 | from typing_extensions import override 6 | from dataclasses import field, dataclass 7 | from typing import TYPE_CHECKING, List, Type, Union, Iterable, Optional 8 | 9 | from nonebot.exception import NetworkError 10 | from nonebot.internal.driver import Request 11 | 12 | from nonebot.adapters import Message as BaseMessage 13 | from nonebot.adapters import MessageSegment as BaseMessageSegment 14 | 15 | from .utils import log 16 | from .api.model import Element, UploadResponse 17 | 18 | if TYPE_CHECKING: 19 | from .bot import Bot 20 | 21 | 22 | class MessageSegment(BaseMessageSegment["Message"]): 23 | @classmethod 24 | @override 25 | def get_message_class(cls) -> Type["Message"]: 26 | # 返回适配器的 Message 类型本身 27 | return Message 28 | 29 | @override 30 | def __str__(self) -> str: 31 | shown_data = {k: v for k, v in self.data.items() if not k.startswith("_")} 32 | # 返回该消息段的纯文本表现形式,通常在日志中展示 33 | return self.data["text"] if self.is_text() else f"[{self.type}: {shown_data}]" 34 | 35 | @override 36 | def is_text(self) -> bool: 37 | # 判断该消息段是否为纯文本 38 | return self.type == "text" 39 | 40 | @staticmethod 41 | def text(text: str) -> "MessageSegment": 42 | return MessageSegment("text", {"text": text}) 43 | 44 | @staticmethod 45 | def at(user_id: str, user_name: Optional[str] = None) -> "MessageSegment": 46 | return MessageSegment("at", {"user_id": user_id, "user_name": user_name}) 47 | 48 | @staticmethod 49 | def at_all() -> "MessageSegment": 50 | return MessageSegment("at_all") 51 | 52 | @staticmethod 53 | def image(file: Union[str, Path, BytesIO, bytes]) -> "MessageSegment": 54 | if isinstance(file, str): 55 | file = Path(file) 56 | if isinstance(file, Path): 57 | file = file.read_bytes() 58 | elif isinstance(file, BytesIO): 59 | file = file.getvalue() 60 | return MediaMessageSegment("image", {"file": file}) 61 | 62 | @staticmethod 63 | def file(file: Union[str, Path, BytesIO, bytes]) -> "MessageSegment": 64 | if isinstance(file, str): 65 | file = Path(file) 66 | if isinstance(file, Path): 67 | file = file.read_bytes() 68 | elif isinstance(file, BytesIO): 69 | file = file.getvalue() 70 | return MediaMessageSegment("file", {"file": file}) 71 | 72 | @staticmethod 73 | def voice( 74 | file: Union[str, Path, BytesIO, bytes], duration: int = 1 75 | ) -> "MessageSegment": 76 | if isinstance(file, str): 77 | file = Path(file) 78 | if isinstance(file, Path): 79 | file = file.read_bytes() 80 | elif isinstance(file, BytesIO): 81 | file = file.getvalue() 82 | return MediaMessageSegment("voice", {"file": file, "duration": duration}) 83 | 84 | @staticmethod 85 | def video(file: Union[str, Path, BytesIO, bytes]) -> "MessageSegment": 86 | if isinstance(file, str): 87 | file = Path(file) 88 | if isinstance(file, Path): 89 | file = file.read_bytes() 90 | elif isinstance(file, BytesIO): 91 | file = file.getvalue() 92 | return MediaMessageSegment("video", {"file": file}) 93 | 94 | @staticmethod 95 | def face(face_id: str) -> "MessageSegment": 96 | return MessageSegment("face", {"face_id": face_id}) 97 | 98 | @staticmethod 99 | def reply( 100 | message_seq: str, 101 | message_id: Optional[str] = None, 102 | sender_uin: Optional[str] = None, 103 | ) -> "MessageSegment": 104 | return MessageSegment( 105 | "reply", 106 | {"msg_id": message_id, "msg_seq": message_seq, "sender_uin": sender_uin}, 107 | ) 108 | 109 | @staticmethod 110 | def ark(data: str) -> "MessageSegment": 111 | return MessageSegment("ark", {"data": data}) 112 | 113 | @staticmethod 114 | def market_face( 115 | package_id: str, emoji_id: str, face_name: str, key: str, face_path: str 116 | ) -> "MessageSegment": 117 | log("WARNING", "market_face only can be received!") 118 | return MessageSegment( 119 | "market_face", 120 | { 121 | "package_id": package_id, 122 | "emoji_id": emoji_id, 123 | "face_name": face_name, 124 | "key": key, 125 | "face_path": face_path, 126 | }, 127 | ) 128 | 129 | @staticmethod 130 | def forward(nodes: List["ForwardNode"]) -> "MessageSegment": 131 | return MessageSegment( 132 | "forward", 133 | {"nodes": nodes}, 134 | ) 135 | 136 | 137 | class MediaMessageSegment(MessageSegment): 138 | async def download(self, bot: "Bot") -> bytes: 139 | path = Path(self.data["path"]) 140 | if path.exists(): 141 | with path.open("rb") as f: 142 | return f.read() 143 | resp = await bot.adapter.request( 144 | Request( 145 | "POST", 146 | bot.info.api_base / "message" / "fetchRichMedia", 147 | headers={"Authorization": f"Bearer {bot.info.token}"}, 148 | json={ 149 | "msgId": self.data["_msg_id"], 150 | "chatType": self.data["_chat_type"], 151 | "peerUid": self.data["_peer_uin"], 152 | "elementId": self.data["id"], 153 | "thumbSize": 0, 154 | "downloadType": 2, 155 | }, 156 | ) 157 | ) 158 | if resp.status_code == 200: 159 | return resp.content # type: ignore 160 | raise NetworkError("red", resp) 161 | 162 | async def upload(self, bot: "Bot") -> UploadResponse: 163 | data = self.data["file"] if self.data.get("file") else await self.download(bot) 164 | filename = f"{self.type}_{id(self)}" 165 | if self.type == "voice": 166 | filename += ".amr" 167 | resp = await bot.adapter.request( 168 | Request( 169 | "POST", 170 | bot.info.api_base / "upload", 171 | headers={ 172 | "Authorization": f"Bearer {bot.info.token}", 173 | }, 174 | files={f"file_{self.type}": (filename, data)}, 175 | ) 176 | ) 177 | return UploadResponse.parse_raw(resp.content) # type: ignore 178 | 179 | 180 | class Message(BaseMessage[MessageSegment]): 181 | @classmethod 182 | @override 183 | def get_segment_class(cls) -> Type[MessageSegment]: 184 | # 返回适配器的 MessageSegment 类型本身 185 | return MessageSegment 186 | 187 | @staticmethod 188 | @override 189 | def _construct(msg: str) -> Iterable[MessageSegment]: 190 | yield MessageSegment.text(msg) 191 | 192 | @classmethod 193 | def from_red_message( 194 | cls, message: List[Element], msg_id: str, chat_type: int, peer_uin: str 195 | ) -> "Message": 196 | msg = Message() 197 | for element in message: 198 | if element.elementType == 1: 199 | if TYPE_CHECKING: 200 | assert element.textElement 201 | text = element.textElement 202 | if not text.atType: 203 | msg.append(MessageSegment.text(text.content)) 204 | elif text.atType == 1: 205 | msg.append(MessageSegment.at_all()) 206 | elif text.atType == 2: 207 | # fmt: off 208 | msg.append(MessageSegment.at(text.atNtUin or text.atNtUid, text.content[1:])) # type: ignore # noqa: E501 209 | # fmt: on 210 | if element.elementType == 2: 211 | if TYPE_CHECKING: 212 | assert element.picElement 213 | pic = element.picElement 214 | msg.append( 215 | MediaMessageSegment( 216 | "image", 217 | { 218 | "md5": pic.md5HexStr, 219 | "size": pic.fileSize, 220 | "id": element.elementId, 221 | "uuid": pic.fileUuid, 222 | "path": pic.sourcePath, 223 | "width": pic.picWidth, 224 | "height": pic.picHeight, 225 | "_msg_id": msg_id, 226 | "_chat_type": chat_type, 227 | "_peer_uin": peer_uin, 228 | }, 229 | ) 230 | ) 231 | if element.elementType == 3: 232 | if TYPE_CHECKING: 233 | assert element.fileElement 234 | file = element.fileElement 235 | msg.append( 236 | MediaMessageSegment( 237 | "file", 238 | { 239 | "id": element.elementId, 240 | "md5": file.fileMd5, 241 | "name": file.fileName, 242 | "size": file.fileSize, 243 | "uuid": file.fileUuid, 244 | "_msg_id": msg_id, 245 | "_chat_type": chat_type, 246 | "_peer_uin": peer_uin, 247 | }, 248 | ) 249 | ) 250 | if element.elementType == 4: 251 | if TYPE_CHECKING: 252 | assert element.pttElement 253 | ptt = element.pttElement 254 | msg.append( 255 | MediaMessageSegment( 256 | "voice", 257 | { 258 | "id": element.elementId, 259 | "name": ptt.fileName, 260 | "path": ptt.filePath, 261 | "md5": ptt.md5HexStr, 262 | "type": ptt.voiceChangeType, 263 | "text": ptt.text, 264 | "duration": ptt.duration, 265 | "amplitudes": ptt.waveAmplitudes, 266 | "uuid": ptt.fileUuid, 267 | "_msg_id": msg_id, 268 | "_chat_type": chat_type, 269 | "_peer_uin": peer_uin, 270 | }, 271 | ) 272 | ) 273 | if element.elementType == 5: 274 | if TYPE_CHECKING: 275 | assert element.videoElement 276 | video = element.videoElement 277 | msg.append( 278 | MediaMessageSegment( 279 | "video", 280 | { 281 | "id": element.elementId, 282 | "path": video.filePath, 283 | "name": video.fileName, 284 | "md5": video.videoMd5, 285 | "format": video.fileFormat, 286 | "time": video.fileTime, 287 | "size": video.fileSize, 288 | "uuid": video.fileUuid, 289 | "thumb_md5": video.thumbMd5, 290 | "thumb_size": video.thumbSize, 291 | "thumb_width": video.thumbWidth, 292 | "thumb_height": video.thumbHeight, 293 | "thumb_path": video.thumbPath, 294 | "busiType": video.busiType, 295 | "subBusiType": video.subBusiType, 296 | "transferStatus": video.transferStatus, 297 | "progress": video.progress, 298 | "invalidState": video.invalidState, 299 | "fileSubId": video.fileSubId, 300 | "fileBizId": video.fileBizId, 301 | "_msg_id": msg_id, 302 | "_chat_type": chat_type, 303 | "_peer_uin": peer_uin, 304 | }, 305 | ) 306 | ) 307 | if element.elementType == 6: 308 | if TYPE_CHECKING: 309 | assert element.faceElement 310 | face = element.faceElement 311 | msg.append(MessageSegment.face(str(face.faceIndex))) 312 | if element.elementType == 7: 313 | if TYPE_CHECKING: 314 | assert element.replyElement 315 | reply = element.replyElement 316 | msg.append( 317 | MessageSegment( 318 | "reply", 319 | { 320 | "_origin": reply, 321 | "msg_id": reply.sourceMsgIdInRecords, 322 | "msg_seq": reply.replayMsgSeq, 323 | # reply 元素仍然只有 senderUid 324 | "sender_uin": reply.senderUid, 325 | }, 326 | ) 327 | ) 328 | if element.elementType == 10: 329 | if TYPE_CHECKING: 330 | assert element.arkElement 331 | ark = element.arkElement 332 | msg.append(MessageSegment.ark(ark.bytesData)) 333 | if element.elementType == 11: 334 | if TYPE_CHECKING: 335 | assert element.marketFaceElement 336 | market_face = element.marketFaceElement 337 | msg.append( 338 | MessageSegment( 339 | "market_face", 340 | { 341 | "package_id": market_face.emojiPackageId, 342 | "face_name": market_face.faceName, 343 | "emoji_id": market_face.emojiId, 344 | "key": market_face.key, 345 | "static_path": market_face.staticFacePath, 346 | "dynamic_path": market_face.dynamicFacePath, 347 | }, 348 | ) 349 | ) 350 | if element.elementType == 16: 351 | if TYPE_CHECKING: 352 | assert element.multiForwardMsgElement 353 | forward_msg = element.multiForwardMsgElement 354 | msg.append( 355 | MessageSegment( 356 | "forward", 357 | { 358 | "xml": forward_msg.xmlContent, 359 | "id": forward_msg.resId, 360 | "name": forward_msg.fileName, 361 | }, 362 | ) 363 | ) 364 | return msg 365 | 366 | async def export(self, bot: "Bot") -> List[dict]: 367 | res = [] 368 | for seg in self: 369 | if seg.type == "text": 370 | res.append( 371 | {"elementType": 1, "textElement": {"content": seg.data["text"]}} 372 | ) 373 | elif seg.type == "at": 374 | res.append( 375 | { 376 | "elementType": 1, 377 | "textElement": { 378 | "atType": 2, 379 | "atNtUin": seg.data["user_id"], 380 | "content": f"@{seg.data['user_name'] or seg.data['user_id']}", # noqa: E501 381 | }, 382 | } 383 | ) 384 | elif seg.type == "at_all": 385 | res.append({"elementType": 1, "textElement": {"atType": 1}}) 386 | elif seg.type == "image": 387 | if TYPE_CHECKING: 388 | assert isinstance(seg, MediaMessageSegment) 389 | resp = await seg.upload(bot) 390 | file = Path(resp.ntFilePath) 391 | res.append( 392 | { 393 | "elementType": 2, 394 | "picElement": { 395 | "original": True, 396 | "md5HexStr": resp.md5, 397 | "picWidth": resp.imageInfo and resp.imageInfo.width, 398 | "picHeight": resp.imageInfo and resp.imageInfo.height, 399 | "fileSize": resp.fileSize, 400 | "fileName": file.name, 401 | "sourcePath": resp.ntFilePath, 402 | }, 403 | } 404 | ) 405 | elif seg.type == "file": 406 | if TYPE_CHECKING: 407 | assert isinstance(seg, MediaMessageSegment) 408 | resp = await seg.upload(bot) 409 | file = Path(resp.ntFilePath) 410 | res.append( 411 | { 412 | "elementType": 3, 413 | "fileElement": { 414 | "fileMd5": resp.md5, 415 | "fileSize": resp.fileSize, 416 | "fileName": file.name, 417 | "filePath": resp.ntFilePath, 418 | "picHeight": 0, 419 | "picWidth": 0, 420 | "picThumbPath": {}, 421 | "file10MMd5": "", 422 | "fileSha": "", 423 | "fileSha3": "", 424 | "fileUuid": "", 425 | "fileSubId": "", 426 | "thumbFileSize": 750, 427 | }, 428 | } 429 | ) 430 | elif seg.type == "voice": 431 | if TYPE_CHECKING: 432 | assert isinstance(seg, MediaMessageSegment) 433 | resp = await seg.upload(bot) 434 | file = Path(resp.ntFilePath) 435 | res.append( 436 | { 437 | "elementType": 4, 438 | "pttElement": { 439 | "canConvert2Text": True, 440 | "md5HexStr": resp.md5, 441 | "fileSize": resp.fileSize, 442 | "fileName": file.name, 443 | "filePath": resp.ntFilePath, 444 | "duration": seg.data["duration"], 445 | "formatType": 1, 446 | "voiceType": 1, 447 | "voiceChangeType": 0, 448 | "playState": 1, 449 | "waveAmplitudes": seg.get( 450 | "amplitudes", [99 for _ in range(17)] 451 | ), 452 | }, 453 | } 454 | ) 455 | elif seg.type == "video": 456 | raise NotImplementedError( 457 | "Unsupported MessageSegment type: " f"{seg.type}" 458 | ) 459 | elif seg.type == "face": 460 | res.append( 461 | { 462 | "elementType": 6, 463 | "faceElement": {"faceIndex": seg.data["face_id"]}, 464 | } 465 | ) 466 | elif seg.type == "reply": 467 | res.append( 468 | { 469 | "elementType": 7, 470 | "replyElement": { 471 | "replayMsgId": seg.data["msg_id"], 472 | "replayMsgSeq": seg.data["msg_seq"], 473 | "senderUin": seg.data["sender_uin"], 474 | "senderUinStr": str(seg.data["sender_uin"]), 475 | }, 476 | } 477 | ) 478 | elif seg.type == "ark": 479 | res.append( 480 | {"elementType": 10, "arkElement": {"bytesData": seg.data["data"]}} 481 | ) 482 | elif seg.type == "market_face": 483 | raise NotImplementedError( 484 | "Unsupported MessageSegment type: " f"{seg.type}" 485 | ) 486 | elif seg.type == "forward_msg": 487 | raise NotImplementedError( 488 | "Unsupported MessageSegment type: " f"{seg.type}" 489 | ) 490 | return res 491 | 492 | 493 | @dataclass 494 | class ForwardNode: 495 | uin: str 496 | name: str 497 | message: Message 498 | group: Union[int, str, None] = None 499 | time: datetime = field(default_factory=datetime.now) 500 | 501 | async def export(self, seq: int, bot: "Bot", group: int) -> dict: 502 | elems = [] 503 | for seg in self.message: 504 | if seg.type == "text": 505 | elems.append({"text": {"str": seg.data["text"]}}) 506 | elif seg.type == "at": 507 | elems.append({"text": {"str": f"@{seg.data['user_id']}"}}) 508 | elif seg.type == "at_all": 509 | elems.append({"text": {"str": "@全体成员"}}) 510 | elif seg.type == "image": 511 | if TYPE_CHECKING: 512 | assert isinstance(seg, MediaMessageSegment) 513 | resp = await seg.upload(bot) 514 | md5 = resp.md5 515 | file = Path(resp.ntFilePath) 516 | pid = f"{{{md5[:8].upper()}-{md5[8:12].upper()}-{md5[12:16].upper()}-{md5[16:20].upper()}-{md5[20:].upper()}}}{file.suffix}" # noqa: E501 517 | elems.append( 518 | { 519 | "customFace": { 520 | "filePath": pid, 521 | "fileId": random.randint(0, 65535), 522 | "serverIp": -1740138629, 523 | "serverPort": 80, 524 | "fileType": 1001, 525 | "useful": 1, 526 | "md5": [int(md5[i : i + 2], 16) for i in range(0, 32, 2)], 527 | "imageType": 1001, 528 | "width": resp.imageInfo and resp.imageInfo.width, 529 | "height": resp.imageInfo and resp.imageInfo.height, 530 | "size": resp.fileSize, 531 | "origin": 0, 532 | "thumbWidth": 0, 533 | "thumbHeight": 0, 534 | "pbReserve": [2, 0], 535 | } 536 | } 537 | ) 538 | else: 539 | elems.append({"text": {"str": f"[{seg.type}]"}}) 540 | return { 541 | "head": { 542 | "field2": self.uin, 543 | "field8": { 544 | "field1": int(self.group) if self.group else group, 545 | "field4": self.name, 546 | }, 547 | }, 548 | "content": { 549 | "field1": 82, 550 | "field4": random.randint(0, 4294967295), 551 | "field5": seq, 552 | "field6": int(self.time.timestamp()), 553 | "field7": 1, 554 | "field8": 0, 555 | "field9": 0, 556 | "field15": {"field1": 0, "field2": 0}, 557 | }, 558 | "body": {"richText": {"elems": elems}}, 559 | } 560 | -------------------------------------------------------------------------------- /pdm.lock: -------------------------------------------------------------------------------- 1 | # This file is @generated by PDM. 2 | # It is not intended for manual editing. 3 | 4 | [metadata] 5 | groups = ["default", "auto_detect", "dev"] 6 | strategy = ["cross_platform"] 7 | lock_version = "4.4.1" 8 | content_hash = "sha256:b50add1943ac68339079b500890322c2ac5013f7d0542db080ac49049430e599" 9 | 10 | [[package]] 11 | name = "anyio" 12 | version = "4.0.0" 13 | requires_python = ">=3.8" 14 | summary = "High level compatibility layer for multiple asynchronous event loop implementations" 15 | dependencies = [ 16 | "exceptiongroup>=1.0.2; python_version < \"3.11\"", 17 | "idna>=2.8", 18 | "sniffio>=1.1", 19 | ] 20 | files = [ 21 | {file = "anyio-4.0.0-py3-none-any.whl", hash = "sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f"}, 22 | {file = "anyio-4.0.0.tar.gz", hash = "sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a"}, 23 | ] 24 | 25 | [[package]] 26 | name = "black" 27 | version = "24.1.1" 28 | requires_python = ">=3.8" 29 | summary = "The uncompromising code formatter." 30 | dependencies = [ 31 | "click>=8.0.0", 32 | "mypy-extensions>=0.4.3", 33 | "packaging>=22.0", 34 | "pathspec>=0.9.0", 35 | "platformdirs>=2", 36 | "tomli>=1.1.0; python_version < \"3.11\"", 37 | "typing-extensions>=4.0.1; python_version < \"3.11\"", 38 | ] 39 | files = [ 40 | {file = "black-24.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2588021038bd5ada078de606f2a804cadd0a3cc6a79cb3e9bb3a8bf581325a4c"}, 41 | {file = "black-24.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a95915c98d6e32ca43809d46d932e2abc5f1f7d582ffbe65a5b4d1588af7445"}, 42 | {file = "black-24.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa6a0e965779c8f2afb286f9ef798df770ba2b6cee063c650b96adec22c056a"}, 43 | {file = "black-24.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5242ecd9e990aeb995b6d03dc3b2d112d4a78f2083e5a8e86d566340ae80fec4"}, 44 | {file = "black-24.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fc1ec9aa6f4d98d022101e015261c056ddebe3da6a8ccfc2c792cbe0349d48b7"}, 45 | {file = "black-24.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0269dfdea12442022e88043d2910429bed717b2d04523867a85dacce535916b8"}, 46 | {file = "black-24.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3d64db762eae4a5ce04b6e3dd745dcca0fb9560eb931a5be97472e38652a161"}, 47 | {file = "black-24.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:5d7b06ea8816cbd4becfe5f70accae953c53c0e53aa98730ceccb0395520ee5d"}, 48 | {file = "black-24.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e2c8dfa14677f90d976f68e0c923947ae68fa3961d61ee30976c388adc0b02c8"}, 49 | {file = "black-24.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a21725862d0e855ae05da1dd25e3825ed712eaaccef6b03017fe0853a01aa45e"}, 50 | {file = "black-24.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07204d078e25327aad9ed2c64790d681238686bce254c910de640c7cc4fc3aa6"}, 51 | {file = "black-24.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:a83fe522d9698d8f9a101b860b1ee154c1d25f8a82ceb807d319f085b2627c5b"}, 52 | {file = "black-24.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:08b34e85170d368c37ca7bf81cf67ac863c9d1963b2c1780c39102187ec8dd62"}, 53 | {file = "black-24.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7258c27115c1e3b5de9ac6c4f9957e3ee2c02c0b39222a24dc7aa03ba0e986f5"}, 54 | {file = "black-24.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40657e1b78212d582a0edecafef133cf1dd02e6677f539b669db4746150d38f6"}, 55 | {file = "black-24.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e298d588744efda02379521a19639ebcd314fba7a49be22136204d7ed1782717"}, 56 | {file = "black-24.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:34afe9da5056aa123b8bfda1664bfe6fb4e9c6f311d8e4a6eb089da9a9173bf9"}, 57 | {file = "black-24.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:854c06fb86fd854140f37fb24dbf10621f5dab9e3b0c29a690ba595e3d543024"}, 58 | {file = "black-24.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3897ae5a21ca132efa219c029cce5e6bfc9c3d34ed7e892113d199c0b1b444a2"}, 59 | {file = "black-24.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:ecba2a15dfb2d97105be74bbfe5128bc5e9fa8477d8c46766505c1dda5883aac"}, 60 | {file = "black-24.1.1-py3-none-any.whl", hash = "sha256:5cdc2e2195212208fbcae579b931407c1fa9997584f0a415421748aeafff1168"}, 61 | {file = "black-24.1.1.tar.gz", hash = "sha256:48b5760dcbfe5cf97fd4fba23946681f3a81514c6ab8a45b50da67ac8fbc6c7b"}, 62 | ] 63 | 64 | [[package]] 65 | name = "certifi" 66 | version = "2023.7.22" 67 | requires_python = ">=3.6" 68 | summary = "Python package for providing Mozilla's CA Bundle." 69 | files = [ 70 | {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, 71 | {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, 72 | ] 73 | 74 | [[package]] 75 | name = "cfgv" 76 | version = "3.4.0" 77 | requires_python = ">=3.8" 78 | summary = "Validate configuration and produce human readable error messages." 79 | files = [ 80 | {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, 81 | {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, 82 | ] 83 | 84 | [[package]] 85 | name = "click" 86 | version = "8.1.7" 87 | requires_python = ">=3.7" 88 | summary = "Composable command line interface toolkit" 89 | dependencies = [ 90 | "colorama; platform_system == \"Windows\"", 91 | ] 92 | files = [ 93 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 94 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 95 | ] 96 | 97 | [[package]] 98 | name = "colorama" 99 | version = "0.4.6" 100 | requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 101 | summary = "Cross-platform colored terminal text." 102 | files = [ 103 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 104 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 105 | ] 106 | 107 | [[package]] 108 | name = "distlib" 109 | version = "0.3.7" 110 | summary = "Distribution utilities" 111 | files = [ 112 | {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, 113 | {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, 114 | ] 115 | 116 | [[package]] 117 | name = "exceptiongroup" 118 | version = "1.1.3" 119 | requires_python = ">=3.7" 120 | summary = "Backport of PEP 654 (exception groups)" 121 | files = [ 122 | {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, 123 | {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, 124 | ] 125 | 126 | [[package]] 127 | name = "filelock" 128 | version = "3.12.4" 129 | requires_python = ">=3.8" 130 | summary = "A platform independent file lock." 131 | files = [ 132 | {file = "filelock-3.12.4-py3-none-any.whl", hash = "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4"}, 133 | {file = "filelock-3.12.4.tar.gz", hash = "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd"}, 134 | ] 135 | 136 | [[package]] 137 | name = "h11" 138 | version = "0.14.0" 139 | requires_python = ">=3.7" 140 | summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 141 | files = [ 142 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 143 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 144 | ] 145 | 146 | [[package]] 147 | name = "h2" 148 | version = "4.1.0" 149 | requires_python = ">=3.6.1" 150 | summary = "HTTP/2 State-Machine based protocol implementation" 151 | dependencies = [ 152 | "hpack<5,>=4.0", 153 | "hyperframe<7,>=6.0", 154 | ] 155 | files = [ 156 | {file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"}, 157 | {file = "h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb"}, 158 | ] 159 | 160 | [[package]] 161 | name = "hpack" 162 | version = "4.0.0" 163 | requires_python = ">=3.6.1" 164 | summary = "Pure-Python HPACK header compression" 165 | files = [ 166 | {file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"}, 167 | {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"}, 168 | ] 169 | 170 | [[package]] 171 | name = "httpcore" 172 | version = "0.18.0" 173 | requires_python = ">=3.8" 174 | summary = "A minimal low-level HTTP client." 175 | dependencies = [ 176 | "anyio<5.0,>=3.0", 177 | "certifi", 178 | "h11<0.15,>=0.13", 179 | "sniffio==1.*", 180 | ] 181 | files = [ 182 | {file = "httpcore-0.18.0-py3-none-any.whl", hash = "sha256:adc5398ee0a476567bf87467063ee63584a8bce86078bf748e48754f60202ced"}, 183 | {file = "httpcore-0.18.0.tar.gz", hash = "sha256:13b5e5cd1dca1a6636a6aaea212b19f4f85cd88c366a2b82304181b769aab3c9"}, 184 | ] 185 | 186 | [[package]] 187 | name = "httpx" 188 | version = "0.25.0" 189 | requires_python = ">=3.8" 190 | summary = "The next generation HTTP client." 191 | dependencies = [ 192 | "certifi", 193 | "httpcore<0.19.0,>=0.18.0", 194 | "idna", 195 | "sniffio", 196 | ] 197 | files = [ 198 | {file = "httpx-0.25.0-py3-none-any.whl", hash = "sha256:181ea7f8ba3a82578be86ef4171554dd45fec26a02556a744db029a0a27b7100"}, 199 | {file = "httpx-0.25.0.tar.gz", hash = "sha256:47ecda285389cb32bb2691cc6e069e3ab0205956f681c5b2ad2325719751d875"}, 200 | ] 201 | 202 | [[package]] 203 | name = "httpx" 204 | version = "0.25.0" 205 | extras = ["http2"] 206 | requires_python = ">=3.8" 207 | summary = "The next generation HTTP client." 208 | dependencies = [ 209 | "h2<5,>=3", 210 | "httpx==0.25.0", 211 | ] 212 | files = [ 213 | {file = "httpx-0.25.0-py3-none-any.whl", hash = "sha256:181ea7f8ba3a82578be86ef4171554dd45fec26a02556a744db029a0a27b7100"}, 214 | {file = "httpx-0.25.0.tar.gz", hash = "sha256:47ecda285389cb32bb2691cc6e069e3ab0205956f681c5b2ad2325719751d875"}, 215 | ] 216 | 217 | [[package]] 218 | name = "hyperframe" 219 | version = "6.0.1" 220 | requires_python = ">=3.6.1" 221 | summary = "HTTP/2 framing layer for Python" 222 | files = [ 223 | {file = "hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15"}, 224 | {file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"}, 225 | ] 226 | 227 | [[package]] 228 | name = "identify" 229 | version = "2.5.29" 230 | requires_python = ">=3.8" 231 | summary = "File identification library for Python" 232 | files = [ 233 | {file = "identify-2.5.29-py2.py3-none-any.whl", hash = "sha256:24437fbf6f4d3fe6efd0eb9d67e24dd9106db99af5ceb27996a5f7895f24bf1b"}, 234 | {file = "identify-2.5.29.tar.gz", hash = "sha256:d43d52b86b15918c137e3a74fff5224f60385cd0e9c38e99d07c257f02f151a5"}, 235 | ] 236 | 237 | [[package]] 238 | name = "idna" 239 | version = "3.4" 240 | requires_python = ">=3.5" 241 | summary = "Internationalized Domain Names in Applications (IDNA)" 242 | files = [ 243 | {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, 244 | {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, 245 | ] 246 | 247 | [[package]] 248 | name = "isort" 249 | version = "5.13.2" 250 | requires_python = ">=3.8.0" 251 | summary = "A Python utility / library to sort Python imports." 252 | files = [ 253 | {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, 254 | {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, 255 | ] 256 | 257 | [[package]] 258 | name = "loguru" 259 | version = "0.7.2" 260 | requires_python = ">=3.5" 261 | summary = "Python logging made (stupidly) simple" 262 | dependencies = [ 263 | "colorama>=0.3.4; sys_platform == \"win32\"", 264 | "win32-setctime>=1.0.0; sys_platform == \"win32\"", 265 | ] 266 | files = [ 267 | {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"}, 268 | {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"}, 269 | ] 270 | 271 | [[package]] 272 | name = "multidict" 273 | version = "6.0.4" 274 | requires_python = ">=3.7" 275 | summary = "multidict implementation" 276 | files = [ 277 | {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, 278 | {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, 279 | {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, 280 | {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"}, 281 | {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"}, 282 | {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"}, 283 | {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"}, 284 | {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"}, 285 | {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"}, 286 | {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"}, 287 | {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"}, 288 | {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"}, 289 | {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"}, 290 | {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"}, 291 | {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"}, 292 | {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"}, 293 | {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"}, 294 | {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"}, 295 | {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"}, 296 | {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"}, 297 | {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"}, 298 | {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"}, 299 | {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"}, 300 | {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"}, 301 | {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"}, 302 | {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"}, 303 | {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"}, 304 | {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"}, 305 | {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"}, 306 | {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"}, 307 | {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"}, 308 | {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"}, 309 | {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"}, 310 | {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"}, 311 | {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"}, 312 | {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"}, 313 | {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"}, 314 | {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"}, 315 | {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"}, 316 | {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"}, 317 | {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"}, 318 | {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"}, 319 | {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"}, 320 | {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"}, 321 | {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"}, 322 | {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"}, 323 | {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"}, 324 | {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"}, 325 | {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"}, 326 | {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"}, 327 | {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"}, 328 | {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"}, 329 | {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"}, 330 | {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"}, 331 | {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"}, 332 | {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"}, 333 | {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"}, 334 | {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"}, 335 | {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"}, 336 | {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, 337 | {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, 338 | ] 339 | 340 | [[package]] 341 | name = "mypy-extensions" 342 | version = "1.0.0" 343 | requires_python = ">=3.5" 344 | summary = "Type system extensions for programs checked with the mypy type checker." 345 | files = [ 346 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 347 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 348 | ] 349 | 350 | [[package]] 351 | name = "nodeenv" 352 | version = "1.8.0" 353 | requires_python = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" 354 | summary = "Node.js virtual environment builder" 355 | dependencies = [ 356 | "setuptools", 357 | ] 358 | files = [ 359 | {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, 360 | {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, 361 | ] 362 | 363 | [[package]] 364 | name = "nonebot2" 365 | version = "2.2.0" 366 | requires_python = ">=3.8,<4.0" 367 | summary = "An asynchronous python bot framework." 368 | dependencies = [ 369 | "loguru<1.0.0,>=0.6.0", 370 | "pydantic!=2.5.0,!=2.5.1,<3.0.0,>=1.10.0", 371 | "pygtrie<3.0.0,>=2.4.1", 372 | "python-dotenv<2.0.0,>=0.21.0", 373 | "tomli<3.0.0,>=2.0.1; python_version < \"3.11\"", 374 | "typing-extensions<5.0.0,>=4.4.0", 375 | "yarl<2.0.0,>=1.7.2", 376 | ] 377 | files = [ 378 | {file = "nonebot2-2.2.0-py3-none-any.whl", hash = "sha256:447fa63d384414c0e610f4ce6d2b3999db81ac2becd8d86716c4117013dc032f"}, 379 | {file = "nonebot2-2.2.0.tar.gz", hash = "sha256:138800846fa3dc635bda9f2ddc589519ee8d9d3b401013fbb95e47676fc830fb"}, 380 | ] 381 | 382 | [[package]] 383 | name = "nonebot2" 384 | version = "2.2.0" 385 | extras = ["httpx", "websockets"] 386 | requires_python = ">=3.8,<4.0" 387 | summary = "An asynchronous python bot framework." 388 | dependencies = [ 389 | "httpx[http2]<1.0.0,>=0.20.0", 390 | "nonebot2==2.2.0", 391 | "websockets>=10.0", 392 | ] 393 | files = [ 394 | {file = "nonebot2-2.2.0-py3-none-any.whl", hash = "sha256:447fa63d384414c0e610f4ce6d2b3999db81ac2becd8d86716c4117013dc032f"}, 395 | {file = "nonebot2-2.2.0.tar.gz", hash = "sha256:138800846fa3dc635bda9f2ddc589519ee8d9d3b401013fbb95e47676fc830fb"}, 396 | ] 397 | 398 | [[package]] 399 | name = "packaging" 400 | version = "23.2" 401 | requires_python = ">=3.7" 402 | summary = "Core utilities for Python packages" 403 | files = [ 404 | {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, 405 | {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, 406 | ] 407 | 408 | [[package]] 409 | name = "pathspec" 410 | version = "0.11.2" 411 | requires_python = ">=3.7" 412 | summary = "Utility library for gitignore style pattern matching of file paths." 413 | files = [ 414 | {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, 415 | {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, 416 | ] 417 | 418 | [[package]] 419 | name = "platformdirs" 420 | version = "3.10.0" 421 | requires_python = ">=3.7" 422 | summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 423 | files = [ 424 | {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, 425 | {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, 426 | ] 427 | 428 | [[package]] 429 | name = "pre-commit" 430 | version = "3.5.0" 431 | requires_python = ">=3.8" 432 | summary = "A framework for managing and maintaining multi-language pre-commit hooks." 433 | dependencies = [ 434 | "cfgv>=2.0.0", 435 | "identify>=1.0.0", 436 | "nodeenv>=0.11.1", 437 | "pyyaml>=5.1", 438 | "virtualenv>=20.10.0", 439 | ] 440 | files = [ 441 | {file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"}, 442 | {file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"}, 443 | ] 444 | 445 | [[package]] 446 | name = "pydantic" 447 | version = "1.10.12" 448 | requires_python = ">=3.7" 449 | summary = "Data validation and settings management using python type hints" 450 | dependencies = [ 451 | "typing-extensions>=4.2.0", 452 | ] 453 | files = [ 454 | {file = "pydantic-1.10.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718"}, 455 | {file = "pydantic-1.10.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe"}, 456 | {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b"}, 457 | {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d"}, 458 | {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09"}, 459 | {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed"}, 460 | {file = "pydantic-1.10.12-cp310-cp310-win_amd64.whl", hash = "sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a"}, 461 | {file = "pydantic-1.10.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc"}, 462 | {file = "pydantic-1.10.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405"}, 463 | {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62"}, 464 | {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494"}, 465 | {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246"}, 466 | {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33"}, 467 | {file = "pydantic-1.10.12-cp311-cp311-win_amd64.whl", hash = "sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f"}, 468 | {file = "pydantic-1.10.12-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62"}, 469 | {file = "pydantic-1.10.12-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb"}, 470 | {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0"}, 471 | {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c"}, 472 | {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d"}, 473 | {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33"}, 474 | {file = "pydantic-1.10.12-cp38-cp38-win_amd64.whl", hash = "sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47"}, 475 | {file = "pydantic-1.10.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6"}, 476 | {file = "pydantic-1.10.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523"}, 477 | {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86"}, 478 | {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1"}, 479 | {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe"}, 480 | {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb"}, 481 | {file = "pydantic-1.10.12-cp39-cp39-win_amd64.whl", hash = "sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d"}, 482 | {file = "pydantic-1.10.12-py3-none-any.whl", hash = "sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942"}, 483 | {file = "pydantic-1.10.12.tar.gz", hash = "sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303"}, 484 | ] 485 | 486 | [[package]] 487 | name = "pygtrie" 488 | version = "2.5.0" 489 | summary = "A pure Python trie data structure implementation." 490 | files = [ 491 | {file = "pygtrie-2.5.0-py3-none-any.whl", hash = "sha256:8795cda8105493d5ae159a5bef313ff13156c5d4d72feddefacaad59f8c8ce16"}, 492 | {file = "pygtrie-2.5.0.tar.gz", hash = "sha256:203514ad826eb403dab1d2e2ddd034e0d1534bbe4dbe0213bb0593f66beba4e2"}, 493 | ] 494 | 495 | [[package]] 496 | name = "python-dotenv" 497 | version = "1.0.0" 498 | requires_python = ">=3.8" 499 | summary = "Read key-value pairs from a .env file and set them as environment variables" 500 | files = [ 501 | {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, 502 | {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, 503 | ] 504 | 505 | [[package]] 506 | name = "pyyaml" 507 | version = "6.0.1" 508 | requires_python = ">=3.6" 509 | summary = "YAML parser and emitter for Python" 510 | files = [ 511 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, 512 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, 513 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, 514 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, 515 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, 516 | {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, 517 | {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, 518 | {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, 519 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, 520 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, 521 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, 522 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, 523 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, 524 | {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, 525 | {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, 526 | {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, 527 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, 528 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, 529 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, 530 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, 531 | {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, 532 | {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, 533 | {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, 534 | {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, 535 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, 536 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, 537 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, 538 | {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, 539 | {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, 540 | {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, 541 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, 542 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, 543 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, 544 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, 545 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, 546 | {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, 547 | {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, 548 | {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, 549 | {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, 550 | ] 551 | 552 | [[package]] 553 | name = "ruff" 554 | version = "0.2.1" 555 | requires_python = ">=3.7" 556 | summary = "An extremely fast Python linter and code formatter, written in Rust." 557 | files = [ 558 | {file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:dd81b911d28925e7e8b323e8d06951554655021df8dd4ac3045d7212ac4ba080"}, 559 | {file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dc586724a95b7d980aa17f671e173df00f0a2eef23f8babbeee663229a938fec"}, 560 | {file = "ruff-0.2.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c92db7101ef5bfc18e96777ed7bc7c822d545fa5977e90a585accac43d22f18a"}, 561 | {file = "ruff-0.2.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13471684694d41ae0f1e8e3a7497e14cd57ccb7dd72ae08d56a159d6c9c3e30e"}, 562 | {file = "ruff-0.2.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a11567e20ea39d1f51aebd778685582d4c56ccb082c1161ffc10f79bebe6df35"}, 563 | {file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:00a818e2db63659570403e44383ab03c529c2b9678ba4ba6c105af7854008105"}, 564 | {file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be60592f9d218b52f03384d1325efa9d3b41e4c4d55ea022cd548547cc42cd2b"}, 565 | {file = "ruff-0.2.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbd2288890b88e8aab4499e55148805b58ec711053588cc2f0196a44f6e3d855"}, 566 | {file = "ruff-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3ef052283da7dec1987bba8d8733051c2325654641dfe5877a4022108098683"}, 567 | {file = "ruff-0.2.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7022d66366d6fded4ba3889f73cd791c2d5621b2ccf34befc752cb0df70f5fad"}, 568 | {file = "ruff-0.2.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0a725823cb2a3f08ee743a534cb6935727d9e47409e4ad72c10a3faf042ad5ba"}, 569 | {file = "ruff-0.2.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0034d5b6323e6e8fe91b2a1e55b02d92d0b582d2953a2b37a67a2d7dedbb7acc"}, 570 | {file = "ruff-0.2.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e5cb5526d69bb9143c2e4d2a115d08ffca3d8e0fddc84925a7b54931c96f5c02"}, 571 | {file = "ruff-0.2.1-py3-none-win32.whl", hash = "sha256:6b95ac9ce49b4fb390634d46d6ece32ace3acdd52814671ccaf20b7f60adb232"}, 572 | {file = "ruff-0.2.1-py3-none-win_amd64.whl", hash = "sha256:e3affdcbc2afb6f5bd0eb3130139ceedc5e3f28d206fe49f63073cb9e65988e0"}, 573 | {file = "ruff-0.2.1-py3-none-win_arm64.whl", hash = "sha256:efababa8e12330aa94a53e90a81eb6e2d55f348bc2e71adbf17d9cad23c03ee6"}, 574 | {file = "ruff-0.2.1.tar.gz", hash = "sha256:3b42b5d8677cd0c72b99fcaf068ffc62abb5a19e71b4a3b9cfa50658a0af02f1"}, 575 | ] 576 | 577 | [[package]] 578 | name = "setuptools" 579 | version = "68.2.2" 580 | requires_python = ">=3.8" 581 | summary = "Easily download, build, install, upgrade, and uninstall Python packages" 582 | files = [ 583 | {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, 584 | {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, 585 | ] 586 | 587 | [[package]] 588 | name = "sniffio" 589 | version = "1.3.0" 590 | requires_python = ">=3.7" 591 | summary = "Sniff out which async library your code is running under" 592 | files = [ 593 | {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, 594 | {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, 595 | ] 596 | 597 | [[package]] 598 | name = "tomli" 599 | version = "2.0.1" 600 | requires_python = ">=3.7" 601 | summary = "A lil' TOML parser" 602 | files = [ 603 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 604 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 605 | ] 606 | 607 | [[package]] 608 | name = "typing-extensions" 609 | version = "4.8.0" 610 | requires_python = ">=3.8" 611 | summary = "Backported and Experimental Type Hints for Python 3.8+" 612 | files = [ 613 | {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, 614 | {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, 615 | ] 616 | 617 | [[package]] 618 | name = "virtualenv" 619 | version = "20.24.5" 620 | requires_python = ">=3.7" 621 | summary = "Virtual Python Environment builder" 622 | dependencies = [ 623 | "distlib<1,>=0.3.7", 624 | "filelock<4,>=3.12.2", 625 | "platformdirs<4,>=3.9.1", 626 | ] 627 | files = [ 628 | {file = "virtualenv-20.24.5-py3-none-any.whl", hash = "sha256:b80039f280f4919c77b30f1c23294ae357c4c8701042086e3fc005963e4e537b"}, 629 | {file = "virtualenv-20.24.5.tar.gz", hash = "sha256:e8361967f6da6fbdf1426483bfe9fca8287c242ac0bc30429905721cefbff752"}, 630 | ] 631 | 632 | [[package]] 633 | name = "websockets" 634 | version = "11.0.3" 635 | requires_python = ">=3.7" 636 | summary = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" 637 | files = [ 638 | {file = "websockets-11.0.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3ccc8a0c387629aec40f2fc9fdcb4b9d5431954f934da3eaf16cdc94f67dbfac"}, 639 | {file = "websockets-11.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d67ac60a307f760c6e65dad586f556dde58e683fab03323221a4e530ead6f74d"}, 640 | {file = "websockets-11.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d27a4832cc1a0ee07cdcf2b0629a8a72db73f4cf6de6f0904f6661227f256f"}, 641 | {file = "websockets-11.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffd7dcaf744f25f82190856bc26ed81721508fc5cbf2a330751e135ff1283564"}, 642 | {file = "websockets-11.0.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7622a89d696fc87af8e8d280d9b421db5133ef5b29d3f7a1ce9f1a7bf7fcfa11"}, 643 | {file = "websockets-11.0.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bceab846bac555aff6427d060f2fcfff71042dba6f5fca7dc4f75cac815e57ca"}, 644 | {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:54c6e5b3d3a8936a4ab6870d46bdd6ec500ad62bde9e44462c32d18f1e9a8e54"}, 645 | {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:41f696ba95cd92dc047e46b41b26dd24518384749ed0d99bea0a941ca87404c4"}, 646 | {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:86d2a77fd490ae3ff6fae1c6ceaecad063d3cc2320b44377efdde79880e11526"}, 647 | {file = "websockets-11.0.3-cp310-cp310-win32.whl", hash = "sha256:2d903ad4419f5b472de90cd2d40384573b25da71e33519a67797de17ef849b69"}, 648 | {file = "websockets-11.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:1d2256283fa4b7f4c7d7d3e84dc2ece74d341bce57d5b9bf385df109c2a1a82f"}, 649 | {file = "websockets-11.0.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e848f46a58b9fcf3d06061d17be388caf70ea5b8cc3466251963c8345e13f7eb"}, 650 | {file = "websockets-11.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa5003845cdd21ac0dc6c9bf661c5beddd01116f6eb9eb3c8e272353d45b3288"}, 651 | {file = "websockets-11.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b58cbf0697721120866820b89f93659abc31c1e876bf20d0b3d03cef14faf84d"}, 652 | {file = "websockets-11.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:660e2d9068d2bedc0912af508f30bbeb505bbbf9774d98def45f68278cea20d3"}, 653 | {file = "websockets-11.0.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1f0524f203e3bd35149f12157438f406eff2e4fb30f71221c8a5eceb3617b6b"}, 654 | {file = "websockets-11.0.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:def07915168ac8f7853812cc593c71185a16216e9e4fa886358a17ed0fd9fcf6"}, 655 | {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b30c6590146e53149f04e85a6e4fcae068df4289e31e4aee1fdf56a0dead8f97"}, 656 | {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:619d9f06372b3a42bc29d0cd0354c9bb9fb39c2cbc1a9c5025b4538738dbffaf"}, 657 | {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:01f5567d9cf6f502d655151645d4e8b72b453413d3819d2b6f1185abc23e82dd"}, 658 | {file = "websockets-11.0.3-cp311-cp311-win32.whl", hash = "sha256:e1459677e5d12be8bbc7584c35b992eea142911a6236a3278b9b5ce3326f282c"}, 659 | {file = "websockets-11.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:e7837cb169eca3b3ae94cc5787c4fed99eef74c0ab9506756eea335e0d6f3ed8"}, 660 | {file = "websockets-11.0.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fb06eea71a00a7af0ae6aefbb932fb8a7df3cb390cc217d51a9ad7343de1b8d0"}, 661 | {file = "websockets-11.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8a34e13a62a59c871064dfd8ffb150867e54291e46d4a7cf11d02c94a5275bae"}, 662 | {file = "websockets-11.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4841ed00f1026dfbced6fca7d963c4e7043aa832648671b5138008dc5a8f6d99"}, 663 | {file = "websockets-11.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a073fc9ab1c8aff37c99f11f1641e16da517770e31a37265d2755282a5d28aa"}, 664 | {file = "websockets-11.0.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68b977f21ce443d6d378dbd5ca38621755f2063d6fdb3335bda981d552cfff86"}, 665 | {file = "websockets-11.0.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1a99a7a71631f0efe727c10edfba09ea6bee4166a6f9c19aafb6c0b5917d09c"}, 666 | {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bee9fcb41db2a23bed96c6b6ead6489702c12334ea20a297aa095ce6d31370d0"}, 667 | {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4b253869ea05a5a073ebfdcb5cb3b0266a57c3764cf6fe114e4cd90f4bfa5f5e"}, 668 | {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1553cb82942b2a74dd9b15a018dce645d4e68674de2ca31ff13ebc2d9f283788"}, 669 | {file = "websockets-11.0.3-cp38-cp38-win32.whl", hash = "sha256:f61bdb1df43dc9c131791fbc2355535f9024b9a04398d3bd0684fc16ab07df74"}, 670 | {file = "websockets-11.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:03aae4edc0b1c68498f41a6772d80ac7c1e33c06c6ffa2ac1c27a07653e79d6f"}, 671 | {file = "websockets-11.0.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:777354ee16f02f643a4c7f2b3eff8027a33c9861edc691a2003531f5da4f6bc8"}, 672 | {file = "websockets-11.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8c82f11964f010053e13daafdc7154ce7385ecc538989a354ccc7067fd7028fd"}, 673 | {file = "websockets-11.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3580dd9c1ad0701169e4d6fc41e878ffe05e6bdcaf3c412f9d559389d0c9e016"}, 674 | {file = "websockets-11.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f1a3f10f836fab6ca6efa97bb952300b20ae56b409414ca85bff2ad241d2a61"}, 675 | {file = "websockets-11.0.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df41b9bc27c2c25b486bae7cf42fccdc52ff181c8c387bfd026624a491c2671b"}, 676 | {file = "websockets-11.0.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:279e5de4671e79a9ac877427f4ac4ce93751b8823f276b681d04b2156713b9dd"}, 677 | {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1fdf26fa8a6a592f8f9235285b8affa72748dc12e964a5518c6c5e8f916716f7"}, 678 | {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:69269f3a0b472e91125b503d3c0b3566bda26da0a3261c49f0027eb6075086d1"}, 679 | {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:97b52894d948d2f6ea480171a27122d77af14ced35f62e5c892ca2fae9344311"}, 680 | {file = "websockets-11.0.3-cp39-cp39-win32.whl", hash = "sha256:c7f3cb904cce8e1be667c7e6fef4516b98d1a6a0635a58a57528d577ac18a128"}, 681 | {file = "websockets-11.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c792ea4eabc0159535608fc5658a74d1a81020eb35195dd63214dcf07556f67e"}, 682 | {file = "websockets-11.0.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f2e58f2c36cc52d41f2659e4c0cbf7353e28c8c9e63e30d8c6d3494dc9fdedcf"}, 683 | {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de36fe9c02995c7e6ae6efe2e205816f5f00c22fd1fbf343d4d18c3d5ceac2f5"}, 684 | {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ac56b661e60edd453585f4bd68eb6a29ae25b5184fd5ba51e97652580458998"}, 685 | {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e052b8467dd07d4943936009f46ae5ce7b908ddcac3fda581656b1b19c083d9b"}, 686 | {file = "websockets-11.0.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:42cc5452a54a8e46a032521d7365da775823e21bfba2895fb7b77633cce031bb"}, 687 | {file = "websockets-11.0.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e6316827e3e79b7b8e7d8e3b08f4e331af91a48e794d5d8b099928b6f0b85f20"}, 688 | {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8531fdcad636d82c517b26a448dcfe62f720e1922b33c81ce695d0edb91eb931"}, 689 | {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c114e8da9b475739dde229fd3bc6b05a6537a88a578358bc8eb29b4030fac9c9"}, 690 | {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e063b1865974611313a3849d43f2c3f5368093691349cf3c7c8f8f75ad7cb280"}, 691 | {file = "websockets-11.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:92b2065d642bf8c0a82d59e59053dd2fdde64d4ed44efe4870fa816c1232647b"}, 692 | {file = "websockets-11.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0ee68fe502f9031f19d495dae2c268830df2760c0524cbac5d759921ba8c8e82"}, 693 | {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcacf2c7a6c3a84e720d1bb2b543c675bf6c40e460300b628bab1b1efc7c034c"}, 694 | {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b67c6f5e5a401fc56394f191f00f9b3811fe843ee93f4a70df3c389d1adf857d"}, 695 | {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d5023a4b6a5b183dc838808087033ec5df77580485fc533e7dab2567851b0a4"}, 696 | {file = "websockets-11.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ed058398f55163a79bb9f06a90ef9ccc063b204bb346c4de78efc5d15abfe602"}, 697 | {file = "websockets-11.0.3-py3-none-any.whl", hash = "sha256:6681ba9e7f8f3b19440921e99efbb40fc89f26cd71bf539e45d8c8a25c976dc6"}, 698 | {file = "websockets-11.0.3.tar.gz", hash = "sha256:88fc51d9a26b10fc331be344f1781224a375b78488fc343620184e95a4b27016"}, 699 | ] 700 | 701 | [[package]] 702 | name = "win32-setctime" 703 | version = "1.1.0" 704 | requires_python = ">=3.5" 705 | summary = "A small Python utility to set file creation time on Windows" 706 | files = [ 707 | {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, 708 | {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, 709 | ] 710 | 711 | [[package]] 712 | name = "yarl" 713 | version = "1.9.2" 714 | requires_python = ">=3.7" 715 | summary = "Yet another URL library" 716 | dependencies = [ 717 | "idna>=2.0", 718 | "multidict>=4.0", 719 | ] 720 | files = [ 721 | {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82"}, 722 | {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8"}, 723 | {file = "yarl-1.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9"}, 724 | {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560"}, 725 | {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bf345c3a4f5ba7f766430f97f9cc1320786f19584acc7086491f45524a551ac"}, 726 | {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a96c19c52ff442a808c105901d0bdfd2e28575b3d5f82e2f5fd67e20dc5f4ea"}, 727 | {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:891c0e3ec5ec881541f6c5113d8df0315ce5440e244a716b95f2525b7b9f3608"}, 728 | {file = "yarl-1.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c3a53ba34a636a256d767c086ceb111358876e1fb6b50dfc4d3f4951d40133d5"}, 729 | {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:566185e8ebc0898b11f8026447eacd02e46226716229cea8db37496c8cdd26e0"}, 730 | {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2b0738fb871812722a0ac2154be1f049c6223b9f6f22eec352996b69775b36d4"}, 731 | {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:32f1d071b3f362c80f1a7d322bfd7b2d11e33d2adf395cc1dd4df36c9c243095"}, 732 | {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3"}, 733 | {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:56ff08ab5df8429901ebdc5d15941b59f6253393cb5da07b4170beefcf1b2528"}, 734 | {file = "yarl-1.9.2-cp310-cp310-win32.whl", hash = "sha256:8ea48e0a2f931064469bdabca50c2f578b565fc446f302a79ba6cc0ee7f384d3"}, 735 | {file = "yarl-1.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:50f33040f3836e912ed16d212f6cc1efb3231a8a60526a407aeb66c1c1956dde"}, 736 | {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:646d663eb2232d7909e6601f1a9107e66f9791f290a1b3dc7057818fe44fc2b6"}, 737 | {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aff634b15beff8902d1f918012fc2a42e0dbae6f469fce134c8a0dc51ca423bb"}, 738 | {file = "yarl-1.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a83503934c6273806aed765035716216cc9ab4e0364f7f066227e1aaea90b8d0"}, 739 | {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b25322201585c69abc7b0e89e72790469f7dad90d26754717f3310bfe30331c2"}, 740 | {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22a94666751778629f1ec4280b08eb11815783c63f52092a5953faf73be24191"}, 741 | {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ec53a0ea2a80c5cd1ab397925f94bff59222aa3cf9c6da938ce05c9ec20428d"}, 742 | {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:159d81f22d7a43e6eabc36d7194cb53f2f15f498dbbfa8edc8a3239350f59fe7"}, 743 | {file = "yarl-1.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:832b7e711027c114d79dffb92576acd1bd2decc467dec60e1cac96912602d0e6"}, 744 | {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:95d2ecefbcf4e744ea952d073c6922e72ee650ffc79028eb1e320e732898d7e8"}, 745 | {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9"}, 746 | {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:783185c75c12a017cc345015ea359cc801c3b29a2966c2655cd12b233bf5a2be"}, 747 | {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:b8cc1863402472f16c600e3e93d542b7e7542a540f95c30afd472e8e549fc3f7"}, 748 | {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:822b30a0f22e588b32d3120f6d41e4ed021806418b4c9f0bc3048b8c8cb3f92a"}, 749 | {file = "yarl-1.9.2-cp311-cp311-win32.whl", hash = "sha256:a60347f234c2212a9f0361955007fcf4033a75bf600a33c88a0a8e91af77c0e8"}, 750 | {file = "yarl-1.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:be6b3fdec5c62f2a67cb3f8c6dbf56bbf3f61c0f046f84645cd1ca73532ea051"}, 751 | {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5610f80cf43b6202e2c33ba3ec2ee0a2884f8f423c8f4f62906731d876ef4fac"}, 752 | {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b9a4e67ad7b646cd6f0938c7ebfd60e481b7410f574c560e455e938d2da8e0f4"}, 753 | {file = "yarl-1.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:83fcc480d7549ccebe9415d96d9263e2d4226798c37ebd18c930fce43dfb9574"}, 754 | {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fcd436ea16fee7d4207c045b1e340020e58a2597301cfbcfdbe5abd2356c2fb"}, 755 | {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84e0b1599334b1e1478db01b756e55937d4614f8654311eb26012091be109d59"}, 756 | {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3458a24e4ea3fd8930e934c129b676c27452e4ebda80fbe47b56d8c6c7a63a9e"}, 757 | {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:838162460b3a08987546e881a2bfa573960bb559dfa739e7800ceeec92e64417"}, 758 | {file = "yarl-1.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4e2d08f07a3d7d3e12549052eb5ad3eab1c349c53ac51c209a0e5991bbada78"}, 759 | {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:de119f56f3c5f0e2fb4dee508531a32b069a5f2c6e827b272d1e0ff5ac040333"}, 760 | {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:149ddea5abf329752ea5051b61bd6c1d979e13fbf122d3a1f9f0c8be6cb6f63c"}, 761 | {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:674ca19cbee4a82c9f54e0d1eee28116e63bc6fd1e96c43031d11cbab8b2afd5"}, 762 | {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:9b3152f2f5677b997ae6c804b73da05a39daa6a9e85a512e0e6823d81cdad7cc"}, 763 | {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5415d5a4b080dc9612b1b63cba008db84e908b95848369aa1da3686ae27b6d2b"}, 764 | {file = "yarl-1.9.2-cp38-cp38-win32.whl", hash = "sha256:f7a3d8146575e08c29ed1cd287068e6d02f1c7bdff8970db96683b9591b86ee7"}, 765 | {file = "yarl-1.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:63c48f6cef34e6319a74c727376e95626f84ea091f92c0250a98e53e62c77c72"}, 766 | {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75df5ef94c3fdc393c6b19d80e6ef1ecc9ae2f4263c09cacb178d871c02a5ba9"}, 767 | {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c027a6e96ef77d401d8d5a5c8d6bc478e8042f1e448272e8d9752cb0aff8b5c8"}, 768 | {file = "yarl-1.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3b078dbe227f79be488ffcfc7a9edb3409d018e0952cf13f15fd6512847f3f7"}, 769 | {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59723a029760079b7d991a401386390c4be5bfec1e7dd83e25a6a0881859e716"}, 770 | {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b03917871bf859a81ccb180c9a2e6c1e04d2f6a51d953e6a5cdd70c93d4e5a2a"}, 771 | {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1012fa63eb6c032f3ce5d2171c267992ae0c00b9e164efe4d73db818465fac3"}, 772 | {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a74dcbfe780e62f4b5a062714576f16c2f3493a0394e555ab141bf0d746bb955"}, 773 | {file = "yarl-1.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c56986609b057b4839968ba901944af91b8e92f1725d1a2d77cbac6972b9ed1"}, 774 | {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2c315df3293cd521033533d242d15eab26583360b58f7ee5d9565f15fee1bef4"}, 775 | {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b7232f8dfbd225d57340e441d8caf8652a6acd06b389ea2d3222b8bc89cbfca6"}, 776 | {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:53338749febd28935d55b41bf0bcc79d634881195a39f6b2f767870b72514caf"}, 777 | {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:066c163aec9d3d073dc9ffe5dd3ad05069bcb03fcaab8d221290ba99f9f69ee3"}, 778 | {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8288d7cd28f8119b07dd49b7230d6b4562f9b61ee9a4ab02221060d21136be80"}, 779 | {file = "yarl-1.9.2-cp39-cp39-win32.whl", hash = "sha256:b124e2a6d223b65ba8768d5706d103280914d61f5cae3afbc50fc3dfcc016623"}, 780 | {file = "yarl-1.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18"}, 781 | {file = "yarl-1.9.2.tar.gz", hash = "sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571"}, 782 | ] 783 | --------------------------------------------------------------------------------