feat: add IM admin management
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
package imadmin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
cryptorand "crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
)
|
||||
|
||||
const maxResponseBody = 10 << 20
|
||||
|
||||
type AdminScope struct {
|
||||
TenantId int64
|
||||
OperatorId int64
|
||||
RoleKey string
|
||||
RequestId string
|
||||
}
|
||||
|
||||
type Page[T any] struct {
|
||||
Items []T `json:"items"`
|
||||
Page int `json:"page"`
|
||||
PerPage int `json:"per_page"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
conf Config
|
||||
http *http.Client
|
||||
}
|
||||
|
||||
func NewClient(conf Config) *Client {
|
||||
return &Client{
|
||||
conf: conf,
|
||||
http: &http.Client{Timeout: conf.Timeout},
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultClient(ctx context.Context) *Client {
|
||||
conf := LoadConfig(ctx)
|
||||
return NewClient(conf)
|
||||
}
|
||||
|
||||
func (c *Client) CreateCode(ctx context.Context, scope AdminScope, in any, out any) error {
|
||||
return c.doJSON(ctx, http.MethodPost, "/admin/v1/im/registration-invite-codes", scope, in, out)
|
||||
}
|
||||
|
||||
func (c *Client) CreateBatch(ctx context.Context, scope AdminScope, in any, out any) error {
|
||||
return c.doJSON(ctx, http.MethodPost, "/admin/v1/im/registration-invite-code-batches", scope, in, out)
|
||||
}
|
||||
|
||||
func (c *Client) ListCodes(ctx context.Context, scope AdminScope, in any, out any) error {
|
||||
if hasInviteCode(in) {
|
||||
return c.doJSON(ctx, http.MethodPost, "/admin/v1/im/registration-invite-codes/search", scope, in, out)
|
||||
}
|
||||
path := "/admin/v1/im/registration-invite-codes"
|
||||
if query := toQuery(in); query != "" {
|
||||
path += "?" + query
|
||||
}
|
||||
return c.doJSON(ctx, http.MethodGet, path, scope, nil, out)
|
||||
}
|
||||
|
||||
func (c *Client) GetBatch(ctx context.Context, scope AdminScope, batchId int64, out any) error {
|
||||
return c.doJSON(ctx, http.MethodGet, fmt.Sprintf("/admin/v1/im/registration-invite-code-batches/%d", batchId), scope, nil, out)
|
||||
}
|
||||
|
||||
func (c *Client) RevokeCode(ctx context.Context, scope AdminScope, codeId int64) error {
|
||||
return c.doJSON(ctx, http.MethodPost, fmt.Sprintf("/admin/v1/im/registration-invite-codes/%d/revoke", codeId), scope, nil, nil)
|
||||
}
|
||||
|
||||
func (c *Client) RevokeBatch(ctx context.Context, scope AdminScope, batchId int64, out any) error {
|
||||
return c.doJSON(ctx, http.MethodPost, fmt.Sprintf("/admin/v1/im/registration-invite-code-batches/%d/revoke", batchId), scope, nil, out)
|
||||
}
|
||||
|
||||
func (c *Client) ExportCodes(ctx context.Context, scope AdminScope, in any) (*http.Response, error) {
|
||||
req, err := c.newRequest(ctx, http.MethodPost, "/admin/v1/im/registration-invite-codes/export", scope, in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return nil, gerror.New("IM服务暂不可用")
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
defer resp.Body.Close()
|
||||
return nil, c.parseError(resp)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *Client) doJSON(ctx context.Context, method string, path string, scope AdminScope, in any, out any) error {
|
||||
req, err := c.newRequest(ctx, method, path, scope, in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return gerror.New("IM服务暂不可用")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return c.parseError(resp)
|
||||
}
|
||||
if out == nil {
|
||||
return nil
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBody))
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "读取IM响应失败")
|
||||
}
|
||||
var envelope struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Reason string `json:"reason"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
if err = json.Unmarshal(body, &envelope); err == nil && len(envelope.Data) > 0 {
|
||||
if envelope.Code != 0 {
|
||||
return stableError(envelope.Message, envelope.Reason)
|
||||
}
|
||||
return json.Unmarshal(envelope.Data, out)
|
||||
}
|
||||
return json.Unmarshal(body, out)
|
||||
}
|
||||
|
||||
func (c *Client) newRequest(ctx context.Context, method string, path string, scope AdminScope, in any) (*http.Request, error) {
|
||||
if c.conf.BaseUrl == "" {
|
||||
return nil, gerror.New("IM管理API未配置")
|
||||
}
|
||||
if c.conf.ServiceSecret == "" {
|
||||
return nil, gerror.New("IM管理API密钥未配置")
|
||||
}
|
||||
var body io.Reader
|
||||
bodyBytes := []byte{}
|
||||
if in != nil {
|
||||
b, err := json.Marshal(in)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "序列化IM请求失败")
|
||||
}
|
||||
bodyBytes = b
|
||||
body = bytes.NewReader(bodyBytes)
|
||||
}
|
||||
baseUrl := strings.TrimRight(c.conf.BaseUrl, "/")
|
||||
req, err := http.NewRequestWithContext(ctx, method, baseUrl+path, body)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "创建IM请求失败")
|
||||
}
|
||||
if scope.RequestId == "" {
|
||||
scope.RequestId = gctx.CtxId(ctx)
|
||||
}
|
||||
timestamp := strconv.FormatInt(time.Now().UTC().Unix(), 10)
|
||||
nonce, err := randomNonce()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bodyHash := sha256.Sum256(bodyBytes)
|
||||
canonical := strings.Join([]string{
|
||||
method,
|
||||
requestTarget(req),
|
||||
c.conf.ServiceId,
|
||||
fmt.Sprintf("%d", scope.TenantId),
|
||||
fmt.Sprintf("%d", scope.OperatorId),
|
||||
timestamp,
|
||||
nonce,
|
||||
scope.RequestId,
|
||||
hex.EncodeToString(bodyHash[:]),
|
||||
}, "\n")
|
||||
mac := hmac.New(sha256.New, []byte(c.conf.ServiceSecret))
|
||||
_, _ = mac.Write([]byte(canonical))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "IM-HMAC "+hex.EncodeToString(mac.Sum(nil)))
|
||||
req.Header.Set("X-IM-Service-ID", c.conf.ServiceId)
|
||||
req.Header.Set("X-IM-Tenant-ID", fmt.Sprintf("%d", scope.TenantId))
|
||||
req.Header.Set("X-IM-Operator-ID", fmt.Sprintf("%d", scope.OperatorId))
|
||||
req.Header.Set("X-IM-Timestamp", timestamp)
|
||||
req.Header.Set("X-IM-Nonce", nonce)
|
||||
req.Header.Set("X-HotGo-Role-Key", scope.RoleKey)
|
||||
req.Header.Set("X-Request-ID", scope.RequestId)
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (c *Client) parseError(resp *http.Response) error {
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 64<<10))
|
||||
var envelope struct {
|
||||
Message string `json:"message"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
_ = json.Unmarshal(body, &envelope)
|
||||
return stableError(envelope.Message, envelope.Reason)
|
||||
}
|
||||
|
||||
func stableError(message string, reason string) error {
|
||||
if message != "" {
|
||||
return gerror.New(message)
|
||||
}
|
||||
if reason != "" {
|
||||
return gerror.New(reason)
|
||||
}
|
||||
return gerror.New("IM服务暂不可用")
|
||||
}
|
||||
|
||||
func hasInviteCode(in any) bool {
|
||||
b, err := json.Marshal(in)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
var m map[string]any
|
||||
if err = json.Unmarshal(b, &m); err != nil {
|
||||
return false
|
||||
}
|
||||
v, ok := m["inviteCode"]
|
||||
if ok && strings.TrimSpace(fmt.Sprint(v)) != "" {
|
||||
return true
|
||||
}
|
||||
v, ok = m["invite_code"]
|
||||
return ok && strings.TrimSpace(fmt.Sprint(v)) != ""
|
||||
}
|
||||
|
||||
func requestTarget(req *http.Request) string {
|
||||
target := req.URL.EscapedPath()
|
||||
if req.URL.RawQuery != "" {
|
||||
target += "?" + req.URL.RawQuery
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
func randomNonce() (string, error) {
|
||||
buf := make([]byte, 16)
|
||||
if _, err := cryptorand.Read(buf); err != nil {
|
||||
return "", gerror.Wrap(err, "生成IM请求随机数失败")
|
||||
}
|
||||
return hex.EncodeToString(buf), nil
|
||||
}
|
||||
|
||||
func toQuery(in any) string {
|
||||
b, err := json.Marshal(in)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
var m map[string]any
|
||||
if err = json.Unmarshal(b, &m); err != nil {
|
||||
return ""
|
||||
}
|
||||
values := url.Values{}
|
||||
for key, value := range m {
|
||||
if value == nil {
|
||||
continue
|
||||
}
|
||||
text := strings.TrimSpace(fmt.Sprint(value))
|
||||
if text == "" || text == "0" || text == "<nil>" {
|
||||
continue
|
||||
}
|
||||
values.Set(key, text)
|
||||
}
|
||||
return values.Encode()
|
||||
}
|
||||
Reference in New Issue
Block a user