Permalink
Browse files

initial commit

  • Loading branch information...
0 parents commit db81cdaedbdba042be20caffddaa5ef3e99166a4 @zapnap committed Jan 30, 2009
@@ -0,0 +1,2 @@
+*.db
+log/*.log
@@ -0,0 +1,20 @@
+Copyright (c) 2009 Nick Plante
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,25 @@
+= Retweet: A Twitter Application Template
+
+A base application template for building simple Twitter web apps with Sinatra and DataMapper. If you just want to build a simple keyword-based aggregator (such as http://tweetdreams.org), all you need to do is edit <tt>environment.rb</tt> to set the name of your app and the API search keyword(s), edit the CSS, and go.
+
+== Configuration
+
+Dependencies and all configuration is done in <tt>environment.rb</tt>. Your database is also set up here. DataMapper will use sqlite3 by default. Tests use the sqlite3-memory adapter (no configuration needed).
+
+Add your controller actions in <tt>application.rb</tt>. Views for these actions are placed in the <tt>views</tt> directory. Static files, including a stock stylesheet, go in the <tt>public</tt> directory. Models go in the <tt>lib</tt> directory and are auto-loaded.
+
+== Testing
+
+Rspec is included in the template. Add your specs in <tt>spec</tt>; just require <tt>spec_helper.rb</tt> to pre-configure the test environment. To run the specs:
+
+ rake spec
+
+== Getting Started
+
+ rake db:migrate
+ rake twitter:update
+ ruby application.rb
+
+== Status Updates
+
+Run <tt>rake twitter:update</tt> to update the cached tweets. You can schedule this as a cron job to fire every few minutes if you like.
@@ -0,0 +1,35 @@
+require 'environment'
+require 'spec/rake/spectask'
+
+task :default => :test
+task :test => :spec
+
+if !defined?(Spec)
+ puts "spec targets require RSpec"
+else
+ desc "Run all examples"
+ Spec::Rake::SpecTask.new('spec') do |t|
+ t.spec_files = FileList['spec/**/*.rb']
+ t.spec_opts = ['-cfs']
+ end
+end
+
+namespace :db do
+ desc 'Auto-migrate the database (destroys data)'
+ task :migrate do
+ DataMapper.auto_migrate!
+ end
+
+ desc 'Auto-upgrade the database (preserves data)'
+ task :upgrade do
+ DataMapper.auto_upgrade!
+ end
+end
+
+namespace :twitter do
+ desc 'Update the local status cache'
+ task :update do
+ count = Status.update
+ puts "#{count} new status updates retrieved"
+ end
+end
@@ -0,0 +1,37 @@
+require 'sinatra'
+require 'environment'
+
+configure do
+ set :views, "#{File.dirname(__FILE__)}/views"
+end
+
+error do
+ e = request.env['sinatra.error']
+ puts e.to_s
+ puts e.backtrace.join('\n')
+ 'Application error'
+end
+
+helpers do
+ def highlight(text)
+ SiteConfig.search_keywords.each do |keyword|
+ text = text.gsub(/(#{keyword})/i, '<span class="highlight">\1</span>')
+ end
+ activate_links(text)
+ end
+
+ def activate_links(text)
+ text.gsub(/((https?:\/\/|www\.)([-\w\.]+)+(:\d+)?(\/([\w\/_\.]*(\?\S+)?)?)?)/, '<a href="\1">\1</a>'). \
+ gsub(/@(\w+)/, '<a href="http://twitter.com/\1">@\1</a>')
+ end
+
+ def profile_link(user_name)
+ "<a href=\"http://twitter.com/#{user_name}\">#{user_name}</a>"
+ end
+end
+
+# root page
+get '/' do
+ @statuses = Status.all(:order => [:created_at.desc], :limit => SiteConfig.status_length)
+ haml :main
+end
@@ -0,0 +1,11 @@
+require 'application'
+
+set :run, false
+set :environment, :production
+
+FileUtils.mkdir_p 'log' unless File.exists?('log')
+log = File.new("log/sinatra.log", "a")
+STDOUT.reopen(log)
+STDERR.reopen(log)
+
+run Sinatra::Application
@@ -0,0 +1,28 @@
+require 'rubygems'
+require 'dm-core'
+require 'dm-validations'
+require 'dm-aggregates'
+require 'haml'
+require 'ostruct'
+require 'twitter'
+
+require 'sinatra' unless defined?(Sinatra)
+
+configure do
+ SiteConfig = OpenStruct.new(
+ :title => 'Your Twitter App', # title of application
+ :author => 'zapnap', # your twitter user name for attribution
+ :url_base => 'http://localhost:4567/', # base URL for your site
+ :search_keywords => ['thundercats', 'snarf'], # search API keyword
+ :status_length => 20 # number of tweets to display
+ )
+
+ DataMapper.setup(:default, "sqlite3:///#{File.expand_path(File.dirname(__FILE__))}/#{Sinatra::Base.environment}.db")
+
+ # load models
+ $LOAD_PATH.unshift("#{File.dirname(__FILE__)}/lib")
+ Dir.glob("#{File.dirname(__FILE__)}/lib/*.rb") { |lib| require File.basename(lib, '.*') }
+end
+
+# prevent Object#id warnings
+Object.send(:undef_method, :id)
@@ -0,0 +1,55 @@
+class Status
+ include DataMapper::Resource
+
+ ATTR_MAP = {
+ :id => :twitter_id,
+ :text => :text,
+ :from_user => :from_user_name,
+ :from_user_id => :from_user_id,
+ :to_user => :to_user_name,
+ :to_user_id => :to_user_id,
+ :profile_image_url => :profile_image_url,
+ :created_at => :created_at
+ }
+
+ property :id, Serial
+ property :twitter_id, Integer
+ property :text, String, :length => 0..255
+ property :from_user_id, Integer
+ property :from_user_name, String, :length => 0..255
+ property :to_user_id, Integer
+ property :to_user_name, String, :length => 0..255
+ property :profile_image_url, String, :length => 0..255
+ property :created_at, DateTime
+
+ validates_present :twitter_id, :text, :from_user_id, :from_user_name, :created_at
+ validates_is_unique :twitter_id
+
+ # create a new record from Twitter status data
+ def self.create_from_twitter(status_data)
+ s = self.new
+ ATTR_MAP.each { |k,v| s.send("#{v.to_s}=", status_data.send(k)) }
+ s.save
+ s
+ end
+
+ # updates the local status cache from Twitter, returns number of new messages
+ def self.update
+ count = 0
+ begin
+ SiteConfig.search_keywords.each do |keyword|
+ Twitter::Search.new(keyword).each do |s|
+ unless self.first(:twitter_id => s.id)
+ self.create_from_twitter(s)
+ count += 1
+ end
+ end
+ end
+
+ rescue SocketError => e
+ puts e
+ end
+
+ count
+ end
+end
@@ -0,0 +1,15 @@
+$(document).ready(function() {
+ $('ul.status li:first').addClass('selected');
+
+ $(this).everyTime(10000, function() {
+ prev = $('li.selected');
+ next = prev.next('li:first');
+
+ if (next.length == 0) {
+ next = $('ul.status li:first');
+ }
+
+ prev.hide().removeClass('selected');
+ next.show().addClass('selected');
+ });
+});
Oops, something went wrong.
@@ -0,0 +1,140 @@
+jQuery.fn.extend({
+ everyTime: function(interval, label, fn, times, belay) {
+ return this.each(function() {
+ jQuery.timer.add(this, interval, label, fn, times, belay);
+ });
+ },
+ oneTime: function(interval, label, fn) {
+ return this.each(function() {
+ jQuery.timer.add(this, interval, label, fn, 1);
+ });
+ },
+ stopTime: function(label, fn) {
+ return this.each(function() {
+ jQuery.timer.remove(this, label, fn);
+ });
+ }
+});
+
+jQuery.extend({
+ timer: {
+ guid: 1,
+ global: {},
+ regex: /^([0-9]+)\s*(.*s)?$/,
+ powers: {
+ // Yeah this is major overkill...
+ 'ms': 1,
+ 'cs': 10,
+ 'ds': 100,
+ 's': 1000,
+ 'das': 10000,
+ 'hs': 100000,
+ 'ks': 1000000
+ },
+ timeParse: function(value) {
+ if (value == undefined || value == null)
+ return null;
+ var result = this.regex.exec(jQuery.trim(value.toString()));
+ if (result[2]) {
+ var num = parseInt(result[1], 10);
+ var mult = this.powers[result[2]] || 1;
+ return num * mult;
+ } else {
+ return value;
+ }
+ },
+ add: function(element, interval, label, fn, times, belay) {
+ var counter = 0;
+
+ if (jQuery.isFunction(label)) {
+ if (!times)
+ times = fn;
+ fn = label;
+ label = interval;
+ }
+
+ interval = jQuery.timer.timeParse(interval);
+
+ if (typeof interval != 'number' || isNaN(interval) || interval <= 0)
+ return;
+
+ if (times && times.constructor != Number) {
+ belay = !!times;
+ times = 0;
+ }
+
+ times = times || 0;
+ belay = belay || false;
+
+ if (!element.$timers)
+ element.$timers = {};
+
+ if (!element.$timers[label])
+ element.$timers[label] = {};
+
+ fn.$timerID = fn.$timerID || this.guid++;
+
+ var handler = function() {
+ if (belay && this.inProgress)
+ return;
+ this.inProgress = true;
+ if ((++counter > times && times !== 0) || fn.call(element, counter) === false)
+ jQuery.timer.remove(element, label, fn);
+ this.inProgress = false;
+ };
+
+ handler.$timerID = fn.$timerID;
+
+ if (!element.$timers[label][fn.$timerID])
+ element.$timers[label][fn.$timerID] = window.setInterval(handler,interval);
+
+ if ( !this.global[label] )
+ this.global[label] = [];
+ this.global[label].push( element );
+
+ },
+ remove: function(element, label, fn) {
+ var timers = element.$timers, ret;
+
+ if ( timers ) {
+
+ if (!label) {
+ for ( label in timers )
+ this.remove(element, label, fn);
+ } else if ( timers[label] ) {
+ if ( fn ) {
+ if ( fn.$timerID ) {
+ window.clearInterval(timers[label][fn.$timerID]);
+ delete timers[label][fn.$timerID];
+ }
+ } else {
+ for ( var fn in timers[label] ) {
+ window.clearInterval(timers[label][fn]);
+ delete timers[label][fn];
+ }
+ }
+
+ for ( ret in timers[label] ) break;
+ if ( !ret ) {
+ ret = null;
+ delete timers[label];
+ }
+ }
+
+ for ( ret in timers ) break;
+ if ( !ret )
+ element.$timers = null;
+ }
+ }
+ }
+});
+
+if (jQuery.browser.msie)
+ jQuery(window).one("unload", function() {
+ var global = jQuery.timer.global;
+ for ( var label in global ) {
+ var els = global[label], i = els.length;
+ while ( --i )
+ jQuery.timer.remove(els[i], label);
+ }
+ });
Oops, something went wrong.

0 comments on commit db81cda

Please sign in to comment.