icqq
Version:
QQ protocol for NodeJS!
726 lines (725 loc) • 24.5 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Friend = exports.User = void 0;
const crypto_1 = require("crypto");
const stream_1 = require("stream");
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const core_1 = require("./core");
const errors_1 = require("./errors");
const common_1 = require("./common");
const message_1 = require("./message");
const internal_1 = require("./internal");
const protobuf_1 = require("./core/protobuf");
const weakmap = new WeakMap();
/** 用户 */
class User extends internal_1.Contactable {
/** `this.uid`的别名 */
get user_id() {
return this.uid;
}
static as(uid) {
return new User(this, Number(uid));
}
constructor(c, uid) {
super(c);
this.uid = uid;
(0, common_1.lock)(this, "uid");
}
/** 返回作为好友的实例 */
asFriend(strict = false) {
return this.c.pickFriend(this.uid, strict);
}
/** 返回作为某群群员的实例 */
asMember(gid, strict = false) {
return this.c.pickMember(gid, this.uid, strict);
}
/**
* 获取头像url
* @param size 头像大小,默认`0`
* @returns 头像的url地址
*/
getAvatarUrl(size = 0) {
return `https://q1.qlogo.cn/g?b=qq&s=${size}&nk=` + this.uid;
}
async getAddFriendSetting() {
const FS = core_1.jce.encodeStruct([this.c.uin, this.uid, 3004, 0, null, 1]);
const body = core_1.jce.encodeWrapper({ FS }, "mqq.IMService.FriendListServiceServantObj", "GetUserAddFriendSettingReq");
const payload = await this.c.sendUni("friendlist.getUserAddFriendSetting", body);
return core_1.jce.decodeWrapper(payload)[2];
}
/**
* 点赞,支持陌生人点赞
* @param times 点赞次数,默认1次
*/
async thumbUp(times = 1) {
if (times > 20)
times = 20;
let ReqFavorite;
if (this.c.fl.get(this.uid)) {
ReqFavorite = core_1.jce.encodeStruct([
core_1.jce.encodeNested([
this.c.uin,
1,
this.c.sig.seq + 1,
1,
0,
Buffer.from("0C180001060131160131", "hex"),
]),
this.uid,
0,
1,
Number(times),
]);
}
else {
ReqFavorite = core_1.jce.encodeStruct([
core_1.jce.encodeNested([
this.c.uin,
1,
this.c.sig.seq + 1,
1,
0,
Buffer.from("0C180001060131160135", "hex"),
]),
this.uid,
0,
5,
Number(times),
]);
}
const body = core_1.jce.encodeWrapper({ ReqFavorite }, "VisitorSvc", "ReqFavorite", this.c.sig.seq + 1);
const payload = await this.c.sendUni("VisitorSvc.ReqFavorite", body);
return core_1.jce.decodeWrapper(payload)[0][3] === 0;
}
/** 查看资料 */
async getSimpleInfo() {
const arr = [null, 0, "", [this.uid], 1, 1, 0, 0, 0, 1, 0, 1];
arr[101] = 1;
const req = core_1.jce.encodeStruct(arr);
const body = core_1.jce.encodeWrapper({ req }, "KQQ.ProfileService.ProfileServantObj", "GetSimpleInfo");
const payload = await this.c.sendUni("ProfileService.GetSimpleInfo", body);
const nested = core_1.jce.decodeWrapper(payload);
for (let v of nested) {
return {
/** 账号 */
user_id: v[1],
/** 昵称 */
nickname: (v[5] || ""),
/** 性别 */
sex: (v[3] ? (v[3] === -1 ? "unknown" : "female") : "male"),
/** 年龄 */
age: (v[4] || 0),
/** 地区 */
area: (v[13] + " " + v[14] + " " + v[15]).trim(),
};
}
(0, errors_1.drop)(errors_1.ErrorCode.UserNotExists);
}
/**
* 获取`time`往前的`cnt`条聊天记录
* @param time 默认当前时间,为时间戳的分钟数(`Date.now() / 1000`)
* @param cnt 聊天记录条数,默认`20`,超过`20`按`20`处理
* @returns 私聊消息列表,服务器记录不足`cnt`条则返回能获取到的最多消息记录
*/
async getChatHistory(time = (0, common_1.timestamp)(), cnt = 20) {
if (cnt > 20)
cnt = 20;
const body = core_1.pb.encode({
1: this.uid,
2: Number(time),
3: 0,
4: Number(cnt),
});
const payload = await this.c.sendUni("MessageSvc.PbGetOneDayRoamMsg", body);
const obj = core_1.pb.decode(payload), messages = [];
if (obj[1] > 0 || !obj[6])
return messages;
!Array.isArray(obj[6]) && (obj[6] = [obj[6]]);
for (const proto of obj[6]) {
try {
messages.push(new message_1.PrivateMessage(proto, this.c.uin));
}
catch { }
}
return messages;
}
/**
* 标记`time`之前为已读
* @param time 默认当前时间,为时间戳的分钟数(`Date.now() / 1000`)
*/
async markRead(time = (0, common_1.timestamp)()) {
const body = core_1.pb.encode({
3: {
2: {
1: this.uid,
2: Number(time),
},
},
});
await this.c.sendUni("PbMessageSvc.PbMsgReadedReport", body);
}
async recallMsg(param, rand = 0, time = 0) {
if (param instanceof message_1.PrivateMessage)
var { seq, rand, time } = param;
else if (typeof param === "string")
var { seq, rand, time } = (0, message_1.parseDmMessageId)(param);
else
var seq = param;
const body = core_1.pb.encode({
1: [
{
1: [
{
1: this.c.uin,
2: this.uid,
3: Number(seq),
4: (0, message_1.rand2uuid)(Number(rand)),
5: Number(time),
6: Number(rand),
},
],
2: 0,
3: {
1: this.c.fl.has(this.uid) || this.c.sl.has(this.uid) ? 0 : 1,
},
4: 1,
},
],
});
const payload = await this.c.sendUni("PbMessageSvc.PbMsgWithDraw", body);
return core_1.pb.decode(payload)[1][1] <= 2;
}
_getRouting(file = false) {
if (Reflect.has(this, "gid"))
return {
3: {
1: (0, common_1.code2uin)(Reflect.get(this, "gid")),
2: this.uid,
},
};
return file ? { 15: { 1: this.uid, 2: 4 } } : { 1: { 1: this.uid } };
}
/**
* 发送一条消息
* @param content 消息内容
* @param source 引用回复的消息
*/
async sendMsg(content, source) {
const { rich, brief } = await this._preprocess(content, source);
return this._sendMsg({ 1: rich }, brief);
}
async _sendMsg(proto3, brief, file = false) {
const seq = this.c.sig.seq + 1;
const rand = (0, crypto_1.randomBytes)(4).readUInt32BE();
const body = core_1.pb.encode({
1: this._getRouting(file),
2: common_1.PB_CONTENT,
3: proto3,
4: seq,
5: rand,
6: (0, internal_1.buildSyncCookie)(this.c.sig.session.readUInt32BE()),
});
const payload = await this.c.sendUni("MessageSvc.PbSendMsg", body);
const rsp = core_1.pb.decode(payload);
if (rsp[1] !== 0 || rsp[14] === 0) {
this.c.logger.error(`failed to send: [Private: ${this.uid}] ${rsp[2]}(${rsp[1]})`);
(0, errors_1.drop)(rsp[1] || -70, rsp[2] || '私聊消息发送失败,可能被风控');
}
this.c.logger.info(`succeed to send: [Private(${this.uid})] ` + brief);
this.c.stat.sent_msg_cnt++;
const time = rsp[3];
const message_id = (0, message_1.genDmMessageId)(this.uid, seq, rand, time, 1);
const messageRet = { message_id, seq, rand, time };
this.c.emit("send", messageRet);
return messageRet;
}
/**
* 回添双向好友
* @param seq 申请消息序号
* @param remark 好友备注
*/
async addFriendBack(seq, remark = "") {
const body = core_1.pb.encode({
1: 1,
2: Number(seq),
3: this.uid,
4: 10,
5: 2004,
6: 1,
7: 0,
8: {
1: 2,
52: String(remark),
},
});
const payload = await this.c.sendUni("ProfileService.Pb.ReqSystemMsgAction.Friend", body);
return core_1.pb.decode(payload)[1][1] === 0;
}
/**
* 处理好友申请
* @param seq 申请消息序号
* @param yes 是否同意
* @param remark 好友备注
* @param block 是否屏蔽来自此用户的申请
*/
async setFriendReq(seq, yes = true, remark = "", block = false) {
const body = core_1.pb.encode({
1: 1,
2: Number(seq),
3: this.uid,
4: 1,
5: 6,
6: 7,
8: {
1: yes ? 2 : 3,
52: String(remark),
53: block ? 1 : 0,
},
});
const payload = await this.c.sendUni("ProfileService.Pb.ReqSystemMsgAction.Friend", body);
return core_1.pb.decode(payload)[1][1] === 0;
}
/**
* 处理入群申请
* @param gid 群号
* @param seq 申请消息序号
* @param yes 是否同意
* @param reason 若拒绝,拒绝的原因
* @param block 是否屏蔽来自此用户的申请
*/
async setGroupReq(gid, seq, yes = true, reason = "", block = false) {
const body = core_1.pb.encode({
1: 1,
2: Number(seq),
3: this.uid,
4: 1,
5: 3,
6: 31,
7: 1,
8: {
1: yes ? 11 : 12,
2: Number(gid),
50: String(reason),
53: block ? 1 : 0,
},
});
const payload = await this.c.sendUni("ProfileService.Pb.ReqSystemMsgAction.Group", body);
return core_1.pb.decode(payload)[1][1] === 0;
}
/**
* 处理群邀请
* @param gid 群号
* @param seq 申请消息序号
* @param yes 是否同意
* @param block 是否屏蔽来自此群的邀请
*/
async setGroupInvite(gid, seq, yes = true, block = false) {
const body = core_1.pb.encode({
1: 1,
2: Number(seq),
3: this.uid,
4: 1,
5: 3,
6: 10016,
7: 2,
8: {
1: yes ? 11 : 12,
2: Number(gid),
53: block ? 1 : 0,
},
});
const payload = await this.c.sendUni("ProfileService.Pb.ReqSystemMsgAction.Group", body);
return core_1.pb.decode(payload)[1][1] === 0;
}
/**
* 获取文件信息
* @param fid 文件id
*/
async getFileInfo(fid) {
const body = core_1.pb.encode({
1: 1200,
14: {
10: this.c.uin,
20: fid,
30: 2,
},
101: 3,
102: 104,
99999: { 1: 90200 },
});
const payload = await this.c.sendUni("OfflineFilleHandleSvr.pb_ftn_CMD_REQ_APPLY_DOWNLOAD-1200", body);
const rsp = core_1.pb.decode(payload)[14];
if (rsp[10] !== 0)
(0, errors_1.drop)(errors_1.ErrorCode.OfflineFileNotExists, rsp[20]);
const obj = rsp[30];
let url = String(obj[50]);
if (!url.startsWith("http"))
url = `http://${obj[30]}:${obj[40]}` + url;
return {
name: String(rsp[40][7]),
fid: String(rsp[40][6]),
md5: rsp[40][100].toHex(),
size: rsp[40][3],
duration: rsp[40][4],
url,
};
}
/**
* 获取离线文件下载地址
* @param fid 文件id
*/
async getFileUrl(fid) {
return (await this.getFileInfo(fid)).url;
}
}
exports.User = User;
/** 好友 */
class Friend extends User {
static as(uid, strict = false) {
const info = this.fl.get(uid);
if (strict && !info)
throw new Error(uid + `不是你的好友`);
let friend = weakmap.get(info);
if (friend)
return friend;
friend = new Friend(this, Number(uid), info);
if (info)
weakmap.set(info, friend);
return friend;
}
/** 好友资料 */
get info() {
return this._info;
}
/** 昵称 */
get nickname() {
return this.info?.nickname;
}
/** 性别 */
get sex() {
return this.info?.sex;
}
/** 备注 */
get remark() {
return this.info?.remark;
}
/** 分组id */
get class_id() {
return this.info?.class_id;
}
/** 分组名 */
get class_name() {
return this.c.classes.get(this.info?.class_id);
}
constructor(c, uid, _info) {
super(c, uid);
this._info = _info;
(0, common_1.hide)(this, "_info");
}
/** 设置备注 */
async setRemark(remark) {
const req = core_1.jce.encodeStruct([this.uid, String(remark || "")]);
const body = core_1.jce.encodeWrapper({ req }, "KQQ.ProfileService.ProfileServantObj", "ChangeFriendName");
await this.c.sendUni("ProfileService.ChangeFriendName", body);
}
/** 设置分组(注意:如果分组id不存在也会成功) */
async setClass(id) {
const buf = Buffer.alloc(10);
(buf[0] = 1), (buf[2] = 5);
buf.writeUInt32BE(this.uid, 3);
buf[7] = Number(id);
const MovGroupMemReq = core_1.jce.encodeStruct([this.c.uin, 0, buf]);
const body = core_1.jce.encodeWrapper({ MovGroupMemReq }, "mqq.IMService.FriendListServiceServantObj", "MovGroupMemReq");
await this.c.sendUni("friendlist.MovGroupMemReq", body);
}
/** 戳一戳 */
async poke(self = false) {
const body = core_1.pb.encode({
1: self ? this.c.uin : this.uid,
5: this.uid,
});
const payload = await this.c.sendOidb("OidbSvc.0xed3", body);
return core_1.pb.decode(payload)[3] === 0;
}
/**
* 删除好友
* @param block 屏蔽此好友的申请,默认为`true`
*/
async delete(block = true) {
const DF = core_1.jce.encodeStruct([this.c.uin, this.uid, 2, block ? 1 : 0]);
const body = core_1.jce.encodeWrapper({ DF }, "mqq.IMService.FriendListServiceServantObj", "DelFriendReq");
const payload = await this.c.sendUni("friendlist.delFriend", body);
this.c.sl.delete(this.uid);
return core_1.jce.decodeWrapper(payload)[2] === 0;
}
/**
* 发送离线文件
* @param file `string`表示从该本地文件路径获取,`Buffer`表示直接发送这段内容
* @param filename 对方看到的文件名,`file`为`Buffer`时,若留空则自动以md5命名
* @param callback 监控上传进度的回调函数,拥有一个"百分比进度"的参数
* @returns 文件id(撤回时使用)
*/
async sendFile(file, filename, callback) {
let filesize, filemd5, filesha, filestream;
if (file instanceof Uint8Array) {
if (!Buffer.isBuffer(file))
file = Buffer.from(file);
filesize = file.length;
(filemd5 = (0, common_1.md5)(file)), (filesha = (0, common_1.sha)(file));
filename = filename ? String(filename) : "file" + filemd5.toString("hex");
filestream = stream_1.Readable.from(file, { objectMode: false, highWaterMark: 524288 });
}
else {
file = String(file);
filesize = (await fs_1.default.promises.stat(file)).size;
[filemd5, filesha] = await (0, common_1.fileHash)(file);
filename = filename ? String(filename) : path_1.default.basename(file);
filestream = fs_1.default.createReadStream(file, { highWaterMark: 524288 });
}
const body1700 = core_1.pb.encode({
1: 1700,
2: 6,
19: {
10: this.c.uin,
20: this.uid,
30: filesize,
40: filename,
50: filemd5,
60: filesha,
70: "/storage/emulated/0/Android/data/com.tencent.mobileqq/Tencent/QQfile_recv/" +
filename,
80: 0,
90: 0,
100: 0,
110: filemd5,
},
101: 3,
102: 104,
200: 1,
});
const payload = await this.c.sendUni("OfflineFilleHandleSvr.pb_ftn_CMD_REQ_APPLY_UPLOAD_V3-1700", body1700);
const rsp1700 = core_1.pb.decode(payload)[19];
if (rsp1700[10] !== 0)
(0, errors_1.drop)(rsp1700[10], rsp1700[20]);
const fid = rsp1700[90].toBuffer();
if (!rsp1700[110]) {
const ext = core_1.pb.encode({
1: 100,
2: 2,
100: {
100: {
1: 3,
100: this.c.uin,
200: this.uid,
400: 0,
700: payload,
},
200: {
100: filesize,
200: filemd5,
300: filesha,
400: filemd5,
600: fid,
700: rsp1700[220].toBuffer(),
},
300: {
100: 2,
200: String(this.c.apk.subid),
300: 2,
400: "d92615c5",
600: 4,
},
400: {
100: filename,
},
},
200: 1,
});
await internal_1.highwayHttpUpload.call(this.c, filestream, {
md5: filemd5,
size: filesize,
cmdid: internal_1.CmdID.OfflineFile,
ext,
callback,
});
}
const body800 = core_1.pb.encode({
1: 800,
2: 7,
10: {
10: this.c.uin,
20: this.uid,
30: fid,
},
101: 3,
102: 104,
});
await this.c.sendUni("OfflineFilleHandleSvr.pb_ftn_CMD_REQ_UPLOAD_SUCC-800", body800);
const proto3 = {
2: {
1: {
1: 0,
3: fid,
4: filemd5,
5: filename,
6: filesize,
9: 1,
},
},
};
await this._sendMsg(proto3, `[文件:${filename}]`, true);
return String(fid);
}
/**
* 撤回离线文件
* @param fid 文件id
*/
async recallFile(fid) {
const body = core_1.pb.encode({
1: 400,
2: 0,
6: {
1: this.c.uin,
2: fid,
},
101: 3,
102: 104,
200: 1,
});
const payload = await this.c.sendUni("OfflineFilleHandleSvr.pb_ftn_CMD_REQ_RECALL-400", body);
const rsp = core_1.pb.decode(payload)[6];
return rsp[1] === 0;
}
/**
* 转发离线文件
* @param fid 文件fid
* @param group_id 群号,转发群文件时填写
* @returns 转发成功后新文件的id
*/
async forwardFile(fid, group_id = 0) {
let new_fid;
if (group_id > 0) {
const body = core_1.pb.encode({
3: {
1: group_id,
2: 3,
3: 102,
4: `/${fid}`,
5: 3,
6: this.uid,
},
});
const payload = await this.c.sendOidbSvcTrpcTcp("OidbSvcTrpcTcp.0x6d9_2", body);
const rsp = payload[3];
if (rsp[1] !== 0)
(0, errors_1.drop)(rsp[1], rsp[2]);
new_fid = rsp[4];
const info = await this.getFileInfo(new_fid);
const proto3 = {
2: {
1: {
1: 0,
3: new_fid,
4: Buffer.from(info.md5, "hex"),
5: info.name,
6: info.size,
9: 1,
},
},
};
await this._sendMsg(proto3, `[文件:${info.name}]`, true);
}
else {
const body = core_1.pb.encode({
1: 700,
2: 0,
9: {
10: this.c.uin,
20: this.uid,
30: fid,
},
101: 3,
102: 104,
200: 1,
});
const payload = await this.c.sendUni("OfflineFilleHandleSvr.pb_ftn_CMD_REQ_APPLY_FORWARD_FILE-700", body);
const rsp = core_1.pb.decode(payload)[9];
new_fid = rsp[50];
const ticket = rsp[60];
if (rsp[10] !== 0)
(0, errors_1.drop)(rsp[10], rsp[20]);
const info = await this.getFileInfo(fid);
const proto3 = {
2: {
1: {
1: 0,
3: new_fid,
4: Buffer.from(info.md5, "hex"),
5: info.name,
6: info.size,
9: 1,
57: ticket,
},
},
};
await this._sendMsg(proto3, `[文件:${info.name}]`, true);
}
return String(new_fid);
}
/**
* 查找机器人与这个人的共群
* @returns
*/
async searchSameGroup() {
let body = core_1.pb.encode({
"1": 3316,
"2": 0,
"3": 0,
"4": {
"1": this.c.uin,
"2": this.uid,
"4": 1,
"5": [
{
"3": {
"1": this.c.uin,
"2": this.uid,
},
"5": 3436,
},
{
"3": {
"1": {
"1": {
"6": `${this.uid}`,
},
"2": 1,
},
},
"5": 3460,
},
],
"6": 0,
},
"6": "android 8.9.28",
});
const payload = await this.c.sendUni("OidbSvc.0xcf4", body);
let res = await (0, protobuf_1.decodePb)(payload);
//console.log();
//console.log((res as any)[4][12]);
if (!res[4][12][1]) {
return [];
}
return res[4][12][1].map((item) => {
return {
groupName: item["3"],
Group_Id: item["1"],
};
});
}
}
exports.Friend = Friend;