jugyo-termtter 0.7.6 → 0.7.7

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