stocktwits 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README +0 -0
- data/README.rdoc +17 -0
- data/Rakefile +63 -0
- data/VERSION +1 -0
- data/app/controllers/sessions_controller.rb +68 -0
- data/app/models/stocktwits/basic_user.rb +64 -0
- data/app/models/stocktwits/generic_user.rb +123 -0
- data/app/models/stocktwits/oauth_user.rb +46 -0
- data/app/models/stocktwits/plain_user.rb +53 -0
- data/app/views/sessions/_login.html.erb +18 -0
- data/app/views/sessions/new.html.erb +5 -0
- data/config/routes.rb +6 -0
- data/generators/stocktwits/USAGE +12 -0
- data/generators/stocktwits/stocktwits_generator.rb +42 -0
- data/generators/stocktwits/templates/migration.rb +20 -0
- data/generators/stocktwits/templates/stocktwits.yml +66 -0
- data/generators/stocktwits/templates/user.rb +5 -0
- data/lib/stocktwits.rb +103 -0
- data/lib/stocktwits/controller_extensions.rb +72 -0
- data/lib/stocktwits/cryptify.rb +30 -0
- data/lib/stocktwits/dispatcher/basic.rb +46 -0
- data/lib/stocktwits/dispatcher/oauth.rb +26 -0
- data/lib/stocktwits/dispatcher/plain.rb +44 -0
- data/lib/stocktwits/dispatcher/shared.rb +42 -0
- data/rails/init.rb +6 -0
- data/spec/application.rb +1 -0
- data/spec/controllers/controller_extensions_spec.rb +162 -0
- data/spec/controllers/sessions_controller_spec.rb +221 -0
- data/spec/debug.log +397 -0
- data/spec/fixtures/config/twitter_auth.yml +17 -0
- data/spec/fixtures/factories.rb +28 -0
- data/spec/fixtures/fakeweb.rb +18 -0
- data/spec/fixtures/stocktwits.rb +5 -0
- data/spec/models/stocktwits/basic_user_spec.rb +138 -0
- data/spec/models/stocktwits/generic_user_spec.rb +146 -0
- data/spec/models/stocktwits/oauth_user_spec.rb +100 -0
- data/spec/schema.rb +25 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +107 -0
- data/spec/stocktwits/cryptify_spec.rb +51 -0
- data/spec/stocktwits/dispatcher/basic_spec.rb +83 -0
- data/spec/stocktwits/dispatcher/oauth_spec.rb +72 -0
- data/spec/stocktwits/dispatcher/shared_spec.rb +26 -0
- data/spec/stocktwits_spec.rb +173 -0
- data/stocktwits.gemspec +116 -0
- 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
|
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
|
+
|