From 35aa8783d998c48505a554902ab021ecfc9d9178 Mon Sep 17 00:00:00 2001 From: liufei Date: Fri, 26 Apr 2024 10:52:25 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20synologychat=20=E4=BA=A4?= =?UTF-8?q?=E4=BA=92=E8=BF=9E=E6=8E=A5=E5=A4=B1=E8=B4=A5=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/conf/moduleconf.py | 53 +++++++++++ app/message/client/synologychat.py | 32 +++++-- app/message/client/webhook.py | 142 +++++++++++++++++++++++++++++ app/message/message.py | 114 +++++++++++++++++++++++ web/static/img/webhook_icon.png | Bin 0 -> 11695 bytes 5 files changed, 334 insertions(+), 7 deletions(-) create mode 100644 app/message/client/webhook.py create mode 100644 web/static/img/webhook_icon.png diff --git a/app/conf/moduleconf.py b/app/conf/moduleconf.py index 3c20803..4828678 100644 --- a/app/conf/moduleconf.py +++ b/app/conf/moduleconf.py @@ -443,6 +443,59 @@ class ModuleConf(object): } } }, + "webhook": { + "name": "Webhook", + "img_url": "../static/img/webhook_icon.png", + "config": { + "url": { + "id": "url", + "required": True, + "title": "URL", + "tooltip": "", + "type": "text", + "placeholder": "https://xxx.com/your_api/" + }, + "method": { + "id": "method", + "required": True, + "title": "HTTP方法", + "tooltip": "GET方法中请求体将被忽略,由于查询参数不支持复杂格式,发送列表类消息请使用POST", + "type": "select", + "options": { + "GET": "GET", + "POST": "POST", + "PUT": "PUT", + "PATCH": "PATCH", + "DELETE": "DELETE", + }, + "default": "POST" + }, + "query_params": { + "id": "query_params", + "required": False, + "title": "额外查询参数", + "tooltip": "JSON字符串", + "type": "text", + "placeholder": """{"search": "keyword"}""" + }, + "json_body": { + "id": "json_body", + "required": False, + "title": "额外请求体", + "tooltip": "JSON字符串,GET方法中被忽略,请勿使用title/text/image/url/user_id/medias作为key", + "type": "text", + "placeholder": """{"id": 123, "name": "abcd"}""" + }, + "token": { + "id": "token", + "required": False, + "title": "Token", + "tooltip": "会放在Header的Authorization中", + "type": "text", + "placeholder": """Authorization-Token""" + }, + } + }, }, "switch": { "download_start": { diff --git a/app/message/client/synologychat.py b/app/message/client/synologychat.py index 56d8bfe..05ade38 100644 --- a/app/message/client/synologychat.py +++ b/app/message/client/synologychat.py @@ -5,6 +5,7 @@ from app.message.client._base import _IMessageClient from app.utils import ExceptionUtils, RequestUtils, StringUtils from config import Config +import log lock = Lock() @@ -72,19 +73,29 @@ def send_msg(self, title, text="", image="", url="", user_id=""): if url and image: caption = f"{caption}\n\n<{url}|查看详情>" payload_data = {'text': quote(caption)} - if image: - payload_data['file_url'] = quote(image) + #if image: + # payload_data['file_url'] = quote(image) if user_id: payload_data['user_ids'] = [int(user_id)] + ret, msg = self.__send_request(payload_data) + if not ret: + log.error(f"【Message Err1】%s" % msg) + return ret, msg else: userids = self.__get_bot_users() if not userids: return False, "机器人没有对任何用户可见" - payload_data['user_ids'] = userids - return self.__send_request(payload_data) + for userid in userids: + payload_data['user_ids'] = [int(userid)] + ret, msg = self.__send_request(payload_data) + if not ret: + log.error(f"【Message Err1】%s" % msg) + return ret, msg + return True, "" except Exception as msg_e: ExceptionUtils.exception_traceback(msg_e) + log.error(f"【Message Err2】%x" % str(msg_e)) return False, str(msg_e) def send_list_msg(self, medias: list, user_id="", title="", **kwargs): @@ -123,10 +134,14 @@ def send_list_msg(self, medias: list, user_id="", title="", **kwargs): user_ids = self.__get_bot_users() payload_data = { "text": quote(caption), - "file_url": quote(image), - "user_ids": user_ids + #"file_url": quote(image), } - return self.__send_request(payload_data) + for userid in user_ids: + payload_data["user_ids"] = [int(userid)] + ret, msg = self.__send_request(payload_data) + if not ret: + return ret, msg + return True, "" except Exception as msg_e: ExceptionUtils.exception_traceback(msg_e) return False, str(msg_e) @@ -152,10 +167,13 @@ def __send_request(self, payload_data): 发送消息请求 """ payload = f"payload={json.dumps(payload_data)}" + #log.error(f"【Message】%s" % payload) + #log.error(f"【url】%s" % self._webhook_url) ret = self._req.post_res(url=self._webhook_url, data=payload) if ret and ret.status_code == 200: result = ret.json() if result: + # log.error(f"【ret】%s" % result) errno = result.get('error', {}).get('code') errmsg = result.get('error', {}).get('errors') if not errno: diff --git a/app/message/client/webhook.py b/app/message/client/webhook.py new file mode 100644 index 0000000..8f98f44 --- /dev/null +++ b/app/message/client/webhook.py @@ -0,0 +1,142 @@ +import json +from threading import Lock + +import requests + +from app.message.client._base import _IMessageClient +from app.utils import ExceptionUtils +from config import Config + +lock = Lock() + + +class Webhook(_IMessageClient): + schema = "webhook" + + _client_config = {} + _domain = None + _url = None + _method = None + _query_params = None + _json_body = None + _token = None + + def __init__(self, config): + self._config = Config() + self._client_config = config + self.init_config() + + @classmethod + def __parse_json(cls, json_str, attr_name): + json_str = json_str.strip() + if not json_str: + return None + try: + return json.loads(json_str) + except json.JSONDecodeError as e: + raise ValueError(f"{attr_name} Json解析失败:{json_str}") from e + + def init_config(self): + if self._client_config: + self._url = self._client_config.get("url") + self._method = self._client_config.get("method") + self._query_params = self.__parse_json(self._client_config.get("query_params"), 'query_params') + self._json_body = self.__parse_json(self._client_config.get("json_body"), 'json_body') + self._token = self._client_config.get("token") + + @classmethod + def match(cls, ctype): + return True if ctype == cls.schema else False + + def send_msg(self, title, text="", image="", url="", user_id=""): + """ + 发送web请求 + :param title: 消息标题 + :param text: 消息内容 + :param image: 消息图片地址 + :param url: 点击消息转转的URL + :param user_id: 用户ID,如有则只发消息给该用户 + :user_id: 发送消息的目标用户ID,为空则发给管理员 + """ + if not title and not text: + return False, "标题和内容不能同时为空" + if not self._url: + return False, "url参数未配置" + if not self._method: + return False, "method参数未配置" + try: + media_data = { + 'title': title, + 'text': text, + 'image': image, + 'url': url, + 'user_id': user_id + } + query_params = self._query_params.copy() if self._query_params else {} + json_body = self._json_body.copy() if self._json_body else {} + if self._method == 'GET': + query_params.update(media_data) + else: + json_body.update(media_data) + return self.__send_request(query_params, json_body) + + except Exception as msg_e: + ExceptionUtils.exception_traceback(msg_e) + return False, str(msg_e) + + def send_list_msg(self, medias: list, user_id="", title="", **kwargs): + """ + 发送列表类消息 + """ + if not title: + return False, "title为空" + if not medias or not isinstance(medias, list): + return False, "medias错误" + if not self._url: + return False, "url参数未配置" + if not self._method: + return False, "method参数未配置" + if self._method == 'GET': + return False, "GET不支持发送发送列表类消息" + try: + medias_data = [{ + 'title': media.get_title_string(), + 'url': media.get_detail_url(), + 'type': media.get_type_string(), + 'vote': media.get_vote_string() + } for media in medias] + + query_params = self._query_params.copy() if self._query_params else {} + json_body = self._json_body.copy() if self._json_body else {} + json_body.update({ + 'title': title, + 'user_id': title, + 'medias': medias_data, + }) + return self.__send_request(query_params, json_body) + except Exception as msg_e: + ExceptionUtils.exception_traceback(msg_e) + return False, str(msg_e) + + def __send_request(self, query_params, json_body): + """ + 发送消息请求 + """ + response = requests.request(self._method, + self._url, + params=query_params, + json=json_body, + headers=self.header) + if not response: + return False, "未获取到返回信息" + if 200 <= response.status_code <= 299: + return True, "" + else: + return False, f"请求失败:{response.status_code}" + + @property + def header(self): + r = {"Content-Type": "application/json"} + if self._token: + r['Authorization'] = self._token + return r diff --git a/app/message/message.py b/app/message/message.py index 4cbf42d..d4d2bb3 100644 --- a/app/message/message.py +++ b/app/message/message.py @@ -294,6 +294,8 @@ def send_transfer_movie_message(self, in_from: Enum, media_info, exist_filenum, msg_str = f"{msg_str},类别:{media_info.category}" if media_info.get_resource_type_string(): msg_str = f"{msg_str},质量:{media_info.get_resource_type_string()}" + if StringUtils.is_string_and_not_empty(media_info.get_resource_team_string()): + msg_str = f"{msg_str},发布组/字幕组:{media_info.get_resource_team_string()}" msg_str = f"{msg_str},大小:{StringUtils.str_filesize(media_info.size)},来自:{in_from.value}" if exist_filenum != 0: msg_str = f"{msg_str},{exist_filenum}个文件已存在" @@ -325,6 +327,10 @@ def send_transfer_tv_message(self, message_medias: dict, in_from: Enum): msg_str = f"类型:{item_info.type.value}" if item_info.category: msg_str = f"{msg_str},类别:{item_info.category}" + if StringUtils.is_string_and_not_empty(item_info.get_resource_type_string()): + msg_str = f"{msg_str},质量:{item_info.get_resource_type_string()}" + if StringUtils.is_string_and_not_empty(item_info.get_resource_team_string()): + msg_str = f"{msg_str},发布组/字幕组:{item_info.get_resource_team_string()}" if item_info.total_episodes == 1: msg_str = f"{msg_str},大小:{StringUtils.str_filesize(item_info.size)},来自:{in_from.value}" else: @@ -341,6 +347,114 @@ def send_transfer_tv_message(self, message_medias: dict, in_from: Enum): image=item_info.get_message_image(), url='history') + def send_simplify_transfer_movie_message(self, in_from: Enum, media_info, exist_filenum, category_flag): + """ + 发送精简转移电影的消息 + :param in_from: 转移来源 + :param media_info: 转移的媒体信息 + :param exist_filenum: 已存在的文件数 + :param category_flag: 二级分类开关 + :return: 发送状态、错误信息 + """ + msg_title = f"{media_info.get_title_string()} 已入库" + + msg_str = "" + + # 获取自定义,如:简体内嵌.v2 + if StringUtils.is_string_and_not_empty(media_info.get_customization_string()): + msg_str = f"{media_info.get_customization_string()}" + + # 获取字幕组,如:云光字幕组 + if StringUtils.is_string_and_not_empty(media_info.get_resource_team_string()): + if StringUtils.is_string_and_not_empty(msg_str): + msg_str = f"{msg_str}.{media_info.get_resource_team_string()}" + else: + msg_str = f"{media_info.get_resource_team_string()}" + + # 获取大小,如:500MB + if StringUtils.is_string_and_not_empty(msg_str): + msg_str = f"{msg_str},{StringUtils.str_filesize(media_info.size)}" + else: + msg_str = f"{StringUtils.str_filesize(media_info.size)}" + + # 获取质量,如:WEB-DL 1080p + if StringUtils.is_string_and_not_empty(media_info.get_resource_type_string()): + msg_str = f"{msg_str},{media_info.get_resource_type_string()}" + + # 获取类型,如:动漫 + if media_info.category: + if category_flag: + msg_str = f"{msg_str},类型:{media_info.category}" + + if exist_filenum != 0: + msg_str = f"{msg_str},{exist_filenum}个文件已存在" + + # 插入消息中心 + self.messagecenter.insert_system_message(title=msg_title, content=msg_str) + # 发送消息 + for client in self._active_clients: + if "transfer_finished" in client.get("switchs"): + self.__sendmsg( + client=client, + title=msg_title, + text=msg_str, + image=media_info.get_message_image(), + url='history' + ) + + def send_simplify_transfer_tv_message(self, message_medias: dict, in_from: Enum): + """ + 发送转移电视剧/动漫的消息 + """ + for item_info in message_medias.values(): + if item_info.total_episodes == 1: + msg_title = f"{item_info.get_title_string()} {item_info.get_season_episode_string()} 已入库" + else: + msg_title = f"{item_info.get_title_string()} {item_info.get_season_string()} 共{item_info.total_episodes}集 已入库" + + msg_str = "" + + # 获取自定义,如:简体内嵌.v2 + if StringUtils.is_string_and_not_empty(item_info.get_customization_string()): + msg_str = f"{item_info.get_customization_string()}" + + # 获取字幕组,如:云光字幕组 + if StringUtils.is_string_and_not_empty(item_info.get_resource_team_string()): + if StringUtils.is_string_and_not_empty(msg_str): + msg_str = f"{msg_str}.{item_info.get_resource_team_string()}" + else: + msg_str = f"{item_info.get_resource_team_string()}" + + # 获取大小,如:500MB + if StringUtils.is_string_and_not_empty(msg_str): + msg_str = f"{msg_str},{StringUtils.str_filesize(item_info.size)}" + else: + msg_str = f"{StringUtils.str_filesize(item_info.size)}" + + # 获取质量,如:WEB-DL 1080p + if StringUtils.is_string_and_not_empty(item_info.get_resource_type_string()): + msg_str = f"{msg_str},{item_info.get_resource_type_string()}" + + # 获取类型,如:动漫 + if item_info.category: + msg_str = f"{msg_str},类型:{item_info.type.value}" + + # 获取类别,如:动漫 + if item_info.category: + msg_str = f"{msg_str},{item_info.category}" + + # 插入消息中心 + self.messagecenter.insert_system_message(title=msg_title, content=msg_str) + # 发送消息 + for client in self._active_clients: + if "transfer_finished" in client.get("switchs"): + self.__sendmsg( + client=client, + title=msg_title, + text=msg_str, + image=item_info.get_message_image(), + url='history') + def send_download_fail_message(self, item, error_msg): """ 发送下载失败的消息 diff --git a/web/static/img/webhook_icon.png b/web/static/img/webhook_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c8a99dbd3194f7693c7e7cabff7535fd81ed3df8 GIT binary patch literal 11695 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 l!v8CMiT}6PQ9N}2D>ST literal 0 HcmV?d00001