Documentation
¶
Overview ¶
Package ilink provides a client for the Tencent WeChat iLink Bot API.
iLink is Tencent's official WeChat personal account Bot API, exposed through the OpenClaw platform. It allows developers to receive and send WeChat messages over a standard HTTP/JSON long-poll interface.
Authentication ¶
Login is performed by scanning a QR code with the WeChat app. The resulting token must be persisted by the caller and supplied when constructing a Client.
login, err := ilink.StartLogin(ctx)
if err != nil { ... }
fmt.Println("Scan:", login.URL)
token, err := login.Wait(ctx)
if err != nil { ... }
// persist token.BotToken for reuse
Receiving messages ¶
Implement the Handler interface and call Client.ListenAndServe:
client := ilink.NewClient(token)
err := client.ListenAndServe(ctx, ilink.HandlerFunc(func(ctx context.Context, msg *ilink.Message) error {
fmt.Println(msg.From, ":", msg.Text())
return client.Reply(ctx, msg, ilink.TextMessage("你好!"))
}))
Sending messages ¶
Use Client.Send to send a message to any user:
err := client.Send(ctx, &ilink.OutboundMessage{
To: "o9cq800kum_xxx@im.wechat",
ContextToken: msg.ContextToken,
Items: []ilink.Item{ilink.TextMessage("Hello")},
})
Media ¶
Images, files, voices and videos are uploaded to Tencent CDN with AES-128-ECB encryption before sending. Use Client.UploadMedia to prepare a MediaItem.
Index ¶
- Constants
- Variables
- func DecryptMedia(item *CDNMedia, ciphertext []byte) ([]byte, error)
- func DownloadAndDecrypt(ctx context.Context, cdnURL string, item *CDNMedia) ([]byte, error)
- type APIError
- type CDNMedia
- type Client
- func (c *Client) ListenAndServe(ctx context.Context, h Handler) error
- func (c *Client) RecallMessage(ctx context.Context, msgID int64) error
- func (c *Client) Reply(ctx context.Context, to *Message, items ...Item) error
- func (c *Client) Send(ctx context.Context, msg *OutboundMessage) error
- func (c *Client) SendTyping(ctx context.Context, userID string, status bool) error
- func (c *Client) UploadMedia(ctx context.Context, data []byte, opts *UploadOptions) (*CDNMedia, error)
- type FileItem
- type Handler
- type HandlerFunc
- type ImageItem
- type Item
- type ItemType
- type Login
- type MediaItemdeprecated
- type Message
- type Option
- type OutboundMessage
- type RefMessage
- type TextItem
- type Token
- type UploadOptions
- type VideoItem
- type VoiceEncodeType
- type VoiceItem
Examples ¶
Constants ¶
const ( UploadMediaTypeImage = 1 UploadMediaTypeVideo = 2 UploadMediaTypeFile = 3 UploadMediaTypeVoice = 4 )
UploadMediaType constants mirror UploadMediaType from upstream types.ts.
Variables ¶
var ( // ErrQRCodeExpired is returned by Login.Wait when the QR code expires // before the user scans it. ErrQRCodeExpired = errors.New("ilink: QR code expired") // ErrQRCodeCancelled is returned by Login.Wait when the user cancels // the scan on the phone. ErrQRCodeCancelled = errors.New("ilink: QR code scan cancelled") // ErrLoginTimeout is returned by Login.Wait when the context deadline is // exceeded before the user confirms the scan. ErrLoginTimeout = errors.New("ilink: timed out waiting for QR code confirmation") // ErrSessionTimeout is returned by ListenAndServe when the server reports // errcode=-14, indicating the bot session has expired and re-login is // required. ErrSessionTimeout = errors.New("ilink: session timeout (re-login required)") )
Sentinel errors returned by the package.
Functions ¶
func DecryptMedia ¶
DecryptMedia decrypts a CDN-downloaded encrypted blob using the AES key stored in the CDNMedia. Use this to read images/files received from users.
data, _ := ilink.DownloadAndDecrypt(ctx, cdnURL, msg.Items[0].Image.Media) // or, if you downloaded the bytes yourself: plain, err := ilink.DecryptMedia(msg.Items[0].Image.Media, ciphertext)
Example ¶
ExampleDecryptMedia shows how to decrypt a received media file.
package main
import (
"context"
"log"
"os"
"github.com/lib-x/ilink"
)
func main() {
// Assume msg is an inbound *ilink.Message containing an image.
var msg *ilink.Message
for _, item := range msg.Items {
if item.Type != ilink.ItemTypeImage || item.Image == nil {
continue
}
img := item.Image
if img.Media == nil {
continue
}
// Use FullURL if available, otherwise construct from EncryptQueryParam.
cdnURL := img.Media.FullURL
if cdnURL == "" {
cdnURL = "https://novac2c.cdn.weixin.qq.com/c2c?" + img.Media.EncryptQueryParam
}
plain, err := ilink.DownloadAndDecrypt(context.Background(), cdnURL, img.Media)
if err != nil {
log.Fatal(err)
}
os.WriteFile("received.jpg", plain, 0o644)
}
}
Output:
func DownloadAndDecrypt ¶
DownloadAndDecrypt is a convenience function that fetches ciphertext from a CDN URL and decrypts it using the key stored in item.
When item.FullURL is populated the caller may use it directly; otherwise construct the URL from item.EncryptQueryParam and the CDN base:
https://novac2c.cdn.weixin.qq.com/c2c?<EncryptQueryParam>
Types ¶
type APIError ¶
type APIError struct {
// Ret is the error code returned by the server.
Ret int
// Op is the API endpoint that returned the error.
Op string
}
APIError represents a non-zero ret code returned by the iLink API.
type CDNMedia ¶ added in v0.1.1
type CDNMedia struct {
// EncryptQueryParam is the encrypted CDN parameter string used to
// download or reference the file.
EncryptQueryParam string
// AESKey is the base64-encoded 128-bit AES key.
AESKey string
// EncryptType: 0 = encrypt fileid only; 1 = pack thumbnail/mid-size info.
EncryptType int
// FullURL is the complete download URL returned by the server (when present).
// If empty, construct the URL from EncryptQueryParam and the CDN base.
FullURL string
}
CDNMedia is a CDN reference to an AES-128-ECB-encrypted media file. It is embedded in ImageItem, VoiceItem, FileItem, and VideoItem.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is a WeChat iLink bot client. Create one with NewClient.
A Client is safe for concurrent use by multiple goroutines.
func NewClient ¶
NewClient creates a new iLink Client authenticated with the given Token.
token, _ := login.Wait(ctx) client := ilink.NewClient(token)
func (*Client) ListenAndServe ¶
ListenAndServe starts the long-poll loop, calling h.ServeMessage for every inbound message. It blocks until ctx is cancelled or a non-recoverable error occurs, and returns ctx.Err() on clean shutdown.
If the server signals errcode=-14 (session timeout), ListenAndServe returns ErrSessionTimeout so the caller can re-authenticate.
Handler errors are logged via the client's logger and do not stop the loop. Use ctx cancellation to shut down gracefully.
err := client.ListenAndServe(ctx, ilink.HandlerFunc(func(ctx context.Context, msg *ilink.Message) error {
return client.Reply(ctx, msg, ilink.TextMessage("pong"))
}))
Example ¶
ExampleClient_ListenAndServe shows the minimal echo-bot pattern.
package main
import (
"context"
"encoding/json"
"log"
"os"
"os/signal"
"github.com/lib-x/ilink"
)
func main() {
// Load a previously persisted token.
data, err := os.ReadFile("token.json")
if err != nil {
log.Fatal(err)
}
var token ilink.Token
if err := json.Unmarshal(data, &token); err != nil {
log.Fatal(err)
}
client := ilink.NewClient(token)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
err = client.ListenAndServe(ctx, ilink.HandlerFunc(func(ctx context.Context, msg *ilink.Message) error {
log.Printf("message from %s: %s", msg.From, msg.Text())
return client.Reply(ctx, msg, ilink.TextMessage("pong"))
}))
if err != nil {
log.Fatal(err)
}
}
Output:
func (*Client) RecallMessage ¶ added in v0.1.1
RecallMessage withdraws a previously sent message. msgID is the message_id field from an outbound message's server response.
Note: message recall may have a time limit enforced by WeChat.
func (*Client) Reply ¶
Reply is a convenience wrapper around Client.Send that copies the ContextToken from an inbound message so WeChat routes the reply correctly.
return client.Reply(ctx, msg, ilink.TextMessage("pong"))
func (*Client) Send ¶
func (c *Client) Send(ctx context.Context, msg *OutboundMessage) error
Send delivers an OutboundMessage to a WeChat user.
Example ¶
ExampleClient_Send shows how to proactively send a message to a user.
package main
import (
"context"
"log"
"github.com/lib-x/ilink"
)
func main() {
client := ilink.NewClient(ilink.Token{BotToken: "…"})
err := client.Send(context.Background(), &ilink.OutboundMessage{
To: "o9cq800kum_xxx@im.wechat",
ContextToken: "AARzJWAF…", // from a previous inbound message
Items: []ilink.Item{ilink.TextMessage("Hello!")},
})
if err != nil {
log.Fatal(err)
}
}
Output:
func (*Client) SendTyping ¶
SendTyping sends a typing status indicator to the given user. status=true starts the "typing…" indicator; status=false cancels it.
func (*Client) UploadMedia ¶
func (c *Client) UploadMedia(ctx context.Context, data []byte, opts *UploadOptions) (*CDNMedia, error)
UploadMedia encrypts data with a fresh AES-128 key, uploads the ciphertext to Tencent CDN, and returns a CDNMedia ready to embed in an Item.
data, _ := os.ReadFile("photo.jpg")
media, err := client.UploadMedia(ctx, data, &ilink.UploadOptions{
FileName: "photo.jpg",
MediaType: ilink.UploadMediaTypeImage,
ToUserID: msg.From,
})
if err != nil { ... }
err = client.Reply(ctx, msg, ilink.Item{
Type: ilink.ItemTypeImage,
Image: &ilink.ImageItem{Media: media},
})
Example ¶
ExampleClient_UploadMedia shows how to upload an image and send it.
package main
import (
"context"
"log"
"os"
"github.com/lib-x/ilink"
)
func main() {
client := ilink.NewClient(ilink.Token{BotToken: "…"})
ctx := context.Background()
data, err := os.ReadFile("photo.jpg")
if err != nil {
log.Fatal(err)
}
media, err := client.UploadMedia(ctx, data, &ilink.UploadOptions{
FileName: "photo.jpg",
MediaType: ilink.UploadMediaTypeImage,
ToUserID: "o9cq800kum_xxx@im.wechat",
})
if err != nil {
log.Fatal(err)
}
// Send the uploaded image in a message.
err = client.Send(ctx, &ilink.OutboundMessage{
To: "o9cq800kum_xxx@im.wechat",
ContextToken: "AARzJWAF…",
Items: []ilink.Item{
{
Type: ilink.ItemTypeImage,
Image: &ilink.ImageItem{Media: media},
},
},
})
if err != nil {
log.Fatal(err)
}
}
Output:
type FileItem ¶ added in v0.1.1
type FileItem struct {
// Media is the CDN reference to the file.
Media *CDNMedia
// FileName is the original file name.
FileName string
// MD5 is the plaintext file MD5 (hex).
MD5 string
// Size is the plaintext file size in bytes.
Size int64
}
FileItem is a file attachment.
type Handler ¶
Handler is implemented by any value that can handle an inbound Message. It mirrors the net/http.Handler contract: return nil on success; return a non-nil error to signal that processing failed (the polling loop logs the error and continues).
type HandlerFunc ¶
HandlerFunc is an adapter to allow the use of ordinary functions as [Handler]s, analogous to net/http.HandlerFunc.
func (HandlerFunc) ServeMessage ¶
func (f HandlerFunc) ServeMessage(ctx context.Context, msg *Message) error
ServeMessage calls f(ctx, msg).
type ImageItem ¶ added in v0.1.1
type ImageItem struct {
// Media is the CDN reference to the full-size image.
Media *CDNMedia
// ThumbMedia is the CDN reference to the thumbnail, if present.
ThumbMedia *CDNMedia
// AESKey is the raw hex AES-128 key (16 bytes) preferred for inbound
// decryption over Media.AESKey.
AESKey string
// URL is a direct image URL (when provided by the server).
URL string
// Dimensions of the thumbnail and high-def variants (pixels / bytes).
ThumbWidth int
ThumbHeight int
ThumbSize int
MidSize int
HDSize int
}
ImageItem is an image message with optional thumbnail.
type Item ¶
type Item struct {
Type ItemType
Text *TextItem
Image *ImageItem
Voice *VoiceItem
File *FileItem
Video *VideoItem
// RefMsg is set when this item quotes a previously sent message.
RefMsg *RefMessage
// MsgID is the per-item message ID, set on inbound items.
MsgID string
// CreateTimeMs is the item creation timestamp in milliseconds (inbound only).
CreateTimeMs int64
// UpdateTimeMs is the item last-update timestamp in milliseconds (inbound only).
UpdateTimeMs int64
// IsCompleted indicates the item content is fully delivered (inbound only).
IsCompleted bool
}
Item is a single content element inside a message. Exactly one of the item fields will be non-nil, determined by Type.
func TextMessage ¶
TextMessage returns an Item containing a plain text payload.
type Login ¶
type Login struct {
// QRCode is the raw QR code string that can be encoded as an image.
QRCode string
// URL is a pre-rendered QR code image URL ready for display in a browser.
URL string
// contains filtered or unexported fields
}
Login represents an in-progress QR code login session. Call StartLogin to begin, then Login.Wait to block until the user scans.
func StartLogin ¶
StartLogin initiates a new QR code login session and returns a Login value. Display Login.URL or encode Login.QRCode as an image, then call Login.Wait.
Example ¶
ExampleStartLogin shows how to perform a QR code login and persist the token.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"github.com/lib-x/ilink"
)
func main() {
ctx := context.Background()
login, err := ilink.StartLogin(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println("Scan this URL with WeChat:", login.URL)
token, err := login.Wait(ctx)
if err != nil {
log.Fatal(err)
}
// Persist token.BotToken for reuse across restarts.
data, _ := json.Marshal(token)
os.WriteFile("token.json", data, 0o600)
}
Output:
type Message ¶
type Message struct {
// Seq is the message sequence number.
Seq int64
// MessageID is the unique message ID.
MessageID int64
// From is the sender's user ID (format: "xxx@im.wechat").
From string
// To is the bot's own user ID (format: "xxx@im.bot").
To string
// GroupID is set when the message was sent in a group chat.
GroupID string
// SessionID is the conversation session identifier.
SessionID string
// CreateTimeMs is the message creation timestamp in milliseconds.
CreateTimeMs int64
// UpdateTimeMs is the message last-update timestamp in milliseconds.
UpdateTimeMs int64
// ContextToken must be echoed back verbatim when replying, so WeChat
// associates the reply with the correct conversation window.
ContextToken string
// Items holds the message content elements in order.
Items []Item
}
Message is an inbound message received from a WeChat user.
func (*Message) Text ¶
Text returns the concatenated text of all ItemTypeText items in the message, or an empty string if there are none.
type Option ¶
type Option func(*Client)
Option configures a Client. Pass options to NewClient.
func WithBaseURL ¶
WithBaseURL overrides the iLink API base URL. Useful for testing against a mock server. The Token.BaseURL returned during login takes precedence.
func WithHTTPClient ¶
WithHTTPClient replaces the default HTTP client used for all API calls.
func WithLogger ¶
WithLogger sets the structured logger used by Client.ListenAndServe to report polling errors and handler panics. The default is slog.Default.
type OutboundMessage ¶
type OutboundMessage struct {
// To is the recipient user ID (format: "xxx@im.wechat").
To string
// GroupID optionally targets a group chat.
GroupID string
// ContextToken must match the context_token from the inbound message
// that triggered this reply, so WeChat routes it to the right chat window.
// Use [Client.Reply] to populate this automatically.
ContextToken string
// Items contains the content to send. At least one item is required.
Items []Item
}
OutboundMessage is a message to be sent to a WeChat user.
type RefMessage ¶ added in v0.1.1
type RefMessage struct {
// Item is the referenced message item.
Item *Item
// Title is a plaintext summary of the referenced content.
Title string
}
RefMessage represents a quoted / referenced message.
type Token ¶
type Token struct {
// BotToken is the Bearer token used to authenticate all API requests.
BotToken string `json:"bot_token"`
// BaseURL is the API base URL returned by the server for this account.
// If empty, the global default is used.
BaseURL string `json:"baseurl,omitempty"`
}
Token holds the credentials obtained after a successful QR code login. Persist this value; supply it to NewClient to avoid re-scanning on restart.
type UploadOptions ¶
type UploadOptions struct {
// FileName is embedded in the CDN reference for file-type attachments.
FileName string
// MediaType selects the upload slot: use the UploadMediaType* constants.
// Defaults to UploadMediaTypeImage when zero.
MediaType int
// ToUserID is the target recipient's user ID. Required for routing when
// the server needs it to resolve the CDN bucket.
ToUserID string
// ThumbData is raw thumbnail bytes for IMAGE or VIDEO uploads.
// When set, a separate CDN slot is allocated for the thumbnail.
ThumbData []byte
// NoThumb suppresses thumbnail allocation even when ThumbData is empty.
NoThumb bool
}
UploadOptions configures a Client.UploadMedia call.
type VideoItem ¶
type VideoItem struct {
// Media is the CDN reference to the video.
Media *CDNMedia
// ThumbMedia is the CDN reference to the thumbnail.
ThumbMedia *CDNMedia
// VideoSize is the plaintext video size in bytes.
VideoSize int
// PlayLengthMs is the video duration in milliseconds.
PlayLengthMs int
// VideoMD5 is the plaintext video MD5.
VideoMD5 string
ThumbSize int
ThumbWidth int
ThumbHeight int
}
VideoItem is a video message with an optional thumbnail.
type VoiceEncodeType ¶ added in v0.1.1
type VoiceEncodeType int
VoiceEncodeType indicates the audio encoding of a voice message.
const ( VoiceEncodePCM VoiceEncodeType = 1 VoiceEncodeADPCM VoiceEncodeType = 2 VoiceEncodeFeature VoiceEncodeType = 3 VoiceEncodeSpeex VoiceEncodeType = 4 VoiceEncodeAMR VoiceEncodeType = 5 VoiceEncodeSilk VoiceEncodeType = 6 VoiceEncodeMP3 VoiceEncodeType = 7 VoiceEncodeOGGSpeex VoiceEncodeType = 8 )
type VoiceItem ¶
type VoiceItem struct {
// Media is the CDN reference to the voice file.
Media *CDNMedia
// EncodeType is the audio encoding; [VoiceEncodeSilk] is the most common.
EncodeType VoiceEncodeType
// BitsPerSample and SampleRate describe the audio format.
BitsPerSample int
SampleRate int
// PlaytimeMs is the audio duration in milliseconds.
PlaytimeMs int
// RecognText is the ASR transcript (inbound only, may be empty).
RecognText string
}
VoiceItem is a voice message.