Permalink
Browse files

Improvements, fixes, cleanups.

  * Read a channel topic upon request.
  * Cosmetic changes.
  * Do not handle errors generically. This needs to be better thought
    out.
  * Username and Realname default to the nick.
  * Refactored the main loop.
  * Added ability to auto-join channels.
  • Loading branch information...
1 parent 96b4bf1 commit 3a3f67f1aa95b1dd22f5cbd894297b0912641e4e @david committed Jun 19, 2008
Showing with 142 additions and 33 deletions.
  1. +1 −1 TODO
  2. +21 −0 lib/minibot/commands.rb
  3. +36 −22 lib/minibot/daemon.rb
  4. +2 −2 lib/minibot/events.rb
  5. +18 −0 spec/commands_spec.rb
  6. +63 −7 spec/daemon_spec.rb
  7. +1 −1 spec/events_spec.rb
View
2 TODO
@@ -6,9 +6,9 @@ TODO:
* Implement relevant parts of CTCP
* Discard events generated by the bot (parts, joins, others)?
* Handle PINGs
-* Handle server events (":servername ddd ...")
* Detect when using a registered nick.
* Add ability to reply both to public (by prefixing the username) and private messages
* Bot should reconnect upon disconnection
+* Logging.
* Error handling.
* Signal trapping for graceful exiting.
View
@@ -1,9 +1,30 @@
+require 'ostruct'
+
module MiniBot
module Commands
def join(*channels)
channels.each { |channel| write "JOIN #{channel}" }
end
+ def topic(channel)
+ write "TOPIC #{channel}"
+
+ # 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
+ end
+ end
+
+ [ topic[1].chomp, meta[1].chomp, Time.at(meta[1].chomp.to_i) ]
+ end
+
private
def write(str)
View
@@ -5,41 +5,62 @@ class Daemon
include Events
include Commands
+ attr_reader :config, :commands
+
DEFAULTS = {
- :join => [],
- :port => 6667
+ :port => 6667,
+ :channels => []
}
def run
begin
- connect(@options[:server], @options[:port])
- authenticate(@options[:nick], @options[:username], @options[:realname])
+ connect @config[:server], @config[:port]
+ authenticate @config[:nick], @config[:username], @config[:realname]
+ join *@config[:channels]
main_loop
ensure
close
end
end
- def error(num, message)
- error = *Events::Constants.constants.select { |c| Events::Constants.const_get(c) == num }
-
- raise "IRC Error: #{error}: #{message}"
- end
-
private
def close
@socket.close if @socket
end
- def initialize(options)
- @options = DEFAULTS.merge options.symbolize_keys
+ def initialize(config)
+ @config = DEFAULTS.merge config
- @options[:username] ||= @options[:nick]
+ @config[:username] ||= @config[:nick]
+ @config[:realname] ||= @config[:nick]
+ @commands = []
end
def main_loop
- loop { dispatch(@socket.readline) }
+ 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
+ end
end
def connect(server, port)
@@ -52,14 +73,7 @@ def authenticate(nick, username, realname)
end
# Used by the Commands module.
- def socket
- @socket
- end
+ attr_reader :socket
end
end
-class Hash
- def symbolize_keys
- inject({}) { |h, (k, v)| h[k.to_sym] = v; h }
- end
-end
View
@@ -38,7 +38,7 @@ def user_kicked(channel, kicker, kicked, message)
def ready
end
- def error(num)
+ def error(num, message)
end
private
@@ -48,7 +48,7 @@ def dispatch(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)
+ 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]
View
@@ -19,4 +19,22 @@ class CommandBot
bot.join "#testchannel", "#anotherchannel"
end
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)
+ Time.should_receive(:at).and_return("whoa")
+
+ topic, author, time = bot.topic "#datamapper"
+ topic.should == "Documentation! http://datamapper.rubyforge.org/"
+ author.should == "ssmoot"
+ time.should == "whoa"
+ end
+ end
end
View
@@ -1,30 +1,37 @@
require File.join(File.dirname(__FILE__), 'spec_helper')
describe MiniBot::Daemon do
- def options
+ def config
{ :server => 'irc.freenode.net',
:username => 'spec',
:nick => 'nick',
:realname => 'Spec User' }
end
def daemon(opts = {})
- MiniBot::Daemon.new(options.merge(opts))
+ MiniBot::Daemon.new(config.merge(opts))
end
describe "defaults" do
it "should use the default port when no port is specified" do
socket = mock("socket", :null_object => true)
d = daemon
- d.instance_variable_get("@options")[:port].should == 6667
+ d.instance_variable_get("@config")[:port].should == 6667
end
it "should use the nick when the username is not specified" do
socket = mock("socket", :null_object => true)
d = daemon(:username => nil)
- d.instance_variable_get("@options")[:username].should == 'nick'
+ d.instance_variable_get("@config")[:username].should == 'nick'
+ end
+
+ it "should return an empty array when no channels are specified" do
+ socket = mock("socket", :null_object => true)
+
+ d = daemon
+ d.instance_variable_get("@config")[:channels].should == []
end
end
@@ -47,10 +54,59 @@ def daemon(opts = {})
d.instance_variable_get("@socket").should == socket
end
- describe "error handling" do
- it "should raise on unknow errors" do
+ describe "running" do
+ it "should auto join channels" do
+ channels = %w{#one #two}
+ d = daemon({ :channels => channels })
+ d.stub!(:connect)
+ d.stub!(:authenticate)
+ d.stub!(:main_loop)
+
+ 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
- lambda { d.error 433, "This is a message!" }.should raise_error(RuntimeError)
+
+ 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
@@ -55,7 +55,7 @@ def initialize
it "should dispatch parts" do
d = EventBot.new
d.should_receive(:user_parted).with('#ior3k', 'ior3k')
- d.send :dispatch, ":ior3k!n=ior3k@213.63.55.41 PART :#ior3k"
+ d.send :dispatch, ":ior3k!n=ior3k@213.63.55.41 PART #ior3k"
end
it "should dispatch ready" do

0 comments on commit 3a3f67f

Please sign in to comment.