Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Refactorings, improvements.

  * Cosmetic changes.
  * Added Server class to encapsulate reading and writing to the socket.
  * Write now accepts optional expected reply numbers and a block which
    it yields with each reply message.
  * All specs pass.
  • Loading branch information...
commit 2865177bcc54181f722f72a4f479902eafafd087 1 parent 12dbc52
@david authored
View
1  TODO
@@ -12,3 +12,4 @@ TODO:
* Logging.
* Error handling.
* Signal trapping for graceful exiting.
+* Add ability to stop logging for a given period of time
View
1  lib/minibot.rb
@@ -1,3 +1,4 @@
+require File.join(File.dirname(__FILE__), 'minibot', 'server')
require File.join(File.dirname(__FILE__), 'minibot', 'constants')
require File.join(File.dirname(__FILE__), 'minibot', 'events')
require File.join(File.dirname(__FILE__), 'minibot', 'commands')
View
28 lib/minibot/commands.rb
@@ -2,35 +2,29 @@
module MiniBot
module Commands
+ include Events::Constants
+
def join(*channels)
channels.each { |channel| write "JOIN #{channel}" }
end
- def topic(channel)
- write "TOPIC #{channel}"
+ TOPIC_REPLIES = [ RPL_NOTOPIC, [ RPL_TOPIC, RPL_TOPIC_META ], RPL_TOPIC ]
- # TODO: This is ugly.
- topic = meta = nil
- until topic && meta
- read_commands
- commands.each do |c|
- if (match = /:\S+ 332 \S+ #{channel} :(.+)/.match c)
- topic = match
- elsif (match = /:\S+ 333 \S+ #{channel} (.+) (\d+)/.match c)
- meta = match
- end
+ def topic(channel)
+ topic = author = timestamp = nil
+ server.write "TOPIC #{channel}", *TOPIC_REPLIES do |code, reply|
+ if code == RPL_TOPIC
+ topic = reply
+ elsif code == RPL_TOPIC_META
+ author, timestamp = *reply.split
end
end
- [ topic[1].chomp, meta[1].chomp, Time.at(meta[2].chomp.to_i) ]
+ [ topic, author, timestamp && Time.at(timestamp.to_i) ]
end
private
- def write(str)
- socket.print "#{str}\r\n"
- end
-
def pong
write "PONG #{@host_name}"
end
View
7 lib/minibot/constants.rb
@@ -1,8 +1,11 @@
module MiniBot
module Events
module Constants
- RPL_WELCOME = 1
- ERR_NICKNAMEINUSE = 433
+ RPL_WELCOME = "001"
+ RPL_NOTOPIC = "331"
+ RPL_TOPIC = "332"
+ RPL_TOPIC_META = "333"
+ ERR_NICKNAMEINUSE = "433"
end
end
end
View
48 lib/minibot/daemon.rb
@@ -5,7 +5,7 @@ class Daemon
include Events
include Commands
- attr_reader :config, :commands
+ attr_reader :config, :server
DEFAULTS = {
:port => 6667,
@@ -19,14 +19,18 @@ def run
join *@config[:channels]
main_loop
ensure
- close
+ disconnect
end
end
private
- def close
- @socket.close if @socket
+ def connect(server, port)
+ @server = Server.connect server, port
+ end
+
+ def disconnect
+ @server.disconnect
end
def initialize(config)
@@ -34,46 +38,18 @@ def initialize(config)
@config[:username] ||= @config[:nick]
@config[:realname] ||= @config[:nick]
- @commands = []
end
def main_loop
- loop do
- read_commands
- while c = next_command
- dispatch c
- end
- end
- end
-
- def read_commands
- buffer = @socket.recvfrom(512).first
- commands = buffer.split /\n/
-
- @commands.last << commands.shift if @commands.last && @commands.last[-1] != ?\r
-
- @commands += commands
- end
-
- def next_command
- if @commands.first && @commands.first[-1] == ?\r
- @commands.shift.chomp
- else
- nil
+ while msg = @server.next_message
+ dispatch msg
end
end
- def connect(server, port)
- @socket = TCPSocket.new(server, port)
- end
-
def authenticate(nick, username, realname)
- write "USER #{username} 0 xxx :#{realname}"
- write "NICK #{nick}"
+ @server.write "USER #{username} 0 xxx :#{realname}"
+ @server.write "NICK #{nick}"
end
-
- # Used by the Commands module.
- attr_reader :socket
end
end
View
64 lib/minibot/events.rb
@@ -20,7 +20,7 @@ def user_action(channel, nick, message)
def invited(channel, nick)
end
- def default(command_str)
+ def default(message_str)
end
def pinged
@@ -43,42 +43,44 @@ def error(num, message)
private
- def dispatch(command)
- if match = (/^:(\w+)!.+ PRIVMSG (#\w+) :([^\001].+)/.match command)
- message match[2], match[1], match[3]
- elsif match = (/^:(\w+)!.+ JOIN :(#\w+)/.match command)
- user_joined match[2], match[1]
- elsif match = (/^:(\w+)!.+ PART (#\w+)/.match command)
- user_parted match[2], match[1]
- elsif match = (/^:(\w+)!.+ PRIVMSG (#\w+) :\001ACTION (.+)\001/.match command)
- user_action match[2], match[1], match[3]
- elsif match = (/^:(\w+)!.+ PRIVMSG #{@nick} :(.+)/.match command)
- private_message match[1], match[2]
- elsif match = (/^:(\w+)!.+ INVITE \w+ :(#\w+)/.match command)
- invited match[2], match[1]
- elsif match = (/^PING/.match command)
- send :pinged
- elsif match = (/^:(\w+)!.+ TOPIC (#\w+) :(.+)/.match command)
- topic_changed match[2], match[1], match[3]
- elsif match = (/^:(\w+)!.+ KICK (#\w+) #{@nick} :(.+)/.match command)
- kicked match[2], match[1], match[3]
- elsif match = (/^:(\w+)!.+ KICK (#\w+) (\w+) :(.+)/.match command)
- user_kicked match[2], match[1], match[3], match[4]
- elsif match = (/^:\S+ (\d{3}).*?(:.*)?$/.match command)
- code = match[1].to_i
-
- if code == RPL_WELCOME
- ready
- elsif error?(code)
- error code, match[2].sub(/:/, '')
+ 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)
+ user_joined *match.values_at(2, 1)
+ elsif match = /^:(\w+)!.+ PART (#\w+)/.match(srv_msg)
+ user_parted *match.values_at(2, 1)
+ elsif match = /^:(\w+)!.+ PRIVMSG (#\w+) :\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)
+ invited *match.values_at(2, 1)
+ elsif match = /^PING/.match(srv_msg)
+ pinged
+ elsif match = /^:(\w+)!.+ TOPIC (#\w+) :(.+)/.match(srv_msg)
+ topic_changed *match.values_at(2, 1, 3)
+ elsif match = /^:(\w+)!.+ KICK (#\w+) #{@nick} :(.+)/.match(srv_msg)
+ kicked *match.values_at(2, 1, 3)
+ elsif match = /^:(\w+)!.+ KICK (#\w+) (\w+) :(.+)/.match(srv_msg)
+ user_kicked *match.values_at(2, 1, 3, 4)
+ elsif match = /^:\S+ #{RPL_WELCOME} .*?(:.*)?$/.match(srv_msg)
+ ready
+ elsif match = /^:\S+ (\d{3}) \S+ (:.*)?$/.match(srv_msg)
+ code, msg = *match.values_at(1, 2)
+
+ if error?(code)
+ error code, msg.sub(/:/, '')
+ else
+ default(srv_msg)
end
else
- default command
+ default(srv_msg)
end
end
def error?(num)
- return (400 .. 599).include? num
+ return ("400" .. "599").include? num
end
end
end
View
45 spec/commands_spec.rb
@@ -3,6 +3,12 @@
describe "MiniBot::Commands" do
class CommandBot
include MiniBot::Commands
+
+ def initialize(server = nil)
+ @server = server
+ end
+
+ attr_reader :server
end
describe "#join" do
@@ -21,14 +27,12 @@ class CommandBot
end
describe "#topic" do
- it "should return the right data" do
- commands = [
- ":zelazny.freenode.net 332 ee123 #datamapper :Documentation! http://datamapper.rubyforge.org/",
- ":zelazny.freenode.net 333 ee123 #datamapper ssmoot 1212697142" ]
- bot = CommandBot.new
- bot.should_receive(:write).with("TOPIC #datamapper")
- bot.stub!(:read_commands)
- bot.stub!(:commands).and_return(commands)
+ it "should return the topic data" do
+ bot = CommandBot.new(mock "server")
+ bot.server.should_receive(:write).
+ with("TOPIC #datamapper", *MiniBot::Commands::TOPIC_REPLIES).
+ and_yield("332", "Documentation! http://datamapper.rubyforge.org/").
+ and_yield("333", "ssmoot 1212697142")
Time.should_receive(:at).with(1212697142).and_return("whoa")
topic, author, time = bot.topic "#datamapper"
@@ -36,5 +40,30 @@ class CommandBot
author.should == "ssmoot"
time.should == "whoa"
end
+
+ 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).
+ and_yield("331", "There isn't a topic.")
+
+ topic, author, time = bot.topic "#datamapper"
+ topic.should be_nil
+ author.should be_nil
+ time.should be_nil
+ end
+
+ 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).
+ and_yield("332", "TOPIC!").
+ and_yield("400", "Mary had a little lamb")
+
+ topic, author, time = bot.topic "#datamapper"
+ topic.should == "TOPIC!"
+ author.should be_nil
+ time.should be_nil
+ end
end
end
View
66 spec/daemon_spec.rb
@@ -35,23 +35,21 @@ def daemon(opts = {})
end
end
- it "should authenticate" do
- socket = mock("socket", :null_object => true)
- socket.should_receive(:print).with("USER spec 0 xxx :Spec User\r\n").ordered
- socket.should_receive(:print).with("NICK nick\r\n").ordered
-
+ it "should connect" do
d = daemon
- d.should_receive(:socket).any_number_of_times.and_return(socket)
- d.send(:authenticate, 'nick', 'spec', 'Spec User')
+ MiniBot::Server.should_receive(:connect).with('server', 'port')
+ d.send :connect, 'server', 'port'
end
- it "should connect" do
- socket = mock("socket", :null_object => true)
- TCPSocket.should_receive(:new).with('irc.freenode.net', 6667).and_return(socket)
+ it "should authenticate" do
+ server = mock("server", :null_object => true)
+ server.should_receive(:write).with("USER spec 0 xxx :Spec User").ordered
+ server.should_receive(:write).with("NICK nick").ordered
d = daemon
- d.send(:connect, 'irc.freenode.net', 6667)
- d.instance_variable_get("@socket").should == socket
+ d.instance_variable_set("@server", server)
+ d.should_receive(:server).any_number_of_times.and_return(server)
+ d.send(:authenticate, 'nick', 'spec', 'Spec User')
end
describe "running" do
@@ -61,52 +59,10 @@ def daemon(opts = {})
d.stub!(:connect)
d.stub!(:authenticate)
d.stub!(:main_loop)
+ d.stub!(:disconnect)
d.should_receive(:join).with("#one", "#two")
d.run
end
end
-
- describe "reading" do
- it "should fetch commands" do
- socket = mock("socket", :null_object => true)
- buffer = ("a" * 254) + "\r\n" + ("b" * 254) + "\r\n"
- socket.stub!(:recvfrom).and_return([buffer, nil])
-
- d = daemon
-
- d.instance_variable_set("@socket", socket)
- d.send :read_commands
- end
-
- it "should return complete commands only" do
- socket = mock("socket", :null_object => true)
- buffer = ("a" * 254) + "\r\n" + ("b" * 256)
- socket.stub!(:recvfrom).and_return([buffer, nil])
-
- d = daemon
-
- d.instance_variable_set("@socket", socket)
- d.send :read_commands
- d.send(:next_command).should == "a" * 254
- d.send(:next_command).should be_nil
- d.instance_variable_get("@commands").first.should == ("b" * 256)
- end
-
- it "should join incomplete commands" do
- socket = mock("socket", :null_object => true)
- buffer = ("a" * 254) + "\r\n" + ("b" * 256)
- socket.stub!(:recvfrom).and_return([buffer, nil])
-
- d = daemon
-
- d.instance_variable_set("@socket", socket)
- d.send :read_commands
- d.send :next_command
-
- socket.stub!(:recvfrom).and_return(["bbbb\r\n", nil])
- d.send :read_commands
- d.send(:next_command).should == "b" * 260
- end
- end
end
View
4 spec/events_spec.rb
@@ -66,8 +66,8 @@ def initialize
it "should dispatch errors" do
d = EventBot.new
- d.should_receive(:error).with(433, "Nickname is already in use.")
- d.send :dispatch, ":card.freenode.net 433 * logbot :Nickname is already in use."
+ d.should_receive(:error).with("433", "Nickname is already in use.")
+ d.send :dispatch, ":card.freenode.net 433 logbot :Nickname is already in use."
end
it "should dispatch topic changes" do
Please sign in to comment.
Something went wrong with that request. Please try again.