Permalink
Browse files

initial commit

  • Loading branch information...
0 parents commit a415c1a8dead5ee5e243d879571a7155eb6fa967 @digitalhobbit committed Nov 8, 2009
Showing with 276 additions and 0 deletions.
  1. +8 −0 README.markdown
  2. +112 −0 public/stylesheets/style.css
  3. +20 −0 twatcher.rb
  4. +35 −0 tweet.rb
  5. +35 −0 tweet_store.rb
  6. +26 −0 twitter_filter.rb
  7. +17 −0 views/index.haml
  8. +11 −0 views/layout.haml
  9. +12 −0 views/tweets.haml
@@ -0,0 +1,8 @@
+twatcher-lite
+=============
+
+This is a simplified version of the code that powers [Twatcher](http://twatcher.com). It mainly goes along with [my blog post]() and provides an easier way to follow along than manually copying and pasting the code from the blog into the various files.
+
+Please refer to the blog post for instructions on how to install and run this application.
+
+I'll eventually publish the full project, which includes configuration options, RSpec specs, etc. Stay tuned. :)
@@ -0,0 +1,112 @@
+body {
+ background-color: #505;
+}
+
+h1 {
+ text-align: center;
+ color: #ee0;
+ font-size: 300%;
+ font-family: "Chalkboard", "Comic Sans MS", sans-serif;
+}
+
+#container {
+ margin: 1em auto;
+ position: relative;
+ width: 540px;
+ text-align: left;
+}
+
+#content {
+ width: 100%;
+ margin: 0px;
+ padding: 0px;
+}
+
+ul.tweets {
+ margin: 50px 0 0 0;
+ list-style: none;
+ background-color: #dfd;
+ padding: 0px 10px;
+}
+
+li:first-child.tweet {
+ border-top: none;
+}
+
+li.tweet {
+ position: relative;
+ border-top: 1px dashed #aaa;
+ padding: 0.7em 0px 0.6em 0px;
+ margin: 0px 0px;
+}
+
+li.latest {
+ display: none;
+}
+
+.avatar {
+ display: block;
+ height: 50px;
+ width: 50px;
+ position: absolute;
+ left: 0px;
+ margin: 0px 10px 0px 0px;
+ overflow: hidden;
+}
+
+.avatar a {
+ text-decoration: none;
+}
+
+.avatar img {
+ height: 48px;
+ width: 48px;
+ border-color: transparent;
+ border-width: 0px;
+}
+
+.main {
+ display: block;
+ margin-left: 60px;
+ min-height: 50px;
+ width: 455px;
+ overflow: hidden;
+}
+
+.text {
+ margin-top: 0;
+ padding-top: 0;
+}
+
+.text a {
+ text-decoration: none;
+ color: #000;
+}
+
+.text a:hover {
+ text-decoration: underline;
+ color: #00f;
+}
+
+.meta {
+ display: block;
+ margin: 3px 0px 0px 20px;
+ color: #444;
+ font-size: 0.764em;
+}
+
+.meta a {
+ text-decoration: none;
+ color: #444;
+}
+
+.meta a:hover {
+ text-decoration: underline;
+ color: #00f;
+}
+
+.lol {
+ font-size: 130%;
+ font-weight: bold;
+ color: #f00;
+}
@@ -0,0 +1,20 @@
+require 'sinatra'
+require 'haml'
+require File.join(File.dirname(__FILE__), 'tweet_store')
+
+STORE = TweetStore.new
+
+get '/' do
+ @tweets = STORE.tweets
+ haml :index
+end
+
+get '/latest' do
+ # We're using a Javascript variable to keep track of the time the latest
+ # tweet was received, so we can request only newer tweets here. Might want
+ # to consider using Last-Modified HTTP header as a slightly cleaner
+ # solution (but requires more jQuery code).
+ @tweets = STORE.tweets(5, (params[:since] || 0).to_i)
+ @tweet_class = 'latest' # So we can hide and animate
+ haml :tweets, :layout => false
+end
@@ -0,0 +1,35 @@
+class Tweet
+
+ def initialize(data)
+ @data = data
+ end
+
+ def user_link
+ "http://twitter.com/#{username}"
+ end
+
+ # Makes links clickable, highlights LOL, etc.
+ def filtered_text
+ filter_lol(filter_urls(text))
+ end
+
+ private
+
+ # So we can call tweet.text instead of tweet['text']
+ def method_missing(name)
+ @data[name.to_s]
+ end
+
+ def filter_lol(text)
+ # Note that we're using a list of characters rather than just \b to avoid
+ # replacing LOL inside a URL.
+ text.gsub(/^(.*[\s\.\,\;])?(lol)(\b)/i, '\1<span class="lol">\2</span>\3')
+ end
+
+ def filter_urls(text)
+ # The regex could probably still be improved, but this seems to do the
+ # trick for most cases.
+ text.gsub(/(https?:\/\/\w+(\.\w+)+(\/[\w\+\-\,\%]+)*(\?[\w\[\]]+(=\w*)?(&\w+(=\w*)?)*)?(#\w+)?)/i, '<a href="\1">\1</a>')
+ end
+
+end
@@ -0,0 +1,35 @@
+require 'json'
+require 'redis'
+require File.join(File.dirname(__FILE__), 'tweet')
+
+class TweetStore
+
+ REDIS_KEY = 'tweets'
+ NUM_TWEETS = 20
+ TRIM_THRESHOLD = 100
+
+ def initialize
+ @db = Redis.new
+ @trim_count = 0
+ end
+
+ # Retrieves the specified number of tweets, but only if they are more recent
+ # than the specified timestamp.
+ def tweets(limit=15, since=0)
+ @db.list_range(REDIS_KEY, 0, limit - 1).collect {|t|
+ Tweet.new(JSON.parse(t))
+ }.reject {|t| t.received_at <= since} # In 1.8.7, should use drop_while instead
+ end
+
+ def push(data)
+ @db.push_head(REDIS_KEY, data.to_json)
+
+ @trim_count += 1
+ if (@trim_count > 100)
+ # Periodically trim the list so it doesn't grow too large.
+ @db.list_trim(REDIS_KEY, 0, NUM_TWEETS)
+ @trim_count = 0
+ end
+ end
+
+end
@@ -0,0 +1,26 @@
+require 'tweetstream'
+require File.join(File.dirname(__FILE__), 'tweet_store')
+
+USERNAME = "my_username" # Replace with your Twitter user
+PASSWORD = "my_password" # and your Twitter password
+STORE = TweetStore.new
+
+TweetStream::Client.new(USERNAME, PASSWORD).track('lol') do |status|
+ # Ignore replies. Probably not relevant in your own filter app, but we want
+ # to filter out funny tweets that stand on their own, not responses.
+ if status.text !~ /^@\w+/
+ # Yes, we could just store the Status object as-is, since it's actually just a
+ # subclass of Hash. But Twitter results include lots of fields that we don't
+ # care about, so let's keep it simple and efficient for the web app.
+ STORE.push(
+ 'id' => status[:id],
+ 'text' => status.text,
+ 'username' => status.user.screen_name,
+ 'userid' => status.user[:id],
+ 'name' => status.user.name,
+ 'profile_image_url' => status.user.profile_image_url,
+ 'created_at' => status.created_at,
+ 'received_at' => Time.new.to_i
+ )
+ end
+end
@@ -0,0 +1,17 @@
+:javascript
+ function refreshTweets() {
+ $.get('/latest', {since: window.latestTweet}, function(data) {
+ $('.tweets').prepend(data);
+ $('.latest').slideDown('slow');
+ $('.tweets li:gt(50)').remove();
+
+ setTimeout(refreshTweets, 10000);
+ });
+ }
+ $(function() {
+ setTimeout(refreshTweets, 10000);
+ });
+
+%h1 Recent LOL Tweets
+%ul.tweets
+ = haml :tweets, :layout => false
@@ -0,0 +1,11 @@
+!!! Strict
+%html{:xmlns=> "http://www.w3.org/1999/xhtml", 'xml:lang' => "en", :lang => "en"}
+ %head
+ %meta{'http-equiv' => "Content-Type", 'content' => "text/html; charset=utf-8"}
+ %title twatcher
+ %link{:rel => 'stylesheet', :href => '/stylesheets/style.css', :type => 'text/css'}
+ %script{:type => 'text/javascript', :src => 'http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js'}
+ %body
+ #container
+ #content
+ = yield
@@ -0,0 +1,12 @@
+- @tweets.each do |tweet|
+ %li.tweet{:class => @tweet_class}
+ %span.avatar
+ %a{:href => tweet.user_link}
+ %img{:src => tweet.profile_image_url, :alt => tweet.username, :height => 48, :width => 48}
+ %span.main
+ %span.text= tweet.filtered_text
+ %span.meta== &#8212; #{tweet.name} (<a href="#{tweet.user_link}">@#{tweet.username}</a>)
+
+- if !@tweets.empty?
+ :javascript
+ window.latestTweet = #{@tweets[0].received_at};

0 comments on commit a415c1a

Please sign in to comment.