# 继承推送服务

pull/639/head
vapao 2023-11-07 18:14:58 +08:00
parent cfe33658e8
commit d125112cd9
15 changed files with 398 additions and 51 deletions

View File

@ -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):

View File

@ -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)

View File

@ -16,6 +16,7 @@ KEYS_DEFAULT = {
'mail_service': {},
'private_key': None,
'public_key': None,
'spug_push_key': None,
}

View File

@ -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),
]

View File

@ -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)

26
spug_api/libs/push.py Normal file
View File

@ -0,0 +1,26 @@
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# 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 []

View File

@ -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)

View File

@ -0,0 +1,15 @@
/**
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
* Copyright (c) <spug.dev@gmail.com>
* Released under the AGPL-3.0 License.
*/
import React from 'react'
function Link(props) {
return (
<a target="_blank" rel="noopener noreferrer" href={props.href}>{props.title}</a>
)
}
export default Link

View File

@ -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,
}

View File

@ -3,16 +3,24 @@
* Copyright (c) <spug.dev@gmail.com>
* 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 () {
<Form.Item name="desc" label="备注信息">
<Input.TextArea placeholder="请输入备注信息"/>
</Form.Item>
<Form.Item required name="contacts" valuePropName="targetKeys" label="选择联系人">
<Transfer
rowKey={item => item.id}
titles={['已有联系人', '已选联系人']}
listStyle={{width: 199}}
dataSource={contactStore.records}
render={item => item.name}/>
</Form.Item>
<Spin spinning={fetching}>
<Form.Item required name="contacts" valuePropName="targetKeys" label="选择联系人">
<Transfer
rowKey={item => item.id}
titles={['已有联系人', '已选联系人']}
listStyle={{width: 199}}
dataSource={contacts}
render={item => item.name}/>
</Form.Item>
</Spin>
</Form>
</Modal>
)

View File

@ -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']
}}>
<Table.Column title="组名称" dataIndex="name"/>
<Table.Column ellipsis title="成员" dataIndex="contacts"
render={value => value.map(x => lds.get(this.state.contactMap, `${x}.name`)).join(',')}/>
<Table.Column ellipsis title="成员" dataIndex="contacts" render={value => `${value.length}`}/>
<Table.Column ellipsis title="描述信息" dataIndex="desc"/>
{hasPermission('alarm.group.edit|alarm.group.del') && (
<Table.Column title="操作" render={info => (

View File

@ -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'},
];

View File

@ -0,0 +1,114 @@
/**
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
* Copyright (c) <spug.dev@gmail.com>
* 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 (
<React.Fragment>
<div className={css.title}>推送服务设置</div>
<div style={{maxWidth: 340}}>
<Form.Item label="推送助手账户绑定" labelCol={{span: 24}} style={{marginTop: 12}}
extra={<div>请登录 <Link href="https://push.spug.cc/login" title="推送助手"/>至个人中心 /
个人设置查看用户ID注意保密该ID请勿泄漏给第三方</div>}>
<Input.Group compact>
<Input
value={store.settings.spug_push_key}
onChange={e => store.settings.spug_push_key = e.target.value}
style={{width: 'calc(100% - 100px)'}}
placeholder="请输入要绑定的推送助手用户ID"/>
<Button
type="primary"
style={{width: 80, marginLeft: 20}}
onClick={handleBind}
loading={loading}>确定</Button>
</Input.Group>
{/*<Input.Group compact>*/}
{/* <Input bordered={false} style={{width: 'calc(100% - 100px)', paddingLeft: 0}} value="32uu73******64823d"/>*/}
{/* <Button style={{width: 80, marginLeft: 20}}>解绑</Button>*/}
{/*</Input.Group>*/}
</Form.Item>
</div>
<Form.Item style={{marginTop: 24}}
extra={<div> 如需充值请至 <Link href="https://push.spug.cc/buy/sms" title="推送助手"/>具体计费规则及说明请查看推送助手官网
</div>}>
<div className={css.statistic}>
<Spin spinning={fetching}>
<div className={css.body}>
<div className={css.item}>
<div className={css.title}>短信余额</div>
<div className={css.value}>{balance.sms_balance}</div>
</div>
<div className={css.item}>
<div className={css.title}>语音余额</div>
<div className={css.value}>{balance.voice_balance}</div>
</div>
<div className={css.item}>
<div className={css.title}>邮件余额</div>
<div className={css.value}>{balance.mail_balance}</div>
{isVip ? (
<div className={clsNames(css.tips, css.active)}>+ 会员免费20封 / </div>
) : (
<div className={css.tips}>会员免费20封 / </div>
)}
</div>
<div className={css.item}>
<div className={css.title}>微信公众号余额</div>
<div className={css.value}>{balance.wx_mp_balance}</div>
{isVip ? (
<div className={clsNames(css.tips, css.active)}>+ 会员免费100条 / </div>
) : (
<div className={css.tips}>会员免费20封 / </div>
)}
</div>
</div>
</Spin>
</div>
</Form.Item>
</React.Fragment>
)
})

View File

@ -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 {
<Menu.Item key="security">安全设置</Menu.Item>
<Menu.Item key="ldap">LDAP设置</Menu.Item>
<Menu.Item key="key">密钥设置</Menu.Item>
<Menu.Item key="push">推送服务设置</Menu.Item>
<Menu.Item key="alarm">报警服务设置</Menu.Item>
<Menu.Item key="service">开放服务设置</Menu.Item>
<Menu.Item key="about">关于</Menu.Item>
@ -59,6 +61,7 @@ class Index extends React.Component {
{selectedKeys[0] === 'security' && <SecuritySetting/>}
{selectedKeys[0] === 'ldap' && <LDAPSetting/>}
{selectedKeys[0] === 'alarm' && <AlarmSetting/>}
{selectedKeys[0] === 'push' && <PushSetting/>}
{selectedKeys[0] === 'service' && <OpenService/>}
{selectedKeys[0] === 'key' && <KeySetting/>}
{selectedKeys[0] === 'about' && <About/>}

View File

@ -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;
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;
}
}
}
}