From ab6363c77f40b8349bd57681a3def599a817cf3a Mon Sep 17 00:00:00 2001 From: weixin_46064585 Date: Thu, 6 Jun 2024 10:44:00 +0800 Subject: [PATCH] Thu Jun 6 10:44:00 CST 2024 inscode --- define.js | 64 + demo.js | 406 +++++++ helper.js | 103 ++ index.js | 3299 ++++++++++++++++++++++++++++++++++++++++++++------ package.json | 1 + 5 files changed, 3521 insertions(+), 352 deletions(-) create mode 100644 define.js create mode 100644 demo.js create mode 100644 helper.js diff --git a/define.js b/define.js new file mode 100644 index 0000000..1083195 --- /dev/null +++ b/define.js @@ -0,0 +1,64 @@ +'use strict' + +exports.wsEventType = [ + 'log', + 'cmdRet', + 'userEvent', + 'sysEvent', +] + +exports.userEvents = [ + 'qrcode', // 登陆二维码 + 'scan', // 扫码登陆状态 + 'login', // 登陆完成 + 'loaded', // 通讯录载入完毕 + 'logout', // 注销登录(账户退出) + 'close', // 任务断线 + 'warn', // 错误 + 'sns', // 朋友圈事件(朋友圈小圆点) + 'notify', // 推送消息通知 + 'autoLogin', // 自动重连成功通知 + 'push', // 推送消息(系统、好友消息等) +] + +exports.loginType = { + auto : 'auto', // 断线重连 + request: 'request', // 二次登陆 + qrcode : 'qrcode', // 扫码登陆 + phone : 'phone', // 手机验证码登陆 + user : 'user', // 账号密码登陆 +} + +exports.blacklist = [ + 'appbrandcustomerservicemsg', + 'blogapp', + 'cardpackage', + 'facebookapp', + 'feedsapp', + 'filehelper', + 'floatbottle', + 'floatbottle', // 漂流瓶 + 'fmessage', // 朋友推荐 + 'helper_entry', + 'lbsapp', + 'linkedinplugin', + 'masssendapp', + 'medianote', // 语音记事本 + 'mphelper', // 公众平台安全助手 + 'newsapp', // 腾讯新闻 + 'notification_messages', + 'notifymessage', + 'officialaccounts', + 'pc_share', + 'qmessage', // qq离线消息 + 'qqfriend', + 'qqmail', + 'qqsync', + 'shakeapp', + 'tmessage', + 'voiceinputapp', + 'voicevoipapp', + 'voipapp', + 'weibo', // 微博-未知 + 'weixin', // 腾讯团队 +] \ No newline at end of file diff --git a/demo.js b/demo.js new file mode 100644 index 0000000..e664c80 --- /dev/null +++ b/demo.js @@ -0,0 +1,406 @@ +'use strict' + +const log4js = require('log4js') +const Padchat = require('./index') +const fs = require('fs') +const util = require('util') +const qrcode = require('qrcode-terminal') + +/** +* 创建日志目录 +*/ + +try { + require('fs').mkdirSync('./logs') +} catch (e) { + if (e.code !== 'EEXIST') { + console.error('Could not set up log directory, error: ', e) + process.exit(1) + } +} + +// try { +// log4js.configure('./log4js.json') +// } catch (e) { +// console.error('载入log4js日志输出配置错误: ', e) +// process.exit(1); +// } + +const logger = log4js.getLogger('app') +const dLog = log4js.getLogger('dev') + +logger.info('demo start!') + +const autoData = { + wxData: '', + token: '', +} +let server = '' +server = 'ws://47.99.211.34:8181/' +// 永久免费使用 + +try { + const tmpBuf = fs.readFileSync('./config.json') + const data = JSON.parse(String(tmpBuf)) + autoData.wxData = data.wxData + autoData.token = data.token + logger.info('载入设备参数与自动登陆数据:%o ', autoData) +} catch (e) { + logger.warn('没有在本地发现设备登录参数或解析数据失败!如首次登录请忽略!') +} + +const wx = new Padchat(server) +logger.info('当前连接接口服务器为:', server) +let disconnectCount = 0 // 断开计数 +let connected = false // 成功连接标志 + +wx + .on('close', (code, msg) => { + // 需要注意关闭代码为3201-3203的错误,重连也用,具体参考文档中`close`事件说明 + if (code > 3200) { + logger.warn(`Websocket已关闭!code: ${code} - ${msg}`) + return + } + logger.info(`Websocket已关闭!code: ${code} - ${msg}`) + // 根据是否成功连接过判断本次是未能连接成功还是与服务器连接中断 + if (connected) { + connected = false + disconnectCount++ + logger.info(`第 ${disconnectCount} 次与服务器连接断开!现在将重试连接服务器。`) + } else { + logger.debug(`未能连接服务器!将重试连接服务器。`) + } + // 重新启动websocket连接 + wx.start() + }) + .on('open', async () => { + let ret + logger.info('连接成功!') + connected = true + + // 非首次登录时最好使用以前成功登录时使用的设备参数, + // 否则可能会被tx服务器怀疑账号被盗,导致手机端被登出 + ret = await wx.init() + if (!ret.success) { + logger.error('新建任务失败!', ret) + return + } + logger.info('新建任务成功, json: ', ret) + + //先尝试使用断线重连方式登陆 + if (autoData.token) { + ret = await wx.login('auto', autoData) + if (ret.success) { + logger.info('断线重连请求成功!', ret) + return + } + logger.warn('断线重连请求失败!', ret) + + ret = await wx.login('request', autoData) + if (ret.success) { + logger.info('自动登录请求成功!', ret) + return + } + logger.warn('自动登录请求失败!', ret) + } + + ret = await wx.login('qrcode') + if (!ret.success) { + logger.error('使用qrcode登录模式失败!', ret) + return + } + logger.info('使用qrcode登录模式!') + }) + .on('qrcode', data => { + // 如果存在url,则直接在终端中生成二维码并显示 + if (data.url) { + logger.info(`登陆二维码内容为: "${data.url}",请使用微信扫描下方二维码登陆!`) + qrcode.generate(data.url, { small: true }) + } else { + logger.error(`未能获得登陆二维码!`) + } + }) + .on('scan', data => { + switch (data.status) { + case 0: + logger.info('等待扫码...', data) + break; + case 1: + // { + // status : 1, + // expiredTime: 239, + // headUrl : 'http://wx.qlogo.cn/mmhead/ver_1/xxxxxxx/0', //头像url + // nickName : '复仇者联盟' //昵称 + // } + logger.info('已扫码,请在手机端确认登陆...', data) + break; + case 2: + // { + // password : '***hide***', // 可忽略 + // status : 2, + // expiredTime: 238, + // headUrl : 'http://wx.qlogo.cn/mmhead/ver_1/xxxxxxx/0', //头像url + // subStatus : 0 // 登陆操作状态码 + // 以下字段仅在登录成功时有效 + // external : '1', + // email : '', + // uin : 149806460, // 微信账号uin,全局唯一 + // deviceType : 'android', // 登陆的主设备类型 + // nickName : '复仇者联盟' //昵称 + // userName : 'wxid_xxxxxx', // 微信账号id,全局唯一 + // phoneNumber: '18012345678', // 微信账号绑定的手机号 + // } + switch (data.subStatus) { + case 0: + logger.info('扫码成功!登陆成功!', data) + break; + case 1: + logger.info('扫码成功!登陆失败!', data) + break; + default: + logger.info('扫码成功!未知状态码!', data) + break; + } + break; + // 如果等待登陆超时或手机上点击了取消登陆,需要重新调用登陆 + case 3: + logger.info('二维码已过期!请重新调用登陆接口!', data) + break; + case 4: + logger.info('手机端已取消登陆!请重新调用登陆接口!', data) + break; + default: + logger.warn('未知登陆状态!请重新调用登陆接口!', data) + break; + } + }) + .on('login', async () => { + logger.info('微信账号登陆成功!') + let ret + + ret = await wx.getMyInfo() + logger.info('当前账号信息:', ret.data) + + // 如果不想同步通讯录信息,可关闭同步通讯录 + // await wx.setSyncContact(false) + + // 主动同步通讯录 + await wx.syncContact() + + await saveAutoData() + }) + .on('autoLogin', async () => { + // 自动重连后需要保存新的自动登陆数据 + await saveAutoData() + }) + .on('logout', ({ msg }) => { + logger.info('微信账号已退出!', msg) + }) + .on('over', ({ msg }) => { + logger.info('任务实例已关闭!', msg) + }) + .on('loaded', async () => { + logger.info('通讯录同步完毕!') + + // 主动触发同步消息 + await wx.syncMsg() + + const ret = await wx.sendMsg('filehelper', '你登录了!') + logger.info('发送信息结果:', ret) + }) + .on('sns', (data, msg) => { + logger.info('收到朋友圈事件!请查看朋友圈新消息哦!', msg) + }) + + .on('contact', async data => { + logger.info('收到推送联系人:%s - %s\n', data.userName, data.nickName, JSON.stringify(data)) + }) + .on('push', async data => { + // 消息类型 data.mType + // 1 文字消息 + // 3 收到图片消息 + // 34 语音消息 + // 35 用户头像buf + // 37 收到好友请求消息 + // 42 名片消息 + // 43 视频消息 + // 47 表情消息 + // 48 定位消息 + // 49 APP消息(文件 或者 链接 H5) + // 50 语音通话 + // 51 状态通知(如打开与好友/群的聊天界面) + // 52 语音通话通知 + // 53 语音通话邀请 + // 62 小视频 + // 2000 转账消息 + // 2001 收到红包消息 + // 3000 群邀请 + // 9999 系统通知 + // 10000 微信通知信息. 微信群信息变更通知,多为群名修改,进群,离群信息,不包含群内聊天信息 + // 10002 撤回消息 + // -------------------------------- + // 注意,如果是来自微信群的消息,data.content字段中包含发言人的wxid及其发言内容,需要自行提取 + // 各类复杂消息,data.content中是xml格式的文本内容,需要自行从中提取各类数据。(如好友请求) + let rawFile + switch (data.mType) { + case 3: + logger.info('收到来自 %s 的图片消息,包含图片数据:%s,xml内容:\n%s', data.fromUser, !!data.data, data.content) + rawFile = data.data || null + logger.info('图片缩略图数据base64尺寸:%d', rawFile.length) + await wx.getMsgImage(data) + .then(ret => { + rawFile = ret.data.image || '' + logger.info('获取消息原始图片结果:%s, 获得图片base64尺寸:%d', ret.success, rawFile.length) + }) + logger.info('图片数据base64尺寸:%d', rawFile.length) + await wx.sendImage('filehelper', rawFile) + .then(ret => { + logger.info('转发图片信息给 %s 结果:', 'filehelper', ret) + }) + .catch(e => { + logger.warn('转发图片信息异常:', e.message) + }) + break + + case 43: + logger.info('收到来自 %s 的视频消息,包含视频数据:%s,xml内容:\n%s', data.fromUser, !!data.data, data.content) + rawFile = data.data || null + if (!rawFile) { + await wx.getMsgVideo(data) + .then(ret => { + rawFile = ret.data.video || '' + logger.info('获取消息原始视频结果:%s, 获得视频base64尺寸:%d', ret.success, rawFile.length) + }) + } + logger.info('视频数据base64尺寸:%d', rawFile.length) + break + + case 1: + if (data.fromUser === 'newsapp') { // 腾讯新闻发的信息太长 + break + } + logger.info('收到来自 %s 的文本消息:', data.fromUser, data.description || data.content) + if (/ding/.test(data.content)) { + await wx.sendMsg(data.fromUser, 'dong. receive:' + data.content) + .then(ret => { + logger.info('回复信息给%s 结果:', data.fromUser, ret) + }) + .catch(e => { + logger.warn('回复信息异常:', e.message) + }) + } else if (/^#.*/.test(data.content) || /^[\w]*:\n#.*/.test(data.content)) { + await onMsg(data) + .catch(e => { + logger.warn('处理信息异常:', e) + }) + } + break + + case 34: + logger.info('收到来自 %s 的语音消息,包含语音数据:%s,xml内容:\n%s', data.fromUser, !!data.data, data.content) + // 超过30Kb的语音数据不会包含在推送信息中,需要主动拉取 + rawFile = data.data || null + if (!rawFile) { + // BUG: 超过60Kb的语音数据,只能拉取到60Kb,也就是说大约36~40秒以上的语音会丢失后边部分语音内容 + await wx.getMsgVoice(data) + .then(ret => { + rawFile = ret.data.voice || '' + logger.info('获取消息原始语音结果:%s, 获得语音base64尺寸:%d,拉取到数据尺寸:%d', ret.success, rawFile.length, ret.data.size) + }) + } + logger.info('语音数据base64尺寸:%d', rawFile.length) + if (rawFile.length > 0) { + let match = data.content.match(/length="(\d+)"/) || [] + const length = match[1] || 0 + match = data.content.match(/voicelength="(\d+)"/) || [] + const ms = match[1] || 0 + logger.info('语音数据语音长度:%d ms,xml内记录尺寸:%d', ms, length) + + await wx.sendVoice('filehelper', rawFile, ms) + .then(ret => { + logger.info('转发语音信息给 %s 结果:', 'filehelper', ret) + }) + .catch(e => { + logger.warn('转发语音信息异常:', e.message) + }) + } + break + + case 49: + logger.info('收到一条来自 %s 的appmsg富媒体消息:', data.fromUser, data) + break + + case 10002: + if (data.fromUser === 'weixin') { + //每次登陆,会收到一条系统垃圾推送,过滤掉 + break + } + logger.info('用户 %s 撤回了一条消息:', data.fromUser, data) + break + + default: + logger.info('收到推送消息:', data) + break + } + }) + .on('error', e => { + logger.error('ws 错误:', e.message) + }) + .on('warn', e => { + logger.error('任务出现错误:', e.message) + }) + +/** + * @description 保存自动登陆数据 + */ +async function saveAutoData() { + let ret = await wx.getWxData() + if (!ret.success) { + logger.warn('获取设备参数未成功! json:', ret) + return + } + logger.info('获取设备参数成功, json: ', ret) + Object.assign(autoData, { wxData: ret.data.wxData }) + + ret = await wx.getLoginToken() + if (!ret.success) { + logger.warn('获取自动登陆数据未成功! json:', ret) + return + } + logger.info('获取自动登陆数据成功, json: ', ret) + Object.assign(autoData, { token: ret.data.token }) + + // NOTE: 这里将设备参数保存到本地,以后再次登录此账号时提供相同参数 + fs.writeFileSync('./config.json', JSON.stringify(autoData, null, 2)) + logger.info('设备参数已写入到 ./config.json文件') +} + +async function onMsg(data) { + const content = data.content.replace(/^[\w:\n]*#/m, '') + let [cmd, ...args] = content.split('\n') + + args = args.map(str => { + try { + str = JSON.parse(str) + } catch (e) { + } + return str + }) + if (cmd && wx[cmd] && typeof wx[cmd] === 'function') { + logger.info('执行函数 %s,参数:', cmd, args) + await wx[cmd](...args) + .then(ret => { + logger.info('执行函数 %s 结果:%o', cmd, ret) + }) + .catch(e => { + logger.warn('执行函数 %s 异常:', e) + }) + } +} +process.on('uncaughtException', e => { + logger.error('Main', 'uncaughtException:', e) +}) + +process.on('unhandledRejection', e => { + logger.error('Main', 'unhandledRejection:', e) +}) \ No newline at end of file diff --git a/helper.js b/helper.js new file mode 100644 index 0000000..a7c8c28 --- /dev/null +++ b/helper.js @@ -0,0 +1,103 @@ +'use strict' + +// 将object中的属性名称转换为全驼峰格式 + + +module.exports = { + toCamelCase, + toUnderLine, + structureXml, +} + +/** + * 将object中的属性名称从下划线转换为驼峰格式 + * (包含子数据的属性名,会遍历转换) + * + * @param {any} obj 要转换的object + * @param {boolean} [big=true] 是否转换为大驼峰格式 + * @returns {Object} 转换后的object + */ +function toCamelCase(obj, big = false) { + if (obj instanceof Array) { + return obj.map(item => toCamelCase(item)) + } + if (!(obj instanceof Object)) { + return obj + } + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + let newKey = key.replace(/_(\w)/g, (match, val, offset) => { return val.toUpperCase() }) + if (big) { + newKey = newKey.replace(/^(\w)/, (match, val, offset) => { return val.toUpperCase() }) + } else { + // 将首字母转换为小写(部分字段名称开头两个为大写,比如`PYInitial`/`MMBizMenu`) + // 跳过`X-WECHAT-KEY`和`X-WECHAT-UIN`这种字段 + newKey = newKey.replace(/^(\w{2})/, (match, val, offset) => { return val.toLowerCase() }) + } + if ((obj[key] instanceof Array) || (obj[key] instanceof Object)) { + obj[key] = toCamelCase(obj[key]) + } + if (newKey !== key) { + obj[newKey] = obj[key] + delete obj[key] + } + } + } + return obj +} + +/** + * 将object中的属性名称从驼峰转换为下划线格式 + * + * @param {any} obj 要转换的object + * @returns {Object} 转换后的object + */ +function toUnderLine(obj) { + const newObj = {} + if (!(obj instanceof Object)) { + return obj + } + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + const newKey = key.replace(/([A-Z])/g, (match, val, offset) => { return (offset ? '_' : '') + val }).toLowerCase() + newObj[newKey] = obj[key] + } + } + return newObj +} + +/** + * 组装appmsg消息体 + * + * @param {object} object 消息体参数 + * @param {object} [object.appid] - appid,忽略即可 + * @param {object} [object.sdkver] - sdk版本,忽略即可 + * @param {object} [object.title] - 标题 + * @param {object} [object.des] - 描述 + * @param {object} [object.url] - 链接url + * @param {object} [object.thumburl] - 缩略图url + * ``` + * { + * appid = '', //appid,忽略即可 + * sdkver = '', //sdk版本,忽略即可 + * title = '', //标题 + * des = '', //描述 + * url = '', //链接url + * thumburl = '', //缩略图url + * } + * ``` + * @returns {String} 组装的消息体 + */ +function structureXml(object) { + const { appid = '', sdkver = '', title = '', des = '', url = '', thumburl = '' } = object + return ` +${title} +${des} +view +5 +0 + +${url} +${thumburl} +`.replace(/\n/g, '') +} \ No newline at end of file diff --git a/index.js b/index.js index e664c80..bd1daed 100644 --- a/index.js +++ b/index.js @@ -1,406 +1,3001 @@ 'use strict' -const log4js = require('log4js') -const Padchat = require('./index') -const fs = require('fs') -const util = require('util') -const qrcode = require('qrcode-terminal') +const EventEmitter = require('events') +const Websocket = require('ws') +const UUID = require('uuid') + +const Helper = require('./helper') +const { + wsEventType, + loginType, + blacklist, +} = require('./define') + +const server = 'ws://127.0.0.1:7777' /** -* 创建日志目录 -*/ + * Padchat模块 + * + * 使用websocket与服务器进行通讯,拥有以下事件 + * + * Event | 说明 + * ---- | ---- + * qrcode | 推送的二维码 + * scan | 扫码状态 + * push | 新信息事件 + * login | 登录 + * loaded | 通讯录载入完毕 + * logout | 注销登录 + * over | 实例注销(账号不退出)(要再登录需要重新调用init) + * warn | 错误信息 + * sns | 朋友圈更新事件 + * + * **接口返回数据结构:** 所有接口均返回以下结构数据: + * ``` + { + success: true, // 执行是否成功 + err : '', // 错误提示 + msg : '', // 附加信息 + data : {} // 返回结果 + } + * ``` + * + * @class Padchat + * @extends {EventEmitter} + */ +class Padchat extends EventEmitter { + /** + * Creates an instance of Padchat. + * @param {string} [url] - 服务器url,后缀你的token:`ws://127.0.0.1:7777/{YourToken}` + * 如:`ws://127.0.0.1:7777/mytoken_123456` + * @memberof Padchat + */ + constructor(url = server) { + super() + this.url = url + // 向ws服务器提交指令后,返回结果的超时时间,单位毫秒 + this.sendTimeout = 30 * 1000 + this.connected = false + this._lastStartTime = 0 + this.ws = {} + this.openSyncMsg = true //是否同步消息 + this.openSyncContact = true //是否同步联系人 + this.loaded = false //通讯录载入完毕 + this.cmdSeq = 1 + this._starting = false + this.start() + } -try { - require('fs').mkdirSync('./logs') -} catch (e) { - if (e.code !== 'EEXIST') { - console.error('Could not set up log directory, error: ', e) - process.exit(1) + /** + * 启动websocket连接 + * + * @memberof Padchat + */ + async start() { + if (this._starting) { + return + } + this._starting = true + // 限制启动ws连接间隔时间 + if (Date.now() - this._lastStartTime < 200) { + await sleep(1000) + } + this._lastStartTime = Date.now() + if (this.ws instanceof Websocket && this.ws.readyState === this.ws.OPEN) { + this.ws.terminate() + } + this.ws = new Websocket(this.url) + .on('message', (msg) => { + onWsMsg.call(this, msg) + }) + .on('open', () => { + this.connected = true + /** + * Open event + * websocket连接打开事件 + * + * @event Padchat#open + * @example + * const wx = new Padchat() + * wx.on('open',()=>{ + * console.log(`连接成功!`) + * }) + * + * @memberof Padchat + */ + this.emit('open') + }) + .on('close', (code = 0, msg = '') => { + this.connected = false + /** + * Close event + * websocket连接关闭事件。可在此事件中调用`Padchat.start()`发起重连 + * + * @event Padchat#close + * @property {number} code - 关闭代码 + *
`3201`: 你的Token是无效的,请联系我们获取有效的Token,以连接padchat-sdk + *
`3202`: 你的Token已经登录了一个机器人,请不要用同一个Token登录多个机器人 + *
`3203`: 你的Token已经过期了,请联系我们进行续费 + * @property {string} [msg] - 关闭说明 + * @example + * const wx = new Padchat() + * wx.on('close',(code,msg)=>{ + * console.log(`Websocket 已关闭!code: ${code} - ${msg}`) + * wx.start() + * }) + * + * @memberof Padchat + */ + this.emit('close', code, msg) + }) + .on('error', (e) => { + /** + * Error event + * 错误事件 + * + * @event Padchat#error + * @property {error} error - 报错信息 + * @example + * const wx = new Padchat() + * wx.on('error',e=>{ + * console.log('Websocket 错误:', e.message) + * }) + * + * @memberof Padchat + */ + this.emit('error', e) + }) + this._starting = false } -} -// try { -// log4js.configure('./log4js.json') -// } catch (e) { -// console.error('载入log4js日志输出配置错误: ', e) -// process.exit(1); -// } + /** + * ws发送数据 + * + * @param {object} data - 数据 + * @returns {Promise} 返回ws处理结果 + * @private + * @memberof Padchat + */ + async _send(data) { + return new Promise((resolve, reject) => { + if (!this.connected || !(this.ws instanceof Websocket)) { + reject('websocket未连接!') + } + this.ws.send(JSON.stringify(data), e => { + if (e) { + reject(new Error(`ws发送数据失败! err: ${e.message}`)) + } else { + resolve(true) + } + }) + }) + } -const logger = log4js.getLogger('app') -const dLog = log4js.getLogger('dev') + /** + * 包装ws发送数据 + * + * @param {object} data - 要发送的数据 + * @param {number} timeout - 发送超时时间 + * @returns {Promise} 返回ws处理结果 + * @private + * @memberof Padchat + */ + async asyncSend(data, timeout = this.sendTimeout) { + if (!data.cmdId) { + data.cmdId = '' + this.cmdSeq++ + if (this.cmdSeq >= 0xffffff00) { + this.cmdSeq = 1 + } + } + return new Promise((res, rej) => { + try { + getCmdRecv.call(this, data.cmdId, timeout) + .then(data => { + // console.info('getCmdRecv ret data:', data) + res(data.payload) + }) + this._send(data) + .then(async ret => { + // console.info('asyncSend ret: %s', ret) + return ret + }) + } catch (e) { + rej(e) + } + }) + } -logger.info('demo start!') + /** + * 包装ws发送指令数据包 + * + * @param {string} cmd - 要操作的接口 + * @param {object} data - 要发送的数据 + * @returns {Promise} 返回ws处理结果 + * @private + * @memberof Padchat + */ + async sendCmd(cmd, data = {}) { + if (data.rawMsgData) { + // 清洗掉无用而占空间的字段 + data.rawMsgData = clearRawMsg(data.rawMsgData) + data.rawMsgData = Helper.toUnderLine(data.rawMsgData) + } -const autoData = { - wxData: '', - token: '', -} -let server = '' -server = 'ws://47.99.211.34:8181/' -// 永久免费使用 - -try { - const tmpBuf = fs.readFileSync('./config.json') - const data = JSON.parse(String(tmpBuf)) - autoData.wxData = data.wxData - autoData.token = data.token - logger.info('载入设备参数与自动登陆数据:%o ', autoData) -} catch (e) { - logger.warn('没有在本地发现设备登录参数或解析数据失败!如首次登录请忽略!') -} + return await this.asyncSend({ + type: 'user', + cmd, + payload: data, + }) + .then(ret => { + // 用于抓取操作接口对应的返回数据,便于写入文档 + this.emit('cmdRet', cmd, ret) + return ret + }) + .catch(e => { + throw e + }) + } + + /** + * 初始化 + * + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + error : '', + success: true + } + * ``` + * @memberof Padchat + */ + async init() { + return await this.sendCmd('init') + } -const wx = new Padchat(server) -logger.info('当前连接接口服务器为:', server) -let disconnectCount = 0 // 断开计数 -let connected = false // 成功连接标志 + /** + * 关闭微信实例(不退出登陆) + * + * @returns {Promise} 返回Promise,注意捕捉catch + * @memberof Padchat + */ + async close() { + return await this.sendCmd('close') + } -wx - .on('close', (code, msg) => { - // 需要注意关闭代码为3201-3203的错误,重连也用,具体参考文档中`close`事件说明 - if (code > 3200) { - logger.warn(`Websocket已关闭!code: ${code} - ${msg}`) - return + /** + * 登录账号 + * 首次登录不需要传入`wxData`,登陆成功后本地保存`wxData`和`token`,以后使用断线重连或二次登陆,可降低封号概率。 + * 任何登陆方式,使用成功登陆过的`wxData`都可降低封号概率。 + * + * @param {string} type - 登录类型,默认为扫码登录 + *
`auto` **断线重连**,用于短时间使用`wxData`和`token`再次登录。`token`有效期很短,如果登陆失败,建议使用二次登陆方式 + *
`request` **二次登陆**。需要提供`wxData`和`token`数据,手机端会弹出确认框,点击后登陆。不容易封号 + *
`qrcode` **扫码登录**(现在此模式已经可以返回二维码内容的url了) + *
`phone` **手机验证码登录**,建议配合`wxData`使用 + *
`user` **账号密码登录**,建议配合`wxData`使用 + * + * @param {object} data - 附加数据 + * @param {string} [data.wxData] - 设备信息数据,登录后使用 `getDeviceInfo` 接口获得。
使用此数据可免设备安全验证,不容易封号 + * @param {string} [data.token] - 使用用任意方式登录成功后,使用 `getAutoLoginData` 接口获得。
此token有过期时间,断开登录状态一段时间后会过期。 + * @param {string} [data.phone] - 手机号 + * @param {string} [data.code] - 手机验证码 + * @param {string} [data.username] - 用户名/qq号/手机号 + * @param {string} [data.password] - 密码 + * + * @example 扫码登陆 + * const wx = new Padchat() + * wx.on('open',()=>{ + * await wx.init() + * await wx.login('qrcode',{wxData:'xxx'}) + * }) + * + * @example 账号密码登陆 + * const wx = new Padchat() + * wx.on('open',()=>{ + * await wx.init() + * await wx.login('user',{wxData:'xxx',username:'name',password:'123456'}) + * }) + * + * @example 手机验证码 + * const wx = new Padchat() + * wx.on('open',()=>{ + * await wx.init() + * await wx.login('phone',{wxData:'xxx',phone:'13512345678',code:'123456'}) + * }) + * + * @example 断线重连 + * const wx = new Padchat() + * wx.on('open',()=>{ + * await wx.init() + * await wx.login('auto',{wxData:'xxx',token:'xxxxx'}) + * }) + * + * @example 二次登陆 + * const wx = new Padchat() + * wx.on('open',()=>{ + * await wx.init() + * await wx.login('request',{wxData:'xxx',token:'xxxxx'}) + * }) + * + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + error : '', + msg : '请使用手机微信扫码登陆!', + success: true + } + * ``` + * @memberof Padchat + */ + async login(type = 'qrcode', data = {}) { + const _data = { + loginType: '', + wxData : data.wxData || null, } - logger.info(`Websocket已关闭!code: ${code} - ${msg}`) - // 根据是否成功连接过判断本次是未能连接成功还是与服务器连接中断 - if (connected) { - connected = false - disconnectCount++ - logger.info(`第 ${disconnectCount} 次与服务器连接断开!现在将重试连接服务器。`) - } else { - logger.debug(`未能连接服务器!将重试连接服务器。`) + if (!loginType[type]) { + throw new Error('login type error!') } - // 重新启动websocket连接 - wx.start() - }) - .on('open', async () => { - let ret - logger.info('连接成功!') - connected = true - - // 非首次登录时最好使用以前成功登录时使用的设备参数, - // 否则可能会被tx服务器怀疑账号被盗,导致手机端被登出 - ret = await wx.init() - if (!ret.success) { - logger.error('新建任务失败!', ret) - return + + switch (type) { + case loginType.auto: + case loginType.request: + if (!data.token || !data.wxData) { + throw new Error('login data error!') + } + _data.token = data.token || null + break + case loginType.phone: + if (!data.phone) { + // code + throw new Error('login data error!') + } + _data.phone = data.phone + _data.code = data.code + break + case loginType.user: + if (!data.username || !data.password) { + throw new Error('login data error!') + } + _data.username = data.username + _data.password = data.password + break + default: + break } - logger.info('新建任务成功, json: ', ret) + _data.loginType = loginType[type] + return await this.sendCmd('login', _data) + } + + /** + * 获取设备62数据 + * + * 事实上,只要你有一次登陆成功,以后一直用这个62数据,不需要更换。 + * + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + error: '', success: true, + data : + { + wxData: 'xxxxx' //设备62数据 + } + } + * ``` + * @memberof Padchat + */ + async getWxData() { + return await this.sendCmd('getWxData', {}) + } - //先尝试使用断线重连方式登陆 - if (autoData.token) { - ret = await wx.login('auto', autoData) - if (ret.success) { - logger.info('断线重连请求成功!', ret) - return + /** + * 获取二次登陆数据 + * + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + error : '', + success: true, + data : + { + message: '', + status : 0, + token : 'xxxx', //二次登陆token + uin : 14900000 //微信号uin,唯一值 } - logger.warn('断线重连请求失败!', ret) + } + * ``` + * @memberof Padchat + */ + async getLoginToken() { + return await this.sendCmd('getLoginToken', {}) + } - ret = await wx.login('request', autoData) - if (ret.success) { - logger.info('自动登录请求成功!', ret) - return + /** + * 获取微信号信息 + * + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + error : '', + success: true, + data: + { + userName: 'wxid_xxxx', //微信号id,注意不一定是微信号,全局唯一 + uin : 101234567 //微信号uin,全局唯一 } - logger.warn('自动登录请求失败!', ret) + } + * ``` + * @memberof Padchat + */ + async getMyInfo() { + return await this.sendCmd('getMyInfo') + } + + /** + * 设置是否同步联系人 + * + * @memberof Padchat + */ + setSyncContact(open) { + this.openSyncContact = !!open + } + + /** + * 设置是否同步消息 + * + * @memberof Padchat + */ + setSyncMsg(open) { + this.openSyncMsg = !!open + } + + /** + * 同步消息 + * + * 使用此接口手动触发同步消息,一般用于刚登陆后调用,可立即开始同步消息。 + * 否则会在有新消息时才开始同步消息。 + * + * @returns {Promise} 返回Promise,注意捕捉catch + * @memberof Padchat + */ + async syncMsg() { + return await this.sendCmd('syncMsg') + } + + /** + * 同步通讯录 + * + * 使用此接口可以触发同步通讯录,如果设置`reset`为`true`,则在通讯录同步完毕后触发`loaded`事件 + * + * @param {boolean} [reset=false] 是否重置通讯录加载完毕状态 + * @returns {Promise} 返回Promise,注意捕捉catch + * @memberof Padchat + */ + async syncContact(reset = false) { + if (reset) { + this.loaded = false } + return await this.sendCmd('syncContact') + } - ret = await wx.login('qrcode') - if (!ret.success) { - logger.error('使用qrcode登录模式失败!', ret) - return + /** + * 退出登录 + * + * @returns {Promise} 返回Promise,注意捕捉catch + * @memberof Padchat + */ + async logout() { + return await this.sendCmd('logout', {}) + } + + /** + * 关闭推送 + * + * @description 仅用于开发调试,不需要使用 + * @private + * @returns {Promise} 返回Promise,注意捕捉catch + * @memberof Padchat + */ + async closePush() { + return await this.sendCmd('closePush', {}) + } + + /** + * 启动心跳 + * + * @description 仅用于开发调试,不需要使用 + * @private + * @returns {Promise} 返回Promise,注意捕捉catch + * @memberof Padchat + */ + async heartBeat() { + return await this.sendCmd('heartBeat', {}) + } + + /** + * 发送文字信息 + * + * @param {string} toUserName - 接收者的wxid + * @param {string} content - 内容文本 + * @param {Array} [atList=[]] - 向群内发信息时,要@的用户wxid数组。 + * 内容文本中要有@同样数量的用户昵称,不足时,将自动前缀空白的@符号及换行符 + * + * @example at群成员示例 - content内@了群成员昵称 + * wx.sendMsg('123456@chatroom','@nickname1 @nickname2 message body',['wxid1','wxid2']) + * // 显示内容: + * // `@nickname1 @nickname2 message body` + * + * @example at群成员示例 - content内遗漏@群成员: + * wx.sendMsg('123456@chatroom','@nickname1 message body',['wxid1','wxid2','wxid3']) + * // 显示内容: + * // `@@ + * // @nickname1 message body` + * + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + error: '', success: true, + data : { + message: '', + msgId : '5172746684759824075', + status : 0 } - logger.info('使用qrcode登录模式!') - }) - .on('qrcode', data => { - // 如果存在url,则直接在终端中生成二维码并显示 - if (data.url) { - logger.info(`登陆二维码内容为: "${data.url}",请使用微信扫描下方二维码登陆!`) - qrcode.generate(data.url, { small: true }) - } else { - logger.error(`未能获得登陆二维码!`) + } + * ``` + * @memberof Padchat + */ + async sendMsg(toUserName, content, atList = []) { + if (Array.isArray(atList) && atList.length > 0) { + const arr = content.match(/@/g) + const num = Array.isArray(arr) ? arr.length : 0 + if (num < atList.length) { + content = '@'.repeat(atList.length - num) + '\n' + content + } } - }) - .on('scan', data => { - switch (data.status) { - case 0: - logger.info('等待扫码...', data) - break; - case 1: - // { - // status : 1, - // expiredTime: 239, - // headUrl : 'http://wx.qlogo.cn/mmhead/ver_1/xxxxxxx/0', //头像url - // nickName : '复仇者联盟' //昵称 - // } - logger.info('已扫码,请在手机端确认登陆...', data) - break; - case 2: - // { - // password : '***hide***', // 可忽略 - // status : 2, - // expiredTime: 238, - // headUrl : 'http://wx.qlogo.cn/mmhead/ver_1/xxxxxxx/0', //头像url - // subStatus : 0 // 登陆操作状态码 - // 以下字段仅在登录成功时有效 - // external : '1', - // email : '', - // uin : 149806460, // 微信账号uin,全局唯一 - // deviceType : 'android', // 登陆的主设备类型 - // nickName : '复仇者联盟' //昵称 - // userName : 'wxid_xxxxxx', // 微信账号id,全局唯一 - // phoneNumber: '18012345678', // 微信账号绑定的手机号 - // } - switch (data.subStatus) { - case 0: - logger.info('扫码成功!登陆成功!', data) - break; - case 1: - logger.info('扫码成功!登陆失败!', data) - break; - default: - logger.info('扫码成功!未知状态码!', data) - break; - } - break; - // 如果等待登陆超时或手机上点击了取消登陆,需要重新调用登陆 - case 3: - logger.info('二维码已过期!请重新调用登陆接口!', data) - break; - case 4: - logger.info('手机端已取消登陆!请重新调用登陆接口!', data) - break; - default: - logger.warn('未知登陆状态!请重新调用登陆接口!', data) - break; + return await this.sendCmd('sendMsg', { + toUserName, + content, + atList, + }) + } + + /** + * 群发文字信息 + * + * FIXME: 此接口有问题,暂停使用 + * + * @param {Array} [userList=[]] - 接收者wxid数组 + * @param {string} content - 内容文本 + * @returns {Promise} 返回Promise,注意捕捉catch + * @deprecated + * @memberof Padchat + */ + async massMsg(userList = [], content) { + return new Error('此接口存在问题,停用!') + // return await this.sendCmd('massMsg', { + // userList, + // content, + // }) + } + + /** + * 发送App消息(含小程序) + * + * @param {string} toUserName - 接收者的wxid + * @param {object|string} content - app消息体文本(appmsg xml结构) + * @param {object} object - app消息体对象(消息体文本和对象二选一) + * @param {string} [object.appid] - appid,忽略即可 + * @param {string} [object.sdkver] - sdk版本,忽略即可 + * @param {string} [object.title] - 标题 + * @param {string} [object.des] - 描述 + * @param {string} [object.url] - 链接url + * @param {string} [object.thumburl] - 缩略图url + * + * @example 发送app消息对象 + * // 直接在第二个参数中传入消息体对象 + * await wx.sendAppMsg('filehelper',{ + appid = '', //appid,忽略即可 + sdkver = '', //sdk版本,忽略即可 + title = '', //标题 + des = '', //描述 + url = '', //链接url + thumburl = '', //缩略图url + }) + * + * @example 发送app消息体文本 + * // 如第二个参数传入非空文本,则忽略第三个参数 + * await wx.sendAppMsg('filehelper','标题描述view50http://wx.qq.comhttp://wx.qq.com/logo.png') + * + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + error: '', success: true, + data : { + message: '', + msgId : '2195811529497100215', + status : 0 } - }) - .on('login', async () => { - logger.info('微信账号登陆成功!') - let ret + } + * ``` + * @memberof Padchat + */ + async sendAppMsg(toUserName, content, object = {}) { + if (!content) { + content = object + } + if (typeof (content) === 'object') { + content = Helper.structureXml(content) + } + return await this.sendCmd('sendAppMsg', { + toUserName, + content, + }) + } + + /** + * 分享名片 + * + * @param {string} toUserName - 接收者的wxid + * @param {string} content - 内容文本 + * @param {string} userId - 被分享人wxid + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + error: '', success: true, + data : { + message: '', + msgId : '1797099903789182796', + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async shareCard(toUserName, content, userId) { + return await this.sendCmd('shareCard', { + toUserName, + content, + userId, + }) + } + + /** + * @returns {Promise} 返回Promise,注意捕捉catch + * @private + * @memberof Padchat + */ + async dumpWxuser() { + return await this.sendCmd('dumpWxuser', { + }) + } - ret = await wx.getMyInfo() - logger.info('当前账号信息:', ret.data) + /** + * 发送图片消息 + * + * @param {string} toUserName - 接收者的wxid + * @param {Buffer|string} file - 图片Buffer数据或base64 + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + error: '', success: true, + data : { + message: '', + msgId : '1797099903789182796', + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async sendImage(toUserName, file) { + if (file instanceof Buffer) { + file = file.toString('base64') + } + return await this.sendCmd('sendImage', { + toUserName, + file, + }) + } - // 如果不想同步通讯录信息,可关闭同步通讯录 - // await wx.setSyncContact(false) + /** + * 发送语音消息 + * 注意:只能发送silk格式的语音文件 + * + * @param {string} toUserName - 接收者的wxid + * @param {Buffer|string} file - 语音Buffer数据或base64 + * @param {number} time - 语音时间,单位为毫秒 + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + error: '', success: true, + data : { + data : 2490, //语音文件尺寸 + message: '', + msgId : '136722815749654341', + size : 0, + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async sendVoice(toUserName, file, time = 0) { + if (file instanceof Buffer) { + file = file.toString('base64') + } + return await this.sendCmd('sendVoice', { + toUserName, + file, + time: time * 1 + }) + } - // 主动同步通讯录 - await wx.syncContact() + /** + * 获取消息原始图片 + * + * 在push事件中收到的data数据是缩略图图片数据,使用本接口获取原图数据 + * + * @param {object} rawMsgData - 推送的消息结构体,即`push`事件中收到的Object + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : + { + image : 'base64_xxxx', //base64编码的原图数据 + message: '', + size : 8139, //图片数据尺寸 + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async getMsgImage(rawMsgData) { + return await this.sendCmd('getMsgImage', { + rawMsgData, + }) + } - await saveAutoData() - }) - .on('autoLogin', async () => { - // 自动重连后需要保存新的自动登陆数据 - await saveAutoData() - }) - .on('logout', ({ msg }) => { - logger.info('微信账号已退出!', msg) - }) - .on('over', ({ msg }) => { - logger.info('任务实例已关闭!', msg) - }) - .on('loaded', async () => { - logger.info('通讯录同步完毕!') + /** + * 获取消息原始视频 + * + * 在push事件中只获得推送通知,不包含视频数据,需要使用本接口获取视频文件数据 + * + * @param {object} rawMsgData - 推送的消息结构体,即`push`事件中收到的Object + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : + { + message: '', + size : 160036, //视频数据尺寸 + status : 0, + video : 'base64_xxxx' //base64编码的视频数据 + } + } + * ``` + * @memberof Padchat + */ - // 主动触发同步消息 - await wx.syncMsg() + async getMsgVideo(rawMsgData) { + return await this.sendCmd('getMsgVideo', { + rawMsgData, + }) + } - const ret = await wx.sendMsg('filehelper', '你登录了!') - logger.info('发送信息结果:', ret) - }) - .on('sns', (data, msg) => { - logger.info('收到朋友圈事件!请查看朋友圈新消息哦!', msg) - }) + /** + * 获取消息语音数据 + * + * 这个接口获取到的与push事件中接收到的数据一致,是base64编码的silk格式语音数据 + * + * BUG: 超过60Kb的语音数据,只能拉取到60Kb,也就是说大约36~40秒以上的语音会丢失后边部分语音内容 + * + * @param {object} rawMsgData - 推送的消息结构体,即`push`事件中收到的Object + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : + { + message: '', + size : 2490, //语音数据尺寸 + status : 0, + voice : 'base64_xxxx' //base64编码的语音数据 + } + } + * ``` + * @memberof Padchat + */ + async getMsgVoice(rawMsgData) { + return await this.sendCmd('getMsgVoice', { + rawMsgData, + }) + } - .on('contact', async data => { - logger.info('收到推送联系人:%s - %s\n', data.userName, data.nickName, JSON.stringify(data)) - }) - .on('push', async data => { - // 消息类型 data.mType - // 1 文字消息 - // 3 收到图片消息 - // 34 语音消息 - // 35 用户头像buf - // 37 收到好友请求消息 - // 42 名片消息 - // 43 视频消息 - // 47 表情消息 - // 48 定位消息 - // 49 APP消息(文件 或者 链接 H5) - // 50 语音通话 - // 51 状态通知(如打开与好友/群的聊天界面) - // 52 语音通话通知 - // 53 语音通话邀请 - // 62 小视频 - // 2000 转账消息 - // 2001 收到红包消息 - // 3000 群邀请 - // 9999 系统通知 - // 10000 微信通知信息. 微信群信息变更通知,多为群名修改,进群,离群信息,不包含群内聊天信息 - // 10002 撤回消息 - // -------------------------------- - // 注意,如果是来自微信群的消息,data.content字段中包含发言人的wxid及其发言内容,需要自行提取 - // 各类复杂消息,data.content中是xml格式的文本内容,需要自行从中提取各类数据。(如好友请求) - let rawFile - switch (data.mType) { - case 3: - logger.info('收到来自 %s 的图片消息,包含图片数据:%s,xml内容:\n%s', data.fromUser, !!data.data, data.content) - rawFile = data.data || null - logger.info('图片缩略图数据base64尺寸:%d', rawFile.length) - await wx.getMsgImage(data) - .then(ret => { - rawFile = ret.data.image || '' - logger.info('获取消息原始图片结果:%s, 获得图片base64尺寸:%d', ret.success, rawFile.length) - }) - logger.info('图片数据base64尺寸:%d', rawFile.length) - await wx.sendImage('filehelper', rawFile) - .then(ret => { - logger.info('转发图片信息给 %s 结果:', 'filehelper', ret) - }) - .catch(e => { - logger.warn('转发图片信息异常:', e.message) - }) - break + /** + * 创建群 + * + * 注意:如果有用户存在问题不能进群,则会建群失败。 + * 但判断是否成功应以`userName`字段 + * + * @param {string[]} userList - 用户wxid数组 + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : + { + message : 'Everything is OK', //操作结果提示,失败为`MemberList are wrong` + status : 0, + userName: '5658541000@chatroom' //如果建群成功,则返回群id + } + } + * ``` + * @memberof Padchat + */ + async createRoom(userList) { + return await this.sendCmd('createRoom', { + userList, + }) + } - case 43: - logger.info('收到来自 %s 的视频消息,包含视频数据:%s,xml内容:\n%s', data.fromUser, !!data.data, data.content) - rawFile = data.data || null - if (!rawFile) { - await wx.getMsgVideo(data) - .then(ret => { - rawFile = ret.data.video || '' - logger.info('获取消息原始视频结果:%s, 获得视频base64尺寸:%d', ret.success, rawFile.length) - }) - } - logger.info('视频数据base64尺寸:%d', rawFile.length) - break + /** + * 获取群成员信息 + * + * @param {string} groupId - 群id + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : + { + chatroomId: 700000001, + count : 3, //群成员数量 + member : //群成员列表 + [{ + bigHead : 'http://wx.qlogo.cn/xxx/0', //大头像url + chatroomNickName: '', //群内昵称 + invitedBy : 'binsee', //进群邀请人 + nickName : '小木匠', //昵称 + smallHead : 'http://wx.qlogo.cn/xxx/132', //小头像url + userName : 'wxid_xxxx' //wxid + }], + message : '', + status : 0, + userName: '5658541000@chatroom' //群id + } + } + * ``` + * @memberof Padchat + */ + async getRoomMembers(groupId) { + return await this.sendCmd('getRoomMembers', { + groupId, + }) + } - case 1: - if (data.fromUser === 'newsapp') { // 腾讯新闻发的信息太长 - break - } - logger.info('收到来自 %s 的文本消息:', data.fromUser, data.description || data.content) - if (/ding/.test(data.content)) { - await wx.sendMsg(data.fromUser, 'dong. receive:' + data.content) - .then(ret => { - logger.info('回复信息给%s 结果:', data.fromUser, ret) - }) - .catch(e => { - logger.warn('回复信息异常:', e.message) - }) - } else if (/^#.*/.test(data.content) || /^[\w]*:\n#.*/.test(data.content)) { - await onMsg(data) - .catch(e => { - logger.warn('处理信息异常:', e) - }) - } - break + /** + * 添加群成员 + * + * @param {string} groupId - 群id + * @param {string} userId - 用户wxid + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : { + message: 'Everything is OK', //失败为`MemberList are wrong` + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async addRoomMember(groupId, userId) { + return await this.sendCmd('addRoomMember', { + groupId, + userId, + }) + } - case 34: - logger.info('收到来自 %s 的语音消息,包含语音数据:%s,xml内容:\n%s', data.fromUser, !!data.data, data.content) - // 超过30Kb的语音数据不会包含在推送信息中,需要主动拉取 - rawFile = data.data || null - if (!rawFile) { - // BUG: 超过60Kb的语音数据,只能拉取到60Kb,也就是说大约36~40秒以上的语音会丢失后边部分语音内容 - await wx.getMsgVoice(data) - .then(ret => { - rawFile = ret.data.voice || '' - logger.info('获取消息原始语音结果:%s, 获得语音base64尺寸:%d,拉取到数据尺寸:%d', ret.success, rawFile.length, ret.data.size) - }) - } - logger.info('语音数据base64尺寸:%d', rawFile.length) - if (rawFile.length > 0) { - let match = data.content.match(/length="(\d+)"/) || [] - const length = match[1] || 0 - match = data.content.match(/voicelength="(\d+)"/) || [] - const ms = match[1] || 0 - logger.info('语音数据语音长度:%d ms,xml内记录尺寸:%d', ms, length) - - await wx.sendVoice('filehelper', rawFile, ms) - .then(ret => { - logger.info('转发语音信息给 %s 结果:', 'filehelper', ret) - }) - .catch(e => { - logger.warn('转发语音信息异常:', e.message) - }) + /** + * 邀请群成员 + * 会给对方发送一条邀请消息,无法判断对方是否真的接收到 + * + * @param {string} groupId - 群id + * @param {string} userId - 用户wxid + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : { + message: '', + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async inviteRoomMember(groupId, userId) { + return await this.sendCmd('inviteRoomMember', { + groupId, + userId, + }) + } + + /** + * 删除群成员 + * + * @param {string} groupId - 群id + * @param {string} userId - 用户wxid + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : { + message: '', + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async deleteRoomMember(groupId, userId) { + return await this.sendCmd('deleteRoomMember', { + groupId, + userId, + }) + } + + /** + * 退出群 + * + * @param {string} groupId - 群id + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : { + message: '', + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async quitRoom(groupId) { + return await this.sendCmd('quitRoom', { + groupId, + }) + } + + /** + * 设置群公告 + * + * @param {string} groupId - 群id + * @param {string} content - 群公告内容 + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : { + message: '', + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async setRoomAnnouncement(groupId, content) { + return await this.sendCmd('setRoomAnnouncement', { + groupId, + content, + }) + } + + /** + * 设置群名称 + * + * @param {string} groupId - 群id + * @param {string} content - 群名称 + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : { + message: '', + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async setRoomName(groupId, content) { + return await this.sendCmd('setRoomName', { + groupId, + content, + }) + } + + /** + * 获取微信群二维码 + * + * @param {string} groupId - 群id + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : + { + footer : '该二维码7天内(4月13日前)有效,重新进入将更新', + message: '', + qrCode : '', //进群二维码图片base64 + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async getRoomQrcode(groupId) { + return await this.sendCmd('getRoomQrcode', { + groupId, + style: 0, + }) + } + + /** + * 获取用户/群信息 + * + * @param {string} userId - 用户wxid/群id + * @returns {Promise} 返回Promise,注意捕捉catch + * + * 微信用户/公众号返回: + * + * ``` + { + success: true, + data : + { + bigHead : 'http://wx.qlogo.cn/xxx/0', //大头像url + city : 'mesa', //城市 + country : 'CN', //国家 + intro : '', //简介(公众号主体) + label : '', //(标签) + message : '', + nickName : '杉木', //昵称 + provincia : 'Henan', //省份 + pyInitial : 'SM', //昵称拼音简写 + quanPin : 'shamu', //昵称拼音 + remark : '', //备注 + remarkPyInitial: '', //备注拼音简写 + remarkQuanPin : '', //备注拼音 + sex : 1, //性别:1男2女 + signature : '签名', //个性签名 + smallHead : 'http://wx.qlogo.cn/xxx/132', //小头像url + status : 0, + stranger : 'v1_xxx@stranger', //用户v1码,从未加过好友则为空 + ticket : 'v2_xxx@stranger', //用户v2码,如果非空则为单向好友(非对方好友) + userName : 'binxxx' //用户wxid + } + } + * ``` + * + * 微信群返回: + * + * ``` + { + success: true, + data : { + city : '', + country : '', + intro : '', + label : '', + member : [], //群成员wxid数组 + message : '', + provincia : '', + remark : '', + sex : 0, + signature : '', + status : 0, + stranger : 'v1_xxx@stranger', + ticket : '', + bigHead : '', + chatroomId : 700001234, + chatroomOwner : 'wxid_xxx', + maxMemberCount : 500, //群最大人数 + memberCount : 377, //群当前人数 + nickName : 'Wechaty Developers\' Home', //群名称 + pyInitial : 'WECHATYDEVELOPERSHOME', + quanPin : 'WechatyDevelopersHome', + remarkPyInitial: '', + remarkQuanPin : '', + smallHead : 'http://wx.qlogo.cn/xxx/0', //群头像url + userName : '1234567890@chatroom' + } + } + * ``` + * @memberof Padchat + */ + async getContact(userId) { + return await this.sendCmd('getContact', { + userId, + }) + } + + /** + * 搜索用户 + * 可用此接口来判断是否已经加对方为好友 + * + * @param {string} userId - 用户wxid + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : + { + bigHead : 'http://wx.qlogo.cn/xxx/0', //大头像url + city : 'mesa', //城市 + country : 'CN', //国家 + message : '', + nickName : '杉木', //昵称 + provincia: 'Henan', //省份 + pyInitial: 'SM', //昵称拼音简写 + quanPin : 'shamu', //昵称拼音 + sex : 1, //性别:1男2女 + signature: '签名', //个性签名 + smallHead: 'http://wx.qlogo.cn/xxx/132', //小头像url + status : 0, + stranger : 'v1_xxx@stranger', //好友为空,非好友显示v2码 + userName : 'binxxx' //是自己好友显示wxid,非好友为v1码 + } + } + * ``` + * @memberof Padchat + */ + async searchContact(userId) { + return await this.sendCmd('searchContact', { + userId, + }) + } + + /** + * 删除好友 + * + * @param {string} userId - 用户wxid + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : { + message: '', + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async deleteContact(userId) { + return await this.sendCmd('deleteContact', { + userId, + }) + } + + /** + * 获取用户二维码 + * 仅限获取自己的二维码,无法获取其他人的二维码 + * + * @param {string} userId - 用户wxid + * @param {Number} style - 二维码风格。可用范围0-3 + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : + { + footer : '', + message: '', + qrCode : '', //二维码图片base64 + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async getContactQrcode(userId, style = 0) { + return await this.sendCmd('getUserQrcode', { + userId, + style, + }) + } + + /** + * 通过好友验证 + * + * @param {string} stranger - 用户stranger数据 + * @param {string} ticket - 用户ticket数据 + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : { + message: '', + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async acceptUser(stranger, ticket) { + return await this.sendCmd('acceptUser', { + stranger, + ticket, + }) + } + + /** + * 添加好友 + * + * @param {string} stranger - 用户stranger数据 + * @param {string} ticket - 用户ticket数据 + * @param {Number} type - 添加好友途径 +
`0` : 通过微信号搜索 +
`1` : 搜索QQ号 +
`3` : 通过微信号搜索 +
`4` : 通过QQ好友添加 +
`8` : 通过群聊 +
`12`: 来自QQ好友 +
`14`: 通过群聊 +
`15`: 通过搜索手机号 +
`17`: 通过名片分享 //未验证 +
`22`: 通过摇一摇打招呼方式 //未验证 +
`25`: 通过漂流瓶 //未验证 +
`30`: 通过二维码方式 //未验证 + * @param {string} [content=''] - 验证信息 + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : { + message: '', + status : 0 //如果对方设置了验证,会返回-44 + } + } + * ``` + * @memberof Padchat + */ + async addContact(stranger, ticket, type = 3, content = '') { + return await this.sendCmd('addContact', { + stranger, + ticket, + type, + content, + }) + } + + /** + * 打招呼 + * 如果已经是好友,会收到由系统自动发送,来自对方的一条文本信息 + * “xx已通过你的朋友验证请求,现在可以开始聊天了” + * + * @param {string} stranger - 用户stranger数据 + * @param {string} ticket - 用户ticket数据 + * @param {string} content - 打招呼内容 + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : { + message: '', + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async sayHello(stranger, ticket, content = '') { + return await this.sendCmd('sayHello', { + stranger, + ticket, + content, + }) + } + + /** + * 设置备注 + * + * @param {string} userId - 用户wxid + * @param {string} remark - 备注名称 + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : { + message: '', + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async setRemark(userId, remark) { + return await this.sendCmd('setRemark', { + userId, + remark, + }) + } + + /** + * 设置头像 + * + * @param {Buffer|string} file - 图片Buffer数据或base64 + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : + { + bigHead : 'http://wx.qlogo.cn/mmhead/ver_1/xxx/0', + data : 1527, //图片文件尺寸 + message : '', + size : 1527, //图片文件尺寸 + smallHead: 'http://wx.qlogo.cn/mmhead/ver_1/xxx/132', + status : 0 } - break + } + * ``` + * @memberof Padchat + */ + async setHeadImg(file) { + if (file instanceof Buffer) { + file = file.toString('base64') + } + return await this.sendCmd('setHeadImg', { + file, + }) + } - case 49: - logger.info('收到一条来自 %s 的appmsg富媒体消息:', data.fromUser, data) - break + /** 朋友圈系列接口 */ - case 10002: - if (data.fromUser === 'weixin') { - //每次登陆,会收到一条系统垃圾推送,过滤掉 - break + /** + * 上传图片到朋友圈 + * NOTE: 此接口只能上传图片,并不会将图片发到朋友圈中 + * + * @param {Buffer|string} file - 图片Buffer数据或base64 + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : + { + bigHead : 'http://mmsns.qpic.cn/mmsns/xxx/0', + data : 1527, //图片文件尺寸 + message : '', + size : 1527, //图片文件尺寸 + smallHead: 'http://mmsns.qpic.cn/mmsns/xxx/150', + status : 0 } - logger.info('用户 %s 撤回了一条消息:', data.fromUser, data) - break + } + * ``` + * @memberof Padchat + */ + async snsUpload(file) { + if (file instanceof Buffer) { + file = file.toString('base64') + } + return await this.sendCmd('snsUpload', { + file, + }) + } - default: - logger.info('收到推送消息:', data) - break + /** + * 操作朋友圈 + * + * @param {string} momentId - 朋友圈信息id + * @param {Number} type - 操作类型,1为删除朋友圈,4为删除评论,5为取消赞 + * @param {Number} commentId - 操作类型,当type为4时,对应删除评论的id,其他状态为0 + * @param {Number} commentType - 操作类型,当删除评论时可用,需与评论type字段一致 + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : { + message: '', + status : 0 } - }) - .on('error', e => { - logger.error('ws 错误:', e.message) - }) - .on('warn', e => { - logger.error('任务出现错误:', e.message) - }) + } + * ``` + * @memberof Padchat + */ + async snsobjectOp(momentId, type, commentId, commentType = 2) { + return await this.sendCmd('snsobjectOp', { + momentId, + type, + commentId, + commentType, + }) + } + + /** + * 发朋友圈 + * + * @param {string} content - 文本内容或`Timelineobject`结构体文本 + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : + { + data: + { + create_time: 1523015689, + description: //朋友圈信息xml结构体文本 + '12775981595019653292wxid_xxx1523015689来自代码发的朋友圈030002', + id : '12775981595019653292', //朋友圈信息id + nick_name: '小木匠', + user_name: 'wxid_xxxx' + }, + message: '', + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async snsSendMoment(content) { + return await this.sendCmd('snsSendMoment', { + content, + }) + } + + /** + * 查看用户朋友圈 + * + * @param {string} userId - 用户wxid + * @param {string} [momentId=''] - 朋友圈信息id + * 首次传入空即获取第一页,以后传入上次拉取的最后一条信息id + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : + { + count: 1, + data : //朋友圈信息结构数组(无评论和点赞数据) + [{ + create_time: 1523015689, + description: '12775981595019653292wxid_xxx1523015689来自代码发的朋友圈030 0 0 2 ', + id : '12775981595019653292', + nick_name : '小木匠', + user_name : 'wxid_xxx' + }], + message: '', + page : '81cb2ad01ebc219f', + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async snsUserPage(userId, momentId = '') { + return await this.sendCmd('snsUserPage', { + userId, + momentId, + }) + } + + /** + * 查看朋友圈动态 + * + * @param {string} [momentId=''] - 朋友圈信息id + * 首次传入空即获取第一页,以后传入上次拉取的最后一条信息id + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : + { + count: 1, + data : //朋友圈信息结构数组(无评论和点赞数据) + [{ + create_time: 1523015689, + description: '12775981595019653292wxid_xxx1523015689来自代码发的朋友圈030 0 0 2 ', + id : '12775981595019653292', + nick_name : '小木匠', + user_name : 'wxid_xxx' + }], + message: '', + page : '81cb2ad01ebc219f', + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async snsTimeline(momentId = '') { + return await this.sendCmd('snsTimeline', { + momentId, + }) + } + + /** + * 获取朋友圈信息详情 + * + * @param {string} momentId - 朋友圈信息id + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : { + data : {}, //朋友圈信息结构 + message: '', + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async snsGetObject(momentId) { + return await this.sendCmd('snsGetObject', { + momentId, + }) + } + + /** + * 评论朋友圈 + * + * @param {string} userId - 用户wxid + * @param {string} momentId - 朋友圈信息id + * @param {string} content - 内容文本 + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : { + data : {}, //朋友圈信息结构 + message: '', + status : 0 + } + } + * ``` + * @memberof Padchat + */ + + async snsComment(userId, momentId, content) { + return await this.sendCmd('snsComment', { + userId, + momentId, + content, + }) + } + + /** + * 朋友圈点赞 + * + * @param {string} userId - 用户wxid + * @param {string} momentId - 朋友圈信息id + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : { + data : {}, //朋友圈信息结构 + message: '', + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async snsLike(userId, momentId) { + return await this.sendCmd('snsLike', { + userId, + momentId, + }) + } + + /** 收藏系列接口 */ + + /** + * 同步收藏消息 + * + * @param {string} [favKey=''] - 同步key,首次不用传入 + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : + { + continue: 0, + data : //收藏消息列表,如果没有则为null + [ + { + flag: 0, //首次标志,0为有效,1为已取消收藏 + id : 3, //收藏id + seq : 652265243, //收藏随机值 + time: 1515042008, //收藏时间 + type: 5 //收藏类型:1文本;2图片;3语音;4视频;5图文消息 + } + ], + key : 'kzTKsdjD6PM0bbQv+oP7vQ==', //下次的同步key,类似分页 + message: '', + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async syncFav(favKey = '') { + return await this.sendCmd('syncFav', { + favKey, + }) + } + + /** + * 添加收藏 + * + * @param {string} content - 内容文本 + * @returns {Promise} 返回Promise,注意捕捉catch + * @memberof Padchat + */ + async addFav(content) { + return await this.sendCmd('addFav', { + content, + }) + } + + /** + * 获取收藏消息详情 + * + * @param {Number} favId - 收藏id + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : + { + data: + [ + , { + flag : 0, //收藏状态:0为有效,1为无效(已删除) + id : 3, //收藏id,如果为0则为无效收藏 + object: //收藏对象结构体文本 + 'DaoCloud 首席架构师王天青:下一代应用转型之道、术、器gh_4b6a20bcdd8bwxid_xxx5353367357590009973http://mp.weixin.qq.com/s?__biz=MzA5MzA2Njk5OA==&mid=2650096972&idx=1&sn=8707378d0c0bdc0d14d1ac93972c5862&chksm=886266d5bf15efc386050508a2cafb1adb806196f40f4f1bde8e944926c7fb6c6e54a14875c7&scene=0#rdgh_4b6a20bcdd8bDaoCloud 首席架构师王天青:下一代应用转型之道、术、器DaoCloud 受邀出席第13届信息化领袖峰会,立足于 DaoCloud 为传统企业数字化转型旅途中的丰富实践,与大家共话《下一代应用转型之道、术、器》,探讨如何“用新技术原力现代化你的企业应用”。http://mmbiz.qpic.cn/mmbiz_jpg/icGWTH9VkFq315HbKuKtWeWlcVDNPAswdhYA0kIskz0GcEQp6nJetC2aSBNfpibp1wKNHf8kYjUibkCF6SgbMIocw/640?wxtype=jpeg&wxfrom=0http://mmbiz.qpic.cn/mmbiz_jpg/icGWTH9VkFq315HbKuKtWeWlcVDNPAswdhYA0kIskz0GcEQp6nJetC2aSBNfpibp1wKNHf8kYjUibkCF6SgbMIocw/640?wxtype=jpeg&wxfrom=0', + //文本消息收藏结构 + // '接收到你发送的内容了! 原内容:syncwxid_xxxbinsee5451059336571949850tedhj' + // 视频 + // 'wxid_xxx4674258153@chatroomwxid_xxx786100356842168336304c02010004453043020100020408eddd7c02030f4fed020419a0360a02045ac9271704206162313437386338616237383833333266336564343335666166363435646331020227110201000400304c02010004453043020100020408eddd7c02030f4fed0204b94c716402045ac9271704203865383031656465633132333661303939346365663837643165316539363663020227110201000400ab1478c8ab788332f3ed435faf645dc18e801edec1236a0994cef87d1e1e966c8e801edec1236a0994cef87d1e1e966c324b6cffbba04142bfabf5cdd0621b4092377ab1478c8ab788332f3ed435faf645dc14fcedfae8fcaa571504c5fd9f2abfa0a5658' + // 语音 + // 'wxid_xxx4674258153@chatroomwxid_xxx3687245278820959898silk304c02010004453043020100020408eddd7c02030f4fed020419a2360a02045ac92711042030646439623262316234646639366264333838313131366462353338313435370202271102010004000dd9b2b1b4df96bd3881116db538145724650dd9b2b1b4df96bd3881116db5381457d348a2942af6d188100855d48dc753734186' + seq : 652265243, + status: 0, //0为有效收藏,1为无效收藏 + time : 1515042008 + } + ], + message: '', + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async getFav(favId) { + return await this.sendCmd('getFav', { + favId, + }) + } + + /** + * 删除收藏 + * + * @param {Number} favId - 收藏id + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : + { + data: + [ + { + flag : 0, + id : 0, + object: '', + seq : 0, + status: 1, //返回删除的收藏id + time : 0 + }, + ], + message: '', + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async deleteFav(favId) { + return await this.sendCmd('deleteFav', { + favId, + }) + } + + /** 标签系列接口 */ + + /** + * 获取所有标签 + * + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : + { + label: //标签列表 + [{ + id : 1, //标签id + name: '测试标签' //标签名称 + }], + message: '', + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async getLabelList() { + return await this.sendCmd('getLabelList', {}) + } + + /** + * 添加标签 + * + * @param {string} label - 标签名称 + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : { + message: '', + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async addLabel(label) { + return await this.sendCmd('addLabel', { + label, + }) + } + + /** + * 删除标签 + * + * @param {string} labelId - 标签id,注意是id是string类型 + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : { + message: '', + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async deleteLabel(labelId) { + return await this.sendCmd('deleteLabel', { + labelId, + }) + } + + /** + * 设置用户标签 + * + * @param {string} userId - 用户wxid + * @param {string} labelId - 标签id + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : { + message: '', + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async setLabel(userId, labelId) { + return await this.sendCmd('setLabel', { + userId, + labelId, + }) + } + + /** 转账及红包接口 */ + + /** + * 查看转账消息 + * + * @param {object} rawMsgData - 推送的消息结构体,即`push`事件中收到的Object + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : + { + external: + { + retcode : '0', + retmsg : 'ok', + fee : 20, //转账金额(单位为分) + transStatus : 2000, //状态:2000未收款;2001已收款 + feeType : '1', + payTime : 1523176700, + modifyTime : 0, + refundBankType: 'BANK', + payerName : 'binsee', + receiverName : 'wxid_8z66rux8lysr22', + statusDesc : '待确认收款', //收款描述 + // '已收钱' //已收款 + // '待%s确认收款' //等待对方收款 + // '%s已收钱' //对方已收款 + statusSupplementary: '', //状态补充信息 + // 未领取: + // '1天内未确认,将退还给对方。<_wc_custom_link_ href="weixin://wcpay/transfer/rebacksendmsg">立即退还', + delayConfirmFlag: 0, + // + // 已领取: + // '<_wc_custom_link_ href="weixin://wcpay/transfer/watchbalance">查看零钱' + // + // 等待对方收款: + // '1天内朋友未确认,将退还给你。<_wc_custom_link_ href="weixin://wcpay/transfer/retrysendmsg">重发转账消息' + isPayer: false + // + // 对方已收款为空 + }, + message: '', + status : 0 + } + } + * ``` + * @deprecated + * @memberof Padchat + */ + async queryTransfer(rawMsgData) { + return await this.sendCmd('queryTransfer', { + rawMsgData, + }) + } + + /** + * 接受转账 + * + * @param {object} rawMsgData - 推送的消息结构体,即`push`事件中收到的Object + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : + { + external: + { + fee : 20, //转账金额(单位为分) + payer : '085exxx', //付款id + receiver: '085exxx', //接收id + retcode : '0', + retmsg : 'ok', + feeType : '1' + }, + message: '', + status : 0 + } + } + * ``` + * @deprecated + * @memberof Padchat + */ + async acceptTransfer(rawMsgData) { + return await this.sendCmd('acceptTransfer', { + rawMsgData, + }) + } + + /** + * 接收红包 + * + * @param {object} rawMsgData - 推送的消息结构体,即`push`事件中收到的Object + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : + { + external: //扩展数据结构 + { + retcode : 0, + retmsg : 'ok', + sendId : '10000xxx', //发送id + wishing : '发3个红包', //红包祝语 + isSender : 0, //是否自己发送 + receiveStatus : 0, //接收状态:0未接收;2已领取 + hbStatus : 3, //红包状态:3未领取完;4已领取完毕 + statusMess : '发了一个红包,金额随机', // + hbType : 1, //红包类型 + watermark : '', + sendUserName : 'binsee', //发送者wxid + timingIdentifier: 'C6E370xxx', + agreeDuty : //未知含义,非必然 + { + title : '', + serviceProtocolWording: '', + serviceProtocolUrl : '', + buttonWording : '', + delayExpiredTime : 0, + agreedFlag : 1 + } + }, + key : 'C6E370xxx', //红包key,用于领取红包 + message: '', + status : 0 + } + } + * ``` + * @deprecated + * @memberof Padchat + */ + async receiveRedPacket(rawMsgData) { + return await this.sendCmd('receiveRedPacket', { + rawMsgData, + }) + } + + /** + * 查看红包信息 + * NOTE: 如果是别人发的红包,未领取且未领取完毕时,无法取到红包信息 + * + * @param {object} rawMsgData - 推送的消息结构体,即`push`事件中收到的Object + * @param {Number} [index=0] - 列表索引。 + * 每页11个,查看第二页11,查看第三页22,以此类推 + * @returns {Promise} 返回Promise,注意捕捉catch + * 未先接收红包返回结果: + * ``` + { + success: true, + data : + { + external: //扩展数据 + { + retcode : 0, + retmsg : 'ok', + operationHeader: [0], + record : [0] + }, + message: '', + status : 0 + } + } + * ``` + * + * 接收红包后查询结果: + * ``` + { + success: true, + data : + { + external: + { + retcode : 0, + retmsg : 'ok', + recNum : 2, //已领取数 + totalNum : 2, //红包个数 + totalAmount : 100, //红包总金额(单位为分) + sendId : '10000xxx', //发送id + amount : 85, //领取到金额(单位为分) + wishing : 'Hello!', //红包祝语 + isSender : 1, //是否是自己发送的 + receiveId : '10000xxx', //接收id + hasWriteAnswer : 1, //是否已写回复 + operationHeader: [], //未知 + hbType : 1, //红包类型 + isContinue : 0, //是否已领取完 + hbStatus : 3, //红包状态:2未领取;3未领取完;4已领取完毕 + // 普通红包或单发红包是2,随机红包是3或4 + receiveStatus: 2, //接收状态:0未接收;2已领取 + statusMess : '成功领到', //状态提示,未领取为空 + headTitle : '', //红包头部标题 + // '已领取1/2个,共0.01/0.02元' //自己发的红包未领取完时 + // '2个红包共1.00元,15秒被抢光' //自己发的红包未领取完时 + // '领取2/3个' //别人发的红包未领取完时 + // '2个红包,42秒被抢光' //别人发的红包未领取完时 + canShare : 0, //是否可分享 + hbKind : 1, //红包种类 + recAmount: 100, //已领取金额(单位为分) + record : + [ + { + receiveAmount: 85, //领取金额(单位为分) + receiveTime : '1523169782', //领取时间戳字符串 + answer : '', //领取者留言,仅查询接口有效 + receiveId : '10000xxx', + state : 1, + gameTips : '手气最佳', //仅红包领取完毕时,手气最佳者有此字段 + receiveOpenId: '10000xxx', + userName : 'wxid_xxx' //领取者wxid + }, + { + receiveAmount: 15, + receiveTime : '1523174612', + answer : '谢谢红包', + receiveId : '1000039501001804086017706218338', + state : 1, + receiveOpenId: '1000039501001804086017706218338', + userName : 'binsee' + }, + ], + operationTail: //操作提示:仅自己发的红包有效 + { + name : '未领取的红包,将于24小时后发起退款', + type : 'Text', + content: '', + enable : 1, + iconUrl: '', + ossKey : 4294967295 + }, + atomicFunc : { enable: 0 }, + jumpChange : 1, + changeWording: '已存入零钱,可直接提现', //查询接口返回'已存入零钱,可直接转账' + sendUserName : 'wxid_xxx' //发送者wxid + }, + message: '', + status : 0 + } + } + * ``` + * @deprecated + * @memberof Padchat + */ + async queryRedPacket(rawMsgData, index = 0) { + return await this.sendCmd('queryRedPacket', { + rawMsgData, + index, + }) + } + + /** + * 领取红包 + * + * @param {object} rawMsgData - 推送的消息结构体,即`push`事件中收到的Object + * @param {string} key - 红包的验证key,通过调用 receiveRedPacket 获得 + * @returns {Promise} 返回Promise,注意捕捉catch + * 已领取过红包: + * ``` + { + success: true, + data : + { + external: { + retcode: 268502336, + retmsg : '你已领取过该红包' + }, + message: '', + status : 0 + } + } + * ``` + * + * 未领取过的红包: + * ``` + { + success: true, + data : + { + external: + { + retcode : 0, + retmsg : 'ok', + sendId : '1000039501201804087013251181768', + amount : 1, + recNum : 2, + recAmount : 2, + totalNum : 3, + totalAmount : 4, + hasWriteAnswer : 0, + hbType : 1, + isSender : 0, + isContinue : 0, + receiveStatus : 2, + hbStatus : 3, + statusMess : '', + wishing : '发3个红包', + receiveId : '1000039501001804087013251181768', + headTitle : '领取2/3个', + canShare : 0, + operationHeader: [], + record : + [ + { + receiveAmount: 1, + receiveTime : '1523171198', + answer : '', + receiveId : '1000039501001804087013251181768', + state : 1, + receiveOpenId: '1000039501001804087013251181768', + userName : 'wxid_xxx' + }, + { + receiveAmount: 1, + receiveTime : '1523170992', + answer : '', + receiveId : '1000039501000804087013251181768', + state : 1, + receiveOpenId: '1000039501000804087013251181768', + userName : 'binsee' + } + ], + watermark : '', + jumpChange : 1, + changeWording : '已存入零钱,可直接提现', + sendUserName : 'binsee', + SystemMsgContext: //系统消息内容 + ' 你领取了$binsee$的<_wc_custom_link_ color="#FD9931" href="weixin://weixinhongbao/opendetail?sendid=1000039501201804087013251181768&sign=68b9858edbc9ff8a88fb8c8fa987edaad88078b31daf6e7af4dba06e78849e50b29a3c1d10bad4893aff116a0db80c7d8a3aa96a5247e1ed095d88e66983fc6fd9f6f6dc8243411ef97727cf0bc698c3&ver=6">红包', + sessionUserName: '4674258153@chatroom', //会话wxid/chatroom + realNameInfo : { guideFlag: 0 } + }, + message: '', + status : 0 + } + } + * ``` + * @deprecated + * @memberof Padchat + */ + async openRedPacket(rawMsgData, key) { + return await this.sendCmd('openRedPacket', { + rawMsgData, + key, + }) + } + /** 公众号系列接口 */ + + /** + * 搜索公众号 + * + * @param {string} content - 公众号名称等关键字 + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : + { + code: 0, + info: + { + continueFlag: 1, //仍有数据标志 + cookies : //cookie数据 + { + businessType: 1, + isHomepage : 1, + query : '"腾讯"', + scene : 2 + }, + data: //返回的搜索相关数据 + [{ + count: 20, + items: //搜索结果列表 + [{ + aliasName: 'tencent', + brandFlag: 2, + brandInfo: + { + urls: + [{ + title: '查看历史消息', + url : + 'http://mp.weixin.qq.com/mp/getmasssendmsg?__biz=MzA3NDEyMDgzMw==#wechat_webview_type=1&wechat_redirect', + titleKey: '__mp_wording__brandinfo_history_massmsg' + }] + }, + docID: '3074120833', + externalInfo: + { + Appid: 'wx06441a33a2a67de4', + BindWxaInfo: + { + wxaEntryInfo: + [{ + title : '腾讯+', + username: 'gh_3a5568e1268b@app', + iconUrl : 'http://mmbiz.qpic.cn/mmbiz_png/xxx/0?wx_fmt=png' + }] + }, + FunctionFlag : 1, + InteractiveMode : '2', + IsAgreeProtocol : '1', + IsHideInputToolbarInMsg: '0', + IsShowHeadImgInMsg : '1', + RegisterSource : + { + IntroUrl: + 'http://mp.weixin.qq.com/mp/getverifyinfo?__biz=MzA3NDEyMDgzMw==&type=reg_info#wechat_redirect', + RegisterBody: '深圳市腾讯计算机系统有限公司' + }, + RoleId : '1', + ScanQRCodeType: 1, + ServiceType : 0, + VerifySource : + { + Description: '深圳市腾讯计算机系统有限公司', + IntroUrl : + 'http://mp.weixin.qq.com/mp/getverifyinfo?__biz=MzA3NDEyMDgzMw==#wechat_webview_type=1&wechat_redirect', + Type : 0, + VerifyBizType: 1 + } + }, + friendsFollowCount: 0, + headHDImgUrl : 'http://wx.qlogo.cn/mmhead/xxx/0', + headImgUrl : 'http://wx.qlogo.cn/mmhead/xxx/132', + iconUrl : 'http://mmbiz.qpic.cn/mmbiz_png/xxx/0?wx_fmt=png', + nickName : '腾讯', + nickNameHighlight : '腾讯', + segQuery : ' 腾讯', + signature : '腾讯公司唯一官方帐号。', + signatureHighlight: '腾讯公司唯一官方帐号。', + userName : 'gh_88b080670a71', + verifyFlag : 24 + }], + keywordList: ['腾讯'], + resultType : 0, + title : '公众号', + totalCount : 1900, + type : 1 + }], + direction : 2, + exposeMs : 500, + isDivide : 0, + isExpose : 1, + isHomePage: 1, + lang : 'zh_CN', + monitorMs : 100, + offset : 20, + query : '"腾讯"', + resultType: 0, + ret : 0, + searchID : '18232918846508425807' + }, + message: '', + offset : 20, + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async searchMp(content) { + return await this.sendCmd('searchMp', { + content, + }) + } + + /** + * 获取公众号信息 + * + * @param {string} ghName - 公众号gh名称,即`gh_`格式的id + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : + { + info: + { + alias : 'tencent', + appid : 'wx06441a33a2a67de4', + bigHeadImgUrl: 'http://wx.qlogo.cn/mmhead/xxx/0', + bindKFUin : '0', + bindWxaInfo : + { + wxaEntryInfo: + [{ + username: 'gh_3a5568e1268b@app', + title : '腾讯+', + iconUrl : 'http://mmbiz.qpic.cn/mmbiz_png/xxx/0?wx_fmt=png' + }], + bizEntryInfo: [] + }, + bitMask : '4294967295', + brandIconURL: 'http://mmbiz.qpic.cn/mmbiz_png/xxx/0?wx_fmt=png', + brandInfo : + { + urls: + [{ + title : '查看历史消息', + url : 'http://mp.weixin.qq.com/mp/getmasssendmsg?xxxx', + titleKey: '__mp_wording__brandinfo_history_massmsg' + }] + }, + functionFlag : '1', + interactiveMode : '2', + isAgreeProtocol : '1', + isHideInputToolbarInMsg: '0', + isShowHeadImgInMsg : '1', + mmBizMenu : + { + uin : 3074120833, + version : 425306837, + interactiveMode: 2, + updateTime : 1518401098, + buttonList : + [ + { + id : 425306837, + type : 0, + name : '产品体验', + key : 'rselfmenu_2', + value: '', + subButtonList: + [{ + id : 425306837, + type : 2, + name : '往期内测', + key : 'rselfmenu_2_1', + value : 'http://mp.weixin.qq.com/mp/xxxxx', + subButtonList: [], + nativeUrl : '' + }], + nativeUrl: '' + }] + }, + nickName : '腾讯', + pyInitial: 'TX', + quanPin : 'tengxun', + registerSource: + { + registerBody: '深圳市腾讯计算机系统有限公司', + introUrl : 'http://mp.weixin.qq.com/mp/getverifyinfo?xxxx' + }, + roleId : '1', + scanQRCodeType : '1', + serviceType : '0', + signature : '腾讯公司唯一官方帐号。', + smallHeadImgUrl: 'http://wx.qlogo.cn/mmhead/xxx/132', + userName : 'gh_88b080670a71', + verifyFlag : '24', + verifyInfo : '深圳市腾讯计算机系统有限公司', + verifySource : + { + description : '深圳市腾讯计算机系统有限公司', + introUrl : 'http://mp.weixin.qq.com/mp/getverifyinfo?xxx', + type : 0, + verifyBizType: 1 + } + }, + message: ' ', + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async getSubscriptionInfo(ghName) { + return await this.sendCmd('getSubscriptionInfo', { + ghName, + }) + } + + /** + * 操作公众号菜单 + * + * @param {string} ghName - 公众号gh名称,即`gh_`格式的id + * @param {Number} menuId - 菜单id + * @param {string} menuKey - 菜单key + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : { + message: '', + status : 0 + } + } + * ``` + * @memberof Padchat + */ + async operateSubscription(ghName, menuId, menuKey) { + return await this.sendCmd('operateSubscription', { + ghName, + menuId, + menuKey, + }) + } + + /** + * 获取网页访问授权 + * + * @param {string} ghName - 公众号gh名称,即`gh_`格式的id + * @param {string} url - 网页url + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : + { + info: + { + 'X-WECHAT-KEY': 'xxxxxxxxxxxx', //授权key + 'X-WECHAT-UIN': 'MTQ5ODA2NDYw' //授权uin + }, + message: '', + status : 0, + fullUrl: //完整授权访问url + 'https://mp.weixin.qq.com/s?__biz=MzA5MDAwOTExMw==&mid=200126214&idx=1&sn=a1e7410ec56de5b6c4810dd7f7db8a47&chksm=1e0b3470297cbd666198666278421aed0a131d775561c08f52db0c82ce0e6a9546aac072a20e&mpshare=1&scene=1&srcid=0408bN3ACxqAH6jyq4vCBP9e&ascene=7&devicetype=iPad+iPhone+OS9.0.2&version=16060125&nettype=WIFI&lang=zh_CN&fontScale=100&pass_ticket=ZQW8EHr9vk%2BPGoWzmON4ev8I0MmliT4mp1ERTPEl8lc%3D&wx_header=1', + shareUrl: //分享url + 'http://mp.weixin.qq.com/s/QiB3FPE6fJmV6asvvxIkvA' + } + } + * ``` + * @memberof Padchat + */ + async getRequestToken(ghName, url) { + return await this.sendCmd('getRequestToken', { + ghName, + url, + }) + } + + /** + * 访问网页 + * + * @param {string} url - 网页url地址 + * @param {string} xKey - 访问Key,用`getRequestToken`获取 + * @param {string} xUin - 访问uin,用`getRequestToken`获取 + * @returns {Promise} 返回Promise,注意捕捉catch + * ``` + { + success: true, + data : + { + message : '', + response: //完整的访问结果原始数据文本(包含http头数据) + 'HTTP/1.1 200 OK\r\nContent-Security-Policy: script-src \'self\' \'unsafe-inline\' \'unsafe-eval\' http://*.qq.com https://*.qq.com http://*.weishi.com https://*.weishi.com xxxxxxxxxxxxxxxxxxxxxxxxxxx', + status: 0 + } + } + * ``` + * @memberof Padchat + */ + async requestUrl(url, xKey, xUin) { + return await this.sendCmd('requestUrl', { + url, + xKey, + xUin, + }) + } +} /** - * @description 保存自动登陆数据 + * 等待指定cmdId返回 + * + * @param {string} cmdId - 要监听的cmdId + * @param {number} [timeout=3000] 超时时间默认为3秒 + * @private + * @returns {Promise} */ -async function saveAutoData() { - let ret = await wx.getWxData() - if (!ret.success) { - logger.warn('获取设备参数未成功! json:', ret) - return +async function getCmdRecv(cmdId, timeout = 3000) { + if (!cmdId) { + throw new Error('未指定cmdID!') } - logger.info('获取设备参数成功, json: ', ret) - Object.assign(autoData, { wxData: ret.data.wxData }) + cmdId = 'RET#' + cmdId + // console.log('进入 getCmdRecv,应该监听: %s', cmdId) + + return new Promise((res, rej) => { + // 如果某操作超过指定时间没有返回结果,则认为是操作超时 + const timeOutHandle = setTimeout(() => { + this.removeAllListeners(cmdId) + rej(new Error('等待指令操作结果超时!当前超时时间为:' + timeout * 1000)) + }, timeout * 1000) + + this.once(cmdId, data => { + // console.log('监听到 %s 事件', cmdId, data) + clearTimeout(timeOutHandle) + res(data) + }) + }) +} - ret = await wx.getLoginToken() - if (!ret.success) { - logger.warn('获取自动登陆数据未成功! json:', ret) +/** + * 加工处理ws接收到的数据 + * + * @param {string} msg - ws连接接收到的文本消息 + * @private + */ +async function onWsMsg(msg) { + let data + // console.log('进入 onWsMsg', msg) + try { + if (typeof msg === 'string') { + data = JSON.parse(msg) + } else { + throw new Error('ws传输的数据不是字符串格式!') + } + } catch (e) { + this.emit('error', new Error('解析msg数据失败: ' + e.message)) return } - logger.info('获取自动登陆数据成功, json: ', ret) - Object.assign(autoData, { token: ret.data.token }) - // NOTE: 这里将设备参数保存到本地,以后再次登录此账号时提供相同参数 - fs.writeFileSync('./config.json', JSON.stringify(autoData, null, 2)) - logger.info('设备参数已写入到 ./config.json文件') -} + if (data.payload) { + if (data.payload.data) { + // 解析扩展数据的json文本 + + if (data.payload.data.external) { + try { + //解析红包及转账接口返回数据 + data.payload.data.external = JSON.parse(data.payload.data.external) + } catch (e) { } + } + + if (data.payload.data.info) { + try { + //解析公众号接口返回数据 + data.payload.data.info = JSON.parse(data.payload.data.info) + + const info = data.payload.data.info + const fields = [ + 'BrandInfo', + 'externalInfo', + 'MMBizMenu', + 'RegisterSource', + 'VerifySource', + 'Location', + 'cookies', + 'brandInfo', + 'BindWxaInfo', + ] + + // 解析`getSubscriptionInfo`接口返回数据字段 + fields.forEach(field => { + if (!info[field]) { return } + try { + info[field] = JSON.parse(info[field]) + } catch (e) { } + }) + + //解析`searchMp`接口返回数据字段 + info.data.forEach((d_item, d_index) => { + //第一层数组 + if (!d_item.items) { return } + const _item = info.data[d_index] -async function onMsg(data) { - const content = data.content.replace(/^[\w:\n]*#/m, '') - let [cmd, ...args] = content.split('\n') + _item.items.forEach((item, index) => { + //第二层数组,即真实的搜索结果列表 + const _item2 = _item.items[index] + fields.forEach(field => { + if (!_item2[field]) { return } + try { + _item2[field] = JSON.parse(_item2[field]) + } catch (e) { } + }) + }) + }) + } catch (e) { } + } - args = args.map(str => { - try { - str = JSON.parse(str) - } catch (e) { + if (data.payload.data.member) { + try { + //解析获取群成员接口返回数据 + data.payload.data.member = JSON.parse(data.payload.data.member) + } catch (e) { } + } } - return str - }) - if (cmd && wx[cmd] && typeof wx[cmd] === 'function') { - logger.info('执行函数 %s,参数:', cmd, args) - await wx[cmd](...args) - .then(ret => { - logger.info('执行函数 %s 结果:%o', cmd, ret) - }) - .catch(e => { - logger.warn('执行函数 %s 异常:', e) - }) + // 转小驼峰 + data.payload = Helper.toCamelCase(data.payload) } + + this.emit('msg', data) + // TODO: 补充push数据格式 + + /** + * 返回数据结果: + data = { + type : 'cmdRet', //返回数据包类型 + cmdId : 'b61eb250-3770-11e8-b00f-595f9d4f3df0', //请求id + taskId : '5', //服务端返回当前实例的任务ID + payload: //荷载数据(以下字段名称为转换为小驼峰后的,原始数据为下划线分隔) + { + error : '', //错误提示 + msg : '', //其他提示信息 + success: true, //接口执行是否成功 + data : //接口执行结果数据,`push`类型无 + { + message: '', + msgId : '1284778244346778513', + status : 0 + }, + list: // 仅`push`类型拥有,包含多个push结构数据 + [ + { + content : '信息内容', //消息内容或xml结构体内容 + continue : 1, + description: '杉木 : 信息内容', //描述内容 + fromUser : 'wxid_001', //发信人 + msgId : '4032724472820776289', //消息id + msgSource : '', + msgType : 5, //消息主类型,类型为5时则用子类型判断 + status : 1, + subType : 1, //消息子类型 + timestamp : 1522921008, //消息时间戳 + toUser : 'wxid_002', //收件人 + uin : 149801234, //用户uin,全局唯一 + mType : 1 //消息类型。等同msgType,当msgType为5时,等同于subType + } + ], + }, + } + */ + + let hasOn + switch (data.type) { + case 'cmdRet': + if (data.type === 'cmdRet' && data.cmdId) { + hasOn = this.emit('RET#' + data.cmdId, data) + if (!hasOn) { + this.emit('warn', new Error(`返回执行结果没有被监听!指令ID:${data.cmdId}`)) + } + } + break; + + case 'userEvent': + switch (data.event) { + case 'warn': + /** + * Warn event + * 实例错误提示 + * + * @event Padchat#warn + * @property {error} error - 报错信息 + * @example + * const wx = new Padchat() + * wx.on('warn',e=>{ + * console.log('任务出现错误:', e.message) + * }) + * + * @memberof Padchat + */ + // 如果success字段为true,则为不严重的问题 + this.emit('warn', new Error('服务器返回错误提示:' + data.payload.error), data.success) + break + case 'qrcode': // 微信扫码登陆,推送二维码 + /** + * Qrcode event + * 登陆二维码推送 + * + * @event Padchat#qrcode + * @property {object} data - 二维码信息 + * @property {string} [data.url] - 登陆二维码解析后的内容url,可使用此url作为内容生成二维码图片后,使用手机扫码登陆 + * @property {string|null} msg - 附加提示信息 + * @example + * const wx = new Padchat() + * wx.on('qrcode',data=>{ + * console.log(`登陆二维码内容为: "${data.url}"`) + * // 可使用`qrcode-terminal`库在终端生成二维码 + * }) + * + * @memberof Padchat + */ + case 'scan': // 微信账号扫码事件 + /** + * Scan event + * 扫码状态推送 + * + * @event Padchat#scan + * @property {object} data - 扫码状态 + * @property {number} data.status - 扫码状态: + *
`0` 等待扫码 + *
`1` 扫码完成,等待手机端确认登陆 + *
`2` 手机端已确认,等待登陆 + *
`3` 二维码过期 + *
`4` 手机端取消登陆 + *
其他状态未知 + * @property {number} [data.subStatus] - 扫码子状态,仅`status`为`2`时有效 + *
`0` 登陆成功 + *
`1` 登陆失败 + *
其他状态未知 + * @property {string} [data.headUrl] - 头像url **(扫码后存在)** + * @property {string} [data.deviceType] - 主设备类型 **(扫码后存在)** + * @property {string} [data.userName] - 账号wxid,全局唯一 **(扫码后存在)** + * @property {number} [data.uin] - 账号uin,全局唯一 **(扫码后存在)** + * @property {string} [data.email] - 账号绑定的邮箱 **(确认登陆后存在)** + * @property {string} [data.phoneNumber] - 账号绑定的手机号 **(确认登陆后存在)** + * @property {number} [data.qq] - 账号绑定的QQ号 **(确认登陆后存在)** + * @property {string} [data.nickName] - 账号昵称 **(确认登陆后存在)** + * @property {number} [data.external] - 是否为扩展设备登陆 + *
`0` 主设备登陆 + *
`1` 扩展设备登录 + * @property {string|null} msg - 附加提示信息 + * @example + * const wx = new Padchat() + * wx.on('scan',data=>{ + * switch (data.status) { + * case 0: + * case 1: + * case 2: + * case 3: + * case 4: + * default: + * break + * } + * }) + * + * @memberof Padchat + */ + case 'login': // 微信账号登陆成功 + /** + * Login event + * 登陆成功推送 + * + * @example + * const wx = new Padchat() + * wx.on('login',()=>{ + * console.log('微信账号登陆成功!') + * }) + * + * @event Padchat#login + */ + case 'autoLogin': // 自动重连成功 + /** + * AutoLogin event + * 自动重连成功推送 + * + * 自动重连后token会变化,可在此时获取新的token,否则使用旧token将不能断线重连,但可以二次登陆(需手机端确认登陆) + * + * @example + * const wx = new Padchat() + * wx.on('autoLogin',()=>{ + * wx.getLoginToken() + * console.log('自动重连成功!') + * }) + * + * @event Padchat#autoLogin + */ + case 'loaded': // 通讯录载入完毕 + /** + * Loaded event + * 通讯录同步完毕推送 + * + * @example + * const wx = new Padchat() + * wx.on('loaded',()=>{ + * console.log('通讯录同步完毕!') + * }) + * + * @event Padchat#loaded + */ + case 'logout': // 微信账号退出 + /** + * Logout event + * 微信账号退出推送 + * + * @example + * const wx = new Padchat() + * wx.on('logout',({error,msg})=>{ + * console.log('微信账号已退出! ',error,msg) + * }) + * + * @event Padchat#logout + * @property {object} data + * @property {string|null} [data.error] - 错误提示信息 + * @property {string|null} [data.msg] - 附加提示信息 + * @property {string|null} [msg] - 附加提示信息(同`data.msg`) + */ + case 'over': // 实例注销(账号不退出) + /** + * Over event + * 实例关闭推送 + * + * @event Padchat#over + * @property {object} data + * @property {string|null} [data.msg] - 附加提示信息 + * @property {string|null} [msg] - 附加提示信息(同`data.msg`) + * + * @example + * const wx = new Padchat() + * wx.on('over',({msg})=>{ + * console.log('任务实例已关闭!',msg) + * }) + * + */ + case 'sns': // 朋友圈事件:新评论 + /** + * Sns event + * 朋友圈通知 + * + * @event Padchat#sns + * @example + * const wx = new Padchat() + * wx.on('sns',()=>{ + * console.log('收到朋友圈事件!') + * }) + * + */ + this.emit(data.event, data.payload || {}, data.payload.msg) + break + + case 'notify': // 推送通知 + if (data.payload.type === 4) { + if (this.openSyncContact) { + await this.syncContact() + } + } else { + if (this.openSyncMsg) { + await this.syncMsg() + } + } + break + case 'push': + if (data.payload && Array.isArray(data.payload.list)) { + const pushContact = data.payload.type === 4 + const event = pushContact ? 'contact' : 'push' + let loaded = false + data.payload.list.forEach(item => { + const type = item.msgType + if (type === 32768 && item.continue === 0) { + //通讯录同步完毕 + loaded = true + } + // 过滤无意义的2048和32768类型数据 + if (type === undefined || type === 2048 || type === 32768) { + return null + } + // 当msg_type为5时,即表示推送的信息类型要用sub_type进行判断 + // 另外增加一个属性来存储好了 + item.mType = item.msgType === 5 ? item.subType : item.msgType + // 解析群成员列表 + if (item.member) { + try { + item.member = JSON.parse(item.member) || [] + } catch (e) { + } + } + /** + * Push event + * 联系人/消息推送 + * + * @event Padchat#push + * @property {number} data.mType - 推送类型 +
`1` : 文字消息 +
`2` : 好友信息推送,包含好友,群,公众号信息 +
`3` : 收到图片消息 +
`34` : 语音消息 +
`35` : 用户头像buf +
`37` : 收到好友请求消息 +
`42` : 名片消息 +
`43` : 视频消息 +
`47` : 表情消息 +
`48` : 定位消息 +
`49` : APP消息(文件 或者 链接 H5) +
`50` : 语音通话 +
`51` : 状态通知(如打开与好友/群的聊天界面) +
`52` : 语音通话通知 +
`53` : 语音通话邀请 +
`62` : 小视频 +
`2000` : 转账消息 +
`2001` : 收到红包消息 +
`3000` : 群邀请 +
`9999` : 系统通知 +
`10000`: 微信通知信息。微信群信息变更,多为群名修改、进群、离群信息,不包含群内聊天信息 +
`10002`: 撤回消息 + * @property {string} [data.fromUser] - (`mType`非2) 发件人 + * @property {string} [data.toUser] - (`mType`非2) 收件人 + * @property {string} [data.content] - (`mType`非2) 消息内容。非文本型消息时,此字段可能为xml结构文本 + * @property {string} [data.msgSource] - (`mType`非2) + * @property {string} [data.data] - 图片、语音、视频等媒体文件base64文本 + * @property {string} [data.description] - (部分`mType`非2) 消息描述 + * @property {string} [data.msgId] - (`mType`非2) 消息id + * @property {string} [data.timestamp] - (`mType`非2) 消息时间戳 + * @property {number} [data.uin] - 当前账号uin + * @property {string} [data.*] - 其他字段请自行输出查看 + * @example + * const util = require('util') + * const wx = new Padchat() + * wx.on('push',data=>{ + * console.log('push:',util.inspect(data, { depth: 10 })) + * }) + */ + + /** + * Contact event + * 联系人/消息推送 + * + * @event Padchat#contact + * @property {number} data.mType - 推送类型 +
`2`: 好友信息推送,包含好友,群,公众号信息 + * @property {string} data.city 城市 + * @property {string} data.country 国家 + * @property {string} data.provincia 省份 + * @property {string} data.intro 公众号主体 + * @property {string} data.stranger 个性签名 + * @property {string} data.label + * @property {number} data.level + * @property {number} data.sex 性别: 1男 2女 0未知 + * @property {string} data.signature v1值 + * @property {number} data.source 联系人添加渠道 + * @property {number} data.uin 当前账号uin (自己微信号的,不是好友的) + * @property {string} data.bigHead 高清头像 + * @property {string} data.smallHead 头像缩略图 + * @property {number} data.bitValue + * @property {number} data.chatroomId 群版本号 + * @property {string} data.chatroomOwner 群主wxid + * @property {number} data.imgFlag + * @property {number} data.maxMemberCount 群成员上限 + * @property {number} data.memberCount 群成员数量 + * @property {number} data.msgType + * @property {string} data.nickName 联系人昵称 + * @property {string} data.pyInitial 昵称拼音简写 + * @property {string} data.quanPin 昵称拼音全拼 + * @property {string} data.remark 备注 + * @property {string} data.remarkPyInitial 备注拼音简写 + * @property {string} data.remarkQuanPin 备注拼音全拼 + * @property {string} data.userName 联系人微信号/wxid + * @example + * const util = require('util') + * const wx = new Padchat() + * wx.on('push',data=>{ + * console.log('push:',util.inspect(data, { depth: 10 })) + * }) + */ + this.emit(event, item) + }) + if (pushContact) { + if (!loaded && this.openSyncContact) { + await this.syncContact() + } + if (loaded && !this.loaded) { + this.loaded = true + this.emit('loaded') + } + } + } + break + default: + this.emit('other', data) + break + } + break + default: + this.emit('other', data) + break; + } +} + +/** + * 清除消息结构中多余字段 + * + * @param {object} obj - 要处理的数据结构 + * @private + * @returns {object} + */ +function clearRawMsg(obj) { + if (typeof obj === 'object') { + delete obj.data + } + return obj } -process.on('uncaughtException', e => { - logger.error('Main', 'uncaughtException:', e) -}) -process.on('unhandledRejection', e => { - logger.error('Main', 'unhandledRejection:', e) -}) \ No newline at end of file +/** + * 非阻塞等待 + * + * @param {*} ms 要等待的毫秒数 + * @private + * @returns {Promise} + */ +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)) +} + + +Padchat.loginType = loginType +Padchat.blacklist = blacklist +module.exports = Padchat \ No newline at end of file diff --git a/package.json b/package.json index 2683372..b3c0175 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "padchat开发包", "main": "index.js", "scripts": { + "dev": "node index.js", "start": "node demo", "doc": "node scripts/docVer.js >nul && jsdoc2md index.js >> docs/index.md", "eslint": "eslint {define,helper,index,demo}.js", -- GitLab