├── typings.json ├── .gitignore ├── README.md ├── package.json ├── src ├── info.js ├── tuling.js ├── msgcontent.js ├── QQ.js ├── poll.js ├── discuss.js ├── group.js ├── buddy.js └── login.js ├── .eslintrc ├── yarn.lock ├── libs ├── contentType.js └── httpclient.js └── app.js /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "globalDependencies": { 3 | "node": "registry:dt/node#6.0.0+20160915134512" 4 | }, 5 | "dependencies": { 6 | "lodash": "registry:npm/lodash#4.0.0+20160723033700" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Vscode config 2 | /.vscode 3 | /jsconfig.json 4 | 5 | # Typings 6 | /typings 7 | 8 | # Nodejs package 9 | /node_modules 10 | 11 | # Workspace tempdata 12 | /cookie.data 13 | /code.png 14 | /npm-debug.log 15 | 16 | # Heroku config 17 | /Procfile 18 | 19 | # Mac 20 | .DS_Store 21 | 22 | # Windows 23 | Thumbs.db 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qq-bot 2 | 基于 SmartQQ 协议和 Node.js 的 QQ 机器人。 3 | 4 | ## Features 5 | * 扫描码登录 6 | * 使用 Cookie 自动登录,无需扫描二维码 7 | * 支持私聊,群聊,讨论组 8 | 9 | ## Get Started 10 | ``` bash 11 | npm install 12 | npm start 13 | ``` 14 | or 15 | ``` bash 16 | yarn install 17 | yarn start 18 | ``` 19 | 20 | ## Bounds 21 | * 如果使用后台挂机模式,可以 `npm` 安装 `forever` 包 22 | 23 | ## TODO 24 | - [ ] 临时会话 25 | - [ ] 保存群~~调戏~~聊天记录 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qq-bot", 3 | "version": "0.1.0", 4 | "description": "Simple QQ Robot. Supports privite, disscus and group chating.", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "node app.js" 8 | }, 9 | "author": "gismanli, rocka", 10 | "license": "ISC", 11 | "dependencies": { 12 | "async": "^1.5.2", 13 | "lodash": "^4.5.1", 14 | "log": "^1.4.0" 15 | } 16 | } -------------------------------------------------------------------------------- /src/info.js: -------------------------------------------------------------------------------- 1 | const client = require('../libs/httpclient'); 2 | 3 | function getSelfInfo (callback) { 4 | var url = 'http://s.web2.qq.com/api/get_self_info2?t=' + Date.now(); 5 | client.get(url, function (ret) { 6 | if (ret.retcode === 0) { 7 | global.auth_options.nickname = ret.result.nick; 8 | } 9 | callback && callback(); 10 | }); 11 | } 12 | 13 | module.exports = { 14 | getSelfInfo: getSelfInfo 15 | } -------------------------------------------------------------------------------- /src/tuling.js: -------------------------------------------------------------------------------- 1 | const client = require('../libs/httpclient'); 2 | 3 | const baseURL = 'http://www.tuling123.com/openapi/api?key=' 4 | const tulingAPIKey = '873ba8257f7835dfc537090fa4120d14'; 5 | const baseURLTail = '&info=' 6 | const tulingURL = baseURL + tulingAPIKey + baseURLTail; 7 | 8 | function get(str, callback) { 9 | client.url_get(tulingURL + encodeURI(str), (err, res, info) => { 10 | callback && callback(JSON.parse(info).text); 11 | }); 12 | } 13 | 14 | module.exports = { 15 | getMsg: get 16 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | }, 12 | "sourceType": "module" 13 | }, 14 | "rules": { 15 | "no-const-assign": "warn", 16 | "no-this-before-super": "warn", 17 | "no-undef": "warn", 18 | "no-unreachable": "warn", 19 | "no-unused-vars": "warn", 20 | "constructor-super": "warn", 21 | "valid-typeof": "warn" 22 | } 23 | } -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | async@^1.5.2: 6 | version "1.5.2" 7 | resolved "http://registry.npm.taobao.org/async/download/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" 8 | 9 | lodash@^4.5.1: 10 | version "4.17.4" 11 | resolved "http://registry.npm.taobao.org/lodash/download/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" 12 | 13 | log@^1.4.0: 14 | version "1.4.0" 15 | resolved "http://registry.npm.taobao.org/log/download/log-1.4.0.tgz#4ba1d890fde249b031dca03bc37eaaf325656f1c" 16 | -------------------------------------------------------------------------------- /libs/contentType.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tMap = new Map(); 4 | 5 | tMap.set('html', 'text/html') 6 | .set('css', 'text/css') 7 | .set('js', 'application/x-javascript') 8 | .set('json', 'application/json') 9 | .set('jpg', 'image/jpeg') 10 | .set('png', 'image/png') 11 | .set('gif', 'image/gif') 12 | .set('ico', 'image/x-icon') 13 | .set('txt', 'text/plain'); 14 | 15 | function get(extName) { 16 | if (tMap.has(extName)) { 17 | return tMap.get(extName); 18 | } else { 19 | return 'text/plain'; 20 | } 21 | } 22 | 23 | module.exports = { 24 | get: get 25 | } -------------------------------------------------------------------------------- /src/msgcontent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var codeMap = [14, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 0, 50, 51, 96, 53, 54, 73, 74, 75, 76, 77, 78, 55, 56, 57, 58, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 32, 113, 114, 115, 63, 64, 59, 33, 34, 116, 36, 37, 38, 91, 92, 93, 29, 117, 72, 45, 42, 39, 62, 46, 47, 71, 95, 118, 119, 120, 121, 122, 123, 124, 27, 21, 23, 25, 26, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170] 4 | 5 | function build(msg, emojiStat) { 6 | let content; 7 | if (emojiStat === true) { 8 | content = JSON.stringify([ 9 | '' + msg, 10 | ['face', Math.floor(Math.random() * codeMap.length)], 11 | ['font', global.font] 12 | ]); 13 | } else { 14 | content = JSON.stringify(['' + msg, ['font', global.font]]); 15 | } 16 | return content; 17 | } 18 | 19 | module.exports = { 20 | bulid: build 21 | } -------------------------------------------------------------------------------- /src/QQ.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | 5 | const login = require('./login'); 6 | const poll = require('./poll'); 7 | 8 | global.appid = 501004106; 9 | global.clientid = 53999199; 10 | global.font = { 11 | 'name': '微软雅黑', 12 | 'size': 12, 13 | 'style': [0, 0, 0], 14 | 'color': '333333' 15 | } 16 | 17 | class QQ { 18 | constructor() { 19 | this.auth_options = {}; 20 | this.toPoll = false; 21 | this.cookie = null; 22 | this.groups = {}; 23 | this.group_code = {}; 24 | this.discus = {}; 25 | this.nickname = ''; 26 | } 27 | 28 | Login() { 29 | fs.exists('./cookie.data', isExist => { 30 | if (isExist) { 31 | fs.readFile('./cookie.data', 'utf-8', function (err, data) { 32 | login._Login(data, function () { 33 | poll.startPoll(); 34 | }); 35 | }); 36 | } else { 37 | login.Login(function () { 38 | poll.startPoll(); 39 | }); 40 | } 41 | }); 42 | } 43 | } 44 | 45 | module.exports = QQ; 46 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const url = require('url'); 5 | const path = require('path'); 6 | const http = require('http'); 7 | 8 | const QQ = require('./src/QQ'); 9 | const ContentType = require('./libs/contentType'); 10 | const regexs = { 11 | extName: /\.(w+)$/ 12 | } 13 | 14 | let qq = new QQ(); 15 | let root = path.resolve('.'); 16 | let server = http.createServer((request, response) => { 17 | var pathName = url.parse(request.url).pathname; 18 | var filePath = path.join(root, pathName); 19 | if (request.method === 'GET') { 20 | // try to find and read local file 21 | fs.stat(filePath, (err, stats) => { 22 | // no error occured, read file 23 | if (!err && stats.isFile()) { 24 | let extName; 25 | try { 26 | extName = regexs.extName.exec(pathName)[1]; 27 | } catch (e) {} 28 | response.writeHead(200, { 'content-Type': ContentType.get(extName) }); 29 | fs.createReadStream(filePath).pipe(response); 30 | // cannot find file, but received index request 31 | } else { 32 | response.writeHead(200, { 'content-Type': 'text/html' }); 33 | response.end('Nothing Here :('); 34 | } 35 | }); 36 | } 37 | }); 38 | 39 | var serverPort = process.env.PORT || 8080; 40 | 41 | server.listen(serverPort); 42 | 43 | qq.Login(); -------------------------------------------------------------------------------- /src/poll.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const async = require('async'); 4 | const Log = require('log'); 5 | const log = new Log('debug'); 6 | const _ = require('lodash'); 7 | const client = require('../libs/httpclient'); 8 | const login = require('./login'); 9 | const group = require('./group'); 10 | const buddy = require('./buddy'); 11 | const discuss = require('./discuss'); 12 | const info = require('./info'); 13 | 14 | var toPoll = false; 15 | 16 | function onPoll(aaa, cb) { 17 | let params = { 18 | r: JSON.stringify({ 19 | ptwebqq: global.auth_options.ptwebqq, 20 | clientid: global.auth_options.clientid, 21 | psessionid: global.auth_options.psessionid, 22 | key: "" 23 | }) 24 | }; 25 | client.post({ 26 | url: "http://d1.web2.qq.com/channel/poll2", 27 | timeout: 65000 28 | }, params, function (ret) { 29 | cb(ret); 30 | }); 31 | }; 32 | 33 | function stopPoll() { 34 | toPoll = false; 35 | }; 36 | 37 | function startPoll() { 38 | toPoll = true; 39 | log.info('polling...'); 40 | if (!global.auth_options.nickname) { 41 | info.getSelfInfo(() => loopPoll(global.auth_options)) 42 | } else { 43 | loopPoll(global.auth_options); 44 | } 45 | }; 46 | 47 | function onDisconnect() { 48 | log.info(`Disconnect.`); 49 | // fixme: 需要重新登录 50 | stopPoll(); 51 | login._Login(client.get_cookies_string(), function () { 52 | startPoll(); 53 | }); 54 | } 55 | 56 | function loopPoll(auth_options) { 57 | if (!toPoll) return; 58 | onPoll(auth_options, function (e) { 59 | _onPoll(e); 60 | loopPoll(); 61 | }) 62 | }; 63 | 64 | function _onPoll(ret) { 65 | if (!ret) return; 66 | if (typeof ret === 'string') return; 67 | if (ret.retcode === 102) return; 68 | if (ret.retcode === 103) { 69 | log.info('请先登录一下WebQQ!'); 70 | toPoll = false; 71 | return; 72 | } 73 | if (ret.retcode != 0) { 74 | return onDisconnect(); 75 | } 76 | if (!Array.isArray(ret.result)) return; 77 | 78 | ret.result = ret.result.sort(function (a, b) { 79 | return a.value.time - b.value.time 80 | }); 81 | 82 | async.eachSeries(ret.result, function (item, next) { 83 | _.extend(item, item.value); 84 | delete item.value; 85 | 86 | if (['input_notify', 'buddies_status_change', 'system_message'].indexOf(item.poll_type) > -1) { 87 | return next(); 88 | } 89 | 90 | async.waterfall([ 91 | next => { 92 | console.log(`[New Message]: ${JSON.stringify(item)}`); 93 | switch (item.poll_type) { 94 | case 'group_message': 95 | group.handle(item); 96 | break; 97 | case 'discu_message': 98 | discuss.handle(item); 99 | break; 100 | case 'message': 101 | buddy.handle(item); 102 | default: 103 | break; 104 | } 105 | next(); 106 | } 107 | ]); 108 | }); 109 | return; 110 | }; 111 | 112 | module.exports = { 113 | onPoll: onPoll, 114 | stopPoll: stopPoll, 115 | startPoll: startPoll, 116 | onDisconnect: onDisconnect, 117 | loopPoll: loopPoll, 118 | _onPoll: _onPoll 119 | } -------------------------------------------------------------------------------- /src/discuss.js: -------------------------------------------------------------------------------- 1 | const msgcontent = require('./msgcontent'); 2 | const client = require('../libs/httpclient'); 3 | const tuling = require('./tuling'); 4 | 5 | const regAtName = new RegExp(`@{global.auth_options.nickname}`); 6 | 7 | /** 8 | * @type {Object} 9 | * 储存所有讨论组信息 10 | */ 11 | let allDiscuss = { 12 | name: new Map(), 13 | did: new Map() 14 | } 15 | 16 | /** 17 | * 向指定did的讨论组发送消息 18 | * 19 | * @param {any} did 20 | * @param {any} msg 21 | * @param {any} callback 22 | */ 23 | function sendMsg(did, msg, callback) { 24 | var params = { 25 | r: JSON.stringify({ 26 | did: did, 27 | content: msgcontent.bulid(msg), 28 | face: 537, 29 | clientid: global.clientid, 30 | msg_id: client.nextMsgId(), 31 | psessionid: global.auth_options.psessionid 32 | }) 33 | }; 34 | 35 | client.post({ 36 | url: 'http://d1.web2.qq.com/channel/send_discu_msg2' 37 | }, params, function (ret) { 38 | callback && callback(ret); 39 | }); 40 | }; 41 | 42 | function getAllDiscuss(callback) { 43 | let options = { 44 | method: 'GET', 45 | protocol: 'http:', 46 | host: 's.web2.qq.com', 47 | path: '/api/get_discus_list?clientid=' + global.auth_options.clientid + '&psessionid=' + global.auth_options.psessionid + '&vfwebqq=' + global.auth_options.vfwebqq + '&t=' + Date.now(), 48 | headers: { 49 | 'Referer': 'http://s.web2.qq.com/proxy.html?v=20130916001&callback=1&id=1', 50 | 'Cookie': client.get_cookies_string() 51 | } 52 | }; 53 | 54 | client.url_get(options, function (err, res, data) { 55 | let list = JSON.parse(data); 56 | list.result.dnamelist.forEach(e => { 57 | allDiscuss.did.set(e.name, e.did); 58 | allDiscuss.name.set(e.did, e.name); 59 | }); 60 | callback && callback(allDiscuss); 61 | }); 62 | } 63 | 64 | /** 65 | * 获取指定did讨论组的信息 66 | * 67 | * @param {any} did 68 | * @param {any} cb 69 | */ 70 | function getDiscussInfo(did, callback) { 71 | var options = { 72 | method: 'GET', 73 | protocol: 'http:', 74 | host: 'd1.web2.qq.com', 75 | path: '/channel/get_discu_info?did=' + did + '&vfwebqq=' + global.auth_options.vfwebqq + '&clientid=' + global.auth_options.clientid + '&psessionid=' + global.auth_options.psessionid + '&t=' + Date.now(), 76 | headers: { 77 | 'Referer': 'http://d1.web2.qq.com/proxy.html?v=20151105001&callback=1&id=2', 78 | 'Cookie': client.get_cookies_string() 79 | } 80 | } 81 | 82 | client.url_get(options, function (err, res, data) { 83 | console.log(res); 84 | callback && callback(data); 85 | }); 86 | }; 87 | 88 | /** 89 | * 用图灵机器人API响应讨论组消息 90 | * 91 | * @param {any} msg 92 | */ 93 | function Handle(msg) { 94 | var isAt = msg.content[1].indexOf('@' + global.auth_options.nickname); 95 | if (isAt > -1) { 96 | tuling.getMsg(msg.content[3].trim(), str => sendMsg(msg.did, str)); 97 | } 98 | } 99 | 100 | /** 101 | * 根据讨论组did获取名称 102 | * 103 | * @param {any} did 104 | * @param {any} callback 105 | * @returns 106 | */ 107 | function getDiscussName(did, callback) { 108 | let name = allDiscuss.name.get(did) 109 | if (callback) return callback(name); 110 | else return name; 111 | } 112 | 113 | /** 114 | * 根据讨论组名称获取临时did 115 | * 116 | * @param {any} name 117 | * @param {any} callback 118 | * @returns 119 | */ 120 | function getDiscussDid(name, callback) { 121 | let did = allDiscuss.did.get(name) 122 | if (callback) return callback(did); 123 | else return did; 124 | } 125 | 126 | module.exports = { 127 | handle: Handle, 128 | getInfo: getDiscussInfo, 129 | getAll: getAllDiscuss, 130 | getName: getDiscussName, 131 | getDid: getDiscussDid 132 | } 133 | -------------------------------------------------------------------------------- /src/group.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const msgcontent = require('./msgcontent'); 4 | const client = require('../libs/httpclient'); 5 | const tuling = require('./tuling'); 6 | 7 | /** 8 | * @type {Object} 9 | * 储存所有群组信息 10 | */ 11 | let allGroups = { 12 | name: new Map(), 13 | uin: new Map() 14 | } 15 | 16 | function hashU(x, K) { 17 | let N, T, U, V; 18 | x += ""; 19 | for (N = [], T = 0; T < K.length; T++) N[T % 4] ^= K.charCodeAt(T); 20 | U = ["EC", "OK"]; 21 | V = []; 22 | V[0] = x >> 24 & 255 ^ U[0].charCodeAt(0); 23 | V[1] = x >> 16 & 255 ^ U[0].charCodeAt(1); 24 | V[2] = x >> 8 & 255 ^ U[1].charCodeAt(0); 25 | V[3] = x & 255 ^ U[1].charCodeAt(1); 26 | U = []; 27 | for (T = 0; T < 8; T++) U[T] = T % 2 == 0 ? N[T >> 1] : V[T >> 1]; 28 | N = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"]; 29 | V = ""; 30 | for (T = 0; T < U.length; T++) { 31 | V += N[U[T] >> 4 & 15]; 32 | V += N[U[T] & 15] 33 | } 34 | return V; 35 | }; 36 | 37 | /** 38 | * 向指定uin的群组发送消息 39 | * 40 | * @param {number} uin group uin 41 | * @param {string} msg msg string 42 | * @param {function} cb callback(httpPOSTReturn) 43 | */ 44 | function sendMsg(uin, msg, cb) { 45 | let params = { 46 | r: JSON.stringify({ 47 | group_uin: uin, 48 | content: msgcontent.bulid(msg), 49 | clientid: global.clientid, 50 | msg_id: client.nextMsgId(), 51 | psessionid: global.auth_options.psessionid 52 | }) 53 | }; 54 | 55 | client.post({ 56 | url: 'http://d1.web2.qq.com/channel/send_qun_msg2' 57 | }, params, function (ret) { 58 | cb && cb(ret); 59 | }); 60 | }; 61 | 62 | /** 63 | * 获取当前QQ号所有群,名称及临时 gid !pass 64 | * 65 | * @param {function} callback callback(mapAllGroups) 66 | */ 67 | function getAllGroups(callback) { 68 | let params = { 69 | r: JSON.stringify({ 70 | vfwebqq: global.auth_options.vfwebqq, 71 | hash: hashU(global.auth_options.uin, global.auth_options.ptwebqq) 72 | }) 73 | }; 74 | 75 | client.post({ 76 | url: 'http://s.web2.qq.com/api/get_group_name_list_mask2' 77 | }, params, function (response) { 78 | response.result.gnamelist.forEach(e => { 79 | allGroups.uin.set(e.name, e.gid); 80 | allGroups.name.set(e.gid, e.name); 81 | }); 82 | callback && callback(allGroups); 83 | }); 84 | }; 85 | 86 | /** 87 | * 根据临时 gid 获取群详细信息 pass! 88 | * 89 | * @param {any} gid 群组gid 90 | * @param {function} callback 91 | */ 92 | function getDetail(uin, callback) { 93 | let gid = parseInt(uin); 94 | let options = { 95 | method: 'GET', 96 | protocol: 'http:', 97 | host: 's.web2.qq.com', 98 | path: '/api/get_group_info_ext2?gcode=' + gid + '&vfwebqq=' + global.auth_options.vfwebqq + '&t=' + Date.now(), 99 | headers: { 100 | 'Cookie': client.get_cookies_string(), 101 | 'Referer': 'http://s.web2.qq.com/proxy.html?v=20130916001&callback=1&id=1' 102 | } 103 | }; 104 | 105 | client.url_get(options, function (err, res, data) { 106 | //TODO: 数据储存 107 | console.log(typeof data); 108 | console.log(data); 109 | callback && callback(data); 110 | }); 111 | }; 112 | 113 | /** 114 | * 使用图灵机器人API处理消息 115 | * 116 | * @param {Object} msg 消息对象/poll返回对象 117 | */ 118 | function Handle(msg) { 119 | let isAt = msg.content.indexOf('@' + global.auth_options.nickname); 120 | if (isAt > -1) { 121 | tuling.getMsg(msg.content[3].trim(), str => sendMsg(msg.group_code, str)); 122 | } 123 | } 124 | 125 | /** 126 | * 根据群uin获取名称 127 | * 128 | * @param {any} uin 129 | * @param {any} callback 130 | * @returns 131 | */ 132 | function getGroupName(uin, callback) { 133 | let name = allGroups.name.get(uin) 134 | if (callback) return callback(name); 135 | else return name; 136 | } 137 | 138 | /** 139 | * 根据群名称获取临时uin 140 | * 141 | * @param {any} name 142 | * @param {any} callback 143 | * @returns 144 | */ 145 | function getGroupUin(name, callback) { 146 | let uin = allGroups.uin.get(name) 147 | if (callback) return callback(uin); 148 | else return uin; 149 | } 150 | 151 | module.exports = { 152 | handle: Handle, 153 | getAll: getAllGroups, 154 | getDetail: getDetail, 155 | getName: getGroupName, 156 | getUin: getGroupUin 157 | } 158 | -------------------------------------------------------------------------------- /src/buddy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const msgcontent = require('./msgcontent'); 4 | const client = require('../libs/httpclient'); 5 | const tuling = require('./tuling'); 6 | 7 | /** 8 | * @type {Object} 9 | * 储存所有好友信息,昵称与uin 10 | */ 11 | let allFriends = { 12 | uin: new Map(), 13 | nick: new Map(), 14 | account: new Map() 15 | } 16 | 17 | function hashU(x, K) { 18 | let N, T, U, V; 19 | x += ""; 20 | for (N = [], T = 0; T < K.length; T++) N[T % 4] ^= K.charCodeAt(T); 21 | U = ["EC", "OK"]; 22 | V = []; 23 | V[0] = x >> 24 & 255 ^ U[0].charCodeAt(0); 24 | V[1] = x >> 16 & 255 ^ U[0].charCodeAt(1); 25 | V[2] = x >> 8 & 255 ^ U[1].charCodeAt(0); 26 | V[3] = x & 255 ^ U[1].charCodeAt(1); 27 | U = []; 28 | for (T = 0; T < 8; T++) U[T] = T % 2 == 0 ? N[T >> 1] : V[T >> 1]; 29 | N = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"]; 30 | V = ""; 31 | for (T = 0; T < U.length; T++) { 32 | V += N[U[T] >> 4 & 15]; 33 | V += N[U[T] & 15] 34 | } 35 | return V; 36 | }; 37 | 38 | 39 | /** 40 | * 获取所有好友昵称和uin 41 | * 42 | * @param {any} callback 43 | */ 44 | function getAllFriends(callback) { 45 | let params = { 46 | r: JSON.stringify({ 47 | vfwebqq: global.auth_options.vfwebqq, 48 | hash: hashU(global.auth_options.uin, global.auth_options.ptwebqq) 49 | }) 50 | }; 51 | 52 | client.post({ 53 | url: 'http://s.web2.qq.com/api/get_user_friends2' 54 | }, params, function (response) { 55 | response.result.info.forEach(e => { 56 | allFriends.uin.set(e.nick, e.uin); 57 | allFriends.nick.set(e.uin, e.nick); 58 | }); 59 | callback && callback(allFriends); 60 | }); 61 | } 62 | 63 | /** 64 | * 根据好友uin获取nick 65 | * 66 | * @param {any} uin 67 | * @param {any} callback 68 | * @returns 69 | */ 70 | function getFriendNick(uin, callback) { 71 | let nick = allFriends.nick.get(uin) 72 | if (callback) return callback(nick); 73 | else return nick; 74 | } 75 | 76 | /** 77 | * 根据好友nick获取uin 78 | * 79 | * @param {any} nick 80 | * @param {any} callback 81 | * @returns 82 | */ 83 | function getFriendUin(nick, callback) { 84 | let uin = allFriends.uin.get(nick) 85 | if (callback) return callback(uin); 86 | else return uin; 87 | } 88 | 89 | /** 90 | * 根据好友uin获取QQ号 91 | * 92 | * @param {number} uin 93 | * @param {function} callback 94 | */ 95 | function getFriendAccount(uin, callback) { 96 | let account = allFriends.account.get(uin); 97 | if (account) return callback && callback(account); 98 | let options = { 99 | method: 'GET', 100 | protocol: 'http:', 101 | host: 's.web2.qq.com', 102 | path: '/api/get_friend_uin2?tuin=' + uin + '&type=1&vfwebqq=' + global.auth_options.vfwebqq + '&t=' + Date.now(), 103 | headers: { 104 | 'Referer': 'http://s.web2.qq.com/proxy.html?v=20130916001&callback=1&id=1', 105 | 'Cookie': client.get_cookies_string() 106 | } 107 | }; 108 | 109 | client.url_get(options, function (err, res, data) { 110 | if (data.account) { 111 | allFriends.account.set(uin, data.account); 112 | callback && callback(data.uin); 113 | } else { callback && callback(data); } 114 | }); 115 | } 116 | 117 | /** 118 | * 向指定uin的好友发送消息 119 | * 120 | * @param {number} uin 121 | * @param {string} msg 122 | * @param {function} callback 123 | */ 124 | function sendMsg(uin, msg, callback) { 125 | let params = { 126 | r: JSON.stringify({ 127 | to: uin, 128 | face: 522, 129 | content: msgcontent.bulid(msg), 130 | clientid: global.clientid, 131 | msg_id: client.nextMsgId(), 132 | psessionid: global.auth_options.psessionid 133 | }) 134 | }; 135 | 136 | client.post({ 137 | url: 'http://d1.web2.qq.com/channel/send_buddy_msg2' 138 | }, params, function (response) { 139 | callback && callback(response); 140 | }); 141 | }; 142 | 143 | /** 144 | * 使用图灵机器人API处理消息 145 | * 146 | * @param {Object} msg 消息对象/poll返回对象 147 | * @param {any} callback 148 | */ 149 | function Handle(item, callback) { 150 | getFriendAccount(item.from_uin); 151 | tuling.getMsg(item.content[1], str => { 152 | sendMsg(item.from_uin, str, callback); 153 | }); 154 | } 155 | 156 | module.exports = { 157 | getAll: getAllFriends, 158 | getUin: getFriendUin, 159 | getNick: getFriendNick, 160 | getAccount: getFriendAccount, 161 | handle: Handle 162 | } -------------------------------------------------------------------------------- /src/login.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const Log = require('log'); 5 | const log = new Log('debug'); 6 | const client = require('../libs/httpclient'); 7 | 8 | let ptwebqq, 9 | vfwebqq; 10 | 11 | function sleep(milliSeconds) { 12 | let startTime = new Date().getTime(); 13 | while (new Date().getTime() < startTime + milliSeconds); 14 | } 15 | 16 | function Login(callback) { 17 | let options = { 18 | protocol: 'https:', 19 | host: 'ui.ptlogin2.qq.com', 20 | path: '/cgi-bin/login?daid=164&target=self&style=16&mibao_css=m_webqq&appid=501004106&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', 21 | headers: { 22 | 'Cookie': client.get_cookies_string(), 23 | 'Referer': 'http://w.qq.com/' 24 | } 25 | }; 26 | 27 | client.url_get(options, function () { 28 | //获取二维码 29 | let url = "https://ssl.ptlogin2.qq.com/ptqrshow?appid=501004106&e=0&l=M&s=5&d=72&v=4&t=" + Math.random(); 30 | client.url_get(url, function (err, res, data) { 31 | fs.writeFile('./code.png', data, 'binary', function (err) { 32 | if (err) { 33 | console.log(err); 34 | } else { 35 | console.log("down success"); 36 | require('child_process').exec('open ./code.png'); 37 | waitingScan(callback); 38 | } 39 | }); 40 | }, function (res) { 41 | res.setEncoding('binary'); 42 | }); 43 | }); 44 | }; 45 | 46 | function _Login(cookie, callback) { 47 | log.info('自动登录...'); 48 | ptwebqq = cookie.match(/ptwebqq=(.+?);/)[1]; 49 | 50 | client.set_cookies(cookie); 51 | 52 | getVfwebqq(ptwebqq, function (ret) { 53 | if (ret.retcode === 0) vfwebqq = ret.result.vfwebqq; 54 | loginToken(ptwebqq, null, function (ret) { 55 | if (ret.retcode === 0) { 56 | // 重新获取二维码登录 57 | if (!ret.result) { 58 | require('child_process').exec('rm -rf cookie.data') 59 | Login(); 60 | return; 61 | } 62 | 63 | log.info('登录成功'); 64 | 65 | global.auth_options = { 66 | clientid: global.clientid, 67 | ptwebqq: ptwebqq, 68 | vfwebqq: vfwebqq, 69 | uin: ret.result.uin, 70 | psessionid: ret.result.psessionid 71 | } 72 | 73 | callback && callback(); 74 | } else { 75 | log.info("登录失败"); 76 | return log.error(ret); 77 | } 78 | }); 79 | }); 80 | } 81 | 82 | function checkVcode(cb) { 83 | let qrsig = client.decode_qrsig(client.get_cookie_key('qrsig')); 84 | let options = { 85 | protocol: 'https:', 86 | host: 'ssl.ptlogin2.qq.com', 87 | path: '/ptqrlogin?ptqrtoken='+qrsig+'&webqq_type=10&remember_uin=1&login2qq=1&aid=501004106&u1=http%3A%2F%2Fw.qq.com%2Fproxy.html%3Flogin2qq%3D1%26webqq_type%3D10&ptredirect=0&ptlang=2052&daid=164&from_ui=1&pttype=1&dumy=&fp=loginerroralert&action=0-0-123332&mibao_css=m_webqq&t=undefined&g=1&js_type=0&js_ver=10141&login_sig=&pt_randsalt=0', 88 | headers: { 89 | 'Cookie': client.get_cookies_string(), 90 | 'Referer': 'https://ui.ptlogin2.qq.com/cgi-bin/login?daid=164&target=self&style=16&mibao_css=m_webqq&appid=501004106&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' 91 | } 92 | }; 93 | 94 | client.url_get(options, function (err, res, data) { 95 | let ret = data.match(/\'(.*?)\'/g).map(function (i) { 96 | let last = i.length - 2; 97 | return i.substr(1, last); 98 | }); 99 | cb(ret); 100 | }); 101 | }; 102 | 103 | function waitingScan(callback) { 104 | log.info("登录 step1: 等待二维码校验结果."); 105 | 106 | checkVcode(function (ret) { 107 | let retCode = parseInt(ret[0]); 108 | if (retCode === 0 && ret[2].match(/^http/)) { 109 | log.info("setp1: 二维码扫描成功."); 110 | require('child_process').exec('rm -rf ./code.png'); 111 | log.info("登录 step2: cookie 获取 ptwebqq"); 112 | 113 | getPtwebqq(ret[2], callback); 114 | } else if (retCode === 66 || retCode === 67) { 115 | sleep(1000); 116 | waitingScan(callback); 117 | } else { 118 | log.error("二维码扫描登录失败.", ret); 119 | return; 120 | } 121 | }); 122 | }; 123 | 124 | function getPtwebqq(url, callback) { 125 | client.url_get(url, function (err, res, data) { 126 | if (!err) { 127 | log.info('获取cookie & ptwebqq成功.'); 128 | ptwebqq = client.get_cookies().filter(function (item) { 129 | return item.match(/ptwebqq/); 130 | }).pop().replace(/ptwebqq\=(.*?);.*/, '$1'); 131 | 132 | getVfwebqq(ptwebqq, function (ret) { 133 | if (ret.retcode === 0) { 134 | vfwebqq = ret.result.vfwebqq; 135 | log.info("获取vfwebqq成功."); 136 | } 137 | 138 | log.info("登录 step4: 获取 uin, psessionid"); 139 | 140 | loginToken(ptwebqq, null, function (ret) { 141 | if (ret.retcode === 0) { 142 | fs.writeFile('./cookie.data', client.get_cookies()); 143 | 144 | log.info('登录成功'); 145 | 146 | global.auth_options = { 147 | clientid: global.clientid, 148 | ptwebqq: ptwebqq, 149 | vfwebqq: vfwebqq, 150 | uin: ret.result.uin, 151 | psessionid: ret.result.psessionid 152 | }; 153 | callback(); 154 | } else { 155 | log.info("登录失败"); 156 | return log.error(ret); 157 | } 158 | }); 159 | }); 160 | } 161 | }); 162 | }; 163 | 164 | function getVfwebqq(ptwebqq, cb) { 165 | log.info('登录 step3: 获取vfwebqq'); 166 | 167 | let options = { 168 | method: 'GET', 169 | protocol: 'http:', 170 | host: 's.web2.qq.com', 171 | path: '/api/getvfwebqq?ptwebqq=' + ptwebqq + '&clientid=' + global.clientid + '&psessionid=&t=' + Math.random(), 172 | headers: { 173 | 'Cookie': client.get_cookies_string(), 174 | 'Origin': 'http://s.web2.qq.com', 175 | 'Referer': 'http://s.web2.qq.com/proxy.html?v=20130916001&callback=1&id=1', 176 | } 177 | }; 178 | client.url_get(options, function (err, res, data) { 179 | let ret; 180 | try { 181 | ret = JSON.parse(data); 182 | } catch (err) { 183 | getVfwebqq(ptwebqq, cb); 184 | return; 185 | } 186 | cb(ret); 187 | }); 188 | } 189 | 190 | function loginToken(ptwebqq, psessionid, cb) { 191 | if (!psessionid) psessionid = null; 192 | let form = { 193 | r: JSON.stringify({ 194 | ptwebqq: ptwebqq, 195 | clientid: global.clientid, 196 | psessionid: psessionid || "", 197 | status: "online" 198 | }) 199 | }; 200 | client.url_post({ 201 | protocol: 'http:', 202 | host: 'd1.web2.qq.com', 203 | path: '/channel/login2', 204 | method: 'POST', 205 | headers: { 206 | 'Origin': 'http://d1.web2.qq.com', 207 | 'Referer': 'http://d1.web2.qq.com/proxy.html?v=20151105001&callback=1&id=2', 208 | } 209 | }, form, function (err, res, data) { 210 | let ret; 211 | try { 212 | ret = JSON.parse(data); 213 | } catch (err) { 214 | loginToken(ptwebqq, psessionid, cb); 215 | return; 216 | } 217 | cb(ret); 218 | }); 219 | }; 220 | 221 | module.exports = { 222 | Login: Login, 223 | _Login: _Login, 224 | getPtwebqq: getPtwebqq, 225 | getVfwebqq: getVfwebqq, 226 | loginToken: loginToken 227 | } 228 | -------------------------------------------------------------------------------- /libs/httpclient.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const https = require("https"); 5 | const http = require('http'); 6 | const querystring = require('querystring'); 7 | const URL = require('url'); 8 | 9 | let all_cookies = []; 10 | 11 | let z = 0; 12 | let q = Date.now(); 13 | q = (q - q % 1E3) / 1E3; 14 | q = q % 1E4 * 1E4; 15 | 16 | function nextMsgId() { 17 | z++; 18 | return q + z; 19 | } 20 | 21 | function get_cookies() { 22 | return all_cookies; 23 | }; 24 | 25 | function get_cookie_key(key) { 26 | var startIndex, endIndex, cookie = get_cookies_string(); 27 | if (cookie.length > 0) { 28 | startIndex = cookie.indexOf(key + '='); 29 | if (startIndex !== -1) { 30 | startIndex = startIndex + key.length + 1; 31 | endIndex = cookie.indexOf(';', startIndex); 32 | if (endIndex === -1) { 33 | endIndex = cookie.length; 34 | } 35 | return decodeURI(cookie.substring(startIndex, endIndex)); 36 | } 37 | } 38 | return ''; 39 | }; 40 | 41 | function get_cookies_string() { 42 | let cookie_map = {}; 43 | all_cookies.forEach(function (ck) { 44 | let v = ck.split(' ')[0]; 45 | let kv = v.trim().split('='); 46 | if (kv[1] != ';') cookie_map[kv[0]] = kv[1]; 47 | }); 48 | let cks = []; 49 | for (let k in cookie_map) { 50 | cks.push(k + '=' + cookie_map[k]); 51 | } 52 | return cks.join(' '); 53 | }; 54 | 55 | 56 | function set_cookies(cks) { 57 | let ck = []; 58 | cks.replace('; ,', ';,').split(';,').forEach(function (item, i) { 59 | if (i != cks.split(';,').length - 1) item += ';'; 60 | ck.push(item); 61 | }); 62 | update_cookies(ck) 63 | }; 64 | 65 | function update_cookies(cks) { 66 | if (cks) { 67 | all_cookies = _.union(all_cookies, cks); 68 | } 69 | }; 70 | 71 | function global_cookies(cookie) { 72 | if (cookie) { 73 | update_cookies(cookie); 74 | } 75 | return get_cookies(); 76 | }; 77 | 78 | function decode_qrsig(t){ 79 | for (var e = 0, i = 0, n = t.length; n > i; ++i)e += (e << 5) + t.charCodeAt(i); return 2147483647 & e 80 | }; 81 | 82 | function url_get(url_or_options, callback, pre_callback) { 83 | let http_or_https = http; 84 | 85 | if (((typeof url_or_options === 'string') && (url_or_options.indexOf('https:') === 0)) || ((typeof url_or_options === 'object') && (url_or_options.protocol === 'https:'))) 86 | http_or_https = https; 87 | 88 | if (process.env.DEBUG) { 89 | console.log(url_or_options); 90 | } 91 | return http_or_https.get(url_or_options, function (resp) { 92 | if (pre_callback !== undefined) pre_callback(resp); 93 | 94 | update_cookies(resp.headers['set-cookie']); 95 | 96 | let res = resp; 97 | let body = ''; 98 | resp.on('data', function (chunk) { 99 | return body += chunk; 100 | }); 101 | return resp.on('end', function () { 102 | if (process.env.DEBUG) { 103 | console.log(resp.statusCode); 104 | console.log(resp.headers); 105 | console.log(body); 106 | } 107 | return callback(0, res, body); 108 | }); 109 | }).on("error", function (e) { 110 | return console.log(e); 111 | }); 112 | }; 113 | 114 | function url_post(options, form, callback) { 115 | let http_or_https = http; 116 | 117 | if (((typeof options === 'object') && (options.protocol === 'https:'))) 118 | http_or_https = https; 119 | 120 | let postData = querystring.stringify(form); 121 | if (typeof options.headers !== 'object') options.headers = {}; 122 | options.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'; 123 | options.headers['Content-Length'] = Buffer.byteLength(postData); 124 | options.headers['Cookie'] = get_cookies_string(); 125 | options.headers['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:27.0) Gecko/20100101 Firefox/27.0'; 126 | if (process.env.DEBUG) { 127 | console.log(options.headers); 128 | console.log(postData); 129 | } 130 | if (options.timeout) { 131 | http_or_https.request(options.timeout, function () { 132 | 133 | }); 134 | } 135 | let req = http_or_https.request(options, function (resp) { 136 | let res = resp; 137 | let body = ''; 138 | resp.on('data', function (chunk) { 139 | return body += chunk; 140 | }); 141 | return resp.on('end', function () { 142 | if (process.env.DEBUG) { 143 | console.log(resp.statusCode); 144 | console.log(resp.headers); 145 | console.log(body); 146 | } 147 | return callback(0, res, body); 148 | }); 149 | }).on("error", function (e) { 150 | return console.log(e); 151 | }); 152 | req.write(postData); 153 | return req.end(); 154 | }; 155 | 156 | function http_request(options, params, callback) { 157 | let append, aurl, body, client, data, query, req; 158 | aurl = URL.parse(options.url); 159 | options.host = aurl.host; 160 | options.path = aurl.path; 161 | options.headers || (options.headers = {}); 162 | client = aurl.protocol === 'https:' ? https : http; 163 | body = ''; 164 | if (params && options.method === 'POST') { 165 | data = querystring.stringify(params); 166 | options.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'; 167 | options.headers['Content-Length'] = Buffer.byteLength(data); 168 | } 169 | if (params && options.method === 'GET') { 170 | query = querystring.stringify(params); 171 | append = aurl.query ? '&' : '?'; 172 | options.path += append + query; 173 | } 174 | options.headers['Cookie'] = get_cookies_string(); 175 | options.headers['Referer'] = 'http://d1.web2.qq.com/proxy.html?v=20151105001&callback=1&id=2'; 176 | // options.headers['Referer'] = 'http://s.web2.qq.com/proxy.html?v=20130916001&callback=1&id=1'; 177 | options.headers['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36'; 178 | if (process.env.DEBUG) { 179 | console.log(options); 180 | console.log(params); 181 | } 182 | req = client.request(options, function (resp) { 183 | if (options.debug) { 184 | console.log("response: " + resp.statusCode); 185 | console.log("cookie: " + resp.headers['set-cookie']); 186 | } 187 | resp.on('data', function (chunk) { 188 | return body += chunk; 189 | }); 190 | return resp.on('end', function () { 191 | if (process.env.DEBUG) { 192 | console.log(resp.statusCode); 193 | console.log(resp.headers); 194 | console.log(body); 195 | } 196 | try { 197 | return handle_resp_body(body, options, callback); 198 | } catch (err) { 199 | console.log(`[HttpClient] ERR: ${err.message}, Resend request...`) 200 | http_request(options, params, callback); 201 | return; 202 | } 203 | }); 204 | }); 205 | req.on("error", function (e) { 206 | return callback(null, e); 207 | }); 208 | if (params && options.method === 'POST') { 209 | req.write(data); 210 | } 211 | return req.end(); 212 | }; 213 | 214 | function handle_resp_body(body, options, callback) { 215 | let ret = null; 216 | ret = JSON.parse(body); 217 | return callback(ret, null); 218 | }; 219 | 220 | function http_get(url, params, callback) { 221 | if (!callback) { 222 | callback = params; 223 | params = null; 224 | } 225 | let options = { 226 | method: 'GET', 227 | url: url 228 | }; 229 | return http_request(options, params, callback); 230 | }; 231 | 232 | function http_post(options, body, callback) { 233 | options.method = 'POST'; 234 | return http_request(options, body, callback); 235 | }; 236 | 237 | module.exports = { 238 | global_cookies: global_cookies, 239 | get_cookies: get_cookies, 240 | set_cookies: set_cookies, 241 | update_cookies: update_cookies, 242 | get_cookie_key: get_cookie_key, 243 | get_cookies_string: get_cookies_string, 244 | decode_qrsig: decode_qrsig, 245 | request: http_request, 246 | get: http_get, 247 | post: http_post, 248 | url_get: url_get, 249 | url_post: url_post, 250 | nextMsgId: nextMsgId 251 | }; 252 | --------------------------------------------------------------------------------