oicq
Version:
QQ protocol!
586 lines (562 loc) • 18.1 kB
JavaScript
/**
* 常用群功能和好友功能
* 相关api
*/
"use strict";
const querystring = require("querystring");
const http = require("http");
const https = require("https");
const pb = require("../algo/pb");
const jce = require("../algo/jce");
const { uinAutoCheck, pipeline, NOOP } = require("../common");
const { highwayUploadStream } = require("../service");
const { ImageBuilder } = require("../message/image");
/**
* @this {import("./ref").Client}
* @param {number} group_id
* @param {number} user_id
* @param {boolean} enable
* @returns {import("./ref").ProtocolResponse}
*/
async function setAdmin(group_id, user_id, enable = true) {
[group_id, user_id] = uinAutoCheck(group_id, user_id);
const buf = Buffer.allocUnsafe(9);
buf.writeUInt32BE(group_id), buf.writeUInt32BE(user_id, 4), buf.writeUInt8(enable ? 1 : 0, 8);
const blob = await this.sendOidb("OidbSvc.0x55c_1", buf);
const result = pb.decode(blob)[3];
if (result === 0) {
try {
const old_role = this.gml.get(group_id).get(user_id).role;
const new_role = enable ? "admin" : "member";
if (old_role !== new_role && old_role !== "owner") {
this.gml.get(group_id).get(user_id).role = new_role;
setImmediate(() => {
this.em("notice.group.admin", {
group_id, user_id, set: !!enable
});
});
}
} catch (e) { }
}
return { result };
}
/**
* 设置头衔
* @this {import("./ref").Client}
* @param {number} group_id
* @param {number} user_id
* @param {string} title
* @param {number} duration
* @returns {import("./ref").ProtocolResponse}
*/
async function setTitle(group_id, user_id, title = "", duration = -1) {
[group_id, user_id] = uinAutoCheck(group_id, user_id);
title = String(title);
duration = parseInt(duration) & 0xffffffff;
const body = pb.encode({
1: group_id,
3: [{
1: user_id,
7: title,
5: title,
6: duration ? duration : -1
}]
});
const blob = await this.sendOidb("OidbSvc.0x8fc_2", body);
const rsp = pb.decode(blob);
return { result: rsp[3] };
}
/**
* 群设置
* @this {import("./ref").Client}
* @param {number} group_id
* @param {string} k
* @param {any} v
* @returns {import("./ref").ProtocolResponse}
*/
async function setting(group_id, k, v) {
[group_id] = uinAutoCheck(group_id);
const settings = {
shutupTime: 17,
ingGroupName: 3,
ingGroupMemo: 4,
};
const tag = settings[k];
if (!tag)
throw new Error("unknown setting key");
const body = {
1: group_id,
2: {},
};
body[2][tag] = v;
const blob = await this.sendOidb("OidbSvc.0x89a_0", pb.encode(body));
const rsp = pb.decode(blob);
return { result: rsp[3] };
}
/**
* @this {import("./ref").Client}
* @param {number} group_id
* @param {number} user_id
* @param {string} card
* @returns {import("./ref").ProtocolResponse}
*/
async function setCard(group_id, user_id, card = "") {
[group_id, user_id] = uinAutoCheck(group_id, user_id);
const MGCREQ = jce.encodeStruct([
0, group_id, 0, [
jce.encodeNested([
user_id, 31, String(card), 0, "", "", ""
])
]
]);
const extra = {
req_id: this.seq_id + 1,
service: "mqq.IMService.FriendListServiceServantObj",
method: "ModifyGroupCardReq",
};
const body = jce.encodeWrapper({ MGCREQ }, extra);
const blob = await this.sendUni("friendlist.ModifyGroupCardReq", body);
const rsp = jce.decode(blob);
const result = rsp[3].length > 0 ? 0 : 1;
return { result };
}
/**
* @this {import("./ref").Client}
* @param {number} group_id
* @param {number} user_id
* @param {boolean} block
* @returns {import("./ref").ProtocolResponse}
*/
async function kickMember(group_id, user_id, block = false) {
[group_id, user_id] = uinAutoCheck(group_id, user_id);
const body = pb.encode({
1: group_id,
2: [{
1: 5,
2: user_id,
3: block ? 1 : 0,
}],
});
const blob = await this.sendOidb("OidbSvc.0x8a0_0", body);
const o = pb.decode(blob)[4];
const result = o[2][1];
try {
var member = this.gml.get(group_id).get(user_id);
} catch { }
if (result === 0 && this.gml.has(group_id) && this.gml.get(group_id).delete(user_id)) {
setImmediate(() => {
this.em("notice.group.decrease", {
group_id, user_id,
operator_id: this.uin,
dismiss: false, member
});
});
}
return { result };
}
/**
* @this {import("./ref").Client}
* @param {number} group_id
* @param {number} user_id
* @param {number} duration
* @returns {import("./ref").ProtocolResponse}
*/
async function muteMember(group_id, user_id, duration = 1800) {
[group_id, user_id] = uinAutoCheck(group_id, user_id);
duration = parseInt(duration);
if (duration > 2592000 || duration < 0)
duration = 2592000;
const buf = Buffer.allocUnsafe(15);
buf.writeUInt32BE(group_id), buf.writeUInt8(32, 4), buf.writeUInt16BE(1, 5);
buf.writeUInt32BE(user_id, 7), buf.writeUInt32BE(duration ? duration : 0, 11);
await this.sendOidb("OidbSvc.0x570_8", buf);
}
/**
* @this {import("./ref").Client}
* @param {number} group_id
* @param {boolean} dismiss
* @returns {import("./ref").ProtocolResponse}
*/
async function quitGroup(group_id, dismiss = false) {
[group_id] = uinAutoCheck(group_id);
let command, buf = Buffer.allocUnsafe(8);
if (dismiss) {
command = 9;
buf.writeUInt32BE(group_id), buf.writeUInt32BE(this.uin, 4);
} else {
command = 2;
buf.writeUInt32BE(this.uin), buf.writeUInt32BE(group_id, 4);
}
const GroupMngReq = jce.encodeStruct([
command, this.uin, buf
]);
const extra = {
req_id: this.seq_id + 1,
service: "KQQ.ProfileService.ProfileServantObj",
method: "GroupMngReq",
};
const body = jce.encodeWrapper({ GroupMngReq }, extra);
const blob = await this.sendUni("ProfileService.GroupMngReq", body);
const rsp = jce.decode(blob);
return { result: rsp[1] };
}
/**
* @this {import("./ref").Client}
* @param {number} group_id 发送的对象,可以是好友uin
* @param {number} user_id 戳一戳的对象
* @returns {import("./ref").ProtocolResponse}
*/
async function pokeMember(group_id, user_id) {
[group_id, user_id] = uinAutoCheck(group_id, user_id);
const o = { 1: user_id };
if (this.gl.has(group_id) || !this.fl.has(group_id))
o[2] = group_id;
else
o[5] = group_id;
const body = pb.encode(o);
const blob = await this.sendOidb("OidbSvc.0xed3", body);
const rsp = pb.decode(blob);
return { result: rsp[3] & 0xffffffff };
}
/**
* @this {import("./ref").Client}
* @param {number} group_id
* @param {string} comment
* @returns {import("./ref").ProtocolResponse}
*/
async function addGroup(group_id, comment = "") {
[group_id] = uinAutoCheck(group_id);
comment = Buffer.from(String(comment)).slice(0, 255);
const buf = Buffer.allocUnsafe(9 + comment.length);
buf.writeUInt32BE(group_id), buf.writeUInt32BE(this.uin, 4), buf.writeUInt8(comment.length, 8);
buf.fill(comment, 9);
const GroupMngReq = jce.encodeStruct([
1,
this.uin, buf, 0, "", 0, 3, 30002, 0, 0, 0,
null, "", null, "", "", 0
]);
const extra = {
req_id: this.seq_id + 1,
service: "KQQ.ProfileService.ProfileServantObj",
method: "GroupMngReq",
};
const body = jce.encodeWrapper({ GroupMngReq }, extra);
const blob = await this.sendUni("ProfileService.GroupMngReq", body);
const rsp = jce.decode(blob);
return { result: rsp[1] };
}
/**
* @this {import("./ref").Client}
* @param {number} group_id
* @param {number} user_id
* @returns {import("./ref").ProtocolResponse}
*/
async function inviteFriend(group_id, user_id) {
[group_id, user_id] = uinAutoCheck(group_id, user_id);
const body = pb.encode({
1: group_id,
2: { 1: user_id }
});
const blob = await this.sendOidb("OidbSvc.oidb_0x758", body);
const result = pb.decode(blob)[4].toBuffer().length > 6 ? 0 : 1;
return { result };
}
/**
* 启用/禁用 匿名
* @this {import("./ref").Client}
* @param {number} group_id
* @param {boolean} enable
* @returns {import("./ref").ProtocolResponse}
*/
async function setAnonymous(group_id, enable = true) {
[group_id] = uinAutoCheck(group_id);
const buf = Buffer.allocUnsafe(5);
buf.writeUInt32BE(group_id), buf.writeUInt8(enable ? 1 : 0, 4);
const blob = await this.sendOidb("OidbSvc.0x568_22", buf);
const rsp = pb.decode(blob);
return { result: rsp[3] };
}
/**
* @param {string} flag
*/
function _parseAnonFlag(flag) {
const split = flag.split("@");
return {
id: split[1],
nick: split[0],
};
}
/**
* @this {import("./ref").Client}
* @param {number} group_id
* @param {string} flag
* @param {number} duration
* @returns {import("./ref").ProtocolResponse}
*/
async function muteAnonymous(group_id, flag, duration = 1800) {
[group_id] = uinAutoCheck(group_id);
duration = parseInt(duration);
if (duration > 2592000 || duration < 0)
duration = 2592000;
const { id, nick } = _parseAnonFlag(flag);
const body = querystring.stringify({
anony_id: id,
group_code: group_id,
seconds: duration,
anony_nick: nick,
bkn: (await this.getCsrfToken()).data.token
});
const cookie = (await this.getCookies("qqweb.qq.com")).data.cookies;
try {
const rsp = await new Promise((resolve, reject) => {
https.request("https://qqweb.qq.com/c/anonymoustalk/blacklist", {
method: "POST",
headers: {
"content-type": "application/x-www-form-urlencoded", cookie
}
}, (res) => {
res.on("data", (chunk) => {
try {
const data = JSON.parse(chunk);
resolve({
result: data.retcode,
emsg: data.msg
});
} catch (e) {
reject(e);
}
});
}).on("error", reject).end(body);
});
return rsp;
} catch (e) {
return { result: -1, emsg: e.message };
}
}
/**
* @param {import("./ref").MediaFile} file
*/
async function _makeImg(file) {
const img = new ImageBuilder(this)
await img.buildNested({
file, cache: false
});
if (img.task)
await img.task;
if (!img.readable)
throw new Error("获取图片失败");
return img;
}
/**
* 设置头像
* @this {import("./ref").Client}
* @param {string|Buffer} file
*/
async function setPortrait(file) {
const img = await _makeImg.call(this, file);
const body = pb.encode({
1281: {
1: this.uin,
2: 0,
3: 16,
4: 1,
6: 3,
7: 5,
}
});
const blob = await this.sendUni("HttpConn.0x6ff_501", body);
const rsp = pb.decode(blob)[1281];
img.cmd = 5;
img.ticket = rsp[1].toBuffer();
await highwayUploadStream.call(this, img.readable, img, rsp[3][2][0][2], rsp[3][2][0][3]);
img.deleteTmpFile();
}
/**
* 群头像
* @this {import("./ref").Client}
* @param {string|Buffer} file
*/
async function setGroupPortrait(group_id, file) {
[group_id] = uinAutoCheck(group_id);
await this.getCookies();
const img = await _makeImg.call(this, file);
const url = `http://htdata3.qq.com/cgi-bin/httpconn?htcmd=0x6ff0072&ver=5520&ukey=${this.sig.skey}&range=0&uin=${this.uin}&seq=${this.seq_id}&groupuin=${group_id}&filetype=3&imagetype=5&userdata=0&subcmd=1&subver=101&clip=0_0_0_0&filesize=` + img.size;
await pipeline(
img.readable,
http.request(
url,
{ method: "POST", headers: { "Content-Length": img.size } },
(res) => res.destroy()
).on("error", NOOP)
);
img.deleteTmpFile();
}
/**
* 获取对方加好友设置(暂不对外开放)
* @this {import("./ref").Client}
* @param {number} user_id
* @returns {number}
*/
async function _getAddSetting(user_id) {
const FS = jce.encodeStruct([
this.uin,
user_id, 3004, 0, null, 1
]);
const extra = {
req_id: this.seq_id + 1,
service: "mqq.IMService.FriendListServiceServantObj",
method: "GetUserAddFriendSettingReq",
};
const body = jce.encodeWrapper({ FS }, extra);
const blob = await this.sendUni("friendlist.getUserAddFriendSetting", body);
const rsp = jce.decode(blob);
if (rsp[4]) return false;
return rsp[2];
}
/**
* @this {import("./ref").Client}
* @param {number} group_id
* @param {number} user_id
* @param {string} comment
* @returns {import("./ref").ProtocolResponse}
*/
async function addFriend(group_id, user_id, comment = "") {
if (group_id == 0) {
group_id = 0;
[user_id] = uinAutoCheck(user_id);
} else {
[group_id, user_id] = uinAutoCheck(group_id, user_id);
}
const type = await _getAddSetting.call(this, user_id);
if (![0, 1, 4].includes(type))
return { result: type };
comment = String(comment);
const AF = jce.encodeStruct([
this.uin,
user_id, type ? 1 : 0, 1, 0, Buffer.byteLength(comment), comment, 0, 1, null, 3004,
11, null, null, group_id ? pb.encode({ 1: group_id }) : null, 0, null, null, 0
]);
const extra = {
req_id: this.seq_id + 1,
service: "mqq.IMService.FriendListServiceServantObj",
method: "AddFriendReq",
};
const body = jce.encodeWrapper({ AF }, extra);
const blob = await this.sendUni("friendlist.addFriend", body);
const rsp = jce.decode(blob);
return { result: rsp[6] };
}
/**
* @this {import("./ref").Client}
* @param {number} user_id
* @param {boolean} block
* @returns {import("./ref").ProtocolResponse}
*/
async function delFriend(user_id, block = true) {
[user_id] = uinAutoCheck(user_id);
const DF = jce.encodeStruct([
this.uin,
user_id, 2, block ? 1 : 0
]);
const extra = {
req_id: this.seq_id + 1,
service: "mqq.IMService.FriendListServiceServantObj",
method: "DelFriendReq",
};
const body = jce.encodeWrapper({ DF }, extra);
const blob = await this.sendUni("friendlist.delFriend", body);
const rsp = jce.decode(blob);
return { result: rsp[2] };
}
/**
* 设置个人资料
* @this {import("./ref").Client}
* @param {number} k
* @param {Buffer|string} v
* @returns {import("./ref").ProtocolResponse}
*/
async function setProfile(k, v) {
v = Buffer.from(v);
const buf = Buffer.allocUnsafe(11 + v.length);
buf.writeUInt32BE(this.uin), buf.writeUInt8(0, 4);
buf.writeInt32BE(k, 5), buf.writeUInt16BE(v.length, 9);
buf.fill(v, 11);
const blob = await this.sendOidb("OidbSvc.0x4ff_9", buf);
const o = pb.decode(blob);
if (o[3] === 34)
o[3] = 0;
return { result: o[3] };
}
/**
* 设置签名
* @this {import("./ref").Client}
* @param {string} sign
* @returns {import("./ref").ProtocolResponse}
*/
async function setSign(sign = "") {
sign = Buffer.from(String(sign)).slice(0, 254);
const body = pb.encode({
1: 2,
2: Date.now(),
3: {
1: 109,
2: { 6: 825110830 },
3: this.apk.ver
},
5: {
1: this.uin,
2: 0,
3: 27 + sign.length,
4: Buffer.concat([
Buffer.from([0x3, sign.length + 1, 0x20]), sign,
Buffer.from([0x91, 0x04, 0x00, 0x00, 0x00, 0x00, 0x92, 0x04, 0x00, 0x00, 0x00, 0x00, 0xA2, 0x04, 0x00, 0x00, 0x00, 0x00, 0xA3, 0x04, 0x00, 0x00, 0x00, 0x00])
]),
5: 0
},
6: 1
});
const blob = await this.sendUni("Signature.auth", body);
const rsp = pb.decode(blob);
return { result: rsp[1], emsg: rsp[2] };
}
/**
* @this {import("./ref").Client}
* @param {number} group_id
* @returns {import("./ref").ProtocolResponse}
*/
async function getGroupNotice(group_id) {
[group_id] = uinAutoCheck(group_id);
const cookie = (await this.getCookies("qun.qq.com")).data.cookies;
const url = `https://web.qun.qq.com/cgi-bin/announce/get_t_list?bkn=${(await this.getCsrfToken()).data.token}&qid=${group_id}&ft=23&s=-1&n=20`;
try {
let data = await new Promise((resolve, reject) => {
https.get(url, { headers: { cookie } }, (res) => {
if (res.statusCode !== 200) {
return reject("statusCode: " + res.statusCode);
}
res.setEncoding("utf-8");
let data = "";
res.on("data", chunk => data += chunk);
res.on("end", () => {
try {
data = JSON.parse(data);
if (data.ec !== 0) {
return reject(data.em);
}
resolve(data.feeds ? data.feeds : []);
} catch {
reject("response error");
}
});
}).on("error", (e) => reject(e.message));
});
return { result: 0, data };
} catch (e) {
return { result: -1, emsg: e };
}
}
module.exports = {
setAdmin, setTitle, setCard, setting, setAnonymous, muteAnonymous, getGroupNotice,
kickMember, muteMember, pokeMember, quitGroup, addGroup, inviteFriend,
setProfile, setSign, addFriend, delFriend, setPortrait, setGroupPortrait
};