tit 1.0.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/.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
+