commit da15b9bae4ce1a37c0fe9c56e830d6b71be78bf3 Author: milkice233 Date: Mon Feb 15 23:32:47 2021 +0800 Initial Commit diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..e69de29 diff --git a/efb_qq_plugin_coolq/ChatMgr.py b/efb_qq_plugin_coolq/ChatMgr.py new file mode 100644 index 0000000..ad09e4d --- /dev/null +++ b/efb_qq_plugin_coolq/ChatMgr.py @@ -0,0 +1,148 @@ +# coding: utf-8 +import contextlib +import logging + +from ehforwarderbot import Chat +from ehforwarderbot.chat import GroupChat, PrivateChat, SystemChat +from ehforwarderbot.types import ChatID + +from efb_qq_slave import QQMessengerChannel + + +class ChatManager: + + def __init__(self, channel: 'QQMessengerChannel'): + self.channel: 'QQMessengerChannel' = channel + self.logger: logging.Logger = logging.getLogger(__name__) + + self.MISSING_GROUP: GroupChat = GroupChat( + channel=self.channel, + uid=ChatID("__error_group__"), + name="Group Missing" + ) + + self.MISSING_CHAT: PrivateChat = PrivateChat( + channel=self.channel, + uid=ChatID("__error_chat__"), + name="Chat Missing" + ) + + """ + def build_efb_chat_as_user(self, uid, is_chat, in_group=None, is_discuss=False): + efb_chat = EFBChat(self.channel) + efb_chat.chat_uid = 'user' + str(uid) + i: dict = self.channel.QQClient.get_stranger_info(uid) + efb_chat.chat_name = i['nickname'] + efb_chat.chat_alias = None + efb_chat.chat_type = ChatType.User + efb_chat.is_chat = is_chat + efb_chat.vendor_specific = {'is_anonymous': False} + if in_group is not None: + efb_chat.group = self.build_efb_chat_as_group(in_group, is_discuss) + return efb_chat + + def build_efb_chat_as_group(self, uid, discuss=False): + efb_chat = EFBChat(self.channel) + if not discuss: + efb_chat.chat_uid = 'group' + str(uid) + i = self.channel.QQClient.get_group_info(uid) + efb_chat.chat_name = i['group_name'] + efb_chat.chat_type = ChatType.Group + efb_chat.vendor_specific = {'is_discuss': False} + # todo Add user to efb_chat.member + else: + efb_chat.chat_uid = 'discuss' + str(uid) + efb_chat.chat_name = 'Discuss Group' # todo Find a way to distinguish from different discuss group + efb_chat.chat_type = ChatType.Group + efb_chat.vendor_specific = {'is_discuss': True} + return efb_chat + + def build_efb_chat_as_anonymous_user(self, nickname, flag, anonymous_id, group_id, is_discuss): + efb_chat = EFBChat(self.channel) + efb_chat.chat_uid = flag + efb_chat.chat_name = nickname + efb_chat.chat_type = ChatType.User + efb_chat.is_chat = False + efb_chat.vendor_specific = {'is_anonymous': True, + 'anonymous_id': anonymous_id} + efb_chat.group = self.build_efb_chat_as_group(group_id, is_discuss) + return efb_chat + + """ + + 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) + chat_name = "" + if i: + chat_name = i['nickname'] + else: + chat_name = context['sender']['nickname'] + efb_chat = PrivateChat(channel=self.channel, + uid='private' + '_' + str(uid), + name=str(chat_name), + alias=None if 'alias' not in context else str(context['alias'])) + return efb_chat + + 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) + chat_name = "" + if i: + chat_name = i['nickname'] + else: + chat_name = context['nickname'] + return chat.add_member(name=str(chat_name), + alias=None if 'alias' not in context else str(context['alias']), + uid=str(member_uid)) + + 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) + 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) + if members: + for member in members: + efb_chat.add_member(name=str(member['card']), + alias=str(member['nickname']), + uid=str(member['user_id'])) + else: + efb_chat.uid = 'discuss' + '_' + str(chat_uid) + efb_chat.name = 'Discuss Group' + '_' + str(chat_uid) + # todo Find a way to distinguish from different discuss group + efb_chat.vendor_specific = {'is_discuss': True} + return efb_chat + + def build_efb_chat_as_anonymous_user(self, chat: Chat, context): + anonymous_data = context['anonymous'] + member_uid = 'anonymous' + '_' + anonymous_data['flag'] + with contextlib.suppress(KeyError): + return chat.get_member(member_uid) + chat_name = '[Anonymous] ' + anonymous_data['name'] + return chat.add_member(name=str(chat_name), + alias=None if 'alias' not in context else str(context['alias']), + uid=str(member_uid), + vendor_specific={'is_anonymous': True, + 'anonymous_id': anonymous_data['id']}) + + def build_efb_chat_as_system_user(self, context): # System user only! + return SystemChat(channel=self.channel, + name=str(context['event_description']), + uid=ChatID("__{context[uid_prefix]}__".format(context=context))) diff --git a/efb_qq_plugin_coolq/CoolQ.py b/efb_qq_plugin_coolq/CoolQ.py new file mode 100644 index 0000000..49a5414 --- /dev/null +++ b/efb_qq_plugin_coolq/CoolQ.py @@ -0,0 +1,1059 @@ +# coding: utf-8 +import logging +import tempfile +import threading +import time +import uuid +from datetime import timedelta, datetime +from gettext import translation +from typing import Any, Dict, List, BinaryIO +import cherrypy +from cherrypy._cpserver import Server + +import cqhttp +from PIL import Image +from cherrypy.process.wspbus import states +from cqhttp import CQHttp +from efb_qq_slave import BaseClient, QQMessengerChannel +from ehforwarderbot import Message, MsgType, Chat, coordinator, Status +from ehforwarderbot.chat import SelfChatMember, ChatMember, SystemChatMember, PrivateChat +from ehforwarderbot.exceptions import EFBMessageError, EFBOperationNotSupported, EFBChatNotFound +from ehforwarderbot.message import MessageCommands, MessageCommand +from ehforwarderbot.status import MessageRemoval +from ehforwarderbot.types import ChatID +from ehforwarderbot.utils import extra +from pkg_resources import resource_filename +from requests import RequestException + +from .ChatMgr import ChatManager +from .Exceptions import CoolQDisconnectedException, CoolQAPIFailureException, CoolQOfflineException, \ + CoolQUnknownException +from .MsgDecorator import QQMsgProcessor +from .Utils import qq_emoji_list, async_send_messages_to_master, process_quote_text, coolq_text_encode, \ + upload_image_smms, download_file_from_qzone, download_user_avatar, download_group_avatar, \ + get_friend_group_via_qq_show, upload_image_vim_cn, upload_image_mi, upload_image_sogou, get_stranger_info_via_qzone + + +class CoolQ(BaseClient): + client_name: str = "CoolQ Client" + client_id: str = "CoolQ" + client_config: Dict[str, Any] + + coolq_bot: CQHttp = None + logger: logging.Logger = logging.getLogger(__name__) + channel: QQMessengerChannel + + translator = translation("efb_qq_slave", + resource_filename('efb_qq_slave', 'Clients/CoolQ/locale'), + fallback=True) + + _ = translator.gettext + ngettext = translator.ngettext + + friend_list = [] + friend_group = {} + friend_remark = {} + group_list = [] + group_member_list: Dict[str, Dict[str, Any]] = {} + discuss_list = [] + extra_group_list = [] + 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) + self.client_config = config[self.client_id] + self.coolq_bot = CQHttp(api_root=self.client_config['api_root'], + access_token=self.client_config['access_token'] + ) + self.channel = channel + self.chat_manager = ChatManager(channel) + + self.is_connected = False + self.is_logged_in = False + self.msg_decorator = QQMsgProcessor(instance=self) + + @self.coolq_bot.on_message + def handle_msg(context): + self.logger.debug(repr(context)) + msg_element = context['message'] + main_text: str = '' + messages: List[Message] = [] + qq_uid = context['user_id'] + at_list = {} + chat: Chat + author: ChatMember + + remark = self.get_friend_remark(qq_uid) + if context['message_type'] == 'private': + context['alias'] = remark + chat: PrivateChat = self.chat_manager.build_efb_chat_as_private(context) + # efb_msg.chat: EFBChat = self.chat_manager.build_efb_chat_as_user(context, True) + 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'] = self._("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: + if remark is not None: + context['nickname'] = remark + g_id = context['group_id'] + member_info = self.coolq_api_query('get_group_member_info', + group_id=g_id, + user_id=qq_uid) + if member_info is not None: + context['alias'] = member_info['card'] + author = self.chat_manager.build_or_get_efb_member(chat, context) + elif context['message_type'] == 'private': + author = chat.other + 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) + + for i in range(len(msg_element)): + msg_type = msg_element[i]['type'] + msg_data = msg_element[i]['data'] + if msg_type == 'text': + main_text += msg_data['text'] + elif msg_type == 'face': + qq_face = int(msg_data['id']) + if qq_face in qq_emoji_list: + main_text += qq_emoji_list[qq_face] + else: + main_text += '\u2753' # ❓ + elif msg_type == 'sface': + main_text += '\u2753' # ❓ + elif msg_type == 'at': + # todo Recheck if bug exists + g_id = context['group_id'] + my_uid = self.get_qq_uid() + self.logger.debug('My QQ uid: %s\n' + 'QQ mentioned: %s\n', my_uid, msg_data['qq']) + group_card = '' + if str(msg_data['qq']) == 'all': + group_card = 'all' + else: + member_info = self.coolq_api_query('get_group_member_info', + group_id=g_id, + user_id=msg_data['qq']) + group_card = "" + if member_info: + group_card = member_info['card'] if member_info['card'] != '' else member_info['nickname'] + self.logger.debug('Group card: {}'.format(group_card)) + substitution_begin = 0 + substitution_end = 0 + if main_text == '': + substitution_begin = len(main_text) + substitution_end = len(main_text) + len(group_card) + 1 + main_text += '@{} '.format(group_card) + else: + substitution_begin = len(main_text) + 1 + substitution_end = len(main_text) + len(group_card) + 2 + main_text += ' @{} '.format(group_card) + if str(my_uid) == str(msg_data['qq']) or str(msg_data['qq']) == 'all': + at_list[(substitution_begin, substitution_end)] = chat.self + else: + messages.extend(self.call_msg_decorator(msg_type, msg_data, chat)) + if main_text != "": + messages.append(self.msg_decorator.qq_text_simple_wrapper(main_text, at_list)) + uid: str = str(uuid.uuid4()) + 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 = uid + '_' + str(coolq_msg_id) + '_' + str(i) + efb_msg.chat = chat + efb_msg.author = author + # if qq_uid != '80000000': + + # 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) + + efb_msg.deliver_to = coordinator.master + + def send_message_wrapper(*args, **kwargs): + threading.Thread(target=async_send_messages_to_master, args=args, kwargs=kwargs).start() + + send_message_wrapper(efb_msg) + + @self.coolq_bot.on_notice('group_increase') + def handle_group_increase_msg(context): + context['event_description'] = self._('\u2139 Group Member Increase Event') + if (context['sub_type']) == 'invite': + text = self._('{nickname}({context[user_id]}) joined the group({group_name}) via invitation') + else: + text = self._('{nickname}({context[user_id]}) joined the group({group_name})') + + 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'] + text = text.format(nickname=self.get_stranger_info(context['user_id'])['nickname'], + context=context, + group_name=group_name) + + context['message'] = text + self.send_efb_group_notice(context) + + @self.coolq_bot.on_notice('group_decrease') + def handle_group_decrease_msg(context): + context['event_description'] = self._("\u2139 Group Member Decrease Event") + 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'] + text = '' + if context['sub_type'] == 'kick_me': + text = self._("You've been kicked from the group({})").format(group_name) + else: + if context['sub_type'] == 'leave': + text = self._('{nickname}({context[user_id]}) quited the group({group_name})') + else: + text = self._('{nickname}({context[user_id]}) was kicked from the group({group_name})') + text = text.format(nickname=self.get_stranger_info(context['user_id'])['nickname'], + context=context, + group_name=group_name) + context['message'] = text + self.send_efb_group_notice(context) + + @self.coolq_bot.on_notice('group_upload') + def handle_group_file_upload_msg(context): + context['event_description'] = self._("\u2139 Group File Upload Event") + + 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'] + + file_info_msg = self._('File ID: {file[id]}\n' + 'Filename: {file[name]}\n' + 'File size: {file[size]}').format(file=context['file']) + member_info = self.coolq_api_query('get_group_member_info', + group_id=context['group_id'], + user_id=context['user_id']) + group_card = member_info['card'] if member_info['card'] != '' else member_info['nickname'] + text = self._('{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) + + cred = self.coolq_api_query('get_credentials') + cookies = cred['cookies'] + csrf_token = cred['csrf_token'] + param_dict = { + 'context': context, + 'cookie': cookies, + 'csrf_token': csrf_token, + 'uin': self.get_qq_uid(), + 'group_id': context['group_id'], + 'file_id': context['file']['id'], + 'filename': context['file']['name'], + 'file_size': context['file']['size'] + } + + threading.Thread(target=self.async_download_file, args=[], kwargs=param_dict).start() + + @self.coolq_bot.on_notice('friend_add') + def handle_friend_add_msg(context): + context['event_description'] = self._('\u2139 New Friend Event') + context['uid_prefix'] = 'friend_add' + text = self._('{nickname}({context[user_id]}) has become your friend!') + text = text.format(nickname=self.get_stranger_info(context['user_id'])['nickname'], + context=context) + context['message'] = text + self.send_msg_to_master(context) + + @self.coolq_bot.on_request('friend') # Add friend request + def handle_add_friend_request(context): + self.logger.debug(repr(context)) + context['event_description'] = self._('\u2139 New Friend Request') + context['uid_prefix'] = 'friend_request' + text = self._('{nickname}({context[user_id]}) wants to be your friend!\n' + 'Here is the verification comment:\n' + '{context[comment]}') + text = text.format(nickname=self.get_stranger_info(context['user_id'])['nickname'], + context=context) + context['message'] = text + commands = [MessageCommand( + name=self._("Accept"), + callable_name="process_friend_request", + kwargs={'result': 'accept', + 'flag': context['flag']} + ), MessageCommand( + name=self._("Decline"), + callable_name="process_friend_request", + kwargs={'result': 'decline', + 'flag': context['flag']} + )] + context['commands'] = commands + self.send_msg_to_master(context) + + @self.coolq_bot.on_request('group') + def handle_group_request(context): + self.logger.debug(repr(context)) + context['uid_prefix'] = 'group_request' + context['group_name'] = self._('[Request]') + 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'], 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) + msg.chat = 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']): + name = "{}({})[{}] ".format( + self.get_stranger_info(context['user_id'])['nickname'], self.get_friend_remark(context['user_id']), + context['user_id']) + else: + name = "{}[{}] ".format(self.get_stranger_info(context['user_id'])['nickname'], context['user_id']) + msg.text = "{} wants to join the group {}({}). \nHere is the comment: {}".format( + name, group_name, context['group_id_orig'], context['comment'] + ) + msg.commands = MessageCommands([MessageCommand( + name=self._("Accept"), + callable_name="process_group_request", + kwargs={'result': 'accept', + 'flag': context['flag'], + 'sub_type': context['sub_type']} + ), MessageCommand( + name=self._("Decline"), + callable_name="process_group_request", + kwargs={'result': 'decline', + 'flag': context['flag'], + 'sub_type': context['sub_type']} + )]) + 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() + + 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) + + @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 self._("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.' + + def logout(self): + raise NotImplementedError + + @extra(name=_("Check CoolQ Status"), + 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) + return 'Done' + + def get_stranger_info(self, user_id): + return get_stranger_info_via_qzone(user_id) + # return self.coolq_api_query('get_stranger_info', user_id=user_id, no_cache=False) + # return self.coolq_bot.get_stranger_info(user_id=user_id, no_cache=False) + + def get_login_info(self) -> Dict[Any, Any]: + res = self.coolq_bot.get_status() + if 'good' in res or 'online' in res: + data = self.coolq_bot.get_login_info() + return {'status': 0, 'data': {'uid': data['user_id'], 'nickname': data['nickname']}} + else: + return {'status': 1} + + def get_groups(self) -> List: + # todo Add support for discuss group iteration + 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) + groups.append(efb_chat) + for i in range(len(self.extra_group_list)): + does_exist = False + for j in range(len(res)): + if str(self.extra_group_list[i]['group_id']) == str(res[i]['group_id']): + does_exist = True + break + if does_exist: + continue + context = {'message_type': 'group', + 'group_id': self.extra_group_list[i]['group_id']} + efb_chat = self.chat_manager.build_efb_chat_as_group(context) + groups.append(efb_chat) + return groups + self.discuss_list + + def get_friends(self) -> List: + # Warning: Experimental API + try: + self.update_friend_list() # Force update friend list + self.update_friend_group() + except CoolQAPIFailureException: + self.deliver_alert_to_master(self._('Failed to retrieve the friend list.\n' + 'Only groups are shown.')) + return [] + res = self.friend_list + users = [] + for i in range(len(res)): # friend group + current_user = res[i] + txt = '' + if str(current_user['user_id']) in self.friend_group: + txt = '[{}] {}' + txt = txt.format(self.friend_group[str(current_user['user_id'])], current_user['nickname']) + else: + txt = '{}' + txt = txt.format(current_user['nickname']) + # Disable nickname & remark comparison for it's too time-consuming + context = {'user_id': str(current_user['user_id']), + 'nickname': txt, + 'alias': current_user['remark']} + efb_chat = self.chat_manager.build_efb_chat_as_private(context) + # efb_chat = self.chat_manager.build_efb_chat_as_user(context, True) + users.append(efb_chat) + ''' + for i in range(len(res)): # friend group + for j in range(len(res[i]['friends'])): + current_user = res[i]['friends'][j] + txt = '[{}] {}' + txt = txt.format(res[i]['friend_group_name'], current_user['remark']) + if current_user['nickname'] == current_user['remark']: # no remark name + context = {'user_id': str(current_user['user_id']), + 'nickname': txt, + 'alias': None} + else: + context = {'user_id': str(current_user['user_id']), + 'nickname': current_user['nickname'], + 'alias': txt} + efb_chat = self.chat_manager.build_efb_chat_as_user(context, True) + users.append(efb_chat) + ''' + return users + + def receive_message(self): + # Replaced by handle_msg() + pass + + def send_message(self, msg: 'Message') -> 'Message': + # todo Add support for edited message + """ + self.logger.info("[%s] Sending message to WeChat:\n" + "uid: %s\n" + "Type: %s\n" + "Text: %s\n" + "Target Chat: %s\n" + "Target uid: %s\n", + msg.uid, + msg.chat.chat_uid, msg.type, msg.text, repr(msg.target.chat), msg.target.uid) + """ + m = QQMsgProcessor(instance=self) + chat_type = msg.chat.uid.split('_') + + self.logger.debug('[%s] Is edited: %s', msg.uid, msg.edit) + if msg.edit: + if self.client_config['is_pro']: + try: + uid_type = msg.uid.split('_') + self.recall_message(uid_type[1]) + except CoolQAPIFailureException: + raise EFBOperationNotSupported(self._("Failed to recall the message!\n" + "This message may have already expired.")) + + if msg.type in [MsgType.Text, MsgType.Link]: + 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) + else: + if isinstance(msg.target, Message): + max_length = 50 + tgt_text = coolq_text_encode(process_quote_text(msg.target.text, max_length)) + tgt_alias = "" + if chat_type[0] != 'private' and not isinstance(msg.target.author, SelfChatMember): + tgt_alias += m.coolq_code_at_wrapper(msg.target.author.uid) + else: + tgt_alias = "" + msg.text = "%s%s\n\n%s" % (tgt_alias, tgt_text, coolq_text_encode(msg.text)) + msg.uid = 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.client_config['is_pro']: # CoolQ Air + if self.client_config['air_option']['upload_to_smms']: + text = '[Image] {}' + smms_data = None + smms_email = self.client_config['air_option']['smms_email'] + smms_password = self.client_config['air_option']['smms_password'] + try: + smms_data = upload_image_smms(msg.file, msg.path, smms_email, smms_password) + except CoolQUnknownException as e: + text = '[Image]' + self.deliver_alert_to_master(self._('Failed to upload the image to sm.ms! Return Msg: ') + + getattr(e, 'message', repr(e))) + else: + if smms_data is not None: + text = text.format(smms_data['url']) + elif 'upload_to_vim_cn' in self.client_config['air_option'] \ + and self.client_config['air_option']['upload_to_vim_cn']: + text = '[Image] {}' + vim_cn_data = None + try: + vim_cn_data = upload_image_vim_cn(msg.file, msg.path) + except CoolQUnknownException as e: + text = '[Image]' + self.deliver_alert_to_master(self._('Failed to upload the image to vim-cn.com! Return Msg: ') + + getattr(e, 'message', repr(e))) + else: + if vim_cn_data is not None: + text = text.format(vim_cn_data) + elif 'upload_to_mi' in self.client_config['air_option'] \ + and self.client_config['air_option']['upload_to_mi']: + text = '[Image] {}' + mi_data = None + try: + mi_data = upload_image_mi(msg.file, msg.path) + except CoolQUnknownException as e: + text = '[Image]' + self.deliver_alert_to_master(self._('Failed to upload the image to mi.com! Return Msg: ') + + getattr(e, 'message', repr(e))) + else: + if mi_data is not None: + text = text.format(mi_data) + elif 'upload_to_sogou' in self.client_config['air_option'] \ + and self.client_config['air_option']['upload_to_sogou']: + text = '[Image] {}' + sogou_data = None + try: + sogou_data = upload_image_sogou(msg.file, msg.path) + except CoolQUnknownException as e: + text = '[Image]' + self.deliver_alert_to_master(self._('Failed to upload the image to sogou.com! Return Msg: ') + + getattr(e, 'message', repr(e))) + else: + if sogou_data is not None: + text = text.format(sogou_data) + else: + text = '[Image]' + else: + if not self.can_send_image: + self.check_features() # Force checking features + raise EFBOperationNotSupported(self._("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: + with tempfile.NamedTemporaryFile(suffix=".gif") as f: + img = Image.open(msg.file) + try: + alpha = img.split()[3] + mask = Image.eval(alpha, lambda a: 255 if a <= 128 else 0) + except IndexError: + mask = Image.eval(img.split()[0], lambda a: 0) + img = img.convert('RGB').convert('P', palette=Image.ADAPTIVE, colors=255) + img.paste(255, mask) + img.save(f, transparency=255) + msg.file.close() + f.seek(0) + text += m.coolq_code_image_wrapper(f, f.name) + msg.uid = 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) + # todo More MsgType Support + elif msg.type is MsgType.Voice: + if not self.can_send_voice: + self.check_features() # Force checking features + raise EFBOperationNotSupported(self._("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) + if msg.text: + self.coolq_send_message(chat_type[0], chat_type[1], msg.text) + return msg + + def call_msg_decorator(self, msg_type: str, *args): + func = getattr(self.msg_decorator, 'qq_{}_wrapper'.format(msg_type)) + return func(*args) + + def get_qq_uid(self): + res = self.get_login_info() + if res['status'] == 0: + return res['data']['uid'] + else: + return None + + def get_group_member_info(self, group_id, user_id): + res = self.coolq_api_query('get_group_member_info', group_id=group_id, user_id=user_id, no_cache=True) + # res = self.coolq_bot.get_group_member_info(group_id=group_id, user_id=user_id, no_cache=True) + return res + pass + + def get_group_member_list(self, group_id, no_cache=True): + if no_cache or group_id not in self.group_member_list \ + or datetime.now() - self.group_member_list[group_id]['time'] > timedelta(hours=1): # Force Update + try: + member_list = self.coolq_api_query('get_group_member_list', group_id=group_id) + except CoolQAPIFailureException as e: + self.deliver_alert_to_master(self._("Failed the get group member detail.") + "{}".format(e)) + return None + self.group_member_list[group_id] = { + 'members': member_list, + 'time': datetime.now() + } + return self.group_member_list[group_id]['members'] + + def get_group_info(self, group_id, no_cache=True): + if no_cache or not self.group_list: + self.group_list = self.coolq_api_query('get_group_list') + res = self.group_list + if not res: + self.deliver_alert_to_master(self._("Failed the get group detail")) + return None + # res = self.coolq_bot.get_group_list() + for i in range(len(res)): + if res[i]['group_id'] == group_id: + return res[i] + try: + external_group = self.get_external_group_info(group_id) + except Exception: + return None + else: + new_group = {'group_id': group_id, 'group_name': external_group['group_name']} + for i in range(len(self.extra_group_list)): + if str(self.extra_group_list[i]['group_id']) == str(group_id): + return new_group + self.extra_group_list.append(new_group) + return new_group + + 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 = self.coolq_bot.send_msg(message_type=msg_type, **{keyword + '_id': uid}, message=message) + return str(uuid.uuid4()) + '_' + str(res['message_id']) + + def _coolq_api_wrapper(self, func_name, **kwargs): + try: + func = getattr(self.coolq_bot, func_name) + res = func(**kwargs) + except RequestException as e: + raise CoolQDisconnectedException(self._('Unable to connect to CoolQ Client!' + 'Error Message:\n{}').format(str(e))) + except cqhttp.Error as ex: + api_ex = CoolQAPIFailureException(self._('CoolQ HTTP API encountered an error!\n' + 'Status Code:{} ' + 'Return Code:{}').format(ex.status_code, ex.retcode)) + # if ex.status_code == 200 and ex.retcode == 104: # Cookie expired + setattr(api_ex, 'status_code', ex.status_code) + setattr(api_ex, 'retcode', ex.retcode) + raise api_ex + else: + return res + + def check_running_status(self): + res = self._coolq_api_wrapper('get_status') + if res['good'] or res['online']: + return True + else: + raise CoolQOfflineException(self._("CoolQ Client isn't working correctly!")) + + def coolq_api_query(self, func_name, **kwargs): + """ # 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) + elif self.repeat_counter < 3: + self.deliver_alert_to_master(self._('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 + interval = 300 + try: + flag = self.check_running_status() + except CoolQDisconnectedException as e: + if self.repeat_counter < 3: + self.deliver_alert_to_master(self._("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(self._('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: + if self.repeat_counter < 3: + self.deliver_alert_to_master(self._("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 + 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() + + def deliver_alert_to_master(self, message: str): + context = {'message': message, 'uid_prefix': 'alert', 'event_description': self._('CoolQ Alert')} + self.send_msg_to_master(context) + + def update_friend_group(self): + # Mirai doesn't support retrieving friend group, neither does get_credentials unfortunately + return {} + # Warning: Experimental API + try: + # res = self.coolq_api_query('_get_friend_list') + # relationship = {} + # if res: + # for group in res: + # for friend in group['friends']: + # relationship[str(friend['user_id'])] = str(group['friend_group_name']) + # self.friend_group = relationship + # Use QShow API + cred = self.coolq_api_query('get_credentials') + cookies = cred['cookies'] + csrf_token = cred['csrf_token'] + self.friend_group = get_friend_group_via_qq_show(cookies, csrf_token) + except Exception as e: + self.logger.warning('Failed to update friend group' + str(e)) + + def update_friend_list(self): + self.friend_list = 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: + if(friend['remark']==''): + friend['remark'] = friend['nickname'] + self.friend_remark[str(friend['user_id'])] = { + 'nickname': friend['nickname'], + 'remark': friend['remark'] + } + else: + self.logger.warning('Failed to update friend list') + + def update_group_list(self): + self.group_list = self.coolq_api_query('get_group_list') + if self.group_list: + self.logger.debug('Update group list completed. Entries: %s', len(self.group_list)) + else: + self.logger.warning('Failed to update group list') + + def update_contacts_periodically(self, t_event): + self.logger.debug('Start updating friend & group list') + 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 getattr(ex, 'status_code') == 200 and getattr(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(self._('Errors occurred when updating contacts: ') + + getattr(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() + + def get_friend_remark(self, uid): + if not self.friend_list: + try: + self.update_friend_list() + except CoolQAPIFailureException: + # self.deliver_alert_to_master(self._('Failed to update friend remark name')) + self.logger.exception(self._('Failed to update friend remark name')) + return '' + # if not self.friend_group: + # try: + # self.update_friend_group() + # except CoolQAPIFailureException: + # self.deliver_alert_to_master(self._('Failed to get friend groups')) + # self.logger.exception(self._('Failed to get friend groups')) + # return '' + if str(uid) not in self.friend_remark: + return None # I don't think you have such a friend + return self.friend_remark[str(uid)]['remark'] + ''' + for i in range(len(self.friend_list)): # friend group + for j in range(len(self.friend_list[i]['friend'])): + current_user = self.friend_list[i]['friend'][j] + if current_user['uin'] != str(uid): + continue + return current_user['name'] + ''' + ''' + for i in range(len(self.friend_list)): # friend group + for j in range(len(self.friend_list[i]['friends'])): + current_user = self.friend_list[i]['friends'][j] + if current_user['user_id'] != uid: + continue + if current_user['nickname'] == current_user['remark'] or current_user['nickname'] == '': + # no remark name + return current_user['nickname'] + else: + return current_user['remark'] + return None # I don't think you've got such a friend + ''' + + 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) + try: + author = chat.get_member(SystemChatMember.SYSTEM_ID) + except KeyError: + author = chat.add_system_member() + msg = Message( + uid="__group_notice__.%s" % int(time.time()), + type=MsgType.Text, + chat=chat, + author=author, + text=context['message'], + deliver_to=coordinator.master + ) + coordinator.send_message(msg) + + def send_msg_to_master(self, context): + self.logger.debug(repr(context)) + if not getattr(coordinator, 'master', None): # Master Channel not initialized + raise Exception(context['message']) + chat = self.chat_manager.build_efb_chat_as_system_user(context) + try: + author = chat.get_member(SystemChatMember.SYSTEM_ID) + except KeyError: + author = chat.add_system_member() + msg = Message( + uid="__{context[uid_prefix]}__.{uni_id}".format(context=context, + uni_id=str(int(time.time()))), + type=MsgType.Text, + chat=chat, + author=author, + deliver_to=coordinator.master + ) + + if 'message' in context: + msg.text = context['message'] + if 'commands' in context: + msg.commands = MessageCommands(context['commands']) + coordinator.send_message(msg) + + # As the old saying goes + # A programmer spent 20% of time on coding + # while the rest 80% on considering a variable/function/class name + # todo Deprecated + 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) + return res + + def send_status(self, status: 'Status'): + if isinstance(status, MessageRemoval): + if not isinstance(status.message.author, SelfChatMember): + raise EFBMessageError(self._('You can only recall your own messages.')) + try: + uid_type = status.message.uid.split('_') + self.recall_message(uid_type[1]) + except CoolQAPIFailureException: + raise EFBMessageError( + self._('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) + + def send_cookie_expired_alarm(self): + self.deliver_alert_to_master(self._('Your cookie of CoolQ Client seems to be expired. ' + 'Although it will not affect the normal functioning of sending/receiving ' + 'messages, however, you may encounter issues like failing to retrieve ' + 'friend list. Please consult ' + 'https://github.com/milkice233/efb-qq-slave/wiki/Workaround-for-expired' + '-cookies-of-CoolQ for solutions.')) + + 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) + except CoolQAPIFailureException as e: + return (self._('Failed to process request! Error Message:\n') + + getattr(e, 'message', repr(e))) + return 'Done' + + 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) + except CoolQAPIFailureException as e: + return (self._('Failed to process request! Error Message:\n') + + getattr(e, 'message', repr(e))) + return 'Done' + + def async_download_file(self, context, **kwargs): + res = download_file_from_qzone(**kwargs) + if isinstance(res, str): + context['message'] = self._("[Download] ") + res + self.send_efb_group_notice(context) + elif res is None: + pass + else: + data = {'file': res, 'filename': context['file']['name']} + context['message_type'] = 'group' + efb_msg = self.msg_decorator.qq_file_after_wrapper(data) + efb_msg.uid = str(context['user_id']) + '_' + str(uuid.uuid4()) + '_' + str(1) + efb_msg.text = 'Sent a file\n{}'.format(context['file']['name']) + 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.deliver_to = coordinator.master + async_send_messages_to_master(efb_msg) + + def get_chat_picture(self, chat: 'Chat') -> BinaryIO: + chat_type = chat.uid.split('_') + if chat_type[0] == 'private': + return download_user_avatar(chat_type[1]) + elif chat_type[0] == 'group': + return download_group_avatar(chat_type[1]) + else: + return download_group_avatar("") + + def get_chats(self): + qq_chats = self.get_friends() + group_chats = self.get_groups() + return qq_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) + context = {"user_id": qq_uid} + if remark is not None: + context['alias'] = remark + return 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) + 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) + raise EFBChatNotFound() + + def check_self_update(self, t_event): + 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() + + def poll(self): + self.check_self_update(threading.Event()) + 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'] diff --git a/efb_qq_plugin_coolq/Exceptions.py b/efb_qq_plugin_coolq/Exceptions.py new file mode 100644 index 0000000..b2e372d --- /dev/null +++ b/efb_qq_plugin_coolq/Exceptions.py @@ -0,0 +1,22 @@ +class CoolQClientException(Exception): + pass + + +class CoolQAPIFailureException(CoolQClientException): + pass + + +class CoolQCookieExpiredException(CoolQAPIFailureException): + pass + + +class CoolQOfflineException(CoolQClientException): + pass + + +class CoolQDisconnectedException(CoolQClientException): + pass + + +class CoolQUnknownException(CoolQClientException): + pass diff --git a/efb_qq_plugin_coolq/MsgDecorator.py b/efb_qq_plugin_coolq/MsgDecorator.py new file mode 100644 index 0000000..c17a69c --- /dev/null +++ b/efb_qq_plugin_coolq/MsgDecorator.py @@ -0,0 +1,251 @@ +# coding: utf-8 + +import base64 +import html +import json +import logging +from urllib.parse import quote + +import magic +from ehforwarderbot import Message, MsgType, Chat +from ehforwarderbot.message import LocationAttribute, LinkAttribute, Substitutions + +from . import CoolQ +from .Utils import cq_get_image, download_voice + + +class QQMsgProcessor: + inst: CoolQ + logger: logging.Logger = logging.getLogger(__name__) + + def __init__(self, instance: CoolQ): + self.inst = instance + self._ = instance._ + pass + + def qq_image_wrapper(self, data, chat: Chat = None): + efb_msg = Message() + if 'url' not in data: + efb_msg.type = MsgType.Text + efb_msg.text = self._('[Image Source missing]') + return [efb_msg] + + efb_msg.file = cq_get_image(data['url']) + if efb_msg.file is None: + efb_msg.type = MsgType.Text + efb_msg.text = self._('[Download image failed, please check on your QQ client]') + return [efb_msg] + + efb_msg.type = MsgType.Image + mime = magic.from_file(efb_msg.file.name, mime=True) + if isinstance(mime, bytes): + mime = mime.decode() + efb_msg.filename = data['file'] if 'file' in data else efb_msg.file.name + efb_msg.filename += '.' + str(mime).split('/')[1] + efb_msg.path = efb_msg.file.name + efb_msg.mime = mime + if "gif" in mime: + efb_msg.type = MsgType.Animation + return [efb_msg] + + def qq_record_wrapper(self, data, chat: Chat = None): # Experimental! + efb_msg = Message() + try: + transformed_file = self.inst.coolq_api_query("get_record", file=data['file'], out_format='mp3') + efb_msg.type = MsgType.Audio + efb_msg.file = download_voice(transformed_file['file'], + self.inst.client_config['api_root'].rstrip("/"), + self.inst.client_config['access_token']) + mime = magic.from_file(efb_msg.file.name, mime=True) + if isinstance(mime, bytes): + mime = mime.decode() + efb_msg.path = efb_msg.file.name + efb_msg.mime = mime + except Exception: + efb_msg.type = MsgType.Unsupported + efb_msg.text = self._('[Voice Message] Please check it on your QQ') + logging.getLogger(__name__).exception("Failed to download voice") + return [efb_msg] + + def qq_share_wrapper(self, data, chat: Chat = None): + efb_msg = Message( + type=MsgType.Link, + text='', + attributes=LinkAttribute( + title='' if 'title' not in data else data['title'], + description='' if 'content' not in data else data['content'], + image='' if 'image' not in data else data['image'], + url=data['url'] + ) + ) + return [efb_msg] + + def qq_location_wrapper(self, data, chat: Chat = None): + efb_msg = Message( + text=data['content'], + type=MsgType.Location, + attributes=LocationAttribute(longitude=float(data['lon']), + latitude=float(data['lat'])) + ) + return [efb_msg] + + def qq_shake_wrapper(self, data, chat: Chat = None): + efb_msg = Message( + type=MsgType.Text, + text=self._('[Your friend shakes you!]') + ) + return [efb_msg] + + def qq_contact_wrapper(self, data, chat: Chat = None): + uid = data['id'] + contact_type = data['type'] + efb_msg = Message( + type=MsgType.Text, + text=self._("Chat Recommendation Received\nID: {}\nType: {}").format(uid, contact_type) + ) + return [efb_msg] + + def qq_bface_wrapper(self, data, chat: Chat = None): + efb_msg = Message( + type=MsgType.Unsupported, + text=self._('[Here comes the BigFace Emoji, please check it on your phone]') + ) + return [efb_msg] + + def qq_small_face_wrapper(self, data, chat: Chat = None): + # todo this function's maybe not necessary? + pass + + def qq_sign_wrapper(self, data, chat: Chat = None): + location = self._('at {}').format(data['location']) if 'location' in data else self._('at Unknown Place') + title = '' if 'title' not in data else (self._('with title {}').format(data['title'])) + efb_msg = Message( + type=MsgType.Text, + text=self._('signed in {location} {title}').format(title=title, + location=location) + ) + return [efb_msg] + + def qq_rich_wrapper(self, data: dict, chat: Chat = None): # Buggy, Help needed + efb_messages = list() + efb_msg = Message( + type=MsgType.Unsupported, + text=self._('[Here comes the Rich Text, dumping...] \n') + ) + for key, value in data.items(): + efb_msg.text += key + ': ' + value + '\n' + efb_messages.append(efb_msg) + # Optimizations for rich messages + # Group Broadcast + _ = self.qq_group_broadcast_wrapper(data, chat) + if _ is not None: + efb_messages.append(_) + + return efb_messages + + def qq_music_wrapper(self, data, chat: Chat = None): + efb_msg = Message() + if data['type'] == '163': # Netease Cloud Music + efb_msg.type = MsgType.Text + efb_msg.text = 'https://music.163.com/#/song?id=' + data['id'] + else: + efb_msg.type = MsgType.Text + efb_msg.text = data['text'] + return [efb_msg] # todo Port for other music platform + + def qq_text_simple_wrapper(self, text: str, ats: dict): # This cute function only accepts string! + efb_msg = Message() + efb_msg.type = MsgType.Text + efb_msg.text = text + if ats: # This is used to replace specific text with @blahblah + # And Milkice really requires a brain check + efb_msg.substitutions = Substitutions(ats) + return efb_msg + + def coolq_code_at_wrapper(self, uid): + return '[CQ:at,qq={}]'.format(uid) + + def coolq_code_image_wrapper(self, file, file_path): + if file.closed: + file = open(file.name) + encoded_string = base64.b64encode(file.read()) + # Since base64 doesn't contain characters which isn't allowed in CQ Code, + # there's no need to escape the special characters + return '[CQ:image,file=base64://{}]'.format(encoded_string.decode()) + + def coolq_voice_image_wrapper(self, file, file_path): + if file.closed: + file = open(file.name) + encoded_string = base64.b64encode(file.read()) + # Since base64 doesn't contain characters which isn't allowed in CQ Code, + # there's no need to escape the special characters + return '[CQ:record,file=base64://{}]'.format(encoded_string.decode()) + + def qq_file_after_wrapper(self, data): + efb_msg = Message() + efb_msg.file = data['file'] + efb_msg.type = MsgType.File + mime = magic.from_file(efb_msg.file.name, mime=True) + if isinstance(mime, bytes): + mime = mime.decode() + efb_msg.path = efb_msg.file.name + efb_msg.mime = mime + efb_msg.filename = quote(data['filename']) + return efb_msg + + def qq_group_broadcast_wrapper(self, data, chat: Chat = None): + try: + at_list = {} + content_data = json.loads(data['content']) + text_data = base64.b64decode(content_data['mannounce']['text']).decode("UTF-8") + title_data = base64.b64decode(content_data['mannounce']['title']).decode("UTF-8") + text = "[群公告] 【{title}】\n{text}".format(title=title_data, text=text_data) + + substitution_begin = len(text) + 1 + substitution_end = len(text) + len('@all') + 2 + text += ' @all ' + + at_list[(substitution_begin, substitution_end)] = chat.self + + if 'pic' in content_data['mannounce']: # Picture Attached + # Assuming there's only one picture + data['url'] = "http://gdynamic.qpic.cn/gdynamic/{}/628".format( + content_data['mannounce']['pic'][0]['url']) + efb_message = self.qq_image_wrapper(data)[0] + efb_message.text = text + efb_message.substitutions = Substitutions(at_list) + return [efb_message] + else: + return self.qq_text_simple_wrapper(text, at_list) + except Exception: + return self.qq_group_broadcast_alternative_wrapper(data) + + def qq_group_broadcast_alternative_wrapper(self, data, chat: Chat = None): + 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_data = json.loads(notice_raw_data) + title_data = html.unescape(notice_data[0]['msg']['title']) + text_data = html.unescape(notice_data[0]['msg']['text']) + text = "[群公告] 【{title}】\n{text}".format(title=title_data, text=text_data) + + substitution_begin = len(text) + 1 + substitution_end = len(text) + len('@all') + 2 + text += ' @all ' + + at_list[(substitution_begin, substitution_end)] = chat.self + + if 'pics' in html.unescape(notice_data[0]['msg']): # Picture Attached + # Assuming there's only one picture + data['url'] = "http://gdynamic.qpic.cn/gdynamic/{}/628".format(notice_data[0]['msg']['pics'][0]['id']) + efb_message = self.qq_image_wrapper(data)[0] + efb_message.text = text + efb_message.substitutions = Substitutions(at_list) + return [efb_message] + else: + return self.qq_text_simple_wrapper(text, at_list) + except Exception: + return None diff --git a/efb_qq_plugin_coolq/Utils.py b/efb_qq_plugin_coolq/Utils.py new file mode 100644 index 0000000..f047b66 --- /dev/null +++ b/efb_qq_plugin_coolq/Utils.py @@ -0,0 +1,830 @@ +# coding: utf-8 +import re +import json +import logging +import ntpath +import tempfile +import urllib.request +from gettext import translation +from typing import * +from urllib.error import URLError, HTTPError, ContentTooShortError +from urllib.parse import quote + +import requests +from ehforwarderbot import Message, coordinator +from pkg_resources import resource_filename + +from .Exceptions import CoolQUnknownException + +qq_emoji_list = { # created by JogleLew and jqqqqqqqqqq, optimized based on Tim's emoji support + 0: '😮', + 1: '😣', + 2: '😍', + 3: '😳', + 4: '😎', + 5: '😭', + 6: '☺️', + 7: '😷', + 8: '😴', + 9: '😭', + 10: '😰', + 11: '😡', + 12: '😝', + 13: '😃', + 14: '🙂', + 15: '🙁', + 16: '🤓', + 17: '[Empty]', + 18: '😤', + 19: '😨', + 20: '😏', + 21: '😊', + 22: '🙄', + 23: '😕', + 24: '🤤', + 25: '😪', + 26: '😨', + 27: '😓', + 28: '😬', + 29: '🤑', + 30: '✊', + 31: '😤', + 32: '🤔', + 33: '🤐', + 34: '😵', + 35: '😩', + 36: '💣', + 37: '💀', + 38: '🔨', + 39: '👋', + 40: '[Empty]', + 41: '😮', + 42: '💑', + 43: '🕺', + 44: '[Empty]', + 45: '[Empty]', + 46: '🐷', + 47: '[Empty]', + 48: '[Empty]', + 49: '🤷', + 50: '[Empty]', + 51: '[Empty]', + 52: '[Empty]', + 53: '🎂', + 54: '⚡', + 55: '💣', + 56: '🔪', + 57: '⚽️', + 58: '[Empty]', + 59: '💩', + 60: '☕️', + 61: '🍚', + 62: '[Empty]', + 63: '🌹', + 64: '🥀', + 65: '[Empty]', + 66: '❤️', + 67: '💔️', + 68: '[Empty]', + 69: '🎁', + 70: '[Empty]', + 71: '[Empty]', + 72: '[Empty]', + 73: '[Empty]', + 74: '🌞️', + 75: '🌃', + 76: '👍', + 77: '👎', + 78: '🤝', + 79: '✌️', + 80: '[Empty]', + 81: '[Empty]', + 82: '[Empty]', + 83: '[Empty]', + 84: '[Empty]', + 85: '🥰', + 86: '[怄火]', + 87: '[Empty]', + 88: '[Empty]', + 89: '🍉', + 90: '[Empty]', + 91: '[Empty]', + 92: '[Empty]', + 93: '[Empty]', + 94: '[Empty]', + 95: '[Empty]', + 96: '😅', + 97: '[擦汗]', + 98: '[抠鼻]', + 99: '👏', + 100: '[糗大了]', + 101: '😏', + 102: '😏', + 103: '😏', + 104: '🥱', + 105: '[鄙视]', + 106: '😭', + 107: '😭', + 108: '[阴险]', + 109: '😚', + 110: '🙀', + 111: '[可怜]', + 112: '🔪', + 113: '🍺', + 114: '🏀', + 115: '🏓', + 116: '❤️', + 117: '🐞', + 118: '[抱拳]', + 119: '[勾引]', + 120: '✊', + 121: '[差劲]', + 122: '🤟', + 123: '🚫', + 124: '👌', + 125: '[转圈]', + 126: '[磕头]', + 127: '[回头]', + 128: '[跳绳]', + 129: '👋', + 130: '[激动]', + 131: '[街舞]', + 132: '😘', + 133: '[左太极]', + 134: '[右太极]', + 135: '[Empty]', + 136: '[双喜]', + 137: '🧨', + 138: '🏮', + 139: '💰', + 140: '[K歌]', + 141: '🛍️', + 142: '📧', + 143: '[帅]', + 144: '👏', + 145: '🙏', + 146: '[爆筋]', + 147: '🍭', + 148: '🍼', + 149: '[下面]', + 150: '🍌', + 151: '🛩', + 152: '🚗', + 153: '🚅', + 154: '[车厢]', + 155: '[高铁右车头]', + 156: '🌥', + 157: '下雨', + 158: '💵', + 159: '🐼', + 160: '💡', + 161: '[风车]', + 162: '⏰', + 163: '🌂', + 164: '[彩球]', + 165: '💍', + 166: '🛋', + 167: '[纸巾]', + 168: '💊', + 169: '🔫', + 170: '🐸', + 171: '🍵', + 172: '[眨眼睛]', + 173: '😭', + 174: '[无奈]', + 175: '[卖萌]', + 176: '[小纠结]', + 177: '[喷血]', + 178: '[斜眼笑]', + 179: '[doge]', + 180: '[惊喜]', + 181: '[骚扰]', + 182: '😹', + 183: '[我最美]', + 184: '🦀', + 185: '[羊驼]', + 186: '[Empty]', + 187: '👻', + 188: '🥚', + 189: '[Empty]', + 190: '🌼', + 191: '[Empty]', + 192: '🧧', + 193: '😄', + 194: '😞', + 195: '[Empty]', + 196: '[Empty]', + 197: '[冷漠]', + 198: '[呃]', + 199: '👍', + 200: '👋', + 201: '👍', + 202: '[无聊]', + 203: '[托脸]', + 204: '[吃]', + 205: '💐', + 206: '😨', + 207: '[花痴]', + 208: '[小样儿]', + 209: '[Empty]', + 210: '😭', + 211: '[我不看]', + 212: '[托腮]', + 213: '[Empty]', + 214: '😙', + 215: '[糊脸]', + 216: '[拍头]', + 217: '[扯一扯]', + 218: '[舔一舔]', + 219: '[蹭一蹭]', + 220: '[拽炸天]', + 221: '[顶呱呱]', + 222: '🤗', + 223: '[暴击]', + 224: '🔫', + 225: '[撩一撩]', + 226: '[拍桌]', + 227: '👏', + 228: '[恭喜]', + 229: '🍻', + 230: '[嘲讽]', + 231: '[哼]', + 232: '[佛系]', + 233: '[掐一掐]', + 234: '😮', + 235: '[颤抖]', + 236: '[啃头]', + 237: '[偷看]', + 238: '[扇脸]', + 239: '[原谅]', + 240: '[喷脸]', + 241: '🎂', + 242: '[Empty]', + 243: '[Empty]', + 244: '[Empty]', + 245: '[Empty]', + 246: '[Empty]', + 247: '[Empty]', + 248: '[Empty]', + 249: '[Empty]', + 250: '[Empty]', + 251: '[Empty]', + 252: '[Empty]', + 253: '[Empty]', + 254: '[Empty]', + 255: '[Empty]', +} + +# original text copied from Tim +qq_emoji_text_list = { + 0: '[惊讶]', + 1: '[撇嘴]', + 2: '[色]', + 3: '[发呆]', + 4: '[得意]', + 5: '[流泪]', + 6: '[害羞]', + 7: '[闭嘴]', + 8: '[睡]', + 9: '[大哭]', + 10: '[尴尬]', + 11: '[发怒]', + 12: '[调皮]', + 13: '[呲牙]', + 14: '[微笑]', + 15: '[难过]', + 16: '[酷]', + 17: '[Empty]', + 18: '[抓狂]', + 19: '[吐]', + 20: '[偷笑]', + 21: '[可爱]', + 22: '[白眼]', + 23: '[傲慢]', + 24: '[饥饿]', + 25: '[困]', + 26: '[惊恐]', + 27: '[流汗]', + 28: '[憨笑]', + 29: '[悠闲]', + 30: '[奋斗]', + 31: '[咒骂]', + 32: '[疑问]', + 33: '[嘘]', + 34: '[晕]', + 35: '[折磨]', + 36: '[衰]', + 37: '[骷髅]', + 38: '[敲打]', + 39: '[再见]', + 40: '[Empty]', + 41: '[发抖]', + 42: '[爱情]', + 43: '[跳跳]', + 44: '[Empty]', + 45: '[Empty]', + 46: '[猪头]', + 47: '[Empty]', + 48: '[Empty]', + 49: '[拥抱]', + 50: '[Empty]', + 51: '[Empty]', + 52: '[Empty]', + 53: '[蛋糕]', + 54: '[闪电]', + 55: '[炸弹]', + 56: '[刀]', + 57: '[足球]', + 58: '[Empty]', + 59: '[便便]', + 60: '[咖啡]', + 61: '[饭]', + 62: '[Empty]', + 63: '[玫瑰]', + 64: '[凋谢]', + 65: '[Empty]', + 66: '[爱心]', + 67: '[心碎]', + 68: '[Empty]', + 69: '[礼物]', + 70: '[Empty]', + 71: '[Empty]', + 72: '[Empty]', + 73: '[Empty]', + 74: '[太阳]', + 75: '[月亮]', + 76: '[赞]', + 77: '[踩]', + 78: '[握手]', + 79: '[胜利]', + 80: '[Empty]', + 81: '[Empty]', + 82: '[Empty]', + 83: '[Empty]', + 84: '[Empty]', + 85: '[飞吻]', + 86: '[怄火]', + 87: '[Empty]', + 88: '[Empty]', + 89: '[西瓜]', + 90: '[Empty]', + 91: '[Empty]', + 92: '[Empty]', + 93: '[Empty]', + 94: '[Empty]', + 95: '[Empty]', + 96: '[冷汗]', + 97: '[擦汗]', + 98: '[抠鼻]', + 99: '[鼓掌]', + 100: '[糗大了]', + 101: '[坏笑]', + 102: '[左哼哼]', + 103: '[右哼哼]', + 104: '[哈欠]', + 105: '[鄙视]', + 106: '[委屈]', + 107: '[快哭了]', + 108: '[阴险]', + 109: '[亲亲]', + 110: '[吓]', + 111: '[可怜]', + 112: '[菜刀]', + 113: '[啤酒]', + 114: '[篮球]', + 115: '[乒乓]', + 116: '[示爱]', + 117: '[瓢虫]', + 118: '[抱拳]', + 119: '[勾引]', + 120: '[拳头]', + 121: '[差劲]', + 122: '[爱你]', + 123: '[NO]', + 124: '[OK]', + 125: '[转圈]', + 126: '[磕头]', + 127: '[回头]', + 128: '[跳绳]', + 129: '[挥手]', + 130: '[激动]', + 131: '[街舞]', + 132: '[献吻]', + 133: '[左太极]', + 134: '[右太极]', + 135: '[Empty]', + 136: '[双喜]', + 137: '[鞭炮]', + 138: '[灯笼]', + 139: '[发财]', + 140: '[K歌]', + 141: '[购物]', + 142: '[邮件]', + 143: '[帅]', + 144: '[喝彩]', + 145: '[祈祷]', + 146: '[爆筋]', + 147: '[棒棒糖]', + 148: '[喝奶]', + 149: '[下面]', + 150: '[香蕉]', + 151: '[飞机]', + 152: '[开车]', + 153: '[高铁左车头]', + 154: '[车厢]', + 155: '[高铁右车头]', + 156: '[多云]', + 157: '[下雨]', + 158: '[钞票]', + 159: '[熊猫]', + 160: '[灯泡]', + 161: '[风车]', + 162: '[闹钟]', + 163: '[打伞]', + 164: '[彩球]', + 165: '[钻戒]', + 166: '[沙发]', + 167: '[纸巾]', + 168: '[药]', + 169: '[手枪]', + 170: '[青蛙]', + 171: '[茶]', + 172: '[眨眼睛]', + 173: '[泪奔]', + 174: '[无奈]', + 175: '[卖萌]', + 176: '[小纠结]', + 177: '[喷血]', + 178: '[斜眼笑]', + 179: '[doge]', + 180: '[惊喜]', + 181: '[骚扰]', + 182: '[笑哭]', + 183: '[我最美]', + 184: '[河蟹]', + 185: '[羊驼]', + 186: '[Empty]', + 187: '[幽灵]', + 188: '[蛋]', + 189: '[Empty]', + 190: '[菊花]', + 191: '[Empty]', + 192: '[红包]', + 193: '[大笑]', + 194: '[不开心]', + 195: '[Empty]', + 196: '[Empty]', + 197: '[冷漠]', + 198: '[呃]', + 199: '[好棒]', + 200: '[拜托]', + 201: '[点赞]', + 202: '[无聊]', + 203: '[托脸]', + 204: '[吃]', + 205: '[送花]', + 206: '[害怕]', + 207: '[花痴]', + 208: '[小样儿]', + 209: '[Empty]', + 210: '[飙泪]', + 211: '[我不看]', + 212: '[托腮]', + 213: '[Empty]', + 214: '[啵啵]', + 215: '[糊脸]', + 216: '[拍头]', + 217: '[扯一扯]', + 218: '[舔一舔]', + 219: '[蹭一蹭]', + 220: '[拽炸天]', + 221: '[顶呱呱]', + 222: '[抱抱]', + 223: '[暴击]', + 224: '[开枪]', + 225: '[撩一撩]', + 226: '[拍桌]', + 227: '[拍手]', + 228: '[恭喜]', + 229: '[干杯]', + 230: '[嘲讽]', + 231: '[哼]', + 232: '[佛系]', + 233: '[掐一掐]', + 234: '[惊呆]', + 235: '[颤抖]', + 236: '[啃头]', + 237: '[偷看]', + 238: '[扇脸]', + 239: '[原谅]', + 240: '[喷脸]', + 241: '[生日快乐]', + 242: '[Empty]', + 243: '[Empty]', + 244: '[Empty]', + 245: '[Empty]', + 246: '[Empty]', + 247: '[Empty]', + 248: '[Empty]', + 249: '[Empty]', + 250: '[Empty]', + 251: '[Empty]', + 252: '[Empty]', + 253: '[Empty]', + 254: '[Empty]', + 255: '[Empty]', +} + +qq_sface_list = { + 1: '[拜拜]', + 2: '[鄙视]', + 3: '[菜刀]', + 4: '[沧桑]', + 5: '[馋了]', + 6: '[吃惊]', + 7: '[微笑]', + 8: '[得意]', + 9: '[嘚瑟]', + 10: '[瞪眼]', + 11: '[震惊]', + 12: '[鼓掌]', + 13: '[害羞]', + 14: '[好的]', + 15: '[惊呆了]', + 16: '[静静看]', + 17: '[可爱]', + 18: '[困]', + 19: '[脸红]', + 20: '[你懂的]', + 21: '[期待]', + 22: '[亲亲]', + 23: '[伤心]', + 24: '[生气]', + 25: '[摇摆]', + 26: '[帅]', + 27: '[思考]', + 28: '[震惊哭]', + 29: '[痛心]', + 30: '[偷笑]', + 31: '[挖鼻孔]', + 32: '[抓狂]', + 33: '[笑着哭]', + 34: '[无语]', + 35: '[捂脸]', + 36: '[喜欢]', + 37: '[笑哭]', + 38: '[疑惑]', + 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) -> tempfile: # 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: + if file.seek(0, 2) <= 0: + raise EOFError('File downloaded is Empty') + file.seek(0) + return file + + +def async_send_messages_to_master(msg: Message): + coordinator.send_message(msg) + if msg.file: + msg.file.close() + + +def process_quote_text(text, max_length): # Simple wrapper for processing quoted text + qt_txt = "%s" % text + if max_length > 0: + tgt_text = qt_txt[:max_length] + if len(qt_txt) >= max_length: + tgt_text += "…" + tgt_text = "「%s」" % tgt_text + elif max_length < 0: + tgt_text = "「%s」" % qt_txt + else: + tgt_text = "" + return tgt_text + + +def coolq_text_encode(text: str): # Escape special characters for CQ Code text + expr = (('&', '&'), ('[', '['), (']', ']')) + for r in expr: + text = text.replace(*r) + return text + + +def coolq_para_encode(text: str): # Escape special characters for CQ Code parameters + expr = (('&', '&'), ('[', '['), (']', ']'), (',', ',')) + for r in expr: + text = text.replace(*r) + return text + + +def upload_image_smms(file, path, email, password): # Upload image to sm.ms and return the link + UPLOAD_URL_TOKEN = 'https://sm.ms/api/v2/token' + UPLOAD_URL_IMAGE = 'https://sm.ms/api/v2/upload' + UPLOAD_LOGIN = {'username': email, + 'password': password} + UPLOAD_PARAMS = {'format': 'json', 'ssl': True} + resp = requests.post(UPLOAD_URL_TOKEN, params=UPLOAD_LOGIN) + status = json.loads(resp.text) + if status['code'] == 'success': + token = status['data']['token'] + UPLOAD_HEADER = {'Authorization': token} + else: + logging.getLogger(__name__).warning( + 'WARNING: {}'.format(status['msg'])) + raise CoolQUnknownException(status['msg']) + with open(path, 'rb') as f: + files = {'smfile': f.read()} + resp = requests.post(UPLOAD_URL_IMAGE, files=files, headers=UPLOAD_HEADER, + params=UPLOAD_PARAMS) + status = json.loads(resp.text) + if status['code'] == 'success': + logging.getLogger(__name__).debug('INFO: upload success! url at {}'.format(status['data']['url'])) + return status['data'] + else: + logging.getLogger(__name__).warning('WARNING: {}'.format(status['msg'])) + raise CoolQUnknownException(status['msg']) + + +def upload_image_vim_cn(file, path): # Upload image to img.vim-cn.com and return the link + UPLOAD_URL = 'https://img.vim-cn.com/' + with open(path, 'rb') as f: + files = {'name': f.read()} + resp = requests.post(UPLOAD_URL, files=files) + if resp.status_code != 200: + raise CoolQUnknownException("Failed to upload images to vim-cn.com") + return resp.text + + +def upload_image_sogou(file, path): # Upload image to pic.sogou.com and return the link + UPLOAD_URL = 'https://pic.sogou.com/pic/upload_pic.jsp' + with open(path, 'rb') as f: + files = {'pic_path': f.read()} + resp = requests.post(UPLOAD_URL, files=files) + if resp.status_code != 200: + raise CoolQUnknownException("Failed to upload images to sogou.com") + return "https" + resp.text[4:] # Replace http with https + + +def upload_image_mi(file, path): # Upload image to shopapi.io.mi.com and return the link + UPLOAD_URL = 'https://shopapi.io.mi.com/homemanage/shop/uploadpic' + with open(path, 'rb') as f: + files = {'pic': (ntpath.basename(path), f.read(), "image/jpeg")} + resp = requests.post(UPLOAD_URL, files=files) + if resp.status_code != 200: + raise CoolQUnknownException("Failed to upload images to mi.com") + status = json.loads(resp.text) + print(status) + if status['message'] != "ok": + raise CoolQUnknownException("Failed to upload images to mi.com") + return status['result'] + + +def param_spliter(str_param): + params = str_param.split(";") + param = {} + for _k in params: + key, value = _k.strip().split("=") + param[key] = value + return param + + +def download_file_from_qzone(cookie: str, csrf_token: str, uin, group_id, file_id, filename, file_size): + cookie_arr = param_spliter(cookie) + url = "http://qun.qzone.qq.com/cgi-bin/group_share_get_downurl?uin=" + str(uin) + "&pa=/104/" + \ + str(file_id) + "&groupid=" + str(group_id) + "&bussinessid=0&charset=utf-8&g_tk=" + str(csrf_token) + "&r=888" + ret = requests.get(url, cookies=cookie_arr) + data = json.loads(ret.text.split("(")[1].split(")")[0])['data'] + cookie += "; FTN5K=" + str(data['cookie']) + download_url = data['url'] + download_url += "/" + quote(filename) + if file_size >= 50*1024*1024: # File size is bigger than 50MiB + return _("File is too big to be downloaded") + file = tempfile.NamedTemporaryFile() + try: + opener = urllib.request.build_opener() + opener.addheaders.append(('Cookie', cookie)) + 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: + if file.seek(0, 2) <= 0: + raise EOFError('File downloaded is Empty') + file.seek(0) + return file + ''' + try: + opener = urllib.request.build_opener() + opener.addheaders.append(('Cookie', cookie)) + with opener.open(download_url) as response, tempfile.NamedTemporaryFile() as f: + shutil.copyfileobj(response, f) + if f.seek(0, 2) <= 0: + raise EOFError('File downloaded is Empty') + f.seek(0) + return f + except Exception as e: + logging.getLogger(__name__).warning("Error occurs when downloading files" + str(e)) + return url + ''' + + +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) + 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) + return file + + +def get_friend_group_via_qq_show(cookie: str, csrf_token: str) -> Dict[str, str]: + # This function won't check before execute, instead all the exceptions will be thrown + cookie_arr = param_spliter(cookie) + url = "https://show.qq.com/cgi-bin/qqshow_user_friendgroup?g_tk={csrf_token}&omode=4" \ + .format(csrf_token=csrf_token) + ret = requests.get(url, cookies=cookie_arr) + data = json.loads(ret.text) + friend_group = {} + for i in range(len(data['data']['group'])): # friend group + for j in range(len(data['data']['group'][i]['friend'])): + current_user = str(data['data']['group'][i]['friend'][j]['uin']) + current_group = data['data']['group'][i]['name'] + friend_group[current_user] = current_group + return friend_group + + +def download_voice(filename: str, api_root: str, access_token: str): + file = tempfile.NamedTemporaryFile() + url = '{url}/data/record/{file}'.format(url=api_root, file=filename) + try: + opener = urllib.request.build_opener() + opener.addheaders = [("Authorization", "Bearer {at}".format(at=access_token))] + + 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) + return file + + +def get_stranger_info_via_qzone(uin: str): + pattern = re.compile(r"\((.*)\)") + resp = requests.get("https://users.qzone.qq.com/fcg-bin/cgi_get_portrait.fcg?uins={id}".format(id=uin)) + # Assume that this API is always available + data = pattern.findall(resp.text) + if not data: + return "" + try: + data = json.loads(data[0]) + ret = { + "uin": uin, + "nickname": data[uin][6], + "avatar_url": data[uin][0] + } + return ret + except: + return "" diff --git a/efb_qq_plugin_coolq/__init__.py b/efb_qq_plugin_coolq/__init__.py new file mode 100644 index 0000000..8002f45 --- /dev/null +++ b/efb_qq_plugin_coolq/__init__.py @@ -0,0 +1 @@ +from . import CoolQ diff --git a/efb_qq_plugin_coolq/__version__.py b/efb_qq_plugin_coolq/__version__.py new file mode 100644 index 0000000..876b5de --- /dev/null +++ b/efb_qq_plugin_coolq/__version__.py @@ -0,0 +1,3 @@ +# coding: utf-8 + +__version__ = '2.0.0a0' diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ee14e81 --- /dev/null +++ b/setup.py @@ -0,0 +1,45 @@ +import sys +from setuptools import setup, find_packages + +if sys.version_info < (3, 6): + raise Exception("Python 3.6 or higher is required. Your version is %s." % sys.version) + +__version__ = "" +exec(open('efb_qq_plugin_coolq/__version__.py').read()) + +long_description = open('README.rst').read() + +setup( + name='efb-qq-plugin-coolq', + packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), + version=__version__, + description='EQS plugin for CoolQ API Compatible Client.', + long_description=long_description, + include_package_data=True, + author='Milkice', + author_email='milkice@milkice.me', + url='https://github.com/milkice233/efb-qq-plugin-coolq', + license='GPLv3', + python_requires='>=3.6', + keywords=['ehforwarderbot', 'EH Forwarder Bot', 'EH Forwarder Bot Slave Channel', + 'qq', 'chatbot', 'EQS', 'CoolQ'], + classifiers=[ + "Development Status :: 3 - Alpha", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Intended Audience :: Developers", + "Intended Audience :: End Users/Desktop", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Topic :: Communications :: Chat", + "Topic :: Utilities" + ], + install_requires=[ + "efb-qq-slave", "ehforwarderbot", + "PyYaml", + 'requests', 'python-magic', 'Pillow', 'cqhttp>=1.3.0', 'cherrypy>=18.5.0' + ], + entry_points={ + 'ehforwarderbot.qq.plugin': 'CoolQ = efb_qq_plugin_coolq:CoolQ' + } +) \ No newline at end of file