wechaty-puppet-wechat
Version:
Puppet WeChat for Wechaty
883 lines (759 loc) • 25.7 kB
JavaScript
/**
* Wechaty - https://github.com/chatie/wechaty
*
* @copyright 2016-2018 Huan LI <zixia@zixia.net>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
/**
* Wechaty - Wechat for Bot, and human who talk to bot.
*
* PuppetWeChat: WechatyBro
*
* Inject this js code to browser,
* in order to interactive with wechat web program.
*
* ATTENTION:
*
* JavaScript in this file will be ran inside:
*
* BROWSER
*
* instead of
*
* NODE.js
*
* read more about this in puppet-web-bridge.ts
*/
/* eslint no-undef: off */
/* eslint sort-keys: off */
(function () {
function init () {
if (!angularIsReady()) {
retObj.code = 503 // 503 SERVICE UNAVAILABLE https://httpstatuses.com/503
retObj.message = 'init() without a ready angular env'
return retObj
}
if (WechatyBro.vars.initState === true) {
log('WechatyBro.init() called twice: already inited')
retObj.code = 304 // 304 Not Modified https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
retObj.message = 'init() already inited before. returned with do nothing'
return retObj
}
if (MMCgiLogined()) {
login('page refresh')
}
glueToAngular()
hookEvents()
hookRecalledMsgProcess()
log('init() scanCode: ' + WechatyBro.vars.scanCode)
setTimeout(() => checkScan(), 1000)
heartBeat(true)
log('inited!. ;-D')
WechatyBro.vars.initState = true
retObj.code = 200
retObj.message = 'WechatyBro Init Succ'
return retObj
}
function log (text) {
WechatyBro.emit('log', text)
}
/**
*
* Functions that Glued with AngularJS
*
*/
function MMCgiLogined () {
return !!(window.MMCgi && window.MMCgi.isLogin)
}
function angularIsReady () {
// don't log inside, because we has not yet init clog here.
return !!(
(typeof angular) !== 'undefined'
&& angular.element
&& angular.element('body')
&& angular.element(document).injector()
)
}
function heartBeat (firstTime) {
const TIMEOUT = 15000 // 15s
if (firstTime && WechatyBro.vars.heartBeatTimmer) {
log('heartBeat timer exist when 1st time is true? return for do nothing')
return
}
WechatyBro.emit('heartbeat', 'heartbeat@browser')
WechatyBro.vars.heartBeatTimmer = setTimeout(heartBeat, TIMEOUT)
return TIMEOUT
}
function glueToAngular () {
const injector = angular.element(document).injector()
if (!injector) {
throw new Error('glueToAngular cant get injector(right now)')
}
const accountFactory = injector.get('accountFactory')
// var appFactory = injector.get('appFactory')
const chatroomFactory = injector.get('chatroomFactory')
const chatFactory = injector.get('chatFactory')
const contactFactory = injector.get('contactFactory')
const confFactory = injector.get('confFactory')
const emojiFactory = injector.get('emojiFactory')
const loginFactory = injector.get('loginFactory')
const utilFactory = injector.get('utilFactory')
const http = injector.get('$http')
const state = injector.get('$state')
const mmHttp = injector.get('mmHttp')
const appScope = angular.element('[ng-controller="appController"]').scope()
const rootScope = injector.get('$rootScope')
const loginScope = angular.element('[ng-controller="loginController"]').scope()
/*
// method 1
appFactory.syncOrig = appFactory.sync
appFactory.syncCheckOrig = appFactory.syncCheck
appFactory.sync = function() { WechatyBro.log('appFactory.sync() !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'); return appFactory.syncOrig(arguments) }
appFactory.syncCheck = function() { WechatyBro.log('appFactory.syncCheck() !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'); return appFactory.syncCheckOrig(arguments) }
// method 2
$.ajaxOrig = $.ajax
$.ajax = function() { WechatyBro.log('$.ajax() !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'); return $.ajaxOrig(arguments) }
$.ajax({
url: "https://wx.qq.com/zh_CN/htmledition/v2/images/webwxgeticon.jpg"
, type: "GET"
}).done(function (response) {
alert("success");
})
// method 3 - mmHttp
mmHttp.getOrig = mmHttp.get
mmHttp.get = function() { WechatyBro.log('mmHttp.get() !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'); return mmHttp.getOrig(arguments) }
*/
/**
* generate $scope with a contoller (as it is not assigned in html staticly)
* https://github.com/angular/angular.js/blob/a4e60cb6970d8b6fa9e0af4b9f881ee3ba7fdc99/test/ng/controllerSpec.js#L24
*/
const contentChatScope = rootScope.$new()
injector.get('$controller')('contentChatController', { $scope: contentChatScope })
// get all we need from wx in browser(angularjs)
WechatyBro.glue = {
http,
injector,
mmHttp,
state,
accountFactory,
chatFactory,
chatroomFactory,
confFactory,
contactFactory,
emojiFactory,
loginFactory,
utilFactory,
rootScope,
appScope,
loginScope,
contentChatScope,
}
return true
}
function checkScan () {
// log('checkScan()')
if (loginState()) {
log('checkScan() - already login, no more check, and return(only)') // but I will emit a login event')
// login('checkScan found already login')
return
}
const loginScope = WechatyBro.glue.loginScope
if (!loginScope) {
log('checkScan() - loginScope disappeared, TODO: find out the reason why this happen')
// login('loginScope disappeared')
// return
return setTimeout(checkScan, 1000)
}
// loginScope.code:
// 0: 显示二维码
// 408: 未确认(显示二维码后30秒触发)
// 201: 扫描,未确认
// 200: 登录成功
const code = +loginScope.code
const url = loginScope.qrcodeUrl
log('checkScan() code:' + code + ' url:' + url + ' scanCode:' + WechatyBro.vars.scanCode)
if (url && code !== WechatyBro.vars.scanCode) {
log('checkScan() - code change detected: from '
+ WechatyBro.vars.scanCode
+ ' to '
+ code
)
WechatyBro.emit('scan', {
code : code,
url : url,
})
WechatyBro.vars.scanCode = code
}
if (code !== 200) {
return setTimeout(checkScan, 1000)
}
WechatyBro.vars.scanCode = null
loginScope.code = null
// wait a while because the account maybe blocked by tencent,
// then there will be a dialog should appear
setTimeout(() => login('scan code 200'), 1000)
}
function loginState (state) {
if (typeof state === 'undefined') {
return !!WechatyBro.vars.loginState
}
WechatyBro.vars.loginState = state
}
function login (data) {
log('login(' + data + ')')
loginState(true)
WechatyBro.emit('login', data)
}
function logout (data) {
log('logout(' + data + ')')
loginState(false)
// WechatyBro.emit('logout', data)
if (WechatyBro.glue.loginFactory) {
WechatyBro.glue.loginFactory.loginout(0)
} else {
log('logout() WechatyBro.glue.loginFactory NOT found')
}
setTimeout(() => checkScan(), 1000)
}
function ding (data) {
log('recv ding')
return data || 'dong'
}
function hookEvents () {
const rootScope = WechatyBro.glue.rootScope
const appScope = WechatyBro.glue.appScope
if (!rootScope || !appScope) {
log('hookEvents() no rootScope')
return false
}
rootScope.$on('message:add:success', function (event, data) {
// if (!loginState()) { // in case of we missed the pageInit event
// login('by event[message:add:success]')
// }
WechatyBro.emit('message', data)
})
// rootScope.$on('root:pageInit:success', function (event, data) {
// login('by event[root:pageInit:success]')
// })
// newLoginPage seems not stand for a user login action
// appScope.$on("newLoginPage", function(event, data) {
// login('by event[newLoginPage]')
// })
window.addEventListener('unload', function (e) {
// XXX only 1 event can be emitted here???
WechatyBro.emit('unload', String(e))
log('event unload')
})
return true
}
function hookRecalledMsgProcess () {
const chatFactory = WechatyBro.glue.chatFactory
const utilFactory = WechatyBro.glue.utilFactory
const confFactory = WechatyBro.glue.confFactory
// hook chatFactory._recalledMsgProcess, resolve emit RECALLED type msg
const oldRecalledMsgProcess = chatFactory._recalledMsgProcess
chatFactory._recalledMsgProcess = function (msg) {
try {
oldRecalledMsgProcess.call(chatFactory, msg)
} catch (e) {
log('call oldRecalledMsgProcess failed:' + e.message)
}
const m = Object.assign({}, msg)
let content = utilFactory.htmlDecode(m.MMActualContent)
content = utilFactory.encodeEmoji(content)
// remove <br/> here
content = content.replace(/<br.*?\/>/g, '')
const revokemsg = utilFactory.xml2json(content).revokemsg
if (revokemsg.msgid) {
// add recalled message
m.MsgType = confFactory.MSGTYPE_RECALLED
m.MMActualContent = revokemsg.msgid
chatFactory.addChatMessage(m)
}
}
}
/**
*
* Help Functions which Proxy to WXAPP AngularJS Scope & Factory
* getMsgImg(message.MsgId,'slave')
* getMsgImg(message.MsgId,'big',message)
*/
function getMsgImg (id, type, message) {
const contentChatScope = WechatyBro.glue.contentChatScope
if (!contentChatScope) {
throw new Error('getMsgImg() contentChatScope not found')
}
const path = contentChatScope.getMsgImg(id, type, message)
if(!path)
return null
return window.location.origin + path
// https://wx.qq.com/?&lang=en_US/cgi-bin/mmwebwx-bin/webwxgetmsgimg?&MsgID=4520385745174034093&skey=%40crypt_f9cec94b_a3aa5c868466d81bc518293eb292926e
// https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetmsgimg?&MsgID=8454987316459381112&skey=%40crypt_f9cec94b_bd210b2224f217afeab8d462af70cf53
}
function getMsgEmoticon (id) {
const chatFactory = WechatyBro.glue.chatFactory
const message = chatFactory.getMsg(id)
return message.MMPreviewSrc || getMsgImg(message.MsgId, 'big', message) || message.MMThumbSrc
}
function getMsgVideo (id) {
const contentChatScope = WechatyBro.glue.contentChatScope
if (!contentChatScope) {
throw new Error('getMsgVideo() contentChatScope not found')
}
const path = contentChatScope.getMsgVideo(id)
if(!path)
return null
return window.location.origin + path
}
/**
* from playVoice()
*/
function getMsgVoice (id) {
const confFactory = WechatyBro.glue.confFactory
const accountFactory = WechatyBro.glue.accountFactory
const path = confFactory.API_webwxgetvoice + '?msgid=' + id + '&skey=' + accountFactory.getSkey()
return window.location.origin + path
}
function getMsgPublicLinkImg (id) {
const path = '/cgi-bin/mmwebwx-bin/webwxgetpubliclinkimg?url=xxx&msgid=' + id + '&pictype=location'
return window.location.origin + path
}
function getBaseRequest () {
const accountFactory = WechatyBro.glue.accountFactory
const BaseRequest = accountFactory.getBaseRequest()
return JSON.stringify(BaseRequest)
}
function getPassticket () {
const accountFactory = WechatyBro.glue.accountFactory
return accountFactory.getPassticket()
}
function getCheckUploadUrl () {
const confFactory = WechatyBro.glue.confFactory
return confFactory.API_checkupload
}
function getUploadMediaUrl () {
const confFactory = WechatyBro.glue.confFactory
return confFactory.API_webwxuploadmedia
}
function sendMedia (data) {
const chatFactory = WechatyBro.glue.chatFactory
const confFactory = WechatyBro.glue.confFactory
if (!chatFactory || !confFactory) {
log('sendMedia() chatFactory or confFactory not exist.')
return false
}
try {
const d = {
ToUserName: data.ToUserName,
MediaId: data.MediaId,
MsgType: data.MsgType,
FileName: data.FileName,
FileSize: data.FileSize,
MMFileExt: data.MMFileExt,
}
if (data.Signature) {
d.Signature = data.Signature
}
const m = chatFactory.createMessage(d)
chatFactory.appendMessage(m)
chatFactory.sendMessage(m)
} catch (e) {
log('sendMedia() exception: ' + e.message)
return false
}
return true
}
function forward (baseData, patchData) {
const chatFactory = WechatyBro.glue.chatFactory
const confFactory = WechatyBro.glue.confFactory
if (!chatFactory || !confFactory) {
log('forward() chatFactory or confFactory not exist.')
return false
}
try {
let m = chatFactory.createMessage(baseData)
// Need to override the parametes after called createMessage()
m = Object.assign(m, patchData)
chatFactory.appendMessage(m)
chatFactory.sendMessage(m)
} catch (e) {
log('forward() exception: ' + e.message)
return false
}
return true
}
function send (ToUserName, Content) {
const chatFactory = WechatyBro.glue.chatFactory
const confFactory = WechatyBro.glue.confFactory
if (!chatFactory || !confFactory) {
log('send() chatFactory or confFactory not exist.')
return false
}
try {
const m = chatFactory.createMessage({
ToUserName: ToUserName,
Content: Content,
MsgType: confFactory.MSGTYPE_TEXT,
})
chatFactory.appendMessage(m)
chatFactory.sendMessage(m)
} catch (e) {
log('send() exception: ' + e.message)
return false
}
return true
}
async function getMessage (id) {
const chatFactory = WechatyBro.glue.chatFactory
const confFactory = WechatyBro.glue.confFactory
const rootScope = WechatyBro.glue.rootScope
if (!chatFactory||!rootScope||!confFactory) {
log('chatFactory,rootScope or confFactory not inited')
return null
}
const msg = chatFactory.getMsg(id)
if (!msg) {
return null
}
if(msg.MMStatus==confFactory.MSG_SEND_STATUS_SENDING){
await new Promise((resolve)=>{
const unwatch=rootScope.$watch(()=>msg.MMStatus,()=>{
if(msg.MMStatus!=confFactory.MSG_SEND_STATUS_SENDING){
unwatch();
resolve();
}
});
})
}
const msgWithoutFunction = {}
Object.keys(msg).forEach(function (k) {
if (typeof msg[k] !== 'function') {
msgWithoutFunction[k] = msg[k]
}
})
return msgWithoutFunction
}
function getContact (id) {
const contactFactory = WechatyBro.glue.contactFactory
if (!contactFactory) {
log('contactFactory not inited')
return null
}
let contact = contactFactory.getContact(id)
let contactWithoutFunction = {}
if (contact) {
if (contact.isContact) {
// extend rawObj to identify `stranger`
contact.stranger = !(contact.isContact())
}
Object.keys(contact).forEach(function (k) {
if (typeof contact[k] !== 'function') {
contactWithoutFunction[k] = contact[k]
}
})
} else {
/**
* when `id` does not exist in _contact Array, maybe it is belongs to a stranger in a room.
* try to find in room's member list for this `id`, and return the contact info, if any.
*/
contact = Object.keys(_contacts)
.filter(id => id.match(/^@@/)) // only search in room
.map(id => _contacts[id]) // map to room array
.filter(r => r.MemberList.length) // get rid of room without member list
.filter(r => r.MemberList
.filter(m => m.UserName === id)
.length
)
.map(c => c.MemberList
.filter(m => m.UserName === id)[0]
)[0]
if (contact) {
contact.stranger = true
Object.keys(contact).forEach(k => {
if (typeof contact[k] !== 'function') {
contactWithoutFunction[k] = contact[k]
}
})
} else {
// Not found contact id
contactWithoutFunction = null
}
}
return contactWithoutFunction
}
function getUserName () {
if (!WechatyBro.loginState()) {
return null
}
const accountFactory = WechatyBro.glue.accountFactory
return accountFactory
? accountFactory.getUserName()
: null
}
function contactList () {
const contactFactory = WechatyBro.glue.contactFactory
return new Promise(resolve => retryFind(0, resolve))
// return
// retry 3 times, sleep 300ms between each time
function retryFind (attempt, done) {
attempt = attempt || 0
const contactIdList = contactFactory
.getAllFriendContact()
.map(c => c.UserName)
if (contactIdList && contactIdList.length) {
done(contactIdList)
} else if (attempt > 3) {
done([])
} else {
attempt++
setTimeout(() => retryFind(attempt, done), 1000)
}
}
}
function contactRemark (UserName, remark) {
if (remark === null || remark === undefined) {
remark = ''
}
const contact = _contacts[UserName]
if (!contact) {
throw new Error('contactRemark() can not found UserName ' + UserName)
}
const accountFactory = WechatyBro.glue.accountFactory
const confFactory = WechatyBro.glue.confFactory
const emojiFactory = WechatyBro.glue.emojiFactory
const mmHttp = WechatyBro.glue.mmHttp
return new Promise(resolve => {
mmHttp({
method: 'POST',
url: confFactory.API_webwxoplog,
data: angular.extend({
UserName: UserName,
CmdId: confFactory.oplogCmdId.MODREMARKNAME,
RemarkName: emojiFactory.formatHTMLToSend(remark),
}, accountFactory.getBaseRequest()),
MMRetry: {
count : 3,
timeout : 1e4,
serial : !0,
},
})
.success(() => {
contact.RemarkName = remark
return resolve(true)
})
.error(() => {
return resolve(false) // TODO: use reject???
})
})
}
// function roomFind(filterFunction) {
function roomList () {
const contactFactory = WechatyBro.glue.contactFactory
// var match
// if (!filterFunction) {
// match = () => true
// } else {
// match = eval(filterFunction)
// }
// log(match.toString())
return contactFactory.getAllChatroomContact()
// .filter(r => match(r.NickName))
.map(r => r.UserName)
}
function roomDelMember (ChatRoomName, UserName) {
const chatroomFactory = WechatyBro.glue.chatroomFactory
return chatroomFactory.delMember(ChatRoomName, UserName)
}
function roomAddMember (ChatRoomName, UserName) {
const chatroomFactory = WechatyBro.glue.chatroomFactory
// log(ChatRoomName)
// log(UserName)
return new Promise(resolve => {
// There's no return value of addMember :(
// https://github.com/wechaty/webwx-app-tracker/blob/f22cb043ff4201ee841990dbeb59e22643092f92/formatted/webwxApp.js#L2404-L2413
const timer = setTimeout(() => {
log('roomAddMember() timeout')
// TODO change to reject here. (BREAKING CHANGES)
return resolve(0)
}, 10 * 1000)
chatroomFactory.addMember(ChatRoomName, UserName, function (/* result */) {
clearTimeout(timer)
return resolve(1)
})
})
}
function roomModTopic (ChatRoomName, topic) {
const chatroomFactory = WechatyBro.glue.chatroomFactory
return chatroomFactory.modTopic(ChatRoomName, topic)
}
function roomCreate (UserNameList/* , topic */) {
const UserNameListArg = UserNameList.map(function (n) { return { UserName: n } })
const chatroomFactory = WechatyBro.glue.chatroomFactory
const state = WechatyBro.glue.state
return new Promise(resolve => {
chatroomFactory.create(UserNameListArg)
.then(function (r) {
// eslint-disable-next-line
if (r.BaseResponse && 0 == r.BaseResponse.Ret || -2013 == r.BaseResponse.Ret) {
state.go('chat', { userName: r.ChatRoomName }) // BE CAREFUL: key name is userName, not UserName! 20161001
// if (topic) {
// setTimeout(_ => roomModTopic(r.ChatRoomName, topic), 3000)
// }
if (!r.ChatRoomName) {
throw new Error('chatroomFactory.create() got empty r.ChatRoomName')
}
resolve(r.ChatRoomName)
} else {
reject(new Error('chatroomFactory.create() error with Ret: '
+ r && r.BaseResponse.Ret
+ 'with ErrMsg: '
+ r && r.BaseResponse.ErrMsg
))
}
})
.catch(function (e) {
// TODO change to reject (BREAKIKNG CHANGES)
resolve(
JSON.parse(
JSON.stringify(
e
, Object.getOwnPropertyNames(e)
)
)
)
})
})
}
function verifyUserRequest (UserName, VerifyContent) {
VerifyContent = VerifyContent || ''
const contactFactory = WechatyBro.glue.contactFactory
const confFactory = WechatyBro.glue.confFactory
const Ticket = '' // what's this?
return new Promise(resolve => {
contactFactory.verifyUser({
Opcode: confFactory.VERIFYUSER_OPCODE_SENDREQUEST,
Scene: confFactory.ADDSCENE_PF_WEB,
UserName,
Ticket,
VerifyContent,
})
.then(() => { // succ
// alert('ok')
// log('friendAdd(' + UserName + ', ' + VerifyContent + ') done')
return resolve(true)
})
.catch((err) => { // fail
// alert('not ok')
log('friendAdd(' + UserName + ', ' + VerifyContent + ') fail: ' + err)
resolve(false)
})
})
}
function verifyUserOk (UserName, Ticket) {
const contactFactory = WechatyBro.glue.contactFactory
const confFactory = WechatyBro.glue.confFactory
return new Promise(resolve => {
contactFactory.verifyUser({
UserName: UserName,
Opcode: confFactory.VERIFYUSER_OPCODE_VERIFYOK,
Scene: confFactory.ADDSCENE_PF_WEB,
Ticket: Ticket,
}).then(() => { // succ
// alert('ok')
log('friendVerify(' + UserName + ', ' + Ticket + ') done')
return resolve(true)
}).catch(err => { // fail
// alert('err')
log('friendVerify(' + UserName + ', ' + Ticket + ') fail' + err)
return resolve(false)
})
})
}
// ///////////////////////////////////////////////////////////////////////////
// ///////////////////////////////////////////////////////////////////////////
// ///////////////////////////////////////////////////////////////////////////
/*
* WechatyBro injectio must return this object.
* PuppetWeChatBridge need this to decide if injection is successful.
*/
const retObj = {
code: 200, // 2XX ok, 4XX/5XX error. HTTP like
message: 'any message',
}
if (typeof this.WechatyBro !== 'undefined') {
retObj.code = 201
retObj.message = 'WechatyBro already injected?'
return retObj
}
const WechatyBro = {
glue: {
// will be initialized by glueToAngular() function
},
// glue funcs
// , getLoginStatusCode: function() { return WechatyBro.glue.loginScope.code }
// , getLoginQrImgUrl: function() { return WechatyBro.glue.loginScope.qrcodeUrl }
angularIsReady,
// variable
vars: {
loginState : false,
initState : false,
scanCode : null,
heartBeatTimmer : null,
},
// funcs
ding, // simple return 'dong'
emit: window.wechatyPuppetBridgeEmit, // send event to PuppetWeChatBridge in Node.js
init, // initialize WechatyBro @ Browser
log, // log to Node.js
logout, // logout current logined user
send, // send message to wechat user
getContact,
getMessage,
getUserName,
getMsgImg,
getMsgEmoticon,
getMsgVideo,
getMsgVoice,
getMsgPublicLinkImg,
getBaseRequest,
getPassticket,
getUploadMediaUrl,
sendMedia,
forward,
getCheckUploadUrl,
// for Wechaty Contact Class
contactList,
contactRemark,
// for Wechaty Room Class
roomCreate,
roomAddMember,
roomList,
roomDelMember,
roomModTopic,
// for Friend Request
verifyUserRequest,
verifyUserOk,
// test purpose
isLogin: () => {
log('isLogin() DEPRECATED. use loginState() instead')
return loginState()
},
loginState,
}
this.WechatyBro = WechatyBro
retObj.code = 200
retObj.message = 'WechatyBro Inject Done'
return retObj
}.apply(window))