tca-js 0.0.0-stage → 0.3.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/LICENSE +21 -0
- package/README.md +132 -2
- package/dist/api.d.ts +75 -0
- package/dist/api.js +277 -0
- package/dist/constants.d.ts +86 -0
- package/dist/constants.js +77 -0
- package/dist/context.d.ts +32 -0
- package/dist/context.js +129 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.js +17 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +54 -0
- package/dist/listener.d.ts +43 -0
- package/dist/listener.js +181 -0
- package/dist/lz4.d.ts +5 -0
- package/dist/lz4.js +57 -0
- package/dist/media.d.ts +27 -0
- package/dist/media.js +152 -0
- package/dist/message.d.ts +6 -0
- package/dist/message.js +113 -0
- package/dist/proto.d.ts +43 -0
- package/dist/proto.js +162 -0
- package/dist/qr-login.d.ts +41 -0
- package/dist/qr-login.js +212 -0
- package/dist/types.d.ts +109 -0
- package/dist/types.js +2 -0
- package/package.json +26 -4
package/dist/media.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Context } from './context';
|
|
2
|
+
import type { ImageInput, TikTokImage } from './types';
|
|
3
|
+
/** Ảnh đã tải lên + được kho mã hoá — đủ để gửi tin ảnh. */
|
|
4
|
+
export interface UploadedImage {
|
|
5
|
+
uri: string;
|
|
6
|
+
secretKey: string;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
size: number;
|
|
10
|
+
filename: string;
|
|
11
|
+
}
|
|
12
|
+
export declare class MediaClient {
|
|
13
|
+
private readonly ctx;
|
|
14
|
+
private cred;
|
|
15
|
+
constructor(ctx: Context);
|
|
16
|
+
/** `UploadConfig` trả hai bộ khoá: loại 1 = ImageX (ảnh), loại 2 = VOD (video). Lấy loại 1. */
|
|
17
|
+
private credential;
|
|
18
|
+
/** Gọi ImageX ký AWS SigV4 (vùng cn-north-1, dịch vụ imagex). */
|
|
19
|
+
private imagex;
|
|
20
|
+
/**
|
|
21
|
+
* Tải ảnh lên kho riêng tư của tin nhắn: Apply → POST file → Commit (kho tự mã hoá AES-GCM
|
|
22
|
+
* và trả khoá). Kích thước ảnh lấy từ kết quả Commit — không cần tự đọc ảnh.
|
|
23
|
+
*/
|
|
24
|
+
upload(image: ImageInput): Promise<UploadedImage>;
|
|
25
|
+
/** Tải ảnh trong tin nhắn về và giải mã. Bố cục: IV 12 byte ‖ bản mã ‖ tag 16 byte. */
|
|
26
|
+
download(image: TikTokImage, which?: 'origin' | 'thumb'): Promise<Buffer>;
|
|
27
|
+
}
|
package/dist/media.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MediaClient = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const node_zlib_1 = require("node:zlib");
|
|
6
|
+
const constants_1 = require("./constants");
|
|
7
|
+
const errors_1 = require("./errors");
|
|
8
|
+
const sha256 = (s) => (0, node_crypto_1.createHash)('sha256').update(s).digest('hex');
|
|
9
|
+
const hmac = (k, s) => (0, node_crypto_1.createHmac)('sha256', k).update(s).digest();
|
|
10
|
+
const rfc3986 = (s) => encodeURIComponent(s).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
11
|
+
class MediaClient {
|
|
12
|
+
ctx;
|
|
13
|
+
cred = null;
|
|
14
|
+
constructor(ctx) {
|
|
15
|
+
this.ctx = ctx;
|
|
16
|
+
}
|
|
17
|
+
/** `UploadConfig` trả hai bộ khoá: loại 1 = ImageX (ảnh), loại 2 = VOD (video). Lấy loại 1. */
|
|
18
|
+
async credential() {
|
|
19
|
+
if (this.cred && this.cred.expiresAt - Date.now() > 60_000)
|
|
20
|
+
return this.cred;
|
|
21
|
+
const body = await this.ctx.im('/v1/media/upload_config', constants_1.Cmd.UploadConfig, Buffer.alloc(0));
|
|
22
|
+
const c = body.msgs(1).find((m) => m.num(1) === 1);
|
|
23
|
+
if (!c)
|
|
24
|
+
throw new errors_1.TikTokError('UPLOAD_FAILED', 'upload_config không có khoá ImageX');
|
|
25
|
+
this.cred = {
|
|
26
|
+
serviceId: c.str(2),
|
|
27
|
+
host: c.str(3),
|
|
28
|
+
expiresAt: c.num(4),
|
|
29
|
+
accessKeyId: c.str(6),
|
|
30
|
+
sessionToken: c.str(7),
|
|
31
|
+
secretAccessKey: c.str(8),
|
|
32
|
+
};
|
|
33
|
+
return this.cred;
|
|
34
|
+
}
|
|
35
|
+
/** Gọi ImageX ký AWS SigV4 (vùng cn-north-1, dịch vụ imagex). */
|
|
36
|
+
async imagex(method, params, body) {
|
|
37
|
+
const c = await this.credential();
|
|
38
|
+
const amzDate = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d+/, '');
|
|
39
|
+
const date = amzDate.slice(0, 8);
|
|
40
|
+
const query = Object.keys(params)
|
|
41
|
+
.sort()
|
|
42
|
+
.map((k) => `${rfc3986(k)}=${rfc3986(params[k])}`)
|
|
43
|
+
.join('&');
|
|
44
|
+
const headers = { 'x-amz-date': amzDate, 'x-amz-security-token': c.sessionToken };
|
|
45
|
+
if (body !== undefined)
|
|
46
|
+
headers['x-amz-content-sha256'] = sha256(body);
|
|
47
|
+
const signed = Object.keys(headers).sort();
|
|
48
|
+
const canonical = [
|
|
49
|
+
method,
|
|
50
|
+
'/',
|
|
51
|
+
query,
|
|
52
|
+
signed.map((k) => `${k}:${headers[k]}\n`).join(''),
|
|
53
|
+
signed.join(';'),
|
|
54
|
+
body !== undefined ? headers['x-amz-content-sha256'] : sha256(''),
|
|
55
|
+
].join('\n');
|
|
56
|
+
const scope = `${date}/${constants_1.IMAGEX_REGION}/${constants_1.IMAGEX_SERVICE}/aws4_request`;
|
|
57
|
+
const toSign = ['AWS4-HMAC-SHA256', amzDate, scope, sha256(canonical)].join('\n');
|
|
58
|
+
let k = hmac(`AWS4${c.secretAccessKey}`, date);
|
|
59
|
+
k = hmac(k, constants_1.IMAGEX_REGION);
|
|
60
|
+
k = hmac(k, constants_1.IMAGEX_SERVICE);
|
|
61
|
+
k = hmac(k, 'aws4_request');
|
|
62
|
+
const signature = (0, node_crypto_1.createHmac)('sha256', k).update(toSign).digest('hex');
|
|
63
|
+
const res = await fetch(`https://${c.host}/?${query}`, {
|
|
64
|
+
method,
|
|
65
|
+
headers: {
|
|
66
|
+
...headers,
|
|
67
|
+
Authorization: `AWS4-HMAC-SHA256 Credential=${c.accessKeyId}/${scope}, SignedHeaders=${signed.join(';')}, Signature=${signature}`,
|
|
68
|
+
...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
|
|
69
|
+
},
|
|
70
|
+
body,
|
|
71
|
+
signal: AbortSignal.timeout(this.ctx.options.timeoutMs ?? 20_000),
|
|
72
|
+
});
|
|
73
|
+
const json = (await res.json().catch(() => null));
|
|
74
|
+
const err = json?.ResponseMetadata?.Error;
|
|
75
|
+
if (!res.ok || !json || err) {
|
|
76
|
+
throw new errors_1.TikTokError('UPLOAD_FAILED', `imagex ${params.Action}: ${err?.Message ?? `HTTP ${res.status}`}`, err?.CodeN ?? res.status);
|
|
77
|
+
}
|
|
78
|
+
return json;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Tải ảnh lên kho riêng tư của tin nhắn: Apply → POST file → Commit (kho tự mã hoá AES-GCM
|
|
82
|
+
* và trả khoá). Kích thước ảnh lấy từ kết quả Commit — không cần tự đọc ảnh.
|
|
83
|
+
*/
|
|
84
|
+
async upload(image) {
|
|
85
|
+
const c = await this.credential();
|
|
86
|
+
const ext = (/\.([a-z0-9]{2,5})$/i.exec(image.filename)?.[1] ?? 'jpg').toLowerCase();
|
|
87
|
+
const apply = await this.imagex('GET', {
|
|
88
|
+
Action: 'ApplyImageUpload',
|
|
89
|
+
Version: constants_1.IMAGEX_VERSION,
|
|
90
|
+
ServiceId: c.serviceId,
|
|
91
|
+
FileSize: String(image.data.length),
|
|
92
|
+
FileExtension: `.${ext}`,
|
|
93
|
+
s: (0, node_crypto_1.randomBytes)(6).toString('hex').slice(0, 10),
|
|
94
|
+
});
|
|
95
|
+
const addr = apply.Result.UploadAddress;
|
|
96
|
+
const store = addr.StoreInfos[0];
|
|
97
|
+
const host = addr.UploadHosts[0];
|
|
98
|
+
if (!store || !host)
|
|
99
|
+
throw new errors_1.TikTokError('UPLOAD_FAILED', 'ApplyImageUpload không trả chỗ tải lên');
|
|
100
|
+
const up = await fetch(`https://${host}/upload/v1/${store.StoreUri}`, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: {
|
|
103
|
+
Authorization: store.Auth,
|
|
104
|
+
'Content-CRC32': ((0, node_zlib_1.crc32)(image.data) >>> 0).toString(16).padStart(8, '0'),
|
|
105
|
+
// Thiếu header này kho trả 4004 Not Found.
|
|
106
|
+
'X-Storage-U': this.ctx.ownId,
|
|
107
|
+
'Content-Type': 'application/octet-stream',
|
|
108
|
+
'Content-Disposition': 'attachment; filename="undefined"',
|
|
109
|
+
},
|
|
110
|
+
body: new Uint8Array(image.data),
|
|
111
|
+
signal: AbortSignal.timeout(this.ctx.options.timeoutMs ?? 60_000),
|
|
112
|
+
});
|
|
113
|
+
const upJson = (await up.json().catch(() => null));
|
|
114
|
+
if (!up.ok || upJson?.code !== 2000) {
|
|
115
|
+
throw new errors_1.TikTokError('UPLOAD_FAILED', `upload: ${upJson?.message ?? `HTTP ${up.status}`}`, upJson?.code ?? up.status);
|
|
116
|
+
}
|
|
117
|
+
const commit = await this.imagex('POST', { Action: 'CommitImageUpload', Version: constants_1.IMAGEX_VERSION, ServiceId: c.serviceId }, JSON.stringify({ SessionKey: addr.SessionKey, Functions: [{ name: 'Encryption', input: { Config: { copies: 'cipher_v2' } } }] }));
|
|
118
|
+
const enc = commit.Result.Results?.[0]?.Encryption;
|
|
119
|
+
const plugin = commit.Result.PluginResult?.[0];
|
|
120
|
+
if (!enc?.Uri || !enc.SecretKey)
|
|
121
|
+
throw new errors_1.TikTokError('UPLOAD_FAILED', 'CommitImageUpload không trả bản mã hoá');
|
|
122
|
+
return {
|
|
123
|
+
uri: enc.Uri,
|
|
124
|
+
secretKey: enc.SecretKey,
|
|
125
|
+
width: plugin?.ImageWidth ?? Number(enc.Extra?.img_width ?? 0),
|
|
126
|
+
height: plugin?.ImageHeight ?? Number(enc.Extra?.img_height ?? 0),
|
|
127
|
+
size: plugin?.ImageSize ?? image.data.length,
|
|
128
|
+
filename: image.filename,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/** Tải ảnh trong tin nhắn về và giải mã. Bố cục: IV 12 byte ‖ bản mã ‖ tag 16 byte. */
|
|
132
|
+
async download(image, which = 'origin') {
|
|
133
|
+
const url = which === 'thumb' && image.thumbUrl ? image.thumbUrl : image.url;
|
|
134
|
+
if (!url)
|
|
135
|
+
throw new errors_1.TikTokError('DOWNLOAD_FAILED', 'Ảnh không có link');
|
|
136
|
+
const res = await this.ctx.fetch(url);
|
|
137
|
+
if (!res.ok)
|
|
138
|
+
throw new errors_1.TikTokError('DOWNLOAD_FAILED', `Tải ảnh: HTTP ${res.status}`, res.status);
|
|
139
|
+
const enc = Buffer.from(await res.arrayBuffer());
|
|
140
|
+
if (!image.decryptKey)
|
|
141
|
+
return enc;
|
|
142
|
+
try {
|
|
143
|
+
const d = (0, node_crypto_1.createDecipheriv)('aes-256-gcm', Buffer.from(image.decryptKey, 'hex'), enc.subarray(0, 12));
|
|
144
|
+
d.setAuthTag(enc.subarray(enc.length - 16));
|
|
145
|
+
return Buffer.concat([d.update(enc.subarray(12, enc.length - 16)), d.final()]);
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
throw new errors_1.TikTokError('DOWNLOAD_FAILED', `Giải mã ảnh: ${err instanceof Error ? err.message : String(err)}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
exports.MediaClient = MediaClient;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Msg } from './proto';
|
|
2
|
+
import type { TikTokConversation, TikTokMessage } from './types';
|
|
3
|
+
/** `MessageBody` → `TikTokMessage`. */
|
|
4
|
+
export declare function parseMessage(m: Msg, ownId: string): TikTokMessage;
|
|
5
|
+
/** `ConversationInfo` (trường 2 của get_by_user_init): {1 id, 2 shortId, 3 loại, 6 {1 {1 uid}}…}. */
|
|
6
|
+
export declare function parseConversation(m: Msg, ownId: string): TikTokConversation;
|
package/dist/message.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseMessage = parseMessage;
|
|
4
|
+
exports.parseConversation = parseConversation;
|
|
5
|
+
const constants_1 = require("./constants");
|
|
6
|
+
function parseJson(s) {
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(s);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return s;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Ảnh trong `attachments` (trường 20 → 4): 1 = bản gốc, 2 = ảnh nhỏ; mỗi cái {1 uri, 2 url…, 5 {w,h}, 6 khoá}. */
|
|
15
|
+
function parseImage(m) {
|
|
16
|
+
const media = m.msg(constants_1.MsgF.attachments)?.msg(4);
|
|
17
|
+
const origin = media?.msg(1);
|
|
18
|
+
if (!media || !origin)
|
|
19
|
+
return null;
|
|
20
|
+
const size = origin.msg(5);
|
|
21
|
+
return {
|
|
22
|
+
uri: origin.str(1),
|
|
23
|
+
url: origin.strs(2)[0] ?? '',
|
|
24
|
+
thumbUrl: media.msg(2)?.strs(2)[0] ?? '',
|
|
25
|
+
width: size?.num(1) ?? 0,
|
|
26
|
+
height: size?.num(2) ?? 0,
|
|
27
|
+
decryptKey: origin.str(6),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Sticker (trường 20 → 7): `{1 {1 uri, 2 url, 5 {w,h}}, 2 lời nhắc, 200 {1 {2 map a:sticker_id, a:sticker_type…}}}`.
|
|
32
|
+
* Link trỏ CDN sticker công khai (`…-dm-sticker-sign-…`) — không mã hoá, không cần cookie.
|
|
33
|
+
*/
|
|
34
|
+
function parseSticker(m) {
|
|
35
|
+
const st = m.msg(constants_1.MsgF.attachments)?.msg(7);
|
|
36
|
+
const img = st?.msg(1);
|
|
37
|
+
if (!st || !img)
|
|
38
|
+
return null;
|
|
39
|
+
const size = img.msg(5);
|
|
40
|
+
const ext = st.msg(200)?.msg(1)?.map(2) ?? {};
|
|
41
|
+
return {
|
|
42
|
+
stickerId: ext['a:sticker_id'] ?? '',
|
|
43
|
+
stickerType: Number(ext['a:sticker_type'] ?? 0),
|
|
44
|
+
url: img.str(2),
|
|
45
|
+
width: size?.num(1) ?? 0,
|
|
46
|
+
height: size?.num(2) ?? 0,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Reaction (trường 15, `property_list`): mỗi phần tử `{1 khoá, 2 {1 PropertyItem[]}}`, khoá `e:<emoji>`,
|
|
51
|
+
* `PropertyItem {1 uid, 3 giây, 4 idempotent_id, 6 µs}`. App điện thoại thả ❤️ ghi cả `e:❤️` lẫn `e:love` → gộp.
|
|
52
|
+
*/
|
|
53
|
+
function parseReactions(m) {
|
|
54
|
+
const byEmoji = new Map();
|
|
55
|
+
for (const p of m.msgs(constants_1.MsgF.propertyList)) {
|
|
56
|
+
const key = p.str(1);
|
|
57
|
+
if (!key.startsWith(constants_1.REACTION_PREFIX))
|
|
58
|
+
continue;
|
|
59
|
+
const emoji = key === constants_1.REACTION_LOVE_KEY ? constants_1.REACTION_HEART : key.slice(constants_1.REACTION_PREFIX.length);
|
|
60
|
+
const uids = byEmoji.get(emoji) ?? new Set();
|
|
61
|
+
for (const item of p.msg(2)?.msgs(1) ?? [])
|
|
62
|
+
uids.add(item.id(1));
|
|
63
|
+
if (uids.size)
|
|
64
|
+
byEmoji.set(emoji, uids);
|
|
65
|
+
}
|
|
66
|
+
return [...byEmoji].map(([emoji, uids]) => ({ emoji, userIds: [...uids] }));
|
|
67
|
+
}
|
|
68
|
+
const KIND = {
|
|
69
|
+
[constants_1.MessageType.Text]: 'text',
|
|
70
|
+
[constants_1.MessageType.PrivateImage]: 'image',
|
|
71
|
+
[constants_1.MessageType.Sticker]: 'sticker',
|
|
72
|
+
[constants_1.MessageType.ShareVideo]: 'video',
|
|
73
|
+
[constants_1.MessageType.System]: 'system',
|
|
74
|
+
[constants_1.MessageType.Command]: 'command',
|
|
75
|
+
};
|
|
76
|
+
/** `MessageBody` → `TikTokMessage`. */
|
|
77
|
+
function parseMessage(m, ownId) {
|
|
78
|
+
const type = m.num(constants_1.MsgF.type);
|
|
79
|
+
const content = parseJson(m.str(constants_1.MsgF.content));
|
|
80
|
+
const ext = m.map(constants_1.MsgF.ext);
|
|
81
|
+
const senderId = m.id(constants_1.MsgF.sender);
|
|
82
|
+
const text = type === constants_1.MessageType.Text && content && typeof content === 'object' && 'text' in content
|
|
83
|
+
? String(content.text ?? '')
|
|
84
|
+
: '';
|
|
85
|
+
return {
|
|
86
|
+
conversationId: m.str(constants_1.MsgF.conversationId),
|
|
87
|
+
conversationShortId: m.id(constants_1.MsgF.conversationShortId),
|
|
88
|
+
serverMessageId: m.id(constants_1.MsgF.serverMessageId),
|
|
89
|
+
index: m.id(constants_1.MsgF.indexInConversation),
|
|
90
|
+
type,
|
|
91
|
+
kind: KIND[type] ?? 'other',
|
|
92
|
+
senderId,
|
|
93
|
+
isSelf: senderId === ownId,
|
|
94
|
+
text,
|
|
95
|
+
image: type === constants_1.MessageType.PrivateImage ? parseImage(m) : null,
|
|
96
|
+
sticker: type === constants_1.MessageType.Sticker ? parseSticker(m) : null,
|
|
97
|
+
reactions: parseReactions(m),
|
|
98
|
+
content,
|
|
99
|
+
createdAt: m.num(constants_1.MsgF.createTime),
|
|
100
|
+
clientMessageId: ext['s:client_message_id'] ?? '',
|
|
101
|
+
ext,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** `ConversationInfo` (trường 2 của get_by_user_init): {1 id, 2 shortId, 3 loại, 6 {1 {1 uid}}…}. */
|
|
105
|
+
function parseConversation(m, ownId) {
|
|
106
|
+
const participants = (m.msg(6)?.msgs(1) ?? []).map((p) => p.id(1));
|
|
107
|
+
return {
|
|
108
|
+
conversationId: m.str(1),
|
|
109
|
+
conversationShortId: m.id(2),
|
|
110
|
+
participants,
|
|
111
|
+
peerId: participants.find((p) => p !== ownId) ?? '',
|
|
112
|
+
};
|
|
113
|
+
}
|
package/dist/proto.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protobuf tối thiểu, không cần file `.proto`.
|
|
3
|
+
*
|
|
4
|
+
* TikTok không công bố `.proto` của IM — số trường ở `constants.ts` dựng lại từ byte bắt được
|
|
5
|
+
* trên trình duyệt (09/11/2026). Chỉ cần ba kiểu dây: varint (0), độ dài (2), và bỏ qua 1/5.
|
|
6
|
+
*/
|
|
7
|
+
export interface Field {
|
|
8
|
+
n: number;
|
|
9
|
+
wire: number;
|
|
10
|
+
int: bigint;
|
|
11
|
+
bytes: Buffer;
|
|
12
|
+
}
|
|
13
|
+
/** Tách một message thành danh sách trường. `null` nếu byte không phải protobuf hợp lệ. */
|
|
14
|
+
export declare function decodeFields(b: Buffer): Field[] | null;
|
|
15
|
+
/** Message đã giải — đọc theo số trường. */
|
|
16
|
+
export declare class Msg {
|
|
17
|
+
readonly fields: Field[];
|
|
18
|
+
private constructor();
|
|
19
|
+
static parse(b: Buffer): Msg;
|
|
20
|
+
static tryParse(b: Buffer | undefined): Msg | null;
|
|
21
|
+
has(n: number): boolean;
|
|
22
|
+
int(n: number): bigint;
|
|
23
|
+
num(n: number): number;
|
|
24
|
+
/** Số nguyên 64-bit dạng chuỗi — uid / mã tin vượt `Number.MAX_SAFE_INTEGER`. */
|
|
25
|
+
id(n: number): string;
|
|
26
|
+
bytes(n: number): Buffer;
|
|
27
|
+
str(n: number): string;
|
|
28
|
+
strs(n: number): string[];
|
|
29
|
+
msg(n: number): Msg | null;
|
|
30
|
+
msgs(n: number): Msg[];
|
|
31
|
+
/** `map<string,string>` — mỗi phần tử là message `{1: key, 2: value}`. */
|
|
32
|
+
map(n: number): Record<string, string>;
|
|
33
|
+
}
|
|
34
|
+
/** Ghép message theo thứ tự gọi. */
|
|
35
|
+
export declare class Writer {
|
|
36
|
+
private readonly parts;
|
|
37
|
+
int(n: number, v: bigint | number | string): this;
|
|
38
|
+
bytes(n: number, v: Buffer | string): this;
|
|
39
|
+
msg(n: number, w: Writer): this;
|
|
40
|
+
/** `map<string,string>` theo đúng thứ tự khoá của object. */
|
|
41
|
+
map(n: number, obj: Record<string, string>): this;
|
|
42
|
+
finish(): Buffer;
|
|
43
|
+
}
|
package/dist/proto.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Protobuf tối thiểu, không cần file `.proto`.
|
|
4
|
+
*
|
|
5
|
+
* TikTok không công bố `.proto` của IM — số trường ở `constants.ts` dựng lại từ byte bắt được
|
|
6
|
+
* trên trình duyệt (09/11/2026). Chỉ cần ba kiểu dây: varint (0), độ dài (2), và bỏ qua 1/5.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.Writer = exports.Msg = void 0;
|
|
10
|
+
exports.decodeFields = decodeFields;
|
|
11
|
+
function readVarint(b, i) {
|
|
12
|
+
let x = 0n;
|
|
13
|
+
let shift = 0n;
|
|
14
|
+
let c;
|
|
15
|
+
do {
|
|
16
|
+
if (i >= b.length)
|
|
17
|
+
throw new Error('varint vượt cuối buffer');
|
|
18
|
+
c = b[i++];
|
|
19
|
+
x |= BigInt(c & 127) << shift;
|
|
20
|
+
shift += 7n;
|
|
21
|
+
} while (c & 128);
|
|
22
|
+
return [x, i];
|
|
23
|
+
}
|
|
24
|
+
/** Tách một message thành danh sách trường. `null` nếu byte không phải protobuf hợp lệ. */
|
|
25
|
+
function decodeFields(b) {
|
|
26
|
+
const out = [];
|
|
27
|
+
let i = 0;
|
|
28
|
+
try {
|
|
29
|
+
while (i < b.length) {
|
|
30
|
+
const [key, j] = readVarint(b, i);
|
|
31
|
+
i = j;
|
|
32
|
+
const wire = Number(key & 7n);
|
|
33
|
+
const n = Number(key >> 3n);
|
|
34
|
+
if (n === 0)
|
|
35
|
+
return null;
|
|
36
|
+
if (wire === 0) {
|
|
37
|
+
const [v, k] = readVarint(b, i);
|
|
38
|
+
out.push({ n, wire, int: v, bytes: Buffer.alloc(0) });
|
|
39
|
+
i = k;
|
|
40
|
+
}
|
|
41
|
+
else if (wire === 2) {
|
|
42
|
+
const [len, k] = readVarint(b, i);
|
|
43
|
+
const end = k + Number(len);
|
|
44
|
+
if (end > b.length)
|
|
45
|
+
return null;
|
|
46
|
+
out.push({ n, wire, int: 0n, bytes: b.subarray(k, end) });
|
|
47
|
+
i = end;
|
|
48
|
+
}
|
|
49
|
+
else if (wire === 1 || wire === 5) {
|
|
50
|
+
const size = wire === 1 ? 8 : 4;
|
|
51
|
+
if (i + size > b.length)
|
|
52
|
+
return null;
|
|
53
|
+
out.push({ n, wire, int: 0n, bytes: b.subarray(i, i + size) });
|
|
54
|
+
i += size;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
/** Message đã giải — đọc theo số trường. */
|
|
67
|
+
class Msg {
|
|
68
|
+
fields;
|
|
69
|
+
constructor(fields) {
|
|
70
|
+
this.fields = fields;
|
|
71
|
+
}
|
|
72
|
+
static parse(b) {
|
|
73
|
+
const f = decodeFields(b);
|
|
74
|
+
if (!f)
|
|
75
|
+
throw new Error('Không giải được protobuf');
|
|
76
|
+
return new Msg(f);
|
|
77
|
+
}
|
|
78
|
+
static tryParse(b) {
|
|
79
|
+
if (!b)
|
|
80
|
+
return null;
|
|
81
|
+
const f = decodeFields(b);
|
|
82
|
+
return f ? new Msg(f) : null;
|
|
83
|
+
}
|
|
84
|
+
has(n) {
|
|
85
|
+
return this.fields.some((f) => f.n === n);
|
|
86
|
+
}
|
|
87
|
+
int(n) {
|
|
88
|
+
return this.fields.find((f) => f.n === n)?.int ?? 0n;
|
|
89
|
+
}
|
|
90
|
+
num(n) {
|
|
91
|
+
return Number(this.int(n));
|
|
92
|
+
}
|
|
93
|
+
/** Số nguyên 64-bit dạng chuỗi — uid / mã tin vượt `Number.MAX_SAFE_INTEGER`. */
|
|
94
|
+
id(n) {
|
|
95
|
+
return this.int(n).toString();
|
|
96
|
+
}
|
|
97
|
+
bytes(n) {
|
|
98
|
+
return this.fields.find((f) => f.n === n)?.bytes ?? Buffer.alloc(0);
|
|
99
|
+
}
|
|
100
|
+
str(n) {
|
|
101
|
+
return this.bytes(n).toString('utf8');
|
|
102
|
+
}
|
|
103
|
+
strs(n) {
|
|
104
|
+
return this.fields.filter((f) => f.n === n).map((f) => f.bytes.toString('utf8'));
|
|
105
|
+
}
|
|
106
|
+
msg(n) {
|
|
107
|
+
return Msg.tryParse(this.fields.find((f) => f.n === n)?.bytes);
|
|
108
|
+
}
|
|
109
|
+
msgs(n) {
|
|
110
|
+
return this.fields
|
|
111
|
+
.filter((f) => f.n === n)
|
|
112
|
+
.map((f) => Msg.tryParse(f.bytes))
|
|
113
|
+
.filter((m) => m !== null);
|
|
114
|
+
}
|
|
115
|
+
/** `map<string,string>` — mỗi phần tử là message `{1: key, 2: value}`. */
|
|
116
|
+
map(n) {
|
|
117
|
+
const out = {};
|
|
118
|
+
for (const m of this.msgs(n))
|
|
119
|
+
out[m.str(1)] = m.str(2);
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
exports.Msg = Msg;
|
|
124
|
+
function varint(x) {
|
|
125
|
+
let v = BigInt(x);
|
|
126
|
+
if (v < 0n)
|
|
127
|
+
v = BigInt.asUintN(64, v);
|
|
128
|
+
const out = [];
|
|
129
|
+
do {
|
|
130
|
+
const c = Number(v & 127n);
|
|
131
|
+
v >>= 7n;
|
|
132
|
+
out.push(v ? c | 128 : c);
|
|
133
|
+
} while (v);
|
|
134
|
+
return Buffer.from(out);
|
|
135
|
+
}
|
|
136
|
+
const key = (n, wire) => varint((BigInt(n) << 3n) | BigInt(wire));
|
|
137
|
+
/** Ghép message theo thứ tự gọi. */
|
|
138
|
+
class Writer {
|
|
139
|
+
parts = [];
|
|
140
|
+
int(n, v) {
|
|
141
|
+
this.parts.push(key(n, 0), varint(typeof v === 'string' ? BigInt(v) : v));
|
|
142
|
+
return this;
|
|
143
|
+
}
|
|
144
|
+
bytes(n, v) {
|
|
145
|
+
const b = typeof v === 'string' ? Buffer.from(v, 'utf8') : v;
|
|
146
|
+
this.parts.push(key(n, 2), varint(b.length), b);
|
|
147
|
+
return this;
|
|
148
|
+
}
|
|
149
|
+
msg(n, w) {
|
|
150
|
+
return this.bytes(n, w.finish());
|
|
151
|
+
}
|
|
152
|
+
/** `map<string,string>` theo đúng thứ tự khoá của object. */
|
|
153
|
+
map(n, obj) {
|
|
154
|
+
for (const [k, v] of Object.entries(obj))
|
|
155
|
+
this.msg(n, new Writer().bytes(1, k).bytes(2, v));
|
|
156
|
+
return this;
|
|
157
|
+
}
|
|
158
|
+
finish() {
|
|
159
|
+
return Buffer.concat(this.parts);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
exports.Writer = Writer;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Credentials } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Đăng nhập bằng QR qua Chrome headless.
|
|
4
|
+
*
|
|
5
|
+
* Vì sao cần trình duyệt: API QR của TikTok (`passport/web/check_qrconnect`) đòi chữ ký JS
|
|
6
|
+
* (X-Gnarly / msToken) — gọi trần từ Node bị `error_code 7`. Nên mở đúng trang
|
|
7
|
+
* `tiktok.com/login/qrcode` trong Chrome headless để chính trang tự ký, còn mình chỉ NGHE
|
|
8
|
+
* phản hồi qua CDP: ảnh QR lấy từ body `get_qrcode`, trạng thái từ body `check_qrconnect`.
|
|
9
|
+
* Đăng nhập xong lấy cookie rồi tắt Chrome — mọi việc sau đó (`login`, chat) không cần trình duyệt.
|
|
10
|
+
*/
|
|
11
|
+
export type QrLoginEvent =
|
|
12
|
+
/** Ảnh QR mới (`data:image/png;base64,…`) — hiện cho người dùng quét. Hết hạn thì có ảnh mới. */
|
|
13
|
+
{
|
|
14
|
+
type: 'qr';
|
|
15
|
+
image: string;
|
|
16
|
+
expiresAt: number;
|
|
17
|
+
}
|
|
18
|
+
/** Đã quét, chờ bấm xác nhận trên điện thoại. */
|
|
19
|
+
| {
|
|
20
|
+
type: 'scanned';
|
|
21
|
+
}
|
|
22
|
+
/** Đã xác nhận — đang lấy cookie. */
|
|
23
|
+
| {
|
|
24
|
+
type: 'confirmed';
|
|
25
|
+
};
|
|
26
|
+
export interface QrLoginOptions {
|
|
27
|
+
/** Đường dẫn Chrome / Chromium (vd `/usr/bin/chromium-browser`). */
|
|
28
|
+
chromePath: string;
|
|
29
|
+
onEvent?: (e: QrLoginEvent) => void;
|
|
30
|
+
signal?: AbortSignal;
|
|
31
|
+
/** Tổng thời gian chờ quét (ms) — mặc định 5 phút. */
|
|
32
|
+
timeoutMs?: number;
|
|
33
|
+
/** QR hết hạn (~1 phút) thì tạo lại tối đa chừng này lần. */
|
|
34
|
+
maxRefresh?: number;
|
|
35
|
+
userAgent?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Đăng nhập TikTok bằng QR. Trả `Credentials` (cookie `.tiktok.com` + UA) — đưa vào
|
|
39
|
+
* `new TikTok().login(creds)` và LƯU LẠI để lần sau khỏi quét.
|
|
40
|
+
*/
|
|
41
|
+
export declare function loginQR(opts: QrLoginOptions): Promise<Credentials>;
|