feat(weixin_ilink): 扫码协议与腾讯官方对齐——POST local_token_list + iLink-App-Id/ClientVersion 头 + need_verifycode/redirect 状态机
This commit is contained in:
1 parent
bef398d47a
commit
f5950e4bde
9 files changed
+508
-99
No files matched your search
@@ -1,7 +1,9 @@
|
||||
package weixin_ilink
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -29,6 +31,9 @@ type qrcodeStatusResp struct {
|
||||
BotID string `json:"ilink_bot_id"`
|
||||
UserID string `json:"ilink_user_id"`
|
||||
BaseURL string `json:"baseurl"`
|
||||
// RedirectHost carries the IDC host to poll next when status is
|
||||
// "scaned_but_redirect" (official currentApiBaseUrl redirect).
|
||||
RedirectHost string `json:"redirect_host"`
|
||||
}
|
||||
|
||||
// login is the Connect-side scan-code binding (design/08 §4). It joins (or
|
||||
@@ -81,10 +86,37 @@ func (a *Adapter) runLogin(sess *loginSession, sessCtx context.Context) {
|
||||
}
|
||||
return
|
||||
}
|
||||
if status.Status == "confirmed" && status.BotToken != "" {
|
||||
switch status.Status {
|
||||
case "confirmed":
|
||||
if status.BotToken == "" {
|
||||
a.failLogin(sess, fmt.Errorf("weixin_ilink: login confirmed but no bot_token returned"))
|
||||
return
|
||||
}
|
||||
a.completeLogin(sess, status)
|
||||
return
|
||||
case "binded_redirect":
|
||||
// The scanned bot is already bound to this instance; no new credentials
|
||||
// are issued. Treat it as an already-bound (successful) outcome.
|
||||
a.completeBindedRedirect(sess)
|
||||
return
|
||||
case "verify_code_blocked":
|
||||
a.failLogin(sess, fmt.Errorf("weixin_ilink: verify code blocked, too many failed attempts"))
|
||||
return
|
||||
case "scaned_but_redirect":
|
||||
if status.RedirectHost != "" {
|
||||
a.setPollBaseURL(status.RedirectHost)
|
||||
a.log.Info("weixin_ilink: idc redirect, switching polling host", "host", status.RedirectHost)
|
||||
} else {
|
||||
a.log.Warn("weixin_ilink: scaned_but_redirect without redirect_host, continuing on current host")
|
||||
}
|
||||
case "need_verifycode":
|
||||
// The server asks for a pairing code shown in the phone WeChat. Expose
|
||||
// the state and a hint so the panel can prompt the user; the code itself
|
||||
// is not yet plumbed back to the poll (see TODO).
|
||||
a.setLoginState(LoginStatusNeedVerifyCode, loginNeedVerifyCodeHint)
|
||||
}
|
||||
// wait / scaned / expired / need_verifycode / scaned_but_redirect keep
|
||||
// polling until confirmed or the window elapses.
|
||||
select {
|
||||
case <-timeoutCtx.Done():
|
||||
a.failLogin(sess, fmt.Errorf("%w: %w", ErrLoginTimeout, timeoutCtx.Err()))
|
||||
@@ -119,6 +151,25 @@ func (a *Adapter) completeLogin(sess *loginSession, status qrcodeStatusResp) {
|
||||
sess.done <- nil
|
||||
}
|
||||
|
||||
// completeBindedRedirect treats binded_redirect as an already-bound outcome: the
|
||||
// scanned bot is already linked to this instance, so the server issues no new
|
||||
// credentials and any existing token remains valid.
|
||||
func (a *Adapter) completeBindedRedirect(sess *loginSession) {
|
||||
a.mu.Lock()
|
||||
a.connected = a.cfg.BotToken != ""
|
||||
a.loginStatus = LoginStatusConfirmed
|
||||
a.loginDetail = loginBindedRedirectDetail
|
||||
a.mu.Unlock()
|
||||
|
||||
a.loginMu.Lock()
|
||||
if a.loginSession == sess {
|
||||
a.loginSession = nil
|
||||
}
|
||||
a.loginMu.Unlock()
|
||||
|
||||
sess.done <- nil
|
||||
}
|
||||
|
||||
// failLogin records a failed scan-login and signals the waiting Connect path.
|
||||
func (a *Adapter) failLogin(sess *loginSession, err error) {
|
||||
a.setLoginState(LoginStatusFailed, err.Error())
|
||||
@@ -132,11 +183,30 @@ func (a *Adapter) failLogin(sess *loginSession, err error) {
|
||||
sess.done <- err
|
||||
}
|
||||
|
||||
// fetchQRCode requests a new bot login QR code.
|
||||
// localTokenList returns the locally-held bot tokens to advertise on
|
||||
// get_bot_qrcode, so an already-bound bot can be detected (binded_redirect). On a
|
||||
// fresh bind it returns an empty (non-nil) slice so the wire carries `[]` rather
|
||||
// than `null`, matching the official client.
|
||||
func (a *Adapter) localTokenList() []string {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
if a.cfg.BotToken == "" {
|
||||
return []string{}
|
||||
}
|
||||
return []string{a.cfg.BotToken}
|
||||
}
|
||||
|
||||
// fetchQRCode requests a new bot login QR code. The official client issues this as
|
||||
// a POST carrying local_token_list so the server can detect an existing bind and
|
||||
// answer binded_redirect instead of a fresh code.
|
||||
func (a *Adapter) fetchQRCode(ctx context.Context) (qrcodeResp, error) {
|
||||
path := a.apiURL("/ilink/bot/get_bot_qrcode?bot_type=" + strconv.Itoa(botType))
|
||||
body, err := json.Marshal(map[string]any{"local_token_list": a.localTokenList()})
|
||||
if err != nil {
|
||||
return qrcodeResp{}, fmt.Errorf("weixin_ilink: marshal get_bot_qrcode body: %w", err)
|
||||
}
|
||||
var out qrcodeResp
|
||||
if err := a.doJSON(ctx, http.MethodGet, path, nil, &out); err != nil {
|
||||
if err := a.doJSON(ctx, http.MethodPost, path, bytes.NewReader(body), &out); err != nil {
|
||||
return qrcodeResp{}, err
|
||||
}
|
||||
if out.Qrcode == "" {
|
||||
@@ -145,11 +215,12 @@ func (a *Adapter) fetchQRCode(ctx context.Context) (qrcodeResp, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// pollQRStatus checks whether a QR code has been confirmed.
|
||||
// pollQRStatus checks whether a QR code has been confirmed. It polls the IDC
|
||||
// redirect host if one was recorded (scaned_but_redirect), else the base URL.
|
||||
func (a *Adapter) pollQRStatus(ctx context.Context, qrcode string) (qrcodeStatusResp, error) {
|
||||
path := a.apiURL("/ilink/bot/get_qrcode_status?qrcode=" + url.QueryEscape(qrcode))
|
||||
path := "/ilink/bot/get_qrcode_status?qrcode=" + url.QueryEscape(qrcode)
|
||||
var out qrcodeStatusResp
|
||||
if err := a.doJSON(ctx, http.MethodGet, path, nil, &out); err != nil {
|
||||
if err := a.doJSON(ctx, http.MethodGet, joinURL(a.pollBase(), path), nil, &out); err != nil {
|
||||
return qrcodeStatusResp{}, err
|
||||
}
|
||||
return out, nil
|
||||
|
||||
Reference in new issue
Block a user