204 lines
6.4 KiB
Go
204 lines
6.4 KiB
Go
package hook
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
|
|
"git.pcux.cn/xuans/Smail/utils/credential"
|
|
)
|
|
|
|
const (
|
|
pluginID = "telegram_push"
|
|
maxRuntimeSettings = 64 << 10
|
|
defaultTimeout = 30
|
|
maxTimeout = 120
|
|
)
|
|
|
|
var chatIDPattern = regexp.MustCompile(`^-?[1-9][0-9]{0,18}$`)
|
|
|
|
type pluginRuntimeConfig struct {
|
|
PluginID string `json:"pluginId"`
|
|
Generation uint64 `json:"generation"`
|
|
Settings json.RawMessage `json:"settings,omitempty"`
|
|
Secrets map[string]string `json:"secrets,omitempty"`
|
|
Capabilities []string `json:"capabilities,omitempty"`
|
|
Network []pluginNetworkTarget `json:"network,omitempty"`
|
|
}
|
|
|
|
type pluginNetworkTarget struct {
|
|
Scheme string `json:"scheme"`
|
|
Host string `json:"host"`
|
|
Port int `json:"port"`
|
|
}
|
|
|
|
type pluginSettings struct {
|
|
Proxy string `json:"proxy,omitempty"`
|
|
Timeout int `json:"timeout,omitempty"`
|
|
Debug bool `json:"debug,omitempty"`
|
|
WebDomain string `json:"webDomain,omitempty"`
|
|
HTTPSEnabled bool `json:"httpsEnabled,omitempty"`
|
|
}
|
|
|
|
type settingStore struct {
|
|
mu sync.RWMutex
|
|
settings map[int]telegramSetting
|
|
}
|
|
|
|
type telegramSetting struct {
|
|
ChatID string `json:"chat_id"`
|
|
ShowContent bool `json:"show_content"`
|
|
SpoilerContent bool `json:"spoiler_content"`
|
|
SendAttachments bool `json:"send_attachments"`
|
|
DisableLinkPreview bool `json:"disable_link_preview"`
|
|
}
|
|
|
|
type telegramUserSettingsPayload struct {
|
|
ChatID string `json:"chatId"`
|
|
ShowContent bool `json:"showContent"`
|
|
SpoilerContent bool `json:"spoilerContent"`
|
|
SendAttachments bool `json:"sendAttachments"`
|
|
DisableLinkPreview bool `json:"disableLinkPreview"`
|
|
}
|
|
|
|
func (p telegramUserSettingsPayload) setting() telegramSetting {
|
|
return telegramSetting{
|
|
ChatID: strings.TrimSpace(p.ChatID),
|
|
ShowContent: p.ShowContent,
|
|
SpoilerContent: p.SpoilerContent,
|
|
SendAttachments: p.SendAttachments,
|
|
DisableLinkPreview: p.DisableLinkPreview,
|
|
}
|
|
}
|
|
|
|
func decodeTelegramUserSettings(data json.RawMessage) (telegramUserSettingsPayload, error) {
|
|
var payload telegramUserSettingsPayload
|
|
if err := decodeSingleJSON(data, &payload); err != nil {
|
|
return telegramUserSettingsPayload{}, err
|
|
}
|
|
setting := payload.setting()
|
|
if setting.ChatID != "" && !chatIDPattern.MatchString(setting.ChatID) {
|
|
return telegramUserSettingsPayload{}, errors.New("Telegram 聊天 ID 格式不正确")
|
|
}
|
|
return payload, nil
|
|
}
|
|
|
|
func (h *PmailTelegramPushHook) ConfigureUserSettings(userID int, settings json.RawMessage) error {
|
|
if userID <= 0 {
|
|
return errors.New("用户 ID 无效")
|
|
}
|
|
payload, err := decodeTelegramUserSettings(settings)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return h.settings.update(userID, payload.setting())
|
|
}
|
|
func newSettingStore() *settingStore {
|
|
return &settingStore{settings: make(map[int]telegramSetting)}
|
|
}
|
|
|
|
func defaultTelegramSetting() telegramSetting {
|
|
return telegramSetting{
|
|
ShowContent: true,
|
|
SpoilerContent: true,
|
|
SendAttachments: true,
|
|
DisableLinkPreview: true,
|
|
}
|
|
}
|
|
|
|
func (s *settingStore) get(userID int) telegramSetting {
|
|
s.mu.RLock()
|
|
setting, ok := s.settings[userID]
|
|
s.mu.RUnlock()
|
|
if !ok {
|
|
return defaultTelegramSetting()
|
|
}
|
|
return setting
|
|
}
|
|
|
|
func (s *settingStore) update(userID int, setting telegramSetting) error {
|
|
if userID <= 0 || !validChatID(setting.ChatID) {
|
|
return errors.New("Chat ID 格式不正确")
|
|
}
|
|
setting.ChatID = strings.TrimSpace(setting.ChatID)
|
|
s.mu.Lock()
|
|
s.settings[userID] = setting
|
|
s.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func validChatID(value string) bool {
|
|
value = strings.TrimSpace(value)
|
|
return value == "" || chatIDPattern.MatchString(value)
|
|
}
|
|
|
|
func parseRuntimeConfig(data json.RawMessage) (pluginRuntimeConfig, pluginSettings, string, error) {
|
|
if len(data) == 0 || len(data) > maxRuntimeSettings {
|
|
return pluginRuntimeConfig{}, pluginSettings{}, "", errors.New("插件运行时配置无效")
|
|
}
|
|
var runtime pluginRuntimeConfig
|
|
if err := decodeSingleJSON(data, &runtime); err != nil {
|
|
return pluginRuntimeConfig{}, pluginSettings{}, "", errors.New("插件运行时配置无效")
|
|
}
|
|
if runtime.PluginID != pluginID {
|
|
return pluginRuntimeConfig{}, pluginSettings{}, "", errors.New("插件 ID 不匹配")
|
|
}
|
|
if runtime.Generation == 0 {
|
|
return pluginRuntimeConfig{}, pluginSettings{}, "", errors.New("Telegram 插件配置代次无效")
|
|
}
|
|
if !hasOnlyCapability(runtime.Capabilities, "network") {
|
|
return pluginRuntimeConfig{}, pluginSettings{}, "", errors.New("Telegram 插件能力权限无效")
|
|
}
|
|
var settings pluginSettings
|
|
if len(runtime.Settings) > 0 {
|
|
if err := decodeSingleJSON(runtime.Settings, &settings); err != nil {
|
|
return pluginRuntimeConfig{}, pluginSettings{}, "", errors.New("Telegram 插件配置无效")
|
|
}
|
|
}
|
|
if settings.Timeout == 0 {
|
|
settings.Timeout = defaultTimeout
|
|
}
|
|
if settings.Timeout < 1 || settings.Timeout > maxTimeout {
|
|
return pluginRuntimeConfig{}, pluginSettings{}, "", errors.New("Telegram 请求超时时间无效")
|
|
}
|
|
proxyURL, err := credential.NormalizeTelegramProxyURL(settings.Proxy)
|
|
if err != nil {
|
|
return pluginRuntimeConfig{}, pluginSettings{}, "", err
|
|
}
|
|
settings.Proxy = proxyURL
|
|
secret := strings.TrimSpace(runtime.Secrets["botToken"])
|
|
if secret == "" {
|
|
return pluginRuntimeConfig{}, pluginSettings{}, "", errors.New("Telegram Bot Token 未配置")
|
|
}
|
|
if len(runtime.Secrets) != 1 {
|
|
return pluginRuntimeConfig{}, pluginSettings{}, "", errors.New("Telegram 插件密钥字段无效")
|
|
}
|
|
if !hasOnlyTelegramNetworkTarget(runtime.Network) {
|
|
return pluginRuntimeConfig{}, pluginSettings{}, "", errors.New("Telegram 网络权限未配置")
|
|
}
|
|
return runtime, settings, secret, nil
|
|
}
|
|
|
|
func decodeSingleJSON(data []byte, target any) error {
|
|
decoder := json.NewDecoder(strings.NewReader(string(data)))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(target); err != nil {
|
|
return err
|
|
}
|
|
if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
|
|
return errors.New("trailing JSON data")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func hasOnlyCapability(capabilities []string, expected string) bool {
|
|
return len(capabilities) == 1 && capabilities[0] == expected
|
|
}
|
|
|
|
func hasOnlyTelegramNetworkTarget(targets []pluginNetworkTarget) bool {
|
|
return len(targets) == 1 && strings.EqualFold(targets[0].Scheme, "https") && strings.EqualFold(targets[0].Host, "api.telegram.org") && targets[0].Port == 443
|
|
}
|