fix/wechat-new-protocol
This commit is contained in:
parent
60d7ec20a5
commit
607037472d
5 changed files with 136 additions and 80 deletions
|
|
@ -12,6 +12,14 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
weixinChannelVersion = "2.1.1"
|
||||||
|
weixinIlinkAppID = "bot"
|
||||||
|
// 2.1.1 encoded as 0x00MMNNPP => 0x00020101 => 131329
|
||||||
|
weixinClientVersion = 131329
|
||||||
)
|
)
|
||||||
|
|
||||||
type ApiClient struct {
|
type ApiClient struct {
|
||||||
|
|
@ -80,13 +88,9 @@ func (c *ApiClient) post(ctx context.Context, endpoint string, body any, respons
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
if endpoint == "ilink/bot/get_bot_qrcode" || endpoint == "ilink/bot/get_qrcode_status" {
|
req.Header["iLink-App-Id"] = []string{weixinIlinkAppID}
|
||||||
// QR routes have different headers sometimes, but let's stick to base ones
|
req.Header["iLink-App-ClientVersion"] = []string{strconv.Itoa(weixinClientVersion)}
|
||||||
if endpoint == "ilink/bot/get_qrcode_status" {
|
if endpoint != "ilink/bot/get_bot_qrcode" && endpoint != "ilink/bot/get_qrcode_status" {
|
||||||
// Use direct map assignment to send exact header name the Tencent API expects
|
|
||||||
req.Header["iLink-App-ClientVersion"] = []string{"1"}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
req.Header["AuthorizationType"] = []string{"ilink_bot_token"}
|
req.Header["AuthorizationType"] = []string{"ilink_bot_token"}
|
||||||
req.Header["X-WECHAT-UIN"] = []string{randomWechatUIN()}
|
req.Header["X-WECHAT-UIN"] = []string{randomWechatUIN()}
|
||||||
if c.Token != "" {
|
if c.Token != "" {
|
||||||
|
|
@ -119,7 +123,7 @@ func (c *ApiClient) post(ctx context.Context, endpoint string, body any, respons
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ApiClient) GetUpdates(ctx context.Context, req GetUpdatesReq) (*GetUpdatesResp, error) {
|
func (c *ApiClient) GetUpdates(ctx context.Context, req GetUpdatesReq) (*GetUpdatesResp, error) {
|
||||||
req.BaseInfo = BaseInfo{ChannelVersion: "1.0.2"}
|
req.BaseInfo = BaseInfo{ChannelVersion: weixinChannelVersion}
|
||||||
var resp GetUpdatesResp
|
var resp GetUpdatesResp
|
||||||
err := c.post(ctx, "ilink/bot/getupdates", req, &resp)
|
err := c.post(ctx, "ilink/bot/getupdates", req, &resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -129,7 +133,7 @@ func (c *ApiClient) GetUpdates(ctx context.Context, req GetUpdatesReq) (*GetUpda
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ApiClient) SendMessage(ctx context.Context, req SendMessageReq) (*SendMessageResp, error) {
|
func (c *ApiClient) SendMessage(ctx context.Context, req SendMessageReq) (*SendMessageResp, error) {
|
||||||
req.BaseInfo = BaseInfo{ChannelVersion: "1.0.2"}
|
req.BaseInfo = BaseInfo{ChannelVersion: weixinChannelVersion}
|
||||||
var resp SendMessageResp
|
var resp SendMessageResp
|
||||||
if err := c.post(ctx, "ilink/bot/sendmessage", req, &resp); err != nil {
|
if err := c.post(ctx, "ilink/bot/sendmessage", req, &resp); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -138,7 +142,7 @@ func (c *ApiClient) SendMessage(ctx context.Context, req SendMessageReq) (*SendM
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ApiClient) GetUploadUrl(ctx context.Context, req GetUploadUrlReq) (*GetUploadUrlResp, error) {
|
func (c *ApiClient) GetUploadUrl(ctx context.Context, req GetUploadUrlReq) (*GetUploadUrlResp, error) {
|
||||||
req.BaseInfo = BaseInfo{ChannelVersion: "1.0.2"}
|
req.BaseInfo = BaseInfo{ChannelVersion: weixinChannelVersion}
|
||||||
var resp GetUploadUrlResp
|
var resp GetUploadUrlResp
|
||||||
err := c.post(ctx, "ilink/bot/getuploadurl", req, &resp)
|
err := c.post(ctx, "ilink/bot/getuploadurl", req, &resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -148,7 +152,7 @@ func (c *ApiClient) GetUploadUrl(ctx context.Context, req GetUploadUrlReq) (*Get
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ApiClient) GetConfig(ctx context.Context, req GetConfigReq) (*GetConfigResp, error) {
|
func (c *ApiClient) GetConfig(ctx context.Context, req GetConfigReq) (*GetConfigResp, error) {
|
||||||
req.BaseInfo = BaseInfo{ChannelVersion: "1.0.2"}
|
req.BaseInfo = BaseInfo{ChannelVersion: weixinChannelVersion}
|
||||||
var resp GetConfigResp
|
var resp GetConfigResp
|
||||||
if err := c.post(ctx, "ilink/bot/getconfig", req, &resp); err != nil {
|
if err := c.post(ctx, "ilink/bot/getconfig", req, &resp); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -157,7 +161,7 @@ func (c *ApiClient) GetConfig(ctx context.Context, req GetConfigReq) (*GetConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ApiClient) SendTyping(ctx context.Context, req SendTypingReq) (*SendTypingResp, error) {
|
func (c *ApiClient) SendTyping(ctx context.Context, req SendTypingReq) (*SendTypingResp, error) {
|
||||||
req.BaseInfo = BaseInfo{ChannelVersion: "1.0.2"}
|
req.BaseInfo = BaseInfo{ChannelVersion: weixinChannelVersion}
|
||||||
var resp SendTypingResp
|
var resp SendTypingResp
|
||||||
if err := c.post(ctx, "ilink/bot/sendtyping", req, &resp); err != nil {
|
if err := c.post(ctx, "ilink/bot/sendtyping", req, &resp); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -165,38 +169,51 @@ func (c *ApiClient) SendTyping(ctx context.Context, req SendTypingReq) (*SendTyp
|
||||||
return &resp, nil
|
return &resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ApiClient) GetQRCode(ctx context.Context, botType string) (*QRCodeResponse, error) {
|
func (c *ApiClient) getQR(ctx context.Context, endpoint string, query map[string]string, respObj any) error {
|
||||||
// get_bot_qrcode is GET, not POST
|
|
||||||
u, err := url.Parse(c.BaseURL)
|
u, err := url.Parse(c.BaseURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
u.Path = path.Join(u.Path, "ilink/bot/get_bot_qrcode")
|
u.Path = path.Join(u.Path, endpoint)
|
||||||
q := u.Query()
|
q := u.Query()
|
||||||
q.Set("bot_type", botType)
|
for key, value := range query {
|
||||||
|
q.Set(key, value)
|
||||||
|
}
|
||||||
u.RawQuery = q.Encode()
|
u.RawQuery = q.Encode()
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
req.Header["iLink-App-Id"] = []string{weixinIlinkAppID}
|
||||||
|
req.Header["iLink-App-ClientVersion"] = []string{strconv.Itoa(weixinClientVersion)}
|
||||||
|
|
||||||
resp, err := c.HttpClient.Do(req)
|
resp, err := c.HttpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
respBody, err := io.ReadAll(resp.Body)
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil, fmt.Errorf("get_bot_qrcode failed: %d %s", resp.StatusCode, string(respBody))
|
return fmt.Errorf("%s failed: %d %s", endpoint, resp.StatusCode, string(respBody))
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(respBody, respObj); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ApiClient) GetQRCode(ctx context.Context, botType string) (*QRCodeResponse, error) {
|
||||||
|
// get_bot_qrcode is GET, not POST
|
||||||
var qrcodeResp QRCodeResponse
|
var qrcodeResp QRCodeResponse
|
||||||
if err := json.Unmarshal(respBody, &qrcodeResp); err != nil {
|
if err := c.getQR(ctx, "ilink/bot/get_bot_qrcode", map[string]string{
|
||||||
|
"bot_type": botType,
|
||||||
|
}, &qrcodeResp); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &qrcodeResp, nil
|
return &qrcodeResp, nil
|
||||||
|
|
@ -204,37 +221,10 @@ func (c *ApiClient) GetQRCode(ctx context.Context, botType string) (*QRCodeRespo
|
||||||
|
|
||||||
func (c *ApiClient) GetQRCodeStatus(ctx context.Context, qrcode string) (*StatusResponse, error) {
|
func (c *ApiClient) GetQRCodeStatus(ctx context.Context, qrcode string) (*StatusResponse, error) {
|
||||||
// get_qrcode_status is GET
|
// get_qrcode_status is GET
|
||||||
u, err := url.Parse(c.BaseURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
u.Path = path.Join(u.Path, "ilink/bot/get_qrcode_status")
|
|
||||||
q := u.Query()
|
|
||||||
q.Set("qrcode", qrcode)
|
|
||||||
u.RawQuery = q.Encode()
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req.Header["iLink-App-ClientVersion"] = []string{"1"}
|
|
||||||
|
|
||||||
resp, err := c.HttpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
respBody, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("get_qrcode_status failed: %d %s", resp.StatusCode, string(respBody))
|
|
||||||
}
|
|
||||||
|
|
||||||
var statusResp StatusResponse
|
var statusResp StatusResponse
|
||||||
if err := json.Unmarshal(respBody, &statusResp); err != nil {
|
if err := c.getQR(ctx, "ilink/bot/get_qrcode_status", map[string]string{
|
||||||
|
"qrcode": qrcode,
|
||||||
|
}, &statusResp); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &statusResp, nil
|
return &statusResp, nil
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ func PerformLoginInteractive(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", "", "", fmt.Errorf("failed to create api client: %w", err)
|
return "", "", "", "", fmt.Errorf("failed to create api client: %w", err)
|
||||||
}
|
}
|
||||||
|
pollAPI := api
|
||||||
|
|
||||||
logger.InfoC("weixin", "Requesting Weixin QR code...")
|
logger.InfoC("weixin", "Requesting Weixin QR code...")
|
||||||
qrResp, err := api.GetQRCode(ctx, opts.BotType)
|
qrResp, err := api.GetQRCode(ctx, opts.BotType)
|
||||||
|
|
@ -76,7 +77,7 @@ func PerformLoginInteractive(
|
||||||
case <-timeoutCtx.Done():
|
case <-timeoutCtx.Done():
|
||||||
return "", "", "", "", fmt.Errorf("login timeout")
|
return "", "", "", "", fmt.Errorf("login timeout")
|
||||||
case <-pollTicker.C:
|
case <-pollTicker.C:
|
||||||
statusResp, err := api.GetQRCodeStatus(timeoutCtx, qrResp.Qrcode)
|
statusResp, err := pollAPI.GetQRCodeStatus(timeoutCtx, qrResp.Qrcode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Long poll timeout or temporary error
|
// Long poll timeout or temporary error
|
||||||
continue
|
continue
|
||||||
|
|
@ -99,6 +100,27 @@ func PerformLoginInteractive(
|
||||||
})
|
})
|
||||||
|
|
||||||
return statusResp.BotToken, statusResp.IlinkUserID, statusResp.IlinkBotID, statusResp.Baseurl, nil
|
return statusResp.BotToken, statusResp.IlinkUserID, statusResp.IlinkBotID, statusResp.Baseurl, nil
|
||||||
|
case "scaned_but_redirect":
|
||||||
|
if statusResp.RedirectHost == "" {
|
||||||
|
logger.WarnC(
|
||||||
|
"weixin",
|
||||||
|
"scaned_but_redirect received without redirect_host; continuing on current host",
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nextBaseURL := "https://" + statusResp.RedirectHost + "/"
|
||||||
|
nextAPI, nextErr := NewApiClient(nextBaseURL, "", opts.Proxy)
|
||||||
|
if nextErr != nil {
|
||||||
|
logger.WarnCF("weixin", "Failed to switch QR polling host", map[string]any{
|
||||||
|
"redirect_host": statusResp.RedirectHost,
|
||||||
|
"error": nextErr.Error(),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pollAPI = nextAPI
|
||||||
|
logger.InfoCF("weixin", "Switched QR polling host", map[string]any{
|
||||||
|
"redirect_host": statusResp.RedirectHost,
|
||||||
|
})
|
||||||
case "expired":
|
case "expired":
|
||||||
return "", "", "", "", fmt.Errorf("qrcode expired, please try again")
|
return "", "", "", "", fmt.Errorf("qrcode expired, please try again")
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -169,11 +169,19 @@ func buildCDNUploadURL(base, uploadParam, filekey string) string {
|
||||||
"&filekey=" + url.QueryEscape(filekey)
|
"&filekey=" + url.QueryEscape(filekey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *WeixinChannel) downloadCDNBuffer(ctx context.Context, encryptedQueryParam string) ([]byte, error) {
|
func (c *WeixinChannel) downloadCDNBuffer(
|
||||||
|
ctx context.Context,
|
||||||
|
encryptedQueryParam,
|
||||||
|
fullURL string,
|
||||||
|
) ([]byte, error) {
|
||||||
|
downloadURL := strings.TrimSpace(fullURL)
|
||||||
|
if downloadURL == "" {
|
||||||
|
downloadURL = buildCDNDownloadURL(c.cdnBaseURL(), encryptedQueryParam)
|
||||||
|
}
|
||||||
req, err := http.NewRequestWithContext(
|
req, err := http.NewRequestWithContext(
|
||||||
ctx,
|
ctx,
|
||||||
http.MethodGet,
|
http.MethodGet,
|
||||||
buildCDNDownloadURL(c.cdnBaseURL(), encryptedQueryParam),
|
downloadURL,
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -203,9 +211,10 @@ func (c *WeixinChannel) downloadCDNBuffer(ctx context.Context, encryptedQueryPar
|
||||||
func (c *WeixinChannel) downloadAndDecryptCDNBuffer(
|
func (c *WeixinChannel) downloadAndDecryptCDNBuffer(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
encryptedQueryParam string,
|
encryptedQueryParam string,
|
||||||
|
fullURL string,
|
||||||
key []byte,
|
key []byte,
|
||||||
) ([]byte, error) {
|
) ([]byte, error) {
|
||||||
data, err := c.downloadCDNBuffer(ctx, encryptedQueryParam)
|
data, err := c.downloadCDNBuffer(ctx, encryptedQueryParam, fullURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -310,15 +319,18 @@ func isDownloadableMediaItem(item *MessageItem) bool {
|
||||||
|
|
||||||
switch item.Type {
|
switch item.Type {
|
||||||
case MessageItemTypeImage:
|
case MessageItemTypeImage:
|
||||||
return item.ImageItem != nil && item.ImageItem.Media != nil && item.ImageItem.Media.EncryptQueryParam != ""
|
return item.ImageItem != nil && item.ImageItem.Media != nil &&
|
||||||
|
(item.ImageItem.Media.EncryptQueryParam != "" || item.ImageItem.Media.FullURL != "")
|
||||||
case MessageItemTypeVideo:
|
case MessageItemTypeVideo:
|
||||||
return item.VideoItem != nil && item.VideoItem.Media != nil && item.VideoItem.Media.EncryptQueryParam != ""
|
return item.VideoItem != nil && item.VideoItem.Media != nil &&
|
||||||
|
(item.VideoItem.Media.EncryptQueryParam != "" || item.VideoItem.Media.FullURL != "")
|
||||||
case MessageItemTypeFile:
|
case MessageItemTypeFile:
|
||||||
return item.FileItem != nil && item.FileItem.Media != nil && item.FileItem.Media.EncryptQueryParam != ""
|
return item.FileItem != nil && item.FileItem.Media != nil &&
|
||||||
|
(item.FileItem.Media.EncryptQueryParam != "" || item.FileItem.Media.FullURL != "")
|
||||||
case MessageItemTypeVoice:
|
case MessageItemTypeVoice:
|
||||||
return item.VoiceItem != nil &&
|
return item.VoiceItem != nil &&
|
||||||
item.VoiceItem.Media != nil &&
|
item.VoiceItem.Media != nil &&
|
||||||
item.VoiceItem.Media.EncryptQueryParam != "" &&
|
(item.VoiceItem.Media.EncryptQueryParam != "" || item.VoiceItem.Media.FullURL != "") &&
|
||||||
strings.TrimSpace(item.VoiceItem.Text) == ""
|
strings.TrimSpace(item.VoiceItem.Text) == ""
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|
@ -438,12 +450,17 @@ func (c *WeixinChannel) downloadMediaFromItem(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
data, err := c.downloadAndDecryptCDNBuffer(ctx, item.ImageItem.Media.EncryptQueryParam, func() []byte {
|
data, err := c.downloadAndDecryptCDNBuffer(
|
||||||
if ok {
|
ctx,
|
||||||
return key
|
item.ImageItem.Media.EncryptQueryParam,
|
||||||
}
|
item.ImageItem.Media.FullURL,
|
||||||
return nil
|
func() []byte {
|
||||||
}())
|
if ok {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(),
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
@ -454,7 +471,12 @@ func (c *WeixinChannel) downloadMediaFromItem(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
silk, err := c.downloadAndDecryptCDNBuffer(ctx, item.VoiceItem.Media.EncryptQueryParam, key)
|
silk, err := c.downloadAndDecryptCDNBuffer(
|
||||||
|
ctx,
|
||||||
|
item.VoiceItem.Media.EncryptQueryParam,
|
||||||
|
item.VoiceItem.Media.FullURL,
|
||||||
|
key,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
@ -468,7 +490,12 @@ func (c *WeixinChannel) downloadMediaFromItem(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
data, err := c.downloadAndDecryptCDNBuffer(ctx, item.FileItem.Media.EncryptQueryParam, key)
|
data, err := c.downloadAndDecryptCDNBuffer(
|
||||||
|
ctx,
|
||||||
|
item.FileItem.Media.EncryptQueryParam,
|
||||||
|
item.FileItem.Media.FullURL,
|
||||||
|
key,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
@ -484,7 +511,12 @@ func (c *WeixinChannel) downloadMediaFromItem(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
data, err := c.downloadAndDecryptCDNBuffer(ctx, item.VideoItem.Media.EncryptQueryParam, key)
|
data, err := c.downloadAndDecryptCDNBuffer(
|
||||||
|
ctx,
|
||||||
|
item.VideoItem.Media.EncryptQueryParam,
|
||||||
|
item.VideoItem.Media.FullURL,
|
||||||
|
key,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
@ -701,11 +733,13 @@ func (c *WeixinChannel) uploadLocalFile(
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("getuploadurl failed: ret=%d errcode=%d errmsg=%s", resp.Ret, resp.Errcode, resp.Errmsg)
|
return nil, fmt.Errorf("getuploadurl failed: ret=%d errcode=%d errmsg=%s", resp.Ret, resp.Errcode, resp.Errmsg)
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(resp.UploadParam) == "" {
|
uploadParam := strings.TrimSpace(resp.UploadParam)
|
||||||
return nil, fmt.Errorf("getuploadurl returned empty upload_param")
|
uploadFullURL := strings.TrimSpace(resp.UploadFullURL)
|
||||||
|
if uploadParam == "" && uploadFullURL == "" {
|
||||||
|
return nil, fmt.Errorf("getuploadurl returned no upload URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadParam, err := c.uploadBufferToCDN(ctx, data, resp.UploadParam, filekey, aesKey)
|
downloadParam, err := c.uploadBufferToCDN(ctx, data, uploadParam, uploadFullURL, filekey, aesKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -723,6 +757,7 @@ func (c *WeixinChannel) uploadBufferToCDN(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
plaintext []byte,
|
plaintext []byte,
|
||||||
uploadParam,
|
uploadParam,
|
||||||
|
uploadFullURL,
|
||||||
filekey string,
|
filekey string,
|
||||||
aesKey []byte,
|
aesKey []byte,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
|
|
@ -731,7 +766,13 @@ func (c *WeixinChannel) uploadBufferToCDN(
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadURL := buildCDNUploadURL(c.cdnBaseURL(), uploadParam, filekey)
|
uploadURL := strings.TrimSpace(uploadFullURL)
|
||||||
|
if uploadURL == "" {
|
||||||
|
if strings.TrimSpace(uploadParam) == "" {
|
||||||
|
return "", fmt.Errorf("missing CDN upload URL")
|
||||||
|
}
|
||||||
|
uploadURL = buildCDNUploadURL(c.cdnBaseURL(), uploadParam, filekey)
|
||||||
|
}
|
||||||
var lastErr error
|
var lastErr error
|
||||||
|
|
||||||
for attempt := 1; attempt <= weixinUploadRetryMax; attempt++ {
|
for attempt := 1; attempt <= weixinUploadRetryMax; attempt++ {
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ type GetUploadUrlResp struct {
|
||||||
APIStatus
|
APIStatus
|
||||||
UploadParam string `json:"upload_param,omitempty"`
|
UploadParam string `json:"upload_param,omitempty"`
|
||||||
ThumbUploadParam string `json:"thumb_upload_param,omitempty"`
|
ThumbUploadParam string `json:"thumb_upload_param,omitempty"`
|
||||||
|
UploadFullURL string `json:"upload_full_url,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -69,6 +70,7 @@ type CDNMedia struct {
|
||||||
EncryptQueryParam string `json:"encrypt_query_param,omitempty"`
|
EncryptQueryParam string `json:"encrypt_query_param,omitempty"`
|
||||||
AesKey string `json:"aes_key,omitempty"` // base64 encoded
|
AesKey string `json:"aes_key,omitempty"` // base64 encoded
|
||||||
EncryptType int `json:"encrypt_type,omitempty"`
|
EncryptType int `json:"encrypt_type,omitempty"`
|
||||||
|
FullURL string `json:"full_url,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageItem struct {
|
type ImageItem struct {
|
||||||
|
|
@ -202,9 +204,10 @@ type QRCodeResponse struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type StatusResponse struct {
|
type StatusResponse struct {
|
||||||
Status string `json:"status"` // "wait", "scaned", "confirmed", "expired"
|
Status string `json:"status"` // "wait", "scaned", "confirmed", "expired", "scaned_but_redirect"
|
||||||
BotToken string `json:"bot_token,omitempty"`
|
BotToken string `json:"bot_token,omitempty"`
|
||||||
IlinkBotID string `json:"ilink_bot_id,omitempty"`
|
IlinkBotID string `json:"ilink_bot_id,omitempty"`
|
||||||
Baseurl string `json:"baseurl,omitempty"`
|
Baseurl string `json:"baseurl,omitempty"`
|
||||||
IlinkUserID string `json:"ilink_user_id,omitempty"`
|
IlinkUserID string `json:"ilink_user_id,omitempty"`
|
||||||
|
RedirectHost string `json:"redirect_host,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ func TestDownloadAndDecryptCDNBuffer(t *testing.T) {
|
||||||
typingCache: make(map[string]typingTicketCacheEntry),
|
typingCache: make(map[string]typingTicketCacheEntry),
|
||||||
}
|
}
|
||||||
|
|
||||||
got, err := ch.downloadAndDecryptCDNBuffer(context.Background(), "token", key)
|
got, err := ch.downloadAndDecryptCDNBuffer(context.Background(), "token", "", key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("downloadAndDecryptCDNBuffer() error = %v", err)
|
t.Fatalf("downloadAndDecryptCDNBuffer() error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -120,7 +120,7 @@ func TestUploadBufferToCDN(t *testing.T) {
|
||||||
typingCache: make(map[string]typingTicketCacheEntry),
|
typingCache: make(map[string]typingTicketCacheEntry),
|
||||||
}
|
}
|
||||||
|
|
||||||
got, err := ch.uploadBufferToCDN(context.Background(), plaintext, "upload-param", "file-key", key)
|
got, err := ch.uploadBufferToCDN(context.Background(), plaintext, "upload-param", "", "file-key", key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("uploadBufferToCDN() error = %v", err)
|
t.Fatalf("uploadBufferToCDN() error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue