feat:支持VoceChat

This commit is contained in:
jxxghp 2024-03-06 15:54:40 +08:00
parent 8cb061ff75
commit d112f49a69
7 changed files with 346 additions and 3 deletions

View File

@ -40,7 +40,7 @@ async def user_message(background_tasks: BackgroundTasks, request: Request):
def wechat_verify(echostr: str, msg_signature: str, def wechat_verify(echostr: str, msg_signature: str,
timestamp: Union[str, int], nonce: str) -> Any: timestamp: Union[str, int], nonce: str) -> Any:
""" """
用户消息响应 微信验证响应
""" """
logger.info(f"收到微信验证请求: {echostr}") logger.info(f"收到微信验证请求: {echostr}")
try: try:
@ -60,6 +60,14 @@ def wechat_verify(echostr: str, msg_signature: str,
return PlainTextResponse(sEchoStr) return PlainTextResponse(sEchoStr)
@router.get("/", summary="VoceChat验证")
def vocechat_verify() -> Any:
"""
VoceChat验证响应
"""
return {"status": "OK"}
@router.get("/switchs", summary="查询通知消息渠道开关", response_model=List[NotificationSwitch]) @router.get("/switchs", summary="查询通知消息渠道开关", response_model=List[NotificationSwitch])
def read_switchs(_: schemas.TokenPayload = Depends(verify_token)) -> Any: def read_switchs(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
""" """
@ -72,7 +80,7 @@ def read_switchs(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
for noti in NotificationType: for noti in NotificationType:
return_list.append(NotificationSwitch(mtype=noti.value, wechat=True, return_list.append(NotificationSwitch(mtype=noti.value, wechat=True,
telegram=True, slack=True, telegram=True, slack=True,
synologychat=True)) synologychat=True, vocechat=True))
else: else:
for switch in switchs: for switch in switchs:
return_list.append(NotificationSwitch(**switch)) return_list.append(NotificationSwitch(**switch))

View File

@ -83,7 +83,7 @@ class Settings(BaseSettings):
AUTH_SITE: str = "" AUTH_SITE: str = ""
# 交互搜索自动下载用户ID使用,分割 # 交互搜索自动下载用户ID使用,分割
AUTO_DOWNLOAD_USER: Optional[str] = None AUTO_DOWNLOAD_USER: Optional[str] = None
# 消息通知渠道 telegram/wechat/slack,多个通知渠道用,分隔 # 消息通知渠道 telegram/wechat/slack/synologychat/vocechat,多个通知渠道用,分隔
MESSAGER: str = "telegram" MESSAGER: str = "telegram"
# WeChat企业ID # WeChat企业ID
WECHAT_CORPID: Optional[str] = None WECHAT_CORPID: Optional[str] = None
@ -117,6 +117,12 @@ class Settings(BaseSettings):
SYNOLOGYCHAT_WEBHOOK: str = "" SYNOLOGYCHAT_WEBHOOK: str = ""
# SynologyChat Token # SynologyChat Token
SYNOLOGYCHAT_TOKEN: str = "" SYNOLOGYCHAT_TOKEN: str = ""
# VoceChat地址
VOCECHAT_HOST: str = ""
# VoceChat ApiKey
VOCECHAT_API_KEY: str = ""
# VoceChat 频道ID
VOCECHAT_CHANNEL_ID: str = ""
# 下载器 qbittorrent/transmission # 下载器 qbittorrent/transmission
DOWNLOADER: str = "qbittorrent" DOWNLOADER: str = "qbittorrent"
# 下载器监控开关 # 下载器监控开关

View File

@ -67,6 +67,8 @@ def checkMessage(channel_type: MessageChannel):
return None return None
if channel_type == MessageChannel.SynologyChat and not switch.get("synologychat"): if channel_type == MessageChannel.SynologyChat and not switch.get("synologychat"):
return None return None
if channel_type == MessageChannel.VoceChat and not switch.get("vocechat"):
return None
return func(self, message, *args, **kwargs) return func(self, message, *args, **kwargs)
return wrapper return wrapper

View File

@ -0,0 +1,128 @@
import json
from typing import Optional, Union, List, Tuple, Any, Dict
from app.core.config import settings
from app.core.context import Context, MediaInfo
from app.log import logger
from app.modules import _ModuleBase, checkMessage
from app.modules.vocechat.vocechat import VoceChat
from app.schemas import MessageChannel, CommingMessage, Notification
class VoceChatModule(_ModuleBase):
vocechat: VoceChat = None
def init_module(self) -> None:
self.vocechat = VoceChat()
def stop(self):
pass
def test(self) -> Tuple[bool, str]:
"""
测试模块连接性
"""
state = self.vocechat.get_state()
if state:
return True, ""
return False, "获取VoceChat频道失败"
def init_setting(self) -> Tuple[str, Union[str, bool]]:
return "MESSAGER", "vocechat"
def message_parser(self, body: Any, form: Any,
args: Any) -> Optional[CommingMessage]:
"""
解析消息内容返回字典注意以下约定值
userid: 用户ID
username: 用户名
text: 内容
:param body: 请求体
:param form: 表单
:param args: 参数
:return: 渠道消息体
"""
try:
"""
{
"created_at": 1672048481664, //消息创建的时间戳
"detail": {
"content": "hello this is my message to you", //消息内容
"content_type": "text/plain", //消息类型text/plain纯文本消息text/markdownmarkdown消息vocechat/file文件类消息
"expires_in": null, //消息过期时长如果有大于0数字说明该消息是个限时消息
"properties": null, //一些有关消息的元数据比如at信息文件消息的具体类型信息如果是个图片消息还会有一些宽高图片名称等元信息
"type": "normal" //消息类型normal代表是新消息
},
"from_uid": 7910, //来自于谁
"mid": 2978, //消息ID
"target": { "gid": 2 } //发送给谁gid代表是发送给频道uid代表是发送给个人此时的数据结构举例{"uid":1}
}
"""
# URL参数
print("----VoceChat Body----")
print(body)
print("----VoceChat from----")
print(form)
print("----VoceChat args----")
print(args)
# 报文体
msg_body = json.loads(body)
# 类型
msg_type = msg_body.get("detail", {}).get("type")
if msg_type != "normal":
# 非新消息
return None
# 文本内容
content = msg_body.get("detail", {}).get("content")
# 用户ID
gid = msg_body.get("target", {}).get("gid")
if gid and gid == settings.VOCECHAT_CHANNEL_ID:
# 来自监听频道的消息
userid = f"GID#{gid}"
else:
# 来自个人的消息
userid = f"UID#{msg_body.get('from_uid')}"
# 处理消息内容
if content and userid:
logger.info(f"收到VoceChat消息userid={userid}, text={content}")
return CommingMessage(channel=MessageChannel.VoceChat,
userid=userid, username=userid, text=content)
except Exception as err:
logger.error(f"VoceChat消息处理发生错误{str(err)}")
return None
@checkMessage(MessageChannel.VoceChat)
def post_message(self, message: Notification) -> None:
"""
发送消息
:param message: 消息内容
:return: 成功或失败
"""
self.vocechat.send_msg(title=message.title, text=message.text, userid=message.userid)
@checkMessage(MessageChannel.VoceChat)
def post_medias_message(self, message: Notification, medias: List[MediaInfo]) -> Optional[bool]:
"""
发送媒体信息选择列表
:param message: 消息内容
:param medias: 媒体列表
:return: 成功或失败
"""
# 先发送标题
self.vocechat.send_msg(title=message.title, userid=message.userid)
# 再发送内容
return self.vocechat.send_medias_msg(title=message.title, medias=medias, userid=message.userid)
@checkMessage(MessageChannel.VoceChat)
def post_torrents_message(self, message: Notification, torrents: List[Context]) -> Optional[bool]:
"""
发送种子信息选择列表
:param message: 消息内容
:param torrents: 种子列表
:return: 成功或失败
"""
return self.vocechat.send_torrents_msg(title=message.title, torrents=torrents, userid=message.userid)
def register_commands(self, commands: Dict[str, dict]):
pass

View File

@ -0,0 +1,196 @@
import re
import threading
from typing import Optional, List
from app.core.config import settings
from app.core.context import MediaInfo, Context
from app.core.metainfo import MetaInfo
from app.log import logger
from app.utils.common import retry
from app.utils.http import RequestUtils
from app.utils.string import StringUtils
lock = threading.Lock()
class VoceChat:
# host
_host = None
# apikey
_apikey = None
# 频道ID
_channel_id = None
# 请求对象
_client = None
def __init__(self):
"""
初始化
"""
self._host = settings.VOCECHAT_HOST
if self._host:
if not self._host.endswith("/"):
self._host += "/"
if not self._host.startswith("http"):
self._playhost = "http://" + self._host
self._apikey = settings.VOCECHAT_API_KEY
self._channel_id = settings.VOCECHAT_CHANNEL_ID
if self._apikey and self._host and self._channel_id:
self._client = RequestUtils(headers={
"content-type": "text/markdown",
"x-api-key": self._apikey,
"accept": "application/json; charset=utf-8"
})
def get_state(self):
"""
获取状态
"""
return True if self.get_groups() else False
def get_groups(self):
"""
获取频道列表
"""
if not self._client:
return None
result = self._client.get_res(f"{self._host}api/bot")
if result and result.status_code == 200:
return result.json()
def send_msg(self, title: str, text: str = "", userid: str = None) -> Optional[bool]:
"""
微信消息发送入口支持文本图片链接跳转指定发送对象
:param title: 消息标题
:param text: 消息内容
:param userid: 消息发送对象的ID为空则发给所有人
:return: 发送状态错误信息
"""
if not self._client:
return None
if not title and not text:
logger.warn("标题和内容不能同时为空")
return False
try:
if text:
caption = f"*{title}*\n{text}"
else:
caption = f"*{title}*"
if userid:
chat_id = userid
else:
chat_id = f"GID#{self._channel_id}"
return self.__send_request(userid=chat_id, caption=caption)
except Exception as msg_e:
logger.error(f"发送消息失败:{msg_e}")
return False
def send_medias_msg(self, title: str, medias: List[MediaInfo], userid: str = "") -> Optional[bool]:
"""
发送列表类消息
"""
if not self._client:
return None
try:
index, image, caption = 1, "", "*%s*" % title
for media in medias:
if not image:
image = media.get_message_image()
if media.vote_average:
caption = "%s\n%s. [%s](%s)\n_%s%s_" % (caption,
index,
media.title_year,
media.detail_link,
f"类型:{media.type.value}",
f"评分:{media.vote_average}")
else:
caption = "%s\n%s. [%s](%s)\n_%s_" % (caption,
index,
media.title_year,
media.detail_link,
f"类型:{media.type.value}")
index += 1
if userid:
chat_id = userid
else:
chat_id = f"GID#{self._channel_id}"
return self.__send_request(userid=chat_id, caption=caption)
except Exception as msg_e:
logger.error(f"发送消息失败:{msg_e}")
return False
def send_torrents_msg(self, torrents: List[Context],
userid: str = "", title: str = "") -> Optional[bool]:
"""
发送列表消息
"""
if not self._client:
return None
if not torrents:
return False
try:
index, caption = 1, "*%s*" % title
mediainfo = torrents[0].media_info
for context in torrents:
torrent = context.torrent_info
site_name = torrent.site_name
meta = MetaInfo(torrent.title, torrent.description)
link = torrent.page_url
title = f"{meta.season_episode} " \
f"{meta.resource_term} " \
f"{meta.video_term} " \
f"{meta.release_group}"
title = re.sub(r"\s+", " ", title).strip()
free = torrent.volume_factor
seeder = f"{torrent.seeders}"
caption = f"{caption}\n{index}.【{site_name}】[{title}]({link}) " \
f"{StringUtils.str_filesize(torrent.size)} {free} {seeder}"
index += 1
if userid:
chat_id = userid
else:
chat_id = f"GID#{self._channel_id}"
return self.__send_request(userid=chat_id, caption=caption,
image=mediainfo.get_message_image())
except Exception as msg_e:
logger.error(f"发送消息失败:{msg_e}")
return False
@retry(Exception, logger=logger)
def __send_request(self, userid: str, caption: str) -> bool:
"""
向VoceChat发送报文
userid格式UID#xxx / GID#xxx
"""
if not self._client:
return False
if userid.startswith("GID#"):
action = "send_to_group"
else:
action = "send_to_user"
idstr = userid[4:]
with lock:
try:
result = self._client.post_res(f"{self._host}api/bot/{action}/{idstr}", data=caption)
if result and result.status_code == 200:
return True
else:
logger.error(f"VoceChat发送消息失败{result.text}")
return False
except Exception as msg_e:
logger.error(f"VoceChat发送消息错误{str(msg_e)}")
return False

View File

@ -53,3 +53,5 @@ class NotificationSwitch(BaseModel):
slack: Optional[bool] = False slack: Optional[bool] = False
# SynologyChat开关 # SynologyChat开关
synologychat: Optional[bool] = False synologychat: Optional[bool] = False
# VoceChat开关
vocechat: Optional[bool] = False

View File

@ -116,3 +116,4 @@ class MessageChannel(Enum):
Telegram = "Telegram" Telegram = "Telegram"
Slack = "Slack" Slack = "Slack"
SynologyChat = "SynologyChat" SynologyChat = "SynologyChat"
VoceChat = "VoceChat"