Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Sven Fuchs committed Mar 28, 2010
0 parents commit f66f8d6
Show file tree
Hide file tree
Showing 9 changed files with 270 additions and 0 deletions.
18 changes: 18 additions & 0 deletions lib/command.rb
@@ -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
14 changes: 14 additions & 0 deletions lib/command/base.rb
@@ -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
45 changes: 45 additions & 0 deletions lib/command/message.rb
@@ -0,0 +1,45 @@
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 hoops
database = CouchPotato.database.instance_variable_get(:@database)
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
end
end

COMMAND_PATTERN = /(?:!|#)([\w]+)/
ARGUMENT_PATTERN = /[\S]+:[\S]+/

include SimplyStored::Couch

property :message_id
property :text
property :sender
property :receiver
property :source
property :received_at

view :by_message_id, :key => :message_id
view :view_max_message_id, :type => :custom,
: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 = {})
data.each { |key, value| self.send("#{key}=", value) }
end

def commands
text.scan(COMMAND_PATTERN).flatten
end

def arguments
text.scan(ARGUMENT_PATTERN)
end
end
3 changes: 3 additions & 0 deletions lib/command/poller.rb
@@ -0,0 +1,3 @@
module Command::Poller
autoload :Twitter, 'command/poller/twitter'
end
37 changes: 37 additions & 0 deletions lib/command/poller/twitter.rb
@@ -0,0 +1,37 @@
# checks for new replies only once

require 'twibot'

class Command::Poller::Twitter < Twibot::Bot
def initialize(type, login, password)
super(Twibot::Config.default << {
:login => login,
:password => password,
:process => Command::Message.max_message_id,
:min_interval => 0,
:max_interval => 0
})

add_handler(type, handler(login))
end

def handler(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,
:text => message.text,
:source => 'twitter'
)
end
end

def receive_messages
super.tap { @abort = true } # we only poll once
end

def receive_replies
super.tap { @abort = true } # we only poll once
end
end
8 changes: 8 additions & 0 deletions test/message_test.rb
@@ -0,0 +1,8 @@
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, Command::Message.max_message_id
end
end
73 changes: 73 additions & 0 deletions test/poller_test.rb
@@ -0,0 +1,73 @@
require File.expand_path('../test_helper', __FILE__)
require 'stringio'

class PollerTest < Test::Unit::TestCase
def setup
setup_stubs
end

def process!(from, message, id = '12345')
status = twitter_status(from, message, id)
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
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' }
Command.expects(:queue).with('rugb_test', message)
log = capture_stdout { poller.run! }

assert_match /imposing as @rugb_test/, log
assert_match /Received 1 reply/, log
end

test 'updating w/ a me url and a github handle' do
process!('svenfuchs', '!update json:http://tinyurl.com/yc7t8bv github:svenphoox')
identity = Identity.find_by_handle('svenphoox')

assert_equal 'svenphoox', identity.github['handle']
assert_equal 'Sven', identity.github['name']
end

test 'updating an existing profile' do
process!('svenfuchs', '!create', '12345')
assert Identity.find_by_handle('svenfuchs')

process!('svenfuchs', '!update json:http://tinyurl.com/yc7t8bv', '12346')
identity = Identity.find_by_handle('svenfuchs')

assert_equal 'svenfuchs', identity.twitter['handle']
assert_equal 'Sven Fuchs', identity.twitter['name']
assert_equal 'svenfuchs', identity.json['irc']
end

test 'logs processed messages' do
now = Time.now
Time.stubs(:now).returns(now)

process!('svenfuchs', '!update')
Identity.find_by_handle('svenfuchs')
message = Command::Message.find_by_message_id('12345')

assert_equal 'rugb_test', message.receiver
assert_equal 'svenfuchs', message.sender
assert_equal '!update', message.text
assert_equal '12345', message.message_id
assert_equal now.to_s, Time.parse(message.received_at).to_s
end

test 'does not process an already processed message' do
process!('svenfuchs', '!update')
Identity.find_by_handle('svenfuchs')

Command.expects(:new).never # TODO
Identity.find_by_handle('svenfuchs')
end

end
26 changes: 26 additions & 0 deletions test/test_declarative.rb
@@ -0,0 +1,26 @@
module TestMethod
def self.included(base)
base.class_eval do
def self.test(name, &block)
test_name = "test_#{name.gsub(/\s+/,'_')}".to_sym
defined = instance_method(test_name) rescue false
raise "#{test_name} is already defined in #{self}" if defined
if block_given?
define_method(test_name, &block)
else
define_method(test_name) do
flunk "No implementation provided for #{name}"
end
end
end
end
end
end

class Module
include TestMethod
end

class Test::Unit::TestCase
include TestMethod
end
46 changes: 46 additions & 0 deletions test/test_helper.rb
@@ -0,0 +1,46 @@
$: << File.expand_path('../..', __FILE__)
$: << File.expand_path('../../lib', __FILE__)

require 'test/unit'
require 'rack/test'
require 'mocha'
require 'twibot'
require File.expand_path('../test_declarative', __FILE__)

require 'command'

CouchPotato::Config.database_name = "http://localhost:5984/command"

class Test::Unit::TestCase
def teardown
Command::Message.all.each { |message| message.delete }
end

def command(type, receiver, sender, text = '', source = 'twitter')
Command.new(type, msg(12345, text, sender, receiver, source))
end

def msg(id = 12345, text = 'text', sender = 'sender', receiver = 'receiver', source = 'twitter')
Command::Message.create :message_id => id,
:text => text,
:sender => sender,
:receiver => receiver,
:source => source,
:received_at => Time.now
end

def twitter_status(from, message, id = '12345')
Twitter::Status.new(:id => id, :user => twitter_sender(from), :text => message, :created_at => Time.now)
end

def twitter_sender(name)
Twitter::User.new(:screen_name => name)
end

def capture_stdout
@stdout, $stdout = $stdout, (io = StringIO.new)
yield
$stdout = @stdout
io.string
end
end

0 comments on commit f66f8d6

Please sign in to comment.