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 include_package_data = True
python_requires = >= 3.8 python_requires = >= 3.8
install_requires = install_requires =
python-liquid >= 1.2.1, < 2.0.0
typing-extensions >= 3.10.0.0 typing-extensions >= 3.10.0.0
ordered-set >= 4.1.0, < 5.0.0 ordered-set >= 4.1.0, < 5.0.0
fastapi >= 0.70.0, < 0.71.0 fastapi >= 0.70.0, < 0.71.0

View File

@ -24,9 +24,9 @@ from .setting import (
Settings, Settings,
SettingsIn, SettingsIn,
SettingsOut, SettingsOut,
KeySetOfSettings,
TaskOptions, TaskOptions,
) )
from .setting.typing import KeySetOfSettings
from .notification import ( from .notification import (
EmailNotifier, EmailNotifier,
ServerchanNotifier, 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="icon" type="image/x-icon" href="assets/images/logo.png">
<link rel="manifest" href="manifest.webmanifest"> <link rel="manifest" href="manifest.webmanifest">
<meta name="theme-color" content="#1976d2"> <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> <body>
<app-root></app-root> <app-root></app-root>
<noscript>Please enable JavaScript to continue using this application.</noscript> <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> </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, "configVersion": 1,
"timestamp": 1654692172512, "timestamp": 1655391613431,
"index": "/index.html", "index": "/index.html",
"assetGroups": [ "assetGroups": [
{ {
@ -13,17 +13,17 @@
"urls": [ "urls": [
"/103.5b5d2a6e5a8a7479.js", "/103.5b5d2a6e5a8a7479.js",
"/146.92e3b29c4c754544.js", "/146.92e3b29c4c754544.js",
"/183.8cf3b5282412a0ec.js", "/183.ae1a1102b7d5cbdb.js",
"/202.e15e5ae9f06639b8.js",
"/45.c90c3cea2bf1a66e.js", "/45.c90c3cea2bf1a66e.js",
"/474.7f6529972e383566.js", "/66.9faa0b5a6adf9602.js",
"/66.31f5b9ae46ae9005.js",
"/common.858f777e9296e6f2.js", "/common.858f777e9296e6f2.js",
"/index.html", "/index.html",
"/main.411b4a979eb179f8.js", "/main.888c50197ddf8040.js",
"/manifest.webmanifest", "/manifest.webmanifest",
"/polyfills.4b08448aee19bb22.js", "/polyfills.4b08448aee19bb22.js",
"/runtime.dc2f7d56c437cd78.js", "/runtime.0ce129f346263990.js",
"/styles.1f581691b230dc4d.css" "/styles.2e152d608221c2ee.css"
], ],
"patterns": [] "patterns": []
}, },
@ -1636,10 +1636,10 @@
"hashTable": { "hashTable": {
"/103.5b5d2a6e5a8a7479.js": "cc0240f217015b6d4ddcc14f31fcc42e1c1c282a", "/103.5b5d2a6e5a8a7479.js": "cc0240f217015b6d4ddcc14f31fcc42e1c1c282a",
"/146.92e3b29c4c754544.js": "3824de681dd1f982ea69a065cdf54d7a1e781f4d", "/146.92e3b29c4c754544.js": "3824de681dd1f982ea69a065cdf54d7a1e781f4d",
"/183.8cf3b5282412a0ec.js": "bf93e3b9baf3d6c0eb5e320c4916d5bf540c4cb0", "/183.ae1a1102b7d5cbdb.js": "6cb22d60b0a20214212e6050fbbf33926a4c1346",
"/202.e15e5ae9f06639b8.js": "62335dc98644969539760565ff9c3c472d304287",
"/45.c90c3cea2bf1a66e.js": "e5bfb8cf3803593e6b8ea14c90b3d3cb6a066764", "/45.c90c3cea2bf1a66e.js": "e5bfb8cf3803593e6b8ea14c90b3d3cb6a066764",
"/474.7f6529972e383566.js": "1c74b5c6379705a3110c99767f97feddc42a0d54", "/66.9faa0b5a6adf9602.js": "c2f418ebb80f35402d9f24e5acaf8167c96f9eb3",
"/66.31f5b9ae46ae9005.js": "cc22d2582d8e4c2a83e089d5a1ec32619e439ccd",
"/assets/animal/panda.js": "fec2868bb3053dd2da45f96bbcb86d5116ed72b1", "/assets/animal/panda.js": "fec2868bb3053dd2da45f96bbcb86d5116ed72b1",
"/assets/animal/panda.svg": "bebd302cdc601e0ead3a6d2710acf8753f3d83b1", "/assets/animal/panda.svg": "bebd302cdc601e0ead3a6d2710acf8753f3d83b1",
"/assets/fill/.gitkeep": "da39a3ee5e6b4b0d3255bfef95601890afd80709", "/assets/fill/.gitkeep": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
@ -3234,12 +3234,12 @@
"/assets/twotone/warning.js": "fb2d7ea232f3a99bf8f080dbc94c65699232ac01", "/assets/twotone/warning.js": "fb2d7ea232f3a99bf8f080dbc94c65699232ac01",
"/assets/twotone/warning.svg": "8c7a2d3e765a2e7dd58ac674870c6655cecb0068", "/assets/twotone/warning.svg": "8c7a2d3e765a2e7dd58ac674870c6655cecb0068",
"/common.858f777e9296e6f2.js": "b68ca68e1e214a2537d96935c23410126cc564dd", "/common.858f777e9296e6f2.js": "b68ca68e1e214a2537d96935c23410126cc564dd",
"/index.html": "e7c15c095d8d2a11a60d7211e17e8a38316adf0e", "/index.html": "374ebd2a9b656c5ebcbc9f5a4402b345cd4c7c5c",
"/main.411b4a979eb179f8.js": "4c5e77b0589a77410f84441d0877c1d18cb1357f", "/main.888c50197ddf8040.js": "f506b85641a4598b002c21bc49c9a36e0c058326",
"/manifest.webmanifest": "62c1cb8c5ad2af551a956b97013ab55ce77dd586", "/manifest.webmanifest": "62c1cb8c5ad2af551a956b97013ab55ce77dd586",
"/polyfills.4b08448aee19bb22.js": "8e73f2d42cc13ca353cea5c886d930bd6da08d0d", "/polyfills.4b08448aee19bb22.js": "8e73f2d42cc13ca353cea5c886d930bd6da08d0d",
"/runtime.dc2f7d56c437cd78.js": "fcf22060f48d6229236a14de31cc63c3568db8b1", "/runtime.0ce129f346263990.js": "98698b10b3f873a761f1e1c7fb5a9bcd2f3830ee",
"/styles.1f581691b230dc4d.css": "6f5befbbad57c2b2e80aae855139744b8010d150" "/styles.2e152d608221c2ee.css": "9830389a46daa5b4511e0dd343aad23ca9f9690f"
}, },
"navigationUrls": [ "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 asyncio
import logging
import os
from abc import ABC, abstractmethod 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 ( from tenacity import (
AsyncRetrying, AsyncRetrying,
wait_exponential,
stop_after_delay,
retry_if_exception, 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 ( from ..event import (
Error,
ErrorData,
EventCenter, EventCenter,
LiveBeganEvent, LiveBeganEvent,
LiveEndedEvent, LiveEndedEvent,
SpaceNoEnoughEvent, SpaceNoEnoughEvent,
) )
from ..event.typing import Event 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__ = ( __all__ = (
'Notifier', 'Notifier',
@ -55,12 +60,45 @@ class Notifier(SwitchableMixin, ABC):
notify_ended: bool = False, notify_ended: bool = False,
notify_space: bool = False, notify_space: bool = False,
notify_error: 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: ) -> None:
super().__init__() 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_began = notify_began
self.notify_ended = notify_ended self.notify_ended = notify_ended
self.notify_space = notify_space self.notify_space = notify_space
self.notify_error = notify_error 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: def _do_enable(self) -> None:
events = EventCenter.get_instance().events events = EventCenter.get_instance().events
@ -87,7 +125,8 @@ class Notifier(SwitchableMixin, ABC):
def _on_exception(self, exc: BaseException) -> None: def _on_exception(self, exc: BaseException) -> None:
if self._should_notify_exception(exc): 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: def _should_notify_live_began(self, event: LiveBeganEvent) -> bool:
return self.notify_began return self.notify_began
@ -95,9 +134,7 @@ class Notifier(SwitchableMixin, ABC):
def _should_notify_live_ended(self, event: LiveEndedEvent) -> bool: def _should_notify_live_ended(self, event: LiveEndedEvent) -> bool:
return self.notify_ended return self.notify_ended
def _should_notify_space_no_enough( def _should_notify_space_no_enough(self, event: SpaceNoEnoughEvent) -> bool:
self, event: SpaceNoEnoughEvent
) -> bool:
return self.notify_space return self.notify_space
def _should_notify_exception(self, exc: BaseException) -> bool: def _should_notify_exception(self, exc: BaseException) -> bool:
@ -116,56 +153,184 @@ class Notifier(SwitchableMixin, ABC):
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
def _notify_exception(self, exc: BaseException) -> None: def _notify_exception(self, event: Error) -> None:
raise NotImplementedError 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): class MessageNotifier(Notifier, ABC):
provider: MessagingProvider provider: MessagingProvider
def _notify_live_began(self, event: LiveBeganEvent) -> None: def _notify_live_began(self, event: LiveBeganEvent) -> None:
title = f'{event.data.user_info.name} 开播啦' title, content = self._make_began_message(event)
content = make_live_info_content( self._send_message(title, content, self.began_message_type)
event.data.user_info, event.data.room_info
)
self._send_message(title, content)
def _notify_live_ended(self, event: LiveEndedEvent) -> None: def _notify_live_ended(self, event: LiveEndedEvent) -> None:
title = f'{event.data.user_info.name} 下播了' title, content = self._make_ended_message(event)
content = make_live_info_content( self._send_message(title, content, self.ended_message_type)
event.data.user_info, event.data.room_info
)
self._send_message(title, content)
def _notify_space_no_enough(self, event: SpaceNoEnoughEvent) -> None: def _notify_space_no_enough(self, event: SpaceNoEnoughEvent) -> None:
title, content = self._make_space_message(event)
self._send_message(title, content, self.space_message_type)
def _notify_exception(self, event: Error) -> None:
title, content = self._make_error_message(event)
self._send_message(title, content, self.error_message_type)
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
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 = '空间不足!' title = '空间不足!'
content = make_disk_usage_content(event.data) content = format_exception(e)
self._send_message(title, content) return title, content
def _notify_exception(self, exc: BaseException) -> None: 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 = '出错了~' title = '出错了~'
content = make_exception_content(exc) content = format_exception(e)
self._send_message(title, content) return title, content
def _send_message(self, title: str, content: str) -> None: def _send_message(self, title: str, content: str, msg_type: MessageType) -> None:
asyncio.create_task(self._send_message_async(title, content)) asyncio.create_task(self._send_message_async(title, content, msg_type))
async def _send_message_async(self, title: str, content: str) -> None: async def _send_message_async(
self, title: str, content: str, msg_type: MessageType
) -> None:
try: try:
async for attempt in AsyncRetrying( async for attempt in AsyncRetrying(
reraise=True, reraise=True,
stop=stop_after_delay(300), stop=stop_after_delay(300),
wait=wait_exponential(multiplier=0.1, max=10), wait=wait_exponential(multiplier=0.1, max=10),
retry=retry_if_exception( retry=retry_if_exception(lambda e: not isinstance(e, ValueError)),
lambda e: not isinstance(e, ValueError)
),
): ):
with attempt: with attempt:
await self.provider.send_message(title, content) await self.provider.send_message(title, content, msg_type)
except Exception as e: except Exception as e:
logger.warning('Failed to send a message via {}: {}'.format( logger.warning(
'Failed to send a message via {}: {}'.format(
self.provider.__class__.__name__, repr(e) 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): class EmailNotifier(MessageNotifier):
@ -226,3 +391,15 @@ class TelegramNotifier(MessageNotifier):
def _do_disable(self) -> None: def _do_disable(self) -> None:
super()._do_disable() super()._do_disable()
logger.debug('Disabled Telegram notifier') 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 abc import ABC, abstractmethod
from email.message import EmailMessage from email.message import EmailMessage
from http.client import HTTPException 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 from urllib.parse import urljoin
import aiohttp import aiohttp
from ..setting.typing import (
EmailMessageType,
MessageType,
PushdeerMessageType,
PushplusMessageType,
ServerchanMessageType,
TelegramMessageType,
)
from ..utils.patterns import Singleton from ..utils.patterns import Singleton
__all__ = ( __all__ = (
@ -30,13 +38,12 @@ class MessagingProvider(Singleton, ABC):
super().__init__() super().__init__()
@abstractmethod @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): class EmailService(MessagingProvider):
def __init__( def __init__(
self, self,
@ -54,7 +61,7 @@ class EmailService(MessagingProvider):
self.smtp_port = smtp_port self.smtp_port = smtp_port
async def send_message( async def send_message(
self, subject: str, content: str, msg_type: MSG_TYPE = 'plain' self, subject: str, content: str, msg_type: MessageType
) -> None: ) -> None:
self._check_parameters() self._check_parameters()
await asyncio.get_running_loop().run_in_executor( await asyncio.get_running_loop().run_in_executor(
@ -62,13 +69,14 @@ class EmailService(MessagingProvider):
) )
def _send_email( def _send_email(
self, subject: str, content: str, msg_type: MSG_TYPE = 'plain' self, subject: str, content: str, msg_type: EmailMessageType
) -> None: ) -> None:
msg = EmailMessage() msg = EmailMessage()
msg['Subject'] = subject msg['Subject'] = subject
msg['From'] = self.src_addr msg['From'] = self.src_addr
msg['To'] = self.dst_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: try:
with smtplib.SMTP_SSL(self.smtp_host, self.smtp_port) as smtp: with smtplib.SMTP_SSL(self.smtp_host, self.smtp_port) as smtp:
@ -97,15 +105,19 @@ class Serverchan(MessagingProvider):
super().__init__() super().__init__()
self.sendkey = sendkey 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() self._check_parameters()
await self._post_message(title, content) await self._post_message(title, content, cast(ServerchanMessageType, msg_type))
def _check_parameters(self) -> None: def _check_parameters(self) -> None:
if not self.sendkey: if not self.sendkey:
raise ValueError('No sendkey supplied') 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' url = f'https://sctapi.ftqq.com/{self.sendkey}.send'
payload = {'text': title, 'desp': content} payload = {'text': title, 'desp': content}
@ -129,21 +141,25 @@ class Pushdeer(MessagingProvider):
self.server = server self.server = server
self.pushkey = pushkey 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() self._check_parameters()
await self._post_message(title, content) await self._post_message(title, content, cast(PushdeerMessageType, msg_type))
def _check_parameters(self) -> None: def _check_parameters(self) -> None:
if not self.pushkey: if not self.pushkey:
raise ValueError('No pushkey supplied') 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) url = urljoin(self.server or self._server, self._endpoint)
payload = { payload = {
'pushkey': self.pushkey, 'pushkey': self.pushkey,
'text': title, 'text': title,
'desp': content, 'desp': content,
'type': 'text', 'type': msg_type,
} }
async with aiohttp.ClientSession(raise_for_status=True) as session: async with aiohttp.ClientSession(raise_for_status=True) as session:
async with session.post(url, json=payload) as res: async with session.post(url, json=payload) as res:
@ -166,21 +182,25 @@ class Pushplus(MessagingProvider):
self.token = token self.token = token
self.topic = topic 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() self._check_parameters()
await self._post_message(title, content) await self._post_message(title, content, msg_type)
def _check_parameters(self) -> None: def _check_parameters(self) -> None:
if not self.token: if not self.token:
raise ValueError('No token supplied') 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 = { payload = {
'title': title, 'title': title,
'content': content, 'content': content,
'token': self.token, 'token': self.token,
'topic': self.topic, 'topic': self.topic,
'template': 'html', 'template': msg_type,
} }
async with aiohttp.ClientSession(raise_for_status=True) as session: async with aiohttp.ClientSession(raise_for_status=True) as session:
@ -201,9 +221,11 @@ class Telegram(MessagingProvider):
self.token = token self.token = token
self.chatid = chatid 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() self._check_parameters()
await self._post_message(title, content) await self._post_message(title, content, cast(TelegramMessageType, msg_type))
def _check_parameters(self) -> None: def _check_parameters(self) -> None:
if not self.token: if not self.token:
@ -211,12 +233,14 @@ class Telegram(MessagingProvider):
if not self.chatid: if not self.chatid:
raise ValueError('No chatid supplied') 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' url = f'https://api.telegram.org/bot{self.token}/sendMessage'
payload = { payload = {
'chat_id': self.chatid, 'chat_id': self.chatid,
'text': title + '\n' + content, 'text': title + '\n\n' + content,
'parse_mode': 'HTML', 'parse_mode': 'MarkdownV2' if msg_type == 'markdown' else 'HTML',
} }
async with aiohttp.ClientSession(raise_for_status=True) as session: 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 ( from .models import (
DEFAULT_SETTINGS_FILE, DEFAULT_SETTINGS_FILE,
DanmakuOptions,
DanmakuSettings,
EmailMessageTemplateSettings,
EmailNotificationSettings,
EmailSettings,
EnvSettings, EnvSettings,
HeaderOptions,
HeaderSettings,
LoggingSettings,
NotificationSettings,
NotifierSettings,
OutputSettings,
PostprocessingOptions,
PostprocessingSettings,
PushdeerMessageTemplateSettings,
PushdeerNotificationSettings,
PushdeerSettings,
PushplusMessageTemplateSettings,
PushplusNotificationSettings,
PushplusSettings,
RecorderOptions,
RecorderSettings,
ServerchanMessageTemplateSettings,
ServerchanNotificationSettings,
ServerchanSettings,
Settings, Settings,
SettingsIn, SettingsIn,
SettingsOut, SettingsOut,
SpaceSettings,
HeaderOptions,
HeaderSettings,
DanmakuOptions,
DanmakuSettings,
RecorderOptions,
RecorderSettings,
PostprocessingSettings,
PostprocessingOptions,
TaskOptions, TaskOptions,
TaskSettings, TaskSettings,
OutputSettings, TelegramMessageTemplateSettings,
LoggingSettings,
SpaceSettings,
EmailSettings,
ServerchanSettings,
PushdeerSettings,
PushplusSettings,
TelegramSettings,
NotifierSettings,
NotificationSettings,
EmailNotificationSettings,
ServerchanNotificationSettings,
PushdeerNotificationSettings,
PushplusNotificationSettings,
TelegramNotificationSettings, TelegramNotificationSettings,
TelegramSettings,
WebHookSettings, WebHookSettings,
) )
from .typing import KeyOfSettings, KeySetOfSettings
from .helpers import update_settings, shadow_settings
from .setting_manager import SettingsManager from .setting_manager import SettingsManager
__all__ = ( __all__ = (
'DEFAULT_SETTINGS_FILE', 'DEFAULT_SETTINGS_FILE',
'EnvSettings', 'EnvSettings',
'Settings', 'Settings',
'SettingsIn', 'SettingsIn',
'SettingsOut', 'SettingsOut',
'KeyOfSettings',
'KeySetOfSettings',
'HeaderOptions', 'HeaderOptions',
'HeaderSettings', 'HeaderSettings',
'DanmakuOptions', 'DanmakuOptions',
@ -58,12 +53,16 @@ __all__ = (
'RecorderSettings', 'RecorderSettings',
'PostprocessingSettings', 'PostprocessingSettings',
'PostprocessingOptions', 'PostprocessingOptions',
'TaskOptions', 'TaskOptions',
'TaskSettings', 'TaskSettings',
'OutputSettings', 'OutputSettings',
'LoggingSettings', 'LoggingSettings',
'SpaceSettings', 'SpaceSettings',
'EmailMessageTemplateSettings',
'ServerchanMessageTemplateSettings',
'PushdeerMessageTemplateSettings',
'PushplusMessageTemplateSettings',
'TelegramMessageTemplateSettings',
'EmailSettings', 'EmailSettings',
'ServerchanSettings', 'ServerchanSettings',
'PushdeerSettings', 'PushdeerSettings',
@ -77,7 +76,6 @@ __all__ = (
'PushplusNotificationSettings', 'PushplusNotificationSettings',
'TelegramNotificationSettings', 'TelegramNotificationSettings',
'WebHookSettings', 'WebHookSettings',
'update_settings', 'update_settings',
'shadow_settings', 'shadow_settings',
'SettingsManager', 'SettingsManager',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,6 +16,8 @@ import {
KEYS_OF_PUSHPLUS_SETTINGS, KEYS_OF_PUSHPLUS_SETTINGS,
KEYS_OF_NOTIFIER_SETTINGS, KEYS_OF_NOTIFIER_SETTINGS,
KEYS_OF_NOTIFICATION_SETTINGS, KEYS_OF_NOTIFICATION_SETTINGS,
MessageTemplateSettings,
KEYS_OF_MESSAGE_TEMPLATE_SETTINGS,
} from '../../shared/setting.model'; } from '../../shared/setting.model';
@Component({ @Component({
@ -28,6 +30,7 @@ export class PushplusNotificationSettingsComponent implements OnInit {
pushplusSettings!: PushplusSettings; pushplusSettings!: PushplusSettings;
notifierSettings!: NotifierSettings; notifierSettings!: NotifierSettings;
notificationSettings!: NotificationSettings; notificationSettings!: NotificationSettings;
messageTemplateSettings!: MessageTemplateSettings;
constructor( constructor(
private changeDetector: ChangeDetectorRef, private changeDetector: ChangeDetectorRef,
@ -37,10 +40,14 @@ export class PushplusNotificationSettingsComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.route.data.subscribe((data) => { this.route.data.subscribe((data) => {
const settings = data.settings as PushplusNotificationSettings; const settings = data.settings as PushplusNotificationSettings;
this.changeDetector.markForCheck();
this.pushplusSettings = pick(settings, KEYS_OF_PUSHPLUS_SETTINGS); this.pushplusSettings = pick(settings, KEYS_OF_PUSHPLUS_SETTINGS);
this.notifierSettings = pick(settings, KEYS_OF_NOTIFIER_SETTINGS); this.notifierSettings = pick(settings, KEYS_OF_NOTIFIER_SETTINGS);
this.notificationSettings = pick(settings, KEYS_OF_NOTIFICATION_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" keyOfSettings="serverchanNotification"
></app-event-settings> ></app-event-settings>
</app-page-section> </app-page-section>
<app-page-section name="消息">
<app-message-template-settings
[settings]="messageTemplateSettings"
keyOfSettings="serverchanNotification"
></app-message-template-settings>
</app-page-section>
</ng-template> </ng-template>
</app-sub-page> </app-sub-page>

View File

@ -9,9 +9,11 @@ import { ActivatedRoute } from '@angular/router';
import pick from 'lodash-es/pick'; import pick from 'lodash-es/pick';
import { import {
KEYS_OF_MESSAGE_TEMPLATE_SETTINGS,
KEYS_OF_NOTIFICATION_SETTINGS, KEYS_OF_NOTIFICATION_SETTINGS,
KEYS_OF_NOTIFIER_SETTINGS, KEYS_OF_NOTIFIER_SETTINGS,
KEYS_OF_SERVERCHAN_SETTINGS, KEYS_OF_SERVERCHAN_SETTINGS,
MessageTemplateSettings,
NotificationSettings, NotificationSettings,
NotifierSettings, NotifierSettings,
ServerchanNotificationSettings, ServerchanNotificationSettings,
@ -28,6 +30,7 @@ export class ServerchanNotificationSettingsComponent implements OnInit {
serverchanSettings!: ServerchanSettings; serverchanSettings!: ServerchanSettings;
notifierSettings!: NotifierSettings; notifierSettings!: NotifierSettings;
notificationSettings!: NotificationSettings; notificationSettings!: NotificationSettings;
messageTemplateSettings!: MessageTemplateSettings;
constructor( constructor(
private changeDetector: ChangeDetectorRef, private changeDetector: ChangeDetectorRef,
@ -40,6 +43,10 @@ export class ServerchanNotificationSettingsComponent implements OnInit {
this.serverchanSettings = pick(settings, KEYS_OF_SERVERCHAN_SETTINGS); this.serverchanSettings = pick(settings, KEYS_OF_SERVERCHAN_SETTINGS);
this.notifierSettings = pick(settings, KEYS_OF_NOTIFIER_SETTINGS); this.notifierSettings = pick(settings, KEYS_OF_NOTIFIER_SETTINGS);
this.notificationSettings = pick(settings, KEYS_OF_NOTIFICATION_SETTINGS); this.notificationSettings = pick(settings, KEYS_OF_NOTIFICATION_SETTINGS);
this.messageTemplateSettings = pick(
settings,
KEYS_OF_MESSAGE_TEMPLATE_SETTINGS
);
this.changeDetector.markForCheck(); 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" keyOfSettings="telegramNotification"
></app-event-settings> ></app-event-settings>
</app-page-section> </app-page-section>
<app-page-section name="消息">
<app-message-template-settings
[settings]="messageTemplateSettings"
keyOfSettings="telegramNotification"
></app-message-template-settings>
</app-page-section>
</ng-template> </ng-template>
</app-sub-page> </app-sub-page>

View File

@ -16,6 +16,8 @@ import {
KEYS_OF_TELEGRAM_SETTINGS, KEYS_OF_TELEGRAM_SETTINGS,
KEYS_OF_NOTIFIER_SETTINGS, KEYS_OF_NOTIFIER_SETTINGS,
KEYS_OF_NOTIFICATION_SETTINGS, KEYS_OF_NOTIFICATION_SETTINGS,
MessageTemplateSettings,
KEYS_OF_MESSAGE_TEMPLATE_SETTINGS,
} from '../../shared/setting.model'; } from '../../shared/setting.model';
@Component({ @Component({
@ -28,6 +30,7 @@ export class TelegramNotificationSettingsComponent implements OnInit {
telegramSettings!: TelegramSettings; telegramSettings!: TelegramSettings;
notifierSettings!: NotifierSettings; notifierSettings!: NotifierSettings;
notificationSettings!: NotificationSettings; notificationSettings!: NotificationSettings;
messageTemplateSettings!: MessageTemplateSettings;
constructor( constructor(
private changeDetector: ChangeDetectorRef, private changeDetector: ChangeDetectorRef,
@ -37,10 +40,14 @@ export class TelegramNotificationSettingsComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.route.data.subscribe((data) => { this.route.data.subscribe((data) => {
const settings = data.settings as TelegramNotificationSettings; const settings = data.settings as TelegramNotificationSettings;
this.changeDetector.markForCheck();
this.telegramSettings = pick(settings, KEYS_OF_TELEGRAM_SETTINGS); this.telegramSettings = pick(settings, KEYS_OF_TELEGRAM_SETTINGS);
this.notifierSettings = pick(settings, KEYS_OF_NOTIFIER_SETTINGS); this.notifierSettings = pick(settings, KEYS_OF_NOTIFIER_SETTINGS);
this.notificationSettings = pick(settings, KEYS_OF_NOTIFICATION_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 { OutdirEditDialogComponent } from './output-settings/outdir-edit-dialog/outdir-edit-dialog.component';
import { LogdirEditDialogComponent } from './logging-settings/logdir-edit-dialog/logdir-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 { 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({ @NgModule({
declarations: [ declarations: [
@ -97,6 +99,8 @@ import { PathTemplateEditDialogComponent } from './output-settings/path-template
OutdirEditDialogComponent, OutdirEditDialogComponent,
LogdirEditDialogComponent, LogdirEditDialogComponent,
PathTemplateEditDialogComponent, PathTemplateEditDialogComponent,
MessageTemplateSettingsComponent,
MessageTemplateEditDialogComponent,
], ],
imports: [ imports: [
CommonModule, CommonModule,

View File

@ -175,25 +175,152 @@ export const KEYS_OF_NOTIFICATION_SETTINGS = [
'notifySpace', 'notifySpace',
] as const; ] 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 & export type EmailNotificationSettings = EmailSettings &
NotifierSettings & NotifierSettings &
NotificationSettings; NotificationSettings &
EmailMessageTemplateSettings;
export type ServerchanNotificationSettings = ServerchanSettings & export type ServerchanNotificationSettings = ServerchanSettings &
NotifierSettings & NotifierSettings &
NotificationSettings; NotificationSettings &
ServerchanMessageTemplateSettings;
export type PushdeerNotificationSettings = PushdeerSettings & export type PushdeerNotificationSettings = PushdeerSettings &
NotifierSettings & NotifierSettings &
NotificationSettings; NotificationSettings &
PushdeerMessageTemplateSettings;
export type PushplusNotificationSettings = PushplusSettings & export type PushplusNotificationSettings = PushplusSettings &
NotifierSettings & NotifierSettings &
NotificationSettings; NotificationSettings &
PushdeerMessageTemplateSettings;
export type TelegramNotificationSettings = TelegramSettings & export type TelegramNotificationSettings = TelegramSettings &
NotifierSettings & NotifierSettings &
NotificationSettings; NotificationSettings &
TelegramMessageTemplateSettings;
export interface WebhookEventSettings { export interface WebhookEventSettings {
liveBegan: boolean; liveBegan: boolean;

View File

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

View File

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

View File

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