jugyo-termtter 0.1.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/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