tca-js 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/proto.js DELETED
@@ -1,162 +0,0 @@
1
- "use strict";
2
- /**
3
- * Protobuf tối thiểu, không cần file `.proto`.
4
- *
5
- * TikTok không công bố `.proto` của IM — số trường ở `constants.ts` dựng lại từ byte bắt được
6
- * trên trình duyệt (09/11/2026). Chỉ cần ba kiểu dây: varint (0), độ dài (2), và bỏ qua 1/5.
7
- */
8
- Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.Writer = exports.Msg = void 0;
10
- exports.decodeFields = decodeFields;
11
- function readVarint(b, i) {
12
- let x = 0n;
13
- let shift = 0n;
14
- let c;
15
- do {
16
- if (i >= b.length)
17
- throw new Error('varint vượt cuối buffer');
18
- c = b[i++];
19
- x |= BigInt(c & 127) << shift;
20
- shift += 7n;
21
- } while (c & 128);
22
- return [x, i];
23
- }
24
- /** Tách một message thành danh sách trường. `null` nếu byte không phải protobuf hợp lệ. */
25
- function decodeFields(b) {
26
- const out = [];
27
- let i = 0;
28
- try {
29
- while (i < b.length) {
30
- const [key, j] = readVarint(b, i);
31
- i = j;
32
- const wire = Number(key & 7n);
33
- const n = Number(key >> 3n);
34
- if (n === 0)
35
- return null;
36
- if (wire === 0) {
37
- const [v, k] = readVarint(b, i);
38
- out.push({ n, wire, int: v, bytes: Buffer.alloc(0) });
39
- i = k;
40
- }
41
- else if (wire === 2) {
42
- const [len, k] = readVarint(b, i);
43
- const end = k + Number(len);
44
- if (end > b.length)
45
- return null;
46
- out.push({ n, wire, int: 0n, bytes: b.subarray(k, end) });
47
- i = end;
48
- }
49
- else if (wire === 1 || wire === 5) {
50
- const size = wire === 1 ? 8 : 4;
51
- if (i + size > b.length)
52
- return null;
53
- out.push({ n, wire, int: 0n, bytes: b.subarray(i, i + size) });
54
- i += size;
55
- }
56
- else {
57
- return null;
58
- }
59
- }
60
- }
61
- catch {
62
- return null;
63
- }
64
- return out;
65
- }
66
- /** Message đã giải — đọc theo số trường. */
67
- class Msg {
68
- fields;
69
- constructor(fields) {
70
- this.fields = fields;
71
- }
72
- static parse(b) {
73
- const f = decodeFields(b);
74
- if (!f)
75
- throw new Error('Không giải được protobuf');
76
- return new Msg(f);
77
- }
78
- static tryParse(b) {
79
- if (!b)
80
- return null;
81
- const f = decodeFields(b);
82
- return f ? new Msg(f) : null;
83
- }
84
- has(n) {
85
- return this.fields.some((f) => f.n === n);
86
- }
87
- int(n) {
88
- return this.fields.find((f) => f.n === n)?.int ?? 0n;
89
- }
90
- num(n) {
91
- return Number(this.int(n));
92
- }
93
- /** Số nguyên 64-bit dạng chuỗi — uid / mã tin vượt `Number.MAX_SAFE_INTEGER`. */
94
- id(n) {
95
- return this.int(n).toString();
96
- }
97
- bytes(n) {
98
- return this.fields.find((f) => f.n === n)?.bytes ?? Buffer.alloc(0);
99
- }
100
- str(n) {
101
- return this.bytes(n).toString('utf8');
102
- }
103
- strs(n) {
104
- return this.fields.filter((f) => f.n === n).map((f) => f.bytes.toString('utf8'));
105
- }
106
- msg(n) {
107
- return Msg.tryParse(this.fields.find((f) => f.n === n)?.bytes);
108
- }
109
- msgs(n) {
110
- return this.fields
111
- .filter((f) => f.n === n)
112
- .map((f) => Msg.tryParse(f.bytes))
113
- .filter((m) => m !== null);
114
- }
115
- /** `map<string,string>` — mỗi phần tử là message `{1: key, 2: value}`. */
116
- map(n) {
117
- const out = {};
118
- for (const m of this.msgs(n))
119
- out[m.str(1)] = m.str(2);
120
- return out;
121
- }
122
- }
123
- exports.Msg = Msg;
124
- function varint(x) {
125
- let v = BigInt(x);
126
- if (v < 0n)
127
- v = BigInt.asUintN(64, v);
128
- const out = [];
129
- do {
130
- const c = Number(v & 127n);
131
- v >>= 7n;
132
- out.push(v ? c | 128 : c);
133
- } while (v);
134
- return Buffer.from(out);
135
- }
136
- const key = (n, wire) => varint((BigInt(n) << 3n) | BigInt(wire));
137
- /** Ghép message theo thứ tự gọi. */
138
- class Writer {
139
- parts = [];
140
- int(n, v) {
141
- this.parts.push(key(n, 0), varint(typeof v === 'string' ? BigInt(v) : v));
142
- return this;
143
- }
144
- bytes(n, v) {
145
- const b = typeof v === 'string' ? Buffer.from(v, 'utf8') : v;
146
- this.parts.push(key(n, 2), varint(b.length), b);
147
- return this;
148
- }
149
- msg(n, w) {
150
- return this.bytes(n, w.finish());
151
- }
152
- /** `map<string,string>` theo đúng thứ tự khoá của object. */
153
- map(n, obj) {
154
- for (const [k, v] of Object.entries(obj))
155
- this.msg(n, new Writer().bytes(1, k).bytes(2, v));
156
- return this;
157
- }
158
- finish() {
159
- return Buffer.concat(this.parts);
160
- }
161
- }
162
- exports.Writer = Writer;
@@ -1,41 +0,0 @@
1
- import type { Credentials } from './types';
2
- /**
3
- * Đăng nhập bằng QR qua Chrome headless.
4
- *
5
- * Vì sao cần trình duyệt: API QR của TikTok (`passport/web/check_qrconnect`) đòi chữ ký JS
6
- * (X-Gnarly / msToken) — gọi trần từ Node bị `error_code 7`. Nên mở đúng trang
7
- * `tiktok.com/login/qrcode` trong Chrome headless để chính trang tự ký, còn mình chỉ NGHE
8
- * phản hồi qua CDP: ảnh QR lấy từ body `get_qrcode`, trạng thái từ body `check_qrconnect`.
9
- * Đăng nhập xong lấy cookie rồi tắt Chrome — mọi việc sau đó (`login`, chat) không cần trình duyệt.
10
- */
11
- export type QrLoginEvent =
12
- /** Ảnh QR mới (`data:image/png;base64,…`) — hiện cho người dùng quét. Hết hạn thì có ảnh mới. */
13
- {
14
- type: 'qr';
15
- image: string;
16
- expiresAt: number;
17
- }
18
- /** Đã quét, chờ bấm xác nhận trên điện thoại. */
19
- | {
20
- type: 'scanned';
21
- }
22
- /** Đã xác nhận — đang lấy cookie. */
23
- | {
24
- type: 'confirmed';
25
- };
26
- export interface QrLoginOptions {
27
- /** Đường dẫn Chrome / Chromium (vd `/usr/bin/chromium-browser`). */
28
- chromePath: string;
29
- onEvent?: (e: QrLoginEvent) => void;
30
- signal?: AbortSignal;
31
- /** Tổng thời gian chờ quét (ms) — mặc định 5 phút. */
32
- timeoutMs?: number;
33
- /** QR hết hạn (~1 phút) thì tạo lại tối đa chừng này lần. */
34
- maxRefresh?: number;
35
- userAgent?: string;
36
- }
37
- /**
38
- * Đăng nhập TikTok bằng QR. Trả `Credentials` (cookie `.tiktok.com` + UA) — đưa vào
39
- * `new TikTok().login(creds)` và LƯU LẠI để lần sau khỏi quét.
40
- */
41
- export declare function loginQR(opts: QrLoginOptions): Promise<Credentials>;
package/dist/qr-login.js DELETED
@@ -1,212 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.loginQR = loginQR;
7
- const node_child_process_1 = require("node:child_process");
8
- const promises_1 = require("node:fs/promises");
9
- const node_os_1 = require("node:os");
10
- const node_path_1 = require("node:path");
11
- const ws_1 = __importDefault(require("ws"));
12
- const constants_1 = require("./constants");
13
- const errors_1 = require("./errors");
14
- /** Nối CDP vào một tab mới (một session phẳng trên websocket của trình duyệt). */
15
- async function openTab(browserWs) {
16
- const ws = new ws_1.default(browserWs, { perMessageDeflate: false });
17
- await new Promise((resolve, reject) => {
18
- ws.once('open', () => resolve());
19
- ws.once('error', reject);
20
- });
21
- let id = 0;
22
- let sessionId;
23
- const pending = new Map();
24
- const handlers = [];
25
- ws.on('message', (raw) => {
26
- const m = JSON.parse(raw.toString());
27
- if (m.id !== undefined && pending.has(m.id)) {
28
- const p = pending.get(m.id);
29
- pending.delete(m.id);
30
- if (m.error)
31
- p.reject(new Error(m.error.message));
32
- else
33
- p.resolve(m.result ?? {});
34
- return;
35
- }
36
- if (m.method && (!m.sessionId || m.sessionId === sessionId))
37
- for (const h of handlers)
38
- h(m.method, m.params ?? {});
39
- });
40
- const raw = (method, params = {}, sid) => new Promise((resolve, reject) => {
41
- const i = ++id;
42
- pending.set(i, { resolve: resolve, reject });
43
- ws.send(JSON.stringify({ id: i, method, params, ...(sid ? { sessionId: sid } : {}) }));
44
- });
45
- const target = await raw('Target.createTarget', { url: 'about:blank' });
46
- sessionId = (await raw('Target.attachToTarget', { targetId: target.targetId, flatten: true })).sessionId;
47
- return {
48
- send: (method, params) => raw(method, params, sessionId),
49
- on: (h) => handlers.push(h),
50
- close: () => ws.close(),
51
- };
52
- }
53
- /** Chạy Chrome headless, chờ dòng "DevTools listening on ws://…". */
54
- async function launchChrome(chromePath, userAgent) {
55
- const dir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'tca-chrome-'));
56
- const proc = (0, node_child_process_1.spawn)(chromePath, [
57
- '--headless=new',
58
- '--remote-debugging-port=0',
59
- `--user-data-dir=${dir}`,
60
- '--no-first-run',
61
- '--no-default-browser-check',
62
- '--disable-gpu',
63
- '--disable-dev-shm-usage',
64
- // Trong container chạy root — Chromium đòi tắt sandbox.
65
- '--no-sandbox',
66
- '--window-size=1280,900',
67
- '--lang=vi-VN',
68
- // UA mặc định của headless có chữ "HeadlessChrome" — thay bằng UA thường.
69
- `--user-agent=${userAgent}`,
70
- 'about:blank',
71
- ], { stdio: ['ignore', 'ignore', 'pipe'] });
72
- const ws = await new Promise((resolve, reject) => {
73
- let buf = '';
74
- const timer = setTimeout(() => reject(new Error('Chrome không khởi động sau 20s')), 20_000);
75
- proc.stderr.on('data', (d) => {
76
- buf += d.toString();
77
- const m = /DevTools listening on (ws:\/\/\S+)/.exec(buf);
78
- if (m) {
79
- clearTimeout(timer);
80
- resolve(m[1]);
81
- }
82
- });
83
- proc.once('error', (e) => {
84
- clearTimeout(timer);
85
- reject(e);
86
- });
87
- proc.once('exit', (code) => {
88
- clearTimeout(timer);
89
- reject(new Error(`Chrome thoát (${code}) trước khi sẵn sàng`));
90
- });
91
- }).catch(async (err) => {
92
- proc.kill('SIGKILL');
93
- await (0, promises_1.rm)(dir, { recursive: true, force: true });
94
- throw new errors_1.TikTokError('LOGIN_FAILED', `Không mở được Chrome: ${err instanceof Error ? err.message : String(err)}`);
95
- });
96
- return { proc, ws, dir };
97
- }
98
- /**
99
- * Đăng nhập TikTok bằng QR. Trả `Credentials` (cookie `.tiktok.com` + UA) — đưa vào
100
- * `new TikTok().login(creds)` và LƯU LẠI để lần sau khỏi quét.
101
- */
102
- async function loginQR(opts) {
103
- const userAgent = opts.userAgent || constants_1.DEFAULT_USER_AGENT;
104
- const maxRefresh = opts.maxRefresh ?? 3;
105
- const { proc, ws, dir } = await launchChrome(opts.chromePath, userAgent);
106
- let cdp = null;
107
- try {
108
- cdp = await openTab(ws);
109
- const tab = cdp;
110
- await tab.send('Network.enable');
111
- await tab.send('Page.enable');
112
- const result = new Promise((resolve, reject) => {
113
- const urlById = new Map();
114
- let refreshes = 0;
115
- let scanned = false;
116
- let done = false;
117
- const finish = (err, creds) => {
118
- if (done)
119
- return;
120
- done = true;
121
- clearTimeout(timer);
122
- opts.signal?.removeEventListener('abort', onAbort);
123
- if (err)
124
- reject(err);
125
- else
126
- resolve(creds);
127
- };
128
- const timer = setTimeout(() => finish(new errors_1.TikTokError('LOGIN_FAILED', 'Hết thời gian chờ quét QR')), opts.timeoutMs ?? 5 * 60_000);
129
- const onAbort = () => finish(new errors_1.TikTokError('LOGIN_ABORTED', 'Đã huỷ đăng nhập QR'));
130
- opts.signal?.addEventListener('abort', onAbort);
131
- const body = async (requestId) => {
132
- const r = await tab.send('Network.getResponseBody', { requestId }).catch(() => null);
133
- if (!r)
134
- return null;
135
- try {
136
- return JSON.parse(r.base64Encoded ? Buffer.from(r.body, 'base64').toString() : r.body);
137
- }
138
- catch {
139
- return null;
140
- }
141
- };
142
- /** Sau `confirmed`: trang tự chuyển hướng + đặt cookie phiên — chờ có `sessionid`. */
143
- const collectCookies = async () => {
144
- for (let i = 0; i < 30 && !done; i++) {
145
- const { cookies } = await tab.send('Network.getAllCookies');
146
- const mine = cookies.filter((c) => c.domain.endsWith('tiktok.com'));
147
- if (mine.some((c) => c.name === 'sessionid' && c.value) && mine.some((c) => c.name === 'ttwid')) {
148
- // Trùng tên ở `.tiktok.com` và `www.tiktok.com` → giữ bản `.tiktok.com`.
149
- const byName = new Map();
150
- for (const c of mine.sort((a, b) => (a.domain === '.tiktok.com' ? 1 : 0) - (b.domain === '.tiktok.com' ? 1 : 0))) {
151
- byName.set(c.name, { name: c.name, value: c.value });
152
- }
153
- return finish(null, { cookies: [...byName.values()], userAgent });
154
- }
155
- await new Promise((r) => setTimeout(r, 1000));
156
- }
157
- finish(new errors_1.TikTokError('LOGIN_FAILED', 'Đã xác nhận nhưng không thấy cookie sessionid'));
158
- };
159
- tab.on((method, p) => {
160
- if (done)
161
- return;
162
- if (method === 'Network.responseReceived') {
163
- const res = p.response;
164
- urlById.set(p.requestId, res.url);
165
- return;
166
- }
167
- if (method !== 'Network.loadingFinished')
168
- return;
169
- const url = urlById.get(p.requestId) ?? '';
170
- if (url.includes('/passport/web/get_qrcode/')) {
171
- void body(p.requestId).then((j) => {
172
- const qr = j?.data?.qrcode;
173
- if (typeof qr === 'string' && qr) {
174
- opts.onEvent?.({
175
- type: 'qr',
176
- image: `data:image/png;base64,${qr}`,
177
- expiresAt: Number(j?.data?.expire_time ?? 0) * 1000 || Date.now() + 60_000,
178
- });
179
- }
180
- });
181
- }
182
- else if (url.includes('/passport/web/check_qrconnect/')) {
183
- // Trang hỏi song song 3 vùng (sg/us/eu) — chỉ vùng của tài khoản đổi trạng thái, còn lại vẫn "new".
184
- void body(p.requestId).then((j) => {
185
- const status = String(j?.data?.status ?? '');
186
- if (status === 'scanned' && !scanned) {
187
- scanned = true;
188
- opts.onEvent?.({ type: 'scanned' });
189
- }
190
- else if (status === 'confirmed') {
191
- opts.onEvent?.({ type: 'confirmed' });
192
- void collectCookies();
193
- }
194
- else if (status === 'expired' && !scanned) {
195
- if (refreshes++ >= maxRefresh)
196
- finish(new errors_1.TikTokError('LOGIN_FAILED', 'QR hết hạn, không ai quét'));
197
- else
198
- void tab.send('Page.reload');
199
- }
200
- });
201
- }
202
- });
203
- });
204
- await tab.send('Page.navigate', { url: `${constants_1.WEB}/login/qrcode` });
205
- return await result;
206
- }
207
- finally {
208
- cdp?.close();
209
- proc.kill('SIGKILL');
210
- await (0, promises_1.rm)(dir, { recursive: true, force: true }).catch(() => undefined);
211
- }
212
- }
package/dist/types.d.ts DELETED
@@ -1,109 +0,0 @@
1
- /** Một cookie — đúng dạng `chrome.cookies.getAll` / CDP `Storage.getCookies` trả về (thừa trường không sao). */
2
- export interface CookieInput {
3
- name: string;
4
- value: string;
5
- }
6
- /** Thứ cần để đăng nhập lại — lưu lại sau mỗi lần đăng nhập, như `{cookie, imei, userAgent}` của zca-js. */
7
- export interface Credentials {
8
- /** Cookie của `.tiktok.com`: mảng `{name, value}`, object `{name: value}`, hoặc chuỗi `a=b; c=d`. */
9
- cookies: CookieInput[] | Record<string, string> | string;
10
- /** Mã máy IM (`wid`) — bỏ trống thì `login` hỏi TikTok theo `ttwid`. Chỉ dùng khi không hỏi được. */
11
- deviceId?: string;
12
- userAgent?: string;
13
- }
14
- export interface TikTokOptions {
15
- /** Ghi log gỡ lỗi — mặc định im lặng. */
16
- logger?: (level: 'debug' | 'warn', msg: string) => void;
17
- /** Hạn chờ mỗi request HTTP (ms). */
18
- timeoutMs?: number;
19
- }
20
- /** Đủ để nhắm một hội thoại: gửi tin, đọc tin, đánh dấu đã đọc. Mọi `TikTokMessage` đều thoả. */
21
- export interface ConversationRef {
22
- conversationId: string;
23
- conversationShortId: string;
24
- }
25
- export interface TikTokImage {
26
- /** Mã ảnh trên kho TikTok (đã mã hoá). */
27
- uri: string;
28
- /** Link tải bản gốc — cần cookie, nội dung là AES-256-GCM. */
29
- url: string;
30
- thumbUrl: string;
31
- width: number;
32
- height: number;
33
- /** Khoá AES-256-GCM (hex) để giải `url` — `api.downloadImage` lo việc này. */
34
- decryptKey: string;
35
- }
36
- /** Sticker (nhãn dán) — kể cả sticker khách tự tạo (`stickerType` 15). Link CDN công khai, có chữ ký + hạn vài ngày. */
37
- export interface TikTokSticker {
38
- stickerId: string;
39
- /** 14 = sticker có sẵn của TikTok, 15 = sticker người dùng tự tạo (ảnh chế, AI…). */
40
- stickerType: number;
41
- url: string;
42
- width: number;
43
- height: number;
44
- }
45
- /** Một reaction trên tin: emoji + những người đã thả (uid). `e:love` của app điện thoại đã gộp vào ❤️. */
46
- export interface TikTokReaction {
47
- emoji: string;
48
- userIds: string[];
49
- }
50
- export interface TikTokMessage extends ConversationRef {
51
- serverMessageId: string;
52
- /** Vị trí trong hội thoại (µs) — làm mốc đọc tiếp tin cũ hơn. */
53
- index: string;
54
- /** `MessageType` — 7 chữ, 1802 ảnh, 1805 sticker, 8 chia sẻ video, 1 hệ thống. */
55
- type: number;
56
- /** `command` = lệnh ẩn (đã đọc…) — listener phát qua sự kiện `command`, không qua `message`. */
57
- kind: 'text' | 'image' | 'sticker' | 'video' | 'system' | 'command' | 'other';
58
- senderId: string;
59
- isSelf: boolean;
60
- /** Nội dung chữ (`kind = text`), rỗng với loại khác. */
61
- text: string;
62
- image: TikTokImage | null;
63
- sticker: TikTokSticker | null;
64
- reactions: TikTokReaction[];
65
- /** JSON thô của nội dung (`{"aweType":0,"text":"…"}`…). */
66
- content: unknown;
67
- /** Giờ gửi, ms epoch. */
68
- createdAt: number;
69
- clientMessageId: string;
70
- ext: Record<string, string>;
71
- }
72
- export interface TikTokConversation extends ConversationRef {
73
- participants: string[];
74
- /** Người còn lại trong hội thoại 1-1. */
75
- peerId: string;
76
- }
77
- export interface TikTokUser {
78
- uid: string;
79
- /** `@username` (không có @). */
80
- uniqueId: string;
81
- nickname: string;
82
- /** Ảnh đại diện tốt nhất (ưu tiên jpeg/webp/png, tránh heic trình duyệt không đọc). */
83
- avatar: string;
84
- /** Mọi link ảnh đại diện TikTok trả, xếp theo ưu tiên — link có chữ ký, có hạn; `downloadAvatar` thử lần lượt. */
85
- avatars: string[];
86
- }
87
- export interface TikTokAccount {
88
- uid: string;
89
- username: string;
90
- screenName: string;
91
- avatar: string;
92
- }
93
- /** File đính kèm — giống `attachments` của zca-js. */
94
- export interface ImageInput {
95
- data: Buffer;
96
- /** Chỉ cần đuôi để TikTok biết định dạng: `anh.jpg`, `bill.png`. */
97
- filename: string;
98
- }
99
- export interface MessagePage {
100
- messages: TikTokMessage[];
101
- hasMore: boolean;
102
- }
103
- export interface ConversationPage {
104
- conversations: TikTokConversation[];
105
- /** Tin mới nhất kèm theo — tiện dựng danh sách hộp thư. */
106
- messages: TikTokMessage[];
107
- cursor: string;
108
- hasMore: boolean;
109
- }
package/dist/types.js DELETED
@@ -1,2 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });