drnic-twitter 0.4.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. data/History +130 -0
  2. data/License +19 -0
  3. data/Manifest +68 -0
  4. data/README +84 -0
  5. data/Rakefile +42 -0
  6. data/bin/twitter +14 -0
  7. data/examples/blocks.rb +15 -0
  8. data/examples/direct_messages.rb +29 -0
  9. data/examples/favorites.rb +20 -0
  10. data/examples/friends_followers.rb +25 -0
  11. data/examples/friendships.rb +13 -0
  12. data/examples/identica_timeline.rb +7 -0
  13. data/examples/location.rb +8 -0
  14. data/examples/posting.rb +9 -0
  15. data/examples/replies.rb +27 -0
  16. data/examples/search.rb +18 -0
  17. data/examples/sent_messages.rb +27 -0
  18. data/examples/timeline.rb +34 -0
  19. data/examples/twitter.rb +27 -0
  20. data/examples/verify_credentials.rb +13 -0
  21. data/lib/twitter.rb +32 -0
  22. data/lib/twitter/base.rb +284 -0
  23. data/lib/twitter/cli.rb +419 -0
  24. data/lib/twitter/cli/config.rb +9 -0
  25. data/lib/twitter/cli/helpers.rb +109 -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/direct_message.rb +22 -0
  34. data/lib/twitter/easy_class_maker.rb +43 -0
  35. data/lib/twitter/rate_limit_status.rb +19 -0
  36. data/lib/twitter/search.rb +101 -0
  37. data/lib/twitter/search_result.rb +83 -0
  38. data/lib/twitter/search_result_info.rb +82 -0
  39. data/lib/twitter/status.rb +22 -0
  40. data/lib/twitter/user.rb +38 -0
  41. data/lib/twitter/version.rb +3 -0
  42. data/spec/base_spec.rb +139 -0
  43. data/spec/cli/helper_spec.rb +49 -0
  44. data/spec/direct_message_spec.rb +35 -0
  45. data/spec/fixtures/follower_ids.xml +11 -0
  46. data/spec/fixtures/followers.xml +706 -0
  47. data/spec/fixtures/friend_ids.xml +12 -0
  48. data/spec/fixtures/friends.xml +609 -0
  49. data/spec/fixtures/friends_for.xml +584 -0
  50. data/spec/fixtures/friends_lite.xml +192 -0
  51. data/spec/fixtures/friends_timeline.xml +66 -0
  52. data/spec/fixtures/friendship_already_exists.xml +5 -0
  53. data/spec/fixtures/friendship_created.xml +12 -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_result_info.yml +147 -0
  57. data/spec/fixtures/search_results.json +1 -0
  58. data/spec/fixtures/status.xml +25 -0
  59. data/spec/fixtures/user.xml +38 -0
  60. data/spec/fixtures/user_timeline.xml +465 -0
  61. data/spec/search_spec.rb +100 -0
  62. data/spec/spec.opts +1 -0
  63. data/spec/spec_helper.rb +23 -0
  64. data/spec/status_spec.rb +40 -0
  65. data/spec/user_spec.rb +42 -0
  66. data/twitter.gemspec +42 -0
  67. data/website/css/common.css +47 -0
  68. data/website/images/terminal_output.png +0 -0
  69. data/website/index.html +147 -0
  70. metadata +177 -0
@@ -0,0 +1,419 @@
1
+ require 'rubygems'
2
+
3
+ gem 'main'
4
+ gem 'highline'
5
+ gem 'activerecord'
6
+ gem 'sqlite3-ruby'
7
+
8
+ require 'main'
9
+ require 'highline/import'
10
+ require 'activerecord'
11
+ require 'sqlite3'
12
+
13
+ HighLine.track_eof = false
14
+ CLI_ROOT = File.expand_path(File.join(File.dirname(__FILE__), 'cli'))
15
+ require CLI_ROOT + '/config'
16
+ require CLI_ROOT + '/helpers'
17
+ Dir[CLI_ROOT + '/models/*.rb'].each { |m| require m }
18
+
19
+ include Twitter::CLI::Helpers
20
+
21
+ Main {
22
+ def run
23
+ puts "twitter [command] --help for usage instructions."
24
+ puts "The available commands are: \n install, uninstall, add, remove, list, change, post, befriend, defriend, follow, leave, d, timeline and register_oauth."
25
+ end
26
+
27
+ mode 'install' do
28
+ description 'Creates the sqlite3 database and runs the migrations.'
29
+ def run
30
+ migrate
31
+ attempt_import
32
+ say 'Twitter installed.'
33
+ end
34
+ end
35
+
36
+ mode 'uninstall' do
37
+ description 'Removes the sqlite3 database. There is no undo for this.'
38
+ def run
39
+ FileUtils.rm(Twitter::CLI::Config[:database]) if File.exists?(Twitter::CLI::Config[:database])
40
+ say 'Twitter gem uninstalled.'
41
+ end
42
+ end
43
+
44
+ mode 'add' do
45
+ description 'Adds a new twitter account to the database. Prompts for username and password.'
46
+ argument('username', 'u') {
47
+ optional
48
+ description 'optional username'
49
+ }
50
+ argument('password', 'p') {
51
+ optional
52
+ description 'optional password'
53
+ }
54
+
55
+ def run
56
+ account = Hash.new
57
+ say "Add New Account:"
58
+
59
+ # allows optional username arg
60
+ if params['username'].given?
61
+ account[:username] = params['username'].value
62
+ else
63
+ account[:username] = ask('Username: ') do |q|
64
+ q.validate = /\S+/
65
+ end
66
+ end
67
+
68
+ # allows optional password arg
69
+ if params['password'].given?
70
+ account[:password] = params['password'].value
71
+ else
72
+ account[:password] = ask("Password (won't be displayed): ") do |q|
73
+ q.echo = false
74
+ q.validate = /\S+/
75
+ end
76
+ end
77
+
78
+ do_work do
79
+ base(account[:username], account[:password]).verify_credentials
80
+ Account.add(account)
81
+ say 'Account added.'
82
+ end
83
+ end
84
+ end
85
+
86
+ mode 'remove' do
87
+ description 'Removes a twitter account from the database. If username provided it removes that username else it prompts with list and asks for which one you would like to remove.'
88
+ argument( 'username' ) {
89
+ optional
90
+ description 'username of account you would like to remove'
91
+ }
92
+
93
+ def run
94
+ do_work do
95
+ if params['username'].given?
96
+ account = Account.find_by_username(params['username'].value)
97
+ else
98
+ Account.find(:all, :order => 'username').each do |a|
99
+ say "#{a.id}. #{a}"
100
+ end
101
+ account_id = ask 'Account to remove (enter number): ' do |q|
102
+ q.validate = /\d+/
103
+ end
104
+ end
105
+
106
+ begin
107
+ account = account_id ? Account.find(account_id) : account
108
+ account_name = account.username
109
+ account.destroy
110
+ Account.set_current(Account.first) if Account.new_active_needed?
111
+ say "#{account_name} has been removed.\n"
112
+ rescue ActiveRecord::RecordNotFound
113
+ say "ERROR: Account could not be found. Try again. \n"
114
+ end
115
+ end
116
+ end
117
+ end
118
+
119
+ mode 'list' do
120
+ description 'Lists all the accounts that have been added and puts a * by the current one that is used for posting, etc.'
121
+ def run
122
+ do_work do
123
+ if Account.count == 0
124
+ say 'No accounts have been added.'
125
+ else
126
+ say 'Account List'
127
+ Account.find(:all, :order => 'username').each do |a|
128
+ say a
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+
135
+ mode 'change' do
136
+ description 'Changes the current account being used for posting etc. to the username provided. If no username is provided, a list is presented and you can choose the account from there.'
137
+ argument( 'username' ) {
138
+ optional
139
+ description 'username of account you would like to switched to'
140
+ }
141
+
142
+ def run
143
+ do_work do
144
+ if params['username'].given?
145
+ new_current = Account.find_by_username(params['username'].value)
146
+ else
147
+ Account.find(:all, :order => 'username').each do |a|
148
+ say "#{a.id}. #{a}"
149
+ end
150
+ new_current = ask 'Change current account to (enter number): ' do |q|
151
+ q.validate = /\d+/
152
+ end
153
+ end
154
+
155
+ begin
156
+ current = Account.set_current(new_current)
157
+ say "#{current} is now the current account.\n"
158
+ rescue ActiveRecord::RecordNotFound
159
+ say "ERROR: Account could not be found. Try again. \n"
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ mode 'post' do
166
+ description "Posts a message to twitter using the current account. The following are all valid examples from the command line:
167
+ $ twitter post 'my update'
168
+ $ twitter post my update with quotes
169
+ $ echo 'my update from stdin' | twitter post"
170
+ def run
171
+ do_work do
172
+ post = ARGV.size > 1 ? ARGV.join(" ") : ARGV.shift
173
+ say "Sending twitter update"
174
+ finished, status = false, nil
175
+ progress_thread = Thread.new { until finished; print "."; $stdout.flush; sleep 0.5; end; }
176
+ post_thread = Thread.new(binding()) do |b|
177
+ status = base.post(post, :source => Twitter::SourceName)
178
+ finished = true
179
+ end
180
+ post_thread.join
181
+ progress_thread.join
182
+ say "Got it! New tweet created at: #{status.created_at}\n"
183
+ end
184
+ end
185
+ end
186
+
187
+ mode 'befriend' do
188
+ description "Allows you to add a user as a friend"
189
+ argument('username') {
190
+ required
191
+ description 'username or id of twitterrer to befriend'
192
+ }
193
+
194
+ def run
195
+ do_work do
196
+ username = params['username'].value
197
+ base.create_friendship(username)
198
+ say "#{username} has been added as a friend. follow notifications with 'twitter follow #{username}'"
199
+ end
200
+ end
201
+ end
202
+
203
+ mode 'defriend' do
204
+ description "Allows you to remove a user from being a friend"
205
+ argument('username') {
206
+ required
207
+ description 'username or id of twitterrer to defriend'
208
+ }
209
+
210
+ def run
211
+ do_work do
212
+ username = params['username'].value
213
+ base.destroy_friendship(username)
214
+ say "#{username} has been removed from your friends"
215
+ end
216
+ end
217
+ end
218
+
219
+ mode 'follow' do
220
+ description "Allows you to add notifications for a user (aka Follow Them)"
221
+ argument('username') {
222
+ required
223
+ description 'username or id of twitterrer to follow'
224
+ }
225
+
226
+ def run
227
+ do_work do
228
+ username = params['username'].value
229
+ base.follow(username)
230
+ say "You are now following notifications from #{username}"
231
+ end
232
+ end
233
+ end
234
+
235
+ mode 'leave' do
236
+ description "Allows you to turn off notifications for a user"
237
+ argument('username') {
238
+ required
239
+ description 'username or id of twitterrer to leave'
240
+ }
241
+
242
+ def run
243
+ do_work do
244
+ username = params['username'].value
245
+ base.leave(username)
246
+ say "You are no longer following notifications from #{username}"
247
+ end
248
+ end
249
+ end
250
+
251
+ mode 'd' do
252
+ description "Allows you to direct message a user. The following are all valid examples from the command line:
253
+ $ twitter d jnunemaker 'yo homeboy'
254
+ $ twitter d jnunemaker yo homeboy
255
+ $ echo 'yo homeboy' | twitter d jnunemaker"
256
+ argument('username') {
257
+ required
258
+ description 'username or id of twitterrer to direct message'
259
+ }
260
+
261
+ def run
262
+ do_work do
263
+ username = params['username'].value
264
+ post = ARGV.size > 1 ? ARGV.join(" ") : ARGV.shift
265
+ base.d(username, post)
266
+ say "Direct message sent to #{username}"
267
+ end
268
+ end
269
+ end
270
+
271
+ mode 'timeline' do
272
+ description "Allows you to view your timeline, your friends or the public one"
273
+ argument( 'timeline' ) {
274
+ description 'the timeline you wish to see (friends, public, me)'
275
+ default 'friends'
276
+ }
277
+ option('force', 'f') {
278
+ description "Ignore since_id and show first page of results even if there aren't new ones"
279
+ }
280
+ option('reverse', 'r') {
281
+ description 'Reverse the output so the oldest tweets are at the top'
282
+ }
283
+
284
+ def run
285
+ do_work do
286
+ timeline = params['timeline'].value == 'me' ? 'user' : params['timeline'].value
287
+ options, since_id = {}, Configuration["#{timeline}_since_id"]
288
+ options[:since_id] = since_id if !since_id.nil? && !params['force'].given?
289
+ reverse = params['reverse'].given? ? true : false
290
+ cache = [:friends, :user].include?(timeline)
291
+ collection = base.timeline(timeline.to_sym, options)
292
+ output_tweets(collection, {:cache => cache, :since_prefix => timeline, :reverse => reverse})
293
+ end
294
+ end
295
+ end
296
+
297
+ mode 'replies' do
298
+ description 'Allows you to view all @replies sent to you'
299
+ option('force', 'f') {
300
+ description "Ignore since_id and show first page of replies even if there aren't new ones"
301
+ }
302
+
303
+ def run
304
+ do_work do
305
+ options, since_id = {}, Configuration["replies_since_id"]
306
+ options[:since_id] = since_id if !since_id.nil? && !params['force'].given?
307
+ collection = base.replies(options)
308
+ output_tweets(collection, {:since_prefix => 'replies'})
309
+ end
310
+ end
311
+ end
312
+
313
+ mode 'clear_config' do
314
+ def run
315
+ do_work do
316
+ count = Configuration.count
317
+ Configuration.destroy_all
318
+ say("#{count} configuration entries cleared.")
319
+ end
320
+ end
321
+ end
322
+
323
+ mode 'open' do
324
+ description 'Opens the given twitter user in a browser window'
325
+ argument('username') {
326
+ required
327
+ description "username or id of twitterrer who's page you would like to see"
328
+ }
329
+
330
+ def run
331
+ `open http://twitter.com/#{params['username'].value}`
332
+ end
333
+ end
334
+
335
+ mode 'register_oauth' do
336
+ description 'Registers a new application for oauth registration via Twitter'
337
+ argument( 'username' ) {
338
+ required
339
+ description 'username of account to register the oauth application; if not provided a list will be displayed'
340
+ }
341
+ argument( 'application_name' ) {
342
+ required
343
+ description 'name of application to be registered for oauth with twitter'
344
+ }
345
+ argument( 'application_url' ) {
346
+ required
347
+ description 'url of application to be registered for oauth with twitter'
348
+ }
349
+ argument( 'description' ) {
350
+ required
351
+ description 'description of your organization'
352
+ }
353
+ keyword( 'organization' ) {
354
+ optional
355
+ description 'name of your organization'
356
+ }
357
+ keyword( 'organization_url' ) {
358
+ optional
359
+ description 'url of your organization'
360
+ }
361
+ keyword( 'callback_url' ) {
362
+ optional
363
+ description 'callback url into your application'
364
+ }
365
+ option( 'readwrite' ) {
366
+ description 'your application requires write permissions to each user\'s twitter account'
367
+ }
368
+
369
+ def run
370
+ do_work do
371
+ unless account = Account.find_by_username(params['username'].value)
372
+ say "ERROR: Account could not be found. Try again. \n"
373
+ exit
374
+ end
375
+ begin
376
+ require "mechanize"
377
+ rescue LoadError
378
+ puts <<-EOS.gsub(/^ /, '')
379
+ To use the register_oauth command you need the mechanize gem.
380
+ sudo gem install mechanize
381
+ EOS
382
+ exit 1
383
+ end
384
+ agent = WWW::Mechanize.new
385
+ agent.get("http://twitter.com/login") do |page|
386
+ page.form_with(:action => 'https://twitter.com/sessions') do |f|
387
+ f['session[username_or_email]'] = account.username
388
+ f['session[password]'] = account.password
389
+ end.click_button
390
+ end
391
+ agent.get("http://twitter.com/oauth_clients/new") do |page|
392
+ results_page = page.form_with(:action => '/oauth_clients/create') do |f|
393
+ f['client_application[name]'] = params['application_name'].value
394
+ f['client_application[description]'] = params['description'].value
395
+ f['client_application[url]'] = params['application_url'].value
396
+ f['client_application[organization]'] = params['organization'].value || ""
397
+ f['client_application[organization_url]'] = params['organization_url'].value || ""
398
+ f['client_application[desktop]'] = params['callback_url'].given? ? '0' : '1'
399
+ f['client_application[callback_url]'] = params['callback_url'].value if params['callback_url']
400
+ f['client_application[is_writable]'] = params['readwrite'].given? ? '1' : '0'
401
+ while f.radiobuttons.size > 0
402
+ f.radiobuttons.shift
403
+ end
404
+ end.click_button
405
+ message = results_page.body.match(%r{notification.setMessage\("([^)]+)"\)})[1]
406
+ message.gsub!(/\\/, "")
407
+ puts message
408
+ if message =~ /Unable/
409
+ puts "* " + results_page.search(".error").inner_html
410
+ else
411
+ codes = results_page.search("code")
412
+ puts "Consumer key: #{codes[0].inner_html}"
413
+ puts "Consumer secret: #{codes[1].inner_html}"
414
+ end
415
+ end
416
+ end
417
+ end
418
+ end
419
+ }
@@ -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,109 @@
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 = {
9
+ :cache => false,
10
+ :since_prefix => '',
11
+ :empty_msg => 'Nothing new since your last check.',
12
+ :reverse => false
13
+ }.merge(options)
14
+
15
+ if collection.size > 0
16
+ justify = collection.collect { |s| s.user.screen_name }.max { |a,b| a.length <=> b.length }.length rescue 0
17
+ indention = ' ' * (justify + 3)
18
+ say("\n#{indention}#{collection.size} new tweet(s) found.\n\n")
19
+ collection.reverse! if options[:reverse]
20
+ collection.each do |s|
21
+ Tweet.create_from_tweet(current_account, s) if options[:cache]
22
+
23
+ occurred_at = Time.parse(s.created_at).strftime('On %b %d at %l:%M%P')
24
+ formatted_time = '-' * occurred_at.length + "\n#{indention}#{occurred_at}"
25
+ formatted_name = s.user.screen_name.rjust(justify + 1)
26
+ formatted_msg = ''
27
+
28
+ s.text.split(' ').each_with_index do |word, idx|
29
+ formatted_msg += "#{word} "
30
+
31
+ sixth_word = idx != 0 && idx % 6 == 0
32
+ formatted_msg += "\n#{indention}" if sixth_word
33
+ end
34
+
35
+ say "#{CGI::unescapeHTML(formatted_name)}: #{CGI::unescapeHTML(formatted_msg)}\n#{indention}#{formatted_time}\n\n"
36
+ end
37
+
38
+ Configuration["#{options[:since_prefix]}_since_id"] = options[:reverse] ? collection.last.id : collection.first.id
39
+ else
40
+ say(options[:empty_msg])
41
+ end
42
+ end
43
+
44
+ def base(username=current_account.username, password=current_account.password)
45
+ @base ||= Twitter::Base.new(username, password)
46
+ end
47
+
48
+ def current_account
49
+ @current_account ||= Account.active
50
+ raise Account.count == 0 ? NoAccounts : NoActiveAccount if @current_account.nil?
51
+ @current_account
52
+ end
53
+
54
+ def attempt_import(&block)
55
+ tweet_file = File.join(ENV['HOME'], '.twitter')
56
+ if File.exists?(tweet_file)
57
+ say '.twitter file found, attempting import...'
58
+ config = YAML::load(File.read(tweet_file))
59
+ if !config['email'].nil? && !config['password'].nil?
60
+ Account.add(:username => config['email'], :password => config['password'])
61
+ say 'Account imported'
62
+ block.call if block_given?
63
+ true
64
+ else
65
+ say "Either your username or password were blank in your .twitter file so I could not import. Use 'twitter add' to add an account."
66
+ false
67
+ end
68
+ end
69
+ end
70
+
71
+ def do_work(&block)
72
+ connect
73
+ begin
74
+ block.call
75
+ rescue Twitter::RateExceeded
76
+ say("Twitter says you've been making too many requests. Wait for a bit and try again.")
77
+ rescue Twitter::Unavailable
78
+ say("Twitter is unavailable right now. Try again later.")
79
+ rescue Twitter::CantConnect => msg
80
+ say("Can't connect to twitter because: #{msg}")
81
+ rescue Twitter::CLI::Helpers::NoActiveAccount
82
+ say("You have not set an active account. Use 'twitter change' to set one now.")
83
+ rescue Twitter::CLI::Helpers::NoAccounts
84
+ unless attempt_import { block.call }
85
+ say("You have not created any accounts. Use 'twitter add' to create one now.")
86
+ end
87
+ end
88
+ end
89
+
90
+ def connect
91
+ ActiveRecord::Base.logger = Logger.new('/tmp/twitter_ar_logger.log')
92
+ ActiveRecord::Base.establish_connection(Twitter::CLI::Config)
93
+ ActiveRecord::Base.connection
94
+ end
95
+
96
+ def migrate
97
+ connect
98
+ ActiveRecord::Migrator.migrate("#{CLI_ROOT}/migrations/")
99
+ end
100
+
101
+ def connect_and_migrate
102
+ say('Attempting to establish connection...')
103
+ connect
104
+ say('Connection established...migrating database...')
105
+ migrate
106
+ end
107
+ end
108
+ end
109
+ end