UNPKG

yochat

Version:

基于 Nodejs 的 微信机器人、微信命令行工具. 扩展性高的微信机器人

662 lines (626 loc) 21.1 kB
const fs = require('fs') const EventEmitter = require('events') const request = require('request').defaults({ jar: true }) const parseString = require('xml2js').parseString const qrcode = require('qrcode-terminal') const open = require('open') var config = require('./config.js') const _LOGIN_URL = 'https://login.weixin.qq.com/jslogin?appid=wx782c26e4c19acffb&redirect_uri=https%3A%2F%2Fwx.qq.com%2Fcgi-bin%2Fmmwebwx-bin%2Fwebwxnewloginpage&fun=new&lang=en_US&_=' + new Date().getTime() const _QR_IMAGE_URL = 'https://login.weixin.qq.com/qrcode/' const REDIRECT_URL = 'https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login?loginicon=true&uuid=$0&tip=0&_=' + new Date().getTime() var jar = request.jar() var window = { QRLogin: { code: null, uuid: null }, redirect_uri: '', code: 0, } var fetch = function (url, options) { return new Promise(function (resolve, reject) { request(url, options, (err, httpResponse, body) => { if (autoLogin) { var cookie_string = jar.getCookieString(url); if (cookie_string.length > 0) { jar.setCookie('DeviceID=' + config.DeviceID, 'https://wx.qq.com'); jar.setCookie('DeviceID=' + config.DeviceID, 'https://webpush.wx.qq.com'); fs.writeFileSync('./cookie/cookies.cookie', cookie_string) } } resolve(httpResponse) }) }); }; var wxEvent = new EventEmitter() // cookie容器 var JCookie = {}; // 是否自动保存cookie 方便下次登录 var autoLogin = true; var openBrowser = false; // 消息过滤器 const MsgFilter = async (item, config) => { var TYPE = 'Miss'; switch (item.MsgType) { case 1: TYPE = 'Text' break; case 3: TYPE = 'Picture' break; case 47: TYPE = 'Picture' break; case 42: TYPE = 'NameCard' break; case 49: TYPE = 'Link' break; case 62: TYPE = 'Video' break; } var F_User = null; var T_User = null; var CONTENT = ''; // 群聊消息 if (item.FromUserName.indexOf('@@') === 0 || item.ToUserName.indexOf('@@') === 0) { TYPE = 'Group' var groupInfo = await (WechatCore.getGroupInfo(item.FromUserName.indexOf('@@') === 0 ? item.FromUserName : item.ToUserName)) F_User = groupInfo groupInfo.MemberList.map(f_item => { if (f_item.UserName === (item.ToUserName.indexOf('@@') === 0 ? item.FromUserName : item.ToUserName)) { T_User = f_item; } }) CONTENT = item.Content; if (item.Content.indexOf(':<br/>') > -1) { CONTENT = item.Content.substring(item.Content.indexOf(':<br/>') + 6); } } else { config.MemberList.map(m_item => { if (m_item.UserName === item.FromUserName) { F_User = m_item; } if (m_item.UserName === item.ToUserName) { T_User = m_item; } }) CONTENT = item.Content; } return { fromUser: F_User, // 发送者 toUser: T_User, // 接收者 type: TYPE, // 消息类型 msg: CONTENT, // 消息内容 originMsg: item // 原始消息内容 } } module.exports = WechatCore = { /** * 获取登录二维码 * @method login * @return {Object} */ login: async () => { let QRLoginUUID = await fetch(_LOGIN_URL) eval(QRLoginUUID.body) return { code: window.QRLogin.code, uuid: window.QRLogin.uuid } }, /** * 检查cookie状态, true 为有效 * @method checkCookie * @param {Boolean} [getData=false] 是否获取cookie对象 * @return {Boolean} */ async __checkCookie(getData) { if (!fs.existsSync('./cookie/cookies.cookie')) { if (getData) { return {} } return false } var cookie_string = fs.readFileSync('./cookie/cookies.cookie', 'utf-8') this.setCookie(cookie_string) let data = await fetch('https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=-' + new Date().getTime(), { method: 'post', jar: jar, json: true, followRedirect: false, body: { "BaseRequest": { "Uin": JCookie['wxuin'], "Sid": JCookie['wxsid'], "Skey": "", "DeviceID": "e" + new Date().getTime() } } } ) if (getData) { return data.body } return (data.body.SystemTime !== 0) }, // 等待登录 async check_login(uuid) { var UUID = uuid; if (autoLogin) { let _checkCookie = await (this.__checkCookie(true)) if (_checkCookie.hasOwnProperty('SystemTime') && _checkCookie.SystemTime !== 0) { config.uuid = UUID; config.skey = _checkCookie.SKey; config.pass_ticket = ''; config.wxsid = JCookie['wxsid']; config.wxuin = _checkCookie['User']['Uin']; return config; } } while (true) { let code = await fetch(REDIRECT_URL.replace('$0', window.QRLogin.uuid), { jar: jar }) eval(code.body) config.userAvatar = window.userAvatar if (window.redirect_uri) { let redirect_data = await fetch(window.redirect_uri, { json: true, followRedirect: false, jar: jar }) return new Promise(function (resolve, reject) { parseString(redirect_data.body, async (err, result) => { config.uuid = UUID; config.skey = result.error.skey[0]; config.pass_ticket = result.error.pass_ticket[0]; config.wxsid = result.error.wxsid[0]; config.wxuin = result.error.wxuin[0]; config.cookie = redirect_data.headers['set-cookie'] config.isLogin = true; let init_data = await (fetch('https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=' + new Date().getTime(), { method: 'post', json: true, followRedirect: false, jar: jar, body: { "BaseRequest": { "Uin": "xuin=" + config.wxuin, "Sid": config.wxsid, "Skey": "", "DeviceID": config.DeviceID ? config.DeviceID : config.DeviceID = "e" + new Date().getTime() } } })) config.initData = init_data.body; resolve(config); }) }) } } }, /** * 获取账户资料 * @method getOwnerInfo * @return {config.userInfo} */ getOwnerInfo: async () => { if (config.userInfo && config.SyncKey) { return config.userInfo } let data = await (fetch('https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=' + new Date().getTime(), { method: 'post', json: true, followRedirect: false, jar: jar, body: { "BaseRequest": { "Uin": "xuin=" + config.wxuin, "Sid": config.wxsid, "Skey": "", "DeviceID": config.DeviceID ? config.DeviceID : config.DeviceID = "e" + new Date().getTime() } } })) config.userInfo = data.body; config.SyncKey = data.body.SyncKey; wxEvent.emit('Login', { code: 200, msg: '登录成功' }) return data.body; }, /** * 获取单个用户信息 * @method getUserInfo * @param {String} [userName=''] 用户UserName * @return {Object} */ async getUserInfo(userName) { var userObj = null; if (userName.indexOf('@@') > -1) { config.chatRoomList.map(item => { if (item.UserName === userName) { userObj = item; } }) if (userObj) { return userObj } } let contacts = await this.getContact() contacts.map(item => { if (item.UserName === userName) { userObj = item } }) return userObj; }, /** * 获取用户昵称 * @method getNickName * @param {String} [userName=''] 用户UserName * @return {String} */ async getNickName(userName) { return await this.getUserInfo(userName).NickName }, // 获取联系人列表 async getContact() { if (config.MemberList.length > 0) { return config.MemberList } let data = await (fetch(`https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetcontact?r=${new Date().getTime()}&pass_ticket=${config.pass_ticket}&seq=0&skey=${config.skey}`, { json: true, followRedirect: false, jar: jar })) config.MemberList = data.body.MemberList var chatRooms = [] config.MemberList.map(item => { if (item.UserName.indexOf('@@') > -1) { chatRooms.push({ ChatRoomId: '', UserName: item.UserName }) } }) await this.getGroupInfo(chatRooms) return config.MemberList; }, // 获取群聊信息 async getGroupInfo(userName) { var GroupInfo = await this.getUserInfo(userName); if (GroupInfo) { return GroupInfo } let data = await fetch('https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxbatchgetcontact?lang=zh_CNtype=ex&r=' + new Date().getTime(), { method: 'post', json: true, followRedirect: false, jar: jar, body: { "BaseRequest": { "Uin": config.wxuin, "Sid": config.wxsid, "Skey": config.skey, "DeviceID": config.DeviceID ? config.DeviceID : config.DeviceID = "e" + new Date().getTime() }, Count: 1, List: (userName instanceof Array) ? userName : [ { ChatRoomId: '', UserName: userName } ] } }) if (data.body.Count > 0) { data.body.ContactList.map(item => { var flag = false; config.chatRoomList.map(j => { if (j.UserName === item.UserName) { flag = true; } }) if (!flag) { config.chatRoomList.push(item) } }) } return ((userName instanceof Array) ? data.body.ContactList : data.body.ContactList[0]) }, // 拉取最新消息 pullReceve: async () => { let data = await (fetch(`https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsync?sid=${config.wxsid}&skey=${config.skey}&lang=zh_CN`, { method: 'post', json: true, followRedirect: false, jar: jar, body: { "BaseRequest": { "Uin": "xuin=" + config.wxuin, "Sid": config.wxsid, "Skey": "", "DeviceID": config.DeviceID ? config.DeviceID : config.DeviceID = "e" + new Date().getTime() }, SyncKey: config.SyncKey, rr: '-' + Math.random() } })) config.SyncKey = data.body.SyncKey; data.body.AddMsgList.map(async item => { var filter = await MsgFilter(item, config) filter.type !== 'Miss' ? wxEvent.emit('message', filter) : 0 }) return { count: data.body.AddMsgCount, Msgs: data.body.AddMsgList } }, // 检查是否有新消息 syncCheck: async () => { var url = 'https://webpush.wx.qq.com/cgi-bin/mmwebwx-bin/synccheck' + '?r=' + new Date().getTime() + '&skey=' + config.skey + '&sid=' + config.wxsid + '&uin=' + config.wxuin + '&deviceid=' + (config.DeviceID ? config.DeviceID : config.DeviceID = "e" + new Date().getTime()) + '&synckey=' + (() => { var str = ''; config.SyncKey.List.map((item, i) => { str += item.Key + '_' + item.Val; if (i != config.SyncKey.List.length - 1) { str += '|' } }) return str; })(); var data = await fetch(url, { json: true, followRedirect: false, jar: jar }) // 有时候会乱码 eval(data.body) return (window.synccheck.retcode === '0' && window.synccheck.selector != '0') }, // 消息进程 async MsgServer() { while (true) { let newMsg = await (this.syncCheck()) if (newMsg) { await (this.pullReceve()) } } }, /** * 获取cookie字符串 * @method getCookie * @return {String} 返回cookie的String字符串 */ getCookie() { return jar.getCookieString('https://wx.qq.com'); }, /** * 手动设置cookie * @method setCookie * @param {String} [cookieString=''] Cookie字符串 */ setCookie(cookieString) { var cookies = cookieString.trim().split(';'); JCookie = {}; cookies.map(item => { var trimitem = item.trim() JCookie[trimitem.substring(0, trimitem.indexOf('='))] = trimitem.substring(trimitem.indexOf('=') + 1); }) for (var i = 0; i < cookies.length; ++i) { jar.setCookie(cookies[i].trim(), 'https://wx.qq.com'); jar.setCookie(cookies[i].trim(), 'https://webpush.wx.qq.com'); } console.log(config.DeviceID) }, // ### 发送消息 // Type: 消息类型 // LocalID: // ClientMsgId: sendMsg: async (FromUserName, ToUserName, Content) => { var url = 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg' + '?pass_ticket=' + config.pass_ticket var timeStamp = new Date().getTime(); var data = await fetch(url, { method: 'post', json: true, followRedirect: false, jar: jar, body: { "BaseRequest": { "Uin": parseInt(config.wxuin), "Sid": config.wxsid, "Skey": config.skey, "DeviceID": config.DeviceID ? config.DeviceID : config.DeviceID = "e" + new Date().getTime() }, Msg: { Type: 1, ToUserName, FromUserName, LocalID: timeStamp, Content, ClientMsgId: timeStamp, }, rr: '-' + Math.random() } }) return (data.body.BaseResponse.Ret === 0) }, /** * 全局配置 * @method config * @param {Object} [options={}] 配置 */ config(options) { autoLogin = options.autoLogin || true openBrowser = options.openBrowser || false options.cookie ? this.setCookie(options.cookie) : 0 }, /** * 创建群聊 * @method createChatroom * @param {Array} [memberList=[], Topic=''] 用户列表 群聊名称 * @return {Object} */ async createChatroom(memberList, Topic) { let data = await (fetch(`https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxcreatechatroom?pass_ticket=${config.pass_ticket}&r=${new Date().getTime()}`, { method: 'post', json: true, followRedirect: false, jar: jar, body: { "BaseRequest": { "Uin": parseInt(config.wxuin), "Sid": config.wxsid, "Skey": config.skey, "DeviceID": config.DeviceID ? config.DeviceID : config.DeviceID = "e" + new Date().getTime() }, MemberCount: memberList.length, MemberList: memberList, Topic: Topic || '', } })) return { status: (data.body.MemberCount > 0), error: data.body.BaseResponse.ErrMsg } }, /** * 修改群聊名称 * @method renameChatroom * @param {String} [ChatRoomName='',NewTopic=''] * @return {Boolean} */ async renameChatroom(ChatRoomName, NewTopic) { var url = 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxupdatechatroom?fun=modtopic' let data = await (fetch(url, { method: 'post', json: true, followRedirect: false, jar: jar, body: { "BaseRequest": { "Uin": parseInt(config.wxuin), "Sid": config.wxsid, "Skey": config.skey, "DeviceID": config.DeviceID ? config.DeviceID : config.DeviceID = "e" + new Date().getTime() }, ChatRoomName, NewTopic } })) return (data.body.BaseResponse.ErrMsg === '' && data.body.BaseResponse.Ret === 0) }, /** * 邀请加入群聊 * @method addMemberFromChatroom * @param {String} [ChatRoomName='',NewTopic=''] * @return {Boolean} */ async addMemberFromChatroom(ChatRoomName, userName) { var url = 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxupdatechatroom?fun=addmember' let data = await (fetch(url, { method: 'post', json: true, followRedirect: false, jar: jar, body: { "BaseRequest": { "Uin": parseInt(config.wxuin), "Sid": config.wxsid, "Skey": config.skey, "DeviceID": config.DeviceID ? config.DeviceID : config.DeviceID = "e" + new Date().getTime() }, ChatRoomName, AddMemberList: userName } })) return (data.body.BaseResponse.ErrMsg === '' && data.body.BaseResponse.Ret === 0) }, /** * 移出群聊 * @method deleteMemberFromChatroom * @param {String} [ChatRoomName='',NewTopic=''] * @return {Boolean} */ async deleteMemberFromChatroom(ChatRoomName, userName) { var url = 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxupdatechatroom?fun=delmember' let data = await (fetch(url, { method: 'post', json: true, followRedirect: false, jar: jar, body: { "BaseRequest": { "Uin": parseInt(config.wxuin), "Sid": config.wxsid, "Skey": config.skey, "DeviceID": config.DeviceID ? config.DeviceID : config.DeviceID = "e" + new Date().getTime() }, ChatRoomName, DelMemberList: userName } })) return (data.body.BaseResponse.ErrMsg === '' && data.body.BaseResponse.Ret === 0) }, async run(cb) { let loginInfo = await (this.login()) if (!(await (this.__checkCookie()))) { var url = `https://login.weixin.qq.com/l/${loginInfo.uuid}` console.log('获取登录二维码:', `https://login.weixin.qq.com/qrcode/${loginInfo.uuid}`) openBrowser ? open(`https://login.weixin.qq.com/qrcode/${loginInfo.uuid}`) : qrcode.generate(url) } else { console.log('使用cookie自动登录!') } await this.check_login(loginInfo.uuid) await this.getOwnerInfo() await this.getContact() console.log('登录成功,启动消息服务') this.MsgServer() // 监听消息 cb && cb() }, getConfig: () => { return config; }, listener: wxEvent }