// 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表示添加groupID,false表示移除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 }