tca-js 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +202 -100
- package/dist/index.d.mts +369 -0
- package/dist/index.d.ts +362 -26
- package/dist/index.js +1304 -52
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1262 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +56 -10
- 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
|
-
/** Mốc "mới nhất" khi đọc lịch sử — lớn hơn mọi `index` (µs). */
|
|
14
|
-
const NEWEST = 9999999999999999n;
|
|
15
|
-
/**
|
|
16
|
-
* Một phiên TikTok đã đăng nhập — tạo bằng `new TikTok().login(credentials)`.
|
|
17
|
-
* Mọi lệnh đi thẳng HTTP/WS tới TikTok bằng cookie, không cần trình duyệt.
|
|
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
|
-
/** Cookie hiện tại — lưu lại để lần sau `login` không cần quét QR. */
|
|
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
|
-
/** Phiên còn sống không — gọi định kỳ để phát hiện hết hạn sớm. */
|
|
41
|
-
async isAlive() {
|
|
42
|
-
return (await fetchAccount(this.ctx).catch(() => null)) !== null;
|
|
43
|
-
}
|
|
44
|
-
// ─── Hộp thư ─────────────────────────────────────────────────────────────
|
|
45
|
-
/** Hội thoại gần đây + tin mới nhất của chúng, lật trang bằng `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
|
-
// Con trỏ = số hội thoại đã lấy (0, 10, 20…) — trường 4; trường 3 là mốc thời gian, không dùng để lật.
|
|
52
|
-
cursor: body.id(4),
|
|
53
|
-
hasMore: body.num(5) === 1,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
/** Tin của một hội thoại, mới nhất trước. Đọc tiếp tin cũ hơn: `before = <index của tin cũ nhất>`. */
|
|
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) // hướng: cũ hơn mốc
|
|
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
|
-
/** Hồ sơ người dùng theo uid (tên hiển thị, @username, ảnh đại diện). */
|
|
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
|
-
* Tải ảnh đại diện (thử lần lượt các link, bỏ heic). Không gửi cookie / Referer: CDN ảnh khác miền và
|
|
94
|
-
* link đã có chữ ký. `null` nếu không link nào tải được.
|
|
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
|
-
/** Đánh dấu đã đọc tới tin `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
|
-
// ─── Gửi ──────────────────────────────────────────────────────────────────
|
|
121
|
-
/** Gửi tin chữ vào hội thoại (thường là `msg` vừa nhận — nó mang sẵn mã hội thoại). */
|
|
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
|
-
/** Gửi một ảnh: tải lên kho riêng tư (được mã hoá) rồi gửi tin ảnh. */
|
|
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
|
-
* Đặt reaction CỦA MÌNH trên một tin — như TikTok Web, mỗi người một reaction: `emoji` mới thay cái cũ,
|
|
163
|
-
* `null` = gỡ. Truyền nguyên `msg` vừa đọc (cần `reactions` hiện tại để biết gỡ khoá nào).
|
|
164
|
-
* Gửi `ModifyMessagePropertyRequestBody` (lệnh 705) — xem 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
|
-
// ❤️ thả từ app điện thoại còn nằm ở khoá cũ `e:love` — gỡ luôn kẻo app vẫn hiện tim.
|
|
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 thành công, 1 gửi trùng (cũng là xong); còn lại TikTok từ chối.
|
|
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
|
-
/** Tải ảnh sticker (CDN công khai, không mã hoá, không cần cookie). */
|
|
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', `Tải sticker lỗi: ${res?.status ?? 'mạng'}`);
|
|
205
|
-
return { data: Buffer.from(await res.arrayBuffer()), type: res.headers.get('content-type') ?? 'image/webp' };
|
|
206
|
-
}
|
|
207
|
-
/** Tải + giải mã ảnh của một tin (truyền cả `msg` — tin từ listener thiếu link, hàm tự tra lại). */
|
|
208
|
-
async downloadImage(source, which = 'origin') {
|
|
209
|
-
let image = 'serverMessageId' in source ? source.image : source;
|
|
210
|
-
// Tin đẩy qua websocket chỉ có uri + khoá, không có link tải → tra lại lịch sử theo mã tin.
|
|
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', 'Tin không có ảnh');
|
|
217
|
-
return this.media.download(image, which);
|
|
218
|
-
}
|
|
219
|
-
/** `SendMessageRequestBody`: {1 convId, 2 loại hội thoại, 3 shortId, 4 nội dung, 5 ext, 6 loại tin, 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', 'Thiếu 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
|
-
/** Tài khoản của cookie — `null` nếu cookie không còn đăng nhập. */
|
|
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
|
-
* Link ảnh đại diện trong `avatars` của hồ sơ IM — gom mọi chuỗi `https://…` (cỡ thumb → medium → larger, tuỳ
|
|
252
|
-
* TikTok trả gì), rồi xếp bản trình duyệt đọc được (jpeg/webp/png) lên trước, heic xuống cuối.
|
|
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` — mã máy TikTok cấp theo cookie `ttwid`; IM + websocket dùng làm `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
|
-
* Hằng giao thức IM của TikTok Web — dựng lại từ lưu lượng `tiktok.com/business-suite/messages`
|
|
3
|
-
* (09/11/2026). TikTok đổi phía họ thì sửa ở đây; ghi lại cách dò ra ở 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
|
-
/** App id của TikTok Web (HTTP IM). */
|
|
10
|
-
export declare const AID_WEB = "1988";
|
|
11
|
-
/** App id kênh "frontier" — websocket đẩy tin mới. */
|
|
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
|
-
/** `access_key` của websocket = 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
|
-
/** Lệnh IM (`Request.cmd`) — đồng thời là số trường của body trong `RequestBody`. */
|
|
20
|
-
export declare const Cmd: {
|
|
21
|
-
readonly SendMessage: 100;
|
|
22
|
-
readonly GetMessagesByUserInit: 203;
|
|
23
|
-
readonly GetMessagesByConversation: 301;
|
|
24
|
-
/** Websocket đẩy tin mới. */
|
|
25
|
-
readonly NewMessage: 500;
|
|
26
|
-
/** Thả / gỡ reaction (`property` của tin) — `/v1/message/set_property`. */
|
|
27
|
-
readonly SetMessageProperty: 705;
|
|
28
|
-
readonly GetConversationInfo: 610;
|
|
29
|
-
readonly MarkRead: 2002;
|
|
30
|
-
readonly UploadConfig: 2059;
|
|
31
|
-
};
|
|
32
|
-
/** Body của MarkRead nằm ở trường 604, không trùng số lệnh. */
|
|
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 (nhãn dán) — ảnh công khai trên CDN sticker, không mã hoá. */
|
|
41
|
-
readonly Sticker: 1805;
|
|
42
|
-
/** Lệnh ẩn (đã đọc…) — không hiện trong hội thoại, websocket vẫn đẩy. */
|
|
43
|
-
readonly Command: 50001;
|
|
44
|
-
};
|
|
45
|
-
export declare const ConversationType: {
|
|
46
|
-
readonly OneToOne: 1;
|
|
47
|
-
};
|
|
48
|
-
/** Trường của `Response` (cả HTTP lẫn payload websocket). */
|
|
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
|
-
/** Trường của `MessageBody`. */
|
|
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 khoá "e:<emoji>", 2 {1 PropertyItem[]}}`. */
|
|
70
|
-
readonly propertyList: 15;
|
|
71
|
-
readonly attachments: 20;
|
|
72
|
-
readonly tag: 21;
|
|
73
|
-
};
|
|
74
|
-
/** Thao tác trên `property` của tin (`ModifyPropertyContent.operation`). */
|
|
75
|
-
export declare const PropertyOp: {
|
|
76
|
-
readonly Add: 0;
|
|
77
|
-
readonly Remove: 1;
|
|
78
|
-
};
|
|
79
|
-
/** Khoá reaction = `e:<emoji>`. App điện thoại thả ❤️ còn ghi thêm khoá cũ `e:love` — coi như ❤️. */
|
|
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 — nơi tải ảnh lên (ký kiểu AWS SigV4). */
|
|
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
|
-
* Hằng giao thức IM của TikTok Web — dựng lại từ lưu lượng `tiktok.com/business-suite/messages`
|
|
4
|
-
* (09/11/2026). TikTok đổi phía họ thì sửa ở đây; ghi lại cách dò ra ở 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
|
-
/** App id của TikTok Web (HTTP IM). */
|
|
13
|
-
exports.AID_WEB = '1988';
|
|
14
|
-
/** App id kênh "frontier" — websocket đẩy tin mới. */
|
|
15
|
-
exports.AID_FRONTIER = '1459';
|
|
16
|
-
exports.SDK_VERSION = '1.8.5';
|
|
17
|
-
exports.BUILD_NUMBER = 'feffaa1:master';
|
|
18
|
-
/** `access_key` của websocket = md5(FPID + APP_KEY + device_id + SALT). */
|
|
19
|
-
exports.FRONTIER_FPID = '9';
|
|
20
|
-
exports.FRONTIER_APP_KEY = 'e1bd35ec9db7b8d846de66ed140b1ad9';
|
|
21
|
-
exports.FRONTIER_SALT = 'f8a69f1719916z';
|
|
22
|
-
/** Lệnh IM (`Request.cmd`) — đồng thời là số trường của body trong `RequestBody`. */
|
|
23
|
-
exports.Cmd = {
|
|
24
|
-
SendMessage: 100,
|
|
25
|
-
GetMessagesByUserInit: 203,
|
|
26
|
-
GetMessagesByConversation: 301,
|
|
27
|
-
/** Websocket đẩy tin mới. */
|
|
28
|
-
NewMessage: 500,
|
|
29
|
-
/** Thả / gỡ reaction (`property` của tin) — `/v1/message/set_property`. */
|
|
30
|
-
SetMessageProperty: 705,
|
|
31
|
-
GetConversationInfo: 610,
|
|
32
|
-
MarkRead: 2002,
|
|
33
|
-
UploadConfig: 2059,
|
|
34
|
-
};
|
|
35
|
-
/** Body của MarkRead nằm ở trường 604, không trùng số lệnh. */
|
|
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 (nhãn dán) — ảnh công khai trên CDN sticker, không mã hoá. */
|
|
44
|
-
Sticker: 1805,
|
|
45
|
-
/** Lệnh ẩn (đã đọc…) — không hiện trong hội thoại, websocket vẫn đẩy. */
|
|
46
|
-
Command: 50001,
|
|
47
|
-
};
|
|
48
|
-
exports.ConversationType = { OneToOne: 1 };
|
|
49
|
-
/** Trường của `Response` (cả HTTP lẫn payload websocket). */
|
|
50
|
-
exports.Resp = { cmd: 1, seq: 2, status: 3, error: 4, body: 6, logId: 7 };
|
|
51
|
-
/** Trường của `MessageBody`. */
|
|
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 khoá "e:<emoji>", 2 {1 PropertyItem[]}}`. */
|
|
64
|
-
propertyList: 15,
|
|
65
|
-
attachments: 20,
|
|
66
|
-
tag: 21,
|
|
67
|
-
};
|
|
68
|
-
/** Thao tác trên `property` của tin (`ModifyPropertyContent.operation`). */
|
|
69
|
-
exports.PropertyOp = { Add: 0, Remove: 1 };
|
|
70
|
-
/** Khoá reaction = `e:<emoji>`. App điện thoại thả ❤️ còn ghi thêm khoá cũ `e:love` — coi như ❤️. */
|
|
71
|
-
exports.REACTION_PREFIX = 'e:';
|
|
72
|
-
exports.REACTION_LOVE_KEY = 'e:love';
|
|
73
|
-
exports.REACTION_HEART = '\u2764\uFE0F';
|
|
74
|
-
/** ImageX — nơi tải ảnh lên (ký kiểu AWS SigV4). */
|
|
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
|
-
/** Cookie dạng `name → value` từ mọi kiểu đầu vào của `Credentials.cookies`. */
|
|
4
|
-
export declare function normalizeCookies(input: Credentials['cookies']): Map<string, string>;
|
|
5
|
-
/** Trạng thái dùng chung của một phiên: cookie, mã máy, uid, gọi HTTP. */
|
|
6
|
-
export declare class Context {
|
|
7
|
-
readonly options: TikTokOptions;
|
|
8
|
-
readonly cookies: Map<string, string>;
|
|
9
|
-
/**
|
|
10
|
-
* Mã máy IM = `wid` TikTok cấp theo cookie `ttwid` (`login` tự hỏi). Websocket chỉ nhận đúng
|
|
11
|
-
* mã này — mã tự bịa bị 400 (HTTP IM thì không kén).
|
|
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` kèm cookie + UA + hạn chờ. */
|
|
21
|
-
fetch(url: string, init?: RequestInit & {
|
|
22
|
-
headers?: Record<string, string>;
|
|
23
|
-
}): Promise<Response>;
|
|
24
|
-
getJson<T>(url: string): Promise<T>;
|
|
25
|
-
/** Vỏ `Request` của IM: lệnh + body + đầu mục chung. */
|
|
26
|
-
buildRequest(cmd: number, bodyField: number, body: Buffer): Buffer;
|
|
27
|
-
/**
|
|
28
|
-
* Gọi một lệnh IM qua HTTP. Trả body đã bóc (`ResponseBody[cmd]`).
|
|
29
|
-
* KHÔNG cần chữ ký X-Gnarly/X-Bogus/msToken — server IM chỉ xét cookie (thử 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
|
-
/** Cookie dạng `name → value` từ mọi kiểu đầu vào của `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
|
-
/** Trạng thái dùng chung của một phiên: cookie, mã máy, uid, gọi HTTP. */
|
|
30
|
-
class Context {
|
|
31
|
-
options;
|
|
32
|
-
cookies;
|
|
33
|
-
/**
|
|
34
|
-
* Mã máy IM = `wid` TikTok cấp theo cookie `ttwid` (`login` tự hỏi). Websocket chỉ nhận đúng
|
|
35
|
-
* mã này — mã tự bịa bị 400 (HTTP IM thì không kén).
|
|
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` kèm cookie + UA + hạn chờ. */
|
|
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
|
-
/** Vỏ `Request` của IM: lệnh + body + đầu mục chung. */
|
|
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
|
-
* Gọi một lệnh IM qua HTTP. Trả body đã bóc (`ResponseBody[cmd]`).
|
|
105
|
-
* KHÔNG cần chữ ký X-Gnarly/X-Bogus/msToken — server IM chỉ xét cookie (thử 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}: phản hồi không phải protobuf`);
|
|
119
|
-
const status = resp.num(constants_1.Resp.status);
|
|
120
|
-
if (status !== 0) {
|
|
121
|
-
const desc = resp.str(constants_1.Resp.error);
|
|
122
|
-
// Chưa đăng nhập: 200005 (thử bằng request không cookie).
|
|
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
|
-
/** Lỗi từ TikTok hoặc từ chính thư viện. `code` để bên dùng phân nhánh (vd phiên hết hạn → quét lại). */
|
|
2
|
-
export declare class TikTokError extends Error {
|
|
3
|
-
readonly code: TikTokErrorCode;
|
|
4
|
-
/** Mã gốc TikTok trả (status_code / error_code) nếu có. */
|
|
5
|
-
readonly upstream?: number | string | undefined;
|
|
6
|
-
constructor(code: TikTokErrorCode, message: string,
|
|
7
|
-
/** Mã gốc TikTok trả (status_code / error_code) nếu có. */
|
|
8
|
-
upstream?: number | string | undefined);
|
|
9
|
-
}
|
|
10
|
-
export type TikTokErrorCode =
|
|
11
|
-
/** Cookie không còn đăng nhập — phải lấy phiên mới. */
|
|
12
|
-
'SESSION_EXPIRED' | 'HTTP_ERROR'
|
|
13
|
-
/** IM trả `status_code` khác 0. */
|
|
14
|
-
| 'IM_ERROR' | 'UPLOAD_FAILED' | 'DOWNLOAD_FAILED' | 'BAD_RESPONSE'
|
|
15
|
-
/** Đăng nhập QR không thành (Chrome không mở được, QR hết hạn, không thấy cookie…). */
|
|
16
|
-
| 'LOGIN_FAILED'
|
|
17
|
-
/** Bên gọi huỷ đăng nhập QR (`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
|
-
/** Lỗi từ TikTok hoặc từ chính thư viện. `code` để bên dùng phân nhánh (vd phiên hết hạn → quét lại). */
|
|
5
|
-
class TikTokError extends Error {
|
|
6
|
-
code;
|
|
7
|
-
upstream;
|
|
8
|
-
constructor(code, message,
|
|
9
|
-
/** Mã gốc TikTok trả (status_code / error_code) nếu có. */
|
|
10
|
-
upstream) {
|
|
11
|
-
super(message);
|
|
12
|
-
this.code = code;
|
|
13
|
-
this.upstream = upstream;
|
|
14
|
-
this.name = 'TikTokError';
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
exports.TikTokError = TikTokError;
|