feat(channels/weixin_ilink): 微信 iLink 渠道插件(扫码登录+长轮询+游标+context_token+AES-128-ECB 媒体+24h 主动推送窗口)
This commit is contained in:
1 parent
7831b0ea1b
commit
88dc12466d
13 files changed
+2627
No files matched your search
@@ -0,0 +1,109 @@
|
||||
package weixin_ilink
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// loginPollInterval is the default how often Connect polls the QR confirmation
|
||||
// status. It is the documented ~1s cadence; the Config.LoginInterval field
|
||||
// overrides it (used by tests to keep the flow short).
|
||||
const loginPollInterval = 1 * time.Second
|
||||
|
||||
// qrcodeResp is the get_bot_qrcode response.
|
||||
type qrcodeResp struct {
|
||||
Qrcode string `json:"qrcode"`
|
||||
QrcodeImgContent string `json:"qrcode_img_content"`
|
||||
}
|
||||
|
||||
// qrcodeStatusResp is the get_qrcode_status response. The token and ids are
|
||||
// only populated once status is "confirmed".
|
||||
type qrcodeStatusResp struct {
|
||||
Status string `json:"status"`
|
||||
BotToken string `json:"bot_token"`
|
||||
BotID string `json:"ilink_bot_id"`
|
||||
UserID string `json:"ilink_user_id"`
|
||||
BaseURL string `json:"baseurl"`
|
||||
}
|
||||
|
||||
// login runs the one-time scan-code binding (design/08 §4): request a QR code,
|
||||
// then poll get_qrcode_status until the user confirms. On confirmation it
|
||||
// persists bot_token, bot_id, owner_id and any returned baseurl, so later
|
||||
// requests carry the Bearer token.
|
||||
func (a *Adapter) login(ctx context.Context) error {
|
||||
loginCtx, cancel := context.WithTimeout(ctx, a.cfg.LoginTimeout)
|
||||
defer cancel()
|
||||
|
||||
qr, err := a.fetchQRCode(loginCtx)
|
||||
if err != nil {
|
||||
if loginCtx.Err() != nil {
|
||||
return fmt.Errorf("%w: %w", ErrLoginTimeout, loginCtx.Err())
|
||||
}
|
||||
return fmt.Errorf("weixin_ilink: fetch qrcode: %w", err)
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
a.qrcode = qr.Qrcode
|
||||
a.mu.Unlock()
|
||||
// Log only the renderable URL, never the raw qrcode (it is a secret).
|
||||
a.log.Info("scan the qr code to bind the bot", "url", qr.QrcodeImgContent)
|
||||
|
||||
ticker := time.NewTicker(a.cfg.LoginInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
status, err := a.pollQRStatus(loginCtx, qr.Qrcode)
|
||||
if err != nil {
|
||||
// A deadline/cancel during the poll means the overall window
|
||||
// elapsed; surface ErrLoginTimeout rather than the raw transport
|
||||
// error.
|
||||
if loginCtx.Err() != nil {
|
||||
return fmt.Errorf("%w: %w", ErrLoginTimeout, loginCtx.Err())
|
||||
}
|
||||
return fmt.Errorf("weixin_ilink: poll qr status: %w", err)
|
||||
}
|
||||
if status.Status == "confirmed" && status.BotToken != "" {
|
||||
a.mu.Lock()
|
||||
a.cfg.BotToken = status.BotToken
|
||||
a.cfg.BotID = status.BotID
|
||||
a.cfg.OwnerID = status.UserID
|
||||
if status.BaseURL != "" {
|
||||
a.cfg.BaseURL = status.BaseURL
|
||||
}
|
||||
a.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case <-loginCtx.Done():
|
||||
return fmt.Errorf("%w: %w", ErrLoginTimeout, loginCtx.Err())
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fetchQRCode requests a new bot login QR code.
|
||||
func (a *Adapter) fetchQRCode(ctx context.Context) (qrcodeResp, error) {
|
||||
path := a.apiURL("/ilink/bot/get_bot_qrcode?bot_type=" + strconv.Itoa(botType))
|
||||
var out qrcodeResp
|
||||
if err := a.doJSON(ctx, http.MethodGet, path, nil, &out); err != nil {
|
||||
return qrcodeResp{}, err
|
||||
}
|
||||
if out.Qrcode == "" {
|
||||
return qrcodeResp{}, fmt.Errorf("weixin_ilink: get_bot_qrcode returned empty qrcode")
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// pollQRStatus checks whether a QR code has been confirmed.
|
||||
func (a *Adapter) pollQRStatus(ctx context.Context, qrcode string) (qrcodeStatusResp, error) {
|
||||
path := a.apiURL("/ilink/bot/get_qrcode_status?qrcode=" + url.QueryEscape(qrcode))
|
||||
var out qrcodeStatusResp
|
||||
if err := a.doJSON(ctx, http.MethodGet, path, nil, &out); err != nil {
|
||||
return qrcodeStatusResp{}, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
Reference in new issue
Block a user