From bdc4a13e83a4d6f4505f36df09d88380bd00744c Mon Sep 17 00:00:00 2001 From: loser <1711788888@qq.com> Date: Wed, 2 Nov 2022 19:34:45 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E6=B7=BB=E5=8A=A0=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 3 + internal/conv/conv.go | 108 +++++++++ internal/core/client.go | 146 +++++++++++++ internal/core/error.go | 38 ++++ internal/enum/code.go | 16 ++ internal/enum/enum.go | 50 +++++ internal/random/random.go | 63 ++++++ internal/sign/base64.go | 28 +++ internal/sign/sign.go | 421 ++++++++++++++++++++++++++++++++++++ internal/types/interface.go | 30 +++ internal/types/types.go | 171 +++++++++++++++ 11 files changed, 1074 insertions(+) create mode 100644 go.mod create mode 100644 internal/conv/conv.go create mode 100644 internal/core/client.go create mode 100644 internal/core/error.go create mode 100644 internal/enum/code.go create mode 100644 internal/enum/enum.go create mode 100644 internal/random/random.go create mode 100644 internal/sign/base64.go create mode 100644 internal/sign/sign.go create mode 100644 internal/types/interface.go create mode 100644 internal/types/types.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d616e06 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module ady-tis + +go 1.18 diff --git a/internal/conv/conv.go b/internal/conv/conv.go new file mode 100644 index 0000000..785c076 --- /dev/null +++ b/internal/conv/conv.go @@ -0,0 +1,108 @@ +/** + * @Author: Echo + * @Email:1711788888@qq.com + * @Date: 2021/8/31 11:16 下午 + * @Desc: TODO + */ + +package conv + +import ( + "encoding/json" + "fmt" + "reflect" + "strconv" + "time" +) + +type stringInterface interface { + String() string +} + +type errorInterface interface { + Error() string +} + +func String(any interface{}) string { + switch v := any.(type) { + case nil: + return "" + case string: + return v + case int: + return strconv.Itoa(v) + case int8: + return strconv.Itoa(int(v)) + case int16: + return strconv.Itoa(int(v)) + case int32: + return strconv.Itoa(int(v)) + case int64: + return strconv.FormatInt(v, 10) + case uint: + return strconv.FormatUint(uint64(v), 10) + case uint8: + return strconv.FormatUint(uint64(v), 10) + case uint16: + return strconv.FormatUint(uint64(v), 10) + case uint64: + return strconv.FormatUint(v, 10) + case float32: + return strconv.FormatFloat(float64(v), 'f', -1, 32) + case float64: + return strconv.FormatFloat(v, 'f', -1, 64) + case bool: + return strconv.FormatBool(v) + case []byte: + return string(v) + case time.Time: + return v.String() + case *time.Time: + if v == nil { + return "" + } + return v.String() + default: + if v == nil { + return "" + } + + if i, ok := v.(stringInterface); ok { + return i.String() + } + + if i, ok := v.(errorInterface); ok { + return i.Error() + } + + var ( + rv = reflect.ValueOf(v) + kind = rv.Kind() + ) + + switch kind { + case reflect.Chan, + reflect.Map, + reflect.Slice, + reflect.Func, + reflect.Ptr, + reflect.Interface, + reflect.UnsafePointer: + if rv.IsNil() { + return "" + } + case reflect.String: + return rv.String() + } + + if kind == reflect.Ptr { + return String(rv.Elem().Interface()) + } + + if b, e := json.Marshal(v); e != nil { + return fmt.Sprint(v) + } else { + return string(b) + } + } +} diff --git a/internal/core/client.go b/internal/core/client.go new file mode 100644 index 0000000..f26503e --- /dev/null +++ b/internal/core/client.go @@ -0,0 +1,146 @@ +/** + * @Author: Echo + * @Email:1711788888@qq.com + * @Date: 2021/8/27 11:31 上午 + * @Desc: TODO + */ + +package core + +import ( + "ady-tis/internal/enum" + "ady-tis/internal/sign" + "ady-tis/internal/types" + "fmt" + "math/rand" + "time" + + "git.echol.cn/loser/http" +) + +const ( + defaultBaseUrl = "https://console.tim.qq.com" + defaultVersion = "v4" + defaultContentType = "json" + defaultExpiration = 3600 +) + +var invalidResponse = NewError(enum.InvalidResponseCode, "invalid response") + +type Client interface { + // Get GET请求 + Get(serviceName string, command string, data interface{}, resp interface{}) error + // Post POST请求 + Post(serviceName string, command string, data interface{}, resp interface{}) error + // Put PUT请求 + Put(serviceName string, command string, data interface{}, resp interface{}) error + // Patch PATCH请求 + Patch(serviceName string, command string, data interface{}, resp interface{}) error + // Delete DELETE请求 + Delete(serviceName string, command string, data interface{}, resp interface{}) error +} + +type client struct { + client *http.Client + opt *Options + userSig string + userSigExpireAt int64 +} + +type Options struct { + AppId int // 应用SDKAppID,可在即时通信 IM 控制台 的应用卡片中获取。 + AppSecret string // 密钥信息,可在即时通信 IM 控制台 的应用详情页面中获取,具体操作请参见 获取密钥 + UserId string // 用户ID + Expiration int // UserSig过期时间 +} + +func NewClient(opt *Options) Client { + rand.Seed(time.Now().UnixNano()) + c := new(client) + c.opt = opt + c.client = http.NewClient() + c.client.SetContentType(http.ContentTypeJson) + c.client.SetBaseUrl(defaultBaseUrl) + + return c +} + +// Get GET请求 +func (c *client) Get(serviceName string, command string, data interface{}, resp interface{}) error { + return c.request(http.MethodGet, serviceName, command, data, resp) +} + +// Post POST请求 +func (c *client) Post(serviceName string, command string, data interface{}, resp interface{}) error { + return c.request(http.MethodPost, serviceName, command, data, resp) +} + +// Put PUT请求 +func (c *client) Put(serviceName string, command string, data interface{}, resp interface{}) error { + return c.request(http.MethodPut, serviceName, command, data, resp) +} + +// Patch PATCH请求 +func (c *client) Patch(serviceName string, command string, data interface{}, resp interface{}) error { + return c.request(http.MethodPatch, serviceName, command, data, resp) +} + +// Delete DELETE请求 +func (c *client) Delete(serviceName string, command string, data interface{}, resp interface{}) error { + return c.request(http.MethodDelete, serviceName, command, data, resp) +} + +// request Request请求 +func (c *client) request(method, serviceName, command string, data, resp interface{}) error { + res, err := c.client.Request(method, c.buildUrl(serviceName, command), data) + if err != nil { + return err + } + + if err = res.Scan(resp); err != nil { + return err + } + + if r, ok := resp.(types.ActionBaseRespInterface); ok { + if r.GetActionStatus() == enum.FailActionStatus { + return NewError(r.GetErrorCode(), r.GetErrorInfo()) + } + + if r.GetErrorCode() != enum.SuccessCode { + return NewError(r.GetErrorCode(), r.GetErrorInfo()) + } + } else if r, ok := resp.(types.BaseRespInterface); ok { + if r.GetErrorCode() != enum.SuccessCode { + return NewError(r.GetErrorCode(), r.GetErrorInfo()) + } + } else { + return invalidResponse + } + + return nil +} + +// buildUrl 构建一个请求URL +func (c *client) buildUrl(serviceName string, command string) string { + format := "/%s/%s/%s?sdkappid=%d&identifier=%s&usersig=%s&random=%d&contenttype=%s" + random := rand.Int31() + userSig := c.getUserSig() + //fmt.Println(fmt.Sprintf(format, defaultVersion, serviceName, command, c.opt.AppId, c.opt.UserId, userSig, random, defaultContentType)) + return fmt.Sprintf(format, defaultVersion, serviceName, command, c.opt.AppId, c.opt.UserId, userSig, random, defaultContentType) +} + +// getUserSig 获取签名 +func (c *client) getUserSig() string { + now, expiration := time.Now(), c.opt.Expiration + + if expiration <= 0 { + expiration = defaultExpiration + } + + if c.userSig == "" || c.userSigExpireAt <= now.Unix() { + c.userSig, _ = sign.GenUserSig(c.opt.AppId, c.opt.AppSecret, c.opt.UserId, expiration) + c.userSigExpireAt = now.Add(time.Duration(expiration) * time.Second).Unix() + } + + return c.userSig +} diff --git a/internal/core/error.go b/internal/core/error.go new file mode 100644 index 0000000..eb1dc4d --- /dev/null +++ b/internal/core/error.go @@ -0,0 +1,38 @@ +/** + * @Author: Echo + * @Email:1711788888@qq.com + * @Date: 2021/8/27 1:12 下午 + * @Desc: TODO + */ + +package core + +type Error interface { + error + Code() int + Message() string +} + +type respError struct { + code int + message string +} + +func NewError(code int, message string) Error { + return &respError{ + code: code, + message: message, + } +} + +func (e *respError) Error() string { + return e.message +} + +func (e *respError) Code() int { + return e.code +} + +func (e *respError) Message() string { + return e.message +} diff --git a/internal/enum/code.go b/internal/enum/code.go new file mode 100644 index 0000000..6ee9e94 --- /dev/null +++ b/internal/enum/code.go @@ -0,0 +1,16 @@ +/** + * @Author: wanglin + * @Author: wanglin@vspn.com + * @Date: 2021/11/3 10:45 + * @Desc: TODO + */ + +package enum + +const ( + SuccessActionStatus = "OK" // 成功状态 + FailActionStatus = "FAIL" // 失败状态 + SuccessCode = 0 // 成功 + InvalidParamsCode = -1 // 无效参数(自定义) + InvalidResponseCode = -2 // 无效响应(自定义) +) diff --git a/internal/enum/enum.go b/internal/enum/enum.go new file mode 100644 index 0000000..232b53f --- /dev/null +++ b/internal/enum/enum.go @@ -0,0 +1,50 @@ +/** + * @Author: Echo + * @Email:1711788888@qq.com + * @Date: 2021/8/28 1:14 上午 + * @Desc: TODO + */ + +package enum + +const ( + // 消息类型 + MsgText = "TIMTextElem" // 消息元素 + MsgLocation = "TIMLocationElem" // 地理位置消息元素 + MsgFace = "TIMFaceElem" // 表情消息元素 + MsgCustom = "TIMCustomElem" // 自定义消息元素 + MsgSound = "TIMSoundElem" // 语音消息元素 + MsgImage = "TIMImageElem" // 图像消息元素 + MsgFile = "TIMFileElem" // 文件消息元素 + MsgVideo = "TIMVideoFileElem" // 视频消息元素 + + // 图片格式 + ImageFormatJPG = 1 // JPG格式 + ImageFormatGIF = 2 // GIF格式 + ImageFormatPNG = 3 // PNG格式 + ImageFormatBMP = 4 // BMP格式 + ImageFormatOTHER = 255 // 其他格式 + + // 图片类型 + ImageTypeOriginal = 1 // 原图 + ImageTypePic = 2 // 大图 + ImageTypeThumb = 3 // 缩略图 + + // 标准资料字段 + StandardAttrNickname = "Tag_Profile_IM_Nick" // 昵称 + StandardAttrGender = "Tag_Profile_IM_Gender" // 性别 + StandardAttrBirthday = "Tag_Profile_IM_BirthDay" // 生日 + StandardAttrLocation = "Tag_Profile_IM_Location" // 所在地 + StandardAttrSignature = "Tag_Profile_IM_SelfSignature" // 个性签名 + StandardAttrAllowType = "Tag_Profile_IM_AllowType" // 加好友验证方式 + StandardAttrLanguage = "Tag_Profile_IM_Language" // 语言 + StandardAttrAvatar = "Tag_Profile_IM_Image" // 头像URL + StandardAttrMsgSettings = "Tag_Profile_IM_MsgSettings" // 消息设置 + StandardAttrAdminForbidType = "Tag_Profile_IM_AdminForbidType" // 管理员禁止加好友标识 + StandardAttrLevel = "Tag_Profile_IM_Level" // 等级 + StandardAttrRole = "Tag_Profile_IM_Role" // 角色 + + // 自定义属性前缀 + CustomAttrPrefix = "Tag_Profile_Custom" // 自定义属性前缀 + +) diff --git a/internal/random/random.go b/internal/random/random.go new file mode 100644 index 0000000..59acd97 --- /dev/null +++ b/internal/random/random.go @@ -0,0 +1,63 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/5/12 10:35 + * @Desc: 随机数类库 + */ + +package random + +import ( + "math/rand" + "time" +) + +const ( + AlphaStr = iota // 字母字 + AlphaLowerStr // 小写字母 + AlphaUpperStr // 大写字母 + NumericStr // 数字 + NoZeroNumericStr // 无0数字 +) + +// GenStr 生成指定长度的字符串 +func GenStr(mode, length int) string { + var ( + pos int + lastStr string + seedStr string + ) + + rand.Seed(time.Now().UnixNano()) + switch mode { + case AlphaStr: + seedStr = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + case AlphaLowerStr: + seedStr = "abcdefghijklmnopqrstuvwxyz" + case AlphaUpperStr: + seedStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + case NumericStr: + seedStr = "0123456789" + case NoZeroNumericStr: + seedStr = "123456789" + } + + seedLen := len(seedStr) + for i := 0; i < length; i++ { + pos = rand.Intn(seedLen) + lastStr += seedStr[pos : pos+1] + } + + return lastStr +} + +// GenNumeric 生成指定范围的数字 +func GenNumeric(min int, max int) int { + rand.Seed(time.Now().UnixNano()) + + if min < max { + return rand.Intn(max-min) + min + } else { + return rand.Intn(min-max) + max + } +} diff --git a/internal/sign/base64.go b/internal/sign/base64.go new file mode 100644 index 0000000..d82fabc --- /dev/null +++ b/internal/sign/base64.go @@ -0,0 +1,28 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/5/27 19:15 + * @Desc: BASE64 + */ + +package sign + +import ( + "encoding/base64" + "strings" +) + +func base64urlEncode(data []byte) string { + str := base64.StdEncoding.EncodeToString(data) + str = strings.Replace(str, "+", "*", -1) + str = strings.Replace(str, "/", "-", -1) + str = strings.Replace(str, "=", "_", -1) + return str +} + +func base64urlDecode(str string) ([]byte, error) { + str = strings.Replace(str, "_", "=", -1) + str = strings.Replace(str, "-", "/", -1) + str = strings.Replace(str, "*", "+", -1) + return base64.StdEncoding.DecodeString(str) +} diff --git a/internal/sign/sign.go b/internal/sign/sign.go new file mode 100644 index 0000000..7dbde9d --- /dev/null +++ b/internal/sign/sign.go @@ -0,0 +1,421 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/5/27 19:11 + * @Desc: Transmission signature. + */ + +package sign + +import ( + "bytes" + "compress/zlib" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "errors" + "io/ioutil" + "strconv" + "time" +) + +/** + *【功能说明】用于签发 TRTC 和 IM 服务中必须要使用的 UserSig 鉴权票据 + * + *【参数说明】 + * sdkappid - 应用id + * key - 计算 usersig 用的加密密钥,控制台可获取 + * userid - 用户id,限制长度为32字节,只允许包含大小写英文字母(a-zA-Z)、数字(0-9)及下划线和连词符。 + * expire - UserSig 票据的过期时间,单位是秒,比如 86400 代表生成的 UserSig 票据在一天后就无法再使用了。 + */ + +/** + * Function: Used to issue UserSig that is required by the TRTC and IM services. + * + * Parameter description: + * sdkappid - Application ID + * userid - User ID. The value can be up to 32 bytes in length and contain letters (a-z and A-Z), digits (0-9), underscores (_), and hyphens (-). + * key - The encryption key used to calculate usersig can be obtained from the console. + * expire - UserSig expiration time, in seconds. For example, 86400 indicates that the generated UserSig will expire one day after being generated. + */ +func GenUserSig(sdkappid int, key string, userid string, expire int) (string, error) { + return genSig(sdkappid, key, userid, expire, nil) +} + +func GenUserSigWithBuf(sdkappid int, key string, userid string, expire int, buf []byte) (string, error) { + return genSig(sdkappid, key, userid, expire, buf) +} + +/** + *【功能说明】 + * 用于签发 TRTC 进房参数中可选的 PrivateMapKey 权限票据。 + * PrivateMapKey 需要跟 UserSig 一起使用,但 PrivateMapKey 比 UserSig 有更强的权限控制能力: + * - UserSig 只能控制某个 UserID 有无使用 TRTC 服务的权限,只要 UserSig 正确,其对应的 UserID 可以进出任意房间。 + * - PrivateMapKey 则是将 UserID 的权限控制的更加严格,包括能不能进入某个房间,能不能在该房间里上行音视频等等。 + * 如果要开启 PrivateMapKey 严格权限位校验,需要在【实时音视频控制台】=>【应用管理】=>【应用信息】中打开“启动权限密钥”开关。 + * + *【参数说明】 + * sdkappid - 应用id。 + * key - 计算 usersig 用的加密密钥,控制台可获取。 + * userid - 用户id,限制长度为32字节,只允许包含大小写英文字母(a-zA-Z)、数字(0-9)及下划线和连词符。 + * expire - PrivateMapKey 票据的过期时间,单位是秒,比如 86400 生成的 PrivateMapKey 票据在一天后就无法再使用了。 + * roomid - 房间号,用于指定该 userid 可以进入的房间号 + * privilegeMap - 权限位,使用了一个字节中的 8 个比特位,分别代表八个具体的功能权限开关: + * - 第 1 位:0000 0001 = 1,创建房间的权限 + * - 第 2 位:0000 0010 = 2,加入房间的权限 + * - 第 3 位:0000 0100 = 4,发送语音的权限 + * - 第 4 位:0000 1000 = 8,接收语音的权限 + * - 第 5 位:0001 0000 = 16,发送视频的权限 + * - 第 6 位:0010 0000 = 32,接收视频的权限 + * - 第 7 位:0100 0000 = 64,发送辅路(也就是屏幕分享)视频的权限 + * - 第 8 位:1000 0000 = 200,接收辅路(也就是屏幕分享)视频的权限 + * - privilegeMap == 1111 1111 == 255 代表该 userid 在该 roomid 房间内的所有功能权限。 + * - privilegeMap == 0010 1010 == 42 代表该 userid 拥有加入房间和接收音视频数据的权限,但不具备其他权限。 + */ + +/** + * Function: + * Used to issue PrivateMapKey that is optional for room entry. + * PrivateMapKey must be used together with UserSig but with more powerful permission control capabilities. + * - UserSig can only control whether a UserID has permission to use the TRTC service. As long as the UserSig is correct, the user with the corresponding UserID can enter or leave any room. + * - PrivateMapKey specifies more stringent permissions for a UserID, including whether the UserID can be used to enter a specific room and perform audio/video upstreaming in the room. + * To enable stringent PrivateMapKey permission bit verification, you need to enable permission key in TRTC console > Application Management > Application Info. + * + * Parameter description: + * sdkappid - Application ID + * userid - User ID. The value can be up to 32 bytes in length and contain letters (a-z and A-Z), digits (0-9), underscores (_), and hyphens (-). + * key - The encryption key used to calculate usersig can be obtained from the console. + * roomid - ID of the room to which the specified UserID can enter. + * expire - PrivateMapKey expiration time, in seconds. For example, 86400 indicates that the generated PrivateMapKey will expire one day after being generated. + * privilegeMap - Permission bits. Eight bits in the same byte are used as the permission switches of eight specific features: + * - Bit 1: 0000 0001 = 1, permission for room creation + * - Bit 2: 0000 0010 = 2, permission for room entry + * - Bit 3: 0000 0100 = 4, permission for audio sending + * - Bit 4: 0000 1000 = 8, permission for audio receiving + * - Bit 5: 0001 0000 = 16, permission for video sending + * - Bit 6: 0010 0000 = 32, permission for video receiving + * - Bit 7: 0100 0000 = 64, permission for substream video sending (screen sharing) + * - Bit 8: 1000 0000 = 200, permission for substream video receiving (screen sharing) + * - privilegeMap == 1111 1111 == 255: Indicates that the UserID has all feature permissions of the room specified by roomid. + * - privilegeMap == 0010 1010 == 42: Indicates that the UserID has only the permissions to enter the room and receive audio/video data. + */ + +func GenPrivateMapKey(sdkappid int, key string, userid string, expire int, roomid uint32, privilegeMap uint32) (string, error) { + var userbuf []byte = genUserBuf(userid, sdkappid, roomid, expire, privilegeMap, 0, "") + return genSig(sdkappid, key, userid, expire, userbuf) +} + +/** + *【功能说明】 + * 用于签发 TRTC 进房参数中可选的 PrivateMapKey 权限票据。 + * PrivateMapKey 需要跟 UserSig 一起使用,但 PrivateMapKey 比 UserSig 有更强的权限控制能力: + * - UserSig 只能控制某个 UserID 有无使用 TRTC 服务的权限,只要 UserSig 正确,其对应的 UserID 可以进出任意房间。 + * - PrivateMapKey 则是将 UserID 的权限控制的更加严格,包括能不能进入某个房间,能不能在该房间里上行音视频等等。 + * 如果要开启 PrivateMapKey 严格权限位校验,需要在【实时音视频控制台】=>【应用管理】=>【应用信息】中打开“启动权限密钥”开关。 + * + *【参数说明】 + * sdkappid - 应用id。 + * key - 计算 usersig 用的加密密钥,控制台可获取。 + * userid - 用户id,限制长度为32字节,只允许包含大小写英文字母(a-zA-Z)、数字(0-9)及下划线和连词符。 + * expire - PrivateMapKey 票据的过期时间,单位是秒,比如 86400 生成的 PrivateMapKey 票据在一天后就无法再使用了。 + * roomStr - 字符串房间号,用于指定该 userid 可以进入的房间号 + * privilegeMap - 权限位,使用了一个字节中的 8 个比特位,分别代表八个具体的功能权限开关: + * - 第 1 位:0000 0001 = 1,创建房间的权限 + * - 第 2 位:0000 0010 = 2,加入房间的权限 + * - 第 3 位:0000 0100 = 4,发送语音的权限 + * - 第 4 位:0000 1000 = 8,接收语音的权限 + * - 第 5 位:0001 0000 = 16,发送视频的权限 + * - 第 6 位:0010 0000 = 32,接收视频的权限 + * - 第 7 位:0100 0000 = 64,发送辅路(也就是屏幕分享)视频的权限 + * - 第 8 位:1000 0000 = 200,接收辅路(也就是屏幕分享)视频的权限 + * - privilegeMap == 1111 1111 == 255 代表该 userid 在该 roomid 房间内的所有功能权限。 + * - privilegeMap == 0010 1010 == 42 代表该 userid 拥有加入房间和接收音视频数据的权限,但不具备其他权限。 + */ + +/** + * Function: + * Used to issue PrivateMapKey that is optional for room entry. + * PrivateMapKey must be used together with UserSig but with more powerful permission control capabilities. + * - UserSig can only control whether a UserID has permission to use the TRTC service. As long as the UserSig is correct, the user with the corresponding UserID can enter or leave any room. + * - PrivateMapKey specifies more stringent permissions for a UserID, including whether the UserID can be used to enter a specific room and perform audio/video upstreaming in the room. + * To enable stringent PrivateMapKey permission bit verification, you need to enable permission key in TRTC console > Application Management > Application Info. + * + * Parameter description: + * sdkappid - Application ID + * userid - User ID. The value can be up to 32 bytes in length and contain letters (a-z and A-Z), digits (0-9), underscores (_), and hyphens (-). + * key - The encryption key used to calculate usersig can be obtained from the console. + * roomstr - ID of the room to which the specified UserID can enter. + * expire - PrivateMapKey expiration time, in seconds. For example, 86400 indicates that the generated PrivateMapKey will expire one day after being generated. + * privilegeMap - Permission bits. Eight bits in the same byte are used as the permission switches of eight specific features: + * - Bit 1: 0000 0001 = 1, permission for room creation + * - Bit 2: 0000 0010 = 2, permission for room entry + * - Bit 3: 0000 0100 = 4, permission for audio sending + * - Bit 4: 0000 1000 = 8, permission for audio receiving + * - Bit 5: 0001 0000 = 16, permission for video sending + * - Bit 6: 0010 0000 = 32, permission for video receiving + * - Bit 7: 0100 0000 = 64, permission for substream video sending (screen sharing) + * - Bit 8: 1000 0000 = 200, permission for substream video receiving (screen sharing) + * - privilegeMap == 1111 1111 == 255: Indicates that the UserID has all feature permissions of the room specified by roomid. + * - privilegeMap == 0010 1010 == 42: Indicates that the UserID has only the permissions to enter the room and receive audio/video data. + */ +func GenPrivateMapKeyWithStringRoomID(sdkappid int, key string, userid string, expire int, roomStr string, privilegeMap uint32) (string, error) { + var userbuf []byte = genUserBuf(userid, sdkappid, 0, expire, privilegeMap, 0, roomStr) + return genSig(sdkappid, key, userid, expire, userbuf) +} + +func genUserBuf(account string, dwSdkappid int, dwAuthID uint32, + dwExpTime int, dwPrivilegeMap uint32, dwAccountType uint32, roomStr string) []byte { + + offset := 0 + length := 1 + 2 + len(account) + 20 + len(roomStr) + if len(roomStr) > 0 { + length = length + 2 + } + + userBuf := make([]byte, length) + + //ver + if len(roomStr) > 0 { + userBuf[offset] = 1 + } else { + userBuf[offset] = 0 + } + + offset++ + userBuf[offset] = (byte)((len(account) & 0xFF00) >> 8) + offset++ + userBuf[offset] = (byte)(len(account) & 0x00FF) + offset++ + + for ; offset < len(account)+3; offset++ { + userBuf[offset] = account[offset-3] + } + + //dwSdkAppid + userBuf[offset] = (byte)((dwSdkappid & 0xFF000000) >> 24) + offset++ + userBuf[offset] = (byte)((dwSdkappid & 0x00FF0000) >> 16) + offset++ + userBuf[offset] = (byte)((dwSdkappid & 0x0000FF00) >> 8) + offset++ + userBuf[offset] = (byte)(dwSdkappid & 0x000000FF) + offset++ + + //dwAuthId + userBuf[offset] = (byte)((dwAuthID & 0xFF000000) >> 24) + offset++ + userBuf[offset] = (byte)((dwAuthID & 0x00FF0000) >> 16) + offset++ + userBuf[offset] = (byte)((dwAuthID & 0x0000FF00) >> 8) + offset++ + userBuf[offset] = (byte)(dwAuthID & 0x000000FF) + offset++ + + //dwExpTime now+300; + currTime := time.Now().Unix() + var expire = currTime + int64(dwExpTime) + userBuf[offset] = (byte)((expire & 0xFF000000) >> 24) + offset++ + userBuf[offset] = (byte)((expire & 0x00FF0000) >> 16) + offset++ + userBuf[offset] = (byte)((expire & 0x0000FF00) >> 8) + offset++ + userBuf[offset] = (byte)(expire & 0x000000FF) + offset++ + + //dwPrivilegeMap + userBuf[offset] = (byte)((dwPrivilegeMap & 0xFF000000) >> 24) + offset++ + userBuf[offset] = (byte)((dwPrivilegeMap & 0x00FF0000) >> 16) + offset++ + userBuf[offset] = (byte)((dwPrivilegeMap & 0x0000FF00) >> 8) + offset++ + userBuf[offset] = (byte)(dwPrivilegeMap & 0x000000FF) + offset++ + + //dwAccountType + userBuf[offset] = (byte)((dwAccountType & 0xFF000000) >> 24) + offset++ + userBuf[offset] = (byte)((dwAccountType & 0x00FF0000) >> 16) + offset++ + userBuf[offset] = (byte)((dwAccountType & 0x0000FF00) >> 8) + offset++ + userBuf[offset] = (byte)(dwAccountType & 0x000000FF) + offset++ + + if len(roomStr) > 0 { + userBuf[offset] = (byte)((len(roomStr) & 0xFF00) >> 8) + offset++ + userBuf[offset] = (byte)(len(roomStr) & 0x00FF) + offset++ + + for ; offset < length; offset++ { + userBuf[offset] = roomStr[offset-(length-len(roomStr))] + } + } + + return userBuf +} + +func hmacsha256(sdkappid int, key string, identifier string, currTime int64, expire int, base64UserBuf *string) string { + var contentToBeSigned string + contentToBeSigned = "TLS.identifier:" + identifier + "\n" + contentToBeSigned += "TLS.sdkappid:" + strconv.Itoa(sdkappid) + "\n" + contentToBeSigned += "TLS.time:" + strconv.FormatInt(currTime, 10) + "\n" + contentToBeSigned += "TLS.expire:" + strconv.Itoa(expire) + "\n" + if nil != base64UserBuf { + contentToBeSigned += "TLS.userbuf:" + *base64UserBuf + "\n" + } + + h := hmac.New(sha256.New, []byte(key)) + h.Write([]byte(contentToBeSigned)) + return base64.StdEncoding.EncodeToString(h.Sum(nil)) +} + +func genSig(sdkappid int, key string, identifier string, expire int, userbuf []byte) (string, error) { + currTime := time.Now().Unix() + sigDoc := make(map[string]interface{}) + sigDoc["TLS.ver"] = "2.0" + sigDoc["TLS.identifier"] = identifier + sigDoc["TLS.sdkappid"] = sdkappid + sigDoc["TLS.expire"] = expire + sigDoc["TLS.time"] = currTime + var base64UserBuf string + if nil != userbuf { + base64UserBuf = base64.StdEncoding.EncodeToString(userbuf) + sigDoc["TLS.userbuf"] = base64UserBuf + sigDoc["TLS.sig"] = hmacsha256(sdkappid, key, identifier, currTime, expire, &base64UserBuf) + } else { + sigDoc["TLS.sig"] = hmacsha256(sdkappid, key, identifier, currTime, expire, nil) + } + + data, err := json.Marshal(sigDoc) + if err != nil { + return "", err + } + + var b bytes.Buffer + w := zlib.NewWriter(&b) + if _, err = w.Write(data); err != nil { + return "", err + } + if err = w.Close(); err != nil { + return "", err + } + return base64urlEncode(b.Bytes()), nil +} + +// VerifyUserSig 检验UserSig在now时间点时是否有效 +// VerifyUserSig Check if UserSig is valid at now time +func VerifyUserSig(sdkappid uint64, key string, userid string, usersig string, now time.Time) error { + sig, err := newUserSig(usersig) + if err != nil { + return err + } + return sig.verify(sdkappid, key, userid, now, nil) +} + +// VerifyUserSigWithBuf 检验带UserBuf的UserSig在now时间点是否有效 +// VerifyUserSigWithBuf Check if UserSig with UserBuf is valid at now +func VerifyUserSigWithBuf(sdkappid uint64, key string, userid string, usersig string, now time.Time, userbuf []byte) error { + sig, err := newUserSig(usersig) + if err != nil { + return err + } + return sig.verify(sdkappid, key, userid, now, userbuf) +} + +type userSig struct { + Version string `json:"TLS.ver,omitempty"` + Identifier string `json:"TLS.identifier,omitempty"` + SdkAppID uint64 `json:"TLS.sdkappid,omitempty"` + Expire int64 `json:"TLS.expire,omitempty"` + Time int64 `json:"TLS.time,omitempty"` + UserBuf []byte `json:"TLS.userbuf,omitempty"` + Sig string `json:"TLS.sig,omitempty"` +} + +func newUserSig(usersig string) (userSig, error) { + b, err := base64urlDecode(usersig) + if err != nil { + return userSig{}, err + } + r, err := zlib.NewReader(bytes.NewReader(b)) + if err != nil { + return userSig{}, err + } + data, err := ioutil.ReadAll(r) + if err != nil { + return userSig{}, err + } + if err = r.Close(); err != nil { + return userSig{}, err + } + var sig userSig + if err = json.Unmarshal(data, &sig); err != nil { + return userSig{}, nil + } + return sig, nil +} + +func (u userSig) verify(sdkappid uint64, key string, userid string, now time.Time, userbuf []byte) error { + if sdkappid != u.SdkAppID { + return ErrSdkAppIDNotMatch + } + if userid != u.Identifier { + return ErrIdentifierNotMatch + } + if now.Unix() > u.Time+u.Expire { + return ErrExpired + } + if userbuf != nil { + if u.UserBuf == nil { + return ErrUserBufTypeNotMatch + } + if !bytes.Equal(userbuf, u.UserBuf) { + return ErrUserBufNotMatch + } + } else if u.UserBuf != nil { + return ErrUserBufTypeNotMatch + } + if u.sign(key) != u.Sig { + return ErrSigNotMatch + } + return nil +} + +func (u userSig) sign(key string) string { + var sb bytes.Buffer + sb.WriteString("TLS.identifier:") + sb.WriteString(u.Identifier) + sb.WriteString("\n") + sb.WriteString("TLS.sdkappid:") + sb.WriteString(strconv.FormatUint(u.SdkAppID, 10)) + sb.WriteString("\n") + sb.WriteString("TLS.time:") + sb.WriteString(strconv.FormatInt(u.Time, 10)) + sb.WriteString("\n") + sb.WriteString("TLS.expire:") + sb.WriteString(strconv.FormatInt(u.Expire, 10)) + sb.WriteString("\n") + if u.UserBuf != nil { + sb.WriteString("TLS.userbuf:") + sb.WriteString(base64.StdEncoding.EncodeToString(u.UserBuf)) + sb.WriteString("\n") + } + + h := hmac.New(sha256.New, []byte(key)) + h.Write(sb.Bytes()) + return base64.StdEncoding.EncodeToString(h.Sum(nil)) +} + +// 错误类型 +var ( + ErrSdkAppIDNotMatch = errors.New("sdk appid not match") + ErrIdentifierNotMatch = errors.New("identifier not match") + ErrExpired = errors.New("expired") + ErrUserBufTypeNotMatch = errors.New("userbuf type not match") + ErrUserBufNotMatch = errors.New("userbuf not match") + ErrSigNotMatch = errors.New("sig not match") +) diff --git a/internal/types/interface.go b/internal/types/interface.go new file mode 100644 index 0000000..f6a5240 --- /dev/null +++ b/internal/types/interface.go @@ -0,0 +1,30 @@ +/** + * @Author: Echo + * @Email:1711788888@qq.com + * @Date: 2021/8/27 12:54 下午 + * @Desc: TODO + */ + +package types + +type BaseRespInterface interface { + GetErrorCode() int + GetErrorInfo() string +} + +func (r *BaseResp) GetErrorCode() int { + return r.ErrorCode +} + +func (r *BaseResp) GetErrorInfo() string { + return r.ErrorInfo +} + +type ActionBaseRespInterface interface { + BaseRespInterface + GetActionStatus() string +} + +func (r *ActionBaseResp) GetActionStatus() string { + return r.ActionStatus +} diff --git a/internal/types/types.go b/internal/types/types.go new file mode 100644 index 0000000..cfc6c64 --- /dev/null +++ b/internal/types/types.go @@ -0,0 +1,171 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/5/28 19:24 + * @Desc: TODO + */ + +package types + +type ( + BaseResp struct { + ErrorCode int `json:"ErrorCode"` + ErrorInfo string `json:"ErrorInfo"` + ErrorDisplay string `json:"ErrorDisplay,omitempty"` + } + + ActionBaseResp struct { + BaseResp + ActionStatus string `json:"ActionStatus"` + } + + // AndroidInfo Android离线推送消息 + AndroidInfo struct { + Sound string `json:"Sound,omitempty"` // (选填)Android 离线推送声音文件路径。 + HuaWeiChannelID string `json:"HuaWeiChannelID,omitempty"` // (选填)华为手机 EMUI 10.0 及以上的通知渠道字段。该字段不为空时,会覆盖控制台配置的 ChannelID 值;该字段为空时,不会覆盖控制台配置的 ChannelID 值。 + XiaoMiChannelID string `json:"XiaoMiChannelID,omitempty"` // (选填)小米手机 MIUI 10 及以上的通知类别(Channel)适配字段。该字段不为空时,会覆盖控制台配置的 ChannelID 值;该字段为空时,不会覆盖控制台配置的 ChannelID 值。 + OPPOChannelID string `json:"OPPOChannelID,omitempty"` // (选填)OPPO 手机 Android 8.0 及以上的 NotificationChannel 通知适配字段。该字段不为空时,会覆盖控制台配置的 ChannelID 值;该字段为空时,不会覆盖控制台配置的 ChannelID 值。 + GoogleChannelID string `json:"GoogleChannelID,omitempty"` // (选填)Google 手机 Android 8.0 及以上的通知渠道字段。Google 推送新接口(上传证书文件)支持 channel id,旧接口(填写服务器密钥)不支持。 + VIVOClassification int `json:"VIVOClassification,omitempty"` // (选填)VIVO 手机推送消息分类,“0”代表运营消息,“1”代表系统消息,不填默认为1。 + HuaWeiImportance string `json:"HuaWeiImportance,omitempty"` // (选填)华为推送通知消息分类,取值为 LOW、NORMAL,不填默认为 NORMAL。 + ExtAsHuaweiIntentParam int `json:"ExtAsHuaweiIntentParam,omitempty"` // (选填)在控制台配置华为推送为“打开应用内指定页面”的前提下,传“1”表示将透传内容 Ext 作为 Intent 的参数,“0”表示将透传内容 Ext 作为 Action 参数。不填默认为0。两种传参区别可参见 华为推送文档。 + } + + // ApnsInfo IOS离线推送消息 + ApnsInfo struct { + BadgeMode int `json:"BadgeMode,omitempty"` // (选填)这个字段缺省或者为0表示需要计数,为1表示本条消息不需要计数,即右上角图标数字不增加。 + Title string `json:"Title,omitempty"` // (选填)该字段用于标识 APNs 推送的标题,若填写则会覆盖最上层 Title。 + SubTitle string `json:"SubTitle,omitempty"` // (选填)该字段用于标识 APNs 推送的子标题。 + Image string `json:"Image,omitempty"` // (选填)该字段用于标识 APNs 携带的图片地址,当客户端拿到该字段时,可以通过下载图片资源的方式将图片展示在弹窗上。 + MutableContent int `json:"MutableContent,omitempty"` // (选填)为1表示开启 iOS 10 的推送扩展,默认为0。 + } + + // OfflinePushInfo 离线推送消息 + OfflinePushInfo struct { + PushFlag int `json:"PushFlag,omitempty"` // (选填)推送标识。0表示推送,1表示不离线推送。 + Title string `json:"Title,omitempty"` // (选填)离线推送标题。该字段为 iOS 和 Android 共用。 + Desc string `json:"Desc,omitempty"` // (选填)离线推送内容。该字段会覆盖上面各种消息元素 TIMMsgElement 的离线推送展示文本。若发送的消息只有一个 TIMCustomElem 自定义消息元素,该 Desc 字段会覆盖 TIMCustomElem 中的 Desc 字段。如果两个 Desc 字段都不填,将收不到该自定义消息的离线推送。 + Ext string `json:"Ext,omitempty"` // (选填)离线推送透传内容。由于国内各 Android 手机厂商的推送平台要求各不一样,请保证此字段为 JSON 格式,否则可能会导致收不到某些厂商的离线推送。 + AndroidInfo *AndroidInfo `json:"AndroidInfo,omitempty"` // (选填)Android 离线推送消息 + ApnsInfo *ApnsInfo `json:"ApnsInfo,omitempty"` // (选填)IOS离线推送消息 + } + + // MsgBody 消息内容 + MsgBody struct { + MsgType string `json:"MsgType"` + MsgContent interface{} `json:"MsgContent"` + } + + // TagPair 标签对 + TagPair struct { + Tag string `json:"Tag"` // 标签 + Value interface{} `json:"Value"` // 标签值 + } + + // MsgTextContent 文本消息内容 + MsgTextContent struct { + Text string `json:"Text"` // (必填)消息内容。当接收方为 iOS 或 Android 后台在线时,作为离线推送的文本展示。 + } + + // MsgLocationContent 地理位置消息元素 + MsgLocationContent struct { + Desc string `json:"Desc"` // (必填)地理位置描述信息 + Latitude float64 `json:"Latitude"` // (必填)纬度 + Longitude float64 `json:"Longitude"` // (必填)经度 + } + + // MsgFaceContent 表情消息元素 + MsgFaceContent struct { + Index int `json:"Index"` // (必填)表情索引,用户自定义 + Data string `json:"Data"` // (选填)额外数据 + } + + // MsgCustomContent 自定义消息元素 + MsgCustomContent struct { + Desc string `json:"Desc"` // (选填)自定义消息描述信息。当接收方为 iOS 或 Android 后台在线时,做离线推送文本展示。 若发送自定义消息的同时设置了 OfflinePushInfo.Desc 字段,此字段会被覆盖,请优先填 OfflinePushInfo.Desc 字段。 + Data string `json:"Data"` // (必填)自定义消息数据。 不作为 APNs 的 payload 字段下发,故从 payload 中无法获取 Data 字段 + Ext string `json:"Ext"` // (选填)扩展字段。当接收方为 iOS 系统且应用处在后台时,此字段作为 APNs 请求包 Payloads 中的 Ext 键值下发,Ext 的协议格式由业务方确定,APNs 只做透传。 + Sound string `json:"Sound"` // (选填)自定义 APNs 推送铃音。 + } + + // MsgSoundContent 语音消息元素 + MsgSoundContent struct { + UUID string `json:"UUID"` // (必填)语音的唯一标识,类型为 String。客户端用于索引语音的键值。无法通过该字段下载相应的语音。若需要获取该语音,请升级 IM SDK 版本至4.X。 + Url string `json:"Url"` // (必填)语音下载地址,可通过该 URL 地址直接下载相应语音 + Size int `json:"Size"` // (必填)语音数据大小,单位:字节。 + Second int `json:"Second"` // (必填)语音时长,单位:秒。 + DownloadFlag int `json:"Download_Flag"` // (必填)语音下载方式标记。目前 Download_Flag 取值只能为2,表示可通过Url字段值的 URL 地址直接下载语音。 + } + + // MsgImageContent 图像消息元素 + MsgImageContent struct { + UUID string `json:"UUID"` // (必填)图片序列号。后台用于索引图片的键值。 + ImageFormat int `json:"ImageFormat"` // (必填)图片格式。JPG = 1,GIF = 2,PNG = 3,BMP = 4,其他 = 255。 + ImageInfos []*ImageInfo `json:"ImageInfoArray"` // (必填)原图、缩略图或者大图下载信息。 + } + + // MsgFileContent 文件消息元素 + MsgFileContent struct { + Url string `json:"Url"` // (必填)文件下载地址,可通过该 URL 地址直接下载相应文件 + UUID string `json:"UUID"` // (必填)文件的唯一标识,客户端用于索引文件的键值。 + FileSize int `json:"FileSize"` // (必填)文件数据大小,单位:字节 + FileName string `json:"FileName"` // (必填)文件名称 + DownloadFlag int `json:"Download_Flag"` // (必填)文件下载方式标记。目前 Download_Flag 取值只能为2,表示可通过Url字段值的 URL 地址直接下载文件。 + } + + // MsgVideoContent 视频消息元素 + MsgVideoContent struct { + VideoUUID string `json:"VideoUUID"` // (必填)视频的唯一标识,客户端用于索引视频的键值。 + VideoUrl string `json:"VideoUrl"` // (必填)视频下载地址。可通过该 URL 地址直接下载相应视频 + VideoSize int `json:"VideoSize"` // (必填)视频数据大小,单位:字节 + VideoSecond int `json:"VideoSecond"` // (必填)视频时长,单位:秒 + VideoFormat string `json:"VideoFormat"` // (必填)视频格式,例如 mp4 + VideoDownloadFlag int `json:"VideoDownloadFlag"` // (必填)视频下载方式标记。目前 VideoDownloadFlag 取值只能为2,表示可通过VideoUrl字段值的 URL 地址直接下载视频。 + ThumbUrl string `json:"ThumbUrl"` // (必填)视频缩略图下载地址。可通过该 URL 地址直接下载相应视频缩略图。 + ThumbUUID string `json:"ThumbUUID"` // (必填)视频缩略图的唯一标识,客户端用于索引视频缩略图的键值。 + ThumbSize int `json:"ThumbSize"` // (必填)缩略图大小,单位:字节 + ThumbWidth int `json:"ThumbWidth"` // (必填)缩略图宽度 + ThumbHeight int `json:"ThumbHeight"` // (必填)缩略图高度 + ThumbFormat string `json:"ThumbFormat"` // (必填)缩略图格式,例如 JPG、BMP 等 + ThumbDownloadFlag int `json:"ThumbDownloadFlag"` // (必填)视频缩略图下载方式标记。目前 ThumbDownloadFlag 取值只能为2,表示可通过ThumbUrl字段值的 URL 地址直接下载视频缩略图。 + } + + // ImageInfo 图片下载信息 + ImageInfo struct { + Type int `json:"Type"` // (必填)图片类型: 1-原图,2-大图,3-缩略图。 + Size int `json:"Size"` // (必填)图片数据大小,单位:字节。 + Width int `json:"Width"` // (必填)图片宽度。 + Height int `json:"Height"` // (必填)图片高度。 + Url string `json:"URL"` // (必填)图片下载地址。 + } + + // GenderType 性别类型 + GenderType string + + // AllowType 加好友验证方式 + AllowType string + + // AdminForbidType 管理员禁止加好友标识类型 + AdminForbidType string + + // SyncOtherMachine 同步至其他设备 + SyncOtherMachine int + + // PushFlag 推送标识 + PushFlag int + + // HuaWeiImportance 华为推送通知消息分类 + HuaWeiImportance string + + // HuaweiIntentParam 华为推送为“打开应用内指定页面”的前提下透传参数行为 + HuaweiIntentParam int + + // VivoClassification VIVO手机推送消息分类 + VivoClassification int + + // BadgeMode IOS徽章计数模式 + BadgeMode int + + // MutableContent IOS10的推送扩展开关 + MutableContent int +)