wqq
Version:
基于webqq协议的api模块
678 lines (658 loc) • 19.2 kB
JavaScript
var request = require('request')
var _ = require('lodash')
var fs = require('fs')
var Emitter = require('easy-emitter')
var encrypt = require('./lib/psw-encrypt')
var async = require('async')
var debug = require('debug')('wqq')
var appid = 501004106
var clientid = 53999199
var font = {
'name': '宋体',
'size': 10,
'style': [0, 0, 0],
'color': '000000'
}
var QQ = module.exports = function QQ() {
this.jar = request.jar()
this.request = request.defaults({
jar: this.jar,
headers: {
'Connection': 'keep-alive',
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/39.0.2171.65 Chrome/39.0.2171.65 Safari/537.36'
}
})
this.store = {}
this.form = {}
this.meta = {}
this.groups = {}
this.discus = {}
this.friends = null
this.toPoll = false
// this.failCount = 0
Emitter.call(this)
}
QQ.prototype._onPoll = function _onPoll(d) {
debug('拉取消息包', d)
if (!d) return // 未响应
if (typeof d === 'string') return // 偶尔返回异常html
if (d.retcode === 102) return // 无消息
if (d.retcode === 116) {
return this.jar.setCookie('ptwebqq=' + d.p, 'qq.com')
}
// if (_.contains([
// 100, 120, 121, // NotReLogin, ReLinkFailure
// 103, 112, 113, 115
// ], d.retcode) {
// return this._onDisconnect()
// }
if (d.retcode !== 0) {
return this._onDisconnect()
}
if (!Array.isArray(d.result)) return
d.result = d.result.sort(function (a, b) {
return a.value.time - b.value.time
})
var _this = this
async.eachSeries(d.result, function (d, next) {
_.extend(d, d.value)
delete d.value
// debug('接收消息', d)
if (d.msg_type === 48) { // kick_message
return _this._onKick()
}
if (_.contains([
'input_notify', // 121
'buddies_status_change',
'system_message'
], d.poll_type)) return next()
if (!_.contains([
45, // group_web_message
43, // group_message
42, // discu_message
9 // message
], d.msg_type)) {
debug('未知消息类型', d)
return next()
}
// debug('接收消息', d)
async.waterfall([
function (next) {
if (d.group_code) {
_this.getGroupInfo(d.group_code, function(e, d1) {
d.group_name = d1.ginfo.name
var c = _.find(d1.minfo, { uin: d.send_uin })
if (c) d.send_nick = c.nick
c = _.find(d1.cards, { muin: d.send_uin })
if (c) d.send_gnick = c.card
next()
})
} else if (d.did) {
_this.getDiscuInfo(d.did, function(e, d1) {
d.discu_name = d1.info.discu_name
var c = _.find(d1.mem_info, { uin: d.send_uin })
if (c) d.send_nick = c.nick
next()
})
} else next()
},
function (next) {
_this.getFriendMeta(d.send_uin || d.from_uin, function (e, d1) {
if (d1.account) d.send_account= d1.account
if (d1.account === 80000000) d.anonymous = true
if (d1.nick) d.send_nick = d1.nick
if (d1.mark) d.send_mark = d1.mark
next()
})
},
function (next1) {
var c = d.content
if (!c) return next1()
if (Array.isArray(c[0]) && c[0][0] === 'font') c = c.slice(1)
c = c.map(function (chunk) {
if (typeof chunk === 'string') return chunk
if (chunk[0] === 'face') return chunk
if (chunk[0] === 'cface') {
chunk[1] = chunk[1].name.match(/\.(.*)$/)[1]
return chunk
}
if (chunk[0] === 'offpic') {
chunk[1] = chunk[1].success
return chunk
}
})
c = _.compact(c)
if (typeof c[0] === 'string' &&
/[\u0000\u0002]宋体\r$/.test(c[0])) return
d.content = c
next1()
},
function (next) {
if (d.msg_type === 45) {
var d1 = d.xml.match(/<n t="t" s="(\d+:\d+):\d+"\/>/)
if (!d1) return
d.timestr = d1[1]
d1 = d.xml.match(/<n t="t" s="([^=]*)"\/>/g)
d.file = _.last(d1).match(/<n t="t" s="([^=]*)"\/>/)[1]
}
if (d.poll_type === 'file_message') d.file = d.name
next()
}
], function (e) {
d = _.omit(d, [
'msg_id', 'msg_id2', 'reply_ip', 'seq', 'info_seq',
'xml', 'group_type', 'ver'
])
if (d.msg_type === 9) d.is_private = true
_this.emit('message', d)
next()
})
})
}
QQ.prototype._onDisconnect = function _onDisconnect() {
// fixme: 需要重新登录
this.stopPoll()
this.emit('disconnect')
}
QQ.prototype._onKick = function _onKick() {
this.stopPoll()
this.emit('kick')
}
QQ.prototype.stopPoll = function stopPoll() {
this.toPoll = false
}
QQ.prototype.startPoll = function startPoll() {
this.toPoll = true
this._loopPoll()
}
QQ.prototype._loopPoll = function _loopPoll() {
if (!this.toPoll) return
var _this = this
this._poll(function (e, d) {
_this._onPoll(d)
setTimeout(function(){
_this._loopPoll()
}, e ? 10000 : 0)
})
}
QQ.prototype._poll = function _poll(cb) {
this.request.post({
url: 'http://d.web2.qq.com/channel/poll2',
form: {
r: JSON.stringify({
ptwebqq: this._getPtwebqq(),
psessionid: this.store.psessionid,
key: '',
clientid: clientid
})
},
headers: {
'Referer': 'http://d.web2.qq.com/proxy.html?v=20130916001&callback=1&id=2'
},
//timeout: 20000,
//timeout: 55000, // by @junfan
timeout: 65000, // 现在他们是60秒
json: true
}, function (e, r, d) {
cb(e, d)
})
}
QQ.prototype.changeStatus = function changeStatus(status, cb) {
// 在线online, Q我吧callme, 离开away, 忙碌busy,
// 静音silent, 隐身hidden, 离线offline
this.request({
url: 'http://d.web2.qq.com/channel/change_status2',
qs: {
newstatus: status,
clientid: clientid,
psessionid: this.store.psessionid,
t: Date.now()
},
headers: {
'Host': 'd.web2.qq.com',
'Referer': 'http://d.web2.qq.com/proxy.html?v=20130916001&callback=1&id=2'
},
json: true
}, function (e, r, d) {
cb(e, d.retcode === 0)
})
}
QQ.prototype.sendDiscuMsg = function sendDiscuMsg(did, chunks, cb) {
this.request.post({
url: 'http://d.web2.qq.com/channel/send_discu_msg2',
form: {
r: JSON.stringify({
did: did,
content: JSON.stringify(chunks.concat([['font', font]])),
clientid: clientid,
msg_id: nextMsgId(),
psessionid: this.store.psessionid
})
},
headers: {
'Referer': 'http://d.web2.qq.com/proxy.html?v=20130916001&callback=1&id=2'
},
json: true
}, function (e, r, d) {
cb(e, d.retcode === 0)
})
}
QQ.prototype.sendGroupMsg = function sendGroupMsg(uin, chunks, cb) {
this.request.post({
url: 'http://d.web2.qq.com/channel/send_qun_msg2',
form: {
r: JSON.stringify({
group_uin: uin,
content: JSON.stringify(chunks.concat([['font', font]])),
clientid: clientid,
msg_id: nextMsgId(),
psessionid: this.store.psessionid
})
},
headers: {
'Referer': 'http://d.web2.qq.com/proxy.html?v=20130916001&callback=1&id=2'
},
json: true
}, function (e, r, d) {
cb(e, d.retcode === 0)
})
}
QQ.prototype.sendBuddyMsg = function sendBuddyMsg(uin, chunks, cb) {
this.request.post({
url: 'http://d.web2.qq.com/channel/send_buddy_msg2',
form: {
r: JSON.stringify({
to: uin,
content: JSON.stringify(chunks.concat([['font', font]])),
clientid: clientid,
msg_id: nextMsgId(),
psessionid: this.store.psessionid
})
},
headers: {
'Referer': 'http://d.web2.qq.com/proxy.html?v=20130916001&callback=1&id=2'
},
json: true
}, function (e, r, d) {
cb(e, d.retcode === 0)
})
}
QQ.prototype.getDiscuInfo = function getDiscuInfo(did, cb) {
if (this.discus[did]) return cb(null, this.discus[did])
var _this = this
this.request({
url: 'http://d.web2.qq.com/channel/get_discu_info',
qs: {
did: did,
vfwebqq: this.store.vfwebqq,
clientid: clientid,
psessionid: this.store.psessionid,
t: Date.now()
},
headers: {
'Referer': 'http://d.web2.qq.com/proxy.html?v=20130916001&callback=1&id=2'
},
json: true
}, function (e, r, d) {
d = d.result
d.mcount = d.mem_info.length
d = _.pick(d, ['info', 'mem_info', 'mcount'])
_this.discus[did] = d
cb(e, d)
})
}
QQ.prototype.getGroupInfo = function getGroupInfo(code, cb) {
if (this.groups[code]) return cb(null, this.groups[code])
var _this = this
this.request({
url: 'http://s.web2.qq.com/api/get_group_info_ext2',
qs: {
gcode: code,
vfwebqq: this.store.vfwebqq,
t: Date.now()
},
headers: {
'Referer': 'http://s.web2.qq.com/proxy.html?v=20130916001&callback=1&id=1'
},
json: true
}, function (e, r, d) {
d = d.result
d.mcount = d.minfo.length
d = _.pick(d, ['cards', 'ginfo', 'minfo', 'mcount'])
_this.groups[code] = d
cb(e, d)
})
}
QQ.prototype.getFriendMeta = function getFriendMeta(tuin, cb) {
if (this.meta[tuin]) return cb(null, this.meta[tuin])
var o = { uin: tuin }
var d = _.find(this.friends.marknames, { uin: tuin })
if (d) o.mark = d.markname
d = _.find(this.meta.info, { uin: tuin })
if (d) {
o.nick = d.nick
this.meta[tuin] = o
return cb(e, o)
}
var _this = this
this.getFriendInfo(tuin, function (e, d) {
if (d.account) o.account = d.account
o.nick = d.nick
_this.meta[tuin] = o
cb(e, o)
})
}
QQ.prototype.getSelfInfo = function getSelfInfo(cb) {
this.getFriendInfo(this.store.uin, function (e, d) {
cb(e, d)
})
}
QQ.prototype.getFriendInfo = function getFriendInfo(tuin, cb) {
var _this = this
var o = {}
this._getFriendUin(tuin, function (e, d) {
if (d && d.result) o.account = d.result.account
_this._getFriendInfo(tuin, function (e, d) {
_.extend(o, d && d.result)
_this._getFriendLNick(tuin, function (e, d) {
if (d && d.result) o.lnick = d.result.lnick
cb(e, o)
})
})
})
}
QQ.prototype._getFriendInfo = function _getFriendInfo(tuin, cb) {
this.request({
url: 'http://s.web2.qq.com/api/get_friend_info2',
qs: {
tuin: tuin,
vfwebqq: this.store.vfwebqq,
clientid: clientid,
psessionid: this.store.psessionid,
t: Date.now()
},
headers: {
'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate, sdch',
'Accept-Language': 'zh-CN,zh;q=0.8,en;q=0.6',
'Content-Type': 'utf-8',
'Host': 's.web2.qq.com',
'Referer': 'http://s.web2.qq.com/proxy.html?v=20130916001&callback=1&id=1'
},
json: true
}, function (e, r, d) {
cb(e, d)
})
}
QQ.prototype._getFriendLNick = function _getFriendLNick(tuin, cb) {
this.request({
url: 'http://s.web2.qq.com/api/get_single_long_nick2',
qs: {
tuin: tuin,
vfwebqq: this.store.vfwebqq,
t: Date.now()
},
headers: {
'Referer': 'http://s.web2.qq.com/proxy.html?v=20130916001&callback=1&id=1'
},
json: true
}, function (e, r, d) {
cb(e, d)
})
}
QQ.prototype._getFriendUin = function _getFriendUin(tuin, cb) {
this.request({
url: 'http://s.web2.qq.com/api/get_friend_uin2',
qs: {
tuin: tuin,
type: 1,
vfwebqq: this.store.vfwebqq,
t: Date.now()
},
headers: {
'Referer': 'http://s.web2.qq.com/proxy.html?v=20130916001&callback=1&id=1'
},
json: true
}, function (e, r, d) {
cb(e, d)
})
}
QQ.prototype.getFriendFace = function getFriendFace(uin, cb) {
this.request({
url: 'http://face5.web.qq.com/cgi/svr/face/getface',
qs: {
cache: 1,
type: 1,
f: 40,
uin: uin,
t: Math.floor(Date.now() / 1000)
},
headers: {
'Referer': 'http://w.qq.com/'
},
encoding: null
}, function (e, r, b) {
cb(e, b)
})
}
QQ.prototype._getOnlineBuddies = function _getOnlineBuddies(cb) {
this.request({
url: 'http://d.web2.qq.com/channel/get_online_buddies2',
qs: {
vfwebqq: this.store.vfwebqq,
psessionid: this.store.psessionid,
t: Date.now(),
clientid: clientid
},
headers: {
'Referer': 'http://d.web2.qq.com/proxy.html?v=20130916001&callback=1&id=2'
},
json: true
}, function (e, r, d) {
cb(e, d)
})
}
QQ.prototype._getRecentList = function _getRecentList(cb) {
this.request.post({
url: 'http://d.web2.qq.com/channel/get_recent_list2',
form: {
r: JSON.stringify({
vfwebqq: this.store.vfwebqq,
clientid: clientid,
psessionid: this.store.psessionid
})
},
headers: {
'Referer': 'http://d.web2.qq.com/proxy.html?v=20130916001&callback=1&id=2'
},
json: true
}, function (e, r, d) {
cb(e, d)
})
}
QQ.prototype._getUserFriends = function _getUserFriends(cb) {
this.request.post({
url: 'http://s.web2.qq.com/api/get_user_friends2',
form: {
r: JSON.stringify({
vfwebqq: this.store.vfwebqq,
hash: hashU(this.store.uin, this._getPtwebqq())
})
},
headers: {
'Referer': 'http://s.web2.qq.com/proxy.html?v=20130916001&callback=1&id=1'
},
json: true
}, function (e, r, d) {
cb(e, d)
})
}
QQ.prototype.login = function login(password, vcode, cb) {
var _this = this
this.form.password = password || ''
this.form.vcode = vcode || ''
this._login(function (e, d) {
var m = d.match(/'([^']*)','([^']*)','([^']*)','([^']*)','([^']*)',\s*'([^']*)'/)
if (!/成功/.test(m[5])) return cb(e, false, m[5])
_this.store.nick = m[6]
_this.request({
url: m[3],
headers: {
'Host': 'ptlogin4.web2.qq.com'
}
}, function (e, r, d) {
_this.request({
url: 'http://s.web2.qq.com/api/getvfwebqq',
form: {
r: JSON.stringify({
ptwebqq: _this._getPtwebqq(),
psessionid: '',
t: Date.now(),
clientid: clientid
})
},
headers: {
'Referer': 'http://s.web2.qq.com/proxy.html?v=20130916001&callback=1&id=1'
},
json: true
}, function (e, r, d) {
_this.store.vfwebqq = d.result.vfwebqq
_this.request.post({
url: 'http://d.web2.qq.com/channel/login2',
form: {
r: JSON.stringify({
ptwebqq: _this._getPtwebqq(),
psessionid: '',
status: 'online',
clientid: clientid
})
},
headers: {
'Referer': 'http://d.web2.qq.com/proxy.html?v=20130916001&callback=1&id=2'
},
json: true
}, function (e, r, d) {
if (d.retcode) return cb(e, false)
_this.store._vfwebqq = d.result.vfwebqq
_this.store.psessionid = d.result.psessionid
_this.store.uin = d.result.uin
_this._getUserFriends(function (e, d) {
_this.friends = _.pick(d.result, ['info', 'marknames'])
cb(e, _this.store.nick)
})
})
})
})
})
}
QQ.prototype._login = function _login(cb) {
var s = this.store
var f = this.form
var vcode = s.hasImg ? f.vcode : s.vcode
this.request({
url: 'https://ssl.ptlogin2.qq.com/login?webqq_type=10&remember_uin=1&login2qq=1&u1=http%3A%2F%2Fw.qq.com%2Fproxy.html%3Flogin2qq%3D1%26webqq_type%3D10&h=1&ptredirect=0&ptlang=2052&daid=164&from_ui=1&pttype=1&dumy=&fp=loginerroralert&action=0-20-410040&mibao_css=m_webqq&t=1&g=1&js_type=0&js_ver=10109&login_sig=Efi2l6vPodHobOwrxFL8Q6T8UTg5obqH13BcdS5u3N46ezVr3RyUHjCDEcQkcIoc&pt_vcode_v1=0',
qs: {
u: f.account,
p: encrypt(f.password, s.salt, vcode),
verifycode: vcode,
pt_verifysession_v1: s.verifysession,
pt_randsalt: s.isRandSalt || 0,
aid: appid
},
headers: {
'Referer': 'https://ui.ptlogin2.qq.com/cgi-bin/login?daid=164&target=self&style=16&mibao_css=m_webqq' +
'&appid=' + appid + '&enable_qlogin=0&no_verifyimg=1&s_url=http%3A%2F%2Fw.qq.com%2Fproxy.html&f_url=loginerroralert&strong_login=1&login_state=10&t=20131024001'
}
}, function (e, r, d) {
cb(e, d)
})
}
QQ.prototype.getVcode = function getVcode(account, cb) {
var _this = this
var s = this.store
this.form.account = account || ''
this._checkVcode(function (e, d) {
var m = d.match(/'([^']*)','([^']*)','([^']*)','([^']*)','([^']*)'/)
s.vcode = m[2]
s.salt = Function('return "' + m[3] + '"')()
s.verifysession = m[4] || _this._getVerifysession()
s.isRandSalt = m[5]
s.hasImg = m[1] === '1'
if (!s.hasImg) return cb(e)
_this._getVcodeImage(function (e, b) {
s.verifysession = _this._getVerifysession()
cb(e, b)
})
})
}
QQ.prototype._getVcodeImage = function _getVcodeImage(cb) {
this.request({
url: 'https://ssl.captcha.qq.com/getimage',
qs: {
uin: this.form.account,
r: Math.random(),
aid: appid
},
encoding: null
}, function (e, r, b) {
cb(e, b)
})
}
QQ.prototype._checkVcode = function _checkVcode(cb) {
this.request({
url: 'https://ssl.ptlogin2.qq.com/check?pt_tea=1&js_ver=10109&js_type=0&login_sig=Hr5gU3cuTeCKinsyESg7XnoaYL*TwxclDHeJQbdol197nlPbIPQPQ*8AR1RuN1-7&u1=http%3A%2F%2Fw.qq.com%2Fproxy.html',
qs: {
uin: this.form.account,
r: Math.random(),
appid: appid
},
headers: {
'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate, sdch',
'Accept-Language': 'zh-CN,zh;q=0.8,en;q=0.6',
'Host': 'ssl.ptlogin2.qq.com',
'Referer': 'https://ui.ptlogin2.qq.com/cgi-bin/login?daid=164&target=self&style=16&mibao_css=m_webqq&appid=' + appid + '&enable_qlogin=0&no_verifyimg=1&s_url=http%3A%2F%2Fw.qq.com%2Fproxy.html&f_url=loginerroralert&strong_login=1&login_state=10&t=20131024001',
}
}, function (e, r, d) {
cb(e, d)
})
}
QQ.prototype._getPtwebqq = function _getPtwebqq() {
var cs = this.jar.getCookies('http://w.qq.com')
var c = _.find(cs, { key: 'ptwebqq' })
return c ? c.value : ''
}
QQ.prototype._getVerifysession = function _getVerifysession() {
var cs = this.jar.getCookies('https://ssl.ptlogin2.qq.com')
var c = _.find(cs, { key: 'verifysession' })
return c ? c.value : ''
}
// http://pub.idqqimg.com/smartqq/js/mq.js
var z = 0
var q = Date.now()
q = (q - q % 1E3) / 1E3
q = q % 1E4 * 1E4
function nextMsgId() {
z++
return q + z
}
function hashU(x, K) {
for (var N = K + "password error", T = "", V = [];;)
if (T.length <= N.length) {
T += x;
if (T.length == N.length) break
} else {
T = T.slice(0, N.length);
break
}
for (var U = 0; U < T.length; U++) V[U] = T.charCodeAt(U) ^ N.charCodeAt(U);
N = ["0", "1", "2", "3",
"4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"
];
T = "";
for (U = 0; U < V.length; U++) {
T += N[V[U] >> 4 & 15];
T += N[V[U] & 15]
}
return T
}