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/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 (
|
|
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
|
-
*
|
|
17
|
-
* (API
|
|
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
|
-
/**
|
|
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
|
|
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', '
|
|
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', '
|
|
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
|
}
|
package/dist/listener.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
8
|
+
/** A real message in a conversation (including our own — see `isSelf`). */
|
|
9
9
|
message: [TikTokMessage];
|
|
10
|
-
/**
|
|
10
|
+
/** Hidden command: mark as read… (`content.command_type`). */
|
|
11
11
|
command: [TikTokMessage];
|
|
12
|
-
/**
|
|
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
|
-
*
|
|
18
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
14
|
+
/** The browser sends "hi" every 15–30 seconds; use the lower bound. */
|
|
15
15
|
const HEARTBEAT_MS = 15_000;
|
|
16
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
22
|
+
/** Close code used when we call `stop()` ourselves. */
|
|
23
23
|
exports.MANUAL_CLOSE = 4000;
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
-
//
|
|
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
|
-
/**
|
|
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
|
|
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"
|
|
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; //
|
|
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
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
3
|
-
* `payload_encoding = "__lz4"`
|
|
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
|
-
*
|
|
6
|
-
* `payload_encoding = "__lz4"`
|
|
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
|
|
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
|
-
//
|
|
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
|
-
/**
|
|
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`
|
|
16
|
+
/** `UploadConfig` returns two credential sets: type 1 = ImageX (images), type 2 = VOD (video). Use type 1. */
|
|
17
17
|
private credential;
|
|
18
|
-
/**
|
|
18
|
+
/** Call ImageX signed with AWS SigV4 (region cn-north-1, service imagex). */
|
|
19
19
|
private imagex;
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
-
/**
|
|
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`
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
82
|
-
*
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
/**
|
|
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', '
|
|
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', `
|
|
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', `
|
|
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` (
|
|
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
|
-
/**
|
|
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 (
|
|
32
|
-
*
|
|
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 (
|
|
51
|
-
* `PropertyItem {1 uid, 3
|
|
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` (
|
|
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
|
-
*
|
|
2
|
+
* Minimal protobuf, no `.proto` file needed.
|
|
3
3
|
*
|
|
4
|
-
* TikTok
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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>` —
|
|
31
|
+
/** `map<string,string>` — each entry is a message `{1: key, 2: value}`. */
|
|
32
32
|
map(n: number): Record<string, string>;
|
|
33
33
|
}
|
|
34
|
-
/**
|
|
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>`
|
|
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
|
-
*
|
|
3
|
+
* Minimal protobuf, no `.proto` file needed.
|
|
4
4
|
*
|
|
5
|
-
* TikTok
|
|
6
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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('
|
|
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
|
-
/**
|
|
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>` —
|
|
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
|
-
/**
|
|
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>`
|
|
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));
|
package/dist/qr-login.d.ts
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
1
|
import type { Credentials } from './types';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* QR login via headless Chrome.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* (X-Gnarly / msToken) —
|
|
7
|
-
* `tiktok.com/login/qrcode`
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
18
|
+
/** Scanned, waiting for confirmation on the phone. */
|
|
19
19
|
| {
|
|
20
20
|
type: 'scanned';
|
|
21
21
|
}
|
|
22
|
-
/**
|
|
22
|
+
/** Confirmed — fetching cookies. */
|
|
23
23
|
| {
|
|
24
24
|
type: 'confirmed';
|
|
25
25
|
};
|
|
26
26
|
export interface QrLoginOptions {
|
|
27
|
-
/**
|
|
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
|
-
/**
|
|
31
|
+
/** Total time to wait for a scan (ms) — default 5 minutes. */
|
|
32
32
|
timeoutMs?: number;
|
|
33
|
-
/** QR
|
|
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
|
-
*
|
|
39
|
-
* `new TikTok().login(creds)`
|
|
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>;
|