Files
weixin_ClawBot_Bridge/examples/c/ilink_client.c
T

276 lines
12 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ilink_client.c — 微信 ClawBot iLink 最小示例(C,依赖 libcurl)
* 流程: 扫码登录 → 长轮询收消息 → 回声回复
* 编译: gcc ilink_client.c -o ilink -lcurl
*
* 注意: 为控制依赖,JSON 用最朴素的字符串查找提取,仅适合演示。
* 正式项目请引入 cJSON,媒体加解密见 docs/03(可用 OpenSSL AES-128-ECB)。
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <sys/stat.h>
#include <curl/curl.h>
#define BASE_URL "https://ilinkai.weixin.qq.com"
#define STATE_FILE "bridge-state.json"
/* 扫码轮询 base;收到 scaned_but_redirect 时切换,get_bot_qrcode 仍走固定 BASE_URL */
static char g_base_url[256] = BASE_URL;
/* ---------- 响应缓冲 ---------- */
struct buf { char *data; size_t len; };
static size_t on_write(char *ptr, size_t size, size_t nmemb, void *ud) {
struct buf *b = ud;
size_t n = size * nmemb;
b->data = realloc(b->data, b->len + n + 1);
memcpy(b->data + b->len, ptr, n);
b->len += n;
b->data[b->len] = 0;
return n;
}
/* ---------- 状态: bot_token / 游标(极简 key=value 行存储) ---------- */
static char g_token[512] = "", g_cursor[1024] = "";
static void load_state(void) {
FILE *f = fopen(STATE_FILE, "r");
if (!f) return;
char line[2048], key[64], val[1900];
while (fgets(line, sizeof line, f))
if (sscanf(line, "%63[^=]=%1899[^\n]", key, val) == 2) {
if (!strcmp(key, "bot_token")) snprintf(g_token, sizeof g_token, "%s", val);
if (!strcmp(key, "cursor")) snprintf(g_cursor, sizeof g_cursor, "%s", val);
}
fclose(f);
}
static void save_state(void) {
FILE *f = fopen(STATE_FILE, "w");
fprintf(f, "bot_token=%s\ncursor=%s\n", g_token, g_cursor);
fclose(f);
chmod(STATE_FILE, 0600);
}
/* ---------- 通用请求: 鉴权头 + 防重放 X-WECHAT-UIN + iLink-App 标识 ---------- */
/* 注: get_bot_qrcode / get_qrcode_status(预绑定阶段)无 token 时不带 Authorization,
但 iLink-App-Id / iLink-App-ClientVersion / AuthorizationType / X-WECHAT-UIN 恒带。 */
static char *http(const char *base, const char *method, const char *path, const char *json_body, long timeout) {
CURL *c = curl_easy_init();
struct buf b = {0};
char url[512], uin[32], auth[600];
snprintf(url, sizeof url, "%s%s", base, path);
/* X-WECHAT-UIN: base64(十进制字符串(随机uint32)),简化用伪随机 */
snprintf(uin, sizeof uin, "%u", (unsigned)(rand() ^ (unsigned)time(NULL)));
char uin_b64[64];
/* 极简 base64(仅数字串,查表法) */
const char *T = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
size_t ul = strlen(uin), o = 0;
for (size_t i = 0; i < ul; i += 3) {
unsigned v = uin[i] << 16 | (i+1<ul ? uin[i+1] : 0) << 8 | (i+2<ul ? uin[i+2] : 0);
uin_b64[o++] = T[v>>18&63]; uin_b64[o++] = T[v>>12&63];
uin_b64[o++] = i+1<ul ? T[v>>6&63] : '=';
uin_b64[o++] = i+2<ul ? T[v&63] : '=';
}
uin_b64[o] = 0;
struct curl_slist *hdrs = NULL;
hdrs = curl_slist_append(hdrs, "Content-Type: application/json");
hdrs = curl_slist_append(hdrs, "AuthorizationType: ilink_bot_token");
char uin_hdr[96]; snprintf(uin_hdr, sizeof uin_hdr, "X-WECHAT-UIN: %s", uin_b64);
hdrs = curl_slist_append(hdrs, uin_hdr);
/* 2.4.8 -> (2&0xff)<<16 | (4&0xff)<<8 | (8&0xff) = 132104(数字字符串,非 "1") */
hdrs = curl_slist_append(hdrs, "iLink-App-Id: bot");
hdrs = curl_slist_append(hdrs, "iLink-App-ClientVersion: 132104");
if (g_token[0]) {
snprintf(auth, sizeof auth, "Authorization: Bearer %s", g_token);
hdrs = curl_slist_append(hdrs, auth);
}
curl_easy_setopt(c, CURLOPT_URL, url);
curl_easy_setopt(c, CURLOPT_HTTPHEADER, hdrs);
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, on_write);
curl_easy_setopt(c, CURLOPT_WRITEDATA, &b);
curl_easy_setopt(c, CURLOPT_TIMEOUT, timeout);
if (!strcmp(method, "POST")) curl_easy_setopt(c, CURLOPT_POSTFIELDS, json_body);
CURLcode rc = curl_easy_perform(c);
curl_slist_free_all(hdrs);
curl_easy_cleanup(c);
if (rc != CURLE_OK) { free(b.data); return NULL; } /* 超时/网络错误返回 NULL */
return b.data ? b.data : strdup("");
}
/* ---------- 极简 JSON 字段提取(仅演示!正式项目用 cJSON) ---------- */
static int pick(const char *json, const char *key, char *out, size_t outsz) {
char pat[128]; snprintf(pat, sizeof pat, "\"%s\":\"", key);
const char *p = strstr(json, pat);
if (!p) return 0;
p += strlen(pat);
const char *e = strchr(p, '"');
if (!e || (size_t)(e - p) >= outsz) return 0;
memcpy(out, p, e - p); out[e - p] = 0;
return 1;
}
/* ---------- 接口1: POST /ilink/bot/get_bot_qrcode 获取登录二维码(官方姿势) ---------- */
static char *fetch_qrcode(void) {
/* 官方为 POST + body {"local_token_list":[]},非裸 GET(裸 GET 也能拿到码但非官方姿势)。
首次绑定传空数组;若已持久化其它 bot_token 可收集传入(最多 10 个)。 */
return http(BASE_URL, "POST", "/ilink/bot/get_bot_qrcode?bot_type=3",
"{\"local_token_list\":[]}", 15);
}
/* ---------- 接口2: GET /ilink/bot/get_qrcode_status 轮询扫码状态(完整状态机) ---------- */
static void login(void) {
/* 状态机: wait / scaned / scaned_but_redirect / need_verifycode /
verify_code_blocked / binded_redirect / confirmed / expired
两大坑(务必读 docs/02 §4.5):
① 码服务端 TTL≈60s,生成后须在窗口内扫(超窗服务端自返回 expired,此处换新码)。
② 「换码竞态」:一旦收到 scaned(或 scaned_but_redirect / need_verifycode),
绝不能再换码——换码会砍掉"扫码→确认"会话,微信端显示成功但后端永远收不到 confirmed。 */
char *qr = fetch_qrcode();
char qrcode[256], qrurl[512];
if (!qr || !pick(qr, "qrcode", qrcode, sizeof qrcode)) { fprintf(stderr, "获取二维码失败\n"); exit(1); }
pick(qr, "qrcode_img_content", qrurl, sizeof qrurl);
printf("请用微信扫码: %s\n", qrurl);
printf("(二维码内容即上面的 liteapp URL,请用二维码库编码展示;勿把 qrcode 字段当二维码)\n");
free(qr);
char verify[32] = "";
char status[32], token[512];
for (int i = 0; i < 480; i++) { /* 最多等 8 分钟 */
char path[600];
snprintf(path, sizeof path, "/ilink/bot/get_qrcode_status?qrcode=%s", qrcode);
if (verify[0]) {
size_t len = strlen(path);
snprintf(path + len, sizeof path - len, "&verify_code=%s", verify);
}
char *s = http(g_base_url, "GET", path, NULL, 15);
if (s && pick(s, "status", status, sizeof status)) {
if (!strcmp(status, "confirmed")) {
pick(s, "bot_token", token, sizeof token);
snprintf(g_token, sizeof g_token, "%s", token);
save_state();
printf("登录成功\n");
free(s);
return;
} else if (!strcmp(status, "scaned")) {
verify[0] = 0;
printf("已扫码,请在手机上确认...(此后绝不换码,避免砍掉确认会话)\n");
} else if (!strcmp(status, "scaned_but_redirect")) {
char host[256]; verify[0] = 0;
if (pick(s, "redirect_host", host, sizeof host) && host[0]) {
snprintf(g_base_url, sizeof g_base_url, "https://%s", host);
printf("IDC 重定向,切换轮询至 %s\n", g_base_url);
}
} else if (!strcmp(status, "need_verifycode")) {
printf("请在手机微信上输入显示的数字后回车: ");
fflush(stdout);
if (fgets(verify, sizeof verify, stdin)) {
char *nl = strchr(verify, '\n'); if (nl) *nl = 0;
}
continue;
} else if (!strcmp(status, "verify_code_blocked")) {
char *nq; verify[0] = 0;
printf("配对码多次输入错误,重新获取二维码\n");
nq = fetch_qrcode();
if (!nq || !pick(nq, "qrcode", qrcode, sizeof qrcode)) { free(nq); fprintf(stderr, "获取二维码失败\n"); exit(1); }
pick(nq, "qrcode_img_content", qrurl, sizeof qrurl);
printf("新二维码: %s\n", qrurl);
free(nq);
continue;
} else if (!strcmp(status, "binded_redirect")) {
printf("该 bot 已绑定过本实例,视为连接成功\n");
free(s);
return;
} else if (!strcmp(status, "expired")) {
char *nq; verify[0] = 0;
/* 码过期(服务端作废)→ 换新码;仅在 expired 时换,scaned 后绝不换 */
printf("二维码已过期,重新获取\n");
nq = fetch_qrcode();
if (!nq || !pick(nq, "qrcode", qrcode, sizeof qrcode)) { free(nq); fprintf(stderr, "获取二维码失败\n"); exit(1); }
pick(nq, "qrcode_img_content", qrurl, sizeof qrurl);
printf("新二维码: %s\n", qrurl);
free(nq);
continue;
} else {
fprintf(stderr, "未知状态: %s\n", status);
}
}
free(s);
sleep(1);
}
fprintf(stderr, "扫码超时\n"); exit(1);
}
/* ---------- 你的 AI 后端: 替换成自己项目的调用 ---------- */
static void ask_your_ai(const char *peer, const char *text, char *out, size_t n) {
snprintf(out, n, "你说的是:%s", text);
}
/* ---------- 接口4: POST /ilink/bot/sendmessage ---------- */
static void send_text(const char *to, const char *text, const char *ctx_token) {
/* 演示不处理 JSON 转义与分片,正式项目务必转义 \ " 换行 并按需分片 */
char body[4096];
snprintf(body, sizeof body,
"{\"msg\":{\"to_user_id\":\"%s\",\"client_id\":\"c-%ld\","
"\"message_type\":2,\"message_state\":2,\"context_token\":\"%s\","
"\"item_list\":[{\"type\":1,\"text_item\":{\"text\":\"%s\"}}]}}",
to, (long)time(NULL), ctx_token, text);
free(http(BASE_URL, "POST", "/ilink/bot/sendmessage", body, 15));
}
/* ---------- 接口3: POST /ilink/bot/getupdates 长轮询 ---------- */
static void poll_loop(void) {
int failures = 0;
char body[1200];
for (;;) {
snprintf(body, sizeof body,
"{\"get_updates_buf\":\"%s\",\"base_info\":{\"channel_version\":\"1.0.2\"}}",
g_cursor);
char *resp = http(BASE_URL, "POST", "/ilink/bot/getupdates", body, 45);
if (!resp) { /* 超时/网络错误 */
failures++;
int wait = failures <= 3 ? 2 : 30;
fprintf(stderr, "轮询异常(%d次),%ds 后重试\n", failures, wait);
sleep(wait);
continue;
}
if (strstr(resp, "\"errcode\":-14")) {
fprintf(stderr, "会话过期(errcode=-14),请删除 %s 重新扫码\n", STATE_FILE);
free(resp); exit(2);
}
char buf[1024];
if (pick(resp, "get_updates_buf", buf, sizeof buf) && buf[0]) {
snprintf(g_cursor, sizeof g_cursor, "%s", buf);
save_state(); /* 游标立即落盘 */
}
/* 演示级提取: 仅取第一条消息的文本/context_token/from_user_id */
char peer[128], token[512], text[1500];
if (pick(resp, "from_user_id", peer, sizeof peer) &&
pick(resp, "context_token", token, sizeof token) &&
pick(resp, "text", text, sizeof text) &&
!strstr(resp, "\"message_type\":2")) {
char reply[1700];
ask_your_ai(peer, text, reply, sizeof reply);
send_text(peer, reply, token);
}
free(resp);
failures = 0;
}
}
int main(void) {
srand((unsigned)time(NULL));
curl_global_init(CURL_GLOBAL_DEFAULT);
load_state();
if (!g_token[0]) login();
poll_loop();
curl_global_cleanup();
return 0;
}