stocktwits 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
+