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/README.md +185 -101
- package/dist/api.d.ts +21 -21
- package/dist/api.js +33 -33
- package/dist/constants.d.ts +17 -17
- package/dist/constants.js +17 -17
- package/dist/context.d.ts +8 -8
- package/dist/context.js +10 -10
- package/dist/errors.d.ts +7 -7
- package/dist/errors.js +2 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +7 -7
- package/dist/listener.d.ts +8 -8
- package/dist/listener.js +15 -15
- package/dist/lz4.d.ts +2 -2
- package/dist/lz4.js +4 -4
- package/dist/media.d.ts +6 -6
- package/dist/media.js +12 -12
- package/dist/message.d.ts +1 -1
- package/dist/message.js +6 -6
- package/dist/proto.d.ts +9 -9
- package/dist/proto.js +11 -11
- package/dist/qr-login.d.ts +14 -14
- package/dist/qr-login.js +16 -16
- package/dist/types.d.ts +26 -26
- package/package.json +24 -6
package/dist/qr-login.js
CHANGED
|
@@ -11,7 +11,7 @@ const node_path_1 = require("node:path");
|
|
|
11
11
|
const ws_1 = __importDefault(require("ws"));
|
|
12
12
|
const constants_1 = require("./constants");
|
|
13
13
|
const errors_1 = require("./errors");
|
|
14
|
-
/**
|
|
14
|
+
/** Attach CDP to a new tab (a flat session on the browser websocket). */
|
|
15
15
|
async function openTab(browserWs) {
|
|
16
16
|
const ws = new ws_1.default(browserWs, { perMessageDeflate: false });
|
|
17
17
|
await new Promise((resolve, reject) => {
|
|
@@ -50,7 +50,7 @@ async function openTab(browserWs) {
|
|
|
50
50
|
close: () => ws.close(),
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
|
-
/**
|
|
53
|
+
/** Launch headless Chrome and wait for the "DevTools listening on ws://…" line. */
|
|
54
54
|
async function launchChrome(chromePath, userAgent) {
|
|
55
55
|
const dir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'tca-chrome-'));
|
|
56
56
|
const proc = (0, node_child_process_1.spawn)(chromePath, [
|
|
@@ -61,17 +61,17 @@ async function launchChrome(chromePath, userAgent) {
|
|
|
61
61
|
'--no-default-browser-check',
|
|
62
62
|
'--disable-gpu',
|
|
63
63
|
'--disable-dev-shm-usage',
|
|
64
|
-
//
|
|
64
|
+
// Containers run as root — Chromium requires disabling the sandbox.
|
|
65
65
|
'--no-sandbox',
|
|
66
66
|
'--window-size=1280,900',
|
|
67
67
|
'--lang=vi-VN',
|
|
68
|
-
//
|
|
68
|
+
// The default headless UA contains "HeadlessChrome" — replace it with a normal UA.
|
|
69
69
|
`--user-agent=${userAgent}`,
|
|
70
70
|
'about:blank',
|
|
71
71
|
], { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
72
72
|
const ws = await new Promise((resolve, reject) => {
|
|
73
73
|
let buf = '';
|
|
74
|
-
const timer = setTimeout(() => reject(new Error('Chrome
|
|
74
|
+
const timer = setTimeout(() => reject(new Error('Chrome did not start within 20s')), 20_000);
|
|
75
75
|
proc.stderr.on('data', (d) => {
|
|
76
76
|
buf += d.toString();
|
|
77
77
|
const m = /DevTools listening on (ws:\/\/\S+)/.exec(buf);
|
|
@@ -86,18 +86,18 @@ async function launchChrome(chromePath, userAgent) {
|
|
|
86
86
|
});
|
|
87
87
|
proc.once('exit', (code) => {
|
|
88
88
|
clearTimeout(timer);
|
|
89
|
-
reject(new Error(`Chrome
|
|
89
|
+
reject(new Error(`Chrome exited (${code}) before it was ready`));
|
|
90
90
|
});
|
|
91
91
|
}).catch(async (err) => {
|
|
92
92
|
proc.kill('SIGKILL');
|
|
93
93
|
await (0, promises_1.rm)(dir, { recursive: true, force: true });
|
|
94
|
-
throw new errors_1.TikTokError('LOGIN_FAILED', `
|
|
94
|
+
throw new errors_1.TikTokError('LOGIN_FAILED', `Failed to launch Chrome: ${err instanceof Error ? err.message : String(err)}`);
|
|
95
95
|
});
|
|
96
96
|
return { proc, ws, dir };
|
|
97
97
|
}
|
|
98
98
|
/**
|
|
99
|
-
*
|
|
100
|
-
* `new TikTok().login(creds)`
|
|
99
|
+
* Log in to TikTok via QR. Returns `Credentials` (`.tiktok.com` cookies + UA) — pass them to
|
|
100
|
+
* `new TikTok().login(creds)` and SAVE them so you don't need to scan next time.
|
|
101
101
|
*/
|
|
102
102
|
async function loginQR(opts) {
|
|
103
103
|
const userAgent = opts.userAgent || constants_1.DEFAULT_USER_AGENT;
|
|
@@ -125,8 +125,8 @@ async function loginQR(opts) {
|
|
|
125
125
|
else
|
|
126
126
|
resolve(creds);
|
|
127
127
|
};
|
|
128
|
-
const timer = setTimeout(() => finish(new errors_1.TikTokError('LOGIN_FAILED', '
|
|
129
|
-
const onAbort = () => finish(new errors_1.TikTokError('LOGIN_ABORTED', '
|
|
128
|
+
const timer = setTimeout(() => finish(new errors_1.TikTokError('LOGIN_FAILED', 'Timed out waiting for QR scan')), opts.timeoutMs ?? 5 * 60_000);
|
|
129
|
+
const onAbort = () => finish(new errors_1.TikTokError('LOGIN_ABORTED', 'QR login aborted'));
|
|
130
130
|
opts.signal?.addEventListener('abort', onAbort);
|
|
131
131
|
const body = async (requestId) => {
|
|
132
132
|
const r = await tab.send('Network.getResponseBody', { requestId }).catch(() => null);
|
|
@@ -139,13 +139,13 @@ async function loginQR(opts) {
|
|
|
139
139
|
return null;
|
|
140
140
|
}
|
|
141
141
|
};
|
|
142
|
-
/**
|
|
142
|
+
/** After `confirmed`: the page redirects and sets session cookies — wait for `sessionid`. */
|
|
143
143
|
const collectCookies = async () => {
|
|
144
144
|
for (let i = 0; i < 30 && !done; i++) {
|
|
145
145
|
const { cookies } = await tab.send('Network.getAllCookies');
|
|
146
146
|
const mine = cookies.filter((c) => c.domain.endsWith('tiktok.com'));
|
|
147
147
|
if (mine.some((c) => c.name === 'sessionid' && c.value) && mine.some((c) => c.name === 'ttwid')) {
|
|
148
|
-
//
|
|
148
|
+
// Same name on `.tiktok.com` and `www.tiktok.com` → keep the `.tiktok.com` one.
|
|
149
149
|
const byName = new Map();
|
|
150
150
|
for (const c of mine.sort((a, b) => (a.domain === '.tiktok.com' ? 1 : 0) - (b.domain === '.tiktok.com' ? 1 : 0))) {
|
|
151
151
|
byName.set(c.name, { name: c.name, value: c.value });
|
|
@@ -154,7 +154,7 @@ async function loginQR(opts) {
|
|
|
154
154
|
}
|
|
155
155
|
await new Promise((r) => setTimeout(r, 1000));
|
|
156
156
|
}
|
|
157
|
-
finish(new errors_1.TikTokError('LOGIN_FAILED', '
|
|
157
|
+
finish(new errors_1.TikTokError('LOGIN_FAILED', 'Confirmed but no sessionid cookie found'));
|
|
158
158
|
};
|
|
159
159
|
tab.on((method, p) => {
|
|
160
160
|
if (done)
|
|
@@ -180,7 +180,7 @@ async function loginQR(opts) {
|
|
|
180
180
|
});
|
|
181
181
|
}
|
|
182
182
|
else if (url.includes('/passport/web/check_qrconnect/')) {
|
|
183
|
-
//
|
|
183
|
+
// The page polls 3 regions in parallel (sg/us/eu) — only the account's region changes status, the rest stay "new".
|
|
184
184
|
void body(p.requestId).then((j) => {
|
|
185
185
|
const status = String(j?.data?.status ?? '');
|
|
186
186
|
if (status === 'scanned' && !scanned) {
|
|
@@ -193,7 +193,7 @@ async function loginQR(opts) {
|
|
|
193
193
|
}
|
|
194
194
|
else if (status === 'expired' && !scanned) {
|
|
195
195
|
if (refreshes++ >= maxRefresh)
|
|
196
|
-
finish(new errors_1.TikTokError('LOGIN_FAILED', 'QR
|
|
196
|
+
finish(new errors_1.TikTokError('LOGIN_FAILED', 'QR expired without being scanned'));
|
|
197
197
|
else
|
|
198
198
|
void tab.send('Page.reload');
|
|
199
199
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,87 +1,87 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** A cookie — same shape as `chrome.cookies.getAll` / CDP `Storage.getCookies` returns (extra fields are fine). */
|
|
2
2
|
export interface CookieInput {
|
|
3
3
|
name: string;
|
|
4
4
|
value: string;
|
|
5
5
|
}
|
|
6
|
-
/**
|
|
6
|
+
/** What you need to log in again — save it after each login. */
|
|
7
7
|
export interface Credentials {
|
|
8
|
-
/**
|
|
8
|
+
/** `.tiktok.com` cookies: array of `{name, value}`, object `{name: value}`, or string `a=b; c=d`. */
|
|
9
9
|
cookies: CookieInput[] | Record<string, string> | string;
|
|
10
|
-
/**
|
|
10
|
+
/** IM device id (`wid`) — leave empty and `login` asks TikTok using `ttwid`. Only used when that fails. */
|
|
11
11
|
deviceId?: string;
|
|
12
12
|
userAgent?: string;
|
|
13
13
|
}
|
|
14
14
|
export interface TikTokOptions {
|
|
15
|
-
/**
|
|
15
|
+
/** Debug logging — silent by default. */
|
|
16
16
|
logger?: (level: 'debug' | 'warn', msg: string) => void;
|
|
17
|
-
/**
|
|
17
|
+
/** Timeout per HTTP request (ms). */
|
|
18
18
|
timeoutMs?: number;
|
|
19
19
|
}
|
|
20
|
-
/**
|
|
20
|
+
/** Enough to target a conversation: send, read, mark as read. Every `TikTokMessage` satisfies it. */
|
|
21
21
|
export interface ConversationRef {
|
|
22
22
|
conversationId: string;
|
|
23
23
|
conversationShortId: string;
|
|
24
24
|
}
|
|
25
25
|
export interface TikTokImage {
|
|
26
|
-
/**
|
|
26
|
+
/** Image id in TikTok storage (encrypted). */
|
|
27
27
|
uri: string;
|
|
28
|
-
/**
|
|
28
|
+
/** Original download link — needs cookies, content is AES-256-GCM. */
|
|
29
29
|
url: string;
|
|
30
30
|
thumbUrl: string;
|
|
31
31
|
width: number;
|
|
32
32
|
height: number;
|
|
33
|
-
/**
|
|
33
|
+
/** AES-256-GCM key (hex) to decrypt `url` — `api.downloadImage` handles this. */
|
|
34
34
|
decryptKey: string;
|
|
35
35
|
}
|
|
36
|
-
/** Sticker
|
|
36
|
+
/** Sticker — including user-made stickers (`stickerType` 15). Public CDN link, signed + expires in a few days. */
|
|
37
37
|
export interface TikTokSticker {
|
|
38
38
|
stickerId: string;
|
|
39
|
-
/** 14 =
|
|
39
|
+
/** 14 = built-in TikTok sticker, 15 = user-made sticker (memes, AI…). */
|
|
40
40
|
stickerType: number;
|
|
41
41
|
url: string;
|
|
42
42
|
width: number;
|
|
43
43
|
height: number;
|
|
44
44
|
}
|
|
45
|
-
/**
|
|
45
|
+
/** A reaction on a message: emoji + users who reacted (uid). The mobile app's `e:love` is merged into ❤️. */
|
|
46
46
|
export interface TikTokReaction {
|
|
47
47
|
emoji: string;
|
|
48
48
|
userIds: string[];
|
|
49
49
|
}
|
|
50
50
|
export interface TikTokMessage extends ConversationRef {
|
|
51
51
|
serverMessageId: string;
|
|
52
|
-
/**
|
|
52
|
+
/** Position in the conversation (µs) — cursor for paging older messages. */
|
|
53
53
|
index: string;
|
|
54
|
-
/** `MessageType` — 7
|
|
54
|
+
/** `MessageType` — 7 text, 1802 image, 1805 sticker, 8 video share, 1 system. */
|
|
55
55
|
type: number;
|
|
56
|
-
/** `command` =
|
|
56
|
+
/** `command` = hidden command (read receipt…) — listener emits it via the `command` event, not `message`. */
|
|
57
57
|
kind: 'text' | 'image' | 'sticker' | 'video' | 'system' | 'command' | 'other';
|
|
58
58
|
senderId: string;
|
|
59
59
|
isSelf: boolean;
|
|
60
|
-
/**
|
|
60
|
+
/** Text content (`kind = text`), empty for other kinds. */
|
|
61
61
|
text: string;
|
|
62
62
|
image: TikTokImage | null;
|
|
63
63
|
sticker: TikTokSticker | null;
|
|
64
64
|
reactions: TikTokReaction[];
|
|
65
|
-
/** JSON
|
|
65
|
+
/** Raw JSON content (`{"aweType":0,"text":"…"}`…). */
|
|
66
66
|
content: unknown;
|
|
67
|
-
/**
|
|
67
|
+
/** Send time, ms epoch. */
|
|
68
68
|
createdAt: number;
|
|
69
69
|
clientMessageId: string;
|
|
70
70
|
ext: Record<string, string>;
|
|
71
71
|
}
|
|
72
72
|
export interface TikTokConversation extends ConversationRef {
|
|
73
73
|
participants: string[];
|
|
74
|
-
/**
|
|
74
|
+
/** The other participant in a 1-1 conversation. */
|
|
75
75
|
peerId: string;
|
|
76
76
|
}
|
|
77
77
|
export interface TikTokUser {
|
|
78
78
|
uid: string;
|
|
79
|
-
/** `@username` (
|
|
79
|
+
/** `@username` (without @). */
|
|
80
80
|
uniqueId: string;
|
|
81
81
|
nickname: string;
|
|
82
|
-
/**
|
|
82
|
+
/** Best avatar (prefers jpeg/webp/png, avoids heic which browsers can't read). */
|
|
83
83
|
avatar: string;
|
|
84
|
-
/**
|
|
84
|
+
/** All avatar links TikTok returns, by priority — signed, expiring links; `downloadAvatar` tries them in order. */
|
|
85
85
|
avatars: string[];
|
|
86
86
|
}
|
|
87
87
|
export interface TikTokAccount {
|
|
@@ -90,10 +90,10 @@ export interface TikTokAccount {
|
|
|
90
90
|
screenName: string;
|
|
91
91
|
avatar: string;
|
|
92
92
|
}
|
|
93
|
-
/**
|
|
93
|
+
/** Image attachment. */
|
|
94
94
|
export interface ImageInput {
|
|
95
95
|
data: Buffer;
|
|
96
|
-
/**
|
|
96
|
+
/** Only the extension matters so TikTok knows the format: `photo.jpg`, `bill.png`. */
|
|
97
97
|
filename: string;
|
|
98
98
|
}
|
|
99
99
|
export interface MessagePage {
|
|
@@ -102,7 +102,7 @@ export interface MessagePage {
|
|
|
102
102
|
}
|
|
103
103
|
export interface ConversationPage {
|
|
104
104
|
conversations: TikTokConversation[];
|
|
105
|
-
/**
|
|
105
|
+
/** Latest messages included — handy for building an inbox list. */
|
|
106
106
|
messages: TikTokMessage[];
|
|
107
107
|
cursor: string;
|
|
108
108
|
hasMore: boolean;
|
package/package.json
CHANGED
|
@@ -1,17 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tca-js",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Unofficial TikTok Chat API for JavaScript — read/send TikTok direct messages (text, images, reactions) and listen over websocket using session cookies, no browser needed.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "rice2411",
|
|
7
|
-
"repository": {
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/rice2411/tca-js.git"
|
|
10
|
+
},
|
|
8
11
|
"homepage": "https://github.com/rice2411/tca-js#readme",
|
|
9
|
-
"bugs": {
|
|
10
|
-
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/rice2411/tca-js/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"tiktok",
|
|
17
|
+
"tiktok-api",
|
|
18
|
+
"tiktok-dm",
|
|
19
|
+
"tiktok-chat",
|
|
20
|
+
"direct-message",
|
|
21
|
+
"unofficial",
|
|
22
|
+
"chat",
|
|
23
|
+
"websocket"
|
|
24
|
+
],
|
|
11
25
|
"main": "./dist/index.js",
|
|
12
26
|
"types": "./dist/index.d.ts",
|
|
13
|
-
"files": [
|
|
14
|
-
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=22"
|
|
32
|
+
},
|
|
15
33
|
"scripts": {
|
|
16
34
|
"build": "rm -rf dist && tsc -p tsconfig.json",
|
|
17
35
|
"prepublishOnly": "npm run build",
|