diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a09c56d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.idea diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d3d9dbc --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +# Makefile for nbartookt + +# Go parameters +# GOCMD=GOOS=linux GOARCH=amd64 go +# GOBUILD=CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo +ifndef GOCMD + GOCMD=go +endif +ifndef GOBUILD + GOBUILD=$(GOCMD) build +endif +GOCLEAN=$(GOCMD) clean +GOINSTALL=$(GOCMD) install +GOTEST=$(GOCMD) test +GODEP=$(GOTEST) -i +GOFMT=gofmt -w +BINARY_NAME=wukong-eos + +all: test +test: + $(GOTEST) -v -cover=true -coverprofile=./sdk.cover ./... + go tool cover -html=./sdk.cover -o ./sdk.html +fmt: + find ./ -name "*.go" | xargs gofmt -w diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..ce117b4 --- /dev/null +++ b/client/client.go @@ -0,0 +1,345 @@ +package client + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "github.com/gorilla/websocket" + "github.com/open-dingtalk/dingtalk-stream-sdk-go/handler" + "github.com/open-dingtalk/dingtalk-stream-sdk-go/logger" + "github.com/open-dingtalk/dingtalk-stream-sdk-go/payload" + "github.com/open-dingtalk/dingtalk-stream-sdk-go/utils" + "io" + "net/http" + "sync" + "time" +) + +/** + * @Author linya.jj + * @Date 2023/3/22 14:23 + */ + +type StreamClient struct { + AppCredential *AppCredentialConfig + UserAgent *UserAgentConfig + AutoReconnect bool + + subscriptions map[string]map[string]handler.IFrameHandler + + conn *websocket.Conn + sessionId string + mutex sync.Mutex +} + +func NewStreamClient(options ...ClientOption) *StreamClient { + cli := &StreamClient{} + + defaultOptions := []ClientOption{ + WithSubscription(utils.SubscriptionTypeKSystem, "disconnect", cli.OnDisconnect), + WithSubscription(utils.SubscriptionTypeKSystem, "ping", cli.OnPing), + WithUserAgent(NewDingtalkGoSDKUserAgent()), + WithAutoReconnect(true), + } + + for _, option := range defaultOptions { + option(cli) + } + + for _, option := range options { + if option == nil { + continue + } + + option(cli) + } + + return cli +} + +func (cli *StreamClient) Start(ctx context.Context) error { + if cli.conn != nil { + return nil + } + + cli.mutex.Lock() + defer cli.mutex.Unlock() + + if cli.conn != nil { + return nil + } + + endpoint, err := cli.GetConnectionEndpoint(ctx) + if err != nil { + return err + } + + wssUrl := fmt.Sprintf("%s?ticket=%s", endpoint.Endpoint, endpoint.Ticket) + + header := make(http.Header) + + conn, resp, err := websocket.DefaultDialer.Dial(wssUrl, header) + if err != nil { + return err + } + + // 建连失败 + if resp.StatusCode >= http.StatusBadRequest { + return utils.ErrorFromHttpResponseBody(resp) + } + + cli.conn = conn + cli.sessionId = endpoint.Ticket + + logger.GetLogger().Infof("connect success, sessionId=[%s]", cli.sessionId) + + go cli.processLoop() + + return nil +} + +func (cli *StreamClient) processLoop() { + defer func() { + if err := recover(); err != nil { + logger.GetLogger().Errorf("connection process panic due to unknown reason, error=[%s]", err) + } + if cli.AutoReconnect { + go cli.reconnect() + } + }() + + for { + if cli.conn == nil { + logger.GetLogger().Errorf("connection process connect nil, maybe disconnected.") + return + } + + messageType, message, err := cli.conn.ReadMessage() + if err != nil { + logger.GetLogger().Errorf("connection process read message error: messageType=[%d] message=[%s] error=[%s]", messageType, string(message), err) + return + } + + logger.GetLogger().Debugf("ReadRawMessage : messageType=[%d] message=[%s]", messageType, string(message)) + + go cli.processDataFrame(message) + } +} + +func (cli *StreamClient) processDataFrame(rawData []byte) { + defer func() { + if err := recover(); err != nil { + logger.GetLogger().Errorf("connection processDataFrame panic, error=[%s]", err) + } + }() + + dataFrame, err := payload.DecodeDataFrame(rawData) + if err != nil { + logger.GetLogger().Errorf("connection process decode data frame error: length=[%d] error=[%s]", len(rawData), err) + return + } + + if dataFrame == nil || dataFrame.Headers == nil { + logger.GetLogger().Errorf("connection processDataFrame dataFrame nil.") + return + } + + frameHandler, err := cli.GetHandler(dataFrame.Type, dataFrame.GetTopic()) + if err != nil { + logger.GetLogger().Errorf("connection processDataFrame unregistered handler: type=[%s] topic=[%s]", dataFrame.Type, dataFrame.GetTopic()) + return + } + + dataAck, err := frameHandler(context.Background(), dataFrame) + + if dataAck == nil && err != nil { + dataAck = payload.NewErrorDataFrameResponse(dataFrame.GetMessageId(), err) + } + + if dataAck == nil { + return + } + + errSend := cli.SendDataFrameResponse(context.Background(), dataAck) + logger.GetLogger().Debugf("SendFrameAck dataAck=[%v", dataAck) + + if errSend != nil { + logger.GetLogger().Errorf("connection processDataFrame send response error: error=[%s]", errSend) + } +} + +func (cli *StreamClient) Close() { + if cli.conn == nil { + return + } + + cli.mutex.Lock() + defer cli.mutex.Unlock() + + if cli.conn == nil { + return + } + + if err := cli.conn.Close(); err != nil { + logger.GetLogger().Errorf("StreamClient close. error=[%s]", err) + } + cli.conn = nil + cli.sessionId = "" +} + +func (cli *StreamClient) reconnect() { + defer func() { + if err := recover(); err != nil { + logger.GetLogger().Errorf("reconect panic due to unknown reason. error=[%s]", err) + } + }() + + cli.Close() + + for { + err := cli.Start(context.Background()) + if err != nil { + logger.GetLogger().Errorf("StreamClient reconnect error. error=[%s]", err) + time.Sleep(time.Second * 3) + } else { + logger.GetLogger().Infof("StreamClient reconnect success") + return + } + } + +} + +func (cli *StreamClient) GetHandler(stype, stopic string) (handler.IFrameHandler, error) { + subs := cli.subscriptions[stype] + if subs == nil || subs[stopic] == nil { + return nil, errors.New("HandlerNotRegistedForTypeTopic_" + stype + "_" + stopic) + } + + return subs[stopic], nil +} + +func (cli *StreamClient) CheckConfigValid() error { + if err := cli.AppCredential.Valid(); err != nil { + return err + } + + if err := cli.UserAgent.Valid(); err != nil { + return err + } + + if cli.subscriptions == nil { + return errors.New("subscriptionsNil") + } + + for ttype, subs := range cli.subscriptions { + if _, ok := utils.SubscriptionTypeSet[ttype]; !ok { + return errors.New("UnKnownSubscriptionType_" + ttype) + } + + if len(subs) <= 0 { + return errors.New("NoHandlersRegistedForType_" + ttype) + } + + for ttopic, h := range subs { + if h == nil { + return errors.New("HandlerNilForTypeTopic_" + ttype + "_" + ttopic) + } + } + } + + return nil +} + +func (cli *StreamClient) GetConnectionEndpoint(ctx context.Context) (*payload.ConnectionEndpointResponse, error) { + if err := cli.CheckConfigValid(); err != nil { + return nil, err + } + + requestModel := payload.ConnectionEndpointRequest{ + ClientId: cli.AppCredential.ClientId, + ClientSecret: cli.AppCredential.ClientSecret, + UserAgent: cli.UserAgent.UserAgent, + Subscriptions: make([]*payload.SubscriptionModel, 0), + } + + for ttype, subs := range cli.subscriptions { + for ttopic, _ := range subs { + requestModel.Subscriptions = append(requestModel.Subscriptions, &payload.SubscriptionModel{ + Type: ttype, + Topic: ttopic, + }) + } + } + + requestJsonBody, _ := json.Marshal(requestModel) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, utils.GetConnectionEndpointAPIUrl, bytes.NewReader(requestJsonBody)) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + + httpClient := &http.Client{ + Transport: http.DefaultTransport, + Timeout: 5 * time.Second, //设置超时,包含connection时间、任意重定向时间、读取response body时间 + } + + resp, err := httpClient.Do(req) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return nil, utils.ErrorFromHttpResponseBody(resp) + } + + defer resp.Body.Close() + + responseJsonBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + endpoint := &payload.ConnectionEndpointResponse{} + + if err := json.Unmarshal(responseJsonBody, endpoint); err != nil { + return nil, err + } + + if err := endpoint.Valid(); err != nil { + return nil, err + } + + return endpoint, nil +} + +func (cli *StreamClient) OnDisconnect(ctx context.Context, df *payload.DataFrame) (*payload.DataFrameResponse, error) { + logger.GetLogger().Debugf("StreamClient.OnDisconnect") + + cli.Close() + return nil, nil +} + +func (cli *StreamClient) OnPing(ctx context.Context, df *payload.DataFrame) (*payload.DataFrameResponse, error) { + dfPong := payload.NewDataFrameAckPong(df.GetMessageId()) + dfPong.Data = df.Data + + return dfPong, nil +} + +// 返回正常数据包 +func (cli *StreamClient) SendDataFrameResponse(ctx context.Context, resp *payload.DataFrameResponse) error { + if resp == nil { + return errors.New("SendDataFrameResponseError_ResponseNil") + } + + if cli.conn == nil { + logger.GetLogger().Errorf("SendDataFrameResponse error, conn nil, maybe disconnected.") + return errors.New("disconnected") + } + return cli.conn.WriteJSON(resp) +} diff --git a/client/client_test.go b/client/client_test.go new file mode 100644 index 0000000..7de6c24 --- /dev/null +++ b/client/client_test.go @@ -0,0 +1,49 @@ +package client + +import ( + "testing" +) + +/** + * @Author linya.jj + * @Date 2023/3/22 14:23 + */ + +func TestNewDingtalkOpenStreamClient(t *testing.T) { +} + +func TestDingtalkOpenStreamClient_Start(t *testing.T) { +} + +func TestDingtalkOpenStreamClient_processDataFrame(t *testing.T) { +} + +func TestDingtalkOpenStreamClient_Close(t *testing.T) { + +} + +func TestDingtalkOpenStreamClient_reconnect(t *testing.T) { +} + +func TestDingtalkOpenStreamClient_GetHandler(t *testing.T) { +} + +func TestDingtalkOpenStreamClient_CheckConfigValid(t *testing.T) { +} + +func TestDingtalkOpenStreamClient_GetConnectionEndpoint(t *testing.T) { + +} + +func TestDingtalkOpenStreamClient_OnDisconnect(t *testing.T) { + +} + +func TestDingtalkOpenStreamClient_OnPing(t *testing.T) { +} + +func TestDingtalkOpenStreamClient_SendDataFrameResponse(t *testing.T) { +} + +func TestDingtalkOpenStreamClient_SendErrorResponse(t *testing.T) { +} diff --git a/client/config.go b/client/config.go new file mode 100644 index 0000000..23743f5 --- /dev/null +++ b/client/config.go @@ -0,0 +1,58 @@ +package client + +import ( + "errors" +) + +/** + * @Author linya.jj + * @Date 2023/3/22 14:50 + */ + +// 应用秘钥信息 +type AppCredentialConfig struct { + ClientId string `json:"clientKey" yaml:"clientKey"` //自建应用appKey; 三方应用suiteKey + ClientSecret string `json:"clientSecret" yaml:"clientSecret"` //自建应用appSecret; 三方应用suiteSecret +} + +func NewAppCredentialConfig(clientId, clientSecret string) *AppCredentialConfig { + return &AppCredentialConfig{ + ClientId: clientId, + ClientSecret: clientSecret, + } +} + +func (c *AppCredentialConfig) Valid() error { + if c == nil { + return errors.New("AppCredentialConfigNil") + } + + if c.ClientId == "" || c.ClientSecret == "" { + return errors.New("AppCredentialConfigEmpty") + } + + return nil +} + +// UA信息 +type UserAgentConfig struct { + UserAgent string `json:"user_agent"` +} + +func NewDingtalkGoSDKUserAgent() *UserAgentConfig { + return &UserAgentConfig{ + UserAgent: "dingtalk-sdk-go/0.1.0", + } +} + +func (c *UserAgentConfig) Valid() error { + if c == nil { + return errors.New("UserAgentConfigNil") + } + + if c.UserAgent == "" { + return errors.New("UserAgentConfigEmpty") + } + + return nil +} diff --git a/client/config_test.go b/client/config_test.go new file mode 100644 index 0000000..3eade8b --- /dev/null +++ b/client/config_test.go @@ -0,0 +1,33 @@ +package client + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +/** + * @Author linya.jj + * @Date 2023/3/22 14:50 + */ + +func TestAppCredentialConfig_Valid(t *testing.T) { + conf := NewAppCredentialConfig("clientId", "clientSecret") + assert.Nil(t, conf.Valid()) + + conf.ClientId = "" + assert.NotNil(t, conf.Valid()) + + conf = nil + assert.NotNil(t, conf.Valid()) +} + +func TestDingtalkGoSDKUserAgent_Valid(t *testing.T) { + conf := NewDingtalkGoSDKUserAgent() + assert.Nil(t, conf.Valid()) + + conf.UserAgent = "" + assert.NotNil(t, conf.Valid()) + + conf = nil + assert.NotNil(t, conf.Valid()) +} diff --git a/client/option.go b/client/option.go new file mode 100644 index 0000000..ea74a0f --- /dev/null +++ b/client/option.go @@ -0,0 +1,46 @@ +package client + +import "github.com/open-dingtalk/dingtalk-stream-sdk-go/handler" + +/** + * @Author linya.jj + * @Date 2023/3/22 14:48 + */ + +type ClientOption func(*StreamClient) + +func WithAutoReconnect(autoReconnect bool) ClientOption { + return func(c *StreamClient) { + c.AutoReconnect = autoReconnect + } +} + +func WithAppCredential(cred *AppCredentialConfig) ClientOption { + return func(c *StreamClient) { + c.AppCredential = cred + } +} + +func WithSubscription(stype, stopic string, frameHandler handler.IFrameHandler) ClientOption { + return func(c *StreamClient) { + if c.subscriptions == nil { + c.subscriptions = make(map[string]map[string]handler.IFrameHandler) + } + + if _, ok := c.subscriptions[stype]; !ok { + c.subscriptions[stype] = make(map[string]handler.IFrameHandler) + } + + c.subscriptions[stype][stopic] = frameHandler + } +} + +func WithUserAgent(ua *UserAgentConfig) ClientOption { + return func(c *StreamClient) { + if ua.Valid() != nil { + ua = NewDingtalkGoSDKUserAgent() + } + + c.UserAgent = ua + } +} diff --git a/client/option_test.go b/client/option_test.go new file mode 100644 index 0000000..ac07abf --- /dev/null +++ b/client/option_test.go @@ -0,0 +1,38 @@ +package client + +import ( + "context" + "github.com/stretchr/testify/assert" + "github.com/open-dingtalk/dingtalk-stream-sdk-go/payload" + "testing" +) + +/** + * @Author linya.jj + * @Date 2023/3/22 14:48 + */ + +func TestWithAppCredential(t *testing.T) { + op := WithAppCredential(NewAppCredentialConfig("clientId", "clientSecret")) + + c := NewStreamClient(op) + assert.Equal(t, "clientId", c.AppCredential.ClientId) + assert.Equal(t, "clientSecret", c.AppCredential.ClientSecret) +} + +func TestWithSubscription(t *testing.T) { + op := WithSubscription("stype", "stopic", func(ctx context.Context, df *payload.DataFrame) (*payload.DataFrameResponse, error) { + return nil, nil + }) + + c := NewStreamClient(op) + h, err := c.GetHandler("stype", "stopic") + assert.Nil(t, err) + assert.NotNil(t, h) +} + +func TestWithUserAgent(t *testing.T) { + op := WithUserAgent(NewDingtalkGoSDKUserAgent()) + c := NewStreamClient(op) + assert.NotNil(t, c.UserAgent) +} diff --git a/event/event.go b/event/event.go new file mode 100644 index 0000000..bb2165b --- /dev/null +++ b/event/event.go @@ -0,0 +1,73 @@ +package event + +import ( + "github.com/open-dingtalk/dingtalk-stream-sdk-go/payload" + "strconv" +) + +/** + * @Author linya.jj + * @Date 2023/4/26 17:15 + */ + +const ( + DataFrameHeaderKEventId = "eventId" + DataFrameHeaderKEventBornTime = "eventBornTime" + DataFrameHeaderKEventCorpId = "eventCorpId" + DataFrameHeaderKEventType = "eventType" + DataFrameHeaderKEventUnifiedAppId = "eventUnifiedAppId" +) + +type EventHeader struct { + EventId string `json:"eventId"` + EventBornTime int64 `json:"eventBornTime"` + EventCorpId string `json:"eventCorpId"` + EventType string `json:"eventType"` + EventUnifiedAppId string `json:"eventUnifiedAppId"` +} + +func NewEventHeaderFromDataFrame(df *payload.DataFrame) *EventHeader { + if df == nil { + return &EventHeader{} + } + + eventHeader := &EventHeader{ + EventId: df.GetHeader(DataFrameHeaderKEventId), + EventBornTime: 0, + EventCorpId: df.GetHeader(DataFrameHeaderKEventCorpId), + EventType: df.GetHeader(DataFrameHeaderKEventType), + EventUnifiedAppId: df.GetHeader(DataFrameHeaderKEventUnifiedAppId), + } + + if ts, err := strconv.ParseInt(df.GetHeader(DataFrameHeaderKEventBornTime), 10, 64); err == nil { + eventHeader.EventBornTime = ts + } + + return eventHeader +} + +type EventProcessStatusType string + +var ( + EventProcessStatusKSuccess EventProcessStatusType = "SUCCESS" + EventProcessStatusKLater EventProcessStatusType = "LATER" +) + +type EventProcessResult struct { + Status EventProcessStatusType `json:"status"` + Message string `json:"message"` +} + +func NewEventProcessResultSuccess() *EventProcessResult { + return &EventProcessResult{ + Status: EventProcessStatusKSuccess, + Message: "success", + } +} + +func NewEventProcessResultLater() *EventProcessResult { + return &EventProcessResult{ + Status: EventProcessStatusKLater, + Message: "later", + } +} diff --git a/event/event_handler.go b/event/event_handler.go new file mode 100644 index 0000000..ecd6d7e --- /dev/null +++ b/event/event_handler.go @@ -0,0 +1,26 @@ +package event + +import ( + "context" + "github.com/open-dingtalk/dingtalk-stream-sdk-go/logger" +) + +/** + * @Author linya.jj + * @Date 2023/4/27 09:25 + */ + +type IEventHandler func(c context.Context, header *EventHeader, rawData []byte) (EventProcessStatusType, error) + +func EventHandlerDoNothing(c context.Context, header *EventHeader, rawData []byte) (EventProcessStatusType, error) { + logger.GetLogger().Debugf("EventHandlerDoNothing header=[%s], rawData=[%s]", + header, rawData) + + return EventProcessStatusKSuccess, nil +} + +func EventHandlerSaveToRDS(c context.Context, header *EventHeader, rawData []byte) (EventProcessStatusType, error) { + // TODO save data to rds here + + return EventProcessStatusKSuccess, nil +} diff --git a/event/event_handler_test.go b/event/event_handler_test.go new file mode 100644 index 0000000..7c06fc8 --- /dev/null +++ b/event/event_handler_test.go @@ -0,0 +1,24 @@ +package event + +import ( + "context" + "github.com/stretchr/testify/assert" + "testing" +) + +/** + * @Author linya.jj + * @Date 2023/4/27 09:25 + */ + +func TestEventHandlerDoNothing(t *testing.T) { + status, err := EventHandlerDoNothing(context.Background(), nil, []byte("")) + assert.Nil(t, err) + assert.Equal(t, EventProcessStatusKSuccess, status) +} + +func TestEventHandlerSaveToRDS(t *testing.T) { + status, err := EventHandlerSaveToRDS(context.Background(), nil, []byte("")) + assert.Nil(t, err) + assert.Equal(t, EventProcessStatusKSuccess, status) +} diff --git a/event/event_test.go b/event/event_test.go new file mode 100644 index 0000000..072c883 --- /dev/null +++ b/event/event_test.go @@ -0,0 +1,47 @@ +package event + +import ( + "github.com/stretchr/testify/assert" + "github.com/open-dingtalk/dingtalk-stream-sdk-go/payload" + "github.com/open-dingtalk/dingtalk-stream-sdk-go/utils" + "testing" +) + +/** + * @Author linya.jj + * @Date 2023/4/26 17:15 + */ + +func TestNewEventHeaderFromDataFrame(t *testing.T) { + assert.NotNil(t, NewEventHeaderFromDataFrame(nil)) + + df := &payload.DataFrame{ + SpecVersion: "version", + Type: utils.SubscriptionTypeKEvent, + Time: 12345678, + Headers: payload.DataFrameHeader{ + DataFrameHeaderKEventId: "eventId", + DataFrameHeaderKEventBornTime: "1234567890", + DataFrameHeaderKEventCorpId: "eventCorpId", + DataFrameHeaderKEventType: "eventType", + DataFrameHeaderKEventUnifiedAppId: "eventUnifiedAppId", + }, + Data: "", + } + + eh := NewEventHeaderFromDataFrame(df) + assert.NotNil(t, eh) + assert.Equal(t, "eventId", eh.EventId) + assert.Equal(t, int64(1234567890), eh.EventBornTime) + assert.Equal(t, "eventCorpId", eh.EventCorpId) + assert.Equal(t, "eventType", eh.EventType) + assert.Equal(t, "eventUnifiedAppId", eh.EventUnifiedAppId) +} + +func TestNewEventProcessResultSuccess(t *testing.T) { + assert.NotNil(t, NewEventProcessResultSuccess()) +} + +func TestNewEventProcessResultLater(t *testing.T) { + assert.NotNil(t, NewEventProcessResultLater()) +} diff --git a/event/frame_handler.go b/event/frame_handler.go new file mode 100644 index 0000000..9884aac --- /dev/null +++ b/event/frame_handler.go @@ -0,0 +1,63 @@ +package event + +import ( + "context" + "encoding/json" + "github.com/open-dingtalk/dingtalk-stream-sdk-go/logger" + "github.com/open-dingtalk/dingtalk-stream-sdk-go/payload" +) + +/** + * @Author linya.jj + * @Date 2023/4/26 17:15 + */ + +type DefaultEventFrameHandler struct { + defaultHandler IEventHandler +} + +func NewDefaultEventFrameHandler(defaultHandler IEventHandler) *DefaultEventFrameHandler { + return &DefaultEventFrameHandler{ + defaultHandler: defaultHandler, + } +} + +func (h *DefaultEventFrameHandler) OnEventReceived(ctx context.Context, df *payload.DataFrame) (*payload.DataFrameResponse, error) { + eventHeader := NewEventHeaderFromDataFrame(df) + + if h.defaultHandler == nil { + logger.GetLogger().Warningf("No event handler found, drop this event. eventType=[%s], eventId=[%s], eventCorpId=[%s]", + eventHeader.EventType, eventHeader.EventId, eventHeader.EventCorpId) + + return nil, nil + } + + ret, err := h.defaultHandler(ctx, eventHeader, []byte(df.Data)) + if err != nil { + logger.GetLogger().Errorf("Event handler process error. eventType=[%s], eventId=[%s], eventCorpId=[%s] err=[%s]", + eventHeader.EventType, eventHeader.EventId, eventHeader.EventCorpId, err) + + ret = EventProcessStatusKLater + } + + result := NewEventProcessResultSuccess() + code := payload.DataFrameResponseStatusCodeKOK + if ret != EventProcessStatusKSuccess { + code = payload.DataFrameResponseStatusCodeKInternalError + result = NewEventProcessResultLater() + } + + resultStr, _ := json.Marshal(result) + + frameResp := &payload.DataFrameResponse{ + Code: code, + Headers: payload.DataFrameHeader{ + payload.DataFrameHeaderKContentType: payload.DataFrameContentTypeKJson, + payload.DataFrameHeaderKMessageId: df.GetMessageId(), + }, + Message: "ok", + Data: string(resultStr), + } + + return frameResp, nil +} diff --git a/event/frame_handler_test.go b/event/frame_handler_test.go new file mode 100644 index 0000000..ff27dbb --- /dev/null +++ b/event/frame_handler_test.go @@ -0,0 +1,50 @@ +package event + +import ( + "context" + "errors" + "github.com/stretchr/testify/assert" + "github.com/open-dingtalk/dingtalk-stream-sdk-go/payload" + "testing" +) + +/** + * @Author linya.jj + * @Date 2023/4/26 17:15 + */ + +func EventHandlerSuccess(c context.Context, header *EventHeader, rawData []byte) (EventProcessStatusType, error) { + return EventProcessStatusKSuccess, nil +} + +func EventHandlerLater(c context.Context, header *EventHeader, rawData []byte) (EventProcessStatusType, error) { + return EventProcessStatusKLater, nil +} + +func EventHandlerLaterError(c context.Context, header *EventHeader, rawData []byte) (EventProcessStatusType, error) { + return EventProcessStatusKLater, errors.New("error") +} + +func TestDefaultEventFrameHandler_OnEventReceived(t *testing.T) { + defh := NewDefaultEventFrameHandler(nil) + ret, err := defh.OnEventReceived(context.Background(), nil) + assert.Nil(t, ret) + assert.Nil(t, err) + + df := &payload.DataFrame{} + + defh = NewDefaultEventFrameHandler(EventHandlerSuccess) + ret, err = defh.OnEventReceived(context.Background(), df) + assert.Equal(t, payload.DataFrameResponseStatusCodeKOK, ret.Code) + assert.Nil(t, err) + + defh = NewDefaultEventFrameHandler(EventHandlerLater) + ret, err = defh.OnEventReceived(context.Background(), df) + assert.Equal(t, payload.DataFrameResponseStatusCodeKInternalError, ret.Code) + assert.Nil(t, err) + + defh = NewDefaultEventFrameHandler(EventHandlerLaterError) + ret, err = defh.OnEventReceived(context.Background(), df) + assert.Equal(t, payload.DataFrameResponseStatusCodeKInternalError, ret.Code) + assert.Nil(t, err) +} diff --git a/example/example_bot.go b/example/example_bot.go new file mode 100644 index 0000000..0acff20 --- /dev/null +++ b/example/example_bot.go @@ -0,0 +1,47 @@ +package main + +import ( + "context" + "github.com/open-dingtalk/dingtalk-stream-sdk-go/client" + "github.com/open-dingtalk/dingtalk-stream-sdk-go/logger" + "github.com/open-dingtalk/dingtalk-stream-sdk-go/payload" + "github.com/open-dingtalk/dingtalk-stream-sdk-go/utils" +) + +/** + * @Author linya.jj + * @Date 2023/3/22 18:30 + */ + +func OnBotCallback(ctx context.Context, df *payload.DataFrame) (*payload.DataFrameResponse, error) { + frameResp := &payload.DataFrameResponse{ + Code: 200, + Headers: payload.DataFrameHeader{ + payload.DataFrameHeaderKContentType: payload.DataFrameContentTypeKJson, + payload.DataFrameHeaderKMessageId: df.GetMessageId(), + }, + Message: "ok", + Data: "", + } + + return frameResp, nil +} + +func RunBotListener() { + logger.SetLogger(logger.NewStdTestLogger()) + + cli := client.NewStreamClient( + client.WithAppCredential(client.NewAppCredentialConfig("your-client-id", "your-client-secret")), + client.WithUserAgent(client.NewDingtalkGoSDKUserAgent()), + client.WithSubscription(utils.SubscriptionTypeKCallback, payload.BotMessageCallbackTopic, OnBotCallback), + ) + + err := cli.Start(context.Background()) + if err != nil { + panic(err) + } + + defer cli.Close() + + select {} +} diff --git a/example/example_event.go b/example/example_event.go new file mode 100644 index 0000000..5844dca --- /dev/null +++ b/example/example_event.go @@ -0,0 +1,35 @@ +package main + +import ( + "context" + "github.com/open-dingtalk/dingtalk-stream-sdk-go/client" + "github.com/open-dingtalk/dingtalk-stream-sdk-go/event" + "github.com/open-dingtalk/dingtalk-stream-sdk-go/logger" + "github.com/open-dingtalk/dingtalk-stream-sdk-go/utils" +) + +/** + * @Author linya.jj + * @Date 2023/3/22 18:30 + */ + +func RunEventListener() { + logger.SetLogger(logger.NewStdTestLogger()) + + eventHandler := event.NewDefaultEventFrameHandler(event.EventHandlerDoNothing) + + cli := client.NewStreamClient( + client.WithAppCredential(client.NewAppCredentialConfig("your-client-id", "your-client-secret")), + client.WithUserAgent(client.NewDingtalkGoSDKUserAgent()), + client.WithSubscription(utils.SubscriptionTypeKEvent, "*", eventHandler.OnEventReceived), + ) + + err := cli.Start(context.Background()) + if err != nil { + panic(err) + } + + defer cli.Close() + + select {} +} diff --git a/example/example_main.go b/example/example_main.go new file mode 100644 index 0000000..7be3220 --- /dev/null +++ b/example/example_main.go @@ -0,0 +1,12 @@ +package main + +/** + * @Author linya.jj + * @Date 2023/3/22 18:30 + */ + +func main() { + RunBotListener() + + select {} +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b70cd2a --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/open-dingtalk/dingtalk-stream-sdk-go + +go 1.18 + +require ( + github.com/google/uuid v1.3.0 + github.com/gorilla/websocket v1.5.0 + github.com/stretchr/testify v1.8.2 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c3dad28 --- /dev/null +++ b/go.sum @@ -0,0 +1,21 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/handler/ihandler.go b/handler/ihandler.go new file mode 100644 index 0000000..4cf5a69 --- /dev/null +++ b/handler/ihandler.go @@ -0,0 +1,13 @@ +package handler + +import ( + "context" + "github.com/open-dingtalk/dingtalk-stream-sdk-go/payload" +) + +/** + * @Author linya.jj + * @Date 2023/3/22 14:27 + */ + +type IFrameHandler func(c context.Context, df *payload.DataFrame) (*payload.DataFrameResponse, error) diff --git a/handler/ihandler_test.go b/handler/ihandler_test.go new file mode 100644 index 0000000..59a8582 --- /dev/null +++ b/handler/ihandler_test.go @@ -0,0 +1,13 @@ +package handler + +import ( + "testing" +) + +/** + * @Author linya.jj + * @Date 2023/3/22 14:27 + */ + +func TestOnEventReceived(t *testing.T) { +} diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..42905f8 --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,56 @@ +package logger + +/** + * @Author linya.jj + * @Date 2023/3/22 14:30 + */ + +type ILogger interface { + Debugf(format string, args ...interface{}) + + Infof(format string, args ...interface{}) + + Warningf(format string, args ...interface{}) + + Errorf(format string, args ...interface{}) + + Fatalf(format string, args ...interface{}) +} + +var ( + sdkLogger ILogger +) + +func SetLogger(customLogger ILogger) { + sdkLogger = customLogger +} + +func GetLogger() ILogger { + if sdkLogger == nil { + sdkLogger = &doNothingLogger{} + } + return sdkLogger +} + +type doNothingLogger struct { +} + +func (l *doNothingLogger) Debugf(format string, args ...interface{}) { + +} + +func (l *doNothingLogger) Infof(format string, args ...interface{}) { + +} + +func (l *doNothingLogger) Warningf(format string, args ...interface{}) { + +} + +func (l *doNothingLogger) Errorf(format string, args ...interface{}) { + +} + +func (l *doNothingLogger) Fatalf(format string, args ...interface{}) { + +} diff --git a/logger/logger_test.go b/logger/logger_test.go new file mode 100644 index 0000000..95e5270 --- /dev/null +++ b/logger/logger_test.go @@ -0,0 +1,19 @@ +package logger + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +/** + * @Author linya.jj + * @Date 2023/3/22 14:30 + */ + +func TestSetGetSDKLogger(t *testing.T) { + assert.NotNil(t, GetLogger()) + + stdLogger := NewStdTestLogger() + SetLogger(stdLogger) + assert.Equal(t, stdLogger, GetLogger()) +} diff --git a/logger/std_logger.go b/logger/std_logger.go new file mode 100644 index 0000000..8b9e7be --- /dev/null +++ b/logger/std_logger.go @@ -0,0 +1,49 @@ +package logger + +import ( + "fmt" + "time" +) + +/** + * @Author linya.jj + * @Date 2023/3/22 14:32 + */ + +// This logger is only for debug. Do not use it online. +type StdTestLogger struct { +} + +func NewStdTestLogger() *StdTestLogger { + return &StdTestLogger{} +} + +func (l *StdTestLogger) Debugf(format string, args ...interface{}) { + fmt.Printf("%s [Debug] ", time.Now().String()) + fmt.Printf(format, args...) + fmt.Print("\n") +} + +func (l *StdTestLogger) Infof(format string, args ...interface{}) { + fmt.Printf("%s [INFO] ", time.Now().String()) + fmt.Printf(format, args...) + fmt.Print("\n") +} + +func (l *StdTestLogger) Warningf(format string, args ...interface{}) { + fmt.Printf("%s [WARNING] ", time.Now().String()) + fmt.Printf(format, args...) + fmt.Print("\n") +} + +func (l *StdTestLogger) Errorf(format string, args ...interface{}) { + fmt.Printf("%s [ERROR] ", time.Now().String()) + fmt.Printf(format, args...) + fmt.Print("\n") +} + +func (l *StdTestLogger) Fatalf(format string, args ...interface{}) { + fmt.Printf("%s [FATAL] ", time.Now().String()) + fmt.Printf(format, args...) + fmt.Print("\n") +} diff --git a/logger/std_logger_test.go b/logger/std_logger_test.go new file mode 100644 index 0000000..69e270a --- /dev/null +++ b/logger/std_logger_test.go @@ -0,0 +1,20 @@ +package logger + +import ( + "testing" +) + +/** + * @Author linya.jj + * @Date 2023/3/22 14:32 + */ + +func TestStdLogger_Output(t *testing.T) { + stdLogger := NewStdTestLogger() + + stdLogger.Debugf("logger level: %s", "debug") + stdLogger.Infof("logger level: %s", "info") + stdLogger.Warningf("logger level: %s", "warning") + stdLogger.Errorf("logger level: %s", "error") + stdLogger.Fatalf("logger level: %s", "fatal") +} diff --git a/payload/connection.go b/payload/connection.go new file mode 100644 index 0000000..de20f28 --- /dev/null +++ b/payload/connection.go @@ -0,0 +1,39 @@ +package payload + +import "errors" + +/** + * @Author linya.jj + * @Date 2023/3/22 18:22 + */ + +type SubscriptionModel struct { + Type string `json:"type"` + Topic string `json:"topic"` +} + +// 长连接接入点请求 +type ConnectionEndpointRequest struct { + ClientId string `json:"clientId"` //自建应用appKey; 三方应用suiteKey + ClientSecret string `json:"clientSecret"` //自建应用appSecret; 三方应用suiteSecret + Subscriptions []*SubscriptionModel `json:"subscriptions"` + UserAgent string `json:"ua"` +} + +// 长连接接入点参数 +type ConnectionEndpointResponse struct { + Endpoint string `json:"endpoint"` + Ticket string `json:"ticket"` +} + +func (r *ConnectionEndpointResponse) Valid() error { + if r == nil { + return errors.New("ConnectionEndpointResponseNil") + } + + if r.Endpoint == "" || r.Ticket == "" { + return errors.New("ConnectionEndpointResponseContentEmpty") + } + + return nil +} diff --git a/payload/connection_test.go b/payload/connection_test.go new file mode 100644 index 0000000..0876747 --- /dev/null +++ b/payload/connection_test.go @@ -0,0 +1,26 @@ +package payload + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +/** + * @Author linya.jj + * @Date 2023/3/22 18:22 + */ + +func TestConnectionEndpointResponse_Valid(t *testing.T) { + resp := &ConnectionEndpointResponse{ + Endpoint: "ep", + Ticket: "ti", + } + + assert.Nil(t, resp.Valid()) + + resp.Endpoint = "" + assert.NotNil(t, resp.Valid()) + + resp = nil + assert.NotNil(t, resp.Valid()) +} diff --git a/payload/data_frame.go b/payload/data_frame.go new file mode 100644 index 0000000..3ac7b7a --- /dev/null +++ b/payload/data_frame.go @@ -0,0 +1,141 @@ +package payload + +import ( + "encoding/json" + "strconv" +) + +/** + * @Author linya.jj + * @Date 2023/3/31 09:57 + */ + +type DataFrameHeader map[string]string + +func (h DataFrameHeader) Get(key string) string { + return h[key] +} + +func (h DataFrameHeader) Set(key, value string) { + h[key] = value +} + +type DataFrame struct { + SpecVersion string `json:"specVersion"` + Type string `json:"type"` + Time int64 `json:"time"` + Headers DataFrameHeader `json:"headers""` + Data string `json:"data"` +} + +func (df *DataFrame) Encode() []byte { + if df == nil { + return nil + } + + data, _ := json.Marshal(df) + return data +} + +func (df *DataFrame) GetTopic() string { + if df == nil { + return "" + } + + return df.Headers.Get(DataFrameHeaderKTopic) +} + +func (df *DataFrame) GetMessageId() string { + if df == nil { + return "" + } + + return df.Headers.Get(DataFrameHeaderKMessageId) +} + +func (df *DataFrame) GetTimestamp() int64 { + if df == nil { + return 0 + } + + strTs := df.Headers.Get(DataFrameHeaderKTime) + ts, err := strconv.ParseInt(strTs, 10, 64) + if err != nil { + return 0 + } + return ts +} + +func (df *DataFrame) GetHeader(header string) string { + if df == nil { + return "" + } + + return df.Headers.Get(header) +} + +func DecodeDataFrame(rawData []byte) (*DataFrame, error) { + df := &DataFrame{} + + err := json.Unmarshal(rawData, df) + if err != nil { + return nil, err + } + + return df, nil +} + +type DataFrameResponse struct { + Code int `json:"code"` + Headers DataFrameHeader `json:"headers"` + Message string `json:"message"` + Data string `json:"data"` +} + +func (df *DataFrameResponse) Encode() []byte { + if df == nil { + return nil + } + + data, _ := json.Marshal(df) + return data +} + +func DecodeDataFrameResponse(rawData []byte) (*DataFrameResponse, error) { + resp := &DataFrameResponse{} + + err := json.Unmarshal(rawData, resp) + if err != nil { + return nil, err + } + + return resp, nil +} + +func NewDataFrameAckPong(messageId string) *DataFrameResponse { + return &DataFrameResponse{ + Code: DataFrameResponseStatusCodeKOK, + Headers: DataFrameHeader{ + DataFrameHeaderKContentType: DataFrameContentTypeKJson, + DataFrameHeaderKMessageId: messageId, + }, + Message: "ok", + Data: "", //TODO data内容暂留空 + } +} + +func NewErrorDataFrameResponse(messageId string, err error) *DataFrameResponse { + if err == nil { + return nil + } + + return &DataFrameResponse{ + Code: 400, //TODO errorcode 细化 + Headers: DataFrameHeader{ + DataFrameHeaderKContentType: DataFrameContentTypeKJson, + DataFrameHeaderKMessageId: messageId, + }, + Message: err.Error(), + Data: "", //TODO data内容暂留空 + } +} diff --git a/payload/data_frame_test.go b/payload/data_frame_test.go new file mode 100644 index 0000000..1bb4208 --- /dev/null +++ b/payload/data_frame_test.go @@ -0,0 +1,77 @@ +package payload + +import ( + "errors" + "github.com/stretchr/testify/assert" + "testing" +) + +/** + * @Author linya.jj + * @Date 2023/3/31 09:57 + */ + +func TestDataFrameHeader_GetSet(t *testing.T) { + h := &DataFrameHeader{} + h.Set("k1", "v1") + h.Set("k2", "v2") + + assert.Equal(t, "v1", h.Get("k1")) + assert.Equal(t, "v2", h.Get("k2")) + assert.Equal(t, "", h.Get("k_notexist")) + + h.Set("k1", "v11") + assert.Equal(t, "v11", h.Get("k1")) +} + +func TestDataFrame_EncodeDecode(t *testing.T) { + df := &DataFrame{ + SpecVersion: "sv", + Type: "t", + Time: 123456, + Headers: DataFrameHeader{"k1": "v1"}, + Data: "data", + } + + ss := df.Encode() + assert.NotEqual(t, "", string(ss)) + + df0, err := DecodeDataFrame(ss) + assert.Nil(t, err) + assert.EqualValues(t, df, df0) + + ss = []byte(`{"time":"time"}`) + _, err = DecodeDataFrame([]byte(ss)) + assert.NotNil(t, err) +} + +func TestDataFrameResponse_EncodeDecode(t *testing.T) { + resp := &DataFrameResponse{ + Code: 200, + Headers: DataFrameHeader{"k1": "v1"}, + Message: "msg", + Data: "data", + } + + ss := resp.Encode() + assert.NotEqual(t, "", string(ss)) + + resp0, err := DecodeDataFrameResponse(ss) + assert.Nil(t, err) + assert.EqualValues(t, resp, resp0) + + ss = []byte(`{"code":"code"}`) + _, err = DecodeDataFrameResponse(ss) + assert.NotNil(t, err) +} + +func TestNewDataFrameAckPong(t *testing.T) { + pong := NewDataFrameAckPong("messageId") + assert.NotNil(t, pong) +} + +func TestNewErrorDataFrameResponse(t *testing.T) { + errResp := NewErrorDataFrameResponse("messageId", errors.New("error")) + assert.NotNil(t, errResp) + assert.Equal(t, "error", errResp.Message) +} diff --git a/payload/utils.go b/payload/utils.go new file mode 100644 index 0000000..40d85e5 --- /dev/null +++ b/payload/utils.go @@ -0,0 +1,30 @@ +package payload + +import ( + "fmt" + "github.com/google/uuid" +) + +/** + * @Author linya.jj + * @Date 2023/4/7 15:13 + */ + +const ( + DataFrameHeaderKTopic = "topic" + DataFrameHeaderKContentType = "contentType" + DataFrameHeaderKMessageId = "messageId" + DataFrameHeaderKTime = "time" + + DataFrameContentTypeKJson = "application/json" + DataFrameContentTypeKBase64 = "base64String" + + DataFrameResponseStatusCodeKOK = 200 + DataFrameResponseStatusCodeKInternalError = 500 + + BotMessageCallbackTopic = "bot_got_msg" //机器人消息统一回调topic +) + +func GenerateMessageId(prefix string) string { + return fmt.Sprintf("%s-%s", prefix, uuid.New().String()) +} diff --git a/payload/utils_test.go b/payload/utils_test.go new file mode 100644 index 0000000..4babd02 --- /dev/null +++ b/payload/utils_test.go @@ -0,0 +1,17 @@ +package payload + +import ( + "github.com/stretchr/testify/assert" + "strings" + "testing" +) + +/** + * @Author linya.jj + * @Date 2023/4/7 15:13 + */ + +func TestGenerateMessageId(t *testing.T) { + assert.NotEqual(t, "", GenerateMessageId("prefix-")) + assert.True(t, strings.HasPrefix(GenerateMessageId("prefix-"), "prefix-")) +} diff --git a/utils/errors.go b/utils/errors.go new file mode 100644 index 0000000..67ba31b --- /dev/null +++ b/utils/errors.go @@ -0,0 +1,28 @@ +package utils + +import ( + "errors" + "io" + "net/http" +) + +/** + * @Author linya.jj + * @Date 2023/3/31 09:51 + */ + +// 把http response的内容转换成error对象 +func ErrorFromHttpResponseBody(resp *http.Response) error { + if resp == nil { + return errors.New("HttpResponseNil") + } + + defer resp.Body.Close() + + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + return errors.New(string(responseBody)) +} diff --git a/utils/errors_test.go b/utils/errors_test.go new file mode 100644 index 0000000..3265810 --- /dev/null +++ b/utils/errors_test.go @@ -0,0 +1,23 @@ +package utils + +import ( + "github.com/stretchr/testify/assert" + "io" + "net/http" + "strings" + "testing" +) + +/** + * @Author linya.jj + * @Date 2023/3/31 09:51 + */ + +func TestErrorFromHttpResponse(t *testing.T) { + assert.NotNil(t, ErrorFromHttpResponseBody(nil)) + + resp := &http.Response{ + Body: io.NopCloser(strings.NewReader("error")), + } + assert.NotNil(t, ErrorFromHttpResponseBody(resp)) +} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..8097ca8 --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,24 @@ +package utils + +/** + * @Author linya.jj + * @Date 2023/3/22 15:23 + */ + +const ( + GetConnectionEndpointAPIUrl = "https://api.dingtalk.com/v1.0/gateway/connections/open" +) + +const ( + SubscriptionTypeKSystem = "SYSTEM" //系统请求 + SubscriptionTypeKEvent = "EVENT" //事件 + SubscriptionTypeKCallback = "CALLBACK" //回调 +) + +var ( + SubscriptionTypeSet = map[string]bool{ + SubscriptionTypeKSystem: true, + SubscriptionTypeKEvent: true, + SubscriptionTypeKCallback: true, + } +)