@@ -0,0 +1,879 @@
|
||||
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)...)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package imclient
|
||||
|
||||
const (
|
||||
DefaultSignKey = "shDOUArrDhpeAMw9FGY79Zmy3MLWwNWy"
|
||||
DefaultAESKey = "4d5bc50346c22dde12be2c3b1b89ada6"
|
||||
)
|
||||
@@ -0,0 +1,41 @@
|
||||
package imclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const sessionExpiredCode = 401
|
||||
|
||||
type APIError struct {
|
||||
StatusCode int
|
||||
Code int
|
||||
Message string
|
||||
Body []byte
|
||||
}
|
||||
|
||||
func (e *APIError) Error() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
if e.Code != 0 {
|
||||
return fmt.Sprintf("im api error: status=%d code=%d message=%s", e.StatusCode, e.Code, e.Message)
|
||||
}
|
||||
return fmt.Sprintf("im api error: status=%d message=%s", e.StatusCode, e.Message)
|
||||
}
|
||||
|
||||
func IsSessionExpired(err error) bool {
|
||||
apiErr, ok := err.(*APIError)
|
||||
if !ok || apiErr == nil {
|
||||
return false
|
||||
}
|
||||
if apiErr.Code == sessionExpiredCode || apiErr.StatusCode == http.StatusUnauthorized {
|
||||
return true
|
||||
}
|
||||
msg := strings.ToLower(apiErr.Message)
|
||||
return strings.Contains(msg, "session has expired") ||
|
||||
strings.Contains(msg, "log in again") ||
|
||||
strings.Contains(msg, "token expired") ||
|
||||
strings.Contains(msg, "token invalid")
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
package imclient
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protowire"
|
||||
)
|
||||
|
||||
type historyMessageWire struct {
|
||||
MessageID json.RawMessage `json:"messageId"`
|
||||
ID json.RawMessage `json:"id"`
|
||||
MsgID json.RawMessage `json:"msgId"`
|
||||
MID json.RawMessage `json:"mid"`
|
||||
SenderID int64 `json:"senderId"`
|
||||
ReceiverID int64 `json:"receiverId"`
|
||||
Content string `json:"content"`
|
||||
Text string `json:"text"`
|
||||
Body string `json:"body"`
|
||||
MsgContent string `json:"msgContent"`
|
||||
MessageContent string `json:"messageContent"`
|
||||
Payload json.RawMessage `json:"payload"`
|
||||
ExtInfo json.RawMessage `json:"extInfo"`
|
||||
Attachment json.RawMessage `json:"attachment"`
|
||||
Message json.RawMessage `json:"message"`
|
||||
MessageType json.RawMessage `json:"messageType"`
|
||||
Seq int64 `json:"seq"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
}
|
||||
|
||||
func (m *MessageDetail) UnmarshalJSON(data []byte) error {
|
||||
var wire historyMessageWire
|
||||
if err := json.Unmarshal(data, &wire); err != nil {
|
||||
return err
|
||||
}
|
||||
m.SenderID = wire.SenderID
|
||||
m.ReceiverID = wire.ReceiverID
|
||||
m.Seq = wire.Seq
|
||||
m.CreatedAt = wire.CreatedAt
|
||||
m.MessageID = firstNonEmpty(
|
||||
jsonScalarString(wire.MessageID),
|
||||
jsonScalarString(wire.MsgID),
|
||||
jsonScalarString(wire.MID),
|
||||
jsonScalarString(wire.ID),
|
||||
)
|
||||
m.MessageType = normalizeHistoryMessageType(wire.MessageType)
|
||||
if m.MessageType == "" {
|
||||
m.MessageType = extractPayloadMessageType(wire.Payload)
|
||||
}
|
||||
m.Content = firstNonEmpty(
|
||||
strings.TrimSpace(wire.Content),
|
||||
strings.TrimSpace(wire.Text),
|
||||
strings.TrimSpace(wire.Body),
|
||||
strings.TrimSpace(wire.MsgContent),
|
||||
strings.TrimSpace(wire.MessageContent),
|
||||
extractPayloadContent(wire.Payload, m.MessageID, strconv.FormatInt(m.SenderID, 10), strconv.FormatInt(m.ReceiverID, 10)),
|
||||
extractPayloadContent(wire.ExtInfo, m.MessageID, strconv.FormatInt(m.SenderID, 10), strconv.FormatInt(m.ReceiverID, 10)),
|
||||
extractAttachmentContent(wire.Attachment),
|
||||
)
|
||||
if nested := decodeHistoryMessageWire(wire.Message); nested != nil {
|
||||
if m.MessageID == "" {
|
||||
m.MessageID = nested.messageID
|
||||
}
|
||||
if m.Content == "" {
|
||||
m.Content = nested.content
|
||||
}
|
||||
if m.MessageType == "" {
|
||||
m.MessageType = nested.messageType
|
||||
}
|
||||
}
|
||||
if m.MessageID == "" && m.Seq > 0 {
|
||||
m.MessageID = strconv.FormatInt(m.Seq, 10)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type decodedHistoryMessage struct {
|
||||
messageID string
|
||||
content string
|
||||
messageType string
|
||||
}
|
||||
|
||||
func decodeHistoryMessageWire(raw json.RawMessage) *decodedHistoryMessage {
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return nil
|
||||
}
|
||||
var nested historyMessageWire
|
||||
if err := json.Unmarshal(raw, &nested); err != nil {
|
||||
return nil
|
||||
}
|
||||
msg := &decodedHistoryMessage{
|
||||
messageID: firstNonEmpty(
|
||||
jsonScalarString(nested.MessageID),
|
||||
jsonScalarString(nested.MsgID),
|
||||
jsonScalarString(nested.MID),
|
||||
jsonScalarString(nested.ID),
|
||||
),
|
||||
messageType: normalizeHistoryMessageType(nested.MessageType),
|
||||
}
|
||||
if msg.messageType == "" {
|
||||
msg.messageType = extractPayloadMessageType(nested.Payload)
|
||||
}
|
||||
msg.content = firstNonEmpty(
|
||||
strings.TrimSpace(nested.Content),
|
||||
strings.TrimSpace(nested.Text),
|
||||
strings.TrimSpace(nested.Body),
|
||||
strings.TrimSpace(nested.MsgContent),
|
||||
strings.TrimSpace(nested.MessageContent),
|
||||
extractPayloadContent(nested.Payload, msg.messageID, strconv.FormatInt(nested.SenderID, 10), strconv.FormatInt(nested.ReceiverID, 10)),
|
||||
extractPayloadContent(nested.ExtInfo, msg.messageID, strconv.FormatInt(nested.SenderID, 10), strconv.FormatInt(nested.ReceiverID, 10)),
|
||||
extractAttachmentContent(nested.Attachment),
|
||||
)
|
||||
if msg.messageID == "" && msg.content == "" && msg.messageType == "" {
|
||||
return nil
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func jsonScalarString(raw json.RawMessage) string {
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return ""
|
||||
}
|
||||
var s string
|
||||
if err := json.Unmarshal(raw, &s); err == nil {
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
var n json.Number
|
||||
if err := json.Unmarshal(raw, &n); err == nil {
|
||||
return n.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractPayloadContent(raw json.RawMessage, excludes ...string) string {
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return ""
|
||||
}
|
||||
var s string
|
||||
if err := json.Unmarshal(raw, &s); err == nil {
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
var payload map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &payload); err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, key := range []string{"text", "content", "body"} {
|
||||
if value := jsonScalarString(payload[key]); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
if value := jsonScalarString(payload["data"]); value != "" {
|
||||
if content := extractProtobufPayloadText(value, excludes...); content != "" {
|
||||
return content
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractPayloadMessageType(raw json.RawMessage) string {
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return ""
|
||||
}
|
||||
var payload map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &payload); err != nil {
|
||||
return ""
|
||||
}
|
||||
return normalizeHistoryMessageType(payload["type"])
|
||||
}
|
||||
|
||||
func extractAttachmentContent(raw json.RawMessage) string {
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return ""
|
||||
}
|
||||
var attachment map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &attachment); err != nil {
|
||||
return ""
|
||||
}
|
||||
if fileName := jsonScalarString(attachment["fileName"]); fileName != "" {
|
||||
return fileName
|
||||
}
|
||||
if url := jsonScalarString(attachment["url"]); url != "" {
|
||||
return url
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func normalizeHistoryMessageType(raw json.RawMessage) string {
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return ""
|
||||
}
|
||||
var s string
|
||||
if err := json.Unmarshal(raw, &s); err == nil {
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
var n int
|
||||
if err := json.Unmarshal(raw, &n); err == nil {
|
||||
return historyMessageTypeName(n)
|
||||
}
|
||||
return strings.TrimSpace(string(raw))
|
||||
}
|
||||
|
||||
func historyMessageTypeName(messageType int) string {
|
||||
switch messageType {
|
||||
case MessageTypeText:
|
||||
return "TEXT"
|
||||
case 1:
|
||||
return "AUDIO"
|
||||
case MessageTypeImage:
|
||||
return "IMAGE"
|
||||
case 3:
|
||||
return "VIDEO"
|
||||
case 4:
|
||||
return "FILE"
|
||||
case 5:
|
||||
return "EMOJI"
|
||||
case 100:
|
||||
return "CUSTOMIZED"
|
||||
default:
|
||||
return strconv.Itoa(messageType)
|
||||
}
|
||||
}
|
||||
|
||||
func extractProtobufPayloadText(encoded string, excludes ...string) string {
|
||||
raw, err := base64.StdEncoding.DecodeString(encoded)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
excludeSet := make(map[string]struct{}, len(excludes))
|
||||
for _, item := range excludes {
|
||||
if item = strings.TrimSpace(item); item != "" {
|
||||
excludeSet[item] = struct{}{}
|
||||
}
|
||||
}
|
||||
candidates := make([]string, 0, 4)
|
||||
collectProtoTextCandidates(raw, 0, excludeSet, &candidates)
|
||||
if len(candidates) == 0 {
|
||||
return ""
|
||||
}
|
||||
return candidates[len(candidates)-1]
|
||||
}
|
||||
|
||||
func collectProtoTextCandidates(raw []byte, depth int, excludes map[string]struct{}, candidates *[]string) {
|
||||
if len(raw) == 0 || depth > 8 {
|
||||
return
|
||||
}
|
||||
for len(raw) > 0 {
|
||||
_, typ, n := protowire.ConsumeTag(raw)
|
||||
if n < 0 {
|
||||
return
|
||||
}
|
||||
raw = raw[n:]
|
||||
switch typ {
|
||||
case protowire.BytesType:
|
||||
value, m := protowire.ConsumeBytes(raw)
|
||||
if m < 0 {
|
||||
return
|
||||
}
|
||||
if candidate := protoStringCandidate(value, excludes); candidate != "" {
|
||||
*candidates = append(*candidates, candidate)
|
||||
}
|
||||
collectProtoTextCandidates(value, depth+1, excludes, candidates)
|
||||
raw = raw[m:]
|
||||
default:
|
||||
m := protowire.ConsumeFieldValue(0, typ, raw)
|
||||
if m < 0 {
|
||||
return
|
||||
}
|
||||
raw = raw[m:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func protoStringCandidate(raw []byte, excludes map[string]struct{}) string {
|
||||
if len(raw) == 0 || !utf8.Valid(raw) {
|
||||
return ""
|
||||
}
|
||||
value := strings.TrimSpace(string(raw))
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
if _, ok := excludes[value]; ok {
|
||||
return ""
|
||||
}
|
||||
if isLongNumber(value) {
|
||||
return ""
|
||||
}
|
||||
for _, r := range value {
|
||||
if unicode.IsControl(r) && r != '\n' && r != '\r' && r != '\t' {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func isLongNumber(value string) bool {
|
||||
if len(value) < 9 {
|
||||
return false
|
||||
}
|
||||
for _, r := range value {
|
||||
if r < '0' || r > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user