Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit.

  • Loading branch information...
commit 9708516e62d00572985f79b483189454f0cc39a2 0 parents
@twe4ked authored
1  .gitignore
@@ -0,0 +1 @@
+.sass-cache
23 Gemfile
@@ -0,0 +1,23 @@
+source :rubygems
+
+gem 'sinatra-reloader'
+gem 'rack-flash3', :require => 'rack-flash'
+gem 'sinatra', :require => 'sinatra/base'
+gem 'compass'
+gem 'twitter'
+gem 'oauth'
+gem 'sass'
+gem 'thin'
+gem 'haml'
+gem 'rake'
+
+# DataMapper
+gem 'dm-core'
+gem 'dm-timestamps'
+gem 'dm-migrations'
+gem 'dm-validations'
+gem 'dm-postgres-adapter'
+
+group :development, :test do
+ gem 'pry'
+end
103 Gemfile.lock
@@ -0,0 +1,103 @@
+GEM
+ remote: http://rubygems.org/
+ specs:
+ activesupport (3.2.3)
+ i18n (~> 0.6)
+ multi_json (~> 1.0)
+ addressable (2.2.8)
+ backports (2.5.1)
+ chunky_png (1.2.5)
+ coderay (1.0.6)
+ compass (0.12.1)
+ chunky_png (~> 1.2)
+ fssm (>= 0.2.7)
+ sass (~> 3.1)
+ daemons (1.1.8)
+ data_objects (0.10.8)
+ addressable (~> 2.1)
+ dm-core (1.2.0)
+ addressable (~> 2.2.6)
+ dm-do-adapter (1.2.0)
+ data_objects (~> 0.10.6)
+ dm-core (~> 1.2.0)
+ dm-migrations (1.2.0)
+ dm-core (~> 1.2.0)
+ dm-postgres-adapter (1.2.0)
+ dm-do-adapter (~> 1.2.0)
+ do_postgres (~> 0.10.6)
+ dm-timestamps (1.2.0)
+ dm-core (~> 1.2.0)
+ dm-validations (1.2.0)
+ dm-core (~> 1.2.0)
+ do_postgres (0.10.8)
+ data_objects (= 0.10.8)
+ eventmachine (0.12.10)
+ faraday (0.8.0)
+ multipart-post (~> 1.1)
+ fssm (0.2.9)
+ haml (3.1.4)
+ i18n (0.6.0)
+ method_source (0.7.1)
+ multi_json (1.3.2)
+ multipart-post (1.1.5)
+ oauth (0.4.6)
+ pry (0.9.8.4)
+ coderay (~> 1.0.5)
+ method_source (~> 0.7.1)
+ slop (>= 2.4.4, < 3)
+ rack (1.4.1)
+ rack-flash3 (1.0.1)
+ rack
+ rack
+ rack-protection (1.2.0)
+ rack
+ rack-test (0.6.1)
+ rack (>= 1.0)
+ rake (0.9.2.2)
+ sass (3.1.16)
+ simple_oauth (0.1.7)
+ sinatra (1.3.2)
+ rack (~> 1.3, >= 1.3.6)
+ rack-protection (~> 1.2)
+ tilt (~> 1.3, >= 1.3.3)
+ sinatra-contrib (1.3.1)
+ backports (>= 2.0)
+ eventmachine
+ rack-protection
+ rack-test
+ sinatra (~> 1.3.0)
+ tilt (~> 1.3)
+ sinatra-reloader (1.0)
+ sinatra-contrib
+ slop (2.4.4)
+ thin (1.3.1)
+ daemons (>= 1.0.9)
+ eventmachine (>= 0.12.6)
+ rack (>= 1.0.0)
+ tilt (1.3.3)
+ twitter (2.2.5)
+ activesupport (>= 2.3.9, < 4)
+ faraday (~> 0.8)
+ multi_json (~> 1.3)
+ simple_oauth (~> 0.1.6)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ compass
+ dm-core
+ dm-migrations
+ dm-postgres-adapter
+ dm-timestamps
+ dm-validations
+ haml
+ oauth
+ pry
+ rack-flash3
+ rake
+ sass
+ sinatra
+ sinatra-reloader
+ thin
+ twitter
12 README.md
@@ -0,0 +1,12 @@
+tmp/follow
+==========
+
+Development
+-----------
+
+You will need to set the following environment variables.
+
+ ENV['TWITTER_CONSUMER_KEY']
+ ENV['TWITTER_CONSUMER_SECRET']
+
+To start Sinatra use `rackup`.
45 Rakefile
@@ -0,0 +1,45 @@
+namespace :db do
+ desc 'Auto-migrate the database (destroys data)'
+ task :migrate => :environment do
+ DataMapper.auto_migrate!
+ end
+
+ desc 'Auto-upgrade the database (preserves data)'
+ task :upgrade => :environment do
+ DataMapper.auto_upgrade!
+ end
+end
+
+# This task is called by the Heroku scheduler add-on
+desc 'Unfollow all users that need to be unfollowed today'
+task :unfollow_users => :environment do
+ # find all users that need to be unfollowed today
+ unfollows = Unfollow.all(:date => Date.today)
+
+ puts "Unfollowing #{unfollows.size} users..."
+
+ unfollows.each do |unfollow|
+ client = Twitter::Client.new(
+ :oauth_token => unfollow.oauth_token,
+ :oauth_token_secret => unfollow.oauth_token_secret
+ )
+
+ # unfollow user
+ client.unfollow unfollow.user
+
+ # TODO: catch errors of the user is unfollowed twice
+ # notify the user of the unfollow
+ client.direct_message_create client.current_user.id, "tmp/follow has unfollowed @#{unfollow.user} for you."
+
+ puts "Unfollowed '#{unfollow.user}'"
+
+ # remove record
+ unfollow.destroy
+ end
+
+ puts 'Done.'
+end
+
+task :environment do
+ require './tmpfollow'
+end
3  config.ru
@@ -0,0 +1,3 @@
+require './tmpfollow'
+
+run TmpFollow
14 lib/fixnum.rb
@@ -0,0 +1,14 @@
+class Fixnum
+ def ordinalize
+ if (11..13).include?(self % 100)
+ "#{self}th"
+ else
+ case self % 10
+ when 1; "#{self}st"
+ when 2; "#{self}nd"
+ when 3; "#{self}rd"
+ else "#{self}th"
+ end
+ end
+ end
+end
13 lib/unfollow.rb
@@ -0,0 +1,13 @@
+require 'dm-timestamps'
+
+class Unfollow
+ include DataMapper::Resource
+
+ property :id, Serial
+ property :user, String, :required => true
+ property :oauth_token, String, :required => true
+ property :oauth_token_secret, String, :required => true
+ property :date, Date, :required => true
+
+ timestamps :at
+end
BIN  public/background.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  public/logo.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
141 tmpfollow.rb
@@ -0,0 +1,141 @@
+ENV['RACK_ENV'] ||= 'development'
+
+require 'bundler'
+Bundler.require(:default, ENV['RACK_ENV'].to_sym)
+
+require 'logger'
+require 'sinatra/reloader' if Sinatra::Base.development?
+
+class TmpFollow < Sinatra::Base
+ enable :sessions
+ use Rack::Flash
+
+ configure do
+ Dir.glob('./lib/*.rb') { |file| require file }
+
+ database_url = ENV['SHARED_DATABASE_URL'] || "postgres://#{ENV['USER']}@localhost/tmpfollow_#{development? ? 'development' : 'test'}"
+
+ DataMapper.setup(:default, database_url)
+ DataMapper::Logger.new($stdout, :debug)
+
+ Twitter.configure do |config|
+ config.consumer_key = ENV['TWITTER_CONSUMER_KEY']
+ config.consumer_secret = ENV['TWITTER_CONSUMER_SECRET']
+ end
+
+ Compass.configuration do |config|
+ config.project_path = File.dirname(__FILE__)
+ config.sass_dir = 'views'
+ end
+
+ set :haml, {format: :html5}
+ end
+
+ helpers do
+ def flashes
+ [:notice, :alert].map do |type|
+ "<p class='flash #{type}'>#{flash[type]}</p>" if flash[type] != nil
+ end.join("\n")
+ end
+ end
+
+ get '/' do
+ haml :index
+ end
+
+ before '/follow' do
+ # validate username matches twitter username format
+ # TODO: remove the '@' if it exists
+ unless params[:user] =~ /^[A-Za-z0-9_]+/
+ flash[:alert] = 'Username invalid.'
+ redirect to '/'
+ end
+
+ # validate days is an integer
+ unless is_numeric?(params[:days]) || params[:days] == ''
+ flash[:alert] = 'Not a number.'
+ redirect to '/'
+ end
+ end
+
+ post '/follow' do
+ unfollow = Unfollow.new.tap do |u|
+ u.user = params[:user]
+ u.date = Date.today + params[:days].to_i
+ u.oauth_token = session[:oauth_token]
+ u.oauth_token_secret = session[:oauth_token_secret]
+ end
+
+ client = Twitter::Client.new(
+ :oauth_token => unfollow.oauth_token,
+ :oauth_token_secret => unfollow.oauth_token_secret
+ )
+ client.follow unfollow.user
+
+ if unfollow.save
+ flash[:notice] = "@#{unfollow.user} will be unfollowed on #{unfollow.date.strftime("%A, %b #{unfollow.date.day.ordinalize}")}!"
+ else
+ flash[:alert] = 'Not saved.'
+ end
+
+ redirect to '/'
+ end
+
+ # redirects the user to Twitter for authentication
+ get '/twitter' do
+ callback_url = "#{base_url}/twitter/callback"
+ request_token = oauth_consumer.get_request_token(:oauth_callback => callback_url)
+ session[:request_token] = request_token.token
+ session[:request_token_secret] = request_token.secret
+ redirect request_token.authorize_url
+ end
+
+ # used by Twitter as the callback URL after the user has authenticated
+ get '/twitter/callback' do
+ request_token = OAuth::RequestToken.new(oauth_consumer, session[:request_token], session[:request_token_secret])
+ begin
+ @oauth_tokens = request_token.get_access_token(
+ {},
+ :oauth_token => params[:oauth_token],
+ :oauth_verifier => params[:oauth_verifier]
+ )
+ rescue OAuth::Unauthorized => exception
+ flash[:alert] = 'Authenticating with Twitter failed.'
+ redirect to '/'
+ # exception.message
+ end
+
+ # store oauth_token and oauth_token_secret in the session
+ session[:oauth_token] = @oauth_tokens.token
+ session[:oauth_token_secret] = @oauth_tokens.secret
+
+ redirect to '/'
+ end
+
+ get '/clear' do
+ session[:oauth_token] = nil
+ session[:oauth_token_secret] = nil
+
+ flash[:alert] = 'Tokens cleared.'
+ redirect to '/'
+ end
+
+ get '/application.css' do
+ scss :application, Compass.sass_engine_options
+ end
+
+ private
+
+ def base_url
+ port = (request.port == 80) ? "" : ":#{request.port.to_s}"
+ "http://#{request.host}#{port}"
+ end
+
+ def oauth_consumer
+ OAuth::Consumer.new(ENV['TWITTER_CONSUMER_KEY'], ENV['TWITTER_CONSUMER_SECRET'], :site => 'https://twitter.com')
+ end
+
+ def is_numeric?(str)
+ !!str.to_s.match(/\A[+-]?\d+?(\.\d+)?\Z/)
+ end
+end
85 views/application.scss
@@ -0,0 +1,85 @@
+@import 'compass/reset';
+@import 'compass/css3';
+
+$twitter: #4099ff;
+
+body{
+ background:#ddd;
+ font:12px/120% 'Helvetica Neue', sans-serif;
+ text-align:center;
+ padding:40px 0 0;
+
+ @include text-shadow(0 1px 0 #fff);
+
+ h1{
+ text-indent:-9001px;
+ background:url('logo.png');
+ width:130px;
+ height:26px;
+ margin:0 auto 4px;
+ }
+
+ p{margin-bottom:20px;
+ &.flash{
+ &.alert{color:darkred;}
+ &.notice{color:darkgreen;}
+ }
+ }
+
+ a{color:#000;}
+
+ form{
+ font-size:14px;
+ margin-bottom:20px;
+
+ input{
+ font:14px/120% 'Helvetica Neue', sans-serif;
+ padding:0px;
+ border:none;
+ border-bottom:1px solid #666;
+ background:#ddd;
+ width:100px;
+
+ &:focus{outline:none;}
+
+ &#days{
+ width:20px;
+ text-align:center;
+ }
+
+ &#at{
+ width:16px;
+ margin-right:-8px;
+ }
+
+ &[type=submit]{
+ width:auto;
+ border:none;
+ background:#488fe0;
+ padding:2px 6px 3px;
+ color:#fff;
+ border:1px solid darken($twitter, 15%);
+
+ @include text-shadow(0 1px 0 darken($twitter, 20%));
+ @include box-shadow(0 1px 0 darken($twitter, 30%));
+ @include background(linear-gradient(darken($twitter, 5%), $twitter));
+ @include border-radius(4px);
+
+ &:hover{@include background(linear-gradient($twitter, lighten($twitter, 5%)));}
+ &:hover{@include background(linear-gradient(lighten($twitter, 5%), $twitter));}
+ }
+ }
+ }
+
+ footer{
+ background:#000 url('background.png');
+ color:#eee;
+ position:absolute;
+ bottom:0;
+ width:100%;
+ padding:40px 0;
+
+ @include box-shadow(inset 0px 5px 8px -2px #000);
+ @include text-shadow(0 1px 0 #000);
+ }
+}
11 views/index.haml
@@ -0,0 +1,11 @@
+- if session[:oauth_token] == nil && session[:oauth_token_secret] == nil
+ %a{:href => '/twitter'} Authenticate with Twitter
+- else
+ %form{action: '/follow', method: 'post'}
+ Follow
+ %input{disabled: true, value: '@', id: 'at'}
+ %input{name: 'user', id: 'user', autocomplete: 'off', value: params[:user]}
+ for
+ %input{name: 'days', id: 'days', autocomplete: 'off', value: params[:days]}
+ days
+ %input{type: 'submit', value: 'tmp/follow'}
15 views/layout.haml
@@ -0,0 +1,15 @@
+!!! 5
+%html
+ %head
+ %title tmp/follow
+ %link{href: '/application.css', rel: 'stylesheet', type: 'text/css'}
+ %body
+ = flashes
+
+ %h1 tmp/follow
+ %p Temporarily follow people on Twitter
+
+ = yield
+
+ %footer
+ Made by Odin Dutton
Please sign in to comment.
Something went wrong with that request. Please try again.