Update README.md
[rainbowstream.git] / rainbowstream / rainbow.py
... / ...
CommitLineData
1"""
2Colorful user's timeline stream
3"""
4from __future__ import print_function
5from multiprocessing import Process
6from dateutil import parser
7
8import os
9import os.path
10import sys
11import signal
12import argparse
13import time
14import datetime
15
16from twitter.stream import TwitterStream, Timeout, HeartbeatTimeout, Hangup
17from twitter.api import *
18from twitter.oauth import OAuth, read_token_file
19from twitter.oauth_dance import oauth_dance
20from twitter.util import printNicely
21
22from .colors import *
23from .config import *
24from .interactive import *
25from .db import *
26
27g = {}
28db = RainbowDB()
29cmdset = [
30 'switch',
31 'home',
32 'view',
33 't',
34 'rt',
35 'rep',
36 'del',
37 's',
38 'fr',
39 'fl',
40 'h',
41 'c',
42 'q'
43]
44
45
46def draw(t, keyword=None, fil=[], ig=[]):
47 """
48 Draw the rainbow
49 """
50
51 # Retrieve tweet
52 tid = t['id']
53 text = t['text']
54 screen_name = t['user']['screen_name']
55 name = t['user']['name']
56 created_at = t['created_at']
57 date = parser.parse(created_at)
58 date = date - datetime.timedelta(seconds=time.timezone)
59 clock = date.strftime('%Y/%m/%d %H:%M:%S')
60
61 # Filter and ignore
62 screen_name = '@' + screen_name
63 if fil and screen_name not in fil:
64 return
65 if ig and screen_name in ig:
66 return
67
68 res = db.tweet_query(tid)
69 if not res:
70 db.store(tid)
71 res = db.tweet_query(tid)
72 rid = res[0].rainbow_id
73
74 # Format info
75 user = cycle_color(name) + grey(' ' + screen_name + ' ')
76 meta = grey('[' + clock + '] [id=' + str(rid) + ']')
77 tweet = text.split()
78 # Highlight RT
79 tweet = map(lambda x: grey(x) if x == 'RT' else x, tweet)
80 # Highlight screen_name
81 tweet = map(lambda x: cycle_color(x) if x[0] == '@' else x, tweet)
82 # Highlight link
83 tweet = map(lambda x: cyan(x) if x[0:7] == 'http://' else x, tweet)
84 # Highlight search keyword
85 if keyword:
86 tweet = map(
87 lambda x: on_yellow(x) if
88 ''.join(c for c in x if c.isalnum()).lower() == keyword.lower()
89 else x,
90 tweet
91 )
92 tweet = ' '.join(tweet)
93
94 # Draw rainbow
95 line1 = u"{u:>{uw}}:".format(
96 u=user,
97 uw=len(user) + 2,
98 )
99 line2 = u"{c:>{cw}}".format(
100 c=meta,
101 cw=len(meta) + 2,
102 )
103 line3 = ' ' + tweet
104
105 printNicely('')
106 printNicely(line1)
107 printNicely(line2)
108 printNicely(line3)
109
110
111def parse_arguments():
112 """
113 Parse the arguments
114 """
115 parser = argparse.ArgumentParser(description=__doc__ or "")
116 parser.add_argument(
117 '-to',
118 '--timeout',
119 help='Timeout for the stream (seconds).')
120 parser.add_argument(
121 '-ht',
122 '--heartbeat-timeout',
123 help='Set heartbeat timeout.',
124 default=90)
125 parser.add_argument(
126 '-nb',
127 '--no-block',
128 action='store_true',
129 help='Set stream to non-blocking.')
130 parser.add_argument(
131 '-tt',
132 '--track-keywords',
133 help='Search the stream for specific text.')
134 parser.add_argument(
135 '-fil',
136 '--filter',
137 help='Filter specific screen_name.')
138 parser.add_argument(
139 '-ig',
140 '--ignore',
141 help='Ignore specific screen_name.')
142 return parser.parse_args()
143
144
145def authen():
146 """
147 Authenticate with Twitter OAuth
148 """
149 # When using rainbow stream you must authorize.
150 twitter_credential = os.environ.get(
151 'HOME',
152 os.environ.get(
153 'USERPROFILE',
154 '')) + os.sep + '.rainbow_oauth'
155 if not os.path.exists(twitter_credential):
156 oauth_dance("Rainbow Stream",
157 CONSUMER_KEY,
158 CONSUMER_SECRET,
159 twitter_credential)
160 oauth_token, oauth_token_secret = read_token_file(twitter_credential)
161 return OAuth(
162 oauth_token,
163 oauth_token_secret,
164 CONSUMER_KEY,
165 CONSUMER_SECRET)
166
167
168def get_decorated_name():
169 """
170 Beginning of every line
171 """
172 t = Twitter(auth=authen())
173 name = '@' + t.account.verify_credentials()['screen_name']
174 g['original_name'] = name[1:]
175 g['decorated_name'] = grey('[') + grey(name) + grey(']: ')
176
177
178def switch():
179 """
180 Switch stream
181 """
182 try:
183 target = g['stuff'].split()[0]
184
185 # Filter and ignore
186 args = parse_arguments()
187 try :
188 if g['stuff'].split()[-1] == '-f':
189 only = raw_input('Only nicks: ')
190 ignore = raw_input('Ignore nicks: ')
191 args.filter = only.split(',')
192 args.ignore = ignore.split(',')
193 elif g['stuff'].split()[-1] == '-d':
194 args.filter = ONLY_LIST
195 args.ignore = IGNORE_LIST
196 except:
197 printNicely(red('Sorry, wrong format.'))
198 return
199
200 # Public stream
201 if target == 'public':
202 keyword = g['stuff'].split()[1]
203 if keyword[0] == '#':
204 keyword = keyword[1:]
205 # Kill old process
206 os.kill(g['stream_pid'], signal.SIGKILL)
207 args.track_keywords = keyword
208 # Start new process
209 p = Process(
210 target=stream,
211 args=(
212 PUBLIC_DOMAIN,
213 args))
214 p.start()
215 g['stream_pid'] = p.pid
216
217 # Personal stream
218 elif target == 'mine':
219 # Kill old process
220 os.kill(g['stream_pid'], signal.SIGKILL)
221 # Start new process
222 p = Process(
223 target=stream,
224 args=(
225 USER_DOMAIN,
226 args,
227 g['original_name']))
228 p.start()
229 g['stream_pid'] = p.pid
230 printNicely('')
231 printNicely(green('Stream switched.'))
232 if args.filter:
233 printNicely(cyan('Only: ' + str(args.filter)))
234 if args.ignore:
235 printNicely(red('Ignore: ' + str(args.ignore)))
236 printNicely('')
237 except:
238 printNicely(red('Sorry I can\'t understand.'))
239 g['prefix'] = False
240
241
242def home():
243 """
244 Home
245 """
246 t = Twitter(auth=authen())
247 num = HOME_TWEET_NUM
248 if g['stuff'].isdigit():
249 num = g['stuff']
250 for tweet in reversed(t.statuses.home_timeline(count=num)):
251 draw(t=tweet)
252 printNicely('')
253
254
255def view():
256 """
257 Friend view
258 """
259 t = Twitter(auth=authen())
260 user = g['stuff'].split()[0]
261 if user[0] == '@':
262 try:
263 num = int(g['stuff'].split()[1])
264 except:
265 num = HOME_TWEET_NUM
266 for tweet in reversed(t.statuses.user_timeline(count=num, screen_name=user[1:])):
267 draw(t=tweet)
268 printNicely('')
269 else:
270 printNicely(red('A name should begin with a \'@\''))
271
272
273def tweet():
274 """
275 Tweet
276 """
277 t = Twitter(auth=authen())
278 t.statuses.update(status=g['stuff'])
279 g['prefix'] = False
280
281
282def retweet():
283 """
284 ReTweet
285 """
286 t = Twitter(auth=authen())
287 try:
288 id = int(g['stuff'].split()[0])
289 tid = db.rainbow_query(id)[0].tweet_id
290 t.statuses.retweet(id=tid, include_entities=False, trim_user=True)
291 except:
292 printNicely(red('Sorry I can\'t retweet for you.'))
293 g['prefix'] = False
294
295
296def reply():
297 """
298 Reply
299 """
300 t = Twitter(auth=authen())
301 try:
302 id = int(g['stuff'].split()[0])
303 tid = db.rainbow_query(id)[0].tweet_id
304 user = t.statuses.show(id=tid)['user']['screen_name']
305 status = ' '.join(g['stuff'].split()[1:])
306 status = '@' + user + ' ' + status.decode('utf-8')
307 t.statuses.update(status=status, in_reply_to_status_id=tid)
308 except:
309 printNicely(red('Sorry I can\'t understand.'))
310 g['prefix'] = False
311
312
313def delete():
314 """
315 Delete
316 """
317 t = Twitter(auth=authen())
318 try:
319 id = int(g['stuff'].split()[0])
320 tid = db.rainbow_query(id)[0].tweet_id
321 t.statuses.destroy(id=tid)
322 printNicely(green('Okay it\'s gone.'))
323 except:
324 printNicely(red('Sorry I can\'t delete this tweet for you.'))
325
326
327def search():
328 """
329 Search
330 """
331 t = Twitter(auth=authen())
332 try:
333 if g['stuff'][0] == '#':
334 rel = t.search.tweets(q=g['stuff'])['statuses']
335 if len(rel):
336 printNicely('Newest tweets:')
337 for i in reversed(xrange(SEARCH_MAX_RECORD)):
338 draw(t=rel[i], keyword=g['stuff'].strip()[1:])
339 printNicely('')
340 else:
341 printNicely(magenta('I\'m afraid there is no result'))
342 else:
343 printNicely(red('A keyword should be a hashtag (like \'#AKB48\')'))
344 except:
345 printNicely(red('Sorry I can\'t understand.'))
346
347
348def friend():
349 """
350 List of friend (following)
351 """
352 t = Twitter(auth=authen())
353 g['friends'] = t.friends.ids()['ids']
354 for i in g['friends']:
355 screen_name = t.users.lookup(user_id=i)[0]['screen_name']
356 user = cycle_color('@' + screen_name)
357 print(user, end=' ')
358 printNicely('')
359
360
361def follower():
362 """
363 List of follower
364 """
365 t = Twitter(auth=authen())
366 g['followers'] = t.followers.ids()['ids']
367 for i in g['followers']:
368 screen_name = t.users.lookup(user_id=i)[0]['screen_name']
369 user = cycle_color('@' + screen_name)
370 print(user, end=' ')
371 printNicely('')
372
373
374def help():
375 """
376 Help
377 """
378 usage = '''
379 Hi boss! I'm ready to serve you right now!
380 -------------------------------------------------------------
381 You are already on your personal stream:
382 "switch public #AKB" will switch to public stream and follow "AKB" keyword.
383 "switch mine" will switch back to your personal stream.
384 "switch mine -f" will prompt to enter the filter.
385 "Only nicks" filter will decide nicks will be INCLUDE ONLY.
386 "Ignore nicks" filter will decide nicks will be EXCLUDE.
387 "switch mine -d" will use the config's ONLY_LIST and IGNORE_LIST(see config.py).
388 For more action:
389 "home" will show your timeline. "home 7" will show 7 tweet.
390 "view @bob" will show your friend @bob's home.
391 "t oops" will tweet "oops" immediately.
392 "rt 12345" will retweet to tweet with id "12345".
393 "rep 12345 oops" will reply "oops" to tweet with id "12345".
394 "del 12345" will delete tweet with id "12345".
395 "s #AKB48" will search for "AKB48" and return 5 newest tweet.
396 "fr" will list out your following people.
397 "fl" will list out your followers.
398 "h" will show this help again.
399 "c" will clear the terminal.
400 "q" will exit.
401 -------------------------------------------------------------
402 Have fun and hang tight!
403 '''
404 printNicely(usage)
405
406
407def clear():
408 """
409 Clear screen
410 """
411 os.system('clear')
412 g['prefix'] = False
413
414
415def quit():
416 """
417 Exit all
418 """
419 os.system('rm -rf rainbow.db')
420 os.kill(g['stream_pid'], signal.SIGKILL)
421 sys.exit()
422
423
424def reset():
425 """
426 Reset prefix of line
427 """
428 if g['reset']:
429 printNicely(green('Need tips ? Type "h" and hit Enter key!'))
430 g['prefix'] = True
431 g['reset'] = False
432
433
434def process(cmd):
435 """
436 Process switch
437 """
438 return dict(zip(
439 cmdset,
440 [
441 switch,
442 home,
443 view,
444 tweet,
445 retweet,
446 reply,
447 delete,
448 search,
449 friend,
450 follower,
451 help,
452 clear,
453 quit
454 ]
455 )).get(cmd, reset)
456
457
458def listen():
459 """
460 Listen to user's input
461 """
462 d = dict(zip(
463 cmdset,
464 [
465 ['public #','mine'], # switch
466 [], # home
467 ['@'], # view
468 [], # tweet
469 [], # retweet
470 [], # reply
471 [], # delete
472 ['#'], # search
473 [], # friend
474 [], # follower
475 [], # help
476 [], # clear
477 [], # quit
478 ]
479 ))
480 init_interactive_shell(d)
481 first = True
482 while True:
483 if g['prefix'] and not first:
484 line = raw_input(g['decorated_name'])
485 else:
486 line = raw_input()
487 try:
488 cmd = line.split()[0]
489 except:
490 cmd = ''
491 # Save cmd to global variable and call process
492 g['stuff'] = ' '.join(line.split()[1:])
493 process(cmd)()
494 first = False
495
496
497def stream(domain, args, name='Rainbow Stream'):
498 """
499 Track the stream
500 """
501
502 # The Logo
503 art_dict = {
504 USER_DOMAIN: name,
505 PUBLIC_DOMAIN: args.track_keywords,
506 SITE_DOMAIN: 'Site Stream',
507 }
508 ascii_art(art_dict[domain])
509
510 # These arguments are optional:
511 stream_args = dict(
512 timeout=args.timeout,
513 block=not args.no_block,
514 heartbeat_timeout=args.heartbeat_timeout)
515
516 # Track keyword
517 query_args = dict()
518 if args.track_keywords:
519 query_args['track'] = args.track_keywords
520
521 # Get stream
522 stream = TwitterStream(
523 auth=authen(),
524 domain=domain,
525 **stream_args)
526
527 if domain == USER_DOMAIN:
528 tweet_iter = stream.user(**query_args)
529 elif domain == SITE_DOMAIN:
530 tweet_iter = stream.site(**query_args)
531 else:
532 if args.track_keywords:
533 tweet_iter = stream.statuses.filter(**query_args)
534 else:
535 tweet_iter = stream.statuses.sample()
536
537 # Iterate over the stream.
538 for tweet in tweet_iter:
539 if tweet is None:
540 printNicely("-- None --")
541 elif tweet is Timeout:
542 printNicely("-- Timeout --")
543 elif tweet is HeartbeatTimeout:
544 printNicely("-- Heartbeat Timeout --")
545 elif tweet is Hangup:
546 printNicely("-- Hangup --")
547 elif tweet.get('text'):
548 draw(t=tweet, keyword=args.track_keywords, fil=args.filter, ig=args.ignore)
549
550
551def fly():
552 """
553 Main function
554 """
555 # Spawn stream process
556 args = parse_arguments()
557 get_decorated_name()
558 p = Process(target=stream, args=(USER_DOMAIN, args, g['original_name']))
559 p.start()
560
561 # Start listen process
562 g['prefix'] = True
563 g['reset'] = True
564 g['stream_pid'] = p.pid
565 listen()
566