Skip to content

Commit

Permalink
mtweet
Browse files Browse the repository at this point in the history
  • Loading branch information
steveyen committed Jan 6, 2011
1 parent 61f221c commit d828016
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 58 deletions.
20 changes: 13 additions & 7 deletions README.md
@@ -1,21 +1,20 @@
Retwis-RB
mtweet
=========

An example Twitter application using the Redis key-value database.

Daniel Lucraft (dan@fluentradical.com)
An example Twitter application using the Membase NoSQL database.

Requirements
------------

* Ruby
* Sinatra: sudo gem install sinatra
* Redis: http://code.google.com/p/redis/
* Memcache-client: sudo gem install memcache-client
* Membase: http://membase.org

Starting Application
--------------------

Make sure the redis server is running.
Make sure the membase server is running (at 127.0.0.1:11211).

Run:

ruby app.rb
Expand All @@ -24,3 +23,10 @@ License
-------

MIT

Thanks
------

Daniel Lucraft (dan@fluentradical.com) for the original Retwis-rb
at http://danlucraft.com/blog/tag/retwis-rb

11 changes: 3 additions & 8 deletions app.rb
Expand Up @@ -2,19 +2,14 @@
require 'rubygems'
require 'sinatra'
require 'erb'
require 'rubyredis'

require 'store'
require 'domain'
require 'login-signup'

set :sessions, true

def redis
$redis ||= RedisClient.new(:timeout => nil)
end

before do
keys = redis.keys("*")
def mb
$mb ||= Store.new(['127.0.0.1:11211'])
end

get '/' do
Expand Down
65 changes: 28 additions & 37 deletions domain.rb
Expand Up @@ -3,7 +3,7 @@ class Timeline
def self.page(page)
from = (page-1)*10
to = (page)*10
post_ids = redis.list_range("timeline", from, to)
post_ids = mb.list_range("timeline", from, to)
post_ids.map {|post_id| Post.new(post_id)}
end
end
Expand All @@ -27,42 +27,42 @@ def #{name}
end
def _#{name}
redis.get("#{klass}:id:" + id.to_s + ":#{name}")
mb.get("#{klass}:id:" + id.to_s + ":#{name}")
end
def #{name}=(val)
redis.set("#{klass}:id:" + id.to_s + ":#{name}", val)
mb.set("#{klass}:id:" + id.to_s + ":#{name}", val)
end
RUBY
end
end

class User < Model
def self.find_by_username(username)
if id = redis.get("user:username:#{username}")
if id = mb.get("user:username:#{username}")
User.new(id)
end
end

def self.find_by_id(id)
if redis.key?("user:id:#{id}:username")
if mb.exists?("user:id:#{id}:username")
User.new(id)
end
end

def self.create(username, password)
user_id = redis.incr("user:uid")
user_id = mb.incr("user:uid")
salt = User.new_salt
redis.set("user:id:#{user_id}:username", username)
redis.set("user:username:#{username}", user_id)
redis.set("user:id:#{user_id}:salt", salt)
redis.set("user:id:#{user_id}:hashed_password", hash_pw(salt, password))
redis.push_head("users", user_id)
mb.set("user:id:#{user_id}:username", username)
mb.set("user:username:#{username}", user_id)
mb.set("user:id:#{user_id}:salt", salt)
mb.set("user:id:#{user_id}:hashed_password", hash_pw(salt, password))
mb.push("users", user_id)
User.new(user_id)
end

def self.new_users
redis.list_range("users", 0, 10).map do |user_id|
mb.list_range("users", 0, 10).map do |user_id|
User.new(user_id)
end
end
Expand All @@ -82,85 +82,85 @@ def self.hash_pw(salt, password)

def posts(page=1)
from, to = (page-1)*10, page*10
redis.list_range("user:id:#{id}:posts", from, to).map do |post_id|
mb.list_range("user:id:#{id}:posts", from, to).map do |post_id|
Post.new(post_id)
end
end

def timeline(page=1)
from, to = (page-1)*10, page*10
redis.list_range("user:id:#{id}:timeline", from, to).map do |post_id|
mb.list_range("user:id:#{id}:timeline", from, to).map do |post_id|
Post.new(post_id)
end
end

def mentions(page=1)
from, to = (page-1)*10, page*10
redis.list_range("user:id:#{id}:mentions", from, to).map do |post_id|
mb.list_range("user:id:#{id}:mentions", from, to).map do |post_id|
Post.new(post_id)
end
end

def add_post(post)
redis.push_head("user:id:#{id}:posts", post.id)
redis.push_head("user:id:#{id}:timeline", post.id)
mb.push("user:id:#{id}:posts", post.id)
mb.push("user:id:#{id}:timeline", post.id)
end

def add_timeline_post(post)
redis.push_head("user:id:#{id}:timeline", post.id)
mb.push("user:id:#{id}:timeline", post.id)
end

def add_mention(post)
redis.push_head("user:id:#{id}:mentions", post.id)
mb.push("user:id:#{id}:mentions", post.id)
end

def follow(user)
return if user == self
redis.set_add("user:id:#{id}:followees", user.id)
mb.set_add("user:id:#{id}:followees", user.id)
user.add_follower(self)
end

def stop_following(user)
redis.set_delete("user:id:#{id}:followees", user.id)
mb.set_delete("user:id:#{id}:followees", user.id)
user.remove_follower(self)
end

def following?(user)
redis.set_member?("user:id:#{id}:followees", user.id)
mb.set_member?("user:id:#{id}:followees", user.id)
end

def followers
redis.set_members("user:id:#{id}:followers").map do |user_id|
mb.set_members("user:id:#{id}:followers").map do |user_id|
User.new(user_id)
end
end

def followees
redis.set_members("user:id:#{id}:followees").map do |user_id|
mb.set_members("user:id:#{id}:followees").map do |user_id|
User.new(user_id)
end
end

protected

def add_follower(user)
redis.set_add("user:id:#{id}:followers", user.id)
mb.set_add("user:id:#{id}:followers", user.id)
end

def remove_follower(user)
redis.set_delete("user:id:#{id}:followers", user.id)
mb.set_delete("user:id:#{id}:followers", user.id)
end
end

class Post < Model
def self.create(user, content)
post_id = redis.incr("post:uid")
post_id = mb.incr("post:uid")
post = Post.new(post_id)
post.content = content
post.user_id = user.id
post.created_at = Time.now.to_s
post.user.add_post(post)
redis.push_head("timeline", post_id)
mb.push("timeline", post_id)
post.user.followers.each do |follower|
follower.add_timeline_post(post)
end
Expand All @@ -183,12 +183,3 @@ def user
User.new(user_id)
end
end









2 changes: 1 addition & 1 deletion login-signup.rb
Expand Up @@ -26,7 +26,7 @@
post '/signup' do
if params[:username] !~ /^\w+$/
@signup_error = "Username must only contain letters, numbers and underscores."
elsif redis.key?("user:username:#{params[:username]}")
elsif mb.exists?("user:username:#{params[:username]}")
@signup_error = "That username is taken."
elsif params[:username].length < 4
@signup_error = "Username must be at least 4 characters"
Expand Down
70 changes: 70 additions & 0 deletions store.rb
@@ -0,0 +1,70 @@
# A storage abstraction over memcached-client API.
#
require 'memcache'

class Store < MemCache
def to_s
"Store to #{self.servers}"
end

def exists?(key)
self.get(key) != nil
end

def incr(key)
n = super(key)
return n if n
self.add(key, '0', 0, true)
self.incr(key)
end

def list_range(key, from, to)
list = self.get(key, true) # Ex: nil or "+1+2+3".
return [] unless list
(list.split('+').drop(1) || []).slice(from, to - from + 1)
end

def push(key, value, prefix="+")
# Lists are '+' delimited.
if self.prepend(key, "#{prefix}#{value}").match("NOT_STORED")
self.add(key, '', 0, true)
self.push(key, value)
end
end

def set_members_hash(key)
s = self.get(key, true) # Ex: nil or "+1+2-1"
return {} unless s
acts = s.split(/[^\+-]/) # Ex: ["+", "+", "-"]
vals = s.split(/[\+-]/).drop(1) # Ex: ["1", "2", "1"]
m = {}
i = acts.length - 1
while i >= 0
val = vals[i]
if acts[i] == '+'
m[val] = true
else
m.delete(val)
end
i = i - 1
end
m
end

def set_members(key)
self.set_members_hash(key).keys()
end

def set_add(key, value)
self.push(key, value, prefix="+")
end

def set_delete(key, value)
self.push(key, value, prefix="-")
end

def set_member?(key, value)
self.set_members_hash(key)[value]
end
end

7 changes: 4 additions & 3 deletions views/footer.erb
Expand Up @@ -2,9 +2,10 @@
</div>
<div class="span-24 last">
<hr />
Retwis-RB is a simple Twitter clone written in Ruby and Sinatra to show off
the <a href="http://code.google.com/p/redis/">Redis key-value database</a>.
The code is on <a href="http://github.com/danlucraft/retwis-rb">Github</a>.
mtweet is a simple twitter clone that uses
the <a href="http://www.membase.org">Membase database</a>.
Thanks to <a href="http://github.com/danlucraft/retwis-rb">retwis-rb</a>
for the original fork source.
</div>
</div>
</body>
Expand Down
4 changes: 2 additions & 2 deletions views/header.erb
@@ -1,6 +1,6 @@
<ntml>
<head>
<title>Retwis-RB</title>
<title>mtweet</title>
<link rel="stylesheet" href="/css/screen.css" type="text/css" media="screen, projection">
<link rel="stylesheet" href="/css/print.css" type="text/css" media="print">
<!--[if IE]>
Expand All @@ -13,7 +13,7 @@
<div class="container">
<div id="header" class="span-24">
<div class="span-12">
<h1>Retwis-RB</h1>
<h1>mtweet</h1>
</div>
<div class="span-12 last right-align">
<% if @logged_in_user %>
Expand Down

0 comments on commit d828016

Please sign in to comment.