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.
- 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
|
+
|