Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

first commit

  • Loading branch information...
commit f66f8d6a52b5186ac315e3cb2d9133991a1859e2 0 parents
Sven Fuchs authored
18 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 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 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  lib/command/poller.rb
@@ -0,0 +1,3 @@
+module Command::Poller
+ autoload :Twitter, 'command/poller/twitter'
+end
37 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 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 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 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 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
Please sign in to comment.
Something went wrong with that request. Please try again.