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.
- 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
|