Initial YakPanel commit

This commit is contained in:
Niranjan
2026-04-07 02:04:22 +05:30
commit 2826d3e7f3
5359 changed files with 1390724 additions and 0 deletions

168
mod/base/msg/__init__.py Normal file
View File

@@ -0,0 +1,168 @@
import json
import os.path
from .weixin_msg import WeiXinMsg
from .mail_msg import MailMsg
from .web_hook_msg import WebHookMsg
from .feishu_msg import FeiShuMsg
from .dingding_msg import DingDingMsg
from .sms_msg import SMSMsg
# from .wx_account_msg import WeChatAccountMsg
from .tg_msg import TgMsg
from .manager import SenderManager
from .util import read_file,write_file
from mod.base.push_mod import SenderConfig, PUSH_DATA_PATH
# 把旧地告警系统的信息通道更新
def update_mod_push_msg():
if os.path.exists(PUSH_DATA_PATH + "/update_sender.pl"):
return
# else:
# with open(PUSH_DATA_PATH + "/update_sender.pl", "w") as f:
# f.write("")
# WeChatAccountMsg.refresh_config(force=True)
sms_status = False
sc = SenderConfig()
for conf in sc.config:
if conf["sender_type"] == "sms":
sms_status = True
break
# sms 取消自动添加
# if not sms_status:
# sc.config.append({
# "id": sc.nwe_id(),
# "used": True,
# "sender_type": "sms",
# "data": {},
# "original": True # 标记这个通道是该类型 旧有的通道, 同时也是默认通道
# })
panel_data_path = "/www/server/panel/data"
# weixin
if os.path.exists(panel_data_path + "/weixin.json"):
try:
weixin_data = json.loads(read_file(panel_data_path + "/weixin.json"))
except:
weixin_data = None
if isinstance(weixin_data, dict) and "weixin_url" in weixin_data:
sc.config.append({
"id": sc.nwe_id(),
"used": True,
"sender_type": "weixin",
"data": {
"url": weixin_data["weixin_url"],
"title": "weixin" if "title" not in weixin_data else weixin_data["title"]
},
"original": True
})
# mail
stmp_file = panel_data_path + "/stmp_mail.json"
mail_list_file = panel_data_path + "/mail_list.json"
if os.path.exists(stmp_file) and os.path.exists(mail_list_file):
stmp_data = None
try:
stmp_data = json.loads(read_file(stmp_file))
mail_list_data = json.loads(read_file(mail_list_file))
except:
mail_list_data = None
if isinstance(stmp_data, dict):
if 'qq_mail' in stmp_data or 'qq_stmp_pwd' in stmp_data or 'hosts' in stmp_data:
sc.config.append({
"id": sc.nwe_id(),
"used": True,
"sender_type": "mail",
"data": {
"send": stmp_data,
"title": "mail",
"receive": [] if not mail_list_data else mail_list_data,
},
"original": True
})
# webhook
webhook_file = panel_data_path + "/hooks_msg.json"
if os.path.exists(stmp_file) and os.path.exists(mail_list_file):
try:
webhook_data = json.loads(read_file(webhook_file))
except:
webhook_data = None
if isinstance(webhook_data, list):
for i in webhook_data:
i["title"] = i["name"]
sc.config.append({
"id": sc.nwe_id(),
"used": True,
"sender_type": "webhook",
"data": i,
})
# feishu
if os.path.exists(panel_data_path + "/feishu.json"):
try:
feishu_data = json.loads(read_file(panel_data_path + "/feishu.json"))
except:
feishu_data = None
if isinstance(feishu_data, dict) and "feishu_url" in feishu_data:
sc.config.append({
"id": sc.nwe_id(),
"used": True,
"sender_type": "feishu",
"data": {
"url": feishu_data["feishu_url"],
"title": "feishu" if "title" not in feishu_data else feishu_data["title"]
},
"original": True
})
# dingding
if os.path.exists(panel_data_path + "/dingding.json"):
try:
dingding_data = json.loads(read_file(panel_data_path + "/dingding.json"))
except:
dingding_data = None
if isinstance(dingding_data, dict) and "dingding_url" in dingding_data:
sc.config.append({
"id": sc.nwe_id(),
"used": True,
"sender_type": "dingding",
"data": {
"url": dingding_data["dingding_url"],
"title": "dingding" if "title" not in dingding_data else dingding_data["title"]
},
"original": True
})
# tg
if os.path.exists(panel_data_path + "/tg_bot.json"):
try:
tg_data = json.loads(read_file(panel_data_path + "/tg_bot.json"))
except:
tg_data = None
if isinstance(tg_data, dict) and "bot_token" in tg_data:
sc.config.append({
"id": sc.nwe_id(),
"used": True,
"sender_type": "tg",
"data": {
"my_id": tg_data["my_id"],
"bot_token": tg_data["bot_token"],
"title": "tg" if "title" not in tg_data else tg_data["title"],
},
"original": True
})
sc.save_config()
write_file(PUSH_DATA_PATH + "/update_sender.pl", "")

Binary file not shown.

View File

@@ -0,0 +1,156 @@
# coding: utf-8
# +-------------------------------------------------------------------
# | yakpanel
# +-------------------------------------------------------------------
# | Copyright (c) 2015-2020 yakpanel(https://www.yakpanel.com) All rights reserved.
# +-------------------------------------------------------------------
# | Author: baozi <
# | 消息通道邮箱模块(新)
# +-------------------------------------------------------------------
import re
import json
import requests
import traceback
import socket
import requests.packages.urllib3.util.connection as urllib3_cn
from requests.packages import urllib3
from typing import Optional, Union
from .util import write_push_log, get_test_msg
import public
# 关闭警告
urllib3.disable_warnings()
class DingDingMsg:
def __init__(self, dingding_data):
self.id = dingding_data["id"]
self.config = dingding_data["data"]
def send_msg(self, msg: str, title) -> Optional[str]:
"""
钉钉发送信息
@msg 消息正文
"""
if not self.config:
return public.lang('DingTalk information is not correctly configured')
# user没有时默认为空
if "user" not in self.config:
self.config['user'] = []
if "isAtAll" not in self.config:
self.config['isAtAll'] = []
if not isinstance(self.config['url'], str):
return public.lang('The DingTalk configuration is incorrect, please reconfigure the DingTalk robot')
at_info = ''
for user in self.config['user']:
if re.match(r"^[0-9]{11}$", str(user)):
at_info += '@' + user + ' '
if at_info:
msg = msg + '\n\n>' + at_info
headers = {'Content-Type': 'application/json'}
data = {
"msgtype": "markdown",
"markdown": {
"title": "Server notifications",
"text": msg
},
"at": {
"atMobiles": self.config['user'],
"isAtAll": self.config['isAtAll']
}
}
status = False
error = None
try:
def allowed_gai_family():
family = socket.AF_INET
return family
allowed_gai_family_lib = urllib3_cn.allowed_gai_family
urllib3_cn.allowed_gai_family = allowed_gai_family
response = requests.post(
url=self.config["url"],
data=json.dumps(data),
verify=False,
headers=headers,
timeout=10
)
urllib3_cn.allowed_gai_family = allowed_gai_family_lib
if response.json()["errcode"] == 0:
status = True
except:
error = traceback.format_exc()
status = False
write_push_log("dingding", status, title)
return error
@classmethod
def check_args(cls, args: dict) -> Union[dict, str]:
if "url" not in args or "title" not in args:
return public.lang('Incomplete information')
title = args["title"]
if len(title) > 15:
return public.lang('Note names cannot be longer than 15 characters')
if "user" in args and isinstance(args["user"], list):
user = args["user"]
else:
user = []
if "atall" in args and isinstance(args["atall"], bool):
atall = args["atall"]
else:
atall = True
data = {
"url": args["url"],
"user": user,
"title": title,
"isAtAll": atall,
}
test_obj = cls({"data": data, "id": None})
test_msg = {
"msg_list": ['>configuration state: <font color=#20a53a> Success </font>\n\n']
}
test_task = get_test_msg("Message channel configuration reminders")
res = test_obj.send_msg(
test_task.to_dingding_msg(test_msg, test_task.the_push_public_data()),
"Message channel configuration reminders"
)
if res is None:
return data
return res
def test_send_msg(self) -> Optional[str]:
test_msg = {
"msg_list": ['>configuration state: <font color=#20a53a> Success </font>\n\n']
}
test_task = get_test_msg("Message channel configuration reminders")
res = self.send_msg(
test_task.to_dingding_msg(test_msg, test_task.the_push_public_data()),
"Message channel configuration reminders"
)
if res is None:
return None
return res

140
mod/base/msg/feishu_msg.py Normal file
View File

@@ -0,0 +1,140 @@
#coding: utf-8
# +-------------------------------------------------------------------
# | yakpanel
# +-------------------------------------------------------------------
# | Copyright (c) 2015-2020 yakpanel(http://www.yakpanel.com) All rights reserved.
# +-------------------------------------------------------------------
# | Author: lx
# | 消息通道飞书通知模块
# +-------------------------------------------------------------------
import re
import json
import requests
import traceback
import socket
import public
import requests.packages.urllib3.util.connection as urllib3_cn
from requests.packages import urllib3
from typing import Optional, Union
from .util import write_push_log, get_test_msg
# 关闭警告
urllib3.disable_warnings()
class FeiShuMsg:
def __init__(self, feishu_data):
self.id = feishu_data["id"]
self.config = feishu_data["data"]
@classmethod
def check_args(cls, args: dict) -> Union[dict, str]:
if "url" not in args or "title" not in args:
return public.lang('Incomplete information')
title = args["title"]
if len(title) > 15:
return public.lang('Note names cannot be longer than 15 characters')
if "user" in args and isinstance(args["user"], list):
user = args["user"]
else:
user = []
if "atall" in args and isinstance(args["atall"], bool):
atall = args["atall"]
else:
atall = True
data = {
"url": args["url"],
"user": user,
"title": title,
"isAtAll": atall,
}
test_obj = cls({"data": data, "id": None})
test_msg = {
"msg_list": ['>configuration state: Success\n\n']
}
test_task = get_test_msg("Message channel configuration reminders")
res = test_obj.send_msg(
test_task.to_feishu_msg(test_msg, test_task.the_push_public_data()),
"Message channel configuration reminders"
)
if res is None:
return data
return res
def send_msg(self, msg: str, title: str) -> Optional[str]:
"""
飞书发送信息
@msg 消息正文
"""
if not self.config:
return public.lang('Feishu information is not configured correctly.')
reg = '<font.+>(.+)</font>'
tmp = re.search(reg, msg)
if tmp:
tmp = tmp.groups()[0]
msg = re.sub(reg, tmp, msg)
if "isAtAll" not in self.config:
self.config["isAtAll"] = True
if self.config["isAtAll"]:
msg += "<at userid='all'>All</at>"
headers = {'Content-Type': 'application/json'}
data = {
"msg_type": "text",
"content": {
"text": msg
}
}
status = False
error = None
try:
def allowed_gai_family():
family = socket.AF_INET
return family
allowed_gai_family_lib = urllib3_cn.allowed_gai_family
urllib3_cn.allowed_gai_family = allowed_gai_family
rdata = requests.post(
url=self.config['url'],
data=json.dumps(data),
verify=False,
headers=headers,
timeout=10
).json()
urllib3_cn.allowed_gai_family = allowed_gai_family_lib
if "StatusCode" in rdata and rdata["StatusCode"] == 0:
status = True
except:
error = traceback.format_exc()
write_push_log("feishu", status, title)
return error
def test_send_msg(self) -> Optional[str]:
test_msg = {
"msg_list": ['>configuration state: <font color=#20a53a> Success </font>\n\n']
}
test_task = get_test_msg("Message channel configuration reminders")
res = self.send_msg(
test_task.to_feishu_msg(test_msg, test_task.the_push_public_data()),
"Message channel configuration reminders"
)
if res is None:
return None
return res

158
mod/base/msg/mail_msg.py Normal file
View File

@@ -0,0 +1,158 @@
#coding: utf-8
# +-------------------------------------------------------------------
# | yakpanel
# +-------------------------------------------------------------------
# | Copyright (c) 2015-2020 yakpanel(http://www.yakpanel.com) All rights reserved.
# +-------------------------------------------------------------------
# | Author: 沐落 <cjx@yakpanel.com>
# | Author: lx
# | 消息通道邮箱模块
# +-------------------------------------------------------------------
import smtplib
import traceback
from email.mime.text import MIMEText
from email.utils import formataddr
from typing import Tuple, Union, Optional
import public
from mod.base.msg.util import write_push_log, write_mail_push_log, get_test_msg
class MailMsg:
def __init__(self, mail_data):
self.id = mail_data["id"]
self.config = mail_data["data"]
@classmethod
def check_args(cls, args: dict) -> Tuple[bool, Union[dict, str]]:
if "send" not in args or "receive" not in args or len(args["receive"]) < 1:
return False, "Incomplete information, there must be a sender and at least one receiver"
if "title" not in args:
return False, "There is no necessary remark information"
title = args["title"]
if len(title) > 15:
return False, 'Note names cannot be longer than 15 characters'
send_data = args["send"]
send = {}
for i in ("qq_mail", "qq_stmp_pwd", "hosts", "port"):
if i not in send_data:
return False, "The sender configuration information is incomplete"
send[i] = send_data[i].strip()
receive_data = args["receive"]
if isinstance(receive_data, str):
receive_list = [i.strip() for i in receive_data.split("\n") if i.strip()]
else:
receive_list = [i.strip() for i in receive_data if i.strip()]
data = {
"send": send,
"title": title,
"receive": receive_list,
}
test_obj = cls({"data": data, "id": None})
test_msg = {
"msg_list": ['>configuration state: Success<br>']
}
test_task = get_test_msg("Message channel configuration reminders")
res = test_obj.send_msg(
test_task.to_mail_msg(test_msg, test_task.the_push_public_data()),
"Message channel configuration reminders"
)
if res is None or res.find("Failed to send mail to some recipients") != -1:
return True, data
return False, res
def send_msg(self, msg: str, title: str):
"""
邮箱发送
@msg 消息正文
@title 消息标题
"""
if not self.config:
return public.lang('Mailbox information is not configured correctly')
if 'port' not in self.config['send']:
self.config['send']['port'] = 465
receive_list = self.config['receive']
error_list, success_list = [], []
error_msg_dict = {}
for email in receive_list:
if not email.strip():
continue
server = None
try:
data = MIMEText(msg, 'html', 'utf-8')
data['From'] = formataddr((self.config['send']['qq_mail'], self.config['send']['qq_mail']))
data['To'] = formataddr((self.config['send']['qq_mail'], email.strip()))
data['Subject'] = title
port = int(self.config['send']['port'])
host = str(self.config['send']['hosts'])
user = self.config['send']['qq_mail']
pwd = self.config['send']['qq_stmp_pwd']
if port == 465:
# SSL direct connection
server = smtplib.SMTP_SSL(host, port, timeout=10)
else:
# Standard connection, possibly with STARTTLS
server = smtplib.SMTP(host, port, timeout=10)
try:
# Attempt to upgrade to a secure connection
server.starttls()
except smtplib.SMTPNotSupportedError:
# The server does not support STARTTLS, proceed with an insecure connection
pass
server.login(user, pwd)
server.sendmail(user, [email.strip(), ], data.as_string())
success_list.append(email)
except:
err_msg = traceback.format_exc()
# public.print_log(f"邮件发送失败: {err_msg}")
error_list.append(email)
error_msg_dict[email] = err_msg
finally:
if server:
server.quit()
if not error_list and not success_list:
return public.lang('The receiving mailbox is not configured')
if not error_list:
write_push_log("mail", True, title, success_list)
return None
if not success_list:
write_push_log("mail", False, title, error_list)
first_error_msg = list(error_msg_dict.values())[0]
# 修复 IndexError
return public.lang('Failed to send message, Recipient of failed to send:{}, Error: {}', error_list, first_error_msg)
write_mail_push_log(title, error_list, success_list)
return public.lang('Failed to send mail to some recipients, including:{}',error_list)
def test_send_msg(self) -> Optional[str]:
test_msg = {
"msg_list": ['>configuration state: <font color=#20a53a> Success </font>\n\n']
}
test_task = get_test_msg("Message channel configuration reminders")
res = self.send_msg(
test_task.to_mail_msg(test_msg, test_task.the_push_public_data()),
"Message channel configuration reminders"
)
if res is None:
return None
return res

362
mod/base/msg/manager.py Normal file
View File

@@ -0,0 +1,362 @@
import time
import traceback
from mod.base.push_mod import SenderConfig
from .weixin_msg import WeiXinMsg
from .mail_msg import MailMsg
from .tg_msg import TgMsg
from .web_hook_msg import WebHookMsg
from .feishu_msg import FeiShuMsg
from .dingding_msg import DingDingMsg
from .sms_msg import SMSMsg
# from .wx_account_msg import WeChatAccountMsg
import json
from mod.base import json_response
from .util import write_file, read_file
import sys,os
sys.path.insert(0, "/www/server/panel/class/")
import public
# 短信会自动添加到 sender 库中的第一个 且通过官方接口更新
# 微信公众号信息通过官网接口更新, 不写入数据库,需要时由文件中读取并序列化
# 其他告警通道本质都类似于web hook 在确认完数据信息无误后,都可以自行添加或启用
class SenderManager:
def __init__(self):
self.custom_parameter_filename = "/www/server/panel/data/mod_push_data/custom_parameter.pl"
self.init_default_sender()
def set_sender_conf(self, get):
args = json.loads(get.sender_data.strip())
try:
sender_id = None
try:
if hasattr(get, "sender_id"):
sender_id = get.sender_id.strip()
if not sender_id:
sender_id = None
sender_type = get.sender_type.strip()
args = json.loads(get.sender_data.strip())
except (json.JSONDecoder, AttributeError, TypeError):
return json_response(status=False, msg=public.lang('The parameter is incorrect'))
sender_config = SenderConfig()
if sender_id is not None:
tmp = sender_config.get_by_id(sender_id)
if tmp is None:
sender_id = None
if sender_type == "weixin":
data = WeiXinMsg.check_args(args)
if isinstance(data, str):
return json_response(status=False, data=data, msg=public.lang('Test send failed'))
elif sender_type == "mail":
_, data = MailMsg.check_args(args)
if isinstance(data, str):
return json_response(status=False, data=data, msg=public.lang('Test send failed'))
elif sender_type == "tg":
_, data = TgMsg.check_args(args)
if isinstance(data, str):
return json_response(status=False, data=data, msg=data)
elif sender_type == "webhook":
custom_parameter = args.get("custom_parameter", {})
if custom_parameter:
try:
public.writeFile(self.custom_parameter_filename, json.dumps(custom_parameter))
except:
pass
data = WebHookMsg.check_args(args)
if isinstance(data, str):
return json_response(status=False, data=data, msg=public.lang('Test send failed'))
# 从文件读取并删除文件
try:
if os.path.exists(self.custom_parameter_filename):
custom_parameter = json.loads(public.readFile(self.custom_parameter_filename))
data['custom_parameter'] = custom_parameter
os.remove(self.custom_parameter_filename)
except:
pass
elif sender_type == "feishu":
data = FeiShuMsg.check_args(args)
if isinstance(data, str):
return json_response(status=False, data=data, msg=public.lang('Test send failed'))
elif sender_type == "dingding":
data = DingDingMsg.check_args(args)
if isinstance(data, str):
return json_response(status=False, data=data, msg=public.lang('Test send failed'))
else:
return json_response(status=False, msg=public.lang('A type that is not supported by the current interface'))
# Check if the sender configuration already exists
existing_sender = any(
conf for conf in sender_config.config
if conf['sender_type'] == sender_type and 'title' in conf['data'] and conf['data']['title'] == data['title'] and conf['id'] != sender_id
)
# for conf in sender_config.config:
# if conf['sender_type'] == sender_type and 'title' in conf['data'] and conf['data']['title'] == data[
# 'title'] and conf['id'] != sender_id:
# public.print_log('000 -{}'.format(conf['sender_type']))
# public.print_log('000 -{}'.format(sender_type))
#
# public.print_log('111 conf -{}'.format(conf['sender_type']))
# public.print_log('111 -{}'.format(sender_type))
#
# public.print_log('222 conf -{}'.format(conf['data']['title']))
# public.print_log('222 data -{}'.format(data['title']))
#
# public.print_log('333 conf -{}'.format(conf['id']))
# public.print_log('333 -{}'.format(sender_id))
if existing_sender:
return json_response(status=False, msg=public.lang('The same send configuration already exists and cannot be added repeatedly'))
now_sender_id = None
if not sender_id:
now_sender_id = sender_config.nwe_id()
sender_config.config.append(
{
"id": now_sender_id,
"sender_type": sender_type,
"data": data,
"used": True,
})
else:
now_sender_id = sender_id
tmp = sender_config.get_by_id(sender_id)
tmp["data"].update(data)
# type_senders = [conf for conf in sender_config.config if conf['sender_type'] == sender_type]
# if len(type_senders) == 1:
# for conf in sender_config.config:
# conf["original"] = (conf['id'] == now_sender_id)
sender_config.save_config()
if sender_type == "webhook":
self.set_default_for_compatible(sender_config.get_by_id(now_sender_id))
return json_response(status=True, msg=public.lang('Saved successfully'))
except:
public.print_log('Error:{}'.format(str(public.get_error_info())))
@staticmethod
def change_sendr_used(get):
try:
sender_id = get.sender_id.strip()
except (AttributeError, TypeError):
return json_response(status=False, msg=public.lang('The parameter is incorrect'))
sender_config = SenderConfig()
tmp = sender_config.get_by_id(sender_id)
if tmp is None:
return json_response(status=False, msg=public.lang('Corresponding sender not found'))
tmp["used"] = not tmp["used"]
sender_config.save_config()
return json_response(status=True, msg=public.lang('Saved successfully'))
@staticmethod
def remove_sender(get):
try:
sender_id = get.sender_id.strip()
except (AttributeError, TypeError):
return json_response(status=False, msg=public.lang('The parameter is incorrect'))
sender_config = SenderConfig()
tmp = sender_config.get_by_id(sender_id)
if tmp is None:
return json_response(status=False, msg=public.lang('Corresponding sender not found'))
sender_config.config.remove(tmp)
sender_config.save_config()
return json_response(status=True, msg=public.lang('Successfully delete'))
@staticmethod
def get_sender_list(get):
# 微信, 飞书, 钉钉, web-hook 邮箱
refresh = False
try:
if hasattr(get, 'refresh'):
refresh = get.refresh.strip()
if refresh in ("1", "true"):
refresh = True
except (AttributeError, TypeError):
return json_response(status=False, msg=public.lang('The parameter is incorrect'))
res = []
# WeChatAccountMsg.refresh_config(force=refresh)
simple = ("weixin", "mail", "webhook", "feishu", "dingding", "tg")
for conf in SenderConfig().config:
if conf["sender_type"] in simple or conf["sender_type"] == "wx_account":
res.append(conf)
# 去掉短信设置
# elif conf["sender_type"] == "sms":
# conf["data"] = SMSMsg(conf).refresh_config(force=refresh)
# res.append(conf)
res.sort(key=lambda x: x["sender_type"])
return json_response(status=True, data=res)
@staticmethod
def test_send_msg(get):
try:
sender_id = get.sender_id.strip()
except (json.JSONDecoder, AttributeError, TypeError):
return json_response(status=False, msg=public.lang('The parameter is incorrect'))
sender_config = SenderConfig()
tmp = sender_config.get_by_id(sender_id)
if tmp is None:
return json_response(status=False, msg=public.lang('Corresponding sender not found'))
sender_type = tmp["sender_type"]
if sender_type == "weixin":
sender_obj = WeiXinMsg(tmp)
elif sender_type == "mail":
sender_obj = MailMsg(tmp)
elif sender_type == "webhook":
sender_obj = WebHookMsg(tmp)
elif sender_type == "feishu":
sender_obj = FeiShuMsg(tmp)
elif sender_type == "dingding":
sender_obj = DingDingMsg(tmp)
elif sender_type == "tg":
sender_obj = TgMsg(tmp)
# elif sender_type == "wx_account":
# sender_obj = WeChatAccountMsg(tmp)
else:
return json_response(status=False, msg=public.lang('A type that is not supported by the current interface'))
res = sender_obj.test_send_msg()
if isinstance(res, str):
return json_response(status=False, data=res, msg=public.lang('Test send failed'))
return json_response(status=True, msg=public.lang('The sending was successful'))
@staticmethod
def set_default_for_compatible(sender_data: dict):
if sender_data["sender_type"] in ("sms", "wx_account"):
return
panel_data = "/www/server/panel/data"
if sender_data["sender_type"] == "weixin":
weixin_file = "{}/weixin.json".format(panel_data)
write_file(weixin_file, json.dumps({
"state": 1,
"weixin_url": sender_data["data"]["url"],
"title": sender_data["data"]["title"],
"list": {
"default": {
"data": sender_data["data"]["url"],
"title": sender_data["data"]["title"],
"status": 1,
"addtime": int(time.time())
}
}
}))
elif sender_data["sender_type"] == "mail":
stmp_mail_file = "{}/stmp_mail.json".format(panel_data)
mail_list_file = "{}/mail_list.json".format(panel_data)
write_file(stmp_mail_file, json.dumps(sender_data["data"]["send"]))
write_file(mail_list_file, json.dumps(sender_data["data"]["receive"]))
elif sender_data["sender_type"] == "feishu":
feishu_file = "{}/feishu.json".format(panel_data)
write_file(feishu_file, json.dumps({
"feishu_url": sender_data["data"]["url"],
"title": sender_data["data"]["title"],
"isAtAll": True,
"user": []
}))
elif sender_data["sender_type"] == "dingding":
dingding_file = "{}/dingding.json".format(panel_data)
write_file(dingding_file, json.dumps({
"dingding_url": sender_data["data"]["url"],
"title": sender_data["data"]["title"],
"isAtAll": True,
"user": []
}))
elif sender_data["sender_type"] == "tg":
tg_file = "{}/tg_bot.json".format(panel_data)
write_file(tg_file, json.dumps({
"my_id": sender_data["data"]["my_id"],
"bot_token": sender_data["data"]["bot_token"],
"title": sender_data["data"]["title"]
}))
elif sender_data["sender_type"] == "webhook":
webhook_file = "{}/hooks_msg.json".format(panel_data)
try:
webhook_data = json.loads(read_file(webhook_file))
except:
webhook_data =[]
target_idx = -1
for idx, i in enumerate(webhook_data):
if i["name"] == sender_data["data"]["title"]:
target_idx = idx
break
else:
sender_data["data"]["name"] = sender_data["data"]["title"]
webhook_data.append(sender_data["data"])
if target_idx != -1:
sender_data["data"]["name"] = sender_data["data"]["title"]
webhook_data[target_idx] = sender_data["data"]
write_file(webhook_file, json.dumps(webhook_data))
def init_default_sender(self):
import os,sys
sys.path.insert(0, "/www/server/panel/mod/project/push")
import msgconfMod
sender_config = SenderConfig()
sender_types = set(conf['sender_type'] for conf in sender_config.config)
all_types = {"feishu", "dingding", "weixin", "mail", "webhook"} # 所有可能的类型
for sender_type in sender_types:
type_senders = [conf for conf in sender_config.config if conf['sender_type'] == sender_type]
# 检查是否已有默认通道
has_default = any(conf.get('original', False) for conf in type_senders)
if has_default:
continue
if len(type_senders) == 1:
# 只有一个通道,设置为默认通道
for conf in type_senders:
get = public.dict_obj()
get['sender_id'] = conf['id']
get['sender_type'] = conf['sender_type']
self.set_default_sender(get)
else:
# 有多个通道,根据添加时间设置默认通道
sorted_senders = sorted(type_senders, key=lambda x: x['data'].get('create_time', ''))
if sorted_senders:
get = public.dict_obj()
get['sender_id'] = sorted_senders[0]['id']
get['sender_type'] = sorted_senders[0]['sender_type']
self.set_default_sender(get)
# 检查没有通道的类型,并删除对应文件
missing_types = all_types - sender_types
for missing_type in missing_types:
file_path = f"/www/server/panel/data/{missing_type}.json"
if os.path.exists(file_path):
os.remove(file_path)

123
mod/base/msg/sms_msg.py Normal file
View File

@@ -0,0 +1,123 @@
# coding: utf-8
# +-------------------------------------------------------------------
# | yakpanel
# +-------------------------------------------------------------------
# | Copyright (c) 2015-2020 yakpanel(http://www.yakpanel.com) All rights reserved.
# +-------------------------------------------------------------------
# | Author: baozi
# | 消息通道 短信模块(新)
# +-------------------------------------------------------------------
import json
import os
import time
import traceback
from typing import Union, Optional
from mod.base.push_mod import SenderConfig
from .util import write_push_log, PANEL_PATH, write_file, read_file, public_http_post
class SMSMsg:
API_URL = 'https://www.yakpanel.com/api/wmsg'
USER_PATH = '{}/data/userInfo.json'.format(PANEL_PATH)
# 构造方法
def __init__(self, msm_data: dict):
self.id = msm_data["id"]
self.data = msm_data["data"]
self.user_info = None
try:
self.user_info = json.loads(read_file(self.USER_PATH))
except:
self.user_info = None
self._PDATA = {
"access_key": "" if self.user_info is None else 'B' * 32,
"data": {}
}
def refresh_config(self, force=False):
if "last_refresh_time" not in self.data:
self.data["last_refresh_time"] = 0
if self.data.get("last_refresh_time") + 60 * 60 * 24 < time.time() or force: # 一天最多更新一次
result = self._request('get_user_sms')
if not isinstance(result, dict) or ("status" in result and not result["status"]):
return {
"count": 0,
"total": 0
}
sc = SenderConfig()
tmp = sc.get_by_id(self.id)
if tmp is not None:
result["last_refresh_time"] = time.time()
tmp["data"] = result
sc.save_config()
else:
result = self.data
return result
def send_msg(self, sm_type: str, sm_args: dict):
"""
@发送短信
@sm_type 预警类型, ssl_end|YakPanel SSL到期提醒
@sm_args 预警参数
"""
if not self.user_info:
return "未成功绑定官网账号,无法发送信息,请尝试重新绑定"
tmp = sm_type.split('|')
if "|" in sm_type and len(tmp) >= 2:
s_type = tmp[0]
title = tmp[1]
else:
s_type = sm_type
title = 'YakPanel 告警提醒'
sm_args = self.canonical_data(sm_args)
self._PDATA['data']['sm_type'] = s_type
self._PDATA['data']['sm_args'] = sm_args
print(s_type)
print(sm_args)
result = self._request('send_msg')
u_key = '{}****{}'.format(self.user_info['username'][:3], self.user_info['username'][-3:])
print(result)
if isinstance(result, str):
write_push_log("短信", False, title, [u_key])
return result
if result['status']:
write_push_log("短信", True, title, [u_key])
return None
else:
write_push_log("短信", False, title, [u_key])
return result.get("msg", "发送错误")
@staticmethod
def canonical_data(args):
"""规范数据内容"""
if not isinstance(args, dict):
return args
new_args = {}
for param, value in args.items():
if type(value) != str:
new_str = str(value)
else:
new_str = value.replace(".", "_").replace("+", "")
new_args[param] = new_str
return new_args
def push_data(self, data):
return self.send_msg(data['sm_type'], data['sm_args'])
# 发送请求
def _request(self, d_name: str) -> Union[dict, str]:
pdata = {
'access_key': self._PDATA['access_key'],
'data': json.dumps(self._PDATA['data'])
}
try:
import public
api_root = public.GetConfigValue('home').rstrip('/') + '/api/wmsg'
result = public_http_post(api_root + '/' + d_name, pdata)
result = json.loads(result)
return result
except Exception:
return traceback.format_exc()

105
mod/base/msg/test.json Normal file
View File

@@ -0,0 +1,105 @@
[
{
"id": "f4e98e478b85e876",
"used": true,
"sender_type": "sms",
"data": {}
},
{
"id": "fb3e9e409b9d7c27",
"sender_type": "mail",
"data": {
"send": {
"qq_mail": "1191604998@qq.com",
"qq_stmp_pwd": "alvonbfcwhlahbcg",
"hosts": "smtp.qq.com",
"port": "465"
},
"title": "test_mail",
"receive": [
"1191604998@qq.com",
"225326944@qq.com"
]
},
"used": true
},
{
"id": "79900d4fb37fa83d",
"sender_type": "feishu",
"data": {
"url": "https://open.feishu.cn/open-apis/bot/v2/hook/ba6a3f77-0349-4492-a8ad-0b4c99435bf1",
"user": [],
"title": "test_feishu",
"isAtAll": true
},
"used": true
},
{
"id": "8f70de4baa89133e",
"sender_type": "webhook",
"data": {
"title": "webhook",
"url": "http://192.168.69.172:11211",
"query": {},
"headers": {},
"body_type": "json",
"custom_parameter": {},
"method": "POST",
"ssl_verify": null,
"status": true
},
"used": true
},
{
"id": "10bcf5439299d9dd",
"used": true,
"sender_type": "wx_account",
"data": {
"id": "jsbRCBBinMmFjYjczNTQyYmUzxNDiWQw",
"uid": 1228262,
"is_subscribe": 1,
"head_img": "https://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83epBUaqBcCkkxtKwuaOHLy1qjGeDvmf1hZsrkFGNrldyRgSuA3sYB1xlgKv1Z98PUciaxju71PUKchA/132",
"nickname": "沈涛",
"status": 1,
"create_time": "2023-12-27 11:30:15",
"update_time": "2023-12-27 11:30:15",
"remaining": 98,
"title": "沈涛"
}
},
{
"id": "2c7c094eb23ddaae",
"used": true,
"sender_type": "webhook",
"data": {
"url": "http://192.168.69.159:8888/hook?access_key=IUSEViIMMhQio1WyP0ztCyoa8sIBjaWulihhcJX4rRJ4sW79",
"query": {},
"headers": {},
"body_type": "json",
"custom_parameter": {},
"method": "GET",
"ssl_verify": 1,
"status": true,
"name": "aaa",
"title": "aaa"
}
},
{
"id": "63c30845916fa722",
"used": true,
"sender_type": "feishu",
"data": {
"url": "https://open.feishu.cn/open-apis/bot/v2/hook/c6906d9f-01c5-4a74-80bd-3ccda33bf4ec",
"title": "amber"
}
},
{
"id": "6ccf834a95010bed",
"used": true,
"sender_type": "dingding",
"data": {
"url": "https://oapi.dingtalk.com/robot/send?access_token=00732dec605edc1c07f441eb9d470c8bdfa301c4ce89959916fe535d08c09043",
"title": "dd"
}
}
]

252
mod/base/msg/tg_msg.py Normal file
View File

@@ -0,0 +1,252 @@
# coding: utf-8
# +-------------------------------------------------------------------
# | YakPanel
# +-------------------------------------------------------------------
# | Copyright (c) 2015-2020 YakPanel(www.yakpanel.com) All rights reserved.
# +-------------------------------------------------------------------
# | Author: jose <zhw@yakpanel.com>
# | 消息通道电报模块
# +-------------------------------------------------------------------
import sys, os, re, public, json, requests
try:
import telegram
except:
public.ExecShell("btpip install -I python-telegram-bot")
import telegram
panelPath = "/www/server/panel"
os.chdir(panelPath)
sys.path.insert(0, panelPath + "/class/")
from requests.packages import urllib3
# 关闭警告
urllib3.disable_warnings()
from typing import Union, Optional
from mod.base.msg.util import write_push_log, get_test_msg
class TgMsg:
conf_path = "{}/data/tg_bot.json".format(panelPath)
__tg_info = None
__module_name = None
__default_pl = "{}/data/default_msg_channel.pl".format(panelPath)
def __init__(self, conf):
self.conf = conf
self.bot_token = self.conf['data']['bot_token']
self.my_id = self.conf['data']['my_id']
def get_version_info(self, get):
"""
获取版本信息
"""
data = {}
data['ps'] = 'Use telegram bots to send receive panel notifications'
data['version'] = '1.0'
data['date'] = '2022-08-10'
data['author'] = 'YakPanel'
data['title'] = 'Telegram'
data['help'] = 'http://www.yakpanel.com'
return data
def get_config(self, get):
"""
获取tg配置
"""
data = {}
if self.__tg_info:
data = self.__tg_info
data['default'] = self.__get_default_channel()
return data
def set_config(self, get):
"""
设置tg bot
@my_id tg id
@bot_token 机器人token
"""
if not hasattr(get, 'my_id') or not hasattr(get, 'bot_token'):
return public.returnMsg(False, public.lang("Please fill in the complete information"))
title = 'Default'
if hasattr(get, 'title'):
title = get.title
if len(title) > 7:
return public.returnMsg(False, public.lang("Note name cannot exceed 7 characters"))
self.__tg_info = {"my_id": get.my_id.strip(), "bot_token": get.bot_token, "title": title}
try:
info = public.get_push_info('Notification Configuration Reminder',
['>Configuration status<font color=#20a53a>successfully</font>\n\n'])
ret = self.send_msg(info['msg'], get.my_id.strip(), get.bot_token)
except:
ret = self.send_msg('YakPanel alarm test', get.my_id.strip(), get.bot_token)
if ret:
if 'default' in get and get['default']:
public.writeFile(self.__default_pl, self.__module_name)
public.writeFile(self.conf_path, json.dumps(self.__tg_info))
return public.returnMsg(True, public.lang("successfully set"))
else:
return ret
def get_send_msg(self, msg):
"""
@name 处理md格式
"""
try:
title = 'YakPanel notifications'
if msg.find("####") >= 0:
try:
title = re.search(r"####(.+)", msg).groups()[0]
except:
pass
else:
info = public.get_push_info('Notification Configuration Reminder', ['>Send Content: ' + msg])
msg = info['msg']
except:
pass
return msg, title
async def send_msg_async(self, bot_token, chat_id, msg):
"""
tg发送信息
@msg 消息正文
"""
bot = telegram.Bot(token=bot_token)
await bot.send_message(chat_id=chat_id, text=msg, parse_mode='MarkdownV2')
# 外部也调用
def send_msg(self, msg, title):
"""
tg发送信息
@msg 消息正文
"""
bot_token = self.bot_token
chat_id = self.my_id
msg = msg.strip()
msg = self.escape_markdown_v2(msg)
import asyncio
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(self.send_msg_async(bot_token, chat_id, msg))
write_push_log("Telegram", True, title)
public.print_log('message sent successfully!')
loop.close()
return None
except Exception as e:
public.print_log('tg sent error:{}'.format(str(public.get_error_info())))
write_push_log("Telegram", False, title)
return public.lang("Telegram Failed to send {}",e)
def escape_markdown_v2(self, text):
"""
Escape special characters for Telegram's MarkdownV2 mode.
"""
# 所有需要转义的 MarkdownV2 字符
escape_chars = r'\_*[]()~`>#+-=|{}.!'
for ch in escape_chars:
text = text.replace(ch, '\\' + ch)
return text
@classmethod
def check_args(cls, args: dict) -> Union[dict, str]:
my_id = args.get('my_id', None).strip()
bot_token = args.get('bot_token', None)
if not my_id or not bot_token:
return public.lang('Incomplete information')
title = args.get('title', 'Default')
if len(title) > 15:
return public.lang('Note name cannot exceed 15 characters')
data = {
"my_id": my_id,
"bot_token": bot_token,
"title": title
}
conf = {
"data": data
}
# 调用TgMsg的方法
tg = TgMsg(conf)
try:
test_msg = {
"msg_list": ['>configuration state: <font color=#20a53a> Success </font>\n\n']
}
test_task = get_test_msg("Message channel configuration reminders")
ret = tg.send_msg(
test_task.to_tg_msg(test_msg, test_task.the_push_public_data()),
"Message channel configuration reminders"
)
except:
ret = tg.send_msg('YakPanel alarm test', "Message channel configuration reminders")
# 测试失败也添加
if ret:
return False, ret
else:
return True, data
def test_send_msg(self) -> Optional[str]:
test_msg = {
"msg_list": ['>configuration state: <font color=#20a53a> Success </font>\n\n']
}
test_task = get_test_msg("Message channel configuration reminders")
res = self.send_msg(
test_task.to_tg_msg(test_msg, test_task.the_push_public_data()),
"Message channel configuration reminders"
)
if res is None:
return None
return res
def push_data(self, data):
"""
@name 统一发送接口
@data 消息内容
{"module":"mail","title":"标题","msg":"内容","to_email":"xx@qq.com","sm_type":"","sm_args":{}}
"""
return self.send_msg(data['msg'])
def __get_default_channel(self):
"""
@获取默认消息通道
"""
try:
if public.readFile(self.__default_pl) == self.__module_name:
return True
except:
pass
return False
def uninstall(self):
if os.path.exists(self.conf_path):
os.remove(self.conf_path)

139
mod/base/msg/util.py Normal file
View File

@@ -0,0 +1,139 @@
import sys
from typing import Optional, List, Tuple
from mod.base.push_mod import BaseTask, WxAccountMsgBase, WxAccountMsg, get_push_public_data
if "/www/server/panel/class" not in sys.path:
sys.path.insert(0, "/www/server/panel/class")
import public
PANEL_PATH = "/www/server/panel"
public_http_post = public.httpPost
def write_push_log(
module_name: str,
status: bool,
title: str,
user: Optional[List[str]] = None):
"""
记录 告警推送情况
@param module_name: 通道方式
@param status: 是否成功
@param title: 标题
@param user: 推送到的用户,可以为空,如:钉钉 不需要
@return:
"""
if status:
status_str = '<span style="color:#20a53a;"> Success </span>'
else:
status_str = '<span style="color:red;">Fail</span>'
if not user:
user_str = '[ default ]'
else:
user_str = '[ {} ]'.format(",".join(user))
log = 'Title: [{}], Notification: [{}], Result: [{}], Addressee: {}'.format(title, module_name, status_str, user_str)
public.WriteLog('Alarm notification', log)
return True
def write_mail_push_log(
title: str,
error_user: List[str],
success_user: List[str],
):
"""
记录 告警推送情况
@param title: 标题
@param error_user: 失败的用户
@param success_user: 成功的用户
@return:
"""
e_fmt = '<span style="color:#20a53a;">{}</span>'
s_fmt = '<span style="color:red;">{}</span>'
error_user_msg = ",".join([e_fmt.format(i) for i in error_user])
success_user = ",".join([s_fmt.format(i) for i in success_user])
log = 'Title: [{}], notification method: [Email], send failed recipients: {}, send successful recipients: {}'.format(
title, error_user_msg, success_user
)
public.WriteLog('Alarm notification', log)
return True
def write_file(filename: str, s_body: str, mode='w+') -> bool:
"""
写入文件内容
@filename 文件名
@s_body 欲写入的内容
return bool 若文件不存在则尝试自动创建
"""
try:
fp = open(filename, mode=mode)
fp.write(s_body)
fp.close()
return True
except:
try:
fp = open(filename, mode=mode, encoding="utf-8")
fp.write(s_body)
fp.close()
return True
except:
return False
def read_file(filename, mode='r') -> Optional[str]:
"""
读取文件内容
@filename 文件名
return string(bin) 若文件不存在则返回None
"""
import os
if not os.path.exists(filename):
return None
fp = None
try:
fp = open(filename, mode=mode)
f_body = fp.read()
except:
return None
finally:
if fp and not fp.closed:
fp.close()
return f_body
class _TestMsgTask(BaseTask):
"""
用来测试的短息
"""
@staticmethod
def the_push_public_data():
return get_push_public_data()
def get_keywords(self, task_data: dict) -> str:
pass
def to_sms_msg(self, push_data: dict, push_public_data: dict) -> Tuple[str, dict]:
raise NotImplementedError()
def to_wx_account_msg(self, push_data: dict, push_public_data: dict) -> WxAccountMsg:
msg = WxAccountMsg.new_msg()
msg.thing_type = self.title
msg.msg = "The message channel was configured successfully"
return msg
def get_test_msg(title: str, task_name="Message channel configuration reminders") -> _TestMsgTask:
"""
用来测试的短息
"""
t = _TestMsgTask()
t.title = title
t.template_name = task_name
return t

View File

@@ -0,0 +1,227 @@
# coding: utf-8
# +-------------------------------------------------------------------
# | yakpanel
# +-------------------------------------------------------------------
# | Copyright (c) 2015-2020 yakpanel(http://www.yakpanel.com) All rights reserved.
# +-------------------------------------------------------------------
# | Author: baozi <baozi@yakpanel.com>
# | 消息通道HOOK模块
# +-------------------------------------------------------------------
import requests
from typing import Optional, Union
from urllib3.util import parse_url
from .util import write_push_log, get_test_msg
import json
import public
# config = {
# "name": "default",
# "url": "https://www.yakpanel.com",
# "query": {
# "aaa": "111"
# },
# "header": {
# "AAA": "BBBB",
# },
# "body_type": ["json", "form_data", "null"],
# "custom_parameter": {
# "rrr": "qqqq"
# },
# "method": ["GET", "POST", "PUT", "PATCH"],
# "ssl_verify": [True, False]
# }
# #
# # 1.自动解析Query参数拼接并展示给用户 # 可不做
# # 2.自定义Header头 # 必做
# # 3.Body中的内容是: type:str="首页磁盘告警", time:int=168955427, data:str="xxxxxx" #
# # 4.自定义参数: key=value 添加在Body中 # 可不做
# # 5.请求类型自定义 # 必做
# # 以上内容需要让用户可测试--!
class WebHookMsg(object):
DEFAULT_HEADERS = {
"User-Agent": "Yak-Panel",
}
def __init__(self, hook_data: dict):
self.id = hook_data["id"]
self.config = hook_data["data"]
def _replace_and_parse(self, value, real_data):
"""替换占位符并递归解析JSON字符串"""
if isinstance(value, str):
value = value.replace("$1", json.dumps(real_data, ensure_ascii=False))
elif isinstance(value, dict):
for k, v in value.items():
value[k] = self._replace_and_parse(v, real_data)
return value
def send_msg(self, msg: str, title:str, push_type:str) -> Optional[str]:
the_url = parse_url(self.config['url'])
ssl_verify = self.config.get("ssl_verify", None)
if ssl_verify is None:
ssl_verify = the_url.scheme == "https"
else:
ssl_verify = bool(int(ssl_verify)) # 转换为布尔值
real_data = {
"title": title,
"msg": msg,
"type": push_type,
}
custom_parameter = self.config.get("custom_parameter", {})
if not isinstance(custom_parameter, dict):
custom_parameter = {} # 如果 custom_parameter 不是字典,则设置为空字典
# 处理custom_parameter将$1替换为real_data内容并递归解析
custom_data = {}
for k, v in custom_parameter.items():
custom_data[k] = self._replace_and_parse(v, real_data)
if custom_data:
real_data = custom_data
data = None
json_data = None
headers = self.DEFAULT_HEADERS.copy()
if self.config["body_type"] == "json":
json_data = real_data
elif self.config["body_type"] == "form_data":
data = real_data
for k, v in self.config.get("headers", {}).items():
if not isinstance(v, str):
v = str(v)
headers[k] = v
status = False
error = None
timeout = 10
if data:
for k, v in data.items():
if isinstance(v, str):
continue
else:
data[k]=json.dumps(v)
for i in range(3):
try:
if json_data is not None:
res = requests.request(
method=self.config["method"],
url=str(the_url),
json=json_data,
headers=headers,
timeout=timeout,
verify=ssl_verify,
)
else:
res = requests.request(
method=self.config["method"],
url=str(the_url),
data=data,
headers=headers,
timeout=timeout,
verify=ssl_verify,
)
if res.status_code == 200:
status = True
break
else:
status = False
return res.text
except (requests.exceptions.Timeout, requests.exceptions.ConnectionError):
timeout += 5
continue
except requests.exceptions.RequestException as e:
error = str(e)
break
write_push_log("Web Hook", status, title)
return error
@classmethod
def check_args(cls, args) -> Union[str, dict]:
"""配置hook"""
try:
title = args['title']
url = args["url"]
query = args.get("query", {})
headers = args.get("headers", {})
body_type = args.get("body_type", "json")
custom_parameter = args.get("custom_parameter", {})
method = args.get("method", "POST")
ssl_verify = args.get("ssl_verify", None) # null Ture
except (ValueError, KeyError):
return public.lang('The parameter is incorrect')
the_url = parse_url(url)
if the_url.scheme is None or the_url.host is None:
return"URL parsing error, which may not be a legitimate URL"
for i in (query, headers, custom_parameter):
if not isinstance(i, dict):
return public.lang('Parameter format error')
if body_type not in ('json', 'form_data', 'null'):
return public.lang('The body type must be json,form data, or null')
if method not in ('GET', 'POST', 'PUT', 'PATCH'):
return public.lang('The sending method is incorrect')
if ssl_verify not in (True, False, None):
return public.lang('Verify if the SSL option is wrong')
title = title.strip()
if title == "":
return"The name cannot be empty"
data = {
"title": title,
"url": url,
"query": query,
"headers": headers,
"body_type": body_type,
"custom_parameter": custom_parameter,
"method": method,
"ssl_verify": ssl_verify,
"status": True
}
test_obj = cls({"data": data, "id": None})
test_msg = {
"msg_list": ['>configuration state: Success\n\n']
}
test_task = get_test_msg("Message channel configuration reminders")
res = test_obj.send_msg(
test_task.to_web_hook_msg(test_msg, test_task.the_push_public_data()),
"Message channel configuration reminders",
"Message channel configuration reminders"
)
if res is None:
return data
return res
def test_send_msg(self) -> Optional[str]:
test_msg = {
"msg_list": ['>configuration state: <font color=#20a53a> Success </font>\n\n']
}
test_task = get_test_msg("Message channel configuration reminders")
res = self.send_msg(
test_task.to_web_hook_msg(test_msg, test_task.the_push_public_data()),
"Message channel configuration reminders",
"Message channel configuration reminders"
)
if res is None:
return None
return res

131
mod/base/msg/weixin_msg.py Normal file
View File

@@ -0,0 +1,131 @@
# coding: utf-8
# +-------------------------------------------------------------------
# | yakpanel
# +-------------------------------------------------------------------
# | Copyright (c) 2015-2020 yakpanel(http://www.yakpanel.com) All rights reserved.
# +-------------------------------------------------------------------
# | Author: baozi <baozi@yakpanel.com>
# | 消息通道邮箱模块
# +-------------------------------------------------------------------
import re
import json
import requests
import traceback
import socket
import public
import requests.packages.urllib3.util.connection as urllib3_cn
from requests.packages import urllib3
from typing import Optional, Union
from .util import write_push_log, get_test_msg
# 关闭警告
urllib3.disable_warnings()
class WeiXinMsg:
def __init__(self, weixin_data):
self.id = weixin_data["id"]
self.config = weixin_data["data"]
@classmethod
def check_args(cls, args: dict) -> Union[dict, str]:
if "url" not in args or "title" not in args:
return public.lang('Incomplete information')
title = args["title"]
if len(title) > 15:
return public.lang('Note names cannot be longer than 15 characters')
data = {
"url": args["url"],
"title": title,
}
test_obj = cls({"data": data, "id": None})
test_msg = {
"msg_list": ['>configuration state: <font color=#20a53a> Success </font>\n']
}
test_task = get_test_msg("Message channel configuration reminders")
res = test_obj.send_msg(
test_task.to_weixin_msg(test_msg, test_task.the_push_public_data()),
"Message channel configuration reminders"
)
if res is None:
return data
return res
def send_msg(self, msg: str, title: str) -> Optional[str]:
"""
@name 微信发送信息
@msg string 消息正文(正文内容,必须包含
1、服务器名称
2、IP地址
3、发送时间
)
@to_user string 指定发送人
"""
if not self.config:
return public.lang('WeChat information is not configured correctly')
reg = '<font.+>(.+)</font>'
tmp = re.search(reg, msg)
if tmp:
tmp = tmp.groups()[0]
msg = re.sub(reg, tmp, msg)
data = {
"msgtype": "markdown",
"markdown": {
"content": msg
}
}
headers = {'Content-Type': 'application/json'}
status = False
error = None
try:
def allowed_gai_family():
family = socket.AF_INET
return family
allowed_gai_family_lib = urllib3_cn.allowed_gai_family
urllib3_cn.allowed_gai_family = allowed_gai_family
response = requests.post(
url=self.config["url"],
data=json.dumps(data),
verify=False,
headers=headers,
timeout=10
)
urllib3_cn.allowed_gai_family = allowed_gai_family_lib
if response.json()["errcode"] == 0:
status = True
except:
error = traceback.format_exc()
write_push_log("weixin", status, title)
return error
def test_send_msg(self) -> Optional[str]:
test_msg = {
"msg_list": ['>configuration state: <font color=#20a53a> Success </font>\n\n']
}
test_task = get_test_msg("Message channel configuration reminders")
res = self.send_msg(
test_task.to_weixin_msg(test_msg, test_task.the_push_public_data()),
"Message channel configuration reminders",
)
if res is None:
return None
return res

View File

@@ -0,0 +1,565 @@
# coding: utf-8
# +-------------------------------------------------------------------
# | yakpanel
# +-------------------------------------------------------------------
# | Copyright (c) 2015-2020 yakpanel(http://www.yakpanel.com) All rights reserved.
# +-------------------------------------------------------------------
# | Author: baozi <baozi@yakpanel.com>
# | 消息通道微信公众号模块
# +-------------------------------------------------------------------
import os, sys
import time, base64
import re
import json
import requests
import traceback
import socket
import public
import requests.packages.urllib3.util.connection as urllib3_cn
from requests.packages import urllib3
from typing import Optional, Union, List, Dict, Any
from .util import write_push_log, get_test_msg, read_file, public_http_post
from mod.base.push_mod import WxAccountMsg, SenderConfig
from mod.base import json_response
# 关闭警告
urllib3.disable_warnings()
class WeChatAccountMsg:
USER_PATH = '/www/server/panel/data/userInfo.json'
need_refresh_file = '/www/server/panel/data/mod_push_data/refresh_wechat_account.tip'
refresh_time = '/www/server/panel/data/mod_push_data/refresh_wechat_account_time.pl'
def __init__(self, *config_data):
if len(config_data) == 0:
self.config = None
elif len(config_data) == 1:
self.config = config_data[0]["data"]
else:
self.config = config_data[0]["data"]
self.config["users"] = [i["data"]['id'] for i in config_data]
self.config["users_nickname"] = [i["data"]['nickname'] for i in config_data]
try:
self.user_info = json.loads(read_file(self.USER_PATH))
except:
self.user_info = None
@classmethod
def get_user_info(cls) -> Optional[dict]:
try:
return json.loads(read_file(cls.USER_PATH))
except:
return None
@classmethod
def last_refresh(cls):
tmp = read_file(cls.refresh_time)
if not tmp:
last_refresh_time = 0
else:
try:
last_refresh_time = int(tmp)
except:
last_refresh_time = 0
return last_refresh_time
@staticmethod
def get_local_ip() -> str:
"""获取内网IP"""
import socket
s = None
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(('8.8.8.8', 80))
ip = s.getsockname()[0]
return ip
except:
pass
finally:
if s is not None:
s.close()
return '127.0.0.1'
def send_msg(self, msg: WxAccountMsg) -> Optional[str]:
if self.user_info is None:
return public.lang('No user information was obtained')
if public.is_self_hosted():
return public.lang('WeChat official account cloud features are not available in self-hosted mode.')
msg.set_ip_address(self.user_info["address"], self.get_local_ip())
template_id, msg_data = msg.to_send_data()
url = "https://wafapi2.yakpanel.com/api/v2/user/wx_web/send_template_msg_v3"
wx_account_ids = self.config["users"] if "users" in self.config else [self.config["id"], ]
data = {
"uid": self.user_info["uid"],
"access_key": 'B' * 32,
"data": base64.b64encode(json.dumps(msg_data).encode('utf-8')).decode('utf-8'),
"wx_account_ids": base64.b64encode(json.dumps(wx_account_ids).encode('utf-8')).decode('utf-8'),
}
if template_id != "":
data["template_id"] = template_id
status = False
error = None
user_name = self.config["users_nickname"] if "users_nickname" in self.config else [self.config["nickname"], ]
try:
resp = public_http_post(url, data)
x = json.loads(resp)
if x["success"]:
status = True
else:
status = False
error = x["res"]
except:
error = traceback.format_exc()
write_push_log("wx_account", status, msg.thing_type, user_name)
return error
@classmethod
def refresh_config(cls, force: bool = False):
if os.path.exists(cls.need_refresh_file):
force = True
os.remove(cls.need_refresh_file)
if force or cls.last_refresh() + 60 * 10 < time.time():
cls._get_by_web()
@classmethod
def _get_by_web(cls) -> Optional[List]:
user_info = cls.get_user_info()
if user_info is None:
return None
if public.is_self_hosted():
return None
url = "https://wafapi2.yakpanel.com/api/v2/user/wx_web/bound_wx_accounts"
data = {
"uid": user_info["uid"],
"access_key": 'B' * 32,
"serverid": user_info["server_id"]
}
try:
data = json.loads(public_http_post(url, data))
if not data["success"]:
return None
except:
return None
cls._save_user_info(data["res"])
return data["res"]
@staticmethod
def _save_user_info(user_config_list: List[Dict[str, Any]]):
print(user_config_list)
user_config_dict = {i["hex"]: i for i in user_config_list}
remove_list = []
sc = SenderConfig()
for i in sc.config:
if i['sender_type'] != "wx_account":
continue
if i['data'].get("hex", None) in user_config_dict:
i['data'].update(user_config_dict[i['data']["hex"]])
user_config_dict.pop(i['data']["hex"])
else:
remove_list.append(i)
for r in remove_list:
sc.config.remove(r)
if user_config_dict: # 还有多的
for v in user_config_dict.values():
v["title"] = v["nickname"]
sc.config.append({
"id": sc.nwe_id(),
"used": True,
"sender_type": "wx_account",
"data": v
})
sc.save_config()
@classmethod
def unbind(cls, wx_account_uid: str):
user_info = cls.get_user_info()
if user_info is None:
return json_response(status=True, msg=public.lang('The user binding information was not obtained'))
url = "https://wafapi2.yakpanel.com/api/v2/user/wx_web/unbind_wx_accounts"
data = {
"uid": user_info["uid"],
"access_key": 'B' * 32,
"serverid": user_info["server_id"],
"ids": str(wx_account_uid)
}
try:
datas = json.loads(public_http_post(url, data))
if datas["success"]:
return json_response(status=True, data=datas, msg=public.lang('The unbinding is successful'))
else:
return json_response(status=False, data=datas, msg=datas["res"])
except:
return json_response(status=True, msg=public.lang('Failed to link to the cloud'))
@classmethod
def get_auth_url(cls):
user_info = cls.get_user_info()
if user_info is None:
return json_response(status=True, msg=public.lang('The user binding information was not obtained'))
if public.is_self_hosted():
return json_response(status=False, msg=public.lang('WeChat official account cloud features are not available in self-hosted mode.'))
url = "https://wafapi2.yakpanel.com/api/v2/user/wx_web/get_auth_url"
data = {
"uid": user_info["uid"],
"access_key": 'B' * 32,
"serverid": user_info["server_id"],
}
try:
datas = json.loads(public_http_post(url, data))
if datas["success"]:
return json_response(status=True, data=datas)
else:
return json_response(status=False, data=datas, msg=datas["res"])
except:
return json_response(status=True, msg=public.lang('Failed to link to the cloud'))
def test_send_msg(self) -> Optional[str]:
test_msg = {
"msg_list": ['>configuration state: <font color=#20a53a> Success </font>\n\n']
}
test_task = get_test_msg("Message channel configuration reminders")
res = self.send_msg(
test_task.to_wx_account_msg(test_msg, test_task.the_push_public_data()),
)
if res is None:
return None
return res
# class wx_account_msg:
# __module_name = None
# __default_pl = "{}/data/default_msg_channel.pl".format(panelPath)
# conf_path = '{}/data/wx_account_msg.json'.format(panelPath)
# user_info = None
#
# def __init__(self):
# try:
# self.user_info = json.loads(public.ReadFile("{}/data/userInfo.json".format(public.get_panel_path())))
# except:
# self.user_info = None
# self.__module_name = self.__class__.__name__.replace('_msg', '')
#
# def get_version_info(self, get):
# """
# 获取版本信息
# """
# data = {}
# data['ps'] = 'YakPanel 微信公众号,用于接收面板消息推送'
# data['version'] = '1.0'
# data['date'] = '2022-08-15'
# data['author'] = 'YakPanel'
# data['title'] = '微信公众号'
# data['help'] = 'http://www.yakpanel.com'
# return data
#
# def get_local_ip(self):
# '''获取内网IP'''
# import socket
# try:
# s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# s.connect(('8.8.8.8', 80))
# ip = s.getsockname()[0]
# return ip
# finally:
# s.close()
# return '127.0.0.1'
#
# def get_config(self, get):
# """
# 微信公众号配置
# """
# if os.path.exists(self.conf_path):
# # 60S内不重复加载
# start_time = int(time.time())
# if os.path.exists("data/wx_account_msg.lock"):
# lock_time = 0
# try:
# lock_time = int(public.ReadFile("data/wx_account_msg.lock"))
# except:
# pass
# # 大于60S重新加载
# if start_time - lock_time > 60:
# public.run_thread(self.get_web_info2)
# public.WriteFile("data/wx_account_msg.lock", str(start_time))
# else:
# public.WriteFile("data/wx_account_msg.lock", str(start_time))
# public.run_thread(self.get_web_info2)
# data = json.loads(public.ReadFile(self.conf_path))
#
# if not 'list' in data: data['list'] = {}
#
# title = '默认'
# if 'res' in data and 'nickname' in data['res']: title = data['res']['nickname']
#
# data['list']['default'] = {'title': title, 'data': ''}
#
# data['default'] = self.__get_default_channel()
# return data
# else:
# public.run_thread(self.get_web_info2)
# return {"success": False, "res": "未获取到配置信息"}
#
# def set_config(self, get):
# """
# @设置默认值
# """
# if 'default' in get and get['default']:
# public.writeFile(self.__default_pl, self.__module_name)
#
# return public.returnMsg(True, '设置成功')
#
# def get_web_info(self, get):
# if self.user_info is None: return public.returnMsg(False, 'The user binding information was not obtained')
# url = "https://wafapi2.yakpanel.com/api/v2/user/wx_web/info"
# data = {
# "uid": self.user_info["uid"],
# "access_key": self.user_info["access_key"],
# "serverid": self.user_info["server_id"]
# }
# try:
#
# datas = json.loads(public.httpPost(url, data))
#
# if datas["success"]:
# public.WriteFile(self.conf_path, json.dumps(datas))
# return public.returnMsg(True, datas)
# else:
# public.WriteFile(self.conf_path, json.dumps(datas))
# return public.returnMsg(False, datas)
# except:
# public.WriteFile(self.conf_path, json.dumps({"success": False, "res": "链接云端失败,请检查网络"}))
# return public.returnMsg(False, "链接云端失败,请检查网络")
#
# def unbind(self):
# if self.user_info is None:
# return public.returnMsg(False, 'The user binding information was not obtained')
# url = "https://wafapi2.yakpanel.com/api/v2/user/wx_web/unbind"
# data = {
# "uid": self.user_info["uid"],
# "access_key": self.user_info["access_key"],
# "serverid": self.user_info["server_id"]
# }
# try:
#
# datas = json.loads(public.httpPost(url, data))
#
# if os.path.exists(self.conf_path):
# os.remove(self.conf_path)
#
# if datas["success"]:
# return public.returnMsg(True, datas)
# else:
# return public.returnMsg(False, datas)
# except:
# public.WriteFile(self.conf_path, json.dumps({"success": False, "res": "链接云端失败,请检查网络"}))
# return public.returnMsg(False, "链接云端失败,请检查网络")
#
# def get_web_info2(self):
# if self.user_info is None:
# return public.returnMsg(False, 'The user binding information was not obtained')
# url = "https://wafapi2.yakpanel.com/api/v2/user/wx_web/info"
# data = {
# "uid": self.user_info["uid"],
# "access_key": self.user_info["access_key"],
# "serverid": self.user_info["server_id"]
# }
# try:
# datas = json.loads(public.httpPost(url, data))
# if datas["success"]:
# public.WriteFile(self.conf_path, json.dumps(datas))
# return public.returnMsg(True, datas)
# else:
# public.WriteFile(self.conf_path, json.dumps(datas))
# return public.returnMsg(False, datas)
# except:
# public.WriteFile(self.conf_path, json.dumps({"success": False, "res": "链接云端失败"}))
# return public.returnMsg(False, "链接云端失败")
#
# def get_send_msg(self, msg):
# """
# @name 处理md格式
# """
# try:
# import re
# title = 'YakPanel 告警通知'
# if msg.find("####") >= 0:
# try:
# title = re.search(r"####(.+)", msg).groups()[0]
# except:
# pass
#
# msg = msg.replace("####", ">").replace("\n\n", "\n").strip()
# s_list = msg.split('\n')
#
# if len(s_list) > 3:
# s_title = s_list[0].replace(" ", "")
# s_list = s_list[3:]
# s_list.insert(0, s_title)
# msg=public.lang('\n').join(s_list)
#
# s_list = []
# for msg_info in msg.split('\n'):
# reg = '<font.+>(.+)</font>'
# tmp = re.search(reg, msg_info)
# if tmp:
# tmp = tmp.groups()[0]
# msg_info = re.sub(reg, tmp, msg_info)
# s_list.append(msg_info)
# msg=public.lang('\n').join(s_list)
# except:
# pass
# return msg, title
#
# def send_msg(self, msg):
# """
# 微信发送信息
# @msg 消息正文
# """
#
# if self.user_info is None:
# return public.returnMsg(False, '未获取到用户信息')
#
# if not isinstance(msg, str):
# return self.send_msg_v2(msg)
#
# msg, title = self.get_send_msg(msg)
# url = "https://wafapi2.yakpanel.com/api/v2/user/wx_web/send_template_msg_v2"
# datassss = {
# "first": {
# "value": "堡塔主机告警",
# },
# "keyword1": {
# "value": "内网IP " + self.get_local_ip() + "\n外网IP " + self.user_info[
# "address"] + " \n服务器别名 " + public.GetConfigValue("title"),
# },
# "keyword2": {
# "value": "堡塔主机告警",
# },
# "keyword3": {
# "value": msg,
# },
# "remark": {
# "value": "如有疑问请联系YakPanel 支持",
# },
# }
# data = {
# "uid": self.user_info["uid"],
# "access_key": self.user_info["access_key"],
# "data": base64.b64encode(json.dumps(datassss).encode('utf-8')).decode('utf-8')
# }
#
# try:
# res = {}
# error, success = 0, 0
#
# x = json.loads(public.httpPost(url, data))
# conf = self.get_config(None)['list']
#
# # 立即刷新剩余次数
# public.run_thread(self.get_web_info2)
#
# res[conf['default']['title']] = 0
# if x['success']:
# res[conf['default']['title']] = 1
# success += 1
# else:
# error += 1
#
# try:
# public.write_push_log(self.__module_name, title, res)
# except:
# pass
#
# result = public.returnMsg(True, '发送完成,发送成功{},发送失败{}.'.format(success, error))
# result['success'] = success
# result['error'] = error
# return result
#
# except:
# print(public.get_error_info())
# return public.returnMsg(False, '微信消息发送失败。 --> {}'.format(public.get_error_info()))
#
# def push_data(self, data):
# if isinstance(data, dict):
# return self.send_msg(data['msg'])
# else:
# return self.send_msg_v2(data)
#
# def uninstall(self):
# if os.path.exists(self.conf_path):
# os.remove(self.conf_path)
#
# def send_msg_v2(self, msg):
# from push.base_push import WxAccountMsgBase, WxAccountMsg
# if self.user_info is None:
# return public.returnMsg(False, '未获取到用户信息')
#
# if isinstance(msg, public.dict_obj):
# msg = getattr(msg, "msg", "测试信息")
# if len(msg) >= 20:
# return self.send_msg(msg)
#
# if isinstance(msg, str):
# the_msg = WxAccountMsg.new_msg()
# the_msg.thing_type = msg
# the_msg.msg = msg
# msg = the_msg
#
# if not isinstance(msg, WxAccountMsgBase):
# return public.returnMsg(False, '消息类型错误')
#
# msg.set_ip_address(self.user_info["address"], self.get_local_ip())
#
# template_id, msg_data = msg.to_send_data()
# url = "https://wafapi2.yakpanel.com/api/v2/user/wx_web/send_template_msg_v2"
# data = {
# "uid": self.user_info["uid"],
# "access_key": self.user_info["access_key"],
# "data": base64.b64encode(json.dumps(msg_data).encode('utf-8')).decode('utf-8'),
# }
# if template_id != "":
# data["template_id"] = template_id
#
# try:
# error, success = 0, 0
# resp = public.httpPost(url, data)
# x = json.loads(resp)
# conf = self.get_config(None)['list']
#
# # 立即刷新剩余次数
# public.run_thread(self.get_web_info2)
#
# res = {
# conf['default']['title']: 0
# }
# if x['success']:
# res[conf['default']['title']] = 1
# success += 1
# else:
# error += 1
#
# try:
# public.write_push_log(self.__module_name, msg.thing_type, res)
# except:
# pass
# result = public.returnMsg(True, '发送完成,发送成功{},发送失败{}.'.format(success, error))
# result['success'] = success
# result['error'] = error
# return result
#
# except:
# return public.returnMsg(False, '微信消息发送失败。 --> {}'.format(public.get_error_info()))