通知推送拆成两个渠道:QQ 机器人和 MeoW,各自带开关和测试连通
用户要求两件事:① QQ bot 推送要能测连通、并加一个开关;② 照 MeoW 的接口 文档(https://www.chuckfang.com/MeoW/api_doc.html)加一个推送渠道,同样要有 测试和开关。确认过的三个决定:QQ bot 就是现有 webhook;事件勾选两个渠道 共用、开关按渠道分开;MeoW 只填一个昵称。 1) 通知推送改成「共享事件勾选 + 每渠道一个开关」 - notify.go: notifySetting 扩成两个渠道,新增 qqReady()/meowReady(); sendNotify 按渠道各自起后台 goroutine 分发,事件没勾选、或两个渠道都 没启用时什么都不做 - config.go: 新增 notifyEnabled(QQ 机器人渠道开关)、meowEnabled、 meowNickname、meowMsgType、meowHtmlHeight、meowImgUrl - 关掉开关只是暂停推送,地址、昵称这些配置都留着,随时能再打开 2) 老配置的兼容(重要) - notifyEnabled 是普通 bool,老 config.json 里没有这个字段会解成 false, 升级后会把已经配好的推送静默关掉 - 解法:loadConfig() 里先置 true 再 json.Unmarshal(Unmarshal 只覆盖文件里 出现的字段),默认值里也写 true;文件里显式写 false 的仍然会关掉 - 已加回归测试 TestNotifyEnabledBackwardCompat 覆盖三种写法 3) MeoW 渠道(新增 meow.go) - 按文档走 POST {昵称},title/msg/url/imgUrl 放 json body, msgType/htmlHeight 是 query 参数(文档明确说这两个不属于 JSON 参数) - 文档自己前后矛盾:响应格式那节写 msg,示例写 message,两个字段都解析 - 空字段一律不放进 body:body 里的 title 优先级高于路径参数, 传空串会把 MeoW 的默认标题顶掉 - 业务状态码翻成中文(400 参数错、403 内容被拦、404 昵称未注册、 429 频控、500 服务器错误),失败信息直接显示在页面上 - msgType 为 html 时渲染成 HTML(首行加粗、链接可点、内容转义), 高度默认 200、范围 40-2000;事件正文复用 webhook 的那份 text 4) MeoW 的错误昵称会封 IP,所以加了保护(踩出来的) - 实测用不存在的昵称打真实接口,返回的提示是「一天之内累计3次输错对方 昵称您的IP将被封禁」。而开播+开始录制+下播+合并完成一天轻松触发 4 次, 昵称配错就会把用户 IP 封了 - meow.go: 新增 meowBadNick 记录,pushMeow 收到 404 就记下这个昵称; sendNotify 和测试接口命中记录时直接跳过/拒绝并说明原因, 改掉昵称或重启程序即恢复 - 只有 404 触发拦截:429 是频控、400 是参数问题,都应该继续尝试 - 已加测试 TestMeowBadNickGuard(404 要拦、429/400 不能拦、不影响别的昵称) 5) 测试连通 - /api/notify/test 合并成一个接口,用 channel 区分 qq / meow, 每个渠道的配置下面各有一个「测试连通」按钮 - 用页面上当前填的值直接测、不用先保存,留空的项自动用已保存的值补齐; 测试不受渠道开关和事件勾选影响(本来就是为了打开之前先确认配置) - 成功返回 {ok, detail},detail 就是页面上的结论:QQ 渠道显示 webhook 的 HTTP 状态码和耗时,MeoW 渠道显示它返回的 status 和耗时; webhook 返回 200 只代表地址口令没问题,能不能收到要看机器人那边, 这点也写在结果里 - /api/status 新增 meowBlocked,页面上红字显示「当前推不出去:…」 (昵称没填、格式不对、或已被判过未注册) 6) 页面 - 通知区分成「事件勾选 / 渠道一 QQ 机器人 / 渠道二 MeoW」三块, 每块用一条带标题的细线隔开,两个渠道各带开关和测试按钮 - 消息类型选 text 时自动收起 HTML 高度那一行 另外:README 的通知推送一节重写(两个渠道的对比表、MeoW 的频控与封禁提醒、 测试连通的说明),配置文件详解补上新字段;doc/webapi.md 补 /api/notify/test 和 meowBlocked。doc/momoko-acfunlive-notify.py 本来就处理 event="test",不用改。 已验证:新增 notify_test.go,15 个用例全部通过(URL 拼接与转义、昵称校验、 消息类型与高度归一、HTML 渲染与转义、接口成功/失败/空字段处理、6 种业务状态码 的文案、404 拦截保护、老配置兼容、测试接口各分支),其中 TestSendNotifyFanOut 是端到端分发测试:两个渠道都开→各收到 1 条、关掉任一个→只剩另一个、 两个都关→0 条、事件没勾→0 条。真机冒烟两轮:页面控件齐全、老配置默认开启、 QQ 渠道实测本地 webhook 收到完整推送(含 Authorization 和 X-Auth-Token 头与群号)、 空值回退到已保存配置、6 种非法输入全 400、开关能落盘且关开关不清地址、 MeoW 真实接口返回的 404 被正确翻成中文。全量 34 个测试通过, Windows/Linux 双平台构建、go vet、gofmt 全干净。
This commit is contained in:
@@ -53,7 +53,10 @@ const webuiHTML = `<!DOCTYPE html>
|
||||
button:active,a.ghost:active{opacity:.8}
|
||||
button:disabled{opacity:.5}
|
||||
.hint{font-size:12px;color:var(--sub);margin-top:6px;line-height:1.6}
|
||||
.hint.bad{color:#d63046}
|
||||
.hr{height:1px;background:var(--soft);margin:12px -14px}
|
||||
.ch{display:flex;align-items:center;gap:8px;font-size:12px;font-weight:600;color:#333;margin:12px 0 8px}
|
||||
.ch::after{content:"";flex:1;height:1px;background:var(--soft)}
|
||||
.row{display:flex;flex-wrap:wrap;align-items:center;gap:6px 12px;padding:10px 0;border-top:1px solid var(--soft)}
|
||||
.row:first-child{border-top:none;padding-top:2px}
|
||||
.uid{font-size:12px;color:var(--sub);font-variant-numeric:tabular-nums}
|
||||
@@ -114,21 +117,44 @@ const webuiHTML = `<!DOCTYPE html>
|
||||
<div class="hint">录制是 -c copy 直接封装,不转码,四种格式画质完全一样,只是容器不同。正常停止(停止按钮、Ctrl+C)都没问题;如果进程被强杀(任务管理器结束进程、断电),mp4 / flv / mkv 都会整个报废,实测只有 ts 还能播出已经录到的部分。合并出来的成片始终是 mp4。</div>
|
||||
<div class="hint">同一主播下播后 20 秒内又上播会当成网络波动,接着录同一场,最终只合成一个成片。</div>
|
||||
<div class="hr"></div>
|
||||
<div class="grid">
|
||||
<div class="f"><label>通知推送地址(留空 = 不推送,POST JSON)</label><input type="text" id="notifyUrl" placeholder="例如 http://127.0.0.1:8080/acfunlive/notify"></div>
|
||||
<div class="f"><label>推送口令(留空 = 不带,作为 Bearer 发送)</label><input type="text" id="notifyToken" placeholder="可选"></div>
|
||||
<div class="f"><label>通知群号(随推送下发,机器人用它决定发到哪)</label><input type="text" id="notifyGroup" inputmode="numeric" placeholder="例如 777163254,留空则用机器人自己的默认群"></div>
|
||||
</div>
|
||||
<div style="margin-top:8px">
|
||||
<div class="ch">通知推送</div>
|
||||
<div>
|
||||
<label class="chk"><input type="checkbox" id="notifyLiveOn">开播</label>
|
||||
<label class="chk"><input type="checkbox" id="notifyLiveOff">下播</label>
|
||||
<label class="chk"><input type="checkbox" id="notifyRecordStart">开始录制</label>
|
||||
<label class="chk"><input type="checkbox" id="notifyMergeDone">合并完成</label>
|
||||
</div>
|
||||
<div class="hint">勾选的事件会以 JSON 推送到上面的地址(字段 event / uid / name / title / url / file / group / text / time,text 可以直接发给群);推送失败只记警告,不影响录制</div>
|
||||
<div class="hint">这一行勾的是「推哪些事件」,下面两个渠道共用;推送失败只记警告,不影响录制</div>
|
||||
|
||||
<div class="ch">渠道一 · QQ 机器人(webhook)</div>
|
||||
<div><label class="chk"><input type="checkbox" id="notifyEnabled">启用这个渠道</label></div>
|
||||
<div class="grid" style="margin-top:8px">
|
||||
<div class="f"><label>通知推送地址(留空 = 不推送,POST JSON)</label><input type="text" id="notifyUrl" placeholder="例如 http://127.0.0.1:8080/acfunlive/notify"></div>
|
||||
<div class="f"><label>推送口令(留空 = 不带,作为 Bearer 发送)</label><input type="text" id="notifyToken" placeholder="可选"></div>
|
||||
<div class="f"><label>通知群号(随推送下发,机器人用它决定发到哪)</label><input type="text" id="notifyGroup" inputmode="numeric" placeholder="例如 777163254,留空则用机器人自己的默认群"></div>
|
||||
</div>
|
||||
<div class="hint">推送体是 json,字段 event / uid / name / title / url / file / group / text / time,text 可以直接发给群</div>
|
||||
<div style="margin-top:8px;display:flex;flex-wrap:wrap;gap:8px;align-items:center">
|
||||
<button class="ghost" onclick="testNotify(this)">发送测试通知</button>
|
||||
<span class="hint" style="margin:0">用上面填写的地址直接测试,不需要先保存</span>
|
||||
<button class="ghost" onclick="testNotify(this,'qq')">测试连通</button>
|
||||
<span class="hint" style="margin:0">用上面填写的地址发一条测试推送,不用先保存</span>
|
||||
</div>
|
||||
|
||||
<div class="ch">渠道二 · MeoW</div>
|
||||
<div><label class="chk"><input type="checkbox" id="meowEnabled">启用这个渠道</label></div>
|
||||
<div class="grid" style="margin-top:8px">
|
||||
<div class="f"><label>MeoW 昵称(接收者)</label><input type="text" id="meowNickname" placeholder="你在 MeoW 里的昵称"></div>
|
||||
<div class="f"><label>消息类型</label><select id="meowMsgType">
|
||||
<option value="text">text(纯文本)</option>
|
||||
<option value="html">html(在 App 里渲染)</option>
|
||||
</select></div>
|
||||
<div class="f" id="meowHeightRow"><label>HTML 显示高度(像素)</label><input type="text" id="meowHtmlHeight" inputmode="numeric" placeholder="200"></div>
|
||||
<div class="f"><label>通知图标 URL(可选)</label><input type="text" id="meowImgUrl" placeholder="建议 216×216 的 PNG,留空用 MeoW 默认图标"></div>
|
||||
</div>
|
||||
<div class="hint">推给 MeoW 里这个昵称,手机上直接收通知,点开可以跳回直播间;接口有频控(3 秒 1 条、每分钟 15 条、每小时 60 条)</div>
|
||||
<div class="hint bad" id="meowBlocked" style="display:none"></div>
|
||||
<div style="margin-top:8px;display:flex;flex-wrap:wrap;gap:8px;align-items:center">
|
||||
<button class="ghost" onclick="testNotify(this,'meow')">测试连通</button>
|
||||
<span class="hint" style="margin:0">用上面填写的昵称发一条测试推送,不用先保存</span>
|
||||
</div>
|
||||
<div style="margin-top:10px">
|
||||
<label class="chk"><input type="checkbox" id="webExternal">允许外网访问(关闭时只有本机能打开本页)</label>
|
||||
@@ -329,10 +355,20 @@ async function refresh(){
|
||||
setVal("notifyUrl", st.notifyUrl||"");
|
||||
setVal("notifyToken", st.notifyToken||"");
|
||||
setVal("notifyGroup", st.notifyGroup||"");
|
||||
setChk("notifyEnabled", st.notifyEnabled);
|
||||
setChk("notifyLiveOn", st.notifyLiveOn);
|
||||
setChk("notifyLiveOff", st.notifyLiveOff);
|
||||
setChk("notifyRecordStart", st.notifyRecordStart);
|
||||
setChk("notifyMergeDone", st.notifyMergeDone);
|
||||
setChk("meowEnabled", st.meowEnabled);
|
||||
setVal("meowNickname", st.meowNickname||"");
|
||||
setVal("meowMsgType", st.meowMsgType||"text");
|
||||
setVal("meowHtmlHeight", st.meowHtmlHeight||200);
|
||||
setVal("meowImgUrl", st.meowImgUrl||"");
|
||||
toggleMeowHeight();
|
||||
var mb = document.getElementById("meowBlocked");
|
||||
if(st.meowBlocked){ mb.style.display = ""; mb.textContent = "当前推不出去:" + st.meowBlocked; }
|
||||
else mb.style.display = "none";
|
||||
setChk("webExternal", st.webExternal);
|
||||
setChk("autoMerge", st.autoMerge);
|
||||
setChk("mergeDelVideo", st.mergeDelVideo);
|
||||
@@ -412,6 +448,7 @@ async function saveSettings(b){
|
||||
mergeSubdir: document.getElementById("mergeSubdir").value.trim(),
|
||||
ffmpegPath: document.getElementById("ffmpegPath").value.trim(),
|
||||
recordFormat: document.getElementById("recordFormat").value,
|
||||
notifyEnabled: document.getElementById("notifyEnabled").checked,
|
||||
notifyUrl: document.getElementById("notifyUrl").value.trim(),
|
||||
notifyToken: document.getElementById("notifyToken").value.trim(),
|
||||
notifyGroup: document.getElementById("notifyGroup").value.trim(),
|
||||
@@ -419,17 +456,28 @@ async function saveSettings(b){
|
||||
notifyLiveOff: document.getElementById("notifyLiveOff").checked,
|
||||
notifyRecordStart: document.getElementById("notifyRecordStart").checked,
|
||||
notifyMergeDone: document.getElementById("notifyMergeDone").checked,
|
||||
meowEnabled: document.getElementById("meowEnabled").checked,
|
||||
meowNickname: document.getElementById("meowNickname").value.trim(),
|
||||
meowMsgType: document.getElementById("meowMsgType").value,
|
||||
meowHtmlHeight: parseInt(document.getElementById("meowHtmlHeight").value, 10) || 0,
|
||||
meowImgUrl: document.getElementById("meowImgUrl").value.trim(),
|
||||
webExternal: document.getElementById("webExternal").checked
|
||||
});
|
||||
if(ok) showMsg(true, "已保存");
|
||||
if(b) b.disabled = false;
|
||||
}
|
||||
// 勾选类设置修改后立即保存,避免被定时刷新还原
|
||||
["autoMerge","mergeDelVideo","mergeDelDanmu","webExternal","notifyLiveOn","notifyLiveOff","notifyRecordStart","notifyMergeDone"].forEach(function(id){
|
||||
["autoMerge","mergeDelVideo","mergeDelDanmu","webExternal","notifyEnabled","notifyLiveOn","notifyLiveOff","notifyRecordStart","notifyMergeDone","meowEnabled"].forEach(function(id){
|
||||
document.getElementById(id).addEventListener("change", function(){ saveSettings(null); });
|
||||
});
|
||||
// 下拉框改完也立即保存
|
||||
document.getElementById("recordFormat").addEventListener("change", function(){ saveSettings(null); });
|
||||
// HTML 高度只对 html 类型有意义,选 text 时收起来
|
||||
function toggleMeowHeight(){
|
||||
var isHTML = document.getElementById("meowMsgType").value === "html";
|
||||
document.getElementById("meowHeightRow").style.display = isHTML ? "" : "none";
|
||||
}
|
||||
document.getElementById("meowMsgType").addEventListener("change", function(){ toggleMeowHeight(); saveSettings(null); });
|
||||
var fileList = [];
|
||||
function fmtSize(n){
|
||||
if(!n) return "0 B";
|
||||
@@ -598,19 +646,27 @@ function syncMergeState(st){
|
||||
if(st.mergeErr) showMsg(false, "合并失败:" + st.mergeErr);
|
||||
else showMsg(true, "手动合并结束,结果见运行日志");
|
||||
}
|
||||
async function testNotify(b){
|
||||
async function testNotify(b, channel){
|
||||
if(b) b.disabled = true;
|
||||
var r = null;
|
||||
var body = {channel: channel || "qq"};
|
||||
if(body.channel === "meow"){
|
||||
body.nickname = document.getElementById("meowNickname").value.trim();
|
||||
body.msgType = document.getElementById("meowMsgType").value;
|
||||
body.imgUrl = document.getElementById("meowImgUrl").value.trim();
|
||||
body.htmlHeight = parseInt(document.getElementById("meowHtmlHeight").value, 10) || 0;
|
||||
}else{
|
||||
body.url = document.getElementById("notifyUrl").value.trim();
|
||||
body.token = document.getElementById("notifyToken").value.trim();
|
||||
body.group = document.getElementById("notifyGroup").value.trim();
|
||||
}
|
||||
var r = null, d = null;
|
||||
try{
|
||||
r = await api("/api/notify/test", {method:"POST", headers:{"Content-Type":"application/json"}, body: JSON.stringify({
|
||||
url: document.getElementById("notifyUrl").value.trim(),
|
||||
token: document.getElementById("notifyToken").value.trim(),
|
||||
group: document.getElementById("notifyGroup").value.trim()
|
||||
})});
|
||||
r = await api("/api/notify/test", {method:"POST", headers:{"Content-Type":"application/json"}, body: JSON.stringify(body)});
|
||||
if(r && r.ok) d = await r.json();
|
||||
}catch(e){}
|
||||
if(r && r.status === 401){ showLogin("管理口令已失效,请重新输入"); if(b) b.disabled = false; return; }
|
||||
if(r && r.ok) showMsg(true, "测试通知已发送,请查看是否收到");
|
||||
else showMsg(false, "发送失败:" + (r ? await r.text() : "连接失败"));
|
||||
if(r && r.ok) showMsg(true, "测试成功:" + ((d && d.detail) ? d.detail : "推送已发出"));
|
||||
else showMsg(false, "测试失败:" + (r ? await r.text() : "连接失败"));
|
||||
if(b) b.disabled = false;
|
||||
}
|
||||
async function addUid(){
|
||||
@@ -686,6 +742,7 @@ type webStatus struct {
|
||||
MergeSubdir string `json:"mergeSubdir"`
|
||||
FFmpegPath string `json:"ffmpegPath"`
|
||||
RecordFormat string `json:"recordFormat"`
|
||||
NotifyEnabled bool `json:"notifyEnabled"`
|
||||
NotifyURL string `json:"notifyUrl"`
|
||||
NotifyToken string `json:"notifyToken"`
|
||||
NotifyGroup string `json:"notifyGroup"`
|
||||
@@ -693,6 +750,12 @@ type webStatus struct {
|
||||
NotifyLiveOff bool `json:"notifyLiveOff"`
|
||||
NotifyRecordStart bool `json:"notifyRecordStart"`
|
||||
NotifyMergeDone bool `json:"notifyMergeDone"`
|
||||
MeowEnabled bool `json:"meowEnabled"`
|
||||
MeowNickname string `json:"meowNickname"`
|
||||
MeowMsgType string `json:"meowMsgType"`
|
||||
MeowHTMLHeight int `json:"meowHtmlHeight"`
|
||||
MeowImgURL string `json:"meowImgUrl"`
|
||||
MeowBlocked string `json:"meowBlocked"` // MeoW 推不出去的原因,空字符串表示正常
|
||||
MergeBusy bool `json:"mergeBusy"` // 手动合并是否正在进行
|
||||
MergeOutName string `json:"mergeOutName"` // 正在进行的手动合并的输出文件名
|
||||
MergeErr string `json:"mergeErr"` // 最近一次手动合并的错误
|
||||
@@ -739,6 +802,7 @@ func statusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
FFmpegPath: config.FFmpegPath,
|
||||
RecordFormat: normalizeRecordFormat(config.RecordFormat),
|
||||
DefaultBitrate: config.DefaultBitrate,
|
||||
NotifyEnabled: config.NotifyEnabled,
|
||||
NotifyURL: config.NotifyURL,
|
||||
NotifyToken: config.NotifyToken,
|
||||
NotifyGroup: config.NotifyGroup,
|
||||
@@ -746,6 +810,12 @@ func statusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
NotifyLiveOff: config.NotifyLiveOff,
|
||||
NotifyRecordStart: config.NotifyRecordStart,
|
||||
NotifyMergeDone: config.NotifyMergeDone,
|
||||
MeowEnabled: config.MeowEnabled,
|
||||
MeowNickname: config.MeowNickname,
|
||||
MeowMsgType: normalizeMeowMsgType(config.MeowMsgType),
|
||||
MeowHTMLHeight: clampMeowHTMLHeight(config.MeowHTMLHeight),
|
||||
MeowImgURL: config.MeowImgURL,
|
||||
MeowBlocked: meowStatusProblem(),
|
||||
MergeBusy: mergeBusy,
|
||||
MergeOutName: mergeOut,
|
||||
MergeErr: mergeErr,
|
||||
@@ -809,6 +879,7 @@ func webConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
MergeSubdir string `json:"mergeSubdir"`
|
||||
FFmpegPath string `json:"ffmpegPath"`
|
||||
RecordFormat string `json:"recordFormat"`
|
||||
NotifyEnabled bool `json:"notifyEnabled"`
|
||||
NotifyURL string `json:"notifyUrl"`
|
||||
NotifyToken string `json:"notifyToken"`
|
||||
NotifyGroup string `json:"notifyGroup"`
|
||||
@@ -816,6 +887,11 @@ func webConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
NotifyLiveOff bool `json:"notifyLiveOff"`
|
||||
NotifyRecordStart bool `json:"notifyRecordStart"`
|
||||
NotifyMergeDone bool `json:"notifyMergeDone"`
|
||||
MeowEnabled bool `json:"meowEnabled"`
|
||||
MeowNickname string `json:"meowNickname"`
|
||||
MeowMsgType string `json:"meowMsgType"`
|
||||
MeowHTMLHeight int `json:"meowHtmlHeight"`
|
||||
MeowImgURL string `json:"meowImgUrl"`
|
||||
}
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
@@ -875,6 +951,25 @@ func webConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, msg, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
meowNickname := strings.TrimSpace(req.MeowNickname)
|
||||
if msg := checkMeowNickname(meowNickname, true); msg != "" {
|
||||
http.Error(w, msg, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
meowMsgType := strings.ToLower(strings.TrimSpace(req.MeowMsgType))
|
||||
if meowMsgType == "" {
|
||||
meowMsgType = meowMsgTypeText
|
||||
}
|
||||
if meowMsgType != meowMsgTypeText && meowMsgType != meowMsgTypeHTML {
|
||||
http.Error(w, "MeoW 消息类型只能是 text 或 html:"+meowMsgType, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.MeowHTMLHeight < 0 || req.MeowHTMLHeight > maxMeowHTMLHeight {
|
||||
http.Error(w, fmt.Sprintf("MeoW 的 HTML 显示高度只能是 0(用默认 %d)到 %d 之间的整数",
|
||||
defaultMeowHTMLHeight, maxMeowHTMLHeight), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
meowImgURL := strings.TrimSpace(req.MeowImgURL)
|
||||
|
||||
streamers.Lock()
|
||||
config.Directory = req.Directory
|
||||
@@ -890,6 +985,7 @@ func webConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
config.MergeSubdir = mergeSubdir
|
||||
config.FFmpegPath = strings.TrimSpace(req.FFmpegPath)
|
||||
config.RecordFormat = recordFormat
|
||||
config.NotifyEnabled = req.NotifyEnabled
|
||||
config.NotifyURL = notifyURL
|
||||
config.NotifyToken = strings.TrimSpace(req.NotifyToken)
|
||||
config.NotifyGroup = strings.TrimSpace(req.NotifyGroup)
|
||||
@@ -897,6 +993,11 @@ func webConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
config.NotifyLiveOff = req.NotifyLiveOff
|
||||
config.NotifyRecordStart = req.NotifyRecordStart
|
||||
config.NotifyMergeDone = req.NotifyMergeDone
|
||||
config.MeowEnabled = req.MeowEnabled
|
||||
config.MeowNickname = meowNickname
|
||||
config.MeowMsgType = meowMsgType
|
||||
config.MeowHTMLHeight = clampMeowHTMLHeight(req.MeowHTMLHeight)
|
||||
config.MeowImgURL = meowImgURL
|
||||
// 修改了子文件夹名后确保新文件夹存在
|
||||
if err := os.MkdirAll(recordingDir(), 0755); err != nil {
|
||||
lPrintWarnf("创建文件夹 %s 失败:%v", recordingDir(), err)
|
||||
@@ -918,8 +1019,10 @@ func webConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
lPrintf("通过 web UI 修改了设置:目录 %q、自动合并 %v、合并删视频 %v、合并删弹幕 %v、成片目录 %q、合并码率 %d、默认下载码率 %d、外网访问 %v、录播子文件夹 %q、成片子文件夹 %q、ffmpeg 路径 %q、录播格式 %q",
|
||||
req.Directory, req.AutoMerge, req.MergeDelVideo, req.MergeDelDanmu, req.MergeOutputDir,
|
||||
req.MergeBitrate, req.DefaultBitrate, req.WebExternal, recordSubdir, mergeSubdir, config.FFmpegPath, recordFormat)
|
||||
lPrintf("推送设置:地址 %q、群号 %q、开播 %v、下播 %v、开始录制 %v、合并完成 %v",
|
||||
notifyURL, config.NotifyGroup, req.NotifyLiveOn, req.NotifyLiveOff, req.NotifyRecordStart, req.NotifyMergeDone)
|
||||
lPrintf("推送设置:QQ 机器人渠道 %v(地址 %q、群号 %q)、MeoW 渠道 %v(昵称 %q、类型 %s、高度 %d);事件 开播 %v、下播 %v、开始录制 %v、合并完成 %v",
|
||||
req.NotifyEnabled, notifyURL, config.NotifyGroup, req.MeowEnabled, meowNickname, meowMsgType,
|
||||
clampMeowHTMLHeight(req.MeowHTMLHeight),
|
||||
req.NotifyLiveOn, req.NotifyLiveOff, req.NotifyRecordStart, req.NotifyMergeDone)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte("true"))
|
||||
@@ -1016,24 +1119,42 @@ func webStreamerRemoveHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// 发送一条测试通知,验证推送地址、群号和口令是否配置正确
|
||||
// 测试某个推送渠道能不能连通。
|
||||
// 页面上填的值优先,留空的项用已经保存的配置补齐,所以改完不用先保存也能测
|
||||
func notifyTestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// 允许直接用设置区里还没保存的地址测试,没传就用已经保存的配置
|
||||
var req struct {
|
||||
URL string `json:"url"`
|
||||
Token string `json:"token"`
|
||||
Group string `json:"group"`
|
||||
Channel string `json:"channel"` // qq(默认)或 meow
|
||||
URL string `json:"url"`
|
||||
Token string `json:"token"`
|
||||
Group string `json:"group"`
|
||||
Nickname string `json:"nickname"`
|
||||
MsgType string `json:"msgType"`
|
||||
ImgURL string `json:"imgUrl"`
|
||||
Height int `json:"htmlHeight"`
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
addr := strings.TrimSpace(req.URL)
|
||||
token := strings.TrimSpace(req.Token)
|
||||
group := strings.TrimSpace(req.Group)
|
||||
if addr == "" {
|
||||
n := currentNotifySetting()
|
||||
addr, token = n.url, n.token
|
||||
switch strings.ToLower(strings.TrimSpace(req.Channel)) {
|
||||
case "", "qq", "webhook":
|
||||
testNotifyQQ(w, req.URL, req.Token, req.Group)
|
||||
case "meow":
|
||||
testNotifyMeow(w, req.Nickname, req.MsgType, req.ImgURL, req.Height)
|
||||
default:
|
||||
http.Error(w, "未知的推送渠道:"+req.Channel, http.StatusBadRequest)
|
||||
}
|
||||
if group == "" {
|
||||
group = currentNotifySetting().group
|
||||
}
|
||||
|
||||
// 给 webhook 发一条测试推送
|
||||
func testNotifyQQ(w http.ResponseWriter, addr, token, group string) {
|
||||
n := currentNotifySetting()
|
||||
if addr = strings.TrimSpace(addr); addr == "" {
|
||||
addr = n.url
|
||||
}
|
||||
if token = strings.TrimSpace(token); token == "" {
|
||||
token = n.token
|
||||
}
|
||||
if group = strings.TrimSpace(group); group == "" {
|
||||
group = n.group
|
||||
}
|
||||
if addr == "" {
|
||||
http.Error(w, "还没有填写通知推送地址", http.StatusBadRequest)
|
||||
@@ -1051,15 +1172,80 @@ func notifyTestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
Text: "这是一条测试通知,收到就说明推送地址和口令都配置正确",
|
||||
Time: getLogTime(),
|
||||
}
|
||||
if err := postNotify(addr, token, p); err != nil {
|
||||
start := time.Now()
|
||||
code, err := postNotify(addr, token, p)
|
||||
if err != nil {
|
||||
lPrintWarnf("推送测试通知失败:%v", err)
|
||||
http.Error(w, "推送失败:"+err.Error(), http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
lPrintln("测试通知推送成功:" + addr)
|
||||
writeTestResult(w, fmt.Sprintf("webhook 返回 HTTP %d,用时 %s。地址和口令没问题,能不能收到还要看机器人那边有没有正常处理",
|
||||
code, elapsedText(time.Since(start))))
|
||||
}
|
||||
|
||||
// 给 MeoW 发一条测试推送
|
||||
func testNotifyMeow(w http.ResponseWriter, nick, msgType, imgURL string, height int) {
|
||||
n := currentNotifySetting()
|
||||
if nick = strings.TrimSpace(nick); nick == "" {
|
||||
nick = n.meowNick
|
||||
}
|
||||
if imgURL = strings.TrimSpace(imgURL); imgURL == "" {
|
||||
imgURL = n.meowImg
|
||||
}
|
||||
if strings.TrimSpace(msgType) == "" {
|
||||
msgType = n.meowMsgType
|
||||
}
|
||||
if height <= 0 {
|
||||
height = n.meowHTML
|
||||
}
|
||||
if nick == "" {
|
||||
http.Error(w, "还没有填写 MeoW 昵称", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if msg := checkMeowNickname(nick, false); msg != "" {
|
||||
http.Error(w, msg, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// 已经被 MeoW 判过「未注册」的昵称不再重试:官方提示一天内累计 3 次输错昵称就封 IP
|
||||
if reason := meowBadNickReason(nick); reason != "" {
|
||||
http.Error(w, fmt.Sprintf("昵称 %s 之前已被 MeoW 判定为%s,为避免 IP 被封不再重试。"+
|
||||
"确认昵称没填错(或换一个昵称重新保存)就能恢复,重启程序也会清掉这个记录", nick, reason),
|
||||
http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
msgType = normalizeMeowMsgType(msgType)
|
||||
title := "测试推送"
|
||||
plain := "这是一条测试通知,收到就说明 MeoW 渠道配置正确"
|
||||
content := plain
|
||||
if msgType == meowMsgTypeHTML {
|
||||
content = meowHTMLText(title + "\n" + plain)
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
if err := pushMeow(nick, title, content, "", imgURL, msgType, height); err != nil {
|
||||
lPrintWarnf("推送测试通知到 MeoW 失败:%v", err)
|
||||
detail := "推送失败:" + err.Error()
|
||||
if meowBadNickReason(nick) != "" {
|
||||
detail += "。MeoW 一天内累计 3 次收到不存在的昵称就会封 IP,这里已经暂停向这个昵称推送"
|
||||
}
|
||||
http.Error(w, detail, http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
lPrintf("测试通知推送成功(MeoW):%s", nick)
|
||||
writeTestResult(w, fmt.Sprintf("MeoW 返回 status 200,用时 %s。手机上应该已经收到通知(昵称 %s)",
|
||||
elapsedText(time.Since(start)), nick))
|
||||
}
|
||||
|
||||
// 测试结果,detail 会显示在页面的提示里
|
||||
func writeTestResult(w http.ResponseWriter, detail string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte("true"))
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "detail": detail})
|
||||
}
|
||||
|
||||
func elapsedText(d time.Duration) string {
|
||||
return fmt.Sprintf("%.2f 秒", d.Seconds())
|
||||
}
|
||||
|
||||
// 显示日志,支持 ?since=<偏移量> 只返回新增的部分,避免每 3 秒全量传输
|
||||
|
||||
Reference in New Issue
Block a user