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,
|
||||
timestamp: Union[str, int], nonce: str) -> Any:
|
||||
"""
|
||||
用户消息响应
|
||||
微信验证响应
|
||||
"""
|
||||
logger.info(f"收到微信验证请求: {echostr}")
|
||||
try:
|
||||
@ -60,6 +60,14 @@ def wechat_verify(echostr: str, msg_signature: str,
|
||||
return PlainTextResponse(sEchoStr)
|
||||
|
||||
|
||||
@router.get("/", summary="VoceChat验证")
|
||||
def vocechat_verify() -> Any:
|
||||
"""
|
||||
VoceChat验证响应
|
||||
"""
|
||||
return {"status": "OK"}
|
||||
|
||||
|
||||
@router.get("/switchs", summary="查询通知消息渠道开关", response_model=List[NotificationSwitch])
|
||||
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:
|
||||
return_list.append(NotificationSwitch(mtype=noti.value, wechat=True,
|
||||
telegram=True, slack=True,
|
||||
synologychat=True))
|
||||
synologychat=True, vocechat=True))
|
||||
else:
|
||||
for switch in switchs:
|
||||
return_list.append(NotificationSwitch(**switch))
|
||||
|
@ -83,7 +83,7 @@ class Settings(BaseSettings):
|
||||
AUTH_SITE: str = ""
|
||||
# 交互搜索自动下载用户ID,使用,分割
|
||||
AUTO_DOWNLOAD_USER: Optional[str] = None
|
||||
# 消息通知渠道 telegram/wechat/slack,多个通知渠道用,分隔
|
||||
# 消息通知渠道 telegram/wechat/slack/synologychat/vocechat,多个通知渠道用,分隔
|
||||
MESSAGER: str = "telegram"
|
||||
# WeChat企业ID
|
||||
WECHAT_CORPID: Optional[str] = None
|
||||
@ -117,6 +117,12 @@ class Settings(BaseSettings):
|
||||
SYNOLOGYCHAT_WEBHOOK: str = ""
|
||||
# SynologyChat Token
|
||||
SYNOLOGYCHAT_TOKEN: str = ""
|
||||
# VoceChat地址
|
||||
VOCECHAT_HOST: str = ""
|
||||
# VoceChat ApiKey
|
||||
VOCECHAT_API_KEY: str = ""
|
||||
# VoceChat 频道ID
|
||||
VOCECHAT_CHANNEL_ID: str = ""
|
||||
# 下载器 qbittorrent/transmission
|
||||
DOWNLOADER: str = "qbittorrent"
|
||||
# 下载器监控开关
|
||||
|
@ -67,6 +67,8 @@ def checkMessage(channel_type: MessageChannel):
|
||||
return None
|
||||
if channel_type == MessageChannel.SynologyChat and not switch.get("synologychat"):
|
||||
return None
|
||||
if channel_type == MessageChannel.VoceChat and not switch.get("vocechat"):
|
||||
return None
|
||||
return func(self, message, *args, **kwargs)
|
||||
|
||||
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
|
||||
# SynologyChat开关
|
||||
synologychat: Optional[bool] = False
|
||||
# VoceChat开关
|
||||
vocechat: Optional[bool] = False
|
||||
|
@ -116,3 +116,4 @@ class MessageChannel(Enum):
|
||||
Telegram = "Telegram"
|
||||
Slack = "Slack"
|
||||
SynologyChat = "SynologyChat"
|
||||
VoceChat = "VoceChat"
|
||||
|
Loading…
x
Reference in New Issue
Block a user