dustin-twitter 0.3.2.1

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.
Files changed (67) hide show
  1. data/History.txt +89 -0
  2. data/License.txt +19 -0
  3. data/Manifest.txt +66 -0
  4. data/README.txt +75 -0
  5. data/Rakefile +4 -0
  6. data/TODO.txt +2 -0
  7. data/bin/twitter +15 -0
  8. data/config/hoe.rb +74 -0
  9. data/config/requirements.rb +17 -0
  10. data/examples/blocks.rb +15 -0
  11. data/examples/direct_messages.rb +26 -0
  12. data/examples/favorites.rb +20 -0
  13. data/examples/friends_followers.rb +25 -0
  14. data/examples/friendships.rb +13 -0
  15. data/examples/location.rb +8 -0
  16. data/examples/replies.rb +26 -0
  17. data/examples/sent_messages.rb +26 -0
  18. data/examples/timeline.rb +33 -0
  19. data/examples/twitter.rb +27 -0
  20. data/examples/verify_credentials.rb +13 -0
  21. data/lib/twitter/base.rb +248 -0
  22. data/lib/twitter/cli/config.rb +9 -0
  23. data/lib/twitter/cli/helpers.rb +97 -0
  24. data/lib/twitter/cli/migrations/20080722194500_create_accounts.rb +13 -0
  25. data/lib/twitter/cli/migrations/20080722194508_create_tweets.rb +16 -0
  26. data/lib/twitter/cli/migrations/20080722214605_add_account_id_to_tweets.rb +9 -0
  27. data/lib/twitter/cli/migrations/20080722214606_create_configurations.rb +13 -0
  28. data/lib/twitter/cli/models/account.rb +33 -0
  29. data/lib/twitter/cli/models/configuration.rb +13 -0
  30. data/lib/twitter/cli/models/tweet.rb +20 -0
  31. data/lib/twitter/cli.rb +328 -0
  32. data/lib/twitter/direct_message.rb +22 -0
  33. data/lib/twitter/easy_class_maker.rb +43 -0
  34. data/lib/twitter/rate_limit_status.rb +19 -0
  35. data/lib/twitter/status.rb +22 -0
  36. data/lib/twitter/user.rb +37 -0
  37. data/lib/twitter/version.rb +9 -0
  38. data/lib/twitter.rb +20 -0
  39. data/script/destroy +14 -0
  40. data/script/generate +14 -0
  41. data/script/txt2html +74 -0
  42. data/setup.rb +1585 -0
  43. data/spec/base_spec.rb +109 -0
  44. data/spec/cli/helper_spec.rb +35 -0
  45. data/spec/direct_message_spec.rb +35 -0
  46. data/spec/fixtures/followers.xml +706 -0
  47. data/spec/fixtures/friends.xml +609 -0
  48. data/spec/fixtures/friends_for.xml +584 -0
  49. data/spec/fixtures/friends_lite.xml +192 -0
  50. data/spec/fixtures/friends_timeline.xml +66 -0
  51. data/spec/fixtures/public_timeline.xml +148 -0
  52. data/spec/fixtures/rate_limit_status.xml +7 -0
  53. data/spec/fixtures/status.xml +25 -0
  54. data/spec/fixtures/user.xml +38 -0
  55. data/spec/fixtures/user_timeline.xml +465 -0
  56. data/spec/spec.opts +1 -0
  57. data/spec/spec_helper.rb +8 -0
  58. data/spec/status_spec.rb +40 -0
  59. data/spec/user_spec.rb +42 -0
  60. data/tasks/deployment.rake +41 -0
  61. data/tasks/environment.rake +7 -0
  62. data/tasks/website.rake +17 -0
  63. data/twitter.gemspec +49 -0
  64. data/website/css/common.css +47 -0
  65. data/website/images/terminal_output.png +0 -0
  66. data/website/index.html +138 -0
  67. metadata +176 -0
@@ -0,0 +1,248 @@
1
+ # This is the base class for the twitter library. It makes all the requests
2
+ # to twitter, parses the xml (using hpricot) and returns ruby objects to play with.
3
+ #
4
+ # For complete documentation on the options, check out the twitter api docs.
5
+ # http://groups.google.com/group/twitter-development-talk/web/api-documentation
6
+ module Twitter
7
+ class Base
8
+
9
+ # Initializes the configuration for making requests to twitter
10
+ def initialize(email, password, host='twitter.com')
11
+ @config, @config[:email], @config[:password] = {}, email, password
12
+ @api_host = host
13
+ end
14
+
15
+ # Returns an array of statuses for a timeline; Defaults to your friends timeline.
16
+ def timeline(which=:friends, options={})
17
+ raise UnknownTimeline unless [:friends, :public, :user].include?(which)
18
+ auth = which.to_s.include?('public') ? false : true
19
+ statuses(call("#{which}_timeline", :auth => auth, :since => options[:since], :args => parse_options(options)))
20
+ end
21
+
22
+ # Returns an array of users who are in your friends list
23
+ def friends(options={})
24
+ users(call(:friends, {:args => parse_options(options)}))
25
+ end
26
+
27
+ # Returns an array of users who are friends for the id or username passed in
28
+ def friends_for(id, options={})
29
+ friends(options.merge({:id => id}))
30
+ end
31
+
32
+ # Returns an array of users who are following you
33
+ def followers(options={})
34
+ users(call(:followers, {:args => parse_options(options)}))
35
+ end
36
+
37
+ def followers_for(id, options={})
38
+ followers(options.merge({:id => id}))
39
+ end
40
+
41
+ # Returns a single status for a given id
42
+ def status(id)
43
+ statuses(call("show/#{id}")).first
44
+ end
45
+
46
+ # returns all the profile information and the last status for a user
47
+ def user(id_or_screenname)
48
+ users(request("users/show/#{id_or_screenname}.xml", :auth => true)).first
49
+ end
50
+
51
+ # Returns an array of statuses that are replies
52
+ def replies(options={})
53
+ statuses(call(:replies, :since => options[:since], :args => parse_options(options)))
54
+ end
55
+
56
+ # Destroys a status by id
57
+ def destroy(id)
58
+ call("destroy/#{id}")
59
+ end
60
+
61
+ def rate_limit_status
62
+ RateLimitStatus.new_from_xml request("account/rate_limit_status.xml", :auth => true)
63
+ end
64
+
65
+ # waiting for twitter to correctly implement this in the api as it is documented
66
+ def featured
67
+ users(call(:featured))
68
+ end
69
+
70
+ # Returns an array of all the direct messages for the authenticated user
71
+ def direct_messages(options={})
72
+ doc = request(build_path('direct_messages.xml', parse_options(options)), {:auth => true, :since => options[:since]})
73
+ (doc/:direct_message).inject([]) { |dms, dm| dms << DirectMessage.new_from_xml(dm); dms }
74
+ end
75
+ alias :received_messages :direct_messages
76
+
77
+ # Returns direct messages sent by auth user
78
+ def sent_messages(options={})
79
+ doc = request(build_path('direct_messages/sent.xml', parse_options(options)), {:auth => true, :since => options[:since]})
80
+ (doc/:direct_message).inject([]) { |dms, dm| dms << DirectMessage.new_from_xml(dm); dms }
81
+ end
82
+
83
+ # destroys a give direct message by id if the auth user is a recipient
84
+ def destroy_direct_message(id)
85
+ request("direct_messages/destroy/#{id}.xml", :auth => true)
86
+ end
87
+
88
+ # Sends a direct message <code>text</code> to <code>user</code>
89
+ def d(user, text)
90
+ url = URI.parse("http://#{@api_host}/direct_messages/new.xml")
91
+ req = Net::HTTP::Post.new(url.path)
92
+ req.basic_auth(@config[:email], @config[:password])
93
+ req.set_form_data({'text' => text, 'user' => user})
94
+ response = Net::HTTP.new(url.host, url.port).start { |http| http.request(req) }
95
+ DirectMessage.new_from_xml(parse(response.body).at('direct_message'))
96
+ end
97
+
98
+ # Befriends id_or_screenname for the auth user
99
+ def create_friendship(id_or_screenname)
100
+ users(request("friendships/create/#{id_or_screenname}.xml", :auth => true)).first
101
+ end
102
+
103
+ # Defriends id_or_screenname for the auth user
104
+ def destroy_friendship(id_or_screenname)
105
+ users(request("friendships/destroy/#{id_or_screenname}.xml", :auth => true)).first
106
+ end
107
+
108
+ # Returns true if friendship exists, false if it doesn't.
109
+ def friendship_exists?(user_a, user_b)
110
+ doc = request(build_path("friendships/exists.xml", {:user_a => user_a, :user_b => user_b}), :auth => true)
111
+ doc.at('friends').innerHTML == 'true' ? true : false
112
+ end
113
+
114
+ # Updates your location and returns Twitter::User object
115
+ def update_location(location)
116
+ users(request(build_path('account/update_location.xml', {'location' => location}), :auth => true)).first
117
+ end
118
+
119
+ # Updates your deliver device and returns Twitter::User object
120
+ def update_delivery_device(device)
121
+ users(request(build_path('account/update_delivery_device.xml', {'device' => device}), :auth => true)).first
122
+ end
123
+
124
+ # Turns notifications by id_or_screenname on for auth user.
125
+ def follow(id_or_screenname)
126
+ users(request("notifications/follow/#{id_or_screenname}.xml", :auth => true)).first
127
+ end
128
+
129
+ # Turns notifications by id_or_screenname off for auth user.
130
+ def leave(id_or_screenname)
131
+ users(request("notifications/leave/#{id_or_screenname}.xml", :auth => true)).first
132
+ end
133
+
134
+ # Returns the most recent favorite statuses for the autenticating user
135
+ def favorites(options={})
136
+ statuses(request(build_path('favorites.xml', parse_options(options)), :auth => true))
137
+ end
138
+
139
+ # Favorites the status specified by id for the auth user
140
+ def create_favorite(id)
141
+ statuses(request("favorites/create/#{id}.xml", :auth => true)).first
142
+ end
143
+
144
+ # Un-favorites the status specified by id for the auth user
145
+ def destroy_favorite(id)
146
+ statuses(request("favorites/destroy/#{id}.xml", :auth => true)).first
147
+ end
148
+
149
+ # Blocks the user specified by id for the auth user
150
+ def block(id)
151
+ users(request("blocks/create/#{id}.xml", :auth => true)).first
152
+ end
153
+
154
+ # Unblocks the user specified by id for the auth user
155
+ def unblock(id)
156
+ users(request("blocks/destroy/#{id}.xml", :auth => true)).first
157
+ end
158
+
159
+ # Posts a new update to twitter for auth user.
160
+ def post(status, options={})
161
+ form_data = {'status' => status}
162
+ form_data.merge({'source' => options[:source]}) if options[:source]
163
+ url = URI.parse("http://#{@api_host}/statuses/update.xml")
164
+ req = Net::HTTP::Post.new(url.path)
165
+ req.basic_auth(@config[:email], @config[:password])
166
+ req.set_form_data(form_data)
167
+ response = Net::HTTP.new(url.host, url.port).start { |http| http.request(req) }
168
+ Status.new_from_xml(parse(response.body).at('status'))
169
+ end
170
+ alias :update :post
171
+
172
+ # Verifies the credentials for the auth user.
173
+ # raises Twitter::CantConnect on failure.
174
+ def verify_credentials
175
+ request('account/verify_credentials', :auth => true)
176
+ end
177
+
178
+ private
179
+ # Converts an hpricot doc to an array of statuses
180
+ def statuses(doc)
181
+ (doc/:status).inject([]) { |statuses, status| statuses << Status.new_from_xml(status); statuses }
182
+ end
183
+
184
+ # Converts an hpricot doc to an array of users
185
+ def users(doc)
186
+ (doc/:user).inject([]) { |users, user| users << User.new_from_xml(user); users }
187
+ end
188
+
189
+ # Calls whatever api method requested that deals with statuses
190
+ #
191
+ # ie: call(:public_timeline, :auth => false)
192
+ def call(method, options={})
193
+ options.reverse_merge!({ :auth => true, :args => {} })
194
+ # Following line needed as lite=false doesn't work in the API: http://tinyurl.com/yo3h5d
195
+ options[:args].delete(:lite) unless options[:args][:lite]
196
+ args = options.delete(:args)
197
+ request(build_path("statuses/#{method.to_s}.xml", args), options)
198
+ end
199
+
200
+ # Makes a request to twitter.
201
+ def request(path, options={})
202
+ options.reverse_merge!({:headers => { "User-Agent" => @config[:email] }})
203
+ unless options[:since].blank?
204
+ since = options[:since].kind_of?(Date) ? options[:since].strftime('%a, %d-%b-%y %T GMT') : options[:since].to_s
205
+ options[:headers]["If-Modified-Since"] = since
206
+ end
207
+
208
+ begin
209
+ response = Net::HTTP.start(@api_host, 80) do |http|
210
+ req = Net::HTTP::Get.new('/' + path, options[:headers])
211
+ req.basic_auth(@config[:email], @config[:password]) if options[:auth]
212
+ http.request(req)
213
+ end
214
+ rescue => error
215
+ raise CantConnect, error.message
216
+ end
217
+
218
+ if %w[200 304].include?(response.code)
219
+ response = parse(response.body)
220
+ raise RateExceeded if (response/:hash/:error).text =~ /Rate limit exceeded/
221
+ response
222
+ elsif response.code == '503'
223
+ raise Unavailable, response.message
224
+ elsif response.code == '401'
225
+ raise CantConnect, 'Authentication failed. Check your username and password'
226
+ else
227
+ raise CantConnect, "Twitter is returning a #{response.code}: #{response.message}"
228
+ end
229
+ end
230
+
231
+ # Given a path and a hash, build a full path with the hash turned into a query string
232
+ def build_path(path, options)
233
+ path += "?#{options.to_query}" unless options.blank?
234
+ path
235
+ end
236
+
237
+ # Tries to get all the options in the correct format before making the request
238
+ def parse_options(options)
239
+ options[:since] = options[:since].kind_of?(Date) ? options[:since].strftime('%a, %d-%b-%y %T GMT') : options[:since].to_s if options[:since]
240
+ options
241
+ end
242
+
243
+ # Converts a string response into an Hpricot xml element.
244
+ def parse(response)
245
+ Hpricot.XML(response || '')
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,9 @@
1
+ module Twitter
2
+ module CLI
3
+ Config = {
4
+ :adapter => 'sqlite3',
5
+ :database => File.join(ENV['HOME'], '.twitter.db'),
6
+ :timeout => 5000
7
+ }
8
+ end
9
+ end
@@ -0,0 +1,97 @@
1
+ module Twitter
2
+ module CLI
3
+ module Helpers
4
+ class NoActiveAccount < StandardError; end
5
+ class NoAccounts < StandardError; end
6
+
7
+ def output_tweets(collection, options={})
8
+ options.reverse_merge!({
9
+ :cache => false,
10
+ :since_prefix => '',
11
+ :empty_msg => 'Nothing new since your last check.'
12
+ })
13
+ if collection.size > 0
14
+ justify = collection.collect { |s| s.user.screen_name }.max { |a,b| a.length <=> b.length }.length rescue 0
15
+ indention = ' ' * (justify + 3)
16
+ say("\n#{indention}#{collection.size} new tweet(s) found.\n\n")
17
+ collection.each do |s|
18
+ Tweet.create_from_tweet(current_account, s) if options[:cache]
19
+ occurred_at = Time.parse(s.created_at).strftime('On %b %d at %l:%M%P')
20
+ formatted_time = '-' * occurred_at.length + "\n#{indention}#{occurred_at}"
21
+ formatted_name = s.user.screen_name.rjust(justify + 1)
22
+ formatted_msg = ''
23
+ s.text.split(' ').in_groups_of(6, false) { |row| formatted_msg += row.join(' ') + "\n#{indention}" }
24
+ say "#{CGI::unescapeHTML(formatted_name)}: #{CGI::unescapeHTML(formatted_msg)}#{formatted_time}\n\n"
25
+ end
26
+ Configuration["#{options[:since_prefix]}_since_id"] = collection.first.id
27
+ else
28
+ say(options[:empty_msg])
29
+ end
30
+ end
31
+
32
+ def base(username=current_account.username, password=current_account.password)
33
+ @base ||= Twitter::Base.new(username, password)
34
+ end
35
+
36
+ def current_account
37
+ @current_account ||= Account.active
38
+ raise Account.count == 0 ? NoAccounts : NoActiveAccount if @current_account.blank?
39
+ @current_account
40
+ end
41
+
42
+ def attempt_import(&block)
43
+ tweet_file = File.join(ENV['HOME'], '.twitter')
44
+ if File.exists?(tweet_file)
45
+ say '.twitter file found, attempting import...'
46
+ config = YAML::load(File.read(tweet_file))
47
+ if !config['email'].blank? && !config['password'].blank?
48
+ Account.add(:username => config['email'], :password => config['password'])
49
+ say 'Account imported'
50
+ block.call if block_given?
51
+ true
52
+ else
53
+ say "Either your username or password were blank in your .twitter file so I could not import. Use 'twitter add' to add an account."
54
+ false
55
+ end
56
+ end
57
+ end
58
+
59
+ def do_work(&block)
60
+ connect
61
+ begin
62
+ block.call
63
+ rescue Twitter::RateExceeded
64
+ say("Twitter says you've been making too many requests. Wait for a bit and try again.")
65
+ rescue Twitter::Unavailable
66
+ say("Twitter is unavailable right now. Try again later.")
67
+ rescue Twitter::CantConnect => msg
68
+ say("Can't connect to twitter because: #{msg}")
69
+ rescue Twitter::CLI::Helpers::NoActiveAccount
70
+ say("You have not set an active account. Use 'twitter change' to set one now.")
71
+ rescue Twitter::CLI::Helpers::NoAccounts
72
+ unless attempt_import { block.call }
73
+ say("You have not created any accounts. Use 'twitter add' to create one now.")
74
+ end
75
+ end
76
+ end
77
+
78
+ def connect
79
+ ActiveRecord::Base.logger = Logger.new('/tmp/twitter_ar_logger.log')
80
+ ActiveRecord::Base.establish_connection(Twitter::CLI::Config)
81
+ ActiveRecord::Base.connection
82
+ end
83
+
84
+ def migrate
85
+ connect
86
+ ActiveRecord::Migrator.migrate("#{CLI_ROOT}/migrations/")
87
+ end
88
+
89
+ def connect_and_migrate
90
+ say('Attempting to establish connection...')
91
+ connect
92
+ say('Connection established...migrating database...')
93
+ migrate
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,13 @@
1
+ class CreateAccounts < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :accounts do |t|
4
+ t.string :username, :password
5
+ t.boolean :current
6
+ t.timestamps
7
+ end
8
+ end
9
+
10
+ def self.down
11
+ drop_table :accounts
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ class CreateTweets < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :tweets do |t|
4
+ t.datetime :occurred_at
5
+ t.boolean :truncated, :favorited, :user_protected, :default => false
6
+ t.integer :twitter_id, :user_id, :in_reply_to_status_id, :in_reply_to_user_id, :user_followers_count
7
+ t.text :body
8
+ t.string :source, :user_name, :user_screen_name, :user_location, :user_description, :user_profile_image_url, :user_url
9
+ t.timestamps
10
+ end
11
+ end
12
+
13
+ def self.down
14
+ drop_table :tweets
15
+ end
16
+ end
@@ -0,0 +1,9 @@
1
+ class AddAccountIdToTweets < ActiveRecord::Migration
2
+ def self.up
3
+ add_column :tweets, :account_id, :integer
4
+ end
5
+
6
+ def self.down
7
+ remove_column :tweets, :account_id
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ class CreateConfigurations < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :configurations do |t|
4
+ t.string :key
5
+ t.text :data
6
+ t.timestamps
7
+ end
8
+ end
9
+
10
+ def self.down
11
+ drop_table :accounts
12
+ end
13
+ end
@@ -0,0 +1,33 @@
1
+ class Account < ActiveRecord::Base
2
+ named_scope :current, :conditions => {:current => true}
3
+
4
+ has_many :tweets, :dependent => :destroy
5
+
6
+ def self.add(hash)
7
+ username = hash.delete(:username)
8
+ account = find_or_initialize_by_username(username)
9
+ account.attributes = hash
10
+ account.save
11
+ set_current(account) if new_active_needed?
12
+ end
13
+
14
+ def self.active
15
+ current.first
16
+ end
17
+
18
+ def self.set_current(account_or_id)
19
+ account = account_or_id.is_a?(Account) ? account_or_id : find(account_or_id)
20
+ account.update_attribute :current, true
21
+ Account.update_all "current = 0", "id != #{account.id}"
22
+ account
23
+ end
24
+
25
+ def self.new_active_needed?
26
+ self.current.count == 0 && self.count > 0
27
+ end
28
+
29
+ def to_s
30
+ "#{current? ? '*' : ' '} #{username}"
31
+ end
32
+ alias to_str to_s
33
+ end
@@ -0,0 +1,13 @@
1
+ class Configuration < ActiveRecord::Base
2
+ serialize :data
3
+
4
+ def self.[](key)
5
+ key = find_by_key(key.to_s)
6
+ key.nil? ? nil : key.data
7
+ end
8
+
9
+ def self.[]=(key, data)
10
+ c = find_or_create_by_key(key.to_s)
11
+ c.update_attribute :data, data
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ class Tweet < ActiveRecord::Base
2
+ belongs_to :account
3
+
4
+ def self.create_from_tweet(account, s)
5
+ tweet = account.tweets.find_or_initialize_by_twitter_id(s.id)
6
+ tweet.body = s.text
7
+ tweet.occurred_at = s.created_at
8
+
9
+ %w[truncated favorited in_reply_to_status_id in_reply_to_user_id source].each do |m|
10
+ tweet.send("#{m}=", s.send(m))
11
+ end
12
+
13
+ %w[id followers_count name screen_name location description
14
+ profile_image_url url protected].each do |m|
15
+ tweet.send("user_#{m}=", s.user.send(m))
16
+ end
17
+ tweet.save!
18
+ tweet
19
+ end
20
+ end