Files
open-im-server-deploy/pkg/common/webhook/config_manager.go
kim.dev.6789 e50142a3b9 复制项目
2026-01-14 22:16:44 +08:00

434 lines
15 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright © 2023 OpenIM. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package webhook
import (
"context"
"encoding/json"
"sync"
"time"
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/config"
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database"
"github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/log"
"github.com/openimsdk/tools/utils/datautil"
)
const (
// WebhookConfigKey 数据库中的webhook配置键
WebhookConfigKey = "webhook_config"
// DefaultRefreshInterval 默认刷新间隔30秒方便调试
DefaultRefreshInterval = 30 * time.Second
)
// ConfigManager webhook配置管理器支持从数据库读取和定时刷新
type ConfigManager struct {
db database.SystemConfig
defaultConfig *config.Webhooks
mu sync.RWMutex
cachedConfig *config.Webhooks
lastUpdate time.Time
refreshInterval time.Duration
stopCh chan struct{}
}
// NewConfigManager 创建webhook配置管理器
func NewConfigManager(db database.SystemConfig, defaultConfig *config.Webhooks) *ConfigManager {
cm := &ConfigManager{
db: db,
defaultConfig: defaultConfig,
cachedConfig: defaultConfig,
refreshInterval: DefaultRefreshInterval,
stopCh: make(chan struct{}),
}
return cm
}
// Start 启动配置管理器,开始定时刷新
func (cm *ConfigManager) Start(ctx context.Context) error {
// 立即加载一次配置
log.ZInfo(ctx, "webhook config manager starting, initial refresh...")
if err := cm.Refresh(ctx); err != nil {
log.ZWarn(ctx, "initial webhook config refresh failed, using default config", err)
} else {
currentConfig := cm.GetConfig()
log.ZInfo(ctx, "webhook config manager started successfully", "url", currentConfig.URL, "refresh_interval", cm.refreshInterval)
}
// 启动定时刷新goroutine
go cm.refreshLoop(ctx)
return nil
}
// Stop 停止配置管理器
func (cm *ConfigManager) Stop() {
close(cm.stopCh)
}
// refreshLoop 定时刷新循环
func (cm *ConfigManager) refreshLoop(ctx context.Context) {
ticker := time.NewTicker(cm.refreshInterval)
defer ticker.Stop()
log.ZInfo(ctx, "webhook config refresh loop started", "interval", cm.refreshInterval)
for {
select {
case <-ticker.C:
log.ZDebug(ctx, "webhook config scheduled refresh triggered")
if err := cm.Refresh(ctx); err != nil {
log.ZWarn(ctx, "webhook config refresh failed", err)
}
case <-cm.stopCh:
log.ZInfo(ctx, "webhook config refresh loop stopped")
return
}
}
}
// Refresh 从数据库刷新配置
func (cm *ConfigManager) Refresh(ctx context.Context) error {
// 从数据库读取配置
sysConfig, err := cm.db.FindByKey(ctx, WebhookConfigKey)
if err != nil {
// 如果查询出错,使用默认配置
log.ZWarn(ctx, "failed to get webhook config from database, using default config", err)
cm.mu.Lock()
cm.cachedConfig = cm.defaultConfig
cm.lastUpdate = time.Now()
cm.mu.Unlock()
return nil
}
// 如果数据库中没有配置,使用默认配置
if sysConfig == nil {
cm.mu.Lock()
cm.cachedConfig = cm.defaultConfig
cm.lastUpdate = time.Now()
cm.mu.Unlock()
log.ZInfo(ctx, "webhook config not found in database, using default config", "default_url", cm.defaultConfig.URL)
return nil
}
// 检查值类型
if sysConfig.ValueType != model.SystemConfigValueTypeJSON {
cm.mu.Lock()
cm.cachedConfig = cm.defaultConfig
cm.lastUpdate = time.Now()
cm.mu.Unlock()
log.ZWarn(ctx, "webhook config valueType is not json, using default config", nil, "value_type", sysConfig.ValueType)
return nil
}
// 如果配置被禁用,使用默认配置
if !sysConfig.Enabled {
cm.mu.Lock()
cm.cachedConfig = cm.defaultConfig
cm.lastUpdate = time.Now()
cm.mu.Unlock()
log.ZInfo(ctx, "webhook config is disabled, using default config", "default_url", cm.defaultConfig.URL)
return nil
}
// 如果配置值为空,使用默认配置
if sysConfig.Value == "" {
cm.mu.Lock()
cm.cachedConfig = cm.defaultConfig
cm.lastUpdate = time.Now()
cm.mu.Unlock()
log.ZInfo(ctx, "webhook config value is empty, using default config", "default_url", cm.defaultConfig.URL)
return nil
}
valuePreview := sysConfig.Value
if len(valuePreview) > 100 {
valuePreview = valuePreview[:100] + "..."
}
log.ZDebug(ctx, "webhook config value found", "value_length", len(sysConfig.Value), "value_preview", valuePreview)
// 解析配置
var webhookConfig config.Webhooks
if err := json.Unmarshal([]byte(sysConfig.Value), &webhookConfig); err != nil {
// 如果解析失败,使用默认配置
log.ZWarn(ctx, "failed to unmarshal webhook config, using default config", err)
cm.mu.Lock()
cm.cachedConfig = cm.defaultConfig
cm.lastUpdate = time.Now()
cm.mu.Unlock()
return nil
}
// 验证解析后的配置确保AttentionIds不为nil
if webhookConfig.AfterSendGroupMsg.AttentionIds == nil {
webhookConfig.AfterSendGroupMsg.AttentionIds = []string{}
}
normalized, ok := normalizeWebhookConfig(cm.defaultConfig, &webhookConfig)
if !ok {
log.ZWarn(ctx, "webhook config URL is empty, using default config", nil)
cm.mu.Lock()
cm.cachedConfig = cm.defaultConfig
cm.lastUpdate = time.Now()
cm.mu.Unlock()
return nil
}
// 更新缓存
cm.mu.Lock()
oldURL := cm.cachedConfig.URL
oldAttentionIdsCount := len(cm.cachedConfig.AfterSendGroupMsg.AttentionIds)
cm.cachedConfig = normalized
cm.lastUpdate = time.Now()
newAttentionIdsCount := len(normalized.AfterSendGroupMsg.AttentionIds)
cm.mu.Unlock()
// 如果URL或AttentionIds发生变化记录日志
urlChanged := oldURL != webhookConfig.URL
attentionIdsChanged := oldAttentionIdsCount != newAttentionIdsCount
if urlChanged || attentionIdsChanged {
log.ZInfo(ctx, "webhook config updated from database",
"old_url", oldURL, "new_url", webhookConfig.URL,
"old_attention_ids_count", oldAttentionIdsCount,
"new_attention_ids_count", newAttentionIdsCount,
"url_changed", urlChanged,
"attention_ids_changed", attentionIdsChanged)
} else {
log.ZDebug(ctx, "webhook config refreshed (no change)", "url", webhookConfig.URL, "attention_ids_count", newAttentionIdsCount)
}
return nil
}
// GetConfig 获取当前缓存的配置
func (cm *ConfigManager) GetConfig() *config.Webhooks {
cm.mu.RLock()
defer cm.mu.RUnlock()
return cm.cachedConfig
}
// normalizeWebhookConfig 校验并填充缺省值,返回是否有效
func normalizeWebhookConfig(defaultCfg *config.Webhooks, cfg *config.Webhooks) (*config.Webhooks, bool) {
if cfg == nil {
return nil, false
}
if cfg.URL == "" {
return nil, false
}
normalized := *cfg // 浅拷贝
// 填充默认值,避免数据库配置错误导致回调不可用
if normalized.AfterSendGroupMsg.AttentionIds == nil {
normalized.AfterSendGroupMsg.AttentionIds = []string{}
}
applyBeforeDefaults := func(dst *config.BeforeConfig, fallback config.BeforeConfig) {
if dst.Timeout <= 0 {
dst.Timeout = fallback.Timeout
}
}
applyAfterDefaults := func(dst *config.AfterConfig, fallback config.AfterConfig) {
if dst.Timeout <= 0 {
dst.Timeout = fallback.Timeout
}
if dst.AttentionIds == nil {
dst.AttentionIds = []string{}
}
}
if defaultCfg != nil {
applyBeforeDefaults(&normalized.BeforeSendSingleMsg, defaultCfg.BeforeSendSingleMsg)
applyBeforeDefaults(&normalized.BeforeUpdateUserInfoEx, defaultCfg.BeforeUpdateUserInfoEx)
applyAfterDefaults(&normalized.AfterUpdateUserInfoEx, defaultCfg.AfterUpdateUserInfoEx)
applyAfterDefaults(&normalized.AfterSendSingleMsg, defaultCfg.AfterSendSingleMsg)
applyBeforeDefaults(&normalized.BeforeSendGroupMsg, defaultCfg.BeforeSendGroupMsg)
applyBeforeDefaults(&normalized.BeforeMsgModify, defaultCfg.BeforeMsgModify)
applyAfterDefaults(&normalized.AfterSendGroupMsg, defaultCfg.AfterSendGroupMsg)
applyAfterDefaults(&normalized.AfterMsgSaveDB, defaultCfg.AfterMsgSaveDB)
applyAfterDefaults(&normalized.AfterUserOnline, defaultCfg.AfterUserOnline)
applyAfterDefaults(&normalized.AfterUserOffline, defaultCfg.AfterUserOffline)
applyAfterDefaults(&normalized.AfterUserKickOff, defaultCfg.AfterUserKickOff)
applyBeforeDefaults(&normalized.BeforeOfflinePush, defaultCfg.BeforeOfflinePush)
applyBeforeDefaults(&normalized.BeforeOnlinePush, defaultCfg.BeforeOnlinePush)
applyBeforeDefaults(&normalized.BeforeGroupOnlinePush, defaultCfg.BeforeGroupOnlinePush)
applyBeforeDefaults(&normalized.BeforeAddFriend, defaultCfg.BeforeAddFriend)
applyBeforeDefaults(&normalized.BeforeUpdateUserInfo, defaultCfg.BeforeUpdateUserInfo)
applyAfterDefaults(&normalized.AfterUpdateUserInfo, defaultCfg.AfterUpdateUserInfo)
applyBeforeDefaults(&normalized.BeforeCreateGroup, defaultCfg.BeforeCreateGroup)
applyAfterDefaults(&normalized.AfterCreateGroup, defaultCfg.AfterCreateGroup)
applyBeforeDefaults(&normalized.BeforeMemberJoinGroup, defaultCfg.BeforeMemberJoinGroup)
applyBeforeDefaults(&normalized.BeforeSetGroupMemberInfo, defaultCfg.BeforeSetGroupMemberInfo)
applyAfterDefaults(&normalized.AfterSetGroupMemberInfo, defaultCfg.AfterSetGroupMemberInfo)
applyAfterDefaults(&normalized.AfterQuitGroup, defaultCfg.AfterQuitGroup)
applyAfterDefaults(&normalized.AfterKickGroupMember, defaultCfg.AfterKickGroupMember)
applyAfterDefaults(&normalized.AfterDismissGroup, defaultCfg.AfterDismissGroup)
}
return &normalized, true
}
// GetURL 获取当前webhook URL
func (cm *ConfigManager) GetURL() string {
cm.mu.RLock()
defer cm.mu.RUnlock()
return cm.cachedConfig.URL
}
// SetRefreshInterval 设置刷新间隔
func (cm *ConfigManager) SetRefreshInterval(interval time.Duration) {
cm.mu.Lock()
defer cm.mu.Unlock()
cm.refreshInterval = interval
}
// GetLastUpdate 获取最后更新时间
func (cm *ConfigManager) GetLastUpdate() time.Time {
cm.mu.RLock()
defer cm.mu.RUnlock()
return cm.lastUpdate
}
// UpdateAttentionIds 更新webhook配置中的attentionIds添加或移除群ID
// add: true表示添加groupIDfalse表示移除groupID
func UpdateAttentionIds(ctx context.Context, db database.SystemConfig, groupID string, add bool) error {
if groupID == "" {
return nil
}
if db == nil {
return nil
}
// 获取当前配置
sysConfig, err := db.FindByKey(ctx, WebhookConfigKey)
if err != nil {
log.ZWarn(ctx, "UpdateAttentionIds: failed to get webhook config from database", err, "groupID", groupID, "add", add)
return errs.WrapMsg(err, "failed to get webhook config from database")
}
if sysConfig == nil {
log.ZDebug(ctx, "UpdateAttentionIds: webhook config not found in database, skipping", "groupID", groupID, "add", add)
return nil
}
if !sysConfig.Enabled {
log.ZDebug(ctx, "UpdateAttentionIds: webhook config is disabled, skipping", "groupID", groupID, "add", add)
return nil
}
if sysConfig.Value == "" {
log.ZDebug(ctx, "UpdateAttentionIds: webhook config value is empty, skipping", "groupID", groupID, "add", add)
return nil
}
// 检查值类型
if sysConfig.ValueType != model.SystemConfigValueTypeJSON {
log.ZWarn(ctx, "UpdateAttentionIds: webhook config valueType is not json, skipping", nil, "groupID", groupID, "add", add, "value_type", sysConfig.ValueType)
return nil
}
// 解析当前配置
var webhookConfig config.Webhooks
if err := json.Unmarshal([]byte(sysConfig.Value), &webhookConfig); err != nil {
log.ZWarn(ctx, "UpdateAttentionIds: failed to unmarshal webhook config", err, "groupID", groupID, "add", add)
return errs.WrapMsg(err, "failed to unmarshal webhook config")
}
// 记录更新前的Enable状态
enableBefore := webhookConfig.AfterSendGroupMsg.Enable
// 验证解析后的配置是否有效
if webhookConfig.AfterSendGroupMsg.AttentionIds == nil {
webhookConfig.AfterSendGroupMsg.AttentionIds = []string{}
}
// 更新afterSendGroupMsg的attentionIds
attentionIds := webhookConfig.AfterSendGroupMsg.AttentionIds
oldCount := len(attentionIds)
var updated bool
if add {
// 添加groupID如果不存在
if !datautil.Contain(groupID, attentionIds...) {
attentionIds = append(attentionIds, groupID)
webhookConfig.AfterSendGroupMsg.AttentionIds = attentionIds
updated = true
log.ZInfo(ctx, "UpdateAttentionIds: adding groupID to attentionIds", "groupID", groupID, "old_count", oldCount, "new_count", len(attentionIds), "enable", enableBefore)
} else {
log.ZDebug(ctx, "UpdateAttentionIds: groupID already exists in attentionIds, skipping", "groupID", groupID, "count", oldCount)
return nil
}
} else {
// 移除groupID
newAttentionIds := make([]string, 0, len(attentionIds))
for _, id := range attentionIds {
if id != groupID {
newAttentionIds = append(newAttentionIds, id)
}
}
if len(newAttentionIds) != len(attentionIds) {
webhookConfig.AfterSendGroupMsg.AttentionIds = newAttentionIds
updated = true
log.ZInfo(ctx, "UpdateAttentionIds: removing groupID from attentionIds", "groupID", groupID, "old_count", oldCount, "new_count", len(newAttentionIds), "enable", enableBefore)
} else {
log.ZDebug(ctx, "UpdateAttentionIds: groupID not found in attentionIds, skipping", "groupID", groupID, "count", oldCount)
return nil
}
}
if !updated {
return nil
}
// 验证配置URL是否存在必要的验证
if webhookConfig.URL == "" {
log.ZWarn(ctx, "UpdateAttentionIds: webhook config URL is empty, skipping update", nil, "groupID", groupID, "add", add)
return errs.ErrArgs.WrapMsg("webhook config URL is empty")
}
// 记录更新后的Enable状态确保没有被修改
enableAfter := webhookConfig.AfterSendGroupMsg.Enable
if enableBefore != enableAfter {
log.ZWarn(ctx, "UpdateAttentionIds: Enable field changed unexpectedly", nil,
"groupID", groupID, "add", add,
"enable_before", enableBefore, "enable_after", enableAfter)
// 恢复原始值确保不会修改Enable字段
webhookConfig.AfterSendGroupMsg.Enable = enableBefore
}
// 序列化更新后的配置只更新attentionIds不改变其他配置
updatedValue, err := json.Marshal(webhookConfig)
if err != nil {
log.ZWarn(ctx, "UpdateAttentionIds: failed to marshal updated webhook config", err, "groupID", groupID, "add", add)
return errs.WrapMsg(err, "failed to marshal updated webhook config")
}
// 更新数据库
if err := db.Update(ctx, WebhookConfigKey, map[string]any{
"value": string(updatedValue),
}); err != nil {
log.ZWarn(ctx, "UpdateAttentionIds: failed to update webhook config in database", err, "groupID", groupID, "add", add)
return errs.WrapMsg(err, "failed to update webhook config in database")
}
log.ZInfo(ctx, "UpdateAttentionIds: successfully updated attentionIds in database",
"groupID", groupID, "add", add,
"attention_ids_count", len(webhookConfig.AfterSendGroupMsg.AttentionIds),
"enable", webhookConfig.AfterSendGroupMsg.Enable)
return nil
}