From f176b81e5b8ebb6b8f95fb5cb44fe41e9431e5a7 Mon Sep 17 00:00:00 2001 From: zlh-debug <3530766280@qq.com> Date: Wed, 13 Dec 2023 22:31:43 +0800 Subject: [PATCH] dddd --- lib/client.ts | 20 +- lib/core/base-client.ts | 747 ++++++++++++++++++++++++---------------- lib/core/device.ts | 197 +++++++++-- lib/core/tlv.ts | 49 ++- package.json | 2 +- 5 files changed, 689 insertions(+), 326 deletions(-) diff --git a/lib/client.ts b/lib/client.ts index a523d5f4..ce0b21ac 100644 --- a/lib/client.ts +++ b/lib/client.ts @@ -157,6 +157,7 @@ export class Client extends BaseClient { cache_group_member: true, reconn_interval: 5, data_dir: path.join(require?.main?.path || process.cwd(), "data"), + ver: "8.9.63" as Version, ...conf, } @@ -171,7 +172,7 @@ export class Client extends BaseClient { fs.writeFile(file, JSON.stringify(device, null, 2), NOOP) } - super(uin, config.platform, device) + super(uin, config.platform, config.ver, device) this.logger = log4js.getLogger(`[${this.apk.display}:${uin}]`) ;(this.logger as log4js.Logger).level = config.log_level @@ -737,11 +738,28 @@ export interface Config { ffprobe_path?: string /** 请求签名接口 */ sign_api_addr?: string + /** QQ协议版本 */ + ver: Version } /** 数据统计 */ export type Statistics = Client["stat"] +/** 协议版本 */ +export type Version = + "8.9.63" + | "8.9.68" + | "8.9.70" + | "8.9.71" + | "8.9.73" + | "8.9.75" + | "8.9.76" + | "8.9.78" + | "8.9.80" + | "8.9.83" + | "8.9.85" + | "8.9.88" + function createDataDir(dir: string, uin: number) { if (!fs.existsSync(dir)) fs.mkdirSync(dir, {mode: 0o755, recursive: true}) diff --git a/lib/core/base-client.ts b/lib/core/base-client.ts index 659375a7..c683fd1f 100644 --- a/lib/core/base-client.ts +++ b/lib/core/base-client.ts @@ -11,6 +11,7 @@ import * as pb from "./protobuf" import * as jce from "./jce" import {BUF0, BUF4, BUF16, NOOP, md5, timestamp, lock, hide, unzip, int32ip2str} from "./constants" import {ShortDevice, Device, generateFullDevice, Platform, Apk, getApkInfo} from "./device" +import {Version} from "../client"; const FN_NEXT_SEQ = Symbol("FN_NEXT_SEQ") const FN_SEND = Symbol("FN_SEND") @@ -33,6 +34,13 @@ export class ApiRejection { } } +type Packet = { + cmd: string + type: number + callbackId?: number + body: Buffer +} + export enum QrcodeResult { OtherError = 0, Timeout = 0x11, @@ -104,7 +112,9 @@ export class BaseClient extends EventEmitter { d2key: BUF0, t104: BUF0, t174: BUF0, + t546: BUF0, t547: BUF0, + t553: BUF0, qrsig: BUF0, /** 大数据上传通道 */ bigdata: { @@ -137,7 +147,7 @@ export class BaseClient extends EventEmitter { protected heartbeat = NOOP // 心跳定时器 private [HEARTBEAT]!: NodeJS.Timeout - private ssoPacketList: any = []; + private ssoPacketList: Packet[] = []; /** 数据统计 */ protected readonly statistics = { start_time: timestamp(), @@ -151,20 +161,18 @@ export class BaseClient extends EventEmitter { remote_ip: "", remote_port: 0, } - protected signLoginCmd = [ - 'wtlogin.login', - 'wtlogin.exchange_emp' - ] protected signCmd = [ + 'wtlogin.login', + 'wtlogin.exchange_emp', 'MessageSvc.PbSendMsg', 'trpc.o3.ecdh_access.EcdhAccess.SsoEstablishShareKey', 'trpc.o3.ecdh_access.EcdhAccess.SsoSecureA2Establish', 'trpc.o3.ecdh_access.EcdhAccess.SsoSecureA2Access' - ] + ]; - constructor(public readonly uin: number, p: Platform = Platform.Android, d?: ShortDevice) { + constructor(public readonly uin: number, p: Platform = Platform.Android, v: Version = "8.9.63", d?: ShortDevice) { super() - this.apk = getApkInfo(p) + this.apk = getApkInfo(p, v) this.device = generateFullDevice(d || uin) this.sig.ksid = Buffer.from(`|${this.device.imei}|${this.apk.name}`) this[NET].on("error", err => this.emit("internal.verbose", err.message, VerboseLevel.Error)) @@ -233,250 +241,312 @@ export class BaseClient extends EventEmitter { } /** 使用接收到的token登录 */ - tokenLogin(token: Buffer) { - if (![144, 152, 160].includes(token.length)) + async tokenLogin(token: Buffer = BUF0) { + if (![144, 152, 160, 0].includes(token.length)) throw new Error("bad token") - this.sig.session = randomBytes(4) - this.sig.randkey = randomBytes(16) - this[ECDH] = new Ecdh - this.sig.d2key = token.slice(0, 16) - if (token.length === 160) { - this.sig.d2 = token.slice(16, token.length - 80) - this.sig.tgt = token.slice(token.length - 80) - } else { - this.sig.d2 = token.slice(16, token.length - 72) - this.sig.tgt = token.slice(token.length - 72) + if (token.length) { + this.sig.session = randomBytes(4) + this.sig.randkey = randomBytes(16) + this[ECDH] = new Ecdh + this.sig.d2key = token.slice(0, 16) + if (token.length === 160) { + this.sig.d2 = token.slice(16, token.length - 80) + this.sig.tgt = token.slice(token.length - 80) + } else { + this.sig.d2 = token.slice(16, token.length - 72) + this.sig.tgt = token.slice(token.length - 72) + } + this.sig.tgtgt = md5(this.sig.d2key) } - this.sig.tgtgt = md5(this.sig.d2key) - const t = tlv.getPacker(this) - let tlv_count = 18 - const writer = new Writer() + const t = tlv.getPacker(this); + const tlvs = [ + t(0x8), + t(0x18), + t(0x100), + t(0x108), + t(0x10a), + t(0x112), + t(0x116), + t(0x141), + t(0x142), + t(0x143), + t(0x144), + t(0x145), + t(0x147), + t(0x154), + t(0x177), + t(0x187), + t(0x188), + t(0x511) + ]; + if (this.apk.ssover >= 5) { + tlvs.push(t(0x544, -1, await this.getT544("810_a"))); + if (this.sig.t553) tlvs.push(t(0x553)); + } + if (this.device.qImei16) tlvs.push(t(0x545, this.device.qImei16)) + else { + tlvs.push(t(0x194)); + tlvs.push(t(0x202)); + } + let writer = new Writer() .writeU16(11) - .writeU16(tlv_count) - .writeBytes(t(0x100)) - .writeBytes(t(0x10a)) - .writeBytes(t(0x116)) - .writeBytes(t(0x108)) - .writeBytes(t(0x144)) - .writeBytes(t(0x143)) - .writeBytes(t(0x142)) - .writeBytes(t(0x154)) - .writeBytes(t(0x18)) - .writeBytes(t(0x141)) - .writeBytes(t(0x8)) - .writeBytes(t(0x147)) - .writeBytes(t(0x177)) - .writeBytes(t(0x187)) - .writeBytes(t(0x188)) - .writeBytes(t(0x194)) - .writeBytes(t(0x511)) - .writeBytes(t(0x202)) - const body = writer.read() - this[FN_SEND_LOGIN]("wtlogin.exchange_emp", body) + .writeU16(tlvs.length); + for (let tlv of tlvs) writer.writeBytes(tlv); + const body = writer.read(); + if (token != BUF0) { + this[FN_SEND_LOGIN]("wtlogin.exchange_emp", body); + return BUF0; + } + return body; } /** T544接口 */ async getT544(cmd: string): Promise { - let t544 = BUF0 - if (this.sig.sign_addr) { - let body = { + let sign = BUF0; + if (this.sig.sign_addr && this.apk.qua) { + const time = Date.now(); + let qImei36 = this.device.qImei36 || this.device.qImei16; + let post_params = { ver: this.apk.ver, - uin: this.uin, + uin: this.uin || 0, data: cmd, + android_id: this.device.android_id, + qimei36: qImei36 || this.device.android_id, guid: this.device.guid.toString('hex'), version: this.apk.sdkver + }; + let url = new URL(this.sig.sign_addr); + let path = url.pathname; + if (path.substring(path.length - 1) === '/') { + path += 'energy'; + } else { + path = path.replace(/\/sign$/, '/energy'); } - let url = new URL(this.sig.sign_addr) - url.pathname = "/energy" - const {data} = await axios.get<{ code: number, msg: string, data: any }>(url.href, { - params: body, - timeout: 10000, - headers: { - 'User-Agent': `Dalvik/2.1.0 (Linux; U; Android ${this.device.version.release}; PCRT00 Build/N2G48H)`, - 'Content-Type': "application/x-www-form-urlencoded" - } - }) - this.emit("internal.verbose", `getT544 ${cmd} result: ${JSON.stringify(data)}`, VerboseLevel.Debug); - if (data.code == 0) { + url.pathname = path; + const data = await get.bind(this)(url.href, post_params); + this.emit("internal.verbose", `getT544 ${cmd} result(${Date.now() - time}ms): ${JSON.stringify(data)}`, VerboseLevel.Debug); + if (data.code === 0) { if (typeof (data.data) === 'string') { - t544 = Buffer.from(data.data, 'hex'); + sign = Buffer.from(data.data, 'hex'); } else if (typeof (data.data?.sign) === 'string') { - t544 = Buffer.from(data.data.sign, 'hex'); + sign = Buffer.from(data.data.sign, 'hex'); } - } else if (data.code == 1) { - if (data.msg.includes('Uin is not registered.')) { - if (await this.apiRegister()) { - return await this.getT544(cmd); + } else { + if (data.code === 1) { + if (data.msg.includes('Uin is not registered.')) { + if (await this.apiRegister.call(this)) { + return await this.getT544(cmd); + } } } - } else { - this.emit("internal.verbose", `签名api(energy)异常: ${cmd} result: ${JSON.stringify(data)}`, VerboseLevel.Error); + this.emit("internal.verbose", `签名api(energy)异常: ${cmd} result(${Date.now() - time}ms): ${JSON.stringify(data)}`, VerboseLevel.Error); } } - return t544 + return sign; } /** 签名接口 */ - async getSign(cmd: String, seq: number, body: Buffer): Promise { - let sign = BUF0; + async getSign(cmd: string, seq: number, body: Buffer): Promise { + let params = BUF0; if (!this.sig.sign_addr) { - return sign; + return params; } let qImei36 = this.device.qImei36 || this.device.qImei16; - if (qImei36 && this.apk.qua) { + if (this.apk.qua) { + const time = Date.now(); + let post_params = { + qua: this.apk.qua, + uin: this.uin || 0, + cmd: cmd, + seq: seq, + android_id: this.device.android_id, + qimei36: qImei36 || this.device.android_id, + buffer: body.toString('hex'), + guid: this.device.guid.toString('hex'), + }; let url = new URL(this.sig.sign_addr); - let post_params = `qua=${this.apk.qua}&uin=${this.uin}&cmd=${cmd}&seq=${seq}&buffer=${body.toString('hex')}`; - url.pathname = '/sign'; - const {data} = await axios.post(url.href, post_params, { - timeout: 10000, - headers: { - 'User-Agent': `Dalvik/2.1.0 (Linux; U; Android ${this.device.version.release}; PCRT00 Build/N2G48H)`, - 'Content-Type': "application/x-www-form-urlencoded" - } - }).catch(() => ({data: {code: -1}})); - this.emit("internal.verbose", `getSign ${cmd} result: ${JSON.stringify(data)}`, VerboseLevel.Debug); - if (data.code == 0) { + let path = url.pathname; + if (path.substring(path.length - 1) === '/') { + path += 'sign'; + } + url.pathname = path; + const data = await get.bind(this)(url.href, post_params, true); + this.emit("internal.verbose", `getSign ${cmd} result(${Date.now() - time}ms): ${JSON.stringify(data)}`, VerboseLevel.Debug); + if (data.code === 0) { const Data = data.data || {}; - sign = this.generateSignPacket(Data.sign, Data.token, Data.extra); + params = this.generateSignPacket(Data.sign, Data.token, Data.extra); let list = Data.ssoPacketList || Data.requestCallback || []; if (list.length < 1 && cmd.includes('wtlogin')) { this.requestToken().then(); } else { this.ssoPacketListHandler(list).then(); } - } else if (data.code == 1) { - if (data.msg.includes('Uin is not registered.')) { - if (await this.apiRegister()) { - return await this.getSign(cmd, seq, body); + } else { + if (data.code === 1) { + if (data.msg.includes('Uin is not registered.')) { + if (await this.apiRegister.call(this)) { + return await this.getSign(cmd, seq, body); + } } } - } else { - this.emit("internal.verbose", `签名api异常: ${cmd} result: ${JSON.stringify(data)}`, VerboseLevel.Error); + this.emit("internal.verbose", `签名api异常: ${cmd} result(${Date.now() - time}ms): ${JSON.stringify(data)}`, VerboseLevel.Error); } } - return sign; + return params; } /** 签名接口注册 */ async apiRegister() { let qImei36 = this.device.qImei36 || this.device.qImei16; + const time = Date.now(); let post_params = { - uin: this.uin, + uin: this.uin || 0, android_id: this.device.android_id, qimei36: qImei36, guid: this.device.guid.toString('hex') }; let url = new URL(this.sig.sign_addr); - url.pathname = '/register'; - const {data} = await axios.get(url.href, { - params: post_params, - timeout: 15000, - headers: { - 'User-Agent': `Dalvik/2.1.0 (Linux; U; Android ${this.device.version.release}; PCRT00 Build/N2G48H)`, - 'Content-Type': "application/x-www-form-urlencoded" - } - }).catch(() => ({data: {code: -1}})); - this.emit("internal.verbose", `register result: ${JSON.stringify(data)}`, VerboseLevel.Debug); + let path = url.pathname; + if (path.substring(path.length - 1) === '/') { + path += 'register'; + } else { + path = path.replace(/\/sign$/, '/register'); + } + url.pathname = path; + const data = await get.bind(this)(url.href, post_params); + this.emit("internal.verbose", `register result(${Date.now() - time}ms): ${JSON.stringify(data)}`, VerboseLevel.Debug); if (data.code == 0) { return true; - } else { - this.emit("internal.verbose", `签名api注册异常:result: ${JSON.stringify(data)}`, VerboseLevel.Error); } + ; + this.emit("internal.verbose", `签名api注册异常:result(${Date.now() - time}ms): ${JSON.stringify(data)}`, VerboseLevel.Error); return false; } generateSignPacket(sign: String, token: String, extra: String) { - let qImei36 = this.device.qImei36 || this.device.qImei16; let pb_data = { - 9: 1, - 12: qImei36, - 14: 0, - 16: this.uin, - 18: 0, - 19: 1, - 20: 1, - 21: 0, - 24: { - 1: Buffer.from(sign, 'hex'), - 2: Buffer.from(token, 'hex'), - 3: Buffer.from(extra, 'hex') - }, - 28: 3 - }; + 1: Buffer.from(sign, 'hex'), + 2: Buffer.from(token, 'hex'), + 3: Buffer.from(extra, 'hex') + } return Buffer.from(pb.encode(pb_data)); } + buildReserveFields(cmd: string, sec_info: any) { + let qImei36 = this.device.qImei36 || this.device.qImei16; + let reserveFields; + if (this.apk.ssover >= 20 && false) { + reserveFields = { + 9: 1, + 11: 2052, + 12: qImei36, + 14: 0, + 15: '', + 16: this.uin || '', + 18: 0, + 19: 1, + 20: 1, + 21: 32, + 24: sec_info, + 26: 100, + 28: 3 + }; + } else { + reserveFields = { + 9: 1, + 12: qImei36, + 14: 0, + 16: this.uin, + 18: 0, + 19: 1, + 20: 1, + 21: 0, + 24: sec_info, + 28: 3 + }; + } + return Buffer.from(pb.encode(reserveFields)); + } + async requestToken() { - if ((Date.now() - this.sig.requestTokenTime) >= (50 * 60 * 1000)) { + if ((Date.now() - this.sig.requestTokenTime) >= (60 * 60 * 1000)) { this.sig.requestTokenTime = Date.now(); let list = await this.requestSignToken(); await this.ssoPacketListHandler(list); } } - async requestSignToken() { + async requestSignToken(): Promise<[]> { if (!this.sig.sign_addr) { return []; } let qImei36 = this.device.qImei36 || this.device.qImei16; - let post_params = { - ver: this.apk.ver, - qua: this.apk.qua, - uin: this.uin, - androidId: this.device.android_id, - qimei36: qImei36, - guid: this.device.guid.toString('hex'), - }; - let url = new URL(this.sig.sign_addr); - url.pathname = '/request_token'; - const {data} = await axios.get(url.href, { - params: post_params, - timeout: 10000, - headers: { - 'User-Agent': `Dalvik/2.1.0 (Linux; U; Android ${this.device.version.release}; PCRT00 Build/N2G48H)`, - 'Content-Type': "application/x-www-form-urlencoded" + if (this.apk.qua) { + const time = Date.now(); + let post_params = { + uin: this.uin || 0, + android_id: this.device.android_id, + qimei36: qImei36 || this.device.android_id, + guid: this.device.guid.toString('hex'), + }; + let url = new URL(this.sig.sign_addr); + let path = url.pathname; + if (path.substring(path.length - 1) === '/') { + path += 'request_token'; + } else { + path = path.replace(/\/sign$/, '/request_token'); + } + url.pathname = path; + const data = await get.bind(this)(url.href, post_params); + this.emit("internal.verbose", `requestSignToken result(${Date.now() - time}ms): ${JSON.stringify(data)}`, VerboseLevel.Debug); + if (data.code === 0) { + let ssoPacketList = data.data?.ssoPacketList || data.data?.requestCallback || data.data; + if (!ssoPacketList || ssoPacketList.length < 1) return []; + return ssoPacketList; + } else if (data.code === 1) { + if (data.msg.includes('Uin is not registered.')) { + if (await this.apiRegister.call(this)) { + return await this.requestSignToken(); + } + } } - }).catch(() => ({data: {code: -1}})); - this.emit("internal.verbose", `requestSignToken result: ${JSON.stringify(data)}`, VerboseLevel.Debug); - if (data.code >= 0) { - let ssoPacketList = data.data?.ssoPacketList || data.data?.requestCallback || data.data; - if (!ssoPacketList || ssoPacketList.length < 1) return []; - return ssoPacketList; } return []; } async ssoPacketListHandler(list: any) { + let handle = (list: any) => { + let new_list: Packet[] = []; + for (let val of list) { + try { + let data = pb.decode(Buffer.from(val.body, 'hex')); + val.type = data[1].toString(); + } catch (err) { + } + new_list.push(val); + } + return new_list; + }; if (list === null && this.isOnline()) { if (this.ssoPacketList.length > 0) { list = this.ssoPacketList; this.ssoPacketList = []; } } - if (!list || list.length < 1) return; + if (!list || !list.length) return; if (!this.isOnline()) { - let handle = (list: any) => { - let new_list = []; - for (let val of list) { - try { - let data = pb.decode(Buffer.from(val.body, 'hex')); - val.type = data[1].toString(); - } catch (err) { - } - new_list.push(val); - } - return new_list; - }; list = handle(list); if (this.ssoPacketList.length > 0) { + let list1 = this.ssoPacketList; + this.ssoPacketList = []; for (let val of list) { - let ssoPacket: any = this.ssoPacketList.find((data: any) => { + let ssoPacket: any = list1.find((data: any) => { return data.cmd === val.cmd && data.type === val.type; }); if (ssoPacket) { ssoPacket.body = val.body; } else { - this.ssoPacketList.push(val); + list1.push(val); } } } else { @@ -487,12 +557,12 @@ export class BaseClient extends EventEmitter { for (let ssoPacket of list) { let cmd = ssoPacket.cmd; - let body = Buffer.from(ssoPacket.body, 'hex'); - let callbackId = ssoPacket.callbackId; + let body = Buffer.from(ssoPacket.body as unknown as string, 'hex'); + let callbackId = ssoPacket.callbackId as number; let payload = await this.sendUni(cmd, body); this.emit("internal.verbose", `sendUni ${cmd} result: ${payload.toString('hex')}`, VerboseLevel.Debug); if (callbackId > -1) { - await this.ssoPacketListHandler(await this.submitSsoPacket(cmd, callbackId, payload)); + await this.submitSsoPacket(cmd, callbackId, payload); } } } @@ -502,33 +572,34 @@ export class BaseClient extends EventEmitter { return []; } let qImei36 = this.device.qImei36 || this.device.qImei16; - let post_params = { - ver: this.apk.ver, - qua: this.apk.qua, - uin: this.uin, - cmd: cmd, - callbackId: callbackId, - callback_id: callbackId, - androidId: this.device.android_id, - qimei36: qImei36, - buffer: body.toString('hex'), - guid: this.device.guid.toString('hex'), - }; - let url = new URL(this.sig.sign_addr); - url.pathname = '/submit'; - const {data} = await axios.get(url.href, { - params: post_params, - timeout: 10000, - headers: { - 'User-Agent': `Dalvik/2.1.0 (Linux; U; Android ${this.device.version.release}; PCRT00 Build/N2G48H)`, - 'Content-Type': "application/x-www-form-urlencoded" + if (this.apk.qua) { + const time = Date.now(); + let post_params = { + ver: this.apk.ver, + qua: this.apk.qua, + uin: this.uin || 0, + cmd: cmd, + callback_id: callbackId, + android_id: this.device.android_id, + qimei36: qImei36 || this.device.android_id, + buffer: body.toString('hex'), + guid: this.device.guid.toString('hex'), + }; + let url = new URL(this.sig.sign_addr); + let path = url.pathname; + if (path.substring(path.length - 1) === '/') { + path += 'submit'; + } else { + path = path.replace(/\/sign$/, '/submit'); + } + url.pathname = path; + const data = await get.bind(this)(url.href, post_params); + this.emit("internal.verbose", `submitSsoPacket result(${Date.now() - time}ms): ${JSON.stringify(data)}`, VerboseLevel.Debug); + if (data.code === 0) { + let ssoPacketList = data.data?.ssoPacketList || data.data?.requestCallback || data.data; + if (!ssoPacketList || ssoPacketList.length < 1) return []; + return ssoPacketList; } - }).catch(() => ({data: {code: -1}})); - this.emit("internal.verbose", `submitSsoPacket result: ${JSON.stringify(data)}`, VerboseLevel.Debug); - if (data.code >= 0) { - let ssoPacketList = data.data?.ssoPacketList || data.data?.requestCallback || data.data; - if (!ssoPacketList || ssoPacketList.length < 1) return []; - return ssoPacketList; } return []; } @@ -543,54 +614,71 @@ export class BaseClient extends EventEmitter { this.sig.tgtgt = randomBytes(16) this[ECDH] = new Ecdh const t = tlv.getPacker(this) - let body = new Writer() + const tlvs = [ + t(0x1), + t(0x8), + t(0x18), + t(0x100), + t(0x106, md5pass), + t(0x107), + t(0x116), + t(0x141), + t(0x142), + t(0x144), + t(0x145), + t(0x147), + t(0x154), + t(0x177), + t(0x187), + t(0x188), + t(0x191), + t(0x511), + t(0x516), + t(0x521, 0), + t(0x525), + t(0x542), + t(0x548) + ] + if (this.apk.ssover >= 12) { + tlvs.push(t(0x544, -1, await this.getT544("810_9"))) + if (this.sig.t553) tlvs.push(t(0x553)) + } + if (this.device.qImei16) tlvs.push(t(0x545, this.device.qImei16)) + else { + tlvs.push(t(0x194)) + tlvs.push(t(0x202)) + } + let writer = new Writer() .writeU16(9) - .writeU16(25) - .writeBytes(t(0x18)) - .writeBytes(t(0x1)) - .writeBytes(t(0x106, md5pass)) - .writeBytes(t(0x116)) - .writeBytes(t(0x100)) - .writeBytes(t(0x107)) - .writeBytes(t(0x142)) - .writeBytes(t(0x144)) - .writeBytes(t(0x145)) - .writeBytes(t(0x147)) - .writeBytes(t(0x154)) - .writeBytes(t(0x141)) - .writeBytes(t(0x8)) - .writeBytes(t(0x511)) - .writeBytes(t(0x187)) - .writeBytes(t(0x188)) - .writeBytes(t(0x194)) - .writeBytes(t(0x191)) - .writeBytes(t(0x202)) - .writeBytes(t(0x177)) - .writeBytes(t(0x516)) - .writeBytes(t(0x521)) - .writeBytes(t(0x525)) - .writeBytes(t(0x544, -1, 9, await this.getT544("810_9"))) - .writeBytes(t(0x545)) - .read() - this[FN_SEND_LOGIN]("wtlogin.login", body) + .writeU16(tlvs.length) + for (let tlv of tlvs) writer.writeBytes(tlv) + this[FN_SEND_LOGIN]("wtlogin.login", writer.read()) } /** 收到滑动验证码后,用于提交滑动验证码 */ async submitSlider(ticket: string) { + try { + if (this.sig.t546.length) this.sig.t547 = this.calcPoW(this.sig.t546) + } catch { + } ticket = String(ticket).trim() const t = tlv.getPacker(this) - let tlv_count = this.sig.t547.length ? 6 : 5 - if (this.apk.ssover <= 12) tlv_count-- - const body = new Writer() + const tlvs = [ + t(0x8), + t(0x104), + t(0x116), + t(0x193, ticket), + ] + if (this.apk.ssover >= 12) { + tlvs.push(t(0x544, -1, await this.getT544("810_2"))) + if (this.sig.t553) tlvs.push(t(0x553)) + } + if (this.sig.t547.length) tlvs.push(t(0x547)) + let writer = new Writer() .writeU16(2) - .writeU16(tlv_count) - .writeBytes(t(0x193, ticket)) - .writeBytes(t(0x8)) - .writeBytes(t(0x104)) - .writeBytes(t(0x116)) - if (this.sig.t547.length) body.writeBytes(t(0x547)) - body.writeBytes(t(0x544, -1, 2, await this.getT544("810_2"))) - this[FN_SEND_LOGIN]("wtlogin.login", body.read()) + .writeU16(tlvs.length) + for (let tlv of tlvs) writer.writeBytes(tlv) + this[FN_SEND_LOGIN]("wtlogin.login", writer.read()) } /** 收到设备锁验证请求后,用于发短信 */ @@ -615,21 +703,25 @@ export class BaseClient extends EventEmitter { if (Buffer.byteLength(code) !== 6) code = "123456" const t = tlv.getPacker(this) - let tlv_count = 8 - if (this.apk.ssover <= 12) tlv_count-- - const writer = new Writer() + const tlvs = [ + t(0x8), + t(0x104), + t(0x116), + t(0x174), + t(0x17c, code), + t(0x198), + t(0x401) + ] + if (this.apk.ssover >= 12) { + tlvs.push(t(0x544, -1, await this.getT544("810_7"))) + if (this.sig.t553) tlvs.push(t(0x553)) + } + if (this.sig.t547.length) tlvs.push(t(0x547)) + let writer = new Writer() .writeU16(7) - .writeU16(tlv_count) - .writeBytes(t(0x8)) - .writeBytes(t(0x104)) - .writeBytes(t(0x116)) - .writeBytes(t(0x174)) - .writeBytes(t(0x17c, code)) - .writeBytes(t(0x401)) - .writeBytes(t(0x198)) - .writeBytes(t(0x544, -1, 7, await this.getT544("810_7"))) - .read() - this[FN_SEND_LOGIN]("wtlogin.login", writer) + .writeU16(tlvs.length) + for (let tlv of tlvs) writer.writeBytes(tlv) + this[FN_SEND_LOGIN]("wtlogin.login", writer.read()) } /** 获取登录二维码(模拟手表协议扫码登录) */ @@ -826,6 +918,72 @@ export class BaseClient extends EventEmitter { } } + calcPoW(data: any) { + if (!data || data.length === 0) return Buffer.alloc(0); + const stream = Readable.from(data, {objectMode: false}); + const version = stream.read(1).readUInt8(); + const typ = stream.read(1).readUInt8(); + const hashType = stream.read(1).readUInt8(); + let ok = stream.read(1).readUInt8() === 0; + const maxIndex = stream.read(2).readUInt16BE(); + const reserveBytes = stream.read(2); + const src = stream.read(stream.read(2).readUInt16BE()); + const tgt = stream.read(stream.read(2).readUInt16BE()); + const cpy = stream.read(stream.read(2).readUInt16BE()); + if (hashType !== 1) { + this.emit("internal.verbose", `Unsupported tlv546 hash type ${hashType}`, VerboseLevel.Warn); + return Buffer.alloc(0); + } + let inputNum = BigInt("0x" + src.toString("hex")); + switch (typ) { + case 1: + // TODO + // See https://github.com/mamoe/mirai/blob/cc7f35519ea7cc03518a57dc2ee90d024f63be0e/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLoginExt.kt#L207 + this.emit("internal.verbose", `Unsupported tlv546 algorithm type ${typ}`, VerboseLevel.Warn); + break; + case 2: + // Calc SHA256 + let dst; + let elp = 0, cnt = 0; + if (tgt.length === 32) { + const start = Date.now(); + let hash = createHash("sha256").update(Buffer.from(inputNum.toString(16).padStart(256, "0"), "hex")).digest(); + while (Buffer.compare(hash, tgt) !== 0) { + inputNum++; + hash = createHash("sha256").update(Buffer.from(inputNum.toString(16).padStart(256, "0"), "hex")).digest(); + cnt++; + if (cnt > 6000000) { + this.emit("internal.verbose", "Calculating PoW cost too much time, maybe something wrong", VerboseLevel.Error); + throw new Error("Calculating PoW cost too much time, maybe something wrong"); + } + } + ok = true; + dst = Buffer.from(inputNum.toString(16).padStart(256, "0"), "hex"); + elp = Date.now() - start; + this.emit("internal.verbose", `Calculating PoW of plus ${cnt} times cost ${elp} ms`, VerboseLevel.Debug); + } + if (!ok) return Buffer.alloc(0); + const body = new Writer() + .writeU8(version) + .writeU8(typ) + .writeU8(hashType) + .writeU8(ok ? 1 : 0) + .writeU16(maxIndex) + .writeBytes(reserveBytes) + .writeTlv(src) + .writeTlv(tgt) + .writeTlv(cpy); + if (dst) body.writeTlv(dst) + body.writeU32(elp) + .writeU32(cnt); + return body.read(); + default: + this.emit("internal.verbose", `Unsupported tlv546 algorithm type ${typ}`, VerboseLevel.Warn); + break; + } + return Buffer.alloc(0); + } + /** 发送一个业务包不等待返回 */ async writeUni(cmd: string, body: Uint8Array, seq = 0) { this.statistics.sent_pkt_cnt++ @@ -854,12 +1012,12 @@ async function buildUniPktSign(this: BaseClient, cmd: string, body: Uint8Array, function buildUniPkt(this: BaseClient, cmd: string, body: Uint8Array, seq = 0, BodySign: Buffer = BUF0) { seq = seq || this[FN_NEXT_SEQ]() this.emit("internal.verbose", `send:${cmd} seq:${seq}`, VerboseLevel.Debug) - let len = cmd.length + 20 + let len: number const sso = new Writer() .writeWithLength(new Writer() .writeWithLength(cmd) .writeWithLength(this.sig.session) - .writeWithLength(BodySign || BUF0) + .writeWithLength(this.buildReserveFields(cmd, BodySign)) .read()) .writeWithLength(body) .read(); @@ -890,8 +1048,11 @@ function ssoListener(this: BaseClient, cmd: string, payload: Buffer, seq: number } break case "QualityTest.PushList": + this.writeUni(cmd, BUF0, seq).then() + break case "OnlinePush.SidTicketExpired": this.writeUni(cmd, BUF0, seq).then() + refreshToken.call(this, true).then() break case "ConfigPushSvc.PushReq": { if (payload[0] === 0) @@ -1024,7 +1185,7 @@ async function register(this: BaseClient, logout = false, reflush = false) { const payload = await this[FN_SEND](pkt, 10, "StatSvc.register", seq) if (logout) return const rsp = jce.decodeWrapper(payload) - const result = rsp[9] ? true : false + const result = !!rsp[9] if (!result && !reflush) { this.emit("internal.error.token") } else { @@ -1039,7 +1200,7 @@ async function register(this: BaseClient, logout = false, reflush = false) { this.emit("internal.verbose", "heartbeat timeout x 2", VerboseLevel.Error) this[NET].destroy() }) - }).then(refreshToken.bind(this)) + }).then(refreshToken.bind(this, false)) }, this.interval * 1000) } } catch { @@ -1058,8 +1219,8 @@ async function syncTimeDiff(this: BaseClient) { }).catch(NOOP) } -async function refreshToken(this: BaseClient) { - if (!this.isOnline() || timestamp() - this.sig.emp_time < 14000) +async function refreshToken(this: BaseClient, force: boolean = false) { + if ((!this.isOnline() || timestamp() - this.sig.emp_time < 14000) && !force) return const t = tlv.getPacker(this) const body = new Writer() @@ -1126,7 +1287,7 @@ async function buildLoginPacket(this: BaseClient, cmd: LoginCmd, body: Buffer, t if (cmd === "wtlogin.trans_emp") { uin = 0 cmdid = 0x812 - subappid = getApkInfo(Platform.Watch).subid + // subappid = getApkInfo(Platform.Watch).subid } if (type === 2) { body = new Writer() @@ -1157,7 +1318,7 @@ async function buildLoginPacket(this: BaseClient, cmd: LoginCmd, body: Buffer, t } let BodySign = BUF0; - if (this.signLoginCmd.includes(cmd)) { + if (this.signCmd.includes(cmd)) { BodySign = await this.getSign(cmd, seq, Buffer.from(body)); } @@ -1174,7 +1335,7 @@ async function buildLoginPacket(this: BaseClient, cmd: LoginCmd, body: Buffer, t .writeU32(4) .writeU16(this.sig.ksid.length + 2) .writeBytes(this.sig.ksid) - .writeWithLength(BodySign) + .writeWithLength(this.buildReserveFields(cmd, BodySign)) .read() ) .writeWithLength(body) @@ -1219,52 +1380,6 @@ async function buildCode2dPacket(this: BaseClient, cmdid: number, head: number, return await buildLoginPacket.call(this, "wtlogin.trans_emp", body) } -function calcPoW(this: BaseClient, data: any) { - if (!data || data.length === 0) return Buffer.alloc(0); - const stream = Readable.from(data, {objectMode: false}); - const a = stream.read(1).readUInt8(); // a - const typ = stream.read(1).readUInt8(); // typ - const c = stream.read(1).readUInt8(); // c - let ok = stream.read(1).readUInt8() !== 0; // ok - const e = stream.read(2).readUInt16BE(); // e - const f = stream.read(2).readUInt16BE(); // f - const src = stream.read(stream.read(2).readUInt16BE()); // scr - const tgt = stream.read(stream.read(2).readUInt16BE()); // tgt - const cpy = stream.read(stream.read(2).readUInt16BE()); // cpy - - let dst = Buffer.alloc(0) - let elp = 0, cnt = 0 - if (typ === 2 && tgt.length === 32) { - let tmp = BigInt("0x" + src.toString("hex")); - const start = Date.now() - let hash = createHash("sha256").update(Buffer.from(tmp.toString(16), "hex")).digest() - while (Buffer.compare(hash, tgt)) { - tmp++ - hash = createHash("sha256").update(Buffer.from(tmp.toString(16), "hex")).digest() - cnt++ - } - ok = true - dst = Buffer.from(tmp.toString(16), "hex") - elp = Date.now() - start - } - const writer = new Writer() - .writeU8(a) - .writeU8(typ) - .writeU8(c) - .writeU8(ok ? 1 : 0) - .writeU16(e) - .writeU16(f) - .writeTlv(src) - .writeTlv(tgt) - .writeTlv(cpy) - if (ok) { - writer.writeTlv(dst) - .writeU32(elp) - .writeU32(cnt) - } - return writer.read() -} - function decodeT119(this: BaseClient, t119: Buffer) { const r = Readable.from(tea.decrypt(t119, this.sig.tgtgt), {objectMode: false}) r.read(2) @@ -1304,7 +1419,7 @@ function decodeLoginResponse(this: BaseClient, payload: Buffer): any { const type = r.read(1).readUInt8() as number r.read(2) const t = readTlv(r) - if (t[0x546]) this.sig.t547 = calcPoW.call(this, t[0x546]) + if (t[0x546]) this.sig.t546 = t[0x546] if (type === 204) { this.sig.t104 = t[0x104] this.emit("internal.verbose", "unlocking...", VerboseLevel.Mark) @@ -1354,6 +1469,14 @@ function decodeLoginResponse(this: BaseClient, payload: Buffer): any { return this.emit("internal.verify", t[0x204]?.toString() || "", phone) } + if (type === 235) { + return this.emit("internal.error.login", type, `[登陆失败](${type})当前设备信息被拉黑,建议删除package.json后重新登录!`) + } + + if (type === 237) { + return this.emit("internal.error.login", type, `[登陆失败](${type})当前QQ登录频繁,暂时被限制登录,建议更换QQ或稍后再尝试登录!`) + } + if (t[0x149]) { const stream = Readable.from(t[0x149], {objectMode: false}) stream.read(2) @@ -1371,4 +1494,28 @@ function decodeLoginResponse(this: BaseClient, payload: Buffer): any { } this.emit("internal.error.login", type, `[登陆失败]未知错误`) +} + +async function get(this: BaseClient, url: string, params: object = {}, post: boolean = false) { + const config: any = { + timeout: 30000, + headers: { + 'User-Agent': `Dalvik/2.1.0 (Linux; U; Android ${this.device.version.release}; PCRT00 Build/N2G48H)`, + 'Content-Type': "application/x-www-form-urlencoded" + } + }; + let data: any = {code: -1}; + let num: number = 0; + while (data.code == -1 && num < 3) { + if (num > 0) await new Promise((resolve) => setTimeout(resolve, 2000)); + num++; + if (post) { + data = await axios.post(url, params, config).catch(err => ({data: {code: -1, msg: err?.message}})); + } else { + config.params = params; + data = await axios.get(url, config).catch(err => ({data: {code: -1, msg: err?.message}})); + } + data = data.data; + } + return data; } \ No newline at end of file diff --git a/lib/core/device.ts b/lib/core/device.ts index 6b7b8056..67d7f158 100644 --- a/lib/core/device.ts +++ b/lib/core/device.ts @@ -3,6 +3,7 @@ import {createHash, randomBytes} from "crypto" import {formatDateTime, md5, randomString} from "./constants" import axios from "axios"; import {BaseClient, VerboseLevel} from "./base-client"; +import {Version} from "../client"; const secret = "ZdJqM15EeO2zWc08" const ws = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" @@ -108,26 +109,181 @@ export enum Platform { iPad = 5, } -export type Apk = typeof mobile +export type Apk = { + id: string + app_key: string + name: string + version: string + ver: string + sign: Buffer + buildtime: number + appid: number + subid: number + bitmap: number + main_sig_map: number + sub_sig_map: number + sdkver: string + display: string + qua: string + ssover: number +} //android const mobile = { id: "com.tencent.mobileqq", - app_key: '0S200MNJT807V3GE', - name: "A8.9.63.11390", - version: "8.9.63.11390", - ver: "8.9.63", - sign: Buffer.from([0xA6, 0xB7, 0x45, 0xBF, 0x24, 0xA2, 0xC2, 0x77, 0x52, 0x77, 0x16, 0xF6, 0xF3, 0x6E, 0xB6, 0x8D]), - buildtime: 1685069178, appid: 16, - subid: 537164840, - bitmap: 150470524, + app_key: '0S200MNJT807V3GE', + sign: Buffer.from('A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D'.split(' ').map(s => parseInt(s, 16))), main_sig_map: 16724722, - sub_sig_map: 0x10400, - sdkver: "6.0.0.2546", - display: "Android", - qua: 'V1_AND_SQ_8.9.63_4194_YYB_D', - ssover: 20, + sub_sig_map: 66560, + display: "Android" +} + +const mobileMap: { [key: string]: Apk } = { + "8.9.88": { + name: "A8.9.88.46a07457", + version: "8.9.88.13035", + ver: "8.9.88", + buildtime: 1697015435, + subid: 537182769, + bitmap: 150470524, + sdkver: "6.0.0.2556", + qua: 'V1_AND_SQ_8.9.88_4852_YYB_D', + ssover: 21, + ...mobile + }, + "8.9.85": { + name: "A8.9.85.3377f9bf", + version: "8.9.85.12820", + ver: "8.9.85", + buildtime: 1697015435, + subid: 537180568, + bitmap: 150470524, + sdkver: "6.0.0.2556", + qua: 'V1_AND_SQ_8.9.85_4766_YYB_D', + ssover: 21, + ...mobile + }, + "8.9.83": { + name: "A8.9.83.c9a61e5e", + version: "8.9.83.12605", + ver: "8.9.83", + buildtime: 1691565978, + subid: 537178646, + bitmap: 150470524, + sdkver: "6.0.0.2554", + qua: 'V1_AND_SQ_8.9.83_4680_YYB_D', + ssover: 20, + ...mobile + }, + "8.9.80": { + name: "A8.9.80.57a42f50", + version: "8.9.80.12440", + ver: "8.9.80", + buildtime: 1691565978, + subid: 537176863, + bitmap: 150470524, + sdkver: "6.0.0.2554", + qua: 'V1_AND_SQ_8.9.80_4614_YYB_D', + ssover: 20, + ...mobile + }, + "8.9.78": { + name: "A8.9.78.d5d9d71d", + version: "8.9.78.12275", + ver: "8.9.78", + buildtime: 1691565978, + subid: 537175315, + bitmap: 150470524, + sdkver: "6.0.0.2554", + qua: 'V1_AND_SQ_8.9.78_4548_YYB_D', + ssover: 20, + ...mobile + }, + "8.9.76": { + name: "A8.9.76.c71a1fa8", + version: "8.9.76.12115", + ver: "8.9.76", + buildtime: 1691565978, + subid: 537173477, + bitmap: 150470524, + sdkver: "6.0.0.2554", + qua: 'V1_AND_SQ_8.9.76_4484_YYB_D', + ssover: 20, + ...mobile + }, + "8.9.75": { + name: "A8.9.75.354d41fc", + version: "8.9.75.12110", + ver: "8.9.75", + buildtime: 1691565978, + subid: 537173381, + bitmap: 150470524, + sdkver: "6.0.0.2554", + qua: 'V1_AND_SQ_8.9.75_4482_YYB_D', + ssover: 20, + ...mobile + }, + "8.9.73": { + name: "A8.9.73.11945", + version: "8.9.73.11945", + ver: "8.9.73", + buildtime: 1690371091, + subid: 537171689, + bitmap: 150470524, + sdkver: "6.0.0.2553", + qua: 'V1_AND_SQ_8.9.73_4416_YYB_D', + ssover: 20, + ...mobile + }, + "8.9.71": { + name: "A8.9.71.9fd08ae5", + version: "8.9.71.11735", + ver: "8.9.71", + buildtime: 1688720082, + subid: 537170024, + bitmap: 150470524, + sdkver: "6.0.0.2551", + qua: 'V1_AND_SQ_8.9.71_4332_YYB_D', + ssover: 20, + ...mobile + }, + "8.9.70": { + name: "A8.9.70.b4332bd3", + version: "8.9.70.11730", + ver: "8.9.70", + buildtime: 1688720082, + subid: 537169928, + bitmap: 150470524, + sdkver: "6.0.0.2551", + qua: 'V1_AND_SQ_8.9.70_4330_YYB_D', + ssover: 20, + ...mobile + }, + "8.9.68": { + name: "A8.9.68.e757227e", + version: "8.9.68.11565", + ver: "8.9.68", + buildtime: 1687254022, + subid: 537168313, + bitmap: 150470524, + sdkver: "6.0.0.2549", + qua: 'V1_AND_SQ_8.9.68_4264_YYB_D', + ssover: 20, + ...mobile + }, + "8.9.63": { + name: "A8.9.63.5156de84", + version: "8.9.63.11390", + ver: "8.9.63", + buildtime: 1685069178, + subid: 537164840, + bitmap: 150470524, + sdkver: "6.0.0.2546", + qua: 'V1_AND_SQ_8.9.63_4194_YYB_D', + ssover: 20, + ...mobile + } } //watch @@ -170,10 +326,10 @@ const hd: Apk = { ssover: 12 } -const apklist: { [platform in Platform]: Apk } = { - [Platform.Android]: mobile, +const apklist: { [platform in Platform]: Apk | { [key: string]: Apk } } = { + [Platform.Android]: mobileMap, [Platform.aPad]: { - ...mobile, + ...mobileMap["8.9.63"], subid: 537152242, display: 'aPad' }, @@ -187,11 +343,12 @@ const apklist: { [platform in Platform]: Apk } = { ver: '8.9.33', ssover: 19, display: 'iPad' - }, + } } -export function getApkInfo(p: Platform): Apk { - return apklist[p] || apklist[Platform.Android] +export function getApkInfo(p: Platform, version: Version = "8.9.63"): Apk { + if (p === Platform.Android) return (apklist[p] as { [key: string]: Apk })[version] + return apklist[p] as Apk || (apklist[Platform.Android] as { [key: string]: Apk })["8.9.63"] } export async function requestQImei(this: BaseClient) { diff --git a/lib/core/tlv.ts b/lib/core/tlv.ts index c3d3358a..b7583fcb 100644 --- a/lib/core/tlv.ts +++ b/lib/core/tlv.ts @@ -148,6 +148,9 @@ const map: { [tag: number]: (this: BaseClient, ...args: any[]) => Writer } = { 0x10a: function () { return new Writer().writeBytes(this.sig.tgt) }, + 0x112: function () { + return new Writer().writeTlv(String(this.uin)); + }, 0x116: function () { return new Writer() .writeU8(0) @@ -206,13 +209,15 @@ const map: { [tag: number]: (this: BaseClient, ...args: any[]) => Writer } = { }, 0x147: function () { return new Writer() - .writeU32(this.apk.appid) - .writeTlv(this.apk.version) + .writeU32(this.apk.appid).writeTlv(this.apk.version) .writeTlv(this.apk.sign) }, 0x154: function () { return new Writer().writeU32(this.sig.seq + 1) }, + 0x16a: function (srm_token) { + return new Writer().writeBytes(srm_token) + }, 0x16e: function () { return new Writer().writeBytes(this.device.model) }, @@ -316,6 +321,10 @@ const map: { [tag: number]: (this: BaseClient, ...args: any[]) => Writer } = { .writeU16(0x536) // tag .writeTlv(Buffer.from([0x1, 0x0])) // zero }, + 0x523: function () { + return new Writer() + .writeTlv(Buffer.from([0x1, 0x0])) + }, 0x52d: function () { const d = this.device const buf = pb.encode({ @@ -331,8 +340,11 @@ const map: { [tag: number]: (this: BaseClient, ...args: any[]) => Writer } = { }) return new Writer().writeBytes(buf) }, - 0x544: function (v: number, subCmd: number, signDate: Buffer) { - if (v === -1) { + 0x542: function () { + return new Writer().writeBytes(Buffer.from([0x4A, 0x02, 0x60, 0x01])); + }, + 0x544: function (v: number, subCmd: number, signDate?: Buffer) { + if (signDate) { return new Writer().writeBytes(signDate) } const salt = new Writer() @@ -358,6 +370,35 @@ const map: { [tag: number]: (this: BaseClient, ...args: any[]) => Writer } = { }, 0x547: function () { return new Writer().writeBytes(this.sig.t547); + }, + 0x548: function () { + // copy from https://github.com/Icalingua-plus-plus/oicq-icalingua-plus-plus/blob/master/lib/wtlogin/tlv.js + const src = crypto.randomBytes(128); + while (src[0] === 0 || src[0] === 255) src[0] = crypto.randomBytes(1)[0]; + const srcNum = BigInt('0x' + src.toString("hex")); + const cnt = 10000; + const dstNum = srcNum + BigInt(cnt); + const dst = Buffer.from(dstNum.toString(16).padStart(256, "0"), "hex"); + const tgt = crypto.createHash("sha256").update(dst).digest(); + const writer = new Writer() + .writeU8(1) //version + .writeU8(2) //typ + .writeU8(1) //hashType + .writeU8(2) //ok + .writeU16(10) //maxIndex + .writeBytes(Buffer.from([0, 0])) //reserveBytes + .writeTlv(src) + .writeTlv(tgt) + const cpy = writer.read(); + const t546 = writer + .writeBytes(cpy) + .writeTlv(cpy) + .read(); + const t548 = this.calcPoW(t546); + return new Writer().writeBytes(t548); + }, + 0x553: function () { + return new Writer().writeBytes(this.sig.t553); } } diff --git a/package.json b/package.json index 4b26bf73..e86611b6 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "main": "lib/index.js", "types": "lib/index.d.ts", "scripts": { - "test": "tsc && node ./bin/oicq" + "test": "tsc && node test.js" }, "engines": { "node": ">= v14"