// 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" "net/http" "net/url" "sync" "git.imall.cloud/openim/open-im-server-deploy/pkg/callbackstruct" "git.imall.cloud/openim/open-im-server-deploy/pkg/common/config" "git.imall.cloud/openim/open-im-server-deploy/pkg/common/servererrs" "git.imall.cloud/openim/protocol/constant" "github.com/openimsdk/tools/log" "github.com/openimsdk/tools/mcontext" "github.com/openimsdk/tools/mq/memamq" "github.com/openimsdk/tools/utils/httputil" ) type Client struct { client *httputil.HTTPClient url string queue *memamq.MemoryQueue configManager *ConfigManager mu sync.RWMutex } const ( webhookWorkerCount = 2 webhookBufferSize = 100 Key = "key" ) func NewWebhookClient(url string, options ...*memamq.MemoryQueue) *Client { var queue *memamq.MemoryQueue if len(options) > 0 && options[0] != nil { queue = options[0] } else { queue = memamq.NewMemoryQueue(webhookWorkerCount, webhookBufferSize) } http.DefaultTransport.(*http.Transport).MaxConnsPerHost = 100 // Enhance the default number of max connections per host return &Client{ client: httputil.NewHTTPClient(httputil.NewClientConfig()), url: url, queue: queue, } } // NewWebhookClientWithManager 创建支持动态配置的webhook client func NewWebhookClientWithManager(configManager *ConfigManager, options ...*memamq.MemoryQueue) *Client { var queue *memamq.MemoryQueue if len(options) > 0 && options[0] != nil { queue = options[0] } else { queue = memamq.NewMemoryQueue(webhookWorkerCount, webhookBufferSize) } http.DefaultTransport.(*http.Transport).MaxConnsPerHost = 100 // Enhance the default number of max connections per host return &Client{ client: httputil.NewHTTPClient(httputil.NewClientConfig()), url: configManager.GetURL(), queue: queue, configManager: configManager, } } // getURL 获取当前webhook URL(支持动态配置) func (c *Client) getURL() string { if c.configManager != nil { url := c.configManager.GetURL() log.ZDebug(context.Background(), "webhook getURL from config manager", "url", url) return url } c.mu.RLock() defer c.mu.RUnlock() log.ZDebug(context.Background(), "webhook getURL from static config", "url", c.url) return c.url } // GetConfig returns the latest webhook config from the manager when available, // falling back to the provided default configuration. func (c *Client) GetConfig(defaultConfig *config.Webhooks) *config.Webhooks { if c == nil { return defaultConfig } if c.configManager != nil { if cfg := c.configManager.GetConfig(); cfg != nil { return cfg } } return defaultConfig } func (c *Client) SyncPost(ctx context.Context, command string, req callbackstruct.CallbackReq, resp callbackstruct.CallbackResp, before *config.BeforeConfig) error { return c.post(ctx, command, req, resp, before.Timeout) } func (c *Client) AsyncPost(ctx context.Context, command string, req callbackstruct.CallbackReq, resp callbackstruct.CallbackResp, after *config.AfterConfig) { log.ZDebug(ctx, "webhook AsyncPost called", "command", command, "enable", after.Enable) if after.Enable { log.ZInfo(ctx, "webhook AsyncPost queued", "command", command, "timeout", after.Timeout) c.queue.Push(func() { c.post(ctx, command, req, resp, after.Timeout) }) } else { log.ZDebug(ctx, "webhook AsyncPost skipped (disabled)", "command", command) } } func (c *Client) AsyncPostWithQuery(ctx context.Context, command string, req callbackstruct.CallbackReq, resp callbackstruct.CallbackResp, after *config.AfterConfig, queryParams map[string]string) { log.ZDebug(ctx, "webhook AsyncPostWithQuery called", "command", command, "enable", after.Enable) if after.Enable { log.ZInfo(ctx, "webhook AsyncPostWithQuery queued", "command", command, "timeout", after.Timeout) c.queue.Push(func() { c.postWithQuery(ctx, command, req, resp, after.Timeout, queryParams) }) } else { log.ZDebug(ctx, "webhook AsyncPostWithQuery skipped (disabled)", "command", command) } } func (c *Client) post(ctx context.Context, command string, input interface{}, output callbackstruct.CallbackResp, timeout int) error { ctx = mcontext.WithMustInfoCtx([]string{mcontext.GetOperationID(ctx), mcontext.GetOpUserID(ctx), mcontext.GetOpUserPlatform(ctx), mcontext.GetConnID(ctx)}) fullURL := c.getURL() + "/" + command log.ZInfo(ctx, "webhook", "url", fullURL, "input", input, "config", timeout) operationID, _ := ctx.Value(constant.OperationID).(string) b, err := c.client.Post(ctx, fullURL, map[string]string{constant.OperationID: operationID}, input, timeout) if err != nil { return servererrs.ErrNetwork.WrapMsg(err.Error(), "post url", fullURL) } if err = json.Unmarshal(b, output); err != nil { return servererrs.ErrData.WithDetail(err.Error() + " response format error") } if err := output.Parse(); err != nil { return err } log.ZInfo(ctx, "webhook success", "url", fullURL, "input", input, "response", string(b)) return nil } func (c *Client) postWithQuery(ctx context.Context, command string, input interface{}, output callbackstruct.CallbackResp, timeout int, queryParams map[string]string) error { ctx = mcontext.WithMustInfoCtx([]string{mcontext.GetOperationID(ctx), mcontext.GetOpUserID(ctx), mcontext.GetOpUserPlatform(ctx), mcontext.GetConnID(ctx)}) fullURL := c.getURL() + "/" + command parsedURL, err := url.Parse(fullURL) if err != nil { return servererrs.ErrNetwork.WrapMsg(err.Error(), "failed to parse URL", fullURL) } query := parsedURL.Query() operationID, _ := ctx.Value(constant.OperationID).(string) for key, value := range queryParams { query.Set(key, value) } parsedURL.RawQuery = query.Encode() fullURL = parsedURL.String() log.ZInfo(ctx, "webhook", "url", fullURL, "input", input, "config", timeout) b, err := c.client.Post(ctx, fullURL, map[string]string{constant.OperationID: operationID}, input, timeout) if err != nil { return servererrs.ErrNetwork.WrapMsg(err.Error(), "post url", fullURL) } if err = json.Unmarshal(b, output); err != nil { return servererrs.ErrData.WithDetail(err.Error() + " response format error") } if err := output.Parse(); err != nil { return err } log.ZInfo(ctx, "webhook success", "url", fullURL, "input", input, "response", string(b)) return nil }