jugyo-termtter 0.7.6 → 0.7.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,298 @@
1
+ module Termtter
2
+ class CommandNotFound < StandardError; end
3
+
4
+ module Client
5
+
6
+ @@hooks = []
7
+ @@commands = {}
8
+ @@new_commands = {}
9
+ @@completions = []
10
+ @@filters = []
11
+ @@helps = []
12
+
13
+ class << self
14
+ def public_storage
15
+ @@public_storage ||= {}
16
+ end
17
+
18
+ %w[hook completion filter].each do |n|
19
+ eval <<-EOF
20
+ def add_#{n}(&b)
21
+ @@#{n}s << b
22
+ end
23
+ EOF
24
+ end
25
+
26
+ def add_command(regex, &block)
27
+ @@commands[regex] = block
28
+ end
29
+
30
+ def register_command(arg)
31
+ command = case arg
32
+ when Command
33
+ arg
34
+ when Hash
35
+ Command.new(arg)
36
+ else
37
+ raise ArgumentError, 'must be given Termtter::Command or Hash'
38
+ end
39
+ @@new_commands[command.name] = command
40
+ end
41
+
42
+ def get_command(name)
43
+ @@new_commands[name]
44
+ end
45
+
46
+ def add_macro(r, s)
47
+ add_command(r) do |m, t|
48
+ call_commands(s % m.to_a[1..-1])
49
+ end
50
+ end
51
+
52
+ def add_help(name, desc)
53
+ @@helps << [name, desc]
54
+ end
55
+
56
+ %w[hooks commands completions helps filters].each do |n|
57
+ eval <<-EOF
58
+ def clear_#{n}
59
+ @@#{n}.clear
60
+ end
61
+ EOF
62
+ end
63
+
64
+ # memo: each filter must return Array of Status
65
+ def apply_filters(statuses)
66
+ filtered = statuses.map{|s| s.dup }
67
+ @@filters.each do |f|
68
+ filtered = f.call(filtered)
69
+ end
70
+ filtered
71
+ rescue => e
72
+ handle_error(e)
73
+ statuses
74
+ end
75
+
76
+ def do_hooks(statuses, event)
77
+ @@hooks.each do |h|
78
+ begin
79
+ h.call(statuses.dup, event, Termtter::API.twitter)
80
+ rescue => e
81
+ handle_error(e)
82
+ end
83
+ end
84
+ end
85
+
86
+ # TODO: delete argument "tw" when unnecessary
87
+ def call_hooks(statuses, event, tw = nil)
88
+ do_hooks(statuses, :pre_filter)
89
+ do_hooks(apply_filters(statuses), event)
90
+ end
91
+
92
+ def call_commands(text, tw = nil)
93
+ return if text.empty?
94
+
95
+ command_found = false
96
+ @@commands.each do |key, command|
97
+ if key =~ text
98
+ command_found = true
99
+ begin
100
+ command.call($~, Termtter::API.twitter)
101
+ rescue => e
102
+ handle_error(e)
103
+ end
104
+ end
105
+ end
106
+
107
+ @@new_commands.each do |key, command|
108
+ command_info = command.match?(text)
109
+ # TODO: call hook for before command here.
110
+ if command_info
111
+ command_found = true
112
+ result = command.execute(command_info[1])
113
+ if result
114
+ # TODO: call hook for after command with result.
115
+ end
116
+ end
117
+ end
118
+
119
+ raise CommandNotFound unless command_found
120
+ end
121
+
122
+ def pause
123
+ @@pause = true
124
+ end
125
+
126
+ def resume
127
+ @@pause = false
128
+ @@update_thread.run
129
+ end
130
+
131
+ def exit
132
+ call_hooks([], :exit)
133
+ @@main_thread.kill
134
+ @@update_thread.kill
135
+ @@input_thread.kill
136
+ end
137
+
138
+ def load_default_plugins
139
+ plugin 'standard_plugins'
140
+ plugin 'stdout'
141
+ end
142
+
143
+ def load_config
144
+ conf_file = File.expand_path('~/.termtter')
145
+ if File.exist? conf_file
146
+ wrap_require do
147
+ load conf_file
148
+ end
149
+ else
150
+ HighLine.track_eof = false
151
+ ui = HighLine.new
152
+ username = ui.ask('your twitter username: ')
153
+ password = ui.ask('your twitter password: ') { |q| q.echo = false }
154
+
155
+ File.open(File.expand_path('~/.termtter'), 'w') {|io|
156
+ plugins = Dir.glob(File.dirname(__FILE__) + "/../lib/plugin/*.rb").map {|f|
157
+ f.match(%r|lib/plugin/(.*?).rb$|)[1]
158
+ }
159
+ plugins -= %w[stdout standard_plugins]
160
+ plugins.each do |p|
161
+ io.puts "#plugin '#{p}'"
162
+ end
163
+
164
+ io.puts
165
+ io.puts "configatron.user_name = '#{username}'"
166
+ io.puts "configatron.password = '#{password}'"
167
+ io.puts "#configatron.update_interval = 120"
168
+ io.puts "#configatron.proxy.host = 'proxy host'"
169
+ io.puts "#configatron.proxy.port = '8080'"
170
+ io.puts "#configatron.proxy.user_name = 'proxy user'"
171
+ io.puts "#configatron.proxy.password = 'proxy password'"
172
+ io.puts
173
+ io.puts "# vim: set filetype=ruby"
174
+ }
175
+ puts "generated: ~/.termtter"
176
+ puts "enjoy!"
177
+ wrap_require do
178
+ load conf_file
179
+ end
180
+ end
181
+ end
182
+
183
+ def setup_readline
184
+ Readline.basic_word_break_characters= "\t\n\"\\'`><=;|&{("
185
+ Readline.completion_proc = proc {|input|
186
+ begin
187
+ # FIXME: when migrate to Termtter::Command
188
+ completions = @@completions.map {|completion|
189
+ completion.call(input)
190
+ }
191
+ completions += @@new_commands.map {|name, command|
192
+ command.complement(input)
193
+ }
194
+ completions.flatten.compact
195
+ rescue => e
196
+ handle_error(e)
197
+ end
198
+ }
199
+ vi_or_emacs = configatron.editing_mode
200
+ unless vi_or_emacs.empty?
201
+ Readline.__send__("#{vi_or_emacs}_editing_mode")
202
+ end
203
+ end
204
+
205
+ def setup_api()
206
+ Termtter::API.setup()
207
+ end
208
+
209
+ def run
210
+ load_default_plugins()
211
+ load_config()
212
+ setup_readline()
213
+ setup_api()
214
+
215
+ puts 'initializing...'
216
+ initialized = false
217
+ @@pause = false
218
+ call_hooks([], :initialize)
219
+
220
+ @@input_thread = nil
221
+ @@update_thread = Thread.new do
222
+ since_id = nil
223
+ loop do
224
+ begin
225
+ Thread.stop if @@pause
226
+
227
+ statuses = Termtter::API.twitter.get_friends_timeline(since_id)
228
+ unless statuses.empty?
229
+ since_id = statuses[0].id
230
+ end
231
+ print "\e[1K\e[0G" if !statuses.empty? && !win?
232
+ call_hooks(statuses, :update_friends_timeline)
233
+ initialized = true
234
+ @@input_thread.kill if @@input_thread && !statuses.empty?
235
+ rescue OpenURI::HTTPError => e
236
+ if e.message == '401 Unauthorized'
237
+ puts 'Could not login'
238
+ puts 'plese check your account settings'
239
+ exit!
240
+ end
241
+ ensure
242
+ sleep configatron.update_interval
243
+ end
244
+ end
245
+ end
246
+
247
+ until initialized; end
248
+
249
+ begin
250
+ stty_save = `stty -g`.chomp
251
+ trap("INT") { system "stty", stty_save; exit }
252
+ rescue Errno::ENOENT
253
+ end
254
+
255
+ @@main_thread = Thread.new do
256
+ loop do
257
+ @@input_thread = create_input_thread()
258
+ @@input_thread.join
259
+ end
260
+ end
261
+ @@main_thread.join
262
+ end
263
+
264
+ def create_input_thread()
265
+ Thread.new do
266
+ erb = ERB.new(configatron.prompt)
267
+ while buf = Readline.readline(erb.result(Termtter::API.twitter.__send__(:binding)), true)
268
+ Readline::HISTORY.pop if /^(u|update)\s+(.+)$/ =~ buf
269
+ begin
270
+ call_commands(buf)
271
+ rescue CommandNotFound => e
272
+ puts "Unknown command \"#{buf}\""
273
+ puts 'Enter "help" for instructions'
274
+ end
275
+ end
276
+ exit # exit when press Control-D
277
+ end
278
+ end
279
+
280
+ def wrap_require
281
+ # FIXME: delete this method after the major version up
282
+ alias original_require require
283
+ def require(s)
284
+ if %r|^termtter/(.*)| =~ s
285
+ puts "[WARNING] use plugin '#{$1}' instead of require"
286
+ puts " Such a legacy .termtter file will not be supported until version 1.0.0"
287
+ s = "plugin/#{$1}"
288
+ end
289
+ original_require s
290
+ end
291
+ yield
292
+ alias require original_require
293
+ end
294
+ private :wrap_require
295
+ end
296
+ end
297
+ end
298
+
@@ -0,0 +1,67 @@
1
+ module Termtter
2
+ class Command
3
+ attr_accessor :name, :aliases, :exec_proc, :completion_proc, :help
4
+
5
+ # args
6
+ # name: Symbol as command name
7
+ # aliases: Array of command alias (ex. ['u', 'up'])
8
+ # exec_proc: Proc for procedure of the command. If need the proc must return object for hook.
9
+ # completion_proc: Proc for input completion. The proc must return Array of candidates (Optional)
10
+ # help: help text for the command (Optional)
11
+ def initialize(args)
12
+ raise ArgumentError, ":name is not given." unless args.has_key?(:name)
13
+ @name = args[:name].to_sym
14
+ @aliases = args[:aliases] || []
15
+ @exec_proc = args[:exec_proc] || proc {|arg|}
16
+ @completion_proc = args[:completion_proc] || proc {|command, arg| [] }
17
+ @help = args[:help]
18
+ end
19
+
20
+ def complement(input)
21
+ command_info = match?(input)
22
+ if command_info
23
+ [completion_proc.call(command_info[0], command_info[1])].flatten.compact
24
+ else
25
+ [name.to_s].grep(/^#{Regexp.quote(input)}/)
26
+ end
27
+ end
28
+
29
+ # MEMO: Termtter:Client からはこのメソッドを呼び出すことになると思う。
30
+ def exec_if_match(input)
31
+ command_info = match?(input)
32
+ if command_info
33
+ result = execute(command_info[1])
34
+ unless result.nil?
35
+ return result
36
+ else
37
+ return true
38
+ end
39
+ else
40
+ return nil
41
+ end
42
+ end
43
+
44
+ # return array like [command, arg]
45
+ def match?(input)
46
+ if pattern =~ input
47
+ [$2 || $3, $4] # $2 or $3 => command, $4 => argument
48
+ else
49
+ nil
50
+ end
51
+ end
52
+
53
+ def execute(arg)
54
+ exec_proc.call(arg)
55
+ end
56
+
57
+ def pattern
58
+ commands_regex = commands.map {|i| Regexp.quote(i) }.join('|')
59
+ /^\s*((#{commands_regex})|(#{commands_regex})\s+(.*?))\s*$/
60
+ end
61
+
62
+ def commands
63
+ aliases.unshift(name.to_s)
64
+ end
65
+ end
66
+ end
67
+
@@ -0,0 +1,37 @@
1
+ module Termtter
2
+ class Connection
3
+ attr_reader :protocol, :port, :proxy_uri
4
+
5
+ def initialize
6
+ @proxy_host = configatron.proxy.host
7
+ @proxy_port = configatron.proxy.port
8
+ @proxy_user = configatron.proxy.user_name
9
+ @proxy_password = configatron.proxy.password
10
+ @proxy_uri = nil
11
+ @enable_ssl = configatron.enable_ssl
12
+ @protocol = "http"
13
+ @port = 80
14
+
15
+ unless @proxy_host.empty?
16
+ @http_class = Net::HTTP::Proxy(@proxy_host, @proxy_port,
17
+ @proxy_user, @proxy_password)
18
+ @proxy_uri = "http://" + @proxy_host + ":" + @proxy_port + "/"
19
+ else
20
+ @http_class = Net::HTTP
21
+ end
22
+
23
+ if @enable_ssl
24
+ @protocol = "https"
25
+ @port = 443
26
+ end
27
+ end
28
+
29
+ def start(host, port, &block)
30
+ http = @http_class.new(host, port)
31
+ http.use_ssl = @enable_ssl
32
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE if http.use_ssl
33
+ http.start(&block)
34
+ end
35
+ end
36
+ end
37
+
@@ -0,0 +1,24 @@
1
+ module Termtter
2
+ class Status
3
+ %w(
4
+ id text created_at truncated
5
+ in_reply_to_status_id in_reply_to_user_id in_reply_to_screen_name
6
+ user_id user_name user_screen_name user_url user_profile_image_url
7
+ ).each do |attr|
8
+ attr_accessor attr.to_sym
9
+ end
10
+
11
+ def eql?(other); self.id == other.id end
12
+ def hash; self.id end
13
+
14
+ def english?
15
+ self.class.english?(self.text)
16
+ end
17
+
18
+ # english? :: String -> Boolean
19
+ def self.english?(message)
20
+ /[一-龠]+|[ぁ-ん]+|[ァ-ヴー]+|[a-zA-Z0-9]+/ !~ message
21
+ end
22
+ end
23
+ end
24
+
@@ -0,0 +1,138 @@
1
+ require 'highline'
2
+
3
+ module Termtter
4
+ class Twitter
5
+
6
+ def initialize(user_name, password, connection)
7
+ @user_name = user_name
8
+ @password = password
9
+ @connection = connection
10
+ end
11
+
12
+ def update_status(status)
13
+ @connection.start("twitter.com", @connection.port) do |http|
14
+ uri = '/statuses/update.xml'
15
+ http.request(post_request(uri), "status=#{CGI.escape(status)}&source=#{APP_NAME}")
16
+ end
17
+ status
18
+ end
19
+
20
+ def direct_message(user, status)
21
+ @connection.start("twitter.com", @connection.port) do |http|
22
+ uri = '/direct_messages/new.xml'
23
+ http.request(post_request(uri), "user=#{CGI.escape(user)}&text=#{CGI.escape(status)}&source=#{APP_NAME}")
24
+ end
25
+ [user, status]
26
+ end
27
+
28
+ def get_user_profile(screen_name)
29
+ uri = "#{@connection.protocol}://twitter.com/users/show/#{screen_name}.json"
30
+ return JSON.parse(open(uri, :http_basic_authentication => [user_name, password], :proxy => @connection.proxy_uri).read)
31
+ end
32
+
33
+ def get_friends_timeline(since_id = nil)
34
+ uri = "#{@connection.protocol}://twitter.com/statuses/friends_timeline.json"
35
+ uri << "?since_id=#{since_id}" if since_id
36
+ return get_timeline(uri)
37
+ end
38
+
39
+ def get_user_timeline(screen_name)
40
+ return get_timeline("#{@connection.protocol}://twitter.com/statuses/user_timeline/#{screen_name}.json")
41
+ rescue OpenURI::HTTPError => e
42
+ puts "No such user: #{screen_name}"
43
+ nears = near_users(screen_name)
44
+ puts "near users: #{nears}" unless nears.empty?
45
+ return []
46
+ end
47
+
48
+ def search(query)
49
+ results = JSON.parse(open("#{@connection.protocol}://search.twitter.com/search.json?q=" + CGI.escape(query)).read, :proxy => @connection.proxy_uri)['results']
50
+ return results.map do |s|
51
+ status = Status.new
52
+ status.id = s['id']
53
+ status.text = CGI.unescapeHTML(s['text']).gsub(/(\n|\r)/, '').gsub(query, color(color(query, 41), 37))
54
+ status.created_at = Time.utc(*ParseDate::parsedate(s["created_at"])).localtime
55
+ status.user_screen_name = s['from_user']
56
+ status
57
+ end
58
+ end
59
+
60
+ def show(id, rth = false)
61
+ get_status = lambda { get_timeline("#{@connection.protocol}://twitter.com/statuses/show/#{id}.json")[0] }
62
+ statuses = []
63
+ statuses << status = Array(Client.public_storage[:log]).detect(get_status) {|s| s.id == id.to_i }
64
+ statuses << show(id, true) if rth && id = status.in_reply_to_status_id
65
+ statuses.flatten
66
+ end
67
+
68
+ def replies
69
+ return get_timeline("#{@connection.protocol}://twitter.com/statuses/replies.json")
70
+ end
71
+
72
+ def get_timeline(uri)
73
+ data = JSON.parse(open(uri, :http_basic_authentication => [user_name, password], :proxy => @connection.proxy_uri).read)
74
+ data = [data] unless data.instance_of? Array
75
+ return data.map do |s|
76
+ status = Status.new
77
+ status.created_at = Time.utc(*ParseDate::parsedate(s["created_at"])).localtime
78
+ %w(id text truncated in_reply_to_status_id in_reply_to_user_id in_reply_to_screen_name).each do |key|
79
+ status.__send__("#{key}=".to_sym, s[key])
80
+ end
81
+ %w(id name screen_name url profile_image_url).each do |key|
82
+ status.__send__("user_#{key}=".to_sym, s["user"][key])
83
+ end
84
+ status.text = CGI.unescapeHTML(status.text).gsub(/(\n|\r)/, '')
85
+ status
86
+ end
87
+ end
88
+
89
+ # note: APILimit.reset_time_in_seconds == APILimit.reset_time.to_i
90
+ APILIMIT = Struct.new("APILimit", :reset_time, :reset_time_in_seconds, :remaining_hits, :hourly_limit)
91
+ def get_rate_limit_status
92
+ uri = 'http://twitter.com/account/rate_limit_status.json'
93
+ data = JSON.parse(open(uri, :http_basic_authentication => [user_name, password], :proxy => @connection.proxy_uri).read)
94
+
95
+ reset_time = Time.parse(data['reset_time'])
96
+ reset_time_in_seconds = data['reset_time_in_seconds'].to_i
97
+
98
+ APILIMIT.new(reset_time, reset_time_in_seconds, data['remaining_hits'], data['hourly_limit'])
99
+ end
100
+
101
+ alias :api_limit :get_rate_limit_status
102
+
103
+ private
104
+
105
+ def user_name
106
+ unless @user_name.instance_of? String
107
+ HighLine.track_eof = false
108
+ @user_name = HighLine.new.ask('your twitter username: ')
109
+ end
110
+ @user_name
111
+ end
112
+
113
+ def password
114
+ unless @password.instance_of? String
115
+ HighLine.track_eof = false
116
+ @password = HighLine.new.ask('your twitter password: ') { |q| q.echo = false }
117
+ end
118
+ @password
119
+ end
120
+
121
+ def near_users(screen_name)
122
+ Client::public_storage[:users].select {|user|
123
+ /#{user}/i =~ screen_name || /#{screen_name}/i =~ user
124
+ }.join(', ')
125
+ end
126
+
127
+ def post_request(uri)
128
+ req = Net::HTTP::Post.new(uri)
129
+ req.basic_auth(user_name, password)
130
+ req.add_field('User-Agent', 'Termtter http://github.com/jugyo/termtter')
131
+ req.add_field('X-Twitter-Client', 'Termtter')
132
+ req.add_field('X-Twitter-Client-URL', 'http://github.com/jugyo/termtter')
133
+ req.add_field('X-Twitter-Client-Version', '0.1')
134
+ req
135
+ end
136
+ end
137
+ end
138
+