stocktwits 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.
Files changed (49) hide show
  1. data/.document +5 -0
  2. data/.gitignore +21 -0
  3. data/LICENSE +20 -0
  4. data/README +0 -0
  5. data/README.rdoc +17 -0
  6. data/Rakefile +63 -0
  7. data/VERSION +1 -0
  8. data/app/controllers/sessions_controller.rb +68 -0
  9. data/app/models/stocktwits/basic_user.rb +64 -0
  10. data/app/models/stocktwits/generic_user.rb +123 -0
  11. data/app/models/stocktwits/oauth_user.rb +46 -0
  12. data/app/models/stocktwits/plain_user.rb +53 -0
  13. data/app/views/sessions/_login.html.erb +18 -0
  14. data/app/views/sessions/new.html.erb +5 -0
  15. data/config/routes.rb +6 -0
  16. data/generators/stocktwits/USAGE +12 -0
  17. data/generators/stocktwits/stocktwits_generator.rb +42 -0
  18. data/generators/stocktwits/templates/migration.rb +20 -0
  19. data/generators/stocktwits/templates/stocktwits.yml +66 -0
  20. data/generators/stocktwits/templates/user.rb +5 -0
  21. data/lib/stocktwits.rb +103 -0
  22. data/lib/stocktwits/controller_extensions.rb +72 -0
  23. data/lib/stocktwits/cryptify.rb +30 -0
  24. data/lib/stocktwits/dispatcher/basic.rb +46 -0
  25. data/lib/stocktwits/dispatcher/oauth.rb +26 -0
  26. data/lib/stocktwits/dispatcher/plain.rb +44 -0
  27. data/lib/stocktwits/dispatcher/shared.rb +42 -0
  28. data/rails/init.rb +6 -0
  29. data/spec/application.rb +1 -0
  30. data/spec/controllers/controller_extensions_spec.rb +162 -0
  31. data/spec/controllers/sessions_controller_spec.rb +221 -0
  32. data/spec/debug.log +397 -0
  33. data/spec/fixtures/config/twitter_auth.yml +17 -0
  34. data/spec/fixtures/factories.rb +28 -0
  35. data/spec/fixtures/fakeweb.rb +18 -0
  36. data/spec/fixtures/stocktwits.rb +5 -0
  37. data/spec/models/stocktwits/basic_user_spec.rb +138 -0
  38. data/spec/models/stocktwits/generic_user_spec.rb +146 -0
  39. data/spec/models/stocktwits/oauth_user_spec.rb +100 -0
  40. data/spec/schema.rb +25 -0
  41. data/spec/spec.opts +1 -0
  42. data/spec/spec_helper.rb +107 -0
  43. data/spec/stocktwits/cryptify_spec.rb +51 -0
  44. data/spec/stocktwits/dispatcher/basic_spec.rb +83 -0
  45. data/spec/stocktwits/dispatcher/oauth_spec.rb +72 -0
  46. data/spec/stocktwits/dispatcher/shared_spec.rb +26 -0
  47. data/spec/stocktwits_spec.rb +173 -0
  48. data/stocktwits.gemspec +116 -0
  49. metadata +158 -0
@@ -0,0 +1,53 @@
1
+ require 'net/http'
2
+
3
+ module Stocktwits
4
+ module PlainUser
5
+ def self.included(base)
6
+ base.class_eval do
7
+
8
+ end
9
+
10
+ base.extend Stocktwits::PlainUser::ClassMethods
11
+ end
12
+
13
+ module ClassMethods
14
+ def verify_credentials(login, password)
15
+
16
+ response = Stocktwits.net.start { |http|
17
+ request = Net::HTTP::Get.new(Stocktwits.base_url + "/users/show/#{login}.json")
18
+ http.request(request)
19
+ }
20
+
21
+ if response.code == '200'
22
+ JSON.parse(response.body)
23
+ else
24
+ false
25
+ end
26
+ end
27
+
28
+ def authenticate(login, password = nil)
29
+ if stocktwits_hash = verify_credentials(login, password)
30
+ user = identify_or_create_from_stocktwits_hash_and_password(stocktwits_hash, password)
31
+ user
32
+ else
33
+ nil
34
+ end
35
+ end
36
+
37
+ def identify_or_create_from_stocktwits_hash_and_password(stocktwits_hash, password)
38
+ if user = User.find_by_stocktwits_id(stocktwits_hash['id'].to_s)
39
+ user.login = stocktwits_hash['login']
40
+ user.assign_stocktwits_attributes(stocktwits_hash)
41
+ user.save
42
+ user
43
+ else
44
+ user = User.new_from_stocktwits_hash(stocktwits_hash)
45
+ user.save
46
+ user
47
+ end
48
+ end
49
+
50
+ end
51
+ end
52
+ end
53
+
@@ -0,0 +1,18 @@
1
+ <% form_tag session_path, :id => 'login_form' do %>
2
+ <div class='field'>
3
+ <label for='login'>Stocktwits Username:</label>
4
+ <%= text_field_tag 'login', nil, :class => 'text_field' %><br/>
5
+ <em>On "plain" strategy, you need only the login</em>
6
+ </div>
7
+ <div class='field'>
8
+ <label for='password'>Password:</label>
9
+ <%= password_field_tag 'password', nil, :class => 'password_field' %>
10
+ </div>
11
+ <!--<div class='checkbox-field'>
12
+ <%= check_box_tag 'remember_me' %> <label for='remember_me'>Keep Me Logged In</label>
13
+ </div>-->
14
+ <div class='field submit'>
15
+ <%= submit_tag 'Log In', :class => 'submit' %>
16
+ </div>
17
+ <% end %>
18
+
@@ -0,0 +1,5 @@
1
+ <h1>Log In Via StockTwits</h1>
2
+
3
+ <p>This application utilizes your Stocktwits username and password for authentication; you do not have to create a separate account here. To log in, just enter your Stocktwits credentials in the form below.</p>
4
+
5
+ <%= render :partial => 'login_form' %>
data/config/routes.rb ADDED
@@ -0,0 +1,6 @@
1
+ ActionController::Routing::Routes.draw do |map|
2
+ map.login '/login', :controller => 'sessions', :action => 'new'
3
+ map.logout '/logout', :controller => 'sessions', :action => 'destroy'
4
+ map.resource :session
5
+ map.oauth_callback '/oauth_callback', :controller => 'sessions', :action => 'oauth_callback'
6
+ end
@@ -0,0 +1,12 @@
1
+ Stocktwits Generator
2
+ =====================
3
+
4
+ The Stocktwits generator allows you to generate the components necessary to implement Twitter as a Single Sign-On provider for your site.
5
+
6
+ To run it, you simply need to call it:
7
+
8
+ script/generate stocktwits
9
+
10
+ This will generate the migration necessary for the users table as well as generate a User model that extends the appropriate Stocktwits model template and a config/stocktwits.yml that allows you to set your OAuth consumer key and secret (if required).
11
+
12
+ By default, Stocktwits uses Plain as its authentication strategy. If you wish to use HTTP Basic you can pass in the --basic option or OAuth if you pass the -O or --oauth option.
@@ -0,0 +1,42 @@
1
+ class StocktwitsGenerator < Rails::Generator::Base
2
+ default_options :oauth => false, :basic => false, :plain => true
3
+
4
+ def manifest
5
+ record do |m|
6
+ m.class_collisions 'User'
7
+
8
+ m.migration_template 'migration.rb', 'db/migrate', :migration_file_name => 'stocktwits_migration'
9
+ m.template 'user.rb', File.join('app','models','user.rb')
10
+ m.template 'stocktwits.yml', File.join('config','stocktwits.yml')
11
+ end
12
+ end
13
+
14
+ protected
15
+
16
+ def banner
17
+ "Usage: #{$0} stocktwits"
18
+ end
19
+
20
+ def add_options!(opt)
21
+ opt.separator ''
22
+ opt.separator 'Options:'
23
+
24
+ opt.on('-O', '--oauth', 'Use the OAuth authentication strategy to connect to Stocktwits. (default)') { |v|
25
+ options[:oauth] = v
26
+ options[:basic] = !v
27
+ options[:plain] = !v
28
+ }
29
+
30
+ opt.on('-B', '--basic', 'Use the HTTP Basic authentication strategy to connect to Stocktwits.') { |v|
31
+ options[:basic] = v
32
+ options[:oauth] = !v
33
+ options[:plain] = !v
34
+ }
35
+
36
+ opt.on('-P', '--plain', 'Use a plain HTTP request strategy with no auth to connect to Stocktwits.') { |v|
37
+ options[:plain] = v
38
+ options[:basic] = !v
39
+ options[:oauth] = !v
40
+ }
41
+ end
42
+ end
@@ -0,0 +1,20 @@
1
+ class StocktwitsMigration < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :users do |t|
4
+ t.string :stocktwits_id
5
+ t.string :login
6
+ # t.string :access_token - uncomment if you change your are planning to use the "oauth" strategy
7
+ # t.string :access_secret - uncomment if you change your are planning to use the "oauth" strategy
8
+
9
+ # t.binary :crypted_password - uncomment if you change your are planning to use the "basic" strategy
10
+ # t.string :salt - uncomment if you change your are planning to use the "basic" strategy
11
+
12
+
13
+ t.timestamps
14
+ end
15
+ end
16
+
17
+ def self.down
18
+ drop_table :users
19
+ end
20
+ end
@@ -0,0 +1,66 @@
1
+ # OAuth example
2
+ # ------------
3
+ # development:
4
+ # strategy: oauth
5
+ # oauth_consumer_key: devkey
6
+ # oauth_consumer_secret: devsecret
7
+ # base_url: "http://api.stocktwits.com"
8
+ # authorize_path: "/oauth/authenticate"
9
+ # api_timeout: 10
10
+ # remember_for: 14 # days
11
+ # oauth_callback: "http://localhost:3000/oauth_callback"
12
+ # test:
13
+ # strategy: oauth
14
+ # oauth_consumer_key: testkey
15
+ # oauth_consumer_secret: testsecret
16
+ # base_url: "http://api.stocktwits.com"
17
+ # authorize_path: "/oauth/authenticate"
18
+ # api_timeout: 10
19
+ # remember_for: 14 # days
20
+ # oauth_callback: "http://localhost:3000/oauth_callback"
21
+ # production:
22
+ # strategy: oauth
23
+ # oauth_consumer_key: prodkey
24
+ # oauth_consumer_secret: prodsecret
25
+ # authorize_path: "/oauth/authenticate"
26
+ # base_url: "http://api.stocktwits.com"
27
+ # api_timeout: 10
28
+ # remember_for: 14 # days
29
+
30
+ # Basic Example
31
+ # -------------
32
+ # development:
33
+ # strategy: basic
34
+ # api_timeout: 10
35
+ # base_url: "http://api.stocktwits.com"
36
+ # # randomly generated key for encrypting Stocktwits passwords
37
+ # encryption_key: "<%= key = ActiveSupport::SecureRandom.hex(12) %>"
38
+ # remember_for: 14 # days
39
+ # test:
40
+ # strategy: basic
41
+ # api_timeout: 10
42
+ # base_url: "http://api.stocktwits.com"
43
+ # encryption_key: "<%= key %>"
44
+ # remember_for: 14 # days
45
+ # production:
46
+ # strategy: basic
47
+ # api_timeout: 10
48
+ # encryption_key: "<%= key %>"
49
+ # remember_for: 14 # days
50
+
51
+ # Plain Example
52
+ # -------------
53
+ # development:
54
+ # strategy: plain
55
+ # api_timeout: 10
56
+ # base_url: "http://api.stocktwits.com"
57
+ # remember_for: 14 # days
58
+ # test:
59
+ # strategy: basic
60
+ # api_timeout: 10
61
+ # base_url: "http://api.stocktwits.com"
62
+ # remember_for: 14 # days
63
+ # production:
64
+ # strategy: basic
65
+ # api_timeout: 10
66
+ # remember_for: 14 # days
@@ -0,0 +1,5 @@
1
+ class User < Stocktwits::GenericUser
2
+ # Extend and define your user model as you see fit.
3
+ # All of the authentication logic is handled by the
4
+ # parent Stocktwits::GenericUser class.
5
+ end
data/lib/stocktwits.rb ADDED
@@ -0,0 +1,103 @@
1
+ module Stocktwits
2
+ class Error < StandardError; end
3
+
4
+ def self.config(environment=Rails.env)
5
+ @config ||= {}
6
+ @config[environment] ||= YAML.load(File.open(Rails.root.to_s + '/config/stocktwits.yml').read)[environment]
7
+ end
8
+
9
+ def self.base_url
10
+ config['base_url'] || 'https://api.stocktwits.com'
11
+ end
12
+
13
+ def self.path_prefix
14
+ URI.parse(base_url).path
15
+ end
16
+
17
+ def self.api_timeout
18
+ config['api_timeout'] || 10
19
+ end
20
+
21
+ def self.encryption_key
22
+ if strategy != :plain
23
+ raise Stocktwits::Cryptify::Error, 'You must specify an encryption_key in config/stocktwits.yml' if config['encryption_key'].blank?
24
+ end
25
+ config['encryption_key']
26
+ end
27
+
28
+ def self.oauth_callback?
29
+ config.key?('oauth_callback')
30
+ end
31
+
32
+ def self.oauth_callback
33
+ config['oauth_callback']
34
+ end
35
+
36
+ def self.remember_for
37
+ (config['remember_for'] || 14).to_i
38
+ end
39
+
40
+ def self.strategy
41
+ strat = config['strategy']
42
+ raise ArgumentError, 'Invalid StockTwits Strategy: Valid strategies are oauth and basic.' unless %w(oauth basic plain).include?(strat)
43
+ strat.to_sym
44
+ rescue Errno::ENOENT
45
+ :basic
46
+ end
47
+
48
+ def self.oauth?
49
+ strategy == :oauth
50
+ end
51
+
52
+ def self.basic?
53
+ strategy == :basic
54
+ end
55
+
56
+ def self.plain?
57
+ strategy == :plain
58
+ end
59
+
60
+ def self.consumer
61
+ options = {:site => Stocktwits.base_url}
62
+ [ :authorize_path,
63
+ :request_token_path,
64
+ :access_token_path,
65
+ :scheme,
66
+ :signature_method ].each do |oauth_option|
67
+ options[oauth_option] = Stocktwits.config[oauth_option.to_s] if Stocktwits.config[oauth_option.to_s]
68
+ end
69
+
70
+ OAuth::Consumer.new(
71
+ config['oauth_consumer_key'],
72
+ config['oauth_consumer_secret'],
73
+ options
74
+ )
75
+ end
76
+
77
+ def self.net
78
+ uri = URI.parse(Stocktwits.base_url)
79
+ net = Net::HTTP.new(uri.host, uri.port)
80
+ net.use_ssl = uri.scheme == 'https'
81
+ net.read_timeout = Stocktwits.api_timeout
82
+ net
83
+ end
84
+
85
+ def self.authorize_path
86
+ config['authorize_path'] || '/oauth/authorize'
87
+ end
88
+ end
89
+
90
+ require 'stocktwits/controller_extensions'
91
+ require 'stocktwits/cryptify'
92
+ require 'stocktwits/dispatcher/shared'
93
+ require 'stocktwits/dispatcher/oauth'
94
+ require 'stocktwits/dispatcher/basic'
95
+ require 'stocktwits/dispatcher/plain'
96
+
97
+
98
+ module Stocktwits
99
+ module Dispatcher
100
+ class Error < StandardError; end
101
+ class Unauthorized < Error; end
102
+ end
103
+ end
@@ -0,0 +1,72 @@
1
+ module Stocktwits
2
+ # These methods borrow HEAVILY from Rick Olsen's
3
+ # Restful Authentication. All cleverness props
4
+ # go to him, not me.
5
+ module ControllerExtensions
6
+ def self.included(base)
7
+ base.send :helper_method, :current_user, :logged_in?, :authorized?
8
+ end
9
+
10
+ protected
11
+
12
+ def authentication_failed(message, destination='/')
13
+ flash[:error] = message
14
+ redirect_to destination
15
+ end
16
+
17
+ def authentication_succeeded(message = 'You have logged in successfully.', destination = '/')
18
+ flash[:notice] = message
19
+ redirect_back_or_default destination
20
+ end
21
+
22
+ def current_user
23
+ @current_user ||=
24
+ if session[:user_id]
25
+ User.find_by_id(session[:user_id])
26
+ elsif cookies[:remember_token]
27
+ User.from_remember_token(cookies[:remember_token])
28
+ else
29
+ false
30
+ end
31
+ end
32
+
33
+ def current_user=(new_user)
34
+ session[:user_id] = new_user.id
35
+ @current_user = new_user
36
+ end
37
+
38
+ def authorized?
39
+ !!current_user
40
+ end
41
+
42
+ def login_required
43
+ authorized? || access_denied
44
+ end
45
+
46
+ def access_denied
47
+ store_location
48
+ redirect_to login_path
49
+ end
50
+
51
+ def store_location
52
+ session[:return_to] = request.request_uri
53
+ end
54
+
55
+ def redirect_back_or_default(default)
56
+ redirect_to(session[:return_to] || default)
57
+ session[:return_to] = nil
58
+ end
59
+
60
+ def logged_in?
61
+ !!current_user
62
+ end
63
+
64
+ def logout_keeping_session!
65
+ session[:user_id] = nil
66
+ @current_user = nil
67
+ cookies.delete(:remember_token)
68
+ end
69
+ end
70
+ end
71
+
72
+ ActionController::Base.send(:include, Stocktwits::ControllerExtensions)
@@ -0,0 +1,30 @@
1
+ module Stocktwits
2
+ module Cryptify
3
+ class Error < StandardError; end
4
+
5
+ def self.encrypt(data)
6
+ salt = generate_salt
7
+ {:encrypted_data => EzCrypto::Key.encrypt_with_password(Stocktwits.encryption_key, salt, data), :salt => salt}
8
+ end
9
+
10
+ def self.decrypt(encrypted_data_or_hash, salt=nil)
11
+ case encrypted_data_or_hash
12
+ when String
13
+ encrypted_data = encrypted_data_or_hash
14
+ raise ArgumentError, 'Must provide a salt to decrypt.' unless salt
15
+ when Hash
16
+ encrypted_data = encrypted_data_or_hash[:encrypted_data]
17
+ salt = encrypted_data_or_hash[:salt]
18
+ else
19
+ raise ArgumentError, 'Must provide either an encrypted hash result or encrypted string and salt.'
20
+ end
21
+
22
+ EzCrypto::Key.decrypt_with_password(Stocktwits.encryption_key, salt, encrypted_data)
23
+ end
24
+
25
+ def self.generate_salt
26
+ ActiveSupport::SecureRandom.hex(4)
27
+ end
28
+ end
29
+ end
30
+