├── views ├── error.ejs └── index.ejs ├── utils ├── strings.js └── sign.js ├── public └── stylesheets │ └── style.css ├── routes └── index.js ├── README.md ├── package.json ├── .gitignore ├── config.test.js ├── LICENSE ├── tasks ├── tuling.js └── statuses.js ├── app.js ├── bin └── www ├── controllers └── oauth.js └── do └── do.js /views/error.ejs: -------------------------------------------------------------------------------- 1 |

<%= message %>

2 |

<%= error.status %>

3 |
<%= error.stack %>
4 | -------------------------------------------------------------------------------- /utils/strings.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | exports.realObj = function (obj) { 4 | return _.omitBy(dict,_.isUndefined) 5 | } -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | -------------------------------------------------------------------------------- /views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= title %> 5 | 6 | 7 | 8 |

<%= title %>

9 |

Welcome to <%= title %>

10 | 11 | 12 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var oauth = require('../controllers/oauth'); 4 | 5 | /* GET home page. */ 6 | router.get('/oauth', 7 | oauth.getUnauthorizedRequestToken, 8 | oauth.authorizeForRequestToken); 9 | 10 | router.get('/callback', 11 | oauth.authorizeRequestTokenForAccessToken); 12 | 13 | module.exports = router; 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 饭否聊天机器人 2 | === 3 | 4 | 使用步骤 5 | --- 6 | ### install 7 | `npm install` 8 | 9 | ### init 10 | - 填写 `config.test.js`, 并改名成`config.js` 11 | - 本机启动 `redis-server` 12 | - 获取 `oauth` 信息 13 | - `node bin/www` 14 | - 本机访问 `{hostname:port}/fanfou/chatbot/oauth` 15 | 16 | ### start 17 | - 启动定时服务 `node do/do.js` 18 | 19 | 技术栈 20 | --- 21 | - redis (GET SET SUB PUB) 22 | - express (http) 23 | - lodash (Object 操作) 24 | - later(定时任务) 25 | - 图灵机器人 API 26 | - 饭否机器人 API 27 | 28 | License 29 | --- 30 | [MIT](LiCENSE) 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fanfou-chatbot", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www" 7 | }, 8 | "dependencies": { 9 | "body-parser": "~1.13.2", 10 | "cookie-parser": "~1.3.5", 11 | "debug": "^2.2.0", 12 | "ejs": "^2.5.7", 13 | "express": "~4.13.1", 14 | "ioredis": "^1.15.1", 15 | "later": "^1.2.0", 16 | "lodash": "^4.6.1", 17 | "morgan": "~1.6.1", 18 | "request": "^2.69.0", 19 | "serve-favicon": "~2.3.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .AppleDouble 3 | .LSOverride 4 | dump.rdb 5 | 6 | # Icon must end with two \r 7 | Icon 8 | 9 | 10 | # Thumbnails 11 | ._* 12 | 13 | # Files that might appear in the root of a volume 14 | .DocumentRevisions-V100 15 | .fseventsd 16 | .Spotlight-V100 17 | .TemporaryItems 18 | .Trashes 19 | .VolumeIcon.icns 20 | 21 | # Directories potentially created on remote AFP share 22 | .AppleDB 23 | .AppleDesktop 24 | Network Trash Folder 25 | Temporary Items 26 | .apdisk 27 | 28 | # npm 29 | node_modules 30 | *.log 31 | *.gz 32 | 33 | # my 34 | test.js 35 | config.js -------------------------------------------------------------------------------- /config.test.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | fanfou: { 3 | ConsumerKey:'Your Fanfou Consumer Key', 4 | ConsumerSecret:'Your Fanfou Consumer Secret', 5 | RequestTokenURL:'http://fanfou.com/oauth/request_token', 6 | AccessTokenURL:'http://fanfou.com/oauth/access_token', 7 | AuthorizeURL:'http://fanfou.com/oauth/authorize', 8 | oauthSignatureMethod:'HMAC-SHA1', 9 | CallbackURL: 'http://127.0.0.1:5850/fanfou/chatbot/callback' 10 | }, 11 | tuling:{ 12 | Api:'http://www.tuling123.com/openapi/api', 13 | Key:'Your Tuling Api Key' 14 | }, 15 | settings: { 16 | port: '5850' 17 | } 18 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) zkaip 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /tasks/tuling.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | var config = require('../config'); 3 | // var strings = require('../utils/strings'); 4 | 5 | var _ = require('lodash'); 6 | 7 | var tuling = config.tuling; 8 | 9 | module.exports = function (reply, callback) { 10 | request.post({ 11 | url: tuling.Api, 12 | form: _.assign({key: tuling.Key},reply) 13 | }, function (error, response, result) { 14 | if (!error && response.statusCode == 200) { 15 | var body = JSON.parse(result) 16 | if(/^400/.test(body.code)){ 17 | callback( { 18 | code: 0, 19 | text: 'retry' 20 | }) 21 | } 22 | switch(body.code) { 23 | case 100000: 24 | callback(body); 25 | break; 26 | case 200000: 27 | callback({ 28 | code: body.code, 29 | text: body.text + body.url 30 | }); 31 | break; 32 | case 302000: 33 | var text = body.list[parseInt(body.list.length*Math.random())]; 34 | callback({ 35 | code: body.code, 36 | text: text.article+' '+ text.detailurl 37 | }); 38 | break; 39 | case 308000: 40 | var text = body.list[0]; 41 | callback({ 42 | code: body.code, 43 | text: body.text + text.name + ' ' + text.info + text.detailurl 44 | }); 45 | break; 46 | } 47 | } 48 | }) 49 | } -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var favicon = require('serve-favicon'); 4 | var logger = require('morgan'); 5 | var cookieParser = require('cookie-parser'); 6 | var bodyParser = require('body-parser'); 7 | var config = require('./config'); 8 | 9 | var routes = require('./routes/index'); 10 | 11 | var app = express(); 12 | 13 | // view engine setup 14 | app.set('views', path.join(__dirname, 'views')); 15 | app.set('view engine', 'ejs'); 16 | 17 | // uncomment after placing your favicon in /public 18 | //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 19 | app.use(logger('dev')); 20 | app.use(bodyParser.json()); 21 | app.use(bodyParser.urlencoded({ extended: false })); 22 | app.use(cookieParser()); 23 | app.use(express.static(path.join(__dirname, 'public'))); 24 | 25 | 26 | 27 | app.use('/fanfou/chatbot', routes); 28 | 29 | // catch 404 and forward to error handler 30 | app.use(function(req, res, next) { 31 | var err = new Error('Not Found'); 32 | err.status = 404; 33 | next(err); 34 | }); 35 | 36 | // error handlers 37 | 38 | // development error handler 39 | // will print stacktrace 40 | if (app.get('env') === 'development') { 41 | app.use(function(err, req, res, next) { 42 | res.status(err.status || 500); 43 | res.render('error', { 44 | message: err.message, 45 | error: err 46 | }); 47 | }); 48 | } 49 | 50 | // production error handler 51 | // no stacktraces leaked to user 52 | app.use(function(err, req, res, next) { 53 | res.status(err.status || 500); 54 | res.render('error', { 55 | message: err.message, 56 | error: {} 57 | }); 58 | }); 59 | 60 | 61 | module.exports = app; 62 | -------------------------------------------------------------------------------- /tasks/statuses.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | var config = require('../config'); 3 | var fanfou = config.fanfou; 4 | var sign = require('../utils/sign'); 5 | var _ = require('lodash'); 6 | 7 | // POST Message 8 | function postMessage(post, others,callback) { 9 | // console.log(post) 10 | var base_url = 'http://api.fanfou.com/statuses/update.json'; 11 | if (post.code) { 12 | var dict = {} 13 | var obj = _.assign(dict,{ 14 | status: post.text.length > 140?post.text.substr(0,140):post.text, 15 | format: 'html' 16 | },others) 17 | console.log('others:',others); 18 | console.log('obj:',obj); 19 | sign.getAccessDict(obj, function (err, dict) { 20 | console.log('AccessDict:',dict) 21 | sign.signatureAccess ('POST', base_url, dict, function (err1, oauth_signature) { 22 | _.assign(dict,obj,{ 23 | oauth_signature:oauth_signature 24 | }) 25 | console.log('PostDict:',dict) 26 | request.post({ 27 | url: base_url, 28 | form: dict 29 | }, 30 | function (error, response, body) { 31 | console.log(error+' : '+body) 32 | callback(error, body) 33 | } 34 | ) 35 | }) 36 | }) 37 | } 38 | } 39 | 40 | // get Replies 41 | function getReplies(obj, callback) { 42 | var base_url = 'http://api.fanfou.com/statuses/replies.json'; 43 | sign.getAccessDict(obj, function (err, dict) { 44 | // console.log('AccessDict:',dict) 45 | sign.signatureAccess ('GET', base_url, dict, function (err1, oauth_signature) { 46 | // console.log(oauth_signature) 47 | request.get( 48 | sign.url('GET', base_url,dict, oauth_signature), 49 | function (error, response, body) { 50 | // console.log(body) 51 | if (!error && response.statusCode == 200) { 52 | callback(body) 53 | } 54 | }) 55 | }) 56 | 57 | }) 58 | } 59 | 60 | module.exports = { 61 | getReplies:getReplies, 62 | postMessage:postMessage 63 | } 64 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('fanfou-chatbot:server'); 9 | var http = require('http'); 10 | var config = require('../config') 11 | 12 | /** 13 | * Get port from environment and store in Express. 14 | */ 15 | 16 | var port = normalizePort(process.env.PORT || config.settings.port); 17 | app.set('port', port); 18 | 19 | /** 20 | * Create HTTP server. 21 | */ 22 | 23 | var server = http.createServer(app); 24 | 25 | /** 26 | * Listen on provided port, on all network interfaces. 27 | */ 28 | 29 | server.listen(port); 30 | server.on('error', onError); 31 | server.on('listening', onListening); 32 | 33 | /** 34 | * Normalize a port into a number, string, or false. 35 | */ 36 | 37 | function normalizePort(val) { 38 | var port = parseInt(val, 10); 39 | 40 | if (isNaN(port)) { 41 | // named pipe 42 | return val; 43 | } 44 | 45 | if (port >= 0) { 46 | // port number 47 | return port; 48 | } 49 | 50 | return false; 51 | } 52 | 53 | /** 54 | * Event listener for HTTP server "error" event. 55 | */ 56 | 57 | function onError(error) { 58 | if (error.syscall !== 'listen') { 59 | throw error; 60 | } 61 | 62 | var bind = typeof port === 'string' 63 | ? 'Pipe ' + port 64 | : 'Port ' + port; 65 | 66 | // handle specific listen errors with friendly messages 67 | switch (error.code) { 68 | case 'EACCES': 69 | console.error(bind + ' requires elevated privileges'); 70 | process.exit(1); 71 | break; 72 | case 'EADDRINUSE': 73 | console.error(bind + ' is already in use'); 74 | process.exit(1); 75 | break; 76 | default: 77 | throw error; 78 | } 79 | } 80 | 81 | /** 82 | * Event listener for HTTP server "listening" event. 83 | */ 84 | 85 | function onListening() { 86 | var addr = server.address(); 87 | var bind = typeof addr === 'string' 88 | ? 'pipe ' + addr 89 | : 'port ' + addr.port; 90 | debug('Listening on ' + bind); 91 | } 92 | -------------------------------------------------------------------------------- /controllers/oauth.js: -------------------------------------------------------------------------------- 1 | var querystring = require('querystring'); 2 | var Redis = require('ioredis'); 3 | var config = require('../config'); 4 | var request = require('request'); 5 | var sign = require('../utils/sign'); 6 | var fanfou = config.fanfou; 7 | var redis = new Redis(); 8 | 9 | 10 | exports.getUnauthorizedRequestToken = function(req, res, next) { 11 | var base_url = fanfou.RequestTokenURL; 12 | var dict = sign.getDict(); 13 | var base_string = sign.baseString('GET', base_url, dict) 14 | var base_secret = fanfou.ConsumerSecret+'&'; 15 | 16 | dict.oauth_signature = sign.signature(base_string, base_secret) 17 | 18 | request.get( 19 | sign.url('GET',base_url, dict), 20 | function (error, response, body) { 21 | if (!error && response.statusCode == 200) { 22 | var result = querystring.parse(body) 23 | redis.set('fanfou_oauth_token', result.oauth_token); 24 | redis.set('fanfou_oauth_token_secret', result.oauth_token_secret); 25 | next(); 26 | } 27 | }) 28 | } 29 | 30 | exports.authorizeForRequestToken = function (req, res, next) { 31 | redis.get('fanfou_oauth_token', function (err, oauth_token) { 32 | var dict = { 33 | oauth_token: oauth_token, 34 | callback_url: encodeURIComponent(fanfou.CallbackURL) 35 | } 36 | res.redirect(sign.url('GET',fanfou.AuthorizeURL, dict)) 37 | }) 38 | } 39 | 40 | exports.authorizeRequestTokenForAccessToken = function (req, res, next) { 41 | var base_url = fanfou.AccessTokenURL; 42 | // 获取字典 43 | var dict = sign.getDict({oauth_token:req.query.oauth_token}) 44 | redis.get('fanfou_oauth_token_secret', function (err,oauth_token_secret) { 45 | // 签名 46 | var base_string = sign.baseString('GET', base_url, dict) 47 | var base_secret = fanfou.ConsumerSecret+'&'+oauth_token_secret; 48 | dict.oauth_signature = sign.signature (base_string, base_secret); 49 | request.get(sign.url('GET',base_url, dict), function (error, response, body) { 50 | if (!error && response.statusCode == 200) { 51 | var oauth_token = querystring.parse(body); 52 | redis.set('fanfou_access_token', oauth_token.oauth_token); 53 | redis.set('fanfou_access_token_secret', oauth_token.oauth_token_secret); 54 | return res.json(oauth_token); 55 | } 56 | }) 57 | }) 58 | } -------------------------------------------------------------------------------- /do/do.js: -------------------------------------------------------------------------------- 1 | var statuses = require('../tasks/statuses'); 2 | var getReplies = statuses.getReplies; 3 | var postMessage = statuses.postMessage; 4 | var tuling = require('../tasks/tuling'); 5 | 6 | var later = require('later'); 7 | var Redis = require('ioredis'); 8 | var pubReply = new Redis(); 9 | var subReply = new Redis(); 10 | var redis = new Redis() 11 | 12 | var sched = later.parse.recur().every(10).second() 13 | 14 | subReply.subscribe('reply', function (err, count) { 15 | later.setInterval(function () { 16 | redis.get('fanfou_reply_since_id',function (err1,since_id) { 17 | getReplies(since_id?{since_id:since_id}:{count:'1'},function (replies) { 18 | // console.log(replies) 19 | var replies = JSON.parse(replies) 20 | if (replies[0]) { 21 | var replyID = replies[0].id 22 | // console.log('replyID',replyID) 23 | redis.set('fanfou_reply_since_id',replyID); 24 | } 25 | // console.log('---------') 26 | for (var i = replies.length - 1; i >= 0; i--) { 27 | var reply = replies[i]; 28 | // console.log(reply.id, reply.text); 29 | if (/^/) {} 30 | var replyObj = { 31 | info: reply.text.replace(/^@聊天机器人\s{1,}/g,''), 32 | userid: reply.user.id, 33 | } 34 | var others = { 35 | screen_name: reply.user.screen_name, 36 | in_reply_to_user_id: reply.user.id, 37 | in_reply_to_status_id: reply.id 38 | } 39 | console.log('reply:',reply); 40 | console.log('replyOthers:',others) 41 | pubReply.publish('reply',JSON.stringify({ 42 | reply:replyObj, 43 | others:others 44 | })) 45 | } 46 | // console.log('---------') 47 | }) 48 | }) 49 | }, sched); 50 | }) 51 | 52 | 53 | subReply.on('message', function (channel, message) { 54 | var all = JSON.parse(message); 55 | tuling(all.reply,function (post) { 56 | // console.log(post) 57 | // console.log(post.code) 58 | post.text = ('@'+all.others.screen_name+' '+post.text).replace('
','\n'); 59 | // console.log(post) 60 | // console.log("========") 61 | if(post.code){ 62 | postMessage(post, all.others, function (error, result) { 63 | // // console.log(result) 64 | }) 65 | } 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /utils/sign.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'); 2 | var querystring = require('querystring'); 3 | var _ = require('lodash'); 4 | 5 | var config = require('../config'); 6 | var fanfou = config.fanfou; 7 | 8 | var Redis = require('ioredis'); 9 | var redis = new Redis(); 10 | 11 | function hmacSha1 (str, key) { 12 | var hmac = crypto.createHmac('sha1',key); 13 | hmac.update(str); 14 | return hmac.digest().toString('base64'); 15 | } 16 | 17 | function nonce () { 18 | return Math.random().toString(36).substr(2); 19 | } 20 | 21 | function kvsort (dict) { 22 | var kv = [] 23 | for(var key of Object.keys(dict).sort()){ 24 | kv.push(key+"%3D"+dict[key]) 25 | } 26 | return kv.join('%26'); 27 | } 28 | 29 | function baseString (method, base_url, dict) { 30 | // console.log('base_string:',method + '&' + encodeURIComponent(base_url) + '&' + kvsort(dict)) 31 | return method + '&' + encodeURIComponent(base_url) + '&' + kvsort(dict); 32 | } 33 | 34 | function signature (base_string, base_secret) { 35 | return hmacSha1(base_string, base_secret) 36 | } 37 | 38 | function signatureAccess (method, base_url, dict, callback) { 39 | redis.get('fanfou_access_token_secret', function (err, access_token_secret) { 40 | callback(err, signature ( 41 | baseString(method, base_url, dict), 42 | fanfou.ConsumerSecret+'&'+access_token_secret 43 | ) 44 | ) 45 | }) 46 | } 47 | 48 | function url (method ,base_url, dict, oauth_signature) { 49 | switch (method) { 50 | case 'GET': 51 | return base_url+'?'+querystring.stringify(dict)+(oauth_signature?('&oauth_signature='+encodeURIComponent(oauth_signature)):''); 52 | break; 53 | case 'POST': 54 | return base_url; 55 | break; 56 | } 57 | } 58 | 59 | function getDict (obj) { 60 | return _.assign( 61 | { 62 | oauth_consumer_key:fanfou.ConsumerKey, 63 | oauth_signature_method:fanfou.oauthSignatureMethod, 64 | oauth_timestamp:parseInt(new Date()/1000)+'', 65 | oauth_nonce:nonce() 66 | } 67 | ,obj); 68 | } 69 | 70 | function getAccessDict (obj,callback) { 71 | redis.get('fanfou_access_token',function (err, access_token) { 72 | // console.log(access_token) 73 | var object = 74 | _.assign( 75 | { 76 | oauth_consumer_key:fanfou.ConsumerKey, 77 | oauth_token:access_token, 78 | oauth_signature_method:fanfou.oauthSignatureMethod, 79 | oauth_timestamp:parseInt(new Date()/1000)+'', 80 | oauth_nonce:nonce() 81 | } 82 | ,encodeAccessDict(obj)) 83 | callback(err, object) 84 | }) 85 | } 86 | 87 | 88 | function encodeAccessDict (dict) { 89 | return _.mapValues(dict, function (str) { 90 | return encodeURIComponent(str).replace(/\%/g,'%25') 91 | }) 92 | } 93 | 94 | module.exports = { 95 | hmacSha1: hmacSha1, 96 | nonce: nonce, 97 | kvsort: kvsort, 98 | baseString: baseString, 99 | signature: signature, 100 | signatureAccess:signatureAccess, 101 | url: url, 102 | getDict: getDict, 103 | getAccessDict:getAccessDict, 104 | } --------------------------------------------------------------------------------