diff --git a/.flake8 b/.flake8 index 49b26c1..5b814f7 100644 --- a/.flake8 +++ b/.flake8 @@ -3,12 +3,13 @@ ignore = ;W503 line break before binary operator W503, ;E203 whitespace before ':' - E203, + E203 ; exclude file exclude = .tox, .git, + __init__.py, __pycache__, build, dist, diff --git a/README.md b/README.md index b6f34fb..5cb3916 100644 --- a/README.md +++ b/README.md @@ -38,12 +38,28 @@ import qqbot import qqbot token = qqbot.Token("{appid}","{token}") -api = qqbot.UserAPITestCase(token, False) +api = qqbot.UserAPI(token, False) + user = api.me() print(user.username) # 打印机器人名字 ``` +async 示例: + +``` py +import qqbot + +token = qqbot.Token("{appid}","{token}") +api = qqbot.AsyncUserAPI(token, False) + +# 获取loop +loop = asyncio.get_event_loop() +user = loop.run_until_complete(api.me()) + +print(user.username) # 打印机器人名字 +``` + ## qqbot-事件监听 异步模块基于 websocket 技术用于监听频道内的相关事件,如消息、成员变化等事件,用于开发者对事件进行相应的处理。 @@ -78,7 +94,34 @@ print(user.username) # 打印机器人名字 msg_api.post_message(message.channel_id, send) ``` - 注:当前支持事件及回调数据对象为: +- async 示例: + + ``` py + # async的异步接口的使用示例 + t_token = qqbot.Token(test_config["token"]["appid"], test_config["token"]["token"]) + qqbot_handler = qqbot.Handler(qqbot.HandlerType.AT_MESSAGE_EVENT_HANDLER, _message_handler) + qqbot.async_listen_events(t_token, False, qqbot_handler) + ``` + ``` py + async def _message_handler(event, message: qqbot.Message): + """ + 定义事件回调的处理 + + :param event: 事件类型 + :param message: 事件对象(如监听消息是Message对象) + """ + msg_api = qqbot.AsyncMessageAPI(t_token, False) + # 打印返回信息 + qqbot.logger.info("event %s" % event + ",receive message %s" % message.content) + for i in range(5): + await asyncio.sleep(5) + # 构造消息发送请求数据对象 + send = qqbot.MessageSendRequest("<@%s>谢谢你,加油 " % message.author.id, message.id) + # 通过api发送回复消息 + await msg_api.post_message(message.channel_id, send) + + ``` +- 注:当前支持事件及回调数据对象为: ``` py class HandlerType(Enum): diff --git a/README.rst b/README.rst index b3f41d8..7b41550 100644 --- a/README.rst +++ b/README.rst @@ -2,19 +2,18 @@ qq-bot-python ============= sdk安装 -======= +------- 外发版本通过下面方式安装 .. code:: bash - pip install qq-bot - + pip install qq-bot # 注意是 qq-bot 而不是 qqbot! 更新包的话需要添加 ``--upgrade`` ``注:需要python3.7+`` sdk使用 -======= +------- 需要使用的地方import SDK @@ -22,6 +21,11 @@ sdk使用 import qqbot +示例机器人 +---------- + +```examples`` <./examples/>`__ 目录下存放示例机器人,可供实现参考。 + qqbot-API --------- @@ -41,11 +45,27 @@ qqbot-API import qqbot token = qqbot.Token("{appid}","{token}") - api = qqbot.UserAPITestCase(token, False) + api = qqbot.UserAPI(token, False) + user = api.me() print(user.username) # 打印机器人名字 +async 示例: + +.. code:: py + + import qqbot + + token = qqbot.Token("{appid}","{token}") + api = qqbot.AsyncUserAPI(token, False) + + # 获取loop + loop = asyncio.get_event_loop() + user = loop.run_until_complete(api.me()) + + print(user.username) # 打印机器人名字 + qqbot-事件监听 -------------- @@ -62,59 +82,89 @@ qqbot-事件监听 比如下面这个例子:需要监听机器人被@后消息并进行相应的回复。 - 先初始化需要用的 ``token`` 对象 + - 通过 ``qqbot.listen_events`` 注册需要监听的事件 + - 通过 ``qqbot.HandlerType`` 定义需要监听的事件(部分事件可能需要权限申请) -.. code:: py - + .. code:: py - t_token = qqbot.Token(test_config["token"]["appid"], test_config["token"]["token"]) - # 注册事件类型和回调,可以注册多个 - qqbot_handler = qqbot.Handler(qqbot.HandlerType.AT_MESSAGE_EVENT_HANDLER, _message_handler) - qqbot.listen_events(t_token, False, qqbot_handler) + t_token = qqbot.Token(test_config["token"]["appid"], test_config["token"]["token"]) + # 注册事件类型和回调,可以注册多个 + qqbot_handler = qqbot.Handler(qqbot.HandlerType.AT_MESSAGE_EVENT_HANDLER, _message_handler) + qqbot.listen_events(t_token, False, qqbot_handler) - 最后定义注册事件回调执行函数,如 ``_message_handler`` 。 -.. code:: py - - def _message_handler(event, message: Message): - msg_api = qqbot.MessageAPI(t_token, False) - # 打印返回信息 - qqbot.logger.info("event %s" % event + ",receive message %s" % message.content) - # 构造消息发送请求数据对象 - send = qqbot.MessageSendRequest("<@%s>谢谢你,加油" % message.author.id, message.id) - # 通过api发送回复消息 - msg_api.post_message(message.channel_id, send) - -注:当前支持事件及回调数据对象为: - -.. code:: py - - class HandlerType(Enum): - PLAIN_EVENT_HANDLER = 0 #透传事件 - GUILD_EVENT_HANDLER = 1 #频道事件 - GUILD_MEMBER_EVENT_HANDLER = 2 #频道成员事件 - CHANNEL_EVENT_HANDLER = 3 #子频道事件 - MESSAGE_EVENT_HANDLER = 4 #消息事件 - AT_MESSAGE_EVENT_HANDLER = 5 #At消息事件 - # DIRECT_MESSAGE_EVENT_HANDLER = 6 #私信消息事件 - # AUDIO_EVENT_HANDLER = 7 #音频事件 - -事件回调函数的参数 1 为事件名称,参数 2 返回具体的数据对象。 - -.. code:: py - - #透传事件(无具体的数据对象,根据后台返回Json对象) - def _plain_handler(event, data): - #频道事件 - def _guild_handler(event, guild:Guild): - #频道成员事件 - def _guild_member_handler(event, guild_member: GuildMember): - #子频道事件 - def _channel_handler(event, channel: Channel): - #消息事件 #At消息事件 - def _message_handler(event, message: Message): + .. code:: py + + def _message_handler(event, message: Message): + msg_api = qqbot.MessageAPI(t_token, False) + # 打印返回信息 + qqbot.logger.info("event %s" % event + ",receive message %s" % message.content) + # 构造消息发送请求数据对象 + send = qqbot.MessageSendRequest("<@%s>谢谢你,加油" % message.author.id, message.id) + # 通过api发送回复消息 + msg_api.post_message(message.channel_id, send) + +- async 示例: + + .. code:: py + + # async的异步接口的使用示例 + t_token = qqbot.Token(test_config["token"]["appid"], test_config["token"]["token"]) + qqbot_handler = qqbot.Handler(qqbot.HandlerType.AT_MESSAGE_EVENT_HANDLER, _message_handler) + qqbot.async_listen_events(t_token, False, qqbot_handler) + + .. code:: py + + async def _message_handler(event, message: qqbot.Message): + """ + 定义事件回调的处理 + + :param event: 事件类型 + :param message: 事件对象(如监听消息是Message对象) + """ + msg_api = qqbot.AsyncMessageAPI(t_token, False) + # 打印返回信息 + qqbot.logger.info("event %s" % event + ",receive message %s" % message.content) + for i in range(5): + await asyncio.sleep(5) + # 构造消息发送请求数据对象 + send = qqbot.MessageSendRequest("<@%s>谢谢你,加油 " % message.author.id, message.id) + # 通过api发送回复消息 + await msg_api.post_message(message.channel_id, send) + +- 注:当前支持事件及回调数据对象为: + + .. code:: py + + class HandlerType(Enum): + PLAIN_EVENT_HANDLER = 0 # 透传事件 + GUILD_EVENT_HANDLER = 1 # 频道事件 + GUILD_MEMBER_EVENT_HANDLER = 2 # 频道成员事件 + CHANNEL_EVENT_HANDLER = 3 # 子频道事件 + MESSAGE_EVENT_HANDLER = 4 # 消息事件 + AT_MESSAGE_EVENT_HANDLER = 5 # At消息事件 + # DIRECT_MESSAGE_EVENT_HANDLER = 6 # 私信消息事件 + # AUDIO_EVENT_HANDLER = 7 # 音频事件 + + 事件回调函数的参数 1 为事件名称,参数 2 返回具体的数据对象。 + + .. code:: py + + # 透传事件(无具体的数据对象,根据后台返回Json对象) + def _plain_handler(event, data): + # 频道事件 + def _guild_handler(event, guild:Guild): + # 频道成员事件 + def _guild_member_handler(event, guild_member: GuildMember): + # 子频道事件 + def _channel_handler(event, channel: Channel): + # 消息事件 + # At消息事件 + def _message_handler(event, message: Message): 日志打印 -------- @@ -177,3 +227,54 @@ NOTSET 0 export QQBOT_DISABLE_LOG=1 # 1表示禁用日志 +sdk开发 +======= + +环境配置 +-------- + +.. code:: bash + + pip install -r requirements.txt # 安装依赖的pip包 + + pre-commit install # 安装格式化代码的钩子 + + python3 setup.py sdist bdist_wheel # 打包SDK + +单元测试 +-------- + +代码库提供API接口测试和 websocket 的单测用例,位于 ``tests`` +目录中。如果需要自己运行,可以在 ``tests`` 目录重命名 ``.test.yaml`` +文件后添加自己的测试参数启动测试: + +.. code:: yaml + + # test yaml 用于设置test相关的参数,开源版本需要去掉参数 + token: + appid: "xxx" + token: "xxxxx" + test_params: + guild_id: "xx" + guild_owner_id: "xx" + guild_owner_name: "xx" + guild_test_member_id: "xx" + guild_test_role_id: "xx" + channel_id: "xx" + channel_name: "xx" + robot_name: "xxx" + is_sandbox: False + +单测执行方法: + +先确保已安装 ``pytest`` : + +.. code:: bash + + pip install pytest + +然后在项目根目录下执行单测: + +.. code:: bash + + pytest diff --git a/examples/run_websocket.py b/examples/run_websocket.py index 0edc866..3657e48 100644 --- a/examples/run_websocket.py +++ b/examples/run_websocket.py @@ -19,7 +19,7 @@ def _message_handler(event, message: qqbot.Message): # 打印返回信息 qqbot.logger.info("event %s" % event + ",receive message %s" % message.content) # 构造消息发送请求数据对象 - send = qqbot.MessageSendRequest("<@%s>谢谢你,加油" % message.author.id, message.id) + send = qqbot.MessageSendRequest("收到你的消息: %s" % message.content) # 通过api发送回复消息 msg_api.post_message(message.channel_id, send) @@ -35,7 +35,7 @@ def _direct_message_handler(event, message: qqbot.Message): # 打印返回信息 qqbot.logger.info("event %s" % event + ",receive message %s" % message.content) # 构造消息发送请求数据对象 - send = qqbot.MessageSendRequest("<@%s>谢谢你,加油" % message.author.id, message.id) + send = qqbot.MessageSendRequest("收到你的私信消息了:%s" % message.content, message.id) # 通过api发送回复消息 msg_api.post_direct_message(message.guild_id, send) @@ -46,3 +46,9 @@ def _direct_message_handler(event, message: qqbot.Message): qqbot.HandlerType.AT_MESSAGE_EVENT_HANDLER, _message_handler ) qqbot.listen_events(t_token, False, qqbot_handler) + + # # 多事件监听 + # qqbot_dms_handler = qqbot.Handler( + # qqbot.HandlerType.DIRECT_MESSAGE_EVENT_HANDLER, _direct_message_handler + # ) + # qqbot.listen_events(t_token, False, qqbot_handler, qqbot_dms_handler) diff --git a/examples/run_websocket_async.py b/examples/run_websocket_async.py new file mode 100644 index 0000000..d54328b --- /dev/null +++ b/examples/run_websocket_async.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import asyncio +import os.path + +import qqbot +from qqbot.core.util.yaml_util import YamlUtil + +test_config = YamlUtil.read(os.path.join(os.path.dirname(__file__), "config.yaml")) + + +async def _message_handler(event, message: qqbot.Message): + """ + 定义事件回调的处理 + + :param event: 事件类型 + :param message: 事件对象(如监听消息是Message对象) + """ + msg_api = qqbot.AsyncMessageAPI(t_token, False) + # 打印返回信息 + qqbot.logger.info("event %s" % event + ",receive message %s" % message.content) + for i in range(5): + await asyncio.sleep(5) + # 构造消息发送请求数据对象 + send = qqbot.MessageSendRequest("<@%s>谢谢你,加油 " % message.author.id, message.id) + # 通过api发送回复消息 + await msg_api.post_message(message.channel_id, send) + + +if __name__ == "__main__": + # async的异步接口的使用示例 + t_token = qqbot.Token(test_config["token"]["appid"], test_config["token"]["token"]) + qqbot_handler = qqbot.Handler( + qqbot.HandlerType.AT_MESSAGE_EVENT_HANDLER, _message_handler + ) + qqbot.async_listen_events(t_token, False, qqbot_handler) diff --git a/qqbot/__init__.py b/qqbot/__init__.py index 3f45d01..96fffe6 100644 --- a/qqbot/__init__.py +++ b/qqbot/__init__.py @@ -1,524 +1,32 @@ # -*- coding: utf-8 -*- -import json -from enum import Enum -from typing import List -from qqbot.core.network.http import Http, HttpStatus -from qqbot.core.network.url import get_url, APIConstant -from qqbot.core.network.websocket.ws_intents_handler import register_handlers, Handler -from qqbot.core.network.websocket.ws_session_manager import SessionManager -from qqbot.core.util import logging -from qqbot.core.util.json_util import JsonUtil -from qqbot.model.audio import AudioControl -from qqbot.model.channel import ( - Channel, - ChannelResponse, - CreateChannelRequest, - PatchChannelRequest, +from .api import ( + listen_events, + GuildAPI, + GuildRoleAPI, + GuildMemberAPI, + ChannelAPI, + ChannelPermissionsAPI, + UserAPI, + AudioAPI, + MessageAPI, + WebsocketAPI, ) -from qqbot.model.channel_permissions import ( - ChannelPermissions, - ChannelPermissionsUpdateRequest, -) -from qqbot.model.guild import Guild -from qqbot.model.guild_member import QueryParams -from qqbot.model.guild_role import ( - GuildRoles, - RoleUpdateResult, - RoleUpdateRequest, - RoleUpdateFilter, - RoleUpdateInfo, -) -from qqbot.model.member import User, Member -from qqbot.model.message import ( - MessageSendRequest, - Message, - CreateDirectMessageRequest, - DirectMessageGuild, - MessagesPager, + +from .async_api import ( + async_listen_events, + AsyncGuildAPI, + AsyncGuildRoleAPI, + AsyncGuildMemberAPI, + AsyncChannelAPI, + AsyncChannelPermissionsAPI, + AsyncUserAPI, + AsyncAudioAPI, + AsyncMessageAPI, + AsyncWebsocketAPI, ) -from qqbot.model.token import Token +from .core.network.ws.ws_intents_handler import HandlerType, Handler +from .core.util import logging +from .model import * logger = logging.getLogger(__name__) - - -class HandlerType(Enum): - PLAIN_EVENT_HANDLER = 0 - GUILD_EVENT_HANDLER = 1 - GUILD_MEMBER_EVENT_HANDLER = 2 - CHANNEL_EVENT_HANDLER = 3 - MESSAGE_EVENT_HANDLER = 4 - AT_MESSAGE_EVENT_HANDLER = 5 - DIRECT_MESSAGE_EVENT_HANDLER = 6 - AUDIO_EVENT_HANDLER = 7 - - -def listen_events(t_token: Token, is_sandbox: bool, *handlers: Handler): - """ - 注册并监听频道相关事件 - - :param t_token: Token对象 - :param handlers: 包含事件类型和事件回调的Handler对象,支持多个对象 - :param is_sandbox:是否沙盒环境,默认为False - """ - # 通过api获取websocket链接 - ws_api = WebsocketAPI(t_token, is_sandbox) - ws_ap = ws_api.ws() - # 新建和注册监听事件 - t_intent = register_handlers(handlers) - # 实例一个session_manager - manager = SessionManager() - manager.start(ws_ap, t_token.bot_token(), t_intent) - - -class APIBase: - timeout = 3 - - def __init__(self, token: Token, is_sandbox: bool): - """ - API初始化信息 - - :param token: Token对象 - :param is_sandbox: 是否沙盒环境 - """ - self.is_sandbox = is_sandbox - self.token = token - self.http = Http(self.timeout, token.get_string(), token.get_type()) - - def with_timeout(self, timeout): - self.timeout = timeout - return self - - -class GuildAPI(APIBase): - """ - 频道相关接口 - """ - - def get_guild(self, guild_id: str) -> Guild: - """ - 获取频道信息 - - :param guild_id: 频道ID(一般从事件中获取相关的ID信息) - :return: 频道Guild对象 - """ - url = get_url(APIConstant.guildURI, self.is_sandbox).format(guild_id=guild_id) - response = self.http.get(url) - return json.loads(response.content, object_hook=Guild) - - -class GuildRoleAPI(APIBase): - """ - 频道身份组相关接口 - """ - - def get_guild_roles(self, guild_id: str) -> GuildRoles: - """ - 获取频道身份组列表 - - :param guild_id:频道ID - :return:GuildRoles对象 - """ - url = get_url(APIConstant.rolesURI, self.is_sandbox).format(guild_id=guild_id) - response = self.http.get(url) - return json.loads(response.content, object_hook=GuildRoles) - - def create_guild_role( - self, guild_id: str, role_info: RoleUpdateInfo - ) -> RoleUpdateResult: - """ - 创建频道身份组 - - :param guild_id:频道ID - :param role_info:RoleUpdateInfo对象,需要自己创建的身份数据 - :return:RoleUpdateResult对象 - """ - url = get_url(APIConstant.rolesURI, self.is_sandbox).format(guild_id=guild_id) - params = RoleUpdateRequest() - params.filter = RoleUpdateFilter(1, 1, 1) - params.guild_id = guild_id - params.info = role_info - serialize = JsonUtil.obj2json_serialize(params) - response = self.http.post(url, request=serialize) - return json.loads(response.content, object_hook=RoleUpdateResult) - - def update_guild_role( - self, guild_id: str, role_id: str, role_info: RoleUpdateInfo - ) -> RoleUpdateResult: - """ - 修改频道身份组 - - :param guild_id:频道ID - :param role_id:身份组ID - :param role_info:更新后的RoleUpdateInfo对象 - :return:RoleUpdateResult对象 - """ - url = get_url(APIConstant.roleURI, self.is_sandbox).format( - guild_id=guild_id, role_id=role_id - ) - params = RoleUpdateRequest() - params.filter = RoleUpdateFilter(1, 1, 1) - params.guild_id = guild_id - params.info = role_info - serialize = JsonUtil.obj2json_serialize(params) - response = self.http.patch(url, request=serialize) - return json.loads(response.content, object_hook=RoleUpdateResult) - - def delete_guild_role(self, guild_id: str, role_id: str) -> bool: - """ - 删除频道身份组 - - :param guild_id: 频道ID - :param role_id: 身份组ID - :return: 是否删除成功 - """ - url = get_url(APIConstant.roleURI, self.is_sandbox).format( - guild_id=guild_id, role_id=role_id - ) - response = self.http.delete(url) - return response.status_code == HttpStatus.ACTION_OK - - def create_guild_role_member( - self, - guild_id: str, - role_id: str, - user_id: str, - role_req: Channel = None, - ) -> bool: - """ - 增加频道身份组成员 - 需要使用的 token 对应的用户具备删除身份组成员权限。如果是机器人,要求被添加为管理员。 - 如果要删除的身份组ID是5-子频道管理员,需要增加channel对象来指定具体是哪个子频道 - - :param guild_id:频道ID - :param role_id:身份组ID - :param user_id:用户ID - :param role_req:RoleMemberRequest数据对象 - :return:是否添加成功 - """ - url = get_url(APIConstant.memberRoleURI, self.is_sandbox).format( - guild_id=guild_id, role_id=role_id, user_id=user_id - ) - response = self.http.put(url, request=JsonUtil.obj2json_serialize(role_req)) - return response.status_code == HttpStatus.ACTION_OK - - def delete_guild_role_member( - self, - guild_id: str, - role_id: str, - user_id: str, - role_req: Channel = None, - ) -> bool: - """ - 删除频道身份组成员 - 需要使用的 token 对应的用户具备删除身份组成员权限。如果是机器人,要求被添加为管理员。 - 如果要删除的身份组ID是5-子频道管理员,需要增加channel对象来指定具体是哪个子频道 - - :param guild_id:频道ID - :param role_id:身份组ID - :param user_id:用户ID - :param role_req:RoleMemberRequest数据对象 - :return:是否删除成功 - """ - url = get_url(APIConstant.memberRoleURI, self.is_sandbox).format( - guild_id=guild_id, role_id=role_id, user_id=user_id - ) - response = self.http.delete(url, request=JsonUtil.obj2json_serialize(role_req)) - return response.status_code == HttpStatus.ACTION_OK - - -class GuildMemberAPI(APIBase): - """ - 成员相关接口,添加成员到用户组等 - """ - - def get_guild_member(self, guild_id: str, user_id: str) -> Member: - """ - 获取频道指定成员 - - :param guild_id:频道ID - :param user_id:用户ID(一般从事件消息中获取) - :return: - """ - url = get_url(APIConstant.guildMemberURI, self.is_sandbox).format( - guild_id=guild_id, user_id=user_id - ) - response = self.http.get(url) - return json.loads(response.content, object_hook=Member) - - def get_guild_members( - self, guild_id: str, guild_member_pager: QueryParams - ) -> List[Member]: - """ - 获取成员列表,需要申请接口权限 - - :param guild_id: 频道ID - :param guild_member_pager: GuildMembersPager分页数据对象 - :return: Member列表 - """ - url = get_url(APIConstant.guildMembersURI, self.is_sandbox).format( - guild_id=guild_id - ) - response = self.http.get(url, params=guild_member_pager.__dict__) - return json.loads(response.content, object_hook=Member) - - -class ChannelAPI(APIBase): - """子频道相关接口""" - - def get_channel(self, channel_id) -> Channel: - """ - 获取子频道信息 - - :param channel_id:子频道ID - :return:子频道对象Channel - """ - url = get_url(APIConstant.channelURI, self.is_sandbox).format( - channel_id=channel_id - ) - response = self.http.get(url) - return json.loads(response.content, object_hook=Channel) - - def get_channels(self, guild_id: str) -> List[Channel]: - """ - 获取频道下的子频道列表 - - :param guild_id: 频道ID - :return: Channel列表 - """ - url = get_url(APIConstant.channelsURI, self.is_sandbox).format( - guild_id=guild_id - ) - response = self.http.get(url) - return json.loads(response.content, object_hook=Channel) - - def create_channel( - self, guild_id: str, request: CreateChannelRequest - ) -> ChannelResponse: - """ - 创建子频道 - - :param guild_id: 频道ID - :param request: 创建子频道的请求对象CreateChannelRequest - :return ChannelResponse 对象 - """ - url = get_url(APIConstant.channelsURI, self.is_sandbox).format( - guild_id=guild_id - ) - request_json = JsonUtil.obj2json_serialize(request) - response = self.http.post(url, request_json) - return json.loads(response.content, object_hook=ChannelResponse) - - def update_channel( - self, channel_id: str, request: PatchChannelRequest - ) -> ChannelResponse: - """ - 修改子频道 - - :param channel_id: 频道ID - :param request: PatchChannelRequest - :return ChannelResponse 对象 - """ - url = get_url(APIConstant.channelURI, self.is_sandbox).format( - channel_id=channel_id - ) - request_json = JsonUtil.obj2json_serialize(request) - response = self.http.patch(url, request_json) - return json.loads(response.content, object_hook=ChannelResponse) - - def delete_channel(self, channel_id: str) -> ChannelResponse: - """ - 删除子频道 - - :param channel_id: 频道ID - :return ChannelResponse 对象 - """ - url = get_url(APIConstant.channelURI, self.is_sandbox).format( - channel_id=channel_id - ) - response = self.http.delete(url) - return json.loads(response.content, object_hook=ChannelResponse) - - -class ChannelPermissionsAPI(APIBase): - """子频道权限相关接口""" - - def get_channel_permissions( - self, channel_id: str, user_id: str - ) -> ChannelPermissions: - """ - 获取指定子频道的权限 - - :param channel_id:子频道ID - :param user_id:用户ID - :return:ChannelPermissions对象 - """ - url = get_url(APIConstant.channelPermissionsURI, self.is_sandbox).format( - channel_id=channel_id, user_id=user_id - ) - response = self.http.get(url) - return json.loads(response.content, object_hook=ChannelPermissions) - - def update_channel_permissions( - self, channel_id, user_id, request: ChannelPermissionsUpdateRequest - ) -> bool: - """ - 修改指定子频道的权限 - - :param channel_id:子频道ID - :param user_id:用户ID - :param request:ChannelPermissionsUpdateRequest数据对象(构造可以查看具体的对象注释) - :return: - """ - url = get_url(APIConstant.channelPermissionsURI, self.is_sandbox).format( - channel_id=channel_id, user_id=user_id - ) - if request.add != "": - request.add = str(int(request.add, 16)) - if request.remove != "": - request.remove = str(int(request.remove, 16)) - response = self.http.put(url, request=JsonUtil.obj2json_serialize(request)) - return response.status_code == HttpStatus.ACTION_OK - - -class MessageAPI(APIBase): - """消息""" - - def get_message(self, channel_id: str, message_id: str) -> Message: - """ - 获取指定消息 - - :param channel_id: 频道ID - :param message_id: 消息ID - :return: Message 对象 - """ - url = get_url(APIConstant.messageURI, self.is_sandbox).format( - channel_id=channel_id, message_id=message_id - ) - response = self.http.get(url) - return json.loads(response.content, object_hook=Message) - - def get_messages(self, channel_id: str, pager: MessagesPager) -> List[Message]: - """ - 获取指定消息列表 - - :param channel_id: 频道ID - :param pager: MessagesPager对象 - :return: Message 对象 - """ - url = get_url(APIConstant.messagesURI, self.is_sandbox).format( - channel_id=channel_id - ) - query = {} - if pager.limit != "": - query["limit"] = pager.limit - - if pager.type != "" and pager.id != "": - query[pager.type] = pager.id - - response = self.http.get(url, params=query) - return json.loads(response.content, object_hook=Message) - - def post_message( - self, channel_id: str, message_send: MessageSendRequest - ) -> Message: - """ - 发送消息 - - 要求操作人在该子频道具有发送消息的权限。 - 发送成功之后,会触发一个创建消息的事件。 - 被动回复消息有效期为 5 分钟 - 主动推送消息每日每个子频道限 2 条 - 发送消息接口要求机器人接口需要链接到websocket gateway 上保持在线状态 - - :param channel_id: 子频道ID - :param message_send: MessageSendRequest对象 - :return: Message对象 - """ - - url = get_url(APIConstant.messagesURI, self.is_sandbox).format( - channel_id=channel_id - ) - request_json = JsonUtil.obj2json_serialize(message_send) - response = self.http.post(url, request_json) - return json.loads(response.content, object_hook=Message) - - def create_direct_message( - self, create_direct_message: CreateDirectMessageRequest - ) -> DirectMessageGuild: - """ - 创建私信频道 - - :param create_direct_message: 构造request数据 - :return: 私信频道对象 - """ - url = get_url(APIConstant.userMeDMURI, self.is_sandbox) - request_json = JsonUtil.obj2json_serialize(create_direct_message) - response = self.http.post(url, request_json) - return json.loads(response.content, object_hook=DirectMessageGuild) - - def post_direct_message( - self, guild_id: str, message_send: MessageSendRequest - ) -> Message: - """ - 发送私信 - - :param guild_id: 创建的私信频道id - :param message_send: 发送消息的数据请求对象 MessageSendRequest - :return Message对象 - """ - url = get_url(APIConstant.dmsURI, self.is_sandbox).format(guild_id=guild_id) - request_json = JsonUtil.obj2json_serialize(message_send) - response = self.http.post(url, request_json) - return json.loads(response.content, object_hook=Message) - - -class AudioAPI(APIBase): - """音频接口""" - - def post_audio(self, channel_id: str, audio_control: AudioControl) -> bool: - """ - 音频控制 - - :param channel_id:频道ID - :param audio_control:AudioControl对象 - :return:是否成功 - """ - url = get_url(APIConstant.audioControlURI, self.is_sandbox).format( - channel_id=channel_id - ) - request_json = JsonUtil.obj2json_serialize(audio_control) - response = self.http.post(url, request=request_json) - return response.status_code == HttpStatus.ACTION_OK - - -class UserAPI(APIBase): - """用户相关接口""" - - def me(self) -> User: - """ - :return:使用当前用户信息填充的 User 对象 - """ - url = get_url(APIConstant.userMeURI, self.is_sandbox) - response = self.http.get(url) - return json.loads(response.content, object_hook=User) - - def me_guilds(self) -> List[Guild]: - """ - 当前用户所加入的 Guild 对象列表 - - :return:Guild对象列表 - """ - url = get_url(APIConstant.userMeGuildsURI, self.is_sandbox) - response = self.http.get(url) - return json.loads(response.content, object_hook=Guild) - - -class WebsocketAPI(APIBase): - """WebsocketAPI""" - - def ws(self): - url = get_url(APIConstant.gatewayBotURI, self.is_sandbox) - response = self.http.get(url) - websocket_ap = json.loads(response.content) - return websocket_ap diff --git a/qqbot/api.py b/qqbot/api.py new file mode 100644 index 0000000..99d25a0 --- /dev/null +++ b/qqbot/api.py @@ -0,0 +1,509 @@ +# -*- coding: utf-8 -*- +import json +from typing import List + +from qqbot.core.network.http import Http, HttpStatus +from qqbot.core.network.url import get_url, APIConstant +from qqbot.core.network.ws.ws_intents_handler import Handler, register_handlers +from qqbot.core.network.ws_sync.ws_session_manager import SessionManager +from qqbot.core.util.json_util import JsonUtil +from qqbot.model.audio import AudioControl +from qqbot.model.channel import ( + Channel, + ChannelResponse, + CreateChannelRequest, + PatchChannelRequest, +) +from qqbot.model.channel_permissions import ( + ChannelPermissions, + ChannelPermissionsUpdateRequest, +) +from qqbot.model.guild import Guild +from qqbot.model.guild_member import QueryParams +from qqbot.model.guild_role import ( + GuildRoles, + RoleUpdateResult, + RoleUpdateRequest, + RoleUpdateFilter, + RoleUpdateInfo, +) +from qqbot.model.member import User, Member +from qqbot.model.message import ( + MessageSendRequest, + Message, + CreateDirectMessageRequest, + DirectMessageGuild, + MessagesPager, +) +from qqbot.model.token import Token + + +def listen_events(t_token: Token, is_sandbox: bool, *handlers: Handler): + """ + 注册并监听频道相关事件 + + :param t_token: Token对象 + :param handlers: 包含事件类型和事件回调的Handler对象,支持多个对象 + :param is_sandbox:是否沙盒环境,默认为False + """ + # 通过api获取websocket链接 + ws_api = WebsocketAPI(t_token, is_sandbox) + ws_ap = ws_api.ws() + # 新建和注册监听事件 + t_intent = register_handlers(handlers) + # 实例一个session_manager + manager = SessionManager() + manager.start(ws_ap, t_token.bot_token(), t_intent) + + +class APIBase: + timeout = 3 + + def __init__(self, token: Token, is_sandbox: bool): + """ + API初始化信息 + + :param token: Token对象 + :param is_sandbox: 是否沙盒环境 + """ + self.is_sandbox = is_sandbox + self.token = token + self.http = Http(self.timeout, token.get_string(), token.get_type()) + + def with_timeout(self, timeout): + self.timeout = timeout + return self + + +class GuildAPI(APIBase): + """ + 频道相关接口 + """ + + def get_guild(self, guild_id: str) -> Guild: + """ + 获取频道信息 + + :param guild_id: 频道ID(一般从事件中获取相关的ID信息) + :return: 频道Guild对象 + """ + url = get_url(APIConstant.guildURI, self.is_sandbox).format(guild_id=guild_id) + response = self.http.get(url) + return json.loads(response.content, object_hook=Guild) + + +class GuildRoleAPI(APIBase): + """ + 频道身份组相关接口 + """ + + def get_guild_roles(self, guild_id: str) -> GuildRoles: + """ + 获取频道身份组列表 + + :param guild_id:频道ID + :return:GuildRoles对象 + """ + url = get_url(APIConstant.rolesURI, self.is_sandbox).format(guild_id=guild_id) + response = self.http.get(url) + return json.loads(response.content, object_hook=GuildRoles) + + def create_guild_role( + self, guild_id: str, role_info: RoleUpdateInfo + ) -> RoleUpdateResult: + """ + 创建频道身份组 + + :param guild_id:频道ID + :param role_info:RoleUpdateInfo对象,需要自己创建的身份数据 + :return:RoleUpdateResult对象 + """ + url = get_url(APIConstant.rolesURI, self.is_sandbox).format(guild_id=guild_id) + params = RoleUpdateRequest() + params.filter = RoleUpdateFilter(1, 1, 1) + params.guild_id = guild_id + params.info = role_info + serialize = JsonUtil.obj2json_serialize(params) + response = self.http.post(url, request=serialize) + return json.loads(response.content, object_hook=RoleUpdateResult) + + def update_guild_role( + self, guild_id: str, role_id: str, role_info: RoleUpdateInfo + ) -> RoleUpdateResult: + """ + 修改频道身份组 + + :param guild_id:频道ID + :param role_id:身份组ID + :param role_info:更新后的RoleUpdateInfo对象 + :return:RoleUpdateResult对象 + """ + url = get_url(APIConstant.roleURI, self.is_sandbox).format( + guild_id=guild_id, role_id=role_id + ) + params = RoleUpdateRequest() + params.filter = RoleUpdateFilter(1, 1, 1) + params.guild_id = guild_id + params.info = role_info + serialize = JsonUtil.obj2json_serialize(params) + response = self.http.patch(url, request=serialize) + return json.loads(response.content, object_hook=RoleUpdateResult) + + def delete_guild_role(self, guild_id: str, role_id: str) -> bool: + """ + 删除频道身份组 + + :param guild_id: 频道ID + :param role_id: 身份组ID + :return: 是否删除成功 + """ + url = get_url(APIConstant.roleURI, self.is_sandbox).format( + guild_id=guild_id, role_id=role_id + ) + response = self.http.delete(url) + return response.status_code == HttpStatus.ACTION_OK + + def create_guild_role_member( + self, + guild_id: str, + role_id: str, + user_id: str, + role_req: Channel = None, + ) -> bool: + """ + 增加频道身份组成员 + 需要使用的 token 对应的用户具备删除身份组成员权限。如果是机器人,要求被添加为管理员。 + 如果要删除的身份组ID是5-子频道管理员,需要增加channel对象来指定具体是哪个子频道 + + :param guild_id:频道ID + :param role_id:身份组ID + :param user_id:用户ID + :param role_req:RoleMemberRequest数据对象 + :return:是否添加成功 + """ + url = get_url(APIConstant.memberRoleURI, self.is_sandbox).format( + guild_id=guild_id, role_id=role_id, user_id=user_id + ) + response = self.http.put(url, request=JsonUtil.obj2json_serialize(role_req)) + return response.status_code == HttpStatus.ACTION_OK + + def delete_guild_role_member( + self, + guild_id: str, + role_id: str, + user_id: str, + role_req: Channel = None, + ) -> bool: + """ + 删除频道身份组成员 + 需要使用的 token 对应的用户具备删除身份组成员权限。如果是机器人,要求被添加为管理员。 + 如果要删除的身份组ID是5-子频道管理员,需要增加channel对象来指定具体是哪个子频道 + + :param guild_id:频道ID + :param role_id:身份组ID + :param user_id:用户ID + :param role_req:RoleMemberRequest数据对象 + :return:是否删除成功 + """ + url = get_url(APIConstant.memberRoleURI, self.is_sandbox).format( + guild_id=guild_id, role_id=role_id, user_id=user_id + ) + response = self.http.delete(url, request=JsonUtil.obj2json_serialize(role_req)) + return response.status_code == HttpStatus.ACTION_OK + + +class GuildMemberAPI(APIBase): + """ + 成员相关接口,添加成员到用户组等 + """ + + def get_guild_member(self, guild_id: str, user_id: str) -> Member: + """ + 获取频道指定成员 + + :param guild_id:频道ID + :param user_id:用户ID(一般从事件消息中获取) + :return: + """ + url = get_url(APIConstant.guildMemberURI, self.is_sandbox).format( + guild_id=guild_id, user_id=user_id + ) + response = self.http.get(url) + return json.loads(response.content, object_hook=Member) + + def get_guild_members( + self, guild_id: str, guild_member_pager: QueryParams + ) -> List[Member]: + """ + 获取成员列表,需要申请接口权限 + + :param guild_id: 频道ID + :param guild_member_pager: GuildMembersPager分页数据对象 + :return: Member列表 + """ + url = get_url(APIConstant.guildMembersURI, self.is_sandbox).format( + guild_id=guild_id + ) + response = self.http.get(url, params=guild_member_pager.__dict__) + return json.loads(response.content, object_hook=Member) + + +class ChannelAPI(APIBase): + """子频道相关接口""" + + def get_channel(self, channel_id) -> Channel: + """ + 获取子频道信息 + + :param channel_id:子频道ID + :return:子频道对象Channel + """ + url = get_url(APIConstant.channelURI, self.is_sandbox).format( + channel_id=channel_id + ) + response = self.http.get(url) + return json.loads(response.content, object_hook=Channel) + + def get_channels(self, guild_id: str) -> List[Channel]: + """ + 获取频道下的子频道列表 + + :param guild_id: 频道ID + :return: Channel列表 + """ + url = get_url(APIConstant.channelsURI, self.is_sandbox).format( + guild_id=guild_id + ) + response = self.http.get(url) + return json.loads(response.content, object_hook=Channel) + + def create_channel( + self, guild_id: str, request: CreateChannelRequest + ) -> ChannelResponse: + """ + 创建子频道 + + :param guild_id: 频道ID + :param request: 创建子频道的请求对象CreateChannelRequest + :return ChannelResponse 对象 + """ + url = get_url(APIConstant.channelsURI, self.is_sandbox).format( + guild_id=guild_id + ) + request_json = JsonUtil.obj2json_serialize(request) + response = self.http.post(url, request_json) + return json.loads(response.content, object_hook=ChannelResponse) + + def update_channel( + self, channel_id: str, request: PatchChannelRequest + ) -> ChannelResponse: + """ + 修改子频道 + + :param channel_id: 频道ID + :param request: PatchChannelRequest + :return ChannelResponse 对象 + """ + url = get_url(APIConstant.channelURI, self.is_sandbox).format( + channel_id=channel_id + ) + request_json = JsonUtil.obj2json_serialize(request) + response = self.http.patch(url, request_json) + return json.loads(response.content, object_hook=ChannelResponse) + + def delete_channel(self, channel_id: str) -> ChannelResponse: + """ + 删除子频道 + + :param channel_id: 频道ID + :return ChannelResponse 对象 + """ + url = get_url(APIConstant.channelURI, self.is_sandbox).format( + channel_id=channel_id + ) + response = self.http.delete(url) + return json.loads(response.content, object_hook=ChannelResponse) + + +class ChannelPermissionsAPI(APIBase): + """子频道权限相关接口""" + + def get_channel_permissions( + self, channel_id: str, user_id: str + ) -> ChannelPermissions: + """ + 获取指定子频道的权限 + + :param channel_id:子频道ID + :param user_id:用户ID + :return:ChannelPermissions对象 + """ + url = get_url(APIConstant.channelPermissionsURI, self.is_sandbox).format( + channel_id=channel_id, user_id=user_id + ) + response = self.http.get(url) + return json.loads(response.content, object_hook=ChannelPermissions) + + def update_channel_permissions( + self, channel_id, user_id, request: ChannelPermissionsUpdateRequest + ) -> bool: + """ + 修改指定子频道的权限 + + :param channel_id:子频道ID + :param user_id:用户ID + :param request:ChannelPermissionsUpdateRequest数据对象(构造可以查看具体的对象注释) + :return: + """ + url = get_url(APIConstant.channelPermissionsURI, self.is_sandbox).format( + channel_id=channel_id, user_id=user_id + ) + if request.add != "": + request.add = str(int(request.add, 16)) + if request.remove != "": + request.remove = str(int(request.remove, 16)) + response = self.http.put(url, request=JsonUtil.obj2json_serialize(request)) + return response.status_code == HttpStatus.ACTION_OK + + +class MessageAPI(APIBase): + """消息""" + + def get_message(self, channel_id: str, message_id: str) -> Message: + """ + 获取指定消息 + + :param channel_id: 频道ID + :param message_id: 消息ID + :return: Message 对象 + """ + url = get_url(APIConstant.messageURI, self.is_sandbox).format( + channel_id=channel_id, message_id=message_id + ) + response = self.http.get(url) + return json.loads(response.content, object_hook=Message) + + def get_messages(self, channel_id: str, pager: MessagesPager) -> List[Message]: + """ + 获取指定消息列表 + + :param channel_id: 频道ID + :param pager: MessagesPager对象 + :return: Message 对象 + """ + url = get_url(APIConstant.messagesURI, self.is_sandbox).format( + channel_id=channel_id + ) + query = {} + if pager.limit != "": + query["limit"] = pager.limit + + if pager.type != "" and pager.id != "": + query[pager.type] = pager.id + + response = self.http.get(url, params=query) + return json.loads(response.content, object_hook=Message) + + def post_message( + self, channel_id: str, message_send: MessageSendRequest + ) -> Message: + """ + 发送消息 + + 要求操作人在该子频道具有发送消息的权限。 + 发送成功之后,会触发一个创建消息的事件。 + 被动回复消息有效期为 5 分钟 + 主动推送消息每日每个子频道限 2 条 + 发送消息接口要求机器人接口需要链接到websocket gateway 上保持在线状态 + + :param channel_id: 子频道ID + :param message_send: MessageSendRequest对象 + :return: Message对象 + """ + + url = get_url(APIConstant.messagesURI, self.is_sandbox).format( + channel_id=channel_id + ) + request_json = JsonUtil.obj2json_serialize(message_send) + response = self.http.post(url, request_json) + return json.loads(response.content, object_hook=Message) + + def create_direct_message( + self, create_direct_message: CreateDirectMessageRequest + ) -> DirectMessageGuild: + """ + 创建私信频道 + + :param create_direct_message: 构造request数据 + :return: 私信频道对象 + """ + url = get_url(APIConstant.userMeDMURI, self.is_sandbox) + request_json = JsonUtil.obj2json_serialize(create_direct_message) + response = self.http.post(url, request_json) + return json.loads(response.content, object_hook=DirectMessageGuild) + + def post_direct_message( + self, guild_id: str, message_send: MessageSendRequest + ) -> Message: + """ + 发送私信 + + :param guild_id: 创建的私信频道id + :param message_send: 发送消息的数据请求对象 MessageSendRequest + :return Message对象 + """ + url = get_url(APIConstant.dmsURI, self.is_sandbox).format(guild_id=guild_id) + request_json = JsonUtil.obj2json_serialize(message_send) + response = self.http.post(url, request_json) + return json.loads(response.content, object_hook=Message) + + +class AudioAPI(APIBase): + """音频接口""" + + def post_audio(self, channel_id: str, audio_control: AudioControl) -> bool: + """ + 音频控制 + + :param channel_id:频道ID + :param audio_control:AudioControl对象 + :return:是否成功 + """ + url = get_url(APIConstant.audioControlURI, self.is_sandbox).format( + channel_id=channel_id + ) + request_json = JsonUtil.obj2json_serialize(audio_control) + response = self.http.post(url, request=request_json) + return response.status_code == HttpStatus.ACTION_OK + + +class UserAPI(APIBase): + """用户相关接口""" + + def me(self) -> User: + """ + :return:使用当前用户信息填充的 User 对象 + """ + url = get_url(APIConstant.userMeURI, self.is_sandbox) + response = self.http.get(url) + return json.loads(response.content, object_hook=User) + + def me_guilds(self) -> List[Guild]: + """ + 当前用户所加入的 Guild 对象列表 + + :return:Guild对象列表 + """ + url = get_url(APIConstant.userMeGuildsURI, self.is_sandbox) + response = self.http.get(url) + return json.loads(response.content, object_hook=Guild) + + +class WebsocketAPI(APIBase): + """WebsocketAPI""" + + def ws(self): + url = get_url(APIConstant.gatewayBotURI, self.is_sandbox) + response = self.http.get(url) + websocket_ap = json.loads(response.content) + return websocket_ap diff --git a/qqbot/async_api.py b/qqbot/async_api.py new file mode 100644 index 0000000..d2660d3 --- /dev/null +++ b/qqbot/async_api.py @@ -0,0 +1,525 @@ +# -*- coding: utf-8 -*- + +# 异步api +import json +from typing import List + +import aiohttp + +from qqbot import WebsocketAPI +from qqbot.core.network.async_http import AsyncHttp +from qqbot.core.network.url import get_url, APIConstant +from qqbot.core.network.ws.ws_intents_handler import Handler, register_handlers +from qqbot.core.network.ws_async.ws_async_manager import SessionManager +from qqbot.core.util.json_util import JsonUtil +from qqbot.model.audio import AudioControl +from qqbot.model.channel import ( + Channel, + ChannelResponse, + CreateChannelRequest, + PatchChannelRequest, +) +from qqbot.model.channel_permissions import ( + ChannelPermissions, + ChannelPermissionsUpdateRequest, +) +from qqbot.model.guild import Guild +from qqbot.model.guild_member import QueryParams +from qqbot.model.guild_role import ( + GuildRoles, + RoleUpdateResult, + RoleUpdateRequest, + RoleUpdateFilter, + RoleUpdateInfo, +) +from qqbot.model.member import User, Member +from qqbot.model.message import ( + MessageSendRequest, + Message, + CreateDirectMessageRequest, + DirectMessageGuild, + MessagesPager, +) +from qqbot.model.token import Token + + +def async_listen_events(t_token: Token, is_sandbox: bool, *handlers: Handler): + """ + 异步注册并监听频道相关事件 + + :param t_token: Token对象 + :param handlers: 包含事件类型和事件回调的Handler对象,支持多个对象 + :param is_sandbox:是否沙盒环境,默认为False + """ + # 通过api获取websocket链接 + ws_api = WebsocketAPI(t_token, is_sandbox) + ws_ap = ws_api.ws() + # 新建和注册监听事件 + t_intent = register_handlers(handlers) + # 实例一个session_manager + manager = SessionManager() + manager.start(ws_ap, t_token.bot_token(), t_intent) + + +class AsyncAPIBase: + timeout = 3 + client_session = aiohttp.ClientSession() + + def __init__(self, token: Token, is_sandbox: bool): + """ + API初始化信息 + + :param token: Token对象 + :param is_sandbox: 是否沙盒环境 + """ + self.is_sandbox = is_sandbox + self.token = token + self.http_async = AsyncHttp( + self.client_session, self.timeout, token.get_string(), token.get_type() + ) + + def with_timeout(self, timeout): + self.timeout = timeout + return self + + +class AsyncGuildAPI(AsyncAPIBase): + """ + 频道相关接口 + """ + + async def get_guild(self, guild_id: str) -> Guild: + """ + 获取频道信息 + + :param guild_id: 频道ID(一般从事件中获取相关的ID信息) + :return: 频道Guild对象 + """ + url = get_url(APIConstant.guildURI, self.is_sandbox).format(guild_id=guild_id) + response = await self.http_async.get(url) + return json.loads(response, object_hook=Guild) + + +class AsyncGuildRoleAPI(AsyncAPIBase): + """ + 频道身份组相关接口 + """ + + async def get_guild_roles(self, guild_id: str) -> GuildRoles: + """ + 获取频道身份组列表 + + :param guild_id:频道ID + :return:GuildRoles对象 + """ + url = get_url(APIConstant.rolesURI, self.is_sandbox).format(guild_id=guild_id) + response = await self.http_async.get(url) + return json.loads(response, object_hook=GuildRoles) + + async def create_guild_role( + self, guild_id: str, role_info: RoleUpdateInfo + ) -> RoleUpdateResult: + """ + 创建频道身份组 + + :param guild_id:频道ID + :param role_info:RoleUpdateInfo对象,需要自己创建的身份数据 + :return:RoleUpdateResult对象 + """ + url = get_url(APIConstant.rolesURI, self.is_sandbox).format(guild_id=guild_id) + params = RoleUpdateRequest() + params.filter = RoleUpdateFilter(1, 1, 1) + params.guild_id = guild_id + params.info = role_info + serialize = JsonUtil.obj2json_serialize(params) + response = await self.http_async.post(url, request=serialize) + return json.loads(response, object_hook=RoleUpdateResult) + + async def update_guild_role( + self, guild_id: str, role_id: str, role_info: RoleUpdateInfo + ) -> RoleUpdateResult: + """ + 修改频道身份组 + + :param guild_id:频道ID + :param role_id:身份组ID + :param role_info:更新后的RoleUpdateInfo对象 + :return:RoleUpdateResult对象 + """ + url = get_url(APIConstant.roleURI, self.is_sandbox).format( + guild_id=guild_id, role_id=role_id + ) + params = RoleUpdateRequest() + params.filter = RoleUpdateFilter(1, 1, 1) + params.guild_id = guild_id + params.info = role_info + serialize = JsonUtil.obj2json_serialize(params) + response = await self.http_async.patch(url, request=serialize) + return json.loads(response, object_hook=RoleUpdateResult) + + async def delete_guild_role(self, guild_id: str, role_id: str) -> bool: + """ + 删除频道身份组 + + :param guild_id: 频道ID + :param role_id: 身份组ID + :return: 是否删除成功 + """ + url = get_url(APIConstant.roleURI, self.is_sandbox).format( + guild_id=guild_id, role_id=role_id + ) + response = await self.http_async.delete(url) + return response == "" + + async def create_guild_role_member( + self, + guild_id: str, + role_id: str, + user_id: str, + role_req: Channel = None, + ) -> bool: + """ + 增加频道身份组成员 + 需要使用的 token 对应的用户具备删除身份组成员权限。如果是机器人,要求被添加为管理员。 + 如果要删除的身份组ID是5-子频道管理员,需要增加channel对象来指定具体是哪个子频道 + + :param guild_id:频道ID + :param role_id:身份组ID + :param user_id:用户ID + :param role_req:RoleMemberRequest数据对象 + :return:是否添加成功 + """ + url = get_url(APIConstant.memberRoleURI, self.is_sandbox).format( + guild_id=guild_id, role_id=role_id, user_id=user_id + ) + response = await self.http_async.put( + url, request=JsonUtil.obj2json_serialize(role_req) + ) + return response == "" + + async def delete_guild_role_member( + self, + guild_id: str, + role_id: str, + user_id: str, + role_req: Channel = None, + ) -> bool: + """ + 删除频道身份组成员 + 需要使用的 token 对应的用户具备删除身份组成员权限。如果是机器人,要求被添加为管理员。 + 如果要删除的身份组ID是5-子频道管理员,需要增加channel对象来指定具体是哪个子频道 + + :param guild_id:频道ID + :param role_id:身份组ID + :param user_id:用户ID + :param role_req:RoleMemberRequest数据对象 + :return:是否删除成功 + """ + url = get_url(APIConstant.memberRoleURI, self.is_sandbox).format( + guild_id=guild_id, role_id=role_id, user_id=user_id + ) + response = await self.http_async.delete( + url, request=JsonUtil.obj2json_serialize(role_req) + ) + return response == "" + + +class AsyncGuildMemberAPI(AsyncAPIBase): + """ + 成员相关接口,添加成员到用户组等 + """ + + async def get_guild_member(self, guild_id: str, user_id: str) -> Member: + """ + 获取频道指定成员 + + :param guild_id:频道ID + :param user_id:用户ID(一般从事件消息中获取) + :return: + """ + url = get_url(APIConstant.guildMemberURI, self.is_sandbox).format( + guild_id=guild_id, user_id=user_id + ) + response = await self.http_async.get(url) + return json.loads(response, object_hook=Member) + + async def get_guild_members( + self, guild_id: str, guild_member_pager: QueryParams + ) -> List[Member]: + """ + 获取成员列表,需要申请接口权限 + + :param guild_id: 频道ID + :param guild_member_pager: GuildMembersPager分页数据对象 + :return: Member列表 + """ + url = get_url(APIConstant.guildMembersURI, self.is_sandbox).format( + guild_id=guild_id + ) + response = await self.http_async.get(url, params=guild_member_pager.__dict__) + return json.loads(response, object_hook=Member) + + +class AsyncChannelAPI(AsyncAPIBase): + """子频道相关接口""" + + async def get_channel(self, channel_id) -> Channel: + """ + 获取子频道信息 + + :param channel_id:子频道ID + :return:子频道对象Channel + """ + url = get_url(APIConstant.channelURI, self.is_sandbox).format( + channel_id=channel_id + ) + response = await self.http_async.get(url) + return json.loads(response, object_hook=Channel) + + async def get_channels(self, guild_id: str) -> List[Channel]: + """ + 获取频道下的子频道列表 + + :param guild_id: 频道ID + :return: Channel列表 + """ + url = get_url(APIConstant.channelsURI, self.is_sandbox).format( + guild_id=guild_id + ) + response = await self.http_async.get(url) + return json.loads(response, object_hook=Channel) + + async def create_channel( + self, guild_id: str, request: CreateChannelRequest + ) -> ChannelResponse: + """ + 创建子频道 + + :param guild_id: 频道ID + :param request: 创建子频道的请求对象CreateChannelRequest + :return ChannelResponse 对象 + """ + url = get_url(APIConstant.channelsURI, self.is_sandbox).format( + guild_id=guild_id + ) + request_json = JsonUtil.obj2json_serialize(request) + response = await self.http_async.post(url, request_json) + return json.loads(response, object_hook=ChannelResponse) + + async def update_channel( + self, channel_id: str, request: PatchChannelRequest + ) -> ChannelResponse: + """ + 修改子频道 + + :param channel_id: 频道ID + :param request: PatchChannelRequest + :return ChannelResponse 对象 + """ + url = get_url(APIConstant.channelURI, self.is_sandbox).format( + channel_id=channel_id + ) + request_json = JsonUtil.obj2json_serialize(request) + response = await self.http_async.patch(url, request_json) + return json.loads(response, object_hook=ChannelResponse) + + async def delete_channel(self, channel_id: str) -> ChannelResponse: + """ + 删除子频道 + + :param channel_id: 频道ID + :return ChannelResponse 对象 + """ + url = get_url(APIConstant.channelURI, self.is_sandbox).format( + channel_id=channel_id + ) + response = await self.http_async.delete(url) + return json.loads(response, object_hook=ChannelResponse) + + +class AsyncChannelPermissionsAPI(AsyncAPIBase): + """子频道权限相关接口""" + + async def get_channel_permissions( + self, channel_id: str, user_id: str + ) -> ChannelPermissions: + """ + 获取指定子频道的权限 + + :param channel_id:子频道ID + :param user_id:用户ID + :return:ChannelPermissions对象 + """ + url = get_url(APIConstant.channelPermissionsURI, self.is_sandbox).format( + channel_id=channel_id, user_id=user_id + ) + response = await self.http_async.get(url) + return json.loads(response, object_hook=ChannelPermissions) + + async def update_channel_permissions( + self, channel_id, user_id, request: ChannelPermissionsUpdateRequest + ) -> bool: + """ + 修改指定子频道的权限 + + :param channel_id:子频道ID + :param user_id:用户ID + :param request:ChannelPermissionsUpdateRequest数据对象(构造可以查看具体的对象注释) + :return: + """ + url = get_url(APIConstant.channelPermissionsURI, self.is_sandbox).format( + channel_id=channel_id, user_id=user_id + ) + if request.add != "": + request.add = str(int(request.add, 16)) + if request.remove != "": + request.remove = str(int(request.remove, 16)) + response = await self.http_async.put( + url, request=JsonUtil.obj2json_serialize(request) + ) + return response == "" + + +class AsyncMessageAPI(AsyncAPIBase): + """消息""" + + async def get_message(self, channel_id: str, message_id: str) -> Message: + """ + 获取指定消息 + + :param channel_id: 频道ID + :param message_id: 消息ID + :return: Message 对象 + """ + url = get_url(APIConstant.messageURI, self.is_sandbox).format( + channel_id=channel_id, message_id=message_id + ) + response = await self.http_async.get(url) + return json.loads(response, object_hook=Message) + + async def get_messages( + self, channel_id: str, pager: MessagesPager + ) -> List[Message]: + """ + 获取指定消息列表 + + :param channel_id: 频道ID + :param pager: MessagesPager对象 + :return: Message 对象 + """ + url = get_url(APIConstant.messagesURI, self.is_sandbox).format( + channel_id=channel_id + ) + query = {} + if pager.limit != "": + query["limit"] = pager.limit + + if pager.type != "" and pager.id != "": + query[pager.type] = pager.id + + response = await self.http_async.get(url, params=query) + return json.loads(response, object_hook=Message) + + async def post_message( + self, channel_id: str, message_send: MessageSendRequest + ) -> Message: + """ + 发送消息 + + 要求操作人在该子频道具有发送消息的权限。 + 发送成功之后,会触发一个创建消息的事件。 + 被动回复消息有效期为 5 分钟 + 主动推送消息每日每个子频道限 2 条 + 发送消息接口要求机器人接口需要链接到websocket gateway 上保持在线状态 + + :param channel_id: 子频道ID + :param message_send: MessageSendRequest对象 + :return: Message对象 + """ + + url = get_url(APIConstant.messagesURI, self.is_sandbox).format( + channel_id=channel_id + ) + request_json = JsonUtil.obj2json_serialize(message_send) + response = await self.http_async.post(url, request_json) + return json.loads(response, object_hook=Message) + + async def create_direct_message( + self, create_direct_message: CreateDirectMessageRequest + ) -> DirectMessageGuild: + """ + 创建私信频道 + + :param create_direct_message: 构造request数据 + :return: 私信频道对象 + """ + url = get_url(APIConstant.userMeDMURI, self.is_sandbox) + request_json = JsonUtil.obj2json_serialize(create_direct_message) + response = await self.http_async.post(url, request_json) + return json.loads(response, object_hook=DirectMessageGuild) + + async def post_direct_message( + self, guild_id: str, message_send: MessageSendRequest + ) -> Message: + """ + 发送私信 + + :param guild_id: 创建的私信频道id + :param message_send: 发送消息的数据请求对象 MessageSendRequest + :return Message对象 + """ + url = get_url(APIConstant.dmsURI, self.is_sandbox).format(guild_id=guild_id) + request_json = JsonUtil.obj2json_serialize(message_send) + response = await self.http_async.post(url, request_json) + return json.loads(response, object_hook=Message) + + +class AsyncAudioAPI(AsyncAPIBase): + """音频接口""" + + async def post_audio(self, channel_id: str, audio_control: AudioControl) -> bool: + """ + 音频控制 + + :param channel_id:频道ID + :param audio_control:AudioControl对象 + :return:是否成功 + """ + url = get_url(APIConstant.audioControlURI, self.is_sandbox).format( + channel_id=channel_id + ) + request_json = JsonUtil.obj2json_serialize(audio_control) + response = await self.http_async.post(url, request=request_json) + return response == "" + + +class AsyncUserAPI(AsyncAPIBase): + """用户相关接口""" + + async def me(self) -> User: + """ + :return:使用当前用户信息填充的 User 对象 + """ + url = get_url(APIConstant.userMeURI, self.is_sandbox) + response = await self.http_async.get(url) + return json.loads(response, object_hook=User) + + async def me_guilds(self) -> List[Guild]: + """ + 当前用户所加入的 Guild 对象列表 + + :return:Guild对象列表 + """ + url = get_url(APIConstant.userMeGuildsURI, self.is_sandbox) + response = await self.http_async.get(url) + return json.loads(response, object_hook=Guild) + + +class AsyncWebsocketAPI(AsyncAPIBase): + """WebsocketAPI""" + + async def ws(self): + url = get_url(APIConstant.gatewayBotURI, self.is_sandbox) + response = await self.http_async.get(url) + websocket_ap = json.loads(response) + return websocket_ap diff --git a/qqbot/core/__init__.py b/qqbot/core/__init__.py index e69de29..40a96af 100644 --- a/qqbot/core/__init__.py +++ b/qqbot/core/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/qqbot/core/network/async_http.py b/qqbot/core/network/async_http.py new file mode 100644 index 0000000..95defc1 --- /dev/null +++ b/qqbot/core/network/async_http.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +import json + +from aiohttp import ClientResponse + +from qqbot.core.exception.error import ( + AuthenticationFailedError, + NotFoundError, + MethodNotAllowedError, + SequenceNumberError, + ServerError, +) +from qqbot.core.util import logging + +X_TPS_TRACE_ID = "X-Tps-trace-Id" + +logger = logging.getLogger(__name__) + +HttpErrorDict = { + 401: AuthenticationFailedError, + 404: NotFoundError, + 405: MethodNotAllowedError, + 429: SequenceNumberError, + 500: ServerError, + 504: ServerError, +} + + +class HttpErrorMessage: + def __init__(self, data=None): + self.message = "" + self.code = 0 + if data is not None: + self.__dict__ = data + + +class HttpStatus: + OK = 200 + ACTION_OK = 204 + + +def _handle_response(api_url, response: ClientResponse, content: str): + if response.status in (HttpStatus.ACTION_OK, HttpStatus.OK): + return + else: + logger.error( + "http request error with api_url:%s, error: %s, content: %s, trace_id:%s" + % ( + api_url, + response.status, + content, + response.headers.get(X_TPS_TRACE_ID), + ) # trace_id 用于定位接口问题 + ) + error_message_: HttpErrorMessage = json.loads( + content, object_hook=HttpErrorMessage + ) + error_dict_get = HttpErrorDict.get(response.status) + if error_dict_get is None: + raise ServerError(error_message_.message) + raise error_dict_get(msg=error_message_.message) + + +class AsyncHttp: + def __init__(self, session, time_out, token, type): + self.timeout = time_out + self.token = token + self.scheme = type + self.session = session + + async def get(self, api_url, params=None): + headers = { + "Authorization": self.scheme + " " + self.token, + "User-Agent": "botpy", + } + logger.debug("http get headers: %s, api_url: %s" % (headers, api_url)) + async with self.session.get( + url=api_url, params=params, timeout=self.timeout, headers=headers + ) as resp: + content = await resp.text() + _handle_response(api_url, resp, content) + return content + + async def post(self, api_url, request=None, params=None): + headers = { + "Authorization": self.scheme + " " + self.token, + "User-Agent": "botpy", + } + logger.debug( + "http post headers: %s, api_url: %s, request: %s" + % (headers, api_url, request) + ) + async with self.session.post( + url=api_url, + params=params, + json=request, + timeout=self.timeout, + headers=headers, + ) as resp: + content = await resp.text() + _handle_response(api_url, resp, content) + return content + + async def delete(self, api_url, request=None, params=None): + headers = { + "Authorization": self.scheme + " " + self.token, + "User-Agent": "botpy", + } + logger.debug("http delete headers: %s, api_url: %s" % (headers, api_url)) + async with self.session.delete( + url=api_url, + params=params, + json=request, + timeout=self.timeout, + headers=headers, + ) as resp: + content = await resp.text() + _handle_response(api_url, resp, content) + return content + + async def put(self, api_url, request=None, params=None): + headers = { + "Authorization": self.scheme + " " + self.token, + "User-Agent": "botpy", + } + logger.debug( + "http put headers: %s, api_url: %s, request: %s" + % (headers, api_url, request) + ) + async with self.session.put( + url=api_url, + params=params, + json=request, + timeout=self.timeout, + headers=headers, + ) as resp: + content = await resp.text() + _handle_response(api_url, resp, content) + return content + + async def patch(self, api_url, request=None, params=None): + headers = { + "Authorization": self.scheme + " " + self.token, + "User-Agent": "botpy", + } + logger.debug( + "http patch headers: %s, api_url: %s, request: %s" + % (headers, api_url, request) + ) + async with self.session.patch( + url=api_url, + params=params, + json=request, + timeout=self.timeout, + headers=headers, + ) as resp: + content = await resp.text() + _handle_response(api_url, resp, content) + return content diff --git a/qqbot/core/network/ws/__init__.py b/qqbot/core/network/ws/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/qqbot/core/network/ws/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/qqbot/core/network/websocket/__init__.py b/qqbot/core/network/ws/dto/__init__.py similarity index 100% rename from qqbot/core/network/websocket/__init__.py rename to qqbot/core/network/ws/dto/__init__.py diff --git a/qqbot/core/network/websocket/dto/enum_intents.py b/qqbot/core/network/ws/dto/enum_intents.py similarity index 100% rename from qqbot/core/network/websocket/dto/enum_intents.py rename to qqbot/core/network/ws/dto/enum_intents.py diff --git a/qqbot/core/network/websocket/dto/enum_opcode.py b/qqbot/core/network/ws/dto/enum_opcode.py similarity index 100% rename from qqbot/core/network/websocket/dto/enum_opcode.py rename to qqbot/core/network/ws/dto/enum_opcode.py diff --git a/qqbot/core/network/websocket/dto/ws_payload.py b/qqbot/core/network/ws/dto/ws_payload.py similarity index 90% rename from qqbot/core/network/websocket/dto/ws_payload.py rename to qqbot/core/network/ws/dto/ws_payload.py index b3cdb28..e7cdb4b 100644 --- a/qqbot/core/network/websocket/dto/ws_payload.py +++ b/qqbot/core/network/ws/dto/ws_payload.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from qqbot.core.network.websocket.dto.enum_intents import Intents +from qqbot.core.network.ws.dto.enum_intents import Intents from qqbot.model.token import Token diff --git a/qqbot/core/network/websocket/ws_event.py b/qqbot/core/network/ws/ws_event.py similarity index 96% rename from qqbot/core/network/websocket/ws_event.py rename to qqbot/core/network/ws/ws_event.py index 92c72ac..15c26f5 100644 --- a/qqbot/core/network/websocket/ws_event.py +++ b/qqbot/core/network/ws/ws_event.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from qqbot.core.network.websocket.dto.enum_intents import Intents +from qqbot.core.network.ws.dto.enum_intents import Intents class WsEvent: diff --git a/qqbot/core/network/ws/ws_handler.py b/qqbot/core/network/ws/ws_handler.py new file mode 100644 index 0000000..25f7628 --- /dev/null +++ b/qqbot/core/network/ws/ws_handler.py @@ -0,0 +1,13 @@ +class DefaultHandler: + """ + 持有handler的实例 + """ + + plain = None + guild = None + guild_member = None + channel = None + message = None + at_message = None + direct_message = None + audio = None diff --git a/qqbot/core/network/websocket/ws_intents_handler.py b/qqbot/core/network/ws/ws_intents_handler.py similarity index 95% rename from qqbot/core/network/websocket/ws_intents_handler.py rename to qqbot/core/network/ws/ws_intents_handler.py index aad04e7..3d3a202 100644 --- a/qqbot/core/network/websocket/ws_intents_handler.py +++ b/qqbot/core/network/ws/ws_intents_handler.py @@ -2,8 +2,8 @@ from enum import Enum -from qqbot.core.network.websocket.ws_event import WsEvent -from qqbot.core.network.websocket.ws_event_handler import DefaultHandler +from qqbot.core.network.ws.ws_event import WsEvent +from qqbot.core.network.ws.ws_handler import DefaultHandler class Handler: diff --git a/qqbot/core/network/websocket/ws_session.py b/qqbot/core/network/ws/ws_session.py similarity index 100% rename from qqbot/core/network/websocket/ws_session.py rename to qqbot/core/network/ws/ws_session.py diff --git a/qqbot/core/network/ws_async/__init__.py b/qqbot/core/network/ws_async/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/qqbot/core/network/ws_async/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/qqbot/core/network/ws_async/ws_async_client.py b/qqbot/core/network/ws_async/ws_async_client.py new file mode 100644 index 0000000..861edf9 --- /dev/null +++ b/qqbot/core/network/ws_async/ws_async_client.py @@ -0,0 +1,212 @@ +# -*- coding: utf-8 -*- +import asyncio +import json +import traceback + +import aiohttp +from aiohttp import WSMessage, ClientWebSocketResponse + +from qqbot.core.exception.error import WebsocketError +from qqbot.core.network.ws.dto.enum_intents import Intents +from qqbot.core.network.ws.dto.enum_opcode import OpCode +from qqbot.core.network.ws.dto.ws_payload import ( + WSPayload, + WsIdentifyData, + WSResumeData, +) +from qqbot.core.network.ws_async.ws_async_handler import parse_and_handle +from qqbot.core.util import logging + +logger = logging.getLogger(__name__) + + +class Client: + def __init__(self, session, session_manager, connected_callback=None): + self.session = session + self.ws_conn = None + self.session_manager = session_manager + self.connected_callback = connected_callback + self.can_reconnect = False + + async def on_error(self, exception: BaseException): + logger.error( + "on_error: websocket connection: %s, exception : %s" + % (self.ws_conn, exception) + ) + traceback.print_exc() + + async def on_close(self, ws, close_status_code, close_msg): + logger.info( + "on_close: websocket connection %s" % ws + + ", code: %s" % close_status_code + + ", msg: %s" % close_msg + ) + # 这种不能重新链接 + if ( + close_status_code == WebsocketError.CodeConnCloseErr + or close_status_code == WebsocketError.CodeInvalidSession + or self.can_reconnect is False + ): + self.session.session_id = "" + self.session.last_seq = 0 + # 断连后启动一个新的链接并透传当前的session,不使用内部重连的方式,避免死循环 + self.session_manager.session_pool.add(self.session) + asyncio.ensure_future(self.session_manager.session_pool.run()) + + async def on_message(self, ws, message): + logger.info("on_message: %s" % message) + message_event = json.loads(message) + if await self._is_system_event(message_event, ws): + return + if "t" in message_event.keys() and message_event["t"] == "READY": + event_seq = message_event["s"] + if event_seq > 0: + self.session.last_seq = event_seq + await self._ready_handler(message_event) + return + if "t" in message_event.keys(): + await parse_and_handle(message_event, message) + + async def on_connected(self, ws): + logger.info("ws client connected ok") + self.ws_conn = ws + await self.connected_callback(self) + # 心跳检查 + asyncio.ensure_future(self._send_heartbeat(interval=30)) + + async def connect(self): + """ + websocket向服务器端发起链接,并定时发送心跳 + """ + + logger.info("ws client start connect") + ws_url = self.session.url + if ws_url == "": + raise Exception("session url is none") + + async with aiohttp.ClientSession() as session: + async with session.ws_connect(self.session.url) as ws_conn: + while True: + msg: WSMessage + msg = await ws_conn.receive() + await self.dispatch(msg, ws_conn) + if ws_conn.closed: + logger.info("ws is closed, stop circle receive msg") + break + + async def dispatch(self, msg, ws_conn): + """ + ws事件分发 + """ + if msg.type == aiohttp.WSMsgType.TEXT: + await self.on_message(ws_conn, msg.data) + elif msg.type == aiohttp.WSMsgType.ERROR: + await self.on_error(ws_conn.exception()) + elif ( + msg.type == aiohttp.WSMsgType.CLOSED or msg.type == aiohttp.WSMsgType.CLOSE + ): + await self.on_close(ws_conn, ws_conn.close_code, msg.extra) + + async def identify(self): + """ + websocket鉴权 + """ + if self.session.intent == 0: + self.session.intent = Intents.INTENT_GUILDS.value + logger.info("ws:%s start identify" % self.ws_conn) + identify_event = json.dumps( + WSPayload( + WsIdentifyData( + token=self.session.token.get_string(), + intents=self.session.intent, + shard=[ + self.session.shards.shard_id, + self.session.shards.shard_count, + ], + ).__dict__, + op=OpCode.WS_IDENTITY.value, + ).__dict__ + ) + await self.send_msg(identify_event) + + async def send_msg(self, event_json): + """ + websocket发送消息 + :param event_json: + """ + send_msg = event_json + logger.info("send_msg: %s" % send_msg) + if isinstance(self.ws_conn, ClientWebSocketResponse): + if self.ws_conn.closed: + logger.error("send_msg: websocket connection has closed") + else: + await self.ws_conn.send_str(data=send_msg) + + async def reconnect(self): + """ + websocket重连 + """ + logger.info("ws:%s is reconnected" % self.ws_conn) + resume_event = json.dumps( + WSPayload( + WSResumeData( + token=self.session.token.get_string(), + session_id=self.session.session_id, + seq=self.session.last_seq, + ).__dict__, + op=OpCode.WS_RESUME.value, + ).__dict__ + ) + await self.send_msg(resume_event) + + async def _ready_handler(self, message_event): + data = message_event["d"] + self.version = data["version"] + self.session.session_id = data["session_id"] + self.session.shards.shard_id = data["shard"][0] + self.session.shards.shard_count = data["shard"][1] + self.user = data["user"] + + async def _is_system_event(self, message_event, ws): + """ + 系统事件 + :param message_event:消息 + :param ws:websocket + :return: + """ + event_op = message_event["op"] + if event_op == OpCode.WS_HELLO.value: + await self.on_connected(ws) + return True + if event_op == OpCode.WS_HEARTBEAT_ACK.value: + return True + if event_op == OpCode.WS_RECONNECT.value: + self.can_reconnect = True + return True + if event_op == OpCode.WS_INVALID_SESSION.value: + self.can_reconnect = False + return True + return False + + async def _send_heartbeat(self, interval): + """ + 心跳包 + :param interval: 间隔时间 + """ + logger.info("start send heartbeat") + while True: + heartbeat_event = json.dumps( + WSPayload( + op=OpCode.WS_HEARTBEAT.value, d=self.session.last_seq + ).__dict__ + ) + if self.ws_conn is None: + logger.error("ws is None") + return + else: + if self.ws_conn.closed: + logger.info("ws is closed, stop circle heartbeat") + return + else: + await asyncio.sleep(interval) + await self.send_msg(heartbeat_event) diff --git a/qqbot/core/network/ws_async/ws_async_handler.py b/qqbot/core/network/ws_async/ws_async_handler.py new file mode 100644 index 0000000..945da09 --- /dev/null +++ b/qqbot/core/network/ws_async/ws_async_handler.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +import asyncio +import json + +from qqbot.core.network.ws.ws_event import WsEvent +from qqbot.core.network.ws.ws_handler import DefaultHandler +from qqbot.core.util import logging +from qqbot.model.audio import AudioAction +from qqbot.model.channel import Channel +from qqbot.model.guild import Guild +from qqbot.model.guild_member import GuildMember +from qqbot.model.message import Message + +logger = logging.getLogger(__name__) + + +async def parse_and_handle(message_event, message): + event_type = message_event["t"] + call_handler = event_handler_dict.get(event_type) + + if call_handler is None: + if DefaultHandler.plain is not None: + plain = DefaultHandler.plain + asyncio.ensure_future(plain(event_type, message)) + else: + return + else: + asyncio.ensure_future(call_handler(event_type, message)) + + +async def _handle_event_guild(message_event, message): + callback = DefaultHandler.guild + if callback is None: + return + guild: Guild = json.loads(_parse_data(message), object_hook=Guild) + await callback(message_event, guild) + + +async def _handle_event_channel(message_event, message): + callback = DefaultHandler.channel + if callback is None: + return + channel: Channel = json.loads(_parse_data(message), object_hook=Channel) + await callback(message_event, channel) + + +async def _handle_event_guild_member(message_event, message): + callback = DefaultHandler.guild_member + if callback is None: + return + guild_member: GuildMember = json.loads( + _parse_data(message), object_hook=GuildMember + ) + await callback(message_event, guild_member) + + +async def _handle_event_at_message_create(message_event, message): + callback = DefaultHandler.at_message + if callback is None: + return + at_message: Message = json.loads(_parse_data(message), object_hook=Message) + await callback(message_event, at_message) + + +async def _handle_event_message_create(message_event, message): + callback = DefaultHandler.message + if callback is None: + return + msg: Message = json.loads(_parse_data(message), object_hook=Message) + await callback(message_event, msg) + + +async def _handle_event_direct_message_create(message_event, message): + callback = DefaultHandler.direct_message + if callback is None: + return + msg: Message = json.loads(_parse_data(message), object_hook=Message) + await callback(message_event, msg) + + +async def _handle_event_audio(message_event, message): + callback = DefaultHandler.audio + if callback is None: + return + audio_action: AudioAction = json.loads( + _parse_data(message), object_hook=AudioAction + ) + await callback(message_event, audio_action) + + +event_handler_dict = { + WsEvent.EventGuildCreate: _handle_event_guild, + WsEvent.EventGuildUpdate: _handle_event_guild, + WsEvent.EventGuildDelete: _handle_event_guild, + WsEvent.EventChannelCreate: _handle_event_channel, + WsEvent.EventChannelUpdate: _handle_event_channel, + WsEvent.EventChannelDelete: _handle_event_channel, + WsEvent.EventGuildMemberAdd: _handle_event_guild_member, + WsEvent.EventGuildMemberUpdate: _handle_event_guild_member, + WsEvent.EventGuildMemberRemove: _handle_event_guild_member, + WsEvent.EventAtMessageCreate: _handle_event_at_message_create, + WsEvent.EventMessageCreate: _handle_event_message_create, + WsEvent.EventDirectMessageCreate: _handle_event_direct_message_create, + WsEvent.EventAudioStart: _handle_event_audio, + WsEvent.EventAudioFinish: _handle_event_audio, + WsEvent.EventAudioOnMic: _handle_event_audio, + WsEvent.EventAudioOffMic: _handle_event_audio, +} + + +def _parse_data(message): + json_obj = json.loads(message) + if "d" in json_obj.keys(): + data = json_obj["d"] + return json.dumps(data) diff --git a/qqbot/core/network/ws_async/ws_async_manager.py b/qqbot/core/network/ws_async/ws_async_manager.py new file mode 100644 index 0000000..e52a4b1 --- /dev/null +++ b/qqbot/core/network/ws_async/ws_async_manager.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- + +import asyncio + +from qqbot.core.network.ws_async.ws_async_client import Client +from qqbot.core.network.ws_async.ws_async_pool import SessionPool +from qqbot.core.network.ws.dto.enum_intents import Intents +from qqbot.core.network.ws.ws_session import Session, ShardConfig +from qqbot.core.util import logging +from qqbot.model.token import Token + +logger = logging.getLogger(__name__) + + +def _loop_exception_handler(loop, context): + # first, handle with default handler + loop.default_exception_handler(context) + + exception = context.get("exception") + if isinstance(exception, ZeroDivisionError): + print(context) + loop.stop() + + +async def _on_connected(ws_client): + if ws_client.ws_conn is None: + raise Exception("websocket connection failed ") + if ws_client.session.session_id != "": + await ws_client.reconnect() + else: + await ws_client.identify() + + +def _check_session_limit(websocket_ap): + return websocket_ap["shards"] > websocket_ap["session_start_limit"]["remaining"] + + +def _cal_interval(max_concurrency): + """ + :param max_concurrency:每5s可以创建的session数 + :return: 链接间隔时间 + """ + return round(5 / max_concurrency) + + +class SessionManager: + session_pool: SessionPool + + def start(self, websocket_ap, token=Token, intent=Intents): + logger.info( + "session manager start with: %s" % websocket_ap + + ", token:%s" % token + + ", intent:%s" % intent + ) + # 每个机器人创建的连接数不能超过remaining剩余连接数 + if _check_session_limit(websocket_ap): + raise Exception("session limit exceeded") + # 根据session限制建立链接 + session_interval = _cal_interval( + websocket_ap["session_start_limit"]["max_concurrency"] + ) + shards_count = websocket_ap["shards"] + logger.info( + "session_interval: %s, shards: %s" % (session_interval, shards_count) + ) + # 根据限制建立分片的并发链接数 + self.init_session_pool( + intent, shards_count, token, websocket_ap, session_interval + ) + + def init_session_pool( + self, intent, shards_count, token, websocket_ap, session_interval + ): + + # 实例一个session_pool + self.session_pool = SessionPool( + max_async=websocket_ap["session_start_limit"]["max_concurrency"], + session_manager=self, + loop=asyncio.get_event_loop(), + ) + for i in range(shards_count): + session = Session( + session_id="", + url=websocket_ap["url"], + intent=intent, + last_seq=0, + token=token, + shards=ShardConfig(i, shards_count), + ) + self.session_pool.add(session) + self.start_session(session_interval) + + def start_session(self, session_interval=5): + pool = self.session_pool + loop = pool.loop + loop.set_exception_handler(_loop_exception_handler) + try: + loop.run_until_complete(pool.run(session_interval)) + loop.run_forever() + except KeyboardInterrupt: + logger.info("ws pool is stopped by key board interrupt") + # cancel all tasks lingering + + async def new_connect(self, session, time_interval): + """ + newConnect 启动一个新的连接,如果连接在监听过程中报错了,或者被远端关闭了链接,需要识别关闭的原因,能否继续 resume + 如果能够 resume,则往 sessionChan 中放入带有 sessionID 的 session + 如果不能,则清理掉 sessionID,将 session 放入 sessionChan 中 + session 的启动,交给 start 中的 for 循环执行,session 不自己递归进行重连,避免递归深度过深 + + param session: session对象 + """ + await asyncio.sleep(time_interval) + logger.info("_new_connect:%s" % session) + + client = Client(session, self, _on_connected) + try: + await client.connect() + except (Exception, KeyboardInterrupt, SystemExit) as e: + await client.on_error(e) diff --git a/qqbot/core/network/ws_async/ws_async_pool.py b/qqbot/core/network/ws_async/ws_async_pool.py new file mode 100644 index 0000000..d7cd1c9 --- /dev/null +++ b/qqbot/core/network/ws_async/ws_async_pool.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +import asyncio + +from qqbot.core.util import logging + +logger = logging.getLogger(__name__) + + +class SessionPool: + """ + SessionPool主要支持session的重连,可以根据session的状态动态设置是否需要进行重连操作 + 这里通过设置session_id=""空则任务session需要重连 + """ + + def __init__(self, max_async, session_manager, loop=None): + self.max_async = max_async + self.session_manager = session_manager + self.loop: asyncio.AbstractEventLoop = ( + asyncio.get_event_loop() if loop is None else loop + ) + # session链接同时最大并发数 + self.session_list = [] + + async def run(self, session_interval=5): + loop = self.loop + + # 根据并发数同时建立多个future + # 后台有频率限制,根据间隔时间发起链接请求 + index = 0 + session_list = self.session_list + # 需要执行的链接列表,通过time_interval控制启动时间 + tasks = [] + + while len(session_list) > 0: + logger.info("session list circle run") + time_interval = session_interval * (index + 1) + logger.info( + "async start session connect with max_async: %s, and list size: %s" + % (self.max_async, len(session_list)) + ) + for i in range(self.max_async): + if len(session_list) == 0: + break + logger.info("session list pop session with index %d" % i) + tasks.append( + asyncio.ensure_future( + self._runner(session_list.pop(i), time_interval), loop=loop + ) + ) + index += self.max_async + + await asyncio.wait(tasks) + + async def _runner(self, session, time_interval): + logger.info( + "run session with session: %s, time_interval: %s" % (session, time_interval) + ) + await self.session_manager.new_connect(session, time_interval) + + def add(self, session): + logger.info("add session: %s" % session) + self.session_list.append(session) + + def close(self): + logger.info("session loop closed") + self.loop.close() diff --git a/qqbot/core/network/ws_sync/__init__.py b/qqbot/core/network/ws_sync/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/qqbot/core/network/ws_sync/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/qqbot/core/network/websocket/ws_client.py b/qqbot/core/network/ws_sync/ws_client.py similarity index 96% rename from qqbot/core/network/websocket/ws_client.py rename to qqbot/core/network/ws_sync/ws_client.py index 140d6b4..54d95f7 100644 --- a/qqbot/core/network/websocket/ws_client.py +++ b/qqbot/core/network/ws_sync/ws_client.py @@ -6,14 +6,14 @@ import websocket from qqbot.core.exception.error import WebsocketError -from qqbot.core.network.websocket.dto.enum_intents import Intents -from qqbot.core.network.websocket.dto.enum_opcode import OpCode -from qqbot.core.network.websocket.dto.ws_payload import ( +from qqbot.core.network.ws.dto.enum_intents import Intents +from qqbot.core.network.ws.dto.enum_opcode import OpCode +from qqbot.core.network.ws.dto.ws_payload import ( WSPayload, WsIdentifyData, WSResumeData, ) -from qqbot.core.network.websocket.ws_event_handler import parse_and_handle +from qqbot.core.network.ws_sync.ws_event_handler import parse_and_handle from qqbot.core.util import logging logger = logging.getLogger(__name__) diff --git a/qqbot/core/network/websocket/ws_event_handler.py b/qqbot/core/network/ws_sync/ws_event_handler.py similarity index 92% rename from qqbot/core/network/websocket/ws_event_handler.py rename to qqbot/core/network/ws_sync/ws_event_handler.py index e62ceef..3b887dc 100644 --- a/qqbot/core/network/websocket/ws_event_handler.py +++ b/qqbot/core/network/ws_sync/ws_event_handler.py @@ -2,7 +2,8 @@ import json -from qqbot.core.network.websocket.ws_event import WsEvent +from qqbot.core.network.ws.ws_event import WsEvent +from qqbot.core.network.ws.ws_handler import DefaultHandler from qqbot.core.util import logging from qqbot.model.audio import AudioAction from qqbot.model.channel import Channel @@ -13,21 +14,6 @@ logger = logging.getLogger(__name__) -class DefaultHandler: - """ - 持有handler的实例 - """ - - plain = None - guild = None - guild_member = None - channel = None - message = None - at_message = None - direct_message = None - audio = None - - def parse_and_handle(message_event, message): event_type = message_event["t"] call_handler = event_handler_dict.get(event_type) diff --git a/qqbot/core/network/websocket/ws_session_manager.py b/qqbot/core/network/ws_sync/ws_session_manager.py similarity index 92% rename from qqbot/core/network/websocket/ws_session_manager.py rename to qqbot/core/network/ws_sync/ws_session_manager.py index 455d73d..c2b730d 100644 --- a/qqbot/core/network/websocket/ws_session_manager.py +++ b/qqbot/core/network/ws_sync/ws_session_manager.py @@ -2,10 +2,10 @@ import asyncio -from qqbot.core.network.websocket.dto.enum_intents import Intents -from qqbot.core.network.websocket.ws_client import Client -from qqbot.core.network.websocket.ws_session import Session, ShardConfig -from qqbot.core.network.websocket.ws_session_pool import SessionPool +from qqbot.core.network.ws_sync.ws_client import Client +from qqbot.core.network.ws_sync.ws_session_pool import SessionPool +from qqbot.core.network.ws.dto.enum_intents import Intents +from qqbot.core.network.ws.ws_session import Session, ShardConfig from qqbot.core.util import logging from qqbot.model.token import Token @@ -77,7 +77,6 @@ def init_session_pool( max_async=websocket_ap["session_start_limit"]["max_concurrency"], session_manager=self, loop=asyncio.get_event_loop(), - session_count=shards_count, ) for i in range(shards_count): session = Session( diff --git a/qqbot/core/network/websocket/ws_session_pool.py b/qqbot/core/network/ws_sync/ws_session_pool.py similarity index 100% rename from qqbot/core/network/websocket/ws_session_pool.py rename to qqbot/core/network/ws_sync/ws_session_pool.py diff --git a/qqbot/model/__init__.py b/qqbot/model/__init__.py index 40a96af..2d0abcb 100644 --- a/qqbot/model/__init__.py +++ b/qqbot/model/__init__.py @@ -1 +1,9 @@ # -*- coding: utf-8 -*- +from .audio import AudioControl, STATUS +from .channel import ChannelType, ChannelSubType, PatchChannelRequest +from .channel import CreateChannelRequest +from .channel_permissions import ChannelPermissionsUpdateRequest +from .guild_member import QueryParams +from .guild_role import RoleUpdateInfo +from .message import CreateDirectMessageRequest, MessageSendRequest, Message +from .token import Token diff --git a/requirements.txt b/requirements.txt index a7551a7..8134c65 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ requests websocket-client pre-commit -PyYAML \ No newline at end of file +PyYAML +aiohttp>=3.6.0,<3.8.0 \ No newline at end of file diff --git a/setup.py b/setup.py index 77fb7bf..f358518 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,6 @@ from setuptools import setup, find_packages - setup( name="qq-bot", version=os.getenv("VERSION_NAME"), @@ -18,7 +17,7 @@ # 执照 license="Tencent", # 安装依赖 - install_requires=["requests", "websocket-client"], + install_requires=["requests", "websocket-client", "aiohttp>=3.6.0,<3.8.0"], # 分类 classifiers=[ # 发展时期,常见的如下 diff --git a/tests/core/network/websocket/__init__.py b/tests/core/network/websocket/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/qqbot/core/network/websocket/dto/__init__.py b/tests/core/network/ws/__init__.py similarity index 100% rename from qqbot/core/network/websocket/dto/__init__.py rename to tests/core/network/ws/__init__.py diff --git a/tests/core/network/websocket/test_ws_event.py b/tests/core/network/ws/test_ws_event.py similarity index 85% rename from tests/core/network/websocket/test_ws_event.py rename to tests/core/network/ws/test_ws_event.py index 42b75c4..ff5083a 100644 --- a/tests/core/network/websocket/test_ws_event.py +++ b/tests/core/network/ws/test_ws_event.py @@ -3,7 +3,7 @@ import unittest -from qqbot.core.network.websocket.ws_event import WsEvent +from qqbot.core.network.ws.ws_event import WsEvent class EventTestCase(unittest.TestCase): diff --git a/tests/test_api.py b/tests/test_api.py index 9f9905c..34df501 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -4,23 +4,17 @@ import unittest import qqbot -from qqbot import CreateDirectMessageRequest, MessageSendRequest, CreateChannelRequest from qqbot.core.exception.error import ( AuthenticationFailedError, SequenceNumberError, ServerError, ) from qqbot.core.util import logging -from qqbot.model.audio import AudioControl, STATUS -from qqbot.model.channel import ChannelType, ChannelSubType, PatchChannelRequest -from qqbot.model.channel_permissions import ChannelPermissionsUpdateRequest -from qqbot.model.guild_member import QueryParams -from qqbot.model.token import Token from tests import test_config logger = logging.getLogger(__name__) -token = Token(test_config["token"]["appid"], test_config["token"]["token"]) +token = qqbot.Token(test_config["token"]["appid"], test_config["token"]["token"]) test_params_ = test_config["test_params"] GUILD_ID = test_params_["guild_id"] GUILD_OWNER_ID = test_params_["guild_owner_id"] @@ -83,7 +77,7 @@ def test_guild_member(self): self.assertEqual(GUILD_OWNER_NAME, member.user.username) def test_guild_members(self): - query_params = QueryParams("0", 1) + query_params = qqbot.QueryParams("0", 1) try: members = self.api.get_guild_members(GUILD_ID, query_params) print(members) @@ -104,17 +98,17 @@ def test_channels(self): def test_create_update_delete_channel(self): # create - request = CreateChannelRequest( + request = qqbot.CreateChannelRequest( "channel_test", - ChannelType.TEXT_CHANNEL, - ChannelSubType.TALK, + qqbot.ChannelType.TEXT_CHANNEL, + qqbot.ChannelSubType.TALK, 99, CHANNEL_PARENT_ID, ) channel = self.api.create_channel(GUILD_ID, request) # patch - patch_channel = PatchChannelRequest( - "update_channel", ChannelType.TEXT_CHANNEL, 99, CHANNEL_PARENT_ID + patch_channel = qqbot.PatchChannelRequest( + "update_channel", qqbot.ChannelType.TEXT_CHANNEL, 99, CHANNEL_PARENT_ID ) api_patch_channel = self.api.update_channel(channel.id, patch_channel) self.assertEqual("update_channel", api_patch_channel.name) @@ -133,7 +127,7 @@ def test_channel_permissions(self): self.assertEqual("6", channel_permissions.permissions) def test_channel_permissions_update(self): - request = ChannelPermissionsUpdateRequest("0x0000000002", "") + request = qqbot.ChannelPermissionsUpdateRequest("0x0000000002", "") result = self.api.update_channel_permissions( CHANNEL_ID, GUILD_TEST_MEMBER_ID, request ) @@ -156,7 +150,7 @@ class AudioTestCase(unittest.TestCase): api = qqbot.AudioAPI(token, IS_SANDBOX) def test_post_audio(self): - audio = AudioControl("", "Test", STATUS.START) + audio = qqbot.AudioControl("", "Test", qqbot.STATUS.START) try: result = self.api.post_audio(CHANNEL_ID, audio) print(result) @@ -182,9 +176,9 @@ def test_get_message(self): def test_create_and_send_dms(self): try: # 私信接口需要链接ws,单元测试无法测试可以在run_websocket测试 - request = CreateDirectMessageRequest(GUILD_ID, GUILD_OWNER_ID) + request = qqbot.CreateDirectMessageRequest(GUILD_ID, GUILD_OWNER_ID) direct_message_guild = self.api.create_direct_message(request) - send_msg = MessageSendRequest("test") + send_msg = qqbot.MessageSendRequest("test") message = self.api.post_direct_message( direct_message_guild.guild_id, send_msg ) diff --git a/tests/test_async_api.py b/tests/test_async_api.py new file mode 100644 index 0000000..d049994 --- /dev/null +++ b/tests/test_async_api.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import asyncio +import unittest + +import qqbot +from qqbot.core.exception.error import ( + AuthenticationFailedError, + SequenceNumberError, + ServerError, +) +from qqbot.core.util import logging +from tests import test_config + +logger = logging.getLogger(__name__) + +token = qqbot.Token(test_config["token"]["appid"], test_config["token"]["token"]) +test_params_ = test_config["test_params"] +GUILD_ID = test_params_["guild_id"] +GUILD_OWNER_ID = test_params_["guild_owner_id"] +GUILD_OWNER_NAME = test_params_["guild_owner_name"] +GUILD_TEST_MEMBER_ID = test_params_["guild_test_member_id"] +GUILD_TEST_ROLE_ID = test_params_["guild_test_role_id"] +CHANNEL_ID = test_params_["channel_id"] +CHANNEL_NAME = test_params_["channel_name"] +CHANNEL_PARENT_ID = test_params_["channel_parent_id"] +ROBOT_NAME = test_params_["robot_name"] +IS_SANDBOX = test_params_["is_sandbox"] + + +class GuildAPITestCase(unittest.TestCase): + api = qqbot.AsyncGuildAPI(token, IS_SANDBOX) + loop = asyncio.get_event_loop() + + def test_guild(self): + guild = self.loop.run_until_complete(self.api.get_guild(GUILD_ID)) + self.assertNotEqual("", guild.name) + + +class GuildRoleAPITest(unittest.TestCase): + api = qqbot.AsyncGuildRoleAPI(token, IS_SANDBOX) + loop = asyncio.get_event_loop() + + def test_guild_roles(self): + guild_roles = self.loop.run_until_complete(self.api.get_guild_roles(GUILD_ID)) + self.assertEqual(GUILD_ID, guild_roles.guild_id) + + def test_guild_role_create_update_delete(self): + role_info = qqbot.RoleUpdateInfo("Test Role", 4278245297, 0) + result = self.loop.run_until_complete( + self.api.create_guild_role(GUILD_ID, role_info) + ) + role_id = result.role_id + self.assertEqual("Test Role", result.role.name) + + role_info = qqbot.RoleUpdateInfo("Test Update Role", 4278245297, 0) + result = self.loop.run_until_complete( + self.api.update_guild_role(GUILD_ID, role_id, role_info) + ) + self.assertEqual("Test Update Role", result.role.name) + + result = self.loop.run_until_complete( + self.api.delete_guild_role(GUILD_ID, role_id) + ) + self.assertEqual(True, result) + + def test_guild_role_member_add_delete(self): + result = self.loop.run_until_complete( + self.api.create_guild_role_member( + GUILD_ID, GUILD_TEST_ROLE_ID, GUILD_TEST_MEMBER_ID + ) + ) + self.assertEqual(True, result) + + def test_guild_role_member_delete(self): + result = self.loop.run_until_complete( + self.api.delete_guild_role_member( + GUILD_ID, GUILD_TEST_ROLE_ID, GUILD_TEST_MEMBER_ID + ) + ) + self.assertEqual(True, result) + + +class GuildMemberAPITestCase(unittest.TestCase): + api = qqbot.AsyncGuildMemberAPI(token, IS_SANDBOX) + loop = asyncio.get_event_loop() + + def test_guild_member(self): + member = self.loop.run_until_complete( + self.api.get_guild_member(GUILD_ID, GUILD_OWNER_ID) + ) + self.assertEqual(GUILD_OWNER_NAME, member.user.username) + + def test_guild_members(self): + query_params = qqbot.QueryParams("0", 1) + try: + members = self.loop.run_until_complete( + self.api.get_guild_members(GUILD_ID, query_params) + ) + print(members) + except AuthenticationFailedError as e: + print(e.args) + + +class ChannelAPITestCase(unittest.TestCase): + api = qqbot.AsyncChannelAPI(token, IS_SANDBOX) + loop = asyncio.get_event_loop() + + def test_channel(self): + channel = self.loop.run_until_complete(self.api.get_channel(CHANNEL_ID)) + self.assertEqual(CHANNEL_NAME, channel.name) + + def test_channels(self): + channels = self.loop.run_until_complete(self.api.get_channels(GUILD_ID)) + self.assertNotEqual(0, len(channels)) + + def test_create_update_delete_channel(self): + # create + request = qqbot.CreateChannelRequest( + "channel_test", + qqbot.ChannelType.TEXT_CHANNEL, + qqbot.ChannelSubType.TALK, + 99, + CHANNEL_PARENT_ID, + ) + channel = self.loop.run_until_complete( + self.api.create_channel(GUILD_ID, request) + ) + # patch + patch_channel = qqbot.PatchChannelRequest( + "update_channel", qqbot.ChannelType.TEXT_CHANNEL, 99, CHANNEL_PARENT_ID + ) + api_patch_channel = self.loop.run_until_complete( + self.api.update_channel(channel.id, patch_channel) + ) + self.assertEqual("update_channel", api_patch_channel.name) + # delete + delete_channel = self.loop.run_until_complete( + self.api.delete_channel(channel.id) + ) + self.assertEqual("update_channel", delete_channel.name) + + +class ChannelPermissionsTestCase(unittest.TestCase): + api = qqbot.AsyncChannelPermissionsAPI(token, IS_SANDBOX) + loop = asyncio.get_event_loop() + + def test_channel_permissions(self): + channel_permissions = self.loop.run_until_complete( + self.api.get_channel_permissions(CHANNEL_ID, GUILD_OWNER_ID) + ) + self.assertEqual("6", channel_permissions.permissions) + + def test_channel_permissions_update(self): + request = qqbot.ChannelPermissionsUpdateRequest("0x0000000002", "") + result = self.loop.run_until_complete( + self.api.update_channel_permissions( + CHANNEL_ID, GUILD_TEST_MEMBER_ID, request + ) + ) + self.assertEqual(True, result) + + +class UserAPITestCase(unittest.TestCase): + api = qqbot.AsyncUserAPI(token, IS_SANDBOX) + loop = asyncio.get_event_loop() + + def test_me(self): + user = self.loop.run_until_complete(self.api.me()) + self.assertEqual(ROBOT_NAME, user.username) + + def test_me_guilds(self): + guilds = self.loop.run_until_complete(self.api.me_guilds()) + self.assertNotEqual(0, len(guilds)) + + +class AudioTestCase(unittest.TestCase): + api = qqbot.AsyncAudioAPI(token, IS_SANDBOX) + loop = asyncio.get_event_loop() + + def test_post_audio(self): + audio = qqbot.AudioControl("", "Test", qqbot.STATUS.START) + try: + result = self.loop.run_until_complete( + self.api.post_audio(CHANNEL_ID, audio) + ) + print(result) + except AuthenticationFailedError as e: + print(e) + + +class MessageTestCase(unittest.TestCase): + api = qqbot.AsyncMessageAPI(token, IS_SANDBOX) + loop = asyncio.get_event_loop() + + def test_create_and_send_dms(self): + try: + # 私信接口需要链接ws,单元测试无法测试可以在run_websocket测试 + request = qqbot.CreateDirectMessageRequest(GUILD_ID, GUILD_OWNER_ID) + direct_message_guild = self.loop.run_until_complete( + self.api.create_direct_message(request) + ) + send_msg = qqbot.MessageSendRequest("test") + message = self.loop.run_until_complete( + self.api.post_direct_message(direct_message_guild.guild_id, send_msg) + ) + print(message.content) + except (SequenceNumberError, ServerError) as e: + print(e) + + +class WebsocketTestCase(unittest.TestCase): + api = qqbot.AsyncWebsocketAPI(token, IS_SANDBOX) + loop = asyncio.get_event_loop() + + def test_ws(self): + ws = self.loop.run_until_complete(self.api.ws()) + self.assertEqual(ws["url"], "wss://api.sgroup.qq.com/websocket") + + +if __name__ == "__main__": + unittest.main()