add filter
[rainbowstream.git] / rainbowstream / rainbow.py
CommitLineData
91476ec3
O
1"""
2Colorful user's timeline stream
3"""
78b81730
O
4from __future__ import print_function
5from multiprocessing import Process
6from dateutil import parser
7
b2b933a9 8import os
9import os.path
10import sys
11import signal
12import argparse
13import time
14import datetime
91476ec3 15
91476ec3 16from twitter.stream import TwitterStream, Timeout, HeartbeatTimeout, Hangup
54277114 17from twitter.api import *
91476ec3 18from twitter.oauth import OAuth, read_token_file
8c840a83 19from twitter.oauth_dance import oauth_dance
91476ec3 20from twitter.util import printNicely
91476ec3 21
2a6238f5
O
22from .colors import *
23from .config import *
94a5f62e 24from .interactive import *
18cab06a 25from .db import *
2a6238f5 26
f405a7d0 27g = {}
18cab06a 28db = RainbowDB()
94a5f62e 29cmdset = [
42fde775 30 'switch',
94a5f62e 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]
22be990e 44
b2b933a9 45
d51b4107 46def draw(t, keyword=None, fil=[], ig=[]):
91476ec3
O
47 """
48 Draw the rainbow
49 """
d51b4107 50
91476ec3 51 # Retrieve tweet
829cc2d8 52 tid = t['id']
91476ec3
O
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)
e20af1c3 58 date = date - datetime.timedelta(seconds=time.timezone)
59 clock = date.strftime('%Y/%m/%d %H:%M:%S')
91476ec3 60
d51b4107
O
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
18cab06a
O
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
91476ec3 74 # Format info
d51b4107 75 user = cycle_color(name) + grey(' ' + screen_name + ' ')
e20af1c3 76 meta = grey('[' + clock + '] [id=' + str(rid) + ']')
8c840a83 77 tweet = text.split()
b8dda704 78 # Highlight RT
2a6238f5 79 tweet = map(lambda x: grey(x) if x == 'RT' else x, tweet)
b8dda704 80 # Highlight screen_name
2a6238f5 81 tweet = map(lambda x: cycle_color(x) if x[0] == '@' else x, tweet)
b8dda704 82 # Highlight link
2a6238f5 83 tweet = map(lambda x: cyan(x) if x[0:7] == 'http://' else x, tweet)
b8dda704 84 # Highlight search keyword
7a431249 85 if keyword:
22be990e 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 )
8c840a83 92 tweet = ' '.join(tweet)
91476ec3
O
93
94 # Draw rainbow
06773ffe 95 line1 = u"{u:>{uw}}:".format(
2a6238f5
O
96 u=user,
97 uw=len(user) + 2,
91476ec3 98 )
06773ffe 99 line2 = u"{c:>{cw}}".format(
829cc2d8
O
100 c=meta,
101 cw=len(meta) + 2,
06773ffe
O
102 )
103 line3 = ' ' + tweet
91476ec3 104
94a5f62e 105 printNicely('')
f405a7d0
O
106 printNicely(line1)
107 printNicely(line2)
108 printNicely(line3)
91476ec3
O
109
110
111def parse_arguments():
112 """
113 Parse the arguments
114 """
91476ec3 115 parser = argparse.ArgumentParser(description=__doc__ or "")
2a6238f5
O
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.')
d51b4107
O
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.')
91476ec3
O
142 return parser.parse_args()
143
144
54277114
O
145def authen():
146 """
7b674cef 147 Authenticate with Twitter OAuth
54277114 148 """
8c840a83 149 # When using rainbow stream you must authorize.
2a6238f5
O
150 twitter_credential = os.environ.get(
151 'HOME',
152 os.environ.get(
153 'USERPROFILE',
154 '')) + os.sep + '.rainbow_oauth'
8c840a83
O
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)
54277114 161 return OAuth(
2a6238f5
O
162 oauth_token,
163 oauth_token_secret,
164 CONSUMER_KEY,
165 CONSUMER_SECRET)
91476ec3 166
54277114
O
167
168def get_decorated_name():
169 """
170 Beginning of every line
171 """
172 t = Twitter(auth=authen())
c5ff542b 173 name = '@' + t.account.verify_credentials()['screen_name']
42fde775 174 g['original_name'] = name[1:]
f405a7d0 175 g['decorated_name'] = grey('[') + grey(name) + grey(']: ')
54277114 176
f405a7d0 177
42fde775 178def switch():
179 """
180 Switch stream
181 """
182 try:
183 target = g['stuff'].split()[0]
184
d51b4107
O
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
42fde775 200 # Public stream
201 if target == 'public':
202 keyword = g['stuff'].split()[1]
203 if keyword[0] == '#':
204 keyword = keyword[1:]
42fde775 205 # Kill old process
206 os.kill(g['stream_pid'], signal.SIGKILL)
42fde775 207 args.track_keywords = keyword
42fde775 208 # Start new process
209 p = Process(
d51b4107 210 target=stream,
42fde775 211 args=(
d51b4107 212 PUBLIC_DOMAIN,
42fde775 213 args))
214 p.start()
215 g['stream_pid'] = p.pid
216
217 # Personal stream
218 elif target == 'mine':
42fde775 219 # Kill old process
220 os.kill(g['stream_pid'], signal.SIGKILL)
42fde775 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
d51b4107 230 printNicely('')
1551a7d3 231 printNicely(green('Stream switched.'))
d51b4107
O
232 if args.filter:
233 printNicely(cyan('Only: ' + str(args.filter)))
234 if args.ignore:
235 printNicely(red('Ignore: ' + str(args.ignore)))
236 printNicely('')
42fde775 237 except:
238 printNicely(red('Sorry I can\'t understand.'))
239 g['prefix'] = False
240
241
7b674cef 242def home():
243 """
244 Home
245 """
246 t = Twitter(auth=authen())
94a5f62e 247 num = HOME_TWEET_NUM
7b674cef 248 if g['stuff'].isdigit():
94a5f62e 249 num = g['stuff']
250 for tweet in reversed(t.statuses.home_timeline(count=num)):
7b674cef 251 draw(t=tweet)
94a5f62e 252 printNicely('')
7b674cef 253
254
255def view():
256 """
257 Friend view
258 """
259 t = Twitter(auth=authen())
260 user = g['stuff'].split()[0]
b8fbcb70 261 if user[0] == '@':
262 try:
94a5f62e 263 num = int(g['stuff'].split()[1])
b8fbcb70 264 except:
94a5f62e 265 num = HOME_TWEET_NUM
266 for tweet in reversed(t.statuses.user_timeline(count=num, screen_name=user[1:])):
b8fbcb70 267 draw(t=tweet)
94a5f62e 268 printNicely('')
b8fbcb70 269 else:
c91f75f2 270 printNicely(red('A name should begin with a \'@\''))
7b674cef 271
272
f405a7d0 273def tweet():
54277114 274 """
7b674cef 275 Tweet
54277114
O
276 """
277 t = Twitter(auth=authen())
f405a7d0 278 t.statuses.update(status=g['stuff'])
94a5f62e 279 g['prefix'] = False
f405a7d0 280
b2b933a9 281
1ba4abfd
O
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
b2b933a9 290 t.statuses.retweet(id=tid, include_entities=False, trim_user=True)
1ba4abfd 291 except:
c91f75f2 292 printNicely(red('Sorry I can\'t retweet for you.'))
94a5f62e 293 g['prefix'] = False
1ba4abfd
O
294
295
7b674cef 296def reply():
829cc2d8 297 """
7b674cef 298 Reply
829cc2d8
O
299 """
300 t = Twitter(auth=authen())
7b674cef 301 try:
302 id = int(g['stuff'].split()[0])
18cab06a
O
303 tid = db.rainbow_query(id)[0].tweet_id
304 user = t.statuses.show(id=tid)['user']['screen_name']
7b674cef 305 status = ' '.join(g['stuff'].split()[1:])
306 status = '@' + user + ' ' + status.decode('utf-8')
18cab06a 307 t.statuses.update(status=status, in_reply_to_status_id=tid)
7b674cef 308 except:
c91f75f2 309 printNicely(red('Sorry I can\'t understand.'))
94a5f62e 310 g['prefix'] = False
7b674cef 311
312
313def delete():
314 """
315 Delete
316 """
317 t = Twitter(auth=authen())
318 try:
319 id = int(g['stuff'].split()[0])
18cab06a
O
320 tid = db.rainbow_query(id)[0].tweet_id
321 t.statuses.destroy(id=tid)
c91f75f2 322 printNicely(green('Okay it\'s gone.'))
7b674cef 323 except:
c91f75f2 324 printNicely(red('Sorry I can\'t delete this tweet for you.'))
829cc2d8
O
325
326
f405a7d0
O
327def search():
328 """
7b674cef 329 Search
f405a7d0
O
330 """
331 t = Twitter(auth=authen())
94a5f62e 332 try:
333 if g['stuff'][0] == '#':
334 rel = t.search.tweets(q=g['stuff'])['statuses']
c91f75f2 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'))
94a5f62e 342 else:
c91f75f2 343 printNicely(red('A keyword should be a hashtag (like \'#AKB48\')'))
94a5f62e 344 except:
c91f75f2 345 printNicely(red('Sorry I can\'t understand.'))
b2b933a9 346
f405a7d0 347
843647ad
O
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']
22be990e 356 user = cycle_color('@' + screen_name)
843647ad 357 print(user, end=' ')
42fde775 358 printNicely('')
843647ad
O
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']
22be990e 369 user = cycle_color('@' + screen_name)
843647ad 370 print(user, end=' ')
42fde775 371 printNicely('')
843647ad
O
372
373
f405a7d0
O
374def help():
375 """
7b674cef 376 Help
f405a7d0
O
377 """
378 usage = '''
379 Hi boss! I'm ready to serve you right now!
006f8cc4 380 -------------------------------------------------------------
d70182eb 381 You are already on your personal stream:
42fde775 382 "switch public #AKB" will switch to public stream and follow "AKB" keyword.
383 "switch mine" will switch back to your personal stream.
d51b4107
O
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).
42fde775 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.
006f8cc4 401 -------------------------------------------------------------
843647ad 402 Have fun and hang tight!
f405a7d0
O
403 '''
404 printNicely(usage)
f405a7d0
O
405
406
843647ad 407def clear():
f405a7d0 408 """
7b674cef 409 Clear screen
f405a7d0 410 """
843647ad 411 os.system('clear')
7186f557 412 g['prefix'] = False
f405a7d0
O
413
414
843647ad 415def quit():
b8dda704
O
416 """
417 Exit all
418 """
8e633322 419 os.system('rm -rf rainbow.db')
843647ad
O
420 os.kill(g['stream_pid'], signal.SIGKILL)
421 sys.exit()
b8dda704
O
422
423
94a5f62e 424def reset():
f405a7d0 425 """
94a5f62e 426 Reset prefix of line
f405a7d0 427 """
c91f75f2 428 if g['reset']:
429 printNicely(green('Need tips ? Type "h" and hit Enter key!'))
94a5f62e 430 g['prefix'] = True
c91f75f2 431 g['reset'] = False
54277114
O
432
433
94a5f62e 434def process(cmd):
54277114 435 """
94a5f62e 436 Process switch
54277114 437 """
94a5f62e 438 return dict(zip(
439 cmdset,
b2b933a9 440 [
42fde775 441 switch,
b2b933a9 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 ]
94a5f62e 455 )).get(cmd, reset)
456
457
458def listen():
42fde775 459 """
460 Listen to user's input
461 """
d51b4107
O
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)
b2b933a9 481 first = True
482 while True:
94a5f62e 483 if g['prefix'] and not first:
484 line = raw_input(g['decorated_name'])
485 else:
486 line = raw_input()
843647ad
O
487 try:
488 cmd = line.split()[0]
489 except:
490 cmd = ''
f405a7d0 491 # Save cmd to global variable and call process
843647ad
O
492 g['stuff'] = ' '.join(line.split()[1:])
493 process(cmd)()
94a5f62e 494 first = False
54277114
O
495
496
42fde775 497def stream(domain, args, name='Rainbow Stream'):
54277114 498 """
f405a7d0 499 Track the stream
54277114 500 """
d51b4107 501
54277114 502 # The Logo
42fde775 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])
d51b4107 509
91476ec3
O
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
2a6238f5 522 stream = TwitterStream(
22be990e 523 auth=authen(),
42fde775 524 domain=domain,
2a6238f5 525 **stream_args)
91476ec3 526
42fde775 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.
91476ec3
O
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'):
d51b4107 548 draw(t=tweet, keyword=args.track_keywords, fil=args.filter, ig=args.ignore)
54277114
O
549
550
551def fly():
552 """
553 Main function
554 """
42fde775 555 # Spawn stream process
556 args = parse_arguments()
54277114 557 get_decorated_name()
42fde775 558 p = Process(target=stream, args=(USER_DOMAIN, args, g['original_name']))
559 p.start()
560
561 # Start listen process
94a5f62e 562 g['prefix'] = True
c91f75f2 563 g['reset'] = True
f405a7d0 564 g['stream_pid'] = p.pid
94a5f62e 565 listen()
fef2b940 566