Files
acfunlive-remake/notify.go
T
2026-09-25 13:16:54 +08:00

174 lines
4.8 KiB
Go

// 开播、下播、开始录制、合并完成等事件的推送
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"sync"
"time"
)
// 推送的事件名,外部(如 NoneBot 机器人)按这个字段区分事件
const (
notifyEventLiveOn = "liveOn"
notifyEventLiveOff = "liveOff"
notifyEventRecordStart = "recordStart"
notifyEventMergeDone = "mergeDone"
)
// 推送到 webhook 的请求体
type notifyPayload struct {
Event string `json:"event"` // 事件名:liveOn、liveOff、recordStart、mergeDone
UID int `json:"uid"` // 主播 uid
Name string `json:"name"` // 主播名字
Title string `json:"title"` // 直播标题(下播和合并完成时可能为空)
URL string `json:"url"` // 直播间地址
File string `json:"file"` // 相关文件路径(录制开始时的视频文件、合并完成时的成片)
Group string `json:"group"` // web UI 里配置的通知群号,机器人可以直接用它决定发到哪个群
Text string `json:"text"` // 可以直接发出去的文本,机器人拿到后原样发送即可
Time string `json:"time"` // 事件发生的时刻
}
// 推送用的 http 客户端,超时短一点,避免推送把任务拖住
var notifyClient = &http.Client{Timeout: 10 * time.Second}
// 已经推送过开播通知的 liveID,避免重连或改设置时重复推送
var notifiedLive struct {
sync.Mutex
ids map[string]struct{}
}
// 首次见到这个 liveID 时返回 true
func notifiedLiveOnce(liveID string) bool {
if liveID == "" {
return false
}
notifiedLive.Lock()
defer notifiedLive.Unlock()
if notifiedLive.ids == nil {
notifiedLive.ids = make(map[string]struct{})
}
if _, ok := notifiedLive.ids[liveID]; ok {
return false
}
notifiedLive.ids[liveID] = struct{}{}
// 长时间运行后简单地清一次,避免无限增长
if len(notifiedLive.ids) > 4096 {
notifiedLive.ids = map[string]struct{}{liveID: {}}
}
return true
}
// 推送设置的一份快照,避免在推送过程中读到正在被修改的配置
type notifySetting struct {
url string
token string
group string
liveOn bool
liveOff bool
recordStart bool
mergeDone bool
}
func currentNotifySetting() notifySetting {
streamers.RLock()
defer streamers.RUnlock()
return notifySetting{
url: strings.TrimSpace(config.NotifyURL),
token: strings.TrimSpace(config.NotifyToken),
group: strings.TrimSpace(config.NotifyGroup),
liveOn: config.NotifyLiveOn,
liveOff: config.NotifyLiveOff,
recordStart: config.NotifyRecordStart,
mergeDone: config.NotifyMergeDone,
}
}
// 该事件是否开启了推送
func (n notifySetting) enabled(event string) bool {
switch event {
case notifyEventLiveOn:
return n.liveOn
case notifyEventLiveOff:
return n.liveOff
case notifyEventRecordStart:
return n.recordStart
case notifyEventMergeDone:
return n.mergeDone
}
return false
}
// 推送一个事件。没配置推送地址或没开启该事件时什么都不做,
// 推送在后台 goroutine 里进行,失败只打警告,不会影响录制
func sendNotify(event string, uid int, name, title, url, file, text string) {
n := currentNotifySetting()
if n.url == "" || !n.enabled(event) {
return
}
p := notifyPayload{
Event: event,
UID: uid,
Name: name,
Title: title,
URL: url,
File: file,
Group: n.group,
Text: text,
Time: getLogTime(),
}
// 推送地址可能在推送过程中被改掉,这里把地址和口令带进闭包,保证用的是同一次配置
addr, token := n.url, n.token
safeGo("推送"+event+"通知", func() {
if err := postNotify(addr, token, p); err != nil {
lPrintWarnf("推送%s通知失败:%v", event, err)
return
}
lPrintf("已推送%s通知:%s", event, p.Text)
})
}
// 把事件 POST 到 webhook
func postNotify(addr, token string, p notifyPayload) error {
data, err := json.Marshal(p)
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPost, addr, bytes.NewReader(data))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("X-Auth-Token", token)
}
resp, err := notifyClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("HTTP %d %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
return nil
}
// 检查设置里的推送地址,返回错误信息,空字符串表示通过
func checkNotifyURL(addr string) string {
addr = strings.TrimSpace(addr)
if addr == "" {
return ""
}
if !strings.HasPrefix(addr, "http://") && !strings.HasPrefix(addr, "https://") {
return "推送地址必须以 http:// 或 https:// 开头:" + addr
}
return ""
}