feat: add message template

This commit is contained in:
acgnhik 2022-06-18 16:25:23 +08:00
parent 1decffc266
commit 2a6498a910
58 changed files with 1498 additions and 332 deletions

View File

@ -35,6 +35,7 @@ package_dir =
include_package_data = True
python_requires = >= 3.8
install_requires =
python-liquid >= 1.2.1, < 2.0.0
typing-extensions >= 3.10.0.0
ordered-set >= 4.1.0, < 5.0.0
fastapi >= 0.70.0, < 0.71.0

View File

@ -24,9 +24,9 @@ from .setting import (
Settings,
SettingsIn,
SettingsOut,
KeySetOfSettings,
TaskOptions,
)
from .setting.typing import KeySetOfSettings
from .notification import (
EmailNotifier,
ServerchanNotifier,

View File

@ -0,0 +1,16 @@
<h2>异常信息:</h2>
<pre>
{{ event.data.detail }}
</pre>
<div>
<p>
<span><b>事件 ID</b></span>
<span>{{ event.id }}</span>
</p>
<p>
<span><b>事件时间</b></span>
<span>{{ event.date }}</span>
</p>
</div>

View File

@ -0,0 +1,70 @@
<p>
<span><b>主播</b></span>
<span>
<a
href="https://space.bilibili.com/{{ event.data.user_info.uid }}"
_blank
title="打开主播个人空间页面"
>{{ event.data.user_info.name }}</a
>
</span>
</p>
<p>
<span><b>标题</b></span>
<span>
<a
href="https://live.bilibili.com/{{ event.data.room_info.room_id }}"
_blank
title="打开直播间页面"
>{{ event.data.room_info.title }}</a
>
</span>
</p>
<p>
<span><b>分区</b></span>
<span>
<a
href="https://live.bilibili.com/p/eden/area-tags?parentAreaId={{
event.data.room_info.parent_area_id
}}&areaId={{ event.data.room_info.area_id }}"
_blank
title="打开直播分区页面"
>{{ event.data.room_info.parent_area_name }}·{{
event.data.room_info.area_name }}
</a>
</span>
</p>
<p>
<span><b>房间</b></span>
{%- if event.data.room_info.short_room_id > 0 -%}
<span>
<a
href="https://live.bilibili.com/{{ event.data.room_info.short_room_id }}"
target="_blank"
>
{{ event.data.room_info.short_room_id }}</a
>
</span>
<span>, </span>
{%- endif -%}
<span>
<a
href="https://live.bilibili.com/{{ event.data.room_info.room_id }}"
_blank
title="打开直播间页面"
>{{ event.data.room_info.room_id }}</a
>
</span>
</p>
<p>
<span><b>开播</b></span>
<span
>{{ event.data.room_info.live_start_time | datetimestring | date: "%Y-%m-%d
%H:%M:%S" }}</span
>
</p>
<div>
<a href="{{ event.data.room_info.cover }}" _blank title="打开封面图片">
<img src="{{ event.data.room_info.cover }}" />
</a>
</div>

View File

@ -0,0 +1,63 @@
<p>
<span><b>主播</b></span>
<span>
<a
href="https://space.bilibili.com/{{ event.data.user_info.uid }}"
_blank
title="打开主播个人空间页面"
>{{ event.data.user_info.name }}</a
>
</span>
</p>
<p>
<span><b>标题</b></span>
<span>
<a
href="https://live.bilibili.com/{{ event.data.room_info.room_id }}"
_blank
title="打开直播间页面"
>{{ event.data.room_info.title }}</a
>
</span>
</p>
<p>
<span><b>分区</b></span>
<span>
<a
href="https://live.bilibili.com/p/eden/area-tags?parentAreaId={{
event.data.room_info.parent_area_id
}}&areaId={{ event.data.room_info.area_id }}"
_blank
title="打开直播分区页面"
>{{ event.data.room_info.parent_area_name }}·{{
event.data.room_info.area_name }}
</a>
</span>
</p>
<p>
<span><b>房间</b></span>
{%- if event.data.room_info.short_room_id > 0 -%}
<span>
<a
href="https://live.bilibili.com/{{ event.data.room_info.short_room_id }}"
target="_blank"
>
{{ event.data.room_info.short_room_id }}</a
>
</span>
<span>, </span>
{%- endif -%}
<span>
<a
href="https://live.bilibili.com/{{ event.data.room_info.room_id }}"
_blank
title="打开直播间页面"
>{{ event.data.room_info.room_id }}</a
>
</span>
</p>
<div>
<a href="{{ event.data.room_info.cover }}" _blank title="打开封面图片">
<img src="{{ event.data.room_info.cover }}" />
</a>
</div>

View File

@ -0,0 +1,31 @@
<p>
<span><b>路径</b></span>
<span>{{ event.data.path }}</span>
</p>
<p>
<span><b>阈值</b></span>
<span>{{ event.data.threshold | naturalsize }}</span>
</p>
<p>
<span><b>硬盘容量</b></span>
<span>{{ event.data.usage.total | naturalsize }}</span>
</p>
<p>
<span><b>已用空间</b></span>
<span>{{ event.data.usage.used | naturalsize }}</span>
</p>
<p>
<span><b>可用空间</b></span>
<span>{{ event.data.usage.free | naturalsize }}</span>
</p>
<div>
<p>
<span><b>事件 ID</b></span>
<span>{{ event.id }}</span>
</p>
<p>
<span><b>事件时间</b></span>
<span>{{ event.date }}</span>
</p>
</div>

View File

@ -0,0 +1,5 @@
异常信息:
```python
{{ event.data.detail }}
```

View File

@ -0,0 +1,16 @@
主播:[{{ event.data.user_info.name }}](https://space.bilibili.com/{{ event.data.user_info.uid }})
标题:[{{ event.data.room_info.title }}](https://live.bilibili.com/{{ event.data.room_info.room_id }})
分区:[{{ event.data.room_info.parent_area_name }}·{{ event.data.room_info.area_name }}](https://live.bilibili.com/p/eden/area-tags?parentAreaId={{ event.data.room_info.parent_area_id }}&areaId={{ event.data.room_info.area_id }})
房间:[
{%- if event.data.room_info.short_room_id > 0 -%}
{{ event.data.room_info.short_room_id }}{% raw %}, {% endraw %}
{%- endif -%}
{{ event.data.room_info.room_id }}
](https://live.bilibili.com/{{ event.data.room_info.room_id }})
{% if event.data.room_info.live_start_time > 0 %}
开播:{{ event.data.room_info.live_start_time | datetimestring }}
{% endif %}
![直播间封面]({{ event.data.room_info.cover }})

View File

@ -0,0 +1,14 @@
主播:[{{ event.data.user_info.name }}](https://space.bilibili.com/{{ event.data.user_info.uid }})
标题:[{{ event.data.room_info.title }}](https://live.bilibili.com/{{ event.data.room_info.room_id }})
分区:[{{ event.data.room_info.parent_area_name }}·{{ event.data.room_info.area_name }}](https://live.bilibili.com/p/eden/area-tags?parentAreaId={{ event.data.room_info.parent_area_id }}&areaId={{ event.data.room_info.area_id }})
房间:[
{%- if event.data.room_info.short_room_id > 0 -%}
{{ event.data.room_info.short_room_id }}{% raw %}, {% endraw %}
{%- endif -%}
{{ event.data.room_info.room_id }}
](https://live.bilibili.com/{{ event.data.room_info.room_id }})
![直播间封面]({{ event.data.room_info.cover }})

View File

@ -0,0 +1,9 @@
路径:{{ event.data.path }}
阈值:{{ event.data.threshold | naturalsize }}
硬盘容量:{{ event.data.usage.total | naturalsize }}
已用空间:{{ event.data.usage.used | naturalsize }}
可用空间:{{ event.data.usage.free | naturalsize }}

View File

@ -0,0 +1,3 @@
异常信息:
{{ event.data.detail }}

View File

@ -0,0 +1,14 @@
主播:{{ event.data.user_info.name }}
标题:{{ event.data.room_info.title }}
分区:{{ event.data.room_info.parent_area_name }}·{{ event.data.room_info.area_name }}
房间:
{%- if event.data.room_info.short_room_id > 0 -%}
{{ event.data.room_info.short_room_id }}{% raw %}, {% endraw %}
{%- endif -%}
{{ event.data.room_info.room_id }}
{% if event.data.room_info.live_start_time > 0 %}
开播:{{ event.data.room_info.live_start_time | datetimestring }}
{% endif %}

View File

@ -0,0 +1,11 @@
主播:{{ event.data.user_info.name }}
标题:{{ event.data.room_info.title }}
分区:{{ event.data.room_info.parent_area_name }}·{{ event.data.room_info.area_name }}
房间:
{%- if event.data.room_info.short_room_id > 0 -%}
{{ event.data.room_info.short_room_id }}{% raw %}, {% endraw %}
{%- endif -%}
{{ event.data.room_info.room_id }}

View File

@ -0,0 +1,9 @@
路径:{{ event.data.path }}
阈值:{{ event.data.threshold | naturalsize }}
硬盘容量:{{ event.data.usage.total | naturalsize }}
已用空间:{{ event.data.usage.used | naturalsize }}
可用空间:{{ event.data.usage.free | naturalsize }}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -6,10 +6,10 @@
<link rel="icon" type="image/x-icon" href="assets/images/logo.png">
<link rel="manifest" href="manifest.webmanifest">
<meta name="theme-color" content="#1976d2">
<style>html,body{width:100%;height:100%}*,*:before,*:after{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{margin:0;color:#000000d9;font-size:14px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-variant:tabular-nums;line-height:1.5715;background-color:#fff;font-feature-settings:"tnum","tnum"}html{--antd-wave-shadow-color:#1890ff;--scroll-bar:0}</style><link rel="stylesheet" href="styles.1f581691b230dc4d.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.1f581691b230dc4d.css"></noscript></head>
<style>html,body{width:100%;height:100%}*,*:before,*:after{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{margin:0;color:#000000d9;font-size:14px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-variant:tabular-nums;line-height:1.5715;background-color:#fff;font-feature-settings:"tnum","tnum"}html{--antd-wave-shadow-color:#1890ff;--scroll-bar:0}</style><link rel="stylesheet" href="styles.2e152d608221c2ee.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.2e152d608221c2ee.css"></noscript></head>
<body>
<app-root></app-root>
<noscript>Please enable JavaScript to continue using this application.</noscript>
<script src="runtime.dc2f7d56c437cd78.js" type="module"></script><script src="polyfills.4b08448aee19bb22.js" type="module"></script><script src="main.411b4a979eb179f8.js" type="module"></script>
<script src="runtime.0ce129f346263990.js" type="module"></script><script src="polyfills.4b08448aee19bb22.js" type="module"></script><script src="main.888c50197ddf8040.js" type="module"></script>
</body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"configVersion": 1,
"timestamp": 1654692172512,
"timestamp": 1655391613431,
"index": "/index.html",
"assetGroups": [
{
@ -13,17 +13,17 @@
"urls": [
"/103.5b5d2a6e5a8a7479.js",
"/146.92e3b29c4c754544.js",
"/183.8cf3b5282412a0ec.js",
"/183.ae1a1102b7d5cbdb.js",
"/202.e15e5ae9f06639b8.js",
"/45.c90c3cea2bf1a66e.js",
"/474.7f6529972e383566.js",
"/66.31f5b9ae46ae9005.js",
"/66.9faa0b5a6adf9602.js",
"/common.858f777e9296e6f2.js",
"/index.html",
"/main.411b4a979eb179f8.js",
"/main.888c50197ddf8040.js",
"/manifest.webmanifest",
"/polyfills.4b08448aee19bb22.js",
"/runtime.dc2f7d56c437cd78.js",
"/styles.1f581691b230dc4d.css"
"/runtime.0ce129f346263990.js",
"/styles.2e152d608221c2ee.css"
],
"patterns": []
},
@ -1636,10 +1636,10 @@
"hashTable": {
"/103.5b5d2a6e5a8a7479.js": "cc0240f217015b6d4ddcc14f31fcc42e1c1c282a",
"/146.92e3b29c4c754544.js": "3824de681dd1f982ea69a065cdf54d7a1e781f4d",
"/183.8cf3b5282412a0ec.js": "bf93e3b9baf3d6c0eb5e320c4916d5bf540c4cb0",
"/183.ae1a1102b7d5cbdb.js": "6cb22d60b0a20214212e6050fbbf33926a4c1346",
"/202.e15e5ae9f06639b8.js": "62335dc98644969539760565ff9c3c472d304287",
"/45.c90c3cea2bf1a66e.js": "e5bfb8cf3803593e6b8ea14c90b3d3cb6a066764",
"/474.7f6529972e383566.js": "1c74b5c6379705a3110c99767f97feddc42a0d54",
"/66.31f5b9ae46ae9005.js": "cc22d2582d8e4c2a83e089d5a1ec32619e439ccd",
"/66.9faa0b5a6adf9602.js": "c2f418ebb80f35402d9f24e5acaf8167c96f9eb3",
"/assets/animal/panda.js": "fec2868bb3053dd2da45f96bbcb86d5116ed72b1",
"/assets/animal/panda.svg": "bebd302cdc601e0ead3a6d2710acf8753f3d83b1",
"/assets/fill/.gitkeep": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
@ -3234,12 +3234,12 @@
"/assets/twotone/warning.js": "fb2d7ea232f3a99bf8f080dbc94c65699232ac01",
"/assets/twotone/warning.svg": "8c7a2d3e765a2e7dd58ac674870c6655cecb0068",
"/common.858f777e9296e6f2.js": "b68ca68e1e214a2537d96935c23410126cc564dd",
"/index.html": "e7c15c095d8d2a11a60d7211e17e8a38316adf0e",
"/main.411b4a979eb179f8.js": "4c5e77b0589a77410f84441d0877c1d18cb1357f",
"/index.html": "374ebd2a9b656c5ebcbc9f5a4402b345cd4c7c5c",
"/main.888c50197ddf8040.js": "f506b85641a4598b002c21bc49c9a36e0c058326",
"/manifest.webmanifest": "62c1cb8c5ad2af551a956b97013ab55ce77dd586",
"/polyfills.4b08448aee19bb22.js": "8e73f2d42cc13ca353cea5c886d930bd6da08d0d",
"/runtime.dc2f7d56c437cd78.js": "fcf22060f48d6229236a14de31cc63c3568db8b1",
"/styles.1f581691b230dc4d.css": "6f5befbbad57c2b2e80aae855139744b8010d150"
"/runtime.0ce129f346263990.js": "98698b10b3f873a761f1e1c7fb5a9bcd2f3830ee",
"/styles.2e152d608221c2ee.css": "9830389a46daa5b4511e0dd343aad23ca9f9690f"
},
"navigationUrls": [
{

View File

@ -1 +1 @@
(()=>{"use strict";var e,v={},m={};function r(e){var i=m[e];if(void 0!==i)return i.exports;var t=m[e]={exports:{}};return v[e].call(t.exports,t,t.exports,r),t.exports}r.m=v,e=[],r.O=(i,t,o,f)=>{if(!t){var a=1/0;for(n=0;n<e.length;n++){for(var[t,o,f]=e[n],c=!0,l=0;l<t.length;l++)(!1&f||a>=f)&&Object.keys(r.O).every(b=>r.O[b](t[l]))?t.splice(l--,1):(c=!1,f<a&&(a=f));if(c){e.splice(n--,1);var d=o();void 0!==d&&(i=d)}}return i}f=f||0;for(var n=e.length;n>0&&e[n-1][2]>f;n--)e[n]=e[n-1];e[n]=[t,o,f]},r.n=e=>{var i=e&&e.__esModule?()=>e.default:()=>e;return r.d(i,{a:i}),i},r.d=(e,i)=>{for(var t in i)r.o(i,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:i[t]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((i,t)=>(r.f[t](e,i),i),[])),r.u=e=>(592===e?"common":e)+"."+{45:"c90c3cea2bf1a66e",66:"31f5b9ae46ae9005",103:"5b5d2a6e5a8a7479",146:"92e3b29c4c754544",183:"8cf3b5282412a0ec",474:"7f6529972e383566",592:"858f777e9296e6f2"}[e]+".js",r.miniCssF=e=>{},r.o=(e,i)=>Object.prototype.hasOwnProperty.call(e,i),(()=>{var e={},i="blrec:";r.l=(t,o,f,n)=>{if(e[t])e[t].push(o);else{var a,c;if(void 0!==f)for(var l=document.getElementsByTagName("script"),d=0;d<l.length;d++){var u=l[d];if(u.getAttribute("src")==t||u.getAttribute("data-webpack")==i+f){a=u;break}}a||(c=!0,(a=document.createElement("script")).type="module",a.charset="utf-8",a.timeout=120,r.nc&&a.setAttribute("nonce",r.nc),a.setAttribute("data-webpack",i+f),a.src=r.tu(t)),e[t]=[o];var s=(g,b)=>{a.onerror=a.onload=null,clearTimeout(p);var _=e[t];if(delete e[t],a.parentNode&&a.parentNode.removeChild(a),_&&_.forEach(h=>h(b)),g)return g(b)},p=setTimeout(s.bind(null,void 0,{type:"timeout",target:a}),12e4);a.onerror=s.bind(null,a.onerror),a.onload=s.bind(null,a.onload),c&&document.head.appendChild(a)}}})(),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var e;r.tu=i=>(void 0===e&&(e={createScriptURL:t=>t},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("angular#bundler",e))),e.createScriptURL(i))})(),r.p="",(()=>{var e={666:0};r.f.j=(o,f)=>{var n=r.o(e,o)?e[o]:void 0;if(0!==n)if(n)f.push(n[2]);else if(666!=o){var a=new Promise((u,s)=>n=e[o]=[u,s]);f.push(n[2]=a);var c=r.p+r.u(o),l=new Error;r.l(c,u=>{if(r.o(e,o)&&(0!==(n=e[o])&&(e[o]=void 0),n)){var s=u&&("load"===u.type?"missing":u.type),p=u&&u.target&&u.target.src;l.message="Loading chunk "+o+" failed.\n("+s+": "+p+")",l.name="ChunkLoadError",l.type=s,l.request=p,n[1](l)}},"chunk-"+o,o)}else e[o]=0},r.O.j=o=>0===e[o];var i=(o,f)=>{var l,d,[n,a,c]=f,u=0;if(n.some(p=>0!==e[p])){for(l in a)r.o(a,l)&&(r.m[l]=a[l]);if(c)var s=c(r)}for(o&&o(f);u<n.length;u++)r.o(e,d=n[u])&&e[d]&&e[d][0](),e[n[u]]=0;return r.O(s)},t=self.webpackChunkblrec=self.webpackChunkblrec||[];t.forEach(i.bind(null,0)),t.push=i.bind(null,t.push.bind(t))})()})();
(()=>{"use strict";var e,v={},m={};function r(e){var i=m[e];if(void 0!==i)return i.exports;var t=m[e]={exports:{}};return v[e].call(t.exports,t,t.exports,r),t.exports}r.m=v,e=[],r.O=(i,t,o,f)=>{if(!t){var a=1/0;for(n=0;n<e.length;n++){for(var[t,o,f]=e[n],c=!0,l=0;l<t.length;l++)(!1&f||a>=f)&&Object.keys(r.O).every(p=>r.O[p](t[l]))?t.splice(l--,1):(c=!1,f<a&&(a=f));if(c){e.splice(n--,1);var d=o();void 0!==d&&(i=d)}}return i}f=f||0;for(var n=e.length;n>0&&e[n-1][2]>f;n--)e[n]=e[n-1];e[n]=[t,o,f]},r.n=e=>{var i=e&&e.__esModule?()=>e.default:()=>e;return r.d(i,{a:i}),i},r.d=(e,i)=>{for(var t in i)r.o(i,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:i[t]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((i,t)=>(r.f[t](e,i),i),[])),r.u=e=>(592===e?"common":e)+"."+{45:"c90c3cea2bf1a66e",66:"9faa0b5a6adf9602",103:"5b5d2a6e5a8a7479",146:"92e3b29c4c754544",183:"ae1a1102b7d5cbdb",202:"e15e5ae9f06639b8",592:"858f777e9296e6f2"}[e]+".js",r.miniCssF=e=>{},r.o=(e,i)=>Object.prototype.hasOwnProperty.call(e,i),(()=>{var e={},i="blrec:";r.l=(t,o,f,n)=>{if(e[t])e[t].push(o);else{var a,c;if(void 0!==f)for(var l=document.getElementsByTagName("script"),d=0;d<l.length;d++){var u=l[d];if(u.getAttribute("src")==t||u.getAttribute("data-webpack")==i+f){a=u;break}}a||(c=!0,(a=document.createElement("script")).type="module",a.charset="utf-8",a.timeout=120,r.nc&&a.setAttribute("nonce",r.nc),a.setAttribute("data-webpack",i+f),a.src=r.tu(t)),e[t]=[o];var s=(g,p)=>{a.onerror=a.onload=null,clearTimeout(b);var _=e[t];if(delete e[t],a.parentNode&&a.parentNode.removeChild(a),_&&_.forEach(h=>h(p)),g)return g(p)},b=setTimeout(s.bind(null,void 0,{type:"timeout",target:a}),12e4);a.onerror=s.bind(null,a.onerror),a.onload=s.bind(null,a.onload),c&&document.head.appendChild(a)}}})(),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var e;r.tu=i=>(void 0===e&&(e={createScriptURL:t=>t},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("angular#bundler",e))),e.createScriptURL(i))})(),r.p="",(()=>{var e={666:0};r.f.j=(o,f)=>{var n=r.o(e,o)?e[o]:void 0;if(0!==n)if(n)f.push(n[2]);else if(666!=o){var a=new Promise((u,s)=>n=e[o]=[u,s]);f.push(n[2]=a);var c=r.p+r.u(o),l=new Error;r.l(c,u=>{if(r.o(e,o)&&(0!==(n=e[o])&&(e[o]=void 0),n)){var s=u&&("load"===u.type?"missing":u.type),b=u&&u.target&&u.target.src;l.message="Loading chunk "+o+" failed.\n("+s+": "+b+")",l.name="ChunkLoadError",l.type=s,l.request=b,n[1](l)}},"chunk-"+o,o)}else e[o]=0},r.O.j=o=>0===e[o];var i=(o,f)=>{var l,d,[n,a,c]=f,u=0;if(n.some(b=>0!==e[b])){for(l in a)r.o(a,l)&&(r.m[l]=a[l]);if(c)var s=c(r)}for(o&&o(f);u<n.length;u++)r.o(e,d=n[u])&&e[d]&&e[d][0](),e[n[u]]=0;return r.O(s)},t=self.webpackChunkblrec=self.webpackChunkblrec||[];t.forEach(i.bind(null,0)),t.push=i.bind(null,t.push.bind(t))})()})();

View File

@ -1,61 +0,0 @@
import humanize
from ..event import SpaceNoEnoughEventData
from ..bili.models import UserInfo, RoomInfo
from ..exception import format_exception
live_info_template = """
主播: {user_name}
标题: {room_title}
分区: {area_name}
房间: {room_id}
"""
disk_usage_template = """
路径: {path}
阈值: {threshold}
硬盘容量: {usage_total}
已用空间: {usage_used}
可用空间: {usage_free}
"""
exception_template = """
异常信息
{exception_message}
"""
def make_live_info_content(user_info: UserInfo, room_info: RoomInfo) -> str:
return live_info_template.format(
user_name=user_info.name,
room_title=room_info.title,
area_name=room_info.area_name,
room_id=room_info.room_id,
)
def make_disk_usage_content(data: SpaceNoEnoughEventData) -> str:
return disk_usage_template.format(
path=data.path,
threshold=humanize.naturalsize(data.threshold),
usage_total=humanize.naturalsize(data.usage.total),
usage_used=humanize.naturalsize(data.usage.used),
usage_free=humanize.naturalsize(data.usage.free),
)
def make_exception_content(exc: BaseException) -> str:
return exception_template.format(
exception_message=format_exception(exc),
)

View File

@ -1,37 +1,42 @@
import logging
import asyncio
import logging
import os
from abc import ABC, abstractmethod
from datetime import datetime
from typing import Final, Optional, Tuple
import attr
import humanize
from liquid import Environment
from liquid.filter import math_filter
from pkg_resources import resource_string
from tenacity import (
AsyncRetrying,
wait_exponential,
stop_after_delay,
retry_if_exception,
stop_after_delay,
wait_exponential,
)
from .providers import (
EmailService,
MessagingProvider,
Serverchan,
Pushdeer,
Pushplus,
Telegram,
)
from .message import (
make_live_info_content,
make_disk_usage_content,
make_exception_content,
)
from ..utils.mixins import SwitchableMixin
from ..exception import ExceptionCenter
from ..event import (
Error,
ErrorData,
EventCenter,
LiveBeganEvent,
LiveEndedEvent,
SpaceNoEnoughEvent,
)
from ..event.typing import Event
from ..exception import ExceptionCenter, format_exception
from ..setting.typing import MessageType
from ..utils.mixins import SwitchableMixin
from .providers import (
EmailService,
MessagingProvider,
Pushdeer,
Pushplus,
Serverchan,
Telegram,
)
__all__ = (
'Notifier',
@ -55,12 +60,45 @@ class Notifier(SwitchableMixin, ABC):
notify_ended: bool = False,
notify_space: bool = False,
notify_error: bool = False,
began_message_type: MessageType = 'text',
began_message_title: str = '',
began_message_content: str = '',
ended_message_type: MessageType = 'text',
ended_message_title: str = '',
ended_message_content: str = '',
space_message_type: MessageType = 'text',
space_message_title: str = '',
space_message_content: str = '',
error_message_type: MessageType = 'text',
error_message_title: str = '',
error_message_content: str = '',
) -> None:
super().__init__()
self._liquid_env = Environment()
self._liquid_env.add_filter('intcomma', math_filter(humanize.intcomma))
self._liquid_env.add_filter('naturalsize', math_filter(humanize.naturalsize))
self._liquid_env.add_filter(
'datetimestring',
math_filter(lambda n: datetime.fromtimestamp(n).isoformat()),
)
self.notify_began = notify_began
self.notify_ended = notify_ended
self.notify_space = notify_space
self.notify_error = notify_error
self.began_message_type = began_message_type
self.began_message_title = began_message_title
self.began_message_content = began_message_content
self.ended_message_type = ended_message_type
self.ended_message_title = ended_message_title
self.ended_message_content = ended_message_content
self.space_message_type = space_message_type
self.space_message_title = space_message_title
self.space_message_content = space_message_content
self.error_message_type = error_message_type
self.error_message_title = error_message_title
self.error_message_content = error_message_content
def _do_enable(self) -> None:
events = EventCenter.get_instance().events
@ -87,7 +125,8 @@ class Notifier(SwitchableMixin, ABC):
def _on_exception(self, exc: BaseException) -> None:
if self._should_notify_exception(exc):
self._notify_exception(exc)
error = Error.from_data(ErrorData.from_exc(exc))
self._notify_exception(error)
def _should_notify_live_began(self, event: LiveBeganEvent) -> bool:
return self.notify_began
@ -95,9 +134,7 @@ class Notifier(SwitchableMixin, ABC):
def _should_notify_live_ended(self, event: LiveEndedEvent) -> bool:
return self.notify_ended
def _should_notify_space_no_enough(
self, event: SpaceNoEnoughEvent
) -> bool:
def _should_notify_space_no_enough(self, event: SpaceNoEnoughEvent) -> bool:
return self.notify_space
def _should_notify_exception(self, exc: BaseException) -> bool:
@ -116,56 +153,184 @@ class Notifier(SwitchableMixin, ABC):
raise NotImplementedError
@abstractmethod
def _notify_exception(self, exc: BaseException) -> None:
def _notify_exception(self, event: Error) -> None:
raise NotImplementedError
@abstractmethod
def _make_began_message(self, event: LiveBeganEvent) -> Tuple[str, str]:
raise NotImplementedError
@abstractmethod
def _make_ended_message(self, event: LiveEndedEvent) -> Tuple[str, str]:
raise NotImplementedError
@abstractmethod
def _make_space_message(self, event: SpaceNoEnoughEvent) -> Tuple[str, str]:
raise NotImplementedError
@abstractmethod
def _make_error_message(self, event: Error) -> Tuple[str, str]:
raise NotImplementedError
BEGAN_MESSAGE_TITLE: Final[str] = '{{ event.data.user_info.name }} 开播啦'
ENDED_MESSAGE_TITLE: Final[str] = '{{ event.data.user_info.name }} 下播了'
SPACE_MESSAGE_TITLE: Final[str] = '空间不足!'
ERROR_MESSAGE_TITLE: Final[str] = '出错了~'
class MessageNotifier(Notifier, ABC):
provider: MessagingProvider
def _notify_live_began(self, event: LiveBeganEvent) -> None:
title = f'{event.data.user_info.name} 开播啦'
content = make_live_info_content(
event.data.user_info, event.data.room_info
)
self._send_message(title, content)
title, content = self._make_began_message(event)
self._send_message(title, content, self.began_message_type)
def _notify_live_ended(self, event: LiveEndedEvent) -> None:
title = f'{event.data.user_info.name} 下播了'
content = make_live_info_content(
event.data.user_info, event.data.room_info
)
self._send_message(title, content)
title, content = self._make_ended_message(event)
self._send_message(title, content, self.ended_message_type)
def _notify_space_no_enough(self, event: SpaceNoEnoughEvent) -> None:
title = '空间不足!'
content = make_disk_usage_content(event.data)
self._send_message(title, content)
title, content = self._make_space_message(event)
self._send_message(title, content, self.space_message_type)
def _notify_exception(self, exc: BaseException) -> None:
title = '出错了~'
content = make_exception_content(exc)
self._send_message(title, content)
def _notify_exception(self, event: Error) -> None:
title, content = self._make_error_message(event)
self._send_message(title, content, self.error_message_type)
def _send_message(self, title: str, content: str) -> None:
asyncio.create_task(self._send_message_async(title, content))
def _make_began_message(self, event: LiveBeganEvent) -> Tuple[str, str]:
env = os.environ.copy()
try:
template = self._liquid_env.from_string(self._get_began_message_title())
title = template.render(event=attr.asdict(event), env=env)
template = self._liquid_env.from_string(self._get_began_message_content())
content = template.render(event=attr.asdict(event), env=env)
except Exception as e:
logger.warning(f'Failed to render began message template: {repr(e)}')
title = f'{event.data.user_info.name} 开播啦'
content = format_exception(e)
return title, content
async def _send_message_async(self, title: str, content: str) -> None:
def _make_ended_message(self, event: LiveEndedEvent) -> Tuple[str, str]:
env = os.environ.copy()
try:
template = self._liquid_env.from_string(self._get_ended_message_title())
title = template.render(event=attr.asdict(event), env=env)
template = self._liquid_env.from_string(self._get_ended_message_content())
content = template.render(event=attr.asdict(event), env=env)
except Exception as e:
logger.warning(f'Failed to render ended message template: {repr(e)}')
title = f'{event.data.user_info.name} 下播了'
content = format_exception(e)
return title, content
def _make_space_message(self, event: SpaceNoEnoughEvent) -> Tuple[str, str]:
env = os.environ.copy()
try:
template = self._liquid_env.from_string(self._get_space_message_title())
title = template.render(event=attr.asdict(event), env=env)
template = self._liquid_env.from_string(self._get_space_message_content())
content = template.render(event=attr.asdict(event), env=env)
except Exception as e:
logger.warning(f'Failed to render space message template: {repr(e)}')
title = '空间不足!'
content = format_exception(e)
return title, content
def _make_error_message(self, event: Error) -> Tuple[str, str]:
env = os.environ.copy()
try:
template = self._liquid_env.from_string(self._get_error_message_title())
title = template.render(event=attr.asdict(event), env=env)
template = self._liquid_env.from_string(self._get_error_message_content())
content = template.render(event=attr.asdict(event), env=env)
except Exception as e:
logger.warning(f'Failed to render error message template: {repr(e)}')
title = '出错了~'
content = format_exception(e)
return title, content
def _send_message(self, title: str, content: str, msg_type: MessageType) -> None:
asyncio.create_task(self._send_message_async(title, content, msg_type))
async def _send_message_async(
self, title: str, content: str, msg_type: MessageType
) -> None:
try:
async for attempt in AsyncRetrying(
reraise=True,
stop=stop_after_delay(300),
wait=wait_exponential(multiplier=0.1, max=10),
retry=retry_if_exception(
lambda e: not isinstance(e, ValueError)
),
retry=retry_if_exception(lambda e: not isinstance(e, ValueError)),
):
with attempt:
await self.provider.send_message(title, content)
await self.provider.send_message(title, content, msg_type)
except Exception as e:
logger.warning('Failed to send a message via {}: {}'.format(
self.provider.__class__.__name__, repr(e)
))
logger.warning(
'Failed to send a message via {}: {}'.format(
self.provider.__class__.__name__, repr(e)
)
)
def _get_began_message_title(self) -> str:
return self.began_message_title or BEGAN_MESSAGE_TITLE
def _get_began_message_content(self, msg_type: Optional[MessageType] = None) -> str:
if self.began_message_content:
return self.began_message_content
msg_type = msg_type or self.began_message_type
if msg_type == 'markdown':
relpath = '../data/message_templates/markdown/live-began.md'
elif msg_type == 'html':
relpath = '../data/message_templates/html/live-began.html'
else:
relpath = '../data/message_templates/text/live-began.txt'
return resource_string(__name__, relpath).decode('utf-8')
def _get_ended_message_title(self) -> str:
return self.ended_message_title or ENDED_MESSAGE_TITLE
def _get_ended_message_content(self, msg_type: Optional[MessageType] = None) -> str:
if self.ended_message_content:
return self.ended_message_content
msg_type = msg_type or self.ended_message_type
if msg_type == 'markdown':
relpath = '../data/message_templates/markdown/live-ended.md'
elif msg_type == 'html':
relpath = '../data/message_templates/html/live-ended.html'
else:
relpath = '../data/message_templates/text/live-ended.txt'
return resource_string(__name__, relpath).decode('utf-8')
def _get_space_message_title(self) -> str:
return self.space_message_title or SPACE_MESSAGE_TITLE
def _get_space_message_content(self, msg_type: Optional[MessageType] = None) -> str:
if self.space_message_content:
return self.space_message_content
msg_type = msg_type or self.space_message_type
if msg_type == 'markdown':
relpath = '../data/message_templates/markdown/space-no-enough.md'
elif msg_type == 'html':
relpath = '../data/message_templates/html/space-no-enough.html'
else:
relpath = '../data/message_templates/text/space-no-enough.txt'
return resource_string(__name__, relpath).decode('utf-8')
def _get_error_message_title(self) -> str:
return self.error_message_title or ERROR_MESSAGE_TITLE
def _get_error_message_content(self, msg_type: Optional[MessageType] = None) -> str:
if self.error_message_content:
return self.error_message_content
msg_type = msg_type or self.error_message_type
if msg_type == 'markdown':
relpath = '../data/message_templates/markdown/error.md'
elif msg_type == 'html':
relpath = '../data/message_templates/html/error.html'
else:
relpath = '../data/message_templates/text/error.txt'
return resource_string(__name__, relpath).decode('utf-8')
class EmailNotifier(MessageNotifier):
@ -226,3 +391,15 @@ class TelegramNotifier(MessageNotifier):
def _do_disable(self) -> None:
super()._do_disable()
logger.debug('Disabled Telegram notifier')
def _get_began_message_content(self, msg_type: Optional[MessageType] = None) -> str:
return super()._get_began_message_content(msg_type='text')
def _get_ended_message_content(self, msg_type: Optional[MessageType] = None) -> str:
return super()._get_ended_message_content(msg_type='text')
def _get_space_message_content(self, msg_type: Optional[MessageType] = None) -> str:
return super()._get_space_message_content(msg_type='text')
def _get_error_message_content(self, msg_type: Optional[MessageType] = None) -> str:
return super()._get_error_message_content(msg_type='text')

View File

@ -5,11 +5,19 @@ import ssl
from abc import ABC, abstractmethod
from email.message import EmailMessage
from http.client import HTTPException
from typing import Final, Literal, TypedDict, Dict, Any, cast
from typing import Any, Dict, Final, TypedDict, cast
from urllib.parse import urljoin
import aiohttp
from ..setting.typing import (
EmailMessageType,
MessageType,
PushdeerMessageType,
PushplusMessageType,
ServerchanMessageType,
TelegramMessageType,
)
from ..utils.patterns import Singleton
__all__ = (
@ -30,13 +38,12 @@ class MessagingProvider(Singleton, ABC):
super().__init__()
@abstractmethod
async def send_message(self, title: str, content: str) -> None:
async def send_message(
self, title: str, content: str, msg_type: MessageType
) -> None:
...
MSG_TYPE = Literal['plain', 'html']
class EmailService(MessagingProvider):
def __init__(
self,
@ -54,7 +61,7 @@ class EmailService(MessagingProvider):
self.smtp_port = smtp_port
async def send_message(
self, subject: str, content: str, msg_type: MSG_TYPE = 'plain'
self, subject: str, content: str, msg_type: MessageType
) -> None:
self._check_parameters()
await asyncio.get_running_loop().run_in_executor(
@ -62,13 +69,14 @@ class EmailService(MessagingProvider):
)
def _send_email(
self, subject: str, content: str, msg_type: MSG_TYPE = 'plain'
self, subject: str, content: str, msg_type: EmailMessageType
) -> None:
msg = EmailMessage()
msg['Subject'] = subject
msg['From'] = self.src_addr
msg['To'] = self.dst_addr
msg.set_content(content, subtype=msg_type, charset='utf-8')
subtype = 'html' if msg_type == 'html' else 'plain'
msg.set_content(content, subtype=subtype, charset='utf-8')
try:
with smtplib.SMTP_SSL(self.smtp_host, self.smtp_port) as smtp:
@ -97,15 +105,19 @@ class Serverchan(MessagingProvider):
super().__init__()
self.sendkey = sendkey
async def send_message(self, title: str, content: str) -> None:
async def send_message(
self, title: str, content: str, msg_type: MessageType
) -> None:
self._check_parameters()
await self._post_message(title, content)
await self._post_message(title, content, cast(ServerchanMessageType, msg_type))
def _check_parameters(self) -> None:
if not self.sendkey:
raise ValueError('No sendkey supplied')
async def _post_message(self, title: str, content: str) -> None:
async def _post_message(
self, title: str, content: str, msg_type: ServerchanMessageType
) -> None:
url = f'https://sctapi.ftqq.com/{self.sendkey}.send'
payload = {'text': title, 'desp': content}
@ -129,21 +141,25 @@ class Pushdeer(MessagingProvider):
self.server = server
self.pushkey = pushkey
async def send_message(self, title: str, content: str) -> None:
async def send_message(
self, title: str, content: str, msg_type: MessageType
) -> None:
self._check_parameters()
await self._post_message(title, content)
await self._post_message(title, content, cast(PushdeerMessageType, msg_type))
def _check_parameters(self) -> None:
if not self.pushkey:
raise ValueError('No pushkey supplied')
async def _post_message(self, title: str, content: str) -> None:
async def _post_message(
self, title: str, content: str, msg_type: PushdeerMessageType
) -> None:
url = urljoin(self.server or self._server, self._endpoint)
payload = {
'pushkey': self.pushkey,
'text': title,
'desp': content,
'type': 'text',
'type': msg_type,
}
async with aiohttp.ClientSession(raise_for_status=True) as session:
async with session.post(url, json=payload) as res:
@ -166,21 +182,25 @@ class Pushplus(MessagingProvider):
self.token = token
self.topic = topic
async def send_message(self, title: str, content: str) -> None:
async def send_message(
self, title: str, content: str, msg_type: MessageType
) -> None:
self._check_parameters()
await self._post_message(title, content)
await self._post_message(title, content, msg_type)
def _check_parameters(self) -> None:
if not self.token:
raise ValueError('No token supplied')
async def _post_message(self, title: str, content: str) -> None:
async def _post_message(
self, title: str, content: str, msg_type: PushplusMessageType
) -> None:
payload = {
'title': title,
'content': content,
'token': self.token,
'topic': self.topic,
'template': 'html',
'template': msg_type,
}
async with aiohttp.ClientSession(raise_for_status=True) as session:
@ -201,9 +221,11 @@ class Telegram(MessagingProvider):
self.token = token
self.chatid = chatid
async def send_message(self, title: str, content: str) -> None:
async def send_message(
self, title: str, content: str, msg_type: MessageType
) -> None:
self._check_parameters()
await self._post_message(title, content)
await self._post_message(title, content, cast(TelegramMessageType, msg_type))
def _check_parameters(self) -> None:
if not self.token:
@ -211,12 +233,14 @@ class Telegram(MessagingProvider):
if not self.chatid:
raise ValueError('No chatid supplied')
async def _post_message(self, title: str, content: str) -> None:
async def _post_message(
self, title: str, content: str, msg_type: TelegramMessageType
) -> None:
url = f'https://api.telegram.org/bot{self.token}/sendMessage'
payload = {
'chat_id': self.chatid,
'text': title + '\n' + content,
'parse_mode': 'HTML',
'text': title + '\n\n' + content,
'parse_mode': 'MarkdownV2' if msg_type == 'markdown' else 'HTML',
}
async with aiohttp.ClientSession(raise_for_status=True) as session:

View File

@ -1,55 +1,50 @@
from .helpers import shadow_settings, update_settings
from .models import (
DEFAULT_SETTINGS_FILE,
DanmakuOptions,
DanmakuSettings,
EmailMessageTemplateSettings,
EmailNotificationSettings,
EmailSettings,
EnvSettings,
HeaderOptions,
HeaderSettings,
LoggingSettings,
NotificationSettings,
NotifierSettings,
OutputSettings,
PostprocessingOptions,
PostprocessingSettings,
PushdeerMessageTemplateSettings,
PushdeerNotificationSettings,
PushdeerSettings,
PushplusMessageTemplateSettings,
PushplusNotificationSettings,
PushplusSettings,
RecorderOptions,
RecorderSettings,
ServerchanMessageTemplateSettings,
ServerchanNotificationSettings,
ServerchanSettings,
Settings,
SettingsIn,
SettingsOut,
HeaderOptions,
HeaderSettings,
DanmakuOptions,
DanmakuSettings,
RecorderOptions,
RecorderSettings,
PostprocessingSettings,
PostprocessingOptions,
SpaceSettings,
TaskOptions,
TaskSettings,
OutputSettings,
LoggingSettings,
SpaceSettings,
EmailSettings,
ServerchanSettings,
PushdeerSettings,
PushplusSettings,
TelegramSettings,
NotifierSettings,
NotificationSettings,
EmailNotificationSettings,
ServerchanNotificationSettings,
PushdeerNotificationSettings,
PushplusNotificationSettings,
TelegramMessageTemplateSettings,
TelegramNotificationSettings,
TelegramSettings,
WebHookSettings,
)
from .typing import KeyOfSettings, KeySetOfSettings
from .helpers import update_settings, shadow_settings
from .setting_manager import SettingsManager
__all__ = (
'DEFAULT_SETTINGS_FILE',
'EnvSettings',
'Settings',
'SettingsIn',
'SettingsOut',
'KeyOfSettings',
'KeySetOfSettings',
'HeaderOptions',
'HeaderSettings',
'DanmakuOptions',
@ -58,12 +53,16 @@ __all__ = (
'RecorderSettings',
'PostprocessingSettings',
'PostprocessingOptions',
'TaskOptions',
'TaskSettings',
'OutputSettings',
'LoggingSettings',
'SpaceSettings',
'EmailMessageTemplateSettings',
'ServerchanMessageTemplateSettings',
'PushdeerMessageTemplateSettings',
'PushplusMessageTemplateSettings',
'TelegramMessageTemplateSettings',
'EmailSettings',
'ServerchanSettings',
'PushdeerSettings',
@ -77,7 +76,6 @@ __all__ = (
'PushplusNotificationSettings',
'TelegramNotificationSettings',
'WebHookSettings',
'update_settings',
'shadow_settings',
'SettingsManager',

View File

@ -1,41 +1,38 @@
from __future__ import annotations
import logging
import os
import re
import logging
from typing_extensions import Annotated
from typing import (
ClassVar,
Collection,
Final,
List,
Optional,
TypeVar,
)
from typing import ClassVar, Collection, Final, List, Optional, TypeVar
import toml
from pydantic import BaseModel as PydanticBaseModel
from pydantic import Field, BaseSettings, validator, PrivateAttr
from pydantic.networks import HttpUrl, EmailStr
from pydantic import BaseSettings, Field, PrivateAttr, validator
from pydantic.networks import EmailStr, HttpUrl
from typing_extensions import Annotated
from ..bili.typing import StreamFormat, QualityNumber
from ..postprocess import DeleteStrategy
from ..bili.typing import QualityNumber, StreamFormat
from ..core.cover_downloader import CoverSaveStrategy
from ..logging.typing import LOG_LEVEL
from ..postprocess import DeleteStrategy
from ..utils.string import camel_case
from .typing import (
EmailMessageType,
PushdeerMessageType,
PushplusMessageType,
ServerchanMessageType,
TelegramMessageType,
)
logger = logging.getLogger(__name__)
__all__ = (
'DEFAULT_SETTINGS_FILE',
'EnvSettings',
'Settings',
'SettingsIn',
'SettingsOut',
'HeaderOptions',
'HeaderSettings',
'DanmakuOptions',
@ -44,7 +41,6 @@ __all__ = (
'RecorderSettings',
'PostprocessingSettings',
'PostprocessingOptions',
'TaskOptions',
'TaskSettings',
'OutputSettings',
@ -57,6 +53,11 @@ __all__ = (
'TelegramSettings',
'NotifierSettings',
'NotificationSettings',
'EmailMessageTemplateSettings',
'ServerchanMessageTemplateSettings',
'PushdeerMessageTemplateSettings',
'PushplusMessageTemplateSettings',
'TelegramMessageTemplateSettings',
'EmailNotificationSettings',
'ServerchanNotificationSettings',
'PushdeerNotificationSettings',
@ -67,9 +68,7 @@ __all__ = (
DEFAULT_OUT_DIR: Final[str] = os.environ.get('DEFAULT_OUT_DIR', '.')
DEFAULT_LOG_DIR: Final[str] = os.environ.get(
'DEFAULT_LOG_DIR', '~/.blrec/logs/'
)
DEFAULT_LOG_DIR: Final[str] = os.environ.get('DEFAULT_LOG_DIR', '~/.blrec/logs/')
DEFAULT_SETTINGS_FILE: Final[str] = os.environ.get(
'DEFAULT_SETTINGS_FILE', '~/.blrec/settings.toml'
)
@ -80,8 +79,7 @@ class EnvSettings(BaseSettings):
out_dir: Optional[str] = None
log_dir: Optional[str] = None
api_key: Annotated[
Optional[str],
Field(min_length=8, max_length=80, regex=r'[a-zA-Z\d\-]{8,80}'),
Optional[str], Field(min_length=8, max_length=80, regex=r'[a-zA-Z\d\-]{8,80}'),
] = None
class Config:
@ -102,9 +100,7 @@ class BaseModel(PydanticBaseModel):
return camel_case(string)
@staticmethod
def _validate_with_collection(
value: _V, allowed_values: Collection[_V]
) -> None:
def _validate_with_collection(value: _V, allowed_values: Collection[_V]) -> None:
if value not in allowed_values:
raise ValueError(
f'the value {value} does not be allowed, '
@ -118,9 +114,11 @@ class HeaderOptions(BaseModel):
class HeaderSettings(HeaderOptions):
user_agent: str = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' \
'AppleWebKit/537.36 (KHTML, like Gecko) ' \
user_agent: str = (
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/89.0.4389.114 Safari/537.36'
)
cookie: str = ''
@ -149,7 +147,7 @@ class RecorderOptions(BaseModel):
read_timeout: Optional[int] # seconds
disconnection_timeout: Optional[int] # seconds
buffer_size: Annotated[ # bytes
Optional[int], Field(ge=4096, le=1024 ** 2 * 512, multiple_of=2)
Optional[int], Field(ge=4096, le=1024**2 * 512, multiple_of=2)
]
save_cover: Optional[bool]
cover_save_strategy: Optional[CoverSaveStrategy]
@ -169,9 +167,7 @@ class RecorderOptions(BaseModel):
return value
@validator('disconnection_timeout')
def _validate_disconnection_timeout(
cls, value: Optional[int]
) -> Optional[int]:
def _validate_disconnection_timeout(cls, value: Optional[int]) -> Optional[int]:
if value is not None:
allowed_values = frozenset(60 * i for i in (3, 5, 10, 15, 20, 30))
cls._validate_with_collection(value, allowed_values)
@ -185,7 +181,7 @@ class RecorderSettings(RecorderOptions):
read_timeout: int = 3
disconnection_timeout: int = 600
buffer_size: Annotated[
int, Field(ge=4096, le=1024 ** 2 * 512, multiple_of=2)
int, Field(ge=4096, le=1024**2 * 512, multiple_of=2)
] = 8192
save_cover: bool = False
cover_save_strategy: CoverSaveStrategy = CoverSaveStrategy.DEFAULT
@ -244,7 +240,7 @@ class OutputOptions(BaseModel):
def _validate_filesize_limit(cls, value: Optional[int]) -> Optional[int]:
# allowed 1 ~ 20 GB, 0 indicates not limit.
if value is not None:
allowed_values = frozenset(1024 ** 3 * i for i in range(0, 21))
allowed_values = frozenset(1024**3 * i for i in range(0, 21))
cls._validate_with_collection(value, allowed_values)
return value
@ -288,11 +284,11 @@ class TaskOptions(BaseModel):
@classmethod
def from_settings(cls, settings: TaskSettings) -> TaskOptions:
return cls(**settings.dict(
include={
'output', 'header', 'danmaku', 'recorder', 'postprocessing'
}
))
return cls(
**settings.dict(
include={'output', 'header', 'danmaku', 'recorder', 'postprocessing'}
)
)
class TaskSettings(TaskOptions):
@ -312,8 +308,10 @@ class LoggingSettings(BaseModel):
log_dir: Annotated[str, Field(default_factory=log_dir_factory)]
console_log_level: LOG_LEVEL = 'INFO'
max_bytes: Annotated[
int, Field(ge=1024 ** 2, le=1024 ** 2 * 10, multiple_of=1024 ** 2)
] = 1024 ** 2 * 10 # allowed 1 ~ 10 MB
int, Field(ge=1024**2, le=1024**2 * 10, multiple_of=1024**2)
] = (
1024**2 * 10
) # allowed 1 ~ 10 MB
backup_count: Annotated[int, Field(ge=1, le=30)] = 30
@validator('log_dir')
@ -325,7 +323,7 @@ class LoggingSettings(BaseModel):
class SpaceSettings(BaseModel):
check_interval: int = 60 # 1 minutes
space_threshold: int = 1024 ** 3 # 1 GB
space_threshold: int = 1024**3 # 1 GB
recycle_records: bool = False
@validator('check_interval')
@ -336,7 +334,7 @@ class SpaceSettings(BaseModel):
@validator('space_threshold')
def _validate_threshold(cls, value: int) -> int:
allowed_values = frozenset(1024 ** 3 * i for i in (1, 3, 5, 10, 20))
allowed_values = frozenset(1024**3 * i for i in (1, 3, 5, 10, 20))
cls._validate_with_collection(value, allowed_values)
return value
@ -395,9 +393,7 @@ class TelegramSettings(BaseModel):
@validator('token')
def _validate_token(cls, value: str) -> str:
if value != '' and not re.fullmatch(
r'[0-9]{8,10}:[a-zA-Z0-9_-]{35}', value
):
if value != '' and not re.fullmatch(r'[0-9]{8,10}:[a-zA-Z0-9_-]{35}', value):
raise ValueError('token is invalid')
return value
@ -419,32 +415,134 @@ class NotificationSettings(BaseModel):
notify_space: bool = True
class MessageTemplateSettings(BaseModel):
began_message_type: str
began_message_title: str
began_message_content: str
ended_message_type: str
ended_message_title: str
ended_message_content: str
space_message_type: str
space_message_title: str
space_message_content: str
error_message_type: str
error_message_title: str
error_message_content: str
class EmailMessageTemplateSettings(MessageTemplateSettings):
began_message_type: EmailMessageType = 'html'
began_message_title: str = ''
began_message_content: str = ''
ended_message_type: EmailMessageType = 'html'
ended_message_title: str = ''
ended_message_content: str = ''
space_message_type: EmailMessageType = 'html'
space_message_title: str = ''
space_message_content: str = ''
error_message_type: EmailMessageType = 'html'
error_message_title: str = ''
error_message_content: str = ''
class ServerchanMessageTemplateSettings(MessageTemplateSettings):
began_message_type: ServerchanMessageType = 'markdown'
began_message_title: str = ''
began_message_content: str = ''
ended_message_type: ServerchanMessageType = 'markdown'
ended_message_title: str = ''
ended_message_content: str = ''
space_message_type: ServerchanMessageType = 'markdown'
space_message_title: str = ''
space_message_content: str = ''
error_message_type: ServerchanMessageType = 'markdown'
error_message_title: str = ''
error_message_content: str = ''
class PushdeerMessageTemplateSettings(MessageTemplateSettings):
began_message_type: PushdeerMessageType = 'markdown'
began_message_title: str = ''
began_message_content: str = ''
ended_message_type: PushdeerMessageType = 'markdown'
ended_message_title: str = ''
ended_message_content: str = ''
space_message_type: PushdeerMessageType = 'markdown'
space_message_title: str = ''
space_message_content: str = ''
error_message_type: PushdeerMessageType = 'markdown'
error_message_title: str = ''
error_message_content: str = ''
class PushplusMessageTemplateSettings(MessageTemplateSettings):
began_message_type: PushplusMessageType = 'markdown'
began_message_title: str = ''
began_message_content: str = ''
ended_message_type: PushplusMessageType = 'markdown'
ended_message_title: str = ''
ended_message_content: str = ''
space_message_type: PushplusMessageType = 'markdown'
space_message_title: str = ''
space_message_content: str = ''
error_message_type: PushplusMessageType = 'markdown'
error_message_title: str = ''
error_message_content: str = ''
class TelegramMessageTemplateSettings(MessageTemplateSettings):
began_message_type: TelegramMessageType = 'html'
began_message_title: str = ''
began_message_content: str = ''
ended_message_type: TelegramMessageType = 'html'
ended_message_title: str = ''
ended_message_content: str = ''
space_message_type: TelegramMessageType = 'html'
space_message_title: str = ''
space_message_content: str = ''
error_message_type: TelegramMessageType = 'html'
error_message_title: str = ''
error_message_content: str = ''
class EmailNotificationSettings(
EmailSettings, NotifierSettings, NotificationSettings
EmailSettings, NotifierSettings, NotificationSettings, EmailMessageTemplateSettings
):
pass
class ServerchanNotificationSettings(
ServerchanSettings, NotifierSettings, NotificationSettings
ServerchanSettings,
NotifierSettings,
NotificationSettings,
ServerchanMessageTemplateSettings,
):
pass
class PushdeerNotificationSettings(
PushdeerSettings, NotifierSettings, NotificationSettings
PushdeerSettings,
NotifierSettings,
NotificationSettings,
PushplusMessageTemplateSettings,
):
pass
class PushplusNotificationSettings(
PushplusSettings, NotifierSettings, NotificationSettings
PushplusSettings,
NotifierSettings,
NotificationSettings,
PushplusMessageTemplateSettings,
):
pass
class TelegramNotificationSettings(
TelegramSettings, NotifierSettings, NotificationSettings
TelegramSettings,
NotifierSettings,
NotificationSettings,
TelegramMessageTemplateSettings,
):
pass
@ -487,14 +585,12 @@ class Settings(BaseModel):
postprocessing: PostprocessingSettings = PostprocessingSettings()
space: SpaceSettings = SpaceSettings()
email_notification: EmailNotificationSettings = EmailNotificationSettings()
serverchan_notification: ServerchanNotificationSettings = \
serverchan_notification: ServerchanNotificationSettings = (
ServerchanNotificationSettings()
pushdeer_notification: PushdeerNotificationSettings = \
PushdeerNotificationSettings()
pushplus_notification: PushplusNotificationSettings = \
PushplusNotificationSettings()
telegram_notification: TelegramNotificationSettings = \
TelegramNotificationSettings()
)
pushdeer_notification: PushdeerNotificationSettings = PushdeerNotificationSettings()
pushplus_notification: PushplusNotificationSettings = PushplusNotificationSettings()
telegram_notification: TelegramNotificationSettings = TelegramNotificationSettings()
webhooks: Annotated[List[WebHookSettings], Field(max_items=50)] = []
@classmethod
@ -525,9 +621,7 @@ class Settings(BaseModel):
cls, webhooks: List[WebHookSettings]
) -> List[WebHookSettings]:
if len(webhooks) >= cls._MAX_WEBHOOKS:
raise ValueError(
f'Out of max webhooks limits: {cls._MAX_WEBHOOKS}'
)
raise ValueError(f'Out of max webhooks limits: {cls._MAX_WEBHOOKS}')
return webhooks

View File

@ -1,31 +1,37 @@
from __future__ import annotations
import asyncio
from typing import Optional, TYPE_CHECKING, cast
from .helpers import update_settings, shadow_settings
import asyncio
from typing import TYPE_CHECKING, Optional, cast
from ..exception import NotFoundError
from ..logging import configure_logger
from ..notification import (
EmailService,
Notifier,
Pushdeer,
Pushplus,
Serverchan,
Telegram,
)
from ..webhook import WebHook
from .helpers import shadow_settings, update_settings
from .models import (
DanmakuOptions,
HeaderOptions,
MessageTemplateSettings,
NotificationSettings,
NotifierSettings,
OutputOptions,
PostprocessingOptions,
RecorderOptions,
Settings,
SettingsIn,
SettingsOut,
HeaderOptions,
DanmakuOptions,
RecorderOptions,
PostprocessingOptions,
TaskOptions,
TaskSettings,
NotifierSettings,
NotificationSettings,
)
from .typing import KeySetOfSettings
from ..webhook import WebHook
from ..notification import (
Notifier, EmailService, Serverchan, Pushdeer, Pushplus, Telegram
)
from ..logging import configure_logger
from ..exception import NotFoundError
if TYPE_CHECKING:
from ..application import Application
@ -40,9 +46,7 @@ class SettingsManager:
include: Optional[KeySetOfSettings] = None,
exclude: Optional[KeySetOfSettings] = None,
) -> SettingsOut:
return SettingsOut(
**self._settings.dict(include=include, exclude=exclude)
)
return SettingsOut(**self._settings.dict(include=include, exclude=exclude))
async def change_settings(self, settings: SettingsIn) -> SettingsOut:
changed = False
@ -70,19 +74,15 @@ class SettingsManager:
if changed:
await self.dump_settings()
return self.get_settings(
cast(KeySetOfSettings, settings.__fields_set__)
)
return self.get_settings(cast(KeySetOfSettings, settings.__fields_set__))
def get_task_options(self, room_id: int) -> TaskOptions:
if (settings := self.find_task_settings(room_id)):
if settings := self.find_task_settings(room_id):
return TaskOptions.from_settings(settings)
raise NotFoundError(f'task settings of room {room_id} not found')
async def change_task_options(
self,
room_id: int,
options: TaskOptions,
self, room_id: int, options: TaskOptions
) -> TaskOptions:
settings = self.find_task_settings(room_id)
assert settings is not None
@ -211,11 +211,7 @@ class SettingsManager:
await self.dump_settings()
async def apply_task_header_settings(
self,
room_id: int,
options: HeaderOptions,
*,
update_session: bool = True,
self, room_id: int, options: HeaderOptions, *, update_session: bool = True
) -> None:
final_settings = self._settings.header.copy()
shadow_settings(options, final_settings)
@ -224,42 +220,26 @@ class SettingsManager:
)
def apply_task_danmaku_settings(
self,
room_id: int,
options: DanmakuOptions,
self, room_id: int, options: DanmakuOptions
) -> None:
final_settings = self._settings.danmaku.copy()
shadow_settings(options, final_settings)
self._app._task_manager.apply_task_danmaku_settings(
room_id, final_settings
)
self._app._task_manager.apply_task_danmaku_settings(room_id, final_settings)
def apply_task_recorder_settings(
self,
room_id: int,
options: RecorderOptions,
self, room_id: int, options: RecorderOptions
) -> None:
final_settings = self._settings.recorder.copy()
shadow_settings(options, final_settings)
self._app._task_manager.apply_task_recorder_settings(
room_id, final_settings
)
self._app._task_manager.apply_task_recorder_settings(room_id, final_settings)
def apply_task_output_settings(
self,
room_id: int,
options: OutputOptions,
) -> None:
def apply_task_output_settings(self, room_id: int, options: OutputOptions) -> None:
final_settings = self._settings.output.copy()
shadow_settings(options, final_settings)
self._app._task_manager.apply_task_output_settings(
room_id, final_settings
)
self._app._task_manager.apply_task_output_settings(room_id, final_settings)
def apply_task_postprocessing_settings(
self,
room_id: int,
options: PostprocessingOptions,
self, room_id: int, options: PostprocessingOptions
) -> None:
final_settings = self._settings.postprocessing.copy()
shadow_settings(options, final_settings)
@ -286,21 +266,15 @@ class SettingsManager:
async def apply_header_settings(self) -> None:
for settings in self._settings.tasks:
await self.apply_task_header_settings(
settings.room_id, settings.header
)
await self.apply_task_header_settings(settings.room_id, settings.header)
def apply_danmaku_settings(self) -> None:
for settings in self._settings.tasks:
self.apply_task_danmaku_settings(
settings.room_id, settings.danmaku
)
self.apply_task_danmaku_settings(settings.room_id, settings.danmaku)
def apply_recorder_settings(self) -> None:
for settings in self._settings.tasks:
self.apply_task_recorder_settings(
settings.room_id, settings.recorder
)
self.apply_task_recorder_settings(settings.room_id, settings.recorder)
def apply_postprocessing_settings(self) -> None:
for settings in self._settings.tasks:
@ -327,6 +301,7 @@ class SettingsManager:
self._apply_email_settings(notifier.provider)
self._apply_notifier_settings(notifier, settings)
self._apply_notification_settings(notifier, settings)
self._apply_message_template_settings(notifier, settings)
def apply_serverchan_notification_settings(self) -> None:
notifier = self._app._serverchan_notifier
@ -334,6 +309,7 @@ class SettingsManager:
self._apply_serverchan_settings(notifier.provider)
self._apply_notifier_settings(notifier, settings)
self._apply_notification_settings(notifier, settings)
self._apply_message_template_settings(notifier, settings)
def apply_pushdeer_notification_settings(self) -> None:
notifier = self._app._pushdeer_notifier
@ -341,6 +317,7 @@ class SettingsManager:
self._apply_pushdeer_settings(notifier.provider)
self._apply_notifier_settings(notifier, settings)
self._apply_notification_settings(notifier, settings)
self._apply_message_template_settings(notifier, settings)
def apply_pushplus_notification_settings(self) -> None:
notifier = self._app._pushplus_notifier
@ -348,6 +325,7 @@ class SettingsManager:
self._apply_pushplus_settings(notifier.provider)
self._apply_notifier_settings(notifier, settings)
self._apply_notification_settings(notifier, settings)
self._apply_message_template_settings(notifier, settings)
def apply_telegram_notification_settings(self) -> None:
notifier = self._app._telegram_notifier
@ -355,6 +333,7 @@ class SettingsManager:
self._apply_telegram_settings(notifier.provider)
self._apply_notifier_settings(notifier, settings)
self._apply_notification_settings(notifier, settings)
self._apply_message_template_settings(notifier, settings)
def apply_webhooks_settings(self) -> None:
webhooks = [WebHook.from_settings(s) for s in self._settings.webhooks]
@ -397,3 +376,19 @@ class SettingsManager:
notifier.notify_ended = settings.notify_ended
notifier.notify_error = settings.notify_error
notifier.notify_space = settings.notify_space
def _apply_message_template_settings(
self, notifier: Notifier, settings: MessageTemplateSettings
) -> None:
notifier.began_message_type = settings.began_message_type
notifier.began_message_title = settings.began_message_title
notifier.began_message_content = settings.began_message_content
notifier.ended_message_type = settings.ended_message_type
notifier.ended_message_title = settings.ended_message_title
notifier.ended_message_content = settings.ended_message_content
notifier.space_message_type = settings.space_message_type
notifier.space_message_title = settings.space_message_title
notifier.space_message_content = settings.space_message_content
notifier.error_message_type = settings.error_message_type
notifier.error_message_title = settings.error_message_title
notifier.error_message_content = settings.error_message_content

View File

@ -1,4 +1,15 @@
from typing import AbstractSet, Literal
from typing import AbstractSet, Literal, Union
TextMessageType = Literal['text']
HtmlMessageType = Literal['html']
MarkdownMessageType = Literal['markdown']
MessageType = Union[TextMessageType, MarkdownMessageType, HtmlMessageType]
EmailMessageType = Union[TextMessageType, HtmlMessageType]
ServerchanMessageType = MarkdownMessageType
PushdeerMessageType = Union[TextMessageType, MarkdownMessageType]
PushplusMessageType = Union[TextMessageType, MarkdownMessageType, HtmlMessageType]
TelegramMessageType = Union[MarkdownMessageType, HtmlMessageType]
KeyOfSettings = Literal[

View File

@ -3,7 +3,7 @@ from typing import Callable, Iterable, Iterator, List, Optional, cast
from fastapi import Query
from blrec.setting import KeyOfSettings, KeySetOfSettings
from blrec.setting.typing import KeyOfSettings, KeySetOfSettings
from .schemas import DataSelection, AliasKeyOfSettings
from ..bili.models import LiveStatus

View File

@ -7,9 +7,9 @@ from ..dependencies import settings_include_set, settings_exclude_set
from ...setting import (
SettingsIn,
SettingsOut,
KeySetOfSettings,
TaskOptions,
)
from ...setting.typing import KeySetOfSettings
from ...application import Application

View File

@ -17,5 +17,12 @@
keyOfSettings="emailNotification"
></app-event-settings>
</app-page-section>
<app-page-section name="消息">
<app-message-template-settings
[settings]="messageTemplateSettings"
keyOfSettings="emailNotification"
></app-message-template-settings>
</app-page-section>
</ng-template>
</app-sub-page>

View File

@ -13,9 +13,11 @@ import {
EmailSettings,
NotifierSettings,
NotificationSettings,
MessageTemplateSettings,
KEYS_OF_EMAIL_SETTINGS,
KEYS_OF_NOTIFIER_SETTINGS,
KEYS_OF_NOTIFICATION_SETTINGS,
KEYS_OF_MESSAGE_TEMPLATE_SETTINGS,
} from '../../shared/setting.model';
@Component({
@ -28,6 +30,7 @@ export class EmailNotificationSettingsComponent implements OnInit {
emailSettings!: EmailSettings;
notifierSettings!: NotifierSettings;
notificationSettings!: NotificationSettings;
messageTemplateSettings!: MessageTemplateSettings;
constructor(
private changeDetector: ChangeDetectorRef,
@ -40,6 +43,10 @@ export class EmailNotificationSettingsComponent implements OnInit {
this.emailSettings = pick(settings, KEYS_OF_EMAIL_SETTINGS);
this.notifierSettings = pick(settings, KEYS_OF_NOTIFIER_SETTINGS);
this.notificationSettings = pick(settings, KEYS_OF_NOTIFICATION_SETTINGS);
this.messageTemplateSettings = pick(
settings,
KEYS_OF_MESSAGE_TEMPLATE_SETTINGS
);
this.changeDetector.markForCheck();
});
}

View File

@ -19,6 +19,13 @@
keyOfSettings="pushdeerNotification"
></app-event-settings>
</app-page-section>
<app-page-section name="消息">
<app-message-template-settings
[settings]="messageTemplateSettings"
keyOfSettings="pushdeerNotification"
></app-message-template-settings>
</app-page-section>
</ng-template>
</app-sub-page>

View File

@ -9,9 +9,11 @@ import { ActivatedRoute } from '@angular/router';
import pick from 'lodash-es/pick';
import {
KEYS_OF_MESSAGE_TEMPLATE_SETTINGS,
KEYS_OF_NOTIFICATION_SETTINGS,
KEYS_OF_NOTIFIER_SETTINGS,
KEYS_OF_PUSHDEER_SETTINGS,
MessageTemplateSettings,
NotificationSettings,
NotifierSettings,
PushdeerNotificationSettings,
@ -28,6 +30,7 @@ export class PushdeerNotificationSettingsComponent implements OnInit {
pushdeerSettings!: PushdeerSettings;
notifierSettings!: NotifierSettings;
notificationSettings!: NotificationSettings;
messageTemplateSettings!: MessageTemplateSettings;
constructor(
private changeDetector: ChangeDetectorRef,
@ -40,6 +43,10 @@ export class PushdeerNotificationSettingsComponent implements OnInit {
this.pushdeerSettings = pick(settings, KEYS_OF_PUSHDEER_SETTINGS);
this.notifierSettings = pick(settings, KEYS_OF_NOTIFIER_SETTINGS);
this.notificationSettings = pick(settings, KEYS_OF_NOTIFICATION_SETTINGS);
this.messageTemplateSettings = pick(
settings,
KEYS_OF_MESSAGE_TEMPLATE_SETTINGS
);
this.changeDetector.markForCheck();
});
}

View File

@ -19,5 +19,12 @@
keyOfSettings="pushplusNotification"
></app-event-settings>
</app-page-section>
<app-page-section name="消息">
<app-message-template-settings
[settings]="messageTemplateSettings"
keyOfSettings="pushplusNotification"
></app-message-template-settings>
</app-page-section>
</ng-template>
</app-sub-page>

View File

@ -16,6 +16,8 @@ import {
KEYS_OF_PUSHPLUS_SETTINGS,
KEYS_OF_NOTIFIER_SETTINGS,
KEYS_OF_NOTIFICATION_SETTINGS,
MessageTemplateSettings,
KEYS_OF_MESSAGE_TEMPLATE_SETTINGS,
} from '../../shared/setting.model';
@Component({
@ -28,6 +30,7 @@ export class PushplusNotificationSettingsComponent implements OnInit {
pushplusSettings!: PushplusSettings;
notifierSettings!: NotifierSettings;
notificationSettings!: NotificationSettings;
messageTemplateSettings!: MessageTemplateSettings;
constructor(
private changeDetector: ChangeDetectorRef,
@ -37,10 +40,14 @@ export class PushplusNotificationSettingsComponent implements OnInit {
ngOnInit(): void {
this.route.data.subscribe((data) => {
const settings = data.settings as PushplusNotificationSettings;
this.changeDetector.markForCheck();
this.pushplusSettings = pick(settings, KEYS_OF_PUSHPLUS_SETTINGS);
this.notifierSettings = pick(settings, KEYS_OF_NOTIFIER_SETTINGS);
this.notificationSettings = pick(settings, KEYS_OF_NOTIFICATION_SETTINGS);
this.messageTemplateSettings = pick(
settings,
KEYS_OF_MESSAGE_TEMPLATE_SETTINGS
);
this.changeDetector.markForCheck();
});
}
}

View File

@ -19,5 +19,12 @@
keyOfSettings="serverchanNotification"
></app-event-settings>
</app-page-section>
<app-page-section name="消息">
<app-message-template-settings
[settings]="messageTemplateSettings"
keyOfSettings="serverchanNotification"
></app-message-template-settings>
</app-page-section>
</ng-template>
</app-sub-page>

View File

@ -9,9 +9,11 @@ import { ActivatedRoute } from '@angular/router';
import pick from 'lodash-es/pick';
import {
KEYS_OF_MESSAGE_TEMPLATE_SETTINGS,
KEYS_OF_NOTIFICATION_SETTINGS,
KEYS_OF_NOTIFIER_SETTINGS,
KEYS_OF_SERVERCHAN_SETTINGS,
MessageTemplateSettings,
NotificationSettings,
NotifierSettings,
ServerchanNotificationSettings,
@ -28,6 +30,7 @@ export class ServerchanNotificationSettingsComponent implements OnInit {
serverchanSettings!: ServerchanSettings;
notifierSettings!: NotifierSettings;
notificationSettings!: NotificationSettings;
messageTemplateSettings!: MessageTemplateSettings;
constructor(
private changeDetector: ChangeDetectorRef,
@ -40,6 +43,10 @@ export class ServerchanNotificationSettingsComponent implements OnInit {
this.serverchanSettings = pick(settings, KEYS_OF_SERVERCHAN_SETTINGS);
this.notifierSettings = pick(settings, KEYS_OF_NOTIFIER_SETTINGS);
this.notificationSettings = pick(settings, KEYS_OF_NOTIFICATION_SETTINGS);
this.messageTemplateSettings = pick(
settings,
KEYS_OF_MESSAGE_TEMPLATE_SETTINGS
);
this.changeDetector.markForCheck();
});
}

View File

@ -0,0 +1,76 @@
<nz-modal
[nzTitle]="title"
nzCentered
[(nzVisible)]="visible"
[nzOkDisabled]="settingsForm.invalid"
(nzOnOk)="handleConfirm()"
(nzOnCancel)="handleCancel()"
>
<ng-container *nzModalContent>
<ng-template #messageTemplateTip>
<p>
语法、变量参考
<a href="https://github.com/acgnhiki/blrec/wiki/MessageTemplate" _blank
>wiki</a
>
</p>
<p>空值将使用默认消息模板</p>
</ng-template>
<form nz-form [nzLayout]="'vertical'" [formGroup]="settingsForm">
<nz-form-item class="setting-item input">
<nz-form-label
class="setting-label"
nzFor="messageTitle"
nzNoColon
[nzTooltipTitle]="messageTemplateTip"
>
消息标题
</nz-form-label>
<nz-form-control class="setting-control input">
<input type="text" nz-input formControlName="messageTitle" />
</nz-form-control>
</nz-form-item>
<nz-form-item class="setting-item switch">
<nz-form-label class="setting-label" nzFor="messageType" nzNoColon>
消息类型
</nz-form-label>
<nz-form-control class="setting-control select">
<nz-select
formControlName="messageType"
[nzOptions]="MESSAGE_TYPE_OPTIONS"
>
</nz-select>
</nz-form-control>
</nz-form-item>
<nz-form-item class="setting-item textarea">
<nz-form-label
class="setting-label"
nzFor="messageTitle"
nzNoColon
[nzTooltipTitle]="messageTemplateTip"
>
消息内容
</nz-form-label>
<nz-form-control class="setting-control textarea">
<textarea
nz-input
[rows]="10"
wrap="off"
formControlName="messageContent"
></textarea>
</nz-form-control>
</nz-form-item>
</form>
</ng-container>
<ng-template #modalFooter>
<button nz-button nzType="default" (click)="handleCancel()">取消</button>
<button
nz-button
nzType="default"
(click)="handleConfirm()"
[disabled]="settingsForm.invalid"
>
确定
</button>
</ng-template>
</nz-modal>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MessageTemplateEditDialogComponent } from './message-template-edit-dialog.component';
describe('MessageTemplateEditDialogComponent', () => {
let component: MessageTemplateEditDialogComponent;
let fixture: ComponentFixture<MessageTemplateEditDialogComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MessageTemplateEditDialogComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(MessageTemplateEditDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,102 @@
import {
Component,
OnInit,
OnChanges,
ChangeDetectionStrategy,
Input,
Output,
EventEmitter,
ChangeDetectorRef,
} from '@angular/core';
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
import { NzSelectOptionInterface } from 'ng-zorro-antd/select/select.types';
import { MessageType } from 'src/app/settings/shared/setting.model';
import { CommonMessageTemplateSettings } from '../message-template-settings.component';
@Component({
selector: 'app-message-template-edit-dialog',
templateUrl: './message-template-edit-dialog.component.html',
styleUrls: ['./message-template-edit-dialog.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MessageTemplateEditDialogComponent implements OnInit, OnChanges {
@Input() value!: CommonMessageTemplateSettings;
@Input() messageTypes: MessageType[] = [];
@Input() title: string = '修改消息模板';
@Input() visible: boolean = false;
@Output() visibleChange = new EventEmitter<boolean>();
@Output() cancel = new EventEmitter<undefined>();
@Output() confirm = new EventEmitter<CommonMessageTemplateSettings>();
readonly settingsForm: FormGroup;
MESSAGE_TYPE_OPTIONS: NzSelectOptionInterface[] = [];
constructor(
formBuilder: FormBuilder,
private changeDetector: ChangeDetectorRef
) {
this.settingsForm = formBuilder.group({
messageType: [''],
messageTitle: [''],
messageContent: [''],
});
}
get messageTypeControl() {
return this.settingsForm.get('messageType') as FormControl;
}
get messageTitleControl() {
return this.settingsForm.get('messageTitle') as FormControl;
}
get messageContentControl() {
return this.settingsForm.get('messageContent') as FormControl;
}
ngOnInit(): void {
this.MESSAGE_TYPE_OPTIONS = Array.from(new Set(this.messageTypes)).map(
(type: MessageType) => ({
label: type,
value: type,
})
);
}
ngOnChanges(): void {
this.setValue();
}
open(): void {
this.setValue();
this.setVisible(true);
}
close(): void {
this.setVisible(false);
}
setVisible(visible: boolean): void {
this.visible = visible;
this.visibleChange.emit(visible);
this.changeDetector.markForCheck();
}
setValue(): void {
this.settingsForm.setValue(this.value);
this.changeDetector.markForCheck();
}
handleCancel(): void {
this.cancel.emit();
this.close();
}
handleConfirm(): void {
this.confirm.emit(this.settingsForm.value);
this.close();
}
}

View File

@ -0,0 +1,51 @@
<a
class="setting-item actionable"
(click)="beganMessageTemplateEditDialog.open()"
><span class="setting-label">开播消息模板</span></a
>
<app-message-template-edit-dialog
#beganMessageTemplateEditDialog
[title]="'修改开播消息模板'"
[value]="beganMessageTemplateSettings"
[messageTypes]="messageTypes"
(confirm)="changeBeganMessageTemplateSettings($event)"
></app-message-template-edit-dialog>
<a
class="setting-item actionable"
(click)="endedMessageTemplateEditDialog.open()"
><span class="setting-label">下播消息模板</span></a
>
<app-message-template-edit-dialog
#endedMessageTemplateEditDialog
[title]="'修改下播消息模板'"
[value]="endedMessageTemplateSettings"
[messageTypes]="messageTypes"
(confirm)="changeEndedMessageTemplateSettings($event)"
></app-message-template-edit-dialog>
<a
class="setting-item actionable"
(click)="errorMessageTemplateEditDialog.open()"
><span class="setting-label">异常消息模板</span></a
>
<app-message-template-edit-dialog
#errorMessageTemplateEditDialog
[title]="'修改异常消息模板'"
[value]="errorMessageTemplateSettings"
[messageTypes]="messageTypes"
(confirm)="changeErrorMessageTemplateSettings($event)"
></app-message-template-edit-dialog>
<a
class="setting-item actionable"
(click)="spaceMessageTemplateEditDialog.open()"
><span class="setting-label">空间不足消息模板</span></a
>
<app-message-template-edit-dialog
#spaceMessageTemplateEditDialog
[title]="'修改空间不足消息模板'"
[value]="spaceMessageTemplateSettings"
[messageTypes]="messageTypes"
(confirm)="changeSpaceMessageTemplateSettings($event)"
></app-message-template-edit-dialog>

View File

@ -0,0 +1 @@
@use "src/app/settings/shared/styles/_setting.scss";

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MessageTemplateSettingsComponent } from './message-template-settings.component';
describe('MessageTemplateSettingsComponent', () => {
let component: MessageTemplateSettingsComponent;
let fixture: ComponentFixture<MessageTemplateSettingsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MessageTemplateSettingsComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(MessageTemplateSettingsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,167 @@
import {
Component,
ChangeDetectionStrategy,
Input,
ChangeDetectorRef,
OnInit,
OnChanges,
SimpleChanges,
} from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { tap } from 'rxjs/operators';
import { NzMessageService } from 'ng-zorro-antd/message';
import { retry } from 'src/app/shared/rx-operators';
import {
MessageTemplateSettings,
MessageType,
} from 'src/app/settings/shared/setting.model';
import { SettingService } from 'src/app/settings/shared/services/setting.service';
export interface CommonMessageTemplateSettings {
messageType: string;
messageTitle: string;
messageContent: string;
}
@Component({
selector: 'app-message-template-settings',
templateUrl: './message-template-settings.component.html',
styleUrls: ['./message-template-settings.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MessageTemplateSettingsComponent implements OnInit, OnChanges {
@Input() settings!: MessageTemplateSettings;
@Input() keyOfSettings!:
| 'emailNotification'
| 'serverchanNotification'
| 'pushdeerNotification'
| 'pushplusNotification'
| 'telegramNotification';
messageTypes!: MessageType[];
beganMessageTemplateSettings!: CommonMessageTemplateSettings;
endedMessageTemplateSettings!: CommonMessageTemplateSettings;
spaceMessageTemplateSettings!: CommonMessageTemplateSettings;
errorMessageTemplateSettings!: CommonMessageTemplateSettings;
constructor(
private changeDetector: ChangeDetectorRef,
private message: NzMessageService,
private settingService: SettingService
) {}
ngOnInit(): void {
switch (this.keyOfSettings) {
case 'emailNotification':
this.messageTypes = ['text', 'html'];
break;
case 'serverchanNotification':
this.messageTypes = ['markdown'];
break;
case 'pushdeerNotification':
this.messageTypes = ['markdown', 'text'];
break;
case 'pushplusNotification':
this.messageTypes = ['markdown', 'text', 'html'];
break;
case 'telegramNotification':
this.messageTypes = ['markdown', 'html'];
break;
}
}
ngOnChanges(changes: SimpleChanges): void {
this.updateCommonSettings();
}
changeBeganMessageTemplateSettings(
settings: CommonMessageTemplateSettings
): void {
const settingsToChange = {
beganMessageType: settings.messageType,
beganMessageTitle: settings.messageTitle,
beganMessageContent: settings.messageContent,
};
this.changeMessageTemplateSettings(settingsToChange).subscribe();
}
changeEndedMessageTemplateSettings(
settings: CommonMessageTemplateSettings
): void {
const settingsToChange = {
endedMessageType: settings.messageType,
endedMessageTitle: settings.messageTitle,
endedMessageContent: settings.messageContent,
};
this.changeMessageTemplateSettings(settingsToChange).subscribe();
}
changeSpaceMessageTemplateSettings(
settings: CommonMessageTemplateSettings
): void {
const settingsToChange = {
spaceMessageType: settings.messageType,
spaceMessageTitle: settings.messageTitle,
spaceMessageContent: settings.messageContent,
};
this.changeMessageTemplateSettings(settingsToChange).subscribe();
}
changeErrorMessageTemplateSettings(
settings: CommonMessageTemplateSettings
): void {
const settingsToChange = {
errorMessageType: settings.messageType,
errorMessageTitle: settings.messageTitle,
errorMessageContent: settings.messageContent,
};
this.changeMessageTemplateSettings(settingsToChange).subscribe();
}
changeMessageTemplateSettings(settings: Partial<MessageTemplateSettings>) {
return this.settingService
.changeSettings({ [this.keyOfSettings]: settings })
.pipe(
retry(3, 300),
tap(
(settings) => {
this.message.success('修改消息模板设置成功');
this.settings = {
...this.settings,
...settings[this.keyOfSettings],
};
this.updateCommonSettings();
this.changeDetector.markForCheck();
},
(error: HttpErrorResponse) => {
this.message.error(`修改消息模板设置出错: ${error.message}`);
}
)
);
}
private updateCommonSettings(): void {
this.beganMessageTemplateSettings = {
messageType: this.settings.beganMessageType,
messageTitle: this.settings.beganMessageTitle,
messageContent: this.settings.beganMessageContent,
};
this.endedMessageTemplateSettings = {
messageType: this.settings.endedMessageType,
messageTitle: this.settings.endedMessageTitle,
messageContent: this.settings.endedMessageContent,
};
this.spaceMessageTemplateSettings = {
messageType: this.settings.spaceMessageType,
messageTitle: this.settings.spaceMessageTitle,
messageContent: this.settings.spaceMessageContent,
};
this.errorMessageTemplateSettings = {
messageType: this.settings.errorMessageType,
messageTitle: this.settings.errorMessageTitle,
messageContent: this.settings.errorMessageContent,
};
}
}

View File

@ -19,5 +19,12 @@
keyOfSettings="telegramNotification"
></app-event-settings>
</app-page-section>
<app-page-section name="消息">
<app-message-template-settings
[settings]="messageTemplateSettings"
keyOfSettings="telegramNotification"
></app-message-template-settings>
</app-page-section>
</ng-template>
</app-sub-page>

View File

@ -16,6 +16,8 @@ import {
KEYS_OF_TELEGRAM_SETTINGS,
KEYS_OF_NOTIFIER_SETTINGS,
KEYS_OF_NOTIFICATION_SETTINGS,
MessageTemplateSettings,
KEYS_OF_MESSAGE_TEMPLATE_SETTINGS,
} from '../../shared/setting.model';
@Component({
@ -28,6 +30,7 @@ export class TelegramNotificationSettingsComponent implements OnInit {
telegramSettings!: TelegramSettings;
notifierSettings!: NotifierSettings;
notificationSettings!: NotificationSettings;
messageTemplateSettings!: MessageTemplateSettings;
constructor(
private changeDetector: ChangeDetectorRef,
@ -37,10 +40,14 @@ export class TelegramNotificationSettingsComponent implements OnInit {
ngOnInit(): void {
this.route.data.subscribe((data) => {
const settings = data.settings as TelegramNotificationSettings;
this.changeDetector.markForCheck();
this.telegramSettings = pick(settings, KEYS_OF_TELEGRAM_SETTINGS);
this.notifierSettings = pick(settings, KEYS_OF_NOTIFIER_SETTINGS);
this.notificationSettings = pick(settings, KEYS_OF_NOTIFICATION_SETTINGS);
this.messageTemplateSettings = pick(
settings,
KEYS_OF_MESSAGE_TEMPLATE_SETTINGS
);
this.changeDetector.markForCheck();
});
}
}

View File

@ -63,6 +63,8 @@ import { WebhookListComponent } from './webhook-settings/webhook-list/webhook-li
import { OutdirEditDialogComponent } from './output-settings/outdir-edit-dialog/outdir-edit-dialog.component';
import { LogdirEditDialogComponent } from './logging-settings/logdir-edit-dialog/logdir-edit-dialog.component';
import { PathTemplateEditDialogComponent } from './output-settings/path-template-edit-dialog/path-template-edit-dialog.component';
import { MessageTemplateSettingsComponent } from './notification-settings/shared/components/message-template-settings/message-template-settings.component';
import { MessageTemplateEditDialogComponent } from './notification-settings/shared/components/message-template-settings/message-template-edit-dialog/message-template-edit-dialog.component';
@NgModule({
declarations: [
@ -97,6 +99,8 @@ import { PathTemplateEditDialogComponent } from './output-settings/path-template
OutdirEditDialogComponent,
LogdirEditDialogComponent,
PathTemplateEditDialogComponent,
MessageTemplateSettingsComponent,
MessageTemplateEditDialogComponent,
],
imports: [
CommonModule,

View File

@ -175,25 +175,152 @@ export const KEYS_OF_NOTIFICATION_SETTINGS = [
'notifySpace',
] as const;
export type TextMessageType = 'text';
export type HtmlMessageType = 'html';
export type MarkdownMessageType = 'markdown';
export type MessageType =
| TextMessageType
| MarkdownMessageType
| HtmlMessageType;
export type EmailMessageType = TextMessageType | HtmlMessageType;
export type ServerchanMessageType = MarkdownMessageType;
export type PushdeerMessageType = TextMessageType | MarkdownMessageType;
export type PushplusMessageType =
| TextMessageType
| MarkdownMessageType
| HtmlMessageType;
export type TelegramMessageType = MarkdownMessageType | HtmlMessageType;
export interface MessageTemplateSettings {
beganMessageType: string;
beganMessageTitle: string;
beganMessageContent: string;
endedMessageType: string;
endedMessageTitle: string;
endedMessageContent: string;
spaceMessageType: string;
spaceMessageTitle: string;
spaceMessageContent: string;
errorMessageType: string;
errorMessageTitle: string;
errorMessageContent: string;
}
export const KEYS_OF_MESSAGE_TEMPLATE_SETTINGS = [
'beganMessageType',
'beganMessageTitle',
'beganMessageContent',
'endedMessageType',
'endedMessageTitle',
'endedMessageContent',
'spaceMessageType',
'spaceMessageTitle',
'spaceMessageContent',
'errorMessageType',
'errorMessageTitle',
'errorMessageContent',
] as const;
export interface EmailMessageTemplateSettings {
beganMessageType: EmailMessageType;
beganMessageTitle: string;
beganMessageContent: string;
endedMessageType: EmailMessageType;
endedMessageTitle: string;
endedMessageContent: string;
spaceMessageType: EmailMessageType;
spaceMessageTitle: string;
spaceMessageContent: string;
errorMessageType: EmailMessageType;
errorMessageTitle: string;
errorMessageContent: string;
}
export interface ServerchanMessageTemplateSettings {
beganMessageType: ServerchanMessageType;
beganMessageTitle: string;
beganMessageContent: string;
endedMessageType: ServerchanMessageType;
endedMessageTitle: string;
endedMessageContent: string;
spaceMessageType: ServerchanMessageType;
spaceMessageTitle: string;
spaceMessageContent: string;
errorMessageType: ServerchanMessageType;
errorMessageTitle: string;
errorMessageContent: string;
}
export interface PushdeerMessageTemplateSettings {
beganMessageType: PushdeerMessageType;
beganMessageTitle: string;
beganMessageContent: string;
endedMessageType: PushdeerMessageType;
endedMessageTitle: string;
endedMessageContent: string;
spaceMessageType: PushdeerMessageType;
spaceMessageTitle: string;
spaceMessageContent: string;
errorMessageType: PushdeerMessageType;
errorMessageTitle: string;
errorMessageContent: string;
}
export interface PushplusMessageTemplateSettings {
beganMessageType: PushplusMessageType;
beganMessageTitle: string;
beganMessageContent: string;
endedMessageType: PushplusMessageType;
endedMessageTitle: string;
endedMessageContent: string;
spaceMessageType: PushplusMessageType;
spaceMessageTitle: string;
spaceMessageContent: string;
errorMessageType: PushplusMessageType;
errorMessageTitle: string;
errorMessageContent: string;
}
export interface TelegramMessageTemplateSettings {
beganMessageType: PushplusMessageType;
beganMessageTitle: string;
beganMessageContent: string;
endedMessageType: PushplusMessageType;
endedMessageTitle: string;
endedMessageContent: string;
spaceMessageType: PushplusMessageType;
spaceMessageTitle: string;
spaceMessageContent: string;
errorMessageType: PushplusMessageType;
errorMessageTitle: string;
errorMessageContent: string;
}
export type EmailNotificationSettings = EmailSettings &
NotifierSettings &
NotificationSettings;
NotificationSettings &
EmailMessageTemplateSettings;
export type ServerchanNotificationSettings = ServerchanSettings &
NotifierSettings &
NotificationSettings;
NotificationSettings &
ServerchanMessageTemplateSettings;
export type PushdeerNotificationSettings = PushdeerSettings &
NotifierSettings &
NotificationSettings;
NotificationSettings &
PushdeerMessageTemplateSettings;
export type PushplusNotificationSettings = PushplusSettings &
NotifierSettings &
NotificationSettings;
NotificationSettings &
PushdeerMessageTemplateSettings;
export type TelegramNotificationSettings = TelegramSettings &
NotifierSettings &
NotificationSettings;
NotificationSettings &
TelegramMessageTemplateSettings;
export interface WebhookEventSettings {
liveBegan: boolean;

View File

@ -1,5 +1,5 @@
@use '../../../shared/styles/layout';
@use '../../../shared/styles/list';
@use "../../../shared/styles/layout";
@use "../../../shared/styles/list";
.settings-page {
@extend %inner-page;
@ -43,6 +43,11 @@ a.setting-item {
text-decoration: none;
color: inherit;
@include actionable;
height: 60px;
&:not(:first-child) {
height: 61px;
}
}
.setting-label {
@ -90,6 +95,11 @@ a.setting-item {
max-width: 100% !important;
width: 100% !important;
margin-left: 0;
&::-webkit-scrollbar {
width: 4px;
height: 4px;
}
}
@media screen and (max-width: 332px) {

View File

@ -19,6 +19,7 @@
&::-webkit-scrollbar {
background-color: transparent;
width: 4px;
height: 4px;
}
&::-webkit-scrollbar-track {

View File

@ -2,6 +2,7 @@
@media screen and (min-width: 768px) {
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {