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/api.js
CHANGED
|
@@ -10,11 +10,11 @@ const listener_1 = require("./listener");
|
|
|
10
10
|
const media_1 = require("./media");
|
|
11
11
|
const message_1 = require("./message");
|
|
12
12
|
const proto_1 = require("./proto");
|
|
13
|
-
/**
|
|
13
|
+
/** "Latest" cursor for reading history — greater than any `index` (µs). */
|
|
14
14
|
const NEWEST = 9999999999999999n;
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* A logged-in TikTok session — create with `new TikTok().login(credentials)`.
|
|
17
|
+
* All calls go directly over HTTP/WS to TikTok using cookies, no browser needed.
|
|
18
18
|
*/
|
|
19
19
|
class API {
|
|
20
20
|
ctx;
|
|
@@ -30,44 +30,44 @@ class API {
|
|
|
30
30
|
getOwnId() {
|
|
31
31
|
return this.ctx.ownId;
|
|
32
32
|
}
|
|
33
|
-
/**
|
|
33
|
+
/** Current cookies — save them so the next `login` needs no QR scan. */
|
|
34
34
|
getCookies() {
|
|
35
35
|
return [...this.ctx.cookies].map(([name, value]) => ({ name, value }));
|
|
36
36
|
}
|
|
37
37
|
getContext() {
|
|
38
38
|
return { deviceId: this.ctx.deviceId, userAgent: this.ctx.userAgent };
|
|
39
39
|
}
|
|
40
|
-
/**
|
|
40
|
+
/** Whether the session is still alive — call periodically to detect expiry early. */
|
|
41
41
|
async isAlive() {
|
|
42
42
|
return (await fetchAccount(this.ctx).catch(() => null)) !== null;
|
|
43
43
|
}
|
|
44
|
-
// ───
|
|
45
|
-
/**
|
|
44
|
+
// ─── Inbox ─────────────────────────────────────────────────────────────────
|
|
45
|
+
/** Recent conversations + their latest messages, paginated with `cursor`. */
|
|
46
46
|
async getConversations(cursor = '0') {
|
|
47
47
|
const body = await this.ctx.im('/v2/message/get_by_user_init', constants_1.Cmd.GetMessagesByUserInit, new proto_1.Writer().int(1, BigInt(cursor)).finish());
|
|
48
48
|
return {
|
|
49
49
|
messages: body.msgs(1).map((m) => (0, message_1.parseMessage)(m, this.ctx.ownId)),
|
|
50
50
|
conversations: body.msgs(2).map((m) => (0, message_1.parseConversation)(m, this.ctx.ownId)),
|
|
51
|
-
//
|
|
51
|
+
// Cursor = number of conversations fetched so far (0, 10, 20…) — field 4; field 3 is a timestamp, not used for paging.
|
|
52
52
|
cursor: body.id(4),
|
|
53
53
|
hasMore: body.num(5) === 1,
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
|
-
/**
|
|
56
|
+
/** Messages of a conversation, newest first. For older messages: `before = <index of oldest message>`. */
|
|
57
57
|
async getMessages(conv, opts = {}) {
|
|
58
58
|
const limit = Math.min(Math.max(opts.limit ?? 20, 1), 50);
|
|
59
59
|
const body = await this.ctx.im('/v1/message/get_by_conversation', constants_1.Cmd.GetMessagesByConversation, new proto_1.Writer()
|
|
60
60
|
.bytes(1, conv.conversationId)
|
|
61
61
|
.int(2, constants_1.ConversationType.OneToOne)
|
|
62
62
|
.int(3, BigInt(conv.conversationShortId))
|
|
63
|
-
.int(4, 1) //
|
|
63
|
+
.int(4, 1) // direction: older than cursor
|
|
64
64
|
.int(5, opts.before ? BigInt(opts.before) : NEWEST)
|
|
65
65
|
.int(6, limit)
|
|
66
66
|
.finish());
|
|
67
67
|
const messages = body.msgs(1).map((m) => (0, message_1.parseMessage)(m, this.ctx.ownId));
|
|
68
68
|
return { messages, hasMore: messages.length >= limit };
|
|
69
69
|
}
|
|
70
|
-
/**
|
|
70
|
+
/** User profiles by uid (display name, @username, avatar). */
|
|
71
71
|
async getUsers(uids) {
|
|
72
72
|
if (uids.length === 0)
|
|
73
73
|
return [];
|
|
@@ -90,8 +90,8 @@ class API {
|
|
|
90
90
|
}));
|
|
91
91
|
}
|
|
92
92
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
93
|
+
* Download an avatar (tries each URL in turn, skips heic). No cookie / Referer sent: the image CDN is cross-origin and
|
|
94
|
+
* URLs are already signed. `null` if no URL could be downloaded.
|
|
95
95
|
*/
|
|
96
96
|
async downloadAvatar(user) {
|
|
97
97
|
for (const url of user.avatars) {
|
|
@@ -107,7 +107,7 @@ class API {
|
|
|
107
107
|
}
|
|
108
108
|
return null;
|
|
109
109
|
}
|
|
110
|
-
/**
|
|
110
|
+
/** Mark as read up to message `msg`. */
|
|
111
111
|
async markRead(msg) {
|
|
112
112
|
await this.ctx.im('/v3/conversation/mark_read', constants_1.Cmd.MarkRead, new proto_1.Writer()
|
|
113
113
|
.bytes(1, msg.conversationId)
|
|
@@ -117,14 +117,14 @@ class API {
|
|
|
117
117
|
.int(11, 4)
|
|
118
118
|
.finish(), constants_1.MARK_READ_BODY_FIELD);
|
|
119
119
|
}
|
|
120
|
-
// ───
|
|
121
|
-
/**
|
|
120
|
+
// ─── Send ─────────────────────────────────────────────────────────────────
|
|
121
|
+
/** Send a text message to a conversation (usually the just-received `msg` — it carries the conversation ids). */
|
|
122
122
|
async sendMessage(conv, text) {
|
|
123
123
|
const cid = (0, node_crypto_1.randomUUID)();
|
|
124
124
|
await this.send(conv, cid, constants_1.MessageType.Text, JSON.stringify({ aweType: 0, text }), null);
|
|
125
125
|
return { clientMessageId: cid };
|
|
126
126
|
}
|
|
127
|
-
/**
|
|
127
|
+
/** Send an image: upload to the private (encrypted) store, then send an image message. */
|
|
128
128
|
async sendImage(conv, image) {
|
|
129
129
|
const up = await this.media.upload(image);
|
|
130
130
|
const cid = (0, node_crypto_1.randomUUID)();
|
|
@@ -159,9 +159,9 @@ class API {
|
|
|
159
159
|
return { clientMessageId: cid };
|
|
160
160
|
}
|
|
161
161
|
/**
|
|
162
|
-
*
|
|
163
|
-
* `null` =
|
|
164
|
-
*
|
|
162
|
+
* Set YOUR reaction on a message — like TikTok Web, one reaction per user: a new `emoji` replaces the old one,
|
|
163
|
+
* `null` = remove. Pass the whole `msg` just read (current `reactions` are needed to know which key to remove).
|
|
164
|
+
* Sends `ModifyMessagePropertyRequestBody` (command 705) — see README.
|
|
165
165
|
*/
|
|
166
166
|
async setReaction(msg, emoji) {
|
|
167
167
|
const own = this.ctx.ownId;
|
|
@@ -173,7 +173,7 @@ class API {
|
|
|
173
173
|
if (e === emoji)
|
|
174
174
|
continue;
|
|
175
175
|
ops.push({ op: constants_1.PropertyOp.Remove, key: constants_1.REACTION_PREFIX + e });
|
|
176
|
-
// ❤️
|
|
176
|
+
// ❤️ sent from the mobile app still uses the legacy key `e:love` — remove it too or the app keeps showing the heart.
|
|
177
177
|
if (e === constants_1.REACTION_HEART)
|
|
178
178
|
ops.push({ op: constants_1.PropertyOp.Remove, key: constants_1.REACTION_LOVE_KEY });
|
|
179
179
|
}
|
|
@@ -188,12 +188,12 @@ class API {
|
|
|
188
188
|
for (const o of ops)
|
|
189
189
|
target.msg(6, new proto_1.Writer().int(1, o.op).bytes(2, o.key).bytes(4, own));
|
|
190
190
|
const body = await this.ctx.im('/v1/message/set_property', constants_1.Cmd.SetMessageProperty, new proto_1.Writer().msg(1, target).bytes(2, 'deprecated').finish());
|
|
191
|
-
// status: 0
|
|
191
|
+
// status: 0 success, 1 duplicate (also done); anything else means TikTok rejected it.
|
|
192
192
|
const status = body.num(1);
|
|
193
193
|
if (status > 1)
|
|
194
194
|
throw new errors_1.TikTokError('IM_ERROR', `set_property: status ${status}`, status);
|
|
195
195
|
}
|
|
196
|
-
/**
|
|
196
|
+
/** Download a sticker image (public CDN, unencrypted, no cookie needed). */
|
|
197
197
|
async downloadSticker(sticker) {
|
|
198
198
|
const res = await fetch(sticker.url, {
|
|
199
199
|
headers: { 'User-Agent': this.ctx.userAgent },
|
|
@@ -201,25 +201,25 @@ class API {
|
|
|
201
201
|
signal: AbortSignal.timeout(this.ctx.options.timeoutMs ?? 20_000),
|
|
202
202
|
}).catch(() => null);
|
|
203
203
|
if (!res?.ok)
|
|
204
|
-
throw new errors_1.TikTokError('DOWNLOAD_FAILED', `
|
|
204
|
+
throw new errors_1.TikTokError('DOWNLOAD_FAILED', `Sticker download failed: ${res?.status ?? 'network'}`);
|
|
205
205
|
return { data: Buffer.from(await res.arrayBuffer()), type: res.headers.get('content-type') ?? 'image/webp' };
|
|
206
206
|
}
|
|
207
|
-
/**
|
|
207
|
+
/** Download + decrypt a message's image (pass the whole `msg` — listener messages lack the URL, this looks it up). */
|
|
208
208
|
async downloadImage(source, which = 'origin') {
|
|
209
209
|
let image = 'serverMessageId' in source ? source.image : source;
|
|
210
|
-
//
|
|
210
|
+
// Websocket-pushed messages only have uri + key, no download URL → look up history by message id.
|
|
211
211
|
if ('serverMessageId' in source && image && !image.url) {
|
|
212
212
|
const { messages } = await this.getMessages(source, { limit: 5, before: (BigInt(source.index) + 1n).toString() });
|
|
213
213
|
image = messages.find((m) => m.serverMessageId === source.serverMessageId)?.image ?? image;
|
|
214
214
|
}
|
|
215
215
|
if (!image)
|
|
216
|
-
throw new errors_1.TikTokError('DOWNLOAD_FAILED', '
|
|
216
|
+
throw new errors_1.TikTokError('DOWNLOAD_FAILED', 'Message has no image');
|
|
217
217
|
return this.media.download(image, which);
|
|
218
218
|
}
|
|
219
|
-
/** `SendMessageRequestBody`: {1 convId, 2
|
|
219
|
+
/** `SendMessageRequestBody`: {1 convId, 2 conversation type, 3 shortId, 4 content, 5 ext, 6 message type, 7 ticket, 8 client id, …}. */
|
|
220
220
|
async send(conv, cid, type, content, extra) {
|
|
221
221
|
if (!conv.conversationId || !conv.conversationShortId) {
|
|
222
|
-
throw new errors_1.TikTokError('IM_ERROR', '
|
|
222
|
+
throw new errors_1.TikTokError('IM_ERROR', 'Missing conversationId / conversationShortId');
|
|
223
223
|
}
|
|
224
224
|
const w = new proto_1.Writer()
|
|
225
225
|
.bytes(1, conv.conversationId)
|
|
@@ -235,7 +235,7 @@ class API {
|
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
exports.API = API;
|
|
238
|
-
/**
|
|
238
|
+
/** Account for the cookie — `null` if the cookie is no longer logged in. */
|
|
239
239
|
async function fetchAccount(ctx) {
|
|
240
240
|
const res = await ctx.getJson(`${constants_1.WEB}/passport/web/account/info/?aid=${constants_1.AID_FRONTIER}`);
|
|
241
241
|
if (res.message !== 'success' || !res.data?.user_id_str)
|
|
@@ -248,8 +248,8 @@ async function fetchAccount(ctx) {
|
|
|
248
248
|
};
|
|
249
249
|
}
|
|
250
250
|
/**
|
|
251
|
-
*
|
|
252
|
-
* TikTok
|
|
251
|
+
* Avatar URLs from the IM profile `avatars` — collects every `https://…` string (thumb → medium → larger, whatever
|
|
252
|
+
* TikTok returns), then puts browser-readable ones (jpeg/webp/png) first and heic last.
|
|
253
253
|
*/
|
|
254
254
|
function avatarLinks(avatars) {
|
|
255
255
|
const urls = [];
|
|
@@ -268,7 +268,7 @@ function avatarLinks(avatars) {
|
|
|
268
268
|
const ranked = [...urls.filter((u) => !heic(u)), ...urls.filter(heic)];
|
|
269
269
|
return { avatar: ranked[0] ?? '', avatars: ranked };
|
|
270
270
|
}
|
|
271
|
-
/** `wid` —
|
|
271
|
+
/** `wid` — device id TikTok issues per `ttwid` cookie; IM + websocket use it as `device_id`. */
|
|
272
272
|
async function fetchWebId(ctx) {
|
|
273
273
|
const res = await ctx.fetch(`${constants_1.WEB}/api/v1/web-cookie-privacy/config?locale=vi-VN&appId=1988&theme=default&tea=1`);
|
|
274
274
|
if (!res.ok)
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* (09/11/2026). TikTok
|
|
2
|
+
* TikTok Web IM protocol constants — reconstructed from `tiktok.com/business-suite/messages` traffic
|
|
3
|
+
* (09/11/2026). If TikTok changes things on their side, fix them here; how they were found is in the README.
|
|
4
4
|
*/
|
|
5
5
|
export declare const IM_API = "https://im-api-sg.tiktok.com";
|
|
6
6
|
export declare const IM_WS = "wss://im-ws-sg.tiktok.com/ws/v2";
|
|
7
7
|
export declare const WEB = "https://www.tiktok.com";
|
|
8
8
|
export declare const DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36";
|
|
9
|
-
/**
|
|
9
|
+
/** TikTok Web app id (HTTP IM). */
|
|
10
10
|
export declare const AID_WEB = "1988";
|
|
11
|
-
/**
|
|
11
|
+
/** "frontier" channel app id — websocket pushing new messages. */
|
|
12
12
|
export declare const AID_FRONTIER = "1459";
|
|
13
13
|
export declare const SDK_VERSION = "1.8.5";
|
|
14
14
|
export declare const BUILD_NUMBER = "feffaa1:master";
|
|
15
|
-
/** `access_key`
|
|
15
|
+
/** Websocket `access_key` = md5(FPID + APP_KEY + device_id + SALT). */
|
|
16
16
|
export declare const FRONTIER_FPID = "9";
|
|
17
17
|
export declare const FRONTIER_APP_KEY = "e1bd35ec9db7b8d846de66ed140b1ad9";
|
|
18
18
|
export declare const FRONTIER_SALT = "f8a69f1719916z";
|
|
19
|
-
/**
|
|
19
|
+
/** IM command (`Request.cmd`) — also the body's field number in `RequestBody`. */
|
|
20
20
|
export declare const Cmd: {
|
|
21
21
|
readonly SendMessage: 100;
|
|
22
22
|
readonly GetMessagesByUserInit: 203;
|
|
23
23
|
readonly GetMessagesByConversation: 301;
|
|
24
|
-
/** Websocket
|
|
24
|
+
/** Websocket push of new messages. */
|
|
25
25
|
readonly NewMessage: 500;
|
|
26
|
-
/**
|
|
26
|
+
/** Add / remove a reaction (message `property`) — `/v1/message/set_property`. */
|
|
27
27
|
readonly SetMessageProperty: 705;
|
|
28
28
|
readonly GetConversationInfo: 610;
|
|
29
29
|
readonly MarkRead: 2002;
|
|
30
30
|
readonly UploadConfig: 2059;
|
|
31
31
|
};
|
|
32
|
-
/**
|
|
32
|
+
/** MarkRead's body lives in field 604, not the command number. */
|
|
33
33
|
export declare const MARK_READ_BODY_FIELD = 604;
|
|
34
34
|
/** `MessageBody.message_type`. */
|
|
35
35
|
export declare const MessageType: {
|
|
@@ -37,15 +37,15 @@ export declare const MessageType: {
|
|
|
37
37
|
readonly Text: 7;
|
|
38
38
|
readonly ShareVideo: 8;
|
|
39
39
|
readonly PrivateImage: 1802;
|
|
40
|
-
/** Sticker
|
|
40
|
+
/** Sticker — public image on the sticker CDN, not encrypted. */
|
|
41
41
|
readonly Sticker: 1805;
|
|
42
|
-
/**
|
|
42
|
+
/** Hidden command (read receipt…) — not shown in the conversation, but still pushed over websocket. */
|
|
43
43
|
readonly Command: 50001;
|
|
44
44
|
};
|
|
45
45
|
export declare const ConversationType: {
|
|
46
46
|
readonly OneToOne: 1;
|
|
47
47
|
};
|
|
48
|
-
/**
|
|
48
|
+
/** `Response` fields (both HTTP and websocket payload). */
|
|
49
49
|
export declare const Resp: {
|
|
50
50
|
readonly cmd: 1;
|
|
51
51
|
readonly seq: 2;
|
|
@@ -54,7 +54,7 @@ export declare const Resp: {
|
|
|
54
54
|
readonly body: 6;
|
|
55
55
|
readonly logId: 7;
|
|
56
56
|
};
|
|
57
|
-
/**
|
|
57
|
+
/** `MessageBody` fields. */
|
|
58
58
|
export declare const MsgF: {
|
|
59
59
|
readonly conversationId: 1;
|
|
60
60
|
readonly conversationType: 2;
|
|
@@ -66,21 +66,21 @@ export declare const MsgF: {
|
|
|
66
66
|
readonly content: 8;
|
|
67
67
|
readonly ext: 9;
|
|
68
68
|
readonly createTime: 10;
|
|
69
|
-
/** `property_list` — reaction: `{1
|
|
69
|
+
/** `property_list` — reaction: `{1 key "e:<emoji>", 2 {1 PropertyItem[]}}`. */
|
|
70
70
|
readonly propertyList: 15;
|
|
71
71
|
readonly attachments: 20;
|
|
72
72
|
readonly tag: 21;
|
|
73
73
|
};
|
|
74
|
-
/**
|
|
74
|
+
/** Operation on a message `property` (`ModifyPropertyContent.operation`). */
|
|
75
75
|
export declare const PropertyOp: {
|
|
76
76
|
readonly Add: 0;
|
|
77
77
|
readonly Remove: 1;
|
|
78
78
|
};
|
|
79
|
-
/**
|
|
79
|
+
/** Reaction key = `e:<emoji>`. The mobile app reacting ❤️ also writes the legacy key `e:love` — treated as ❤️. */
|
|
80
80
|
export declare const REACTION_PREFIX = "e:";
|
|
81
81
|
export declare const REACTION_LOVE_KEY = "e:love";
|
|
82
82
|
export declare const REACTION_HEART = "\u2764\uFE0F";
|
|
83
|
-
/** ImageX —
|
|
83
|
+
/** ImageX — image upload endpoint (AWS SigV4-style signing). */
|
|
84
84
|
export declare const IMAGEX_REGION = "cn-north-1";
|
|
85
85
|
export declare const IMAGEX_SERVICE = "imagex";
|
|
86
86
|
export declare const IMAGEX_VERSION = "2018-08-01";
|
package/dist/constants.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
* (09/11/2026). TikTok
|
|
3
|
+
* TikTok Web IM protocol constants — reconstructed from `tiktok.com/business-suite/messages` traffic
|
|
4
|
+
* (09/11/2026). If TikTok changes things on their side, fix them here; how they were found is in the README.
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.IMAGEX_VERSION = exports.IMAGEX_SERVICE = exports.IMAGEX_REGION = exports.REACTION_HEART = exports.REACTION_LOVE_KEY = exports.REACTION_PREFIX = exports.PropertyOp = exports.MsgF = exports.Resp = exports.ConversationType = exports.MessageType = exports.MARK_READ_BODY_FIELD = exports.Cmd = exports.FRONTIER_SALT = exports.FRONTIER_APP_KEY = exports.FRONTIER_FPID = exports.BUILD_NUMBER = exports.SDK_VERSION = exports.AID_FRONTIER = exports.AID_WEB = exports.DEFAULT_USER_AGENT = exports.WEB = exports.IM_WS = exports.IM_API = void 0;
|
|
@@ -9,30 +9,30 @@ exports.IM_API = 'https://im-api-sg.tiktok.com';
|
|
|
9
9
|
exports.IM_WS = 'wss://im-ws-sg.tiktok.com/ws/v2';
|
|
10
10
|
exports.WEB = 'https://www.tiktok.com';
|
|
11
11
|
exports.DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36';
|
|
12
|
-
/**
|
|
12
|
+
/** TikTok Web app id (HTTP IM). */
|
|
13
13
|
exports.AID_WEB = '1988';
|
|
14
|
-
/**
|
|
14
|
+
/** "frontier" channel app id — websocket pushing new messages. */
|
|
15
15
|
exports.AID_FRONTIER = '1459';
|
|
16
16
|
exports.SDK_VERSION = '1.8.5';
|
|
17
17
|
exports.BUILD_NUMBER = 'feffaa1:master';
|
|
18
|
-
/** `access_key`
|
|
18
|
+
/** Websocket `access_key` = md5(FPID + APP_KEY + device_id + SALT). */
|
|
19
19
|
exports.FRONTIER_FPID = '9';
|
|
20
20
|
exports.FRONTIER_APP_KEY = 'e1bd35ec9db7b8d846de66ed140b1ad9';
|
|
21
21
|
exports.FRONTIER_SALT = 'f8a69f1719916z';
|
|
22
|
-
/**
|
|
22
|
+
/** IM command (`Request.cmd`) — also the body's field number in `RequestBody`. */
|
|
23
23
|
exports.Cmd = {
|
|
24
24
|
SendMessage: 100,
|
|
25
25
|
GetMessagesByUserInit: 203,
|
|
26
26
|
GetMessagesByConversation: 301,
|
|
27
|
-
/** Websocket
|
|
27
|
+
/** Websocket push of new messages. */
|
|
28
28
|
NewMessage: 500,
|
|
29
|
-
/**
|
|
29
|
+
/** Add / remove a reaction (message `property`) — `/v1/message/set_property`. */
|
|
30
30
|
SetMessageProperty: 705,
|
|
31
31
|
GetConversationInfo: 610,
|
|
32
32
|
MarkRead: 2002,
|
|
33
33
|
UploadConfig: 2059,
|
|
34
34
|
};
|
|
35
|
-
/**
|
|
35
|
+
/** MarkRead's body lives in field 604, not the command number. */
|
|
36
36
|
exports.MARK_READ_BODY_FIELD = 604;
|
|
37
37
|
/** `MessageBody.message_type`. */
|
|
38
38
|
exports.MessageType = {
|
|
@@ -40,15 +40,15 @@ exports.MessageType = {
|
|
|
40
40
|
Text: 7,
|
|
41
41
|
ShareVideo: 8,
|
|
42
42
|
PrivateImage: 1802,
|
|
43
|
-
/** Sticker
|
|
43
|
+
/** Sticker — public image on the sticker CDN, not encrypted. */
|
|
44
44
|
Sticker: 1805,
|
|
45
|
-
/**
|
|
45
|
+
/** Hidden command (read receipt…) — not shown in the conversation, but still pushed over websocket. */
|
|
46
46
|
Command: 50001,
|
|
47
47
|
};
|
|
48
48
|
exports.ConversationType = { OneToOne: 1 };
|
|
49
|
-
/**
|
|
49
|
+
/** `Response` fields (both HTTP and websocket payload). */
|
|
50
50
|
exports.Resp = { cmd: 1, seq: 2, status: 3, error: 4, body: 6, logId: 7 };
|
|
51
|
-
/**
|
|
51
|
+
/** `MessageBody` fields. */
|
|
52
52
|
exports.MsgF = {
|
|
53
53
|
conversationId: 1,
|
|
54
54
|
conversationType: 2,
|
|
@@ -60,18 +60,18 @@ exports.MsgF = {
|
|
|
60
60
|
content: 8,
|
|
61
61
|
ext: 9,
|
|
62
62
|
createTime: 10,
|
|
63
|
-
/** `property_list` — reaction: `{1
|
|
63
|
+
/** `property_list` — reaction: `{1 key "e:<emoji>", 2 {1 PropertyItem[]}}`. */
|
|
64
64
|
propertyList: 15,
|
|
65
65
|
attachments: 20,
|
|
66
66
|
tag: 21,
|
|
67
67
|
};
|
|
68
|
-
/**
|
|
68
|
+
/** Operation on a message `property` (`ModifyPropertyContent.operation`). */
|
|
69
69
|
exports.PropertyOp = { Add: 0, Remove: 1 };
|
|
70
|
-
/**
|
|
70
|
+
/** Reaction key = `e:<emoji>`. The mobile app reacting ❤️ also writes the legacy key `e:love` — treated as ❤️. */
|
|
71
71
|
exports.REACTION_PREFIX = 'e:';
|
|
72
72
|
exports.REACTION_LOVE_KEY = 'e:love';
|
|
73
73
|
exports.REACTION_HEART = '\u2764\uFE0F';
|
|
74
|
-
/** ImageX —
|
|
74
|
+
/** ImageX — image upload endpoint (AWS SigV4-style signing). */
|
|
75
75
|
exports.IMAGEX_REGION = 'cn-north-1';
|
|
76
76
|
exports.IMAGEX_SERVICE = 'imagex';
|
|
77
77
|
exports.IMAGEX_VERSION = '2018-08-01';
|
package/dist/context.d.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { Msg } from './proto';
|
|
2
2
|
import type { Credentials, TikTokOptions } from './types';
|
|
3
|
-
/**
|
|
3
|
+
/** `name → value` cookies from any input shape of `Credentials.cookies`. */
|
|
4
4
|
export declare function normalizeCookies(input: Credentials['cookies']): Map<string, string>;
|
|
5
|
-
/**
|
|
5
|
+
/** Shared state of a session: cookies, device id, uid, HTTP calls. */
|
|
6
6
|
export declare class Context {
|
|
7
7
|
readonly options: TikTokOptions;
|
|
8
8
|
readonly cookies: Map<string, string>;
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* IM device id = the `wid` TikTok issues for the `ttwid` cookie (`login` fetches it). The websocket only
|
|
11
|
+
* accepts this id — a made-up id gets 400 (HTTP IM isn't picky).
|
|
12
12
|
*/
|
|
13
13
|
deviceId: string;
|
|
14
14
|
readonly userAgent: string;
|
|
@@ -17,16 +17,16 @@ export declare class Context {
|
|
|
17
17
|
constructor(creds: Credentials, options?: TikTokOptions);
|
|
18
18
|
cookieHeader(): string;
|
|
19
19
|
log(level: 'debug' | 'warn', msg: string): void;
|
|
20
|
-
/** `fetch`
|
|
20
|
+
/** `fetch` with cookies + UA + timeout. */
|
|
21
21
|
fetch(url: string, init?: RequestInit & {
|
|
22
22
|
headers?: Record<string, string>;
|
|
23
23
|
}): Promise<Response>;
|
|
24
24
|
getJson<T>(url: string): Promise<T>;
|
|
25
|
-
/**
|
|
25
|
+
/** IM `Request` envelope: command + body + common headers. */
|
|
26
26
|
buildRequest(cmd: number, bodyField: number, body: Buffer): Buffer;
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
28
|
+
* Call an IM command over HTTP. Returns the unwrapped body (`ResponseBody[cmd]`).
|
|
29
|
+
* NO X-Gnarly/X-Bogus/msToken signature needed — the IM server only checks cookies (tested 09/11/2026).
|
|
30
30
|
*/
|
|
31
31
|
im(path: string, cmd: number, body: Buffer, bodyField?: number): Promise<Msg>;
|
|
32
32
|
}
|
package/dist/context.js
CHANGED
|
@@ -5,7 +5,7 @@ exports.normalizeCookies = normalizeCookies;
|
|
|
5
5
|
const constants_1 = require("./constants");
|
|
6
6
|
const errors_1 = require("./errors");
|
|
7
7
|
const proto_1 = require("./proto");
|
|
8
|
-
/**
|
|
8
|
+
/** `name → value` cookies from any input shape of `Credentials.cookies`. */
|
|
9
9
|
function normalizeCookies(input) {
|
|
10
10
|
const jar = new Map();
|
|
11
11
|
if (typeof input === 'string') {
|
|
@@ -26,13 +26,13 @@ function normalizeCookies(input) {
|
|
|
26
26
|
}
|
|
27
27
|
return jar;
|
|
28
28
|
}
|
|
29
|
-
/**
|
|
29
|
+
/** Shared state of a session: cookies, device id, uid, HTTP calls. */
|
|
30
30
|
class Context {
|
|
31
31
|
options;
|
|
32
32
|
cookies;
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
34
|
+
* IM device id = the `wid` TikTok issues for the `ttwid` cookie (`login` fetches it). The websocket only
|
|
35
|
+
* accepts this id — a made-up id gets 400 (HTTP IM isn't picky).
|
|
36
36
|
*/
|
|
37
37
|
deviceId;
|
|
38
38
|
userAgent;
|
|
@@ -50,7 +50,7 @@ class Context {
|
|
|
50
50
|
log(level, msg) {
|
|
51
51
|
this.options.logger?.(level, msg);
|
|
52
52
|
}
|
|
53
|
-
/** `fetch`
|
|
53
|
+
/** `fetch` with cookies + UA + timeout. */
|
|
54
54
|
async fetch(url, init = {}) {
|
|
55
55
|
try {
|
|
56
56
|
return await fetch(url, {
|
|
@@ -75,7 +75,7 @@ class Context {
|
|
|
75
75
|
throw new errors_1.TikTokError('HTTP_ERROR', `${url.split('?')[0]}: HTTP ${res.status}`, res.status);
|
|
76
76
|
return (await res.json());
|
|
77
77
|
}
|
|
78
|
-
/**
|
|
78
|
+
/** IM `Request` envelope: command + body + common headers. */
|
|
79
79
|
buildRequest(cmd, bodyField, body) {
|
|
80
80
|
return new proto_1.Writer()
|
|
81
81
|
.int(1, cmd)
|
|
@@ -101,8 +101,8 @@ class Context {
|
|
|
101
101
|
.finish();
|
|
102
102
|
}
|
|
103
103
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
104
|
+
* Call an IM command over HTTP. Returns the unwrapped body (`ResponseBody[cmd]`).
|
|
105
|
+
* NO X-Gnarly/X-Bogus/msToken signature needed — the IM server only checks cookies (tested 09/11/2026).
|
|
106
106
|
*/
|
|
107
107
|
async im(path, cmd, body, bodyField = cmd) {
|
|
108
108
|
const url = `${constants_1.IM_API}${path}?aid=${constants_1.AID_WEB}&version_code=1.0.0&app_name=tiktok_web&device_platform=web_pc`;
|
|
@@ -115,11 +115,11 @@ class Context {
|
|
|
115
115
|
throw new errors_1.TikTokError('HTTP_ERROR', `${path}: HTTP ${res.status}`, res.status);
|
|
116
116
|
const resp = proto_1.Msg.tryParse(Buffer.from(await res.arrayBuffer()));
|
|
117
117
|
if (!resp)
|
|
118
|
-
throw new errors_1.TikTokError('BAD_RESPONSE', `${path}:
|
|
118
|
+
throw new errors_1.TikTokError('BAD_RESPONSE', `${path}: response is not protobuf`);
|
|
119
119
|
const status = resp.num(constants_1.Resp.status);
|
|
120
120
|
if (status !== 0) {
|
|
121
121
|
const desc = resp.str(constants_1.Resp.error);
|
|
122
|
-
//
|
|
122
|
+
// Not logged in: 200005 (tested with a cookieless request).
|
|
123
123
|
const code = status === 200005 ? 'SESSION_EXPIRED' : 'IM_ERROR';
|
|
124
124
|
throw new errors_1.TikTokError(code, `${path}: ${status} ${desc}`.trim(), status);
|
|
125
125
|
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Error from TikTok or from the library itself. `code` lets callers branch (e.g. session expired → rescan). */
|
|
2
2
|
export declare class TikTokError extends Error {
|
|
3
3
|
readonly code: TikTokErrorCode;
|
|
4
|
-
/**
|
|
4
|
+
/** Original TikTok code (status_code / error_code), if any. */
|
|
5
5
|
readonly upstream?: number | string | undefined;
|
|
6
6
|
constructor(code: TikTokErrorCode, message: string,
|
|
7
|
-
/**
|
|
7
|
+
/** Original TikTok code (status_code / error_code), if any. */
|
|
8
8
|
upstream?: number | string | undefined);
|
|
9
9
|
}
|
|
10
10
|
export type TikTokErrorCode =
|
|
11
|
-
/** Cookie
|
|
11
|
+
/** Cookie is no longer logged in — a new session is required. */
|
|
12
12
|
'SESSION_EXPIRED' | 'HTTP_ERROR'
|
|
13
|
-
/** IM
|
|
13
|
+
/** IM returned a non-zero `status_code`. */
|
|
14
14
|
| 'IM_ERROR' | 'UPLOAD_FAILED' | 'DOWNLOAD_FAILED' | 'BAD_RESPONSE'
|
|
15
|
-
/**
|
|
15
|
+
/** QR login failed (Chrome failed to launch, QR expired, no cookie found…). */
|
|
16
16
|
| 'LOGIN_FAILED'
|
|
17
|
-
/**
|
|
17
|
+
/** Caller aborted QR login (`signal`). */
|
|
18
18
|
| 'LOGIN_ABORTED';
|
package/dist/errors.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TikTokError = void 0;
|
|
4
|
-
/**
|
|
4
|
+
/** Error from TikTok or from the library itself. `code` lets callers branch (e.g. session expired → rescan). */
|
|
5
5
|
class TikTokError extends Error {
|
|
6
6
|
code;
|
|
7
7
|
upstream;
|
|
8
8
|
constructor(code, message,
|
|
9
|
-
/**
|
|
9
|
+
/** Original TikTok code (status_code / error_code), if any. */
|
|
10
10
|
upstream) {
|
|
11
11
|
super(message);
|
|
12
12
|
this.code = code;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* tca — TikTok Chat API (
|
|
2
|
+
* tca-js — unofficial TikTok Chat API (speaks the TikTok Web protocol).
|
|
3
3
|
*
|
|
4
4
|
* ```ts
|
|
5
5
|
* const api = await new TikTok().login({ cookies, deviceId });
|
|
@@ -10,15 +10,15 @@
|
|
|
10
10
|
* api.listener.start();
|
|
11
11
|
* ```
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
* (API
|
|
13
|
+
* Getting cookies the first time: `loginQR({ chromePath, onEvent })` — opens headless Chrome on TikTok's QR page
|
|
14
|
+
* (the login API requires a JS signature so it can't be done in pure Node), returns cookies after scanning. See README.
|
|
15
15
|
*/
|
|
16
16
|
import { API } from './api';
|
|
17
17
|
import type { Credentials, TikTokOptions } from './types';
|
|
18
18
|
export declare class TikTok {
|
|
19
19
|
private readonly options;
|
|
20
20
|
constructor(options?: TikTokOptions);
|
|
21
|
-
/**
|
|
21
|
+
/** Log in with existing cookies. Expired cookies → `TikTokError('SESSION_EXPIRED')`. */
|
|
22
22
|
login(credentials: Credentials): Promise<API>;
|
|
23
23
|
}
|
|
24
24
|
export { API } from './api';
|