880 lines
24 KiB
Go
880 lines
24 KiB
Go
package imclient
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/aes"
|
|
"crypto/md5"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"91porn-server/common/crypt/ecb"
|
|
"91porn-server/common/log"
|
|
|
|
"github.com/moul/http2curl"
|
|
)
|
|
|
|
const (
|
|
MessageTypeText = 0
|
|
MessageTypeImage = 2
|
|
)
|
|
|
|
type Config struct {
|
|
Enable bool
|
|
BaseURL string
|
|
MerchantCode string
|
|
TenantCode string
|
|
AppKey string
|
|
ClientID string
|
|
ClientSecret string
|
|
SignKey string
|
|
AESKey string
|
|
EnableSign bool
|
|
EncryptTimestamp bool
|
|
OS string
|
|
OSType string
|
|
BusinessType string
|
|
Language string
|
|
TokenTTL int64
|
|
RequestTimeoutSeconds int
|
|
}
|
|
|
|
type Client struct {
|
|
cfg Config
|
|
httpClient *http.Client
|
|
}
|
|
|
|
type Response struct {
|
|
Code int `json:"code"`
|
|
Data json.RawMessage `json:"data"`
|
|
Msg string `json:"msg"`
|
|
Message string `json:"message"`
|
|
Result string `json:"result"`
|
|
}
|
|
|
|
type Attachment struct {
|
|
URL string `json:"url,omitempty"`
|
|
ThumbURL string `json:"thumbUrl,omitempty"`
|
|
Type string `json:"type,omitempty"`
|
|
Size int64 `json:"size,omitempty"`
|
|
Width int `json:"width,omitempty"`
|
|
Height int `json:"height,omitempty"`
|
|
Length int64 `json:"length,omitempty"`
|
|
FileName string `json:"fileName,omitempty"`
|
|
}
|
|
|
|
type AppTokenRequest struct {
|
|
ClientID string `json:"clientId"`
|
|
ClientSecret string `json:"clientSecret"`
|
|
OSType string `json:"osType"`
|
|
TTL int64 `json:"ttl,omitempty"`
|
|
}
|
|
|
|
type UserTokenRequest struct {
|
|
ClientID string `json:"clientId"`
|
|
ClientSecret string `json:"clientSecret"`
|
|
UserID int64 `json:"userId"`
|
|
OSType string `json:"osType"`
|
|
TTL int64 `json:"ttl,omitempty"`
|
|
}
|
|
|
|
type RegisterRequest struct {
|
|
ThirdPartyID string `json:"thirdPartyId"`
|
|
Password string `json:"password"`
|
|
Nickname string `json:"nickname,omitempty"`
|
|
Avatar string `json:"avatar,omitempty"`
|
|
}
|
|
|
|
type UpdateBaseInfoRequest struct {
|
|
UserID int64 `json:"userId"`
|
|
Name string `json:"name,omitempty"`
|
|
ImgURL string `json:"imgUrl,omitempty"`
|
|
Signature string `json:"signature,omitempty"`
|
|
}
|
|
|
|
type UserBaseInfo struct {
|
|
ID int64 `json:"id"`
|
|
UserPhone string `json:"userPhone"`
|
|
Name string `json:"name"`
|
|
Sex int `json:"sex"`
|
|
ImgURL string `json:"imgUrl"`
|
|
Enable int `json:"enable"`
|
|
CreateTime int64 `json:"createTime"`
|
|
Signature string `json:"signature"`
|
|
}
|
|
|
|
type UpdatePasswordRequest struct {
|
|
UserID int64 `json:"userId"`
|
|
Pwd string `json:"pwd"`
|
|
Code string `json:"code,omitempty"`
|
|
}
|
|
|
|
type UpdateHeadImgRequest struct {
|
|
UserID int64 `json:"userId"`
|
|
ImgURL string `json:"imgUrl"`
|
|
}
|
|
|
|
type SetOnlineStatusRequest struct {
|
|
UserID int64 `json:"userId"`
|
|
ShowOnlineStatus bool `json:"showOnlineStatus"`
|
|
}
|
|
|
|
type BatchOnlineStatusRequest struct {
|
|
UserIDs []int64 `json:"userIds"`
|
|
}
|
|
|
|
type OnlineStatus struct {
|
|
UserID int64 `json:"userId"`
|
|
UserName string `json:"userName"`
|
|
Visible bool `json:"visible"`
|
|
Online bool `json:"online"`
|
|
}
|
|
|
|
type Friend struct {
|
|
FriendID int64 `json:"friendId"`
|
|
Remark string `json:"remark"`
|
|
CreatedAt int64 `json:"createdAt"`
|
|
UpdatedAt int64 `json:"updatedAt"`
|
|
Status int `json:"status"`
|
|
}
|
|
|
|
type AddFriendRequest struct {
|
|
UserID int64 `json:"userId"`
|
|
FriendID int64 `json:"friendId"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
type DirectAddFriendRequest struct {
|
|
UserID int64 `json:"userId"`
|
|
FriendID int64 `json:"friendId"`
|
|
Message string `json:"message,omitempty"`
|
|
Archive bool `json:"archive,omitempty"`
|
|
}
|
|
|
|
type HandleFriendRequest struct {
|
|
RequestID int64 `json:"requestId"`
|
|
UserID int64 `json:"userId"`
|
|
}
|
|
|
|
type PendingFriendRequest struct {
|
|
ID int64 `json:"id"`
|
|
FriendID int64 `json:"friendId"`
|
|
Message string `json:"message"`
|
|
Status int `json:"status"`
|
|
CreatedAt int64 `json:"createdAt"`
|
|
UpdatedAt int64 `json:"updatedAt"`
|
|
HandleTime int64 `json:"handleTime"`
|
|
ExpireTime int64 `json:"expireTime"`
|
|
}
|
|
|
|
type AvailableFriend struct {
|
|
UserID int64 `json:"userId"`
|
|
FriendID int64 `json:"friendId"`
|
|
ConvID string `json:"convId"`
|
|
Remark string `json:"remark"`
|
|
CreatedAt int64 `json:"createdAt"`
|
|
UpdatedAt int64 `json:"updatedAt"`
|
|
DisturbStatus int `json:"disturbStatus"`
|
|
Archive int `json:"archive"`
|
|
}
|
|
|
|
type SetFriendArchiveRequest struct {
|
|
UserID int64 `json:"userId"`
|
|
FriendID int64 `json:"friendId"`
|
|
Archive bool `json:"archive"`
|
|
}
|
|
|
|
type SendMessageRequest struct {
|
|
SenderID int64 `json:"senderId"`
|
|
ReceiverID int64 `json:"receiverId"`
|
|
Content string `json:"content"`
|
|
MessageType int `json:"messageType"`
|
|
ExtInfo string `json:"extInfo,omitempty"`
|
|
ChannelType string `json:"channelType,omitempty"`
|
|
Attachment *Attachment `json:"attachment,omitempty"`
|
|
}
|
|
|
|
type BatchSendMessageRequest struct {
|
|
SenderID int64 `json:"senderId"`
|
|
ReceiverIDSet []int64 `json:"receiverIdSet"`
|
|
Content string `json:"content"`
|
|
MessageType int `json:"messageType"`
|
|
ExtInfo string `json:"extInfo,omitempty"`
|
|
ChannelType string `json:"channelType,omitempty"`
|
|
Attachment *Attachment `json:"attachment,omitempty"`
|
|
}
|
|
|
|
type OnlinePassthroughRequest struct {
|
|
SenderID int64 `json:"senderId,omitempty"`
|
|
ReceiverIDSet []int64 `json:"receiverIdSet"`
|
|
PassthroughType string `json:"passthroughType,omitempty"`
|
|
Content string `json:"content,omitempty"`
|
|
ExtInfo string `json:"extInfo,omitempty"`
|
|
ChannelType string `json:"channelType,omitempty"`
|
|
}
|
|
|
|
type AppPassthroughRequest struct {
|
|
SenderID int64 `json:"senderId,omitempty"`
|
|
PassthroughType string `json:"passthroughType,omitempty"`
|
|
Content string `json:"content,omitempty"`
|
|
ExtInfo string `json:"extInfo,omitempty"`
|
|
ChannelType string `json:"channelType,omitempty"`
|
|
}
|
|
|
|
type PassthroughResult struct {
|
|
MessageID string `json:"messageId"`
|
|
MessageType int `json:"messageType"`
|
|
ReceiverCount int `json:"receiverCount"`
|
|
CreatedAt int64 `json:"createdAt"`
|
|
}
|
|
|
|
type MessageSendResult struct {
|
|
MessageID string `json:"messageId"`
|
|
ConvID string `json:"convId"`
|
|
Seq int64 `json:"seq"`
|
|
CreatedAt int64 `json:"createdAt"`
|
|
}
|
|
|
|
type HistoryMessageRequest struct {
|
|
UserID1 int64 `json:"userId1"`
|
|
UserID2 int64 `json:"userId2"`
|
|
StartTime int64 `json:"startTime,omitempty"`
|
|
EndTime int64 `json:"endTime,omitempty"`
|
|
StartSeq int64 `json:"startSeq,omitempty"`
|
|
EndSeq int64 `json:"endSeq,omitempty"`
|
|
Direction string `json:"direction,omitempty"`
|
|
Size int `json:"size,omitempty"`
|
|
}
|
|
|
|
type MessageDetail struct {
|
|
MessageID string `json:"messageId"`
|
|
SenderID int64 `json:"senderId"`
|
|
ReceiverID int64 `json:"receiverId"`
|
|
Content string `json:"content"`
|
|
MessageType string `json:"messageType"`
|
|
Seq int64 `json:"seq"`
|
|
CreatedAt int64 `json:"createdAt"`
|
|
}
|
|
|
|
type ModifyMessageRequest struct {
|
|
MessageID string `json:"messageId"`
|
|
ConvID string `json:"convId"`
|
|
SenderID int64 `json:"senderId"`
|
|
ReceiverID int64 `json:"receiverId"`
|
|
ModifierID int64 `json:"modifierId"`
|
|
Content string `json:"content,omitempty"`
|
|
ExtInfo string `json:"extInfo,omitempty"`
|
|
}
|
|
|
|
type HideInboxRequest struct {
|
|
UserID int64 `json:"userId"`
|
|
ConvID string `json:"convId"`
|
|
StartSeq int64 `json:"startSeq"`
|
|
EndSeq int64 `json:"endSeq"`
|
|
}
|
|
|
|
func New(cfg Config) *Client {
|
|
timeout := cfg.RequestTimeoutSeconds
|
|
if timeout <= 0 {
|
|
timeout = 10
|
|
}
|
|
return &Client{
|
|
cfg: cfg,
|
|
httpClient: &http.Client{
|
|
Timeout: time.Duration(timeout) * time.Second,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (c *Client) Enabled() bool {
|
|
return c.DisabledReason() == ""
|
|
}
|
|
|
|
// DisabledReason 返回 SDK 不可用的具体原因(未开启 / 缺失的配置项);可用时返回 ""。
|
|
func (c *Client) DisabledReason() string {
|
|
if c == nil {
|
|
return "im client is nil"
|
|
}
|
|
if !c.cfg.Enable {
|
|
return "config Enable=false"
|
|
}
|
|
var missing []string
|
|
if c.cfg.BaseURL == "" {
|
|
missing = append(missing, "BaseURL")
|
|
}
|
|
if c.cfg.MerchantCode == "" {
|
|
missing = append(missing, "MerchantCode")
|
|
}
|
|
if c.tenantCode() == "" {
|
|
missing = append(missing, "TenantCode/AppKey")
|
|
}
|
|
if len(missing) > 0 {
|
|
missingStr := "missing config: " + strings.Join(missing, ", ")
|
|
log.Error(missingStr)
|
|
return missingStr
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (c *Client) AppToken() (string, error) {
|
|
req := AppTokenRequest{
|
|
ClientID: c.cfg.ClientID,
|
|
ClientSecret: c.cfg.ClientSecret,
|
|
OSType: c.osType(),
|
|
TTL: c.tokenTTL(),
|
|
}
|
|
return c.postString("/authenticate/token.e", "", req)
|
|
}
|
|
|
|
func (c *Client) UserToken(userID int64) (string, error) {
|
|
req := UserTokenRequest{
|
|
ClientID: c.cfg.ClientID,
|
|
ClientSecret: c.cfg.ClientSecret,
|
|
UserID: userID,
|
|
OSType: c.osType(),
|
|
TTL: c.tokenTTL(),
|
|
}
|
|
return c.postString("/authenticate/user/token.e", "", req)
|
|
}
|
|
|
|
func (c *Client) Register(req RegisterRequest, token string) (int64, error) {
|
|
var userID int64
|
|
if err := c.postData("/user/register", token, req, &userID); err != nil {
|
|
return 0, err
|
|
}
|
|
return userID, nil
|
|
}
|
|
|
|
func (c *Client) UpdateBaseInfo(req UpdateBaseInfoRequest, token string) error {
|
|
var ok bool
|
|
return c.postData("/user/updateBaseInfo", token, req, &ok)
|
|
}
|
|
|
|
func (c *Client) UserBaseInfo(userID int64, token string) (*UserBaseInfo, error) {
|
|
query := url.Values{}
|
|
query.Set("userId", strconv.FormatInt(userID, 10))
|
|
var info UserBaseInfo
|
|
if err := c.getData("/user/baseUserInfo", token, query, nil, &info); err != nil {
|
|
return nil, err
|
|
}
|
|
return &info, nil
|
|
}
|
|
|
|
func (c *Client) UpdatePassword(req UpdatePasswordRequest, token string) error {
|
|
return c.postData("/user/updatePwd", token, req, nil)
|
|
}
|
|
|
|
func (c *Client) UpdateHeadImg(req UpdateHeadImgRequest, token string) error {
|
|
var ok bool
|
|
return c.postData("/user/updateHeadImg", token, req, &ok)
|
|
}
|
|
|
|
func (c *Client) SetOnlineStatus(req SetOnlineStatusRequest, token string) error {
|
|
var ok bool
|
|
return c.postData("/user/onlineStatus/set", token, req, &ok)
|
|
}
|
|
|
|
func (c *Client) BatchOnlineStatus(req BatchOnlineStatusRequest, token string) ([]OnlineStatus, error) {
|
|
var statuses []OnlineStatus
|
|
if err := c.postData("/user/onlineStatus/batch", token, req, &statuses); err != nil {
|
|
return nil, err
|
|
}
|
|
return statuses, nil
|
|
}
|
|
|
|
func (c *Client) FriendList(userID, now int64, token string) ([]Friend, error) {
|
|
query := url.Values{}
|
|
query.Set("userId", strconv.FormatInt(userID, 10))
|
|
if now > 0 {
|
|
query.Set("now", strconv.FormatInt(now, 10))
|
|
}
|
|
headers := map[string]string{
|
|
"userId": strconv.FormatInt(userID, 10),
|
|
}
|
|
var friends []Friend
|
|
if err := c.getData("/friend/list", token, query, headers, &friends); err != nil {
|
|
return nil, err
|
|
}
|
|
return friends, nil
|
|
}
|
|
|
|
func (c *Client) AddFriend(req AddFriendRequest, token string) (int64, error) {
|
|
var requestID int64
|
|
if err := c.postData("/friend/add", token, req, &requestID); err != nil {
|
|
return 0, err
|
|
}
|
|
return requestID, nil
|
|
}
|
|
|
|
func (c *Client) DirectAddFriend(req DirectAddFriendRequest, token string) error {
|
|
var ok bool
|
|
return c.postData("/friend/add/direct", token, req, &ok)
|
|
}
|
|
|
|
func (c *Client) ConfirmFriend(req HandleFriendRequest, token string) error {
|
|
var ok bool
|
|
return c.postData("/friend/confirm", token, req, &ok)
|
|
}
|
|
|
|
func (c *Client) RejectFriend(req HandleFriendRequest, token string) error {
|
|
var ok bool
|
|
return c.postData("/friend/reject", token, req, &ok)
|
|
}
|
|
|
|
func (c *Client) DeleteFriend(userID, friendID int64, token string) error {
|
|
query := url.Values{}
|
|
query.Set("userId", strconv.FormatInt(userID, 10))
|
|
query.Set("friendId", strconv.FormatInt(friendID, 10))
|
|
var ok bool
|
|
return c.getData("/friend/delete", token, query, nil, &ok)
|
|
}
|
|
|
|
func (c *Client) DeleteAllFriends(userID int64, token string) error {
|
|
query := url.Values{}
|
|
query.Set("userId", strconv.FormatInt(userID, 10))
|
|
var ok bool
|
|
return c.getData("/allFriend/delete", token, query, nil, &ok)
|
|
}
|
|
|
|
func (c *Client) PendingFriendRequests(userID, now int64, token string) ([]PendingFriendRequest, error) {
|
|
query := url.Values{}
|
|
query.Set("userId", strconv.FormatInt(userID, 10))
|
|
if now > 0 {
|
|
query.Set("now", strconv.FormatInt(now, 10))
|
|
}
|
|
var requests []PendingFriendRequest
|
|
if err := c.getData("/friend/pending", token, query, nil, &requests); err != nil {
|
|
return nil, err
|
|
}
|
|
return requests, nil
|
|
}
|
|
|
|
func (c *Client) AvailableFriends(userID int64, token string) ([]AvailableFriend, error) {
|
|
query := url.Values{}
|
|
query.Set("userId", strconv.FormatInt(userID, 10))
|
|
var friends []AvailableFriend
|
|
if err := c.getData("/friend/available/list", token, query, nil, &friends); err != nil {
|
|
return nil, err
|
|
}
|
|
return friends, nil
|
|
}
|
|
|
|
func (c *Client) SetFriendArchive(req SetFriendArchiveRequest, token string) error {
|
|
var ok bool
|
|
return c.postData("/friend/archive", token, req, &ok)
|
|
}
|
|
|
|
func (c *Client) SendMessage(req SendMessageRequest, token string) error {
|
|
_, err := c.SendMessageWithResult(req, token)
|
|
return err
|
|
}
|
|
|
|
func (c *Client) SendMessageWithResult(req SendMessageRequest, token string) (*MessageSendResult, error) {
|
|
if req.ExtInfo == "" {
|
|
req.ExtInfo = "{}"
|
|
}
|
|
if req.ChannelType == "" {
|
|
req.ChannelType = "UNKNOW"
|
|
}
|
|
var result MessageSendResult
|
|
if err := c.postData("/message/send", token, req, &result); err != nil {
|
|
return nil, err
|
|
}
|
|
return &result, nil
|
|
}
|
|
|
|
func (c *Client) BatchSendMessage(req BatchSendMessageRequest, token string) error {
|
|
_, err := c.BatchSendMessageWithResult(req, token)
|
|
return err
|
|
}
|
|
|
|
func (c *Client) BatchSendMessageWithResult(req BatchSendMessageRequest, token string) (json.RawMessage, error) {
|
|
if req.ExtInfo == "" {
|
|
req.ExtInfo = "{}"
|
|
}
|
|
if req.ChannelType == "" {
|
|
req.ChannelType = "UNKNOW"
|
|
}
|
|
var data json.RawMessage
|
|
if err := c.postData("/message/send/batch", token, req, &data); err != nil {
|
|
return nil, err
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
func (c *Client) HistoryMessages(req HistoryMessageRequest, token string) ([]MessageDetail, error) {
|
|
var messages []MessageDetail
|
|
if err := c.postData("/message/history", token, req, &messages); err != nil {
|
|
return nil, err
|
|
}
|
|
return messages, nil
|
|
}
|
|
|
|
func (c *Client) MessageDetail(messageID, convID, token string) (*MessageDetail, error) {
|
|
query := url.Values{}
|
|
query.Set("messageId", messageID)
|
|
query.Set("convId", convID)
|
|
var detail MessageDetail
|
|
if err := c.getData("/message/detail", token, query, nil, &detail); err != nil {
|
|
return nil, err
|
|
}
|
|
return &detail, nil
|
|
}
|
|
|
|
func (c *Client) ModifyMessage(req ModifyMessageRequest, token string) (string, error) {
|
|
var messageID string
|
|
if err := c.postData("/message/modify", token, req, &messageID); err != nil {
|
|
return "", err
|
|
}
|
|
return messageID, nil
|
|
}
|
|
|
|
func (c *Client) HideInbox(req HideInboxRequest, token string) (int64, error) {
|
|
var count int64
|
|
if err := c.postData("/message/inbox/hide", token, req, &count); err != nil {
|
|
return 0, err
|
|
}
|
|
return count, nil
|
|
}
|
|
|
|
func (c *Client) SendOnlinePassthrough(req OnlinePassthroughRequest, token string) (*PassthroughResult, error) {
|
|
if len(req.ReceiverIDSet) == 0 {
|
|
return nil, errors.New("receiverIdSet is empty")
|
|
}
|
|
normalizePassthrough(&req.PassthroughType, &req.ExtInfo, &req.ChannelType)
|
|
var result PassthroughResult
|
|
if err := c.postData("/message/passthrough/send", token, req, &result); err != nil {
|
|
return nil, err
|
|
}
|
|
return &result, nil
|
|
}
|
|
|
|
func (c *Client) SendAppPassthrough(req AppPassthroughRequest, token string) (*PassthroughResult, error) {
|
|
normalizePassthrough(&req.PassthroughType, &req.ExtInfo, &req.ChannelType)
|
|
var result PassthroughResult
|
|
if err := c.postData("/message/passthrough/send/batch/all", token, req, &result); err != nil {
|
|
return nil, err
|
|
}
|
|
return &result, nil
|
|
}
|
|
|
|
func normalizePassthrough(passthroughType, extInfo, channelType *string) {
|
|
if *passthroughType == "" {
|
|
*passthroughType = "AD_NOTIFY"
|
|
}
|
|
if *extInfo == "" {
|
|
*extInfo = "{}"
|
|
}
|
|
if *channelType == "" {
|
|
*channelType = "UNKNOW"
|
|
}
|
|
}
|
|
|
|
func marshalRequestBody(data any) ([]byte, error) {
|
|
switch req := data.(type) {
|
|
case AppTokenRequest:
|
|
return marshalOrderedJSON(map[string]any{
|
|
"clientId": req.ClientID,
|
|
"clientSecret": req.ClientSecret,
|
|
"osType": req.OSType,
|
|
"ttl": req.TTL,
|
|
})
|
|
case UserTokenRequest:
|
|
return marshalOrderedJSON(map[string]any{
|
|
"clientId": req.ClientID,
|
|
"clientSecret": req.ClientSecret,
|
|
"osType": req.OSType,
|
|
"ttl": req.TTL,
|
|
"userId": req.UserID,
|
|
})
|
|
case RegisterRequest:
|
|
fields := map[string]any{
|
|
"password": req.Password,
|
|
"thirdPartyId": req.ThirdPartyID,
|
|
}
|
|
if req.Nickname != "" {
|
|
fields["nickname"] = req.Nickname
|
|
}
|
|
if req.Avatar != "" {
|
|
fields["avatar"] = req.Avatar
|
|
}
|
|
return marshalOrderedJSON(fields)
|
|
default:
|
|
return json.Marshal(data)
|
|
}
|
|
}
|
|
|
|
func marshalOrderedJSON(fields map[string]any) ([]byte, error) {
|
|
keys := make([]string, 0, len(fields))
|
|
for key, value := range fields {
|
|
if value == nil {
|
|
continue
|
|
}
|
|
if s, ok := value.(string); ok && s == "" {
|
|
continue
|
|
}
|
|
if n, ok := value.(int64); ok && n <= 0 {
|
|
continue
|
|
}
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
var buf bytes.Buffer
|
|
buf.WriteByte('{')
|
|
for i, key := range keys {
|
|
if i > 0 {
|
|
buf.WriteByte(',')
|
|
}
|
|
keyJSON, err := json.Marshal(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
valueJSON, err := json.Marshal(fields[key])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
buf.Write(keyJSON)
|
|
buf.WriteByte(':')
|
|
buf.Write(valueJSON)
|
|
}
|
|
buf.WriteByte('}')
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func (c *Client) postString(path, token string, data any) (string, error) {
|
|
var out string
|
|
if err := c.postData(path, token, data, &out); err != nil {
|
|
return "", err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (c *Client) postData(path, token string, data any, out any) error {
|
|
if !c.Enabled() {
|
|
return errors.New("im client is disabled")
|
|
}
|
|
body, err := marshalRequestBody(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp, err := c.do(http.MethodPost, path, token, nil, nil, body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return decodeResponseData(resp, out)
|
|
}
|
|
|
|
func (c *Client) getData(path, token string, query url.Values, headers map[string]string, out any) error {
|
|
if !c.Enabled() {
|
|
return errors.New("im client is disabled")
|
|
}
|
|
resp, err := c.do(http.MethodGet, path, token, query, headers, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return decodeResponseData(resp, out)
|
|
}
|
|
|
|
func decodeResponseData(resp *Response, out any) error {
|
|
if out == nil || len(resp.Data) == 0 || bytes.Equal(resp.Data, []byte("null")) {
|
|
return nil
|
|
}
|
|
return json.Unmarshal(resp.Data, out)
|
|
}
|
|
|
|
func (c *Client) do(method, path, token string, query url.Values, headers map[string]string, body []byte) (*Response, error) {
|
|
fullURL, queryString := c.endpoint(path, query)
|
|
req, err := http.NewRequest(method, fullURL, bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Accept", "application/json")
|
|
req.Header.Set("X-Merchant-Code", c.cfg.MerchantCode)
|
|
req.Header.Set("X-App-Key", c.appKey())
|
|
req.Header.Set("X-Client-Id", c.cfg.ClientID)
|
|
if token != "" {
|
|
req.Header.Set("token", token)
|
|
}
|
|
for k, v := range headers {
|
|
req.Header.Set(k, v)
|
|
}
|
|
for k, v := range c.baseHeaders(queryString, body) {
|
|
req.Header.Set(k, v)
|
|
}
|
|
start := time.Now()
|
|
httpResp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
log.Warn("im sdk request error", log.Any("method", method), log.Any("path", path), log.Any("costMs", time.Since(start).Milliseconds()), log.Any("curl", requestCurl(req, body)), log.E(err))
|
|
return nil, err
|
|
}
|
|
defer httpResp.Body.Close()
|
|
respBody, err := io.ReadAll(httpResp.Body)
|
|
cost := time.Since(start)
|
|
if err != nil {
|
|
log.Warn("im sdk read body error", log.Any("method", method), log.Any("path", path), log.Any("costMs", cost.Milliseconds()), log.E(err))
|
|
return nil, err
|
|
}
|
|
log.Info("im sdk request", log.Any("method", method), log.Any("path", path), log.Any("status", httpResp.StatusCode), log.Any("costMs", cost.Milliseconds()))
|
|
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
|
log.Warn("im sdk http status error", log.Any("method", method), log.Any("path", path), log.Any("status", httpResp.StatusCode), log.Any("curl", requestCurl(req, body)), log.Any("resp", string(respBody)))
|
|
return nil, apiErrorFromResponse(httpResp.StatusCode, respBody)
|
|
}
|
|
var resp Response
|
|
if err = json.Unmarshal(respBody, &resp); err != nil {
|
|
log.Warn("im sdk resp unmarshal error", log.Any("method", method), log.Any("path", path), log.Any("curl", requestCurl(req, body)), log.Any("resp", string(respBody)), log.E(err))
|
|
return nil, err
|
|
}
|
|
if resp.Code != 0 {
|
|
log.Warn("im sdk api error", log.Any("method", method), log.Any("path", path), log.Any("code", resp.Code), log.Any("curl", requestCurl(req, body)), log.Any("resp", string(respBody)))
|
|
return nil, &APIError{
|
|
StatusCode: httpResp.StatusCode,
|
|
Code: resp.Code,
|
|
Message: firstNonEmpty(resp.Message, resp.Msg, resp.Result),
|
|
Body: respBody,
|
|
}
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// requestCurl 把请求还原成 curl 命令字符串,用于报错时打印复现。
|
|
// body 为请求体原始字节:Do 已消费 req.Body,这里用 body 重新填充再生成。
|
|
func requestCurl(req *http.Request, body []byte) string {
|
|
req.Body = io.NopCloser(bytes.NewReader(body))
|
|
cmd, err := http2curl.GetCurlCommand(req)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return cmd.String()
|
|
}
|
|
|
|
func apiErrorFromResponse(statusCode int, body []byte) *APIError {
|
|
resp := Response{}
|
|
_ = json.Unmarshal(body, &resp)
|
|
return &APIError{
|
|
StatusCode: statusCode,
|
|
Code: resp.Code,
|
|
Message: firstNonEmpty(resp.Message, resp.Msg, resp.Result, string(body)),
|
|
Body: body,
|
|
}
|
|
}
|
|
|
|
func (c *Client) endpoint(path string, query url.Values) (string, string) {
|
|
base := strings.TrimRight(c.cfg.BaseURL, "/")
|
|
path = strings.TrimLeft(path, "/")
|
|
u := fmt.Sprintf("%s/api/endpoint/%s/%s/%s", base, url.PathEscape(c.cfg.MerchantCode), url.PathEscape(c.tenantCode()), path)
|
|
if len(query) == 0 {
|
|
return u, ""
|
|
}
|
|
queryString := query.Encode()
|
|
return u + "?" + queryString, queryString
|
|
}
|
|
|
|
func (c *Client) baseHeaders(queryString string, body []byte) map[string]string {
|
|
headers := map[string]string{
|
|
"os": c.os(),
|
|
"osType": c.osType(),
|
|
"businessType": c.businessType(),
|
|
"language": c.language(),
|
|
}
|
|
if !c.cfg.EnableSign || c.cfg.SignKey == "" {
|
|
return headers
|
|
}
|
|
timestamps := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
|
headers["timestamps"] = timestamps
|
|
signTimestamp := timestamps
|
|
if c.cfg.EncryptTimestamp && c.cfg.AESKey != "" {
|
|
if encrypted := encryptTimestamp(timestamps, c.cfg.AESKey); encrypted != "" {
|
|
headers["envTimestamps"] = encrypted
|
|
}
|
|
}
|
|
signBody := queryString
|
|
if len(body) > 0 {
|
|
signBody += string(body)
|
|
}
|
|
sum := md5.Sum([]byte(signBody + signTimestamp + c.cfg.SignKey))
|
|
headers["sign"] = hex.EncodeToString(sum[:])
|
|
return headers
|
|
}
|
|
|
|
func (c *Client) os() string {
|
|
if c.cfg.OS != "" {
|
|
return c.cfg.OS
|
|
}
|
|
return "web"
|
|
}
|
|
|
|
func (c *Client) osType() string {
|
|
if c.cfg.OSType != "" {
|
|
return c.cfg.OSType
|
|
}
|
|
return "web"
|
|
}
|
|
|
|
func (c *Client) businessType() string {
|
|
if c.cfg.BusinessType != "" {
|
|
return c.cfg.BusinessType
|
|
}
|
|
return "im_sdk_customer"
|
|
}
|
|
|
|
func (c *Client) language() string {
|
|
if c.cfg.Language != "" {
|
|
return c.cfg.Language
|
|
}
|
|
return "zh-cn"
|
|
}
|
|
|
|
func (c *Client) tokenTTL() int64 {
|
|
if c.cfg.TokenTTL > 0 {
|
|
return c.cfg.TokenTTL
|
|
}
|
|
return 86400
|
|
}
|
|
|
|
func (c *Client) appKey() string {
|
|
if c.cfg.AppKey != "" {
|
|
return c.cfg.AppKey
|
|
}
|
|
if c.cfg.MerchantCode == "" || c.cfg.TenantCode == "" {
|
|
return ""
|
|
}
|
|
return c.cfg.MerchantCode + "#" + c.cfg.TenantCode
|
|
}
|
|
|
|
func (c *Client) tenantCode() string {
|
|
if c.cfg.TenantCode != "" {
|
|
return c.cfg.TenantCode
|
|
}
|
|
return c.cfg.AppKey
|
|
}
|
|
|
|
func encryptTimestamp(timestamps, key string) string {
|
|
block, err := aes.NewCipher([]byte(key))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
src := zeroPadding([]byte(timestamps), block.BlockSize())
|
|
dst := make([]byte, len(src))
|
|
ecb.NewECBEncrypter(block).CryptBlocks(dst, src)
|
|
return base64.StdEncoding.EncodeToString(dst)
|
|
}
|
|
|
|
func zeroPadding(src []byte, blockSize int) []byte {
|
|
padding := blockSize - len(src)%blockSize
|
|
if padding == blockSize {
|
|
padding = blockSize
|
|
}
|
|
return append(src, bytes.Repeat([]byte{0}, padding)...)
|
|
}
|