Permalink
Browse files

refactor towards extraction of Command as a base library

  • Loading branch information...
Sven Fuchs
Sven Fuchs committed Mar 28, 2010
1 parent 7d38677 commit 8f840dd5a241b3048589efba2f871ba02388b06b
View
2 NOTES
@@ -22,7 +22,7 @@ Sources:
@rugb !join # join the group @rugb
@rugb !attend # attend (?)
@rugb !visit # visit (?)
-
+
@rugb !add svenfuchs # add someone else
View
@@ -0,0 +1,46 @@
+h1. Identity
+
+Identity is an experimental spike at building a distributed identity aggregator. The current implementation is targeted at being used for the Ruby User Group Berlin's website. It hopefully will make it easy for speakers, members, visitors and generelly everybody interested in the group to publish information about themselves on the website.
+
+The general idea is to allow people to instruct a tiny Sinatra application to aggregate identity information through a messaging interface. Currently the only implemented interface uses Twitter replies but it's easy to imagine additional interfaces like IM, email and maybe others.
+
+In order to make the application work on Heroku it only sits there and waits for an http-authenticated ping. When pinged the application polls Twitter replies for a given account and responds to commands contained in these messages by aggregating profile information. It will store these identities to a remote CouchDB so it should be easy to share them with other projects.
+
+Let's say the Twitter account is \@rugb then the following replies will (currently) be recognized:
+
+ @rugb !create [profiles]
+ @rugb !update [profiles]
+ @rugb !join
+
+The !create and !update commands fetch identity information from the given profile sources. The !create command is basically just an alias for !update except that it will always fetch information from Twitter.
+
+Valid sources are either full key/value pairs or URLs identifying a profile source. The following arguments would constitute valid profile sources:
+
+ twitter:svenfuchs
+ github:svenfuchs
+
+ http://twitter.com/svenfuchs
+ http://github.com/svenfuchs
+ http://svenfuchs.com/me.json # pointing to a json doc w/ profile information
+ http://tinyurl.com/yc7t8bv # pointing to a json doc w/ profile information
+
+The !join command just adds the receiver account to a groups array for the given identity.
+
+Examples:
+
+ @rugb !update # creates an identity for the sender if none exists and
+ retrieves and stores profile information from Twitter
+
+ @rugb !update http://svenfuchs.com/me.json # adds the json hash
+
+ @rugb !join # simply adds "rugb" to my groups
+
+
+
+
+
+
+
+
+
+
View
3 app.rb
@@ -1,6 +1,7 @@
$: << File.expand_path('../lib', __FILE__)
require 'rubygems'
require 'sinatra'
+require 'command'
require 'identity'
# should be set through heroku config:add
@@ -29,6 +30,6 @@
get '/ping' do
protected!
- poller = Identity::Poller::Twitter.new(:reply, ENV['twitter_login'], ENV['twitter_password'])
+ poller = Command::Poller::Twitter.new(:reply, ENV['twitter_login'], ENV['twitter_password'])
poller.run!
end
View
@@ -8,4 +8,4 @@
:login => 'rugb_test',
:password => '...'
}
-Identity::Poller::Twitter.new(:reply, /!update/, :update, config).run!
+Command::Poller::Twitter.new(:reply, /!update/, :update, config).run!
View
@@ -0,0 +1,18 @@
+require 'active_support/inflector'
+require 'active_support/core_ext/module/delegation'
+
+class Command
+ autoload :Base, 'command/base'
+ autoload :Message, 'command/message'
+ autoload :Poller, 'command/poller'
+
+ class << self
+ def queue(receiver, message)
+ Message.if_unprocessed(message) do |message|
+ message.commands.each { |type| new(type, message).dispatch } # we don't have a queue yet, so just dispatch
+ end
+ end
+ end
+
+ include Base
+end
View
@@ -0,0 +1,14 @@
+module Command::Base
+ attr_reader :command, :message, :arguments
+ delegate :receiver, :source, :to => :message
+
+ def initialize(command, message)
+ @command = command
+ @message = message
+ @arguments = message.arguments
+ end
+
+ def dispatch
+ send(command)
+ end
+end
@@ -1,13 +1,13 @@
-class Identity::Message
+class Command::Message
class << self
def if_unprocessed(data)
return if find_by_message_id(data[:message_id])
yield create(data.merge(:received_at => Time.now))
end
- def max_message_id # TODO how the fuck would i not jump through all of these
+ def max_message_id # TODO how the fuck would i not jump through all of these hoops
database = CouchPotato.database.instance_variable_get(:@database)
- spec = Identity::Message.view_max_message_id
+ spec = view_max_message_id
query = CouchPotato::View::ViewQuery.new(database, spec.design_document, spec.view_name, spec.map_function, spec.reduce_function)
result = query.query_view!(spec.view_parameters)['rows'].first
result['value'] if result
@@ -19,8 +19,6 @@ def max_message_id # TODO how the fuck would i not jump through all of these
include SimplyStored::Couch
- # TODO gotta sanitize data ...
-
property :message_id
property :text
property :sender
@@ -30,7 +28,7 @@ def max_message_id # TODO how the fuck would i not jump through all of these
view :by_message_id, :key => :message_id
view :view_max_message_id, :type => :custom,
- :map => "function(doc) { if(doc.ruby_class == 'Identity::Message') { emit(null, doc['message_id']); } }",
+ :map => "function(doc) { if(doc.ruby_class == 'Command::Message') { emit(null, doc['message_id']); } }",
:reduce => "function(key, values, rereduce) { return Math.max.apply(Math, values); }"
def initialize(data = {})
View
@@ -0,0 +1,3 @@
+module Command::Poller
+ autoload :Twitter, 'command/poller/twitter'
+end
@@ -2,12 +2,12 @@
require 'twibot'
-class Identity::Poller::Twitter < Twibot::Bot
+class Command::Poller::Twitter < Twibot::Bot
def initialize(type, login, password)
super(Twibot::Config.default << {
:login => login,
:password => password,
- :process => Identity::Message.max_message_id,
+ :process => Command::Message.max_message_id,
:min_interval => 0,
:max_interval => 0
})
@@ -16,8 +16,8 @@ def initialize(type, login, password)
end
def handler(receiver)
- Twibot::Handler.new(Identity::Message::COMMAND_PATTERN) do |message, *args|
- Identity::Command.queue(receiver,
+ Twibot::Handler.new(Command::Message::COMMAND_PATTERN) do |message, *args|
+ Command.queue(receiver,
:receiver => receiver,
:message_id => message.id,
:sender => message.user.screen_name,
@@ -26,7 +26,7 @@ def handler(receiver)
)
end
end
-
+
def receive_messages
super.tap { @abort = true } # we only poll once
end
View
@@ -6,8 +6,6 @@
class Identity
autoload :Command, 'identity/command'
autoload :Helpers, 'identity/helpers'
- autoload :Message, 'identity/message'
- autoload :Poller, 'identity/poller'
autoload :Sources, 'identity/sources'
include SimplyStored::Couch
@@ -71,3 +69,7 @@ def to_json(*args)
{ :handles => handles, :profiles => profiles, :groups => groups, :created_at => created_at }.to_json
end
end
+
+class Command
+ include Identity::Command
+end
View
@@ -1,27 +1,31 @@
-require 'active_support/core_ext/module/delegation'
-require 'active_support/core_ext/string/inflections'
+module Identity::Command
+ def initialize(command, message)
+ super
+ @command = :create if command = :update && sender.created_at.nil?
+ end
+
+ def create
+ arguments << "#{message.source}:#{message.sender}" # i.e. on create we always pull the source's profile (e.g. twitter)
+ arguments.uniq!
+ update
+ end
-class Identity::Command
- autoload :Base, 'identity/command/base'
- autoload :Create, 'identity/command/create'
- autoload :Update, 'identity/command/update'
- autoload :Join, 'identity/command/join'
+ def join
+ sender.groups ||= []
+ sender.groups << receiver
+ sender.save
+ end
+
+ def update
+ Identity::Sources.update_all(sender, arguments)
+ sender.claim
+ sender.save
+ end
- class << self
- def queue(receiver, message)
- Identity::Message.if_unprocessed(message) do |message|
- commands(message).each { |cmd| Identity::Command.build(cmd, message).dispatch } # we don't have a queue yet, so just dispatch
- end
- end
-
- def build(type, message)
- const_get(type.to_s.camelize).new(message)
- end
+ protected
- def commands(message)
- commands = message.commands
- commands.unshift('create').delete('update') unless sender = Identity.find_by_handle(message.sender)
- commands.uniq
+ def sender
+ @sender ||= Identity.find_by_handle(message.sender) || Identity.new
end
- end
-end
+end
+
@@ -1,10 +0,0 @@
-class Identity::Command::Base
- attr_reader :sender, :message, :arguments
- delegate :receiver, :source, :to => :message
-
- def initialize(message)
- @sender = Identity.find_by_handle(message.sender) || Identity.new
- @message = message
- @arguments = message.arguments
- end
-end
@@ -1,10 +0,0 @@
-class Identity::Command
- class Create < Update
- def dispatch
- # i.e. on create make sure we pull the source's profile (e.g. twitter)
- arguments << "#{message.source}:#{message.sender}"
- arguments.uniq!
- super
- end
- end
-end
@@ -1,9 +0,0 @@
-class Identity::Command
- class Join < Base
- def dispatch
- sender.groups ||= []
- sender.groups << receiver
- sender.save
- end
- end
-end
@@ -1,9 +0,0 @@
-class Identity::Command
- class Update < Base
- def dispatch
- Identity::Sources.update_all(sender, arguments)
- sender.claim
- sender.save
- end
- end
-end
View
@@ -1,3 +0,0 @@
-module Identity::Poller
- autoload :Twitter, 'identity/poller/twitter'
-end
@@ -1,8 +1,8 @@
-require File.expand_path('../test_helper', __FILE__)
+require File.expand_path('../../test_helper', __FILE__)
class MessageTwitterTest < Test::Unit::TestCase
test "max_message_id returns the latest message_id" do
%w(12345 12346 12347 12348).each { |id| msg(id).save }
- assert_equal 12348, Identity::Message.max_message_id
+ assert_equal 12348, Command::Message.max_message_id
end
end
@@ -1,4 +1,4 @@
-require File.expand_path('../test_helper', __FILE__)
+require File.expand_path('../../test_helper', __FILE__)
require 'stringio'
class PollerTest < Test::Unit::TestCase
@@ -8,19 +8,19 @@ def setup
def process!(from, message, id = '12345')
status = twitter_status(from, message, id)
- bot = Identity::Poller::Twitter.new(:reply, 'rugb_test', 'password')
+ bot = Command::Poller::Twitter.new(:reply, 'rugb_test', 'password')
bot.handler('rugb_test').dispatch(status)
end
test "polls from twitter once and handles new replies by queueing commands" do
- Identity::Message.stubs(:max_message_id).returns(12345)
- poller = Identity::Poller::Twitter.new(:reply, 'rugb_test', 'password')
+ Command::Message.stubs(:max_message_id).returns(12345)
+ poller = Command::Poller::Twitter.new(:reply, 'rugb_test', 'password')
replies = [twitter_status('svenfuchs', '@rugb_test !update')]
poller.twitter.expects(:status).with(:replies, { :since_id => 12345 }).returns(replies)
message = { :message_id => '12345', :receiver => 'rugb_test', :sender => 'svenfuchs', :text => '@rugb_test !update', :source => 'twitter' }
- Identity::Command.expects(:queue).with('rugb_test', message)
+ Command.expects(:queue).with('rugb_test', message)
log = capture_stdout { poller.run! }
assert_match /imposing as @rugb_test/, log
@@ -53,7 +53,7 @@ def process!(from, message, id = '12345')
process!('svenfuchs', '!update')
Identity.find_by_handle('svenfuchs')
- message = Identity::Message.find_by_message_id('12345')
+ message = Command::Message.find_by_message_id('12345')
assert_equal 'rugb_test', message.receiver
assert_equal 'svenfuchs', message.sender
@@ -66,7 +66,7 @@ def process!(from, message, id = '12345')
process!('svenfuchs', '!update')
Identity.find_by_handle('svenfuchs')
- Identity::Command.expects(:new).never
+ Command.expects(:new).never # TODO
Identity.find_by_handle('svenfuchs')
end
Oops, something went wrong.

0 comments on commit 8f840dd

Please sign in to comment.