# -*- 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