termtter 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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