Permalink
Browse files

Broader match for channel names. Bug fixes.

  • Loading branch information...
1 parent 0188393 commit ed31a20c73224df61bb8c9fa267fc0770fd0717a @david committed Jun 21, 2008
Showing with 355 additions and 22 deletions.
  1. +2 −4 lib/minibot/commands.rb
  2. +2 −0 lib/minibot/constants.rb
  3. +1 −1 lib/minibot/daemon.rb
  4. +14 −9 lib/minibot/events.rb
  5. +121 −0 lib/minibot/server.rb
  6. +8 −8 spec/commands_spec.rb
  7. +207 −0 spec/server_spec.rb
View
6 lib/minibot/commands.rb
@@ -5,14 +5,12 @@ module Commands
include Events::Constants
def join(*channels)
- channels.each { |channel| write "JOIN #{channel}" }
+ channels.each { |channel| server.write "JOIN #{channel}" }
end
- TOPIC_REPLIES = [ RPL_NOTOPIC, [ RPL_TOPIC, RPL_TOPIC_META ], RPL_TOPIC ]
-
def topic(channel)
topic = author = timestamp = nil
- server.write "TOPIC #{channel}", *TOPIC_REPLIES do |code, reply|
+ server.write "TOPIC #{channel}", *EXPECTED_REPLIES_TOPIC do |code, reply|
if code == RPL_TOPIC
topic = reply
elsif code == RPL_TOPIC_META
View
2 lib/minibot/constants.rb
@@ -6,6 +6,8 @@ module Constants
RPL_TOPIC = "332"
RPL_TOPIC_META = "333"
ERR_NICKNAMEINUSE = "433"
+
+ EXPECTED_REPLIES_TOPIC = [ RPL_NOTOPIC, [ RPL_TOPIC, RPL_TOPIC_META ], RPL_TOPIC ]
end
end
end
View
2 lib/minibot/daemon.rb
@@ -41,7 +41,7 @@ def initialize(config)
end
def main_loop
- while msg = @server.next_message
+ while msg = @server.read
dispatch msg
end
end
View
23 lib/minibot/events.rb
@@ -44,25 +44,30 @@ def error(num, message)
private
def dispatch(srv_msg)
- if match = /^:(\w+)!.+ PRIVMSG (#\w+) :([^\001].+)/.match(srv_msg)
- message *match.values_at(2, 1, 3)
- elsif match = /^:(\w+)!.+ JOIN :(#\w+)/.match(srv_msg)
+ if match = /^:(\w+)!.+ PRIVMSG (\S+) :([^\001].+)/.match(srv_msg)
+ target, origin, message = *match.values_at(2, 1, 3)
+ unless target == @nick
+ message target, origin, message
+ else
+ private_message origin, message
+ end
+ elsif match = /^:(\w+)!.+ JOIN :(\S+)/.match(srv_msg)
user_joined *match.values_at(2, 1)
- elsif match = /^:(\w+)!.+ PART (#\w+)/.match(srv_msg)
+ elsif match = /^:(\w+)!.+ PART (\S+)/.match(srv_msg)
user_parted *match.values_at(2, 1)
- elsif match = /^:(\w+)!.+ PRIVMSG (#\w+) :\001ACTION (.+)\001/.match(srv_msg)
+ elsif match = /^:(\w+)!.+ PRIVMSG (\S+) :\001ACTION (.+)\001/.match(srv_msg)
user_action *match.values_at(2, 1, 3)
elsif match = /^:(\w+)!.+ PRIVMSG #{@nick} :(.+)/.match(srv_msg)
private_message *match.values_at(1, 2)
- elsif match = /^:(\w+)!.+ INVITE \w+ :(#\w+)/.match(srv_msg)
+ elsif match = /^:(\w+)!.+ INVITE \w+ :(\S+)/.match(srv_msg)
invited *match.values_at(2, 1)
elsif match = /^PING/.match(srv_msg)
pinged
- elsif match = /^:(\w+)!.+ TOPIC (#\w+) :(.+)/.match(srv_msg)
+ elsif match = /^:(\w+)!.+ TOPIC (\S+) :(.+)/.match(srv_msg)
topic_changed *match.values_at(2, 1, 3)
- elsif match = /^:(\w+)!.+ KICK (#\w+) #{@nick} :(.+)/.match(srv_msg)
+ elsif match = /^:(\w+)!.+ KICK (\S+) #{@nick} :(.+)/.match(srv_msg)
kicked *match.values_at(2, 1, 3)
- elsif match = /^:(\w+)!.+ KICK (#\w+) (\w+) :(.+)/.match(srv_msg)
+ elsif match = /^:(\w+)!.+ KICK (\S+) (\w+) :(.+)/.match(srv_msg)
user_kicked *match.values_at(2, 1, 3, 4)
elsif match = /^:\S+ #{RPL_WELCOME} .*?(:.*)?$/.match(srv_msg)
ready
View
121 lib/minibot/server.rb
@@ -0,0 +1,121 @@
+module MiniBot
+ class Server
+ REPLY_RE = /:\S+ (\d{3}) \S+ :?(.+)/
+
+ def self.connect(server, port)
+ s = new(server, port)
+ s.connect
+ s
+ end
+
+ def connect
+ @socket = TCPSocket.new(@server, @port)
+ end
+
+ def read
+ if @messages.first && message_complete?(@messages.first)
+ to_message(@messages.shift)
+ else
+ read_messages
+ read
+ end
+ end
+
+ def match_code(code, message)
+ if (match = reply?(message)) && match[1] == code
+ match[1, 2]
+ else
+ nil
+ end
+ end
+
+ def write(msg, *replies)
+ @socket.print "#{msg}\r\n"
+
+ unless replies.empty?
+ index = 0
+ matches, messages = catch :halted do
+ loop do
+ index, message = next_message(index)
+ replies.each do |r|
+ case r
+ when String
+ if match = /:\S+ (#{r}) \S+ :?(.+)/.match(message)
+ throw :halted, [[ match ], [ message ]]
+ end
+ when Array
+ matched = []
+ messages = []
+ r.each do |ri|
+ if match = /:\S+ (#{ri}) \S+ :?(.+)/.match(message)
+ matched << match
+ messages << message
+ index, message = next_message(index)
+ elsif !matched.empty?
+ index -= matched.length
+ message = matched.first
+ break
+ end
+ end
+
+ throw :halted, [matched, messages] unless matched.empty?
+ else
+ raise ArgumentError, "Unknown reply argument type: #{r.inspect}", caller
+ end
+ end
+ end
+ end
+
+ messages.each { |m| delete_message(m) }
+ matches.each { |m| yield m[1, 2] }
+ end
+ end
+
+ def extract(regexp)
+
+ end
+
+ def disconnect
+ @socket.close if @socket
+ end
+
+ private
+
+ def delete_message(m)
+ @messages.delete("#{m}\r")
+ end
+
+ def message_complete?(message)
+ message[-1] == ?\r
+ end
+
+ def to_message(raw)
+ raw.chomp
+ end
+
+ def next_message(index)
+ read_messages if !@messages[index] || !message_complete?(@messages[index])
+
+ [index + 1, to_message(@messages[index])]
+ end
+
+ def reply?(msg)
+ REPLY_RE.match msg
+ end
+
+ def initialize(server, port)
+ @server = server
+ @port = port
+ @messages = []
+ end
+
+ def read_messages
+ buffer = @socket.recvfrom(512).first
+ messages = buffer.split /\n/
+
+ @messages.last << messages.shift if @messages.last && @messages.last[-1] != ?\r
+
+ @messages += messages
+ end
+ end
+end
View
16 spec/commands_spec.rb
@@ -13,15 +13,15 @@ def initialize(server = nil)
describe "#join" do
it "should join a channel" do
- bot = CommandBot.new
- bot.should_receive(:write).with("JOIN #testchannel")
+ bot = CommandBot.new(mock "server")
+ bot.server.should_receive(:write).with("JOIN #testchannel")
bot.join "#testchannel"
end
it "should join multiple channels" do
- bot = CommandBot.new
- bot.should_receive(:write).with("JOIN #testchannel")
- bot.should_receive(:write).with("JOIN #anotherchannel")
+ bot = CommandBot.new(mock "server")
+ bot.server.should_receive(:write).with("JOIN #testchannel")
+ bot.server.should_receive(:write).with("JOIN #anotherchannel")
bot.join "#testchannel", "#anotherchannel"
end
end
@@ -30,7 +30,7 @@ def initialize(server = nil)
it "should return the topic data" do
bot = CommandBot.new(mock "server")
bot.server.should_receive(:write).
- with("TOPIC #datamapper", *MiniBot::Commands::TOPIC_REPLIES).
+ with("TOPIC #datamapper", *MiniBot::Commands::EXPECTED_REPLIES_TOPIC).
and_yield("332", "Documentation! http://datamapper.rubyforge.org/").
and_yield("333", "ssmoot 1212697142")
Time.should_receive(:at).with(1212697142).and_return("whoa")
@@ -44,7 +44,7 @@ def initialize(server = nil)
it "should return nil for no topic" do
bot = CommandBot.new(mock "server")
bot.server.should_receive(:write).
- with("TOPIC #datamapper", *MiniBot::Commands::TOPIC_REPLIES).
+ with("TOPIC #datamapper", *MiniBot::Commands::EXPECTED_REPLIES_TOPIC).
and_yield("331", "There isn't a topic.")
topic, author, time = bot.topic "#datamapper"
@@ -56,7 +56,7 @@ def initialize(server = nil)
it "should return only the topic for servers that don't send the metadata" do
bot = CommandBot.new(mock "server")
bot.server.should_receive(:write).
- with("TOPIC #datamapper", *MiniBot::Commands::TOPIC_REPLIES).
+ with("TOPIC #datamapper", *MiniBot::Commands::EXPECTED_REPLIES_TOPIC).
and_yield("332", "TOPIC!").
and_yield("400", "Mary had a little lamb")
View
207 spec/server_spec.rb
@@ -0,0 +1,207 @@
+require File.join(File.dirname(__FILE__), 'spec_helper')
+
+describe MiniBot::Server do
+ def server
+ MiniBot::Server.new "x", "x"
+ end
+
+ it "should connect" do
+ socket = mock "socket"
+ TCPSocket.should_receive(:new).with('irc.freenode.net', 6667).and_return(socket)
+
+ s = MiniBot::Server.connect('irc.freenode.net', 6667)
+ s.instance_variable_get("@socket").should == socket
+ end
+
+ it "should fetch messages" do
+ socket = mock "socket", :null_object => true
+ buffer = ("a" * 254) + "\r\n" + ("b" * 254) + "\r\n"
+ socket.stub!(:recvfrom).and_return([buffer, nil])
+
+ s = server
+
+ s.instance_variable_set "@socket", socket
+ s.send :read_messages
+ s.instance_variable_get("@messages").should == buffer.split(/\n/)
+ end
+
+ it "should write messages" do
+ socket = mock "socket"
+ socket.should_receive(:print).with("how now brown cow\r\n")
+
+ s = server
+ s.instance_variable_set "@socket", socket
+ s.write "how now brown cow"
+ end
+
+ describe "writing with replies" do
+ describe "advancing messages" do
+ it "should not destroy them" do
+ s = server
+ s.instance_variable_set "@messages", ["aaa\r", "bbb\r"]
+
+ cursor, msg = s.send :next_message, 0
+ cursor.should == 1
+ msg.should == "aaa"
+
+ cursor, msg = s.send :next_message, cursor
+ cursor.should == 2
+ msg.should == "bbb"
+
+ s.instance_variable_get("@messages").should == ["aaa\r", "bbb\r"]
+ end
+
+ it "should fetch new ones when no more are left" do
+ socket = mock "socket", :null_object => true
+ socket.stub!(:recvfrom).and_return(["aaa\r\nbbb\r\n", nil])
+
+ s = server
+ s.instance_variable_set "@socket", socket
+ s.instance_variable_set "@messages", []
+
+ cursor, msg = s.send :next_message, 0
+ msg.should == "aaa"
+ end
+
+ it "should detect incomplete ones and fetch more before returning" do
+ socket = mock "socket", :null_object => true
+ socket.stub!(:recvfrom).and_return(["aaa\r\nbbb\r\n", nil])
+
+ s = server
+ s.instance_variable_set "@socket", socket
+ s.instance_variable_set "@messages", ["aa"]
+
+ cursor, msg = s.send :next_message, 0
+ msg.should == "aaaaa"
+ end
+ end
+
+ it "should return the right reply" do
+ socket = mock "socket"
+ socket.stub! :print
+
+ s = server
+ s.instance_variable_set "@socket", socket
+ s.instance_variable_set "@messages", ["aaa\r", ":my.server.com 432 whatever :A message\r"]
+
+ tester = mock "tester"
+ tester.should_receive(:call).with("432", "A message")
+ s.write("duh", "432") { |code, reply| tester.call code, reply }
+ end
+
+ it "should return the second wanted reply when it's found first" do
+ socket = mock "socket"
+ socket.stub! :print
+
+ s = server
+ s.instance_variable_set "@socket", socket
+ s.instance_variable_set "@messages", [
+ "aaa\r",
+ ":my.server.com 433 whatever :A message2\r",
+ ":my.server.com 432 whatever :A message\r"
+ ]
+
+ tester = mock "tester"
+ tester.should_receive(:call).with("433", "A message2")
+ s.write("duh", "433") { |code, reply| tester.call code, reply }
+ end
+
+ it "should return all replies when it's passed an array" do
+ socket = mock "socket", :null_object => true
+ socket.stub! :print
+ socket.stub!(:recvfrom).and_return("1\r\n")
+
+ s = server
+ s.instance_variable_set "@socket", socket
+ s.instance_variable_set "@messages", [
+ "aaa\r",
+ ":my.server.com 433 whatever :A message2\r",
+ ":my.server.com 432 whatever :A message\r"
+ ]
+
+ tester = mock "tester"
+ tester.should_receive(:call).with("433", "A message2")
+ tester.should_receive(:call).with("432", "A message")
+ s.write("duh", ["433", "432"]) { |code, reply| tester.call code, reply }
+ end
+
+ it "should return the second choice when one of the array's codes doesn't match" do
+ socket = mock "socket"
+ socket.stub! :print
+
+ s = server
+ s.instance_variable_set "@socket", socket
+ s.instance_variable_set "@messages", [
+ "aaa\r",
+ ":my.server.com 433 whatever :A message2\r",
+ "bbb\r",
+ ":my.server.com 432 whatever :A message\r"
+ ]
+
+ tester = mock "tester"
+ tester.should_receive(:call).with("433", "A message2")
+ s.write("duh", ["433", "432"], "433") { |code, reply| tester.call code, reply }
+ end
+
+ it "should delete replies from message buffer" do
+ socket = mock "socket", :null_object => true
+ socket.stub! :print
+ socket.stub!(:recvfrom).and_return("1\r\n")
+
+ s = server
+ s.instance_variable_set "@socket", socket
+ s.instance_variable_set "@messages", [
+ "aaa\r",
+ ":my.server.com 433 whatever :A message2\r",
+ ":my.server.com 432 whatever :A message\r"
+ ]
+
+ s.write("duh", ["433", "432"]) { |code, reply| nil }
+ s.instance_variable_get("@messages").should == ["aaa\r", "1\r"]
+ end
+ end
+
+ describe "returning the next message" do
+ it "should return a complete message" do
+ socket = mock "socket", :null_object => true
+ buffer = ("a" * 254) + "\r\n" + ("b" * 256)
+ socket.should_receive(:recvfrom).and_return([buffer, nil])
+
+ s = server
+ s.instance_variable_set "@socket", socket
+
+ s.read.should == "a" * 254
+ s.instance_variable_get("@messages").should == ["b" * 256]
+ end
+
+ it "should join incomplete messages" do
+ socket = mock "socket", :null_object => true
+ buffer = "b" * 256
+ socket.should_receive(:recvfrom).and_return(["bbbb\r\n", nil])
+
+ s = server
+ s.instance_variable_set "@socket", socket
+ s.instance_variable_set "@messages", [buffer]
+
+ s.read.should == "b" * 260
+ end
+
+ it "should return a single complete messages" do
+ s = server
+ s.instance_variable_set "@messages", [("b" * 256) + "\r"]
+
+ s.read.should == "b" * 256
+ s.instance_variable_get("@messages").should == []
+ end
+ end
+
+ it "should disconnect" do
+ socket = mock "socket"
+ socket.should_receive(:close)
+
+ s = server
+ s.instance_variable_set "@socket", socket
+
+ s.disconnect
+ end
+end

0 comments on commit ed31a20

Please sign in to comment.