Compare commits

..

7 Commits
new ... master

Author SHA1 Message Date
2a52da8c61
Support Group Admin Change Event and Member Restrict Event 2022-03-12 18:15:41 +08:00
0ae4cb80f8
fix a bug 2022-03-09 18:56:45 +08:00
19b1b2a28c
Support recall notice 2022-03-09 17:25:31 +08:00
54f8e5466e
Filter qq buld message 2022-03-07 22:26:29 +08:00
2848a901dd
Support New Group Join Request 2022-03-07 22:25:20 +08:00
0a43230e76
Support flash picture 2022-03-07 22:23:50 +08:00
a99269250e
Support group filter 2022-03-07 22:22:35 +08:00
9 changed files with 1132 additions and 1229 deletions

View File

@ -1,17 +1,17 @@
repos:
- repo: https://github.com/psf/black
rev: 22.12.0
rev: 22.1.0
hooks:
- id: black
- repo: https://github.com/PyCQA/flake8
rev: 6.0.0
rev: 4.0.1
hooks:
- id: flake8
additional_dependencies:
- flake8-bugbear
- repo: https://github.com/PyCQA/isort
rev: 5.11.4
rev: 5.10.1
hooks:
- id: isort

View File

@ -38,8 +38,12 @@ efb-qq-plugin-go-cqhttp 是 efb-qq-slave 的插件,需要配合 efb-qq-slave
servers:
# HTTP 通信设置
- http:
# HTTP监听地址
address: 127.0.0.1:5700
# 是否关闭正向 HTTP 服务器
disabled: false
# 服务端监听地址
host: 127.0.0.1
# 服务端监听端口
port: 5700
# 反向 HTTP 超时时间, 单位秒
# 最小值为 5小于 5 将会忽略本项设置
timeout: 5

View File

@ -20,10 +20,10 @@ class ChatManager:
channel=self.channel, uid=ChatID("__error_chat__"), name="Chat Missing"
)
async def build_efb_chat_as_private(self, context):
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 = await self.channel.QQClient.get_stranger_info(uid)
i: dict = self.channel.QQClient.get_stranger_info(uid)
chat_name = ""
if i:
chat_name = i["nickname"]
@ -37,13 +37,13 @@ class ChatManager:
)
return efb_chat
async def build_or_get_efb_member(self, chat: Chat, context):
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 = await self.channel.QQClient.get_stranger_info(member_uid)
i: dict = 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),
)
async def build_efb_chat_as_group(self, context, update_member=False): # Should be cached
is_discuss = context["message_type"] != "group"
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 = await self.channel.QQClient.get_group_info(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 = await self.channel.QQClient.get_group_member_list(chat_uid, False)
members = self.channel.QQClient.get_group_member_list(chat_uid, False)
if members:
for member in members:
efb_chat.add_member(

File diff suppressed because it is too large Load Diff

View File

@ -1,46 +1,44 @@
import asyncio
import base64
import html
import json
import logging
import sys
from typing import TYPE_CHECKING
import magic
from ehforwarderbot import Chat, Message, MsgType
from ehforwarderbot.message import LinkAttribute, LocationAttribute, Substitutions
from . import GoCQHttp
from .Utils import cq_get_image, download_file, download_voice
if TYPE_CHECKING:
from .GoCQHttp import GoCQHttp
class QQMsgProcessor:
inst: "GoCQHttp"
inst: GoCQHttp
logger: logging.Logger = logging.getLogger(__name__)
def __init__(self, instance: "GoCQHttp"):
def __init__(self, instance: GoCQHttp):
self.inst = instance
self._ = instance._
pass
async def qq_image_wrapper(self, data, _: Chat = None):
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 = "[Image Source missing]"
efb_msg.text = self._("[Image Source missing]")
return [efb_msg]
# flash picture
if data.get("type", "") == "flash":
data["url"] = (
f"https://gchat.qpic.cn/gchatpic_new/1/1-1-" f'{data["file"].replace(".image", "").upper()}/0?term=3%27'
)
efb_msg.text = "Send a flash picture."
# flash
if 'type' in data:
if 'flash' == data['type']:
data['url'] = f'https://gchat.qpic.cn/gchatpic_new/1/1-1-' \
f'{data["file"].replace(".image", "").upper()}/0?term=3%27'
efb_msg.text = self._('Send a flash picture.')
efb_msg.file = await cq_get_image(data["url"])
efb_msg.file = cq_get_image(data["url"])
if efb_msg.file is None:
efb_msg.type = MsgType.Text
efb_msg.text = "[Download image failed, please check on your QQ client]"
efb_msg.text = self._("[Download image failed, please check on your QQ client]")
return [efb_msg]
efb_msg.type = MsgType.Image
@ -55,11 +53,11 @@ class QQMsgProcessor:
efb_msg.type = MsgType.Animation
return [efb_msg]
async def qq_record_wrapper(self, data, _: Chat = None): # Experimental!
def qq_record_wrapper(self, data, chat: Chat = None): # Experimental!
efb_msg = Message()
try:
efb_msg.type = MsgType.Audio
efb_msg.file = await download_voice(data["url"])
efb_msg.file = download_voice(data["url"])
mime = magic.from_file(efb_msg.file.name, mime=True)
if isinstance(mime, bytes):
mime = mime.decode()
@ -67,11 +65,11 @@ class QQMsgProcessor:
efb_msg.mime = mime
except Exception:
efb_msg.type = MsgType.Unsupported
efb_msg.text = "[Voice Message] Please check it on your QQ"
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 = None):
def qq_share_wrapper(self, data, chat: Chat = None):
efb_msg = Message(
type=MsgType.Link,
text="",
@ -84,7 +82,7 @@ class QQMsgProcessor:
)
return [efb_msg]
def qq_location_wrapper(self, data, _: Chat = None):
def qq_location_wrapper(self, data, chat: Chat = None):
efb_msg = Message(
text=data["content"],
type=MsgType.Location,
@ -92,23 +90,23 @@ class QQMsgProcessor:
)
return [efb_msg]
def qq_shake_wrapper(self, _, __: Chat = None):
efb_msg = Message(type=MsgType.Text, text=("[Your friend shakes you!]"))
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 = None):
def qq_contact_wrapper(self, data, chat: Chat = None):
uid = data["id"]
contact_type = data["type"]
efb_msg = Message(
type=MsgType.Text,
text=("Chat Recommendation Received\nID: {}\nType: {}").format(uid, contact_type),
text=self._("Chat Recommendation Received\nID: {}\nType: {}").format(uid, contact_type),
)
return [efb_msg]
def qq_bface_wrapper(self, _, __: Chat = None):
def qq_bface_wrapper(self, data, chat: Chat = None):
efb_msg = Message(
type=MsgType.Unsupported,
text=("[Here comes the BigFace Emoji, please check it on your phone]"),
text=self._("[Here comes the BigFace Emoji, please check it on your phone]"),
)
return [efb_msg]
@ -116,33 +114,33 @@ class QQMsgProcessor:
# todo this function's maybe not necessary?
pass
def qq_sign_wrapper(self, data, _: Chat = None):
location = ("at {}").format(data["location"]) if "location" in data else ("at Unknown Place")
title = "" if "title" not in data else (("with title {}").format(data["title"]))
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=("signed in {location} {title}").format(title=title, location=location),
text=self._("signed in {location} {title}").format(title=title, location=location),
)
return [efb_msg]
async def qq_rich_wrapper(self, data: dict, chat: Chat = None): # Buggy, Help needed
def qq_rich_wrapper(self, data: dict, chat: Chat = None): # Buggy, Help needed
efb_messages = list()
efb_msg = Message(
type=MsgType.Unsupported,
text=("[Here comes the Rich Text, dumping...] \n"),
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
_ = await self.qq_group_broadcast_wrapper(data, chat)
_ = self.qq_group_broadcast_wrapper(data, chat)
if _ is not None:
efb_messages.append(_)
return efb_messages
def qq_music_wrapper(self, data, _: Chat = None):
def qq_music_wrapper(self, data, chat: Chat = None):
efb_msg = Message()
if data["type"] == "163": # Netease Cloud Music
efb_msg.type = MsgType.Text
@ -192,7 +190,7 @@ class QQMsgProcessor:
efb_msg.filename = data["filename"]
return efb_msg
async def qq_group_broadcast_wrapper(self, data, chat: Chat):
def qq_group_broadcast_wrapper(self, data, chat: Chat = None):
try:
at_list = {}
content_data = json.loads(data["content"])
@ -211,21 +209,21 @@ class QQMsgProcessor:
data["url"] = "http://gdynamic.qpic.cn/gdynamic/{}/628".format(
content_data["mannounce"]["pic"][0]["url"]
)
efb_message = (await self.qq_image_wrapper(data))[0]
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 asyncio.run(self.qq_group_broadcast_alternative_wrapper(data, chat))
return self.qq_group_broadcast_alternative_wrapper(data)
async def qq_group_broadcast_alternative_wrapper(self, data, chat: Chat):
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 = await self.inst.coolq_api_query("_get_group_notice", group_id=group_id)
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"])
@ -240,7 +238,7 @@ class QQMsgProcessor:
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 = (await self.qq_image_wrapper(data))[0]
efb_message = self.qq_image_wrapper(data)[0]
efb_message.text = text
efb_message.substitutions = Substitutions(at_list)
return [efb_message]
@ -249,13 +247,13 @@ class QQMsgProcessor:
except Exception:
return None
def qq_xml_wrapper(self, data, _: Chat = None):
def qq_xml_wrapper(self, data, chat: Chat = None):
efb_msg = Message()
efb_msg.type = MsgType.Text
efb_msg.text = data["data"]
return [efb_msg]
def qq_json_wrapper(self, data, _: Chat = None):
def qq_json_wrapper(self, data, chat: Chat = None):
efb_msg = Message()
efb_msg.type = MsgType.Text
efb_msg.text = data["data"]
@ -298,17 +296,6 @@ class QQMsgProcessor:
preview=meta_detail1["preview"],
)
# Tencent group photo upload
elif dict_data["app"] == "com.tencent.groupphoto":
album_name = dict_data["meta"]["albumData"]["title"]
photo_urls = ["https://" + i["url"] for i in dict_data["meta"]["albumData"]["pics"]]
efb_msg.text = "【群相册】\n\n{}\n\n{}".format(album_name, "\n".join(photo_urls))
# Tencent group photo album create
elif dict_data["app"] == "com.tencent.qzone.albumShare":
album_name = dict_data["meta"]["albumData"]["title"]
efb_msg.text = "【群相册】\n\n{}".format(album_name)
# Shared third-party Apps
elif dict_data["app"] == "com.tencent.structmsg":
meta_view = dict_data["meta"][dict_data["view"]]
@ -337,14 +324,14 @@ class QQMsgProcessor:
return [efb_msg]
async def qq_video_wrapper(self, data, _: Chat = None):
res = await download_file(data["url"])
def qq_video_wrapper(self, data, chat: Chat = None):
res = download_file(data["url"])
mime = magic.from_file(res.name, mime=True)
if isinstance(mime, bytes):
mime = mime.decode()
efb_msg = Message(type=MsgType.Video, file=res, filename=res.name, mime=mime)
return [efb_msg]
def qq_unsupported_wrapper(self, data, _: Chat = None):
def qq_unsupported_wrapper(self, data, chat: Chat = None):
efb_msg = Message(type=MsgType.Unsupported, text=data)
return [efb_msg]

View File

@ -1,13 +1,13 @@
import logging
import tempfile
from typing import IO, Optional, Union
import urllib.request
from gettext import translation
from urllib.error import ContentTooShortError, HTTPError, URLError
import httpx
import pilk
import pydub
from ehforwarderbot import Message, coordinator
logger = logging.getLogger(__name__)
from pkg_resources import resource_filename
# created by JogleLew and jqqqqqqqqqq, optimized based on Tim's emoji support, updated by xzsk2 to mobileqq v8.8.11
qq_emoji_list = {
@ -639,52 +639,34 @@ 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
async def async_get_file(url: str) -> IO:
temp_file = tempfile.NamedTemporaryFile()
def cq_get_image(image_link: str) -> tempfile: # Download image from QQ
file = tempfile.NamedTemporaryFile()
try:
async with httpx.AsyncClient() as client:
resp = await client.get(url)
temp_file.write(resp.content)
if temp_file.seek(0, 2) <= 0:
raise EOFError("File downloaded is Empty")
temp_file.seek(0)
except Exception as e:
temp_file.close()
raise e
return temp_file
def sync_get_file(url: str) -> IO:
temp_file = tempfile.NamedTemporaryFile()
try:
resp = httpx.get(url)
temp_file.write(resp.content)
if temp_file.seek(0, 2) <= 0:
raise EOFError("File downloaded is Empty")
temp_file.seek(0)
except Exception as e:
temp_file.close()
raise e
return temp_file
async def cq_get_image(image_link: str) -> Optional[IO]: # Download image from QQ
try:
return await async_get_file(image_link)
except Exception as e:
logger.warning("File download failed.")
logger.warning(str(e))
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):
try:
coordinator.send_message(msg)
finally:
if msg.file:
msg.file.close()
coordinator.send_message(msg)
if msg.file:
msg.file.close()
def process_quote_text(text, max_length): # Simple wrapper for processing quoted text
@ -724,54 +706,82 @@ def param_spliter(str_param):
return param
async def download_file(download_url: str) -> Union[IO, str]:
def download_file(download_url):
file = tempfile.NamedTemporaryFile()
try:
return await async_get_file(download_url)
except Exception as e:
logger.warning("Error occurs when downloading files: " + str(e))
return "Error occurs when downloading files: " + str(e)
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:
if file.seek(0, 2) <= 0:
raise EOFError("File downloaded is Empty")
file.seek(0)
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:
return sync_get_file(url)
except Exception as e:
logger.warning("Error occurs when downloading files: " + str(e))
raise
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:
return sync_get_file(url)
except Exception as e:
logger.warning("Error occurs when downloading files: " + str(e))
raise
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
async def download_voice(voice_url: str):
origin_file, audio_file = None, None
def download_voice(voice_url: str):
origin_file = tempfile.NamedTemporaryFile()
try:
origin_file = await async_get_file(voice_url)
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
opener = urllib.request.build_opener()
urllib.request.install_opener(opener)
urllib.request.urlretrieve(voice_url, origin_file.name)
except Exception as e:
if origin_file:
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()
if audio_file:
audio_file.close()
logger.warning("Error occurs when downloading files: " + str(e))
raise
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
return audio_file

View File

@ -1,3 +1,3 @@
from . import GoCQHttp # noqa: F401
__version__ = "3.0.2"
__version__ = "2.2.1"

1116
pdm.lock

File diff suppressed because it is too large Load Diff

View File

@ -6,14 +6,13 @@ dependencies = [
"efb-qq-slave @ git+https://github.com/milkice233/efb-qq-slave@master",
"ehforwarderbot~=2.1.1",
"PyYAML~=6.0",
"requests~=2.27.1",
"python-magic~=0.4.25",
"Pillow~=9.0.1",
"aiocqhttp~=1.4.3",
"quart~=0.17.0",
"hypercorn~=0.13.2",
"cqhttp~=1.3.1",
"CherryPy~=18.6.1",
"pilk~=0.0.2",
"pydub~=0.25.1",
"httpx>=0.23.3",
]
requires-python = ">=3.7"
license = { text = "AGPL-3.0-only" }
@ -57,7 +56,7 @@ build-backend = "pdm.pep517.api"
version = { from = "efb_qq_plugin_go_cqhttp/__init__.py" }
[tool.pdm.dev-dependencies]
dev = ["efb-telegram-master~=2.2.4"]
dev = ["pre-commit", "efb-telegram-master~=2.2.4"]
[tool.black]
line-length = 120