diff --git a/internal/api/admin.go b/internal/api/admin.go index 1d860bf80..886dd38ca 100644 --- a/internal/api/admin.go +++ b/internal/api/admin.go @@ -136,7 +136,32 @@ func (o *AdminApi) AddAdminAccount(c *gin.Context) { } func (o *AdminApi) AddUserAccount(c *gin.Context) { - a2r.Call(chat.ChatClient.AddUserAccount, o.chatClient, c) + var req chat.AddUserAccountReq + if err := c.BindJSON(&req); err != nil { + apiresp.GinError(c, err) + return + } + if err := checker.Validate(&req); err != nil { + apiresp.GinError(c, err) // 参数校验失败 + return + } + + _, err := o.chatClient.AddUserAccount(c, &req) + + userInfo := &sdkws.UserInfo{ + UserID: req.User.UserID, + Nickname: req.User.Nickname, + FaceURL: req.User.FaceURL, + CreateTime: time.Now().UnixMilli(), + } + err = o.imApiCaller.RegisterUser(c, []*sdkws.UserInfo{userInfo}) + if err != nil { + apiresp.GinError(c, err) + return + } + + apiresp.GinSuccess(c, nil) + } func (o *AdminApi) DelAdminAccount(c *gin.Context) { diff --git a/internal/api/chat.go b/internal/api/chat.go index 12e4f1bed..10bc90c38 100644 --- a/internal/api/chat.go +++ b/internal/api/chat.go @@ -15,9 +15,19 @@ package api import ( + "bytes" + "context" + "crypto/md5" + "encoding/hex" + "encoding/json" "fmt" + "github.com/OpenIMSDK/protocol/msg" + "github.com/OpenIMSDK/tools/utils" "io" "net" + "net/http" + "reflect" + "strings" "time" "github.com/OpenIMSDK/chat/pkg/common/apicall" @@ -347,3 +357,303 @@ func (o *ChatApi) SearchFriend(c *gin.Context) { } apiresp.GinSuccess(c, resp) } + +func (m *ChatApi) CallbackExample(c *gin.Context) { + + // 1. Callback after sending a single chat message + var req apistruct.CallbackAfterSendSingleMsgReq + + if err := c.BindJSON(&req); err != nil { + log.ZError(c, "CallbackExample BindJSON failed", err) + apiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap()) + return + } + + resp := apistruct.CallbackAfterSendSingleMsgResp{ + CommonCallbackResp: apistruct.CommonCallbackResp{ + ActionCode: 0, + ErrCode: 200, + ErrMsg: "success", + ErrDlt: "successful", + NextCode: 0, + }, + } + c.JSON(http.StatusOK, resp) + + // 2. If the user receiving the message is a customer service bot, return the message. + + // UserID of the robot account + + if req.SendID == "robotics" || req.RecvID != "robotics" { + return + } + + if req.ContentType != constant.Picture && req.ContentType != constant.Text { + return + } + + // Administrator token + url := "http://127.0.0.1:10009/account/login" + adminID := config.Config.ChatAdmin[0].AdminID + paswd := md5.Sum([]byte(adminID)) + + admin_input := admin.LoginReq{ + Account: config.Config.ChatAdmin[0].AdminID, + Password: hex.EncodeToString(paswd[:]), + } + + header := make(map[string]string, 2) + header["operationID"] = "111" + + b, err := Post(c, url, header, admin_input, 10) + if err != nil { + log.ZError(c, "CallbackExample send message failed", err) + apiresp.GinError(c, errs.ErrInternalServer.WithDetail(err.Error()).Wrap()) + return + } + + type TokenInfo struct { + ErrCode int `json:"errCode"` + ErrMsg string `json:"errMsg"` + ErrDlt string `json:"errDlt"` + Data apistruct.AdminLoginResp `json:"data,omitempty"` + } + + admin_output := &TokenInfo{} + + if err = json.Unmarshal(b, admin_output); err != nil { + log.ZError(c, "CallbackExample unmarshal failed", err) + apiresp.GinError(c, errs.ErrInternalServer.WithDetail(err.Error()).Wrap()) + return + } + + header["token"] = admin_output.Data.AdminToken + + url = "http://127.0.0.1:10008/user/find/public" + + search_input := chat.FindUserFullInfoReq{ + UserIDs: []string{"robotics"}, + } + + b, err = Post(c, url, header, search_input, 10) + if err != nil { + log.ZError(c, "CallbackExample unmarshal failed", err) + apiresp.GinError(c, errs.ErrInternalServer.WithDetail(err.Error()).Wrap()) + return + } + + type UserInfo struct { + ErrCode int `json:"errCode"` + ErrMsg string `json:"errMsg"` + ErrDlt string `json:"errDlt"` + Data chat.FindUserFullInfoResp `json:"data,omitempty"` + } + + search_output := &UserInfo{} + + if err = json.Unmarshal(b, search_output); err != nil { + log.ZError(c, "search_output unmarshal failed", err) + apiresp.GinError(c, errs.ErrInternalServer.WithDetail(err.Error()).Wrap()) + return + } + + if len(search_output.Data.Users) == 0 { + apiresp.GinError(c, errs.ErrRecordNotFound.Wrap("the robotics not found")) + return + } + + log.ZDebug(c, "callback", "searchUserAccount", search_output) + + text := apistruct.TextElem{} + picture := apistruct.PictureElem{} + mapStruct := make(map[string]any) + // Processing text messages + + if err != nil { + log.ZError(c, "CallbackExample get Sender failed", err) + apiresp.GinError(c, errs.ErrInternalServer.WithDetail(err.Error()).Wrap()) + return + } + + // Handle message structures + if req.ContentType == constant.Text { + err = json.Unmarshal([]byte(req.Content), &text) + if err != nil { + log.ZError(c, "CallbackExample unmarshal failed", err) + apiresp.GinError(c, errs.ErrInternalServer.WithDetail(err.Error()).Wrap()) + return + } + log.ZDebug(c, "callback", "text", text) + mapStruct["content"] = text.Content + } else { + err = json.Unmarshal([]byte(req.Content), &picture) + if err != nil { + log.ZError(c, "CallbackExample unmarshal failed", err) + apiresp.GinError(c, errs.ErrInternalServer.WithDetail(err.Error()).Wrap()) + return + } + log.ZDebug(c, "callback", "text", picture) + if strings.Contains(picture.SourcePicture.Type, "/") { + arr := strings.Split(picture.SourcePicture.Type, "/") + picture.SourcePicture.Type = arr[1] + } + + if strings.Contains(picture.BigPicture.Type, "/") { + arr := strings.Split(picture.BigPicture.Type, "/") + picture.BigPicture.Type = arr[1] + } + + if len(picture.SnapshotPicture.Type) == 0 { + picture.SnapshotPicture.Type = picture.SourcePicture.Type + } + + mapStructSnap := make(map[string]interface{}) + if mapStructSnap, err = convertStructToMap(picture.SnapshotPicture); err != nil { + log.ZError(c, "CallbackExample struct to map failed", err) + apiresp.GinError(c, err) + return + } + mapStruct["snapshotPicture"] = mapStructSnap + + mapStructBig := make(map[string]interface{}) + if mapStructBig, err = convertStructToMap(picture.BigPicture); err != nil { + log.ZError(c, "CallbackExample struct to map failed", err) + apiresp.GinError(c, err) + return + } + mapStruct["bigPicture"] = mapStructBig + + mapStructSource := make(map[string]interface{}) + if mapStructSource, err = convertStructToMap(picture.SourcePicture); err != nil { + log.ZError(c, "CallbackExample struct to map failed", err) + apiresp.GinError(c, err) + return + } + mapStruct["sourcePicture"] = mapStructSource + mapStruct["sourcePath"] = picture.SourcePath + } + + log.ZDebug(c, "callback", "mapStruct", mapStruct, "mapStructSnap") + header["token"] = admin_output.Data.ImToken + + input := &apistruct.SendMsgReq{ + RecvID: req.SendID, + SendMsg: apistruct.SendMsg{ + SendID: search_output.Data.Users[0].UserID, + SenderNickname: search_output.Data.Users[0].Nickname, + SenderFaceURL: search_output.Data.Users[0].FaceURL, + SenderPlatformID: req.SenderPlatformID, + Content: mapStruct, + ContentType: req.ContentType, + SessionType: req.SessionType, + SendTime: utils.GetCurrentTimestampByMill(), // millisecond + }, + } + + url = "http://127.0.0.1:10002/msg/send_msg" + + type sendResp struct { + ErrCode int `json:"errCode"` + ErrMsg string `json:"errMsg"` + ErrDlt string `json:"errDlt"` + Data msg.SendMsgResp `json:"data,omitempty"` + } + + output := &sendResp{} + + // Initiate a post request that calls the interface that sends the message (the bot sends a message to user) + b, err = Post(c, url, header, input, 10) + if err != nil { + log.ZError(c, "CallbackExample send message failed", err) + apiresp.GinError(c, errs.ErrInternalServer.WithDetail(err.Error()).Wrap()) + return + } + if err = json.Unmarshal(b, output); err != nil { + log.ZError(c, "CallbackExample unmarshal failed", err) + apiresp.GinError(c, errs.ErrInternalServer.WithDetail(err.Error()).Wrap()) + return + } + res := &msg.SendMsgResp{ + ServerMsgID: output.Data.ServerMsgID, + ClientMsgID: output.Data.ClientMsgID, + SendTime: output.Data.SendTime, + } + + apiresp.GinSuccess(c, res) +} + +// struct to map +func convertStructToMap(input interface{}) (map[string]interface{}, error) { + result := make(map[string]interface{}) + inputType := reflect.TypeOf(input) + + inputValue := reflect.ValueOf(input) + + if inputType.Kind() != reflect.Struct { + return nil, errs.ErrArgs.Wrap("input is not a struct") + } + + for i := 0; i < inputType.NumField(); i++ { + field := inputType.Field(i) + fieldValue := inputValue.Field(i) + + mapKey := field.Tag.Get("mapstructure") + fmt.Println(mapKey) + + if mapKey == "" { + mapKey = field.Name + } + + mapKey = strings.ToLower(mapKey) + + result[mapKey] = fieldValue.Interface() + } + + return result, nil +} + +func Post(ctx context.Context, url string, header map[string]string, data any, timeout int) (content []byte, err error) { + var ( + // define http client. + client = &http.Client{ + Timeout: 15 * time.Second, // max timeout is 15s + } + ) + + if timeout > 0 { + var cancel func() + ctx, cancel = context.WithTimeout(ctx, time.Second*time.Duration(timeout)) + defer cancel() + } + + jsonStr, err := json.Marshal(data) + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonStr)) + if err != nil { + return nil, err + } + + if operationID, _ := ctx.Value(constant.OperationID).(string); operationID != "" { + req.Header.Set(constant.OperationID, operationID) + } + for k, v := range header { + req.Header.Set(k, v) + } + req.Header.Add("content-type", "application/json; charset=utf-8") + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + result, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return result, nil +} diff --git a/internal/api/router.go b/internal/api/router.go index 1c3b7c1ec..deaa85143 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -56,6 +56,8 @@ func NewChatRoute(router gin.IRouter, discov discoveryregistry.SvcDiscoveryRegis router.Group("/callback").POST("/open_im", chat.OpenIMCallback) // Callback + router.Group("/callbackExample").POST("/callbackAfterSendSingleMsgCommand", chat.CallbackExample) + logs := router.Group("/logs", mw.CheckToken) logs.POST("/upload", chat.UploadLogs) } diff --git a/pkg/common/apistruct/chat.go b/pkg/common/apistruct/chat.go index 5d212a386..4496c230a 100644 --- a/pkg/common/apistruct/chat.go +++ b/pkg/common/apistruct/chat.go @@ -14,6 +14,8 @@ package apistruct +import "github.com/OpenIMSDK/protocol/sdkws" + type UserRegisterResp struct { ImToken string `json:"imToken"` ChatToken string `json:"chatToken"` @@ -27,3 +29,105 @@ type LoginResp struct { } type UpdateUserInfoResp struct{} + +type CallbackAfterSendSingleMsgReq struct { + CommonCallbackReq + RecvID string `json:"recvID"` +} + +type CommonCallbackReq struct { + SendID string `json:"sendID"` + CallbackCommand string `json:"callbackCommand"` + ServerMsgID string `json:"serverMsgID"` + ClientMsgID string `json:"clientMsgID"` + OperationID string `json:"operationID"` + SenderPlatformID int32 `json:"senderPlatformID"` + SenderNickname string `json:"senderNickname"` + SessionType int32 `json:"sessionType"` + MsgFrom int32 `json:"msgFrom"` + ContentType int32 `json:"contentType"` + Status int32 `json:"status"` + CreateTime int64 `json:"createTime"` + Content string `json:"content"` + Seq uint32 `json:"seq"` + AtUserIDList []string `json:"atUserList"` + SenderFaceURL string `json:"faceURL"` + Ex string `json:"ex"` +} + +type CallbackAfterSendSingleMsgResp struct { + CommonCallbackResp +} + +type CommonCallbackResp struct { + ActionCode int32 `json:"actionCode"` + ErrCode int32 `json:"errCode"` + ErrMsg string `json:"errMsg"` + ErrDlt string `json:"errDlt"` + NextCode int32 `json:"nextCode"` +} + +type TextElem struct { + Content string `json:"content" validate:"required"` +} + +type PictureElem struct { + SourcePath string `mapstructure:"sourcePath"` + SourcePicture PictureBaseInfo `mapstructure:"sourcePicture" validate:"required"` + BigPicture PictureBaseInfo `mapstructure:"bigPicture" validate:"required"` + SnapshotPicture PictureBaseInfo `mapstructure:"snapshotPicture" validate:"required"` +} + +type PictureBaseInfo struct { + UUID string `mapstructure:"uuid"` + Type string `mapstructure:"type" validate:"required"` + Size int64 `mapstructure:"size"` + Width int32 `mapstructure:"width" validate:"required"` + Height int32 `mapstructure:"height" validate:"required"` + Url string `mapstructure:"url" validate:"required"` +} + +type SendMsgReq struct { + // RecvID uniquely identifies the receiver and is required for one-on-one or notification chat types. + RecvID string `json:"recvID" binding:"required_if" message:"recvID is required if sessionType is SingleChatType or NotificationChatType"` + SendMsg +} + +// SendMsg defines the structure for sending messages with various metadata. +type SendMsg struct { + // SendID uniquely identifies the sender. + SendID string `json:"sendID" binding:"required"` + + // GroupID is the identifier for the group, required if SessionType is 2 or 3. + GroupID string `json:"groupID" binding:"required_if=SessionType 2|required_if=SessionType 3"` + + // SenderNickname is the nickname of the sender. + SenderNickname string `json:"senderNickname"` + + // SenderFaceURL is the URL to the sender's avatar. + SenderFaceURL string `json:"senderFaceURL"` + + // SenderPlatformID is an integer identifier for the sender's platform. + SenderPlatformID int32 `json:"senderPlatformID"` + + // Content is the actual content of the message, required and excluded from Swagger documentation. + Content map[string]any `json:"content" binding:"required" swaggerignore:"true"` + + // ContentType is an integer that represents the type of the content. + ContentType int32 `json:"contentType" binding:"required"` + + // SessionType is an integer that represents the type of session for the message. + SessionType int32 `json:"sessionType" binding:"required"` + + // IsOnlineOnly specifies if the message is only sent when the receiver is online. + IsOnlineOnly bool `json:"isOnlineOnly"` + + // NotOfflinePush specifies if the message should not trigger offline push notifications. + NotOfflinePush bool `json:"notOfflinePush"` + + // SendTime is a timestamp indicating when the message was sent. + SendTime int64 `json:"sendTime"` + + // OfflinePushInfo contains information for offline push notifications. + OfflinePushInfo *sdkws.OfflinePushInfo `json:"offlinePushInfo"` +} \ No newline at end of file