├── 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 |
--------------------------------------------------------------------------------