jugyo-termtter 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ == 0.0.1 2008-12-26
2
+
3
+ * 1 major enhancement:
4
+ * Initial release
data/Manifest.txt ADDED
@@ -0,0 +1,11 @@
1
+ History.txt
2
+ Manifest.txt
3
+ PostInstall.txt
4
+ README.rdoc
5
+ Rakefile
6
+ bin/termtter
7
+ lib/termtter.rb
8
+ lib/termtter/stdout.rb
9
+ lib/termtter/notify-send.rb
10
+ test/test_termtter.rb
11
+
data/PostInstall.txt ADDED
@@ -0,0 +1 @@
1
+
data/README.rdoc ADDED
@@ -0,0 +1,49 @@
1
+ = termtter
2
+
3
+ http://github.com/jugyo/termtter
4
+
5
+ == DESCRIPTION:
6
+
7
+ Termtter is a terminal based Twitter client
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ == SYNOPSIS:
12
+
13
+ Run:
14
+
15
+ termtter
16
+
17
+ == REQUIREMENTS:
18
+
19
+ * nokogiri
20
+ * configatron
21
+
22
+ == INSTALL:
23
+
24
+ sudo gem install jugyo-termtter
25
+
26
+ == LICENSE:
27
+
28
+ (The MIT License)
29
+
30
+ Copyright (c) 2008 FIXME full name
31
+
32
+ Permission is hereby granted, free of charge, to any person obtaining
33
+ a copy of this software and associated documentation files (the
34
+ 'Software'), to deal in the Software without restriction, including
35
+ without limitation the rights to use, copy, modify, merge, publish,
36
+ distribute, sublicense, and/or sell copies of the Software, and to
37
+ permit persons to whom the Software is furnished to do so, subject to
38
+ the following conditions:
39
+
40
+ The above copyright notice and this permission notice shall be
41
+ included in all copies or substantial portions of the Software.
42
+
43
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
44
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
45
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
46
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
47
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
48
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
49
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,29 @@
1
+ %w[rubygems rake rake/clean fileutils newgem rubigen].each { |f| require f }
2
+ require File.dirname(__FILE__) + '/lib/termtter'
3
+
4
+ # Generate all the Rake tasks
5
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
6
+ $hoe = Hoe.new('termtter', Termtter::VERSION) do |p|
7
+ p.developer('jugyo', 'jugyo.org@gmail.com')
8
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
9
+ p.post_install_message = 'PostInstall.txt' # TODO remove if post-install message not required
10
+ p.rubyforge_name = p.name # TODO this is default value
11
+ p.extra_deps = [
12
+ ['nokogiri'],
13
+ ['configatron'],
14
+ ]
15
+ #p.extra_dev_deps = [
16
+ # ['newgem', ">= #{::Newgem::VERSION}"]
17
+ #]
18
+
19
+ p.clean_globs |= %w[**/.DS_Store tmp *.log]
20
+ path = (p.rubyforge_name == p.name) ? p.rubyforge_name : "\#{p.rubyforge_name}/\#{p.name}"
21
+ p.remote_rdoc_dir = File.join(path.gsub(/^#{p.rubyforge_name}\/?/,''), 'rdoc')
22
+ p.rsync_args = '-av --delete --ignore-errors'
23
+ end
24
+
25
+ require 'newgem/tasks' # load /tasks/*.rake
26
+ Dir['tasks/**/*.rake'].each { |t| load t }
27
+
28
+ # TODO - want other tests/tasks run by default? Add them to the list
29
+ # task :default => [:spec, :features]
data/bin/termtter ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $KCODE = 'u'
4
+
5
+ $:.unshift(File.dirname(__FILE__) + "/../lib")
6
+
7
+ require 'termtter'
8
+ require 'termtter/stdout'
9
+ require 'configatron'
10
+
11
+ conf_file = File.expand_path('~/.termtter')
12
+ if File.exist? conf_file
13
+ load conf_file
14
+ else
15
+ puts <<EOS
16
+ The configuration file does not exist.
17
+ Example:
18
+ # ~/.termtter
19
+ configatron.user_name = USERNAME
20
+ configatron.password = PASSWORD
21
+ EOS
22
+ exit 1
23
+ end
24
+
25
+ Termtter::Client.new.run
26
+
data/lib/termtter.rb ADDED
@@ -0,0 +1,212 @@
1
+ require 'rubygems'
2
+ require 'nokogiri'
3
+ require 'open-uri'
4
+ require 'cgi'
5
+ require 'readline'
6
+ require 'enumerator'
7
+ require 'parsedate'
8
+
9
+ $:.unshift(File.dirname(__FILE__)) unless
10
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
11
+
12
+ module Termtter
13
+ VERSION = '0.1.0'
14
+
15
+ class Client
16
+
17
+ @@hooks = []
18
+
19
+ def self.add_hook(&hook)
20
+ @@hooks << hook
21
+ end
22
+
23
+ def self.clear_hook
24
+ @@hooks.clear
25
+ end
26
+
27
+ attr_reader :since_id
28
+
29
+ def initialize
30
+ configatron.set_default(:update_interval, 300)
31
+ configatron.set_default(:debug, false)
32
+ @user_name = configatron.user_name
33
+ @password = configatron.password
34
+ @update_interval = configatron.update_interval
35
+ @debug = configatron.debug
36
+ end
37
+
38
+ def update_status(status)
39
+ req = Net::HTTP::Post.new('/statuses/update.xml')
40
+ req.basic_auth(@user_name, @password)
41
+ req.add_field("User-Agent", "Termtter http://github.com/jugyo/termtter")
42
+ req.add_field("X-Twitter-Client", "Termtter")
43
+ req.add_field("X-Twitter-Client-URL", "http://github.com/jugyo/termtter")
44
+ req.add_field("X-Twitter-Client-Version", "0.1")
45
+ Net::HTTP.start("twitter.com", 80) do |http|
46
+ http.request(req, "status=#{CGI.escape(status)}")
47
+ end
48
+ end
49
+
50
+ def list_friends_timeline
51
+ statuses = get_timeline("http://twitter.com/statuses/friends_timeline.xml")
52
+ call_hooks(statuses, :list_friends_timeline)
53
+ end
54
+
55
+ def update_friends_timeline
56
+ uri = "http://twitter.com/statuses/friends_timeline.xml"
57
+ if @since_id && !@since_id.empty?
58
+ uri += "?since_id=#{@since_id}"
59
+ end
60
+
61
+ statuses = get_timeline(uri, true)
62
+ call_hooks(statuses, :update_friends_timeline)
63
+ end
64
+
65
+ def get_user_timeline(screen_name)
66
+ statuses = get_timeline("http://twitter.com/statuses/user_timeline/#{screen_name}.xml")
67
+ call_hooks(statuses, :list_user_timeline)
68
+ end
69
+
70
+ def search(query)
71
+ doc = Nokogiri::XML(open('http://search.twitter.com/search.atom?q=' + CGI.escape(query)))
72
+
73
+ statuses = []
74
+ ns = {'atom' => 'http://www.w3.org/2005/Atom'}
75
+ doc.xpath('//atom:entry', ns).each do |node|
76
+ status = {}
77
+ published = node.xpath('atom:published', ns).text
78
+ status['created_at'] = Time.utc(*ParseDate::parsedate(published)).localtime
79
+ status['text'] = CGI.unescapeHTML(node.xpath('atom:content', ns).text.gsub(/<\/?[^>]*>/, ''))
80
+ name = node.xpath('atom:author/atom:name', ns).text
81
+ status['user/screen_name'] = name.scan(/^([^\s]+) /).flatten[0]
82
+ status['user/name'] = name.scan(/\((.*)\)/).flatten[0]
83
+ statuses << status
84
+ end
85
+
86
+ call_hooks(statuses, :search)
87
+ return statuses
88
+ end
89
+
90
+ def show(id)
91
+ statuses = get_timeline("http://twitter.com/statuses/show/#{id}.xml")
92
+ call_hooks(statuses, :show)
93
+ end
94
+
95
+ def replies
96
+ statuses = get_timeline("http://twitter.com/statuses/replies.xml")
97
+ call_hooks(statuses, :show)
98
+ end
99
+
100
+ def call_hooks(statuses, event)
101
+ @@hooks.each do |h|
102
+ h.call(statuses.dup, event)
103
+ end
104
+ end
105
+
106
+ def get_timeline(uri, update_since_id = false)
107
+ doc = Nokogiri::XML(open(uri, :http_basic_authentication => [@user_name, @password]))
108
+
109
+ statuses = []
110
+ doc.xpath('//status').each do |s|
111
+ status = {}
112
+ %w(
113
+ id text created_at truncated in_reply_to_status_id in_reply_to_user_id
114
+ user/id user/name user/screen_name
115
+ ).each do |key|
116
+ status[key] = CGI.unescapeHTML(s.xpath(key).text)
117
+ end
118
+ status['created_at'] = Time.utc(*ParseDate::parsedate(status['created_at'])).localtime
119
+ statuses << status
120
+ end
121
+
122
+ if update_since_id && !statuses.empty?
123
+ @since_id = statuses[0]['id']
124
+ end
125
+
126
+ return statuses
127
+ end
128
+
129
+ def run
130
+ pause = false
131
+
132
+ update = Thread.new do
133
+ while true
134
+ if pause
135
+ Thread.stop
136
+ end
137
+ update_friends_timeline()
138
+ sleep @update_interval
139
+ end
140
+ end
141
+
142
+ input = Thread.new do
143
+ while buf = Readline.readline("", true)
144
+ begin
145
+ case buf
146
+ when ''
147
+ # do nothing
148
+ when 'debug'
149
+ if @debug
150
+ update_friends_timeline()
151
+ end
152
+ when /^(post|p)\s+(.*)/, /^(update|u)\s+(.*)/
153
+ unless $2.empty?
154
+ update_status($2)
155
+ puts "=> #{$2}"
156
+ end
157
+ when /^(list|l)\s*$/
158
+ list_friends_timeline()
159
+ when /^(list|l)\s+([^\s]+)/
160
+ get_user_timeline($2)
161
+ when /^(search|s)\s+(.*)/
162
+ unless $2.empty?
163
+ search($2)
164
+ end
165
+ when /^(replies|r)\s*$/
166
+ replies()
167
+ when /^show\s+([^\s]+)/
168
+ show($1)
169
+ when /^pause\s*$/
170
+ pause = true
171
+ when /^resume\s*$/
172
+ pause = false
173
+ update.run
174
+ when /^exit\s*$/
175
+ update.kill
176
+ input.kill
177
+ when /^help\s*$/
178
+ puts <<-EOS
179
+ exit Exit
180
+ help Print this help message
181
+ list List the posts in your friends timeline
182
+ list USERNAME List the posts in the the given user's timeline
183
+ pause Pause updating
184
+ post TEXT Post a new message
185
+ resume Resume updating
186
+ replies List the most recent @replies for the authenticating user
187
+ search TEXT Search for Twitter
188
+ show ID Show a single status
189
+ update TEXT Update friends timeline
190
+ EOS
191
+ else
192
+ puts <<-EOS
193
+ Unknown command "#{buf}"
194
+ Enter "help" for instructions
195
+ EOS
196
+ end
197
+ rescue => e
198
+ puts "Error: #{e}"
199
+ puts e.backtrace.join("\n") if @debug
200
+ end
201
+ end
202
+ end
203
+
204
+ stty_save = `stty -g`.chomp
205
+ trap("INT") { system "stty", stty_save; exit }
206
+
207
+ input.join
208
+ end
209
+
210
+ end
211
+ end
212
+
@@ -0,0 +1,15 @@
1
+ Termtter::Client.add_hook do |statuses, event|
2
+ if !statuses.empty? && event == :update_friends_timeline
3
+ max = 10
4
+
5
+ text = statuses[0..(max - 1)].map{|s|
6
+ status_text = s['text'].gsub(%r{https?://[-_.!~*\'()a-zA-Z0-9;\/?:\@&=+\$,%#]+},'<a href="\0">\0</a>')
7
+ "<b>#{s['user/screen_name']}</b> <span font=\"9.0\">#{status_text}</span>"
8
+ }.join("\n")
9
+
10
+ text += "\n<a href=\"http://twitter.com/\">more…</a>" if statuses.size > max
11
+
12
+ system 'notify-send', 'Termtter', text, '-t', '60000'
13
+ end
14
+ end
15
+
@@ -0,0 +1,42 @@
1
+ def color(str, num)
2
+ "\e[#{num}m#{str}\e[0m"
3
+ end
4
+
5
+ Termtter::Client.add_hook do |statuses, event|
6
+ colors = %w(0 31 32 33 34 35 36 91 92 93 94 95 96)
7
+
8
+ case event
9
+ when :update_friends_timeline, :list_friends_timeline, :list_user_timeline, :show
10
+ unless statuses.empty?
11
+ if event == :update_friends_timeline then statuses = statuses.reverse end
12
+ statuses.each do |s|
13
+ text = s['text'].gsub("\n", '')
14
+ color_num = colors[s['user/screen_name'].hash % colors.size]
15
+ status = "#{s['user/screen_name']}: #{text}"
16
+ unless s['in_reply_to_status_id'].empty?
17
+ status += " (reply to #{s['in_reply_to_status_id']})"
18
+ end
19
+
20
+ case event
21
+ when :update_friends_timeline, :list_friends_timeline
22
+ time_format = '%H:%d:%S'
23
+ else
24
+ time_format = '%m-%d %H:%d'
25
+ end
26
+ time_str = "(#{s['created_at'].strftime(time_format)})"
27
+
28
+ puts "#{color(time_str, 90)} #{color(status, color_num)}"
29
+ end
30
+ end
31
+ when :search
32
+ statuses.each do |s|
33
+ text = s['text'].gsub("\n", '')
34
+ color_num = colors[s['user/screen_name'].hash % colors.size]
35
+ status = "#{s['user/screen_name']}: #{text}"
36
+ time_str = "(#{s['created_at'].strftime('%m-%d %H:%d')})"
37
+
38
+ puts "#{color(time_str, 90)} #{color(status, color_num)}"
39
+ end
40
+ end
41
+ end
42
+
@@ -0,0 +1,83 @@
1
+ require 'rubygems'
2
+ require 'configatron'
3
+ require 'test/unit'
4
+ require File.dirname(__FILE__) + '/../lib/termtter'
5
+
6
+ class TestTermtter < Test::Unit::TestCase
7
+ def setup
8
+ configatron.user_name = 'test'
9
+ configatron.password = 'test'
10
+ @termtter = Termtter::Client.new
11
+
12
+ Termtter::Client.add_hook do |statuses, event|
13
+ @statuses = statuses
14
+ @event = event
15
+ end
16
+ end
17
+
18
+ def test_get_timeline
19
+ def @termtter.open(*arg)
20
+ return File.open(File.dirname(__FILE__) + '/../test/friends_timeline.xml')
21
+ end
22
+
23
+ statuses = @termtter.get_timeline('')
24
+
25
+ assert_equal 3, statuses.size
26
+ assert_equal '102', statuses[0]['user/id']
27
+ assert_equal 'test2', statuses[0]['user/screen_name']
28
+ assert_equal 'Test User 2', statuses[0]['user/name']
29
+ assert_equal 'texttext 2', statuses[0]['text']
30
+ assert_equal 'Thu Dec 25 22:19:57 +0900 2008', statuses[0]['created_at'].to_s
31
+ assert_equal '100', statuses[2]['user/id']
32
+ assert_equal 'test0', statuses[2]['user/screen_name']
33
+ assert_equal 'Test User 0', statuses[2]['user/name']
34
+ assert_equal 'texttext 0', statuses[2]['text']
35
+ assert_equal 'Thu Dec 25 22:10:57 +0900 2008', statuses[2]['created_at'].to_s
36
+ end
37
+
38
+ def test_get_timeline_with_update_since_id
39
+ def @termtter.open(*arg)
40
+ return File.open(File.dirname(__FILE__) + '/../test/friends_timeline.xml')
41
+ end
42
+
43
+ statuses = @termtter.get_timeline('', true)
44
+ assert_equal '10002', @termtter.since_id
45
+ end
46
+
47
+ def test_search
48
+ def @termtter.open(*arg)
49
+ return File.open(File.dirname(__FILE__) + '/../test/search.atom')
50
+ end
51
+
52
+ statuses = @termtter.search('')
53
+ assert_equal 3, statuses.size
54
+ assert_equal 'test2', statuses[0]['user/screen_name']
55
+ assert_equal 'Test User 2', statuses[0]['user/name']
56
+ assert_equal 'texttext 2', statuses[0]['text']
57
+ assert_equal 'Thu Dec 25 22:52:36 +0900 2008', statuses[0]['created_at'].to_s
58
+ assert_equal 'test0', statuses[2]['user/screen_name']
59
+ assert_equal 'Test User 0', statuses[2]['user/name']
60
+ assert_equal 'texttext 0', statuses[2]['text']
61
+ assert_equal 'Thu Dec 25 22:42:36 +0900 2008', statuses[2]['created_at'].to_s
62
+ end
63
+
64
+ def test_add_hook
65
+ def @termtter.open(*arg)
66
+ return File.open(File.dirname(__FILE__) + '/../test/search.atom')
67
+ end
68
+ call_hook = false
69
+ Termtter::Client.add_hook do |statuses, event|
70
+ call_hook = true
71
+ end
72
+ @termtter.search('')
73
+
74
+ assert_equal true, call_hook
75
+
76
+ Termtter::Client.clear_hook()
77
+ call_hook = false
78
+ @termtter.search('')
79
+
80
+ assert_equal false, call_hook
81
+ end
82
+ end
83
+
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jugyo-termtter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - jugyo
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-12-26 00:00:00 -08:00
13
+ default_executable: termtter
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: nokogiri
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: "0"
23
+ version:
24
+ - !ruby/object:Gem::Dependency
25
+ name: configatron
26
+ version_requirement:
27
+ version_requirements: !ruby/object:Gem::Requirement
28
+ requirements:
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ version: "0"
32
+ version:
33
+ - !ruby/object:Gem::Dependency
34
+ name: hoe
35
+ version_requirement:
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 1.8.0
41
+ version:
42
+ description: Termtter is a terminal based Twitter client
43
+ email:
44
+ - jugyo.org@gmail.com
45
+ executables:
46
+ - termtter
47
+ extensions: []
48
+
49
+ extra_rdoc_files:
50
+ - History.txt
51
+ - Manifest.txt
52
+ - PostInstall.txt
53
+ - README.rdoc
54
+ files:
55
+ - History.txt
56
+ - Manifest.txt
57
+ - PostInstall.txt
58
+ - README.rdoc
59
+ - Rakefile
60
+ - bin/termtter
61
+ - lib/termtter.rb
62
+ - lib/termtter/stdout.rb
63
+ - lib/termtter/notify-send.rb
64
+ - test/test_termtter.rb
65
+ has_rdoc: true
66
+ homepage: http://github.com/jugyo/termtter
67
+ post_install_message: PostInstall.txt
68
+ rdoc_options:
69
+ - --main
70
+ - README.rdoc
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: "0"
78
+ version:
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: "0"
84
+ version:
85
+ requirements: []
86
+
87
+ rubyforge_project: termtter
88
+ rubygems_version: 1.2.0
89
+ signing_key:
90
+ specification_version: 2
91
+ summary: Termtter is a terminal based Twitter client
92
+ test_files:
93
+ - test/test_termtter.rb