pjdavis-twitter 0.3.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. data/History.txt +106 -0
  2. data/License.txt +19 -0
  3. data/Manifest.txt +71 -0
  4. data/README.txt +84 -0
  5. data/Rakefile +4 -0
  6. data/bin/twitter +15 -0
  7. data/config/hoe.rb +74 -0
  8. data/config/requirements.rb +17 -0
  9. data/examples/blocks.rb +15 -0
  10. data/examples/direct_messages.rb +28 -0
  11. data/examples/favorites.rb +20 -0
  12. data/examples/friends_followers.rb +25 -0
  13. data/examples/friendships.rb +13 -0
  14. data/examples/identica_timeline.rb +7 -0
  15. data/examples/location.rb +8 -0
  16. data/examples/posting.rb +9 -0
  17. data/examples/replies.rb +26 -0
  18. data/examples/search.rb +17 -0
  19. data/examples/sent_messages.rb +26 -0
  20. data/examples/timeline.rb +33 -0
  21. data/examples/twitter.rb +27 -0
  22. data/examples/verify_credentials.rb +13 -0
  23. data/lib/twitter/base.rb +251 -0
  24. data/lib/twitter/cli/config.rb +9 -0
  25. data/lib/twitter/cli/helpers.rb +97 -0
  26. data/lib/twitter/cli/migrations/20080722194500_create_accounts.rb +13 -0
  27. data/lib/twitter/cli/migrations/20080722194508_create_tweets.rb +16 -0
  28. data/lib/twitter/cli/migrations/20080722214605_add_account_id_to_tweets.rb +9 -0
  29. data/lib/twitter/cli/migrations/20080722214606_create_configurations.rb +13 -0
  30. data/lib/twitter/cli/models/account.rb +33 -0
  31. data/lib/twitter/cli/models/configuration.rb +13 -0
  32. data/lib/twitter/cli/models/tweet.rb +20 -0
  33. data/lib/twitter/cli.rb +328 -0
  34. data/lib/twitter/direct_message.rb +22 -0
  35. data/lib/twitter/easy_class_maker.rb +43 -0
  36. data/lib/twitter/rate_limit_status.rb +19 -0
  37. data/lib/twitter/search.rb +101 -0
  38. data/lib/twitter/status.rb +22 -0
  39. data/lib/twitter/user.rb +37 -0
  40. data/lib/twitter/version.rb +9 -0
  41. data/lib/twitter.rb +21 -0
  42. data/script/destroy +14 -0
  43. data/script/generate +14 -0
  44. data/script/txt2html +74 -0
  45. data/setup.rb +1585 -0
  46. data/spec/base_spec.rb +109 -0
  47. data/spec/cli/helper_spec.rb +35 -0
  48. data/spec/direct_message_spec.rb +35 -0
  49. data/spec/fixtures/followers.xml +706 -0
  50. data/spec/fixtures/friends.xml +609 -0
  51. data/spec/fixtures/friends_for.xml +584 -0
  52. data/spec/fixtures/friends_lite.xml +192 -0
  53. data/spec/fixtures/friends_timeline.xml +66 -0
  54. data/spec/fixtures/public_timeline.xml +148 -0
  55. data/spec/fixtures/rate_limit_status.xml +7 -0
  56. data/spec/fixtures/search_results.json +1 -0
  57. data/spec/fixtures/status.xml +25 -0
  58. data/spec/fixtures/user.xml +38 -0
  59. data/spec/fixtures/user_timeline.xml +465 -0
  60. data/spec/search_spec.rb +101 -0
  61. data/spec/spec.opts +1 -0
  62. data/spec/spec_helper.rb +12 -0
  63. data/spec/status_spec.rb +40 -0
  64. data/spec/user_spec.rb +42 -0
  65. data/tasks/deployment.rake +50 -0
  66. data/tasks/environment.rake +7 -0
  67. data/tasks/website.rake +17 -0
  68. data/twitter.gemspec +49 -0
  69. data/website/css/common.css +47 -0
  70. data/website/images/terminal_output.png +0 -0
  71. data/website/index.html +156 -0
  72. metadata +181 -0
@@ -0,0 +1,13 @@
1
+ require 'rubygems'
2
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'twitter')
3
+ config = YAML::load(open(ENV['HOME'] + '/.twitter'))
4
+
5
+ twitter = Twitter::Base.new(config['email'], config['password'])
6
+
7
+ puts twitter.verify_credentials
8
+
9
+ begin
10
+ Twitter::Base.new('asdf', 'foobar').verify_credentials
11
+ rescue => error
12
+ puts error.message
13
+ end
@@ -0,0 +1,251 @@
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
+ # Initializes the configuration for making requests to twitter
9
+ # Twitter example:
10
+ # Twitter.new('email/username', 'password')
11
+ #
12
+ # Identi.ca example:
13
+ # Twitter.new('email/username', 'password', :api_host => 'identi.ca/api')
14
+ def initialize(email, password, options={})
15
+ @config, @config[:email], @config[:password] = {}, email, password
16
+ @api_host = options.delete(:api_host) || 'twitter.com'
17
+ end
18
+
19
+ # Returns an array of statuses for a timeline; Defaults to your friends timeline.
20
+ def timeline(which=:friends, options={})
21
+ raise UnknownTimeline unless [:friends, :public, :user].include?(which)
22
+ auth = which.to_s.include?('public') ? false : true
23
+ statuses(call("#{which}_timeline", :auth => auth, :since => options[:since], :args => parse_options(options)))
24
+ end
25
+
26
+ # Returns an array of users who are in your friends list
27
+ def friends(options={})
28
+ users(call(:friends, {:args => parse_options(options)}))
29
+ end
30
+
31
+ # Returns an array of users who are friends for the id or username passed in
32
+ def friends_for(id, options={})
33
+ friends(options.merge({:id => id}))
34
+ end
35
+
36
+ # Returns an array of users who are following you
37
+ def followers(options={})
38
+ users(call(:followers, {:args => parse_options(options)}))
39
+ end
40
+
41
+ def followers_for(id, options={})
42
+ followers(options.merge({:id => id}))
43
+ end
44
+
45
+ # Returns a single status for a given id
46
+ def status(id)
47
+ statuses(call("show/#{id}")).first
48
+ end
49
+
50
+ # returns all the profile information and the last status for a user
51
+ def user(id_or_screenname)
52
+ users(request("users/show/#{id_or_screenname}.xml", :auth => true)).first
53
+ end
54
+
55
+ # Returns an array of statuses that are replies
56
+ def replies(options={})
57
+ statuses(call(:replies, :since => options[:since], :args => parse_options(options)))
58
+ end
59
+
60
+ # Destroys a status by id
61
+ def destroy(id)
62
+ call("destroy/#{id}")
63
+ end
64
+
65
+ def rate_limit_status
66
+ RateLimitStatus.new_from_xml request("account/rate_limit_status.xml", :auth => true)
67
+ end
68
+
69
+ # waiting for twitter to correctly implement this in the api as it is documented
70
+ def featured
71
+ users(call(:featured))
72
+ end
73
+
74
+ # Returns an array of all the direct messages for the authenticated user
75
+ def direct_messages(options={})
76
+ doc = request(build_path('direct_messages.xml', parse_options(options)), {:auth => true, :since => options[:since]})
77
+ (doc/:direct_message).inject([]) { |dms, dm| dms << DirectMessage.new_from_xml(dm); dms }
78
+ end
79
+ alias :received_messages :direct_messages
80
+
81
+ # Returns direct messages sent by auth user
82
+ def sent_messages(options={})
83
+ doc = request(build_path('direct_messages/sent.xml', parse_options(options)), {:auth => true, :since => options[:since]})
84
+ (doc/:direct_message).inject([]) { |dms, dm| dms << DirectMessage.new_from_xml(dm); dms }
85
+ end
86
+
87
+ # destroys a give direct message by id if the auth user is a recipient
88
+ def destroy_direct_message(id)
89
+ DirectMessage.new_from_xml(request("direct_messages/destroy/#{id}.xml", :auth => true, :method => :post))
90
+ end
91
+
92
+ # Sends a direct message <code>text</code> to <code>user</code>
93
+ def d(user, text)
94
+ DirectMessage.new_from_xml(request('direct_messages/new.xml', :auth => true, :method => :post, :form_data => {'text' => text, 'user' => user}))
95
+ end
96
+
97
+ # Befriends id_or_screenname for the auth user
98
+ def create_friendship(id_or_screenname)
99
+ users(request("friendships/create/#{id_or_screenname}.xml", :auth => true, :method => :post)).first
100
+ end
101
+
102
+ # Defriends id_or_screenname for the auth user
103
+ def destroy_friendship(id_or_screenname)
104
+ users(request("friendships/destroy/#{id_or_screenname}.xml", :auth => true, :method => :post)).first
105
+ end
106
+
107
+ # Returns true if friendship exists, false if it doesn't.
108
+ def friendship_exists?(user_a, user_b)
109
+ doc = request(build_path("friendships/exists.xml", {:user_a => user_a, :user_b => user_b}), :auth => true)
110
+ doc.at('friends').innerHTML == 'true' ? true : false
111
+ end
112
+
113
+ # Updates your location and returns Twitter::User object
114
+ def update_location(location)
115
+ users(request(build_path('account/update_location.xml', {'location' => location}), :auth => true, :method => :post)).first
116
+ end
117
+
118
+ # Updates your deliver device and returns Twitter::User object
119
+ def update_delivery_device(device)
120
+ users(request(build_path('account/update_delivery_device.xml', {'device' => device}), :auth => true, :method => :post)).first
121
+ end
122
+
123
+ # Turns notifications by id_or_screenname on for auth user.
124
+ def follow(id_or_screenname)
125
+ users(request("notifications/follow/#{id_or_screenname}.xml", :auth => true, :method => :post)).first
126
+ end
127
+
128
+ # Turns notifications by id_or_screenname off for auth user.
129
+ def leave(id_or_screenname)
130
+ users(request("notifications/leave/#{id_or_screenname}.xml", :auth => true, :method => :post)).first
131
+ end
132
+
133
+ # Returns the most recent favorite statuses for the autenticating user
134
+ def favorites(options={})
135
+ statuses(request(build_path('favorites.xml', parse_options(options)), :auth => true))
136
+ end
137
+
138
+ # Favorites the status specified by id for the auth user
139
+ def create_favorite(id)
140
+ statuses(request("favorites/create/#{id}.xml", :auth => true, :method => :post)).first
141
+ end
142
+
143
+ # Un-favorites the status specified by id for the auth user
144
+ def destroy_favorite(id)
145
+ statuses(request("favorites/destroy/#{id}.xml", :auth => true, :method => :post)).first
146
+ end
147
+
148
+ # Blocks the user specified by id for the auth user
149
+ def block(id)
150
+ users(request("blocks/create/#{id}.xml", :auth => true, :method => :post)).first
151
+ end
152
+
153
+ # Unblocks the user specified by id for the auth user
154
+ def unblock(id)
155
+ users(request("blocks/destroy/#{id}.xml", :auth => true, :method => :post)).first
156
+ end
157
+
158
+ # Posts a new update to twitter for auth user.
159
+ def post(status, options={})
160
+ form_data = {'status' => status}
161
+ form_data.merge!({'source' => options[:source]}) if options[:source]
162
+ Status.new_from_xml(request('statuses/update.xml', :auth => true, :method => :post, :form_data => form_data))
163
+ end
164
+ alias :update :post
165
+
166
+ # Verifies the credentials for the auth user.
167
+ # raises Twitter::CantConnect on failure.
168
+ def verify_credentials
169
+ request('account/verify_credentials', :auth => true)
170
+ end
171
+
172
+ private
173
+ # Converts an hpricot doc to an array of statuses
174
+ def statuses(doc)
175
+ (doc/:status).inject([]) { |statuses, status| statuses << Status.new_from_xml(status); statuses }
176
+ end
177
+
178
+ # Converts an hpricot doc to an array of users
179
+ def users(doc)
180
+ (doc/:user).inject([]) { |users, user| users << User.new_from_xml(user); users }
181
+ end
182
+
183
+ # Calls whatever api method requested that deals with statuses
184
+ #
185
+ # ie: call(:public_timeline, :auth => false)
186
+ def call(method, options={})
187
+ options.reverse_merge!({ :auth => true, :args => {} })
188
+ # Following line needed as lite=false doesn't work in the API: http://tinyurl.com/yo3h5d
189
+ options[:args].delete(:lite) unless options[:args][:lite]
190
+ args = options.delete(:args)
191
+ request(build_path("statuses/#{method.to_s}.xml", args), options)
192
+ end
193
+
194
+ # Makes a request to twitter.
195
+ def request(path, options={})
196
+ options.reverse_merge!({
197
+ :headers => { "User-Agent" => @config[:email] },
198
+ :method => :get
199
+ })
200
+ unless options[:since].blank?
201
+ since = options[:since].kind_of?(Date) ? options[:since].strftime('%a, %d-%b-%y %T GMT') : options[:since].to_s
202
+ options[:headers]["If-Modified-Since"] = since
203
+ end
204
+
205
+ uri = URI.parse("http://#{@api_host}")
206
+
207
+ begin
208
+ response = Net::HTTP.start(uri.host, 80) do |http|
209
+ klass = Net::HTTP.const_get options[:method].to_s.downcase.capitalize
210
+ req = klass.new("#{uri.path}/#{path}", options[:headers])
211
+ req.basic_auth(@config[:email], @config[:password]) if options[:auth]
212
+ if options[:method].to_s == 'post' && options[:form_data]
213
+ req.set_form_data(options[:form_data])
214
+ end
215
+ http.request(req)
216
+ end
217
+ rescue => error
218
+ raise CantConnect, error.message
219
+ end
220
+
221
+ if %w[200 304].include?(response.code)
222
+ response = parse(response.body)
223
+ raise RateExceeded if (response/:hash/:error).text =~ /Rate limit exceeded/
224
+ response
225
+ elsif response.code == '503'
226
+ raise Unavailable, response.message
227
+ elsif response.code == '401'
228
+ raise CantConnect, 'Authentication failed. Check your username and password'
229
+ else
230
+ raise CantConnect, "Twitter is returning a #{response.code}: #{response.message}"
231
+ end
232
+ end
233
+
234
+ # Given a path and a hash, build a full path with the hash turned into a query string
235
+ def build_path(path, options)
236
+ path += "?#{options.to_query}" unless options.blank?
237
+ path
238
+ end
239
+
240
+ # Tries to get all the options in the correct format before making the request
241
+ def parse_options(options)
242
+ options[:since] = options[:since].kind_of?(Date) ? options[:since].strftime('%a, %d-%b-%y %T GMT') : options[:since].to_s if options[:since]
243
+ options
244
+ end
245
+
246
+ # Converts a string response into an Hpricot xml element.
247
+ def parse(response)
248
+ Hpricot.XML(response || '')
249
+ end
250
+ end
251
+ 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