tca-js 0.3.1 → 0.4.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 +72 -53
- package/dist/index.d.mts +374 -0
- package/dist/index.d.ts +366 -25
- package/dist/index.js +1319 -52
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1277 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +33 -5
- package/dist/api.d.ts +0 -75
- package/dist/api.js +0 -277
- package/dist/constants.d.ts +0 -86
- package/dist/constants.js +0 -77
- package/dist/context.d.ts +0 -32
- package/dist/context.js +0 -129
- package/dist/errors.d.ts +0 -18
- package/dist/errors.js +0 -17
- package/dist/listener.d.ts +0 -43
- package/dist/listener.js +0 -181
- package/dist/lz4.d.ts +0 -5
- package/dist/lz4.js +0 -57
- package/dist/media.d.ts +0 -27
- package/dist/media.js +0 -152
- package/dist/message.d.ts +0 -6
- package/dist/message.js +0 -113
- package/dist/proto.d.ts +0 -43
- package/dist/proto.js +0 -162
- package/dist/qr-login.d.ts +0 -41
- package/dist/qr-login.js +0 -212
- package/dist/types.d.ts +0 -109
- package/dist/types.js +0 -2
package/dist/api.js
DELETED
|
@@ -1,277 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.API = void 0;
|
|
4
|
-
exports.fetchAccount = fetchAccount;
|
|
5
|
-
exports.fetchWebId = fetchWebId;
|
|
6
|
-
const node_crypto_1 = require("node:crypto");
|
|
7
|
-
const constants_1 = require("./constants");
|
|
8
|
-
const errors_1 = require("./errors");
|
|
9
|
-
const listener_1 = require("./listener");
|
|
10
|
-
const media_1 = require("./media");
|
|
11
|
-
const message_1 = require("./message");
|
|
12
|
-
const proto_1 = require("./proto");
|
|
13
|
-
/** "Latest" cursor for reading history — greater than any `index` (µs). */
|
|
14
|
-
const NEWEST = 9999999999999999n;
|
|
15
|
-
/**
|
|
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
|
-
*/
|
|
19
|
-
class API {
|
|
20
|
-
ctx;
|
|
21
|
-
account;
|
|
22
|
-
listener;
|
|
23
|
-
media;
|
|
24
|
-
constructor(ctx, account) {
|
|
25
|
-
this.ctx = ctx;
|
|
26
|
-
this.account = account;
|
|
27
|
-
this.listener = new listener_1.Listener(ctx);
|
|
28
|
-
this.media = new media_1.MediaClient(ctx);
|
|
29
|
-
}
|
|
30
|
-
getOwnId() {
|
|
31
|
-
return this.ctx.ownId;
|
|
32
|
-
}
|
|
33
|
-
/** Current cookies — save them so the next `login` needs no QR scan. */
|
|
34
|
-
getCookies() {
|
|
35
|
-
return [...this.ctx.cookies].map(([name, value]) => ({ name, value }));
|
|
36
|
-
}
|
|
37
|
-
getContext() {
|
|
38
|
-
return { deviceId: this.ctx.deviceId, userAgent: this.ctx.userAgent };
|
|
39
|
-
}
|
|
40
|
-
/** Whether the session is still alive — call periodically to detect expiry early. */
|
|
41
|
-
async isAlive() {
|
|
42
|
-
return (await fetchAccount(this.ctx).catch(() => null)) !== null;
|
|
43
|
-
}
|
|
44
|
-
// ─── Inbox ─────────────────────────────────────────────────────────────────
|
|
45
|
-
/** Recent conversations + their latest messages, paginated with `cursor`. */
|
|
46
|
-
async getConversations(cursor = '0') {
|
|
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
|
-
return {
|
|
49
|
-
messages: body.msgs(1).map((m) => (0, message_1.parseMessage)(m, this.ctx.ownId)),
|
|
50
|
-
conversations: body.msgs(2).map((m) => (0, message_1.parseConversation)(m, this.ctx.ownId)),
|
|
51
|
-
// Cursor = number of conversations fetched so far (0, 10, 20…) — field 4; field 3 is a timestamp, not used for paging.
|
|
52
|
-
cursor: body.id(4),
|
|
53
|
-
hasMore: body.num(5) === 1,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
/** Messages of a conversation, newest first. For older messages: `before = <index of oldest message>`. */
|
|
57
|
-
async getMessages(conv, opts = {}) {
|
|
58
|
-
const limit = Math.min(Math.max(opts.limit ?? 20, 1), 50);
|
|
59
|
-
const body = await this.ctx.im('/v1/message/get_by_conversation', constants_1.Cmd.GetMessagesByConversation, new proto_1.Writer()
|
|
60
|
-
.bytes(1, conv.conversationId)
|
|
61
|
-
.int(2, constants_1.ConversationType.OneToOne)
|
|
62
|
-
.int(3, BigInt(conv.conversationShortId))
|
|
63
|
-
.int(4, 1) // direction: older than cursor
|
|
64
|
-
.int(5, opts.before ? BigInt(opts.before) : NEWEST)
|
|
65
|
-
.int(6, limit)
|
|
66
|
-
.finish());
|
|
67
|
-
const messages = body.msgs(1).map((m) => (0, message_1.parseMessage)(m, this.ctx.ownId));
|
|
68
|
-
return { messages, hasMore: messages.length >= limit };
|
|
69
|
-
}
|
|
70
|
-
/** User profiles by uid (display name, @username, avatar). */
|
|
71
|
-
async getUsers(uids) {
|
|
72
|
-
if (uids.length === 0)
|
|
73
|
-
return [];
|
|
74
|
-
const q = new URLSearchParams({
|
|
75
|
-
aid: '1988',
|
|
76
|
-
app_name: 'tiktok_web',
|
|
77
|
-
device_platform: 'web_pc',
|
|
78
|
-
source: 'im_chat',
|
|
79
|
-
from_page: 'message',
|
|
80
|
-
user_ids: JSON.stringify(uids),
|
|
81
|
-
});
|
|
82
|
-
const res = await this.ctx.getJson(`${constants_1.WEB}/tiktok/v1/im/user/profile/?${q}`);
|
|
83
|
-
if (res.status_code)
|
|
84
|
-
throw new errors_1.TikTokError('IM_ERROR', `user/profile: ${res.status_code}`, res.status_code);
|
|
85
|
-
return (res.users ?? []).map(({ im_user_profile: p = {} }) => ({
|
|
86
|
-
uid: p.user_id_str ?? '',
|
|
87
|
-
uniqueId: p.unique_id ?? '',
|
|
88
|
-
nickname: p.nick_name ?? '',
|
|
89
|
-
...avatarLinks(p.avatars),
|
|
90
|
-
}));
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
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
|
-
*/
|
|
96
|
-
async downloadAvatar(user) {
|
|
97
|
-
for (const url of user.avatars) {
|
|
98
|
-
const res = await fetch(url, {
|
|
99
|
-
headers: { 'User-Agent': this.ctx.userAgent },
|
|
100
|
-
referrerPolicy: 'no-referrer',
|
|
101
|
-
signal: AbortSignal.timeout(this.ctx.options.timeoutMs ?? 20_000),
|
|
102
|
-
}).catch(() => null);
|
|
103
|
-
const type = res?.headers.get('content-type') ?? '';
|
|
104
|
-
if (!res?.ok || !/^image\/(jpeg|png|webp|gif)/.test(type))
|
|
105
|
-
continue;
|
|
106
|
-
return { data: Buffer.from(await res.arrayBuffer()), type };
|
|
107
|
-
}
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
/** Mark as read up to message `msg`. */
|
|
111
|
-
async markRead(msg) {
|
|
112
|
-
await this.ctx.im('/v3/conversation/mark_read', constants_1.Cmd.MarkRead, new proto_1.Writer()
|
|
113
|
-
.bytes(1, msg.conversationId)
|
|
114
|
-
.int(2, BigInt(msg.conversationShortId))
|
|
115
|
-
.int(3, constants_1.ConversationType.OneToOne)
|
|
116
|
-
.int(4, BigInt(msg.index))
|
|
117
|
-
.int(11, 4)
|
|
118
|
-
.finish(), constants_1.MARK_READ_BODY_FIELD);
|
|
119
|
-
}
|
|
120
|
-
// ─── Send ─────────────────────────────────────────────────────────────────
|
|
121
|
-
/** Send a text message to a conversation (usually the just-received `msg` — it carries the conversation ids). */
|
|
122
|
-
async sendMessage(conv, text) {
|
|
123
|
-
const cid = (0, node_crypto_1.randomUUID)();
|
|
124
|
-
await this.send(conv, cid, constants_1.MessageType.Text, JSON.stringify({ aweType: 0, text }), null);
|
|
125
|
-
return { clientMessageId: cid };
|
|
126
|
-
}
|
|
127
|
-
/** Send an image: upload to the private (encrypted) store, then send an image message. */
|
|
128
|
-
async sendImage(conv, image) {
|
|
129
|
-
const up = await this.media.upload(image);
|
|
130
|
-
const cid = (0, node_crypto_1.randomUUID)();
|
|
131
|
-
const w = String(up.width);
|
|
132
|
-
const h = String(up.height);
|
|
133
|
-
const media = new proto_1.Writer()
|
|
134
|
-
.msg(1, new proto_1.Writer()
|
|
135
|
-
.bytes(1, up.uri)
|
|
136
|
-
.msg(5, new proto_1.Writer().int(1, up.width).int(2, up.height))
|
|
137
|
-
.bytes(6, up.secretKey)
|
|
138
|
-
.int(7, 0))
|
|
139
|
-
.msg(3, new proto_1.Writer()
|
|
140
|
-
.msg(1, new proto_1.Writer().bytes(1, 'You sent a 📷'))
|
|
141
|
-
.msg(2, new proto_1.Writer().bytes(1, 'sent a 📷'))
|
|
142
|
-
.msg(3, new proto_1.Writer().bytes(1, '[Photo]')))
|
|
143
|
-
.msg(200, new proto_1.Writer().msg(1, new proto_1.Writer().bytes(1, up.uri).map(2, {
|
|
144
|
-
image_width: w,
|
|
145
|
-
image_height: h,
|
|
146
|
-
decrypt_key: up.secretKey,
|
|
147
|
-
quote_preview: 'dm_cam_preview_photo',
|
|
148
|
-
sender_preview: 'sender_preview',
|
|
149
|
-
})));
|
|
150
|
-
const extra = new proto_1.Writer()
|
|
151
|
-
.msg(13, new proto_1.Writer().msg(4, media))
|
|
152
|
-
.bytes(14, 'private_image')
|
|
153
|
-
.msg(17, new proto_1.Writer()
|
|
154
|
-
.int(1, 1)
|
|
155
|
-
.bytes(2, up.uri)
|
|
156
|
-
.bytes(3, up.secretKey)
|
|
157
|
-
.msg(10, new proto_1.Writer().int(1, up.width).int(2, up.height).int(4, up.size).bytes(5, up.filename)));
|
|
158
|
-
await this.send(conv, cid, constants_1.MessageType.PrivateImage, '', extra);
|
|
159
|
-
return { clientMessageId: cid };
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
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
|
-
*/
|
|
166
|
-
async setReaction(msg, emoji) {
|
|
167
|
-
const own = this.ctx.ownId;
|
|
168
|
-
const mine = msg.reactions.filter((r) => r.userIds.includes(own)).map((r) => r.emoji);
|
|
169
|
-
const ops = [];
|
|
170
|
-
if (emoji && !mine.includes(emoji))
|
|
171
|
-
ops.push({ op: constants_1.PropertyOp.Add, key: constants_1.REACTION_PREFIX + emoji });
|
|
172
|
-
for (const e of mine) {
|
|
173
|
-
if (e === emoji)
|
|
174
|
-
continue;
|
|
175
|
-
ops.push({ op: constants_1.PropertyOp.Remove, key: constants_1.REACTION_PREFIX + e });
|
|
176
|
-
// ❤️ sent from the mobile app still uses the legacy key `e:love` — remove it too or the app keeps showing the heart.
|
|
177
|
-
if (e === constants_1.REACTION_HEART)
|
|
178
|
-
ops.push({ op: constants_1.PropertyOp.Remove, key: constants_1.REACTION_LOVE_KEY });
|
|
179
|
-
}
|
|
180
|
-
if (ops.length === 0)
|
|
181
|
-
return;
|
|
182
|
-
const target = new proto_1.Writer()
|
|
183
|
-
.bytes(1, msg.conversationId)
|
|
184
|
-
.int(2, constants_1.ConversationType.OneToOne)
|
|
185
|
-
.int(3, BigInt(msg.conversationShortId))
|
|
186
|
-
.int(4, BigInt(msg.serverMessageId))
|
|
187
|
-
.bytes(5, msg.clientMessageId);
|
|
188
|
-
for (const o of ops)
|
|
189
|
-
target.msg(6, new proto_1.Writer().int(1, o.op).bytes(2, o.key).bytes(4, own));
|
|
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 success, 1 duplicate (also done); anything else means TikTok rejected it.
|
|
192
|
-
const status = body.num(1);
|
|
193
|
-
if (status > 1)
|
|
194
|
-
throw new errors_1.TikTokError('IM_ERROR', `set_property: status ${status}`, status);
|
|
195
|
-
}
|
|
196
|
-
/** Download a sticker image (public CDN, unencrypted, no cookie needed). */
|
|
197
|
-
async downloadSticker(sticker) {
|
|
198
|
-
const res = await fetch(sticker.url, {
|
|
199
|
-
headers: { 'User-Agent': this.ctx.userAgent },
|
|
200
|
-
referrerPolicy: 'no-referrer',
|
|
201
|
-
signal: AbortSignal.timeout(this.ctx.options.timeoutMs ?? 20_000),
|
|
202
|
-
}).catch(() => null);
|
|
203
|
-
if (!res?.ok)
|
|
204
|
-
throw new errors_1.TikTokError('DOWNLOAD_FAILED', `Sticker download failed: ${res?.status ?? 'network'}`);
|
|
205
|
-
return { data: Buffer.from(await res.arrayBuffer()), type: res.headers.get('content-type') ?? 'image/webp' };
|
|
206
|
-
}
|
|
207
|
-
/** Download + decrypt a message's image (pass the whole `msg` — listener messages lack the URL, this looks it up). */
|
|
208
|
-
async downloadImage(source, which = 'origin') {
|
|
209
|
-
let image = 'serverMessageId' in source ? source.image : source;
|
|
210
|
-
// Websocket-pushed messages only have uri + key, no download URL → look up history by message id.
|
|
211
|
-
if ('serverMessageId' in source && image && !image.url) {
|
|
212
|
-
const { messages } = await this.getMessages(source, { limit: 5, before: (BigInt(source.index) + 1n).toString() });
|
|
213
|
-
image = messages.find((m) => m.serverMessageId === source.serverMessageId)?.image ?? image;
|
|
214
|
-
}
|
|
215
|
-
if (!image)
|
|
216
|
-
throw new errors_1.TikTokError('DOWNLOAD_FAILED', 'Message has no image');
|
|
217
|
-
return this.media.download(image, which);
|
|
218
|
-
}
|
|
219
|
-
/** `SendMessageRequestBody`: {1 convId, 2 conversation type, 3 shortId, 4 content, 5 ext, 6 message type, 7 ticket, 8 client id, …}. */
|
|
220
|
-
async send(conv, cid, type, content, extra) {
|
|
221
|
-
if (!conv.conversationId || !conv.conversationShortId) {
|
|
222
|
-
throw new errors_1.TikTokError('IM_ERROR', 'Missing conversationId / conversationShortId');
|
|
223
|
-
}
|
|
224
|
-
const w = new proto_1.Writer()
|
|
225
|
-
.bytes(1, conv.conversationId)
|
|
226
|
-
.int(2, constants_1.ConversationType.OneToOne)
|
|
227
|
-
.int(3, BigInt(conv.conversationShortId))
|
|
228
|
-
.bytes(4, content)
|
|
229
|
-
.map(5, { 's:mentioned_users': '', 's:client_message_id': cid })
|
|
230
|
-
.int(6, type)
|
|
231
|
-
.bytes(7, 'deprecated')
|
|
232
|
-
.bytes(8, cid);
|
|
233
|
-
const body = extra ? Buffer.concat([w.finish(), extra.finish()]) : w.finish();
|
|
234
|
-
await this.ctx.im('/v1/message/send', constants_1.Cmd.SendMessage, body);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
exports.API = API;
|
|
238
|
-
/** Account for the cookie — `null` if the cookie is no longer logged in. */
|
|
239
|
-
async function fetchAccount(ctx) {
|
|
240
|
-
const res = await ctx.getJson(`${constants_1.WEB}/passport/web/account/info/?aid=${constants_1.AID_FRONTIER}`);
|
|
241
|
-
if (res.message !== 'success' || !res.data?.user_id_str)
|
|
242
|
-
return null;
|
|
243
|
-
return {
|
|
244
|
-
uid: res.data.user_id_str,
|
|
245
|
-
username: res.data.username ?? '',
|
|
246
|
-
screenName: res.data.screen_name ?? '',
|
|
247
|
-
avatar: res.data.avatar_url ?? '',
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
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
|
-
*/
|
|
254
|
-
function avatarLinks(avatars) {
|
|
255
|
-
const urls = [];
|
|
256
|
-
const walk = (v) => {
|
|
257
|
-
if (typeof v === 'string') {
|
|
258
|
-
if (/^https?:\/\//.test(v) && !urls.includes(v))
|
|
259
|
-
urls.push(v);
|
|
260
|
-
}
|
|
261
|
-
else if (Array.isArray(v))
|
|
262
|
-
v.forEach(walk);
|
|
263
|
-
else if (v && typeof v === 'object')
|
|
264
|
-
Object.values(v).forEach(walk);
|
|
265
|
-
};
|
|
266
|
-
walk(avatars);
|
|
267
|
-
const heic = (u) => /\.heic(\?|$)/i.test(u.split('?')[0] + '?');
|
|
268
|
-
const ranked = [...urls.filter((u) => !heic(u)), ...urls.filter(heic)];
|
|
269
|
-
return { avatar: ranked[0] ?? '', avatars: ranked };
|
|
270
|
-
}
|
|
271
|
-
/** `wid` — device id TikTok issues per `ttwid` cookie; IM + websocket use it as `device_id`. */
|
|
272
|
-
async function fetchWebId(ctx) {
|
|
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
|
-
if (!res.ok)
|
|
275
|
-
return '';
|
|
276
|
-
return /"wid":"(\d+)"/.exec(await res.text())?.[1] ?? '';
|
|
277
|
-
}
|
package/dist/constants.d.ts
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/**
|
|
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
|
-
*/
|
|
5
|
-
export declare const IM_API = "https://im-api-sg.tiktok.com";
|
|
6
|
-
export declare const IM_WS = "wss://im-ws-sg.tiktok.com/ws/v2";
|
|
7
|
-
export declare const WEB = "https://www.tiktok.com";
|
|
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
|
-
/** TikTok Web app id (HTTP IM). */
|
|
10
|
-
export declare const AID_WEB = "1988";
|
|
11
|
-
/** "frontier" channel app id — websocket pushing new messages. */
|
|
12
|
-
export declare const AID_FRONTIER = "1459";
|
|
13
|
-
export declare const SDK_VERSION = "1.8.5";
|
|
14
|
-
export declare const BUILD_NUMBER = "feffaa1:master";
|
|
15
|
-
/** Websocket `access_key` = md5(FPID + APP_KEY + device_id + SALT). */
|
|
16
|
-
export declare const FRONTIER_FPID = "9";
|
|
17
|
-
export declare const FRONTIER_APP_KEY = "e1bd35ec9db7b8d846de66ed140b1ad9";
|
|
18
|
-
export declare const FRONTIER_SALT = "f8a69f1719916z";
|
|
19
|
-
/** IM command (`Request.cmd`) — also the body's field number in `RequestBody`. */
|
|
20
|
-
export declare const Cmd: {
|
|
21
|
-
readonly SendMessage: 100;
|
|
22
|
-
readonly GetMessagesByUserInit: 203;
|
|
23
|
-
readonly GetMessagesByConversation: 301;
|
|
24
|
-
/** Websocket push of new messages. */
|
|
25
|
-
readonly NewMessage: 500;
|
|
26
|
-
/** Add / remove a reaction (message `property`) — `/v1/message/set_property`. */
|
|
27
|
-
readonly SetMessageProperty: 705;
|
|
28
|
-
readonly GetConversationInfo: 610;
|
|
29
|
-
readonly MarkRead: 2002;
|
|
30
|
-
readonly UploadConfig: 2059;
|
|
31
|
-
};
|
|
32
|
-
/** MarkRead's body lives in field 604, not the command number. */
|
|
33
|
-
export declare const MARK_READ_BODY_FIELD = 604;
|
|
34
|
-
/** `MessageBody.message_type`. */
|
|
35
|
-
export declare const MessageType: {
|
|
36
|
-
readonly System: 1;
|
|
37
|
-
readonly Text: 7;
|
|
38
|
-
readonly ShareVideo: 8;
|
|
39
|
-
readonly PrivateImage: 1802;
|
|
40
|
-
/** Sticker — public image on the sticker CDN, not encrypted. */
|
|
41
|
-
readonly Sticker: 1805;
|
|
42
|
-
/** Hidden command (read receipt…) — not shown in the conversation, but still pushed over websocket. */
|
|
43
|
-
readonly Command: 50001;
|
|
44
|
-
};
|
|
45
|
-
export declare const ConversationType: {
|
|
46
|
-
readonly OneToOne: 1;
|
|
47
|
-
};
|
|
48
|
-
/** `Response` fields (both HTTP and websocket payload). */
|
|
49
|
-
export declare const Resp: {
|
|
50
|
-
readonly cmd: 1;
|
|
51
|
-
readonly seq: 2;
|
|
52
|
-
readonly status: 3;
|
|
53
|
-
readonly error: 4;
|
|
54
|
-
readonly body: 6;
|
|
55
|
-
readonly logId: 7;
|
|
56
|
-
};
|
|
57
|
-
/** `MessageBody` fields. */
|
|
58
|
-
export declare const MsgF: {
|
|
59
|
-
readonly conversationId: 1;
|
|
60
|
-
readonly conversationType: 2;
|
|
61
|
-
readonly serverMessageId: 3;
|
|
62
|
-
readonly indexInConversation: 4;
|
|
63
|
-
readonly conversationShortId: 5;
|
|
64
|
-
readonly type: 6;
|
|
65
|
-
readonly sender: 7;
|
|
66
|
-
readonly content: 8;
|
|
67
|
-
readonly ext: 9;
|
|
68
|
-
readonly createTime: 10;
|
|
69
|
-
/** `property_list` — reaction: `{1 key "e:<emoji>", 2 {1 PropertyItem[]}}`. */
|
|
70
|
-
readonly propertyList: 15;
|
|
71
|
-
readonly attachments: 20;
|
|
72
|
-
readonly tag: 21;
|
|
73
|
-
};
|
|
74
|
-
/** Operation on a message `property` (`ModifyPropertyContent.operation`). */
|
|
75
|
-
export declare const PropertyOp: {
|
|
76
|
-
readonly Add: 0;
|
|
77
|
-
readonly Remove: 1;
|
|
78
|
-
};
|
|
79
|
-
/** Reaction key = `e:<emoji>`. The mobile app reacting ❤️ also writes the legacy key `e:love` — treated as ❤️. */
|
|
80
|
-
export declare const REACTION_PREFIX = "e:";
|
|
81
|
-
export declare const REACTION_LOVE_KEY = "e:love";
|
|
82
|
-
export declare const REACTION_HEART = "\u2764\uFE0F";
|
|
83
|
-
/** ImageX — image upload endpoint (AWS SigV4-style signing). */
|
|
84
|
-
export declare const IMAGEX_REGION = "cn-north-1";
|
|
85
|
-
export declare const IMAGEX_SERVICE = "imagex";
|
|
86
|
-
export declare const IMAGEX_VERSION = "2018-08-01";
|
package/dist/constants.js
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
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
|
-
*/
|
|
6
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
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;
|
|
8
|
-
exports.IM_API = 'https://im-api-sg.tiktok.com';
|
|
9
|
-
exports.IM_WS = 'wss://im-ws-sg.tiktok.com/ws/v2';
|
|
10
|
-
exports.WEB = 'https://www.tiktok.com';
|
|
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
|
-
/** TikTok Web app id (HTTP IM). */
|
|
13
|
-
exports.AID_WEB = '1988';
|
|
14
|
-
/** "frontier" channel app id — websocket pushing new messages. */
|
|
15
|
-
exports.AID_FRONTIER = '1459';
|
|
16
|
-
exports.SDK_VERSION = '1.8.5';
|
|
17
|
-
exports.BUILD_NUMBER = 'feffaa1:master';
|
|
18
|
-
/** Websocket `access_key` = md5(FPID + APP_KEY + device_id + SALT). */
|
|
19
|
-
exports.FRONTIER_FPID = '9';
|
|
20
|
-
exports.FRONTIER_APP_KEY = 'e1bd35ec9db7b8d846de66ed140b1ad9';
|
|
21
|
-
exports.FRONTIER_SALT = 'f8a69f1719916z';
|
|
22
|
-
/** IM command (`Request.cmd`) — also the body's field number in `RequestBody`. */
|
|
23
|
-
exports.Cmd = {
|
|
24
|
-
SendMessage: 100,
|
|
25
|
-
GetMessagesByUserInit: 203,
|
|
26
|
-
GetMessagesByConversation: 301,
|
|
27
|
-
/** Websocket push of new messages. */
|
|
28
|
-
NewMessage: 500,
|
|
29
|
-
/** Add / remove a reaction (message `property`) — `/v1/message/set_property`. */
|
|
30
|
-
SetMessageProperty: 705,
|
|
31
|
-
GetConversationInfo: 610,
|
|
32
|
-
MarkRead: 2002,
|
|
33
|
-
UploadConfig: 2059,
|
|
34
|
-
};
|
|
35
|
-
/** MarkRead's body lives in field 604, not the command number. */
|
|
36
|
-
exports.MARK_READ_BODY_FIELD = 604;
|
|
37
|
-
/** `MessageBody.message_type`. */
|
|
38
|
-
exports.MessageType = {
|
|
39
|
-
System: 1,
|
|
40
|
-
Text: 7,
|
|
41
|
-
ShareVideo: 8,
|
|
42
|
-
PrivateImage: 1802,
|
|
43
|
-
/** Sticker — public image on the sticker CDN, not encrypted. */
|
|
44
|
-
Sticker: 1805,
|
|
45
|
-
/** Hidden command (read receipt…) — not shown in the conversation, but still pushed over websocket. */
|
|
46
|
-
Command: 50001,
|
|
47
|
-
};
|
|
48
|
-
exports.ConversationType = { OneToOne: 1 };
|
|
49
|
-
/** `Response` fields (both HTTP and websocket payload). */
|
|
50
|
-
exports.Resp = { cmd: 1, seq: 2, status: 3, error: 4, body: 6, logId: 7 };
|
|
51
|
-
/** `MessageBody` fields. */
|
|
52
|
-
exports.MsgF = {
|
|
53
|
-
conversationId: 1,
|
|
54
|
-
conversationType: 2,
|
|
55
|
-
serverMessageId: 3,
|
|
56
|
-
indexInConversation: 4,
|
|
57
|
-
conversationShortId: 5,
|
|
58
|
-
type: 6,
|
|
59
|
-
sender: 7,
|
|
60
|
-
content: 8,
|
|
61
|
-
ext: 9,
|
|
62
|
-
createTime: 10,
|
|
63
|
-
/** `property_list` — reaction: `{1 key "e:<emoji>", 2 {1 PropertyItem[]}}`. */
|
|
64
|
-
propertyList: 15,
|
|
65
|
-
attachments: 20,
|
|
66
|
-
tag: 21,
|
|
67
|
-
};
|
|
68
|
-
/** Operation on a message `property` (`ModifyPropertyContent.operation`). */
|
|
69
|
-
exports.PropertyOp = { Add: 0, Remove: 1 };
|
|
70
|
-
/** Reaction key = `e:<emoji>`. The mobile app reacting ❤️ also writes the legacy key `e:love` — treated as ❤️. */
|
|
71
|
-
exports.REACTION_PREFIX = 'e:';
|
|
72
|
-
exports.REACTION_LOVE_KEY = 'e:love';
|
|
73
|
-
exports.REACTION_HEART = '\u2764\uFE0F';
|
|
74
|
-
/** ImageX — image upload endpoint (AWS SigV4-style signing). */
|
|
75
|
-
exports.IMAGEX_REGION = 'cn-north-1';
|
|
76
|
-
exports.IMAGEX_SERVICE = 'imagex';
|
|
77
|
-
exports.IMAGEX_VERSION = '2018-08-01';
|
package/dist/context.d.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { Msg } from './proto';
|
|
2
|
-
import type { Credentials, TikTokOptions } from './types';
|
|
3
|
-
/** `name → value` cookies from any input shape of `Credentials.cookies`. */
|
|
4
|
-
export declare function normalizeCookies(input: Credentials['cookies']): Map<string, string>;
|
|
5
|
-
/** Shared state of a session: cookies, device id, uid, HTTP calls. */
|
|
6
|
-
export declare class Context {
|
|
7
|
-
readonly options: TikTokOptions;
|
|
8
|
-
readonly cookies: Map<string, string>;
|
|
9
|
-
/**
|
|
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
|
-
*/
|
|
13
|
-
deviceId: string;
|
|
14
|
-
readonly userAgent: string;
|
|
15
|
-
ownId: string;
|
|
16
|
-
private seq;
|
|
17
|
-
constructor(creds: Credentials, options?: TikTokOptions);
|
|
18
|
-
cookieHeader(): string;
|
|
19
|
-
log(level: 'debug' | 'warn', msg: string): void;
|
|
20
|
-
/** `fetch` with cookies + UA + timeout. */
|
|
21
|
-
fetch(url: string, init?: RequestInit & {
|
|
22
|
-
headers?: Record<string, string>;
|
|
23
|
-
}): Promise<Response>;
|
|
24
|
-
getJson<T>(url: string): Promise<T>;
|
|
25
|
-
/** IM `Request` envelope: command + body + common headers. */
|
|
26
|
-
buildRequest(cmd: number, bodyField: number, body: Buffer): Buffer;
|
|
27
|
-
/**
|
|
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
|
-
*/
|
|
31
|
-
im(path: string, cmd: number, body: Buffer, bodyField?: number): Promise<Msg>;
|
|
32
|
-
}
|
package/dist/context.js
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Context = void 0;
|
|
4
|
-
exports.normalizeCookies = normalizeCookies;
|
|
5
|
-
const constants_1 = require("./constants");
|
|
6
|
-
const errors_1 = require("./errors");
|
|
7
|
-
const proto_1 = require("./proto");
|
|
8
|
-
/** `name → value` cookies from any input shape of `Credentials.cookies`. */
|
|
9
|
-
function normalizeCookies(input) {
|
|
10
|
-
const jar = new Map();
|
|
11
|
-
if (typeof input === 'string') {
|
|
12
|
-
for (const part of input.split(';')) {
|
|
13
|
-
const i = part.indexOf('=');
|
|
14
|
-
if (i > 0)
|
|
15
|
-
jar.set(part.slice(0, i).trim(), part.slice(i + 1).trim());
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
else if (Array.isArray(input)) {
|
|
19
|
-
for (const c of input)
|
|
20
|
-
if (c?.name)
|
|
21
|
-
jar.set(c.name, String(c.value ?? ''));
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
for (const [k, v] of Object.entries(input ?? {}))
|
|
25
|
-
jar.set(k, String(v));
|
|
26
|
-
}
|
|
27
|
-
return jar;
|
|
28
|
-
}
|
|
29
|
-
/** Shared state of a session: cookies, device id, uid, HTTP calls. */
|
|
30
|
-
class Context {
|
|
31
|
-
options;
|
|
32
|
-
cookies;
|
|
33
|
-
/**
|
|
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
|
-
*/
|
|
37
|
-
deviceId;
|
|
38
|
-
userAgent;
|
|
39
|
-
ownId = '';
|
|
40
|
-
seq = 10_000;
|
|
41
|
-
constructor(creds, options = {}) {
|
|
42
|
-
this.options = options;
|
|
43
|
-
this.cookies = normalizeCookies(creds.cookies);
|
|
44
|
-
this.deviceId = creds.deviceId ?? '';
|
|
45
|
-
this.userAgent = creds.userAgent || constants_1.DEFAULT_USER_AGENT;
|
|
46
|
-
}
|
|
47
|
-
cookieHeader() {
|
|
48
|
-
return [...this.cookies].map(([k, v]) => `${k}=${v}`).join('; ');
|
|
49
|
-
}
|
|
50
|
-
log(level, msg) {
|
|
51
|
-
this.options.logger?.(level, msg);
|
|
52
|
-
}
|
|
53
|
-
/** `fetch` with cookies + UA + timeout. */
|
|
54
|
-
async fetch(url, init = {}) {
|
|
55
|
-
try {
|
|
56
|
-
return await fetch(url, {
|
|
57
|
-
...init,
|
|
58
|
-
headers: {
|
|
59
|
-
'User-Agent': this.userAgent,
|
|
60
|
-
Referer: `${constants_1.WEB}/`,
|
|
61
|
-
Origin: constants_1.WEB,
|
|
62
|
-
Cookie: this.cookieHeader(),
|
|
63
|
-
...init.headers,
|
|
64
|
-
},
|
|
65
|
-
signal: AbortSignal.timeout(this.options.timeoutMs ?? 20_000),
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
catch (err) {
|
|
69
|
-
throw new errors_1.TikTokError('HTTP_ERROR', `${url.split('?')[0]}: ${err instanceof Error ? err.message : String(err)}`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
async getJson(url) {
|
|
73
|
-
const res = await this.fetch(url);
|
|
74
|
-
if (!res.ok)
|
|
75
|
-
throw new errors_1.TikTokError('HTTP_ERROR', `${url.split('?')[0]}: HTTP ${res.status}`, res.status);
|
|
76
|
-
return (await res.json());
|
|
77
|
-
}
|
|
78
|
-
/** IM `Request` envelope: command + body + common headers. */
|
|
79
|
-
buildRequest(cmd, bodyField, body) {
|
|
80
|
-
return new proto_1.Writer()
|
|
81
|
-
.int(1, cmd)
|
|
82
|
-
.int(2, this.seq++)
|
|
83
|
-
.bytes(3, constants_1.SDK_VERSION)
|
|
84
|
-
.bytes(4, '')
|
|
85
|
-
.int(5, 3)
|
|
86
|
-
.int(6, 0)
|
|
87
|
-
.bytes(7, constants_1.BUILD_NUMBER)
|
|
88
|
-
.msg(8, new proto_1.Writer().bytes(bodyField, body))
|
|
89
|
-
.bytes(9, this.deviceId)
|
|
90
|
-
.bytes(11, 'web')
|
|
91
|
-
.map(15, {
|
|
92
|
-
aid: constants_1.AID_WEB,
|
|
93
|
-
app_name: 'tiktok_web',
|
|
94
|
-
channel: 'web',
|
|
95
|
-
device_platform: 'web_pc',
|
|
96
|
-
device_id: this.deviceId,
|
|
97
|
-
user_is_login: 'true',
|
|
98
|
-
user_agent: this.userAgent,
|
|
99
|
-
})
|
|
100
|
-
.int(18, 1)
|
|
101
|
-
.finish();
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
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
|
-
*/
|
|
107
|
-
async im(path, cmd, body, bodyField = cmd) {
|
|
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`;
|
|
109
|
-
const res = await this.fetch(url, {
|
|
110
|
-
method: 'POST',
|
|
111
|
-
headers: { 'Content-Type': 'application/x-protobuf', Accept: 'application/x-protobuf' },
|
|
112
|
-
body: new Uint8Array(this.buildRequest(cmd, bodyField, body)),
|
|
113
|
-
});
|
|
114
|
-
if (!res.ok)
|
|
115
|
-
throw new errors_1.TikTokError('HTTP_ERROR', `${path}: HTTP ${res.status}`, res.status);
|
|
116
|
-
const resp = proto_1.Msg.tryParse(Buffer.from(await res.arrayBuffer()));
|
|
117
|
-
if (!resp)
|
|
118
|
-
throw new errors_1.TikTokError('BAD_RESPONSE', `${path}: response is not protobuf`);
|
|
119
|
-
const status = resp.num(constants_1.Resp.status);
|
|
120
|
-
if (status !== 0) {
|
|
121
|
-
const desc = resp.str(constants_1.Resp.error);
|
|
122
|
-
// Not logged in: 200005 (tested with a cookieless request).
|
|
123
|
-
const code = status === 200005 ? 'SESSION_EXPIRED' : 'IM_ERROR';
|
|
124
|
-
throw new errors_1.TikTokError(code, `${path}: ${status} ${desc}`.trim(), status);
|
|
125
|
-
}
|
|
126
|
-
return resp.msg(constants_1.Resp.body)?.msg(cmd) ?? resp.msg(constants_1.Resp.body)?.msg(bodyField) ?? proto_1.Msg.parse(Buffer.alloc(0));
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
exports.Context = Context;
|
package/dist/errors.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/** Error from TikTok or from the library itself. `code` lets callers branch (e.g. session expired → rescan). */
|
|
2
|
-
export declare class TikTokError extends Error {
|
|
3
|
-
readonly code: TikTokErrorCode;
|
|
4
|
-
/** Original TikTok code (status_code / error_code), if any. */
|
|
5
|
-
readonly upstream?: number | string | undefined;
|
|
6
|
-
constructor(code: TikTokErrorCode, message: string,
|
|
7
|
-
/** Original TikTok code (status_code / error_code), if any. */
|
|
8
|
-
upstream?: number | string | undefined);
|
|
9
|
-
}
|
|
10
|
-
export type TikTokErrorCode =
|
|
11
|
-
/** Cookie is no longer logged in — a new session is required. */
|
|
12
|
-
'SESSION_EXPIRED' | 'HTTP_ERROR'
|
|
13
|
-
/** IM returned a non-zero `status_code`. */
|
|
14
|
-
| 'IM_ERROR' | 'UPLOAD_FAILED' | 'DOWNLOAD_FAILED' | 'BAD_RESPONSE'
|
|
15
|
-
/** QR login failed (Chrome failed to launch, QR expired, no cookie found…). */
|
|
16
|
-
| 'LOGIN_FAILED'
|
|
17
|
-
/** Caller aborted QR login (`signal`). */
|
|
18
|
-
| 'LOGIN_ABORTED';
|
package/dist/errors.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TikTokError = void 0;
|
|
4
|
-
/** Error from TikTok or from the library itself. `code` lets callers branch (e.g. session expired → rescan). */
|
|
5
|
-
class TikTokError extends Error {
|
|
6
|
-
code;
|
|
7
|
-
upstream;
|
|
8
|
-
constructor(code, message,
|
|
9
|
-
/** Original TikTok code (status_code / error_code), if any. */
|
|
10
|
-
upstream) {
|
|
11
|
-
super(message);
|
|
12
|
-
this.code = code;
|
|
13
|
-
this.upstream = upstream;
|
|
14
|
-
this.name = 'TikTokError';
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
exports.TikTokError = TikTokError;
|