command line interface based Twitter client
Revision | a089eb23fae184402f54847598deb4ed21225225 (tree) |
---|---|
Time | 2012-12-19 01:25:47 |
Author | hylom <hylom@hylo...> |
Commiter | hylom |
rewrite clienttweets.js
@@ -2,31 +2,39 @@ | ||
2 | 2 | var oauth = require('oauth'); |
3 | 3 | var commander = require('commander'); |
4 | 4 | var Twitter = require('ntwitter'); |
5 | +var fs = require('fs'); | |
6 | +var configFile = '.account'; | |
5 | 7 | |
6 | 8 | // OAuth parameters |
7 | 9 | var consumerKey = 'd3MqFro8tVXtlEzz5hVrZA'; |
8 | -var secretKey = 'RSWIniHQldyhhmLBj2PpP6V4gOnxf3vEEFUEZXT1Q'; | |
10 | +var consumerSecret = 'RSWIniHQldyhhmLBj2PpP6V4gOnxf3vEEFUEZXT1Q'; | |
9 | 11 | var requestUrl = 'https://api.twitter.com/oauth/request_token'; |
10 | 12 | var accessUrl = 'https://api.twitter.com/oauth/access_token'; |
11 | 13 | var authUrl = 'https://api.twitter.com/oauth/authenticate'; |
12 | 14 | |
13 | -exports.authenticate = function (callback) { | |
15 | +/* | |
16 | + * OAuth認証を行う | |
17 | + * @param {Function(err, token, secret)} callback | |
18 | + * 結果を受け取るコールバック関数。 | |
19 | + * err引数にはエラーオブジェクトが、token引数にはアクセストークンが、 | |
20 | + * secret引数にはsecret文字列が格納される | |
21 | + */ | |
22 | +function authenticate(callback) { | |
14 | 23 | var self = this; |
15 | 24 | // Create authenticate client |
16 | 25 | var client = new oauth.OAuth( |
17 | 26 | requestUrl, |
18 | 27 | accessUrl, |
19 | 28 | consumerKey, |
20 | - secretKey, | |
29 | + consumerSecret, | |
21 | 30 | '1.0', |
22 | 31 | 'oob', |
23 | 32 | "HMAC-SHA1" |
24 | 33 | ); |
25 | 34 | |
26 | - // Send auth request to twitter | |
27 | 35 | client.getOAuthRequestToken(getAccessToken); |
28 | 36 | |
29 | - // get access token from request token | |
37 | + // リクエストトークンからアクセストークンを取得する | |
30 | 38 | function getAccessToken(err, token, secret, results) { |
31 | 39 | if (err) { |
32 | 40 | console.log('oauth request failed.'); |
@@ -36,31 +44,95 @@ exports.authenticate = function (callback) { | ||
36 | 44 | console.log('Open URL printed below, and input displayed PIN code.'); |
37 | 45 | console.log(url); |
38 | 46 | commander.prompt('Input PIN: ', function (pin) { |
39 | - client.getOAuthAccessToken(token, secret, pin, createTwitterClient); | |
47 | + client.getOAuthAccessToken(token, secret, pin, receiveToken); | |
40 | 48 | }); |
41 | 49 | } |
42 | 50 | |
43 | - // create twitter object | |
44 | - function createTwitterClient(err, token, secret, results) { | |
51 | + // アクセストークンを受け取る | |
52 | + function receiveToken(err, token, secret, results) { | |
45 | 53 | if (err) { |
46 | - console.log('oauth access token request failed.'); | |
47 | - process.exit(1); | |
54 | + callback(err); | |
55 | + return; | |
48 | 56 | } |
49 | - var twitter = self.createTwitterClient({ | |
50 | - consumerKey: consumerKey, | |
51 | - consumerSecret: secretKey, | |
52 | - accessTokenKey: token, | |
53 | - accessTokenSecret: secret | |
54 | - }, callback); | |
57 | + callback(err, token, secret); | |
55 | 58 | } |
56 | 59 | } |
57 | 60 | |
58 | -exports.createTwitterClient = function (keys, callback) { | |
59 | - var twitter = new Twitter({ | |
60 | - consumer_key: keys.consumerKey, | |
61 | - consumer_secret: keys.consumerSecret, | |
62 | - access_token_key: keys.accessTokenKey, | |
63 | - access_token_secret: keys.accessTokenSecret | |
61 | +/* | |
62 | + * ntwitter.Twitterクラスのインスタンスを生成する | |
63 | + * @param {Object} keys アクセスに使用するキー | |
64 | + * @returns 作成されたインスタンス | |
65 | + */ | |
66 | +function createTwitterClient(keys) { | |
67 | + var twitter = new Twitter(keys); | |
68 | + return twitter; | |
69 | +} | |
70 | + | |
71 | +/* | |
72 | + * .accountファイルにアクセストークン情報を書き出す | |
73 | + * @param {Object} twitter ntwitter.Twitterクラスのインスタンス | |
74 | + * @param {Function(err)} callback エラーオブジェクトを返すコールバック関数 | |
75 | + */ | |
76 | +function writeAuthenticationKeys(twitter, callback) { | |
77 | + var keys = { | |
78 | + access_token_key: twitter.options.access_token_key, | |
79 | + access_token_secret: twitter.options.access_token_secret | |
80 | + }; | |
81 | + fs.writeFile(configFile, JSON.stringify(keys, null, 2), 'utf8', callback); | |
82 | +} | |
83 | + | |
84 | +/* | |
85 | + * .accountファイルに保存しておいたアクセストークン情報を読み出す | |
86 | + * @param {Function(err, key)} callback コールバック関数。 | |
87 | + * err引数にはエラーオブジェクトが、 | |
88 | + * keys引数には読み出したアクセストークン情報を格納するオブジェクト | |
89 | + * 格納される | |
90 | + */ | |
91 | +function readAuthenticationKeys(callback) { | |
92 | + fs.readFile(configFile, 'utf8', function (err, data) { | |
93 | + if (err) { | |
94 | + callback(err); | |
95 | + return; | |
96 | + } | |
97 | + var keys = JSON.parse(data); | |
98 | + callback(err, keys); | |
99 | + }); | |
100 | +} | |
101 | + | |
102 | +/* | |
103 | + * 認証を行ってntwitter.Twitterクラスのインスタンスを返す | |
104 | + * @param {Function(err, twitter)} callback コールバック関数 | |
105 | + * err引数にはエラーオブジェクトが、 | |
106 | + * twitter引数には作成されたインスタンスが格納される | |
107 | + */ | |
108 | +exports.authenticate = function (callback) { | |
109 | + var keys = { | |
110 | + consumer_key: consumerKey, | |
111 | + consumer_secret: consumerSecret | |
112 | + }; | |
113 | + // ファイルに保存しておいたアクセストークンを読み出す | |
114 | + readAuthenticationKeys(function (err, data) { | |
115 | + // 読み出しに失敗したら認証を実行する | |
116 | + if (err || data.access_token_key === undefined | |
117 | + || data.access_token_secret === undefined ) { | |
118 | + authenticate(function (err, token, secret) { | |
119 | + if (err) { | |
120 | + callback(err); | |
121 | + } | |
122 | + keys.access_token_key = token; | |
123 | + keys.access_token_secret = secret; | |
124 | + var twitter = createTwitterClient(keys); | |
125 | + // アクセストークンをファイルに保存しておく | |
126 | + writeAuthenticationKeys(twitter, function (err) { | |
127 | + callback(err, twitter); | |
128 | + }); | |
129 | + }); | |
130 | + return; | |
131 | + } | |
132 | + // 読み出しに成功したらその値からクライアントを作成する | |
133 | + keys.access_token_key = data.access_token_key; | |
134 | + keys.access_token_secret = data.access_token_secret; | |
135 | + var twitter = createTwitterClient(keys); | |
136 | + callback(null, twitter); | |
64 | 137 | }); |
65 | - callback(null, twitter); | |
66 | -}; | |
138 | +} |
@@ -1,178 +1,142 @@ | ||
1 | 1 | #!/usr/local/bin/node |
2 | 2 | |
3 | 3 | var auth = require('./auth'); |
4 | -var fs = require('fs'); | |
5 | -var util = require('util'); | |
6 | 4 | var readline = require('readline'); |
7 | 5 | |
8 | -var configFile = '.account'; | |
6 | +var tweetWriter = require('./tweetwriter'); | |
9 | 7 | |
10 | -var argv = require('optimist') | |
11 | - .usage('$0 - CLI based Twitter client.') | |
12 | - .describe('u', 'Twitter username') | |
13 | - .describe('p', 'Twitter password') | |
14 | - .argv; | |
15 | - | |
16 | -var rl; | |
17 | -var timeLineCache = []; | |
18 | -var isTimeLineAlive = false; | |
19 | -var readlineMode = ''; | |
8 | +var rl = null; | |
20 | 9 | var twitter = null; |
21 | 10 | |
22 | -// 認証キーを読み込み、認証を行ってメインルーチンを実行する | |
23 | -readAuthenticationKeys(function (err, keys) { | |
24 | - if (err) { | |
25 | - authenticate(doMain); | |
26 | - return; | |
27 | - } | |
28 | - auth.createTwitterClient(keys, doMain); | |
29 | -}); | |
30 | - | |
31 | -function pushToCache(texts) { | |
32 | - if (isTimeLineAlive) { | |
33 | - process.stdout.write(texts); | |
34 | - } else { | |
35 | - timeLineCache.push(texts); | |
11 | +// モードを表す疑似クラス | |
12 | +var mode = { | |
13 | + current: null, | |
14 | + start: function (mode) { | |
15 | + if (this.current) { | |
16 | + this.current.exit(); | |
17 | + } | |
18 | + this.current = mode; | |
19 | + this.current.start(); | |
36 | 20 | } |
37 | 21 | } |
38 | 22 | |
39 | -function showTweet(tweet) { | |
40 | - if (util.isArray(tweet)) { | |
41 | - tweet.forEach(showTweet); | |
42 | - return; | |
43 | - } | |
44 | - if (tweet.user === undefined) { | |
45 | - return; | |
23 | +// タイムラインモードを定義 | |
24 | +var timeline = { | |
25 | + start: function () { | |
26 | + if (rl !== null) { | |
27 | + rl.close(); | |
28 | + rl = null; | |
29 | + } | |
30 | + tweetWriter.resume(); | |
31 | + process.stdin.once('data', function () { | |
32 | + mode.start(command); | |
33 | + }); | |
34 | + process.stdin.resume(); | |
35 | + }, | |
36 | + exit: function () { | |
37 | + tweetWriter.pause(); | |
46 | 38 | } |
47 | - var name = tweet.user.name + ' (@' + tweet.user.screen_name + ')'; | |
48 | - var timestamp = tweet.created_at; | |
49 | - var text = tweet.text; | |
50 | - pushToCache(name + ' ' + timestamp + '\n'); | |
51 | - pushToCache(text + '\n'); | |
52 | - pushToCache('\n'); | |
53 | -} | |
39 | +}; | |
54 | 40 | |
55 | -var newTweet; | |
56 | -var modes = { | |
57 | - tweet: { | |
58 | - start: function () { | |
59 | - rl.setPrompt('tweet> '); | |
60 | - process.stdout.write('write tweet message and push Ctrl-C.\n'); | |
61 | - }, | |
62 | - line: function (data) { | |
63 | - if (newTweet === '') { | |
64 | - newTweet = data; | |
65 | - } else { | |
66 | - newTweet = newTweet + '\n' + data; | |
67 | - } | |
68 | - rl.prompt(); | |
69 | - }, | |
70 | - SIGINT: function () { | |
71 | - process.stdout.write('\n----\n'); | |
72 | - process.stdout.write(newTweet + '\n'); | |
73 | - process.stdout.write('----\n'); | |
74 | - var message = 'tweet this message? (y:yes/n:no) '; | |
75 | - function onAnswer(answer) { | |
76 | - if (answer === 'y') { | |
77 | - twitter.updateStatus(newTweet, {}, function (err, data) { | |
78 | - }); | |
79 | - process.stdout.write('tweet done.\n\n'); | |
80 | - exitPrompt(); | |
81 | - } else if (answer === 'n') { | |
82 | - process.stdout.write('tweet canceled.\n\n'); | |
83 | - exitPrompt(); | |
84 | - } else { | |
85 | - rl.question(message, onAnswer); | |
86 | - } | |
41 | +// コマンドラインモードを定義 | |
42 | +var command = { | |
43 | + start: function () { | |
44 | + rl = readline.createInterface({ | |
45 | + input: process.stdin, | |
46 | + output: process.stdout, | |
47 | + completer: function (line) { | |
48 | + var completions = ['tweet', 'quit']; | |
49 | + var hits = completions.filter(function(c) { | |
50 | + return c.indexOf(line) == 0; | |
51 | + }); | |
52 | + // show all completions if none found | |
53 | + return [hits.length ? hits : completions, line]; | |
87 | 54 | } |
88 | - rl.question(message, onAnswer); | |
89 | - }, | |
55 | + }); | |
56 | + rl.on('line', this.line); | |
57 | + rl.on('SIGINT', this.SIGINT); | |
58 | + rl.setPrompt('> '); | |
59 | + rl.prompt(); | |
90 | 60 | }, |
91 | - prompt: { | |
92 | - start: function () { | |
93 | - rl.setPrompt('> '); | |
94 | - }, | |
95 | - line: function (data) { | |
96 | - switch(data) { | |
97 | - case 'tweet': | |
98 | - setReadlineMode('tweet'); | |
99 | - newTweet = ''; | |
100 | - rl.prompt(); | |
101 | - break; | |
102 | - case 'quit': | |
103 | - process.exit(0); | |
104 | - case '': | |
105 | - rl.prompt(); | |
106 | - break; | |
107 | - default: | |
108 | - process.stdout.write('invalid command: ' + data + '\n'); | |
109 | - rl.prompt(); | |
110 | - } | |
111 | - }, | |
112 | - SIGINT: function () { | |
113 | - exitPrompt(); | |
114 | - }, | |
61 | + exit: function () { | |
62 | + }, | |
63 | + line: function (data) { | |
64 | + switch(data) { | |
65 | + case 'tweet': | |
66 | + mode.start(writeMessage); | |
67 | + break; | |
68 | + case 'quit': | |
69 | + process.exit(0); | |
70 | + case '': | |
71 | + mode.start(timeline); | |
72 | + break; | |
73 | + default: | |
74 | + process.stdout.write('invalid command: ' + data + '\n'); | |
75 | + mode.start(timeline); | |
76 | + break; | |
77 | + } | |
78 | + }, | |
79 | + SIGINT: function () { | |
80 | + mode.start(timeline); | |
115 | 81 | } |
116 | 82 | } |
117 | 83 | |
118 | -function setReadlineMode(modeName) { | |
119 | - var mode = modes[modeName]; | |
120 | - rl.removeAllListeners('line'); | |
121 | - rl.removeAllListeners('SIGINT'); | |
122 | - rl.on('line', mode.line); | |
123 | - rl.on('SIGINT', mode.SIGINT); | |
124 | - mode.start(); | |
125 | -} | |
126 | - | |
127 | -function startPrompt() { | |
128 | - rl = readline.createInterface({ | |
129 | - input: process.stdin, | |
130 | - output: process.stdout, | |
131 | - completer: function (line) { | |
132 | - var completions = ['tweet', 'quit']; | |
133 | - var hits = completions.filter(function(c) { | |
134 | - return c.indexOf(line) == 0; | |
135 | - }); | |
136 | - // show all completions if none found | |
137 | - return [hits.length ? hits : completions, line]; | |
84 | +// tweet入力モードを定義 | |
85 | +var newMessage = ''; | |
86 | +var writeMessage = { | |
87 | + start: function () { | |
88 | + rl.removeAllListeners('line'); | |
89 | + rl.removeAllListeners('SIGINT'); | |
90 | + rl.on('line', this.line); | |
91 | + rl.on('SIGINT', this.SIGINT); | |
92 | + rl.setPrompt('tweet> '); | |
93 | + process.stdout.write('write tweet message and push Ctrl-C.\n'); | |
94 | + newMessage = ''; | |
95 | + rl.prompt(); | |
96 | + }, | |
97 | + exit: function () { | |
98 | + }, | |
99 | + line: function (data) { | |
100 | + if (newMessage === '') { | |
101 | + newMessage = data; | |
102 | + } else { | |
103 | + newMessage = newMessage + '\n' + data; | |
138 | 104 | } |
139 | - }); | |
140 | - setReadlineMode('prompt'); | |
141 | - rl.prompt(); | |
142 | -} | |
143 | - | |
144 | -function exitPrompt() { | |
145 | - rl.close(); | |
146 | - startTimeLine(); | |
147 | -} | |
148 | - | |
149 | -function stopTimeLine() { | |
150 | - isTimeLineAlive = false; | |
151 | -} | |
152 | - | |
153 | -function startTimeLine() { | |
154 | - process.stdin.once('data', function () { | |
155 | - stopTimeLine(); | |
156 | - startPrompt(); | |
157 | - }); | |
158 | - process.stdin.resume(); | |
159 | - timeLineCache.forEach(function (data) { | |
160 | - process.stdout.write(data); | |
161 | - }); | |
162 | - timeLineCache = []; | |
163 | - isTimeLineAlive = true; | |
105 | + rl.prompt(); | |
106 | + }, | |
107 | + SIGINT: function () { | |
108 | + process.stdout.write('\n----\n'); | |
109 | + process.stdout.write(newMessage + '\n'); | |
110 | + process.stdout.write('----\n'); | |
111 | + var message = 'tweet this message? (y:yes/n:no) '; | |
112 | + function onAnswer(answer) { | |
113 | + if (answer === 'y') { | |
114 | + twitter.updateStatus(newMessage, {}, function (err, data) { | |
115 | + }); | |
116 | + process.stdout.write('tweet done.\n\n'); | |
117 | + mode.start(timeline); | |
118 | + } else if (answer === 'n') { | |
119 | + process.stdout.write('tweet canceled.\n\n'); | |
120 | + mode.start(timeline); | |
121 | + } else { | |
122 | + rl.question(message, onAnswer); | |
123 | + } | |
124 | + } | |
125 | + rl.question(message, onAnswer); | |
126 | + } | |
164 | 127 | } |
165 | 128 | |
166 | -function doMain(err, twit) { | |
167 | - twitter = twit; | |
168 | - startTimeLine(); | |
169 | - | |
129 | +// User streamによるタイムライン受信を開始 | |
130 | +function startUserStream() { | |
170 | 131 | twitter.getHomeTimeline({}, function (err, data) { |
171 | - showTweet(data); | |
132 | + if (err) { | |
133 | + throw err; | |
134 | + } | |
135 | + tweetWriter.write(data.reverse()); | |
172 | 136 | }); |
173 | 137 | twitter.stream('user', {}, function (stream) { |
174 | 138 | stream.on('data', function (data) { |
175 | - showTweet(data); | |
139 | + tweetWriter.write(data); | |
176 | 140 | }); |
177 | 141 | stream.on('error', function(err) { |
178 | 142 | console.log(err); |
@@ -180,35 +144,14 @@ function doMain(err, twit) { | ||
180 | 144 | }); |
181 | 145 | }; |
182 | 146 | |
183 | -function authenticate(callback) { | |
184 | - auth.authenticate(function(err, twitter) { | |
185 | - if (err) { | |
186 | - callback(err); | |
187 | - return; | |
188 | - } | |
189 | - writeAuthenticationKeys(twitter, function(err) { | |
190 | - callback(null, twitter); | |
191 | - }); | |
192 | - }); | |
193 | -} | |
147 | +// ntwitter.Twitterクラスのインスタンスを生成する | |
148 | +auth.authenticate(function (err, twit) { | |
149 | + twitter = twit; | |
194 | 150 | |
195 | -function readAuthenticationKeys(callback) { | |
196 | - fs.readFile(configFile, 'utf8', function (err, data) { | |
197 | - if (err) { | |
198 | - callback(err); | |
199 | - return; | |
200 | - } | |
201 | - var keys = JSON.parse(data); | |
202 | - callback(err, keys); | |
203 | - }); | |
204 | -} | |
151 | + // User streamを使ったタイムライン受信の開始 | |
152 | + startUserStream(); | |
153 | + | |
154 | + // タイムラインモードを開始 | |
155 | + mode.start(timeline); | |
156 | +}); | |
205 | 157 | |
206 | -function writeAuthenticationKeys(twitter, callback) { | |
207 | - var keys = { | |
208 | - consumerKey: twitter.options.consumer_key, | |
209 | - consumerSecret: twitter.options.consumer_secret, | |
210 | - accessTokenKey: twitter.options.access_token_key, | |
211 | - accessTokenSecret: twitter.options.access_token_secret | |
212 | - }; | |
213 | - fs.writeFile(configFile, JSON.stringify(keys, null, 2), 'utf8', callback); | |
214 | -} |
@@ -0,0 +1,45 @@ | ||
1 | +// tweetの書き込みを行うモジュール | |
2 | + | |
3 | +var buffer = []; | |
4 | +var util = require('util'); | |
5 | +var writable = false; | |
6 | + | |
7 | +exports.write = function (tweet) { | |
8 | + if (util.isArray(tweet)) { | |
9 | + tweet.forEach(_write); | |
10 | + } else { | |
11 | + _write(tweet); | |
12 | + } | |
13 | +}; | |
14 | + | |
15 | +function _write(tweet) { | |
16 | + if (tweet.user === undefined) { | |
17 | + return; | |
18 | + } | |
19 | + var name = tweet.user.name + ' (@' + tweet.user.screen_name + ')'; | |
20 | + var timestamp = tweet.created_at; | |
21 | + var text = tweet.text; | |
22 | + _push(name + ' ' + timestamp + '\n'); | |
23 | + _push(text + '\n'); | |
24 | + _push('\n'); | |
25 | +} | |
26 | + | |
27 | +function _push(data) { | |
28 | + if (writable) { | |
29 | + process.stdout.write(data); | |
30 | + } else { | |
31 | + buffer.push(data); | |
32 | + } | |
33 | +}; | |
34 | + | |
35 | +exports.resume = function () { | |
36 | + buffer.forEach(function (data) { | |
37 | + process.stdout.write(data); | |
38 | + }); | |
39 | + buffer = []; | |
40 | + writable = true; | |
41 | +}, | |
42 | + | |
43 | +exports.pause = function () { | |
44 | + writable = false; | |
45 | +} |