termtter 1.8.0 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/Rakefile +1 -1
  2. data/VERSION +1 -1
  3. data/lib/plugins/aa.rb +1 -1
  4. data/lib/plugins/appendtitle.rb +5 -12
  5. data/lib/plugins/defaults/auto_reload.rb +1 -0
  6. data/lib/plugins/defaults/cache.rb +18 -0
  7. data/lib/plugins/defaults/command_line.rb +1 -1
  8. data/lib/plugins/defaults/fib.rb +15 -6
  9. data/lib/plugins/defaults/retweet.rb +1 -1
  10. data/lib/plugins/defaults/standard_commands.rb +31 -25
  11. data/lib/plugins/defaults/stdout.rb +12 -4
  12. data/lib/plugins/defaults/system.rb +27 -0
  13. data/lib/plugins/dupu.rb +13 -0
  14. data/lib/plugins/erase.rb +4 -0
  15. data/lib/plugins/error_log.rb +17 -0
  16. data/lib/plugins/expand-tinyurl.rb +8 -1
  17. data/lib/plugins/growl.rb +10 -7
  18. data/lib/plugins/hatena_keyword_haiku.rb +88 -0
  19. data/lib/plugins/irc_gw.rb +12 -4
  20. data/lib/plugins/itunes.rb +29 -26
  21. data/lib/plugins/mecab.rb +23 -0
  22. data/lib/plugins/mudan_kinshi.rb +13 -0
  23. data/lib/plugins/ndkn.rb +7 -0
  24. data/lib/plugins/other_user.rb +47 -0
  25. data/lib/plugins/reply_sound.rb +75 -25
  26. data/lib/plugins/ruby-v.rb +10 -0
  27. data/lib/plugins/time_signal.rb +21 -0
  28. data/lib/plugins/tinyurl.rb +6 -4
  29. data/lib/plugins/train.rb +1 -1
  30. data/lib/plugins/translation.rb +2 -0
  31. data/lib/plugins/user_stream.rb +122 -0
  32. data/lib/plugins/whale.rb +28 -0
  33. data/lib/termtter/active_rubytter.rb +4 -0
  34. data/lib/termtter/api.rb +55 -30
  35. data/lib/termtter/client.rb +19 -4
  36. data/lib/termtter/config_setup.rb +10 -2
  37. data/lib/termtter/config_template.erb +4 -2
  38. data/lib/termtter/crypt.rb +13 -0
  39. data/lib/termtter/default_config.rb +5 -2
  40. data/lib/termtter/hookable.rb +4 -0
  41. data/lib/termtter/memory_cache.rb +67 -19
  42. data/lib/termtter/rubytter_proxy.rb +101 -26
  43. data/lib/termtter/system_extensions.rb +22 -18
  44. data/lib/termtter.rb +5 -1
  45. data/spec/termtter/crypt_spec.rb +16 -0
  46. data/spec/termtter/rubytter_proxy_spec.rb +14 -0
  47. metadata +85 -26
@@ -2,11 +2,12 @@ config.set_default(:logger, nil)
2
2
  config.set_default(:update_interval, 120)
3
3
  config.set_default(:prompt, '> ')
4
4
  config.set_default(:devel, false)
5
+ config.set_default(:token_file, "~/.termtter/token")
5
6
  config.set_default(:timeout, 60)
6
- config.set_default(:retry, 1)
7
+ config.set_default(:retry, 3)
7
8
  config.set_default(:splash, <<SPLASH)
8
9
 
9
- <cyan>&lt;(@)//_</cyan> . . <on_green> #{(Time.now.year == 2010 && Time.now.month == 4 && Time.now.day == 1)?'Centertter':'Termtter'} <underline>#{Termtter::VERSION}</underline> </on_green>
10
+ <cyan>&lt;(@)//_</cyan> . . <on_green> #{(Time.now.year == 2011 && Time.now.month == 4 && Time.now.day == 1)?'Centertter':'Termtter'} <underline>#{Termtter::VERSION}</underline> </on_green>
10
11
  <cyan>\\\\</cyan> <on_green> http://termtter.org/ </on_green>
11
12
 
12
13
  SPLASH
@@ -16,3 +17,5 @@ config.system.set_default :run_commands, []
16
17
  config.system.set_default :load_plugins, []
17
18
  config.system.set_default :disable_plugins, []
18
19
  config.system.set_default :eval_scripts, []
20
+
21
+ config.cache.set_default(:memcached_server, 'localhost:11211')
@@ -34,6 +34,10 @@ module Termtter
34
34
  hooks[hook.name] = hook
35
35
  end
36
36
 
37
+ def remove_hook(name)
38
+ hooks.delete(name.to_sym)
39
+ end
40
+
37
41
  def get_hook(name)
38
42
  hooks[name]
39
43
  end
@@ -1,32 +1,80 @@
1
1
  require 'delegate'
2
2
 
3
3
  module Termtter
4
- class MemoryCache < SimpleDelegator
5
- attr_reader :limit
6
4
 
7
- def initialize(limit = 10000)
8
- super(Hash.new)
9
- @keys = []
10
- @limit = limit
5
+ class MemoryCache
6
+ # delegate to storage class
7
+
8
+ def storage
9
+ @storage ||= storage_class.new(config.cache.memcached_server)
11
10
  end
12
11
 
13
- def adjust(key)
14
- unless @keys.include?(key)
15
- @keys << key
16
- while @keys.size > limit
17
- delete(@keys.shift)
18
- end
19
- end
12
+ def method_missing(method, *args, &block)
13
+ storage.__send__(method, *args, &block)
20
14
  end
21
15
 
22
- def []=(key, value)
23
- super
24
- adjust(key)
16
+ protected
17
+ def storage_class
18
+ can_use_memcache? ? MemCache : MemCacheMock
19
+ end
20
+
21
+ def can_use_memcache?
22
+ return unless config.cache.memcached_server
23
+ begin
24
+ require 'memcache'
25
+ MemCache.new(config.cache.memcached_server).stats # when server is wrong, die here.
26
+ rescue StandardError, LoadError
27
+ false
28
+ else
29
+ true
30
+ end
25
31
  end
26
32
 
27
- def store(key, value)
28
- super
29
- adjust(key)
33
+ class MemCacheMock < SimpleDelegator
34
+ def initialize(dummy_server)
35
+ super(Hash.new)
36
+ @keys = []
37
+ @limit = 10000
38
+ end
39
+
40
+ def set(key, value, expiry = 0, raw = false)
41
+ self[key] = try_clone value
42
+ adjust(key)
43
+ self
44
+ end
45
+
46
+ def get(key, raw = false)
47
+ try_clone self[key]
48
+ end
49
+
50
+ def get_multi(*keys)
51
+ results = {}
52
+ keys.each{ |key|
53
+ results[key] = try_clone self[key]
54
+ }
55
+ results
56
+ end
57
+
58
+ def stats
59
+ { "total_items"=> length }
60
+ end
61
+
62
+ def flush_all(delay = 0)
63
+ clear
64
+ end
65
+
66
+ protected
67
+
68
+ def try_clone(a)
69
+ a.clone rescue a
70
+ end
71
+
72
+ def adjust(key)
73
+ return if @keys.include?(key)
74
+ @keys << key
75
+ delete(@keys.shift) while @keys.size > @limit
76
+ end
77
+
30
78
  end
31
79
  end
32
80
  end
@@ -1,14 +1,25 @@
1
1
  # -*- coding: utf-8 -*-
2
- config.set_default(:memory_cache_size, 10000)
2
+ begin
3
+ require 'nokogiri'
4
+ rescue LoadError
5
+ begin
6
+ require 'hpricot'
7
+ rescue LoadError
8
+ end
9
+ end
3
10
 
4
11
  module Termtter
12
+ class JSONError < StandardError; end
5
13
  class RubytterProxy
14
+ class FrequentAccessError < StandardError; end
15
+
6
16
  include Hookable
7
17
 
8
18
  attr_reader :rubytter
9
19
 
10
20
  def initialize(*args)
11
- @rubytter = Rubytter.new(*args)
21
+ @rubytter = OAuthRubytter.new(*args)
22
+ @initial_args = args
12
23
  end
13
24
 
14
25
  def method_missing(method, *args, &block)
@@ -47,23 +58,6 @@ module Termtter
47
58
  end
48
59
  end
49
60
 
50
- def status_cache_store
51
- # TODO: DB store とかにうまいこと切り替えられるようにしたい
52
- @status_cache_store ||= MemoryCache.new(config.memory_cache_size)
53
- end
54
-
55
- def users_cache_store
56
- @users_cache_store ||= MemoryCache.new(config.memory_cache_size)
57
- end
58
-
59
- def cached_user(screen_name)
60
- users_cache_store[screen_name]
61
- end
62
-
63
- def cached_status(id)
64
- status_cache_store[id.to_i]
65
- end
66
-
67
61
  def call_rubytter_or_use_cache(method, *args, &block)
68
62
  case method
69
63
  when :show
@@ -72,6 +66,12 @@ module Termtter
72
66
  store_status_cache(status)
73
67
  end
74
68
  status
69
+ when :user
70
+ unless user = cached_user(args[0])
71
+ user = call_rubytter(method, *args, &block)
72
+ store_user_cache(user)
73
+ end
74
+ user
75
75
  when :home_timeline, :user_timeline, :friends_timeline, :search
76
76
  statuses = call_rubytter(method, *args, &block)
77
77
  statuses.each do |status|
@@ -83,27 +83,102 @@ module Termtter
83
83
  end
84
84
  end
85
85
 
86
+ def cached_user(screen_name_or_id)
87
+ user = Termtter::Client.memory_cache.get(['user', Termtter::Client.normalize_as_user_name(screen_name_or_id.to_s)].join('-'))
88
+ ActiveRubytter.new(user) if user
89
+ end
90
+
91
+ def cached_status(status_id)
92
+ status = Termtter::Client.memory_cache.get(['status', status_id].join('-'))
93
+ ActiveRubytter.new(status) if status
94
+ end
95
+
86
96
  def store_status_cache(status)
87
- return if status_cache_store.key?(status.id)
88
- status_cache_store[status.id] = status
97
+ Termtter::Client.memory_cache.set(['status', status.id].join('-'), status.to_hash, 3600 * 24 * 14)
89
98
  store_user_cache(status.user)
90
99
  end
91
100
 
92
101
  def store_user_cache(user)
93
- return if users_cache_store.key?(user.screen_name)
94
- users_cache_store[user.screen_name] = user
102
+ Termtter::Client.memory_cache.set(['user', user.id.to_i].join('-'), user.to_hash, 3600 * 24)
103
+ Termtter::Client.memory_cache.set(['user', Termtter::Client.normalize_as_user_name(user.screen_name)].join('-'), user.to_hash, 3600 * 24)
104
+ end
105
+
106
+ attr_accessor :safe_mode
107
+ def safe
108
+ new_instance = self.class.new(@rubytter)
109
+ new_instance.safe_mode = true
110
+ self.instance_variables.each{ |v|
111
+ new_instance.instance_variable_set(v, self.instance_variable_get(v))
112
+ }
113
+ new_instance
114
+ end
115
+
116
+ def current_limit
117
+ @limit_manager ||= LimitManager.new(@rubytter)
95
118
  end
96
119
 
97
120
  def call_rubytter(method, *args, &block)
98
- config.retry.times do
121
+ raise FrequentAccessError if @safe_mode && !self.current_limit.safe?
122
+ config.retry.times do |now|
99
123
  begin
100
124
  timeout(config.timeout) do
101
125
  return @rubytter.__send__(method, *args, &block)
102
126
  end
103
- rescue TimeoutError
127
+ rescue Rubytter::APIError => e
128
+ raise e
129
+ rescue JSON::ParserError => e
130
+ if message = error_html_message(e)
131
+ puts message
132
+ raise Rubytter::APIError.new(message)
133
+ else
134
+ raise e
135
+ end
136
+ rescue StandardError, TimeoutError => e
137
+ if now + 1 == config.retry
138
+ raise e
139
+ else
140
+ Termtter::Client.logger.debug("rubytter_proxy: retry (#{e.class.to_s}: #{e.message})")
141
+ end
142
+ end
143
+ end
144
+ end
145
+
146
+ if defined? Nokogiri
147
+ def error_html_message(e)
148
+ Nokogiri(e.message).at('title, h2').text rescue nil
149
+ end
150
+ elsif defined? Hpricot
151
+ def error_html_message(e)
152
+ Hpricot(e.message).at('title, h2').inner_text rescue nil
153
+ end
154
+ else
155
+ def error_html_message(e)
156
+ m = %r'<title>(.*?)</title>'.match(e.message) and m.captures[0] rescue nil
157
+ end
158
+ end
159
+ private :error_html_message
160
+
161
+ class LimitManager
162
+ def initialize(rubytter)
163
+ @rubytter = rubytter
164
+ @limit = nil
165
+ @count = 0
166
+ end
167
+
168
+ def get
169
+ @count += 1
170
+ if @count > 5 || !@limit
171
+ @count = 0
172
+ @limit = @rubytter.limit_status
104
173
  end
174
+ @limit
175
+ end
176
+
177
+ def safe?
178
+ limit = self.get
179
+ threshold = [(Time.parse(limit.reset_time) - Time.now) / 3600 - 0.1, 0.1].max * limit.hourly_limit
180
+ threshold < limit.remaining_hits
105
181
  end
106
- raise TimeoutError, 'execution expired'
107
182
  end
108
183
  end
109
184
  end
@@ -25,9 +25,11 @@ module Readline
25
25
  pathes = Array(ENV['TERMTTER_EXT_LIB'] || [
26
26
  '/usr/lib64/libreadline.so',
27
27
  '/usr/local/lib64/libreadline.so',
28
+ '/usr/local/lib/libreadline.dylib',
28
29
  '/opt/local/lib/libreadline.dylib',
29
30
  '/usr/lib/libreadline.so',
30
31
  '/usr/local/lib/libreadline.so',
32
+ Dir.glob('/lib/libreadline.so*')[-1] || '', # '' is dummy
31
33
  File.join(Gem.bindir, 'readline.dll')
32
34
  ])
33
35
  dlload(pathes.find { |path| File.exist?(path)})
@@ -46,7 +48,7 @@ module Readline
46
48
  end
47
49
  rescue Exception
48
50
  def self.rl_parse_and_bind(str);end
49
- def self.refresh_line;end
51
+ def self.refresh_line;end unless Readline::NATIVE_REFRESH_LINE_METHOD
50
52
  end
51
53
  end
52
54
 
@@ -60,25 +62,27 @@ def create_highline
60
62
  HighLine.new($stdin)
61
63
  end
62
64
 
65
+ class BrowserNotFound < StandardError; end
66
+
63
67
  def open_browser(url)
64
- if ENV['KDE_FULL_SESSION'] == 'true'
65
- system 'kfmclient', 'exec', url
66
- elsif ENV['GNOME_DESKTOP_SESSION_ID']
67
- system 'gnome-open', url
68
- elsif !(/not found/ =~ `which exo-open`)
69
- # FIXME: is fungible system('exo-open').nil? for lambda {...}
70
- system 'exo-open', url
68
+ found = case RUBY_PLATFORM.downcase
69
+ when /linux/
70
+ [['xdg-open'], ['x-www-browser'], ['firefox'], ['w3m', '-X']]
71
+ when /darwin/
72
+ [['open']]
73
+ when /mswin(?!ce)|mingw|bccwin/
74
+ [['start']]
71
75
  else
72
- case RUBY_PLATFORM.downcase
73
- when /linux/
74
- system 'firefox', url
75
- when /darwin/
76
- system 'open', url
77
- when /mswin(?!ce)|mingw|bccwin/
78
- system 'start', url
79
- else
80
- system 'firefox', url
81
- end
76
+ [['xdg-open'], ['firefox'], ['w3m', '-X']]
77
+ end.find do |cmd|
78
+ system *(cmd.dup << url)
79
+ $?.exitstatus != 127
80
+ end
81
+ if found
82
+ # Kernel::__method__ is not suppoted in Ruby 1.8.6 or earlier.
83
+ eval %{ def open_browser(url); system *(#{found}.dup << url); end }
84
+ else
85
+ raise BrowserNotFound
82
86
  end
83
87
  end
84
88
 
data/lib/termtter.rb CHANGED
@@ -14,16 +14,17 @@ require 'net/https'
14
14
  require 'open-uri'
15
15
  require 'optparse'
16
16
  require 'readline'
17
- gem 'rubytter', '>= 0.11.0'
18
17
  require 'rubytter'
19
18
  require 'notify'
20
19
  require 'timeout'
20
+ require 'oauth'
21
21
 
22
22
  module Termtter
23
23
  VERSION = File.read(File.join(File.dirname(__FILE__), '../VERSION')).strip
24
24
  APP_NAME = 'termtter'
25
25
 
26
26
  require 'termtter/config'
27
+ require 'termtter/crypt'
27
28
  require 'termtter/default_config'
28
29
  require 'termtter/optparse'
29
30
  require 'termtter/command'
@@ -43,4 +44,7 @@ module Termtter
43
44
  CONF_DIR = File.expand_path('~/.termtter') unless defined? CONF_DIR
44
45
  CONF_FILE = File.join(Termtter::CONF_DIR, 'config') unless defined? CONF_FILE
45
46
  $:.unshift(CONF_DIR)
47
+
48
+ CONSUMER_KEY = 'eFFLaGJ3M0VMZExvNmtlNHJMVndsQQ=='
49
+ CONSUMER_SECRET = 'cW8xbW9JT3dyT0NHTmVaMWtGbHpjSk1tN0lReTlJYTl0N0trcW9Fdkhr'
46
50
  end
@@ -0,0 +1,16 @@
1
+ # coding: utf-8
2
+
3
+ require File.dirname(__FILE__) + '/../spec_helper'
4
+ require 'termtter/crypt'
5
+
6
+ module Termtter
7
+ describe Crypt do
8
+ it '.crypt crypts string' do
9
+ Crypt.crypt('hi').should be_kind_of(String)
10
+ end
11
+ it '.decrypt decrypts string' do
12
+ a = Crypt.crypt('hi')
13
+ Crypt.decrypt(a).should == 'hi'
14
+ end
15
+ end
16
+ end
@@ -76,5 +76,19 @@ module Termtter
76
76
  @rubytter_mock.should_receive(:show).exactly(0)
77
77
  @twitter.show(1).should == "status"
78
78
  end
79
+
80
+ it 'has safe mode' do
81
+ safe_twitter = @twitter.safe
82
+ safe_twitter.should be_kind_of(RubytterProxy)
83
+ safe_twitter.safe_mode.should be_true
84
+ safe_twitter.rubytter.should == @twitter.rubytter
85
+ end
86
+
87
+ it 'dies when LimitManager.safe? is false' do
88
+ safe_twitter = @twitter.safe
89
+ safe_twitter.current_limit.stub!(:safe?).and_return(false)
90
+
91
+ lambda{ safe_twitter.call_rubytter(:update, 'test') }.should raise_error(Termtter::RubytterProxy::FrequentAccessError)
92
+ end
79
93
  end
80
94
  end