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.
@@ -1,43 +0,0 @@
1
- import { EventEmitter } from 'node:events';
2
- import type { Context } from './context';
3
- import type { TikTokMessage } from './types';
4
- /** Mã đóng do chính mình gọi `stop()`. */
5
- export declare const MANUAL_CLOSE = 4000;
6
- export interface ListenerEvents {
7
- connected: [];
8
- /** Tin thật trong hội thoại (cả tin của chính mình — xem `isSelf`). */
9
- message: [TikTokMessage];
10
- /** Lệnh ẩn: đánh dấu đã đọc… (`content.command_type`). */
11
- command: [TikTokMessage];
12
- /** Mã + lý do đóng. Không phải `MANUAL_CLOSE` thì listener tự nối lại (nếu bật). */
13
- closed: [number, string];
14
- error: [Error];
15
- }
16
- /**
17
- * Nghe tin mới qua websocket `im-ws-sg` (aid 1459) — chỉ cần cookie, như listener của zca-js.
18
- * Sự kiện: `connected`, `message`, `closed`, `error`.
19
- */
20
- export declare class Listener extends EventEmitter<ListenerEvents> {
21
- private readonly ctx;
22
- private ws;
23
- private heartbeat;
24
- private idle;
25
- private reconnectTimer;
26
- private backoff;
27
- private retry;
28
- private readonly seen;
29
- constructor(ctx: Context);
30
- get connected(): boolean;
31
- url(): string;
32
- /** Mở kết nối. `retryOnClose` (mặc định bật): rớt mạng thì tự nối lại, giãn dần tới 1 phút. */
33
- start(opts?: {
34
- retryOnClose?: boolean;
35
- }): void;
36
- stop(): void;
37
- private connect;
38
- private touch;
39
- private clearTimers;
40
- private onFrame;
41
- /** Báo đã nhận — `ack_id = <giờ base36>:<x_frontier_traceid>`, kèm lại seq/logid của khung. */
42
- private ack;
43
- }
package/dist/listener.js DELETED
@@ -1,181 +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.Listener = exports.MANUAL_CLOSE = void 0;
7
- const node_crypto_1 = require("node:crypto");
8
- const node_events_1 = require("node:events");
9
- const ws_1 = __importDefault(require("ws"));
10
- const constants_1 = require("./constants");
11
- const lz4_1 = require("./lz4");
12
- const message_1 = require("./message");
13
- const proto_1 = require("./proto");
14
- /** Trình duyệt gửi "hi" mỗi 15–30 giây; lấy cận dưới. */
15
- const HEARTBEAT_MS = 15_000;
16
- /** Không nhận gì (kể cả "hi" đáp lại) quá chừng này thì coi như chết ngầm, nối lại. */
17
- const IDLE_TIMEOUT_MS = 75_000;
18
- const RECONNECT_MIN_MS = 2_000;
19
- const RECONNECT_MAX_MS = 60_000;
20
- /** Server đẩy cùng một tin vài lần (mỗi thiết bị một khung) — nhớ chừng này mã tin để lọc trùng. */
21
- const DEDUPE_KEEP = 500;
22
- /** Mã đóng do chính mình gọi `stop()`. */
23
- exports.MANUAL_CLOSE = 4000;
24
- /**
25
- * Nghe tin mới qua websocket `im-ws-sg` (aid 1459) — chỉ cần cookie, như listener của zca-js.
26
- * Sự kiện: `connected`, `message`, `closed`, `error`.
27
- */
28
- class Listener extends node_events_1.EventEmitter {
29
- ctx;
30
- ws = null;
31
- heartbeat = null;
32
- idle = null;
33
- reconnectTimer = null;
34
- backoff = RECONNECT_MIN_MS;
35
- retry = true;
36
- seen = new Set();
37
- constructor(ctx) {
38
- super();
39
- this.ctx = ctx;
40
- }
41
- get connected() {
42
- return this.ws?.readyState === ws_1.default.OPEN;
43
- }
44
- url() {
45
- const accessKey = (0, node_crypto_1.createHash)('md5')
46
- .update(constants_1.FRONTIER_FPID + constants_1.FRONTIER_APP_KEY + this.ctx.deviceId + constants_1.FRONTIER_SALT)
47
- .digest('hex');
48
- const q = new URLSearchParams({
49
- aid: constants_1.AID_FRONTIER,
50
- fpid: constants_1.FRONTIER_FPID,
51
- device_platform: 'web',
52
- version_code: 'fws_1.0.0',
53
- access_key: accessKey,
54
- device_id: this.ctx.deviceId,
55
- // Giá trị cookie đã URL-encode sẵn (`1%7C…`) — giải ra để URLSearchParams không mã hoá hai lần.
56
- ttwid: decodeURIComponent(this.ctx.cookies.get('ttwid') ?? ''),
57
- xsack: '1',
58
- xaack: '1',
59
- xsqos: '0',
60
- });
61
- return `${constants_1.IM_WS}?${q}`;
62
- }
63
- /** Mở kết nối. `retryOnClose` (mặc định bật): rớt mạng thì tự nối lại, giãn dần tới 1 phút. */
64
- start(opts = {}) {
65
- this.retry = opts.retryOnClose ?? true;
66
- this.connect();
67
- }
68
- stop() {
69
- this.retry = false;
70
- this.clearTimers();
71
- if (this.reconnectTimer)
72
- clearTimeout(this.reconnectTimer);
73
- this.reconnectTimer = null;
74
- const ws = this.ws;
75
- this.ws = null;
76
- if (ws && ws.readyState <= ws_1.default.OPEN)
77
- ws.close(exports.MANUAL_CLOSE, 'manual');
78
- }
79
- connect() {
80
- const ws = new ws_1.default(this.url(), {
81
- headers: { Cookie: this.ctx.cookieHeader(), 'User-Agent': this.ctx.userAgent, Origin: constants_1.WEB },
82
- });
83
- this.ws = ws;
84
- ws.binaryType = 'nodebuffer';
85
- ws.on('open', () => {
86
- this.backoff = RECONNECT_MIN_MS;
87
- this.heartbeat = setInterval(() => ws.readyState === ws_1.default.OPEN && ws.send('hi'), HEARTBEAT_MS);
88
- this.touch();
89
- this.ctx.log('debug', 'tca: websocket đã nối');
90
- this.emit('connected');
91
- });
92
- ws.on('message', (data, isBinary) => {
93
- this.touch();
94
- if (!isBinary)
95
- return; // "hi" đáp lại
96
- try {
97
- this.onFrame(ws, data);
98
- }
99
- catch (err) {
100
- this.emit('error', err instanceof Error ? err : new Error(String(err)));
101
- }
102
- });
103
- ws.on('error', (err) => this.emit('error', err));
104
- ws.on('close', (code, reason) => {
105
- if (this.ws !== ws)
106
- return; // kết nối cũ đã bị thay
107
- this.ws = null;
108
- this.clearTimers();
109
- this.emit('closed', code, reason.toString());
110
- if (!this.retry || code === exports.MANUAL_CLOSE)
111
- return;
112
- const wait = this.backoff;
113
- this.backoff = Math.min(this.backoff * 2, RECONNECT_MAX_MS);
114
- this.ctx.log('warn', `tca: websocket đóng (${code}) — nối lại sau ${wait / 1000}s`);
115
- this.reconnectTimer = setTimeout(() => this.connect(), wait);
116
- });
117
- }
118
- touch() {
119
- if (this.idle)
120
- clearTimeout(this.idle);
121
- this.idle = setTimeout(() => {
122
- this.ctx.log('warn', 'tca: websocket im lặng quá lâu — nối lại');
123
- this.ws?.terminate();
124
- }, IDLE_TIMEOUT_MS);
125
- }
126
- clearTimers() {
127
- if (this.heartbeat)
128
- clearInterval(this.heartbeat);
129
- if (this.idle)
130
- clearTimeout(this.idle);
131
- this.heartbeat = null;
132
- this.idle = null;
133
- }
134
- onFrame(ws, data) {
135
- const m = proto_1.Msg.parse(data);
136
- const frame = {
137
- seq: m.int(1),
138
- logId: m.int(2),
139
- headers: m.map(5),
140
- encoding: m.str(6),
141
- payload: m.bytes(8),
142
- };
143
- if (frame.headers.need_ack === '1')
144
- this.ack(ws, frame);
145
- if (frame.payload.length === 0)
146
- return;
147
- const payload = frame.encoding === '__lz4' ? (0, lz4_1.lz4Decompress)(frame.payload) : frame.payload;
148
- const resp = proto_1.Msg.tryParse(payload);
149
- if (!resp || resp.num(constants_1.Resp.cmd) !== constants_1.Cmd.NewMessage)
150
- return;
151
- // NewMessageNotify: {2 conversationId, 3 loại, 4 ?, 5 MessageBody}
152
- const body = resp.msg(constants_1.Resp.body)?.msg(constants_1.Cmd.NewMessage)?.msg(5);
153
- if (!body)
154
- return;
155
- const msg = (0, message_1.parseMessage)(body, this.ctx.ownId);
156
- if (!msg.serverMessageId || this.seen.has(msg.serverMessageId))
157
- return;
158
- this.seen.add(msg.serverMessageId);
159
- if (this.seen.size > DEDUPE_KEEP)
160
- this.seen.delete(this.seen.values().next().value);
161
- if (msg.kind === 'command')
162
- this.emit('command', msg);
163
- else
164
- this.emit('message', msg);
165
- }
166
- /** Báo đã nhận — `ack_id = <giờ base36>:<x_frontier_traceid>`, kèm lại seq/logid của khung. */
167
- ack(ws, f) {
168
- const ackId = `${Date.now().toString(36)}:${f.headers.x_frontier_traceid ?? ''}`;
169
- const out = new proto_1.Writer()
170
- .int(1, f.seq)
171
- .int(2, f.logId)
172
- .int(3, 5)
173
- .int(4, 1)
174
- .map(5, { is_ack: '1', ack_id: ackId, ack_code: '0' })
175
- .bytes(9, ackId)
176
- .finish();
177
- if (ws.readyState === ws_1.default.OPEN)
178
- ws.send(out);
179
- }
180
- }
181
- exports.Listener = Listener;
package/dist/lz4.d.ts DELETED
@@ -1,5 +0,0 @@
1
- /**
2
- * Giải nén LZ4 dạng block (không có frame header) — khung websocket có
3
- * `payload_encoding = "__lz4"` nén payload kiểu này. Không kéo thêm thư viện cho ~30 dòng.
4
- */
5
- export declare function lz4Decompress(src: Buffer): Buffer;
package/dist/lz4.js DELETED
@@ -1,57 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.lz4Decompress = lz4Decompress;
4
- /**
5
- * Giải nén LZ4 dạng block (không có frame header) — khung websocket có
6
- * `payload_encoding = "__lz4"` nén payload kiểu này. Không kéo thêm thư viện cho ~30 dòng.
7
- */
8
- function lz4Decompress(src) {
9
- let out = Buffer.alloc(Math.max(src.length * 4, 1024));
10
- let o = 0;
11
- const ensure = (need) => {
12
- if (o + need <= out.length)
13
- return;
14
- let size = out.length * 2;
15
- while (o + need > size)
16
- size *= 2;
17
- const bigger = Buffer.alloc(size);
18
- out.copy(bigger, 0, 0, o);
19
- out = bigger;
20
- };
21
- let i = 0;
22
- while (i < src.length) {
23
- const token = src[i++];
24
- let lit = token >> 4;
25
- if (lit === 15) {
26
- let b;
27
- do {
28
- b = src[i++];
29
- lit += b;
30
- } while (b === 255);
31
- }
32
- ensure(lit);
33
- src.copy(out, o, i, i + lit);
34
- i += lit;
35
- o += lit;
36
- if (i >= src.length)
37
- break;
38
- const offset = src[i] | (src[i + 1] << 8);
39
- i += 2;
40
- if (offset === 0 || offset > o)
41
- throw new Error('lz4: offset hỏng');
42
- let len = token & 15;
43
- if (len === 15) {
44
- let b;
45
- do {
46
- b = src[i++];
47
- len += b;
48
- } while (b === 255);
49
- }
50
- len += 4;
51
- ensure(len);
52
- // Sao từng byte: vùng nguồn và đích được phép chồng nhau.
53
- for (let k = 0; k < len; k++, o++)
54
- out[o] = out[o - offset];
55
- }
56
- return out.subarray(0, o);
57
- }
package/dist/media.d.ts DELETED
@@ -1,27 +0,0 @@
1
- import type { Context } from './context';
2
- import type { ImageInput, TikTokImage } from './types';
3
- /** Ảnh đã tải lên + được kho mã hoá — đủ để gửi tin ảnh. */
4
- export interface UploadedImage {
5
- uri: string;
6
- secretKey: string;
7
- width: number;
8
- height: number;
9
- size: number;
10
- filename: string;
11
- }
12
- export declare class MediaClient {
13
- private readonly ctx;
14
- private cred;
15
- constructor(ctx: Context);
16
- /** `UploadConfig` trả hai bộ khoá: loại 1 = ImageX (ảnh), loại 2 = VOD (video). Lấy loại 1. */
17
- private credential;
18
- /** Gọi ImageX ký AWS SigV4 (vùng cn-north-1, dịch vụ imagex). */
19
- private imagex;
20
- /**
21
- * Tải ảnh lên kho riêng tư của tin nhắn: Apply → POST file → Commit (kho tự mã hoá AES-GCM
22
- * và trả khoá). Kích thước ảnh lấy từ kết quả Commit — không cần tự đọc ảnh.
23
- */
24
- upload(image: ImageInput): Promise<UploadedImage>;
25
- /** Tải ảnh trong tin nhắn về và giải mã. Bố cục: IV 12 byte ‖ bản mã ‖ tag 16 byte. */
26
- download(image: TikTokImage, which?: 'origin' | 'thumb'): Promise<Buffer>;
27
- }
package/dist/media.js DELETED
@@ -1,152 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MediaClient = void 0;
4
- const node_crypto_1 = require("node:crypto");
5
- const node_zlib_1 = require("node:zlib");
6
- const constants_1 = require("./constants");
7
- const errors_1 = require("./errors");
8
- const sha256 = (s) => (0, node_crypto_1.createHash)('sha256').update(s).digest('hex');
9
- const hmac = (k, s) => (0, node_crypto_1.createHmac)('sha256', k).update(s).digest();
10
- const rfc3986 = (s) => encodeURIComponent(s).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
11
- class MediaClient {
12
- ctx;
13
- cred = null;
14
- constructor(ctx) {
15
- this.ctx = ctx;
16
- }
17
- /** `UploadConfig` trả hai bộ khoá: loại 1 = ImageX (ảnh), loại 2 = VOD (video). Lấy loại 1. */
18
- async credential() {
19
- if (this.cred && this.cred.expiresAt - Date.now() > 60_000)
20
- return this.cred;
21
- const body = await this.ctx.im('/v1/media/upload_config', constants_1.Cmd.UploadConfig, Buffer.alloc(0));
22
- const c = body.msgs(1).find((m) => m.num(1) === 1);
23
- if (!c)
24
- throw new errors_1.TikTokError('UPLOAD_FAILED', 'upload_config không có khoá ImageX');
25
- this.cred = {
26
- serviceId: c.str(2),
27
- host: c.str(3),
28
- expiresAt: c.num(4),
29
- accessKeyId: c.str(6),
30
- sessionToken: c.str(7),
31
- secretAccessKey: c.str(8),
32
- };
33
- return this.cred;
34
- }
35
- /** Gọi ImageX ký AWS SigV4 (vùng cn-north-1, dịch vụ imagex). */
36
- async imagex(method, params, body) {
37
- const c = await this.credential();
38
- const amzDate = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d+/, '');
39
- const date = amzDate.slice(0, 8);
40
- const query = Object.keys(params)
41
- .sort()
42
- .map((k) => `${rfc3986(k)}=${rfc3986(params[k])}`)
43
- .join('&');
44
- const headers = { 'x-amz-date': amzDate, 'x-amz-security-token': c.sessionToken };
45
- if (body !== undefined)
46
- headers['x-amz-content-sha256'] = sha256(body);
47
- const signed = Object.keys(headers).sort();
48
- const canonical = [
49
- method,
50
- '/',
51
- query,
52
- signed.map((k) => `${k}:${headers[k]}\n`).join(''),
53
- signed.join(';'),
54
- body !== undefined ? headers['x-amz-content-sha256'] : sha256(''),
55
- ].join('\n');
56
- const scope = `${date}/${constants_1.IMAGEX_REGION}/${constants_1.IMAGEX_SERVICE}/aws4_request`;
57
- const toSign = ['AWS4-HMAC-SHA256', amzDate, scope, sha256(canonical)].join('\n');
58
- let k = hmac(`AWS4${c.secretAccessKey}`, date);
59
- k = hmac(k, constants_1.IMAGEX_REGION);
60
- k = hmac(k, constants_1.IMAGEX_SERVICE);
61
- k = hmac(k, 'aws4_request');
62
- const signature = (0, node_crypto_1.createHmac)('sha256', k).update(toSign).digest('hex');
63
- const res = await fetch(`https://${c.host}/?${query}`, {
64
- method,
65
- headers: {
66
- ...headers,
67
- Authorization: `AWS4-HMAC-SHA256 Credential=${c.accessKeyId}/${scope}, SignedHeaders=${signed.join(';')}, Signature=${signature}`,
68
- ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
69
- },
70
- body,
71
- signal: AbortSignal.timeout(this.ctx.options.timeoutMs ?? 20_000),
72
- });
73
- const json = (await res.json().catch(() => null));
74
- const err = json?.ResponseMetadata?.Error;
75
- if (!res.ok || !json || err) {
76
- throw new errors_1.TikTokError('UPLOAD_FAILED', `imagex ${params.Action}: ${err?.Message ?? `HTTP ${res.status}`}`, err?.CodeN ?? res.status);
77
- }
78
- return json;
79
- }
80
- /**
81
- * Tải ảnh lên kho riêng tư của tin nhắn: Apply → POST file → Commit (kho tự mã hoá AES-GCM
82
- * và trả khoá). Kích thước ảnh lấy từ kết quả Commit — không cần tự đọc ảnh.
83
- */
84
- async upload(image) {
85
- const c = await this.credential();
86
- const ext = (/\.([a-z0-9]{2,5})$/i.exec(image.filename)?.[1] ?? 'jpg').toLowerCase();
87
- const apply = await this.imagex('GET', {
88
- Action: 'ApplyImageUpload',
89
- Version: constants_1.IMAGEX_VERSION,
90
- ServiceId: c.serviceId,
91
- FileSize: String(image.data.length),
92
- FileExtension: `.${ext}`,
93
- s: (0, node_crypto_1.randomBytes)(6).toString('hex').slice(0, 10),
94
- });
95
- const addr = apply.Result.UploadAddress;
96
- const store = addr.StoreInfos[0];
97
- const host = addr.UploadHosts[0];
98
- if (!store || !host)
99
- throw new errors_1.TikTokError('UPLOAD_FAILED', 'ApplyImageUpload không trả chỗ tải lên');
100
- const up = await fetch(`https://${host}/upload/v1/${store.StoreUri}`, {
101
- method: 'POST',
102
- headers: {
103
- Authorization: store.Auth,
104
- 'Content-CRC32': ((0, node_zlib_1.crc32)(image.data) >>> 0).toString(16).padStart(8, '0'),
105
- // Thiếu header này kho trả 4004 Not Found.
106
- 'X-Storage-U': this.ctx.ownId,
107
- 'Content-Type': 'application/octet-stream',
108
- 'Content-Disposition': 'attachment; filename="undefined"',
109
- },
110
- body: new Uint8Array(image.data),
111
- signal: AbortSignal.timeout(this.ctx.options.timeoutMs ?? 60_000),
112
- });
113
- const upJson = (await up.json().catch(() => null));
114
- if (!up.ok || upJson?.code !== 2000) {
115
- throw new errors_1.TikTokError('UPLOAD_FAILED', `upload: ${upJson?.message ?? `HTTP ${up.status}`}`, upJson?.code ?? up.status);
116
- }
117
- const commit = await this.imagex('POST', { Action: 'CommitImageUpload', Version: constants_1.IMAGEX_VERSION, ServiceId: c.serviceId }, JSON.stringify({ SessionKey: addr.SessionKey, Functions: [{ name: 'Encryption', input: { Config: { copies: 'cipher_v2' } } }] }));
118
- const enc = commit.Result.Results?.[0]?.Encryption;
119
- const plugin = commit.Result.PluginResult?.[0];
120
- if (!enc?.Uri || !enc.SecretKey)
121
- throw new errors_1.TikTokError('UPLOAD_FAILED', 'CommitImageUpload không trả bản mã hoá');
122
- return {
123
- uri: enc.Uri,
124
- secretKey: enc.SecretKey,
125
- width: plugin?.ImageWidth ?? Number(enc.Extra?.img_width ?? 0),
126
- height: plugin?.ImageHeight ?? Number(enc.Extra?.img_height ?? 0),
127
- size: plugin?.ImageSize ?? image.data.length,
128
- filename: image.filename,
129
- };
130
- }
131
- /** Tải ảnh trong tin nhắn về và giải mã. Bố cục: IV 12 byte ‖ bản mã ‖ tag 16 byte. */
132
- async download(image, which = 'origin') {
133
- const url = which === 'thumb' && image.thumbUrl ? image.thumbUrl : image.url;
134
- if (!url)
135
- throw new errors_1.TikTokError('DOWNLOAD_FAILED', 'Ảnh không có link');
136
- const res = await this.ctx.fetch(url);
137
- if (!res.ok)
138
- throw new errors_1.TikTokError('DOWNLOAD_FAILED', `Tải ảnh: HTTP ${res.status}`, res.status);
139
- const enc = Buffer.from(await res.arrayBuffer());
140
- if (!image.decryptKey)
141
- return enc;
142
- try {
143
- const d = (0, node_crypto_1.createDecipheriv)('aes-256-gcm', Buffer.from(image.decryptKey, 'hex'), enc.subarray(0, 12));
144
- d.setAuthTag(enc.subarray(enc.length - 16));
145
- return Buffer.concat([d.update(enc.subarray(12, enc.length - 16)), d.final()]);
146
- }
147
- catch (err) {
148
- throw new errors_1.TikTokError('DOWNLOAD_FAILED', `Giải mã ảnh: ${err instanceof Error ? err.message : String(err)}`);
149
- }
150
- }
151
- }
152
- exports.MediaClient = MediaClient;
package/dist/message.d.ts DELETED
@@ -1,6 +0,0 @@
1
- import { Msg } from './proto';
2
- import type { TikTokConversation, TikTokMessage } from './types';
3
- /** `MessageBody` → `TikTokMessage`. */
4
- export declare function parseMessage(m: Msg, ownId: string): TikTokMessage;
5
- /** `ConversationInfo` (trường 2 của get_by_user_init): {1 id, 2 shortId, 3 loại, 6 {1 {1 uid}}…}. */
6
- export declare function parseConversation(m: Msg, ownId: string): TikTokConversation;
package/dist/message.js DELETED
@@ -1,113 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.parseMessage = parseMessage;
4
- exports.parseConversation = parseConversation;
5
- const constants_1 = require("./constants");
6
- function parseJson(s) {
7
- try {
8
- return JSON.parse(s);
9
- }
10
- catch {
11
- return s;
12
- }
13
- }
14
- /** Ảnh trong `attachments` (trường 20 → 4): 1 = bản gốc, 2 = ảnh nhỏ; mỗi cái {1 uri, 2 url…, 5 {w,h}, 6 khoá}. */
15
- function parseImage(m) {
16
- const media = m.msg(constants_1.MsgF.attachments)?.msg(4);
17
- const origin = media?.msg(1);
18
- if (!media || !origin)
19
- return null;
20
- const size = origin.msg(5);
21
- return {
22
- uri: origin.str(1),
23
- url: origin.strs(2)[0] ?? '',
24
- thumbUrl: media.msg(2)?.strs(2)[0] ?? '',
25
- width: size?.num(1) ?? 0,
26
- height: size?.num(2) ?? 0,
27
- decryptKey: origin.str(6),
28
- };
29
- }
30
- /**
31
- * Sticker (trường 20 → 7): `{1 {1 uri, 2 url, 5 {w,h}}, 2 lời nhắc, 200 {1 {2 map a:sticker_id, a:sticker_type…}}}`.
32
- * Link trỏ CDN sticker công khai (`…-dm-sticker-sign-…`) — không mã hoá, không cần cookie.
33
- */
34
- function parseSticker(m) {
35
- const st = m.msg(constants_1.MsgF.attachments)?.msg(7);
36
- const img = st?.msg(1);
37
- if (!st || !img)
38
- return null;
39
- const size = img.msg(5);
40
- const ext = st.msg(200)?.msg(1)?.map(2) ?? {};
41
- return {
42
- stickerId: ext['a:sticker_id'] ?? '',
43
- stickerType: Number(ext['a:sticker_type'] ?? 0),
44
- url: img.str(2),
45
- width: size?.num(1) ?? 0,
46
- height: size?.num(2) ?? 0,
47
- };
48
- }
49
- /**
50
- * Reaction (trường 15, `property_list`): mỗi phần tử `{1 khoá, 2 {1 PropertyItem[]}}`, khoá `e:<emoji>`,
51
- * `PropertyItem {1 uid, 3 giây, 4 idempotent_id, 6 µs}`. App điện thoại thả ❤️ ghi cả `e:❤️` lẫn `e:love` → gộp.
52
- */
53
- function parseReactions(m) {
54
- const byEmoji = new Map();
55
- for (const p of m.msgs(constants_1.MsgF.propertyList)) {
56
- const key = p.str(1);
57
- if (!key.startsWith(constants_1.REACTION_PREFIX))
58
- continue;
59
- const emoji = key === constants_1.REACTION_LOVE_KEY ? constants_1.REACTION_HEART : key.slice(constants_1.REACTION_PREFIX.length);
60
- const uids = byEmoji.get(emoji) ?? new Set();
61
- for (const item of p.msg(2)?.msgs(1) ?? [])
62
- uids.add(item.id(1));
63
- if (uids.size)
64
- byEmoji.set(emoji, uids);
65
- }
66
- return [...byEmoji].map(([emoji, uids]) => ({ emoji, userIds: [...uids] }));
67
- }
68
- const KIND = {
69
- [constants_1.MessageType.Text]: 'text',
70
- [constants_1.MessageType.PrivateImage]: 'image',
71
- [constants_1.MessageType.Sticker]: 'sticker',
72
- [constants_1.MessageType.ShareVideo]: 'video',
73
- [constants_1.MessageType.System]: 'system',
74
- [constants_1.MessageType.Command]: 'command',
75
- };
76
- /** `MessageBody` → `TikTokMessage`. */
77
- function parseMessage(m, ownId) {
78
- const type = m.num(constants_1.MsgF.type);
79
- const content = parseJson(m.str(constants_1.MsgF.content));
80
- const ext = m.map(constants_1.MsgF.ext);
81
- const senderId = m.id(constants_1.MsgF.sender);
82
- const text = type === constants_1.MessageType.Text && content && typeof content === 'object' && 'text' in content
83
- ? String(content.text ?? '')
84
- : '';
85
- return {
86
- conversationId: m.str(constants_1.MsgF.conversationId),
87
- conversationShortId: m.id(constants_1.MsgF.conversationShortId),
88
- serverMessageId: m.id(constants_1.MsgF.serverMessageId),
89
- index: m.id(constants_1.MsgF.indexInConversation),
90
- type,
91
- kind: KIND[type] ?? 'other',
92
- senderId,
93
- isSelf: senderId === ownId,
94
- text,
95
- image: type === constants_1.MessageType.PrivateImage ? parseImage(m) : null,
96
- sticker: type === constants_1.MessageType.Sticker ? parseSticker(m) : null,
97
- reactions: parseReactions(m),
98
- content,
99
- createdAt: m.num(constants_1.MsgF.createTime),
100
- clientMessageId: ext['s:client_message_id'] ?? '',
101
- ext,
102
- };
103
- }
104
- /** `ConversationInfo` (trường 2 của get_by_user_init): {1 id, 2 shortId, 3 loại, 6 {1 {1 uid}}…}. */
105
- function parseConversation(m, ownId) {
106
- const participants = (m.msg(6)?.msgs(1) ?? []).map((p) => p.id(1));
107
- return {
108
- conversationId: m.str(1),
109
- conversationShortId: m.id(2),
110
- participants,
111
- peerId: participants.find((p) => p !== ownId) ?? '',
112
- };
113
- }
package/dist/proto.d.ts DELETED
@@ -1,43 +0,0 @@
1
- /**
2
- * Protobuf tối thiểu, không cần file `.proto`.
3
- *
4
- * TikTok không công bố `.proto` của IM — số trường ở `constants.ts` dựng lại từ byte bắt được
5
- * 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.
6
- */
7
- export interface Field {
8
- n: number;
9
- wire: number;
10
- int: bigint;
11
- bytes: Buffer;
12
- }
13
- /** Tách một message thành danh sách trường. `null` nếu byte không phải protobuf hợp lệ. */
14
- export declare function decodeFields(b: Buffer): Field[] | null;
15
- /** Message đã giải — đọc theo số trường. */
16
- export declare class Msg {
17
- readonly fields: Field[];
18
- private constructor();
19
- static parse(b: Buffer): Msg;
20
- static tryParse(b: Buffer | undefined): Msg | null;
21
- has(n: number): boolean;
22
- int(n: number): bigint;
23
- num(n: number): number;
24
- /** Số nguyên 64-bit dạng chuỗi — uid / mã tin vượt `Number.MAX_SAFE_INTEGER`. */
25
- id(n: number): string;
26
- bytes(n: number): Buffer;
27
- str(n: number): string;
28
- strs(n: number): string[];
29
- msg(n: number): Msg | null;
30
- msgs(n: number): Msg[];
31
- /** `map<string,string>` — mỗi phần tử là message `{1: key, 2: value}`. */
32
- map(n: number): Record<string, string>;
33
- }
34
- /** Ghép message theo thứ tự gọi. */
35
- export declare class Writer {
36
- private readonly parts;
37
- int(n: number, v: bigint | number | string): this;
38
- bytes(n: number, v: Buffer | string): this;
39
- msg(n: number, w: Writer): this;
40
- /** `map<string,string>` theo đúng thứ tự khoá của object. */
41
- map(n: number, obj: Record<string, string>): this;
42
- finish(): Buffer;
43
- }