tca-js 0.3.0 → 0.3.1

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/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.loginQR = exports.MANUAL_CLOSE = exports.Listener = exports.TikTokError = exports.MessageType = exports.API = exports.TikTok = void 0;
4
4
  /**
5
- * tca — TikTok Chat API (giả lập TikTok Web), cùng ý tưởng với zca-js cho Zalo.
5
+ * tca-js — unofficial TikTok Chat API (speaks the TikTok Web protocol).
6
6
  *
7
7
  * ```ts
8
8
  * const api = await new TikTok().login({ cookies, deviceId });
@@ -13,8 +13,8 @@ exports.loginQR = exports.MANUAL_CLOSE = exports.Listener = exports.TikTokError
13
13
  * api.listener.start();
14
14
  * ```
15
15
  *
16
- * Lấy cookie lần đầu: `loginQR({ chromePath, onEvent })` — mở Chrome headless ở trang QR của TikTok
17
- * (API đăng nhập đòi chữ ký JS nên không làm thuần Node được), quét xong trả cookie. Xem README.
16
+ * Getting cookies the first time: `loginQR({ chromePath, onEvent })` — opens headless Chrome on TikTok's QR page
17
+ * (the login API requires a JS signature so it can't be done in pure Node), returns cookies after scanning. See README.
18
18
  */
19
19
  const api_1 = require("./api");
20
20
  const context_1 = require("./context");
@@ -24,19 +24,19 @@ class TikTok {
24
24
  constructor(options = {}) {
25
25
  this.options = options;
26
26
  }
27
- /** Đăng nhập bằng cookie đã có. Cookie hết hạn → `TikTokError('SESSION_EXPIRED')`. */
27
+ /** Log in with existing cookies. Expired cookies → `TikTokError('SESSION_EXPIRED')`. */
28
28
  async login(credentials) {
29
29
  const ctx = new context_1.Context(credentials, this.options);
30
30
  if (!ctx.cookies.get('sessionid') && !ctx.cookies.get('sid_tt')) {
31
- throw new errors_1.TikTokError('SESSION_EXPIRED', 'Cookie không có sessionid');
31
+ throw new errors_1.TikTokError('SESSION_EXPIRED', 'Cookie has no sessionid');
32
32
  }
33
33
  const account = await (0, api_1.fetchAccount)(ctx);
34
34
  if (!account)
35
- throw new errors_1.TikTokError('SESSION_EXPIRED', 'Cookie TikTok không còn đăng nhập');
35
+ throw new errors_1.TikTokError('SESSION_EXPIRED', 'TikTok cookie is no longer logged in');
36
36
  ctx.ownId = account.uid;
37
37
  ctx.deviceId = (await (0, api_1.fetchWebId)(ctx).catch(() => '')) || ctx.deviceId;
38
38
  if (!ctx.deviceId)
39
- throw new errors_1.TikTokError('BAD_RESPONSE', 'Không lấy được wid (device_id) — cookie thiếu ttwid?');
39
+ throw new errors_1.TikTokError('BAD_RESPONSE', 'Could not get wid (device_id) — cookie missing ttwid?');
40
40
  return new api_1.API(ctx, account);
41
41
  }
42
42
  }
@@ -1,21 +1,21 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import type { Context } from './context';
3
3
  import type { TikTokMessage } from './types';
4
- /** Mã đóng do chính mình gọi `stop()`. */
4
+ /** Close code used when we call `stop()` ourselves. */
5
5
  export declare const MANUAL_CLOSE = 4000;
6
6
  export interface ListenerEvents {
7
7
  connected: [];
8
- /** Tin thật trong hội thoại (cả tin của chính mình — xem `isSelf`). */
8
+ /** A real message in a conversation (including our own — see `isSelf`). */
9
9
  message: [TikTokMessage];
10
- /** Lệnh ẩn: đánh dấu đã đọc… (`content.command_type`). */
10
+ /** Hidden command: mark as read… (`content.command_type`). */
11
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). */
12
+ /** Close code + reason. Unless `MANUAL_CLOSE`, the listener reconnects automatically (if enabled). */
13
13
  closed: [number, string];
14
14
  error: [Error];
15
15
  }
16
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`.
17
+ * Listen for new messages via the `im-ws-sg` websocket (aid 1459) — cookies only.
18
+ * Events: `connected`, `message`, `closed`, `error`.
19
19
  */
20
20
  export declare class Listener extends EventEmitter<ListenerEvents> {
21
21
  private readonly ctx;
@@ -29,7 +29,7 @@ export declare class Listener extends EventEmitter<ListenerEvents> {
29
29
  constructor(ctx: Context);
30
30
  get connected(): boolean;
31
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. */
32
+ /** Open the connection. `retryOnClose` (on by default): reconnect on drop, backing off up to 1 minute. */
33
33
  start(opts?: {
34
34
  retryOnClose?: boolean;
35
35
  }): void;
@@ -38,6 +38,6 @@ export declare class Listener extends EventEmitter<ListenerEvents> {
38
38
  private touch;
39
39
  private clearTimers;
40
40
  private onFrame;
41
- /** Báo đã nhận — `ack_id = <giờ base36>:<x_frontier_traceid>`, kèm lại seq/logid của khung. */
41
+ /** Acknowledge receipt — `ack_id = <base36 time>:<x_frontier_traceid>`, echoing the frame's seq/logid. */
42
42
  private ack;
43
43
  }
package/dist/listener.js CHANGED
@@ -11,19 +11,19 @@ const constants_1 = require("./constants");
11
11
  const lz4_1 = require("./lz4");
12
12
  const message_1 = require("./message");
13
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. */
14
+ /** The browser sends "hi" every 15–30 seconds; use the lower bound. */
15
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. */
16
+ /** If nothing (not even a "hi" reply) arrives for this long, treat it as silently dead and reconnect. */
17
17
  const IDLE_TIMEOUT_MS = 75_000;
18
18
  const RECONNECT_MIN_MS = 2_000;
19
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. */
20
+ /** The server pushes the same message several times (one frame per device) — remember this many message IDs to dedupe. */
21
21
  const DEDUPE_KEEP = 500;
22
- /** Mã đóng do chính mình gọi `stop()`. */
22
+ /** Close code used when we call `stop()` ourselves. */
23
23
  exports.MANUAL_CLOSE = 4000;
24
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`.
25
+ * Listen for new messages via the `im-ws-sg` websocket (aid 1459) — cookies only.
26
+ * Events: `connected`, `message`, `closed`, `error`.
27
27
  */
28
28
  class Listener extends node_events_1.EventEmitter {
29
29
  ctx;
@@ -52,7 +52,7 @@ class Listener extends node_events_1.EventEmitter {
52
52
  version_code: 'fws_1.0.0',
53
53
  access_key: accessKey,
54
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.
55
+ // Cookie value is already URL-encoded (`1%7C…`) — decode it so URLSearchParams doesn't double-encode.
56
56
  ttwid: decodeURIComponent(this.ctx.cookies.get('ttwid') ?? ''),
57
57
  xsack: '1',
58
58
  xaack: '1',
@@ -60,7 +60,7 @@ class Listener extends node_events_1.EventEmitter {
60
60
  });
61
61
  return `${constants_1.IM_WS}?${q}`;
62
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. */
63
+ /** Open the connection. `retryOnClose` (on by default): reconnect on drop, backing off up to 1 minute. */
64
64
  start(opts = {}) {
65
65
  this.retry = opts.retryOnClose ?? true;
66
66
  this.connect();
@@ -86,13 +86,13 @@ class Listener extends node_events_1.EventEmitter {
86
86
  this.backoff = RECONNECT_MIN_MS;
87
87
  this.heartbeat = setInterval(() => ws.readyState === ws_1.default.OPEN && ws.send('hi'), HEARTBEAT_MS);
88
88
  this.touch();
89
- this.ctx.log('debug', 'tca: websocket đã nối');
89
+ this.ctx.log('debug', 'tca: websocket connected');
90
90
  this.emit('connected');
91
91
  });
92
92
  ws.on('message', (data, isBinary) => {
93
93
  this.touch();
94
94
  if (!isBinary)
95
- return; // "hi" đáp lại
95
+ return; // "hi" reply
96
96
  try {
97
97
  this.onFrame(ws, data);
98
98
  }
@@ -103,7 +103,7 @@ class Listener extends node_events_1.EventEmitter {
103
103
  ws.on('error', (err) => this.emit('error', err));
104
104
  ws.on('close', (code, reason) => {
105
105
  if (this.ws !== ws)
106
- return; // kết nối cũ đã bị thay
106
+ return; // old connection was replaced
107
107
  this.ws = null;
108
108
  this.clearTimers();
109
109
  this.emit('closed', code, reason.toString());
@@ -111,7 +111,7 @@ class Listener extends node_events_1.EventEmitter {
111
111
  return;
112
112
  const wait = this.backoff;
113
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`);
114
+ this.ctx.log('warn', `tca: websocket closed (${code}) — reconnecting in ${wait / 1000}s`);
115
115
  this.reconnectTimer = setTimeout(() => this.connect(), wait);
116
116
  });
117
117
  }
@@ -119,7 +119,7 @@ class Listener extends node_events_1.EventEmitter {
119
119
  if (this.idle)
120
120
  clearTimeout(this.idle);
121
121
  this.idle = setTimeout(() => {
122
- this.ctx.log('warn', 'tca: websocket im lặng quá lâu — nối lại');
122
+ this.ctx.log('warn', 'tca: websocket silent too long — reconnecting');
123
123
  this.ws?.terminate();
124
124
  }, IDLE_TIMEOUT_MS);
125
125
  }
@@ -148,7 +148,7 @@ class Listener extends node_events_1.EventEmitter {
148
148
  const resp = proto_1.Msg.tryParse(payload);
149
149
  if (!resp || resp.num(constants_1.Resp.cmd) !== constants_1.Cmd.NewMessage)
150
150
  return;
151
- // NewMessageNotify: {2 conversationId, 3 loại, 4 ?, 5 MessageBody}
151
+ // NewMessageNotify: {2 conversationId, 3 type, 4 ?, 5 MessageBody}
152
152
  const body = resp.msg(constants_1.Resp.body)?.msg(constants_1.Cmd.NewMessage)?.msg(5);
153
153
  if (!body)
154
154
  return;
@@ -163,7 +163,7 @@ class Listener extends node_events_1.EventEmitter {
163
163
  else
164
164
  this.emit('message', msg);
165
165
  }
166
- /** Báo đã nhận — `ack_id = <giờ base36>:<x_frontier_traceid>`, kèm lại seq/logid của khung. */
166
+ /** Acknowledge receipt — `ack_id = <base36 time>:<x_frontier_traceid>`, echoing the frame's seq/logid. */
167
167
  ack(ws, f) {
168
168
  const ackId = `${Date.now().toString(36)}:${f.headers.x_frontier_traceid ?? ''}`;
169
169
  const out = new proto_1.Writer()
package/dist/lz4.d.ts CHANGED
@@ -1,5 +1,5 @@
1
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.
2
+ * LZ4 block decompression (no frame header) — websocket frames with
3
+ * `payload_encoding = "__lz4"` compress their payload this way. No extra dependency for ~30 lines.
4
4
  */
5
5
  export declare function lz4Decompress(src: Buffer): Buffer;
package/dist/lz4.js CHANGED
@@ -2,8 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.lz4Decompress = lz4Decompress;
4
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.
5
+ * LZ4 block decompression (no frame header) — websocket frames with
6
+ * `payload_encoding = "__lz4"` compress their payload this way. No extra dependency for ~30 lines.
7
7
  */
8
8
  function lz4Decompress(src) {
9
9
  let out = Buffer.alloc(Math.max(src.length * 4, 1024));
@@ -38,7 +38,7 @@ function lz4Decompress(src) {
38
38
  const offset = src[i] | (src[i + 1] << 8);
39
39
  i += 2;
40
40
  if (offset === 0 || offset > o)
41
- throw new Error('lz4: offset hỏng');
41
+ throw new Error('lz4: invalid offset');
42
42
  let len = token & 15;
43
43
  if (len === 15) {
44
44
  let b;
@@ -49,7 +49,7 @@ function lz4Decompress(src) {
49
49
  }
50
50
  len += 4;
51
51
  ensure(len);
52
- // Sao từng byte: vùng nguồn và đích được phép chồng nhau.
52
+ // Copy byte by byte: source and destination ranges may overlap.
53
53
  for (let k = 0; k < len; k++, o++)
54
54
  out[o] = out[o - offset];
55
55
  }
package/dist/media.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Context } from './context';
2
2
  import type { ImageInput, TikTokImage } from './types';
3
- /** Ảnh đã tải lên + được kho mã hoá — đủ để gửi tin ảnh. */
3
+ /** Uploaded image, encrypted by the store — enough to send an image message. */
4
4
  export interface UploadedImage {
5
5
  uri: string;
6
6
  secretKey: string;
@@ -13,15 +13,15 @@ export declare class MediaClient {
13
13
  private readonly ctx;
14
14
  private cred;
15
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. */
16
+ /** `UploadConfig` returns two credential sets: type 1 = ImageX (images), type 2 = VOD (video). Use type 1. */
17
17
  private credential;
18
- /** Gọi ImageX ký AWS SigV4 (vùng cn-north-1, dịch vụ imagex). */
18
+ /** Call ImageX signed with AWS SigV4 (region cn-north-1, service imagex). */
19
19
  private imagex;
20
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.
21
+ * Upload an image to the private message store: Apply → POST file → Commit (the store encrypts with AES-GCM
22
+ * and returns the key). Image dimensions come from the Commit result — no need to decode the image.
23
23
  */
24
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. */
25
+ /** Download and decrypt a message image. Layout: 12-byte IV ‖ ciphertext ‖ 16-byte tag. */
26
26
  download(image: TikTokImage, which?: 'origin' | 'thumb'): Promise<Buffer>;
27
27
  }
package/dist/media.js CHANGED
@@ -14,14 +14,14 @@ class MediaClient {
14
14
  constructor(ctx) {
15
15
  this.ctx = ctx;
16
16
  }
17
- /** `UploadConfig` trả hai bộ khoá: loại 1 = ImageX (ảnh), loại 2 = VOD (video). Lấy loại 1. */
17
+ /** `UploadConfig` returns two credential sets: type 1 = ImageX (images), type 2 = VOD (video). Use type 1. */
18
18
  async credential() {
19
19
  if (this.cred && this.cred.expiresAt - Date.now() > 60_000)
20
20
  return this.cred;
21
21
  const body = await this.ctx.im('/v1/media/upload_config', constants_1.Cmd.UploadConfig, Buffer.alloc(0));
22
22
  const c = body.msgs(1).find((m) => m.num(1) === 1);
23
23
  if (!c)
24
- throw new errors_1.TikTokError('UPLOAD_FAILED', 'upload_config không có khoá ImageX');
24
+ throw new errors_1.TikTokError('UPLOAD_FAILED', 'upload_config has no ImageX credentials');
25
25
  this.cred = {
26
26
  serviceId: c.str(2),
27
27
  host: c.str(3),
@@ -32,7 +32,7 @@ class MediaClient {
32
32
  };
33
33
  return this.cred;
34
34
  }
35
- /** Gọi ImageX ký AWS SigV4 (vùng cn-north-1, dịch vụ imagex). */
35
+ /** Call ImageX signed with AWS SigV4 (region cn-north-1, service imagex). */
36
36
  async imagex(method, params, body) {
37
37
  const c = await this.credential();
38
38
  const amzDate = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d+/, '');
@@ -78,8 +78,8 @@ class MediaClient {
78
78
  return json;
79
79
  }
80
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.
81
+ * Upload an image to the private message store: Apply → POST file → Commit (the store encrypts with AES-GCM
82
+ * and returns the key). Image dimensions come from the Commit result — no need to decode the image.
83
83
  */
84
84
  async upload(image) {
85
85
  const c = await this.credential();
@@ -96,13 +96,13 @@ class MediaClient {
96
96
  const store = addr.StoreInfos[0];
97
97
  const host = addr.UploadHosts[0];
98
98
  if (!store || !host)
99
- throw new errors_1.TikTokError('UPLOAD_FAILED', 'ApplyImageUpload không trả chỗ tải lên');
99
+ throw new errors_1.TikTokError('UPLOAD_FAILED', 'ApplyImageUpload returned no upload target');
100
100
  const up = await fetch(`https://${host}/upload/v1/${store.StoreUri}`, {
101
101
  method: 'POST',
102
102
  headers: {
103
103
  Authorization: store.Auth,
104
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.
105
+ // Without this header the store returns 4004 Not Found.
106
106
  'X-Storage-U': this.ctx.ownId,
107
107
  'Content-Type': 'application/octet-stream',
108
108
  'Content-Disposition': 'attachment; filename="undefined"',
@@ -118,7 +118,7 @@ class MediaClient {
118
118
  const enc = commit.Result.Results?.[0]?.Encryption;
119
119
  const plugin = commit.Result.PluginResult?.[0];
120
120
  if (!enc?.Uri || !enc.SecretKey)
121
- throw new errors_1.TikTokError('UPLOAD_FAILED', 'CommitImageUpload không trả bản mã hoá');
121
+ throw new errors_1.TikTokError('UPLOAD_FAILED', 'CommitImageUpload returned no encrypted result');
122
122
  return {
123
123
  uri: enc.Uri,
124
124
  secretKey: enc.SecretKey,
@@ -128,14 +128,14 @@ class MediaClient {
128
128
  filename: image.filename,
129
129
  };
130
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. */
131
+ /** Download and decrypt a message image. Layout: 12-byte IV ‖ ciphertext ‖ 16-byte tag. */
132
132
  async download(image, which = 'origin') {
133
133
  const url = which === 'thumb' && image.thumbUrl ? image.thumbUrl : image.url;
134
134
  if (!url)
135
- throw new errors_1.TikTokError('DOWNLOAD_FAILED', 'Ảnh không có link');
135
+ throw new errors_1.TikTokError('DOWNLOAD_FAILED', 'Image has no URL');
136
136
  const res = await this.ctx.fetch(url);
137
137
  if (!res.ok)
138
- throw new errors_1.TikTokError('DOWNLOAD_FAILED', `Tải ảnh: HTTP ${res.status}`, res.status);
138
+ throw new errors_1.TikTokError('DOWNLOAD_FAILED', `Image download: HTTP ${res.status}`, res.status);
139
139
  const enc = Buffer.from(await res.arrayBuffer());
140
140
  if (!image.decryptKey)
141
141
  return enc;
@@ -145,7 +145,7 @@ class MediaClient {
145
145
  return Buffer.concat([d.update(enc.subarray(12, enc.length - 16)), d.final()]);
146
146
  }
147
147
  catch (err) {
148
- throw new errors_1.TikTokError('DOWNLOAD_FAILED', `Giải mã ảnh: ${err instanceof Error ? err.message : String(err)}`);
148
+ throw new errors_1.TikTokError('DOWNLOAD_FAILED', `Image decryption: ${err instanceof Error ? err.message : String(err)}`);
149
149
  }
150
150
  }
151
151
  }
package/dist/message.d.ts CHANGED
@@ -2,5 +2,5 @@ import { Msg } from './proto';
2
2
  import type { TikTokConversation, TikTokMessage } from './types';
3
3
  /** `MessageBody` → `TikTokMessage`. */
4
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}}…}. */
5
+ /** `ConversationInfo` (field 2 of get_by_user_init): {1 id, 2 shortId, 3 type, 6 {1 {1 uid}}…}. */
6
6
  export declare function parseConversation(m: Msg, ownId: string): TikTokConversation;
package/dist/message.js CHANGED
@@ -11,7 +11,7 @@ function parseJson(s) {
11
11
  return s;
12
12
  }
13
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á}. */
14
+ /** Image in `attachments` (field 20 → 4): 1 = original, 2 = thumbnail; each {1 uri, 2 url…, 5 {w,h}, 6 key}. */
15
15
  function parseImage(m) {
16
16
  const media = m.msg(constants_1.MsgF.attachments)?.msg(4);
17
17
  const origin = media?.msg(1);
@@ -28,8 +28,8 @@ function parseImage(m) {
28
28
  };
29
29
  }
30
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.
31
+ * Sticker (field 20 → 7): `{1 {1 uri, 2 url, 5 {w,h}}, 2 hint, 200 {1 {2 map a:sticker_id, a:sticker_type…}}}`.
32
+ * Links point to the public sticker CDN (`…-dm-sticker-sign-…`) — not encrypted, no cookies needed.
33
33
  */
34
34
  function parseSticker(m) {
35
35
  const st = m.msg(constants_1.MsgF.attachments)?.msg(7);
@@ -47,8 +47,8 @@ function parseSticker(m) {
47
47
  };
48
48
  }
49
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.
50
+ * Reaction (field 15, `property_list`): each entry `{1 key, 2 {1 PropertyItem[]}}`, key `e:<emoji>`,
51
+ * `PropertyItem {1 uid, 3 sec, 4 idempotent_id, 6 µs}`. The mobile app writes ❤️ as both `e:❤️` and `e:love` → merged.
52
52
  */
53
53
  function parseReactions(m) {
54
54
  const byEmoji = new Map();
@@ -101,7 +101,7 @@ function parseMessage(m, ownId) {
101
101
  ext,
102
102
  };
103
103
  }
104
- /** `ConversationInfo` (trường 2 của get_by_user_init): {1 id, 2 shortId, 3 loại, 6 {1 {1 uid}}…}. */
104
+ /** `ConversationInfo` (field 2 of get_by_user_init): {1 id, 2 shortId, 3 type, 6 {1 {1 uid}}…}. */
105
105
  function parseConversation(m, ownId) {
106
106
  const participants = (m.msg(6)?.msgs(1) ?? []).map((p) => p.id(1));
107
107
  return {
package/dist/proto.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Protobuf tối thiểu, không cần file `.proto`.
2
+ * Minimal protobuf, no `.proto` file needed.
3
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.
4
+ * TikTok doesn't publish its IM `.proto` — field numbers in `constants.ts` were reconstructed from bytes captured
5
+ * in the browser (09/11/2026). Only three wire types are needed: varint (0), length-delimited (2), and skipping 1/5.
6
6
  */
7
7
  export interface Field {
8
8
  n: number;
@@ -10,9 +10,9 @@ export interface Field {
10
10
  int: bigint;
11
11
  bytes: Buffer;
12
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ệ. */
13
+ /** Split a message into a list of fields. `null` if the bytes are not valid protobuf. */
14
14
  export declare function decodeFields(b: Buffer): Field[] | null;
15
- /** Message đã giải — đọc theo số trường. */
15
+ /** Decoded message — read by field number. */
16
16
  export declare class Msg {
17
17
  readonly fields: Field[];
18
18
  private constructor();
@@ -21,23 +21,23 @@ export declare class Msg {
21
21
  has(n: number): boolean;
22
22
  int(n: number): bigint;
23
23
  num(n: number): number;
24
- /** Số nguyên 64-bit dạng chuỗi — uid / mã tin vượt `Number.MAX_SAFE_INTEGER`. */
24
+ /** 64-bit integer as a string — uids / message IDs exceed `Number.MAX_SAFE_INTEGER`. */
25
25
  id(n: number): string;
26
26
  bytes(n: number): Buffer;
27
27
  str(n: number): string;
28
28
  strs(n: number): string[];
29
29
  msg(n: number): Msg | null;
30
30
  msgs(n: number): Msg[];
31
- /** `map<string,string>` — mỗi phần tử là message `{1: key, 2: value}`. */
31
+ /** `map<string,string>` — each entry is a message `{1: key, 2: value}`. */
32
32
  map(n: number): Record<string, string>;
33
33
  }
34
- /** Ghép message theo thứ tự gọi. */
34
+ /** Build a message in call order. */
35
35
  export declare class Writer {
36
36
  private readonly parts;
37
37
  int(n: number, v: bigint | number | string): this;
38
38
  bytes(n: number, v: Buffer | string): this;
39
39
  msg(n: number, w: Writer): this;
40
- /** `map<string,string>` theo đúng thứ tự khoá của object. */
40
+ /** `map<string,string>` in the object's key order. */
41
41
  map(n: number, obj: Record<string, string>): this;
42
42
  finish(): Buffer;
43
43
  }
package/dist/proto.js CHANGED
@@ -1,9 +1,9 @@
1
1
  "use strict";
2
2
  /**
3
- * Protobuf tối thiểu, không cần file `.proto`.
3
+ * Minimal protobuf, no `.proto` file needed.
4
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.
5
+ * TikTok doesn't publish its IM `.proto` — field numbers in `constants.ts` were reconstructed from bytes captured
6
+ * in the browser (09/11/2026). Only three wire types are needed: varint (0), length-delimited (2), and skipping 1/5.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.Writer = exports.Msg = void 0;
@@ -14,14 +14,14 @@ function readVarint(b, i) {
14
14
  let c;
15
15
  do {
16
16
  if (i >= b.length)
17
- throw new Error('varint vượt cuối buffer');
17
+ throw new Error('varint runs past end of buffer');
18
18
  c = b[i++];
19
19
  x |= BigInt(c & 127) << shift;
20
20
  shift += 7n;
21
21
  } while (c & 128);
22
22
  return [x, i];
23
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ệ. */
24
+ /** Split a message into a list of fields. `null` if the bytes are not valid protobuf. */
25
25
  function decodeFields(b) {
26
26
  const out = [];
27
27
  let i = 0;
@@ -63,7 +63,7 @@ function decodeFields(b) {
63
63
  }
64
64
  return out;
65
65
  }
66
- /** Message đã giải — đọc theo số trường. */
66
+ /** Decoded message — read by field number. */
67
67
  class Msg {
68
68
  fields;
69
69
  constructor(fields) {
@@ -72,7 +72,7 @@ class Msg {
72
72
  static parse(b) {
73
73
  const f = decodeFields(b);
74
74
  if (!f)
75
- throw new Error('Không giải được protobuf');
75
+ throw new Error('Failed to decode protobuf');
76
76
  return new Msg(f);
77
77
  }
78
78
  static tryParse(b) {
@@ -90,7 +90,7 @@ class Msg {
90
90
  num(n) {
91
91
  return Number(this.int(n));
92
92
  }
93
- /** Số nguyên 64-bit dạng chuỗi — uid / mã tin vượt `Number.MAX_SAFE_INTEGER`. */
93
+ /** 64-bit integer as a string — uids / message IDs exceed `Number.MAX_SAFE_INTEGER`. */
94
94
  id(n) {
95
95
  return this.int(n).toString();
96
96
  }
@@ -112,7 +112,7 @@ class Msg {
112
112
  .map((f) => Msg.tryParse(f.bytes))
113
113
  .filter((m) => m !== null);
114
114
  }
115
- /** `map<string,string>` — mỗi phần tử là message `{1: key, 2: value}`. */
115
+ /** `map<string,string>` — each entry is a message `{1: key, 2: value}`. */
116
116
  map(n) {
117
117
  const out = {};
118
118
  for (const m of this.msgs(n))
@@ -134,7 +134,7 @@ function varint(x) {
134
134
  return Buffer.from(out);
135
135
  }
136
136
  const key = (n, wire) => varint((BigInt(n) << 3n) | BigInt(wire));
137
- /** Ghép message theo thứ tự gọi. */
137
+ /** Build a message in call order. */
138
138
  class Writer {
139
139
  parts = [];
140
140
  int(n, v) {
@@ -149,7 +149,7 @@ class Writer {
149
149
  msg(n, w) {
150
150
  return this.bytes(n, w.finish());
151
151
  }
152
- /** `map<string,string>` theo đúng thứ tự khoá của object. */
152
+ /** `map<string,string>` in the object's key order. */
153
153
  map(n, obj) {
154
154
  for (const [k, v] of Object.entries(obj))
155
155
  this.msg(n, new Writer().bytes(1, k).bytes(2, v));
@@ -1,41 +1,41 @@
1
1
  import type { Credentials } from './types';
2
2
  /**
3
- * Đăng nhập bằng QR qua Chrome headless.
3
+ * QR login via headless Chrome.
4
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.
5
+ * Why a browser: TikTok's QR API (`passport/web/check_qrconnect`) requires a JS signature
6
+ * (X-Gnarly / msToken) — calling it bare from Node yields `error_code 7`. So we open the real
7
+ * `tiktok.com/login/qrcode` page in headless Chrome and let it sign itself; we only LISTEN to
8
+ * responses via CDP: the QR image from the `get_qrcode` body, the status from `check_qrconnect`.
9
+ * After login we grab the cookies and close Chrome — everything after (`login`, chat) needs no browser.
10
10
  */
11
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. */
12
+ /** New QR image (`data:image/png;base64,…`) — show it to the user to scan. A new one comes on expiry. */
13
13
  {
14
14
  type: 'qr';
15
15
  image: string;
16
16
  expiresAt: number;
17
17
  }
18
- /** Đã quét, chờ bấm xác nhận trên điện thoại. */
18
+ /** Scanned, waiting for confirmation on the phone. */
19
19
  | {
20
20
  type: 'scanned';
21
21
  }
22
- /** Đã xác nhận — đang lấy cookie. */
22
+ /** Confirmed — fetching cookies. */
23
23
  | {
24
24
  type: 'confirmed';
25
25
  };
26
26
  export interface QrLoginOptions {
27
- /** Đường dẫn Chrome / Chromium (vd `/usr/bin/chromium-browser`). */
27
+ /** Path to Chrome / Chromium (e.g. `/usr/bin/chromium-browser`). */
28
28
  chromePath: string;
29
29
  onEvent?: (e: QrLoginEvent) => void;
30
30
  signal?: AbortSignal;
31
- /** Tổng thời gian chờ quét (ms) — mặc định 5 phút. */
31
+ /** Total time to wait for a scan (ms) — default 5 minutes. */
32
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. */
33
+ /** When the QR expires (~1 minute), regenerate it at most this many times. */
34
34
  maxRefresh?: number;
35
35
  userAgent?: string;
36
36
  }
37
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.
38
+ * Log in to TikTok via QR. Returns `Credentials` (`.tiktok.com` cookies + UA) — pass them to
39
+ * `new TikTok().login(creds)` and SAVE them so you don't need to scan next time.
40
40
  */
41
41
  export declare function loginQR(opts: QrLoginOptions): Promise<Credentials>;