feat:支持VoceChat
This commit is contained in:
parent
8cb061ff75
commit
d112f49a69
@ -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))
|
||||||
|
@ -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"
|
||||||
# 下载器监控开关
|
# 下载器监控开关
|
||||||
|
@ -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
|
||||||
|
128
app/modules/vocechat/__init__.py
Normal file
128
app/modules/vocechat/__init__.py
Normal 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/markdown:markdown消息,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
|
196
app/modules/vocechat/vocechat.py
Normal file
196
app/modules/vocechat/vocechat.py
Normal 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
|
@ -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
|
||||||
|
@ -116,3 +116,4 @@ class MessageChannel(Enum):
|
|||||||
Telegram = "Telegram"
|
Telegram = "Telegram"
|
||||||
Slack = "Slack"
|
Slack = "Slack"
|
||||||
SynologyChat = "SynologyChat"
|
SynologyChat = "SynologyChat"
|
||||||
|
VoceChat = "VoceChat"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user