diff --git a/spug_api/apps/alarm/views.py b/spug_api/apps/alarm/views.py index eb51afd..882a78d 100644 --- a/spug_api/apps/alarm/views.py +++ b/spug_api/apps/alarm/views.py @@ -4,8 +4,10 @@ from django.views.generic import View from libs import json_response, JsonParser, Argument, auth from libs.spug import Notification +from libs.push import get_contacts from apps.alarm.models import Alarm, Group, Contact from apps.monitor.models import Detection +from apps.setting.utils import AppSetting import json @@ -55,8 +57,20 @@ class GroupView(View): class ContactView(View): @auth('alarm.contact.view|alarm.group.view') def get(self, request): - contacts = Contact.objects.all() - return json_response(contacts) + form, error = JsonParser( + Argument('with_push', required=False), + ).parse(request.GET) + if error is None: + response = [] + if form.with_push: + push_key = AppSetting.get('spug_push_key') + if push_key: + response = get_contacts(push_key) + + for item in Contact.objects.all(): + response.append(item.to_dict()) + return json_response(response) + return json_response(error=error) @auth('alarm.contact.add|alarm.contact.edit') def post(self, request): diff --git a/spug_api/apps/monitor/views.py b/spug_api/apps/monitor/views.py index bf5b11e..e0f12df 100644 --- a/spug_api/apps/monitor/views.py +++ b/spug_api/apps/monitor/views.py @@ -37,8 +37,8 @@ class DetectionView(View): ).parse(request.body) if error is None: if set(form.notify_mode).intersection(['1', '2', '4']): - if not AppSetting.get_default('spug_key'): - return json_response(error='报警方式 微信、短信、邮件需要配置调用凭据(系统设置/基本设置),请配置后再启用该报警方式。') + if not AppSetting.get_default('spug_key') and not AppSetting.get_default('spug_push_key'): + return json_response(error='报警方式 微信、短信、邮件需要配置调用凭据(系统设置/基本设置)或推送服务(系统设置/推送服务设置),请配置后再启用该报警方式。') form.targets = json.dumps(form.targets) form.notify_grp = json.dumps(form.notify_grp) diff --git a/spug_api/apps/setting/models.py b/spug_api/apps/setting/models.py index 58e90b0..f33b5bf 100644 --- a/spug_api/apps/setting/models.py +++ b/spug_api/apps/setting/models.py @@ -16,6 +16,7 @@ KEYS_DEFAULT = { 'mail_service': {}, 'private_key': None, 'public_key': None, + 'spug_push_key': None, } diff --git a/spug_api/apps/setting/urls.py b/spug_api/apps/setting/urls.py index 89c6de5..577d5a3 100644 --- a/spug_api/apps/setting/urls.py +++ b/spug_api/apps/setting/urls.py @@ -12,5 +12,6 @@ urlpatterns = [ url(r'^ldap_test/$', ldap_test), url(r'^email_test/$', email_test), url(r'^mfa/$', MFAView.as_view()), - url(r'^about/$', get_about) + url(r'^about/$', get_about), + url(r'^balance/$', get_push_balance), ] diff --git a/spug_api/apps/setting/views.py b/spug_api/apps/setting/views.py index 417cc15..8eb66d9 100644 --- a/spug_api/apps/setting/views.py +++ b/spug_api/apps/setting/views.py @@ -8,6 +8,7 @@ from libs import JsonParser, Argument, json_response, auth from libs.utils import generate_random_str from libs.mail import Mail from libs.spug import send_login_wx_code +from libs.push import get_balance from libs.mixins import AdminView from apps.setting.utils import AppSetting from apps.setting.models import Setting, KEYS_DEFAULT @@ -36,7 +37,8 @@ class SettingView(AdminView): class MFAView(AdminView): def get(self, request): if not request.user.wx_token: - return json_response(error='检测到当前账户未配置微信Token,请配置后再尝试启用MFA认证,否则可能造成系统无法正常登录。') + return json_response( + error='检测到当前账户未配置微信Token,请配置后再尝试启用MFA认证,否则可能造成系统无法正常登录。') code = generate_random_str(6) send_login_wx_code(request.user.wx_token, code) cache.set(f'{request.user.username}:code', code, 300) @@ -105,7 +107,8 @@ def email_test(request): @auth('admin') def mfa_test(request): if not request.user.wx_token: - return json_response(error='检测到当前账户未配置微信Token,请配置后再尝试启用MFA认证,否则可能造成系统无法正常登录。') + return json_response( + error='检测到当前账户未配置微信Token,请配置后再尝试启用MFA认证,否则可能造成系统无法正常登录。') code = generate_random_str(6) send_login_wx_code(request.user.wx_token, code) cache.set(f'{request.user.username}:code', code, 300) @@ -120,3 +123,12 @@ def get_about(request): 'spug_version': settings.SPUG_VERSION, 'django_version': django.get_version() }) + + +@auth('admin') +def get_push_balance(request): + token = AppSetting.get_default('spug_push_key') + if not token: + return json_response(error='请先配置推送服务绑定账户') + res = get_balance(token) + return json_response(res) diff --git a/spug_api/libs/push.py b/spug_api/libs/push.py new file mode 100644 index 0000000..6c0d518 --- /dev/null +++ b/spug_api/libs/push.py @@ -0,0 +1,26 @@ +# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug +# Copyright: (c) +# Released under the AGPL-3.0 License. +import requests + +push_server = 'https://push.spug.cc' + + +def get_balance(token): + res = requests.get(f'{push_server}/spug/balance/', json={'token': token}) + if res.status_code != 200: + raise Exception(f'status code: {res.status_code}') + res = res.json() + if res.get('error'): + raise Exception(res['error']) + return res['data'] + + +def get_contacts(token): + try: + res = requests.post(f'{push_server}/spug/contacts/', json={'token': token}) + res = res.json() + if res['data']: + return res['data'] + except Exception: + return [] diff --git a/spug_api/libs/spug.py b/spug_api/libs/spug.py index d0ff111..e82d803 100644 --- a/spug_api/libs/spug.py +++ b/spug_api/libs/spug.py @@ -6,11 +6,23 @@ from apps.setting.utils import AppSetting from apps.notify.models import Notify from libs.mail import Mail from libs.utils import human_datetime +from libs.push import push_server +from functools import partial import requests import json spug_server = 'https://api.spug.cc' notify_source = 'monitor' +make_no_spug_key_notify = partial( + Notify.make_monitor_notify, + '发送报警信息失败', + '未配置报警服务调用凭据,请在系统管理/系统设置/基本设置/调用凭据中配置。' +) +make_no_push_key_notify = partial( + Notify.make_monitor_notify, + '发送报警信息失败', + '未绑定推送服务,请在系统管理/系统设置/推送服务设置中绑定推送助手账户。' +) def send_login_wx_code(wx_token, code): @@ -33,7 +45,7 @@ class Notification: self.message = message self.duration = duration self.spug_key = AppSetting.get_default('spug_key') - self.u_ids = [] + self.spug_push_key = AppSetting.get_default('spug_push_key') @staticmethod def handle_request(url, data, mode=None): @@ -62,7 +74,7 @@ class Notification: def monitor_by_wx(self, users): if not self.spug_key: - Notify.make_monitor_notify('发送报警信息失败', '未配置报警服务调用凭据,请在系统管理/系统设置/基本设置/调用凭据中配置。') + make_no_spug_key_notify() return data = { 'token': self.spug_key, @@ -99,7 +111,7 @@ class Notification: } self.handle_request(f'{spug_server}/apis/notify/mail/', data, 'spug') else: - Notify.make_monitor_notify('发送报警信息失败', '未配置报警服务调用凭据,请在系统管理/系统设置/报警服务设置中配置。') + make_no_spug_key_notify() def monitor_by_dd(self, users): texts = [ @@ -144,30 +156,82 @@ class Notification: for url in users: self.handle_request(url, data, 'wx') + def monitor_by_spug_push(self, targets): + if not self.spug_push_key: + make_no_push_key_notify() + return + data = { + 'token': self.spug_push_key, + 'targets': list(targets), + 'dataset': { + 'title': self.title, + 'target': self.target, + 'message': self.message, + 'duration': self.duration, + 'event': self.event + } + } + self.handle_request(f'{push_server}/spug/message/', data, 'spug') + def dispatch_monitor(self, modes): - self.u_ids = sum([json.loads(x.contacts) for x in Group.objects.filter(id__in=self.grp)], []) + u_ids, push_ids = [], [] + for item in Group.objects.filter(id__in=self.grp): + for x in json.loads(item.contacts): + if isinstance(x, str) and '_' in x: + push_ids.append(x) + else: + u_ids.append(x) + + targets = set() for mode in modes: if mode == '1': - users = set(x.wx_token for x in Contact.objects.filter(id__in=self.u_ids, wx_token__isnull=False)) + wx_mp_ids = set(x for x in push_ids if x.startswith('wx_mp_')) + targets.update(wx_mp_ids) + users = set(x.wx_token for x in Contact.objects.filter(id__in=u_ids, wx_token__isnull=False)) if not users: - Notify.make_monitor_notify('发送报警信息失败', '未找到可用的通知对象,请确保设置了相关报警联系人的微信Token。') + if not wx_mp_ids: + Notify.make_monitor_notify( + '发送报警信息失败', + '未找到可用的通知对象,请确保设置了相关报警联系人的微信Token。' + ) continue self.monitor_by_wx(users) + elif mode == '2': + sms_ids = set(x for x in push_ids if x.startswith('sms_')) + targets.update(sms_ids) elif mode == '3': - users = set(x.ding for x in Contact.objects.filter(id__in=self.u_ids, ding__isnull=False)) + users = set(x.ding for x in Contact.objects.filter(id__in=u_ids, ding__isnull=False)) if not users: - Notify.make_monitor_notify('发送报警信息失败', '未找到可用的通知对象,请确保设置了相关报警联系人的钉钉。') + Notify.make_monitor_notify( + '发送报警信息失败', + '未找到可用的通知对象,请确保设置了相关报警联系人的钉钉。' + ) continue self.monitor_by_dd(users) elif mode == '4': - users = set(x.email for x in Contact.objects.filter(id__in=self.u_ids, email__isnull=False)) + mail_ids = set(x for x in push_ids if x.startswith('mail_')) + targets.update(mail_ids) + users = set(x.email for x in Contact.objects.filter(id__in=u_ids, email__isnull=False)) if not users: - Notify.make_monitor_notify('发送报警信息失败', '未找到可用的通知对象,请确保设置了相关报警联系人的邮件地址。') + if not mail_ids: + Notify.make_monitor_notify( + '发送报警信息失败', + '未找到可用的通知对象,请确保设置了相关报警联系人的邮件地址。' + ) continue self.monitor_by_email(users) elif mode == '5': - users = set(x.qy_wx for x in Contact.objects.filter(id__in=self.u_ids, qy_wx__isnull=False)) + users = set(x.qy_wx for x in Contact.objects.filter(id__in=u_ids, qy_wx__isnull=False)) if not users: - Notify.make_monitor_notify('发送报警信息失败', '未找到可用的通知对象,请确保设置了相关报警联系人的企业微信。') + Notify.make_monitor_notify( + '发送报警信息失败', + '未找到可用的通知对象,请确保设置了相关报警联系人的企业微信。' + ) continue self.monitor_by_qy_wx(users) + elif mode == '6': + voice_ids = set(x for x in push_ids if x.startswith('voice_')) + targets.update(voice_ids) + + if targets: + self.monitor_by_spug_push(targets) diff --git a/spug_web/src/components/Link.js b/spug_web/src/components/Link.js new file mode 100644 index 0000000..86e422b --- /dev/null +++ b/spug_web/src/components/Link.js @@ -0,0 +1,15 @@ +/** + * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug + * Copyright (c) + * Released under the AGPL-3.0 License. + */ +import React from 'react' + + +function Link(props) { + return ( + {props.title} + ) +} + +export default Link \ No newline at end of file diff --git a/spug_web/src/components/index.js b/spug_web/src/components/index.js index e140894..d9a8b78 100644 --- a/spug_web/src/components/index.js +++ b/spug_web/src/components/index.js @@ -16,6 +16,7 @@ import TableCard from './TableCard'; import Breadcrumb from './Breadcrumb'; import AppSelector from './AppSelector'; import NotFound from './NotFound'; +import Link from './Link'; export { StatisticsCard, @@ -31,4 +32,5 @@ export { Breadcrumb, AppSelector, NotFound, + Link, } diff --git a/spug_web/src/pages/alarm/group/Form.js b/spug_web/src/pages/alarm/group/Form.js index 8604c65..af7c3f8 100644 --- a/spug_web/src/pages/alarm/group/Form.js +++ b/spug_web/src/pages/alarm/group/Form.js @@ -3,16 +3,24 @@ * Copyright (c) * Released under the AGPL-3.0 License. */ -import React, { useState } from 'react'; -import { observer } from 'mobx-react'; -import { Modal, Form, Input, Transfer, message } from 'antd'; +import React, {useEffect, useState} from 'react'; +import {observer} from 'mobx-react'; +import {Modal, Form, Input, Transfer, Spin, message} from 'antd'; import http from 'libs/http'; import store from './store'; -import contactStore from '../contact/store'; export default observer(function () { const [form] = Form.useForm(); + const [contacts, setContacts] = useState([]); const [loading, setLoading] = useState(false); + const [fetching, setFetching] = useState(false); + + useEffect(() => { + setFetching(true) + http.get('/api/alarm/contact/?with_push=1') + .then(res => setContacts(res)) + .finally(() => setFetching(false)) + }, []); function handleSubmit() { setLoading(true); @@ -42,14 +50,16 @@ export default observer(function () { - - item.id} - titles={['已有联系人', '已选联系人']} - listStyle={{width: 199}} - dataSource={contactStore.records} - render={item => item.name}/> - + + + item.id} + titles={['已有联系人', '已选联系人']} + listStyle={{width: 199}} + dataSource={contacts} + render={item => item.name}/> + + ) diff --git a/spug_web/src/pages/alarm/group/Table.js b/spug_web/src/pages/alarm/group/Table.js index 2b19ffa..838efd1 100644 --- a/spug_web/src/pages/alarm/group/Table.js +++ b/spug_web/src/pages/alarm/group/Table.js @@ -11,7 +11,6 @@ import { Action, TableCard, AuthButton } from 'components'; import { http, hasPermission } from 'libs'; import store from './store'; import contactStore from '../contact/store'; -import lds from 'lodash'; @observer class ComTable extends React.Component { @@ -76,8 +75,7 @@ class ComTable extends React.Component { pageSizeOptions: ['10', '20', '50', '100'] }}> - value.map(x => lds.get(this.state.contactMap, `${x}.name`)).join(',')}/> + `${value.length}个`}/> {hasPermission('alarm.group.edit|alarm.group.del') && ( ( diff --git a/spug_web/src/pages/monitor/Step2.js b/spug_web/src/pages/monitor/Step2.js index d4e666a..979792f 100644 --- a/spug_web/src/pages/monitor/Step2.js +++ b/spug_web/src/pages/monitor/Step2.js @@ -14,9 +14,10 @@ import lds from 'lodash'; const modeOptions = [ {label: '微信', 'value': '1'}, - {label: '短信', 'value': '2', disabled: true}, - {label: '钉钉', 'value': '3'}, + {label: '短信', 'value': '2'}, + {label: '电话', 'value': '6'}, {label: '邮件', 'value': '4'}, + {label: '钉钉', 'value': '3'}, {label: '企业微信', 'value': '5'}, ]; diff --git a/spug_web/src/pages/system/setting/PushSetting.js b/spug_web/src/pages/system/setting/PushSetting.js new file mode 100644 index 0000000..aa4fb08 --- /dev/null +++ b/spug_web/src/pages/system/setting/PushSetting.js @@ -0,0 +1,114 @@ +/** + * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug + * Copyright (c) + * Released under the AGPL-3.0 License. + */ +import React, {useEffect, useState} from 'react'; +import {observer} from 'mobx-react'; +import {Form, Input, Button, Spin, message} from 'antd'; +import {Link} from 'components'; +import css from './index.module.css'; +import {http, clsNames} from 'libs'; +import store from './store'; + +export default observer(function () { + const [loading, setLoading] = useState(false); + const [fetching, setFetching] = useState(false); + const [balance, setBalance] = useState({}) + + useEffect(() => { + if (store.settings.spug_push_key) { + fetchBalance() + } + }, []); + + function fetchBalance() { + setFetching(true) + http.get('/api/setting/balance/') + .then(res => setBalance(res)) + .finally(() => { + setLoading(false) + setFetching(false) + }) + } + + function handleBind() { + const spug_push_key = store.settings.spug_push_key + if (!spug_push_key) return message.error('请输入要绑定的推送助手用户ID') + setLoading(true); + http.post('/api/setting/', {data: [{key: 'spug_push_key', value: spug_push_key}]}) + .then(() => { + message.success('保存成功'); + store.fetchSettings(); + fetchBalance() + }) + .finally(() => store.loading = false) + } + + const isVip = true + return ( + +
推送服务设置
+
+ 请登录 ,至个人中心 / + 个人设置查看用户ID,注意保密该ID请勿泄漏给第三方。
}> + + + store.settings.spug_push_key = e.target.value} + style={{width: 'calc(100% - 100px)'}} + placeholder="请输入要绑定的推送助手用户ID"/> + + + {/**/} + {/* */} + {/* */} + {/**/} + + + + 如需充值请至 ,具体计费规则及说明请查看推送助手官网。 + }> +
+ +
+
+
短信余额
+
{balance.sms_balance}
+
+
+
语音余额
+
{balance.voice_balance}
+
+
+
邮件余额
+
{balance.mail_balance}
+ {isVip ? ( +
+ 会员免费20封 / 天
+ ) : ( +
会员免费20封 / 天
+ )} +
+
+
微信公众号余额
+
{balance.wx_mp_balance}
+ {isVip ? ( +
+ 会员免费100条 / 天
+ ) : ( +
会员免费20封 / 天
+ )} +
+
+
+
+
+
+ ) +}) \ No newline at end of file diff --git a/spug_web/src/pages/system/setting/index.js b/spug_web/src/pages/system/setting/index.js index c67d249..aee5bf0 100644 --- a/spug_web/src/pages/system/setting/index.js +++ b/spug_web/src/pages/system/setting/index.js @@ -12,6 +12,7 @@ import LDAPSetting from './LDAPSetting'; import OpenService from './OpenService'; import KeySetting from './KeySetting'; import SecuritySetting from './SecuritySetting'; +import PushSetting from './PushSetting'; import About from './About'; import styles from './index.module.css'; import store from './store'; @@ -49,6 +50,7 @@ class Index extends React.Component { 安全设置 LDAP设置 密钥设置 + 推送服务设置 报警服务设置 开放服务设置 关于 @@ -59,6 +61,7 @@ class Index extends React.Component { {selectedKeys[0] === 'security' && } {selectedKeys[0] === 'ldap' && } {selectedKeys[0] === 'alarm' && } + {selectedKeys[0] === 'push' && } {selectedKeys[0] === 'service' && } {selectedKeys[0] === 'key' && } {selectedKeys[0] === 'about' && } diff --git a/spug_web/src/pages/system/setting/index.module.css b/spug_web/src/pages/system/setting/index.module.css index c6b5c27..3f5c313 100644 --- a/spug_web/src/pages/system/setting/index.module.css +++ b/spug_web/src/pages/system/setting/index.module.css @@ -1,31 +1,117 @@ .container { - display: flex; - background-color: #fff; - padding: 16px 0; + display: flex; + background-color: #fff; + padding: 16px 0; } .left { - flex: 2; - border-right: 1px solid #e8e8e8; + flex: 2; + border-right: 1px solid #e8e8e8; } .right { - flex: 7; - padding: 8px 40px; + flex: 7; + padding: 8px 40px; } .title { - margin-bottom: 24px; - color: rgba(0, 0, 0, .85); - font-weight: 500; - font-size: 20px; - line-height: 28px; + margin-bottom: 24px; + color: rgba(0, 0, 0, .85); + font-weight: 500; + font-size: 20px; + line-height: 28px; } .form { - max-width: 320px; + max-width: 320px; } .keyText { - font-family: "Bitstream Vera Sans Mono", Monaco, "Courier New", Courier, monospace; -} \ No newline at end of file + font-family: "Bitstream Vera Sans Mono", Monaco, "Courier New", Courier, monospace; +} + +.statistic { + background: #fafafa; + border-radius: 4px; + + .body { + display: flex; + flex-direction: row; + + .item { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 180px; + height: 150px; + + &:nth-child(n+2) { + &:before { + content: ' '; + position: absolute; + top: 52px; + left: 0; + width: 1px; + height: 56px; + background: #CCCCCC; + opacity: 0.5; + } + } + + .title { + font-size: 14px; + color: #666666; + margin-bottom: 6px; + } + + .value { + font-size: 40px; + line-height: 46px; + color: #333333; + position: relative; + } + + .tips { + position: absolute; + bottom: 16px; + font-size: 11px; + color: rgba(0, 0, 0, 0.35); + background: rgba(0, 0, 0, 0.04); + border-radius: 10px; + line-height: 20px; + text-align: center; + padding: 0 8px; + cursor: pointer; + } + + .active { + cursor: initial; + background: #f7af40; + color: #ffffff; + } + } + + .buy { + position: absolute; + right: 51px; + top: 110px; + display: flex; + align-items: center; + justify-content: space-between; + height: 32px; + width: 96px; + padding: 0 16px 0 20px; + border-radius: 16px; + color: #2563fc; + font-size: 14px; + cursor: pointer; + + :global(.iconfont) { + font-size: 14px; + } + } + } +} +