termtter 1.8.0 → 1.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/lib/plugins/aa.rb +1 -1
- data/lib/plugins/appendtitle.rb +5 -12
- data/lib/plugins/defaults/auto_reload.rb +1 -0
- data/lib/plugins/defaults/cache.rb +18 -0
- data/lib/plugins/defaults/command_line.rb +1 -1
- data/lib/plugins/defaults/fib.rb +15 -6
- data/lib/plugins/defaults/retweet.rb +1 -1
- data/lib/plugins/defaults/standard_commands.rb +31 -25
- data/lib/plugins/defaults/stdout.rb +12 -4
- data/lib/plugins/defaults/system.rb +27 -0
- data/lib/plugins/dupu.rb +13 -0
- data/lib/plugins/erase.rb +4 -0
- data/lib/plugins/error_log.rb +17 -0
- data/lib/plugins/expand-tinyurl.rb +8 -1
- data/lib/plugins/growl.rb +10 -7
- data/lib/plugins/hatena_keyword_haiku.rb +88 -0
- data/lib/plugins/irc_gw.rb +12 -4
- data/lib/plugins/itunes.rb +29 -26
- data/lib/plugins/mecab.rb +23 -0
- data/lib/plugins/mudan_kinshi.rb +13 -0
- data/lib/plugins/ndkn.rb +7 -0
- data/lib/plugins/other_user.rb +47 -0
- data/lib/plugins/reply_sound.rb +75 -25
- data/lib/plugins/ruby-v.rb +10 -0
- data/lib/plugins/time_signal.rb +21 -0
- data/lib/plugins/tinyurl.rb +6 -4
- data/lib/plugins/train.rb +1 -1
- data/lib/plugins/translation.rb +2 -0
- data/lib/plugins/user_stream.rb +122 -0
- data/lib/plugins/whale.rb +28 -0
- data/lib/termtter/active_rubytter.rb +4 -0
- data/lib/termtter/api.rb +55 -30
- data/lib/termtter/client.rb +19 -4
- data/lib/termtter/config_setup.rb +10 -2
- data/lib/termtter/config_template.erb +4 -2
- data/lib/termtter/crypt.rb +13 -0
- data/lib/termtter/default_config.rb +5 -2
- data/lib/termtter/hookable.rb +4 -0
- data/lib/termtter/memory_cache.rb +67 -19
- data/lib/termtter/rubytter_proxy.rb +101 -26
- data/lib/termtter/system_extensions.rb +22 -18
- data/lib/termtter.rb +5 -1
- data/spec/termtter/crypt_spec.rb +16 -0
- data/spec/termtter/rubytter_proxy_spec.rb +14 -0
- 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,
|
7
|
+
config.set_default(:retry, 3)
|
7
8
|
config.set_default(:splash, <<SPLASH)
|
8
9
|
|
9
|
-
<cyan><(@)//_</cyan> . . <on_green> #{(Time.now.year ==
|
10
|
+
<cyan><(@)//_</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')
|
data/lib/termtter/hookable.rb
CHANGED
@@ -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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
14
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
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 =
|
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
|
-
|
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
|
-
|
94
|
-
|
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
|
-
|
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
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|