├── requirements.txt
├── .gitignore
├── nakuru
├── logger.py
├── __init__.py
├── examples
│ ├── src
│ │ └── 1.jpg
│ └── forward_message.py
├── entities
│ ├── device.py
│ ├── __init__.py
│ ├── friend.py
│ ├── file.py
│ ├── group.py
│ ├── guild.py
│ ├── others.py
│ └── components.py
├── event
│ ├── __init__.py
│ ├── builtins.py
│ ├── enums.py
│ └── models.py
├── network.py
├── misc.py
├── application.py
└── protocol.py
├── logo.png
├── setup.py
├── pyproject.toml
├── LICENSE
├── .github
└── workflows
│ └── python-publish.yml
└── README.md
/requirements.txt:
--------------------------------------------------------------------------------
1 | pydantic==1.10.4
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | __pycache__
3 | dist/
--------------------------------------------------------------------------------
/nakuru/logger.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | logger = logging.getLogger("nakuru")
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/nakuru-project/HEAD/logo.png
--------------------------------------------------------------------------------
/nakuru/__init__.py:
--------------------------------------------------------------------------------
1 | from .application import CQHTTP
2 | from .event.models import *
--------------------------------------------------------------------------------
/nakuru/examples/src/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lxns-Network/nakuru-project/HEAD/nakuru/examples/src/1.jpg
--------------------------------------------------------------------------------
/nakuru/entities/device.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 |
3 | class Device(BaseModel):
4 | app_id: int
5 | device_name: str
6 | device_kind: str
--------------------------------------------------------------------------------
/nakuru/entities/__init__.py:
--------------------------------------------------------------------------------
1 | from .friend import *
2 | from .group import *
3 | from .guild import *
4 | from .file import *
5 | from .device import *
6 | from .others import *
--------------------------------------------------------------------------------
/nakuru/entities/friend.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 | import typing as T
3 |
4 | class Friend(BaseModel):
5 | user_id: int
6 | nickname: str
7 | sex: T.Optional[str]
8 | age: T.Optional[int]
9 | source: T.Optional[str]
--------------------------------------------------------------------------------
/nakuru/event/__init__.py:
--------------------------------------------------------------------------------
1 | from collections import namedtuple
2 | from pydantic import BaseModel
3 |
4 | InternalEvent = namedtuple("Event", ("name", "body"))
5 |
6 | from .enums import ExternalEventTypes
7 |
8 | class ExternalEvent(BaseModel):
9 | type: ExternalEventTypes
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 | setup(
4 | name="nakuru-project",
5 | version="1.0.1",
6 | author="Lxns-Network",
7 | author_email="joinchang1206@gmail.com",
8 | description="一款为 go-cqhttp 的正向 WebSocket 设计的 Python SDK,支持纯 CQ 码与消息链的转换处理",
9 | url="https://github.com/Lxns-Network/nakuru-project",
10 | packages=find_packages(include=("nakuru", "nakuru.*")),
11 | install_requires=[
12 | "aiohttp",
13 | "pydantic",
14 | "Logbook",
15 | "async_lru"
16 | ]
17 | )
18 |
--------------------------------------------------------------------------------
/nakuru/event/builtins.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 | import typing as T
3 |
4 | class Depend:
5 | def __init__(self, func, middlewares=[], cache=True):
6 | self.func = func
7 | self.middlewares = middlewares
8 | self.cache = cache
9 |
10 | class ExecutorProtocol(BaseModel):
11 | callable: T.Callable
12 | dependencies: T.List[Depend]
13 | middlewares: T.List
14 |
15 | class Config:
16 | arbitrary_types_allowed = True
17 |
18 | from . import InternalEvent
19 | from pydantic import BaseModel
20 |
21 | class UnexpectedException(BaseModel):
22 | error: Exception
23 | event: InternalEvent
24 |
25 | class Config:
26 | arbitrary_types_allowed = True
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling"]
3 | build-backend = "hatchling.build"
4 |
5 | [project]
6 | name = "nakuru-project"
7 | version = "1.0.1"
8 | authors = [
9 | { name="Lxns-Network", email="joinchang1206@gmail.com" },
10 | ]
11 | description = "一款为 go-cqhttp 的正向 WebSocket 设计的 Python SDK,支持纯 CQ 码与消息链的转换处理"
12 | readme = "README.md"
13 | requires-python = ">=3.7"
14 | classifiers = [
15 | "Programming Language :: Python :: 3",
16 | "License :: OSI Approved :: MIT License",
17 | "Operating System :: OS Independent",
18 | ]
19 | dependencies = [
20 | "aiohttp",
21 | "pydantic",
22 | "Logbook",
23 | "async_lru"
24 | ]
25 |
26 | [project.urls]
27 | "Homepage" = "https://github.com/Lxns-Network/nakuru-project"
28 | "Bug Tracker" = "https://github.com/Lxns-Network/nakuru-project/issues"
29 |
--------------------------------------------------------------------------------
/nakuru/entities/file.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 | import typing as T
3 |
4 | class File(BaseModel):
5 | id: str
6 | name: str
7 | size: int
8 | busid: int
9 |
10 | class OfflineFile(BaseModel):
11 | name: str
12 | size: int
13 | url: str
14 |
15 | class ImageFile(BaseModel):
16 | size: int
17 | filename: str
18 | url: str
19 |
20 | class GroupFileSystem(BaseModel):
21 | file_count: int
22 | limit_count: int
23 | used_space: int
24 | total_space: int
25 |
26 | class GroupFile(BaseModel):
27 | file_id: str
28 | file_name: str
29 | busid: int
30 | file_size: int
31 | upload_time: int
32 | dead_time: int
33 | modify_time: int
34 | download_times: int
35 | uploader: int
36 | uploader_name: str
37 |
38 | class GroupFolder(BaseModel):
39 | folder_id: str
40 | folder_name: str
41 | create_time: int
42 | creator: int
43 | creator_name: str
44 | total_file_count: int
45 |
46 | class GroupFileTree(BaseModel):
47 | files: T.List[GroupFile]
48 | folders: T.List[GroupFolder]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Lxns-Network
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.
--------------------------------------------------------------------------------
/.github/workflows/python-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will upload a Python Package using Twine when a release is created
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
3 |
4 | # This workflow uses actions that are not certified by GitHub.
5 | # They are provided by a third-party and are governed by
6 | # separate terms of service, privacy policy, and support
7 | # documentation.
8 |
9 | name: Upload Python Package
10 |
11 | on:
12 | release:
13 | types: [published]
14 |
15 | permissions:
16 | contents: read
17 |
18 | jobs:
19 | deploy:
20 |
21 | runs-on: ubuntu-latest
22 |
23 | steps:
24 | - uses: actions/checkout@v3
25 | - name: Set up Python
26 | uses: actions/setup-python@v3
27 | with:
28 | python-version: '3.x'
29 | - name: Install dependencies
30 | run: |
31 | python -m pip install --upgrade pip
32 | pip install build
33 | - name: Build package
34 | run: python -m build
35 | - name: Publish package
36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
37 | with:
38 | user: __token__
39 | password: ${{ secrets.PYPI_API_TOKEN }}
40 |
--------------------------------------------------------------------------------
/nakuru/network.py:
--------------------------------------------------------------------------------
1 | import json
2 | import mimetypes
3 | import typing as T
4 | from pathlib import Path
5 | from .logger import logger
6 |
7 | import aiohttp
8 |
9 | class fetch:
10 | @staticmethod
11 | async def http_post(url, data_map=None, **kwargs):
12 | async with aiohttp.ClientSession() as session:
13 | async with session.post(url, json=data_map, **kwargs) as response:
14 | data = await response.text(encoding="utf-8")
15 | logger.debug(f"Network: requested url={url}, by data_map={data_map}, and status={response.status}, data={data}")
16 | response.raise_for_status()
17 | try:
18 | return json.loads(data)
19 | except json.decoder.JSONDecodeError:
20 | logger.error(f"Network: requested {url} with {data_map}, responsed {data}, decode failed...")
21 |
22 | @staticmethod
23 | async def http_get(url, params=None, **kwargs):
24 | async with aiohttp.ClientSession() as session:
25 | async with session.get(url, params=params, **kwargs) as response:
26 | response.raise_for_status()
27 | data = await response.text(encoding="utf-8")
28 | logger.debug(f"Network: requested url={url}, by params={params}, and status={response.status}, data={data}")
29 | try:
30 | return json.loads(data)
31 | except json.decoder.JSONDecodeError:
32 | logger.error(f"Network: requested {url} with {params}, responsed {data}, decode failed...")
33 |
--------------------------------------------------------------------------------
/nakuru/entities/group.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 | from typing import Optional
3 | import typing as T
4 |
5 | class Member(BaseModel):
6 | user_id: int
7 | nickname: str
8 | card: T.Optional[str]
9 | sex: str
10 | age: int
11 | area: str
12 | level: str
13 | role: T.Optional[str]
14 | title: T.Optional[str]
15 | # 以下是 getGroupMemberInfo 返回的更多结果
16 | group_id: Optional[int]
17 | join_time: Optional[int]
18 | last_sent_time: Optional[int]
19 | unfriendly: Optional[bool]
20 | title_expire_time: Optional[int]
21 | card_changeable: Optional[bool]
22 | shut_up_timestamp: Optional[int]
23 |
24 | class Anonymous(BaseModel):
25 | id: int
26 | name: str
27 | flag: str
28 |
29 | class Group(BaseModel):
30 | group_id: int
31 | group_name: str
32 | group_memo: str
33 | group_create_time: int
34 | group_level: int
35 | member_count: int
36 | max_member_count: int
37 |
38 | class HonorListNode(BaseModel):
39 | user_id: int
40 | nickname: str
41 | avatar: str
42 | description: Optional[str]
43 | day_count: Optional[int]
44 |
45 | class Honor(BaseModel):
46 | group_id: int
47 | current_talkative: Optional[HonorListNode]
48 | talkative_list: Optional[T.List[HonorListNode]]
49 | performer_list: Optional[T.List[HonorListNode]]
50 | legend_list: Optional[T.List[HonorListNode]]
51 | strong_newbie_list: Optional[T.List[HonorListNode]]
52 | emotion_list: Optional[T.List[HonorListNode]]
53 |
54 | class AtAllRemain(BaseModel):
55 | can_at_all: bool
56 | remain_at_all_count_for_group: int
57 | remain_at_all_count_for_uin: int
--------------------------------------------------------------------------------
/nakuru/examples/forward_message.py:
--------------------------------------------------------------------------------
1 | from nakuru import (
2 | CQHTTP,
3 | GroupMessage
4 | )
5 | from nakuru.entities.components import Plain, Node, Image
6 |
7 | app = CQHTTP(
8 | host="127.0.0.1",
9 | port=6700,
10 | http_port=5700
11 | )
12 |
13 | @app.receiver("GroupMessage")
14 | async def _(app: CQHTTP, source: GroupMessage):
15 | # 方法 1
16 | await app.sendGroupForwardMessage(source.group_id, [
17 | Node(name="落雪ちゃん", uin=2941383730, content=[
18 | Plain(text="nc什么时候cos小老师")
19 | ]),
20 | Node(name="盐焗雾喵", uin=2190945952, content=[
21 | Plain(text="今晚就cos小老师")
22 | ]),
23 | Node(name="Rosemoe♪ ~ requiem ~", uin=2073412493, content=[
24 | Plain(text="好耶"),
25 | Image.fromFileSystem("./src/1.jpg")
26 | ])
27 | ])
28 | # 方法 2
29 | await app.sendGroupForwardMessage(source.group_id, [
30 | {
31 | "type": "node",
32 | "data": {
33 | "name": "落雪ちゃん",
34 | "uin": 2941383730,
35 | "content": "nc什么时候cos小老师"
36 | }
37 | },
38 | {
39 | "type": "node",
40 | "data": {
41 | "name": "盐焗雾喵",
42 | "uin": 2190945952,
43 | "content": "今晚就cos小老师"
44 | }
45 | },
46 | {
47 | "type": "node",
48 | "data": {
49 | "name": "Rosemoe♪ ~ requiem ~",
50 | "uin": 2073412493,
51 | "content": [
52 | {
53 | "type": "text",
54 | "data": {"text": "好耶"}
55 | },
56 | {
57 | "type": "image",
58 | "data": {"file": "file:///D:/src/1.jpg"} # 此处需要绝对路径
59 | }
60 | ]
61 | }
62 | }
63 | ])
64 |
65 | app.run()
66 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 | # Nakuru Project
5 | 一款为 [go-cqhttp](https://github.com/Mrs4s/go-cqhttp) 的正向 WebSocket 设计的 Python SDK,支持纯 CQ 码与消息链的转换处理
6 |
7 | 在 [kuriyama](https://github.com/Lxns-Network/mirai-python-sdk) 的基础上改动
8 |
9 | 项目名来源于藍月なくる,图标由[せら](https://www.pixiv.net/users/577968)绘制
10 |
11 |
12 | ## 食用方法
13 | 使用 `pip install nakuru-project` 安装。
14 |
15 | 需要将 go-cqhttp 的正向 WebSocket 与 HTTP 配置项开启。
16 |
17 | ## 示例
18 | 没有文档,源码就是文档。
19 |
20 | ```python
21 | from nakuru import (
22 | CQHTTP,
23 | GroupMessage,
24 | Notify,
25 | GroupMessageRecall,
26 | FriendRequest
27 | )
28 | from nakuru.entities.components import Plain, Image
29 |
30 | app = CQHTTP(
31 | host="127.0.0.1",
32 | port=6700,
33 | http_port=5700,
34 | token="TOKEN" # 可选,如果配置了 Access-Token
35 | )
36 |
37 | @app.receiver("GroupMessage")
38 | async def _(app: CQHTTP, source: GroupMessage):
39 | # 通过纯 CQ 码处理
40 | if source.raw_message == "戳我":
41 | await app.sendGroupMessage(source.group_id, f"[CQ:poke,qq={source.user_id}]")
42 | # 通过消息链处理
43 | chain = source.message
44 | if isinstance(chain[0], Plain):
45 | if chain[0].text == "看":
46 | await app.sendGroupMessage(source.group_id, [
47 | Plain(text="给你看"),
48 | Image.fromFileSystem("D:/好康的.jpg")
49 | ])
50 |
51 | @app.receiver("GroupMessageRecall")
52 | async def _(app: CQHTTP, source: GroupMessageRecall):
53 | await app.sendGroupMessage(source.group_id, "你撤回了一条消息")
54 |
55 | @app.receiver("Notify")
56 | async def _(app: CQHTTP, source: Notify):
57 | if source.sub_type == "poke" and source.target_id == 114514:
58 | await app.sendGroupMessage(source.group_id, "不许戳我")
59 |
60 | @app.receiver("FriendRequest")
61 | async def _(app: CQHTTP, source: FriendRequest):
62 | await app.setFriendRequest(source.flag, True)
63 |
64 | app.run()
65 | ```
66 |
67 | ## 贡献
68 | 欢迎 PR 代码或提交 Issue,项目现在还存在着许多问题。
69 |
--------------------------------------------------------------------------------
/nakuru/event/enums.py:
--------------------------------------------------------------------------------
1 | from .models import *
2 | from enum import Enum
3 |
4 | class ExternalEvents(Enum):
5 | AppInitEvent = AppInitEvent
6 |
7 | GroupFileUpload = GroupFileUpload
8 | GroupAdminChange = GroupAdminChange
9 | GroupMemberDecrease = GroupMemberDecrease
10 | GroupMemberIncrease = GroupMemberIncrease
11 | GroupMemberBan = GroupMemberBan
12 | FriendAdd = FriendAdd
13 | GroupMessageRecall = GroupMessageRecall
14 | FriendMessageRecall = FriendMessageRecall
15 | Notify = Notify
16 | GroupCardChange = GroupCardChange
17 | FriendOfflineFile = FriendOfflineFile
18 | ClientStatusChange = ClientStatusChange
19 | EssenceMessageChange = EssenceMessageChange
20 |
21 | MessageReactionsUpdated = MessageReactionsUpdated
22 | ChannelUpdated = ChannelUpdated
23 | ChannelCreated = ChannelCreated
24 | ChannelDestroyed = ChannelDestroyed
25 | GuildChannelRecall = GuildChannelRecall
26 |
27 | FriendRequest = FriendRequest
28 | GroupRequest = GroupRequest
29 |
30 | class ExternalEventTypes(Enum):
31 | AppInitEvent = "AppInitEvent"
32 |
33 | GroupFileUpload = "GroupFileUpload"
34 | GroupAdminChange = "GroupAdminChange"
35 | GroupMemberDecrease = "GroupMemberDecrease"
36 | GroupMemberIncrease = "GroupMemberIncrease"
37 | GroupMemberBan = "GroupMemberBan"
38 | FriendAdd = "FriendAdd"
39 | GroupMessageRecall = "GroupMessageRecall"
40 | FriendMessageRecall = "FriendMessageRecall"
41 | Notify = "Notify"
42 | GroupCardChange = "GroupCardChange"
43 | FriendOfflineFile = "FriendOfflineFile"
44 | ClientStatusChange = "ClientStatusChange"
45 | EssenceMessageChange = "EssenceMessageChange"
46 |
47 | MessageReactionsUpdated = "MessageReactionsUpdated"
48 | ChannelUpdated = "ChannelUpdated"
49 | ChannelCreated = "ChannelCreated"
50 | ChannelDestroyed = "ChannelDestroyed"
51 | GuildChannelRecall = "GuildChannelRecall"
52 |
53 | FriendRequest = "FriendRequest"
54 | GroupRequest = "GroupRequest"
55 |
--------------------------------------------------------------------------------
/nakuru/entities/guild.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 | from typing import Optional, List
3 | import typing as T
4 |
5 | class BotGuild(BaseModel):
6 | nickname: str
7 | tiny_id: int
8 | avatar_url: str
9 |
10 | class Guild(BaseModel):
11 | guild_id: int
12 | guild_name: str
13 | guild_display_id: Optional[int]
14 | # 以下是 getGuildMetaByGuest 返回的更多结果
15 | guild_profile: Optional[str]
16 | create_time: Optional[int]
17 | max_member_count: Optional[int]
18 | max_robot_count: Optional[int]
19 | max_admin_count: Optional[int]
20 | member_count: Optional[int]
21 | owner_id: Optional[int]
22 |
23 | class SlowMode(BaseModel):
24 | slow_mode_key: int
25 | slow_mode_text: str
26 | speak_frequency: int
27 | slow_mode_circle: int
28 |
29 | class Channel(BaseModel):
30 | owner_guild_id: int
31 | channel_id: int
32 | channel_type: int
33 | channel_name: str
34 | create_time: int
35 | creator_id: int
36 | creator_tiny_id: int
37 | talk_permission: int
38 | visible_type: int
39 | current_slow_mode: int
40 | slow_modes: List[SlowMode]
41 |
42 | class GuildMember(BaseModel):
43 | tiny_id: Optional[int]
44 | user_id: Optional[int]
45 | title: Optional[str]
46 | nickname: str
47 | role: Optional[int]
48 | # 仅论坛子频道
49 | icon_url: Optional[str]
50 |
51 | class GuildMembers(BaseModel):
52 | members: List[GuildMember]
53 | bots: List[GuildMember]
54 | admins: List[GuildMember]
55 |
56 | class Reaction(BaseModel):
57 | emoji_id: str
58 | emoji_index: int
59 | emoji_type: int
60 | emoji_name: str
61 | count: int
62 | clicked: bool
63 |
64 | class Role(BaseModel):
65 | role_id: int
66 | role_name: Optional[str]
67 | argb_color: Optional[int]
68 | independent: Optional[bool]
69 | member_count: Optional[int]
70 | max_count: Optional[int]
71 | owned: Optional[int]
72 | disabled: Optional[bool]
73 |
74 | """
75 | 论坛子频道相关
76 | """
77 |
78 | class TopicChannelFile(BaseModel):
79 | file_id: str
80 | pattern_id: str
81 | url: str
82 | width: int
83 | height: int
84 |
85 | class TopicChannelFeedResource(BaseModel):
86 | images: List[TopicChannelFile]
87 | videos: List[TopicChannelFile]
88 |
89 | class TopicChannelFeed(BaseModel):
90 | id: int
91 | title: str
92 | sub_title: str
93 | create_time: int
94 | guild_id: int
95 | channel_id: int
96 | poster_info: GuildMember
97 | contents: list # TODO 解析论坛子频道 contents,参考 https://github.com/Mrs4s/go-cqhttp/blob/7278f99ed9118fe2e54aca752e62574d5bae3f00/coolq/feed.go#L10
98 | resources: TopicChannelFeedResource
--------------------------------------------------------------------------------
/nakuru/entities/others.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel, Field
2 | from typing import Optional, List
3 |
4 | class Bot(BaseModel):
5 | user_id: int
6 | nickname: str
7 |
8 | class QiDianAccount(BaseModel):
9 | master_id: int
10 | ext_name: str
11 | create_time: int
12 |
13 | class Stranger(BaseModel):
14 | user_id: int
15 | nickname: str
16 | sex: str
17 | age: int
18 | qid: str
19 |
20 | class AppVersion(BaseModel):
21 | app_name: str
22 | app_version: str
23 | app_full_name: str
24 | protocol_version: str
25 | coolq_edition: str
26 | coolq_directory: str
27 | go_cqhttp: bool = Field(..., alias="go-cqhttp")
28 | plugin_version: str
29 | plugin_build_number: int
30 | plugin_build_configuration: str
31 | runtime_version: str
32 | runtime_os: str
33 | version: str
34 | protocol: int
35 |
36 | class NetworkStatistics(BaseModel):
37 | packet_received: int
38 | packet_sent: int
39 | packet_lost: int
40 | message_received: int
41 | message_sent: int
42 | disconnect_times: int
43 | lost_times: int
44 |
45 | class AppStatus(BaseModel):
46 | app_initialized: bool
47 | app_enabled: bool
48 | plugins_good: bool
49 | app_good: bool
50 | online: bool
51 | good: bool
52 | stat: NetworkStatistics
53 |
54 | class TextDetection(BaseModel):
55 | text: str
56 | confidence: int
57 | coordinates: str
58 |
59 | class OCR(BaseModel):
60 | texts: TextDetection
61 | language: str
62 |
63 | class InvitedRequest(BaseModel):
64 | request_id: int
65 | invitor_uin: int
66 | invitor_nick: str
67 | group_id: int
68 | group_name: str
69 | checked: bool
70 | actor: int
71 |
72 | class JoinRequest(BaseModel):
73 | request_id: int
74 | requester_uin: int
75 | requester_nick: str
76 | message: str
77 | group_id: int
78 | group_name: str
79 | checked: bool
80 | actor: int
81 |
82 | class GroupSystemMessage(BaseModel):
83 | invited_requests: Optional[InvitedRequest]
84 | join_requests: Optional[JoinRequest]
85 |
86 | class VipInfo(BaseModel):
87 | user_id: int
88 | nickname: str
89 | level: int
90 | level_speed: float
91 | vip_level: str
92 | vip_growth_speed: int
93 | vip_growth_total: int
94 |
95 | class EssenceMessage(BaseModel):
96 | sender_id: int
97 | sender_nick: int
98 | sender_time: int
99 | operator_id: int
100 | operator_nick: int
101 | operator_time: int
102 | message_id: int
103 |
104 | class ModelShow(BaseModel):
105 | model_show: str
106 | need_pay: bool
107 |
--------------------------------------------------------------------------------
/nakuru/misc.py:
--------------------------------------------------------------------------------
1 | import inspect
2 | import os
3 | import typing as T
4 | from collections import namedtuple
5 |
6 | from .logger import logger
7 |
8 | Parameter = namedtuple("Parameter", ["name", "annotation", "default"])
9 |
10 | TRACEBACKED = os.urandom(32)
11 |
12 | def raiser(error):
13 | raise error
14 |
15 | def protocol_log(func):
16 | async def wrapper(*args, **kwargs):
17 | try:
18 | result = await func(*args, **kwargs)
19 | logger.info(f"Protocol: protocol method {func.__name__} was called")
20 | return result
21 | except Exception as e:
22 | logger.error(f"Protocol: protocol method {func.__name__} raised a error: {e.__class__.__name__}")
23 | raise e
24 |
25 | return wrapper
26 |
27 | def argument_signature(callable_target) -> T.List[Parameter]:
28 | return [
29 | Parameter(
30 | name=name,
31 | annotation=param.annotation if param.annotation != inspect._empty else None,
32 | default=param.default if param.default != inspect._empty else None
33 | )
34 | for name, param in dict(inspect.signature(callable_target).parameters).items()
35 | ]
36 |
37 | import re
38 | from .entities.components import ComponentTypes
39 |
40 | class CQParser:
41 | def __replaceChar(self, string, char, start, end):
42 | string = list(string)
43 | del (string[start:end])
44 | string.insert(start, char)
45 | return ''.join(string)
46 |
47 | # 获得文本中每一个 CQ 码的起始和结束位置
48 | def __getCQIndex(self, text):
49 | cqIndex = []
50 | for m in re.compile("(\[CQ:(.+?)])").finditer(text):
51 | cqIndex.append((m.start(), m.end()))
52 | cqIndex.append((len(text), len(text)))
53 | return cqIndex
54 |
55 | # 转义中括号
56 | def escape(self, text, isEscape=True):
57 | if isEscape:
58 | text = text.replace("&", "&")
59 | text = text.replace(",", ",")
60 | text = text.replace("[", "[")
61 | text = text.replace("]", "]")
62 | else:
63 | text = text.replace("&", "&")
64 | text = text.replace(",", ",")
65 | text = text.replace("[", "[")
66 | text = text.replace("]", "]")
67 | return text
68 |
69 | # 将纯文本转换成类型为 plain 的 CQ 码
70 | def plainToCQ(self, text):
71 | i = j = k = 0
72 | cqIndex = self.__getCQIndex(text)
73 | while i < len(cqIndex):
74 | if i > 0:
75 | if i == 1:
76 | k += 1
77 | else:
78 | j += 1
79 | cqIndex = self.__getCQIndex(text)
80 | if i > 0:
81 | l, r = cqIndex[j][k], cqIndex[j + 1][0]
82 | else:
83 | l, r = 0, cqIndex[0][0]
84 | source_text = text[l:r]
85 | if source_text != "":
86 | text = self.__replaceChar(text, f"[CQ:plain,text={self.escape(source_text)}]", l, r)
87 | i += 1
88 | return text
89 |
90 | def getAttributeList(self, text):
91 | text_array = text.split(",")
92 | text_array.pop(0)
93 | attribute_list = {}
94 | for _ in text_array:
95 | regex_result = re.search(r"^(.*?)=([\s\S]+)", _)
96 | k = regex_result.group(1)
97 | if k == "type":
98 | k = "_type"
99 | v = self.escape(regex_result.group(2), isEscape=False)
100 | attribute_list[k] = v
101 | return attribute_list
102 |
103 | def parseChain(self, text):
104 | text = self.plainToCQ(text)
105 | cqcode_list = re.findall(r'(\[CQ:([\s\S]+?)])', text)
106 | chain = []
107 | for x in cqcode_list:
108 | message_type = re.search(r"^\[CQ\:(.*?)\,", x[0]).group(1)
109 | try:
110 | chain.append(ComponentTypes[message_type].parse_obj(self.getAttributeList(x[1])))
111 | except:
112 | chain.append(ComponentTypes["unknown"].parse_obj({"text": message_type}))
113 | logger.error(f"Protocol: Cannot convert message type: {message_type}")
114 | return chain
--------------------------------------------------------------------------------
/nakuru/event/models.py:
--------------------------------------------------------------------------------
1 | import typing as T
2 | from enum import Enum
3 | from pydantic import BaseModel
4 |
5 | from ..entities import Friend, Member, Anonymous, File, OfflineFile, Device, GuildMember, Reaction, Channel
6 | from ..misc import CQParser
7 |
8 | parser = CQParser()
9 |
10 | class AppInitEvent(BaseModel):
11 | pass
12 |
13 | class MessageItemType(Enum):
14 | FriendMessage = "FriendMessage"
15 | GroupMessage = "GroupMessage"
16 | GuildMessage = "GuildMessage"
17 | BotMessage = "BotMessage"
18 | Message = "Message"
19 |
20 | class FriendMessage(BaseModel):
21 | type: MessageItemType = "FriendMessage"
22 | time: int
23 | self_id: str
24 | sub_type: str
25 | message_id: int
26 | user_id: int
27 | message: T.Union[str, list]
28 | raw_message: str
29 | font: int
30 | sender: Friend
31 |
32 | def __init__(self, message: str, **_):
33 | message = parser.parseChain(message)
34 | super().__init__(message=message, **_)
35 |
36 | class GroupMessage(BaseModel):
37 | type: MessageItemType = "GroupMessage"
38 | self_id: int
39 | sub_type: str
40 | message_id: int
41 | group_id: int
42 | user_id: int
43 | anonymous: T.Optional[Anonymous]
44 | message: T.Union[str, list]
45 | raw_message: str
46 | font: int
47 | sender: Member
48 | time: int
49 |
50 | def __init__(self, message: str, **_):
51 | message = parser.parseChain(message)
52 | super().__init__(message=message, **_)
53 |
54 | class GuildMessage(BaseModel):
55 | type: MessageItemType = "GuildMessage"
56 | self_id: int
57 | self_tiny_id: int
58 | sub_type: str
59 | message_id: str
60 | guild_id: int
61 | channel_id: int
62 | user_id: int
63 | message: T.Union[str, list]
64 | sender: GuildMember
65 | raw_message: T.Optional[str]
66 |
67 | def __init__(self, message: str, **_):
68 | raw_message = message
69 | message = parser.parseChain(message)
70 | super().__init__(message=message, raw_message=raw_message, **_)
71 |
72 | class BotMessage(BaseModel):
73 | type: MessageItemType = "BotMessage"
74 | message_id: T.Union[int, str]
75 |
76 | class Message(BaseModel): # getMessage
77 | type: MessageItemType = "Message"
78 | message_id: int
79 | real_id: int
80 | sender: Member
81 | time: int
82 | message: str
83 | raw_message: str
84 |
85 | def __init__(self, message: str, **_):
86 | message = parser.parseChain(message)
87 | super().__init__(message=message, **_)
88 |
89 | MessageTypes = {
90 | "private": FriendMessage,
91 | "group": GroupMessage,
92 | "guild": GuildMessage
93 | }
94 |
95 | class ForwardMessageSender(BaseModel):
96 | nickname: str
97 | user_id: int
98 |
99 | class ForwardMessageNode(BaseModel):
100 | content: T.Union[str, list]
101 | raw_content: T.Optional[str] # 本来没有的,用于表示原 content
102 | sender: ForwardMessageSender
103 | time: int
104 |
105 | def __init__(self, content: str, **_):
106 | raw_content = content
107 | content = parser.parseChain(content)
108 | super().__init__(content=content, raw_content=raw_content, **_)
109 |
110 | class ForwardMessages(BaseModel):
111 | messages: T.List[ForwardMessageNode]
112 |
113 | class NoticeItemType(Enum):
114 | GroupFileUpload = "GroupFileUpload"
115 | GroupAdminChange = "GroupAdminChange"
116 | GroupMemberDecrease = "GroupMemberDecrease"
117 | GroupMemberIncrease = "GroupMemberIncrease"
118 | GroupMemberBan = "GroupMemberBan"
119 | FriendAdd = "FriendAdd"
120 | GroupMessageRecall = "GroupMessageRecall"
121 | FriendMessageRecall = "FriendMessageRecall"
122 | Notify = "Notify"
123 | GroupCardChange = "GroupCardChange"
124 | FriendOfflineFile = "FriendOfflineFile"
125 | ClientStatusChange = "ClientStatusChange"
126 | EssenceMessageChange = "EssenceMessageChange"
127 | # 以下为频道事件
128 | MessageReactionsUpdated = "MessageReactionsUpdated"
129 | ChannelUpdated = "ChannelUpdated"
130 | ChannelCreated = "ChannelCreated"
131 | ChannelDestroyed = "ChannelDestroyed"
132 | GuildChannelRecall = "GuildChannelRecall"
133 |
134 | class GroupFileUpload(BaseModel):
135 | type: NoticeItemType = "GroupFileUpload"
136 | time: int
137 | self_id: int
138 | group_id: int
139 | user_id: int
140 | file: File
141 |
142 | class GroupAdminChange(BaseModel):
143 | type: NoticeItemType = "GroupAdminChange"
144 | time: int
145 | self_id: int
146 | sub_type: str
147 | group_id: int
148 | user_id: int
149 |
150 | class GroupMemberDecrease(BaseModel):
151 | type: NoticeItemType = "GroupMemberDecrease"
152 | time: int
153 | self_id: int
154 | sub_type: str
155 | group_id: int
156 | operator_id: int
157 | user_id: int
158 |
159 | class GroupMemberIncrease(BaseModel):
160 | type: NoticeItemType = "GroupMemberIncrease"
161 | time: int
162 | self_id: int
163 | sub_type: str
164 | group_id: int
165 | operator_id: int
166 | user_id: int
167 |
168 | class GroupMemberBan(BaseModel):
169 | type: NoticeItemType = "GroupMemberBan"
170 | time: int
171 | self_id: int
172 | sub_type: str
173 | group_id: int
174 | operator_id: int
175 | user_id: int
176 | duration: int
177 |
178 | class FriendAdd(BaseModel):
179 | type: NoticeItemType = "FriendAdd"
180 | time: int
181 | self_id: int
182 | user_id: int
183 |
184 | class GroupMessageRecall(BaseModel):
185 | type: NoticeItemType = "GroupMessageRecall"
186 | time: int
187 | self_id: int
188 | group_id: int
189 | user_id: int
190 | operator_id: int
191 | message_id: int
192 |
193 | class FriendMessageRecall(BaseModel):
194 | type: NoticeItemType = "FriendMessageRecall"
195 | time: int
196 | self_id: int
197 | user_id: int
198 | message_id: int
199 |
200 | class Notify(BaseModel):
201 | type: NoticeItemType = "Notify"
202 | sub_type: str
203 | user_id: int
204 | target_id: T.Optional[int]
205 | time: T.Optional[int]
206 | self_id: T.Optional[int]
207 | group_id: T.Optional[int]
208 | honor_type: T.Optional[str]
209 |
210 | class GroupCardChange(BaseModel):
211 | type: NoticeItemType = "GroupCardChange"
212 | group_id: int
213 | user_id: int
214 | card_new: str
215 | card_old: str
216 |
217 | class FriendOfflineFile(BaseModel):
218 | type: NoticeItemType = "FriendOfflineFile"
219 | user_id: int
220 | file: OfflineFile
221 |
222 | class ClientStatusChange(BaseModel):
223 | type: NoticeItemType = "ClientStatusChange"
224 | client: Device
225 | online: bool
226 |
227 | class EssenceMessageChange(BaseModel):
228 | type: NoticeItemType = "EssenceMessageChange"
229 | sub_type: str
230 | sender_id: int
231 | operator_id: int
232 | message_id: int
233 |
234 | class MessageReactionsUpdated(BaseModel):
235 | type: NoticeItemType = "MessageReactionsUpdated"
236 | guild_id: int
237 | channel_id: int
238 | user_id: int
239 | message_id: str
240 | current_reactions: T.List[Reaction]
241 |
242 | class ChannelUpdated(BaseModel):
243 | type: NoticeItemType = "ChannelUpdated"
244 | guild_id: int
245 | channel_id: int
246 | user_id: int
247 | operator_id: int
248 | old_info: Channel
249 | new_info: Channel
250 |
251 | class ChannelCreated(BaseModel):
252 | guild_id: int
253 | channel_id: int
254 | user_id: int
255 | operator_id: int
256 | channel_info: Channel
257 |
258 | class ChannelDestroyed(BaseModel):
259 | guild_id: int
260 | channel_id: int
261 | user_id: int
262 | operator_id: int
263 | channel_info: Channel
264 |
265 | class GuildChannelRecall(BaseModel):
266 | guild_id: int
267 | channel_id: int
268 | operator_id: int
269 | message_id: str
270 | user_id: int
271 |
272 | NoticeTypes = {
273 | "group_upload": GroupFileUpload,
274 | "group_admin": GroupAdminChange,
275 | "group_decrease": GroupMemberDecrease,
276 | "group_increase": GroupMemberIncrease,
277 | "group_ban": GroupMemberBan,
278 | "friend_add": FriendAdd,
279 | "group_recall": GroupMessageRecall,
280 | "friend_recall": FriendMessageRecall,
281 | "notify": Notify,
282 | "group_card": GroupCardChange,
283 | "offline_file": FriendOfflineFile,
284 | "client_status": ClientStatusChange,
285 | "essence": EssenceMessageChange,
286 | "message_reactions_updated": MessageReactionsUpdated,
287 | "channel_updated": ChannelUpdated,
288 | "channel_created": ChannelCreated,
289 | "channel_destroyed": ChannelDestroyed,
290 | "guild_channel_recall": GuildChannelRecall
291 | }
292 |
293 | class RequestItemType(Enum):
294 | FriendRequest = "FriendRequest"
295 | GroupRequest = "GroupRequest"
296 |
297 | class FriendRequest(BaseModel):
298 | type: RequestItemType = "FriendRequest"
299 | time: int
300 | self_id: int
301 | user_id: int
302 | comment: str
303 | flag: str
304 |
305 | class GroupRequest(BaseModel):
306 | type: RequestItemType = "GroupRequest"
307 | time: int
308 | self_id: int
309 | sub_type: str
310 | group_id: int
311 | user_id: int
312 | comment: str
313 | flag: str
314 |
315 | RequestTypes = {
316 | "friend": FriendRequest,
317 | "group": GroupRequest
318 | }
--------------------------------------------------------------------------------
/nakuru/entities/components.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import json
3 | import os
4 | import typing as T
5 | from enum import Enum
6 |
7 | from pydantic import BaseModel
8 |
9 | from ..logger import logger
10 |
11 |
12 | class ComponentType(Enum):
13 | Plain = "Plain"
14 | Face = "Face"
15 | Record = "Record"
16 | Video = "Video"
17 | At = "At"
18 | RPS = "RPS" # TODO
19 | Dice = "Dice" # TODO
20 | Shake = "Shake" # TODO
21 | Anonymous = "Anonymous" # TODO
22 | Share = "Share"
23 | Contact = "Contact" # TODO
24 | Location = "Location" # TODO
25 | Music = "Music"
26 | Image = "Image"
27 | Reply = "Reply"
28 | RedBag = "RedBag"
29 | Poke = "Poke"
30 | Forward = "Forward"
31 | Node = "Node"
32 | Xml = "Xml"
33 | Json = "Json"
34 | CardImage = "CardImage"
35 | TTS = "TTS"
36 | Unknown = "Unknown"
37 |
38 |
39 | class BaseMessageComponent(BaseModel):
40 | type: ComponentType
41 |
42 | def toString(self):
43 | output = f"[CQ:{self.type.lower()}"
44 | for k, v in self.__dict__.items():
45 | if k == "type" or v is None:
46 | continue
47 | if k == "_type":
48 | k = "type"
49 | if isinstance(v, bool):
50 | v = 1 if v else 0
51 | output += ",%s=%s" % (k, str(v).replace("&", "&") \
52 | .replace(",", ",") \
53 | .replace("[", "[") \
54 | .replace("]", "]"))
55 | output += "]"
56 | return output
57 |
58 | def toDict(self):
59 | data = dict()
60 | for k, v in self.__dict__.items():
61 | if k == "type" or v is None:
62 | continue
63 | if k == "_type":
64 | k = "type"
65 | data[k] = v
66 | return {
67 | "type": self.type.lower(),
68 | "data": data
69 | }
70 |
71 |
72 | class Plain(BaseMessageComponent):
73 | type: ComponentType = "Plain"
74 | text: str
75 | convert: T.Optional[bool] = True # 若为 False 则直接发送未转换 CQ 码的消息
76 |
77 | def __init__(self, text: str, convert: bool = True, **_):
78 | super().__init__(text=text, convert=convert, **_)
79 |
80 | def toString(self): # 没有 [CQ:plain] 这种东西,所以直接导出纯文本
81 | if not self.convert:
82 | return self.text
83 | return self.text.replace("&", "&") \
84 | .replace("[", "[") \
85 | .replace("]", "]")
86 |
87 |
88 | class Face(BaseMessageComponent):
89 | type: ComponentType = "Face"
90 | id: int
91 |
92 | def __init__(self, **_):
93 | super().__init__(**_)
94 |
95 |
96 | class Record(BaseMessageComponent):
97 | type: ComponentType = "Record"
98 | file: T.Optional[str]
99 | magic: T.Optional[bool] = False
100 | url: T.Optional[str]
101 | cache: T.Optional[bool] = True
102 | proxy: T.Optional[bool] = True
103 | timeout: T.Optional[int] = 0
104 | # 额外
105 | path: T.Optional[str]
106 |
107 | def __init__(self, file: T.Optional[str], **_):
108 | for k in _.keys():
109 | if k == "url":
110 | logger.warn("Protocol: you should put url into file parameter")
111 | # Protocol.warn(f"go-cqhttp doesn't support send {self.type} by {k}")
112 | super().__init__(file=file, **_)
113 |
114 | @staticmethod
115 | def fromFileSystem(path, **_):
116 | return Record(file=f"file:///{os.path.abspath(path)}", path=path, **_)
117 |
118 | @staticmethod
119 | def fromURL(url: str, **_):
120 | if url.startswith("http://") or url.startswith("https://"):
121 | return Record(file=url, **_)
122 | raise Exception("not a valid url")
123 |
124 |
125 | class Video(BaseMessageComponent):
126 | type: ComponentType = "Video"
127 | file: str
128 | cover: T.Optional[str]
129 | c: T.Optional[int] = 2
130 | # 额外
131 | path: T.Optional[str]
132 |
133 | def __init__(self, file: str, **_):
134 | for k in _.keys():
135 | if k == "c" and _[k] not in [2, 3]:
136 | logger.warn(f"Protocol: {k}={_[k]} doesn't match values")
137 | super().__init__(file=file, **_)
138 |
139 | @staticmethod
140 | def fromFileSystem(path, **_):
141 | return Video(file=f"file:///{os.path.abspath(path)}", path=path, **_)
142 |
143 | @staticmethod
144 | def fromURL(url: str, **_):
145 | if url.startswith("http://") or url.startswith("https://"):
146 | return Video(file=url, **_)
147 | raise Exception("not a valid url")
148 |
149 |
150 | class At(BaseMessageComponent):
151 | type: ComponentType = "At"
152 | qq: T.Union[int, str] # 此处str为all时代表所有人
153 | name: T.Optional[str]
154 |
155 | def __init__(self, **_):
156 | super().__init__(**_)
157 |
158 |
159 | class AtAll(At):
160 | qq: str = "all"
161 |
162 | def __init__(self, **_):
163 | super().__init__(**_)
164 |
165 |
166 | class RPS(BaseMessageComponent): # TODO
167 | type: ComponentType = "RPS"
168 |
169 | def __init__(self, **_):
170 | super().__init__(**_)
171 |
172 |
173 | class Dice(BaseMessageComponent): # TODO
174 | type: ComponentType = "Dice"
175 |
176 | def __init__(self, **_):
177 | super().__init__(**_)
178 |
179 |
180 | class Shake(BaseMessageComponent): # TODO
181 | type: ComponentType = "Shake"
182 |
183 | def __init__(self, **_):
184 | super().__init__(**_)
185 |
186 |
187 | class Anonymous(BaseMessageComponent): # TODO
188 | type: ComponentType = "Anonymous"
189 | ignore: T.Optional[bool]
190 |
191 | def __init__(self, **_):
192 | super().__init__(**_)
193 |
194 |
195 | class Share(BaseMessageComponent):
196 | type: ComponentType = "Share"
197 | url: str
198 | title: str
199 | content: T.Optional[str]
200 | image: T.Optional[str]
201 |
202 | def __init__(self, **_):
203 | super().__init__(**_)
204 |
205 |
206 | class Contact(BaseMessageComponent): # TODO
207 | type: ComponentType = "Contact"
208 | _type: str # type 字段冲突
209 | id: T.Optional[int]
210 |
211 | def __init__(self, **_):
212 | for k in _.keys():
213 | if k == "_type" and _[k] not in ["qq", "group"]:
214 | logger.warn(f"Protocol: {k}={_[k]} doesn't match values")
215 | super().__init__(**_)
216 |
217 |
218 | class Location(BaseMessageComponent): # TODO
219 | type: ComponentType = "Location"
220 | lat: float
221 | lon: float
222 | title: T.Optional[str]
223 | content: T.Optional[str]
224 |
225 | def __init__(self, **_):
226 | super().__init__(**_)
227 |
228 |
229 | class Music(BaseMessageComponent):
230 | type: ComponentType = "Music"
231 | _type: str
232 | id: T.Optional[int]
233 | url: T.Optional[str]
234 | audio: T.Optional[str]
235 | title: T.Optional[str]
236 | content: T.Optional[str]
237 | image: T.Optional[str]
238 |
239 | def __init__(self, **_):
240 | for k in _.keys():
241 | if k == "_type" and _[k] not in ["qq", "163", "xm", "custom"]:
242 | logger.warn(f"Protocol: {k}={_[k]} doesn't match values")
243 | super().__init__(**_)
244 |
245 |
246 | class Image(BaseMessageComponent):
247 | type: ComponentType = "Image"
248 | file: T.Optional[str]
249 | _type: T.Optional[str]
250 | subType: T.Optional[int]
251 | url: T.Optional[str]
252 | cache: T.Optional[bool] = True
253 | id: T.Optional[int] = 40000
254 | c: T.Optional[int] = 2
255 | # 额外
256 | path: T.Optional[str]
257 |
258 | def __init__(self, file: T.Optional[str], **_):
259 | for k in _.keys():
260 | if (k == "_type" and _[k] not in ["flash", "show", None]) or \
261 | (k == "c" and _[k] not in [2, 3]):
262 | logger.warn(f"Protocol: {k}={_[k]} doesn't match values")
263 | super().__init__(file=file, **_)
264 |
265 | @staticmethod
266 | def fromURL(url: str, **_):
267 | if url.startswith("http://") or url.startswith("https://"):
268 | return Image(file=url, **_)
269 | raise Exception("not a valid url")
270 |
271 | @staticmethod
272 | def fromFileSystem(path, **_):
273 | return Image(file=f"file:///{os.path.abspath(path)}", path=path, **_)
274 |
275 | @staticmethod
276 | def fromBase64(base64: str, **_):
277 | return Image(f"base64://{base64}", **_)
278 |
279 | @staticmethod
280 | def fromBytes(byte: bytes):
281 | return Image.fromBase64(base64.b64encode(byte).decode())
282 |
283 | @staticmethod
284 | def fromIO(IO):
285 | return Image.fromBytes(IO.read())
286 |
287 |
288 | class Reply(BaseMessageComponent):
289 | type: ComponentType = "Reply"
290 | id: int
291 | text: T.Optional[str]
292 | qq: T.Optional[int]
293 | time: T.Optional[int]
294 | seq: T.Optional[int]
295 |
296 | def __init__(self, **_):
297 | super().__init__(**_)
298 |
299 |
300 | class RedBag(BaseMessageComponent):
301 | type: ComponentType = "RedBag"
302 | title: str
303 |
304 | def __init__(self, **_):
305 | super().__init__(**_)
306 |
307 |
308 | class Poke(BaseMessageComponent):
309 | type: ComponentType = "Poke"
310 | qq: int
311 |
312 | def __init__(self, **_):
313 | super().__init__(**_)
314 |
315 |
316 | class Forward(BaseMessageComponent):
317 | type: ComponentType = "Forward"
318 | id: str
319 |
320 | def __init__(self, **_):
321 | super().__init__(**_)
322 |
323 |
324 | class Node(BaseMessageComponent): # 该 component 仅支持使用 sendGroupForwardMessage 发送
325 | type: ComponentType = "Node"
326 | id: T.Optional[int]
327 | name: T.Optional[str]
328 | uin: T.Optional[int]
329 | content: T.Optional[T.Union[str, list]]
330 | seq: T.Optional[T.Union[str, list]] # 不清楚是什么
331 | time: T.Optional[int]
332 |
333 | def __init__(self, content: T.Union[str, list], **_):
334 | if isinstance(content, list):
335 | _content = ""
336 | for chain in content:
337 | _content += chain.toString()
338 | content = _content
339 | super().__init__(content=content, **_)
340 |
341 | def toString(self):
342 | logger.warn("Protocol: node doesn't support stringify")
343 | return ""
344 |
345 |
346 | class Xml(BaseMessageComponent):
347 | type: ComponentType = "Xml"
348 | data: str
349 | resid: T.Optional[int]
350 |
351 | def __init__(self, **_):
352 | super().__init__(**_)
353 |
354 |
355 | class Json(BaseMessageComponent):
356 | type: ComponentType = "Json"
357 | data: T.Union[str, dict]
358 | resid: T.Optional[int] = 0
359 |
360 | def __init__(self, data, **_):
361 | if isinstance(data, dict):
362 | data = json.dumps(data)
363 | super().__init__(data=data, **_)
364 |
365 |
366 | class CardImage(BaseMessageComponent):
367 | type: ComponentType = "CardImage"
368 | file: str
369 | cache: T.Optional[bool] = True
370 | minwidth: T.Optional[int] = 400
371 | minheight: T.Optional[int] = 400
372 | maxwidth: T.Optional[int] = 500
373 | maxheight: T.Optional[int] = 500
374 | source: T.Optional[str]
375 | icon: T.Optional[str]
376 |
377 | def __init__(self, **_):
378 | super().__init__(**_)
379 |
380 | @staticmethod
381 | def fromFileSystem(path, **_):
382 | return CardImage(file=f"file:///{os.path.abspath(path)}", **_)
383 |
384 |
385 | class TTS(BaseMessageComponent):
386 | type: ComponentType = "TTS"
387 | text: str
388 |
389 | def __init__(self, **_):
390 | super().__init__(**_)
391 |
392 |
393 | class Unknown(BaseMessageComponent):
394 | type: ComponentType = "Unknown"
395 | text: str
396 |
397 | def toString(self):
398 | return ""
399 |
400 |
401 | ComponentTypes = {
402 | "plain": Plain,
403 | "face": Face,
404 | "record": Record,
405 | "video": Video,
406 | "at": At,
407 | "rps": RPS,
408 | "dice": Dice,
409 | "shake": Shake,
410 | "anonymous": Anonymous,
411 | "share": Share,
412 | "contact": Contact,
413 | "location": Location,
414 | "music": Music,
415 | "image": Image,
416 | "reply": Reply,
417 | "redbag": RedBag,
418 | "poke": Poke,
419 | "forward": Forward,
420 | "node": Node,
421 | "xml": Xml,
422 | "json": Json,
423 | "cardimage": CardImage,
424 | "tts": TTS,
425 | "unknown": Unknown
426 | }
427 |
--------------------------------------------------------------------------------
/nakuru/application.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import aiohttp
3 | import asyncio
4 | import inspect
5 | import copy
6 | import pydantic
7 | import traceback
8 |
9 | from contextlib import AsyncExitStack
10 | from typing import Callable, NamedTuple, Awaitable, Any, List, Dict
11 | from functools import partial
12 | from async_lru import alru_cache
13 |
14 | from .event import InternalEvent, ExternalEvent
15 | from .event.models import MessageTypes, NoticeTypes, RequestTypes, Friend, Member, GuildMember
16 | from .event.builtins import ExecutorProtocol, Depend
17 | from .event.models import (
18 | FriendMessage, GroupMessage, GuildMessage, MessageItemType
19 | )
20 | from .event.enums import ExternalEvents
21 | from .misc import argument_signature, raiser, TRACEBACKED
22 | from .protocol import CQHTTP_Protocol
23 | from .logger import logger
24 |
25 | class CQHTTP(CQHTTP_Protocol):
26 | event: Dict[
27 | str, List[Callable[[Any], Awaitable]]
28 | ] = {}
29 | lifecycle: Dict[str, List[Callable]] = {
30 | "start": [],
31 | "end": [],
32 | "around": []
33 | }
34 | global_dependencies: List[Depend]
35 | global_middlewares: List
36 |
37 | def __init__(self,
38 | host: str = None,
39 | port: int = None,
40 | http_port: int = None,
41 | token: str = None,
42 | global_dependencies: List[Depend] = None,
43 | global_middlewares: List = None):
44 | self.global_dependencies = global_dependencies or []
45 | self.global_middlewares = global_middlewares or []
46 |
47 | self.headers = {}
48 | if token is not None:
49 | self.headers["Authorization"] = f"Bearer {token}"
50 | self.protocol_params = {
51 | "access_token": token
52 | }
53 | self.baseurl = f"http://{host}:{port}"
54 | self.baseurl_http = f"http://{host}:{http_port}"
55 |
56 | self.closed = False
57 |
58 | def close(self):
59 | self.closed = True
60 |
61 | async def ws_event(self):
62 | async with aiohttp.ClientSession() as session:
63 | async with session.ws_connect(f"{self.baseurl}", headers=self.headers) as ws_connection:
64 | logger.info("Protocol: connected")
65 | while not self.closed:
66 | try:
67 | received_data = await ws_connection.receive_json()
68 | except TypeError:
69 | continue
70 | if received_data:
71 | post_type = received_data["post_type"]
72 | try:
73 | if post_type == "message":
74 | received_data = MessageTypes[received_data["message_type"]].parse_obj(received_data)
75 | elif post_type == "notice":
76 | received_data = NoticeTypes[received_data["notice_type"]].parse_obj(received_data)
77 | elif post_type == "request":
78 | received_data = RequestTypes[received_data["request_type"]].parse_obj(received_data)
79 | else:
80 | continue
81 | except KeyError:
82 | logger.error("Protocol: data parse error: " + str(received_data))
83 | continue
84 | except pydantic.error_wrappers.ValidationError:
85 | logger.error("Protocol: data parse error: " + str(received_data))
86 | continue
87 | await self.queue.put(InternalEvent(
88 | name=self.getEventCurrentName(type(received_data)),
89 | body=received_data
90 | ))
91 |
92 | async def event_runner(self):
93 | while not self.closed:
94 | try:
95 | event_context: NamedTuple[InternalEvent] = await asyncio.wait_for(self.queue.get(), 3)
96 | except asyncio.TimeoutError:
97 | continue
98 |
99 | if event_context.name in self.registeredEventNames:
100 | logger.info(f"Event: handling a event: {event_context.name}")
101 | for event_body in list(self.event.values()) \
102 | [self.registeredEventNames.index(event_context.name)]:
103 | if event_body:
104 | running_loop = asyncio.get_running_loop()
105 | running_loop.create_task(self.executor(event_body, event_context))
106 |
107 | @property
108 | def registeredEventNames(self):
109 | return [self.getEventCurrentName(i) for i in self.event.keys()]
110 |
111 | async def executor(self,
112 | executor_protocol: ExecutorProtocol,
113 | event_context,
114 | extra_parameter={},
115 | lru_cache_sets=None
116 | ):
117 | lru_cache_sets = lru_cache_sets or {}
118 | executor_protocol: ExecutorProtocol
119 | for depend in executor_protocol.dependencies:
120 | if not inspect.isclass(depend.func):
121 | depend_func = depend.func
122 | elif hasattr(depend.func, "__call__"):
123 | depend_func = depend.func.__call__
124 | else:
125 | raise TypeError("must be callable.")
126 |
127 | if depend_func in lru_cache_sets and depend.cache:
128 | depend_func = lru_cache_sets[depend_func]
129 | else:
130 | if depend.cache:
131 | original = depend_func
132 | if inspect.iscoroutinefunction(depend_func):
133 | depend_func = alru_cache(depend_func)
134 | else:
135 | depend_func = lru_cache(depend_func)
136 | lru_cache_sets[original] = depend_func
137 |
138 | result = await self.executor_with_middlewares(
139 | depend_func, depend.middlewares, event_context, lru_cache_sets
140 | )
141 | if result is TRACEBACKED:
142 | return TRACEBACKED
143 |
144 | ParamSignatures = argument_signature(executor_protocol.callable)
145 | PlaceAnnotation = self.get_annotations_mapping()
146 | CallParams = {}
147 | for name, annotation, default in ParamSignatures:
148 | if default:
149 | if isinstance(default, Depend):
150 | if not inspect.isclass(default.func):
151 | depend_func = default.func
152 | elif hasattr(default.func, "__call__"):
153 | depend_func = default.func.__call__
154 | else:
155 | raise TypeError("must be callable.")
156 |
157 | if depend_func in lru_cache_sets and default.cache:
158 | depend_func = lru_cache_sets[depend_func]
159 | else:
160 | if default.cache:
161 | original = depend_func
162 | if inspect.iscoroutinefunction(depend_func):
163 | depend_func = alru_cache(depend_func)
164 | else:
165 | depend_func = lru_cache(depend_func)
166 | lru_cache_sets[original] = depend_func
167 |
168 | CallParams[name] = await self.executor_with_middlewares(
169 | depend_func, default.middlewares, event_context, lru_cache_sets
170 | )
171 | continue
172 | else:
173 | raise RuntimeError("checked a unexpected default value.")
174 | else:
175 | if annotation in PlaceAnnotation:
176 | CallParams[name] = PlaceAnnotation[annotation](event_context)
177 | continue
178 | else:
179 | if name not in extra_parameter:
180 | raise RuntimeError(f"checked a unexpected annotation: {annotation}")
181 |
182 | async with AsyncExitStack() as stack:
183 | sorted_middlewares = self.sort_middlewares(executor_protocol.middlewares)
184 | for async_middleware in sorted_middlewares['async']:
185 | await stack.enter_async_context(async_middleware)
186 | for normal_middleware in sorted_middlewares['normal']:
187 | stack.enter_context(normal_middleware)
188 |
189 | return await self.run_func(executor_protocol.callable, **CallParams, **extra_parameter)
190 |
191 | async def _run(self):
192 | loop = asyncio.get_event_loop()
193 | self.queue = asyncio.Queue(loop=loop) if sys.version_info.minor < 10 else asyncio.Queue()
194 | wsLoop = loop.create_task(self.ws_event())
195 | eventLoop = loop.create_task(self.event_runner())
196 |
197 | await self.queue.put(InternalEvent(
198 | name=self.getEventCurrentName("AppInitEvent"),
199 | body={}
200 | ))
201 | try:
202 | for start_callable in self.lifecycle['start']:
203 | await self.run_func(start_callable, self)
204 |
205 | for around_callable in self.lifecycle['around']:
206 | await self.run_func(around_callable, self)
207 |
208 | except KeyboardInterrupt:
209 | logger.info("catched Ctrl-C, exiting..")
210 | self.close()
211 | except Exception as e:
212 | traceback.print_exc()
213 | finally:
214 | for around_callable in self.lifecycle['around']:
215 | await self.run_func(around_callable, self)
216 |
217 | for end_callable in self.lifecycle['end']:
218 | await self.run_func(end_callable, self)
219 |
220 | # await tasks exit
221 | await wsLoop
222 | await eventLoop
223 |
224 | def run(self):
225 | loop = asyncio.get_event_loop()
226 | loop.run_until_complete(self._run())
227 |
228 | def receiver(self,
229 | event_name,
230 | dependencies: List[Depend] = None,
231 | use_middlewares: List[Callable] = None):
232 | def receiver_warpper(func: Callable):
233 | if not inspect.iscoroutinefunction(func):
234 | raise TypeError("event body must be a coroutine function.")
235 |
236 | self.event.setdefault(event_name, [])
237 | self.event[event_name].append(ExecutorProtocol(
238 | callable=func,
239 | dependencies=(dependencies or []) + self.global_dependencies,
240 | middlewares=(use_middlewares or []) + self.global_middlewares
241 | ))
242 | return func
243 |
244 | return receiver_warpper
245 |
246 | def getEventCurrentName(self, event_value):
247 | class_list = (
248 | GroupMessage,
249 | FriendMessage,
250 | GuildMessage,
251 | *self.get_event_class_name()
252 | )
253 | if inspect.isclass(event_value) and issubclass(event_value, ExternalEvent): # subclass
254 | return event_value.__name__
255 | elif isinstance(event_value, class_list): # normal class
256 | return event_value.__class__.__name__
257 | elif event_value in class_list: # message
258 | return event_value.__name__
259 | elif isinstance(event_value, ( # enum
260 | MessageItemType,
261 | ExternalEvents
262 | )):
263 | return event_value.name
264 | else:
265 | return event_value
266 |
267 | def get_annotations_mapping(self):
268 | return {
269 | CQHTTP: lambda k: self,
270 | FriendMessage: lambda k: k.body \
271 | if self.getEventCurrentName(k.body) == "FriendMessage" else \
272 | raiser(ValueError("you cannot setting a unbind argument.")),
273 | GroupMessage: lambda k: k.body \
274 | if self.getEventCurrentName(k.body) == "GroupMessage" else \
275 | raiser(ValueError("you cannot setting a unbind argument.")),
276 | GuildMessage: lambda k: k.body \
277 | if self.getEventCurrentName(k.body) == "GuildMessage" else \
278 | raiser(ValueError("you cannot setting a unbind argument.")),
279 | Friend: lambda k: k.body.sender \
280 | if self.getEventCurrentName(k.body) == "FriendMessage" else \
281 | raiser(ValueError("Friend is not enable in this type of event.")),
282 | Member: lambda k: k.body.sender \
283 | if self.getEventCurrentName(k.body) == "GroupMessage" else \
284 | raiser(ValueError("Group is not enable in this type of event.")),
285 | GuildMember: lambda k: k.body.sender \
286 | if self.getEventCurrentName(k.body) == "GuildMessage" else \
287 | raiser(ValueError("Group is not enable in this type of event.")),
288 | "Sender": lambda k: k.body.sender \
289 | if self.getEventCurrentName(k.body) in MessageTypes else \
290 | raiser(ValueError("Sender is not enable in this type of event.")),
291 | "Type": lambda k: self.getEventCurrentName(k.body),
292 | **self.gen_event_anno()
293 | }
294 |
295 | def get_event_class_name(self):
296 | def warpper(name, event_context):
297 | if name != event_context.name:
298 | raise ValueError("cannot look up a non-listened event.")
299 | return event_context.body
300 |
301 | return {
302 | event_class.value for event_name, event_class in ExternalEvents.__members__.items()
303 | }
304 |
305 | def gen_event_anno(self):
306 | def warpper(name, event_context):
307 | if name != event_context.name:
308 | raise ValueError("cannot look up a non-listened event.")
309 | return event_context.body
310 |
311 | return {
312 | event_class.value: partial(warpper, copy.copy(event_name)) \
313 | for event_name, event_class in ExternalEvents.__members__.items()
314 | }
315 |
316 | @staticmethod
317 | def sort_middlewares(iterator):
318 | return {
319 | "async": [
320 | i for i in iterator if all([
321 | hasattr(i, "__aenter__"),
322 | hasattr(i, "__aexit__")
323 | ])
324 | ],
325 | "normal": [
326 | i for i in iterator if all([
327 | hasattr(i, "__enter__"),
328 | hasattr(i, "__exit__")
329 | ])
330 | ]
331 | }
332 |
333 | @staticmethod
334 | async def run_func(func, *args, **kwargs):
335 | if inspect.iscoroutinefunction(func):
336 | await func(*args, **kwargs)
337 | else:
338 | func(*args, **kwargs)
339 |
--------------------------------------------------------------------------------
/nakuru/protocol.py:
--------------------------------------------------------------------------------
1 | import typing as T
2 |
3 | from .event.models import BotMessage, Message, Anonymous, ForwardMessages
4 | from .entities import *
5 | from .entities.components import Node
6 | from .network import fetch
7 |
8 |
9 | class CQHTTP_Protocol:
10 | baseurl_http: str
11 | protocol_params: dict = {}
12 |
13 | async def sendFriendMessage(self,
14 | user_id: int,
15 | message: T.Union[str, list],
16 | group_id: T.Optional[int] = None,
17 | auto_escape: bool = False) -> T.Union[BotMessage, bool]:
18 | if isinstance(message, list):
19 | _message = ""
20 | for chain in message:
21 | _message += chain.toString()
22 | message = _message
23 | payload = {
24 | "user_id": user_id,
25 | "message": message,
26 | "auto_escape": auto_escape
27 | }
28 | if group_id:
29 | payload["group_id"] = group_id
30 | result = await fetch.http_post(f"{self.baseurl_http}/send_private_msg", payload, params=self.protocol_params)
31 | if result["status"] == "ok":
32 | return BotMessage.parse_obj(result["data"])
33 | return False
34 |
35 | async def sendGroupMessage(self,
36 | group_id: int,
37 | message: T.Union[str, list],
38 | auto_escape: bool = False) -> T.Union[BotMessage, bool]:
39 | if isinstance(message, list):
40 | _message = ""
41 | for chain in message:
42 | _message += chain.toString()
43 | message = _message
44 | result = await fetch.http_post(f"{self.baseurl_http}/send_group_msg", {
45 | "group_id": group_id,
46 | "message": message,
47 | "auto_escape": auto_escape
48 | }, params=self.protocol_params)
49 | if result["status"] == "ok":
50 | return BotMessage.parse_obj(result["data"])
51 | return False
52 |
53 | async def sendGroupForwardMessage(self,
54 | group_id: int,
55 | messages: list) -> T.Union[BotMessage, bool]:
56 | for i in range(len(messages)):
57 | if isinstance(messages[i], Node):
58 | messages[i] = messages[i].toDict()
59 | result = await fetch.http_post(f"{self.baseurl_http}/send_group_forward_msg", {
60 | "group_id": group_id,
61 | "messages": messages
62 | }, params=self.protocol_params)
63 | if result["status"] == "ok":
64 | return BotMessage.parse_obj(result["data"])
65 | return False
66 |
67 | async def sendPrivateForwardMessage(self,
68 | user_id: int,
69 | messages: list) -> T.Union[BotMessage, bool]:
70 | for i in range(len(messages)):
71 | if isinstance(messages[i], Node):
72 | messages[i] = messages[i].toDict()
73 |
74 | result = await fetch.http_post(f"{self.baseurl_http}/send_private_forward_msg", {
75 | "user_id": user_id,
76 | "messages": messages
77 | }, params=self.protocol_params)
78 | if result["status"] == "ok":
79 | return BotMessage.parse_obj(result["data"])
80 | return False
81 |
82 | async def recall(self, message_id: int) -> bool:
83 | result = await fetch.http_post(f"{self.baseurl_http}/delete_msg", {
84 | "message_id": message_id
85 | }, params=self.protocol_params)
86 | if result["status"] == "ok":
87 | return True
88 | return False
89 |
90 | async def getMessage(self, message_id: int) -> T.Union[Message, bool]:
91 | result = await fetch.http_post(f"{self.baseurl_http}/get_msg", {
92 | "message_id": message_id
93 | }, params=self.protocol_params)
94 | if result["status"] == "ok":
95 | return Message.parse_obj(result["data"])
96 | return False
97 |
98 | async def getForwardMessage(self, message_id: int) -> T.Union[ForwardMessages, bool]:
99 | result = await fetch.http_post(f"{self.baseurl_http}/get_forward_msg", {
100 | "message_id": message_id
101 | }, params=self.protocol_params)
102 | if result["status"] == "ok":
103 | return ForwardMessages.parse_obj(result["data"])
104 | return False
105 |
106 | async def getImage(self, file: str) -> T.Union[ImageFile, bool]:
107 | result = await fetch.http_post(f"{self.baseurl_http}/get_image", {
108 | "file": file
109 | }, params=self.protocol_params)
110 | if result["status"] == "ok":
111 | return ImageFile.parse_obj(result["data"])
112 | return False
113 |
114 | async def kick(self,
115 | group_id: int,
116 | user_id: int,
117 | reject_add_request: bool = False) -> bool:
118 | result = await fetch.http_post(f"{self.baseurl_http}/set_group_kick", {
119 | "group_id": group_id,
120 | "user_id": user_id,
121 | "reject_add_request": reject_add_request
122 | }, params=self.protocol_params)
123 | if result["status"] == "ok":
124 | return True
125 | return False
126 |
127 | async def mute(self,
128 | group_id: int,
129 | user_id: int,
130 | duration: int = 30 * 60) -> bool:
131 | result = await fetch.http_post(f"{self.baseurl_http}/set_group_ban", {
132 | "group_id": group_id,
133 | "user_id": user_id,
134 | "duration": duration
135 | }, params=self.protocol_params)
136 | if result["status"] == "ok":
137 | return True
138 | return False
139 |
140 | async def unmute(self, group_id: int, user_id: int) -> bool:
141 | return await self.mute(group_id, user_id, 0)
142 |
143 | async def muteAnonymous(self,
144 | group_id: int,
145 | flag: str,
146 | duration: int = 30 * 60,
147 | anonymous: Anonymous = None): # TODO
148 | result = await fetch.http_post(f"{self.baseurl_http}/set_group_anonymous_ban", {
149 | "group_id": group_id,
150 | "flag": flag,
151 | "duration": duration
152 | }, params=self.protocol_params)
153 | if result["status"] == "ok":
154 | return True
155 | return False
156 |
157 | async def muteAll(self,
158 | group_id: int,
159 | enable: bool = True) -> bool:
160 | result = await fetch.http_post(f"{self.baseurl_http}/set_group_whole_ban", {
161 | "group_id": group_id,
162 | "enable": enable
163 | }, params=self.protocol_params)
164 | if result["status"] == "ok":
165 | return True
166 | return False
167 |
168 | async def setGroupAdmin(self,
169 | group_id: int,
170 | user_id: int,
171 | enable: bool = True) -> bool:
172 | result = await fetch.http_post(f"{self.baseurl_http}/set_group_admin", {
173 | "group_id": group_id,
174 | "user_id": user_id,
175 | "enable": enable
176 | }, params=self.protocol_params)
177 | if result["status"] == "ok":
178 | return True
179 | return False
180 |
181 | async def setGroupAnonymous(self,
182 | group_id: int,
183 | enable: bool = True) -> bool: # TODO go-cqhttp 暂未支持
184 | result = await fetch.http_post(f"{self.baseurl_http}/set_group_anonymous", {
185 | "group_id": group_id,
186 | "enable": enable
187 | }, params=self.protocol_params)
188 | if result["status"] == "ok":
189 | return True
190 | return False
191 |
192 | async def setGroupCard(self,
193 | group_id: int,
194 | user_id: int,
195 | card: str = "") -> bool:
196 | result = await fetch.http_post(f"{self.baseurl_http}/set_group_card", {
197 | "group_id": group_id,
198 | "user_id": user_id,
199 | "card": card
200 | }, params=self.protocol_params)
201 | if result["status"] == "ok":
202 | return True
203 | return False
204 |
205 | async def setGroupName(self,
206 | group_id: int,
207 | group_name: str) -> bool:
208 | result = await fetch.http_post(f"{self.baseurl_http}/set_group_name", {
209 | "group_id": group_id,
210 | "group_name": group_name
211 | }, params=self.protocol_params)
212 | if result["status"] == "ok":
213 | return True
214 | return False
215 |
216 | async def leave(self,
217 | group_id: int,
218 | is_dismiss: bool = False) -> bool:
219 | result = await fetch.http_post(f"{self.baseurl_http}/set_group_leave", {
220 | "group_id": group_id,
221 | "is_dismiss": is_dismiss
222 | }, params=self.protocol_params)
223 | if result["status"] == "ok":
224 | return True
225 | return False
226 |
227 | async def setGroupSpecialTitle(self,
228 | group_id: int,
229 | user_id: int,
230 | special_title: str = "",
231 | duration: int = -1) -> bool:
232 | result = await fetch.http_post(f"{self.baseurl_http}/set_group_special_title", {
233 | "group_id": group_id,
234 | "user_id": user_id,
235 | "special_title": special_title,
236 | "duration": duration
237 | }, params=self.protocol_params)
238 | if result["status"] == "ok":
239 | return True
240 | return False
241 |
242 | async def setFriendRequest(self,
243 | flag: str,
244 | approve: bool = True,
245 | remark: str = "") -> bool:
246 | result = await fetch.http_post(f"{self.baseurl_http}/set_friend_add_request", {
247 | "flag": flag,
248 | "approve": approve,
249 | "remark": remark
250 | }, params=self.protocol_params)
251 | if result["status"] == "ok":
252 | return True
253 | return False
254 |
255 | async def setGroupRequest(self,
256 | flag: str,
257 | sub_type: str,
258 | approve: bool = True,
259 | reason: str = "") -> bool:
260 | if sub_type not in ["add", "invite"]:
261 | return False
262 | result = await fetch.http_post(f"{self.baseurl_http}/set_group_add_request", {
263 | "flag": flag,
264 | "sub_type": sub_type,
265 | "approve": approve,
266 | "reason": reason
267 | }, params=self.protocol_params)
268 | if result["status"] == "ok":
269 | return True
270 | return False
271 |
272 | async def getLoginInfo(self) -> T.Union[Bot, bool]:
273 | result = await fetch.http_post(f"{self.baseurl_http}/get_login_info", params=self.protocol_params)
274 | if result["status"] == "ok":
275 | return Bot.parse_obj(result["data"])
276 | return False
277 |
278 | async def getQiDianAccountInfo(self) -> T.Union[QiDianAccount, bool]:
279 | result = await fetch.http_post(f"{self.baseurl_http}/qidian_get_account_info")
280 | if result["status"] == "ok":
281 | return QiDianAccount.parse_obj(result["data"])
282 | return False
283 |
284 | async def getStrangerInfo(self,
285 | user_id: int,
286 | no_cache: bool = False) -> T.Union[Stranger, bool]:
287 | result = await fetch.http_post(f"{self.baseurl_http}/get_stranger_info", {
288 | "user_id": user_id,
289 | "no_cache": no_cache
290 | }, params=self.protocol_params)
291 | if result["status"] == "ok":
292 | return Stranger.parse_obj(result["data"])
293 | return False
294 |
295 | async def getFriendList(self) -> T.Union[List[Friend], bool]:
296 | result = await fetch.http_post(f"{self.baseurl_http}/get_friend_list")
297 | if result["status"] == "ok":
298 | return [Friend.parse_obj(friend_info) for friend_info in result["data"]]
299 | return False
300 |
301 | async def deleteFriend(self,
302 | friend_id: int) -> bool:
303 | result = await fetch.http_post(f"{self.baseurl_http}/delete_friend", {
304 | "friend_id": friend_id
305 | }, params=self.protocol_params)
306 | if result["status"] == "ok":
307 | return True
308 | return False
309 |
310 | async def getUnidirectionalFriendList(self) -> T.Union[List[Friend], bool]:
311 | result = await fetch.http_post(f"{self.baseurl_http}/get_unidirectional_friend_list")
312 | if result["status"] == "ok":
313 | return [Friend.parse_obj(friend_info) for friend_info in result["data"]]
314 | return False
315 |
316 | async def deleteUnidirectionalFriend(self,
317 | user_id: int) -> bool:
318 | result = await fetch.http_post(f"{self.baseurl_http}/delete_unidirectional_friend", {
319 | "user_id": user_id
320 | }, params=self.protocol_params)
321 | if result["status"] == "ok":
322 | return True
323 | return False
324 |
325 | async def getGroupInfo(self,
326 | group_id: int,
327 | no_cache: bool = False) -> T.Union[Group, bool]:
328 | result = await fetch.http_post(f"{self.baseurl_http}/get_group_info", {
329 | "group_id": group_id,
330 | "no_cache": no_cache
331 | }, params=self.protocol_params)
332 | if result["status"] == "ok":
333 | return Group.parse_obj(result["data"])
334 | return False
335 |
336 | async def getGroupList(self) -> T.Union[List[Group], bool]:
337 | result = await fetch.http_post(f"{self.baseurl_http}/get_group_list")
338 | if result["status"] == "ok":
339 | return [Group.parse_obj(group_info) for group_info in result["data"]]
340 | return False
341 |
342 | async def getGroupMemberInfo(self,
343 | group_id: int,
344 | user_id: int,
345 | no_cache: bool = False) -> T.Union[Member, bool]:
346 | result = await fetch.http_post(f"{self.baseurl_http}/get_group_member_info", {
347 | "group_id": group_id,
348 | "user_id": user_id,
349 | "no_cache": no_cache
350 | }, params=self.protocol_params)
351 | if result["status"] == "ok":
352 | return Member.parse_obj(result["data"])
353 | return False
354 |
355 | async def getGroupMemberList(self,
356 | group_id: int) -> T.Union[List[Member], bool]:
357 | result = await fetch.http_post(f"{self.baseurl_http}/get_group_member_list", {
358 | "group_id": group_id
359 | }, params=self.protocol_params)
360 | if result["status"] == "ok":
361 | return [Member.parse_obj(member_info) for member_info in result["data"]]
362 | return False
363 |
364 | async def getGroupHonorInfo(self,
365 | group_id: int,
366 | type: str) -> T.Union[Honor, bool]:
367 | result = await fetch.http_post(f"{self.baseurl_http}/get_group_honor_info", {
368 | "group_id": group_id,
369 | "type": type
370 | }, params=self.protocol_params)
371 | if result["status"] == "ok":
372 | return Honor.parse_obj(result["data"])
373 | return False
374 |
375 | async def canSendImage(self) -> bool:
376 | result = await fetch.http_post(f"{self.baseurl_http}/can_send_image")
377 | if result["status"] == "ok":
378 | if result["data"]["yes"]:
379 | return True
380 | return False
381 |
382 | async def canSendRecord(self) -> bool:
383 | result = await fetch.http_post(f"{self.baseurl_http}/can_send_record")
384 | if result["status"] == "ok":
385 | if result["data"]["yes"]:
386 | return True
387 | return False
388 |
389 | async def getVersionInfo(self) -> T.Union[AppVersion, bool]:
390 | result = await fetch.http_post(f"{self.baseurl_http}/get_version_info")
391 | if result["status"] == "ok":
392 | return AppVersion.parse_obj(result["data"])
393 | return False
394 |
395 | async def restartAPI(self, delay: int = 0) -> bool:
396 | result = await fetch.http_post(f"{self.baseurl_http}/set_restart", {
397 | "delay": delay
398 | }, params=self.protocol_params)
399 | if result["status"] == "ok":
400 | return True
401 | return False
402 |
403 | async def setGroupPortrait(self,
404 | group_id: int,
405 | file: str,
406 | cache: int) -> bool:
407 | result = await fetch.http_post(f"{self.baseurl_http}/set_restart", {
408 | "group_id": group_id,
409 | "file": file,
410 | "cache": cache
411 | }, params=self.protocol_params)
412 | if result["status"] == "ok":
413 | return True
414 | return False
415 |
416 | async def ocrImage(self,
417 | image: str) -> T.Union[OCR, bool]:
418 | result = await fetch.http_post(f"{self.baseurl_http}/ocr_image", {
419 | "image": image
420 | }, params=self.protocol_params)
421 | if result["status"] == "ok":
422 | return OCR.parse_obj(result["data"])
423 | return False
424 |
425 | async def getGroupSystemMessage(self) -> T.Union[GroupSystemMessage, bool]:
426 | result = await fetch.http_post(f"{self.baseurl_http}/get_group_system_msg")
427 | if result["status"] == "ok":
428 | return GroupSystemMessage.parse_obj(result["data"])
429 | return False
430 |
431 | async def uploadGroupFile(self, group_id: int, file: str, name: str):
432 | result = await fetch.http_post(f"{self.baseurl_http}/upload_group_file", {
433 | "group_id": group_id,
434 | "file": file,
435 | "name": name
436 | }, params=self.protocol_params)
437 | if result["status"] == "ok":
438 | return True
439 | return False
440 |
441 | async def getGroupFileSystemInfo(self, group_id: int) -> T.Union[GroupFileSystem, bool]:
442 | result = await fetch.http_post(f"{self.baseurl_http}/get_group_file_system_info", {
443 | "group_id": group_id
444 | }, params=self.protocol_params)
445 | if result["status"] == "ok":
446 | return GroupFileSystem.parse_obj(result["data"])
447 | return False
448 |
449 | async def getGroupRootFiles(self, group_id: int) -> T.Union[GroupFileTree, bool]:
450 | result = await fetch.http_post(f"{self.baseurl_http}/get_group_root_files", {
451 | "group_id": group_id
452 | }, params=self.protocol_params)
453 | if result["status"] == "ok":
454 | return GroupFileTree.parse_obj(result["data"])
455 | return False
456 |
457 | async def getGroupFilesByFolder(self,
458 | group_id: int,
459 | folder_id: str) -> T.Union[GroupFileTree, bool]:
460 | result = await fetch.http_post(f"{self.baseurl_http}/get_group_root_files", {
461 | "group_id": group_id,
462 | "folder_id": folder_id
463 | }, params=self.protocol_params)
464 | if result["status"] == "ok":
465 | return GroupFileTree.parse_obj(result["data"])
466 | return False
467 |
468 | async def getGroupFileURL(self,
469 | group_id: int,
470 | file_id: str,
471 | busid: int) -> T.Union[str, bool]:
472 | result = await fetch.http_post(f"{self.baseurl_http}/get_group_file_url", {
473 | "group_id": group_id,
474 | "file_id": file_id,
475 | "busid": busid
476 | }, params=self.protocol_params)
477 | if result["status"] == "ok":
478 | return result["data"]["url"]
479 | return False
480 |
481 | async def getStatus(self) -> T.Union[AppStatus, bool]:
482 | result = await fetch.http_post(f"{self.baseurl_http}/get_status")
483 | if result["status"] == "ok":
484 | return AppStatus.parse_obj(result["data"])
485 | return False
486 |
487 | async def getGroupAtAllRemain(self, group_id: int) -> T.Union[AtAllRemain, bool]:
488 | result = await fetch.http_post(f"{self.baseurl_http}/get_group_at_all_remain", {
489 | "group_id": group_id
490 | }, params=self.protocol_params)
491 | if result["status"] == "ok":
492 | return AtAllRemain.parse_obj(result["data"])
493 | return False
494 |
495 | async def getVipInfo(self, user_id: int) -> T.Union[VipInfo, bool]:
496 | result = await fetch.http_post(f"{self.baseurl_http}/_get_vip_info", {
497 | "user_id": user_id
498 | }, params=self.protocol_params)
499 | if result["status"] == "ok":
500 | return VipInfo.parse_obj(result["data"])
501 | return False
502 |
503 | async def sendGroupNotice(self, group_id: int, content: str):
504 | result = await fetch.http_post(f"{self.baseurl_http}/_send_group_notice", {
505 | "group_id": group_id,
506 | "content": content
507 | }, params=self.protocol_params)
508 | if result["status"] == "ok":
509 | return True
510 | return False
511 |
512 | async def reloadEventFilter(self, file: str):
513 | result = await fetch.http_post(f"{self.baseurl_http}/reload_event_filter", {
514 | "file": file
515 | }, params=self.protocol_params)
516 | if result["status"] == "ok":
517 | return True
518 | return False
519 |
520 | async def downloadFile(self, url: str, headers: str, thread_count=1):
521 | result = await fetch.http_post(f"{self.baseurl_http}/download_file", {
522 | "url": url,
523 | "headers": headers,
524 | "thread_count": thread_count
525 | }, params=self.protocol_params)
526 | if result["status"] == "ok":
527 | return True
528 | return False
529 |
530 | async def getOnlineClients(self, no_cashe: bool) -> T.Union[List[Device], bool]:
531 | result = await fetch.http_post(f"{self.baseurl_http}/get_online_clients", {
532 | "no_cache": no_cashe
533 | }, params=self.protocol_params)
534 | if result["status"] == "ok":
535 | return [Device.parse_obj(device) for device in result["data"]["clients"]]
536 | return False
537 |
538 | async def getGroupMessageHistory(self, group_id: int, message_seq: Optional[int] = None) -> T.Union[List[Message], bool]:
539 | result = await fetch.http_post(f"{self.baseurl_http}/get_group_msg_history", {
540 | "message_seq": message_seq,
541 | "group_id": group_id
542 | }, params=self.protocol_params)
543 | if result["status"] == "ok":
544 | print(result)
545 | return [Message.parse_obj(message) for message in result["data"]["messages"]]
546 | return False
547 |
548 | async def setEssenceMessage(self, message_id: int) -> bool:
549 | result = await fetch.http_post(f"{self.baseurl_http}/set_essence_msg", {
550 | "message_id": message_id
551 | }, params=self.protocol_params) # 草,为什么只有手机看得到
552 | if result["status"] == "ok":
553 | return True
554 | return False
555 |
556 | async def deleteEssenceMessage(self, message_id: int) -> bool:
557 | result = await fetch.http_post(f"{self.baseurl_http}/delete_essence_msg", {
558 | "message_id": message_id
559 | }, params=self.protocol_params) # 这个我没测试过
560 | if result["status"] == "ok":
561 | return True
562 | return False
563 |
564 | async def getEssenceMessageList(self, group_id: int) -> T.Union[EssenceMessage, bool]:
565 | result = await fetch.http_post(f"{self.baseurl_http}/get_essence_msg_list", {
566 | "group_id": group_id
567 | }, params=self.protocol_params)
568 | if result["status"] == "ok":
569 | return EssenceMessage.parse_obj(result["data"])
570 | return False
571 |
572 | async def checkURLSafety(self, url: str) -> int:
573 | result = await fetch.http_post(f"{self.baseurl_http}/check_url_safely", {
574 | "url": url
575 | }, params=self.protocol_params)
576 | if result["status"] == "ok":
577 | return result["level"]
578 | return False
579 |
580 | async def getModelShow(self, model: str) -> T.Union[List[ModelShow], bool]:
581 | result = await fetch.http_post(f"{self.baseurl_http}/_get_model_show", {
582 | "model": model
583 | }, params=self.protocol_params)
584 | if result["status"] == "ok":
585 | return [ModelShow.parse_obj(_model) for _model in result["data"]["variants"]]
586 | return False
587 |
588 | async def setModelShow(self, model: str, model_show: str) -> bool:
589 | result = await fetch.http_post(f"{self.baseurl_http}/_set_model_show", {
590 | "model": model,
591 | "model_show": model_show
592 | }, params=self.protocol_params)
593 | if result["status"] == "ok":
594 | return True
595 | return False
596 |
597 | async def getGuildServiceProfile(self) -> T.Union[BotGuild, bool]:
598 | '''
599 | 获取频道系统内BOT的资料
600 | '''
601 | result = await fetch.http_post(f"{self.baseurl_http}/get_guild_service_profile")
602 | if result["status"] == "ok":
603 | return BotGuild.parse_obj(result["data"])
604 | return False
605 |
606 | async def getGuildList(self) -> T.Union[list, bool]:
607 | '''
608 | 获取频道列表
609 | '''
610 | result = await fetch.http_post(f"{self.baseurl_http}/get_guild_list")
611 | if result["status"] == "ok":
612 | return [Guild.parse_obj(_guild) for _guild in result["data"]]
613 | return False
614 |
615 | async def getGuildMetaByGuest(self, guild_id: int) -> T.Union[Guild, bool]:
616 | '''
617 | 通过访客获取频道元数据
618 | '''
619 | result = await fetch.http_post(f"{self.baseurl_http}/get_guild_meta_by_guest", {
620 | "guild_id": guild_id
621 | }, params=self.protocol_params)
622 | if result["status"] == "ok":
623 | return Guild.parse_obj(result["data"])
624 | return False
625 |
626 | async def getGuildChannelList(self, guild_id: int, no_cache: bool = False) -> T.Union[list, bool]:
627 | '''
628 | 获取子频道列表
629 | '''
630 | result = await fetch.http_post(f"{self.baseurl_http}/get_guild_channel_list", {
631 | "guild_id": guild_id,
632 | "no_cache": no_cache
633 | }, params=self.protocol_params)
634 | if result["status"] == "ok":
635 | return [Channel.parse_obj(_channel) for _channel in result["data"]]
636 | return False
637 |
638 | async def getGuildMembers(self, guild_id: int) -> T.Union[GuildMembers, bool]:
639 | '''
640 | 获取频道成员列表
641 | '''
642 | result = await fetch.http_post(f"{self.baseurl_http}/get_guild_members", {
643 | "guild_id": guild_id
644 | }, params=self.protocol_params)
645 | if result["status"] == "ok":
646 | return GuildMembers.parse_obj(result["data"])
647 | return False
648 |
649 | async def sendGuildChannelMessage(self, guild_id: int, channel_id: int, message: T.Union[str, list]) -> T.Union[BotMessage, bool]:
650 | '''
651 | 发送信息到子频道
652 | '''
653 | if isinstance(message, list):
654 | _message = ""
655 | for chain in message:
656 | _message += chain.toString()
657 | message = _message
658 | result = await fetch.http_post(f"{self.baseurl_http}/send_guild_channel_msg", {
659 | "guild_id": guild_id,
660 | "channel_id": channel_id,
661 | "message": message
662 | }, params=self.protocol_params)
663 | if result["status"] == "ok":
664 | return BotMessage.parse_obj(result["data"])
665 | return False
666 |
667 | async def getGuildRoles(self, guild_id: int) -> T.Union[Role, bool]:
668 | '''
669 | 获取频道身份组列表
670 | '''
671 | result = await fetch.http_post(f"{self.baseurl_http}/get_guild_roles", {
672 | "guild_id": guild_id
673 | }, params=self.protocol_params)
674 | if result["status"] == "ok":
675 | return [Role.parse_obj(_role) for _role in result["data"]]
676 | return False
677 |
678 | async def createGuildRole(self, guild_id: int, name: str, color: int, independent: bool, initial_users: T.List[int] = []) -> T.Union[Role, bool]:
679 | '''
680 | 创建频道身份组
681 | '''
682 | result = await fetch.http_post(f"{self.baseurl_http}/create_guild_role", {
683 | "guild_id": guild_id,
684 | "name": name,
685 | "color": color,
686 | "independent": independent,
687 | "initial_users": initial_users
688 | }, params=self.protocol_params)
689 | if result["status"] == "ok":
690 | return Role.parse_obj(result["data"])
691 | return False
692 |
693 | async def deleteGuildRole(self, guild_id: int, role_id: int) -> bool:
694 | '''
695 | 删除频道身份组
696 | '''
697 | result = await fetch.http_post(f"{self.baseurl_http}/delete_guild_role", {
698 | "guild_id": guild_id,
699 | "role_id": role_id
700 | }, params=self.protocol_params)
701 | if result["status"] == "ok":
702 | return True
703 | return False
704 |
705 | async def setGuildMemberRole(self, guild_id: int, role_id: int, users: T.List[int]) -> bool:
706 | '''
707 | 设置用户在频道中的身份组
708 | '''
709 | result = await fetch.http_post(f"{self.baseurl_http}/set_guild_member_role", {
710 | "guild_id": guild_id,
711 | "role_id": role_id,
712 | "users": users
713 | }, params=self.protocol_params)
714 | if result["status"] == "ok":
715 | return True
716 | return False
717 |
718 | async def editGuildRole(self, guild_id: int, role_id: int, name: str, color: int, independent: bool) -> bool:
719 | '''
720 | 设置用户在频道中的身份组
721 | '''
722 | result = await fetch.http_post(f"{self.baseurl_http}/update_guild_role", {
723 | "guild_id": guild_id,
724 | "role_id": role_id,
725 | "name": name,
726 | "color": color,
727 | "independent": independent # TODO gocq 中参数名写错了,待后续确认
728 | }, params=self.protocol_params)
729 | if result["status"] == "ok":
730 | return True
731 | return False
732 |
733 | async def getTopicChannelFeeds(self, guild_id: int, channel_id: int) -> T.Union[T.List[TopicChannelFeed], bool]:
734 | '''
735 | 获取论坛子频道帖子列表
736 | '''
737 | result = await fetch.http_post(f"{self.baseurl_http}/get_topic_channel_feeds", {
738 | "guild_id": guild_id,
739 | "channel_id": channel_id
740 | }, params=self.protocol_params)
741 | if result["status"] == "ok":
742 | return [TopicChannelFeed.parse_obj(_feed) for _feed in result["data"]]
743 | return False
744 |
--------------------------------------------------------------------------------