├── 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 | }
--------------------------------------------------------------------------------