diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0aef9ac..b10edef 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,12 +15,3 @@ repos: rev: 5.10.1 hooks: - id: isort - - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.942 - hooks: - - id: mypy - additional_dependencies: - - types-requests - - types-setuptools - - types-toml diff --git a/efb_qq_plugin_go_cqhttp/ChatMgr.py b/efb_qq_plugin_go_cqhttp/ChatMgr.py index ee0698a..2ddb477 100644 --- a/efb_qq_plugin_go_cqhttp/ChatMgr.py +++ b/efb_qq_plugin_go_cqhttp/ChatMgr.py @@ -20,10 +20,10 @@ class ChatManager: channel=self.channel, uid=ChatID("__error_chat__"), name="Chat Missing" ) - def build_efb_chat_as_private(self, context): + async def build_efb_chat_as_private(self, context): uid = context["user_id"] if "sender" not in context or "nickname" not in context["sender"]: - i: dict = self.channel.QQClient.get_stranger_info(uid) + i: dict = await self.channel.QQClient.get_stranger_info(uid) chat_name = "" if i: chat_name = i["nickname"] @@ -37,13 +37,13 @@ class ChatManager: ) return efb_chat - def build_or_get_efb_member(self, chat: Chat, context): + async def build_or_get_efb_member(self, chat: Chat, context): member_uid = context["user_id"] with contextlib.suppress(KeyError): return chat.get_member(str(member_uid)) chat_name = "" if "nickname" not in context: - i: dict = self.channel.QQClient.get_stranger_info(member_uid) + i: dict = await self.channel.QQClient.get_stranger_info(member_uid) chat_name = "" if i: chat_name = i["nickname"] @@ -55,20 +55,20 @@ class ChatManager: uid=str(member_uid), ) - def build_efb_chat_as_group(self, context, update_member=False): # Should be cached + async def build_efb_chat_as_group(self, context, update_member=False): # Should be cached is_discuss = False if context["message_type"] == "group" else True chat_uid = context["discuss_id"] if is_discuss else context["group_id"] efb_chat = GroupChat(channel=self.channel, uid=str(chat_uid)) if not is_discuss: efb_chat.uid = "group" + "_" + str(chat_uid) - i = self.channel.QQClient.get_group_info(chat_uid) + i = await self.channel.QQClient.get_group_info(chat_uid) if i is not None: efb_chat.name = str(i["group_name"]) if "group_name" not in context else str(context["group_name"]) else: efb_chat.name = str(chat_uid) efb_chat.vendor_specific = {"is_discuss": False} if update_member: - members = self.channel.QQClient.get_group_member_list(chat_uid, False) + members = await self.channel.QQClient.get_group_member_list(chat_uid, False) if members: for member in members: efb_chat.add_member( diff --git a/efb_qq_plugin_go_cqhttp/GoCQHttp.py b/efb_qq_plugin_go_cqhttp/GoCQHttp.py index ea18b6e..07d94c6 100644 --- a/efb_qq_plugin_go_cqhttp/GoCQHttp.py +++ b/efb_qq_plugin_go_cqhttp/GoCQHttp.py @@ -1,3 +1,4 @@ +import asyncio import copy import logging import tempfile @@ -5,14 +6,11 @@ import threading import time import uuid from datetime import datetime, timedelta -from gettext import translation from typing import Any, BinaryIO, Dict, List, Optional, Tuple, Union -import cherrypy -import cqhttp -from cherrypy._cpserver import Server -from cherrypy.process.wspbus import states -from cqhttp import CQHttp +import aiocqhttp +from aiocqhttp import CQHttp, Event +from aiocqhttp.exceptions import NetworkError from efb_qq_slave import BaseClient, QQMessengerChannel from ehforwarderbot import Chat, Message, MsgType, Status, coordinator from ehforwarderbot.chat import ( @@ -31,9 +29,10 @@ from ehforwarderbot.message import MessageCommand, MessageCommands from ehforwarderbot.status import MessageRemoval from ehforwarderbot.types import ChatID, MessageID from ehforwarderbot.utils import extra +from hypercorn.asyncio import serve +from hypercorn.config import Config as HyperConfig from PIL import Image -from pkg_resources import resource_filename -from requests import RequestException +from quart.logging import create_serving_logger from .ChatMgr import ChatManager from .Exceptions import ( @@ -63,12 +62,6 @@ class GoCQHttp(BaseClient): logger: logging.Logger = logging.getLogger(__name__) channel: QQMessengerChannel - translator = translation( - "efb_qq_slave", - resource_filename("efb_qq_slave", "Clients/CoolQ/locale"), - fallback=True, - ) - friend_list: List[Dict] = [] friend_dict: Dict[int, dict] = {} stranger_dict: Dict[int, dict] = {} @@ -80,14 +73,6 @@ class GoCQHttp(BaseClient): extra_group_list: List[Dict] = [] repeat_counter = 0 update_repeat_counter = 0 - event = threading.Event() - update_contacts_timer: threading.Timer - self_update_timer: threading.Timer - check_status_timer: threading.Timer - cherryServer: Server - - can_send_image: bool = False - can_send_voice: bool = False def __init__(self, client_id: str, config: Dict[str, Any], channel): super().__init__(client_id, config) @@ -103,10 +88,15 @@ class GoCQHttp(BaseClient): self.is_logged_in = False self.msg_decorator = QQMsgProcessor(instance=self) - def forward_msgs_wrapper(msg_elements: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + self.loop = asyncio.get_event_loop() + self.shutdown_event = asyncio.Event() + + asyncio.set_event_loop(self.loop) + + async def forward_msgs_wrapper(msg_elements: List[Dict[str, Any]]) -> List[Dict[str, Any]]: fmt_msgs: List[Dict] = [] for msg in msg_elements: - from_user = self.get_user_info(msg["sender"]["user_id"]) + from_user = await self.get_user_info(msg["sender"]["user_id"]) header_text = {"data": {"text": f'{from_user["remark"]}({from_user["nickname"]}):\n'}, "type": "text"} footer_text = {"data": {"text": "\n- - - - - - - - - - - - - - -\n"}, "type": "text"} msg["content"].insert(0, header_text) @@ -116,12 +106,12 @@ class GoCQHttp(BaseClient): if i == 1: fmt_msgs.pop() msg["content"].pop() - fmt_msgs += forward_msgs_wrapper([inner_msg]) + fmt_msgs += await forward_msgs_wrapper([inner_msg]) else: fmt_msgs.append(inner_msg) return fmt_msgs - def message_element_wrapper( + async def message_element_wrapper( context: Dict[str, Any], msg_element: Dict[str, Any], chat: Chat ) -> Tuple[str, List[Message], List[Tuple[Tuple[int, int], Union[Chat, ChatMember]]]]: msg_type = msg_element["type"] @@ -142,12 +132,12 @@ class GoCQHttp(BaseClient): elif msg_type == "at": # todo Recheck if bug exists g_id = context["group_id"] - my_uid = self.get_qq_uid() + my_uid = await self.get_qq_uid() self.logger.debug("My QQ uid: %s\n" "QQ mentioned: %s\n", my_uid, msg_data["qq"]) if str(msg_data["qq"]) == "all": group_card = "all" else: - member_info = self.get_user_info(msg_data["qq"], group_id=g_id)["in_group_info"] + member_info = (await self.get_user_info(msg_data["qq"], group_id=g_id))["in_group_info"] group_card = member_info["card"] if member_info["card"] != "" else member_info["nickname"] self.logger.debug("Group card: {}".format(group_card)) substitution_begin = len(main_text) @@ -157,34 +147,34 @@ class GoCQHttp(BaseClient): at_dict = ((substitution_begin, substitution_end), chat.self) at_list.append(at_dict) elif msg_type == "reply": - ref_user = self.get_user_info(msg_data["qq"]) + ref_user = await self.get_user_info(msg_data["qq"]) main_text = ( f'「{ref_user["remark"]}({ref_user["nickname"]}):{msg_data["text"]}」\n' "- - - - - - - - - - - - - - -\n" ) elif msg_type == "forward": - forward_msgs = self.coolq_api_query("get_forward_msg", message_id=msg_data["id"])["messages"] + forward_msgs = (await self.coolq_api_query("get_forward_msg", message_id=msg_data["id"]))["messages"] logging.debug(f"Forwarded message: {forward_msgs}") - fmt_forward_msgs = forward_msgs_wrapper(forward_msgs) + fmt_forward_msgs = await forward_msgs_wrapper(forward_msgs) logging.debug(f"Formated forwarded message: {forward_msgs}") header_msg = {"data": {"text": "合并转发消息开始\n- - - - - - - - - - - - - - -\n"}, "type": "text"} footer_msg = {"data": {"text": "合并转发消息结束"}, "type": "text"} fmt_forward_msgs.insert(0, header_msg) fmt_forward_msgs.append(footer_msg) - main_text, messages, _ = message_elements_wrapper(context, fmt_forward_msgs, chat) + main_text, messages, _ = await message_elements_wrapper(context, fmt_forward_msgs, chat) return main_text, messages, [] else: messages.extend(self.call_msg_decorator(msg_type, msg_data, chat)) return main_text, messages, at_list - def message_elements_wrapper( + async def message_elements_wrapper( context: Dict[str, Any], msg_elements: List[Dict[str, Any]], chat: Chat ) -> Tuple[str, List[Message], Dict[Tuple[int, int], Union[Chat, ChatMember]]]: messages: List[Message] = [] main_text: str = "" at_dict: Dict[Tuple[int, int], Union[Chat, ChatMember]] = {} for msg_element in msg_elements: - sub_main_text, sub_messages, sub_at_list = message_element_wrapper(context, msg_element, chat) + sub_main_text, sub_messages, sub_at_list = await message_element_wrapper(context, msg_element, chat) main_text_len = len(main_text) for at_tuple in sub_at_list: pos = ( @@ -197,95 +187,97 @@ class GoCQHttp(BaseClient): return main_text, messages, at_dict @self.coolq_bot.on_message - def handle_msg(context): - self.logger.debug(repr(context)) - msg_elements = context["message"] - qq_uid = context["user_id"] - chat: Chat - author: ChatMember + async def handle_msg(context: Event): + async def _handle_msg(): + self.logger.debug(repr(context)) + msg_elements = context["message"] + qq_uid = context["user_id"] + chat: Chat + author: ChatMember - user = self.get_user_info(qq_uid) - if context["message_type"] == "private": - context["alias"] = user["remark"] - chat: PrivateChat = self.chat_manager.build_efb_chat_as_private(context) - else: - chat = self.chat_manager.build_efb_chat_as_group(context) - - if "anonymous" not in context or context["anonymous"] is None: - if context["message_type"] == "group": - if context["sub_type"] == "notice": - context["event_description"] = "System Notification" - context["uid_prefix"] = "group_notification" - author = chat.add_system_member( - name=context["event_description"], - uid=ChatID("__{context[uid_prefix]}__".format(context=context)), - ) - else: - user = self.get_user_info(qq_uid, group_id=context["group_id"]) - context["nickname"] = user["remark"] - context["alias"] = user["in_group_info"]["card"] - author = self.chat_manager.build_or_get_efb_member(chat, context) - elif context["message_type"] == "private": - author = chat.other + user = await self.get_user_info(qq_uid) + if context["message_type"] == "private": + context["alias"] = user["remark"] + chat: PrivateChat = await self.chat_manager.build_efb_chat_as_private(context) else: - author = self.chat_manager.build_or_get_efb_member(chat, context) - else: # anonymous user in group - author = self.chat_manager.build_efb_chat_as_anonymous_user(chat, context) + chat = await self.chat_manager.build_efb_chat_as_group(context) - main_text, messages, at_dict = message_elements_wrapper(context, msg_elements, chat) + if "anonymous" not in context or context["anonymous"] is None: + if context["message_type"] == "group": + if context["sub_type"] == "notice": + context["event_description"] = "System Notification" + context["uid_prefix"] = "group_notification" + author = chat.add_system_member( + name=context["event_description"], + uid=ChatID("__{context[uid_prefix]}__".format(context=context)), + ) + else: + user = await self.get_user_info(qq_uid, group_id=context["group_id"]) + context["nickname"] = user["remark"] + if user["is_in_group"]: + context["alias"] = user["in_group_info"]["card"] + else: + context["alias"] = user["remark"] + author = await self.chat_manager.build_or_get_efb_member(chat, context) + elif context["message_type"] == "private": + author = chat.other + else: + author = await self.chat_manager.build_or_get_efb_member(chat, context) + else: # anonymous user in group + author = self.chat_manager.build_efb_chat_as_anonymous_user(chat, context) - if main_text != "": - messages.append(self.msg_decorator.qq_text_simple_wrapper(main_text, at_dict)) - coolq_msg_id = context["message_id"] - for i in range(len(messages)): - if not isinstance(messages[i], Message): - continue - efb_msg: Message = messages[i] - efb_msg.uid = ( - f"{chat.uid.split('_')[-1]}_{coolq_msg_id}_{i}" - if i > 0 - else f"{chat.uid.split('_')[-1]}_{coolq_msg_id}" - ) - efb_msg.chat = chat - efb_msg.author = author - # if qq_uid != '80000000': + main_text, messages, at_dict = await message_elements_wrapper(context, msg_elements, chat) - # Append discuss group into group list - if context["message_type"] == "discuss" and efb_msg.chat not in self.discuss_list: - self.discuss_list.append(efb_msg.chat) + if main_text != "": + messages.append(self.msg_decorator.qq_text_simple_wrapper(main_text, at_dict)) + coolq_msg_id = context["message_id"] + for i in range(len(messages)): + if not isinstance(messages[i], Message): + continue + efb_msg: Message = messages[i] + efb_msg.uid = ( + f"{chat.uid.split('_')[-1]}_{coolq_msg_id}_{i}" + if i > 0 + else f"{chat.uid.split('_')[-1]}_{coolq_msg_id}" + ) + efb_msg.chat = chat + efb_msg.author = author + # if qq_uid != '80000000': - efb_msg.deliver_to = coordinator.master + # Append discuss group into group list + if context["message_type"] == "discuss" and efb_msg.chat not in self.discuss_list: + self.discuss_list.append(efb_msg.chat) - def send_message_wrapper(*args, **kwargs): - threading.Thread(target=async_send_messages_to_master, args=args, kwargs=kwargs).start() + efb_msg.deliver_to = coordinator.master + async_send_messages_to_master(efb_msg) - send_message_wrapper(efb_msg) + asyncio.create_task(_handle_msg()) @self.coolq_bot.on_notice("group_increase") - def handle_group_increase_msg(context): + async def handle_group_increase_msg(context: Event): context["event_description"] = "\u2139 Group Member Increase Event" if (context["sub_type"]) == "invite": text = "{nickname}({context[user_id]}) joined the group({group_name}) via invitation" else: text = "{nickname}({context[user_id]}) joined the group({group_name})" - original_group = self.get_group_info(context["group_id"], False) + original_group = await self.get_group_info(context["group_id"], False) group_name = context["group_id"] if original_group is not None and "group_name" in original_group: group_name = original_group["group_name"] text = text.format( - nickname=self.get_stranger_info(context["user_id"])["nickname"], + nickname=(await self.get_stranger_info(context["user_id"]))["nickname"], context=context, group_name=group_name, ) context["message"] = text - self.send_efb_group_notice(context) + await self.send_efb_group_notice(context) @self.coolq_bot.on_notice("group_decrease") - def handle_group_decrease_msg(context): + async def handle_group_decrease_msg(context: Event): context["event_description"] = "\u2139 Group Member Decrease Event" - original_group = self.get_group_info(context["group_id"], False) + original_group = await self.get_group_info(context["group_id"], False) group_name = context["group_id"] if original_group is not None and "group_name" in original_group: group_name = original_group["group_name"] @@ -298,36 +290,36 @@ class GoCQHttp(BaseClient): else: text = "{nickname}({context[user_id]}) was kicked from the group({group_name})" text = text.format( - nickname=self.get_stranger_info(context["user_id"])["nickname"], + nickname=(await self.get_stranger_info(context["user_id"]))["nickname"], context=context, group_name=group_name, ) context["message"] = text - self.send_efb_group_notice(context) + await self.send_efb_group_notice(context) @self.coolq_bot.on_notice("group_admin") - def handle_group_admin_msg(context): + async def handle_group_admin_msg(context: Event): context["event_description"] = "\u2139 Group Admin Change Event" if (context["sub_type"]) == "set": text = "{nickname}({context[user_id]}) has been appointed as the group({group_name}) administrator" else: text = "{nickname}({context[user_id]}) has been de-appointed as the group({group_name}) administrator" - original_group = self.get_group_info(context["group_id"], False) + original_group = await self.get_group_info(context["group_id"], False) group_name = context["group_id"] if original_group is not None and "group_name" in original_group: group_name = original_group["group_name"] text = text.format( - nickname=self.get_stranger_info(context["user_id"])["nickname"], + nickname=(await self.get_stranger_info(context["user_id"]))["nickname"], context=context, group_name=group_name, ) context["message"] = text - self.send_efb_group_notice(context) + await self.send_efb_group_notice(context) @self.coolq_bot.on_notice("group_ban") - def handle_group_ban_msg(context): + async def handle_group_ban_msg(context: Event): context["event_description"] = "\u2139 Group Member Restrict Event" if (context["sub_type"]) == "ban": text = ( @@ -344,79 +336,86 @@ class GoCQHttp(BaseClient): ) time_text = "" - original_group = self.get_group_info(context["group_id"], False) + original_group = await self.get_group_info(context["group_id"], False) group_name = context["group_id"] if original_group is not None and "group_name" in original_group: group_name = original_group["group_name"] text = text.format( - nickname=self.get_stranger_info(context["user_id"])["nickname"], + nickname=(await self.get_stranger_info(context["user_id"]))["nickname"], context=context, time=time_text, group_name=group_name, - nickname_=self.get_stranger_info(context["operator_id"])["nickname"], + nickname_=(await self.get_stranger_info(context["operator_id"]))["nickname"], ) context["message"] = text - self.send_efb_group_notice(context) + await self.send_efb_group_notice(context) @self.coolq_bot.on_notice("offline_file") - def handle_offline_file_upload_msg(context): - context["event_description"] = "\u2139 Offline File Upload Event" - context["uid_prefix"] = "offline_file" - file_info_msg = ("Filename: {file[name]}\n" "File size: {file[size]}").format(file=context["file"]) - user = self.get_user_info(context["user_id"]) - text = "{remark}({nickname}) uploaded a file to you\n" - text = text.format(remark=user["remark"], nickname=user["nickname"]) + file_info_msg - context["message"] = text - self.send_msg_to_master(context) - param_dict = { - "context": context, - "download_url": context["file"]["url"], - } - threading.Thread(target=self.async_download_file, args=[], kwargs=param_dict).start() + async def handle_offline_file_upload_msg(context: Event): + async def _handle_offline_file_upload_msg(): + context["event_description"] = "\u2139 Offline File Upload Event" + context["uid_prefix"] = "offline_file" + file_info_msg = ("Filename: {file[name]}\n" "File size: {file[size]}").format(file=context["file"]) + user = await self.get_user_info(context["user_id"]) + text = "{remark}({nickname}) uploaded a file to you\n" + text = text.format(remark=user["remark"], nickname=user["nickname"]) + file_info_msg + context["message"] = text + self.send_msg_to_master(context) + param_dict = { + "context": context, + "download_url": context["file"]["url"], + } + self.async_download_file(**param_dict) + + asyncio.create_task(_handle_offline_file_upload_msg()) @self.coolq_bot.on_notice("group_upload") - def handle_group_file_upload_msg(context): - context["event_description"] = "\u2139 Group File Upload Event" - context["uid_prefix"] = "group_upload" - original_group = self.get_group_info(context["group_id"], False) - group_name = context["group_id"] - if original_group is not None and "group_name" in original_group: - group_name = original_group["group_name"] + async def handle_group_file_upload_msg(context: Event): + async def _handle_group_file_upload_msg(): + context["event_description"] = "\u2139 Group File Upload Event" + context["uid_prefix"] = "group_upload" + original_group = await self.get_group_info(context["group_id"], False) + group_name = context["group_id"] + if original_group is not None and "group_name" in original_group: + group_name = original_group["group_name"] - file_info_msg = ("File ID: {file[id]}\n" "Filename: {file[name]}\n" "File size: {file[size]}").format( - file=context["file"] - ) - member_info = self.get_user_info(context["user_id"], group_id=context["group_id"])["in_group_info"] - group_card = member_info["card"] if member_info["card"] != "" else member_info["nickname"] - text = "{member_card}({context[user_id]}) uploaded a file to group({group_name})\n" - text = text.format(member_card=group_card, context=context, group_name=group_name) + file_info_msg - context["message"] = text - self.send_efb_group_notice(context) + file_info_msg = ("File ID: {file[id]}\n" "Filename: {file[name]}\n" "File size: {file[size]}").format( + file=context["file"] + ) + member_info = (await self.get_user_info(context["user_id"], group_id=context["group_id"]))[ + "in_group_info" + ] + group_card = member_info["card"] if member_info["card"] != "" else member_info["nickname"] + text = "{member_card}({context[user_id]}) uploaded a file to group({group_name})\n" + text = text.format(member_card=group_card, context=context, group_name=group_name) + file_info_msg + context["message"] = text + await self.send_efb_group_notice(context) - param_dict = { - "context": context, - "group_id": context["group_id"], - "file_id": context["file"]["id"], - "busid": context["file"]["busid"], - } + param_dict = { + "context": context, + "group_id": context["group_id"], + "file_id": context["file"]["id"], + "busid": context["file"]["busid"], + } + self.async_download_group_file(**param_dict) - threading.Thread(target=self.async_download_group_file, args=[], kwargs=param_dict).start() + asyncio.create_task(_handle_group_file_upload_msg()) @self.coolq_bot.on_notice("friend_add") - def handle_friend_add_msg(context): + async def handle_friend_add_msg(context: Event): context["event_description"] = "\u2139 New Friend Event" context["uid_prefix"] = "friend_add" text = "{nickname}({context[user_id]}) has become your friend!" text = text.format( - nickname=self.get_stranger_info(context["user_id"])["nickname"], + nickname=(await self.get_stranger_info(context["user_id"]))["nickname"], context=context, ) context["message"] = text self.send_msg_to_master(context) @self.coolq_bot.on_notice("group_recall") - def handle_group_recall_msg(context): + async def handle_group_recall_msg(context: Event): coolq_msg_id = context["message_id"] chat = GroupChat(channel=self.channel, uid=f"group_{context['group_id']}") @@ -426,11 +425,11 @@ class GoCQHttp(BaseClient): ) @self.coolq_bot.on_notice("friend_recall") - def handle_friend_recall_msg(context): + async def handle_friend_recall_msg(context: Event): coolq_msg_id = context["message_id"] try: - chat: PrivateChat = self.chat_manager.build_efb_chat_as_private(context) + chat: PrivateChat = await self.chat_manager.build_efb_chat_as_private(context) except Exception: return efb_msg = Message(chat=chat, uid=MessageID(f"{chat.uid.split('_')[-1]}_{coolq_msg_id}")) @@ -439,7 +438,7 @@ class GoCQHttp(BaseClient): ) @self.coolq_bot.on_request("friend") # Add friend request - def handle_add_friend_request(context): + async def handle_add_friend_request(context: Event): self.logger.debug(repr(context)) context["event_description"] = "\u2139 New Friend Request" context["uid_prefix"] = "friend_request" @@ -449,7 +448,7 @@ class GoCQHttp(BaseClient): "{context[comment]}" ) text = text.format( - nickname=self.get_stranger_info(context["user_id"])["nickname"], + nickname=(await self.get_stranger_info(context["user_id"]))["nickname"], context=context, ) context["message"] = text @@ -469,34 +468,34 @@ class GoCQHttp(BaseClient): self.send_msg_to_master(context) @self.coolq_bot.on_request("group") - def handle_group_request(context): + async def handle_group_request(context: Event): self.logger.debug(repr(context)) context["uid_prefix"] = "group_request" - context["group_name"] = ("[Request]") + self.get_group_info(context["group_id"])["group_name"] + context["group_name"] = ("[Request]") + (await self.get_group_info(context["group_id"]))["group_name"] context["group_id_orig"] = context["group_id"] context["group_id"] = str(context["group_id"]) + "_notification" context["message_type"] = "group" context["event_description"] = "\u2139 New Group Join Request" - original_group = self.get_group_info(context["group_id_orig"], False) + original_group = await self.get_group_info(context["group_id_orig"], False) group_name = context["group_id"] if original_group is not None and "group_name" in original_group: group_name = original_group["group_name"] msg = Message() msg.uid = "group" + "_" + str(context["group_id"]) msg.author = (self.chat_manager.build_efb_chat_as_system_user(context)).other - msg.chat = self.chat_manager.build_efb_chat_as_group(context) + msg.chat = await self.chat_manager.build_efb_chat_as_group(context) msg.deliver_to = coordinator.master msg.type = MsgType.Text name = "" - if not self.get_friend_remark(context["user_id"]): + if not await self.get_friend_remark(context["user_id"]): name = "{}({})[{}] ".format( - self.get_stranger_info(context["user_id"])["nickname"], - self.get_friend_remark(context["user_id"]), + (await self.get_stranger_info(context["user_id"]))["nickname"], + await self.get_friend_remark(context["user_id"]), context["user_id"], ) else: name = "{}[{}] ".format( - self.get_stranger_info(context["user_id"])["nickname"], + (await self.get_stranger_info(context["user_id"]))["nickname"], context["user_id"], ) msg.text = "{} wants to join the group {}({}). \nHere is the comment: {}".format( @@ -526,50 +525,32 @@ class GoCQHttp(BaseClient): ) coordinator.send_message(msg) - self.check_status_periodically(threading.Event()) - self.update_contacts_timer = threading.Timer(1800, self.update_contacts_periodically, [threading.Event()]) - self.update_contacts_timer.start() - # threading.Thread(target=self.check_running_status).start() + asyncio.run(self.check_status_periodically(run_once=True)) - def run_instance(self, *args, **kwargs): - # threading.Thread(target=self.coolq_bot.run, args=args, kwargs=kwargs, daemon=True).start() - cherrypy.tree.graft(self.coolq_bot.wsgi, "/") - cherrypy.server.unsubscribe() - self.cherryServer = Server() - self.cherryServer.socket_host = self.client_config["host"] - self.cherryServer.socket_port = self.client_config["port"] - self.cherryServer.subscribe() - cherrypy.engine.start() - cherrypy.engine.wait(states.EXITING) + def run_instance(self, host: str, port: int, debug: bool = False): + def _run(): + config = HyperConfig() + config.access_log_format = "%(h)s %(r)s %(s)s %(b)s %(D)s" + access_logger = create_serving_logger() + access_logger.setLevel(logging.WARNING) + config.accesslog = access_logger + config.bind = [f"{host}:{port}"] + if debug is not None: + self.debug = debug + config.use_reloader = debug + config.errorlog = config.accesslog - @extra( - name=("Restart CoolQ Client"), - desc=( - "Force CoolQ to restart\n" - "Usage: {function_name} [-l] [-c] [-e]\n" - " -l: Restart and clean log\n" - " -c: Restart and clean cache\n" - " -e: Restart and clean event\n" - ), - ) - def relogin(self, param: str = ""): - param_dict = dict() - if param: - params = param.split(" ") - for each_param in params: - if each_param == " ": - continue - if each_param == "-l": - param_dict["clean_log"] = "true" - elif each_param == "-c": - param_dict["clean_cache"] = "true" - elif each_param == "-e": - param_dict["clean_event"] = "true" - else: - return ("Unknown parameter: {}.").format(param) - self.logger.debug(repr(param_dict)) - self.coolq_api_query("_set_restart", **param_dict) - return "Done. Please wait for a while." + self.loop.create_task(serve(self.coolq_bot.server_app, config, shutdown_trigger=self.shutdown_event.wait)) + self.loop.create_task(self.check_status_periodically()) + self.loop.create_task(self.update_contacts_periodically()) + self.loop.run_forever() + + self.t = threading.Thread(target=_run) + self.t.daemon = True + self.t.start() + + def relogin(self): + raise NotImplementedError def logout(self): raise NotImplementedError @@ -579,17 +560,17 @@ class GoCQHttp(BaseClient): desc=("Force efb-qq-slave to refresh status from CoolQ Client.\n" "Usage: {function_name}"), ) def login(self, param: str = ""): - self.check_status_periodically(None) + asyncio.run(self.check_status_periodically(run_once=True)) return "Done" - def get_stranger_info(self, user_id: int, no_cache: bool = False) -> Dict[str, Any]: + async def get_stranger_info(self, user_id: int, no_cache: bool = False) -> Dict[str, Any]: user_id = int(user_id) - return self.get_user_info(user_id, no_cache=no_cache) + return await self.get_user_info(user_id, no_cache=no_cache) - def get_login_info(self) -> Dict[Any, Any]: - res = self.coolq_bot.get_status() + async def get_login_info(self) -> Dict[Any, Any]: + res = await self.coolq_bot.get_status() if "good" in res or "online" in res: - data = self.coolq_bot.get_login_info() + data = await self.coolq_bot.get_login_info() return { "status": 0, "data": {"uid": data["user_id"], "nickname": data["nickname"]}, @@ -597,15 +578,15 @@ class GoCQHttp(BaseClient): else: return {"status": 1} - def get_groups(self) -> List: + async def get_groups(self) -> List: # todo Add support for discuss group iteration - self.update_group_list() # Force update group list + await self.update_group_list() # Force update group list res = self.group_list # res = self.coolq_bot.get_group_list() groups = [] for i in range(len(res)): context = {"message_type": "group", "group_id": res[i]["group_id"]} - efb_chat = self.chat_manager.build_efb_chat_as_group(context) + efb_chat = await self.chat_manager.build_efb_chat_as_group(context) groups.append(efb_chat) for i in range(len(self.extra_group_list)): does_exist = False @@ -619,13 +600,13 @@ class GoCQHttp(BaseClient): "message_type": "group", "group_id": self.extra_group_list[i]["group_id"], } - efb_chat = self.chat_manager.build_efb_chat_as_group(context) + efb_chat = await self.chat_manager.build_efb_chat_as_group(context) groups.append(efb_chat) return groups + self.discuss_list - def get_friends(self) -> List: + async def get_friends(self) -> List: try: - self.update_friend_list() # Force update friend list + await self.update_friend_list() # Force update friend list except CoolQAPIFailureException: self.deliver_alert_to_master(("Failed to retrieve the friend list.\n" "Only groups are shown.")) return [] @@ -636,7 +617,7 @@ class GoCQHttp(BaseClient): "nickname": current_user["nickname"], "alias": current_user["remark"], } - efb_chat = self.chat_manager.build_efb_chat_as_private(context) + efb_chat = await self.chat_manager.build_efb_chat_as_private(context) users.append(efb_chat) return users @@ -663,7 +644,7 @@ class GoCQHttp(BaseClient): if msg.edit: try: uid_type = msg.uid.split("_") - self.recall_message(uid_type[1]) + asyncio.run(self.recall_message(uid_type[1])) except CoolQAPIFailureException: raise EFBOperationNotSupported( ("Failed to recall the message!\n" "This message may have already expired.") @@ -673,7 +654,7 @@ class GoCQHttp(BaseClient): if msg.text == "kick`": group_id = chat_type[1] user_id = msg.target.author.uid - self.coolq_api_query("set_group_kick", group_id=group_id, user_id=user_id) + asyncio.run(self.coolq_api_query("set_group_kick", group_id=group_id, user_id=user_id)) else: if isinstance(msg.target, Message): max_length = 50 @@ -688,16 +669,11 @@ class GoCQHttp(BaseClient): tgt_text, coolq_text_encode(msg.text), ) - msg.uid = self.coolq_send_message(chat_type[0], chat_type[1], msg.text) + msg.uid = asyncio.run(self.coolq_send_message(chat_type[0], chat_type[1], msg.text)) self.logger.debug("[%s] Sent as a text message. %s", msg.uid, msg.text) elif msg.type in (MsgType.Image, MsgType.Sticker, MsgType.Animation): self.logger.info("[%s] Image/Sticker/Animation %s", msg.uid, msg.type) text = "" - if not self.can_send_image: - self.check_features() # Force checking features - raise EFBOperationNotSupported( - ("Unable to send image now. Please check your CoolQ version " "or retry later") - ) if msg.type != MsgType.Sticker: text += m.coolq_code_image_wrapper(msg.file, msg.path) else: @@ -715,23 +691,17 @@ class GoCQHttp(BaseClient): f.seek(0) text += m.coolq_code_image_wrapper(f, f.name) if msg.text: - msg.uid = self.coolq_send_message(chat_type[0], chat_type[1], text + coolq_text_encode(msg.text)) + msg.uid = asyncio.run( + self.coolq_send_message(chat_type[0], chat_type[1], text + coolq_text_encode(msg.text)) + ) else: - msg.uid = self.coolq_send_message(chat_type[0], chat_type[1], text) + msg.uid = asyncio.run(self.coolq_send_message(chat_type[0], chat_type[1], text)) # todo More MsgType Support elif msg.type is MsgType.Voice: - if not self.can_send_voice: - self.check_features() # Force checking features - raise EFBOperationNotSupported( - ( - "Unable to send voice now. Please check your CoolQ version " - " and install CoolQ audio library or retry later" - ) - ) text = m.coolq_voice_image_wrapper(msg.file, msg.path) - msg.uid = self.coolq_send_message(chat_type[0], chat_type[1], text) + msg.uid = asyncio.run(self.coolq_send_message(chat_type[0], chat_type[1], text)) if msg.text: - self.coolq_send_message(chat_type[0], chat_type[1], msg.text) + asyncio.run(self.coolq_send_message(chat_type[0], chat_type[1], msg.text)) return msg def call_msg_decorator(self, msg_type: str, *args) -> List[Message]: @@ -744,21 +714,21 @@ class GoCQHttp(BaseClient): else: return func(*args) - def get_qq_uid(self): - res = self.get_login_info() + async def get_qq_uid(self): + res = await self.get_login_info() if res["status"] == 0: return res["data"]["uid"] else: return None - def get_group_member_list(self, group_id, no_cache=False) -> List[Dict[str, Any]]: + async def get_group_member_list(self, group_id, no_cache=False) -> List[Dict[str, Any]]: if ( no_cache or (group_id not in self.group_member_dict) or (datetime.now() - self.group_member_dict[group_id]["time"] > timedelta(hours=1)) ): # Force Update try: - member_list = self.coolq_api_query("get_group_member_list", group_id=group_id, no_cache=no_cache) + member_list = await self.coolq_api_query("get_group_member_list", group_id=group_id, no_cache=no_cache) except CoolQAPIFailureException as e: self.deliver_alert_to_master(("Failed the get group member detail.") + "{}".format(e)) return [] @@ -768,39 +738,48 @@ class GoCQHttp(BaseClient): } return self.group_member_dict[group_id]["members"] - def get_user_info(self, user_id: int, group_id: Optional[str] = None, no_cache=False): + async def get_user_info(self, user_id: int, group_id: Optional[str] = None, no_cache=False): user_id = int(user_id) if no_cache or (not self.friend_list) or (user_id not in self.friend_dict): - self.update_friend_list() + await self.update_friend_list() friend = self.friend_dict.get(user_id) if friend: user = copy.deepcopy(friend) user["is_friend"] = True else: if no_cache: - stranger = self.coolq_api_query("get_stranger_info", user_id=user_id) + stranger = await self.coolq_api_query("get_stranger_info", user_id=user_id) self.stranger_dict[user_id] = stranger stranger = self.stranger_dict.get(user_id) if stranger is None: - stranger = self.coolq_api_query("get_stranger_info", user_id=user_id) + stranger = await self.coolq_api_query("get_stranger_info", user_id=user_id) self.stranger_dict[user_id] = stranger user = copy.deepcopy(stranger) user["is_friend"] = False if group_id is not None: user["is_in_group"] = False - for member in self.get_group_member_list(group_id): - if member["user_id"] == user_id: - user["is_in_group"] = True - user["in_group_info"] = member - break + member = next( + filter(lambda member: member["user_id"] == user_id, await self.get_group_member_list(group_id)), None + ) + if member is None: + member = next( + filter( + lambda member: member["user_id"] == user_id, + await self.get_group_member_list(group_id, no_cache=True), + ), + None, + ) + if member is not None: + user["is_in_group"] = True + user["in_group_info"] = member remark = user.get("remark") if not remark: user["remark"] = user["nickname"] return user - def get_group_info(self, group_id, no_cache=False): + async def get_group_info(self, group_id, no_cache=False): if no_cache or not self.group_list: - self.update_group_list() + await self.update_group_list() group = self.group_dict.get(group_id) if group: return group @@ -809,7 +788,7 @@ class GoCQHttp(BaseClient): if extra_group["group_id"] == group_id: return extra_group try: - external_group = self.get_external_group_info(group_id) + external_group = await self.get_external_group_info(group_id) except CoolQAPIFailureException: self.logger.error(f"Get external group({group_id}) info failed.") return None @@ -817,18 +796,17 @@ class GoCQHttp(BaseClient): self.extra_group_list.append(external_group) return external_group - def coolq_send_message(self, msg_type, uid, message): + async def coolq_send_message(self, msg_type, uid, message): keyword = msg_type if msg_type != "private" else "user" - res = self.coolq_api_query("send_msg", message_type=msg_type, **{keyword + "_id": uid}, message=message) + res = await self.coolq_api_query("send_msg", message_type=msg_type, **{keyword + "_id": uid}, message=message) return str(uid) + "_" + str(res["message_id"]) - def _coolq_api_wrapper(self, func_name, **kwargs): + async def _coolq_api_wrapper(self, func_name, **kwargs): try: - func = getattr(self.coolq_bot, func_name) - res = func(**kwargs) - except RequestException as e: + res = await self.coolq_bot.call_action(func_name, **kwargs) + except NetworkError as e: raise CoolQDisconnectedException(("Unable to connect to CoolQ Client!" "Error Message:\n{}").format(str(e))) - except cqhttp.Error as ex: + except aiocqhttp.Error as ex: api_ex = CoolQAPIFailureException( ("CoolQ HTTP API encountered an error!\n" "Status Code:{} " "Return Code:{}").format( ex.status_code, ex.retcode @@ -840,64 +818,51 @@ class GoCQHttp(BaseClient): else: return res - def check_running_status(self): - res = self._coolq_api_wrapper("get_status") + async def check_running_status(self): + res = await self._coolq_api_wrapper("get_status") if res["good"] or res["online"]: return True else: raise CoolQOfflineException(("CoolQ Client isn't working correctly!")) - def coolq_api_query(self, func_name, **kwargs) -> Any: + async def coolq_api_query(self, func_name, **kwargs) -> Any: """# Do not call get_status too frequently if self.check_running_status(): return self._coolq_api_wrapper(func_name, **kwargs) """ if self.is_logged_in and self.is_connected: - return self._coolq_api_wrapper(func_name, **kwargs) + return await self._coolq_api_wrapper(func_name, **kwargs) elif self.repeat_counter < 3: self.deliver_alert_to_master(("Your status is offline.\n" "You may try login with /0_login")) self.repeat_counter += 1 - def check_status_periodically(self, t_event): - self.logger.debug("Start checking status...") - flag = True + async def check_status_periodically(self, run_once: bool = False): interval = 300 - try: - flag = self.check_running_status() - except CoolQDisconnectedException as e: - if self.repeat_counter < 3: - self.deliver_alert_to_master( - ( - "We're unable to communicate with CoolQ Client.\n" - "Please check the connection and credentials provided.\n" - "{}" - ).format(str(e)) - ) - self.repeat_counter += 1 - self.is_connected = False - self.is_logged_in = False - interval = 3600 - except (CoolQOfflineException, CoolQAPIFailureException): - if self.repeat_counter < 3: - self.deliver_alert_to_master( - ( - "CoolQ is running in abnormal status.\n" - "You may need to relogin your account " - "or have a check in CoolQ Client.\n" - ) - ) - self.repeat_counter += 1 - self.is_connected = True - self.is_logged_in = False - interval = 3600 - else: - if not flag: + while True: + self.logger.debug("Start checking status...") + flag = True + try: + flag = await self.check_running_status() + except CoolQDisconnectedException as e: if self.repeat_counter < 3: self.deliver_alert_to_master( ( - "We don't know why, but status check failed.\n" - "Please enable debug mode and consult the log " - "for more details." + "We're unable to communicate with CoolQ Client.\n" + "Please check the connection and credentials provided.\n" + "{}" + ).format(str(e)) + ) + self.repeat_counter += 1 + self.is_connected = False + self.is_logged_in = False + interval = 3600 + except (CoolQOfflineException, CoolQAPIFailureException): + if self.repeat_counter < 3: + self.deliver_alert_to_master( + ( + "CoolQ is running in abnormal status.\n" + "You may need to relogin your account " + "or have a check in CoolQ Client.\n" ) ) self.repeat_counter += 1 @@ -905,14 +870,27 @@ class GoCQHttp(BaseClient): self.is_logged_in = False interval = 3600 else: - self.logger.debug("Status: OK") - self.is_connected = True - self.is_logged_in = True - self.repeat_counter = 0 - self.check_features() - if t_event is not None and not t_event.is_set(): - self.check_status_timer = threading.Timer(interval, self.check_status_periodically, [t_event]) - self.check_status_timer.start() + if not flag: + if self.repeat_counter < 3: + self.deliver_alert_to_master( + ( + "We don't know why, but status check failed.\n" + "Please enable debug mode and consult the log " + "for more details." + ) + ) + self.repeat_counter += 1 + self.is_connected = True + self.is_logged_in = False + interval = 3600 + else: + self.logger.debug("Status: OK") + self.is_connected = True + self.is_logged_in = True + self.repeat_counter = 0 + if run_once: + return + await asyncio.sleep(interval) def deliver_alert_to_master(self, message: str): context = { @@ -922,8 +900,8 @@ class GoCQHttp(BaseClient): } self.send_msg_to_master(context) - def update_friend_list(self): - self.friend_list = self.coolq_api_query("get_friend_list") + async def update_friend_list(self): + self.friend_list = await self.coolq_api_query("get_friend_list") if self.friend_list: self.logger.debug("Update friend list completed. Entries: %s", len(self.friend_list)) for friend in self.friend_list: @@ -933,8 +911,8 @@ class GoCQHttp(BaseClient): else: self.logger.warning("Failed to update friend list") - def update_group_list(self): - self.group_list = self.coolq_api_query("get_group_list") + async def update_group_list(self): + self.group_list = await self.coolq_api_query("get_group_list") if self.group_list: self.logger.debug("Update group list completed. Entries: %s", len(self.group_list)) for group in self.group_list: @@ -942,37 +920,38 @@ class GoCQHttp(BaseClient): else: self.logger.warning("Failed to update group list") - def update_contacts_periodically(self, t_event): - self.logger.debug("Start updating friend & group list") + async def update_contacts_periodically(self, run_once: bool = False): interval = 1800 - if self.is_connected and self.is_logged_in: - try: - self.update_friend_list() - self.update_group_list() - except CoolQAPIFailureException as ex: - if (ex.status_code) == 200 and (ex.retcode) == 104 and self.update_repeat_counter < 3: - self.send_cookie_expired_alarm() - if self.update_repeat_counter < 3: - self.deliver_alert_to_master(("Errors occurred when updating contacts: ") + (ex.message)) - self.update_repeat_counter += 1 - else: - self.update_repeat_counter = 0 - self.logger.debug("Update completed") - if t_event is not None and not t_event.is_set(): - self.update_contacts_timer = threading.Timer(interval, self.update_contacts_periodically, [t_event]) - self.update_contacts_timer.start() + while True: + self.logger.debug("Start updating friend & group list") + if self.is_connected and self.is_logged_in: + try: + await self.update_friend_list() + await self.update_group_list() + except CoolQAPIFailureException as ex: + if (ex.status_code) == 200 and (ex.retcode) == 104 and self.update_repeat_counter < 3: + self.send_cookie_expired_alarm() + if self.update_repeat_counter < 3: + self.deliver_alert_to_master(("Errors occurred when updating contacts: ") + (ex.message)) + self.update_repeat_counter += 1 + else: + self.update_repeat_counter = 0 + self.logger.debug("Update completed") + if run_once: + return + await asyncio.sleep(interval) - def get_friend_remark(self, uid): + async def get_friend_remark(self, uid): if (not self.friend_list) or (uid not in self.friend_dict): - self.update_friend_list() + await self.update_friend_list() if uid not in self.friend_dict: return None # I don't think you have such a friend return self.friend_dict[uid]["remark"] - def send_efb_group_notice(self, context): + async def send_efb_group_notice(self, context): context["message_type"] = "group" self.logger.debug(repr(context)) - chat = self.chat_manager.build_efb_chat_as_group(context) + chat = await self.chat_manager.build_efb_chat_as_group(context) try: author = chat.get_member(SystemChatMember.SYSTEM_ID) except KeyError: @@ -1014,8 +993,8 @@ class GoCQHttp(BaseClient): # As the old saying goes # A programmer spent 20% of time on coding # while the rest 80% on considering a variable/function/class name - def get_external_group_info(self, group_id): # Special thanks to @lwl12 for thinking of this name - res = self.coolq_api_query("get_group_info", group_id=group_id) + async def get_external_group_info(self, group_id): # Special thanks to @lwl12 for thinking of this name + res = await self.coolq_api_query("get_group_info", group_id=group_id) return res def send_status(self, status: "Status"): @@ -1024,15 +1003,15 @@ class GoCQHttp(BaseClient): raise EFBMessageError(("You can only recall your own messages.")) try: uid_type = status.message.uid.split("_") - self.recall_message(uid_type[1]) + asyncio.run(self.recall_message(uid_type[1])) except CoolQAPIFailureException: raise EFBMessageError(("Failed to recall the message. This message may have already expired.")) else: raise EFBOperationNotSupported() # todo - def recall_message(self, message_id): - self.coolq_api_query("delete_msg", message_id=message_id) + async def recall_message(self, message_id): + await self.coolq_api_query("delete_msg", message_id=message_id) def send_cookie_expired_alarm(self): self.deliver_alert_to_master( @@ -1046,27 +1025,27 @@ class GoCQHttp(BaseClient): ) ) - def process_friend_request(self, result, flag): + async def process_friend_request(self, result, flag): res = "true" if result == "accept" else "false" try: - self.coolq_api_query("set_friend_add_request", approve=res, flag=flag) + await self.coolq_api_query("set_friend_add_request", approve=res, flag=flag) except CoolQAPIFailureException as e: return ("Failed to process request! Error Message:\n") + getattr(e, "message", repr(e)) return "Done" - def process_group_request(self, result, flag, sub_type): + async def process_group_request(self, result, flag, sub_type): res = "true" if result == "accept" else "false" try: - self.coolq_api_query("set_group_add_request", approve=res, flag=flag, sub_type=sub_type) + await self.coolq_api_query("set_group_add_request", approve=res, flag=flag, sub_type=sub_type) except CoolQAPIFailureException as e: return ("Failed to process request! Error Message:\n") + getattr(e, "message", repr(e)) return "Done" - def async_download_file(self, context, download_url): + async def async_download_file(self, context, download_url): res = download_file(download_url) if isinstance(res, str): context["message"] = ("[Download] ") + res - self.send_efb_group_notice(context) + await self.send_efb_group_notice(context) elif res is None: pass else: @@ -1076,17 +1055,17 @@ class GoCQHttp(BaseClient): efb_msg.uid = str(context["user_id"]) + "_" + str(uuid.uuid4()) + "_" + str(1) efb_msg.text = "Sent a file\n{}".format(context["file"]["name"]) if context["uid_prefix"] == "offline_file": - efb_msg.chat = self.chat_manager.build_efb_chat_as_private(context) + efb_msg.chat = asyncio.run(self.chat_manager.build_efb_chat_as_private(context)) elif context["uid_prefix"] == "group_upload": - efb_msg.chat = self.chat_manager.build_efb_chat_as_group(context) - efb_msg.author = self.chat_manager.build_or_get_efb_member(efb_msg.chat, context) + efb_msg.chat = asyncio.run(self.chat_manager.build_efb_chat_as_group(context)) + efb_msg.author = asyncio.run(self.chat_manager.build_or_get_efb_member(efb_msg.chat, context)) efb_msg.deliver_to = coordinator.master async_send_messages_to_master(efb_msg) - def async_download_group_file(self, context, group_id, file_id, busid): - file = self.coolq_api_query("get_group_file_url", group_id=group_id, file_id=file_id, busid=busid) + async def async_download_group_file(self, context, group_id, file_id, busid): + file = await self.coolq_api_query("get_group_file_url", group_id=group_id, file_id=file_id, busid=busid) download_url = file["url"] - self.async_download_file(context, download_url) + await self.async_download_file(context, download_url) def get_chat_picture(self, chat: "Chat") -> BinaryIO: chat_type = chat.uid.split("_") @@ -1098,59 +1077,58 @@ class GoCQHttp(BaseClient): return download_group_avatar("") def get_chats(self): - qq_chats = self.get_friends() - group_chats = self.get_groups() - return qq_chats + group_chats + async def _get_chats(): + return await asyncio.gather(self.get_friends(), self.get_groups()) + + friend_chats, group_chats = asyncio.run(_get_chats()) + return friend_chats + group_chats def get_chat(self, chat_uid: ChatID) -> "Chat": # todo what is member_uid used for? chat_type = chat_uid.split("_") if chat_type[0] == "private": qq_uid = int(chat_type[1]) - remark = self.get_friend_remark(qq_uid) + remark = asyncio.run(self.get_friend_remark(qq_uid)) context: Dict[str, Any] = {"user_id": qq_uid} if remark is not None: context["alias"] = remark - return self.chat_manager.build_efb_chat_as_private(context) + return asyncio.run(self.chat_manager.build_efb_chat_as_private(context)) elif chat_type[0] == "group": group_id = int(chat_type[1]) context = {"message_type": "group", "group_id": group_id} - return self.chat_manager.build_efb_chat_as_group(context, update_member=True) + return asyncio.run(self.chat_manager.build_efb_chat_as_group(context, update_member=True)) elif chat_type[0] == "discuss": discuss_id = int(chat_type[1]) context = {"message_type": "discuss", "discuss_id": discuss_id} - return self.chat_manager.build_efb_chat_as_group(context) + return asyncio.run(self.chat_manager.build_efb_chat_as_group(context)) raise EFBChatNotFound() - def check_self_update(self, t_event): + async def check_self_update(self, run_once: bool = False): interval = 60 * 60 * 24 - latest_version = self.channel.check_updates() - if latest_version is not None: - self.deliver_alert_to_master( - "New version({version}) of EFB-QQ-Slave has released! " - "Please manually update EQS by stopping ehForwarderbot first and then execute " - "pip3 install --upgrade efb-qq-slave".format(version=latest_version) - ) - else: - if t_event is not None and not t_event.is_set(): - self.self_update_timer = threading.Timer(interval, self.check_self_update, [t_event]) - self.self_update_timer.start() + while True: + latest_version = self.channel.check_updates() + if latest_version is not None: + self.deliver_alert_to_master( + "New version({version}) of EFB-QQ-Slave has released! " + "Please manually update EQS by stopping ehForwarderbot first and then execute " + "pip3 install --upgrade efb-qq-slave".format(version=latest_version) + ) + else: + pass + if run_once: + return + await asyncio.sleep(interval) def poll(self): - self.check_self_update(threading.Event()) + asyncio.run(self.check_self_update(run_once=True)) self.run_instance( host=self.client_config["host"], port=self.client_config["port"], debug=False, ) - self.logger.debug("EQS gracefully shut down") def stop_polling(self): - self.update_contacts_timer.cancel() - self.check_status_timer.cancel() - self.self_update_timer.cancel() - cherrypy.engine.exit() - - def check_features(self): - self.can_send_image = self.coolq_api_query("can_send_image")["yes"] - self.can_send_voice = self.coolq_api_query("can_send_record")["yes"] + self.logger.debug("Gracefully stopping QQ Slave") + self.shutdown_event.set() + self.loop.stop() + self.t.join() diff --git a/efb_qq_plugin_go_cqhttp/MsgDecorator.py b/efb_qq_plugin_go_cqhttp/MsgDecorator.py index a1136c1..42152a5 100644 --- a/efb_qq_plugin_go_cqhttp/MsgDecorator.py +++ b/efb_qq_plugin_go_cqhttp/MsgDecorator.py @@ -1,3 +1,4 @@ +import asyncio import base64 import html import json @@ -218,14 +219,14 @@ class QQMsgProcessor: else: return self.qq_text_simple_wrapper(text, at_list) except Exception: - return self.qq_group_broadcast_alternative_wrapper(data, chat) + return asyncio.run(self.qq_group_broadcast_alternative_wrapper(data, chat)) - def qq_group_broadcast_alternative_wrapper(self, data, chat: Chat): + async def qq_group_broadcast_alternative_wrapper(self, data, chat: Chat): try: at_list = {} content_data = json.loads(data["content"]) group_id = content_data["mannounce"]["gc"] - notice_raw_data = self.inst.coolq_api_query("_get_group_notice", group_id=group_id) + notice_raw_data = await self.inst.coolq_api_query("_get_group_notice", group_id=group_id) notice_data = json.loads(notice_raw_data) title_data = html.unescape(notice_data[0]["msg"]["title"]) text_data = html.unescape(notice_data[0]["msg"]["text"]) diff --git a/efb_qq_plugin_go_cqhttp/Utils.py b/efb_qq_plugin_go_cqhttp/Utils.py index 7776b92..8ba0b6c 100644 --- a/efb_qq_plugin_go_cqhttp/Utils.py +++ b/efb_qq_plugin_go_cqhttp/Utils.py @@ -1,14 +1,13 @@ import logging import tempfile -import urllib.request -from gettext import translation from typing import IO, Optional -from urllib.error import ContentTooShortError, HTTPError, URLError import pilk import pydub +import requests from ehforwarderbot import Message, coordinator -from pkg_resources import resource_filename + +logger = logging.getLogger(__name__) # created by JogleLew and jqqqqqqqqqq, optimized based on Tim's emoji support, updated by xzsk2 to mobileqq v8.8.11 qq_emoji_list = { @@ -640,34 +639,30 @@ qq_sface_list = { 39: "[赞]", 40: "[眨眼]", } -translator = translation( - "efb_qq_slave", - resource_filename("efb_qq_slave", "Clients/CoolQ/locale"), - fallback=True, -) -_ = translator.gettext -ngettext = translator.ngettext def cq_get_image(image_link: str) -> Optional[IO]: # Download image from QQ - file = tempfile.NamedTemporaryFile() try: - urllib.request.urlretrieve(image_link, file.name) - except (URLError, HTTPError, ContentTooShortError) as e: - logging.getLogger(__name__).warning("Image download failed.") - logging.getLogger(__name__).warning(str(e)) - return None - else: + resp = requests.get(image_link) + file = tempfile.NamedTemporaryFile() + file.write(resp.content) if file.seek(0, 2) <= 0: raise EOFError("File downloaded is Empty") file.seek(0) - return file + except Exception as e: + file.close() + logger.warning("File download failed.") + logger.warning(str(e)) + return None + return file def async_send_messages_to_master(msg: Message): - coordinator.send_message(msg) - if msg.file: - msg.file.close() + try: + coordinator.send_message(msg) + finally: + if msg.file: + msg.file.close() def process_quote_text(text, max_length): # Simple wrapper for processing quoted text @@ -708,81 +703,76 @@ def param_spliter(str_param): def download_file(download_url): - file = tempfile.NamedTemporaryFile() try: - opener = urllib.request.build_opener() - urllib.request.install_opener(opener) - urllib.request.urlretrieve(download_url, file.name) - except (URLError, HTTPError, ContentTooShortError) as e: - logging.getLogger(__name__).warning("Error occurs when downloading files: " + str(e)) - return _("Error occurs when downloading files: ") + str(e) - else: + resp = requests.get(download_url) + file = tempfile.NamedTemporaryFile() + file.write(resp.content) if file.seek(0, 2) <= 0: raise EOFError("File downloaded is Empty") file.seek(0) - return file + except Exception as e: + file.close() + logger.warning("Error occurs when downloading files: " + str(e)) + return ("Error occurs when downloading files: ") + str(e) + return file def download_user_avatar(uid: str): - file = tempfile.NamedTemporaryFile() url = "https://q1.qlogo.cn/g?b=qq&nk={}&s=0".format(uid) try: - opener = urllib.request.build_opener() - urllib.request.install_opener(opener) - urllib.request.urlretrieve(url, file.name) - except (URLError, HTTPError, ContentTooShortError) as e: - logging.getLogger(__name__).warning("Error occurs when downloading files: " + str(e)) - return _("Error occurs when downloading files: ") + str(e) - if file.seek(0, 2) <= 0: - raise EOFError("File downloaded is Empty") - file.seek(0) + resp = requests.get(url) + file = tempfile.NamedTemporaryFile() + file.write(resp.content) + if file.seek(0, 2) <= 0: + raise EOFError("File downloaded is Empty") + file.seek(0) + except Exception as e: + file.close() + logger.warning("Error occurs when downloading files: " + str(e)) + raise return file def download_group_avatar(uid: str): - file = tempfile.NamedTemporaryFile() url = "https://p.qlogo.cn/gh/{}/{}/".format(uid, uid) try: - opener = urllib.request.build_opener() - urllib.request.install_opener(opener) - urllib.request.urlretrieve(url, file.name) - except (URLError, HTTPError, ContentTooShortError) as e: - logging.getLogger(__name__).warning("Error occurs when downloading files: " + str(e)) - return _("Error occurs when downloading files: ") + str(e) - if file.seek(0, 2) <= 0: - raise EOFError("File downloaded is Empty") - file.seek(0) + resp = requests.get(url) + file = tempfile.NamedTemporaryFile() + file.write(resp.content) + if file.seek(0, 2) <= 0: + raise EOFError("File downloaded is Empty") + file.seek(0) + except Exception as e: + file.close() + logger.warning("Error occurs when downloading files: " + str(e)) + raise return file def download_voice(voice_url: str): - origin_file = tempfile.NamedTemporaryFile() try: - opener = urllib.request.build_opener() - urllib.request.install_opener(opener) - urllib.request.urlretrieve(voice_url, origin_file.name) + resp = requests.get(voice_url) + origin_file = tempfile.NamedTemporaryFile() + origin_file.write(resp.content) + if origin_file.seek(0, 2) <= 0: + raise EOFError("File downloaded is Empty") + origin_file.seek(0) + silk_header = origin_file.read(10) + origin_file.seek(0) + if b"#!SILK_V3" in silk_header: + with tempfile.NamedTemporaryFile() as pcm_file: + pilk.decode(origin_file.name, pcm_file.name) + audio_file = tempfile.NamedTemporaryFile() + pydub.AudioSegment.from_raw(file=pcm_file, sample_width=2, frame_rate=24000, channels=1).export( + audio_file, format="ogg", codec="libopus", parameters=["-vbr", "on"] + ) + else: + audio_file = origin_file except Exception as e: - logging.getLogger(__name__).warning("Error occurs when downloading files: " + str(e)) origin_file.close() - raise e - finally: - opener.close() - if origin_file.seek(0, 2) <= 0: - origin_file.close() - raise EOFError("File downloaded is Empty") - origin_file.seek(0) - silk_header = origin_file.read(10) - origin_file.seek(0) - if b"#!SILK_V3" in silk_header: - with tempfile.NamedTemporaryFile() as pcm_file: - pilk.decode(origin_file.name, pcm_file.name) - origin_file.close() - audio_file = tempfile.NamedTemporaryFile() - pydub.AudioSegment.from_raw(file=pcm_file, sample_width=2, frame_rate=24000, channels=1).export( - audio_file, format="ogg", codec="libopus", parameters=["-vbr", "on"] - ) - else: - audio_file = origin_file + audio_file.close() + logger.warning("Error occurs when downloading files: " + str(e)) + raise return audio_file diff --git a/efb_qq_plugin_go_cqhttp/__init__.py b/efb_qq_plugin_go_cqhttp/__init__.py index 345fd44..28ad84a 100644 --- a/efb_qq_plugin_go_cqhttp/__init__.py +++ b/efb_qq_plugin_go_cqhttp/__init__.py @@ -1,3 +1,3 @@ from . import GoCQHttp # noqa: F401 -__version__ = "2.2.3" +__version__ = "3.0.0" diff --git a/pdm.lock b/pdm.lock index e289e91..0b16a83 100644 --- a/pdm.lock +++ b/pdm.lock @@ -1,3 +1,30 @@ +[[package]] +name = "aiocqhttp" +version = "1.4.3" +requires_python = ">=3.7" +summary = "A Python SDK with async I/O for OneBot (CQHTTP)." +dependencies = [ + "Quart<1.0,>=0.17", + "httpx<1.0,>=0.11", +] + +[[package]] +name = "aiofiles" +version = "0.8.0" +requires_python = ">=3.6,<4.0" +summary = "File support for asyncio." + +[[package]] +name = "anyio" +version = "3.5.0" +requires_python = ">=3.6.2" +summary = "High level compatibility layer for multiple asynchronous event loop implementations" +dependencies = [ + "idna>=2.8", + "sniffio>=1.1", + "typing-extensions; python_version < \"3.8\"", +] + [[package]] name = "apscheduler" version = "3.6.3" @@ -15,6 +42,11 @@ version = "0.2.1" requires_python = ">=3.6" summary = "Backport of the standard library zoneinfo module" +[[package]] +name = "blinker" +version = "1.4" +summary = "Fast, simple object-to-object and broadcast signaling" + [[package]] name = "bullet" version = "2.2.0" @@ -171,6 +203,53 @@ version = "0.18.2" requires_python = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" summary = "Clean single-source support for Python 3 and 2" +[[package]] +name = "h11" +version = "0.12.0" +requires_python = ">=3.6" +summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" + +[[package]] +name = "h2" +version = "4.1.0" +requires_python = ">=3.6.1" +summary = "HTTP/2 State-Machine based protocol implementation" +dependencies = [ + "hpack<5,>=4.0", + "hyperframe<7,>=6.0", +] + +[[package]] +name = "hpack" +version = "4.0.0" +requires_python = ">=3.6.1" +summary = "Pure-Python HPACK header compression" + +[[package]] +name = "httpcore" +version = "0.14.7" +requires_python = ">=3.6" +summary = "A minimal low-level HTTP client." +dependencies = [ + "anyio==3.*", + "certifi", + "h11<0.13,>=0.11", + "sniffio==1.*", +] + +[[package]] +name = "httpx" +version = "0.22.0" +requires_python = ">=3.6" +summary = "The next generation HTTP client." +dependencies = [ + "certifi", + "charset-normalizer", + "httpcore<0.15.0,>=0.14.5", + "rfc3986[idna2008]<2,>=1.3", + "sniffio", +] + [[package]] name = "humanize" version = "4.0.0" @@ -180,6 +259,26 @@ dependencies = [ "importlib-metadata; python_version < \"3.8\"", ] +[[package]] +name = "hypercorn" +version = "0.13.2" +requires_python = ">=3.7" +summary = "A ASGI Server based on Hyper libraries and inspired by Gunicorn" +dependencies = [ + "h11", + "h2>=3.1.0", + "priority", + "toml", + "typing-extensions>=3.7.4; python_version < \"3.8\"", + "wsproto>=0.14.0", +] + +[[package]] +name = "hyperframe" +version = "6.0.1" +requires_python = ">=3.6.1" +summary = "HTTP/2 framing layer for Python" + [[package]] name = "idna" version = "3.3" @@ -308,6 +407,12 @@ dependencies = [ "tempora>=1.8", ] +[[package]] +name = "priority" +version = "2.0.0" +requires_python = ">=3.6.1" +summary = "A pure-Python implementation of the HTTP/2 priority tree" + [[package]] name = "pydub" version = "0.25.1" @@ -358,6 +463,25 @@ version = "6.0" requires_python = ">=3.6" summary = "YAML parser and emitter for Python" +[[package]] +name = "quart" +version = "0.17.0" +requires_python = ">=3.7" +summary = "A Python ASGI web microframework with the same API as Flask" +dependencies = [ + "aiofiles", + "blinker", + "click", + "hypercorn>=0.11.2", + "importlib-metadata; python_version < \"3.8\"", + "itsdangerous", + "jinja2", + "markupsafe", + "toml", + "typing-extensions; python_version < \"3.8\"", + "werkzeug>=2.0.0", +] + [[package]] name = "requests" version = "2.27.1" @@ -378,6 +502,21 @@ dependencies = [ "six>=1.7.0", ] +[[package]] +name = "rfc3986" +version = "1.5.0" +summary = "Validating URI References per RFC 3986" + +[[package]] +name = "rfc3986" +version = "1.5.0" +extras = ["idna2008"] +summary = "Validating URI References per RFC 3986" +dependencies = [ + "idna", + "rfc3986<2,>=1.3", +] + [[package]] name = "ruamel.yaml" version = "0.17.20" @@ -405,6 +544,12 @@ version = "1.16.0" requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" summary = "Python 2 and 3 compatibility utilities" +[[package]] +name = "sniffio" +version = "1.2.0" +requires_python = ">=3.5" +summary = "Sniff out which async library your code is running under" + [[package]] name = "tempora" version = "5.0.1" @@ -415,6 +560,12 @@ dependencies = [ "pytz", ] +[[package]] +name = "toml" +version = "0.10.2" +requires_python = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +summary = "Python Library for Tom's Obvious, Minimal Language" + [[package]] name = "tornado" version = "6.1" @@ -456,6 +607,15 @@ version = "2.0.2" requires_python = ">=3.6" summary = "The comprehensive WSGI web application library." +[[package]] +name = "wsproto" +version = "1.1.0" +requires_python = ">=3.7.0" +summary = "WebSockets state-machine based protocol implementation" +dependencies = [ + "h11<1,>=0.9.0", +] + [[package]] name = "zc.lockfile" version = "2.0" @@ -472,9 +632,20 @@ summary = "Backport of pathlib-compatible object wrapper for zip files" [metadata] lock_version = "3.1" -content_hash = "sha256:978b5cbd8b5efd554ae429512f6909ca9e5be239ca11531f954c51c274b984cc" +content_hash = "sha256:31a92e98cb57c2c24915316aca43a1756ad1d0ad8d5ffdd78631463724d61bd5" [metadata.files] +"aiocqhttp 1.4.3" = [ + {file = "aiocqhttp-1.4.3.tar.gz", hash = "sha256:197de394f97b798515d643814660a41d5ec470bd0b0608682d1e4b553e486768"}, +] +"aiofiles 0.8.0" = [ + {file = "aiofiles-0.8.0-py3-none-any.whl", hash = "sha256:7a973fc22b29e9962d0897805ace5856e6a566ab1f0c8e5c91ff6c866519c937"}, + {file = "aiofiles-0.8.0.tar.gz", hash = "sha256:8334f23235248a3b2e83b2c3a78a22674f39969b96397126cc93664d9a901e59"}, +] +"anyio 3.5.0" = [ + {file = "anyio-3.5.0-py3-none-any.whl", hash = "sha256:b5fa16c5ff93fa1046f2eeb5bbff2dad4d3514d6cda61d02816dba34fa8c3c2e"}, + {file = "anyio-3.5.0.tar.gz", hash = "sha256:a0aeffe2fb1fdf374a8e4b471444f0f3ac4fb9f5a5b542b48824475e0042a5a6"}, +] "apscheduler 3.6.3" = [ {file = "APScheduler-3.6.3-py2.py3-none-any.whl", hash = "sha256:e8b1ecdb4c7cb2818913f766d5898183c7cb8936680710a4d3a966e02262e526"}, {file = "APScheduler-3.6.3.tar.gz", hash = "sha256:3bb5229eed6fbbdafc13ce962712ae66e175aa214c69bed35a06bffcf0c5e244"}, @@ -497,6 +668,9 @@ content_hash = "sha256:978b5cbd8b5efd554ae429512f6909ca9e5be239ca11531f954c51c27 {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, ] +"blinker 1.4" = [ + {file = "blinker-1.4.tar.gz", hash = "sha256:471aee25f3992bd325afa3772f1063dbdbbca947a041b8b89466dc00d606f8b6"}, +] "bullet 2.2.0" = [ {file = "bullet-2.2.0-py3-none-any.whl", hash = "sha256:d22528deb914ce3ff20a4a000fa5ba37179697f384a0748f90691de8a74cf006"}, {file = "bullet-2.2.0.tar.gz", hash = "sha256:dfa0fa81810ad1a9e688815ca04f24af87ff5cdbe803b42fa634b1f50fc9d887"}, @@ -555,10 +729,38 @@ content_hash = "sha256:978b5cbd8b5efd554ae429512f6909ca9e5be239ca11531f954c51c27 "future 0.18.2" = [ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, ] +"h11 0.12.0" = [ + {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, + {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, +] +"h2 4.1.0" = [ + {file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"}, + {file = "h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb"}, +] +"hpack 4.0.0" = [ + {file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"}, + {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"}, +] +"httpcore 0.14.7" = [ + {file = "httpcore-0.14.7-py3-none-any.whl", hash = "sha256:47d772f754359e56dd9d892d9593b6f9870a37aeb8ba51e9a88b09b3d68cfade"}, + {file = "httpcore-0.14.7.tar.gz", hash = "sha256:7503ec1c0f559066e7e39bc4003fd2ce023d01cf51793e3c173b864eb456ead1"}, +] +"httpx 0.22.0" = [ + {file = "httpx-0.22.0-py3-none-any.whl", hash = "sha256:e35e83d1d2b9b2a609ef367cc4c1e66fd80b750348b20cc9e19d1952fc2ca3f6"}, + {file = "httpx-0.22.0.tar.gz", hash = "sha256:d8e778f76d9bbd46af49e7f062467e3157a5a3d2ae4876a4bbfd8a51ed9c9cb4"}, +] "humanize 4.0.0" = [ {file = "humanize-4.0.0-py3-none-any.whl", hash = "sha256:8d86333b8557dacffd4dce1dbe09c81c189e2caf7bb17a970b2212f0f58f10f2"}, {file = "humanize-4.0.0.tar.gz", hash = "sha256:ee1f872fdfc7d2ef4a28d4f80ddde9f96d36955b5d6b0dac4bdeb99502bddb00"}, ] +"hypercorn 0.13.2" = [ + {file = "Hypercorn-0.13.2-py3-none-any.whl", hash = "sha256:ca18f91ab3fa823cbe9e949738f9f2cc07027cd647c80d8f93e4b1a2a175f112"}, + {file = "Hypercorn-0.13.2.tar.gz", hash = "sha256:6307be5cbdf6ba411967d4661202dc4f79bd511b5d318bc4eed88b09418427f8"}, +] +"hyperframe 6.0.1" = [ + {file = "hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15"}, + {file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"}, +] "idna 3.3" = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, @@ -730,6 +932,10 @@ content_hash = "sha256:978b5cbd8b5efd554ae429512f6909ca9e5be239ca11531f954c51c27 {file = "portend-3.1.0-py3-none-any.whl", hash = "sha256:9e735cee3a5c1961f09e3f3ba6dc498198c2d70b473d98d0d1504b8d1e7a3d61"}, {file = "portend-3.1.0.tar.gz", hash = "sha256:239e3116045ea823f6df87d6168107ad75ccc0590e37242af0cc1e98c5d224e4"}, ] +"priority 2.0.0" = [ + {file = "priority-2.0.0-py3-none-any.whl", hash = "sha256:6f8eefce5f3ad59baf2c080a664037bb4725cd0a790d53d59ab4059288faf6aa"}, + {file = "priority-2.0.0.tar.gz", hash = "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0"}, +] "pydub 0.25.1" = [ {file = "pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6"}, {file = "pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f"}, @@ -799,6 +1005,10 @@ content_hash = "sha256:978b5cbd8b5efd554ae429512f6909ca9e5be239ca11531f954c51c27 {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] +"quart 0.17.0" = [ + {file = "Quart-0.17.0-py3-none-any.whl", hash = "sha256:69480e7384935feff1f50293a8cb70c5d31f568af94ed792d043bb368b50bd50"}, + {file = "Quart-0.17.0.tar.gz", hash = "sha256:2cf213d8b83fa701a83e3b3125e9102a937cefd1e97e9583f22ee2fa79139640"}, +] "requests 2.27.1" = [ {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, @@ -806,6 +1016,10 @@ content_hash = "sha256:978b5cbd8b5efd554ae429512f6909ca9e5be239ca11531f954c51c27 "retrying 1.3.3" = [ {file = "retrying-1.3.3.tar.gz", hash = "sha256:08c039560a6da2fe4f2c426d0766e284d3b736e355f8dd24b37367b0bb41973b"}, ] +"rfc3986 1.5.0" = [ + {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, + {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, +] "ruamel.yaml 0.17.20" = [ {file = "ruamel.yaml-0.17.20-py3-none-any.whl", hash = "sha256:810eef9c46523a3f77479c66267a4708255ebe806a2d540078408c2227f011af"}, {file = "ruamel.yaml-0.17.20.tar.gz", hash = "sha256:4b8a33c1efb2b443a93fcaafcfa4d2e445f8e8c29c528d9f5cdafb7cc9e4004c"}, @@ -845,10 +1059,18 @@ content_hash = "sha256:978b5cbd8b5efd554ae429512f6909ca9e5be239ca11531f954c51c27 {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +"sniffio 1.2.0" = [ + {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, + {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, +] "tempora 5.0.1" = [ {file = "tempora-5.0.1-py3-none-any.whl", hash = "sha256:fbca6a229af666ea4ea8b2f9f80ac9a074f7cf53a97987855b1d15b6e93fd63b"}, {file = "tempora-5.0.1.tar.gz", hash = "sha256:cba0f197a64883bf3e73657efbc0324d5bf17179e7769b1385b4d75d26cd9127"}, ] +"toml 0.10.2" = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] "tornado 6.1" = [ {file = "tornado-6.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32"}, {file = "tornado-6.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c"}, @@ -912,6 +1134,10 @@ content_hash = "sha256:978b5cbd8b5efd554ae429512f6909ca9e5be239ca11531f954c51c27 {file = "Werkzeug-2.0.2-py3-none-any.whl", hash = "sha256:63d3dc1cf60e7b7e35e97fa9861f7397283b75d765afcaefd993d6046899de8f"}, {file = "Werkzeug-2.0.2.tar.gz", hash = "sha256:aa2bb6fc8dee8d6c504c0ac1e7f5f7dc5810a9903e793b6f715a9f015bdadb9a"}, ] +"wsproto 1.1.0" = [ + {file = "wsproto-1.1.0-py3-none-any.whl", hash = "sha256:2218cb57952d90b9fca325c0dcfb08c3bda93e8fd8070b0a17f048e2e47a521b"}, + {file = "wsproto-1.1.0.tar.gz", hash = "sha256:a2e56bfd5c7cd83c1369d83b5feccd6d37798b74872866e62616e0ecf111bda8"}, +] "zc.lockfile 2.0" = [ {file = "zc.lockfile-2.0-py2.py3-none-any.whl", hash = "sha256:cc33599b549f0c8a248cb72f3bf32d77712de1ff7ee8814312eb6456b42c015f"}, {file = "zc.lockfile-2.0.tar.gz", hash = "sha256:307ad78227e48be260e64896ec8886edc7eae22d8ec53e4d528ab5537a83203b"}, diff --git a/pyproject.toml b/pyproject.toml index 65ad123..db21514 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,8 +9,9 @@ dependencies = [ "requests~=2.27.1", "python-magic~=0.4.25", "Pillow~=9.0.1", - "cqhttp~=1.3.1", - "CherryPy~=18.6.1", + "aiocqhttp~=1.4.3", + "quart~=0.17.0", + "hypercorn~=0.13.2", "pilk~=0.0.2", "pydub~=0.25.1", ]