tit 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ pkg
@@ -0,0 +1,7 @@
1
+ # tit 1.0.0 2010-01-22
2
+
3
+ * can send and receive tits I mean tweets
4
+ * can do polling
5
+ * notifications
6
+ * geotagging (read and write)
7
+ * doesn't yet make twitter not stupid
data/LICENSE ADDED
@@ -0,0 +1,3 @@
1
+ do what you want 'cause a pirate is free
2
+
3
+ you are a pirate
data/README.markdown ADDED
@@ -0,0 +1,43 @@
1
+ tit
2
+ ===
3
+
4
+ a rather spiteful stupid fucking twitter client for you and your stupid fucking
5
+ friends
6
+
7
+ install
8
+ -------
9
+
10
+ $ (sudo) gem install tit
11
+
12
+ usage
13
+ -----
14
+
15
+ $ tit
16
+
17
+ or
18
+
19
+ $ tit -u "look at my stupid fucking tweet" -G 88.918:-34.879
20
+
21
+ or
22
+
23
+ $ tit -P -n
24
+
25
+ dependencies
26
+ ------------
27
+
28
+ [rest-client][] and [nokogiri][]
29
+
30
+ caveats
31
+ -------
32
+
33
+ geotags are of the form `LAT:LONG`, where `LAT` and `LONG` are floating point
34
+ numbers, indicating, respectively, the number of degrees north or east of the
35
+ center of the earth; make them negative if you live on one or both of the dark
36
+ hemispheres
37
+
38
+ bugs
39
+ ----
40
+
41
+ yeah probably; some of the code is super fucking confusing so you might have
42
+ trouble fixing them but it doesn't do much so I don't think there are many
43
+ issues
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ begin
2
+ require 'jeweler'
3
+ Jeweler::Tasks.new do |gemspec|
4
+ gemspec.name = "tit"
5
+ gemspec.summary = "stupid fucking twitter client"
6
+ gemspec.description = "a stupid fucking twitter client"
7
+ gemspec.email = "leif.walsh@gmail.com"
8
+ gemspec.homepage = "http://github.com/adlaiff6/tit"
9
+ gemspec.authors = ["Leif Walsh"]
10
+ gemspec.add_dependency('nokogiri', '>= 1.4.0')
11
+ gemspec.add_dependency('rest-client', '>= 1.2.0')
12
+ end
13
+ Jeweler::GemcutterTasks.new
14
+ rescue LoadError
15
+ puts "Jeweler not available. Install it with: gem install jeweler"
16
+ end
data/VERSION.yml ADDED
@@ -0,0 +1,5 @@
1
+ ---
2
+ :minor: 0
3
+ :patch: 0
4
+ :major: 1
5
+ :build:
data/bin/tit ADDED
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'tit'
4
+
5
+ def main
6
+ options = {
7
+ :debug => false,
8
+ :action => :home,
9
+ :wait => nil,
10
+ :payload => nil,
11
+ :notify => nil
12
+ }
13
+
14
+ tit = Tit.new
15
+
16
+ tit.opts = OptionParser.new do |opts|
17
+ opts.banner = "Usage: #{File.basename($0)} " +
18
+ "[options] [action [action options]]"
19
+
20
+ opts.separator ""
21
+ opts.separator "Actions:"
22
+
23
+ opts.on("-p", "--public", "Show public timeline") do
24
+ options[:action] = :public
25
+ end
26
+ opts.on("-H", "--home", "Show home timeline (default)") do
27
+ options[:action] = :home
28
+ end
29
+ opts.on("-m", "--mentions", "Show mentions timeline") do
30
+ options[:action] = :mentions
31
+ end
32
+ opts.on("-u", "--update [STATUS]",
33
+ "Update status (read from STDIN if none given)") do |status|
34
+ options[:action] = :update
35
+ options[:payload] ||= {}
36
+ options[:payload]["status"] = status || STDIN
37
+ end
38
+
39
+ opts.separator ""
40
+ opts.separator "Receive options (public/home/mentions):"
41
+
42
+ opts.on("-P", "--poll [N]",
43
+ "Poll for more updates every N secs (default 180)") do |secs|
44
+ options[:wait] = secs || '180'
45
+ options[:wait] = options[:wait].to_i
46
+ options[:wait] = 30 if options[:wait] < 30
47
+ end
48
+
49
+ opts.separator ""
50
+ opts.separator "Poll options:"
51
+
52
+ opts.on("-n", "--notify [PROG]",
53
+ "Send notifications using PROG (default: notify-send)") do |prog|
54
+ options[:notify] = prog || "notify-send"
55
+ end
56
+
57
+ opts.separator ""
58
+ opts.separator "Update options:"
59
+
60
+ opts.on("-G", "--geo LAT:LONG",
61
+ "Set latitude and longitude for update") do |s|
62
+ sp = s.split(/:/)
63
+
64
+ tit.abort("invalid geotag format: #{s}", opts) unless sp.length == 2
65
+
66
+ options[:payload] ||= {}
67
+ options[:payload]["lat"] = sp[0]
68
+ options[:payload]["long"] = sp[1]
69
+ end
70
+
71
+ opts.separator ""
72
+ opts.separator "Common options:"
73
+
74
+ opts.on("-d", "--debug", "Show debugging information") do
75
+ options[:debug] = true
76
+ end
77
+ opts.on_tail("-h", "--help", "Show this message") do
78
+ puts opts
79
+ exit
80
+ end
81
+ end
82
+
83
+ begin
84
+ tit.opts.parse!
85
+ rescue OptionParser::InvalidOption => e
86
+ tit.abort(e.message)
87
+ end
88
+
89
+ # check for option errors
90
+ if Tit::READERS.include? options[:action]
91
+ tit.abort("cannot provide geotag when reading") unless options[:payload].nil?
92
+ tit.abort("cannot notify unless polling") if (options[:wait].nil? and
93
+ not options[:notify].nil?)
94
+ end
95
+ if options[:action] == :update
96
+ tit.abort("need status message") unless options[:payload].include? "status"
97
+ tit.abort("can't repeatedly update status") unless options[:wait].nil?
98
+ tit.abort("can't notify when updating status") unless options[:notify].nil?
99
+ end
100
+
101
+ # do it
102
+ if options[:debug]
103
+ begin
104
+ tit.run(options)
105
+ rescue SocketError, Errno::ENETUNREACH, Errno::ETIMEDOUT, RestClient::Exception => e
106
+ tit.error "got a networking error, are you connected to the intarbutts?"
107
+ puts e
108
+ exit(-1)
109
+ end
110
+ else
111
+ begin
112
+ tit.run(options)
113
+ rescue SocketError, Errno::ENETUNREACH, Errno::ETIMEDOUT, RestClient::Exception => e
114
+ tit.error "got a networking error, are you connected to the intarbutts?"
115
+ puts e
116
+ exit(-1)
117
+ rescue => e
118
+ tit.error "unknown error"
119
+ puts e
120
+ exit(-1)
121
+ end
122
+ end
123
+ end
124
+
125
+ begin
126
+ main
127
+ rescue Interrupt
128
+ puts ""
129
+ exit(130)
130
+ end
data/lib/tit.rb ADDED
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'ftools'
5
+ require 'nokogiri'
6
+ require 'optparse'
7
+ require 'rest_client'
8
+ require 'time' # heh.
9
+ require 'yaml'
10
+
11
+ class String
12
+ def wrapped(cols)
13
+ curlen = 0
14
+ split.inject([[]]) do |rows, word|
15
+ if curlen + word.length > cols
16
+ curlen = word.length + 1
17
+ rows << [word]
18
+ else
19
+ curlen += word.length + 1
20
+ rows << (rows.pop << word)
21
+ end
22
+ end.map { |row| row.join(' ') }
23
+ end
24
+ end
25
+
26
+ class Time
27
+ def time_ago_in_words
28
+ t = Time.now
29
+ secs = t - self
30
+ minutes = secs / 60
31
+ hours = minutes / 60
32
+ days = hours / 24
33
+ if hours <= 12 # show a fuzzy time
34
+ if hours > 2
35
+ "#{hours.to_i} hours ago"
36
+ elsif hours > 1
37
+ "about an hour ago"
38
+ elsif minutes > 2
39
+ "#{minutes.to_i} minutes ago"
40
+ elsif minutes > 1
41
+ "about a minute ago"
42
+ else
43
+ "just now"
44
+ end
45
+ elsif days <= 15 # show time ago in days, with time if it was yesterday or today
46
+ if t.day == day
47
+ "today at #{strftime("%X")}"
48
+ elsif t.day - day == 1
49
+ "yesterday at #{strftime("%X")}"
50
+ else
51
+ "#{days.to_i} days ago"
52
+ end
53
+ else # time in months or years
54
+ months = t.month - month + (t.year - year) * 12
55
+ if months <= 6
56
+ if months == 1
57
+ "last month"
58
+ elsif t.year == year
59
+ "this #{strftime("%B")}"
60
+ else
61
+ "last #{strftime("%B")}"
62
+ end
63
+ else
64
+ if t.year - year == 1
65
+ "last year"
66
+ else
67
+ "#{t.year - year} years ago"
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ =begin rdoc
75
+ Why are you reading the documentation, you cunt?
76
+ =end
77
+ class Tit
78
+ RCFILE = File.join(ENV["HOME"], ".titrc")
79
+
80
+ READERS = [:public, :home, :mentions]
81
+ WRITERS = [:update]
82
+
83
+ URLS = {
84
+ :public => "statuses/public_timeline.xml",
85
+ :home => "statuses/home_timeline.xml",
86
+ :mentions => "statuses/mentions.xml",
87
+ :update => "statuses/update.xml"
88
+ }
89
+
90
+ def initialize
91
+ @username = nil
92
+ @password = nil
93
+ begin
94
+ File.open(RCFILE, "r") do |rc|
95
+ data = YAML.load(rc)
96
+ @username = data["username"]
97
+ @password = data["password"]
98
+ end
99
+ rescue Errno::ENOENT => e
100
+ File.open(RCFILE, "w") do |rc|
101
+ YAML.dump({
102
+ "username" => "<username>",
103
+ "password" => "<password>"
104
+ }, rc)
105
+ end
106
+ end
107
+
108
+ if @username.nil? or @username.eql? "<username>" or
109
+ @password.nil? or @password.eql? "<password>"
110
+ puts "Please fill in your username and password in #{RCFILE}"
111
+ exit(-1)
112
+ end
113
+
114
+ # set up proxy
115
+ RestClient.proxy = ENV['https_proxy']
116
+
117
+ # get terminal width
118
+ @cols = %x[tput cols].to_i
119
+ end
120
+ attr_accessor :opts
121
+
122
+ def resource
123
+ RestClient::Resource.new("https://#{@username}:#{@password}@twitter.com/")
124
+ end
125
+
126
+ def get_tits(action)
127
+ Nokogiri.XML(resource[URLS[action]].get).xpath("//status").map do |xml|
128
+ {
129
+ :username => xml.at_xpath("./user/name").content,
130
+ :userid => xml.at_xpath("./user/screen_name").content,
131
+ :text => xml.xpath("./text").map { |n| n.content },
132
+ :timestamp => Time.parse(xml.at_xpath("./created_at").content),
133
+ :id => xml.at_xpath("./id").content.to_i,
134
+ :geo => xml.at_xpath("./geo").instance_eval do
135
+ unless children.empty?
136
+ n, e = children[1].content.split.map { |s| s.to_f }
137
+ "#{n.abs}#{n >= 0 ? 'N' : 'S'} #{e.abs}#{e >= 0 ? 'E' : 'W'}"
138
+ end
139
+ end
140
+ }
141
+ end
142
+ end
143
+
144
+ def update(payload)
145
+ if payload["status"] == STDIN
146
+ payload["status"] = STDIN.read
147
+ end
148
+
149
+ if payload["status"].length > 140
150
+ puts "your status is too long (by #{payload["status"].length - 140} characters)"
151
+ puts "here is what would get posted:"
152
+ payload["status"][0...140].wrapped(@cols - 2).each { |l| puts " #{l}" }
153
+ exit(-1)
154
+ end
155
+
156
+ resource[URLS[:update]].post(payload)
157
+ end
158
+
159
+ def show_tit(status)
160
+ person = if status[:userid].eql? @username
161
+ "you"
162
+ else
163
+ "#{status[:username]} (#{status[:userid]})"
164
+ end
165
+ at = status[:timestamp].time_ago_in_words
166
+ if status[:geo].nil?
167
+ puts "#{person} said, #{at}:"
168
+ else
169
+ puts "#{person} said, #{at}, from #{status[:geo]}:"
170
+ end
171
+
172
+ status[:text].each do |line|
173
+ line.wrapped(@cols - 2).each { |l| puts " #{l}" }
174
+ end
175
+ puts ""
176
+ end
177
+
178
+ def poll(wait, action, notify)
179
+ tits = {}
180
+ get_tits(action).reverse.each do |status|
181
+ show_tit(status)
182
+ tits[status[:id]] = status
183
+ end
184
+ last_update = Time.now()
185
+ loop do
186
+ print "\r", " " * (s = "Last update was at #{last_update.strftime "%X"}, " +
187
+ "next update at #{(last_update + wait).strftime "%X"}"
188
+ print s
189
+ STDOUT.flush
190
+ sleep(wait)
191
+ s.length), "\r"
192
+ begin
193
+ num_tits = get_tits(action).reverse.reject do |status|
194
+ tits.include? status[:id]
195
+ end.each_with_index do |status, i|
196
+ if i == 0
197
+ puts "more updates (at #{Time.now.strftime "%X"}):\n"
198
+ end
199
+ show_tit(status)
200
+ tits[status[:id]] = status
201
+ end.length
202
+ %x[#{notify} '#{num_tits} new tit#{num_tits == 1 ? '' : 's'}!'] unless notify.nil? or num_tits == 0
203
+ last_update = Time.now()
204
+ rescue SocketError, Errno::ENETUNREACH, Errno::ETIMEDOUT, RestClient::Exception => e
205
+ puts "networking error, will try again later"
206
+ end
207
+ end
208
+ end
209
+
210
+ def run(options)
211
+ if READERS.include? options[:action]
212
+ if options[:wait].nil?
213
+ get_tits(options[:action]).reverse.each &method(:show_tit)
214
+ else
215
+ poll(options[:wait], options[:action], options[:notify])
216
+ end
217
+ elsif options[:action] == :update
218
+ update options[:payload]
219
+ end
220
+ end
221
+
222
+ def error msg
223
+ puts "#{File.basename $0}: #{msg}"
224
+ end
225
+
226
+ def abort msg
227
+ error(msg)
228
+ puts @opts
229
+ exit(-1)
230
+ end
231
+ end
data/tit.gemspec ADDED
@@ -0,0 +1,55 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{tit}
8
+ s.version = "1.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Leif Walsh"]
12
+ s.date = %q{2010-01-22}
13
+ s.default_executable = %q{tit}
14
+ s.description = %q{a stupid fucking twitter client}
15
+ s.email = %q{leif.walsh@gmail.com}
16
+ s.executables = ["tit"]
17
+ s.extra_rdoc_files = [
18
+ "ChangeLog.markdown",
19
+ "LICENSE",
20
+ "README.markdown"
21
+ ]
22
+ s.files = [
23
+ ".gitignore",
24
+ "ChangeLog.markdown",
25
+ "LICENSE",
26
+ "README.markdown",
27
+ "Rakefile",
28
+ "VERSION.yml",
29
+ "bin/tit",
30
+ "lib/tit.rb",
31
+ "tit.gemspec"
32
+ ]
33
+ s.homepage = %q{http://github.com/adlaiff6/tit}
34
+ s.rdoc_options = ["--charset=UTF-8"]
35
+ s.require_paths = ["lib"]
36
+ s.rubygems_version = %q{1.3.5}
37
+ s.summary = %q{stupid fucking twitter client}
38
+
39
+ if s.respond_to? :specification_version then
40
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
41
+ s.specification_version = 3
42
+
43
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
44
+ s.add_runtime_dependency(%q<nokogiri>, [">= 1.4.0"])
45
+ s.add_runtime_dependency(%q<rest-client>, [">= 1.2.0"])
46
+ else
47
+ s.add_dependency(%q<nokogiri>, [">= 1.4.0"])
48
+ s.add_dependency(%q<rest-client>, [">= 1.2.0"])
49
+ end
50
+ else
51
+ s.add_dependency(%q<nokogiri>, [">= 1.4.0"])
52
+ s.add_dependency(%q<rest-client>, [">= 1.2.0"])
53
+ end
54
+ end
55
+
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tit
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Leif Walsh
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-01-22 00:00:00 +01:00
13
+ default_executable: tit
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: nokogiri
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.4.0
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: rest-client
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.2.0
34
+ version:
35
+ description: a stupid fucking twitter client
36
+ email: leif.walsh@gmail.com
37
+ executables:
38
+ - tit
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - ChangeLog.markdown
43
+ - LICENSE
44
+ - README.markdown
45
+ files:
46
+ - .gitignore
47
+ - ChangeLog.markdown
48
+ - LICENSE
49
+ - README.markdown
50
+ - Rakefile
51
+ - VERSION.yml
52
+ - bin/tit
53
+ - lib/tit.rb
54
+ - tit.gemspec
55
+ has_rdoc: true
56
+ homepage: http://github.com/adlaiff6/tit
57
+ licenses: []
58
+
59
+ post_install_message:
60
+ rdoc_options:
61
+ - --charset=UTF-8
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: "0"
69
+ version:
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: "0"
75
+ version:
76
+ requirements: []
77
+
78
+ rubyforge_project:
79
+ rubygems_version: 1.3.5
80
+ signing_key:
81
+ specification_version: 3
82
+ summary: stupid fucking twitter client
83
+ test_files: []
84
+