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