├── .autotest ├── .gitignore ├── CHANGES ├── LICENSE ├── README.rdoc ├── Rakefile ├── TODO ├── examples ├── echo_bot.rb ├── excess_flood.rb └── schema.rb ├── isaac.gemspec ├── lib ├── isaac.rb └── isaac │ └── bot.rb └── test ├── helper.rb ├── test_commands.rb ├── test_events.rb ├── test_helpers.rb ├── test_irc.rb ├── test_message.rb ├── test_parse.rb └── test_queue.rb /.autotest: -------------------------------------------------------------------------------- 1 | require 'autotest/redgreen' 2 | 3 | Autotest.add_hook :initialize do |at| 4 | at.add_mapping(%r|^lib/isaac\.rb$|) do 5 | at.files_matching(%r|test/test.*rb|) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | = 0.2.6 / 2009-12-21 2 | 3 | * Require 'isaac/bot' to avoid top level bot/methods [Julian Langschaedel] 4 | 5 | * Add action(), kick() and mode() [Danny Tatom] 6 | 7 | * SSL support - configure {|c| c.ssl = true} [postmodern] 8 | 9 | * on :join/:part-events [postmodern] 10 | 11 | = 0.2.5 / 2009-04-25 12 | 13 | * Comply to the RFC - lines end with \r\n, not just \n. 14 | 15 | = 0.2.4 / 2009-04-01 16 | 17 | * Bug fixes. 18 | 19 | = 0.2.3 / 2009-04-01 20 | 21 | * Pass regular expression groups to block parameters. 22 | 23 | * Internal refactoring of queue and tests. 24 | 25 | = 0.2.2 / 2009-02-23 26 | 27 | * Irrelevant. 28 | 29 | = 0.2.1 / 2009-02-23 30 | 31 | * config() has been renamed to configure(). 32 | 33 | * part() and topic() commands added. 34 | 35 | * Comply to the RFC and wait for 001-004 messages until sending commands. 36 | 37 | * Respond to CTCP action with the content of configure {|c| c.version = "" }. 38 | 39 | * Improved documentation. 40 | 41 | = 0.2.0 / 2009-02-23 42 | 43 | * This is a complete rewrite of Isaac. 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008, 2009, 2010 Harry Vangberg 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Isaac - the smallish DSL for writing IRC bots 2 | 3 | == Features 4 | * Wraps parsing of incoming messages and raw IRC commands in simple constructs. 5 | * Hides all the ugly regular expressions of matching IRC commands. Leaves only the essentials for you to match. 6 | * Takes care of dull stuff such as replying to PING-messages and avoiding excess flood. 7 | * Uses EventMachine 8 | 9 | == Getting started 10 | An Isaac-bot needs a few basics: 11 | require 'isaac' 12 | configure do |c| 13 | c.nick = "AwesomeBot" 14 | c.server = "irc.freenode.net" 15 | c.port = 6667 16 | end 17 | That's it. Run ruby bot.rb and it will connect to the specified server. 18 | 19 | === Connecting 20 | After the bot has connected to the IRC server you might want to join some channels: 21 | on :connect do 22 | join "#awesome_channel", "#WesternBar" 23 | end 24 | 25 | === Responding to messages 26 | Joining a channel and sitting idle is not much fun. Let's repeat everything being said in these channels: 27 | 28 | on :channel do 29 | msg channel, message 30 | end 31 | 32 | Notice the +channel+ and +message+ variables. Additionally +nick+ and +match+ is 33 | available for channel-events. +nick+ being the sender of the message, +match+ 34 | being an array of captures from the regular expression: 35 | 36 | on :channel, /^quote this: (.*)/ do 37 | msg channel, "Quote: '#{match[0]}' by #{nick}" 38 | end 39 | 40 | If you want to match private messages use the +on :private+ event: 41 | 42 | on :private, /^login (\S+) (\S+)/ do 43 | username = match[0] 44 | password = match[1] 45 | # do something to authorize or whatevz. 46 | msg nick, "Login successful!" 47 | end 48 | 49 | You can also pass the RegExp captures as block arguments: 50 | 51 | on :channel, /catch this: (.*) and this: (.*)/ do |first, last| 52 | # `first` will contain the first regexp capture, 53 | # `last` the second. 54 | end 55 | 56 | === Defining helpers 57 | Helpers should not be defined in the top level, but instead using the +helpers+-constructor: 58 | 59 | helpers do 60 | def rain_check(meeting) 61 | msg nick, "Can I have a rain check on the #{meeting}?" 62 | end 63 | end 64 | 65 | on :private, /date/ do 66 | rain_check("romantic date") 67 | end 68 | 69 | === Errors, errors, errors 70 | Errors, as specified by RFC 1459, can be reacted upon as well. If you e.g. try to send a message to a non-existant nick you will get error 401: "No such nick/channel". 71 | 72 | on :error, 401 do 73 | # Do something. 74 | end 75 | 76 | Available variables: +nick+ and +channel+. 77 | 78 | === Non-top level bots 79 | You might not want to pollute the top-level namespace with Isaac 80 | helpers, or you want to define multiple bots. This can be done 81 | easily, by requiring `isaac/bot` instead of `isaac`: 82 | 83 | require 'isaac/bot' 84 | 85 | bot = Isaac::Bot.new do 86 | configure do 87 | … 88 | end 89 | 90 | on :channel do 91 | … 92 | end 93 | end 94 | 95 | EventMachine.run {bot.start} 96 | 97 | == Contribute 98 | The source is hosted at GitHub: http://github.com/ichverstehe/isaac 99 | 100 | == License 101 | Copyright (c) 2009 Harry Vangberg 102 | 103 | Permission is hereby granted, free of charge, to any person 104 | obtaining a copy of this software and associated documentation 105 | files (the "Software"), to deal in the Software without 106 | restriction, including without limitation the rights to use, 107 | copy, modify, merge, publish, distribute, sublicense, and/or sell 108 | copies of the Software, and to permit persons to whom the 109 | Software is furnished to do so, subject to the following 110 | conditions: 111 | 112 | The above copyright notice and this permission notice shall be 113 | included in all copies or substantial portions of the Software. 114 | 115 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 116 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 117 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 118 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 119 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 120 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 121 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 122 | OTHER DEALINGS IN THE SOFTWARE. 123 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | task :default => :test 2 | 3 | require 'rake/testtask' 4 | 5 | Rake::TestTask.new do |t| 6 | t.libs << "test" << "lib" 7 | t.test_files = FileList['test/test_*.rb'] 8 | t.verbose = true 9 | end 10 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | - Handle disconnects, on :disconnect ? 2 | - mode() for setting nick/channel modes 3 | - a bunch of IRC commands: ban, names, list, etc. 4 | - async! 5 | -------------------------------------------------------------------------------- /examples/echo_bot.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift '../lib' 2 | 3 | require 'isaac' 4 | 5 | configure do |c| 6 | c.nick = "echo_bot" 7 | c.server = "irc.freenode.net" 8 | c.port = 6667 9 | c.verbose = true 10 | end 11 | 12 | on :connect do 13 | join "#awesome_channel" 14 | end 15 | 16 | on :channel, // do 17 | msg channel, message 18 | end 19 | -------------------------------------------------------------------------------- /examples/excess_flood.rb: -------------------------------------------------------------------------------- 1 | require 'lib/isaac.rb' 2 | 3 | configure do |c| 4 | c.nick = "The_Echo_Bot" 5 | c.server = "irc.freenode.net" 6 | c.port = 6667 7 | end 8 | 9 | on :connect do 10 | join "#awesome_channel" 11 | end 12 | 13 | on :channel, /flood/ do 14 | join "#Awesome_Channel" 15 | 20.times { |i| msg '#awesome_channel', "#{i}:: Let me take you down to the city for a while, just a little while, oh yes. This should really exceed, plz thx u" } 16 | end 17 | -------------------------------------------------------------------------------- /examples/schema.rb: -------------------------------------------------------------------------------- 1 | require 'lib/isaac' 2 | 3 | config do |c| 4 | c.nick = "SomeBot" 5 | c.server = "irc.freenode.net" 6 | c.port = 6667 7 | c.realname = 'Isaac Hayes' 8 | c.verbose = true 9 | c.version = 'SchemaBot v0.1.2' 10 | end 11 | 12 | helpers do 13 | def check 14 | msg channel, "this channel, #{channel}, is awesome!" 15 | end 16 | end 17 | 18 | on :connect do 19 | join "#twittirc", "#awesome_channel" 20 | msg 'asdfhaskfdhaskdfhaskdfasdf', 'foo' 21 | end 22 | 23 | on :private, /^t (.*)/ do 24 | msg nick, "You said: " + match[1] 25 | end 26 | 27 | on :channel, /quote/ do 28 | msg channel, "#{nick} requested a quote: 'Smoking, a subtle form of suicide.' - Vonnegut" 29 | end 30 | 31 | on :channel, /status/ do 32 | check 33 | end 34 | 35 | on :error, 401 do 36 | puts "Ok, #{nick} doesn't exist." 37 | end 38 | -------------------------------------------------------------------------------- /isaac.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = "isaac" 3 | s.version = "0.3.0" 4 | s.date = "2009-12-21" 5 | s.summary = "The smallish DSL for writing IRC bots" 6 | s.email = "harry@vangberg.name" 7 | s.homepage = "http://github.com/ichverstehe/isaac" 8 | s.description = "Small DSL for writing IRC bots." 9 | s.rubyforge_project = "isaac" 10 | s.has_rdoc = true 11 | s.authors = ["Harry Vangberg"] 12 | s.files = [ 13 | "README.rdoc", 14 | "CHANGES", 15 | "isaac.gemspec", 16 | "lib/isaac.rb", 17 | "lib/isaac/bot.rb" 18 | ] 19 | s.rdoc_options = ["--main", "README.rdoc"] 20 | s.extra_rdoc_files = ["CHANGES", "README.rdoc"] 21 | end 22 | 23 | -------------------------------------------------------------------------------- /lib/isaac.rb: -------------------------------------------------------------------------------- 1 | require 'isaac/bot' 2 | 3 | $bot = Isaac::Bot.new 4 | 5 | %w(configure helpers on).each do |method| 6 | eval(<<-EOF) 7 | def #{method}(*args, &block) 8 | $bot.#{method}(*args, &block) 9 | end 10 | EOF 11 | end 12 | 13 | at_exit do 14 | unless defined?(Test::Unit) 15 | raise $! if $! 16 | EventMachine.run {$bot.start} 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/isaac/bot.rb: -------------------------------------------------------------------------------- 1 | require 'eventmachine' 2 | 3 | module Isaac 4 | VERSION = '0.2.1' 5 | 6 | Config = Struct.new(:server, :port, :ssl, :password, :nick, :realname, :version, :environment, :verbose, :encoding) 7 | 8 | class Bot 9 | attr_accessor :config, :irc, :nick, :channel, :message, :user, :host, :match, 10 | :error 11 | 12 | def initialize(&b) 13 | @events = {} 14 | @config = Config.new("localhost", 6667, false, nil, "isaac", "Isaac", 'isaac', :production, false, "utf-8") 15 | 16 | instance_eval(&b) if block_given? 17 | end 18 | 19 | def configure(&b) 20 | b.call(@config) 21 | end 22 | 23 | def on(event, match=//, &block) 24 | match = match.to_s if match.is_a? Integer 25 | (@events[event] ||= []) << [Regexp.new(match), block] 26 | end 27 | 28 | def helpers(&b) 29 | instance_eval(&b) 30 | end 31 | 32 | def halt 33 | throw :halt 34 | end 35 | 36 | def raw(command) 37 | @irc.message(command) 38 | end 39 | 40 | def msg(recipient, text) 41 | raw("PRIVMSG #{recipient} :#{text}") 42 | end 43 | 44 | def action(recipient, text) 45 | raw("PRIVMSG #{recipient} :\001ACTION #{text}\001") 46 | end 47 | 48 | def join(*channels) 49 | channels.each {|channel| raw("JOIN #{channel}")} 50 | end 51 | 52 | def part(*channels) 53 | channels.each {|channel| raw("PART #{channel}")} 54 | end 55 | 56 | def topic(channel, text) 57 | raw("TOPIC #{channel} :#{text}") 58 | end 59 | 60 | def mode(channel, option) 61 | raw("MODE #{channel} #{option}") 62 | end 63 | 64 | def kick(channel, user, reason=nil) 65 | raw("KICK #{channel} #{user} :#{reason}") 66 | end 67 | 68 | def quit(message=nil) 69 | command = message ? "QUIT :#{message}" : "QUIT" 70 | raw command 71 | end 72 | 73 | def start 74 | puts "Connecting to #{@config.server}:#{@config.port}" unless @config.environment == :test 75 | @irc = IRC.connect(self, @config) 76 | end 77 | 78 | def message 79 | @message ||= "" 80 | end 81 | 82 | def dispatch(event, msg=nil) 83 | if msg 84 | @nick, @user, @host, @channel, @error, @message = 85 | msg.nick, msg.user, msg.host, msg.channel, msg.error, msg.message 86 | end 87 | 88 | if handler = find(event, message) 89 | regexp, block = *handler 90 | self.match = message.match(regexp).captures 91 | invoke block 92 | end 93 | end 94 | 95 | private 96 | def find(type, message) 97 | if events = @events[type] 98 | events.detect {|regexp,_| message.match(regexp)} 99 | end 100 | end 101 | 102 | def invoke(block) 103 | mc = class << self; self; end 104 | mc.send :define_method, :__isaac_event_handler, &block 105 | 106 | # -1 splat arg, send everything 107 | # 0 no args, send nothing 108 | # 1 defined number of args, send only those 109 | bargs = case block.arity <=> 0 110 | when -1; match 111 | when 0; [] 112 | when 1; match[0..block.arity-1] 113 | end 114 | 115 | catch(:halt) { __isaac_event_handler(*bargs) } 116 | end 117 | end 118 | 119 | class IRC < EventMachine::Connection 120 | def self.connect(bot, config) 121 | EventMachine.connect(config.server, config.port, self, bot, config) 122 | end 123 | 124 | def initialize(bot, config) 125 | @bot, @config = bot, config 126 | @transfered = 0 127 | @registration = [] 128 | end 129 | 130 | def post_init 131 | @data = '' 132 | @queue = Queue.new(self, @bot.config.server) 133 | message "PASS #{@config.password}" if @config.password 134 | message "NICK #{@config.nick}" 135 | message "USER #{@config.nick} 0 * :#{@config.realname}" 136 | @queue.lock 137 | end 138 | 139 | def receive_data(data) 140 | @data << data 141 | loop do 142 | line, rest = @data.split("\n", 2) 143 | return unless rest 144 | @data = rest 145 | parse line 146 | end 147 | end 148 | 149 | def parse(input) 150 | puts "<< #{input}" if @bot.config.verbose 151 | msg = Message.new(input) 152 | 153 | if ("001".."004").include? msg.command 154 | @registration << msg.command 155 | if registered? 156 | @queue.unlock 157 | @bot.dispatch(:connect) 158 | end 159 | elsif msg.command == "PRIVMSG" 160 | if msg.params.last == "\001VERSION\001" 161 | message "NOTICE #{msg.nick} :\001VERSION #{@bot.config.version}\001" 162 | end 163 | 164 | type = msg.channel? ? :channel : :private 165 | @bot.dispatch(type, msg) 166 | elsif msg.error? 167 | @bot.dispatch(:error, msg) 168 | elsif msg.command == "PING" 169 | @queue.unlock 170 | message "PONG :#{msg.params.first}" 171 | elsif msg.command == "PONG" 172 | @queue.unlock 173 | else 174 | event = msg.command.downcase.to_sym 175 | @bot.dispatch(event, msg) 176 | end 177 | end 178 | 179 | def registered? 180 | (("001".."004").to_a - @registration).empty? 181 | end 182 | 183 | def message(msg) 184 | @queue << msg 185 | end 186 | end 187 | 188 | class Message 189 | attr_accessor :raw, :prefix, :command, :params 190 | 191 | def initialize(msg=nil) 192 | @raw = msg 193 | parse if msg 194 | end 195 | 196 | def numeric_reply? 197 | !!numeric_reply 198 | end 199 | 200 | def numeric_reply 201 | @numeric_reply ||= @command.match(/^\d\d\d$/) 202 | end 203 | 204 | def parse 205 | match = @raw.match(/(^:(\S+) )?(\S+)(.*)/) 206 | _, @prefix, @command, raw_params = match.captures 207 | 208 | raw_params.strip! 209 | if match = raw_params.match(/:(.*)/) 210 | @params = match.pre_match.split(" ") 211 | @params << match[1] 212 | else 213 | @params = raw_params.split(" ") 214 | end 215 | end 216 | 217 | def nick 218 | return unless @prefix 219 | @nick ||= @prefix[/^(\S+)!/, 1] 220 | end 221 | 222 | def user 223 | return unless @prefix 224 | @user ||= @prefix[/^\S+!(\S+)@/, 1] 225 | end 226 | 227 | def host 228 | return unless @prefix 229 | @host ||= @prefix[/@(\S+)$/, 1] 230 | end 231 | 232 | def server 233 | return unless @prefix 234 | return if @prefix.match(/[@!]/) 235 | @server ||= @prefix[/^(\S+)/, 1] 236 | end 237 | 238 | def error? 239 | !!error 240 | end 241 | 242 | def error 243 | return @error if @error 244 | @error = command.to_i if numeric_reply? && command[/[45]\d\d/] 245 | end 246 | 247 | def channel? 248 | !!channel 249 | end 250 | 251 | def channel 252 | return @channel if @channel 253 | if regular_command? and params.first.start_with?("#") 254 | @channel = params.first 255 | end 256 | end 257 | 258 | def message 259 | return @message if @message 260 | if error? 261 | @message = error.to_s 262 | elsif regular_command? 263 | @message = params.last 264 | end 265 | end 266 | 267 | private 268 | # This is a late night hack. Fix. 269 | def regular_command? 270 | %w(PRIVMSG JOIN PART QUIT).include? command 271 | end 272 | end 273 | 274 | class Queue 275 | def initialize(connection, server) 276 | # We need server for pinging us out of an excess flood 277 | @connection, @server = connection, server 278 | @queue, @lock, @transfered = [], false, 0 279 | end 280 | 281 | def lock 282 | @lock = true 283 | end 284 | 285 | def unlock 286 | @lock, @transfered = false, 0 287 | invoke 288 | end 289 | 290 | def <<(message) 291 | @queue << message 292 | invoke 293 | end 294 | 295 | private 296 | def message_to_send? 297 | !@lock && !@queue.empty? 298 | end 299 | 300 | def transfered_after_next_send 301 | @transfered + @queue.first.size + 2 # the 2 is for \r\n 302 | end 303 | 304 | def exceed_limit? 305 | transfered_after_next_send > 1472 306 | end 307 | 308 | def lock_and_ping 309 | lock 310 | @connection.send_data "PING :#{@server}\r\n" 311 | end 312 | 313 | def next_message 314 | @queue.shift.to_s.chomp + "\r\n" 315 | end 316 | 317 | def invoke 318 | while message_to_send? 319 | if exceed_limit? 320 | lock_and_ping; break 321 | else 322 | @transfered = transfered_after_next_send 323 | @connection.send_data next_message 324 | # puts ">> #{msg}" if @bot.config.verbose 325 | end 326 | end 327 | end 328 | end 329 | end 330 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift 'lib' 2 | require 'isaac' 3 | require 'rubygems' 4 | require 'test/unit' 5 | require 'contest' 6 | require 'rr' 7 | require 'timeout' 8 | begin 9 | require 'ruby-debug' 10 | rescue LoadError; end 11 | 12 | module Test::Unit::Assertions 13 | def assert_empty_buffer(io) 14 | assert_raise(Errno::EAGAIN) { io.read_nonblock 1 } 15 | end 16 | end 17 | 18 | class MockSocket 19 | def self.pipe 20 | socket1, socket2 = new, new 21 | socket1.in, socket2.out = IO.pipe 22 | socket2.in, socket1.out = IO.pipe 23 | [socket1, socket2] 24 | end 25 | 26 | attr_accessor :in, :out 27 | def gets() 28 | Timeout.timeout(1) {@in.gets} 29 | end 30 | def puts(m) @out.puts(m) end 31 | def print(m) @out.print(m) end 32 | def eof?() @in.eof? end 33 | def empty? 34 | begin 35 | @in.read_nonblock(1) 36 | false 37 | rescue Errno::EAGAIN 38 | true 39 | end 40 | end 41 | end 42 | 43 | class Test::Unit::TestCase 44 | include RR::Adapters::TestUnit 45 | 46 | def mock_bot(&b) 47 | @socket, @server = MockSocket.pipe 48 | stub(TCPSocket).open(anything, anything) {@socket} 49 | bot = Isaac::Bot.new(&b) 50 | bot.config.environment = :test 51 | Thread.start { bot.start } 52 | bot 53 | end 54 | 55 | def bot_is_connected 56 | assert_equal "NICK isaac\r\n", @server.gets 57 | assert_equal "USER isaac 0 * :Isaac\r\n", @server.gets 58 | 1.upto(4) {|i| @server.print ":localhost 00#{i}\r\n"} 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test/test_commands.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestCommands < Test::Unit::TestCase 4 | test "raw messages can be send" do 5 | bot = mock_bot {} 6 | bot_is_connected 7 | 8 | bot.raw "PRIVMSG foo :bar baz" 9 | assert_equal "PRIVMSG foo :bar baz\r\n", @server.gets 10 | end 11 | 12 | test "messages are sent to recipient" do 13 | bot = mock_bot {} 14 | bot_is_connected 15 | 16 | bot.msg "foo", "bar baz" 17 | assert_equal "PRIVMSG foo :bar baz\r\n", @server.gets 18 | end 19 | 20 | test "actions are sent to recipient" do 21 | bot = mock_bot {} 22 | bot_is_connected 23 | 24 | bot.action "foo", "bar" 25 | assert_equal "PRIVMSG foo :\001ACTION bar\001\r\n", @server.gets 26 | end 27 | 28 | test "channels are joined" do 29 | bot = mock_bot {} 30 | bot_is_connected 31 | 32 | bot.join "#foo", "#bar" 33 | assert_equal "JOIN #foo\r\n", @server.gets 34 | assert_equal "JOIN #bar\r\n", @server.gets 35 | end 36 | 37 | test "channels are parted" do 38 | bot = mock_bot {} 39 | bot_is_connected 40 | 41 | bot.part "#foo", "#bar" 42 | assert_equal "PART #foo\r\n", @server.gets 43 | assert_equal "PART #bar\r\n", @server.gets 44 | end 45 | 46 | test "topic is set" do 47 | bot = mock_bot {} 48 | bot_is_connected 49 | 50 | bot.topic "#foo", "bar baz" 51 | assert_equal "TOPIC #foo :bar baz\r\n", @server.gets 52 | end 53 | 54 | test "modes can be set" do 55 | bot = mock_bot {} 56 | bot_is_connected 57 | 58 | bot.mode "#foo", "+o" 59 | assert_equal "MODE #foo +o\r\n", @server.gets 60 | end 61 | 62 | test "can kick users" do 63 | bot = mock_bot {} 64 | bot_is_connected 65 | 66 | bot.kick "foo", "bar", "bein' a baz" 67 | assert_equal "KICK foo bar :bein' a baz\r\n", @server.gets 68 | end 69 | 70 | test "quits" do 71 | bot = mock_bot {} 72 | bot_is_connected 73 | 74 | bot.quit 75 | assert_equal "QUIT\r\n", @server.gets 76 | end 77 | 78 | test "quits with message" do 79 | bot = mock_bot {} 80 | bot_is_connected 81 | 82 | bot.quit "I'm outta here!" 83 | assert_equal "QUIT :I'm outta here!\r\n", @server.gets 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /test/test_events.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestEvents < Test::Unit::TestCase 4 | # This is stupid, but it's just there to make it easier to transform to the new 5 | # Message class. Should be fixed. 6 | def dispatch(type, env) 7 | msg = Isaac::Message.new(":john!doe@example.com PRIVMSG #foo :#{env[:message]}") 8 | @bot.dispatch(type, msg) 9 | end 10 | 11 | test "events are registered" do 12 | @bot = mock_bot { 13 | on(:channel, /Hello/) {msg "foo", "yr formal!"} 14 | on(:channel, /Hey/) {msg "foo", "bar baz"} 15 | } 16 | bot_is_connected 17 | 18 | dispatch(:channel, :message => "Hey") 19 | assert_equal "PRIVMSG foo :bar baz\r\n", @server.gets 20 | end 21 | 22 | test "catch-all events" do 23 | @bot = mock_bot { 24 | on(:channel) {msg "foo", "bar baz"} 25 | } 26 | bot_is_connected 27 | 28 | dispatch(:channel, :message => "lolcat") 29 | assert_equal "PRIVMSG foo :bar baz\r\n", @server.gets 30 | end 31 | 32 | test "event can be halted" do 33 | @bot = mock_bot { 34 | on(:channel, /Hey/) { halt; msg "foo", "bar baz" } 35 | } 36 | bot_is_connected 37 | 38 | dispatch(:channel, :message => "Hey") 39 | assert @server.empty? 40 | end 41 | 42 | test "connect-event is dispatched at connection" do 43 | @bot = mock_bot { 44 | on(:connect) {msg "foo", "bar baz"} 45 | } 46 | bot_is_connected 47 | 48 | assert_equal "PRIVMSG foo :bar baz\r\n", @server.gets 49 | end 50 | 51 | test "regular expression match is accessible" do 52 | @bot = mock_bot { 53 | on(:channel, /foo (bar)/) {msg "foo", match[0]} 54 | } 55 | bot_is_connected 56 | 57 | dispatch(:channel, :message => "foo bar") 58 | 59 | assert_equal "PRIVMSG foo :bar\r\n", @server.gets 60 | end 61 | 62 | test "regular expression matches are handed to block arguments" do 63 | @bot = mock_bot { 64 | on :channel, /(foo) (bar)/ do |a,b| 65 | raw "#{a}" 66 | raw "#{b}" 67 | end 68 | } 69 | bot_is_connected 70 | 71 | dispatch(:channel, :message => "foo bar") 72 | 73 | assert_equal "foo\r\n", @server.gets 74 | assert_equal "bar\r\n", @server.gets 75 | end 76 | 77 | test "only specified number of captures are handed to block args" do 78 | @bot = mock_bot { 79 | on :channel, /(foo) (bar)/ do |a| 80 | raw "#{a}" 81 | end 82 | } 83 | bot_is_connected 84 | 85 | dispatch(:channel, :message => "foo bar") 86 | 87 | assert_equal "foo\r\n", @server.gets 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /test/test_helpers.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestHelpers < Test::Unit::TestCase 4 | test "helpers are registered" do 5 | bot = mock_bot { 6 | helpers { def foo; msg "foo", "bar baz"; end } 7 | on(:private, //) {foo} 8 | } 9 | bot_is_connected 10 | 11 | bot.irc.parse ":johnny!john@doe.com PRIVMSG isaac :hello, you!" 12 | assert_equal "PRIVMSG foo :bar baz\r\n", @server.gets 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/test_irc.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestIrc < Test::Unit::TestCase 4 | test "a new bot connects to IRC" do 5 | bot = mock_bot {} 6 | 7 | assert_equal "NICK isaac\r\n", @server.gets 8 | assert_equal "USER isaac 0 * :#{bot.config.realname}\r\n", @server.gets 9 | end 10 | 11 | test "password is sent if specified" do 12 | bot = mock_bot { 13 | configure {|c| c.password = "foo"} 14 | } 15 | assert_equal "PASS foo\r\n", @server.gets 16 | end 17 | 18 | test "no messages are sent when registration isn't complete" do 19 | bot = mock_bot { 20 | on(:connect) {raw "Connected!"} 21 | } 22 | 2.times { @server.gets } # NICK / USER 23 | bot.dispatch :connect 24 | 25 | assert @server.empty? 26 | end 27 | 28 | test "no messages are sent until registration is complete" do 29 | bot = mock_bot { 30 | on(:connect) {raw "Connected!"} 31 | } 32 | 2.times { @server.gets } # NICK / USER 33 | bot.dispatch :connect 34 | 35 | 1.upto(4) {|i| @server.puts ":localhost 00#{i}"} 36 | assert_equal "Connected!\r\n", @server.gets 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/test_message.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestMessage < Test::Unit::TestCase 4 | include Isaac 5 | 6 | test "host prefix" do 7 | msg = Message.new(":jeff!spicoli@beach.com QUIT") 8 | assert_equal "jeff!spicoli@beach.com", msg.prefix 9 | assert_equal "jeff", msg.nick 10 | assert_equal "spicoli", msg.user 11 | assert_equal "beach.com", msg.host 12 | assert_nil msg.server 13 | end 14 | 15 | test "server prefix" do 16 | msg = Message.new(":some.server.com PING") 17 | assert_equal "some.server.com", msg.prefix 18 | assert_equal "some.server.com", msg.server 19 | assert_nil msg.nick 20 | assert_nil msg.user 21 | assert_nil msg.host 22 | end 23 | 24 | test "without prefix" do 25 | msg = Message.new("PING foo.bar") 26 | assert_nil msg.prefix 27 | assert_nil msg.nick 28 | assert_nil msg.host 29 | end 30 | 31 | test "command" do 32 | msg = Message.new("PING foo.bar") 33 | assert_equal "PING", msg.command 34 | end 35 | 36 | test "numeric reply" do 37 | msg = Message.new("409") 38 | assert msg.numeric_reply? 39 | assert_equal "409", msg.command 40 | end 41 | 42 | test "single param" do 43 | msg = Message.new("PING foo.bar") 44 | assert_equal 1, msg.params.size 45 | assert_equal "foo.bar", msg.params[0] 46 | end 47 | 48 | test "multiple params" do 49 | msg = Message.new("FOO bar baz") 50 | assert_equal 2, msg.params.size 51 | assert_equal ["bar", "baz"], msg.params 52 | end 53 | 54 | test "single param with whitespace" do 55 | msg = Message.new("FOO :bar baz") 56 | assert_equal 1, msg.params.size 57 | assert_equal "bar baz", msg.params[0] 58 | end 59 | 60 | test "single param with whitespace and colon" do 61 | msg = Message.new("FOO :bar :baz") 62 | assert_equal 1, msg.params.size 63 | assert_equal "bar :baz", msg.params[0] 64 | end 65 | 66 | test "multiple params with whitespace" do 67 | msg = Message.new("FOO bar :lol cat") 68 | assert_equal 2, msg.params.size 69 | assert_equal "bar", msg.params[0] 70 | assert_equal "lol cat", msg.params[1] 71 | end 72 | 73 | test "multiple params with whitespace and colon" do 74 | msg = Message.new("FOO bar :lol :cat") 75 | assert_equal 2, msg.params.size 76 | assert_equal "bar", msg.params[0] 77 | assert_equal "lol :cat", msg.params[1] 78 | end 79 | 80 | test "error" do 81 | msg = Message.new("200") 82 | assert_equal false, msg.error? 83 | assert_nil msg.error 84 | 85 | msg = Message.new("400") 86 | assert_equal true, msg.error? 87 | assert_equal 400, msg.error 88 | end 89 | 90 | test "if error, #message has the error code string" do 91 | msg = Message.new("400") 92 | assert_equal "400", msg.message 93 | end 94 | 95 | test "channel has channel name" do 96 | msg = Message.new(":foo!bar@baz.com PRIVMSG #awesome :lol cat") 97 | assert_equal true, msg.channel? 98 | assert_equal "#awesome", msg.channel 99 | end 100 | 101 | test "channel has nothing when receiver is a nick" do 102 | msg = Message.new(":foo!bar@baz.com PRIVMSG john :wazzup boy?") 103 | assert_equal false, msg.channel? 104 | assert_equal nil, msg.channel 105 | end 106 | 107 | test "privmsg has #message" do 108 | msg = Message.new(":foo!bar@baz.com PRIVMSG #awesome :lol cat") 109 | assert_equal "lol cat", msg.message 110 | end 111 | 112 | test "non-privmsg doesn't have #message" do 113 | msg = Message.new("PING :foo bar") 114 | assert_nil msg.message 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /test/test_parse.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestParse < Test::Unit::TestCase 4 | test "ping-pong" do 5 | bot = mock_bot {} 6 | bot_is_connected 7 | 8 | @server.print "PING :foo.bar\r\n" 9 | assert_equal "PONG :foo.bar\r\n", @server.gets 10 | end 11 | 12 | test "join messages dispatches join event" do 13 | bot = mock_bot { 14 | on(:join) {msg channel, "bar baz"} 15 | } 16 | bot_is_connected 17 | 18 | @server.print ":johnny!john@doe.com JOIN #foo\r\n" 19 | assert_equal "PRIVMSG #foo :bar baz\r\n", @server.gets 20 | end 21 | 22 | test "part messages dispatches part events" do 23 | bot = mock_bot { 24 | on(:part) {msg channel, "#{nick} left: #{message}"} 25 | } 26 | bot_is_connected 27 | 28 | @server.print ":johnny!john@doe.com PART #foo :Leaving\r\n" 29 | assert_equal "PRIVMSG #foo :johnny left: Leaving\r\n", @server.gets 30 | end 31 | 32 | test "quit messages dispatches quit events" do 33 | bot = mock_bot { 34 | on(:quit) {msg "#foo", "#{nick} quit: #{message}"} 35 | } 36 | bot_is_connected 37 | 38 | @server.print ":johnny!john@doe.com QUIT :Leaving\r\n" 39 | assert_equal "PRIVMSG #foo :johnny quit: Leaving\r\n", @server.gets 40 | end 41 | 42 | test "private messages dispatches private event" do 43 | bot = mock_bot { 44 | on(:private, //) {msg "foo", "bar baz"} 45 | } 46 | bot_is_connected 47 | 48 | @server.print ":johnny!john@doe.com PRIVMSG isaac :hello, you!\r\n" 49 | assert_equal "PRIVMSG foo :bar baz\r\n", @server.gets 50 | end 51 | 52 | test "channel messages dispatches channel event" do 53 | bot = mock_bot { 54 | on(:channel, //) {msg "foo", "bar baz"} 55 | } 56 | bot_is_connected 57 | 58 | @server.print ":johnny!john@doe.com PRIVMSG #awesome :hello, folks!\r\n" 59 | assert_equal "PRIVMSG foo :bar baz\r\n", @server.gets 60 | end 61 | 62 | test "prefix is optional" do 63 | bot = mock_bot { 64 | on(:channel, //) {msg "foo", "bar baz"} 65 | } 66 | bot_is_connected 67 | 68 | @server.print "PRIVMSG #awesome :hello, folks!\r\n" 69 | assert_equal "PRIVMSG foo :bar baz\r\n", @server.gets 70 | end 71 | 72 | test "private event has environment" do 73 | bot = mock_bot { 74 | on :private, // do 75 | raw nick 76 | raw user 77 | raw host 78 | raw message 79 | end 80 | } 81 | bot_is_connected 82 | 83 | @server.puts ":johnny!john@doe.com PRIVMSG isaac :hello, you!" 84 | assert_equal "johnny\r\n", @server.gets 85 | assert_equal "john\r\n", @server.gets 86 | assert_equal "doe.com\r\n", @server.gets 87 | assert_equal "hello, you!\r\n", @server.gets 88 | end 89 | 90 | test "channel event has environment" do 91 | bot = mock_bot { 92 | on :channel, // do 93 | raw nick 94 | raw user 95 | raw host 96 | raw message 97 | raw channel 98 | end 99 | } 100 | bot_is_connected 101 | 102 | @server.puts ":johnny!john@doe.com PRIVMSG #awesome :hello, folks!" 103 | assert_equal "johnny\r\n", @server.gets 104 | assert_equal "john\r\n", @server.gets 105 | assert_equal "doe.com\r\n", @server.gets 106 | assert_equal "hello, folks!\r\n", @server.gets 107 | assert_equal "#awesome\r\n", @server.gets 108 | end 109 | 110 | test "errors are caught and dispatched" do 111 | bot = mock_bot { 112 | on(:error, 401) { 113 | raw error 114 | } 115 | } 116 | bot_is_connected 117 | 118 | @server.print ":server 401 isaac jeff :No such nick/channel\r\n" 119 | assert_equal "401\r\n", @server.gets 120 | end 121 | 122 | test "prefix is optional for errors" do 123 | bot = mock_bot { 124 | on(:error, 401) { 125 | raw error 126 | } 127 | } 128 | bot_is_connected 129 | 130 | @server.print "401 isaac jeff :No such nick/channel\r\n" 131 | assert_equal "401\r\n", @server.gets 132 | end 133 | 134 | test "ctcp version request are answered" do 135 | bot = mock_bot { 136 | configure {|c| c.version = "Ridgemont 0.1"} 137 | } 138 | bot_is_connected 139 | 140 | @server.print ":jeff!spicoli@name.com PRIVMSG isaac :\001VERSION\001\r\n" 141 | assert_equal "NOTICE jeff :\001VERSION Ridgemont 0.1\001\r\n", @server.gets 142 | end 143 | 144 | test "trailing newlines are removed" do 145 | bot = mock_bot { 146 | on(:channel, /(.*)/) {msg "foo", "#{match[0]} he said"} 147 | } 148 | bot_is_connected 149 | 150 | @server.print ":johnny!john@doe.com PRIVMSG #awesome :hello, folks!\r\n" 151 | assert_equal "PRIVMSG foo :hello, folks! he said\r\n", @server.gets 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /test/test_queue.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestQueue < Test::Unit::TestCase 4 | def flood_bot 5 | bot = mock_bot { 6 | on(:connect) { 7 | # 1472.0 / 16 = 92.0, minus one to accomodate for newline 8 | 16.times { raw "." * 90 } 9 | raw "this should not flood!" 10 | } 11 | } 12 | bot_is_connected 13 | # We don't want to account for the initial NIKC/USER messages 14 | bot.irc.instance_variable_set :@transfered, 0 15 | bot 16 | end 17 | 18 | 19 | test "ping after sending 1472 consequent bytes" do 20 | bot = flood_bot 21 | 22 | bot.dispatch :connect 23 | 16.times { @server.gets } 24 | assert_equal "PING :#{bot.config.server}\r\n", @server.gets 25 | assert @server.empty? 26 | end 27 | 28 | test "reset transfer amount at pong reply" do 29 | bot = flood_bot 30 | 31 | bot.dispatch :connect 32 | 16.times { @server.gets } 33 | @server.gets # PING message 34 | 35 | @server.puts ":localhost PONG :localhost" 36 | assert_equal "this should not flood!\r\n", @server.gets 37 | end 38 | 39 | test "reset transfer amount at server ping" do 40 | bot = flood_bot 41 | 42 | bot.dispatch :connect 43 | 16.times { @server.gets } 44 | @server.gets # PING message triggered by transfer lock 45 | @server.puts "PING :localhost" 46 | 47 | assert_equal "this should not flood!\r\n", @server.gets 48 | end 49 | end 50 | --------------------------------------------------------------------------------