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/README.md CHANGED
@@ -1,133 +1,235 @@
1
- # tca-js — Unofficial TikTok Chat API
1
+ <div align="center">
2
2
 
3
- Read and send TikTok **direct messages** (text, images, reactions) and receive new messages in real time over
4
- websocket — **using session cookies only, no browser needed**. Same idea as [`zca-js`](https://github.com/RFS-ADRENO/zca-js)
5
- for Zalo: it speaks the same protocol as TikTok Web (`tiktok.com/messages`, `tiktok.com/business-suite/messages`).
3
+ # tca-js
6
4
 
7
- > [!WARNING]
8
- > This is an **unofficial** library, not affiliated with or endorsed by TikTok / ByteDance. It relies on undocumented
9
- > internal APIs that can change or break at any time. Automating a TikTok account may violate TikTok's Terms of Service
10
- > and **can get the account restricted or banned** — use it at your own risk, don't spam.
5
+ **Unofficial TikTok Chat API for Node.js**
6
+
7
+ Read and send TikTok direct messages, react, and receive new messages in real time.
11
8
 
12
- ## Install
9
+ [![npm](https://img.shields.io/npm/v/tca-js)](https://www.npmjs.com/package/tca-js)
10
+ [![license](https://img.shields.io/github/license/rice2411/tca-js)](LICENSE)
11
+ [![node](https://img.shields.io/node/v/tca-js)](package.json)
12
+
13
+ </div>
14
+
15
+ > [!WARNING]
16
+ > This project is not affiliated with or endorsed by TikTok / ByteDance. It relies on undocumented APIs that can change
17
+ > at any time. Automating an account may violate TikTok's Terms of Service and can get it restricted — use at your own risk.
18
+
19
+ ## Table of Contents
20
+
21
+ - [Features](#features)
22
+ - [Installation](#installation)
23
+ - [Quick Start](#quick-start)
24
+ - [Authentication](#authentication)
25
+ - [API Reference](#api-reference)
26
+ - [Listener](#listener)
27
+ - [Error Handling](#error-handling)
28
+ - [Examples](#examples)
29
+ - [Contributing](#contributing)
30
+ - [Support Us](#support-us)
31
+ - [License](#license)
32
+
33
+ ## Features
34
+
35
+ - 💬 Send text and image messages
36
+ - 📥 List conversations, page through message history, look up users
37
+ - ⚡ Real-time listener over websocket with auto-reconnect
38
+ - 🖼️ Download and decrypt private images, download stickers and avatars
39
+ - ❤️ Read and set reactions, mark conversations as read
40
+ - 🔐 Cookie-based session — no browser at runtime
41
+ - 📱 Optional QR-code login
42
+ - 📦 Written in TypeScript, ships ESM + CommonJS builds with type definitions, one runtime dependency (`ws`)
43
+
44
+ ## Installation
13
45
 
14
46
  ```bash
15
47
  npm install tca-js
16
48
  ```
17
49
 
18
- Node.js ≥ 22. QR login additionally needs a local Chrome / Chromium (see below); cookie login does not.
50
+ Requires **Node.js 22+**. Both ESM and CommonJS are supported:
19
51
 
20
- ## Quick start
52
+ ```ts
53
+ import { TikTok } from 'tca-js'; // ESM / TypeScript
54
+ const { TikTok } = require('tca-js'); // CommonJS
55
+ ```
56
+
57
+ QR login also needs a local Chrome or Chromium; cookie login does not.
58
+
59
+ ## Quick Start
21
60
 
22
61
  ```ts
23
- import { TikTok, loginQR } from 'tca-js';
62
+ import { TikTok } from 'tca-js';
63
+
64
+ const api = await new TikTok().login({ cookies });
24
65
 
25
- const api = await new TikTok().login(await loginQR({ chromePath, onEvent })); // or saved cookies
26
66
  api.listener.on('message', async (msg) => {
27
67
  if (msg.isSelf) return;
28
- if (msg.image) await api.downloadImage(msg); // decrypted image Buffer
29
- await api.sendMessage(msg, 'Got it, thanks!'); // a message carries its conversation
68
+ await api.sendMessage(msg, `You said: ${msg.text}`);
30
69
  });
31
- api.listener.start(); // reconnects on drop
32
- await api.sendImage(conv, { data: buf, filename: 'photo.jpg' });
70
+
71
+ api.listener.start();
33
72
  ```
34
73
 
35
- ## API
74
+ ## Authentication
36
75
 
37
- | Method | Does |
38
- |---|---|
39
- | `loginQR({chromePath, onEvent, signal})` | QR login through headless Chrome → `Credentials` |
40
- | `new TikTok().login({cookies, deviceId?, userAgent?})` | validate cookies (`passport/web/account/info`), resolve `wid` as `device_id` → `API` |
41
- | `api.getConversations(cursor?)` | 10 conversations per page + latest message; page with `cursor` |
42
- | `api.getMessages(conv, {limit, before})` | messages of a conversation, newest → oldest |
43
- | `api.getUsers(uids)` | display name, `@username`, avatar |
44
- | `api.sendMessage(conv, text)` / `api.sendImage(conv, {data, filename})` | send |
45
- | `api.setReaction(msg, emoji \| null)` | your reaction on a message (one per user, `null` = remove); read via `msg.reactions` |
46
- | `api.downloadSticker(msg.sticker)` | sticker image (public CDN) |
47
- | `api.downloadImage(msg)` | download + decrypt AES-256-GCM (listener messages carry no link — looked up from history) |
48
- | `api.markRead(msg)` · `api.isAlive()` · `api.getCookies()` | |
49
- | `api.listener` | events `connected` · `message` · `command` (read receipts…) · `closed` · `error` |
76
+ ### Cookies
50
77
 
51
- Errors: `TikTokError.code` — `SESSION_EXPIRED` (get fresh cookies), `IM_ERROR`, `UPLOAD_FAILED`, `DOWNLOAD_FAILED`, `HTTP_ERROR`.
78
+ Log in with the cookies of a `tiktok.com` session. Accepted formats:
52
79
 
53
- ## Session — QR login and cookies
80
+ ```ts
81
+ await new TikTok().login({ cookies: [{ name: 'sessionid', value: '…' } /* … */] }); // array
82
+ await new TikTok().login({ cookies: { sessionid: '…', ttwid: '…' } }); // object
83
+ await new TikTok().login({ cookies: 'sessionid=…; ttwid=…' }); // header string
84
+ ```
85
+
86
+ Required cookies: `sessionid`, `sid_tt`, `sid_guard`, `uid_tt`, `ttwid`, `odin_tt`, `store-idc`, `tt-target-idc`.
87
+ A session lasts about 6 months, and logging in does not sign out other devices.
88
+
89
+ ### QR code
90
+
91
+ `loginQR` opens a headless Chrome on TikTok's QR page, emits the QR image, and resolves with credentials once the code is
92
+ scanned and confirmed on the phone.
54
93
 
55
94
  ```ts
56
95
  import { loginQR, TikTok } from 'tca-js';
57
- const creds = await loginQR({ chromePath: '/usr/bin/chromium-browser', onEvent: (e) => { /* qr | scanned | confirmed */ } });
58
- const api = await new TikTok().login(creds); // SAVE creds (cookies) to skip scanning next time
96
+
97
+ const credentials = await loginQR({
98
+ onEvent: (e) => {
99
+ if (e.type === 'qr') showQr(e.image); // data:image/png;base64,…
100
+ },
101
+ });
102
+
103
+ const api = await new TikTok().login(credentials);
104
+ // Save `credentials` to skip scanning next time.
59
105
  ```
60
106
 
61
- - **Reading / sending / websocket only need cookies.** The `X-Gnarly`, `X-Dynosaur`, `X-Bogus`, `msToken` signatures
62
- and `tt-ticket-guard-*` headers the page attaches to IM requests are **not checked** by the server.
63
- - **The QR login API (`passport/web/check_qrconnect`) DOES check signatures** — calling it bare from Node returns
64
- `error_code 7`. So `loginQR` opens **headless Chrome/Chromium** at `tiktok.com/login/qrcode`, lets the page sign its
65
- own requests, and only LISTENS via CDP: the QR image comes from the `get_qrcode` response, status
66
- (`new → scanned → confirmed`, or `expired` → reload for a new QR) from `check_qrconnect`. Once confirmed it reads the
67
- cookies (`Network.getAllCookies`) and closes Chrome. Works with `--no-sandbox` Chromium on Alpine.
68
- - Core cookies: `sessionid`, `sid_tt`, `sid_guard`, `uid_tt`, `ttwid`, `odin_tt`, `store-idc`, `tt-target-idc`.
69
- `sessionid` lives ~6 months. TikTok allows concurrent sessions — logging in here doesn't kick other devices.
70
- - `device_id` = the `wid` TikTok issues for a `ttwid` (`/api/v1/web-cookie-privacy/config`). HTTP IM accepts any id
71
- but **the websocket only accepts the real `wid`** (made-up id → 400).
72
-
73
- ## Protocol (for fixing it when TikTok changes)
74
-
75
- **HTTP IM** — `POST https://im-api-sg.tiktok.com/<path>?aid=1988&…`, protobuf request and response.
76
- `Request {1 cmd, 2 seq, 3 sdk "1.8.5", 5 3, 6 0, 7 build, 8 {<cmd>: body}, 9 device_id, 11 "web", 15 map, 18 1}`,
77
- `Response {1 cmd, 3 status_code, 4 error, 6 {<cmd>: body}}` (`200005` = not logged in).
78
-
79
- | path | cmd | body |
80
- |---|---|---|
81
- | `/v1/message/send` | 100 | `{1 convId, 2 1, 3 shortId, 4 content JSON, 5 ext, 6 type, 7 "deprecated", 8 clientMsgId, [13 image, 14 "private_image", 17 image]}` |
82
- | `/v2/message/get_by_user_init` | 203 | `{1 offset}` → `{1 messages[], 2 conversations[], 4 next offset, 5 has more}` |
83
- | `/v1/message/get_by_conversation` | 301 | `{1 convId, 2 1, 3 shortId, 4 1 (older), 5 index anchor, 6 limit}` |
84
- | `/v3/conversation/mark_read` | 2002 (body at 604) | `{1 convId, 2 shortId, 3 1, 4 index, 11 4}` |
85
- | `/v1/media/upload_config` | 2059 | empty → ImageX STS keys |
86
- | `/v1/message/set_property` | 705 | `{1 {1 convId, 2 1, 3 shortId, 4 serverMsgId, 5 clientMsgId, 6 [{1 op 0 add/1 remove, 2 "e:<emoji>", 4 own uid}]}, 2 ticket}` → `{1 status (0 ok, 1 duplicate), 2 version}` |
87
-
88
- `MessageBody {1 convId, 3 serverMsgId, 4 index, 5 shortId, 6 type, 7 sender, 8 content JSON, 9 ext, 10 time ms, 15 reactions, 18 quote, 20 image/sticker}`.
89
- Types: `7` text (`{"aweType":0,"text":…}`), `1802` private image, `1805` sticker, `8` shared video, `1` system, `50001` hidden command.
90
-
91
- **Reactions** (field 15): `{1 "e:<emoji>", 2 {1 [{1 uid, 3 seconds, 4 idempotent_id, 6 µs}]}}`.
92
- The mobile app writes ❤️ as both `e:❤️` and the legacy `e:love` key (web ignores `e:love`) → `tca-js` merges them into ❤️.
93
- Web offers 12 emoji, one reaction per user (add the new key + remove your old one in the same 705 command).
94
- **Stickers** (`1805`, field 20 → 7): `{1 {1 uri, 2 public CDN url, 5 {w,h}}, 200 {1 {2 map a:sticker_id, a:sticker_type (14 stock / 15 custom)…}}}`.
95
- TikTok Web only DISPLAYS stickers, it has no send button — sending stickers is not implemented.
96
- 1-1 `convId` = `0:1:<smaller uid>:<larger uid>`.
97
-
98
- **Websocket** — `wss://im-ws-sg.tiktok.com/ws/v2?aid=1459&fpid=9&access_key=…&device_id=<wid>&ttwid=…`
99
- + cookies. `access_key = md5("9" + "e1bd35ec9db7b8d846de66ed140b1ad9" + wid + "f8a69f1719916z")`.
100
- Frame `{1 seq, 2 logid, 3 5, 4 1, 5 headers, 6 encoding ("__lz4" = block compression), 7 "pb", 8 Response}`;
101
- new message = `Response.cmd 500`, `body[500][5]` is a `MessageBody`. Keep-alive: send text `"hi"` every 15 s.
102
- Frames with header `need_ack=1` → reply `{1 seq, 2 logid, 3 5, 4 1, 5 {is_ack:1, ack_id, ack_code:0}, 9 ack_id}`,
103
- `ack_id = <Date.now() base36>:<x_frontier_traceid>`. The server may push a message several times → dedupe by message id.
104
-
105
- **Sending images** — `upload_config` → ImageX signed with AWS SigV4 (`cn-north-1`/`imagex`): `ApplyImageUpload` →
106
- `POST https://<UploadHost>/upload/v1/<StoreUri>` (headers `Authorization: <Auth>`, `Content-CRC32`,
107
- **`X-Storage-U: <uid>`** — missing it gives 4004) → `CommitImageUpload` with `Functions: Encryption cipher_v2`
108
- → returns `Uri` + `SecretKey` + dimensions → send message type `1802`.
109
- **Receiving images** — `p0-tiktok-dm-private-sg…` links (need cookies), content is AES-256-GCM:
110
- `12-byte IV ‖ ciphertext ‖ 16-byte tag`, key = `decrypt_key` (hex) in the message.
111
-
112
- ## Re-discovering the protocol when it breaks
113
-
114
- Run Chrome with a debug port and capture traffic over CDP (`Network.*` for every target, including the chat iframe
115
- `tiktok.com/messages?scene=business`), then print the protobuf tree without a `.proto`. The web app's message bundle
116
- (`messages-vmok` manifest) ships full protobufjs schemas — the fastest way to find new commands. Requests the browser
117
- sends are the reference: rebuild them with `Writer`, drop the signatures, check the server still accepts them.
118
-
119
- ## Development
107
+ | Option | Type | Default | Description |
108
+ | ------------ | ------------- | ----------- | ---------------------------------------------------------------------------------- |
109
+ | `chromePath` | `string` | auto-detect | Path to Chrome / Chromium (falls back to `CHROME_PATH`, then common install paths) |
110
+ | `onEvent` | `(e) => void` | — | `qr` · `scanned` · `confirmed` |
111
+ | `signal` | `AbortSignal` | — | Cancel the login |
112
+ | `timeoutMs` | `number` | `300000` | Total time to wait for a scan |
113
+ | `maxRefresh` | `number` | `3` | Max QR regenerations after expiry |
114
+ | `userAgent` | `string` | — | Browser user agent |
115
+
116
+ ## API Reference
117
+
118
+ `new TikTok(options?)` — `logger?: (level, msg) => void`, `timeoutMs?: number`.
119
+ `.login(credentials)` resolves to an `API` instance.
120
+
121
+ ### Conversations & messages
122
+
123
+ | Method | Returns | Description |
124
+ | ---------------------------------------- | ------------------ | ---------------------------------------------------- |
125
+ | `getConversations(cursor?)` | `ConversationPage` | 10 conversations per page with their latest messages |
126
+ | `getMessages(conv, { limit?, before? })` | `MessagePage` | Messages of a conversation, newest first |
127
+ | `sendMessage(conv, text)` | `SendResult` | Send a text message |
128
+ | `sendImage(conv, { data, filename })` | `SendResult` | Send an image (`Buffer`) |
129
+ | `setReaction(msg, emoji \| null)` | `void` | Set your reaction; `null` removes it |
130
+ | `markRead(msg)` | `void` | Mark the conversation as read up to `msg` |
131
+
132
+ `conv` is any object with `conversationId` and `conversationShortId` — every message satisfies it, so you can reply
133
+ with `sendMessage(msg, …)`.
134
+
135
+ ### Users & media
136
+
137
+ | Method | Returns | Description |
138
+ | ----------------------------------------- | ------------------------ | ------------------------------------ |
139
+ | `getUsers(uids)` | `TikTokUser[]` | Nickname, `@username`, avatar |
140
+ | `downloadImage(msg, 'origin' \| 'thumb')` | `Buffer` | Download and decrypt a private image |
141
+ | `downloadSticker(sticker)` | `{ data, type }` | Download a sticker image |
142
+ | `downloadAvatar(user)` | `{ data, type } \| null` | Download a user's avatar |
143
+
144
+ ### Session
145
+
146
+ | Method / property | Description |
147
+ | ----------------- | ------------------------------------------------------------ |
148
+ | `account` | Logged-in account: `uid`, `username`, `screenName`, `avatar` |
149
+ | `getOwnId()` | Own user id |
150
+ | `getCookies()` | Current cookies — persist them to resume the session |
151
+ | `isAlive()` | `true` while the session is still valid |
152
+
153
+ ### Message object
154
+
155
+ | Field | Type | Description |
156
+ | --------------------- | ------------------------------------------------------------------ | ------------------------ |
157
+ | `kind` | `'text' \| 'image' \| 'sticker' \| 'video' \| 'system' \| 'other'` | Message kind |
158
+ | `text` | `string` | Text content |
159
+ | `image` / `sticker` | `object \| null` | Attachment details |
160
+ | `reactions` | `{ emoji, userIds }[]` | Reactions on the message |
161
+ | `senderId` / `isSelf` | `string` / `boolean` | Sender |
162
+ | `createdAt` | `number` | Epoch milliseconds |
163
+
164
+ Full typings are shipped in `dist/index.d.ts`.
165
+
166
+ ## Listener
167
+
168
+ ```ts
169
+ api.listener.on('connected', () => console.log('connected'));
170
+ api.listener.on('message', (msg) => {
171
+ /* new message */
172
+ });
173
+ api.listener.on('command', (msg) => {
174
+ /* read receipts and other hidden commands */
175
+ });
176
+ api.listener.on('closed', (code, reason) => {});
177
+ api.listener.on('error', (err) => {});
178
+
179
+ api.listener.start(); // reconnects automatically; start({ retryOnClose: false }) to disable
180
+ api.listener.stop();
181
+ ```
182
+
183
+ ## Error Handling
184
+
185
+ All library errors are instances of `TikTokError` with a `code`:
186
+
187
+ | Code | Meaning |
188
+ | ----------------------------------- | ------------------------------------------ |
189
+ | `SESSION_EXPIRED` | Cookies are no longer valid — log in again |
190
+ | `IM_ERROR` | TikTok rejected the request |
191
+ | `HTTP_ERROR` · `BAD_RESPONSE` | Network or unexpected response |
192
+ | `UPLOAD_FAILED` · `DOWNLOAD_FAILED` | Media transfer failed |
193
+ | `LOGIN_FAILED` · `LOGIN_ABORTED` | QR login failed or was cancelled |
194
+
195
+ ```ts
196
+ import { TikTokError } from 'tca-js';
197
+
198
+ try {
199
+ await api.sendMessage(conv, 'hi');
200
+ } catch (err) {
201
+ if (err instanceof TikTokError && err.code === 'SESSION_EXPIRED') {
202
+ // refresh credentials
203
+ }
204
+ }
205
+ ```
206
+
207
+ ## Examples
208
+
209
+ - [`examples/qr-login.ts`](examples/qr-login.ts) — log in with a QR code and save the credentials to a file
210
+ - [`examples/echo-bot.ts`](examples/echo-bot.ts) — reply to every incoming text message
211
+
212
+ ## Contributing
213
+
214
+ Issues and pull requests are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). Protocol notes for maintainers are in [docs/PROTOCOL.md](docs/PROTOCOL.md).
120
215
 
121
216
  ```bash
122
- npm install && npm run build
123
- node scripts/qr-login.mjs <chromePath> <output dir> # try QR login (writes qr.png)
124
- node scripts/smoke.mjs --cookies <cookies.json> [--peer <uid>] [--text "…"] [--image <file>] [--listen 10]
217
+ npm install
218
+ npm test # unit tests (no network)
219
+ npm run lint && npm run typecheck
220
+ npm run build
221
+ node scripts/smoke.mjs --cookies cookies.json # read-only smoke test against a real account
125
222
  ```
126
223
 
127
- `cookies.json` holds a real session — keep it out of the repo (already in `.gitignore`).
224
+ ## Support Us
225
+
226
+ - ⭐ **Star the repository** if you find it useful
227
+ - 🔄 **Share** it with others
228
+ - 💡 **Contribute** ideas and code
229
+ - ☕ **Buy me a coffee** — VietQR (MB Bank · `0776750418`)
128
230
 
129
- Source comments are in Vietnamese. Issues and PRs welcome in English or Vietnamese.
231
+ <img src=".github/assets/vietqr-mb.png" alt="VietQR MB Bank 0776750418" width="220">
130
232
 
131
233
  ## License
132
234
 
133
- [MIT](LICENSE)
235
+ [MIT](LICENSE) © rice2411