From c837f92c1a3c2f55aa78b7e9ac4f8717b1278418 Mon Sep 17 00:00:00 2001 From: takayama Date: Thu, 19 Nov 2020 14:10:26 +0900 Subject: [PATCH 01/11] move functions --- lib/core.js | 7 +- lib/message/chat.js | 225 ++------------------------------------------ lib/message/recv.js | 212 +++++++++++++++++++++++++++++++++++++++++ lib/online-push.js | 12 ++- 4 files changed, 233 insertions(+), 223 deletions(-) create mode 100644 lib/message/recv.js diff --git a/lib/core.js b/lib/core.js index c39650e1..494a34e7 100644 --- a/lib/core.js +++ b/lib/core.js @@ -5,7 +5,8 @@ const Readable = require("stream").Readable; const common = require("./common"); const pb = require("./pb"); const jce = require("./jce"); -const chat = require("./message/chat"); +const {buildSyncCookie} = require("./message/chat"); +const {handlePrivateMsg} = require("./message/recv"); const push = require("./online-push"); const sysmsg = require("./sysmsg"); const BUF0 = Buffer.alloc(0); @@ -66,7 +67,7 @@ function onPushNotify(blob) { */ async function getMsg(sync_flag = 0) { if (!this.sync_cookie) - this.sync_cookie = chat.buildSyncCookie.call(this); + this.sync_cookie = buildSyncCookie.call(this); let body = pb.encode({ 1: sync_flag, 2: this.sync_cookie, @@ -134,7 +135,7 @@ async function getMsg(sync_flag = 0) { //私聊消息 else - chat.onPrivateMsg.call(this, type, head, msg[2], msg[3]); + handlePrivateMsg.call(this, type, head, msg[2], msg[3]); } } diff --git a/lib/message/chat.js b/lib/message/chat.js index 15c2970b..7482b66e 100644 --- a/lib/message/chat.js +++ b/lib/message/chat.js @@ -2,11 +2,10 @@ const zlib = require("zlib"); const crypto = require("crypto"); const Builder = require("./builder"); -const parseMessage = require("./parser"); -const {uploadMultiMsg, getPrivateFileUrl} = require("./storage"); +const {uploadMultiMsg} = require("./storage"); const common = require("../common"); const pb = require("../pb"); -const {genC2CMessageId, parseC2CMessageId, genGroupMessageId, parseGroupMessageId} = common; +const {genC2CMessageId, parseC2CMessageId, parseGroupMessageId} = common; //send msg---------------------------------------------------------------------------------------------------- @@ -259,15 +258,18 @@ async function sendB77RichMsg(buf) { //recall---------------------------------------------------------------------------------------------------- +/** + * @this {import("../ref").Client} + */ async function recallMsg(message_id) { let body; if (message_id.length > 24) - body = recallGroupMsg.call(this, message_id); + body = buildRecallGroupMsgBody.call(this, message_id); else - body = recallPrivateMsg.call(this, message_id); + body = buildRecallPrivateMsgBody.call(this, message_id); await this.sendUNI("PbMessageSvc.PbMsgWithDraw", body); } -function recallPrivateMsg(message_id) { +function buildRecallPrivateMsgBody(message_id) { const {user_id, seq, random, time} = parseC2CMessageId(message_id); let type = 0; try { @@ -290,7 +292,7 @@ function recallPrivateMsg(message_id) { }] }); } -function recallGroupMsg(message_id) { +function buildRecallGroupMsgBody(message_id) { const {group_id, seq, random} = parseGroupMessageId(message_id); return pb.encode({ 2: [{ @@ -306,213 +308,6 @@ function recallGroupMsg(message_id) { }); } -//on message---------------------------------------------------------------------------------------------------- - -/** - * @param {141|166|167|208|529} type - * @this {import("../ref").Client} - */ -async function onPrivateMsg(type, head, content, body) { - - ++this.stat.recv_msg_cnt; - const user_id = head[1], - time = head[6], - seq = head[5]; - let sub_type, message_id, font = "unknown"; - - const sender = Object.assign({user_id}, this.fl.get(user_id)); - if (type === 141) { - sub_type = "other"; - if (head[8] && head[8][4]) { - sub_type = "group"; - const group_id = head[8][4]; - sender.group_id = group_id; - } - } else if (type === 167) { - sub_type = "single"; - } else { - sub_type = this.fl.has(user_id) ? "friend" : "single"; - } - if (sender.nickname === undefined) { - const stranger = (await this.getStrangerInfo(user_id, seq%5==0)).data; - if (stranger) { - stranger.group_id = sender.group_id; - Object.assign(sender, stranger); - this.sl.set(user_id, stranger); - } - } - if (type === 529) { - if (head[4] !== 4 || !body[2]) - return; - try { - const fileid = body[2][1][3].raw, - md5 = body[2][1][4].raw.toString("hex"), - name = String(body[2][1][5].raw), - size = body[2][1][6], - duration = body[2][1][51] ? time + body[2][1][51] : 0; - const url = await getPrivateFileUrl.call(this, fileid); - const raw_message = `[CQ:file,url=${url},size=${size},md5=${md5},duration=${duration},busid=0,fileid=${fileid}]`; - this.logger.info(`recv from: [Private: ${user_id}(${sub_type})] ` + raw_message); - this.em("message.private." + sub_type, { - message_id: "", user_id, - message: [{ - type: "file", - data: { - url, size, md5, duration, - busid: "0", - fileid: String(fileid) - } - }], - raw_message, font, sender, time - }); - } catch (e) {} - return; - } - if (body[1] && body[1][2]) { - let random = crypto.randomBytes(4).readInt32BE(); - if (body[1][1]) { - font = String(body[1][1][9].raw); - random = body[1][1][3]; - } - message_id = genC2CMessageId(user_id, seq, random, time); - try { - var {chain, raw_message} = await parseMessage.call(this, body[1], user_id); - } catch (e) {return} - if (raw_message) { - this.logger.info(`recv from: [Private: ${user_id}(${sub_type})] ` + raw_message); - this.em("message.private." + sub_type, { - message_id, user_id, - message: chain, - raw_message, font, sender, time, - auto_reply: !!(content&&content[4]) - }); - } - } -} - -/** - * @this {import("../ref").Client} - */ -async function onGroupMsg(head, body) { - - ++this.stat.recv_msg_cnt; - const user_id = head[1], - time = head[6], - seq = head[5]; - const group = head[9], - group_id = group[1], - group_name = String(group[8].raw); - - this.msgExists(group_id, 0, seq, time); - const message_id = genGroupMessageId(group_id, user_id, seq, body[1][1][3], time); - this.emit(`interval.${group_id}.${body[1][1][3]}`, message_id); - this.getGroupInfo(group_id); - - try { - var {chain, raw_message, extra, anon} = await parseMessage.call(this, body[1], group_id); - } catch (e) {return} - - let font = String(body[1][1][9].raw), - card = String(group[4].raw); - - // 彩色群名片 - if (extra[2]) { - card = String(extra[2].raw); - if (card.startsWith("\n")) - card = card.split("\n").pop().substr(3); - } - - let anonymous = null, user = null; - if (user_id === 80000000) { - anonymous = { - id: anon[6], - name: anon[3] ? String(anon[3].raw) : "80000000", - flag: anon[2] ? anon[2].raw.toString("base64") : "" - }; - } else { - try { - user = (await this.getGroupMemberInfo(group_id, user_id)).data; - if (extra[7]) - user.title = String(extra[7].raw); - if (extra[3]) - user.level = extra[3]; - if (extra[1] && !extra[2]) { - user.card = card = ""; - user.nickname = String(extra[1].raw); - } else { - user.card = card; - } - if (time > user.last_sent_time) { - user.last_sent_time = time; - this.gl.get(group_id).last_sent_time = time; - } - } catch (e) {} - } - - if (user_id === this.uin && this.config.ignore_self) - return; - if (!raw_message) - return; - - if (user) { - var {nickname, sex, age, area, level, role, title} = user; - } else { - var nickname = card, sex = "unknown", age = 0, area = "", level = 0, role = "member", title = ""; - } - const sender = { - user_id, nickname, card, sex, age, area, level, role, title - }; - - const sub_type = anonymous ? "anonymous" : "normal"; - this.logger.info(`recv from: [Group: ${group_name}(${group_id}), Member: ${card?card:nickname}(${user_id})] ` + raw_message); - this.em("message.group." + sub_type, { - message_id, group_id, group_name, user_id, anonymous, - message: chain, - raw_message, font, sender, time - }); -} - -/** - * @this {import("../ref").Client} - */ -async function onDiscussMsg(head, body) { - - ++this.stat.recv_msg_cnt; - const user_id = head[1], - time = head[6], - seq = head[5]; - const discuss = head[13], - discuss_id = discuss[1], - discuss_name = String(discuss[5].raw); - - this.msgExists(discuss_id, 0, seq, time); - - if (user_id === this.uin && this.config.ignore_self) - return; - - const font = String(body[1][1][9].raw), - card = nickname = String(discuss[4].raw); - - const sender = { - user_id, nickname, card - }; - - try { - var {chain, raw_message} = await parseMessage.call(this, body[1], discuss_id); - } catch (e) {return} - - if (!raw_message) - return; - - this.logger.info(`recv from: [Discuss: ${discuss_name}(${discuss_id}), Member: ${card}(${user_id})] ` + raw_message); - this.em("message.discuss", { - discuss_id, discuss_name, user_id, - message: chain, - raw_message, font, sender, time - }); -} - module.exports = { - sendMsg, recallMsg, buildSyncCookie, - onPrivateMsg, onGroupMsg, onDiscussMsg + sendMsg, recallMsg, buildSyncCookie }; diff --git a/lib/message/recv.js b/lib/message/recv.js new file mode 100644 index 00000000..966342e7 --- /dev/null +++ b/lib/message/recv.js @@ -0,0 +1,212 @@ +"use strict"; +const parseMessage = require("./parser"); +const {getPrivateFileUrl} = require("./storage"); +const {genC2CMessageId, genGroupMessageId} = require("../common"); + +/** + * @param {141|166|167|208|529} type + * @this {import("../ref").Client} + */ +async function handlePrivateMsg(type, head, content, body) { + + ++this.stat.recv_msg_cnt; + const user_id = head[1], + time = head[6], + seq = head[5]; + let sub_type, message_id, font = "unknown"; + + const sender = Object.assign({user_id}, this.fl.get(user_id)); + if (type === 141) { + sub_type = "other"; + if (head[8] && head[8][4]) { + sub_type = "group"; + const group_id = head[8][4]; + sender.group_id = group_id; + } + } else if (type === 167) { + sub_type = "single"; + } else { + sub_type = this.fl.has(user_id) ? "friend" : "single"; + } + if (sender.nickname === undefined) { + const stranger = (await this.getStrangerInfo(user_id, seq%5==0)).data; + if (stranger) { + stranger.group_id = sender.group_id; + Object.assign(sender, stranger); + this.sl.set(user_id, stranger); + } + } + if (type === 529) { + if (head[4] !== 4 || !body[2]) + return; + try { + const fileid = body[2][1][3].raw, + md5 = body[2][1][4].raw.toString("hex"), + name = String(body[2][1][5].raw), + size = body[2][1][6], + duration = body[2][1][51] ? time + body[2][1][51] : 0; + const url = await getPrivateFileUrl.call(this, fileid); + const raw_message = `[CQ:file,url=${url},size=${size},md5=${md5},duration=${duration},busid=0,fileid=${fileid}]`; + this.logger.info(`recv from: [Private: ${user_id}(${sub_type})] ` + raw_message); + this.em("message.private." + sub_type, { + message_id: "", user_id, + message: [{ + type: "file", + data: { + url, size, md5, duration, + busid: "0", + fileid: String(fileid) + } + }], + raw_message, font, sender, time + }); + } catch (e) {} + return; + } + if (body[1] && body[1][2]) { + let random = seq; + if (body[1][1]) { + font = String(body[1][1][9].raw); + random = body[1][1][3]; + } + message_id = genC2CMessageId(user_id, seq, random, time); + try { + var {chain, raw_message} = await parseMessage.call(this, body[1], user_id); + } catch (e) {return} + if (raw_message) { + this.logger.info(`recv from: [Private: ${user_id}(${sub_type})] ` + raw_message); + this.em("message.private." + sub_type, { + message_id, user_id, + message: chain, + raw_message, font, sender, time, + auto_reply: !!(content&&content[4]) + }); + } + } +} + +/** + * @this {import("../ref").Client} + */ +async function handleGroupMsg(head, body) { + + ++this.stat.recv_msg_cnt; + const user_id = head[1], + time = head[6], + seq = head[5]; + const group = head[9], + group_id = group[1], + group_name = String(group[8].raw); + + this.msgExists(group_id, 0, seq, time); + const message_id = genGroupMessageId(group_id, user_id, seq, body[1][1][3], time); + this.emit(`interval.${group_id}.${body[1][1][3]}`, message_id); + this.getGroupInfo(group_id); + + try { + var {chain, raw_message, extra, anon} = await parseMessage.call(this, body[1], group_id); + } catch (e) {return} + + let font = String(body[1][1][9].raw), + card = String(group[4].raw); + + // 彩色群名片 + if (extra[2]) { + card = String(extra[2].raw); + if (card.startsWith("\n")) + card = card.split("\n").pop().substr(3); + } + + let anonymous = null, user = null; + if (user_id === 80000000) { + anonymous = { + id: anon[6], + name: anon[3] ? String(anon[3].raw) : "80000000", + flag: anon[2] ? anon[2].raw.toString("base64") : "" + }; + } else { + try { + user = (await this.getGroupMemberInfo(group_id, user_id)).data; + if (extra[7]) + user.title = String(extra[7].raw); + if (extra[3]) + user.level = extra[3]; + if (extra[1] && !extra[2]) { + user.card = card = ""; + user.nickname = String(extra[1].raw); + } else { + user.card = card; + } + if (time > user.last_sent_time) { + user.last_sent_time = time; + this.gl.get(group_id).last_sent_time = time; + } + } catch (e) {} + } + + if (user_id === this.uin && this.config.ignore_self) + return; + if (!raw_message) + return; + + if (user) { + var {nickname, sex, age, area, level, role, title} = user; + } else { + var nickname = card, sex = "unknown", age = 0, area = "", level = 0, role = "member", title = ""; + } + const sender = { + user_id, nickname, card, sex, age, area, level, role, title + }; + + const sub_type = anonymous ? "anonymous" : "normal"; + this.logger.info(`recv from: [Group: ${group_name}(${group_id}), Member: ${card?card:nickname}(${user_id})] ` + raw_message); + this.em("message.group." + sub_type, { + message_id, group_id, group_name, user_id, anonymous, + message: chain, + raw_message, font, sender, time + }); +} + +/** + * @this {import("../ref").Client} + */ +async function handleDiscussMsg(head, body) { + + ++this.stat.recv_msg_cnt; + const user_id = head[1], + time = head[6], + seq = head[5]; + const discuss = head[13], + discuss_id = discuss[1], + discuss_name = String(discuss[5].raw); + + this.msgExists(discuss_id, 0, seq, time); + + if (user_id === this.uin && this.config.ignore_self) + return; + + const font = String(body[1][1][9].raw), + card = nickname = String(discuss[4].raw); + + const sender = { + user_id, nickname, card + }; + + try { + var {chain, raw_message} = await parseMessage.call(this, body[1], discuss_id); + } catch (e) {return} + + if (!raw_message) + return; + + this.logger.info(`recv from: [Discuss: ${discuss_name}(${discuss_id}), Member: ${card}(${user_id})] ` + raw_message); + this.em("message.discuss", { + discuss_id, discuss_name, user_id, + message: chain, + raw_message, font, sender, time + }); +} + +module.exports = { + handlePrivateMsg, handleGroupMsg, handleDiscussMsg +}; diff --git a/lib/online-push.js b/lib/online-push.js index dff3f253..bec5963f 100644 --- a/lib/online-push.js +++ b/lib/online-push.js @@ -1,7 +1,7 @@ "use strict"; const pb = require("./pb"); const jce = require("./jce"); -const chat = require("./message/chat"); +const {handleGroupMsg, handleDiscussMsg} = require("./message/recv"); const {genC2CMessageId, genGroupMessageId} = require("./common"); /** @@ -425,16 +425,18 @@ function onC2CMsgSync(blob, seq) { } function onGroupMsg(blob, seq) { - if (!this.sync_finished) return; + if (!this.sync_finished) + return; const o = pb.decode(blob); - chat.onGroupMsg.call(this, o[1][1], o[1][3]); + handleGroupMsg.call(this, o[1][1], o[1][3]); } function onDiscussMsg(blob, seq) { const o = pb.decode(blob); handleOnlinePush.call(this, o[2], seq); - if (!this.sync_finished) return; - chat.onDiscussMsg.call(this, o[1][1], o[1][3]); + if (!this.sync_finished) + return; + handleDiscussMsg.call(this, o[1][1], o[1][3]); } module.exports = { From f8d4fda1e054ad2d9cb06ca958255fad06dfe4cc Mon Sep 17 00:00:00 2001 From: takayama Date: Fri, 20 Nov 2020 10:49:48 +0900 Subject: [PATCH 02/11] music brief --- lib/message/chat.js | 2 +- lib/message/music.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/message/chat.js b/lib/message/chat.js index 7482b66e..c2ec6c33 100644 --- a/lib/message/chat.js +++ b/lib/message/chat.js @@ -253,7 +253,7 @@ async function sendB77RichMsg(buf) { try { await this.sendUNI("OidbSvc.0xb77_9", buf); } catch {} - return {result: 0, data: {message_id: "该消息暂不支持"}}; + return {result: 0, data: {message_id: ""}}; } //recall---------------------------------------------------------------------------------------------------- diff --git a/lib/message/music.js b/lib/message/music.js index d035cda0..ee78983e 100644 --- a/lib/message/music.js +++ b/lib/message/music.js @@ -93,7 +93,7 @@ async function build(target, type, id, bu) { 12: { 10: title, 11: singer, - 12: title, + 12: "[分享]" + title, 13: jumpUrl, 14: preview, 16: musicUrl, From be19f06d56a61beb14e817fdbc77a6c03b1d155a Mon Sep 17 00:00:00 2001 From: takayama Date: Sat, 21 Nov 2020 01:46:40 +0900 Subject: [PATCH 03/11] add reload api --- README.md | 3 +- client.d.ts | 16 ++++--- client.js | 17 +++++++- docs/api.md | 110 +++++++++++++++++++----------------------------- lib/resource.js | 59 +++++++++++++++----------- 5 files changed, 104 insertions(+), 101 deletions(-) diff --git a/README.md b/README.md index 269bb6fd..54d26268 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,7 @@ [![node engine](https://img.shields.io/node/v/oicq.svg)](https://nodejs.org) ← 注意版本 * QQ(安卓)协议的nodejs实现。也参考了一些其他开源仓库如mirai、miraiGo等。 -* 以高效和稳定为第一目的,在此基础上不断完善功能。 -* 将会逐步支持手机协议的大部分功能。 +* 以高效和稳定为第一目的,在此基础上不断完善,将会逐步支持手机协议的大部分功能。 * 使用 [CQHTTP](https://cqhttp.cc) 风格的API、事件和参数(少量差异),并且原生支持经典的CQ码。 * 本项目使用AGPL-3.0许可证,旨在学习。不推荐也不提供商业化使用的支持。 * 有bug请告诉我!PR请基于dev分支! diff --git a/client.d.ts b/client.d.ts index fb2ed5f1..8a2af93f 100644 --- a/client.d.ts +++ b/client.d.ts @@ -50,13 +50,13 @@ export interface GroupInfo { owner_id?: number, last_join_time?: number, last_sent_time?: number, - shutup_time_whole?: number, - shutup_time_me?: number, + shutup_time_whole?: number, //全员禁言到期时间 + shutup_time_me?: number, //我的禁言到期时间 create_time?: number, grade?: number, max_admin_count?: number, active_member_count?: number, - update_time?: number, + update_time?: number, //当前群资料的最后更新时间 } export interface MemberInfo { group_id?: number, @@ -75,8 +75,8 @@ export interface MemberInfo { title?: string, title_expire_time?: number, card_changeable?: boolean, - shutup_time?: number, - update_time?: number, + shutup_time?: number, //禁言到期时间 + update_time?: number, //此群员资料的最后更新时间 } export interface MessageId { message_id: string @@ -194,7 +194,7 @@ export class Client extends events.EventEmitter { getFriendList(): RetFriendList; getStrangerList(): RetStrangerList; getGroupList(): RetGroupList; - getGroupMemberList(group_id: Uin): Promise; + getGroupMemberList(group_id: Uin, no_cache?: boolean): Promise; getStrangerInfo(user_id: Uin, no_cache?: boolean): Promise; getGroupInfo(group_id: Uin, no_cache?: boolean): Promise; getGroupMemberInfo(group_id: Uin, user_id: Uin, no_cache?: boolean): Promise; @@ -247,6 +247,10 @@ export class Client extends events.EventEmitter { once(event: string, listener: (data: EventData) => void): this; on(event: string, listener: (data: EventData) => void): this; off(event: string, listener: (data: EventData) => void): this; + + //重载完成之前bot不接受其他任何请求,也不会上报任何事件 + reloadFriendList(): Promise; + reloadGroupList(): Promise; } export function createClient(uin: Uin, config?: ConfBot): Client; diff --git a/client.js b/client.js index 43bede1c..d000162b 100644 --- a/client.js +++ b/client.js @@ -448,11 +448,24 @@ class AndroidClient extends Client { return buildApiRet(0, this.gl); } - async getGroupMemberList(group_id) { + async reloadFriendList() { + this.sync_finished = false; + const success = await resource.initFL.call(this); + this.sync_finished = true; + return buildApiRet(success?0:102); + } + async reloadGroupList() { + this.sync_finished = false; + const success = await resource.initGL.call(this); + this.sync_finished = true; + return buildApiRet(success?0:102); + } + + async getGroupMemberList(group_id, no_cache = false) { group_id = parseInt(group_id); if (!checkUin(group_id)) return buildApiRet(100); - if (!this.gml.has(group_id)) + if (!this.gml.has(group_id) || no_cache) this.gml.set(group_id, resource.getGML.call(this, group_id)); let mlist = this.gml.get(group_id); if (mlist instanceof Promise) diff --git a/docs/api.md b/docs/api.md index 597914a7..572160f1 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,18 +1,18 @@ +# 请参考参照头文件 [client.d.ts](../client.d.ts) + +---- + # API -+ [createClient(uin[,config])](#createClient(uin[,config]))) -+ [Class: Client](#Class-Client) - + [系统类API](#系统类API) - + [client.login(password_md5)](#client.login(password_md5)) - + [client.captchaLogin(captcha)](#client.captchaLogin(captcha)) - + [client.terminate()](#client.terminate()) - + [应用类API](#应用类API) - + [获取列表](#获取好友群群员列表和info) - + [发消息和撤回](#发私聊消息群消息) - + [群操作](#群操作踢人禁言退群设置等) - + [加好友](#加好友删好友邀请好友入群点赞) - + [设置状态和资料](#设置状态和资料) -+ [setGlobalConfig(config)](#setGlobalConfig(config)-全局设置) ++ [启动-创建实例](#createClient(uin[,config])) ++ [系统类API](#系统类API) ++ [应用类API](#应用类API) + + [获取列表和资料](#获取好友群群员列表和info) + + [发消息和撤回](#发私聊消息群消息) + + [群操作](#群操作踢人禁言退群设置等) + + [好友操作](#加群加好友删好友邀请好友入群点赞) + + [设置状态和资料](#设置状态和资料) + + [其他](#其他) ---- @@ -21,7 +21,7 @@ + *`uin`* \ + *`config`* \ -创建client一个实例: +创建一个client实例: ```js const oicq = require("oicq"); @@ -79,7 +79,7 @@ const client = oicq.createClient(uin, config); } ``` -使用 [CQHTTP](https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md) 风格的命名和参数。同步函数会直接返回。异步函数标注为 `async` ,返回的是 `Promise` +使用 [CQHTTP](https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md) 风格的命名和参数。 ---- @@ -88,45 +88,13 @@ const client = oicq.createClient(uin, config); + `client.getFriendList()` + `client.getStrangerList()` + `client.getGroupList()` -+ async `client.getGroupMemberList(group_id)` 四个list函数返回的data是ES6的Map类型 -+ async `client.getGroupInfo(group_id[, no_cache])` 获取群资料 - + *`group_id`* \ - + *`group_name`* \ - + *`member_count`* \ - + *`max_member_count`* \ - + *`owner_id`* \ - + *`last_join_time`* \ - + *`last_sent_time`* \ - + *`shutup_time_whole`* \ -1代表全员禁言中,0代表未禁言 - + *`shutup_time_me`* \ 我的禁言到期时间 - + *`create_time`* \ - + *`grade`* \ - + *`max_admin_count`* \ - + *`active_member_count`* \ - + *`update_time`* \ 当前群资料的最后更新时间 -+ async `client.getGroupMemberInfo(group_id, user_id[, no_cache])` 获取群员资料 - + *`group_id`* \ - + *`user_id`* \ - + *`nickname`* \ - + *`card`* \ - + *`sex`* \ - + *`age`* \ - + *`area`* \ - + *`join_time`* \ - + *`last_sent_time`* \ - + *`level`* \ - + *`rank`* \ - + *`role`* \ - + *`title`* \ - + *`title_expire_time`* \ - + *`shutup_time`* \ - + *`update_time`* \ 此群员资料的最后更新时间 -+ async `client.getStrangerInfo(user_id[, no_cache])` 获取陌生人资料 - + *`user_id`* \ - + *`nickname`* \ - + *`sex`* \ - + *`age`* \ - + *`area`* \ ++ async `client.getGroupMemberList(group_id[, no_cache])` ++ async `client.getGroupInfo(group_id[, no_cache])` + + 返回值参照 [GroupInfo](../client.d.ts#GroupInfo) ++ async `client.getGroupMemberInfo(group_id, user_id[, no_cache])` + + 返回值参照 [MemberInfo](../client.d.ts#MemberInfo) ++ async `client.getStrangerInfo(user_id[, no_cache])` + + 返回值参照 [StrangerInfo](../client.d.ts#StrangerInfo) ---- @@ -135,19 +103,21 @@ const client = oicq.createClient(uin, config); message可以使用 `Array` 格式或 `String` 格式,支持CQ码 参考 [消息段类型](https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md) -+ async `client.sendPrivateMsg(user_id, message[, auto_escape])` - + *`message_id`* \ 返回字符串格式的message_id ++ async `client.sendPrivateMsg(user_id, message[, auto_escape])` + + 返回值 *`message_id`* \ + async `client.sendGroupMsg(group_id, user_id, message[, auto_escape])` - + *`message_id`* \ 返回字符串格式的message_id + + 返回值 *`message_id`* \ + async `client.sendDiscussMsg(discuss_id, user_id, message[, auto_escape])` + async `client.deleteMsg(message_id)` +※ auto_escape参数:是否原样输出CQ码(既不解析),默认false + ---- ### 处理申请和邀请 -+ async `client.setFriendAddRequest(flag[, approve, remark, block])` block默认是false -+ async `client.setGroupAddRequest(flag[, approve, reason, block])` block默认是false ++ async `client.setFriendAddRequest(flag[, approve, remark, block])` block默认false ++ async `client.setGroupAddRequest(flag[, approve, reason, block])` block默认false ---- @@ -161,19 +131,19 @@ message可以使用 `Array` 格式或 `String` 格式,支持CQ码 + async `client.setGroupAdmin(group_id, user_id[, enable])` + async `client.setGroupSpecialTitle(group_id, user_id[, special_title, duration])` + async `client.sendGroupNotice(group_id, content)` -+ async `client.sendGroupPoke(group_id, user_id)` 最近新增的戳一戳 ++ async `client.sendGroupPoke(group_id, user_id)` + async `client.setGroupAnonymous(group_id[, enable])` + async `client.setGroupWholeBan(group_id[, enable])` ---- -### 加群加好友、删好友、邀请好友入群、点赞 +### 加群加好友、删好友、邀请好友、点赞 + async `client.addGroup(group_id[, comment])` + async `client.addFriend(group_id, user_id[, comment])` -+ async `client.deleteFriend(user_id[, block])` block默认是true ++ async `client.deleteFriend(user_id[, block])` block默认true + async `client.inviteFriend(group_id, user_id)` -+ async `client.sendLike(user_id[, times])` times默认为1,不能大于20 ++ async `client.sendLike(user_id[, times])` times默认1,不能大于20 ---- @@ -193,12 +163,20 @@ message可以使用 `Array` 格式或 `String` 格式,支持CQ码 ### 其他 -+ async `client.getCookies([domain])` 实验性质,更新可能存在问题 ++ async `client.getCookies([domain])` + async `client.getCsrfToken()` + async `client.cleanCache([type])` - + `client.canSendImage()` + `client.canSendRecord()` -+ `client.getStatus()` ++ `client.getStatus()` 该函数返回一些有用的统计信息 + `client.getVersionInfo()` + `client.getLoginInfo()` + +---- + +### 重载好友列表、群列表 + +注意:一旦调用,重载完成之前bot不接受其他任何请求,也不会上报任何事件 + ++ async `client.reloadFriendList()` ++ async `client.reloadGroupList()` diff --git a/lib/resource.js b/lib/resource.js index 0290281e..ebb9a58a 100644 --- a/lib/resource.js +++ b/lib/resource.js @@ -39,46 +39,52 @@ async function _initFL(start, limit) { method: "GetFriendListReq", }; const body = jce.encodeWrapper({FL}, extra); - try { - const blob = await this.sendUNI("friendlist.getFriendGroupList", body); - const nested = jce.decodeWrapper(blob); - const parent = jce.decode(nested); - for (let v of parent[7]) { - v = jce.decode(v); - this.fl.set(v[0], { - user_id: v[0], - nickname: v[14], - sex: friend_sex_map[v[31]], - age: 0, - area: "unknown", - remark: v[3], - }) - } - return parent[5]; - } catch (e) { - this.logger.warn("初始化好友列表出现异常,未加载完成。"); - return 0; + const blob = await this.sendUNI("friendlist.getFriendGroupList", body); + const nested = jce.decodeWrapper(blob); + const parent = jce.decode(nested); + for (let v of parent[7]) { + v = jce.decode(v); + this.fl.set(v[0], { + user_id: v[0], + nickname: v[14], + sex: friend_sex_map[v[31]], + age: 0, + area: "unknown", + remark: v[3], + }) } + return parent[5]; } /** * 加载好友列表(不对外开放) * @this {import("./ref").Client} + * @returns {Boolean} */ async function initFL() { + this.fl = new Map; let start = 0, limit = 150; while (1) { - const total = await _initFL.call(this, start, limit); - start += limit; - if (start > total) break; + try { + const total = await _initFL.call(this, start, limit); + start += limit; + if (start > total) break; + } catch (e) { + this.logger.debug(e); + this.logger.warn("加载好友列表出现异常,未加载完成。"); + return false; + } } + return true; } /** * 加载群列表(不对外开放) * @this {import("./ref").Client} + * @returns {Boolean} */ async function initGL() { + this.gl = new Map; const GetTroopListReqV2Simplify = jce.encodeStruct([ this.uin, 0, null, [], 1, 8, 0, 1, 1 ]); @@ -112,8 +118,11 @@ async function initGL() { }); } } catch (e) { - this.logger.warn("初始化群列表出现异常,未加载完成。"); + this.logger.debug(e); + this.logger.warn("加载群列表出现异常,未加载完成。"); + return false; } + return true; } async function _getGML(group_id, next_uin) { @@ -247,7 +256,7 @@ async function getGI(group_id, no_cache = false) { } /** - * 群员信息 + * 群员资料 * @this {import("./ref").Client} * @returns {import("./ref").ProtocolResponse} */ @@ -309,7 +318,7 @@ async function getGMI(group_id, user_id, no_cache = false) { } /** - * 陌生人信息(也用于更新好友信息中的一些字段) + * 陌生人资料(也用于更新好友信息中的一些字段) * @this {import("./ref").Client} * @returns {import("./ref").ProtocolResponse} */ From 343193f03557fc0d6946b66eb5a8cda77cea80a8 Mon Sep 17 00:00:00 2001 From: takayama Date: Sat, 21 Nov 2020 11:18:54 +0900 Subject: [PATCH 04/11] update README.md --- README.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 54d26268..e270ba40 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,9 @@ ``` ```js -const oicq = require("oicq"); -const uin = 123456789; -const bot = oicq.createClient(uin); +const {createClient} = require("oicq"); +const uin = 123456789; // your account +const bot = createClient(uin); bot.on("system.login.captcha", ()=>{ process.stdin.once("data", input=>{ @@ -28,11 +28,18 @@ bot.on("system.login.captcha", ()=>{ }); }); -bot.on("message", data=>console.log(data)); +bot.on("system.online", ()=>console.log("bot online!")); +bot.on("message", data=>{ + console.log(data); + if (data.group_id > 0) + bot.sendGroupMsg(data.group_id, "hello"); + else + bot.sendPrivateMsg(data.user_id, "hello"); +}); bot.on("request", data=>console.log(data)); bot.on("notice", data=>console.log(data)); -const password_md5 = "202cb962ac59075b964b07152d234b70"; +const password_md5 = "202cb962ac59075b964b07152d234b70"; // your password md5 bot.login(password_md5); ``` From e51b12b99302fc517b3e4b1826bfc640782cb6ff Mon Sep 17 00:00:00 2001 From: takayama Date: Sat, 21 Nov 2020 13:02:08 +0900 Subject: [PATCH 05/11] add internal.wt.failed --- client.js | 66 +++++++++++++++++++++++------------------------ lib/core.js | 4 +-- lib/ref.d.ts | 1 - lib/wtlogin/wt.js | 14 +++------- 4 files changed, 38 insertions(+), 47 deletions(-) diff --git a/client.js b/client.js index d000162b..62fbf171 100644 --- a/client.js +++ b/client.js @@ -5,7 +5,7 @@ const fs = require("fs"); const path = require("path"); const os = require("os"); const spawn = require("child_process"); -const crypto = require("crypto"); +const {randomBytes} = require("crypto"); const log4js = require("log4js"); const device = require("./device"); const {checkUin, timestamp} = require("./lib/common"); @@ -40,27 +40,25 @@ class Client extends net.Socket { static ONLINE = Symbol("ONLINE"); } class AndroidClient extends Client { - reconn_flag = true; status = Client.OFFLINE; - nickname = ""; age = 0; sex = "unknown"; online_status = 0; - fl = new Map(); //friendList - sl = new Map(); //strangerList - gl = new Map(); //groupList - gml = new Map(); //groupMemberList + fl = new Map; //friendList + sl = new Map; //strangerList + gl = new Map; //groupList + gml = new Map; //groupMemberList recv_timestamp = 0; send_timestamp = 0xffffffff; heartbeat = null; seq_id = 0; - handlers = new Map(); - seq_cache = new Map(); + handlers = new Map; + seq_cache = new Map; - session_id = crypto.randomBytes(4); - random_key = crypto.randomBytes(16); + session_id = randomBytes(4); + random_key = randomBytes(16); sig = { srm_token: BUF0, @@ -80,9 +78,9 @@ class AndroidClient extends Client { sync_finished = false; sync_cookie; - const1 = crypto.randomBytes(4).readUInt32BE(); - const2 = crypto.randomBytes(4).readUInt32BE(); - const3 = crypto.randomBytes(1)[0]; + const1 = randomBytes(4).readUInt32BE(); + const2 = randomBytes(4).readUInt32BE(); + const3 = randomBytes(1)[0]; stat = { start_time: timestamp(), @@ -123,24 +121,21 @@ class AndroidClient extends Client { this.on("error", (err)=>{ this.logger.error(err.message); }); - this.on("close", (e_flag)=>{ + this.on("close", ()=>{ this.read(); - ++this.stat.lost_times; if (this.remoteAddress) this.logger.info(`${this.remoteAddress}:${this.remotePort} closed`); this.stopHeartbeat(); if (this.status === Client.OFFLINE) { this.logger.error("网络不通畅。"); return this.em("system.offline.network", {message: "网络不通畅"}); - } - this.status = Client.OFFLINE; - if (this.reconn_flag) { - if (e_flag) - this.reconn_flag = false; + } else if (this.status === Client.ONLINE) { + ++this.stat.lost_times; setTimeout(()=>{ this._connect(this.register.bind(this)); }, 500); } + this.status = Client.OFFLINE; }); // 在这里拆分包 @@ -149,7 +144,6 @@ class AndroidClient extends Client { let len_buf = this.read(4); let len = len_buf.readInt32BE(); if (this.readableLength >= len - 4) { - this.reconn_flag = true; this.recv_timestamp = Date.now(); const packet = this.read(len - 4); ++this.stat.recv_pkt_cnt; @@ -178,11 +172,16 @@ class AndroidClient extends Client { resource.initGL.call(this) ]); this.logger.info(`加载了${this.fl.size}个好友,${this.gl.size}个群。`); - await core.getMsg.call(this); this.sync_finished = true; this.logger.info("初始化完毕,开始处理消息。"); this.em("system.online"); }); + + this.on("internal.wt.failed", (message)=>{ + this.logger.error(message); + this.terminate(); + this.em("system.offline.network", {message}); + }); } _connect(callback = ()=>{}) { @@ -239,11 +238,12 @@ class AndroidClient extends Client { try { await wt.heartbeat.call(this); } catch { + core.getMsg.call(this); try { await wt.heartbeat.call(this); } catch { this.logger.warn("Heartbeat timeout!"); - if (Date.now() - this.recv_timestamp > 15000) + if (Date.now() - this.recv_timestamp > 3000) this.destroy(); } } @@ -259,24 +259,21 @@ class AndroidClient extends Client { if (!await wt.register.call(this)) throw new Error(); } catch (e) { - this.logger.error("上线失败。"); - this.terminate(); - this.em("system.offline.network", {message: "register失败"}); - return; + return this.emit("internal.wt.failed", "register失败。"); } this.status = Client.ONLINE; if (!this.online_status) this.online_status = 11; - if (this.platform === 1) - this.setOnlineStatus(this.online_status); + this.setOnlineStatus(this.online_status); this.startHeartbeat(); + await core.getMsg.call(this); if (!this.listenerCount("internal.kickoff")) { this.once("internal.kickoff", (data)=>{ this.status = Client.INIT; this.stopHeartbeat(); this.logger.warn(data.info); let sub_type; - if (data.info.includes("另一")) { + if (data.info.includes("一台")) { sub_type = "kickoff"; if (this.config.kickoff) { this.logger.info("3秒后重新连接.."); @@ -410,19 +407,20 @@ class AndroidClient extends Client { captchaLogin(captcha) { if (!this.captcha_sign) - return this.logger.error("未收到图片验证码或已过期,你不能调用captchaLogin函数。"); + return this.logger.warn("未收到图片验证码或已过期,你不能调用captchaLogin函数。"); this._connect(()=>{ wt.captchaLogin.call(this, captcha); }); } terminate() { - this.reconn_flag = false; + if (this.status === Client.ONLINE) + this.status = Client.INIT; this.destroy(); } async logout() { - if (this.isOnline) { + if (this.isOnline()) { try { await wt.register.call(this, true); } catch {} diff --git a/lib/core.js b/lib/core.js index 494a34e7..f195d9db 100644 --- a/lib/core.js +++ b/lib/core.js @@ -168,8 +168,8 @@ function onForceOffline(blob) { function onMSFOffline(blob) { const nested = jce.decodeWrapper(blob); const parent = jce.decode(nested); - if (parent[3].includes("如非本人操作,则密码可能已泄露")) - return; + // if (parent[3].includes("如非本人操作,则密码可能已泄露")) + // return; this.em("internal.kickoff", { type: "ReqMSFOffline", info: `[${parent[4]}]${parent[3]}`, diff --git a/lib/ref.d.ts b/lib/ref.d.ts index 2ef41165..41f3de3b 100644 --- a/lib/ref.d.ts +++ b/lib/ref.d.ts @@ -92,7 +92,6 @@ export interface Statistics { ////////// export class Client extends oicq.Client { - reconn_flag: boolean; config: oicq.ConfBot; status: Symbol; diff --git a/lib/wtlogin/wt.js b/lib/wtlogin/wt.js index 75f78fdf..a523c06f 100644 --- a/lib/wtlogin/wt.js +++ b/lib/wtlogin/wt.js @@ -163,9 +163,7 @@ async function passwordLogin() { try { var blob = await this.send(pkt); } catch (e) { - this.logger.error("未收到passwordLogin响应包。"); - this.terminate(); - return this.em("system.offline.network", {message: "passwordLogin失败"}); + return this.emit("internal.wt.failed", "未收到passwordLogin响应包。"); } decodeLoginResponse.call(this, blob); } @@ -193,9 +191,7 @@ async function captchaLogin(captcha) { try { var blob = await this.send(pkt); } catch (e) { - this.logger.error("未收到captchaLogin响应包。"); - this.terminate(); - return this.em("system.offline.network", {message: "captchaLogin失败"}); + return this.emit("internal.wt.failed", "未收到captchaLogin响应包。"); } decodeLoginResponse.call(this, blob); } @@ -218,9 +214,7 @@ async function deviceLogin() { try { var blob = await this.send(pkt); } catch (e) { - this.logger.error("未收到deviceLogin响应包。"); - this.terminate(); - return this.em("system.offline.network", {message: "deviceLogin失败"}); + return this.emit("internal.wt.failed", "未收到captchaLogin响应包。"); } decodeLoginResponse.call(this, blob); } @@ -432,7 +426,7 @@ function decodeLoginResponse(blob) { // if (t[0x161]) // decodeT161.call(this, t[0x161]); decodeT119.call(this, t[0x119]); - return this.em("internal.login"); + return this.emit("internal.login"); } if (type === 2) { this.t104 = t[0x104] From b732e45a0ca3b5b66f6c76f6911015b5caa2a818 Mon Sep 17 00:00:00 2001 From: takayama Date: Sun, 22 Nov 2020 05:13:45 +0900 Subject: [PATCH 06/11] parse dice rps --- docs/api.md | 12 ++++++------ lib/message/parser.js | 17 ++++++++++++++--- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/docs/api.md b/docs/api.md index 572160f1..998db2e3 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,4 +1,4 @@ -# 请参考参照头文件 [client.d.ts](../client.d.ts) +# 请参照头文件 [client.d.ts](../client.d.ts) ---- @@ -7,10 +7,10 @@ + [启动-创建实例](#createClient(uin[,config])) + [系统类API](#系统类API) + [应用类API](#应用类API) - + [获取列表和资料](#获取好友群群员列表和info) - + [发消息和撤回](#发私聊消息群消息) + + [获取列表和资料](#获取好友群群员列表和资料) + + [发消息和撤回](#发消息和撤回) + [群操作](#群操作踢人禁言退群设置等) - + [好友操作](#加群加好友删好友邀请好友入群点赞) + + [好友操作](#加群加好友删好友邀请好友点赞) + [设置状态和资料](#设置状态和资料) + [其他](#其他) @@ -83,7 +83,7 @@ const client = oicq.createClient(uin, config); ---- -### 获取好友、群、群员列表和info +### 获取好友、群、群员列表和资料 + `client.getFriendList()` + `client.getStrangerList()` @@ -98,7 +98,7 @@ const client = oicq.createClient(uin, config); ---- -### 发私聊消息、群消息 +### 发消息和撤回 message可以使用 `Array` 格式或 `String` 格式,支持CQ码 参考 [消息段类型](https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md) diff --git a/lib/message/parser.js b/lib/message/parser.js index 1f0ec1c2..6fe0f5fb 100644 --- a/lib/message/parser.js +++ b/lib/message/parser.js @@ -27,7 +27,7 @@ async function parseMessage(rich, from = 0) { let extra = {}, anon = {}; const chain = []; let raw_message = ""; - let bface_tmp = null, ignore_text = false; + let bface_tmp = null, bface_magic = null, ignore_text = false; for (let v of elems) { const type = parseInt(Object.keys(Reflect.getPrototypeOf(v))[0]); const msg = {type:"",data:{}}; @@ -74,9 +74,19 @@ async function parseMessage(rich, from = 0) { case 1: //text if (ignore_text) break; if (bface_tmp && o[1]) { - msg.data.file = bface_tmp, msg.type = "bface"; - msg.data.text = String(o[1].raw).replace("[","").replace("]","").trim(); + const text = String(o[1].raw).replace("[","").replace("]","").trim(); + if (text.includes("猜拳") && bface_magic) { + msg.type = "rps"; + msg.data.id = bface_magic.raw[16] - 0x30 + 1; + } else if (text.includes("骰子") && bface_magic) { + msg.type = "dice"; + msg.data.id = bface_magic.raw[16] - 0x30 + 1; + } else { + msg.data.file = bface_tmp, msg.type = "bface"; + msg.data.text = text; + } bface_tmp = null; + bface_magic = null; break; } if (o[3] && o[3].raw[1] === 1) { @@ -95,6 +105,7 @@ async function parseMessage(rich, from = 0) { break; case 6: //bface bface_tmp = o[4].raw.toString("hex") + o[7].raw.toString("hex") + o[5]; + bface_magic = o[12]; break; case 4: //notOnlineImage msg.type = "image"; From 843960d7c8f5170b5fac9143bb79ab6a9b0b38b4 Mon Sep 17 00:00:00 2001 From: takayama Date: Sun, 22 Nov 2020 06:41:22 +0900 Subject: [PATCH 07/11] add shake&poke --- lib/message/builder.js | 49 ++++++++++++++++++++++++++++++++++++++++-- lib/message/face.js | 22 ++++++++++++++++++- lib/message/parser.js | 20 +++++++++++++++-- 3 files changed, 86 insertions(+), 5 deletions(-) diff --git a/lib/message/builder.js b/lib/message/builder.js index f74fc1cc..775311f0 100644 --- a/lib/message/builder.js +++ b/lib/message/builder.js @@ -157,6 +157,8 @@ class Builder { } catch {} } buildMagicFaceElem(type, cq) { + if (!cq) + cq = {}; const rand = (a,b)=>Math.floor(Math.random()*(b-a)+a); if (type === "dice") { cq.text = "骰子"; @@ -266,7 +268,7 @@ class Builder { if (this.type && url && url.includes("gchatpic_new")) { const id = url.match(/-[0-9]+-/); if (id) - fid = parseInt(id[0].replace("-", "")) - 0xffffffff; + fid = parseInt(id[0].replace("-", "")); } if (!this.type && url && url.includes("offpic_new")) { const id = url.match(/\/\/[0-9]+-[0-9]+-[0-9A-Za-z]+/); @@ -431,7 +433,7 @@ class Builder { const {ignore} = cq; const rsp = await getAnonInfo.call(this.c, this.target); if (!rsp) { - if (ignore === "0") { + if (ignore == "0") { this.c.logger.warn("匿名失败,终止发送。"); throw new Error("匿名失败,终止发送。"); } @@ -495,6 +497,43 @@ class Builder { }); } + buildShakeElem() { + this.elems.push({ + 17: { + 1: 0, + 2: 0, + 3: this.target, + } + }); + ++this.stat.length; + } + buildPokeElem(cq) { + let {type, id} = cq; + type = parseInt(type); + id = parseInt(type); + const nested = { + 3: 0, + 4: null, + 5: null, + 6: null, + 7: 0, + 10: 0, + } + if (type === 126) { + nested[4] = id; + nested[5] = Buffer.from([230, 149, 178, 233, 151, 168]); + nested[6] = Buffer.from([55, 46, 50, 46, 48]); + } + this.elems.push({ + 53: { + 1: 2, + 2: nested, + 3: type, + } + }); + ++this.stat.length; + } + /** * @param {import("../../client").MessageElem} */ @@ -542,6 +581,12 @@ class Builder { case "reply": this.buildReplyElem(data); break; + case "shake": + this.buildShakeElem(); + break; + case "poke": + this.buildPokeElem(data); + break; default: this.c.logger.warn("未知的CQ码类型:" + type); break; diff --git a/lib/message/face.js b/lib/message/face.js index d42a3e90..f33ea6ae 100644 --- a/lib/message/face.js +++ b/lib/message/face.js @@ -23,6 +23,26 @@ const map = { 280: "/击掌", }; +const pokemap = { + 0: "回戳", + 1: "戳一戳", + 2: "比心", + 3: "点赞", + 4: "心碎", + 5: "666", + 6: "放大招", + 2000: "敲门", + 2001: "抓一下", + 2002: "碎屏", + 2003: "勾引", + 2004: "手雷", + 2005: "结印", + 2006: "召唤术", + 2007: "玫瑰花", + 2009: "让你皮", + 2011: "宝贝球", +} + module.exports = { - map, + map, pokemap }; diff --git a/lib/message/parser.js b/lib/message/parser.js index 6fe0f5fb..9fe1d540 100644 --- a/lib/message/parser.js +++ b/lib/message/parser.js @@ -55,6 +55,10 @@ async function parseMessage(rich, from = 0) { if (o[6] === 1 && o[7]) return await parseMultiMsg.call(this, o[7].raw, from); break; + case 17: + msg.type = "shake"; + ignore_text = true; + break; case 12: //xml try { [msg.type, msg.data] = await parseXmlElem.call(this, o); @@ -123,7 +127,7 @@ async function parseMessage(rich, from = 0) { else msg.data.url = `http://gchat.qpic.cn/gchatpic_new/0/${from}-${o[7]}-${o[13].raw.toString("hex").toUpperCase()}/0?term=2`; break; - case 53: + case 53: //commonElem if (o[1] === 3) { msg.type = "flash"; if (o[2][1]) //notOnlineImage @@ -138,6 +142,17 @@ async function parseMessage(rich, from = 0) { msg.data.text = face.map[msg.data.id]; else if (o[2][2]) msg.data.text = String(o[2][2].raw); + } else if (o[1] === 2) { + msg.type = "poke"; + msg.data.type = o[3]; + if (o[3] === 126) { + msg.data.id = o[2][4]; + msg.data.name = face.pokemap[o[2][4]]; + } else { + msg.data.id = -1; + msg.data.name = face.pokemap[o[3]]; + } + ignore_text = true; } break; case 9999: //ptt @@ -160,7 +175,8 @@ async function parseMessage(rich, from = 0) { } function genCQMsg(msg) { - return `[CQ:${msg.type},${querystring.stringify(msg.data, ",", "=", {encodeURIComponent: (s)=>s.replace(/&|,|\[|\]/g, escapeCQInside)})}]`; + const data = querystring.stringify(msg.data, ",", "=", {encodeURIComponent: (s)=>s.replace(/&|,|\[|\]/g, escapeCQInside)}); + return `[CQ:` + msg.type + (data ? "," : "") + data + `]`; } async function parseMultiMsg(resid, from) { From e15fea41bb10c708dd57426fe9b47a0a640d7f92 Mon Sep 17 00:00:00 2001 From: takayama Date: Sun, 22 Nov 2020 07:16:57 +0900 Subject: [PATCH 08/11] add json&xml --- docs/project.md | 15 ++++++++++----- lib/message/builder.js | 16 +++++++++++++--- lib/message/parser.js | 13 +++++++------ 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/docs/project.md b/docs/project.md index a276c9de..1b206c9b 100644 --- a/docs/project.md +++ b/docs/project.md @@ -78,20 +78,25 @@ CQ码是指字符串格式下用于表示多媒体内容的方式,形如: |[CQ码]|收|发|说明| |-|-|-|-| -|at|◯|◯|[CQ:at,qq=123456,text=@ABC,dummy=0]
text用来定义@不到时的输出
dummy设为1可以假@| +|at|◯|◯|[CQ:at,qq=123456,text=@ABC,dummy=0]
dummy设为1时假@| |face|◯|◯|[CQ:face,id=104]| |bface|◯|◯|原创表情,[CQ:bface,file=xxxxxxxx,text=摸头]| |dice&rps|◯|◯|骰子和猜拳:
[CQ:dice,id=1]
[CQ:rps,id=1]| |image|◯|◯|参考 [图片](https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md#%E5%9B%BE%E7%89%87)| |record|◯|◯|语音,写法同image
支持任何格式的音频自动转amr(必须将 [ffmpeg](http://ffmpeg.org/download.html) 加入环境变量path)
linux下的ffmpeg不自带amr解码器,可能需要自行编译ffmpeg| |flash|◯|◯|闪照,写法同image| -|anonymous||◯|发匿名,[CQ:anonymous,ignore=1]
ignore可省略,为0时匿名失败不发送| +|anonymous||◯|发匿名,[CQ:anonymous,ignore=1]
ignore为0时匿名失败不发送| |notice|◯||群公告| -|file|◯||群文件| +|file|◯|✕|群文件| |music|◯|◯|[CQ:music,type=qq,id=xxxxxx]
[CQ:music,type=163,id=xxxxxx]| -|video|✕|✕| |location|◯|◯|[CQ:location,address=江西省九江市修水县,lat=29.063940,lng=114.339610]| |contact|◯|✕|联系人或群推荐 -|reply|◯|◯|[CQ:reply,id=xxxxxx] +|reply|◯|◯|[CQ:reply,id=xxxxxx] 通过消息id回复 |share|◯|◯|链接分享 +|shake|◯|◯|[CQ:shake] +|poke|◯|◯|[CQ:poke,type=6] 暂时支持0~6,可以在群里发 +|xml&json|◯|◯|封杀比较严重,不推荐发原生 +|video|✕|✕| |node|✕|◯|[CQ:node,uin=123456789,name=昵称,content=消息内容,time=时间戳]
time可省略,暂时只支持纯文本/s>| + +更详细的参照[此文档](https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md) diff --git a/lib/message/builder.js b/lib/message/builder.js index 775311f0..eeff78d7 100644 --- a/lib/message/builder.js +++ b/lib/message/builder.js @@ -157,8 +157,6 @@ class Builder { } catch {} } buildMagicFaceElem(type, cq) { - if (!cq) - cq = {}; const rand = (a,b)=>Math.floor(Math.random()*(b-a)+a); if (type === "dice") { cq.text = "骰子"; @@ -392,9 +390,11 @@ class Builder { } buildJsonElem(obj, text) { + if (typeof obj !== "string") + obj = JSON.stringify(obj); const elems = [{ 51: { - 1: Buffer.concat([BUF1, zlib.deflateSync(JSON.stringify(obj))]) + 1: Buffer.concat([BUF1, zlib.deflateSync(obj)]) } }]; if (text) { @@ -538,6 +538,8 @@ class Builder { * @param {import("../../client").MessageElem} */ async buildElem(type, data) { + if (!data) + data = {}; switch (type) { case "text": this.buildTextElem(data.text); @@ -587,6 +589,14 @@ class Builder { case "poke": this.buildPokeElem(data); break; + case "json": + if (data.data) + this.buildJsonElem(data.data); + break; + case "xml": + if (data.data) + this.buildXmlElem(data.data, 60); + break; default: this.c.logger.warn("未知的CQ码类型:" + type); break; diff --git a/lib/message/parser.js b/lib/message/parser.js index 9fe1d540..08f59970 100644 --- a/lib/message/parser.js +++ b/lib/message/parser.js @@ -215,8 +215,8 @@ async function parseTransElem(o, from) { async function parseXmlElem(o) { //35合并转发 todo - const buf = zlib.unzipSync(o[1].raw.slice(1)); - const xml = cheerio.load(String(buf)); + const str = String(zlib.unzipSync(o[1].raw.slice(1))); + const xml = cheerio.load(str); if (o[2] === 14) { const data = { type: "qq", @@ -241,12 +241,12 @@ async function parseXmlElem(o) { } return ["share", data]; } else - throw new Error(); + return ["xml", {data: str}]; } async function parseJsonElem(o) { - o = JSON.parse(zlib.unzipSync(o[1].raw.slice(1))); - // console.log(o) + const str = String(zlib.unzipSync(o[1].raw.slice(1))); + o = JSON.parse(str); let type, data = {}; if (o.app === "com.tencent.map") { type = "location"; @@ -262,7 +262,8 @@ async function parseJsonElem(o) { type = "music"; data = music.parse(o); } else { - throw new Error("unknown json msg"); + type = "json"; + data.data = str; } return [type, data]; } From 08ddbc939cd1bb6cf5eff60649e6d17f8bf0f395 Mon Sep 17 00:00:00 2001 From: takayama Date: Sun, 22 Nov 2020 09:17:06 +0900 Subject: [PATCH 09/11] C2C file --- lib/message/parser.js | 29 +++++++++++++++++-- lib/message/recv.js | 65 ++++++++++++++---------------------------- lib/message/storage.js | 4 +-- 3 files changed, 49 insertions(+), 49 deletions(-) diff --git a/lib/message/parser.js b/lib/message/parser.js index 08f59970..eb562587 100644 --- a/lib/message/parser.js +++ b/lib/message/parser.js @@ -4,9 +4,9 @@ const cheerio = require("cheerio"); const querystring = require("querystring"); const music = require("./music"); const face = require("./face"); -const {downloadMultiMsg, getGroupFileUrl} = require("./storage"); +const {downloadMultiMsg, getGroupFileUrl, getC2CFileUrl} = require("./storage"); const pb = require("../pb"); -const {genC2CMessageId, genGroupMessageId} = require("../common"); +const {genC2CMessageId, genGroupMessageId, timestamp} = require("../common"); function escapeCQInside(s) { if (s === "&") return "&"; @@ -268,4 +268,27 @@ async function parseJsonElem(o) { return [type, data]; } -module.exports = parseMessage; +async function parseC2CFileElem(elem) { + const fileid = elem[3].raw, + md5 = elem[4].raw.toString("hex"), + name = String(elem[5].raw), + size = elem[6], + duration = elem[51] ? timestamp() + elem[51] : 0; + const url = await getC2CFileUrl.call(this, fileid); + const msg = { + type: "file", + data: { + name, url, size, md5, duration, + busid: "0", + fileid: String(fileid) + } + }; + const raw_message = genCQMsg(msg); + return { + raw_message, chain: [msg] + }; +} + +module.exports = { + parseMessage, parseC2CFileElem +}; diff --git a/lib/message/recv.js b/lib/message/recv.js index 966342e7..824fa15e 100644 --- a/lib/message/recv.js +++ b/lib/message/recv.js @@ -1,6 +1,5 @@ "use strict"; -const parseMessage = require("./parser"); -const {getPrivateFileUrl} = require("./storage"); +const {parseMessage, parseC2CFileMsg} = require("./parser"); const {genC2CMessageId, genGroupMessageId} = require("../common"); /** @@ -13,7 +12,7 @@ async function handlePrivateMsg(type, head, content, body) { const user_id = head[1], time = head[6], seq = head[5]; - let sub_type, message_id, font = "unknown"; + let sub_type, message_id = "", font = "unknown"; const sender = Object.assign({user_id}, this.fl.get(user_id)); if (type === 141) { @@ -36,52 +35,30 @@ async function handlePrivateMsg(type, head, content, body) { this.sl.set(user_id, stranger); } } + try { + random = body[1][1][3]; + message_id = genC2CMessageId(user_id, seq, random, time); + font = String(body[1][1][9].raw); + } catch {} if (type === 529) { - if (head[4] !== 4 || !body[2]) - return; try { - const fileid = body[2][1][3].raw, - md5 = body[2][1][4].raw.toString("hex"), - name = String(body[2][1][5].raw), - size = body[2][1][6], - duration = body[2][1][51] ? time + body[2][1][51] : 0; - const url = await getPrivateFileUrl.call(this, fileid); - const raw_message = `[CQ:file,url=${url},size=${size},md5=${md5},duration=${duration},busid=0,fileid=${fileid}]`; - this.logger.info(`recv from: [Private: ${user_id}(${sub_type})] ` + raw_message); - this.em("message.private." + sub_type, { - message_id: "", user_id, - message: [{ - type: "file", - data: { - url, size, md5, duration, - busid: "0", - fileid: String(fileid) - } - }], - raw_message, font, sender, time - }); - } catch (e) {} - return; - } - if (body[1] && body[1][2]) { - let random = seq; - if (body[1][1]) { - font = String(body[1][1][9].raw); - random = body[1][1][3]; - } - message_id = genC2CMessageId(user_id, seq, random, time); + if (head[4] !== 4) + return; + var {chain, raw_message} = await parseC2CFileElem.call(this, body[2][1]); + } catch (e) {return} + } else if (body[1] && body[1][2]) { try { var {chain, raw_message} = await parseMessage.call(this, body[1], user_id); } catch (e) {return} - if (raw_message) { - this.logger.info(`recv from: [Private: ${user_id}(${sub_type})] ` + raw_message); - this.em("message.private." + sub_type, { - message_id, user_id, - message: chain, - raw_message, font, sender, time, - auto_reply: !!(content&&content[4]) - }); - } + } + if (raw_message) { + this.logger.info(`recv from: [Private: ${user_id}(${sub_type})] ` + raw_message); + this.em("message.private." + sub_type, { + message_id, user_id, + message: chain, + raw_message, font, sender, time, + auto_reply: !!(content&&content[4]) + }); } } diff --git a/lib/message/storage.js b/lib/message/storage.js index ac9a9af0..943fae52 100644 --- a/lib/message/storage.js +++ b/lib/message/storage.js @@ -359,7 +359,7 @@ async function getGroupFileUrl(group_id, busid, fileid) { * @this {import("../ref").Client} * @param {Buffer|String} fileid */ -async function getPrivateFileUrl(fileid) { +async function getC2CFileUrl(fileid) { const body = pb.encode({ 1: 1200, 14: { @@ -400,6 +400,6 @@ async function getAnonInfo(group_id) { } module.exports = { - uploadImages, uploadPtt, uploadMultiMsg, downloadMultiMsg, getGroupFileUrl, getPrivateFileUrl, + uploadImages, uploadPtt, uploadMultiMsg, downloadMultiMsg, getGroupFileUrl, getC2CFileUrl, setPrivateImageNested, setGroupImageNested, getAnonInfo } From 1239ac9aec75e251622742221a84ce5e404af523 Mon Sep 17 00:00:00 2001 From: takayama Date: Sun, 22 Nov 2020 09:48:12 +0900 Subject: [PATCH 10/11] =?UTF-8?q?=E4=B8=8D=E8=A7=A3=E6=9E=90=E6=94=B6?= =?UTF-8?q?=E5=88=B0=E7=9A=84xml&json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/project.md | 12 +++---- lib/message/parser.js | 82 +++++++++++++++++++++---------------------- 2 files changed, 46 insertions(+), 48 deletions(-) diff --git a/docs/project.md b/docs/project.md index 1b206c9b..b998a990 100644 --- a/docs/project.md +++ b/docs/project.md @@ -86,17 +86,15 @@ CQ码是指字符串格式下用于表示多媒体内容的方式,形如: |record|◯|◯|语音,写法同image
支持任何格式的音频自动转amr(必须将 [ffmpeg](http://ffmpeg.org/download.html) 加入环境变量path)
linux下的ffmpeg不自带amr解码器,可能需要自行编译ffmpeg| |flash|◯|◯|闪照,写法同image| |anonymous||◯|发匿名,[CQ:anonymous,ignore=1]
ignore为0时匿名失败不发送| -|notice|◯||群公告| |file|◯|✕|群文件| -|music|◯|◯|[CQ:music,type=qq,id=xxxxxx]
[CQ:music,type=163,id=xxxxxx]| -|location|◯|◯|[CQ:location,address=江西省九江市修水县,lat=29.063940,lng=114.339610]| -|contact|◯|✕|联系人或群推荐 +|music|json|◯|[CQ:music,type=qq,id=xxxxxx]
[CQ:music,type=163,id=xxxxxx]| +|location|json|◯|[CQ:location,address=江西省九江市修水县,lat=29.063940,lng=114.339610]| |reply|◯|◯|[CQ:reply,id=xxxxxx] 通过消息id回复 -|share|◯|◯|链接分享 |shake|◯|◯|[CQ:shake] |poke|◯|◯|[CQ:poke,type=6] 暂时支持0~6,可以在群里发 -|xml&json|◯|◯|封杀比较严重,不推荐发原生 +|xml&json|◯|◯|可用于接收群公告等消息。封杀比较严重,不推荐发原生。 +|share|xml|◯|链接分享 |video|✕|✕| |node|✕|◯|[CQ:node,uin=123456789,name=昵称,content=消息内容,time=时间戳]
time可省略,暂时只支持纯文本/s>| -更详细的参照[此文档](https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md) +有不明白的参考[此文档](https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md) diff --git a/lib/message/parser.js b/lib/message/parser.js index eb562587..b3c9125b 100644 --- a/lib/message/parser.js +++ b/lib/message/parser.js @@ -216,55 +216,55 @@ async function parseTransElem(o, from) { async function parseXmlElem(o) { //35合并转发 todo const str = String(zlib.unzipSync(o[1].raw.slice(1))); - const xml = cheerio.load(str); - if (o[2] === 14) { - const data = { - type: "qq", - id: xml("summary").last().text().replace("帐号:", ""), - name: xml("title").last().text(), - } - return ["contact", data]; - } else if (o[2] === 15) { - const data = { - type: "group", - id: xml("msg").attr("i_actiondata").replace("group:", ""), - name: xml("msg").attr("brief").replace("推荐群聊:", ""), - url: xml("msg").attr("url").replace("&", "&"), - } - return ["contact", data]; - } else if (o[2] === 1) { - const data = { - url: xml("msg").attr("url"), - title: xml("title").text(), - content: xml("summary").text(), - image: xml("picture") ? xml("picture").attr("cover") : undefined - } - return ["share", data]; - } else + // const xml = cheerio.load(str); + // if (o[2] === 14) { + // const data = { + // type: "qq", + // id: xml("summary").last().text().replace("帐号:", ""), + // name: xml("title").last().text(), + // } + // return ["contact", data]; + // } else if (o[2] === 15) { + // const data = { + // type: "group", + // id: xml("msg").attr("i_actiondata").replace("group:", ""), + // name: xml("msg").attr("brief").replace("推荐群聊:", ""), + // url: xml("msg").attr("url").replace("&", "&"), + // } + // return ["contact", data]; + // } else if (o[2] === 1) { + // const data = { + // url: xml("msg").attr("url"), + // title: xml("title").text(), + // content: xml("summary").text(), + // image: xml("picture") ? xml("picture").attr("cover") : undefined + // } + // return ["share", data]; + // } else return ["xml", {data: str}]; } async function parseJsonElem(o) { const str = String(zlib.unzipSync(o[1].raw.slice(1))); - o = JSON.parse(str); + // o = JSON.parse(str); let type, data = {}; - if (o.app === "com.tencent.map") { - type = "location"; - data = o.meta["Location.Search"]; - if (!data.id) - delete data.id; - delete data.from; - } else if (o.app === "com.tencent.mannounce") { - type = "notice"; - data.title = Buffer.from(o.meta.mannounce.title, "base64").toString(); - data.content = Buffer.from(o.meta.mannounce.text, "base64").toString(); - } else if (o.app === "com.tencent.structmsg" && o.view === "music") { - type = "music"; - data = music.parse(o); - } else { + // if (o.app === "com.tencent.map") { + // type = "location"; + // data = o.meta["Location.Search"]; + // if (!data.id) + // delete data.id; + // delete data.from; + // } else if (o.app === "com.tencent.mannounce") { + // type = "notice"; + // data.title = Buffer.from(o.meta.mannounce.title, "base64").toString(); + // data.content = Buffer.from(o.meta.mannounce.text, "base64").toString(); + // } else if (o.app === "com.tencent.structmsg" && o.view === "music") { + // type = "music"; + // data = music.parse(o); + // } else { type = "json"; data.data = str; - } + // } return [type, data]; } From 3a0cf370e267b6cbd4b948a23b1d9664bbca35e9 Mon Sep 17 00:00:00 2001 From: takayama Date: Sun, 22 Nov 2020 12:50:02 +0900 Subject: [PATCH 11/11] up ver to 1.10.1. --- client.js | 10 +++++++++- lib/ref.d.ts | 1 + package-lock.json | 2 +- package.json | 3 ++- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/client.js b/client.js index 62fbf171..5d0ff1c5 100644 --- a/client.js +++ b/client.js @@ -87,6 +87,7 @@ class AndroidClient extends Client { lost_times: 0, recv_pkt_cnt: 0, sent_pkt_cnt: 0, + lost_pkt_cnt: 0, recv_msg_cnt: 0, sent_msg_cnt: 0, }; @@ -211,6 +212,7 @@ class AndroidClient extends Client { this.write(packet, ()=>{ const id = setTimeout(()=>{ this.handlers.delete(seq_id); + ++this.stat.lost_pkt_cnt; reject(new TimeoutError()); this.em("internal.timeout", {seq_id}); }, timeout); @@ -659,6 +661,7 @@ class AndroidClient extends Client { status: this.online_status, msg_cnt_per_min: this.calcMsgCnt(), statistics: this.stat, + config: this.config }) } getLoginInfo() { @@ -679,7 +682,12 @@ process.OICQ = { logger }; -console.log("OICQ程序启动。当前内核版本:v" + version.version); +console.log(` +########################################################################### +# Package Version: oicq@${version.version} (Release on ${version.upday}) +# View Changelogs:https://github.com/takayama-lily/oicq/releases # +########################################################################### +`); function createDataDir(dir, uin) { if (!fs.existsSync(dir)) diff --git a/lib/ref.d.ts b/lib/ref.d.ts index 41f3de3b..c276a975 100644 --- a/lib/ref.d.ts +++ b/lib/ref.d.ts @@ -85,6 +85,7 @@ export interface Statistics { lost_times: number, recv_pkt_cnt: number, sent_pkt_cnt: number, + lost_pkt_cnt: number, //超时未响应的包 recv_msg_cnt: number, sent_msg_cnt: number, } diff --git a/package-lock.json b/package-lock.json index dc2b00cf..3c195472 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "oicq", - "version": "1.10.9", + "version": "1.10.10", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 4eb8f1fc..c0cb8c1f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "oicq", - "version": "1.10.9", + "version": "1.10.10", + "upday": "2020/11/22", "description": "QQ protocol!", "main": "client.js", "scripts": {