Files
dromara--domain-admin/domain_admin/service/notify_service.py
T
2026-07-13 12:31:34 +08:00

629 lines
17 KiB
Python

# -*- coding: utf-8 -*-
"""
@File : notify_service.py
@Date : 2022-10-30
@Author : Peng Shiyu
"""
from __future__ import print_function, unicode_literals, absolute_import, division
import json
import traceback
from datetime import datetime, timedelta
import requests
from jinja2 import Template
from playhouse.shortcuts import model_to_dict
from domain_admin.enums.config_key_enum import ConfigKeyEnum
from domain_admin.enums.event_enum import EventEnum
from domain_admin.enums.notify_type_enum import NotifyTypeEnum
from domain_admin.log import logger
from domain_admin.model.certificate_model import CertificateModel
from domain_admin.model.domain_info_model import DomainInfoModel
from domain_admin.model.domain_model import DomainModel
from domain_admin.model.group_user_model import GroupUserModel
from domain_admin.model.notify_model import NotifyModel
from domain_admin.service import domain_service, render_service, system_service, async_task_service, group_service
from domain_admin.utils import email_util, json_util
from domain_admin.utils.flask_ext.app_exception import AppException
from domain_admin.utils.open_api import feishu_api, work_weixin_api, ding_talk_api, telegram_api
# 通知参数配置
NOTIFY_CONFIGS = [
{
'event_id': EventEnum.SSL_CERT_EXPIRE,
'email_template': 'cert-email.html',
'email_subject': '[Domain Admin]证书过期提醒',
},
{
'event_id': EventEnum.DOMAIN_EXPIRE,
'email_template': 'domain-email.html',
'email_subject': '[Domain Admin]域名过期提醒',
},
{
'event_id': EventEnum.MONITOR_EXCEPTION,
'email_template': 'monitor-email.html',
'email_subject': '[Domain Admin]监控异常提醒',
},
{
'event_id': EventEnum.MONITOR_EXCEPTION_RESTORE,
'email_template': 'monitor-restore-email.html',
'email_subject': '[Domain Admin]监控异常恢复提醒',
},
{
'event_id': EventEnum.SSL_CERT_FILE_EXPIRE,
'email_template': 'cert-email.html',
'email_subject': '[Domain Admin]托管证书到期提醒',
}
]
def get_notify_config(event_id):
"""
获取通知配置
:param event_id: int
:return:
"""
for config in NOTIFY_CONFIGS:
if config['event_id'] == event_id:
return config
def get_notify_row_value(user_id, type_id):
"""
获取通知配置
:param user_id:
:param type_id:
:return:
"""
notify_row = NotifyModel.select().where(
NotifyModel.user_id == user_id,
NotifyModel.type_id == type_id
).get_or_none()
if not notify_row:
return None
if not notify_row.value:
return None
return notify_row.value
def get_notify_email_list_of_user(user_id):
"""
获取通知配置 - 邮箱列表
:param user_id:
:return: Optional[List[str]]
"""
notify_row_value = get_notify_row_value(user_id, NotifyTypeEnum.Email)
if not notify_row_value:
return None
email_list = notify_row_value.get('email_list')
if not email_list:
return None
return email_list
def get_notify_webhook_row_of_user(user_id):
"""
获取通知配置 - webhook
:param user_id:
:return:
"""
return get_notify_row_value(user_id, NotifyTypeEnum.WebHook)
def notify_webhook_of_user(user_id):
"""
通过 webhook 方式通知用户
:param user_id:
:return:
"""
notify_webhook_row = get_notify_webhook_row_of_user(user_id)
if not notify_webhook_row:
raise AppException('webhook未设置')
method = notify_webhook_row.get('method')
url = notify_webhook_row.get('url')
headers = notify_webhook_row.get('headers')
body = notify_webhook_row.get('body')
if not url:
raise AppException('url未设置')
# 支持模板变量
template = Template(body)
body_render = template.render(get_template_data(user_id))
res = requests.request(method=method, url=url, headers=headers, data=body_render.encode('utf-8'))
res.encoding = res.apparent_encoding
return res.text
def get_template_data(user_id):
# 两种参数形式
domain_list = domain_service.get_domain_info_list(user_id)
return {
'domain_list': domain_list
}
def notify_all_event():
"""
触发所有通知事件
:return: int 成功数量
"""
# fix: TypeError: inner() takes 2 positional arguments but 3 were given
rows = NotifyModel.select().where(
NotifyModel.status == True,
NotifyModel.event_id.in_([
EventEnum.SSL_CERT_EXPIRE,
EventEnum.DOMAIN_EXPIRE,
EventEnum.SSL_CERT_FILE_EXPIRE
])
)
success = 0
for row in rows:
try:
notify_user_about_some_event(row)
except:
logger.error(traceback.format_exc())
success = success + 1
return success
def notify_user_about_monitor_exception(monitor_row, error):
rows = NotifyModel.select().where(
NotifyModel.status == True,
NotifyModel.event_id == EventEnum.MONITOR_EXCEPTION
)
for row in rows:
try:
notify_user(notify_row=row, rows=rows, data={'monitor_row': monitor_row, 'error': str(error)})
except:
logger.error(traceback.format_exc())
def notify_user_about_monitor_exception_restore(monitor_row):
"""
监控异常恢复通知
:param monitor_row:
:param error:
:return:
"""
rows = NotifyModel.select().where(
NotifyModel.status == True,
NotifyModel.event_id == EventEnum.MONITOR_EXCEPTION_RESTORE
)
for row in rows:
try:
notify_user(notify_row=row, rows=rows, data={'monitor_row': monitor_row})
except:
logger.error(traceback.format_exc())
# @async_task_service.sync_task_decorator("触发通知用户")
def notify_user_about_some_event(notify_row):
"""
由于某个事件触发,通知用户
:param notify_row: NotifyModel
:return:
"""
if notify_row.event_id == EventEnum.SSL_CERT_EXPIRE:
# ssl证书
return notify_user_about_cert_expired(notify_row)
elif notify_row.event_id == EventEnum.DOMAIN_EXPIRE:
# 域名过期
return notify_user_about_domain_expired(notify_row)
elif notify_row.event_id == EventEnum.SSL_CERT_FILE_EXPIRE:
# 托管证书到期
return notify_user_about_cert_file_expired(notify_row)
else:
raise AppException("notify_row event_id not support: {}".format(notify_row.event_id))
def notify_user_about_cert_expired(notify_row):
"""
证书过期事件触发
:param notify_row: NotifyModel
:return:
"""
now = datetime.now()
notify_expire_time = now + timedelta(days=notify_row.expire_days)
# 所在分组
group_user_rows = GroupUserModel.select().where(
GroupUserModel.user_id == notify_row.user_id
)
group_user_list = list(group_user_rows)
user_group_ids = [row.group_id for row in group_user_list]
# 注意null的情况
query = DomainModel.select().where(
DomainModel.is_monitor == True
)
# 分组
if notify_row.groups:
query = query.where(
DomainModel.group_id.in_(notify_row.groups)
)
elif user_group_ids:
query = query.where(
(DomainModel.user_id == notify_row.user_id)
| (DomainModel.group_id.in_(user_group_ids))
)
else:
query = query.where(
DomainModel.user_id == notify_row.user_id,
)
rows = query.where(
DomainModel.expire_time.is_null(False),
DomainModel.expire_time <= notify_expire_time
).order_by(
DomainModel.expire_time.asc(),
DomainModel.id.desc()
)
lst = [model_to_dict(
model=row,
extra_attrs=[
'start_date',
'expire_date',
'real_time_expire_days',
'update_time_label',
]
) for row in rows]
for row in lst:
row['expire_days'] = row['real_time_expire_days']
row['comment'] = row['alias']
row['is_auto_update'] = row['auto_update']
row['is_expire_monitor'] = row['is_monitor']
group_service.load_group_name(lst)
if len(lst) > 0:
return notify_user(notify_row, lst)
def notify_user_about_cert_file_expired(notify_row):
"""
托管证书到期
:param notify_row:
:return:
"""
now = datetime.now()
notify_expire_time = now + timedelta(days=notify_row.expire_days)
# 注意null的情况
query = CertificateModel.select()
rows = query.where(
CertificateModel.expire_time.is_null(False),
CertificateModel.expire_time <= notify_expire_time
).order_by(
CertificateModel.expire_time.asc(),
CertificateModel.id.desc()
)
lst = [row.to_dict() for row in rows]
for row in lst:
row['expire_days'] = row['real_time_expire_days']
if len(lst) > 0:
return notify_user(notify_row, lst)
def notify_user_about_domain_expired(notify_row):
"""
域名过期事件触发
:param notify_row: NotifyModel
:return:
"""
now = datetime.now()
notify_expire_time = now + timedelta(days=notify_row.expire_days)
# 所在分组
group_user_rows = GroupUserModel.select().where(
GroupUserModel.user_id == notify_row.user_id
)
group_user_list = list(group_user_rows)
user_group_ids = [row.group_id for row in group_user_list]
# 注意null的情况
query = DomainInfoModel.select().where(
DomainInfoModel.is_expire_monitor == True
)
if notify_row.groups:
query = query.where(
DomainInfoModel.group_id.in_(notify_row.groups)
)
elif user_group_ids:
query = query.where(
(DomainInfoModel.user_id == notify_row.user_id)
| (DomainInfoModel.group_id.in_(user_group_ids))
)
else:
query = query.where(
DomainInfoModel.user_id == notify_row.user_id,
)
rows = query.where(
(DomainInfoModel.domain_expire_time <= notify_expire_time)
| (DomainInfoModel.domain_expire_time.is_null(True))
).order_by(
DomainInfoModel.domain_expire_time.asc(),
DomainInfoModel.id.desc()
)
lst = [model_to_dict(
model=row,
extra_attrs=[
'tags',
'start_date',
'expire_date',
'expire_days',
'tags_str',
'update_time_label',
]
) for row in rows]
for row in lst:
row['start_time'] = row['domain_start_time']
row['expire_time'] = row['domain_expire_time']
# row['start_date'] = row['domain_start_date']
# row['expire_date'] = row['domain_expire_date']
# row['expire_days'] = row['real_domain_expire_days']
group_service.load_group_name(lst)
if len(lst) > 0:
return notify_user(notify_row, lst)
def notify_user(notify_row, rows, data=None):
"""
通知用户
:param notify_row: NotifyModel
:param rows: List
:return:
"""
logger.debug(json_util.json_dump(rows))
data = data if data else {}
# 通知用户
if notify_row.type_id == NotifyTypeEnum.Email:
notify_config = get_notify_config(notify_row.event_id)
if not notify_config:
raise AppException('邮件通知模板未配置')
return notify_user_by_email(
template=notify_config['email_template'],
subject=notify_config['email_subject'],
data={**data, 'list': rows},
email_list=notify_row.email_list
)
elif notify_row.type_id == NotifyTypeEnum.WebHook:
return notify_user_by_webhook(
notify_row=notify_row,
data={
**data,
'list': rows,
'domain_list': rows # 兼容旧版本
})
elif notify_row.type_id == NotifyTypeEnum.WORK_WEIXIN:
return notify_user_by_work_weixin(notify_row=notify_row, data={**data, 'list': rows})
elif notify_row.type_id == NotifyTypeEnum.DING_TALK:
return notify_user_by_ding_talk(notify_row=notify_row, data={**data, 'list': rows})
elif notify_row.type_id == NotifyTypeEnum.FEISHU:
return notify_user_by_feishu(notify_row=notify_row, data={**data, 'list': rows})
elif notify_row.type_id == NotifyTypeEnum.Telegram:
return notify_user_by_telegram(notify_row=notify_row, data={**data, 'list': rows})
else:
logger.warn("type not support")
@async_task_service.sync_task_decorator("触发Webhook通知")
def notify_user_by_webhook(
notify_row,
data):
"""
通过 webhook 方式通知用户
:param notify_row: NotifyModel
:param data: Dict
:return:
"""
logger.info("notify_row.id: %s", notify_row.id)
if not notify_row.webhook_url:
logger.warn("webhook url未设置")
return
# 支持模板变量
template = Template(notify_row.webhook_body)
body_render = template.render(data)
logger.info(body_render)
res = requests.request(
method=notify_row.webhook_method,
url=notify_row.webhook_url,
headers=notify_row.webhook_headers,
data=body_render.encode('utf-8'))
res.encoding = res.apparent_encoding
logger.info(res.text)
return res.text
@async_task_service.sync_task_decorator("触发邮件通知")
def notify_user_by_email(
template,
subject,
data,
email_list,
):
"""
通过邮件通知用户证书到期
:param template: str
:param subject: str
:param data: Dict
:param email_list: List[str]
:return:
"""
send_email_to_user(
template,
subject,
data,
email_list
)
def send_email_to_user(
template,
subject,
data,
email_list,
):
"""
通过邮件通知用户证书到期
:param template: str
:param subject: str
:param data: Dict
:param email_list: List[str]
:return:
"""
if not email_list or len(email_list) == 0:
logger.warn("email_list is empty")
return
content = render_service.render_template(
template=template,
data=data
)
config = system_service.get_system_config()
email_util.send_email(
mail_host=config[ConfigKeyEnum.MAIL_HOST],
mail_port=int(config[ConfigKeyEnum.MAIL_PORT]),
mail_alias=config[ConfigKeyEnum.MAIL_ALIAS],
subject=subject,
content=content,
to_addresses=email_list,
mail_username=config[ConfigKeyEnum.MAIL_USERNAME],
mail_password=config[ConfigKeyEnum.MAIL_PASSWORD],
content_type='html'
)
@async_task_service.sync_task_decorator("触发企业微信通知")
def notify_user_by_work_weixin(notify_row, data):
"""
发送企业微信消息
:param notify_row: NotifyModel
:return:
"""
token = work_weixin_api.get_access_token(notify_row.work_weixin_corpid, notify_row.work_weixin_corpsecret)
logger.info('work weixin token %s', token)
# 支持模板变量
template = Template(notify_row.work_weixin_body)
work_weixin_body = template.render(data)
res = work_weixin_api.send_message(token['access_token'], json.loads(work_weixin_body))
return res
@async_task_service.sync_task_decorator("触发钉钉通知")
def notify_user_by_ding_talk(notify_row, data):
"""
发送钉钉消息
:param notify_row: NotifyModel
:return:
"""
token = ding_talk_api.get_access_token(notify_row.ding_talk_appkey, notify_row.ding_talk_appsecret)
logger.info('ding talk token %s', token)
# 支持模板变量
template = Template(notify_row.ding_talk_body)
ding_talk_body = template.render(data)
res = ding_talk_api.send_message(token['access_token'], json.loads(ding_talk_body))
return res
@async_task_service.sync_task_decorator("触发飞书通知")
def notify_user_by_feishu(notify_row, data):
"""
发送飞书消息
:param notify_row: NotifyModel
:return:
"""
token = feishu_api.get_access_token(notify_row.feishu_app_id, notify_row.feishu_app_secret)
logger.info('feishu token %s', token)
# 支持模板变量
template = Template(notify_row.feishu_body)
feishu_body = template.render(data)
res = feishu_api.send_message(
access_token=token['tenant_access_token'],
body=json.loads(feishu_body),
params=notify_row.feishu_params
)
return res
@async_task_service.sync_task_decorator("触发电报通知")
def notify_user_by_telegram(notify_row: NotifyModel, data):
"""
触发电报通知
:param notify_row: NotifyModel
:return:
"""
# 支持模板变量
template = Template(notify_row.telegram_body)
telegram_body = template.render(data)
data = {
'token': notify_row.telegram_token,
'chat_id': notify_row.telegram_chat_id,
'text': telegram_body,
'proxies': notify_row.telegram_proxies,
}
logger.info(data)
res = telegram_api.send_message(**data)
return res