UNPKG

oicq

Version:
577 lines (576 loc) 20 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Group = exports.Discuss = void 0; const crypto_1 = require("crypto"); const axios_1 = __importDefault(require("axios")); const core_1 = require("./core"); const errors_1 = require("./errors"); const common_1 = require("./common"); const internal_1 = require("./internal"); const message_1 = require("./message"); const gfs_1 = require("./gfs"); const fetchmap = new Map(); const weakmap = new WeakMap(); const GI_BUF = core_1.pb.encode({ 1: 0, 2: 0, 5: 0, 6: 0, 15: "", 29: 0, 36: 0, 37: 0, 45: 0, 46: 0, 49: 0, 50: 0, 54: 0, 89: "", }); /** 讨论组 */ class Discuss extends internal_1.Contactable { constructor(c, gid) { super(c); this.gid = gid; (0, common_1.lock)(this, "gid"); } static as(gid) { return new Discuss(this, Number(gid)); } /** `this.gid`的别名 */ get group_id() { return this.gid; } /** 发送一条消息 */ async sendMsg(content) { const { rich, brief } = await this._preprocess(content); const body = core_1.pb.encode({ 1: { 4: { 1: this.gid } }, 2: common_1.PB_CONTENT, 3: { 1: rich }, 4: (0, crypto_1.randomBytes)(2).readUInt16BE(), 5: (0, crypto_1.randomBytes)(4).readUInt32BE(), 8: 0, }); const payload = await this.c.sendUni("MessageSvc.PbSendMsg", body); const rsp = core_1.pb.decode(payload); if (rsp[1] !== 0) { this.c.logger.error(`failed to send: [Discuss(${this.gid})] ${rsp[2]}(${rsp[1]})`); (0, errors_1.drop)(rsp[1], rsp[2]); } this.c.logger.info(`succeed to send: [Discuss(${this.gid})] ` + brief); return { message_id: "", seq: 0, rand: 0, time: 0, }; } } exports.Discuss = Discuss; /** 群 */ class Group extends Discuss { constructor(c, gid, _info) { super(c, gid); this._info = _info; this.fs = new gfs_1.Gfs(c, gid); (0, common_1.lock)(this, "fs"); (0, common_1.hide)(this, "_info"); } static as(gid, strict = false) { const info = this.gl.get(gid); if (strict && !info) throw new Error(`你尚未加入群` + gid); let group = weakmap.get(info); if (group) return group; group = new Group(this, Number(gid), info); if (info) weakmap.set(info, group); return group; } /** 群资料 */ get info() { if (!this._info || (0, common_1.timestamp)() - this._info?.update_time >= 900) this.renew().catch(common_1.NOOP); return this._info; } get name() { return this.info?.group_name; } /** 我是否是群主 */ get is_owner() { return this.info?.owner_id === this.c.uin; } /** 我是否是管理 */ get is_admin() { return this.is_owner || !!this.info?.admin_flag; } /** 是否全员禁言 */ get all_muted() { return this.info?.shutup_time_whole > (0, common_1.timestamp)(); } /** 我的禁言剩余时间 */ get mute_left() { const t = this.info?.shutup_time_me - (0, common_1.timestamp)(); return t > 0 ? t : 0; } /** 获取一枚群员实例 */ pickMember(uid, strict = false) { return this.c.pickMember(this.gid, uid, strict); } /** 获取群头像url (history=1,2,3...) */ getAvatarUrl(size = 0, history = 0) { return `https://p.qlogo.cn/gh/${this.gid}/${this.gid}${history ? "_" + history : ""}/` + size; } /** 强制刷新资料 */ async renew() { if (this._info) this._info.update_time = (0, common_1.timestamp)(); const body = core_1.pb.encode({ 1: this.c.apk.subid, 2: { 1: this.gid, 2: GI_BUF, }, }); const payload = await this.c.sendOidb("OidbSvc.0x88d_0", body); const proto = core_1.pb.decode(payload)[4][1][3]; if (!proto) { this.c.gl.delete(this.gid); this.c.gml.delete(this.gid); (0, errors_1.drop)(errors_1.ErrorCode.GroupNotJoined); } let info = { group_id: this.gid, group_name: proto[89] ? String(proto[89]) : String(proto[15]), member_count: proto[6], max_member_count: proto[5], owner_id: proto[1], admin_flag: !!proto[50], last_join_time: proto[49], last_sent_time: proto[54], shutup_time_whole: proto[45] ? 0xffffffff : 0, shutup_time_me: proto[46] > (0, common_1.timestamp)() ? proto[46] : 0, create_time: proto[2], grade: proto[36], max_admin_count: proto[29], active_member_count: proto[37], update_time: (0, common_1.timestamp)(), }; info = Object.assign(this.c.gl.get(this.gid) || this._info || {}, info); this.c.gl.set(this.gid, info); this._info = info; weakmap.set(info, this); return info; } async _fetchMembers() { let next = 0; if (!this.c.gml.has(this.gid)) this.c.gml.set(this.gid, new Map); try { while (true) { const GTML = core_1.jce.encodeStruct([ this.c.uin, this.gid, next, (0, common_1.code2uin)(this.gid), 2, 0, 0, 0 ]); const body = core_1.jce.encodeWrapper({ GTML }, "mqq.IMService.FriendListServiceServantObj", "GetTroopMemberListReq"); const payload = await this.c.sendUni("friendlist.GetTroopMemberListReq", body, 10); const nested = core_1.jce.decodeWrapper(payload); next = nested[4]; if (!this.c.gml.has(this.gid)) this.c.gml.set(this.gid, new Map); for (let v of nested[3]) { let info = { group_id: this.gid, user_id: v[0], nickname: v[4] || "", card: v[8] || "", sex: (v[3] ? (v[3] === -1 ? "unknown" : "female") : "male"), age: v[2] || 0, join_time: v[15], last_sent_time: v[16], level: v[14], role: v[18] % 2 === 1 ? "admin" : "member", title: v[23], title_expire_time: v[24] & 0xffffffff, shutup_time: v[30] > (0, common_1.timestamp)() ? v[30] : 0, update_time: 0, }; const list = this.c.gml.get(this.gid); info = Object.assign(list.get(v[0]) || {}, info); if (this.c.gl.get(this.gid)?.owner_id === v[0]) info.role = "owner"; list.set(v[0], info); } if (!next) break; } } catch { this.c.logger.error("加载群员列表超时"); } fetchmap.delete(this.c.uin + "-" + this.gid); const mlist = this.c.gml.get(this.gid); if (!mlist?.size || !this.c.config.cache_group_member) this.c.gml.delete(this.gid); return mlist || new Map(); } /** 获取群员列表 */ async getMemberMap(no_cache = false) { const k = this.c.uin + "-" + this.gid; const fetching = fetchmap.get(k); if (fetching) return fetching; const mlist = this.c.gml.get(this.gid); if (!mlist || no_cache) { const fetching = this._fetchMembers(); fetchmap.set(k, fetching); return fetching; } else { return mlist; } } /** 发送音乐分享 */ async shareMusic(platform, id) { const body = await (0, message_1.buildMusic)(this.gid, platform, id, 1); await this.c.sendOidb("OidbSvc.0xb77_9", core_1.pb.encode(body)); } /** * 发送一条消息 * @param source 引用回复的消息 * @param anony 匿名 */ async sendMsg(content, source, anony = false) { const converter = await this._preprocess(content, source); if (anony) { if (!anony.id) anony = await this.getAnonyInfo(); converter.anonymize(anony); } const rand = (0, crypto_1.randomBytes)(4).readUInt32BE(); const body = core_1.pb.encode({ 1: { 2: { 1: this.gid } }, 2: common_1.PB_CONTENT, 3: { 1: converter.rich }, 4: (0, crypto_1.randomBytes)(2).readUInt16BE(), 5: rand, 8: 0, }); const e = `internal.${this.gid}.${rand}`; let message_id = ""; this.c.once(e, (id) => message_id = id); try { const payload = await this.c.sendUni("MessageSvc.PbSendMsg", body); const rsp = core_1.pb.decode(payload); if (rsp[1] !== 0) { this.c.logger.error(`failed to send: [Group: ${this.gid}] ${rsp[2]}(${rsp[1]})`); (0, errors_1.drop)(rsp[1], rsp[2]); } } finally { this.c.removeAllListeners(e); } // 分片专属屎山 try { if (!message_id) { const time = this.c.config.resend ? (converter.length <= 80 ? 2000 : 500) : 5000; message_id = await new Promise((resolve, reject) => { const timeout = setTimeout(() => { this.c.removeAllListeners(e); reject(); }, time); this.c.once(e, (id) => { clearTimeout(timeout); resolve(id); }); }); } } catch { message_id = await this._sendMsgByFrag(converter); } this.c.logger.info(`succeed to send: [Group(${this.gid})] ` + converter.brief); { const { seq, rand, time } = (0, message_1.parseGroupMessageId)(message_id); return { seq, rand, time, message_id }; } } async _sendMsgByFrag(converter) { if (!this.c.config.resend || !converter.is_chain) (0, errors_1.drop)(errors_1.ErrorCode.RiskMessageError); const fragments = converter.toFragments(); this.c.logger.warn("群消息可能被风控,将尝试使用分片发送"); let n = 0; const rand = (0, crypto_1.randomBytes)(4).readUInt32BE(); const div = (0, crypto_1.randomBytes)(2).readUInt16BE(); for (let frag of fragments) { const body = core_1.pb.encode({ 1: { 2: { 1: this.gid } }, 2: { 1: fragments.length, 2: n++, 3: div }, 3: { 1: frag }, 4: (0, crypto_1.randomBytes)(2).readUInt16BE(), 5: rand, 8: 0, }); this.c.writeUni("MessageSvc.PbSendMsg", body); } const e = `internal.${this.gid}.${rand}`; try { return await new Promise((resolve, reject) => { const timeout = setTimeout(() => { this.c.removeAllListeners(e); reject(); }, 5000); this.c.once(e, (id) => { clearTimeout(timeout); resolve(id); }); }); } catch { (0, errors_1.drop)(errors_1.ErrorCode.SensitiveWordsError); } } async recallMsg(param, rand = 0, pktnum = 1) { if (param instanceof message_1.GroupMessage) var { seq, rand, pktnum } = param; else if (typeof param === "string") var { seq, rand, pktnum } = (0, message_1.parseGroupMessageId)(param); else var seq = param; if (pktnum > 1) { var msg = [], pb_msg = [], n = pktnum, i = 0; while (n-- > 0) { msg.push(core_1.pb.encode({ 1: seq, 2: rand, })); pb_msg.push(core_1.pb.encode({ 1: seq, 3: pktnum, 4: i++ })); ++seq; } var reserver = { 1: 1, 2: pb_msg, }; } else { var msg = { 1: seq, 2: rand, }; var reserver = { 1: 0 }; } const body = core_1.pb.encode({ 2: { 1: 1, 2: 0, 3: this.gid, 4: msg, 5: reserver, } }); const payload = await this.c.sendUni("PbMessageSvc.PbMsgWithDraw", body); return core_1.pb.decode(payload)[2][1] === 0; } /** 设置群名 */ setName(name) { return this._setting({ 3: String(name) }); } /** 全员禁言 */ muteAll(yes = true) { return this._setting({ 17: yes ? 0xffffffff : 0 }); } /** 发送简易群公告 */ announce(content) { return this._setting({ 4: String(content) }); } async _setting(obj) { const body = core_1.pb.encode({ 1: this.gid, 2: obj }); const payload = await this.c.sendOidb("OidbSvc.0x89a_0", body); return core_1.pb.decode(payload)[3] === 0; } /** 允许/禁止匿名 */ async allowAnony(yes = true) { const buf = Buffer.allocUnsafe(5); buf.writeUInt32BE(this.gid); buf.writeUInt8(yes ? 1 : 0, 4); const payload = await this.c.sendOidb("OidbSvc.0x568_22", buf); return core_1.pb.decode(payload)[3] === 0; } /** 设置备注 */ async setRemark(remark = "") { const body = core_1.pb.encode({ 1: { 1: this.gid, 2: (0, common_1.code2uin)(this.gid), 3: String(remark || "") } }); await this.c.sendOidb("OidbSvc.0xf16_1", body); } /** 禁言匿名玩家,默认1800秒 */ async muteAnony(flag, duration = 1800) { const [nick, id] = flag.split("@"); const Cookie = this.c.cookies["qqweb.qq.com"]; let body = new URLSearchParams({ anony_id: id, group_code: String(this.gid), seconds: String(duration), anony_nick: nick, bkn: String(this.c.bkn) }).toString(); await axios_1.default.post("https://qqweb.qq.com/c/anonymoustalk/blacklist", body, { headers: { Cookie, "Content-Type": "application/x-www-form-urlencoded" }, timeout: 5000 }); } /** 获取自己的匿名情报 */ async getAnonyInfo() { const body = core_1.pb.encode({ 1: 1, 10: { 1: this.c.uin, 2: this.gid, } }); const payload = await this.c.sendUni("group_anonymous_generate_nick.group", body); const obj = core_1.pb.decode(payload)[11]; return { enable: !obj[10][1], name: String(obj[3]), id: obj[5], id2: obj[4], expire_time: obj[6], color: String(obj[15]), }; } /** 获取 @全体成员 的剩余次数 */ async getAtAllRemainder() { const body = core_1.pb.encode({ 1: 1, 2: 2, 3: 1, 4: this.c.uin, 5: this.gid, }); const payload = await this.c.sendOidb("OidbSvc.0x8a7_0", body); return core_1.pb.decode(payload)[4][2]; } async _getLastSeq() { const body = core_1.pb.encode({ 1: this.c.apk.subid, 2: { 1: this.gid, 2: { 22: 0 }, }, }); const payload = await this.c.sendOidb("OidbSvc.0x88d_0", body); return core_1.pb.decode(payload)[4][1][3][22]; } /** 标记`seq`之前为已读,默认到最后一条发言 */ async markRead(seq = 0) { const body = core_1.pb.encode({ 1: { 1: this.gid, 2: Number(seq || (await this._getLastSeq())) } }); await this.c.sendUni("PbMessageSvc.PbMsgReadedReport", body); } /** 获取`seq`之前的`cnt`条聊天记录,默认从最后一条发言往前,`cnt`默认20不能超过20 */ async getChatHistory(seq = 0, cnt = 20) { if (cnt > 20) cnt = 20; if (!seq) seq = await this._getLastSeq(); const from_seq = seq - cnt + 1; const body = core_1.pb.encode({ 1: this.gid, 2: from_seq, 3: Number(seq), 6: 0 }); const payload = await this.c.sendUni("MessageSvc.PbGetGroupMsg", body); const obj = core_1.pb.decode(payload), messages = []; if (obj[1] > 0 || !obj[6]) return []; !Array.isArray(obj[6]) && (obj[6] = [obj[6]]); for (const proto of obj[6]) { try { messages.push(new message_1.GroupMessage(proto)); } catch { } } return messages; } /** 获取群文件下载地址 */ async getFileUrl(fid) { return (await this.fs.download(fid)).url; } /** 设置群头像 */ async setAvatar(file) { const img = new message_1.Image({ type: "image", file }); await img.task; const url = `http://htdata3.qq.com/cgi-bin/httpconn?htcmd=0x6ff0072&ver=5520&ukey=${this.c.sig.skey}&range=0&uin=${this.c.uin}&seq=1&groupuin=${this.gid}&filetype=3&imagetype=5&userdata=0&subcmd=1&subver=101&clip=0_0_0_0&filesize=` + img.size; await axios_1.default.post(url, img.readable, { headers: { "Content-Length": String(img.size) } }); img.deleteTmpFile(); } /** 邀请好友入群 */ async invite(uid) { const body = core_1.pb.encode({ 1: this.gid, 2: { 1: Number(uid) } }); const payload = await this.c.sendOidb("OidbSvc.oidb_0x758", body); return core_1.pb.decode(payload)[4].toBuffer().length > 6; } /** 退群/解散 */ async quit() { const buf = Buffer.allocUnsafe(8); buf.writeUInt32BE(this.c.uin); buf.writeUInt32BE(this.gid, 4); const GroupMngReq = core_1.jce.encodeStruct([ 2, this.c.uin, buf ]); const body = core_1.jce.encodeWrapper({ GroupMngReq }, "KQQ.ProfileService.ProfileServantObj", "GroupMngReq"); const payload = await this.c.sendUni("ProfileService.GroupMngReq", body); return core_1.jce.decodeWrapper(payload)[1] === 0; } setAdmin(uid, yes = true) { return this.pickMember(uid).setAdmin(yes); } setTitle(uid, title = "", duration = -1) { return this.pickMember(uid).setTitle(title, duration); } setCard(uid, card = "") { return this.pickMember(uid).setCard(card); } kickMember(uid, block = false) { return this.pickMember(uid).kick(block); } muteMember(uid, duration = 600) { return this.pickMember(uid).mute(duration); } pokeMember(uid) { return this.pickMember(uid).poke(); } } exports.Group = Group;